File Coverage

blib/lib/Mail/Box/Manager.pm
Criterion Covered Total %
statement 208 275 75.6
branch 82 144 56.9
condition 57 144 39.5
subroutine 27 30 90.0
pod 19 21 90.4
total 393 614 64.0


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