File Coverage

blib/lib/Net/IMAP/Client.pm
Criterion Covered Total %
statement 36 644 5.5
branch 0 298 0.0
condition 0 74 0.0
subroutine 12 71 16.9
pod 33 33 100.0
total 81 1120 7.2


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