File Coverage

blib/lib/Net/IMAP/Simple/NB.pm
Criterion Covered Total %
statement 24 288 8.3
branch 0 84 0.0
condition 0 30 0.0
subroutine 8 65 12.3
pod 5 34 14.7
total 37 501 7.3


line stmt bran cond sub pod time code
1             package Net::IMAP::Simple::NB;
2              
3             our $VERSION = '1.1';
4              
5 1     1   942 use strict;
  1         2  
  1         40  
6 1     1   6 use warnings;
  1         2  
  1         38  
7              
8 1     1   15 no warnings 'deprecated';
  1         2  
  1         41  
9              
10 1     1   5 use base qw(Danga::Socket Net::IMAP::Simple);
  1         1  
  1         1155  
11              
12             =head1 NAME
13              
14             Net::IMAP::Simple::NB - Non-blocking IMAP.
15              
16             =head1 SYNOPSIS
17              
18             use Net::IMAP::Simple::NB;
19             use Scalar::Util qw(weaken isweak);
20             Danga::Socket->AddTimer(0, sub {
21             # Create the object
22             my $imap = Net::IMAP::Simple::NB->new('server:143') ||
23             die "Unable to connect to IMAP: $Net::IMAP::Simple::errstr\n";
24            
25             $imap->login('user','password', sub {
26             my $login_ok = shift;
27             if ($login_ok) {
28             print "Login OK\n";
29             $imap->select("INBOX", sub {
30             my $nm = shift;
31             print "Got $nm Messages\n";
32             my $i = 1;
33             my $sub;
34             $sub = sub {
35             weaken($sub) unless isweak($sub);
36             my $headers = shift;
37             print grep { /^Subject:/ } @$headers;
38             $i++;
39             $imap->top($i, $sub) unless $i == $nm;
40             };
41             $imap->top($i, $sub);
42             });
43             }
44             else {
45             warn("Login failed!");
46             }
47             });
48             });
49            
50             Danga::Socket->EventLoop;
51              
52             =head1 DESCRIPTION
53              
54             This module models the Net::IMAP::Simple API, but works non-blocking. It is based
55             on the Danga::Socket framework, rather than anything generic. Sorry if that doesn't
56             fit your world-view.
57              
58             =head1 API
59              
60             The C API models the C API exactly,
61             with the difference that instead of having return values, you supply a callback
62             as the last parameter of the method call. This callback will receive in @_ the
63             same as whatever the C method would have returned.
64              
65             The only real difference aside from that is a slightly modified constructor:
66              
67             =head2 C<< CLASS->new(...) >>
68              
69             my $imap = Net::IMAP::Simple->new( $server [ :port ]);
70             OR
71             my $imap = Net::IMAP::Simple->new( $server [, option_name => option_value ] );
72              
73             This class method constructs a new C object. It takes one
74             required parameter which is the server to connect to, and additional optional
75             parameters.
76              
77             The server parameter may specify just the server, or both the server and port
78             number. To specify an alternate port, seperate it from the server with a colon
79             (C<:>), C.
80              
81             On success an object is returned. On failure, nothing is returned and an error
82             message is set to $Net::IMAP::Simple::errstr.
83              
84             B
85              
86             port => Assign the port number (default: 143)
87              
88             timeout => Connection timeout in seconds.
89              
90             use_v6 => If set to true, attempt to use IPv6
91             -> sockets rather than IPv4 sockets.
92             -> This option requires the
93             -> IO::Socket::INET6 module
94              
95             bindaddr => Assign a local address to bind
96              
97              
98             use_select_cache => Enable select() caching internally
99              
100             select_cache_ttl => The number of seconds to allow a
101             -> select cache result live before running
102             -> select() again.
103              
104             =cut
105              
106 1     1   39273 use constant CLEANUP_TIME => 5; # every N seconds
  1         2  
  1         211  
107              
108 0     0 0   sub max_idle_time { 3600 } # one hour idle timeout
109 0     0 0   sub max_connect_time { 18000 } # 5 hours max connect time
110              
111 0     0 1   sub event_err { my $self = shift; $self->close("Error") }
  0            
112 0     0 1   sub event_hup { my $self = shift; $self->close("Disconnect (HUP)") }
  0            
113 0     0 1   sub close { my $self = shift; warn("Close: $_[0]\n"); $self->SUPER::close(@_); }
  0            
  0            
114              
115 1         9 use fields qw(
116             count
117             server
118             port
119             timeout
120             use_v6
121             bindaddr
122             use_select_cache
123             select_cache_ttl
124             current_callback
125             current_command
126             line
127             create_time
128             alive_time
129             command_final
130             command_process
131             BOXES
132             last
133             working_box
134             _errstr
135             read_bytes
136 1     1   5 );
  1         3  
