File Coverage

blib/lib/Mail/Transport/IMAP4.pm
Criterion Covered Total %
statement 50 255 19.6
branch 18 156 11.5
condition 0 38 0.0
subroutine 10 35 28.5
pod 21 23 91.3
total 99 507 19.5


line stmt bran cond sub pod time code
1             # Copyrights 2001-2019 by [Mark Overmeer].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             # This code is part of distribution Mail-Box-IMAP4. Meta-POD processed with
6             # OODoc into POD and HTML manual-pages. See README.md
7             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
8              
9             package Mail::Transport::IMAP4;
10 3     3   100397 use vars '$VERSION';
  3         8  
  3         133  
11             $VERSION = '3.006';
12              
13 3     3   15 use base 'Mail::Transport::Receive';
  3         5  
  3         1075  
14              
15 3     3   4169 use strict;
  3         6  
  3         53  
16 3     3   11 use warnings;
  3         5  
  3         56  
17              
18 3     3   1254 use Digest::HMAC_MD5; # only availability check for CRAM_MD5
  3         3283  
  3         99  
19 3     3   1855 use Mail::IMAPClient ();
  3         108985  
  3         108  
20 3     3   29 use List::Util qw/first/;
  3         6  
  3         598  
21              
22              
23             sub init($)
24 0     0 0 0 { my ($self, $args) = @_;
25              
26 0   0     0 my $imap = $args->{imap_client} || 'Mail::IMAPClient';
27 0 0       0 if(ref $imap)
28 0         0 { $args->{port} = $imap->Port;
29 0         0 $args->{hostname} = $imap->Server;
30 0         0 $args->{username} = $imap->User;
31 0         0 $args->{password} = $imap->Password;
32             }
33             else
34 0 0 0     0 { $args->{port} ||= $args->{ssl} ? 993 : 143;
35             }
36              
37 0   0     0 $args->{via} ||= 'imap4';
38              
39 0 0       0 $self->SUPER::init($args) or return;
40              
41 0   0     0 $self->authentication($args->{authenticate} || 'AUTO');
42 0         0 $self->{MTI_domain} = $args->{domain};
43              
44 0 0       0 unless(ref $imap)
45             { # Create the IMAP transporter
46 0         0 my %opts;
47             $opts{ucfirst lc} = delete $args->{$_}
48 0         0 for grep /^[A-Z]/, keys %$args;
49              
50             # backwards compatibility
51 0   0     0 $opts{Starttls} ||= $args->{starttls};
52 0   0     0 my $ssl = $opts{Ssl} ||= $args->{ssl};
53              
54 0 0       0 $opts{Ssl} = [ %$ssl ] if ref $ssl eq 'HASH';
55              
56 3     3   1765 use Data::Dumper;
  3         16124  
  3         8208  
57 0         0 warn "CREATE IMAP ", Dumper \%opts;
58 0         0 warn Dumper $args;
59 0 0       0 $imap = $self->createImapClient($imap, %opts)
60             or return undef;
61             }
62            
63 0 0       0 $self->imapClient($imap) or return undef;
64 0 0       0 $self->login or return undef;
65 0         0 $self;
66             }
67              
68             sub url()
69 0     0 0 0 { my $self = shift;
70 0         0 my ($host, $port, $user, $pwd) = $self->remoteHost;
71 0         0 my $name = $self->folderName;
72 0 0       0 my $proto = $self->usesSSL ? 'imap4s' : 'imap4';
73 0         0 "$proto://$user:$pwd\@$host:$port$name";
74             }
75              
76             #------------------------------------------
77              
78              
79 0     0 1 0 sub usesSSL() { shift->imapClient->Ssl }
80              
81              
82             sub authentication(@)
83 0     0 1 0 { my ($self, @types) = @_;
84              
85             # What the client wants to use to login
86              
87             @types
88 0 0       0 or @types = exists $self->{MTI_auth} ? @{$self->{MTI_auth}} : 'AUTO';
  0 0       0  
89              
90 0 0 0     0 @types = qw/CRAM-MD5 DIGEST-MD5 PLAIN NTLM LOGIN/
91             if @types == 1 && $types[0] eq 'AUTO';
92              
93 0         0 $self->{MTI_auth} = \@types;
94              
95 0         0 my @clientside;
96 0         0 foreach my $auth (@types)
97 0 0       0 { push @clientside
    0          
98             , ref $auth eq 'ARRAY' ? $auth
99             : $auth eq 'NTLM' ? [ NTLM => \&Authen::NTLM::ntlm ]
100             : [ $auth => undef ];
101             }
102              
103 0         0 my %clientside = map +($_->[0] => $_), @clientside;
104              
105             # What does the server support? in its order of preference.
106              
107 0 0       0 my $imap = $self->imapClient or return ();
108 0 0       0 my @serverside = map { m/^AUTH=(\S+)/ ? uc($1) : () }
  0         0  
109             $imap->capability;
110              
111 0         0 my @auth;
112 0 0       0 if(@serverside) # server list auth capabilities
113 0 0       0 { @auth = map { $clientside{$_} ? delete $clientside{$_} : () }
  0         0  
114             @serverside;
115             }
116 0 0       0 @auth = @clientside unless @auth; # fallback to client's preference
117              
118 0         0 @auth;
119             }
120              
121              
122             sub domain(;$)
123 0     0 1 0 { my $self = shift;
124 0 0       0 return $self->{MTI_domain} = shift if @_;
125 0 0       0 $self->{MTI_domain} || ($self->remoteHost)[0];
126             }
127              
128             #------------------------------------------
129              
130              
131             sub imapClient(;$)
132 0     0 1 0 { my $self = shift;
133 0 0       0 @_ ? ($self->{MTI_client} = shift) : $self->{MTI_client};
134             }
135              
136              
137             sub createImapClient($@)
138 0     0 1 0 { my ($self, $class, @args) = @_;
139              
140 0         0 my ($host, $port) = $self->remoteHost;
141              
142 0         0 my $debug_level = $self->logPriority('DEBUG')+0;
143 0 0 0     0 if($self->log <= $debug_level || $self->trace <= $debug_level)
144 0         0 { tie *dh, 'Mail::IMAPClient::Debug', $self;
145 0         0 push @args, Debug => 1, Debug_fh => \*dh;
146             }
147              
148 0         0 my $client = $class->new
149             ( Server => $host, Port => $port
150             , User => undef, Password => undef # disable auto-login
151             , Uid => 1 # Safer
152             , Peek => 1 # Don't set \Seen automaticly
153             , @args
154             );
155              
156 0 0       0 $self->log(ERROR => $@), return undef if $@;
157 0         0 $client;
158             }
159              
160              
161             sub login(;$)
162 0     0 1 0 { my $self = shift;
163 0         0 my $imap = $self->imapClient;
164              
165 0 0       0 return $self if $imap->IsAuthenticated;
166              
167 0         0 my ($interval, $retries, $timeout) = $self->retry;
168              
169 0         0 my ($host, $port, $username, $password) = $self->remoteHost;
170 0 0       0 unless(defined $username)
171 0         0 { $self->log(ERROR => "IMAP4 requires a username and password");
172 0         0 return;
173             }
174 0 0       0 unless(defined $password)
175 0         0 { $self->log(ERROR => "IMAP4 username $username requires a password");
176 0         0 return;
177             }
178              
179 0         0 my $warn_fail;
180 0         0 while(1)
181             {
182 0         0 foreach my $auth ($self->authentication)
183 0         0 { my ($mechanism, $challenge) = @$auth;
184              
185 0         0 $imap->User(undef);
186 0         0 $imap->Password(undef);
187 0         0 $imap->Authmechanism(undef); # disable auto-login
188 0         0 $imap->Authcallback(undef);
189              
190 0 0       0 unless($imap->connect)
191 0         0 { $self->log(ERROR => "IMAP cannot connect to $host: "
192             , $imap->LastError);
193 0         0 return undef;
194             }
195              
196 0         0 $imap->User($username);
197 0         0 $imap->Password($password);
198 0         0 $imap->Authmechanism($mechanism);
199 0 0       0 $imap->Authcallback($challenge) if defined $challenge;
200              
201 0 0       0 if($imap->login)
202             {
203 0         0 $self->log(NOTICE => "IMAP4 authenication $mechanism to "
204             . "$username\@$host:$port successful");
205 0         0 return $self;
206             }
207             }
208              
209 0 0 0     0 $self->log(ERROR => "Couldn't contact to $username\@$host:$port")
210             , return undef if $retries > 0 && --$retries == 0;
211              
212 0 0       0 $warn_fail++
213             or $self->log(WARNING => "Failed attempt to login $username\@$host"
214             . ", retrying ".($retries+1)." times");
215              
216 0 0       0 sleep $interval if $interval;
217             }
218              
219 0         0 undef;
220             }
221              
222              
223             sub currentFolder(;$)
224 0     0 1 0 { my $self = shift;
225 0 0       0 return $self->{MTI_folder} unless @_;
226              
227 0         0 my $name = shift;
228              
229 0 0 0     0 if(defined $self->{MTI_folder} && $name eq $self->{MTI_folder})
230 0         0 { $self->log(DEBUG => "Folder $name already selected.");
231 0         0 return $name;
232             }
233              
234             # imap first deselects the old folder so if the next call
235             # fails the server will not have anything selected.
236 0         0 $self->{MTI_folder} = undef;
237              
238 0 0       0 my $imap = $self->imapClient or return;
239              
240 0 0 0     0 if($name eq '/' || $imap->select($name))
241 0         0 { $self->{MTI_folder} = $name;
242 0         0 $self->log(NOTICE => "Selected folder $name");
243 0         0 return 1;
244             }
245              
246             # Just because we couldn't select the folder that doesn't mean it doesn't
247             # exist. It just means that this particular imap client is warning us
248             # that it can't contain messages. So we'll verify that it does exist
249             # and, if so, we'll pretend like we could have selected it as if it were
250             # a regular folder.
251             # IMAPClient::exists() only works reliably for leaf folders so we need
252             # to grep for it ourselves.
253              
254 0 0   0   0 if(first { $_ eq $name } $self->folders)
  0         0  
255 0         0 { $self->{MTI_folder} = $name;
256 0         0 $self->log(NOTICE => "Couldn't select $name but it does exist.");
257 0         0 return 0;
258             }
259              
260 0         0 $self->log(NOTICE => "Folder $name does not exist!");
261 0         0 undef;
262             }
263              
264              
265             sub folders(;$)
266 0     0 1 0 { my $self = shift;
267 0         0 my $top = shift;
268              
269 0 0       0 my $imap = $self->imapClient or return ();
270 0 0 0     0 $top = undef if defined $top && $top eq '/';
271              
272             # We need to force the remote IMAP client to only return folders
273             # *underneath* the folder we specify. By default they want to return
274             # all folders.
275             # Alas IMAPClient always appends the separator so, despite what it says
276             # in its own docs, there's purpose to doing this. We just need
277             # to get whatever we get and postprocess it. ???Still true???
278 0         0 my @folders = $imap->folders($top);
279              
280             # We need to post-process the list returned by IMAPClient.
281             # This selects out the level of directories we're interested in.
282 0         0 my $sep = $imap->separator;
283 0 0       0 my $level = 1 + (defined $top ? () = $top =~ m/\Q$sep\E/g : -1);
284              
285             # There may be duplications, thanks to subdirs so we uniq it
286 0         0 my %uniq;
287 0   0     0 $uniq{(split /\Q$sep\E/, $_)[$level] || ''}++ for @folders;
288 0         0 delete $uniq{''};
289              
290 0         0 keys %uniq;
291             }
292              
293              
294             sub ids($)
295 0     0 1 0 { my $self = shift;
296 0 0       0 my $imap = $self->imapClient or return ();
297 0         0 $imap->messages;
298             }
299              
300              
301             # Explanation in Mail::Box::IMAP4::Message chapter DETAILS
302              
303             my %flags2labels =
304             ( # Standard IMAP4 labels
305             '\Seen' => [seen => 1]
306             , '\Answered' => [replied => 1]
307             , '\Flagged' => [flagged => 1]
308             , '\Deleted' => [deleted => 1]
309             , '\Draft' => [draft => 1]
310             , '\Recent' => [old => 0]
311              
312             # For the Netzwert extension (Mail::Box::Netzwert), some labels were
313             # added. You'r free to support them as well.
314             , '\Spam' => [spam => 1]
315             );
316              
317             my %labels2flags;
318             while(my ($k, $v) = each %flags2labels)
319             { $labels2flags{$v->[0]} = [ $k => $v->[1] ];
320             }
321              
322             # where IMAP4 supports requests for multiple flags at once, we here only
323             # request one set of flags a time (which will be slower)
324              
325             sub getFlags($$)
326 0     0 1 0 { my ($self, $id) = @_;
327 0 0       0 my $imap = $self->imapClient or return ();
328 0         0 my $labels = $self->flagsToLabels(SET => $imap->flags($id));
329              
330             # Add default values for missing flags
331 0         0 foreach my $s (values %flags2labels)
332             { $labels->{$s->[0]} = not $s->[1]
333 0 0       0 unless exists $labels->{$s->[0]};
334             }
335              
336 0         0 $labels;
337             }
338              
339              
340 0     0 1 0 sub listFlags() { keys %flags2labels }
341              
342              
343             # Mail::IMAPClient can only set one value a time, however we do more...
344             sub setFlags($@)
345 0     0 1 0 { my ($self, $id) = (shift, shift);
346              
347 0 0       0 my $imap = $self->imapClient or return ();
348 0         0 my (@set, @unset, @nonstandard);
349              
350 0         0 while(@_)
351 0         0 { my ($label, $value) = (shift, shift);
352 0 0       0 if(my $r = $labels2flags{$label})
353 0         0 { my $flag = $r->[0];
354 0 0       0 $value = $value ? $r->[1] : !$r->[1];
355             # exor can not be used, because value may be string
356 0 0       0 $value ? (push @set, $flag) : (push @unset, $flag);
357             }
358             else
359 0         0 { push @nonstandard, ($label => $value);
360             }
361             }
362              
363 0         0 $imap->set_flag($_, $id) foreach @set;
364 0         0 $imap->unset_flag($_, $id) foreach @unset;
365              
366 0         0 @nonstandard;
367             }
368              
369              
370             sub labelsToFlags(@)
371 7     7 1 6253 { my $thing = shift;
372 7         11 my @set;
373 7 100       15 if(@_==1)
374 2         4 { my $labels = shift;
375 2         8 while(my ($label, $value) = each %$labels)
376 6 50       11 { if(my $r = $labels2flags{$label})
377 6 100       23 { push @set, $r->[0] if ($value ? $r->[1] : !$r->[1]);
    100          
378             }
379             }
380             }
381             else
382 5         11 { while(@_)
383 20         49 { my ($label, $value) = (shift, shift);
384 20 50       48 if(my $r = $labels2flags{$label})
385 20 100       58 { push @set, $r->[0] if ($value ? $r->[1] : !$r->[1]);
    100          
386             }
387             }
388             }
389              
390 7         33 join " ", sort @set;
391             }
392              
393              
394             sub flagsToLabels($@)
395 3     3 1 7704 { my ($thing, $what) = (shift, shift);
396 3         6 my %labels;
397              
398 3         5 my $clear = $what eq 'CLEAR';
399              
400 3         7 foreach my $f (@_)
401 9 50       15 { if(my $lab = $flags2labels{$f})
402 9 50       21 { $labels{$lab->[0]} = $clear ? not($lab->[1]) : $lab->[1];
403             }
404             else
405 0         0 { (my $lab = $f) =~ s,^\\,,;
406 0         0 $labels{$lab}++;
407             }
408             }
409              
410 3 50       7 if($what eq 'REPLACE')
411 3         7 { my %found = map { ($_ => 1) } @_;
  9         16  
412 3         9 foreach my $f (keys %flags2labels)
413 21 100       30 { next if $found{$f};
414 12         17 my $lab = $flags2labels{$f};
415 12         25 $labels{$lab->[0]} = not $lab->[1];
416             }
417             }
418              
419 3 50       13 wantarray ? %labels : \%labels;
420             }
421              
422              
423             sub getFields($@)
424 0     0 1   { my ($self, $id) = (shift, shift);
425 0 0         my $imap = $self->imapClient or return ();
426 0 0         my $parsed = $imap->parse_headers($id, @_) or return ();
427              
428 0           my @fields;
429 0           while(my($n,$c) = each %$parsed)
430 0           { push @fields, map { Mail::Message::Field::Fast->new($n, $_) } @$c;
  0            
431             }
432              
433 0           @fields;
434             }
435              
436              
437             sub getMessageAsString($)
438 0 0   0 1   { my $imap = shift->imapClient or return;
439 0 0         my $uid = ref $_[0] ? shift->unique : shift;
440 0           $imap->message_string($uid);
441             }
442              
443              
444             sub fetch($@)
445 0     0 1   { my ($self, $msgs, @info) = @_;
446 0 0         return () unless @$msgs;
447 0 0         my $imap = $self->imapClient or return ();
448              
449 0           my %msgs = map { ($_->unique => {message => $_} ) } @$msgs;
  0            
450 0           my $lines = $imap->fetch( [keys %msgs], @info );
451              
452             # It's a pity that Mail::IMAPClient::fetch_hash cannot be used for
453             # single messages... now I had to reimplement the decoding...
454 0           while(@$lines)
455 0           { my $line = shift @$lines;
456 0 0         next unless $line =~ /\(.*?UID\s+(\d+)/i;
457 0           my $id = $+;
458 0 0         my $info = $msgs{$id} or next; # wrong uid
459              
460 0 0         if($line =~ s/^[^(]* \( \s* //x )
461 0           { while($line =~ s/(\S+) # field
462             \s+
463             (?: # value
464             \" ( (?:\\.|[^"])+ ) \"
465             | \( ( (?:\\.|[^)])+ ) \)
466             | (\w+)
467             )//xi)
468 0           { $info->{uc $1} = $+;
469             }
470              
471 0 0         if( $line =~ m/^\s* (\S+) [ ]*$/x )
472             { # Text block expected
473 0           my ($key, $value) = (uc $1, '');
474 0           while(@$lines)
475 0           { my $extra = shift @$lines;
476 0           $extra =~ s/\r\n$/\n/;
477 0 0         last if $extra eq ")\n";
478 0           $value .= $extra;
479             }
480 0           $info->{$key} = $value;
481             }
482             }
483              
484             }
485              
486 0           values %msgs;
487             }
488              
489              
490             sub appendMessage($$)
491 0     0 1   { my ($self, $message, $foldername, $date) = @_;
492 0 0         my $imap = $self->imapClient or return ();
493              
494 0 0 0       $date = $imap->Rfc_822($date)
495             if $date && $date !~ m/\D/;
496              
497 0           $imap->append_string
498             ( $foldername, $message->string
499             , $self->labelsToFlags($message->labels)
500             , $date
501             );
502             }
503              
504              
505             sub destroyDeleted($)
506 0     0 1   { my ($self, $folder) = @_;
507 0 0         defined $folder or return;
508              
509 0 0         my $imap = shift->imapClient or return;
510 0           $imap->expunge($folder);
511             }
512              
513              
514             sub createFolder($)
515 0 0   0 1   { my $imap = shift->imapClient or return ();
516 0           $imap->create(shift);
517             }
518              
519              
520             sub deleteFolder($)
521 0 0   0 1   { my $imap = shift->imapClient or return ();
522 0           $imap->delete(shift);
523             }
524              
525             #------------------------------------------
526              
527             sub DESTROY()
528 0     0     { my $self = shift;
529 0           my $imap = $self->imapClient;
530              
531 0           $self->SUPER::DESTROY;
532 0 0         $imap->logout if defined $imap;
533             }
534              
535             #------------------------------------------
536              
537             # Tied filehandle translates IMAP's debug system into Mail::Reporter
538             # calls.
539             sub Mail::IMAPClient::Debug::TIEHANDLE($)
540 0     0     { my ($class, $logger) = @_;
541 0           bless \$logger, $class;
542             }
543              
544             sub Mail::IMAPClient::Debug::PRINT(@)
545 0     0     { my $logger = ${ (shift) };
  0            
546 0           $logger->log(DEBUG => @_);
547             }
548              
549             1;