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-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::MH;
10 11     11   2754 use vars '$VERSION';
  11         19  
  11         537  
11             $VERSION = '3.008';
12              
13 11     11   55 use base 'Mail::Box::Dir';
  11         29  
  11         3874  
14              
15 11     11   60 use strict;
  11         17  
  11         170  
16 11     11   39 use warnings;
  11         18  
  11         216  
17 11     11   44 use filetest 'access';
  11         15  
  11         54  
18              
19 11     11   4032 use Mail::Box::MH::Index;
  11         25  
  11         321  
20 11     11   3845 use Mail::Box::MH::Message;
  11         55  
  11         320  
21 11     11   3690 use Mail::Box::MH::Labels;
  11         22  
  11         292  
22              
23 11     11   61 use Carp;
  11         37  
  11         461  
24 11     11   48 use File::Spec ();
  11         24  
  11         164  
25 11     11   45 use File::Basename 'basename';
  11         18  
  11         338  
26 11     11   53 use IO::Handle ();
  11         27  
  11         21466  
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 295 { my ($self, $args) = @_;
37              
38 28   66     87 $args->{folderdir} ||= $default_folder_dir;
39 28   33     152 $args->{lock_file} ||= $args->{index_filename};
40              
41 28         110 $self->SUPER::init($args);
42              
43 28         80 my $folderdir = $self->folderdir;
44 28         64 my $directory = $self->directory;
45 28 50       290 return unless -d $directory;
46              
47             # About the index
48              
49 28   100     149 $self->{MBM_keep_index} = $args->{keep_index} || 0;
50 28         54 $self->{MBM_index} = $args->{index};
51 28   50     116 $self->{MBM_index_type} = $args->{index_type} || 'Mail::Box::MH::Index';
52 28         65 for($args->{index_filename})
53             { $self->{MBM_index_filename}
54 28 0       116 = !defined $_ ? "$directory/.index" # default
    50          
55             : File::Spec->file_name_is_absolute($_) ? $_ # absolute
56             : "$directory/$_"; # relative
57             }
58              
59             # About labels
60              
61 28         88 $self->{MBM_labels} = $args->{labels};
62 28   50     116 $self->{MBM_labels_type} = $args->{labels_type} || 'Mail::Box::MH::Labels';
63 28         65 for($args->{labels_filename})
64             { $self->{MBM_labels_filename}
65 28 0       119 = !defined $_ ? "$directory/.mh_sequences"
    50          
66             : File::Spec->file_name_is_absolute($_) ? $_ # absolute
67             : "$directory/$_"; # relative
68             }
69              
70 28         113 $self;
71             }
72              
73              
74             sub create($@)
75 2     2 1 11 { my ($thingy, $name, %args) = @_;
76 2   33     10 my $class = ref $thingy || $thingy;
77 2   33     6 my $folderdir = $args{folderdir} || $default_folder_dir;
78 2         7 my $directory = $class->folderToDirectory($name, $folderdir);
79              
80 2 50       22 return $class if -d $directory;
81              
82 2 50       146 if(mkdir $directory, 0700)
83 2         25 { $class->log(PROGRESS => "Created folder $name.");
84 2         31 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 58 { my $class = shift;
94 21 50       72 my $name = @_ % 2 ? shift : undef;
95 21         63 my %args = @_;
96 21   66     82 my $folderdir = $args{folderdir} || $default_folder_dir;
97 21         107 my $directory = $class->folderToDirectory($name, $folderdir);
98              
99 21 100       322 return 0 unless -d $directory;
100 20 100       282 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       118 return unless opendir DIR, $directory;
106 6         99 foreach (readdir DIR)
107 24 50       55 { 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         52 closedir DIR;
113 6         37 0;
114             }
115              
116             #-------------------------------------------
117              
118             sub type() {'mh'}
119              
120             #-------------------------------------------
121              
122             sub listSubFolders(@)
123 46     46 1 325 { my ($class, %args) = @_;
124 46         69 my $dir;
125 46 100       100 if(ref $class)
126 18         63 { $dir = $class->directory;
127 18         37 $class = ref $class;
128             }
129             else
130 28   100     66 { my $folder = $args{folder} || '=';
131 28   66     65 my $folderdir = $args{folderdir} || $default_folder_dir;
132 28         78 $dir = $class->folderToDirectory($folder, $folderdir);
133             }
134              
135 46   100     187 $args{skip_empty} ||= 0;
136 46   100     139 $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     1565 return () unless -d $dir && opendir DIR, $dir;
143              
144 46 100 66     873 my @dirs = grep { !/^\d+$|^\./ && -d "$dir/$_" && -r _ }
  348         1950  
145             readdir DIR;
146              
147 46         485 closedir DIR;
148              
149             # Skip empty folders. If a folder has sub-folders, then it is not
150             # empty.
151 46 100       146 if($args{skip_empty})
152 1         2 { my @not_empty;
153              
154 1         3 foreach my $subdir (@dirs)
155 6 100       92 { if(-f "$dir/$subdir/1")
156             { # Fast found: the first message of a filled folder.
157 2         6 push @not_empty, $subdir;
158 2         3 next;
159             }
160              
161 4 50       80 opendir DIR, "$dir/$subdir" or next;
162 4         65 my @entities = grep !/^\./, readdir DIR;
163 4         30 closedir DIR;
164              
165 4 50       17 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         9 foreach (@entities)
171 4 100       48 { next unless -d "$dir/$subdir/$_";
172 1         4 push @not_empty, $subdir;
173 1         2 last;
174             }
175              
176             }
177              
178 1         5 @dirs = @not_empty;
179             }
180              
181             # Check if the files we want to return are really folders.
182              
183 46 50 33     79 @dirs = map { m/(.*)/ && $1 ? $1 : () } @dirs; # untaint
  53         282  
184 46 100       233 return @dirs unless $args{check};
185              
186 9         41 grep { $class->foundIn("$dir/$_") } @dirs;
  12         62  
187             }
188              
189             #-------------------------------------------
190              
191             sub openSubFolder($)
192 9     9 1 35 { my ($self, $name) = @_;
193              
194 9         34 my $subdir = $self->nameOfSubFolder($name);
195 9 50 66     272 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         55 $self->SUPER::openSubFolder($name, @_);
201             }
202              
203             #-------------------------------------------
204              
205             sub topFolderWithMessages() { 1 }
206              
207             #-------------------------------------------
208              
209              
210             sub appendMessages(@)
211 1     1 1 2 { my $class = shift;
212 1         6 my %args = @_;
213              
214             my @messages = exists $args{message} ? $args{message}
215 1 50       6 : 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         4 my $directory= $self->directory;
222 1 50       18 return unless -d $directory;
223              
224 1         6 my $locker = $self->locker;
225 1 50       4 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         4 foreach my $message (@messages)
233 1         4 { my $filename = "$directory/$msgnr";
234 1 50       5 $message->create($filename)
235             or $self->log(ERROR =>
236             "Unable to write message for $self to $filename: $!\n");
237              
238 1         4 $msgnr++;
239             }
240            
241 1         4 $self->labels->append(@messages);
242 1         4 $self->index->append(@messages);
243              
244 1         4 $locker->unlock;
245 1         5 $self->close(write => 'NEVER');
246              
247 1         9 @messages;
248             }
249              
250             #-------------------------------------------
251              
252              
253             sub highestMessageNumber()
254 1     1 1 2 { my $self = shift;
255              
256             return $self->{MBM_highest_msgnr}
257 1 50       6 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 58 { my $self = shift;
273 29 100       105 return () unless $self->{MBM_keep_index};
274 10 100       45 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         18 , $self->logSettings
279             )
280              
281             }
282              
283             #-------------------------------------------
284              
285              
286             sub labels()
287 29     29 1 52 { my $self = shift;
288 29 100       94 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         77 , $self->logSettings
293             );
294             }
295              
296             #-------------------------------------------
297              
298             sub readMessageFilenames
299 17     17 1 45 { my ($self, $dirname) = @_;
300              
301 17 50       389 opendir DIR, $dirname or return;
302              
303             # list of numerically sorted, untainted filenames.
304             my @msgnrs
305 1718         2062 = sort {$a <=> $b}
306 17 100 66     555 map { /^(\d+)$/ && -f "$dirname/$1" ? $1 : () }
  485         6274  
307             readdir DIR;
308              
309 17         283 closedir DIR;
310              
311 17         149 @msgnrs;
312             }
313              
314             #-------------------------------------------
315              
316             sub readMessages(@)
317 17     17 1 109 { my ($self, %args) = @_;
318              
319 17         53 my $directory = $self->directory;
320 17 50       211 return unless -d $directory;
321              
322 17         69 my $locker = $self->locker;
323 17 50       69 $locker->lock or return;
324              
325 17         54 my @msgnrs = $self->readMessageFilenames($directory);
326              
327 17         50 my $index = $self->{MBM_index};
328 17 50       56 unless($index)
329 17         56 { $index = $self->index;
330 17 100       60 $index->read if $index;
331             }
332              
333 17         979 my $labels = $self->{MBM_labels};
334 17 50       54 unless($labels)
335 17         47 { $labels = $self->labels;
336 17 50       79 $labels->read if $labels;
337             }
338              
339 17         40 my $body_type = $args{body_delayed_type};
340 17         34 my $head_type = $args{head_delayed_type};
341 17         60 my @log = $self->logSettings;
342              
343 17         116 foreach my $msgnr (@msgnrs)
344             {
345 434         919 my $msgfile = "$directory/$msgnr";
346              
347 434         468 my $head;
348 434 100       761 $head = $index->get($msgfile) if $index;
349 434   33     1445 $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         1122 );
357              
358 434 50       1309 my $labref = $labels ? $labels->get($msgnr) : ();
359 434 100       1179 $message->label(seen => 1, $labref ? @$labref : ());
360              
361 434         5099 $message->storeBody($body_type->new(@log, message => $message));
362 434         1558 $self->storeMessage($message);
363             }
364              
365 17         53 $self->{MBM_highest_msgnr} = $msgnrs[-1];
366 17         89 $locker->unlock;
367 17         103 $self;
368             }
369            
370             #-------------------------------------------
371              
372             sub delete(@)
373 9     9 1 381 { my $self = shift;
374 9         37 $self->SUPER::delete(@_);
375              
376 9         21 my $dir = $self->directory;
377 9 50       235 return 1 unless opendir DIR, $dir;
378 9         39 IO::Handle::untaint \*DIR;
379              
380             # directories (subfolders) are not removed, as planned
381 9         2805 unlink "$dir/$_" for readdir DIR;
382 9         113 closedir DIR;
383              
384 9         339 rmdir $dir; # fails when there are subdirs (without recurse)
385             }
386              
387             #-------------------------------------------
388              
389              
390             sub writeMessages($)
391 11     11 1 24 { 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         32 my $locker = $self->locker;
400 11 50       46 $self->log(ERROR => "Cannot write folder $self without lock."), return
401             unless $locker->lock;
402              
403 11 100       36 my $renumber = exists $args->{renumber} ? $args->{renumber} : 1;
404 11         117 my $directory = $self->directory;
405 11         27 my @messages = @{$args->{messages}};
  11         47  
406              
407 11         29 my $writer = 0;
408 11         25 foreach my $message (@messages)
409             {
410 309         566 my $filename = $message->filename;
411              
412 309         337 my $newfile;
413 309 100 66     595 if($renumber || !$filename)
414 264         542 { $newfile = $directory . '/' . ++$writer;
415             }
416             else
417 45         44 { $newfile = $filename;
418 45         673 $writer = basename $filename;
419             }
420              
421 309         615 $message->create($newfile);
422             }
423              
424             # Write the labels- and the index-file.
425              
426 11         47 my $labels = $self->labels;
427 11 50       89 $labels->write(@messages) if $labels;
428              
429 11         46 my $index = $self->index;
430 11 100       51 $index->write(@messages) if $index;
431              
432 11         63 $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     41 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         74 $self;
444             }
445              
446             #-------------------------------------------
447              
448              
449             1;