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