File Coverage

blib/lib/Mail/Box/Manager.pm
Criterion Covered Total %
statement 218 291 74.9
branch 88 148 59.4
condition 55 141 39.0
subroutine 25 28 89.2
pod 17 19 89.4
total 403 627 64.2


line stmt bran cond sub pod time code
1             # Copyrights 2001-2020 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. 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::Box::Manager;
10 14     14   10456 use vars '$VERSION';
  14         32  
  14         940  
11             $VERSION = '3.009';
12              
13 14     14   90 use base 'Mail::Reporter';
  14         93  
  14         1718  
14              
15 14     14   92 use strict;
  14         29  
  14         410  
16 14     14   93 use warnings;
  14         26  
  14         674  
17              
18 14     14   6609 use Mail::Box;
  14         82  
  14         555  
19              
20 14     14   112 use List::Util 'first';
  14         30  
  14         914  
21 14     14   92 use Scalar::Util 'weaken';
  14         30  
  14         50061  
22              
23             # failed compilation will not complain a second time
24             # so we need to keep track.
25             my %require_failed;
26              
27              
28             my @basic_folder_types =
29             ( [ mbox => 'Mail::Box::Mbox' ]
30             , [ mh => 'Mail::Box::MH' ]
31             , [ maildir => 'Mail::Box::Maildir' ]
32             , [ pop => 'Mail::Box::POP3' ]
33             , [ pop3 => 'Mail::Box::POP3' ]
34             , [ pops => 'Mail::Box::POP3s' ]
35             , [ pop3s => 'Mail::Box::POP3s' ]
36             , [ imap => 'Mail::Box::IMAP4' ]
37             , [ imap4 => 'Mail::Box::IMAP4' ]
38             , [ imaps => 'Mail::Box::IMAP4s' ]
39             , [ imap4s => 'Mail::Box::IMAP4s' ]
40             );
41              
42             my @managers; # usually only one, but there may be more around :(
43              
44             sub init($)
45 13     13 0 4773 { my ($self, $args) = @_;
46 13         105 $self->SUPER::init($args);
47              
48             # Register all folder-types. There may be some added later.
49              
50 13         229 my @new_types;
51 13 50       63 if(exists $args->{folder_types})
52             { @new_types = ref $args->{folder_types}[0]
53 0         0 ? @{$args->{folder_types}}
54 0 0       0 : $args->{folder_types};
55             }
56              
57 13         75 my @basic_types = reverse @basic_folder_types;
58 13 50       56 if(my $basic = $args->{autodetect})
59 0 0       0 { my %types = map +($_ => 1), ref $basic ? @$basic : $basic;
60 0         0 @basic_types = grep $types{$_->[0]}, @basic_types;
61             }
62              
63 13         43 $self->{MBM_folder_types} = [];
64 13         90 $self->registerType(@$_) for @new_types, @basic_types;
65              
66 13   100     105 $self->{MBM_default_type} = $args->{default_folder_type} || 'mbox';
67              
68             # Inventory on existing folder-directories.
69 13         44 my $fd = $self->{MBM_folderdirs} = [ ];
70 13 100       55 if(exists $args->{folderdir})
71 1         3 { my @dirs = $args->{folderdir};
72 1 50       4 @dirs = @{$dirs[0]} if ref $dirs[0] eq 'ARRAY';
  0         0  
73 1         4 push @$fd, @dirs;
74             }
75              
76 13 50       51 if(exists $args->{folderdirs})
77 0         0 { my @dirs = $args->{folderdirs};
78 0 0       0 @dirs = @{$dirs[0]} if ref $dirs[0];
  0         0  
79 0         0 push @$fd, @dirs;
80             }
81 13         42 push @$fd, '.';
82              
83 13         38 $self->{MBM_folders} = [];
84 13         34 $self->{MBM_threads} = [];
85              
86 13         32 push @managers, $self;
87 13         71 weaken $managers[-1];
88              
89 13         56 $self;
90             }
91              
92             #-------------------------------------------
93              
94             sub registerType($$@)
95 143     143 1 323 { my ($self, $name, $class, @options) = @_;
96 143         189 unshift @{$self->{MBM_folder_types}}, [$name, $class, @options];
  143         326  
97 143         331 $self;
98             }
99              
100              
101             sub folderdir()
102 2 50   2 1 9 { my $dirs = shift->{MBM_folderdirs} or return ();
103 2 50       18 wantarray ? @$dirs : $dirs->[0];
104             }
105              
106              
107             sub folderTypes()
108 0     0 1 0 { my $self = shift;
109 0         0 my %uniq;
110 0         0 $uniq{$_->[0]}++ foreach @{$self->{MBM_folder_types}};
  0         0  
111 0         0 sort keys %uniq;
112             }
113              
114              
115             sub defaultFolderType()
116 3     3 1 1028 { my $self = shift;
117 3         7 my $name = $self->{MBM_default_type};
118 3 50       14 return $name if $name =~ m/\:\:/; # obviously a class name
119              
120 3         5 foreach my $def (@{$self->{MBM_folder_types}})
  3         10  
121 5 100 66     56 { return $def->[1] if $def->[0] eq $name || $def->[1] eq $name;
122             }
123              
124 0         0 undef;
125             }
126              
127             #-------------------------------------------
128              
129              
130             sub open(@)
131 40     40 1 6346 { my $self = shift;
132 40 100       211 my $name = @_ % 2 ? shift : undef;
133 40         285 my %args = @_;
134 40   100     228 $args{authentication} ||= 'AUTO';
135              
136 40 50 0     216 $name = defined $args{folder} ? $args{folder} : ($ENV{MAIL} || '')
    100          
137             unless defined $name;
138              
139 40 50 33     197 if($name =~ m/^(\w+)\:/ && grep $_ eq $1, $self->folderTypes)
140             { # Complicated folder URL
141 0         0 my %decoded = $self->decodeFolderURL($name);
142 0 0       0 if(keys %decoded)
143             { # accept decoded info
144 0         0 @args{keys %decoded} = values %decoded;
145             }
146             else
147 0         0 { $self->log(ERROR => "Illegal folder URL '$name'.");
148 0         0 return;
149             }
150             }
151             else
152             { # Simple folder name
153 40         114 $args{folder} = $name;
154             }
155              
156             # Do not show password in folder name
157 40         118 my $type = $args{type};
158 40 100 33     469 if(!defined $type) { ; }
    50 33        
    50 33        
    50 33        
    50          
159             elsif($type eq 'pop3' || $type eq 'pop')
160 0   0     0 { my $un = $args{username} ||= $ENV{USER} || $ENV{LOGIN};
      0        
161 0   0     0 my $srv = $args{server_name} ||= 'localhost';
162 0   0     0 my $port = $args{server_port} ||= 110;
163 0         0 $args{folderdir} = $name = "pop3://$un\@$srv:$port";
164             }
165             elsif($type eq 'pop3s' || $type eq 'pops')
166 0   0     0 { my $un = $args{username} ||= $ENV{USER} || $ENV{LOGIN};
      0        
167 0   0     0 my $srv = $args{server_name} ||= 'localhost';
168 0   0     0 my $port = $args{server_port} ||= 995;
169 0         0 $args{folderdir} = $name = "pop3s://$un\@$srv:$port";
170             }
171             elsif($type eq 'imap4' || $type eq 'imap')
172 0   0     0 { my $un = $args{username} ||= $ENV{USER} || $ENV{LOGIN};
      0        
173 0   0     0 my $srv = $args{server_name} ||= 'localhost';
174 0   0     0 my $port = $args{server_port} ||= 143;
175 0         0 $args{folderdir} = $name = "imap4://$un\@$srv:$port";
176             }
177             elsif($type eq 'imap4s' || $type eq 'imaps')
178 0   0     0 { my $un = $args{username} ||= $ENV{USER} || $ENV{LOGIN};
      0        
179 0   0     0 my $srv = $args{server_name} ||= 'localhost';
180 0   0     0 my $port = $args{server_port} ||= 993;
181 0         0 $args{folderdir} = $name = "imap4s://$un\@$srv:$port";
182             }
183              
184 40 50 33     222 unless(defined $name && length $name)
185 0         0 { $self->log(ERROR => "No foldername specified to open.");
186 0         0 return undef;
187             }
188            
189             $args{folderdir} ||= $self->{MBM_folderdirs}->[0]
190 40 50 66     262 if $self->{MBM_folderdirs};
191              
192 40   100     151 $args{access} ||= 'r';
193              
194 40 50 66     258 if($args{create} && $args{access} !~ m/w|a/)
195 0         0 { $self->log(WARNING
196             => "Will never create a folder $name without having write access.");
197 0         0 undef $args{create};
198             }
199              
200             # Do not open twice.
201 40 100       165 if(my $folder = $self->isOpenFolder($name))
202 1         7 { $self->log(ERROR => "Folder $name is already open.");
203 1         26 return undef;
204             }
205              
206             #
207             # Which folder type do we need?
208             #
209              
210 39         232 my ($folder_type, $class, @defaults);
211 39 100       123 if($type)
212             { # User-specified foldertype prevails.
213 26         52 foreach (@{$self->{MBM_folder_types}})
  26         82  
214 39         94 { (my $abbrev, $class, @defaults) = @$_;
215              
216 39 100 100     139 if($type eq $abbrev || $type eq $class)
217 26         56 { $folder_type = $abbrev;
218 26         50 last;
219             }
220             }
221              
222 26 50       72 $self->log(ERROR => "Folder type $type is unknown, using autodetect.")
223             unless $folder_type;
224             }
225              
226 39 100       126 unless($folder_type)
227             { # Try to autodetect foldertype.
228 13         28 foreach (@{$self->{MBM_folder_types}})
  13         46  
229 29 50       86 { next unless $_;
230 29         96 (my $abbrev, $class, @defaults) = @$_;
231 29 100       118 next if $require_failed{$class};
232              
233 25         1764 eval "require $class";
234 25 100       167 if($@)
235 4         50 { $require_failed{$class}++;
236 4         13 next;
237             }
238              
239 21 100       223 if($class->foundIn($name, @defaults, %args))
240 12         63 { $folder_type = $abbrev;
241 12         39 last;
242             }
243             }
244             }
245              
246 39 100       125 unless($folder_type)
247             { # Use specified default
248 1 50       7 if(my $type = $self->{MBM_default_type})
249 1         4 { foreach (@{$self->{MBM_folder_types}})
  1         5  
250 1         3 { (my $abbrev, $class, @defaults) = @$_;
251 1 50 33     5 if($type eq $abbrev || $type eq $class)
252 1         2 { $folder_type = $abbrev;
253 1         3 last;
254             }
255             }
256             }
257             }
258              
259 39 50       124 unless($folder_type)
260             { # use first type (last defined)
261 0         0 ($folder_type, $class, @defaults) = @{$self->{MBM_folder_types}[0]};
  0         0  
262             }
263            
264             #
265             # Try to open the folder
266             #
267              
268 39 50       117 return if $require_failed{$class};
269 39         2839 eval "require $class";
270 39 50       191 if($@)
271 0         0 { $self->log(ERROR => "Failed for folder default $class: $@");
272 0         0 $require_failed{$class}++;
273 0         0 return ();
274             }
275              
276 39         126 push @defaults, manager => $self;
277 39         395 my $folder = $class->new(@defaults, %args);
278 39 100       184 unless(defined $folder)
279             { $self->log(WARNING =>
280             "Folder does not exist, failed opening $folder_type folder $name.")
281 1 50       11 unless $args{access} eq 'd';
282 1         24 return;
283             }
284              
285 38         373 $self->log(PROGRESS => "Opened folder $name ($folder_type).");
286 38         777 push @{$self->{MBM_folders}}, $folder;
  38         129  
287 38         325 $folder;
288             }
289              
290              
291 62     62 1 2265 sub openFolders() { @{shift->{MBM_folders}} }
  62         428  
