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