File Coverage

blib/lib/Mail/Transport/IMAP4.pm
Criterion Covered Total %
statement 47 250 18.8
branch 18 156 11.5
condition 0 38 0.0
subroutine 9 34 26.4
pod 21 23 91.3
total 95 501 18.9


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