292              
293              
294             sub isOpenFolder($)
295 46     46 1 140 { my ($self, $name) = @_;
296 46     58   327 first {$name eq $_->name} $self->openFolders;
  58         187  
297             }
298              
299             #-------------------------------------------
300              
301              
302             sub close($@)
303 37     37 1 155 { my ($self, $folder, %options) = @_;
304 37 50       113 return unless $folder;
305              
306 37         158 my $name = $folder->name;
307 37         97 my @remaining = grep {$name ne $_->name} @{$self->{MBM_folders}};
  89         195  
  37         113  
308              
309             # folder opening failed:
310 37 100       72 return if @{$self->{MBM_folders}} == @remaining;
  37         153  
311              
312 36         122 $self->{MBM_folders} = [ @remaining ];
313 36         70 $_->removeFolder($folder) foreach @{$self->{MBM_threads}};
  36         132  
314              
315             $folder->close(close_by_manager => 1, %options)
316 36 100       134 unless $options{close_by_self};
317              
318 36         154 $self;
319             }
320              
321             #-------------------------------------------
322              
323              
324             sub closeAllFolders(@)
325 5     5 1 20 { my ($self, @options) = @_;
326 5         27 $_->close(@options) for $self->openFolders;
327 5         126 $self;
328             }
329              
330 14   66 14   7870 END { map defined $_ && $_->closeAllFolders, @managers }
331              
332             #-------------------------------------------
333              
334             sub delete($@)
335 0     0 1 0 { my ($self, $name, %args) = @_;
336 0         0 my $recurse = delete $args{recursive};
337              
338 0 0       0 my $folder = $self->open(folder => $name, access => 'd', %args)
339             or return $self; # still successful
340              
341 0         0 $folder->delete(recursive => $recurse);
342             }
343              
344             #-------------------------------------------
345              
346             sub appendMessage(@)
347 6     6 1 259127 { my $self = shift;
348 6         35 my @appended = $self->appendMessages(@_);
349 6 100       640 wantarray ? @appended : $appended[0];
350             }
351              
352             sub appendMessages(@)
353 6     6 0 16 { my $self = shift;
354 6         11 my $folder;
355 6 50 33     42 $folder = shift if !ref $_[0] || $_[0]->isa('Mail::Box');
356              
357 6         14 my @messages;
358 6   100     63 push @messages, shift while @_ && ref $_[0];
359              
360 6         31 my %options = @_;
361 6   33     22 $folder ||= $options{folder};
362              
363             # Try to resolve filenames into opened-files.
364 6 50 66     37 $folder = $self->isOpenFolder($folder) || $folder
365             unless ref $folder;
366              
367 6 100       38 if(ref $folder)
368             { # An open file.
369 3 50       26 unless($folder->isa('Mail::Box'))
370 0         0 { $self->log(ERROR =>
371             "Folder $folder is not a Mail::Box; cannot add a message.\n");
372 0         0 return ();
373             }
374              
375 3         10 foreach (@messages)
376 3 50 33     33 { next unless $_->isa('Mail::Box::Message') && $_->folder;
377 0         0 $self->log(WARNING =>
378             "Use moveMessage() or copyMessage() to move between open folders.");
379             }
380              
381 3         24 return $folder->addMessages(@messages);
382             }
383              
384             # Not an open file.
385             # Try to autodetect the folder-type and then add the message.
386              
387 3         13 my ($name, $class, @gen_options, $found);
388              
389 3         8 foreach (@{$self->{MBM_folder_types}})
  3         57  
390 6         27 { ($name, $class, @gen_options) = @$_;
391 6 50       27 next if $require_failed{$class};
392 6         367 eval "require $class";
393 6 50       30 if($@)
394 0         0 { $require_failed{$class}++;
395 0         0 next;
396             }
397              
398 6 100       39 if($class->foundIn($folder, @gen_options, access => 'a'))
399 3         11 { $found++;
400 3         9 last;
401             }
402             }
403            
404             # The folder was not found at all, so we take the default folder-type.
405 3         13 my $type = $self->{MBM_default_type};
406 3 50 33     16 if(!$found && $type)
407 0         0 { foreach (@{$self->{MBM_folder_types}})
  0         0  
408 0         0 { ($name, $class, @gen_options) = @$_;
409 0 0 0     0 if($type eq $name || $type eq $class)
410 0         0 { $found++;
411 0         0 last;
412             }
413             }
414             }
415              
416             # Even the default foldertype was not found (or nor defined).
417 3 50       11 ($name, $class, @gen_options) = @{$self->{MBM_folder_types}[0]}
  0         0  
418             unless $found;
419              
420 3         36 $class->appendMessages
421             ( type => $name
422             , messages => \@messages
423             , @gen_options
424             , %options
425             , folder => $folder
426             );
427             }
428              
429              
430              
431             sub copyMessage(@)
432 42     42 1 601 { my $self = shift;
433 42         63 my $folder;
434 42 50 33     269 $folder = shift if !ref $_[0] || $_[0]->isa('Mail::Box');
435              
436 42         68 my @messages;
437 42   100     168 while(@_ && ref $_[0])
438 42         62 { my $message = shift;
439 42 50       127 $self->log(ERROR =>
440             "Use appendMessage() to add messages which are not in a folder.")
441             unless $message->isa('Mail::Box::Message');
442 42         120 push @messages, $message;
443             }
444              
445 42         75 my %args = @_;
446 42   33     107 $folder ||= $args{folder};
447 42 50       111 my $share = exists $args{share} ? $args{share} : $args{_delete};
448              
449             # Try to resolve filenames into opened-files.
450 42 50 0     92 $folder = $self->isOpenFolder($folder) || $folder
451             unless ref $folder;
452              
453 42 50       87 unless(ref $folder)
454 0         0 { my @c = $self->appendMessages(@messages, %args, folder => $folder);
455 0 0       0 if($args{_delete})
456 0         0 { $_->label(deleted => 1) for @messages;
457             }
458 0         0 return @c;
459             }
460              
461 42         51 my @coerced;
462 42         68 foreach my $msg (@messages)
463 42 50       107 { if($msg->folder eq $folder) # ignore move to same folder
464 0         0 { push @coerced, $msg;
465 0         0 next;
466             }
467 42         190 push @coerced, $msg->copyTo($folder, share => $args{share});
468 42 100       156 $msg->label(deleted => 1) if $args{_delete};
469             }
470 42         154 @coerced;
471             }
472              
473              
474              
475             sub moveMessage(@)
476 1     1 1 416 { my $self = shift;
477 1         6 $self->copyMessage(@_, _delete => 1);
478             }
479              
480             #-------------------------------------------
481              
482             sub threads(@)
483 3     3 1 324 { my $self = shift;
484 3         9 my @folders;
485 3   100     36 push @folders, shift
      66        
486             while @_ && ref $_[0] && $_[0]->isa('Mail::Box');
487 3         13 my %args = @_;
488              
489 3         10 my $base = 'Mail::Box::Thread::Manager';
490 3   33     18 my $type = $args{threader_type} || $base;
491              
492 3   66     16 my $folders = delete $args{folder} || delete $args{folders};
493 3 50       19 push @folders
    100          
494             , ( !$folders ? ()
495             : ref $folders eq 'ARRAY' ? @$folders
496             : $folders
497             );
498              
499 3 50       12 $self->log(INTERNAL => "No folders specified.")
500             unless @folders;
501              
502 3         6 my $threads;
503 3 50       11 if(ref $type)
504             { # Already prepared object.
505 0 0       0 $self->log(INTERNAL => "You need to pass a $base derived")
506             unless $type->isa($base);
507 0         0 $threads = $type;
508             }
509             else
510             { # Create an object. The code is compiled, which safes us the
511             # need to compile Mail::Box::Thread::Manager when no threads are needed.
512 3         271 eval "require $type";
513 3 50       20 $self->log(INTERNAL => "Unusable threader $type: $@") if $@;
514              
515 3 50       39 $self->log(INTERNAL => "You need to pass a $base derived")
516             unless $type->isa($base);
517              
518 3         27 $threads = $type->new(manager => $self, %args);
519             }
520              
521 3         21 $threads->includeFolder($_) foreach @folders;
522 3         6 push @{$self->{MBM_threads}}, $threads;
  3         10  
523 3         13 $threads;
524             }
525              
526             #-------------------------------------------
527              
528             sub toBeThreaded($@)
529 578     578 1 936 { my $self = shift;
530 578         800 $_->toBeThreaded(@_) foreach @{$self->{MBM_threads}};
  578         1897  
531             }
532              
533              
534             sub toBeUnthreaded($@)
535 0     0 1 0 { my $self = shift;
536 0         0 $_->toBeUnthreaded(@_) foreach @{$self->{MBM_threads}};
  0         0  
537             }
538              
539              
540             sub decodeFolderURL($)
541 16     16 1 4571 { my ($self, $name) = @_;
542              
543             return unless
544 16 100       156 my ($type, $username, $password, $hostname, $port, $path)
545             = $name =~ m!^(\w+)\: # protocol
546             (?://
547             (?:([^:@/]*) # username
548             (?:\:([^@/]*))? # password
549             \@)?
550             ([\w.-]+)? # hostname
551             (?:\:(\d+))? # port number
552             )?
553             (.*) # foldername
554             !x;
555              
556 15   33     70 $username ||= $ENV{USER} || $ENV{LOGNAME};
      66        
557 15   100     45 $password ||= '';
558              
559 15         29 for($username, $password)
560 30         40 { s/\+/ /g;
561 30         47 s/\%([A-Fa-f0-9]{2})/chr hex $1/ge;
  0         0  
562             }
563              
564 15   100     41 $hostname ||= 'localhost';
565              
566 15   100     34 $path ||= '=';
567              
568 15         136 ( type => $type, folder => $path
569             , username => $username, password => $password
570             , server_name => $hostname, server_port => $port
571             );
572             }
573              
574             #-------------------------------------------
575              
576             1;