137              
138             sub new {
139 0     0 1   my ( $class, $server, %opts) = @_;
140              
141 0           my Net::IMAP::Simple::NB $self = fields::new($class);
142            
143 0           $self->{count} = -1;
144            
145 0           my ($srv, $prt) = split(/:/, $server, 2);
146 0 0 0       $prt ||= ($opts{port} ? $opts{port} : $self->_port);
147            
148 0           $self->{server} = $srv;
149 0           $self->{port} = $prt;
150 0 0         $self->{timeout} = ($opts{timeout} ? $opts{timeout} : $self->_timeout);
151 0 0         $self->{use_v6} = ($opts{use_v6} ? 1 : 0);
152 0           $self->{bindaddr} = $opts{bindaddr};
153 0           $self->{use_select_cache} = $opts{use_select_cache};
154 0           $self->{select_cache_ttl} = $opts{select_cache_ttl};
155 0           $self->{line} = '';
156 0           $self->{create_time} = $self->{alive_time} = time;
157              
158             # Pop the port off the address string if it's not an IPv6 IP address
159 0 0 0       if(!$self->{use_v6} && $self->{server} =~ /^[A-Fa-f0-9]{4}:[A-Fa-f0-9]{4}:/ && $self->{server} =~ s/:(\d+)$//g){
      0        
160 0           $self->{port} = $1;
161             }
162            
163 0           my $sock = $self->_connect;
164 0 0         if(!$sock){
165 0           $! =~ s/IO::Socket::INET6?: //g;
166 0           $Net::IMAP::Simple::errstr = "connection failed $!";
167 0           return;
168             }
169            
170 0           $self->SUPER::new($sock);
171 0           $self->watch_read(1);
172            
173 0           return $self;
174             }
175              
176             sub _connect {
177 0     0     my Net::IMAP::Simple::NB $self = shift;
178 0           my $sock = $self->SUPER::_connect;
179 0 0         return unless $sock;
180 0           IO::Handle::blocking($sock, 0);
181 0           return $sock;
182             }
183              
184             sub event_read {
185 0     0 1   my Net::IMAP::Simple::NB $self = shift;
186            
187 0           $self->{alive_time} = time;
188            
189 0           my $bref = $self->read(131072);
190 0 0         return $self->close("read failed: $!") unless defined $bref;
191            
192 0           $self->{line} .= $$bref;
193            
194 0           while ($self->{line} =~ s/^([^\n]*\n)//) {
195 0           my $line = $1;
196 0           $self->process_response_line($line);
197             }
198             }
199              
200             sub process_response_line {
201 0     0 0   my Net::IMAP::Simple::NB $self = shift;
202 0           my $line = shift;
203            
204             #print "S: $line";
205              
206 0 0         if ($self->{read_bytes}) {
    0          
207 0           $self->{read_bytes} -= bytelength($line);
208 0 0         $self->{read_bytes} = 0 if $self->{read_bytes} < 0;
209 0           $self->{command_process}->($line);
210             }
211             elsif ($line =~ /^\*.*\{(\d+)\}$/ ) {
212 0           my $bytes = $1;
213 0           $self->{read_bytes} = $1;
214 0           $self->{command_process}->($line);
215             }
216             else {
217 0           my $ok = $self->_cmd_ok($line);
218 0 0         if ($ok) {
    0          
219 0           $self->{current_command} = undef;
220 0           return $self->{current_callback}->($self->{command_final}->($line));
221             }
222             elsif (defined($ok)) { # $ok is false here
223 0           $self->{current_command} = undef;
224 0           return $self->{current_callback}->();
225             }
226             else {
227 0 0         $self->{command_process}->($line) if $self->{command_process};
228             }
229             }
230             }
231              
232             sub _send_cmd {
233 0     0     my Net::IMAP::Simple::NB $self = shift;
234 0           my ( $name, $value ) = @_;
235 0           my $id = $self->_nextid;
236 0 0         my $cmd = "$id $name" . ($value ? " $value" : "") . "\r\n";
237             #print "C: $cmd";
238 0           $self->write($cmd);
239             }
240              
241             sub _process_cmd {
242 0     0     my Net::IMAP::Simple::NB $self = shift;
243 0           my (%args) = @_;
244 0 0         die "Command currently in progress: $self->{current_command}\n" if $self->{current_command};
245 0   0       $self->{command_final} = $args{final} || die "No final calling $args{cmd}";
246 0   0       $self->{command_process} = $args{process} || die "No process calling $args{cmd}";
247 0           $self->_send_cmd(@{$args{cmd}});
  0            
248 0           $self->{current_command} = $args{cmd};
249             }
250              
251 1     1   1143 use Carp qw(confess);
  1         2  
  1         4405  
