File Coverage

blib/lib/Mail/Box/MH.pm
Criterion Covered Total %
statement 193 203 95.0
branch 59 88 67.0
condition 35 61 57.3
subroutine 22 22 100.0
pod 12 13 92.3
total 321 387 82.9


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::MH;{
13             our $VERSION = '4.01';
14             }
15              
16 11     11   4584 use parent 'Mail::Box::Dir';
  11         27  
  11         86  
17              
18 11     11   756 use strict;
  11         26  
  11         271  
19 11     11   55 use warnings;
  11         21  
  11         689  
20              
21 11     11   62 use Log::Report 'mail-box', import => [ qw/__x error fault trace/ ];
  11         23  
  11         69  
22              
23 11     11   8435 use Mail::Box::MH::Index ();
  11         37  
  11         327  
24 11     11   5849 use Mail::Box::MH::Message ();
  11         40  
  11         323  
25 11     11   6006 use Mail::Box::MH::Labels ();
  11         37  
  11         878  
26              
27 11     11   87 use File::Spec::Functions qw/rel2abs/;
  11         29  
  11         947  
28 11     11   80 use File::Basename qw/basename/;
  11         23  
  11         39485  
29              
30             # Since MailBox 2.052, the use of File::Spec is reduced to the minimum,
31             # because it is too slow. The '/' directory separators do work on
32             # Windows too.
33              
34             #--------------------
35              
36             my $default_folder_dir = exists $ENV{HOME} ? "$ENV{HOME}/.mh" : '.';
37              
38             sub init($)
39 28     28 0 387 { my ($self, $args) = @_;
40 28   66     139 $args->{folderdir} ||= $default_folder_dir;
41 28   33     197 $args->{lock_file} ||= $args->{index_filename};
42              
43 28         183 $self->SUPER::init($args);
44              
45 28         101 my $folderdir = $self->folderdir;
46 28         82 my $directory = $self->directory;
47 28 50       370 -d $directory or return;
48              
49             # About the index
50              
51 28   100     230 $self->{MBM_keep_index} = $args->{keep_index} || 0;
52 28         98 $self->{MBM_index} = $args->{index};
53 28   50     152 $self->{MBM_index_type} = $args->{index_type} || 'Mail::Box::MH::Index';
54              
55 28   50     168 my $ifn = $args->{index_filename} //= '.index';
56 28         121 $self->{MBM_index_filename} = rel2abs $ifn, $directory;
57              
58             # About labels
59              
60 28         741 $self->{MBM_labels} = $args->{labels};
61 28   50     157 $self->{MBM_labels_type} = $args->{labels_type} || 'Mail::Box::MH::Labels';
62              
63 28   50     160 my $lfn = $args->{labels_filename} //= '.mh_sequences';
64 28         144 $self->{MBM_labels_filename} = rel2abs $lfn, $directory;
65              
66 28         575 $self;
67             }
68              
69              
70             sub create($@)
71 2     2 1 13 { my ($thingy, $name, %args) = @_;
72 2   33     12 my $class = ref $thingy || $thingy;
73 2   33     8 my $folderdir = $args{folderdir} || $default_folder_dir;
74 2         10 my $directory = $class->folderToDirectory($name, $folderdir);
75              
76 2 50       29 return $class if -d $directory;
77              
78 2 50       545 mkdir $directory, 0700
79             or fault __x"cannot create MH folder {name}", name => $name;
80              
81 2         23 trace "Created folder $name.";
82 2         100 $class;
83             }
84              
85             #--------------------
86              
87             sub type() {'mh'}
88              
89             sub foundIn($@)
90 21     21 1 93 { my $class = shift;
91 21 50       113 my $name = @_ % 2 ? shift : undef;
92 21         116 my %args = @_;
93 21   66     125 my $folderdir = $args{folderdir} || $default_folder_dir;
94 21         196 my $directory = $class->folderToDirectory($name, $folderdir);
95              
96 21 100       469 -d $directory or return 0;
97 20 100       1654 -f "$directory/1" and return 1; # cheap
98              
99             # More thorough search required in case some numbered messages
100             # disappeared (lost at fsck or copy?)
101              
102 6 50       175 opendir my $dh, $directory or return 0;
103 6         114 foreach (readdir $dh)
104 24 50       60 { m/^[0-9]+$/ or next; # Look for filename which is a number.
105 0         0 closedir $dh;
106 0         0 return 1;
107             }
108              
109 6         73 closedir $dh;
110 6         48 0;
111             }
112              
113             sub listSubFolders(@)
114 46     46 1 667 { my ($class, %args) = @_;
115 46         83 my $dir;
116 46 100       150 if(ref $class)
117 18         73 { $dir = $class->directory;
118 18         50 $class = ref $class;
119             }
120             else
121 28   100     92 { my $folder = $args{folder} || '=';
122 28   66     138 my $folderdir = $args{folderdir} || $default_folder_dir;
123 28         116 $dir = $class->folderToDirectory($folder, $folderdir);
124             }
125              
126 46   100     265 $args{skip_empty} ||= 0;
127 46   100     220 $args{check} ||= 0;
128              
129             # Read the directories from the directory, to find all folders
130             # stored here. Some directories have to be removed because they
131             # are created by all kinds of programs, but are no folders.
132              
133 46 50 33     3175 -d $dir && opendir my $dh, $dir or return ();
134 46 100       1697 my @dirs = grep { !/^\d+$|^\./ && -d "$dir/$_" } readdir $dh;
  348         1915  
135 46         626 closedir $dh;
136              
137             # Skip empty folders. If a folder has sub-folders, then it is not
138             # empty.
139 46 100       170 if($args{skip_empty})
140 1         3 { my @not_empty;
141              
142 1         3 foreach my $subdir (@dirs)
143 6 100       141 { if(-f "$dir/$subdir/1")
144             { # Fast found: the first message of a filled folder.
145 2         5 push @not_empty, $subdir;
146 2         3 next;
147             }
148              
149 4 50       93 opendir my $dh, "$dir/$subdir" or next;
150 4         116 my @entities = grep !/^\./, readdir $dh;
151 4         32 closedir $dh;
152              
153 4 50       19 if(grep /^\d+$/, @entities) # message 1 was not there, but
154 0         0 { push @not_empty, $subdir; # other message-numbers exist.
155 0         0 next;
156             }
157              
158 4         12 foreach (@entities)
159 4 100       47 { -d "$dir/$subdir/$_" or next;
160 1         3 push @not_empty, $subdir;
161 1         4 last;
162             }
163             }
164              
165 1         4 @dirs = @not_empty;
166             }
167              
168             # Check if the files we want to return are really folders.
169              
170 46 50 33     148 @dirs = map { m/(.*)/ && $1 ? $1 : () } @dirs; # untaint
  53         357  
171 46 100       423 $args{check} or return @dirs;
172              
173 9         97 grep $class->foundIn("$dir/$_"), @dirs;
174             }
175              
176             #-------------
177              
178             sub topFolderWithMessages() { 1 }
179              
180              
181             sub appendMessages(@)
182 1     1 1 3 { my $class = shift;
183 1         8 my %args = @_;
184              
185             my @messages
186             = exists $args{message} ? $args{message}
187 1 50       7 : exists $args{messages} ? @{$args{messages}}
  1 50       3  
188             : return ();
189              
190 1 50       7 my $self = $class->new(@_, access => 'r')
191             or return ();
192              
193 1         6 my $locker = $self->locker;
194 1 50       8 $locker->lock
195             or error __x"cannot append message without lock on {folder}.", folder => $self->name;
196              
197 1         5 my $msgnr = $self->highestMessageNumber +1;
198              
199 1         12 my $directory= $self->directory;
200 1         4 foreach my $message (@messages)
201 1         4 { my $filename = "$directory/$msgnr";
202 1         23 $message->create($filename);
203 1         3 $msgnr++;
204             }
205              
206 1         6 $self->labels->append(@messages);
207 1         5 $self->index->append(@messages);
208              
209 1         8 $locker->unlock;
210 1         7 $self->close(write => 'NEVER');
211              
212 1         26 @messages;
213             }
214              
215             #--------------------
216              
217             sub openSubFolder($)
218 9     9 1 41 { my ($self, $name) = @_;
219              
220 9         57 my $subdir = $self->nameOfSubFolder($name);
221 9 50 66     759 -d $subdir || mkdir $subdir, 0755
222             or fault __x"cannot create directory {dir} for subfolder {name}", dir => $subdir, name => $name;
223              
224 9         73 $self->SUPER::openSubFolder($name, @_);
225             }
226              
227             #--------------------
228              
229             sub highestMessageNumber()
230 1     1 1 2 { my $self = shift;
231              
232             return $self->{MBM_highest_msgnr}
233 1 50       31 if exists $self->{MBM_highest_msgnr};
234              
235 0         0 my $directory = $self->directory;
236              
237 0 0       0 opendir my $dh, $directory or return;
238 0         0 my @messages = sort {$a <=> $b} grep /^[0-9]+$/, readdir $dh;
  0         0  
239 0         0 closedir $dh;
240              
241 0         0 $messages[-1];
242             }
243              
244              
245             sub index()
246 29     29 1 60 { my $self = shift;
247 29 100       156 $self->{MBM_keep_index} or return ();
248              
249 10   66     123 $self->{MBM_index} //= $self->{MBM_index_type}->new(filename => $self->{MBM_index_filename});
250             }
251              
252              
253             sub labels()
254 29     29 1 61 { my $self = shift;
255 29   66     396 $self->{MBM_labels} //= $self->{MBM_labels_type}->new(filename => $self->{MBM_labels_filename});
256             }
257              
258             sub readMessageFilenames
259 17     17 1 56 { my ($self, $dirname) = @_;
260              
261 17 50       989 opendir my $dh, $dirname or return;
262              
263             # list of numerically sorted, untainted filenames.
264 489         971 my @msgnrs = sort {$a <=> $b}
265 17 100 66     2562 map { /^(\d+)$/ && -f "$dirname/$1" ? $1 : () } readdir $dh;
  485         7726  
266              
267 17         436 closedir $dh;
268 17         311 @msgnrs;
269             }
270              
271             sub readMessages(@)
272 17     17 1 141 { my ($self, %args) = @_;
273              
274 17         71 my $directory = $self->directory;
275 17 50       346 -d $directory or return;
276              
277 17         125 my $locker = $self->locker;
278 17 50       83 $locker->lock or return;
279              
280 17         82 my @msgnrs = $self->readMessageFilenames($directory);
281              
282 17         72 my $index = $self->{MBM_index};
283 17 50       65 unless($index)
284 17         79 { $index = $self->index;
285 17 100       93 $index->read if $index;
286             }
287              
288 17         217 my $labels = $self->{MBM_labels};
289 17 50       70 unless($labels)
290 17         65 { $labels = $self->labels;
291 17 50       109 $labels->read if $labels;
292             }
293              
294 17         100 my $body_type = $args{body_delayed_type};
295 17         45 my $head_type = $args{head_delayed_type};
296              
297 17         50 foreach my $msgnr (@msgnrs)
298 434         1178 { my $msgfile = "$directory/$msgnr";
299              
300 434         687 my $head;
301 434 100       1035 $head = $index->get($msgfile) if $index;
302 434   33     2180 $head ||= $head_type->new;
303              
304             my $message = $args{message_type}->new(
305 434         1560 head => $head,
306             filename => $msgfile,
307             folder => $self,
308             fix_header => $self->fixHeaders,
309             );
310              
311 434 50       2202 my $labref = $labels ? $labels->get($msgnr) : ();
312 434 100       1763 $message->label(seen => 1, $labref ? @$labref : ());
313              
314 434         7236 $message->storeBody($body_type->new(message => $message));
315 434         2373 $self->storeMessage($message);
316             }
317              
318 17         79 $self->{MBM_highest_msgnr} = $msgnrs[-1];
319 17         112 $locker->unlock;
320 17         242 $self;
321             }
322              
323             sub delete(@)
324 9     9 1 641 { my $self = shift;
325 9         52 $self->SUPER::delete(@_);
326              
327 9         25 my $dir = $self->directory;
328 9 50       257 opendir my $dh, $dir or return 1;
329 9         56 untaint $dh;
330              
331             # directories (subfolders) are not removed, as planned
332 9         7202 unlink "$dir/$_" for readdir $dh;
333 9         115 closedir $dh;
334              
335 9         710 rmdir $dir; # fails when there are subdirs (without recurse)
336             }
337              
338              
339              
340             sub writeMessages($)
341 11     11 1 33 { my ($self, $args) = @_;
342 11 100       70 my $renumber = exists $args->{renumber} ? $args->{renumber} : 1;
343              
344             # Write each message. Two things complicate life:
345             # 1 - we may have a huge folder, which should not be on disk twice
346             # 2 - we may have to replace a message, but it is unacceptable
347             # to remove the original before we are sure that the new version
348             # is on disk.
349              
350 11         42 my $locker = $self->locker;
351 11 50       64 $locker->lock
352             or error __x"cannot write folder {name} without lock.", name => $self->name;
353              
354 11         91 my $directory = $self->directory;
355 11         25 my @messages = @{$args->{messages}};
  11         76  
356              
357 11         30 my $writer = 0;
358 11         27 foreach my $message (@messages)
359 309         727 { my $filename = $message->filename;
360              
361 309         461 my $newfile;
362 309 100 66     839 if($renumber || !$filename)
363 264         595 { $newfile = $directory . '/' . ++$writer;
364             }
365             else
366 45         70 { $newfile = $filename;
367 45         1255 $writer = basename $filename;
368             }
369              
370 309         894 $message->create($newfile);
371             }
372              
373             # Write the labels- and the index-file.
374              
375 11         60 my $labels = $self->labels;
376 11 50       108 $labels->write(@messages) if $labels;
377              
378 11         56 my $index = $self->index;
379 11 100       90 $index->write(@messages) if $index;
380              
381 11         92 $locker->unlock;
382              
383             # Remove an empty folder. This is done last, because the code before
384             # in this method will have cleared the contents of the directory.
385             # If something else is still in the directory, this will fail, but I don't mind.
386 11 50 33     52 rmdir $directory
387             if !@messages && $self->removeEmpty;
388              
389 11         65 $self;
390             }
391              
392             #--------------------
393              
394             1;