File Coverage

blib/lib/Mail/Transport/IMAP4.pm
Criterion Covered Total %
statement 49 238 20.5
branch 18 152 11.8
condition 0 35 0.0
subroutine 10 35 28.5
pod 21 23 91.3
total 98 483 20.2


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