252             sub set_callback {
253 0     0 0   my Net::IMAP::Simple::NB $self = shift;
254 0   0       $self->{current_callback} = shift || confess "set_callback called with no callback";
255             }
256              
257 0     0 0   sub login { my $self = shift; $self->set_callback(pop); $self->SUPER::login(@_); }
  0            
  0            
258 0     0 0   sub messages {my $self = shift; $self->set_callback(pop); $self->SUPER::messages(@_) }
  0            
  0            
259 0     0 0   sub top {my $self = shift; $self->set_callback(pop); $self->SUPER::top(@_) }
  0            
  0            
260 0     0 0   sub seen {my $self = shift; $self->set_callback(pop); $self->SUPER::seen(@_) }
  0            
  0            
261 0     0 0   sub list {my $self = shift; $self->set_callback(pop); $self->SUPER::list(@_) }
  0            
  0            
262 0     0 0   sub get {my $self = shift; $self->set_callback(pop); $self->SUPER::get(@_) }
  0            
  0            
263 0     0 0   sub getfh {my $self = shift; $self->set_callback(pop); $self->SUPER::getfh(@_) }
  0            
  0            
264 0     0 0   sub delete {my $self = shift; $self->set_callback(pop); $self->SUPER::delete(@_) }
  0            
  0            
265 0     0 0   sub create_mailbox {my $self = shift; $self->set_callback(pop); $self->SUPER::create_mailbox(@_) }
  0            
  0            
266 0     0 0   sub delete_mailbox {my $self = shift; $self->set_callback(pop); $self->SUPER::delete_mailbox(@_) }
  0            
  0            
267 0     0 0   sub rename_mailbox {my $self = shift; $self->set_callback(pop); $self->SUPER::rename_mailbox(@_) }
  0            
  0            
268 0     0 0   sub copy {my $self = shift; $self->set_callback(pop); $self->SUPER::copy(@_) }
  0            
  0            
