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-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. 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   8252 use vars '$VERSION';
  14         29  
  14         771  
11             $VERSION = '3.008';
12              
13 14     14   94 use base 'Mail::Reporter';
  14         75  
  14         1473  
14              
15 14     14   84 use strict;
  14         23  
  14         278  
16 14     14   59 use warnings;
  14         23  
  14         564  
17              
18 14     14   5312 use Mail::Box;
  14         44  
  14         455  
19              
20 14     14   96 use List::Util 'first';
  14         27  
  14         664  
21 14     14   71 use Scalar::Util 'weaken';
  14         24  
  14         40489  
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 3726 { my ($self, $args) = @_;
46 13         90 $self->SUPER::init($args);
47              
48             # Register all folder-types. There may be some added later.
49              
50 13         190 my @new_types;
51 13 50       55 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         59 my @basic_types = reverse @basic_folder_types;
58 13 50       47 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         37 $self->{MBM_folder_types} = [];
64 13         78 $self->registerType(@$_) for @new_types, @basic_types;
65              
66 13   100     84 $self->{MBM_default_type} = $args->{default_folder_type} || 'mbox';
67              
68             # Inventory on existing folder-directories.
69 13         38 my $fd = $self->{MBM_folderdirs} = [ ];
70 13 100       44 if(exists $args->{folderdir})
71 1         3 { my @dirs = $args->{folderdir};
72 1 50       3 @dirs = @{$dirs[0]} if ref $dirs[0] eq 'ARRAY';
  0         0  
73 1         3 push @$fd, @dirs;
74             }
75              
76 13 50       44 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         32 push @$fd, '.';
82              
83 13         27 $self->{MBM_folders} = [];
84 13         33 $self->{MBM_threads} = [];
85              
86 13         27 push @managers, $self;
87 13         61 weaken $managers[-1];
88              
89 13         42 $self;
90             }
91              
92             #-------------------------------------------
93              
94             sub registerType($$@)
95 143     143 1 258 { my ($self, $name, $class, @options) = @_;
96 143         153 unshift @{$self->{MBM_folder_types}}, [$name, $class, @options];
  143         263  
97 143         266 $self;
98             }
99              
100              
101             sub folderdir()
102 2 50   2 1 16 { my $dirs = shift->{MBM_folderdirs} or return ();
103 2 50       17 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 1089 { my $self = shift;
117 3         8 my $name = $self->{MBM_default_type};
118 3 50       10 return $name if $name =~ m/\:\:/; # obviously a class name
119              
120 3         6 foreach my $def (@{$self->{MBM_folder_types}})
  3         8  
121 5 100 66     34 { 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 4503 { my $self = shift;
132 40 100       142 my $name = @_ % 2 ? shift : undef;
133 40         218 my %args = @_;
134 40   100     179 $args{authentication} ||= 'AUTO';
135              
136 40 50 0     162 $name = defined $args{folder} ? $args{folder} : ($ENV{MAIL} || '')
    100          
137             unless defined $name;
138              
139 40 50 33     168 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         83 $args{folder} = $name;
154             }
155              
156             # Do not show password in folder name
157 40         72 my $type = $args{type};
158 40 100 33     373 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     182 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     186 if $self->{MBM_folderdirs};
191              
192 40   100     103 $args{access} ||= 'r';
193              
194 40 50 66     186 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       122 if(my $folder = $self->isOpenFolder($name))
202 1         7 { $self->log(ERROR => "Folder $name is already open.");
203 1         22 return undef;
204             }
205              
206             #
207             # Which folder type do we need?
208             #
209              
210 39         173 my ($folder_type, $class, @defaults);
211 39 100       90 if($type)
212             { # User-specified foldertype prevails.
213 26         42 foreach (@{$self->{MBM_folder_types}})
  26         65  
214 39         77 { (my $abbrev, $class, @defaults) = @$_;
215              
216 39 100 100     109 if($type eq $abbrev || $type eq $class)
217 26         41 { $folder_type = $abbrev;
218 26         41 last;
219             }
220             }
221              
222 26 50       78 $self->log(ERROR => "Folder type $type is unknown, using autodetect.")
223             unless $folder_type;
224             }
225              
226 39 100       85 unless($folder_type)
227             { # Try to autodetect foldertype.
228 13         21 foreach (@{$self->{MBM_folder_types}})
  13         40  
229 29 50       78 { next unless $_;
230 29         74 (my $abbrev, $class, @defaults) = @$_;
231 29 100       87 next if $require_failed{$class};
232              
233 25         1378 eval "require $class";
234 25 100       136 if($@)
235 4         11 { $require_failed{$class}++;
236 4         8 next;
237             }
238              
239 21 100       185 if($class->foundIn($name, @defaults, %args))
240 12         55 { $folder_type = $abbrev;
241 12         33 last;
242             }
243             }
244             }
245              
246 39 100       109 unless($folder_type)
247             { # Use specified default
248 1 50       6 if(my $type = $self->{MBM_default_type})
249 1         2 { foreach (@{$self->{MBM_folder_types}})
  1         4  
250 1         3 { (my $abbrev, $class, @defaults) = @$_;
251 1 50 33     6 if($type eq $abbrev || $type eq $class)
252 1         2 { $folder_type = $abbrev;
253 1         2 last;
254             }
255             }
256             }
257             }
258              
259 39 50       81 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       111 return if $require_failed{$class};
269 39         2117 eval "require $class";
270 39 50       149 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         96 push @defaults, manager => $self;
277 39         313 my $folder = $class->new(@defaults, %args);
278 39 100       119 unless(defined $folder)
279             { $self->log(WARNING =>
280             "Folder does not exist, failed opening $folder_type folder $name.")
281 1 50       8 unless $args{access} eq 'd';
282 1         19 return;
283             }
284              
285 38         288 $self->log(PROGRESS => "Opened folder $name ($folder_type).");
286 38         636 push @{$self->{MBM_folders}}, $folder;
  38         97  
287 38         244 $folder;
288             }
289              
290              
291 62     62 1 1652 sub openFolders() { @{shift->{MBM_folders}} }
  62         352  
292              
293              
294             sub isOpenFolder($)
295 46     46 1 126 { my ($self, $name) = @_;
296 46     58   247 first {$name eq $_->name} $self->openFolders;
  58         148  
297             }
298              
299             #-------------------------------------------
300              
301              
302             sub close($@)
303 37     37 1 107 { my ($self, $folder, %options) = @_;
304 37 50       92 return unless $folder;
305              
306 37         99 my $name = $folder->name;
307 37         64 my @remaining = grep {$name ne $_->name} @{$self->{MBM_folders}};
  89         160  
  37         93  
308              
309             # folder opening failed:
310 37 100       61 return if @{$self->{MBM_folders}} == @remaining;
  37         118  
311              
312 36         103 $self->{MBM_folders} = [ @remaining ];
313 36         55 $_->removeFolder($folder) foreach @{$self->{MBM_threads}};
  36         100  
314              
315             $folder->close(close_by_manager => 1, %options)
316 36 100       103 unless $options{close_by_self};
317              
318 36         119 $self;
319             }
320              
321             #-------------------------------------------
322              
323              
324             sub closeAllFolders(@)
325 5     5 1 15 { my ($self, @options) = @_;
326 5         26 $_->close(@options) for $self->openFolders;
327 5         88 $self;
328             }
329              
330 14   66 14   4942 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 205129 { my $self = shift;
348 6         27 my @appended = $self->appendMessages(@_);
349 6 100       41 wantarray ? @appended : $appended[0];
350             }
351              
352             sub appendMessages(@)
353 6     6 0 11 { my $self = shift;
354 6         11 my $folder;
355 6 50 33     29 $folder = shift if !ref $_[0] || $_[0]->isa('Mail::Box');
356              
357 6         14 my @messages;
358 6   100     52 push @messages, shift while @_ && ref $_[0];
359              
360 6         26 my %options = @_;
361 6   33     16 $folder ||= $options{folder};
362              
363             # Try to resolve filenames into opened-files.
364 6 50 66     34 $folder = $self->isOpenFolder($folder) || $folder
365             unless ref $folder;
366              
367 6 100       34 if(ref $folder)
368             { # An open file.
369 3 50       23 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         9 foreach (@messages)
376 3 50 33     29 { 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         20 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         8 my ($name, $class, @gen_options, $found);
388              
389 3         9 foreach (@{$self->{MBM_folder_types}})
  3         12  
390 6         20 { ($name, $class, @gen_options) = @$_;
391 6 50       17 next if $require_failed{$class};
392 6         278 eval "require $class";
393 6 50       23 if($@)
394 0         0 { $require_failed{$class}++;
395 0         0 next;
396             }
397              
398 6 100       34 if($class->foundIn($folder, @gen_options, access => 'a'))
399 3         10 { $found++;
400 3         8 last;
401             }
402             }
403            
404             # The folder was not found at all, so we take the default folder-type.
405 3         10 my $type = $self->{MBM_default_type};
406 3 50 33     15 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       10 ($name, $class, @gen_options) = @{$self->{MBM_folder_types}[0]}
  0         0  
418             unless $found;
419              
420 3         28 $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 314 { my $self = shift;
433 42         49 my $folder;
434 42 50 33     216 $folder = shift if !ref $_[0] || $_[0]->isa('Mail::Box');
435              
436 42         55 my @messages;
437 42   100     125 while(@_ && ref $_[0])
438 42         51 { my $message = shift;
439 42 50       100 $self->log(ERROR =>
440             "Use appendMessage() to add messages which are not in a folder.")
441             unless $message->isa('Mail::Box::Message');
442 42         102 push @messages, $message;
443             }
444              
445 42         64 my %args = @_;
446 42   33     90 $folder ||= $args{folder};
447 42 50       89 my $share = exists $args{share} ? $args{share} : $args{_delete};
448              
449             # Try to resolve filenames into opened-files.
450 42 50 0     85 $folder = $self->isOpenFolder($folder) || $folder
451             unless ref $folder;
452              
453 42 50       71 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         48 my @coerced;
462 42         55 foreach my $msg (@messages)
463 42 50       88 { if($msg->folder eq $folder) # ignore move to same folder
464 0         0 { push @coerced, $msg;
465 0         0 next;
466             }
467 42         143 push @coerced, $msg->copyTo($folder, share => $args{share});
468 42 100       129 $msg->label(deleted => 1) if $args{_delete};
469             }
470 42         130 @coerced;
471             }
472              
473              
474              
475             sub moveMessage(@)
476 1     1 1 274 { my $self = shift;
477 1         5 $self->copyMessage(@_, _delete => 1);
478             }
479              
480             #-------------------------------------------
481              
482             sub threads(@)
483 3     3 1 251 { my $self = shift;
484 3         5 my @folders;
485 3   100     28 push @folders, shift
      66        
486             while @_ && ref $_[0] && $_[0]->isa('Mail::Box');
487 3         10 my %args = @_;
488              
489 3         6 my $base = 'Mail::Box::Thread::Manager';
490 3   33     15 my $type = $args{threader_type} || $base;
491              
492 3   66     11 my $folders = delete $args{folder} || delete $args{folders};
493 3 50       15 push @folders
    100          
494             , ( !$folders ? ()
495             : ref $folders eq 'ARRAY' ? @$folders
496             : $folders
497             );
498              
499 3 50       10 $self->log(INTERNAL => "No folders specified.")
500             unless @folders;
501              
502 3         6 my $threads;
503 3 50       9 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         183 eval "require $type";
513 3 50       18 $self->log(INTERNAL => "Unusable threader $type: $@") if $@;
514              
515 3 50       31 $self->log(INTERNAL => "You need to pass a $base derived")
516             unless $type->isa($base);
517              
518 3         22 $threads = $type->new(manager => $self, %args);
519             }
520              
521 3         16 $threads->includeFolder($_) foreach @folders;
522 3         6 push @{$self->{MBM_threads}}, $threads;
  3         10  
523 3         9 $threads;
524             }
525              
526             #-------------------------------------------
527              
528             sub toBeThreaded($@)
529 578     578 1 724 { my $self = shift;
530 578         697 $_->toBeThreaded(@_) foreach @{$self->{MBM_threads}};
  578         1546  
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 3721 { my ($self, $name) = @_;
542              
543             return unless
544 16 100       130 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     63 $username ||= $ENV{USER} || $ENV{LOGNAME};
      66        
557 15   100     40 $password ||= '';
558              
559 15         23 for($username, $password)
560 30         36 { s/\+/ /g;
561 30         61 s/\%([A-Fa-f0-9]{2})/chr hex $1/ge;
  0         0  
562             }
563              
564 15   100     34 $hostname ||= 'localhost';
565              
566 15   100     24 $path ||= '=';
567              
568 15         117 ( type => $type, folder => $path
569             , username => $username, password => $password
570             , server_name => $hostname, server_port => $port
571             );
572             }
573              
574             #-------------------------------------------
575              
576             1;