File Coverage

blib/lib/Net/IMAP/Client.pm
Criterion Covered Total %
statement 36 619 5.8
branch 0 274 0.0
condition 0 66 0.0
subroutine 12 70 17.1
pod 33 33 100.0
total 81 1062 7.6


line stmt bran cond sub pod time code
1             package Net::IMAP::Client;
2              
3 1     1   32652 use vars qw[$VERSION];
  1         3  
  1         63  
4             $VERSION = '0.9505';
5              
6 1     1   6 use strict;
  1         2  
  1         37  
7 1     1   6 use warnings;
  1         14  
  1         34  
8              
9 1     1   7 use List::Util qw( min max first );
  1         1  
  1         257  
10 1     1   1290 use List::MoreUtils qw( each_array );
  1         1728  
  1         82  
11 1     1   1085 use IO::Socket::INET ();
  1         31712  
  1         24  
12 1     1   1335 use IO::Socket::SSL ();
  1         600797  
  1         43  
13 1     1   12 use Socket qw( SO_KEEPALIVE );
  1         3  
  1         78  
14              
15 1     1   735 use Net::IMAP::Client::MsgSummary ();
  1         4  
  1         5849  
16              
17             our $READ_BUFFER = 4096;
18             my %UID_COMMANDS = map { $_ => 1 } qw( COPY FETCH STORE SEARCH SORT THREAD );
19             my %DEFAULT_ARGS = (
20             uid_mode => 1,
21             timeout => 90,
22             server => '127.0.0.1',
23             port => undef,
24             user => undef,
25             pass => undef,
26             ssl => 0,
27             ssl_verify_peer => 1,
28             socket => undef,
29             _cmd_id => 0,
30             ssl_options => {},
31             );
32              
33             sub new {
34 0     0 1   my ($class, %args) = @_;
35              
36 0 0         my $self = { map {
37 0           $_ => exists $args{$_} ? $args{$_} : $DEFAULT_ARGS{$_}
38             } keys %DEFAULT_ARGS };
39              
40 0           bless $self, $class;
41              
42 0           $self->{notifications} = [];
43 0           eval {
44 0           $self->{greeting} = $self->_socket_getline;
45             };
46 0 0         return $@ ? undef : $self;
47             }
48              
49             sub DESTROY {
50 0     0     my ($self) = @_;
51 0           eval {
52 0 0         $self->quit
53             if $self->{socket}->opened;
54             };
55             }
56              
57             sub uid_mode {
58 0     0 1   my ($self, $val) = @_;
59 0 0         if (defined($val)) {
60 0           return $self->{uid_mode} = $val;
61             } else {
62 0           return $self->{uid_mode};
63             }
64             }
65              
66             ### imap utilities ###
67              
68             sub login {
69 0     0 1   my ($self, $user, $pass) = @_;
70 0   0       $user ||= $self->{user};
71 0   0       $pass ||= $self->{pass};
72 0           $self->{user} = $user;
73 0           $self->{pass} = $pass;
74 0           _string_quote($user);
75 0           _string_quote($pass);
76 0           my ($ok) = $self->_tell_imap(LOGIN => "$user $pass");
77 0           return $ok;
78             }
79              
80             sub logout {
81 0     0 1   my ($self) = @_;
82 0           $self->_send_cmd('LOGOUT');
83 0           $self->_get_socket->close;
84 0           return 1;
85             }
86              
87             *quit = \&logout;
88              
89             sub capability {
90 0     0 1   my ($self, $requirement) = @_;
91 0           my $capability = $self->{capability};
92 0 0         unless ($capability) {
93 0           my ($ok, $lines) = $self->_tell_imap('CAPABILITY');
94 0 0         if ($ok) {
95 0           my $line = $lines->[0][0];
96 0 0         if ($line =~ /^\*\s+CAPABILITY\s+(.*?)\s*$/) {
97 0           $capability = $self->{capability} = [ split(/\s+/, $1) ];
98             }
99             }
100             }
101 0 0 0       if ($requirement && $capability) {
102 0     0     return first { $_ =~ $requirement } @$capability;
  0            
103             }
104 0           return $capability;
105             }
106              
107             sub status {
108 0     0 1   my $self = shift;
109 0           my $a;
110 0           my $wants_one = undef;
111 0 0         if (ref($_[0]) eq 'ARRAY') {
112 0           my @tmp = @{$_[0]};
  0            
113 0           $a = \@tmp;
114             } else {
115 0           $a = [ shift ];
116 0           $wants_one = 1;
117             }
118 0           foreach (@$a) {
119 0           _string_quote($_);
120 0           $_ = "STATUS $_ (MESSAGES RECENT UNSEEN UIDNEXT UIDVALIDITY)";
121             }
122 0           my $results = $self->_tell_imap2(@$a);
123              
124             # remove "NO CLIENT BUG DETECTED" lines as they serve no
125             # purpose beyond the religious zeal of IMAP implementors
126 0           for my $row (@$results) {
127 0 0         if (@{$row->[1]} > 1) {
  0            
128 0           $row->[1] = [ grep { $_->[0] !~ /NO CLIENT BUG DETECTED: STATUS on selected mailbox:/ } @{$row->[1]} ];
  0            
  0            
129             }
130             }
131              
132 0           my %ret;
133             my $name;
134 0           foreach my $i (@$results) {
135 0 0         if ($i->[0]) { # was successful?
136 0           my $tokens = _parse_tokens($i->[1]->[0]);
137 0           $name = $tokens->[2];
138 0           $tokens = $tokens->[3];
139 0           my %tmp = @$tokens;
140 0           $tmp{name} = $name;
141 0           $ret{$name} = \%tmp;
142             }
143             }
144 0 0 0       return $wants_one
145             ? (defined $name and $ret{$name}) # avoid data on undef key
146             : \%ret;
147             }
148              
149             sub select {
150 0     0 1   my ($self, $folder) = @_;
151 0           $self->_select_or_examine($folder, 'SELECT');
152             }
153             sub examine {
154 0     0 1   my ($self, $folder) = @_;
155 0           $self->_select_or_examine($folder, 'EXAMINE');
156             }
157              
158             sub _select_or_examine {
159 0     0     my ($self, $folder, $operation) = @_;
160 0           my $quoted = $folder;
161 0           _string_quote($quoted);
162 0           my ($ok, $lines) = $self->_tell_imap($operation => $quoted);
163 0 0         if ($ok) {
164 0           $self->{selected_folder} = $folder;
165 0           my %info = ();
166 0           foreach my $tmp (@$lines) {
167 0           my $line = $tmp->[0];
168 0 0         if ($line =~ /^\*\s+(\d+)\s+EXISTS/i) {
    0          
    0          
    0          
169 0           $info{messages} = $1 + 0;
170             } elsif ($line =~ /^\*\s+FLAGS\s+\((.*?)\)/i) {
171 0           $info{flags} = [ split(/\s+/, $1) ];
172             } elsif ($line =~ /^\*\s+(\d+)\s+RECENT/i) {
173 0           $info{recent} = $1 + 0;
174             } elsif ($line =~ /^\*\s+OK\s+\[(.*?)\s+(.*?)\]/i) {
175 0           my ($flag, $value) = ($1, $2);
176 0 0         if ($value =~ /\((.*?)\)/) {
177 0           $info{sflags}->{$flag} = [split(/\s+/, $1)];
178             } else {
179 0           $info{sflags}->{$flag} = $value;
180             }
181             }
182             }
183 0   0       $self->{FOLDERS} ||= {};
184 0           $self->{FOLDERS}{$folder} = \%info;
185             }
186 0           return $ok;
187             }
188              
189             sub separator {
190 0     0 1   my ($self) = @_;
191 0           my $sep = $self->{separator};
192 0 0         if (!$sep) {
193 0           my ($ok, $lines) = $self->_tell_imap(LIST => '"" ""');
194 0 0         if ($ok) {
195 0           my $tokens = _parse_tokens($lines->[0]);
196 0           $sep = $self->{separator} = $tokens->[3];
197             } else {
198 0           $sep = undef;
199             }
200             }
201 0           return $sep;
202             }
203              
204             sub folders {
205 0     0 1   my ($self) = @_;
206 0           my ($ok, $lines) = $self->_tell_imap(LIST => '"" "*"');
207 0 0         if ($ok) {
208 0           my @ret = map { _parse_tokens($_)->[4] } @$lines;
  0            
209 0 0         return wantarray ? @ret : \@ret;
210             }
211 0           return undef;
212             }
213              
214             sub _mk_namespace {
215 0     0     my ($ns) = @_;
216 0 0         if ($ns) {
217 0           foreach my $i (@$ns) {
218 0           $i = {
219             prefix => $i->[0],
220             sep => $i->[1],
221             };
222             }
223             }
224 0           return $ns;
225             }
226              
227             sub namespace {
228 0     0 1   my ($self) = @_;
229 0           my ($ok, $lines) = $self->_tell_imap('NAMESPACE');
230 0 0         if ($ok) {
231 0           my $ret = _parse_tokens($lines->[0]);
232 0           splice(@$ret, 0, 2);
233             return {
234 0           personal => _mk_namespace($ret->[0]),
235             other => _mk_namespace($ret->[1]),
236             shared => _mk_namespace($ret->[2]),
237             };
238             }
239             }
240              
241             sub folders_more {
242 0     0 1   my ($self) = @_;
243 0           my ($ok, $lines) = $self->_tell_imap(LIST => '"" "*"');
244 0 0         if ($ok) {
245 0           my %ret = map {
246 0           my $tokens = _parse_tokens($_);
247 0           my $flags = $tokens->[2];
248 0           my $sep = $tokens->[3];
249 0           my $name = $tokens->[4];
250 0           ( $name, { flags => $flags, sep => $sep } );
251             } @$lines;
252 0           return \%ret;
253             }
254 0           return undef;
255             }
256              
257             sub noop {
258 0     0 1   my ($self) = @_;
259 0           my ($ok) = $self->_tell_imap('NOOP', undef, 1);
260 0           return $ok;
261             }
262              
263             sub seq_to_uid {
264 0     0 1   my ($self, @seq_ids) = @_;
265 0           my $ids = join(',', @seq_ids);
266              
267 0           my $save_uid_mode = $self->uid_mode;
268 0           $self->uid_mode(0);
269 0           my ($ok, $lines) = $self->_tell_imap(FETCH => "$ids UID", 1);
270 0           $self->uid_mode($save_uid_mode);
271              
272 0 0         if ($ok) {
273 0 0         my %ret = map {
274 0           $_->[0] =~ /^\*\s+(\d+)\s+FETCH\s*\(\s*UID\s+(\d+)/
275             && ( $1, $2 );
276             } @$lines;
277 0           return \%ret;
278             }
279 0           return undef;
280             }
281              
282             sub search {
283 0     0 1   my ($self, $criteria, $sort, $charset) = @_;
284              
285 0   0       $charset ||= 'UTF-8';
286              
287 0 0         my $cmd = $sort ? 'SORT' : 'SEARCH';
288 0 0         if ($sort) {
289 0 0         if (ref($sort) eq 'ARRAY') {
    0          
290 0           $sort = uc '(' . join(' ', @$sort) . ')';
291             } elsif ($sort !~ /^\(/) {
292 0           $sort = uc "($sort)";
293             }
294 0           $sort =~ s/\s*$/ /;
295 0           $sort =~ s/\^/REVERSE /g;
296             } else {
297 0           $charset = "CHARSET $charset";
298 0           $sort = '';
299             }
300              
301 0 0         if (ref($criteria) eq 'HASH') {
302 0           my @a;
303 0           while (my ($key, $val) = each %$criteria) {
304 0           my $quoted = $val;
305             # don't quote range
306 0 0         _string_quote($quoted) unless uc $key eq 'UID';
307 0           push @a, uc $key, $quoted;
308             }
309 0           $criteria = '(' . join(' ', @a) . ')';
310             }
311              
312 0           my ($ok, $lines) = $self->_tell_imap($cmd => "$sort$charset $criteria", 1);
313 0 0         if ($ok) {
314             # it makes no sense to employ the full token parser here
315             # read past progress messages lacking initial '*'
316 0           foreach my $line (@{$lines->[0]}) {
  0            
317 0 0         if ($line =~ s/^\*\s+(?:SEARCH|SORT)\s+//ig) {
318 0           $line =~ s/\s*$//g;
319 0           return [ map { $_ + 0 } split(/\s+/, $line) ];
  0            
320             }
321             }
322             }
323              
324 0           return undef;
325             }
326              
327             sub get_rfc822_body {
328 0     0 1   my ($self, $msg) = @_;
329 0           my $wants_many = undef;
330 0 0         if (ref($msg) eq 'ARRAY') {
331 0           $msg = join(',', @$msg);
332 0           $wants_many = 1;
333             }
334 0           my ($ok, $lines) = $self->_tell_imap(FETCH => "$msg RFC822", 1);
335 0 0         if ($ok) {
336 0           my @ret = map { $_->[1] } @$lines;
  0            
337 0 0         return $wants_many ? \@ret : $ret[0];
338             }
339 0           return undef;
340             }
341              
342             sub get_part_body {
343 0     0 1   my ($self, $msg, $part) = @_;
344 0           $part = "BODY[$part]";
345 0           my ($ok, $lines) = $self->_tell_imap(FETCH => "$msg $part", 1);
346 0 0         if ($ok) {
347             # it can contain FLAGS notification, i.e. \Seen flag becomes set
348 0           my $tokens = _parse_tokens($lines->[0], 1);
349 0           my %hash = @{$tokens->[3]};
  0            
350 0 0         if ($hash{FLAGS}) {
351 0           $self->_handle_notification($tokens);
352             }
353 0           return $hash{$part};
354             }
355 0           return undef;
356             }
357              
358             sub get_parts_bodies {
359 0     0 1   my ($self, $msg, $parts) = @_;
360 0           my $tmp = join(' ', map { "BODY[$_]" } @$parts);
  0            
361 0           my ($ok, $lines) = $self->_tell_imap(FETCH => "$msg ($tmp)", 1);
362 0 0         if ($ok) {
363             # it can contain FLAGS notification, i.e. \Seen flag becomes set
364 0           my $tokens = _parse_tokens($lines->[0], 1);
365 0           my %hash = @{$tokens->[3]};
  0            
366 0 0         if ($hash{FLAGS}) {
367 0           $self->_handle_notification($tokens);
368             }
369 0           my %ret = map {( $_, $hash{"BODY[$_]"} )} @$parts;
  0            
370 0           return \%ret;
371             }
372 0           return undef;
373             }
374              
375             sub get_summaries {
376 0     0 1   my ($self, $msg, $headers) = @_;
377 0 0         if (!$msg) {
    0          
378 0           $msg = '1:*';
379             } elsif (ref $msg eq 'ARRAY') {
380 0           $msg = join(',', @$msg);
381             }
382 0 0         if ($headers) {
383 0           $headers = " BODY.PEEK[HEADER.FIELDS ($headers)]";
384             } else {
385 0           $headers = '';
386             }
387 0           my ($ok, $lp) = $self->_tell_imap(FETCH => qq[$msg (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE$headers)], 1);
388 0 0         if ($ok) {
389 0           my @ret;
390 0           foreach (@$lp) {
391 0           my $summary;
392 0           my $tokens = _parse_tokens($_); ## in form: [ '*', ID, 'FETCH', [ tokens ]]
393 0 0         if ($tokens->[2] eq 'FETCH') {
394 0           my %hash = @{$tokens->[3]};
  0            
395 0 0         if ($hash{ENVELOPE}) {
396             # full fetch
397 0           $summary = Net::IMAP::Client::MsgSummary->new(\%hash, undef, !!$headers);
398 0           $summary->{seq_id} = $tokens->[1];
399             } else {
400             # 'FETCH' (probably FLAGS) notification!
401 0           $self->_handle_notification($tokens);
402             }
403             } else {
404             # notification!
405 0           $self->_handle_notification($tokens);
406             }
407 0 0         push @ret, $summary
408             if $summary;
409             }
410 0           return \@ret;
411             } else {
412 0           return undef;
413             }
414             }
415              
416             sub fetch {
417 0     0 1   my ($self, $msg, $keys) = @_;
418 0           my $wants_many = undef;
419 0 0         if (ref $msg eq 'ARRAY') {
420 0           $msg = join(',', @$msg);
421 0           $wants_many = 1;
422             }
423 0 0         if (ref $keys eq 'ARRAY') {
424 0           $keys = join(' ', @$keys);
425             }
426 0           my ($ok, $lp) = $self->_tell_imap(FETCH => qq[$msg ($keys)], 1);
427 0 0         if ($ok) {
428 0           my @ret;
429 0           foreach (@$lp) {
430 0           my $tokens = _parse_tokens($_)->[3];
431 0           push @ret, { @$tokens };
432             }
433 0 0 0       return $wants_many || @ret > 1 ? \@ret : $ret[0];
434             }
435             }
436              
437             sub create_folder {
438 0     0 1   my ($self, $folder) = @_;
439 0           my $quoted = $folder;
440 0           _string_quote($quoted);
441 0           my ($ok) = $self->_tell_imap(CREATE => $quoted);
442 0           return $ok;
443             }
444              
445             # recursively removes any subfolders!
446             sub delete_folder {
447 0     0 1   my ($self, $folder) = @_;
448 0           my $quoted = $folder . $self->separator . '*';
449 0           _string_quote($quoted);
450 0           my ($ok, $lines) = $self->_tell_imap(LIST => qq{"" $quoted});
451 0 0         if ($ok) {
452 0           my @subfolders;
453 0           foreach my $line (@$lines) {
454 0           my $tokens = _parse_tokens($line);
455 0           push @subfolders, $tokens->[4];
456             }
457 0           @subfolders = sort { length($b) - length($a) } @subfolders;
  0            
458 0           foreach (@subfolders) {
459 0           _string_quote($_);
460 0           ($ok) = $self->_tell_imap(DELETE => $_);
461             }
462 0           $quoted = $folder;
463 0           _string_quote($quoted);
464 0           ($ok) = $self->_tell_imap(DELETE => $quoted);
465             }
466 0           return $ok;
467             }
468              
469             sub append {
470 0     0 1   my ($self, $folder, $rfc822, $flags, $date) = @_;
471 0 0         die 'message body passed to append() must be a SCALAR reference'
472             unless ref $rfc822 eq 'SCALAR';
473 0           my $quoted = $folder;
474 0           _string_quote($quoted);
475 0           my $args = [ "$quoted " ];
476 0 0         if ($flags) {
477             # my @tmp = @$flags;
478             # $quoted = join(' ', map { _string_quote($_) } @tmp);
479             # push @$args, "($quoted) ";
480 0           push @$args, '(' . join(' ', @$flags) . ') ';
481             }
482 0 0         if ($date) {
483 0           my $tmp = $date;
484 0           _string_quote($tmp);
485 0           push @$args, "$tmp ";
486             }
487 0           push @$args, $rfc822;
488 0           my ($ok) = $self->_tell_imap(APPEND => $args, 1);
489 0           return $ok;
490             }
491              
492             sub copy {
493 0     0 1   my ($self, $msg, $folder) = @_;
494 0           my $quoted = $folder;
495 0           _string_quote($quoted);
496 0 0         if (ref $msg eq 'ARRAY') {
497 0           $msg = join(',', @$msg);
498             }
499 0           my ($ok) = $self->_tell_imap(COPY => "$msg $quoted", 1);
500 0           return $ok;
501             }
502              
503             sub get_flags {
504 0     0 1   my ($self, $msg) = @_;
505 0           my $wants_many = undef;
506 0 0         if (ref($msg) eq 'ARRAY') {
507 0           $msg = join(',', @$msg);
508 0           $wants_many = 1;
509             }
510 0           my ($ok, $lines) = $self->_tell_imap(FETCH => "$msg (UID FLAGS)", 1);
511 0 0         if ($ok) {
512 0           my %ret = map {
513 0           my $tokens = _parse_tokens($_)->[3];
514 0           my %hash = @$tokens;
515 0           $hash{UID} => $hash{FLAGS};
516             } @$lines;
517 0 0         return $wants_many ? \%ret : $ret{$msg};
518             }
519 0           return undef;
520             }
521              
522             sub get_threads {
523 0     0 1   my ($self, $algo, $msg) = @_;
524 0   0       $algo ||= "REFERENCES";
525 0           my ($ok, $lines) = $self->_tell_imap(THREAD => "$algo UTF-8 ALL");
526 0 0         if ($ok) {
527 0           my $result = $lines->[0][0];
528 0           $result =~ s/^\*\s+THREAD\s+//;
529 0           my $parsed = _parse_tokens([ $result ]);
530 0 0         if ($msg) {
531 0           (my $left = $result) =~ s/\b$msg\b.*$//;
532 0           my $thr = 0;
533 0           my $par = 0;
534 0           for (my $i = 0; $i < length($left); ++$i) {
535 0           my $c = substr($left, $i, 1);
536 0 0         if ($c eq '(') {
    0          
537 0           $par++;
538             } elsif ($c eq ')') {
539 0           $par--;
540 0 0         if ($par == 0) {
541 0           $thr++;
542             }
543             }
544             }
545 0           $parsed = $parsed->[$thr];
546             }
547 0           return $parsed;
548             }
549 0           return $ok;
550             }
551              
552             sub _store_helper {
553 0     0     my ($self, $msg, $flags, $cmd) = @_;
554 0 0         if (ref $msg eq 'ARRAY') {
555 0           $msg = join(',', @$msg);
556             }
557 0 0         unless (ref $flags) {
558 0           $flags = [ $flags ];
559             }
560 0           $flags = '(' . join(' ', @$flags) . ')';
561 0           $self->_tell_imap(STORE => "$msg $cmd $flags", 1);
562             }
563              
564             sub store {
565 0     0 1   my ($self, $msg, $flags) = @_;
566 0           $self->_store_helper($msg, $flags, 'FLAGS');
567             }
568              
569             sub add_flags {
570 0     0 1   my ($self, $msg, $flags) = @_;
571 0           $self->_store_helper($msg, $flags, '+FLAGS');
572             }
573              
574             sub del_flags {
575 0     0 1   my ($self, $msg, $flags) = @_;
576 0           $self->_store_helper($msg, $flags, '-FLAGS');
577             }
578              
579             sub delete_message {
580 0     0 1   my ($self, $msg) = @_;
581 0           $self->add_flags($msg, '\\Deleted');
582             }
583              
584             sub expunge {
585 0     0 1   my ($self) = @_;
586 0           my ($ok, $lines) = $self->_tell_imap('EXPUNGE' => undef, 1);
587 0 0 0       if ($ok && $lines && @$lines) {
      0        
588 0           my $ret = $lines->[0][0];
589 0 0         if ($ret =~ /^\*\s+(\d+)\s+EXPUNGE/) {
590 0           return $1 + 0;
591             }
592             }
593 0 0         return $ok ? -1 : undef;
594             }
595              
596             sub last_error {
597 0     0 1   my ($self) = @_;
598 0           $self->{_error} =~ s/\s+$//s; # remove trailing carriage return
599 0           return $self->{_error};
600             }
601              
602             sub notifications {
603 0     0 1   my ($self) = @_;
604 0           my $tmp = $self->{notifications};
605 0           $self->{notifications} = [];
606 0 0         return wantarray ? @$tmp : $tmp;
607             }
608              
609             ##### internal stuff #####
610              
611             sub _get_port {
612 0     0     my ($self) = @_;
613 0   0       return $self->{port} || ($self->{ssl} ? 993 : 143);
614             }
615              
616             sub _get_timeout {
617 0     0     my ($self) = @_;
618 0   0       return $self->{timeout} || 90;
619             }
620              
621             sub _get_server {
622 0     0     my ($self) = @_;
623 0           return $self->{server};
624             }
625              
626             sub _get_ssl_config {
627 0     0     my ($self) = @_;
628 0 0 0       if (!$self->{ssl_verify_peer}
      0        
      0        
629             || !$self->{ssl_ca_path}
630             && !$self->{ssl_ca_file}
631             && $^O ne 'linux') {
632 0           return SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE;
633             }
634              
635 0           my %ssl_config = ( SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER );
636              
637 0 0 0       if ($^O eq 'linux' && !$self->{ssl_ca_path} && !$self->{ssl_ca_file}) {
      0        
638 0           $ssl_config{SSL_ca_path} = '/etc/ssl/certs/';
639 0 0         -d $ssl_config{SSL_ca_path}
640             or die "$ssl_config{SSL_ca_path}: SSL certification directory not found";
641             }
642 0 0         $ssl_config{SSL_ca_path} = $self->{ssl_ca_path} if $self->{ssl_ca_path};
643 0 0         $ssl_config{SSL_ca_file} = $self->{ssl_ca_file} if $self->{ssl_ca_file};
644              
645 0           return %ssl_config;
646             }
647             sub _get_socket {
648 0     0     my ($self) = @_;
649 0           my $socket = $self->{socket} ||= ($self->{ssl} ? 'IO::Socket::SSL' : 'IO::Socket::INET')->new(
650 0 0 0       ( ( %{$self->{ssl_options}} ) x !!$self->{ssl} ),
    0          
651             PeerAddr => $self->_get_server,
652             PeerPort => $self->_get_port,
653             Timeout => $self->_get_timeout,
654             Proto => 'tcp',
655             Blocking => 1,
656             $self->_get_ssl_config,
657             ) or die "failed connect or ssl handshake: $!,$IO::Socket::SSL::SSL_ERROR";
658 0           $socket->sockopt(SO_KEEPALIVE, 1);
659 0           return $socket;
660             }
661              
662             sub _get_next_id {
663 0     0     return ++$_[0]->{_cmd_id};
664             }
665              
666             sub _socket_getline {
667 0     0     local $/ = "\r\n";
668 0           return $_[0]->_get_socket->getline;
669             }
670              
671             sub _socket_write {
672 0     0     my $self = shift;
673             # open LOG, '>>:raw', '/tmp/net-imap-client.log';
674             # print LOG @_;
675             # close LOG;
676 0           $self->_get_socket->write(@_);
677             }
678              
679             sub _send_cmd {
680 0     0     my ($self, $cmd, $args) = @_;
681              
682 0           local $\;
683 1     1   16 use bytes;
  1         3  
  1         11  
684 0           my $id = $self->_get_next_id;
685 0 0 0       if ($self->uid_mode && exists($UID_COMMANDS{$cmd})) {
686 0           $cmd = "UID $cmd";
687             }
688 0           my @literals = ();
689 0 0         if (ref $args eq 'ARRAY') {
690             # may contain literals
691 0           foreach (@$args) {
692 0 0         if (ref $_ eq 'SCALAR') {
693 0           push @literals, $_;
694 0           $_ = '{' . length($$_) . "}\r\n";
695             }
696             }
697 0           $args = join('', @$args);
698             }
699 0           my $socket = $self->_get_socket;
700 0 0         if (@literals == 0) {
701 0 0         $cmd = "NIC$id $cmd" . ($args ? " $args" : '') . "\r\n";
702 0           $self->_socket_write($cmd);
703             } else {
704 0           $cmd = "NIC$id $cmd ";
705 0           $self->_socket_write($cmd);
706 0           my @split = split(/\r\n/, $args);
707              
708 0           my $ea = each_array(@split, @literals);
709 0           while (my ($tmp, $lit) = $ea->()) {
710 0           $self->_socket_write($tmp . "\r\n");
711 0           my $line = $self->_socket_getline;
712             # print STDERR "$line - $tmp\n";
713 0 0         if ($line =~ /^\+/) {
714 0           $self->_socket_write($$lit);
715             } else {
716 0           $self->{_error} = "Expected continuation, got: $line";
717             # XXX: it's really bad if we get here, what to do?
718 0           return undef;
719             }
720             }
721 0           $self->_socket_write("\r\n"); # end of command!
722             }
723 0           $socket->flush;
724 0           return "NIC$id";
725             }
726              
727             sub _read_literal {
728 0     0     my ($self, $count) = @_;
729              
730 0           my $buf;
731 0           my @lines = ();
732 0           my $sock = $self->_get_socket;
733             # print STDERR "\033[1;31m ::: Reading $count bytes ::: \033[0m \n";
734 0           while ($count > 0) {
735 0           my $read = $sock->read($buf, min($count, $READ_BUFFER));
736             # print STDERR "GOT $read / $buf";
737 0           $count -= $read;
738 0 0         last if !$read;
739 0           push @lines, $buf;
740             }
741              
742 0           $buf = join('', @lines);
743 0           return \$buf;
744             }
745              
746             sub _cmd_ok {
747 0     0     my ($self, $res, $id) = @_;
748 0   0       $id ||= $self->{_cmd_id};
749              
750 0 0         if ($res =~ /^NIC$id\s+OK/i) {
    0          
751 0           return 1;
752             } elsif ($res =~ /^NIC$id\s+(?:NO|BAD)(?:\s+(.+))?/i) {
753 0   0       my $error = $1 || 'unknown error';
754 0           $self->{_error} = $error;
755 0           return 0;
756             }
757 0           return undef;
758             }
759              
760             sub _cmd_ok2 {
761 0     0     my ($self, $res) = @_;
762              
763 0 0         if ($res =~ /^(NIC\d+)\s+OK/i) {
    0          
764 0           my $id = $1;
765 0           return ($id, 1);
766             } elsif ($res =~ /^(NIC\d+)\s+(?:NO|BAD)(?:\s+(.+))?/i) {
767 0           my $id = $1;
768 0   0       my $error = $2 || 'unknown error';
769 0           $self->{_error} = $error;
770 0           return ($id, 0, $error);
771             }
772 0           return ();
773             }
774              
775             sub _reconnect_if_needed {
776 0     0     my ($self, $force) = @_;
777 0 0 0       if ($force || !$self->_get_socket->connected) {
778 0           $self->{socket} = undef;
779 0           $self->{greeting} = $self->_socket_getline;
780 0 0         if ($self->login) {
781 0 0         if ($self->{selected_folder}) {
782 0           $self->select($self->{selected_folder});
783             }
784 0           return 1;
785             }
786 0           return undef;
787             }
788 0           return 0;
789             }
790              
791             sub _tell_imap {
792 0     0     my ($self, $cmd, $args, $do_notf) = @_;
793              
794 0           $cmd = uc $cmd;
795              
796 0           my ($lineparts, $ok, $res);
797              
798 0           RETRY1: {
799 0           $self->_send_cmd($cmd, $args);
800 0 0         redo RETRY1 if $self->_reconnect_if_needed;
801             }
802              
803 0           $lineparts = []; # holds results in boxes
804 0           my $accumulator = []; # box for collecting results
805 0           while ($res = $self->_socket_getline) {
806             # print STDERR ">>>>$res<<<<<\n";
807              
808 0 0         if ($res =~ /^\*/) {
809              
810             # store previous box and start a new one
811              
812 0 0         push @$lineparts, $accumulator if @$accumulator;
813 0           $accumulator = [];
814             }
815 0 0         if ($res =~ /(.*)\{(\d+)\}\r\n/) {
816 0           my ($line, $len) = ($1, $2 + 0);
817 0           push @$accumulator,
818             $line,
819             $self->_read_literal($len);
820             } else {
821 0           $ok = $self->_cmd_ok($res);
822 0 0         if (defined($ok)) {
823 0           last;
824             } else {
825 0           push @$accumulator, $res;
826             }
827             }
828             }
829             # store last box
830 0 0         push @$lineparts, $accumulator if @$accumulator;
831              
832 0 0         unless (defined $res) {
833 0 0         goto RETRY1 if $self->_reconnect_if_needed(1);
834             }
835              
836 0 0         if ($do_notf) {
837 1     1   1219 no warnings 'uninitialized';
  1         3  
  1         2877  
838 0           for (my $i = scalar(@$lineparts); --$i >= 0;) {
839 0           my $line = $lineparts->[$i];
840              
841             # 1. notifications don't contain literals
842 0 0         last if scalar(@$line) != 1;
843              
844 0           my $text = $line->[0];
845              
846             # 2. FETCH notifications only contain FLAGS. We make a
847             # promise never to FETCH flags alone intentionally.
848              
849             # 3. Other notifications will have a first token different
850             # from the running command
851              
852 0 0 0       if ( $text =~ /^\*\s+\d+\s+FETCH\s*\(\s*FLAGS\s*\([^\)]*?\)\)/
853             || $text !~ /^\*\s+(?:\d+\s+)?$cmd/ ) {
854 0           my $tokens = _parse_tokens($line);
855 0 0         if ($self->_handle_notification($tokens, 1)) {
856 0           splice @$lineparts, $i, 1;
857             }
858 0           next;
859             }
860              
861 0           last;
862             }
863             }
864              
865 0 0         return wantarray ? ($ok, $lineparts) : $ok ? $lineparts : undef;
    0          
866             }
867              
868             # Variant of the above method that sends multiple commands. After
869             # sending all commands to the server, it waits until all results are
870             # returned and puts them in an array, in the order the commands were
871             # sent.
872             sub _tell_imap2 {
873 0     0     my ($self, @cmd) = @_;
874              
875 0           my %results;
876             my @ids;
877              
878 0           RETRY2: {
879 0           @ids = ();
880 0           foreach (@cmd) {
881 0           push @ids, $self->_send_cmd($_);
882 0 0         redo RETRY2 if $self->_reconnect_if_needed;
883             }
884             }
885              
886 0           %results = ();
887 0           for (0..$#cmd) {
888 0           my $lineparts = [];
889 0           my $accumulator = [];
890 0           my $res;
891 0           while ($res = $self->_socket_getline) {
892             # print STDERR "2: $res";
893 0 0         if ($res =~ /^\*/) {
894 0 0         push @$lineparts, $accumulator if @$accumulator;
895 0           $accumulator = [];
896             }
897 0 0         if ($res =~ /(.*)\{(\d+)\}\r\n/) {
898 0           my ($line, $len) = ($1, $2);
899 0           push @$accumulator,
900             $line,
901             $self->_read_literal($len);
902             } else {
903 0           my ($cmdid, $ok, $error) = $self->_cmd_ok2($res);
904 0 0         if (defined($ok)) {
905 0           $results{$cmdid} = [ $ok, $lineparts, $error ];
906 0           last;
907             } else {
908 0           push @$accumulator, $res;
909             }
910             }
911             }
912 0 0         push @$lineparts, $accumulator if @$accumulator;
913 0 0         unless (defined $res) {
914 0 0         goto RETRY2 if $self->_reconnect_if_needed(1);
915             }
916             }
917              
918 0           my @ret = @results{@ids};
919 0           return \@ret;
920             }
921              
922             sub _string_quote {
923 0     0     $_[0] =~ s/\\/\\\\/g;
924 0           $_[0] =~ s/\"/\\\"/g;
925 0           $_[0] = "\"$_[0]\"";
926             }
927              
928             sub _string_unquote {
929 0 0   0     if ($_[0] =~ s/^"//g) {
930 0           $_[0] =~ s/"$//g;
931 0           $_[0] =~ s/\\\"/\"/g;
932 0           $_[0] =~ s/\\\\/\\/g;
933             }
934             }
935              
936             ##### parse imap response #####
937             #
938             # This is probably the simplest/dumbest way to parse the IMAP output.
939             # Nevertheless it seems to be very stable and fast.
940             #
941             # $input is an array ref containing IMAP output. Normally it will
942             # contain only one entry -- a line of text -- but when IMAP sends
943             # literal data, we read it separately (see _read_literal) and store it
944             # as a scalar reference, therefore it can be like this:
945             #
946             # [ '* 11 FETCH (RFC822.TEXT ', \$DATA, ')' ]
947             #
948             # so that's why the routine looks a bit more complicated.
949             #
950             # It returns an array of tokens. Literal strings are dereferenced so
951             # for the above text, the output will be:
952             #
953             # [ '*', '11', 'FETCH', [ 'RFC822.TEXT', $DATA ] ]
954             #
955             # note that lists are represented as arrays.
956             #
957             sub _parse_tokens {
958 0     0     my ($input, $no_deref) = @_;
959              
960 0           my @tokens = ();
961 0           my @stack = (\@tokens);
962              
963 0           while (my $text = shift @$input) {
964 0 0         if (ref $text) {
965 0 0         push @{$stack[-1]}, ($no_deref ? $text : $$text);
  0            
966 0           next;
967             }
968 0           while (1) {
969 0           $text =~ m/\G\s+/gc;
970 0 0         if ($text =~ m/\G[([]/gc) {
    0          
    0          
    0          
    0          
    0          
971 0           my $sub = [];
972 0           push @{$stack[-1]}, $sub;
  0            
973 0           push @stack, $sub;
974             } elsif ($text =~ m/\G(BODY\[[a-zA-Z0-9._() -]*\])/gc) {
975 0           push @{$stack[-1]}, $1; # let's consider this an atom too
  0            
976             } elsif ($text =~ m/\G[])]/gc) {
977 0           pop @stack;
978             } elsif ($text =~ m/\G\"((?:\\.|[^\"\\])*)\"/gc) {
979 0           my $str = $1;
980             # unescape
981 0           $str =~ s/\\\"/\"/g;
982 0           $str =~ s/\\\\/\\/g;
983 0           push @{$stack[-1]}, $str; # found string
  0            
984             } elsif ($text =~ m/\G(\d+)/gc) {
985 0           push @{$stack[-1]}, $1 + 0; # found numeric
  0            
986             } elsif ($text =~ m/\G([a-zA-Z0-9_\$\\.+\/*&-]+)/gc) {
987 0           my $atom = $1;
988 0 0         if (lc $atom eq 'nil') {
989 0           $atom = undef;
990             }
991 0           push @{$stack[-1]}, $atom; # found atom
  0            
992             } else {
993 0           last;
994             }
995             }
996             }
997              
998 0           return \@tokens;
999             }
1000              
1001             sub _handle_notification {
1002 0     0     my ($self, $tokens, $reverse) = @_;
1003              
1004 1     1   14 no warnings 'uninitialized';
  1         3  
  1         539  
1005 0           my $not;
1006              
1007 0           my $sf = $self->{selected_folder};
1008 0 0         if ($sf) { # otherwise we shouldn't get any notifications, but whatever
1009 0           $sf = $self->{FOLDERS}{$sf};
1010 0 0         if ($tokens->[2] eq 'FETCH') {
    0          
    0          
    0          
    0          
    0          
1011 0           my %data = @{$tokens->[3]};
  0            
1012 0 0         if (my $flags = $data{FLAGS}) {
1013 0           $not = { seq => $tokens->[1] + 0,
1014             flags => $flags };
1015 0 0   0     if (first { $_ eq '\\Deleted' } @$flags) {
  0            
1016 0           --$sf->{messages};
1017 0           $not->{deleted} = 1;
1018             }
1019 0 0         if ($data{UID}) {
1020 0           $not->{uid} = $data{UID};
1021             }
1022             }
1023              
1024             } elsif ($tokens->[2] eq 'EXISTS') {
1025 0           $sf->{messages} = $tokens->[1] + 0;
1026 0           $not = { messages => $tokens->[1] + 0 };
1027              
1028             } elsif ($tokens->[2] eq 'EXPUNGE') {
1029 0           --$sf->{messages};
1030 0           $not = { seq => $tokens->[1] + 0, destroyed => 1 };
1031              
1032             } elsif ($tokens->[2] eq 'RECENT') {
1033 0           $sf->{recent} = $tokens->[1] + 0;
1034 0           $not = { recent => $tokens->[1] + 0 };
1035              
1036             } elsif ($tokens->[1] eq 'FLAGS') {
1037 0           $sf->{flags} = $tokens->[2];
1038 0           $not = { flags => $tokens->[2] };
1039              
1040             } elsif ($tokens->[1] eq 'OK') {
1041 0           $sf->{sflags}{$tokens->[2][0]} = $tokens->[2][1];
1042             }
1043             }
1044              
1045 0 0         if (defined $not) {
1046 0           $not->{folder} = $self->{selected_folder};
1047 0 0         if ($reverse) {
1048 0           unshift @{$self->{notifications}}, $not;
  0            
1049             } else {
1050 0           push @{$self->{notifications}}, $not;
  0            
1051             }
1052 0           return 1;
1053             }
1054              
1055 0           return 0;
1056             }
1057              
1058             1;
1059              
1060             __END__