269              
270              
271             # modified from original to include $last_sub support
272             sub select {
273 0     0 0   my Net::IMAP::Simple::NB $self = shift;
274 0           $self->set_callback(pop);
275            
276 0           my ( $mbox, $last_sub ) = @_;
277            
278 0 0         $mbox = 'INBOX' unless $mbox;
279            
280 0           $self->{working_box} = $mbox;
281            
282 0   0       $self->{BOXES}->{ $mbox }->{proc_time} ||= 0;
283            
284 0 0 0       if($self->{use_select_cache} && (time - $self->{BOXES}->{ $mbox }->{proc_time}) <= $self->{select_cache_ttl}){
285 0           return $self->{BOXES}->{$mbox}->{messages};
286             }
287            
288 0           $self->{BOXES}->{$mbox}->{proc_time} = time;
289            
290 0           my $t_mbox = $mbox;
291            
292             $self->_process_cmd(
293             cmd => [SELECT => _escape($t_mbox)],
294             final => sub {
295 0     0     $self->{last} = $self->{BOXES}->{$mbox}->{messages};
296 0 0         if ($last_sub) {
297 0           return $last_sub->();
298             }
299             else {
300 0           return $self->{last};
301             }
302             },
303             process => sub {
304 0 0   0     if($_[0] =~ /^\*\s+(\d+)\s+EXISTS/i){
    0          
    0          
    0          
305 0           $self->{BOXES}->{$mbox}->{messages} = $1;
306             } elsif($_[0] =~ /^\*\s+FLAGS\s+\((.*?)\)/i){
307 0           $self->{BOXES}->{$mbox}->{flags} = [ split(/\s+/, $1) ];
308             } elsif($_[0] =~ /^\*\s+(\d+)\s+RECENT/i){
309 0           $self->{BOXES}->{$mbox}->{recent} = $1;
310             } elsif($_[0] =~ /^\*\s+OK\s+\[(.*?)\s+(.*?)\]/i){
311 0           my ($flag, $value) = ($1, $2);
312 0 0         if($value =~ /\((.*?)\)/){
313 0           $self->{BOXES}->{$mbox}->{sflags}->{$flag} = [split(/\s+/, $1)];
314             } else {
315 0           $self->{BOXES}->{$mbox}->{oflags}->{$flag} = $value;
316             }
317             }
318             },
319 0           );
320             }
321              
322             sub unread {
323 0     0 0   my ( $self ) = @_;
324 0           $self->set_callback(pop);
325            
326 0           my @list;
327             $self->_process_cmd(
328             cmd => [SEARCH => qq[UNSEEN]],
329 0     0     final => sub { \@list },
330             process => sub {
331 0 0   0     if ($_[0] =~ /\* SEARCH (.*)$/) {
332 0           push @list, split(/\s+/, $1);
333             }
334             },
335 0           );
336             }
337              
338             sub message_info {
339 0     0 0   my ( $self, $number ) = @_;
340 0           $self->set_callback(pop);
341            
342 0           my %flags;
343             my @lines;
344             $self->_process_cmd(
345             cmd => [FETCH => qq[$number (FLAGS INTERNALDATE RFC822.HEADER)]],
346 0     0     final => sub { \%flags, \@lines },
347             process => sub {
348 0 0   0     if (%flags) {
349 0           push @lines, $_[0];
350             }
351             else {
352 0           $_ = shift;
353 0 0         if (/\bFLAGS \((.*?)\)/) {
354 0           $flags{$_}++ for split(/\s+/, $1);
355             }
356 0 0         if (/\bINTERNALDATE "(\d\d-[a-zA-Z]{3}-\d{4} \d\d:\d\d:\d\d [\+\-]\d{4})"/) {
357 0           $flags{DATE} = $1;
358             }
359             }
360             },
361 0           );
362             }
363              
364             sub message {
365 0     0 0   my ( $self, $number ) = @_;
366 0           $self->set_callback(pop);
367            
368 0           my %flags;
369             my @lines;
370             $self->_process_cmd(
371             cmd => [FETCH => qq[$number (FLAGS INTERNALDATE RFC822)]],
372 0     0     final => sub { \%flags, \@lines },
373             process => sub {
374 0 0   0     if (%flags) {
375 0           push @lines, $_[0];
376             }
377             else {
378 0 0         if (/\bFLAGS (\(.*?\))/) {
379 0           $flags{$_}++ for split(/\s+/, $1);
380             }
381 0 0         if (/\bINTERNALDATE "(\d\d-[a-zA-Z]{3}-\d{4} \d\d:\d\d:\d\d [\+\-]\d{4})"/) {
382 0           $flags{DATE} = $1;
383             }
384             }
385             },
386 0           );
387             }
388              
389             sub flags {
390 0     0 0   my Net::IMAP::Simple::NB $self = shift;
391 0           my $cb = pop;
392 0           my $folder = shift;
393            
394 0     0     $self->select($folder, sub { @{ $self->{BOXES}->{ $self->current_box }->{flags} } }, $cb );
  0            
  0            
395             }
396              
397              
398             sub recent {
399 0     0 0   my Net::IMAP::Simple::NB $self = shift;
400 0           my $cb = pop;
401 0           my $folder = shift;
402            
403 0     0     $self->select($folder, sub { $self->{BOXES}->{ $self->current_box }->{recent} }, $cb );
  0            
404             }
405              
406             sub expunge_mailbox {
407 0     0 0   my Net::IMAP::Simple::NB $self = shift;
408 0           my $cb = pop;
409 0           my $box = shift;
410             $self->select($box, sub {
411             $self->_process_cmd(
412             cmd => ['EXPUNGE'],
413 0           final => sub { 1 },
414 0           process => sub { },
415 0     0     );
416 0           }, $cb);
417             }
418              
419             sub folder_subscribe {
420 0     0 0   my ($self, $box) = @_;
421 0           my $cb = pop;
422             $self->select($box, sub {
423             $self->_process_cmd(
424             cmd => [SUBSCRIBE => _escape($box)],
425 0           final => sub { 1 },
426 0           process => sub { },
427 0     0     );
428 0           }, $cb);
429             }
430              
431             sub folder_unsubscribe {
432 0     0 0   my ($self, $box) = @_;
433 0           my $cb = pop;
434             $self->select($box, sub {
435             $self->_process_cmd(
436             cmd => [UNSUBSCRIBE => _escape($box)],
437 0           final => sub { 1 },
438 0           process => sub { },
439 0     0     );
440 0           }, $cb);
441             }
442              
443             sub quit {
444 0     0 0   my Net::IMAP::Simple::NB $self = shift;
445 0           $self->set_callback(pop);
446 0           my ( $hq ) = @_;
447 0           $self->_send_cmd('EXPUNGE');
448            
449 0 0         if(!$hq){
450 0     0     $self->_process_cmd(cmd => ['LOGOUT'], final => sub { $self->close }, process => sub{});
  0            
  0            
451             } else {
452 0           $self->_send_cmd('LOGOUT');
453 0           $self->close;
454             }
455            
456 0           return 1;
457             }
458              
459             sub mailboxes {
460 0     0 0   my Net::IMAP::Simple::NB $self = shift;
461 0           $self->_mailboxes('LIST', @_);
462             }
463              
464             sub mailboxes_subscribed {
465 0     0 0   my Net::IMAP::Simple::NB $self = shift;
466 0           $self->_mailboxes('LSUB', @_);
467             }
468              
469             sub _mailboxes {
470 0     0     my Net::IMAP::Simple::NB $self = shift;
471 0           $self->set_callback(pop);
472 0           my ( $type, $box, $ref ) = @_;
473            
474 0   0       $ref ||= '""';
475 0           my @list;
476 0           my $mode = 'listcheck';
477 0           my $query = [$type => qq[$ref *]];
478 0 0         if ( defined $box ) {
479 0           my $query = [$type => qq[$ref $box]];
480             }
481            
482             # recurse, should probably follow
483             # RFC 2683: 3.2.1.1. Listing Mailboxes
484             return $self->_process_cmd(
485             cmd => $query,
486 0     0     final => sub { _unescape($_) for @list; @list },
  0            
487             process => sub {
488 0     0     my $line = shift;
489             # * LIST (\Unmarked \HasNoChildren) "." "INBOX.web_board"
490 0 0         if ($mode eq 'listcheck') {
    0          
491 0 0 0       if ( $line =~ /^\*\s+(LIST|LSUB).*\s+(\".*?\")\s*$/i ||
    0          
492             $line =~ /^\*\s+(LIST|LSUB).*\s+(\S+)\s*$/i )
493             {
494 0           push @list, $2;
495             }
496             elsif ( $line =~ /^\*\s+(LIST|LSUB).*\s+\{\d+\}\s*$/i ) {
497 0           $mode = 'listextra';
498             }
499             }
500             elsif ($mode eq 'listextra') {
501 0           chomp($line);
502 0           $line =~ s/\r//;
503 0           _escape($line);
504 0           push @list, $line;
505 0           $mode = 'listcheck';
506             }
507             },
508 0           );
509             }
510              
511             sub bytelength {
512 1     1   16 use bytes;
  1         2  
  1         12  
513 0     0 0   return length($_[0]);
514             }
515              
516             sub _escape {
517 0     0     $_[0] =~ s/\\/\\\\/g;
518 0           $_[0] =~ s/\"/\\\"/g;
519 0           $_[0] = "\"$_[0]\"";
520 0           $_[0];
521             }
522              
523             sub _unescape {
524 0     0     $_[0] =~ s/^"//g;
525 0           $_[0] =~ s/"$//g;
526 0           $_[0] =~ s/\\\"/\"/g;
527 0           $_[0] =~ s/\\\\/\\/g;
528             }
529              
530             Danga::Socket->AddTimer(CLEANUP_TIME, \&_do_cleanup);
531              
532             # Cleanup routine to get rid of timed out sockets
533             sub _do_cleanup {
534 0     0     my $now = time;
535            
536 0           Danga::Socket->AddTimer(CLEANUP_TIME, \&_do_cleanup);
537            
538 0           my $sf = __PACKAGE__->get_sock_ref;
539            
540 0           my $conns = 0;
541              
542 0           my %max_age; # classname -> max age (0 means forever)
543             my %max_connect; # classname -> max connect time
544 0           my @to_close;
545 0           while (my $k = each %$sf) {
546 0           my Net::IMAP::Simple::NB $v = $sf->{$k};
547 0           my $ref = ref $v;
548 0 0         next unless $v->isa('Net::IMAP::Simple::NB');
549 0           $conns++;
550 0 0         unless (defined $max_age{$ref}) {
551 0   0       $max_age{$ref} = $ref->max_idle_time || 0;
552 0   0       $max_connect{$ref} = $ref->max_connect_time || 0;
553             }
554 0 0         if (my $t = $max_connect{$ref}) {
555 0 0         if ($v->{create_time} < $now - $t) {
556 0           push @to_close, $v;
557 0           next;
558             }
559             }
560 0 0         if (my $t = $max_age{$ref}) {
561 0 0         if ($v->{alive_time} < $now - $t) {
562 0           push @to_close, $v;
563             }
564             }
565             }
566            
567 0           $_->close("Timeout") foreach @to_close;
568             }
569              
570              
571              
572             1;
573              
574             __END__