File Coverage

blib/lib/Mail/Box/MH.pm
Criterion Covered Total %
statement 207 224 92.4
branch 66 104 63.4
condition 31 54 57.4
subroutine 25 25 100.0
pod 12 13 92.3
total 341 420 81.1


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::MH;
10 11     11   3884 use vars '$VERSION';
  11         29  
  11         653  
11             $VERSION = '3.009';
12              
13 11     11   66 use base 'Mail::Box::Dir';
  11         33  
  11         4880  
14              
15 11     11   76 use strict;
  11         29  
  11         232  
16 11     11   54 use warnings;
  11         20  
  11         292  
17 11     11   53 use filetest 'access';
  11         22  
  11         61  
18              
19 11     11   5029 use Mail::Box::MH::Index;
  11         32  
  11         389  
20 11     11   4663 use Mail::Box::MH::Message;
  11         68  
  11         407  
21 11     11   4531 use Mail::Box::MH::Labels;
  11         31  
  11         371  
22              
23 11     11   73 use Carp;
  11         46  
  11         676  
24 11     11   70 use File::Spec ();
  11         22  
  11         197  
25 11     11   55 use File::Basename 'basename';
  11         23  
  11         418  
26 11     11   61 use IO::Handle ();
  11         19  
  11         27455  
27              
28             # Since MailBox 2.052, the use of File::Spec is reduced to the minimum,
29             # because it is too slow. The '/' directory separators do work on
30             # Windows too.
31              
32              
33             my $default_folder_dir = exists $ENV{HOME} ? "$ENV{HOME}/.mh" : '.';
34              
35             sub init($)
36 28     28 0 722 { my ($self, $args) = @_;
37              
38 28   66     108 $args->{folderdir} ||= $default_folder_dir;
39 28   33     184 $args->{lock_file} ||= $args->{index_filename};
40              
41 28         146 $self->SUPER::init($args);
42              
43 28         99 my $folderdir = $self->folderdir;
44 28         89 my $directory = $self->directory;
45 28 50       383 return unless -d $directory;
46              
47             # About the index
48              
49 28   100     203 $self->{MBM_keep_index} = $args->{keep_index} || 0;
50 28         72 $self->{MBM_index} = $args->{index};
51 28   50     146 $self->{MBM_index_type} = $args->{index_type} || 'Mail::Box::MH::Index';
52 28         83 for($args->{index_filename})
53             { $self->{MBM_index_filename}
54 28 0       144 = !defined $_ ? "$directory/.index" # default
    50          
55             : File::Spec->file_name_is_absolute($_) ? $_ # absolute
56             : "$directory/$_"; # relative
57             }
58              
59             # About labels
60              
61 28         139 $self->{MBM_labels} = $args->{labels};
62 28   50     148 $self->{MBM_labels_type} = $args->{labels_type} || 'Mail::Box::MH::Labels';
63 28         69 for($args->{labels_filename})
64             { $self->{MBM_labels_filename}
65 28 0       113 = !defined $_ ? "$directory/.mh_sequences"
    50          
66             : File::Spec->file_name_is_absolute($_) ? $_ # absolute
67             : "$directory/$_"; # relative
68             }
69              
70 28         148 $self;
71             }
72              
73              
74             sub create($@)
75 2     2 1 14 { my ($thingy, $name, %args) = @_;
76 2   33     12 my $class = ref $thingy || $thingy;
77 2   33     7 my $folderdir = $args{folderdir} || $default_folder_dir;
78 2         10 my $directory = $class->folderToDirectory($name, $folderdir);
79              
80 2 50       29 return $class if -d $directory;
81              
82 2 50       200 if(mkdir $directory, 0700)
83 2         45 { $class->log(PROGRESS => "Created folder $name.");
84 2         39 return $class;
85             }
86             else
87 0         0 { $class->log(ERROR => "Cannot create MH folder $name: $!");
88 0         0 return;
89             }
90             }
91              
92             sub foundIn($@)
93 21     21 1 63 { my $class = shift;
94 21 50       116 my $name = @_ % 2 ? shift : undef;
95 21         75 my %args = @_;
96 21   66     99 my $folderdir = $args{folderdir} || $default_folder_dir;
97 21         131 my $directory = $class->folderToDirectory($name, $folderdir);
98              
99 21 100       369 return 0 unless -d $directory;
100 20 100       373 return 1 if -f "$directory/1";
101              
102             # More thorough search required in case some numbered messages
103             # disappeared (lost at fsck or copy?)
104              
105 6 50       155 return unless opendir DIR, $directory;
106 6         167 foreach (readdir DIR)
107 24 50       71 { next unless m/^\d+$/; # Look for filename which is a number.
108 0         0 closedir DIR;
109 0         0 return 1;
110             }
111              
112 6         79 closedir DIR;
113 6         48 0;
114             }
115              
116             #-------------------------------------------
117              
118             sub type() {'mh'}
119              
120             #-------------------------------------------
121              
122             sub listSubFolders(@)
123 46     46 1 507 { my ($class, %args) = @_;
124 46         78 my $dir;
125 46 100       123 if(ref $class)
126 18         75 { $dir = $class->directory;
127 18         41 $class = ref $class;
128             }
129             else
130 28   100     75 { my $folder = $args{folder} || '=';
131 28   66     89 my $folderdir = $args{folderdir} || $default_folder_dir;
132 28         94 $dir = $class->folderToDirectory($folder, $folderdir);
133             }
134              
135 46   100     234 $args{skip_empty} ||= 0;
136 46   100     177 $args{check} ||= 0;
137              
138             # Read the directories from the directory, to find all folders
139             # stored here. Some directories have to be removed because they
140             # are created by all kinds of programs, but are no folders.
141              
142 46 50 33     2032 return () unless -d $dir && opendir DIR, $dir;
143              
144 46 100 66     1152 my @dirs = grep { !/^\d+$|^\./ && -d "$dir/$_" && -r _ }
  348         2410  
145             readdir DIR;
146              
147 46         617 closedir DIR;
148              
149             # Skip empty folders. If a folder has sub-folders, then it is not
150             # empty.
151 46 100       218 if($args{skip_empty})
152 1         4 { my @not_empty;
153              
154 1         4 foreach my $subdir (@dirs)
155 6 100       117 { if(-f "$dir/$subdir/1")
156             { # Fast found: the first message of a filled folder.
157 2         10 push @not_empty, $subdir;
158 2         4 next;
159             }
160              
161 4 50       85 opendir DIR, "$dir/$subdir" or next;
162 4         87 my @entities = grep !/^\./, readdir DIR;
163 4         44 closedir DIR;
164              
165 4 50       24 if(grep /^\d+$/, @entities) # message 1 was not there, but
166 0         0 { push @not_empty, $subdir; # other message-numbers exist.
167 0         0 next;
168             }
169              
170 4         11 foreach (@entities)
171 4 100       62 { next unless -d "$dir/$subdir/$_";
172 1         4 push @not_empty, $subdir;
173 1         4 last;
174             }
175              
176             }
177              
178 1         6 @dirs = @not_empty;
179             }
180              
181             # Check if the files we want to return are really folders.
182              
183 46 50 33     112 @dirs = map { m/(.*)/ && $1 ? $1 : () } @dirs; # untaint
  53         363  
184 46 100       302 return @dirs unless $args{check};
185              
186 9         38 grep { $class->foundIn("$dir/$_") } @dirs;
  12         79  
187             }
188              
189             #-------------------------------------------
190              
191             sub openSubFolder($)
192 9     9 1 51 { my ($self, $name) = @_;
193              
194 9         51 my $subdir = $self->nameOfSubFolder($name);
195 9 50 66     408 unless(-d $subdir || mkdir $subdir, 0755)
196 0         0 { warn "Cannot create subfolder $name for $self: $!\n";
197 0         0 return;
198             }
199              
200 9         82 $self->SUPER::openSubFolder($name, @_);
201             }
202              
203             #-------------------------------------------
204              
205             sub topFolderWithMessages() { 1 }
206              
207             #-------------------------------------------
208              
209              
210             sub appendMessages(@)
211 1     1 1 3 { my $class = shift;
212 1         7 my %args = @_;
213              
214             my @messages = exists $args{message} ? $args{message}
215 1 50       7 : exists $args{messages} ? @{$args{messages}}
  1 50       4  
216             : return ();
217              
218 1 50       9 my $self = $class->new(@_, access => 'r')
219             or return ();
220              
221 1         8 my $directory= $self->directory;
222 1 50       45 return unless -d $directory;
223              
224 1         8 my $locker = $self->locker;
225 1 50       5 unless($locker->lock)
226 0         0 { $self->log(ERROR => "Cannot append message without lock on $self.");
227 0         0 return;
228             }
229              
230 1         6 my $msgnr = $self->highestMessageNumber +1;
231              
232 1         6 foreach my $message (@messages)
233 1         7 { my $filename = "$directory/$msgnr";
234 1 50       6 $message->create($filename)
235             or $self->log(ERROR =>
236             "Unable to write message for $self to $filename: $!\n");
237              
238 1         6 $msgnr++;
239             }
240            
241 1         6 $self->labels->append(@messages);
242 1         5 $self->index->append(@messages);
243              
244 1         6 $locker->unlock;
245 1         7 $self->close(write => 'NEVER');
246              
247 1         13 @messages;
248             }
249              
250             #-------------------------------------------
251              
252              
253             sub highestMessageNumber()
254 1     1 1 4 { my $self = shift;
255              
256             return $self->{MBM_highest_msgnr}
257 1 50       7 if exists $self->{MBM_highest_msgnr};
258              
259 0         0 my $directory = $self->directory;
260              
261 0 0       0 opendir DIR, $directory or return;
262 0         0 my @messages = sort {$a <=> $b} grep /^\d+$/, readdir DIR;
  0         0  
263 0         0 closedir DIR;
264              
265 0         0 $messages[-1];
266             }
267              
268             #-------------------------------------------
269              
270              
271             sub index()
272 29     29 1 69 { my $self = shift;
273 29 100       139 return () unless $self->{MBM_keep_index};
274 10 100       49 return $self->{MBM_index} if defined $self->{MBM_index};
275              
276             $self->{MBM_index} = $self->{MBM_index_type}->new
277             ( filename => $self->{MBM_index_filename}
278 4         23 , $self->logSettings
279             )
280              
281             }
282              
283             #-------------------------------------------
284              
285              
286             sub labels()
287 29     29 1 66 { my $self = shift;
288 29 100       117 return $self->{MBM_labels} if defined $self->{MBM_labels};
289              
290             $self->{MBM_labels} = $self->{MBM_labels_type}->new
291             ( filename => $self->{MBM_labels_filename}
292 21         109 , $self->logSettings
293             );
294             }
295              
296             #-------------------------------------------
297              
298             sub readMessageFilenames
299 17     17 1 56 { my ($self, $dirname) = @_;
300              
301 17 50       521 opendir DIR, $dirname or return;
302              
303             # list of numerically sorted, untainted filenames.
304             my @msgnrs
305 1718         2288 = sort {$a <=> $b}
306 17 100 66     699 map { /^(\d+)$/ && -f "$dirname/$1" ? $1 : () }
  485         8155  
307             readdir DIR;
308              
309 17         342 closedir DIR;
310              
311 17         187 @msgnrs;
312             }
313              
314             #-------------------------------------------
315              
316             sub readMessages(@)
317 17     17 1 121 { my ($self, %args) = @_;
318              
319 17         64 my $directory = $self->directory;
320 17 50       293 return unless -d $directory;
321              
322 17         99 my $locker = $self->locker;
323 17 50       82 $locker->lock or return;
324              
325 17         76 my @msgnrs = $self->readMessageFilenames($directory);
326              
327 17         72 my $index = $self->{MBM_index};
328 17 50       85 unless($index)
329 17         74 { $index = $self->index;
330 17 100       83 $index->read if $index;
331             }
332              
333 17         1217 my $labels = $self->{MBM_labels};
334 17 50       59 unless($labels)
335 17         59 { $labels = $self->labels;
336 17 50       107 $labels->read if $labels;
337             }
338              
339 17         59 my $body_type = $args{body_delayed_type};
340 17         49 my $head_type = $args{head_delayed_type};
341 17         78 my @log = $self->logSettings;
342              
343 17         148 foreach my $msgnr (@msgnrs)
344             {
345 434         1246 my $msgfile = "$directory/$msgnr";
346              
347 434         555 my $head;
348 434 100       926 $head = $index->get($msgfile) if $index;
349 434   33     1864 $head ||= $head_type->new(@log);
350              
351             my $message = $args{message_type}->new
352             ( head => $head
353             , filename => $msgfile
354             , folder => $self
355             , fix_header => $self->{MB_fix_headers}
356 434         1450 );
357              
358 434 50       1647 my $labref = $labels ? $labels->get($msgnr) : ();
359 434 100       1447 $message->label(seen => 1, $labref ? @$labref : ());
360              
361 434         6242 $message->storeBody($body_type->new(@log, message => $message));
362 434         1934 $self->storeMessage($message);
363             }
364              
365 17         61 $self->{MBM_highest_msgnr} = $msgnrs[-1];
366 17         91 $locker->unlock;
367 17         136 $self;
368             }
369            
370             #-------------------------------------------
371              
372             sub delete(@)
373 9     9 1 476 { my $self = shift;
374 9         47 $self->SUPER::delete(@_);
375              
376 9         25 my $dir = $self->directory;
377 9 50       234 return 1 unless opendir DIR, $dir;
378 9         57 IO::Handle::untaint \*DIR;
379              
380             # directories (subfolders) are not removed, as planned
381 9         3771 unlink "$dir/$_" for readdir DIR;
382 9         163 closedir DIR;
383              
384 9         470 rmdir $dir; # fails when there are subdirs (without recurse)
385             }
386              
387             #-------------------------------------------
388              
389              
390             sub writeMessages($)
391 11     11 1 43 { my ($self, $args) = @_;
392              
393             # Write each message. Two things complicate life:
394             # 1 - we may have a huge folder, which should not be on disk twice
395             # 2 - we may have to replace a message, but it is unacceptable
396             # to remove the original before we are sure that the new version
397             # is on disk.
398              
399 11         41 my $locker = $self->locker;
400 11 50       54 $self->log(ERROR => "Cannot write folder $self without lock."), return
401             unless $locker->lock;
402              
403 11 100       43 my $renumber = exists $args->{renumber} ? $args->{renumber} : 1;
404 11         61 my $directory = $self->directory;
405 11         34 my @messages = @{$args->{messages}};
  11         52  
406              
407 11         21 my $writer = 0;
408 11         32 foreach my $message (@messages)
409             {
410 309         742 my $filename = $message->filename;
411              
412 309         420 my $newfile;
413 309 100 66     711 if($renumber || !$filename)
414 264         686 { $newfile = $directory . '/' . ++$writer;
415             }
416             else
417 45         64 { $newfile = $filename;
418 45         875 $writer = basename $filename;
419             }
420              
421 309         736 $message->create($newfile);
422             }
423              
424             # Write the labels- and the index-file.
425              
426 11         57 my $labels = $self->labels;
427 11 50       117 $labels->write(@messages) if $labels;
428              
429 11         54 my $index = $self->index;
430 11 100       67 $index->write(@messages) if $index;
431              
432 11         66 $locker->unlock;
433              
434             # Remove an empty folder. This is done last, because the code before
435             # in this method will have cleared the contents of the directory.
436              
437 11 0 33     39 if(!@messages && $self->{MB_remove_empty})
438             { # If something is still in the directory, this will fail, but I
439             # don't mind.
440 0         0 rmdir $directory;
441             }
442              
443 11         87 $self;
444             }
445              
446             #-------------------------------------------
447              
448              
449             1;