File Coverage

blib/lib/Mail/Box/File.pm
Criterion Covered Total %
statement 217 251 86.4
branch 73 126 57.9
condition 35 66 53.0
subroutine 31 35 88.5
pod 14 16 87.5
total 370 494 74.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::File;{
13             our $VERSION = '4.01';
14             }
15              
16 25     25   15507 use parent 'Mail::Box';
  25         54  
  25         194  
17              
18 25     25   1960 use strict;
  25         54  
  25         647  
19 25     25   113 use warnings;
  25         85  
  25         2474  
20              
21 25     25   187 use Log::Report 'mail-box', import => [ qw/__x error fault trace warning/ ];
  25         68  
  25         231  
22              
23 25     25   20923 use Mail::Box::File::Message ();
  25         82  
  25         803  
24 25     25   198 use Mail::Message::Body::Lines ();
  25         53  
  25         487  
25 25     25   112 use Mail::Message::Body::File ();
  25         49  
  25         468  
26 25     25   13375 use Mail::Message::Body::Delayed ();
  25         77  
  25         845  
27 25     25   179 use Mail::Message::Body::Multipart ();
  25         77  
  25         457  
28 25     25   112 use Mail::Message::Head ();
  25         54  
  25         598  
29              
30 25     25   146 use File::Copy qw/move/;
  25         54  
  25         1824  
31 25     25   1109 use File::Spec::Functions qw/file_name_is_absolute catfile/;
  25         1808  
  25         1836  
32 25     25   195 use File::Basename qw/dirname basename/;
  25         53  
  25         1567  
33 25     25   137 use Scalar::Util qw/blessed/;
  25         118  
  25         2109  
34             #use POSIX qw/:unistd_h/;
35              
36             my $windows;
37 25     25   91981 BEGIN { $windows = $^O =~ m/mswin32/i }
38              
39             #--------------------
40              
41             my $default_folder_dir = exists $ENV{HOME} ? $ENV{HOME} . '/Mail' : '.';
42              
43             sub _default_body_type($$)
44 946   100 946   4646 { my $size = shift->guessBodySize || 0;
45 946 50       52257 'Mail::Message::Body::'.($size > 100000 ? 'File' : 'Lines');
46             }
47              
48             sub init($)
49 49     49 0 134 { my ($self, $args) = @_;
50 49   66     208 $args->{folderdir} ||= $default_folder_dir;
51 49   50     361 $args->{body_type} ||= \&_default_body_type;
52 49   50     383 $args->{lock_file} ||= '--'; # to be resolved later
53 49         286 $self->SUPER::init($args);
54              
55 49         130 my $class = ref $self;
56 49         181 my $filename = $self->{MBF_filename} = $self->folderToFilename($self->name, $self->folderdir);
57              
58 49 100 66     1918 if(-e $filename) {;} # Folder already exists
    100          
59             elsif($args->{create} && $class->create($args->{folder}, %$args)) {;}
60             else
61 1         110 { error __x"folder file {file} does not exist.", file => $filename;
62             }
63              
64 48         217 $self->{MBF_policy} = $args->{write_policy};
65              
66             # Lock the folder.
67              
68 48         339 my $locker = $self->locker;
69              
70 48         244 my $lockfile = $locker->filename;
71 48 50       280 if($lockfile eq '--') # filename to be used not resolved yet
72 48         443 { my $lockdir = $filename =~ s!/([^/]*)$!!r;
73 48   50     284 my $extension = $args->{lock_extension} || '.lock';
74 48 50       374 my $fn
    50          
75             = file_name_is_absolute($extension) ? $extension
76             : $extension =~ m!^\.! ? "$filename$extension"
77             : catfile($lockdir, $extension);
78              
79 48         841 $locker->filename($fn);
80             }
81              
82             $locker->lock
83 48 50       232 or error __x"cannot get a lock on {type} folder {name}.", type => $class, name => $self->name;
84              
85             # Check if we can write to the folder, if we need to.
86 48 50 66     279 if($self->writable && ! -w $filename)
87 0         0 { warning __x"folder {name} file {file} is write-protected.", name => $self->name, file => $filename;
88 0         0 $self->access('r');
89             }
90              
91             # Start parser if reading is required.
92 48 100       203 $self->parser if $self->access =~ m/r/;
93 48         227 $self;
94             }
95              
96              
97             sub create($@)
98 11     11 1 98 { my ($thingy, $name, %args) = @_;
99 11   33     65 my $class = ref $thingy || $thingy;
100 11   33     46 my $folderdir = $args{folderdir} || $default_folder_dir;
101 11         26 my $subext = $args{subfolder_extension}; # not always available
102 11         50 my $filename = $class->folderToFilename($name, $folderdir, $subext);
103              
104 11 50       207 return $class if -f $filename;
105              
106 11         676 my $dir = dirname $filename;
107 11 50 33     208 if(-f $dir && defined $subext)
108 0         0 { $dir .= $subext;
109 0         0 $filename = catfile $dir, basename $filename;
110             }
111              
112 11 50 66     406 -d $dir || mkdir $dir, 0755
113             or fault __x"cannot create directory {dir} for folder $name: $!", dir => $dir, name => $name;
114              
115 11 100 66     205 $class->moveAwaySubFolder($filename, $subext)
116             if -d $filename && defined $subext;
117              
118 11 50       8114 open my $create, '>:raw', $filename
119             or fault __x"cannot create folder file {file}", file => $filename;
120              
121 11         117 trace "Created folder $name in $filename.";
122 11 50       497 $create->close or return;
123 11         424 $class;
124             }
125              
126             #--------------------
127              
128 175     175 1 103872 sub filename() { $_[0]->{MBF_filename} }
129              
130             sub foundIn($@)
131 0     0 1 0 { my $class = shift;
132 0 0       0 my $name = @_ % 2 ? shift : undef;
133 0         0 my %args = @_;
134 0 0 0     0 $name ||= $args{folder} or return;
135              
136 0   0     0 my $folderdir = $args{folderdir} || $default_folder_dir;
137 0         0 my $filename = $class->folderToFilename($name, $folderdir);
138              
139 0         0 -f $filename;
140             }
141              
142             sub organization() { 'FILE' }
143              
144             sub size()
145 0     0 1 0 { my $self = shift;
146 0 0       0 $self->isModified ? $self->SUPER::size : -s $self->filename;
147             }
148              
149             sub close(@)
150 49     49 1 8421 { my $self = $_[0]; # be careful, we want to set the calling
151 49         129 undef $_[0]; # ref to undef, as the SUPER does.
152 49         107 shift;
153              
154 49         375 my $rc = $self->SUPER::close(@_);
155              
156 49 100       572 if(my $parser = delete $self->{MBF_parser}) { $parser->stop }
  38         318  
157              
158 49         6591 $rc;
159             }
160              
161             #--------------------
162              
163             sub appendMessages(@)
164 1     1 1 4 { my $class = shift;
165 1         8 my %args = @_;
166              
167             my @messages
168             = exists $args{message} ? $args{message}
169 1 50       7 : exists $args{messages} ? @{$args{messages}}
  1 50       5  
170             : return ();
171              
172 1 50       7 my $folder = $class->new(lock_type => 'NONE', @_, access => 'w+')
173             or return ();
174              
175 1         6 my $filename = $folder->filename;
176 1 50       74 open my $out, '>>', $filename
177             or fault __x"cannot append messages to folder file {file}.", file => $filename;
178              
179 1         6 my $msgtype = $class.'::Message';
180 1         3 my @coerced;
181              
182 1         4 foreach my $msg (@messages)
183 1 50       25 { my $coerced = $msg->isa($msgtype) ? $msg : $msgtype->coerce($msg->can('clone') ? $msg->clone : $msg);
    50          
184 1         53 $coerced->write($out);
185 1         4 push @coerced, $coerced;
186             }
187              
188 1         6 my $ok = $folder->close;
189 1 50 33     6 $out->close && $ok
190             or return ();
191              
192 1         99 @coerced;
193             }
194              
195             #--------------------
196              
197             sub parser()
198 100     100 1 208 { my $self = shift;
199 100 100       921 return $self->{MBF_parser} if defined $self->{MBF_parser};
200              
201 39         249 my $source = $self->filename;
202 39   50     138 my $mode = $self->access || 'r';
203 39 100 66     270 $mode = 'r+' if $mode eq 'rw' || $mode eq 'a';
204              
205 39         291 my $parser = $self->{MBF_parser} = Mail::Box::Parser->new(
206             filename => $source,
207             mode => $mode,
208             trusted => $self->isTrusted,
209             fix_header_errors => $self->fixHeaders,
210             );
211 39         15476 $parser->pushSeparator('From ');
212 39         544 $parser;
213             }
214              
215             sub readMessages(@)
216 39     39 1 351 { my ($self, %args) = @_;
217              
218             $self->messageCreateOptions(
219             $args{message_type},
220             folder => $self,
221             head_type => $args{head_type},
222             field_type => $args{field_type},
223             trusted => $args{trusted},
224 39         345 );
225              
226 39         183 $self->updateMessages;
227             }
228              
229              
230             sub updateMessages(@)
231 40     40 1 124 { my ($self, %args) = @_;
232 40 50       175 my $parser = $self->parser or return;
233              
234             # On a directory, simulate an empty folder with only subfolders.
235 40         169 my $filename = $self->filename;
236 40 50       882 return $self if -d $filename;
237              
238 40 100       303 if(my $last = $self->message(-1))
239 1         16 { (undef, my $end) = $last->fileLocation;
240 1         9 $parser->filePosition($end);
241             }
242              
243 40         225 my ($type, @msgopts) = $self->messageCreateOptions;
244 40         95 my $count = 0;
245              
246 40         102 while(1)
247 1304         7022 { my $message = $type->new(@msgopts);
248 1304 100       5917 $message->readFromParser($parser) or last;
249 1264         30381 $self->storeMessage($message);
250 1264         2343 $count++;
251             }
252              
253 40         628 trace "found $count new messages in $filename";
254 40         2122 $self;
255             }
256              
257              
258             sub messageCreateOptions(@)
259 79     79 1 272 { my ($self, @options) = @_;
260 79 100       273 if(@options)
261 39   66     897 { blessed $_ && (ref $_) =~ m/^Mail::/ && weaken $_ for @options;
      66        
262 39         146 $self->{MBF_create_options} = \@options;
263             }
264              
265 79         156 @{$self->{MBF_create_options}};
  79         354  
266             }
267              
268              
269             sub moveAwaySubFolder($$)
270 1     1 1 5 { my ($self, $dir, $extension) = @_;
271              
272 1 50       9 move $dir, $dir.$extension
273             or fault __x"cannot move away sub-folder {dir}", dir => $dir;
274              
275 1         270 $self;
276             }
277              
278             sub delete(@)
279 12     12 1 24 { my $self = shift;
280 12         71 $self->SUPER::delete(@_);
281 12         43 unlink $self->filename;
282             }
283              
284              
285             sub writeMessages($)
286 14     14 1 42 { my ($self, $args) = @_;
287              
288 14         65 my $filename = $self->filename;
289 14 50 33     37 if( ! @{$args->{messages}} && $self->removeEmpty)
  14         176  
290 0 0       0 { unlink $filename
291             or warning __x"cannot remove folder {name} file {file}: {rc}", name => $self->name, file => $filename, rc => $!;
292 0         0 return $self;
293             }
294              
295 14 100       96 my $policy = exists $args->{policy} ? $args->{policy} : $self->{MBF_policy};
296 14   100     110 $policy ||= '';
297              
298 14 50       650 my $success
    100          
    100          
    50          
299             = ! -e $filename ? $self->_write_new($args)
300             : $policy eq 'INPLACE' ? $self->_write_inplace($args)
301             : $policy eq 'REPLACE' ? $self->_write_replace($args)
302             : $self->_write_replace($args) ? 1
303             : $self->_write_inplace($args);
304              
305 14 50       91 $success
306             or error __x"unable to update folder {name}.", name => $self->name;
307              
308             # $self->parser->restart;
309 14         69 $self;
310             }
311              
312             sub _write_new($)
313 0     0   0 { my ($self, $args) = @_;
314              
315 0         0 my $filename = $self->filename;
316 0 0       0 open my $new, ">:raw", $filename
317             or return 0;
318              
319 0         0 my $msgs = $args->{messages};
320 0         0 $_->write($new) for @$msgs;
321 0 0       0 $new->close or return 0;
322              
323 0         0 trace "Wrote new folder $self with ".@$msgs."msgs.";
324 0         0 1;
325             }
326              
327             # First write to a new file, then replace the source folder in one
328             # move. This is much slower than inplace update, but it is safer,
329             # The folder is always in the right shape, even if the program is
330             # interrupted.
331              
332             sub _write_replace($)
333 10     10   37 { my ($self, $args) = @_;
334              
335 10         33 my $filename = $self->filename;
336 10         93 my $tmpnew = $self->tmpNewFolder($filename);
337              
338 10 50       2359 open my $new, '>:raw', $tmpnew or return 0;
339 10 50       435 open my $old, '<:raw', $filename or return 0;
340              
341 10         43 my ($reprint, $kept) = (0,0);
342              
343 10         25 foreach my $message ( @{$args->{messages}} )
  10         64  
344             {
345 216         786 my $newbegin = $new->tell;
346 216         1419 my $oldbegin = $message->fileLocation;
347              
348 216 100       630 if($message->isModified)
349 35         330 { $message->write($new);
350 35 50       153 $message->moveLocation($newbegin - $oldbegin) if defined $oldbegin;
351 35         66 $reprint++;
352 35         119 next;
353             }
354              
355 181         1656 my ($begin, $end) = $message->fileLocation;
356 181         784 my $need = $end-$begin;
357              
358 181         639 $old->seek($begin, 0);
359 181         3521 my $whole;
360 181         528 my $size = $old->read($whole, $need);
361              
362 181 50       3173 $size == $need
363             or error __x"file {name} too short to get write message {msgnr} ({size} < {expect})",
364             msgnr => $message->seqnr, size => $size, expect => $need;
365              
366 181         522 $new->print($whole);
367 181 50       4350 $new->print($Mail::Message::crlf_platform ? "\r\n" : "\n");
368              
369 181         1405 $message->moveLocation($newbegin - $oldbegin);
370 181         431 $kept++;
371             }
372              
373 10         88 my $ok = $new->close;
374 10 50 33     774 $old->close && $ok
375             or return 0;
376              
377 10 50       243 if($windows)
378             { # Windows does not like to move to existing filenames
379 0         0 unlink $filename;
380              
381             # Windows cannot move to files which are opened.
382 0         0 $self->parser->closeFile;
383             }
384              
385 10 50       75 unless(move $tmpnew, $filename)
386 0         0 { unlink $tmpnew;
387 0         0 fault __x"cannot replace {to} by {from} to update folder {name}", to => $filename, from => $tmpnew, name => $self->name;
388             }
389              
390 10         15237 trace "folder $self replaced ($kept, $reprint)";
391 10         550 1;
392             }
393              
394             # Inplace is currently very poorly implemented. From the first
395             # location where changes appear, all messages are rewritten.
396              
397             sub _write_inplace($)
398 4     4   12 { my ($self, $args) = @_;
399              
400 4         10 my @messages = @{$args->{messages}};
  4         32  
401 4         8 my $last;
402              
403 4         12 my ($msgnr, $kept) = (0, 0);
404 4         30 while(@messages)
405 87         115 { my $next = $messages[0];
406 87 100 100     236 last if $next->isModified || $next->seqnr!=$msgnr++;
407 84         118 $last = shift @messages;
408 84         145 $kept++;
409             }
410              
411 4 50 66     34 if(@messages==0 && $msgnr==$self->messages)
412 0         0 { trace "No changes to be written to $self.";
413 0         0 return 1;
414             }
415              
416 4         23 $_->body->load for @messages;
417              
418 4 50       392 my $mode = $^O eq 'MSWin32' ? '>>:raw' : '+<:raw';
419 4         15 my $filename = $self->filename;
420 4 50       303 open my $old, $mode, $filename or return 0;
421              
422             # Chop the folder after the messages which does not have to change.
423              
424 4 100       44 my $end = defined $last ? ($last->fileLocation)[1] : 0;
425              
426 4         36 $end =~ m/(.*)/; # untaint, only required by perl5.6.1
427 4         17 $end = $1;
428              
429 4 50       43 unless($old->truncate($end))
430             { # truncate impossible: try replace writing
431 0         0 $old->close;
432 0         0 return 0;
433             }
434              
435 4 100       5512 unless(@messages)
436             { # All further messages only are flagged to be deleted
437 1 50       14 $old->close or return 0;
438 1         43 trace "Folder $self shortened in-place ($kept kept)";
439 1         64 return 1;
440             }
441              
442             # go to the end of the truncated output file.
443 3         31 $old->seek(0, 2);
444              
445             # Print the messages which have to move.
446 3         51 my $printed = @messages;
447 3         12 foreach my $message (@messages)
448 90         342 { my $oldbegin = $message->fileLocation;
449 90         295 my $newbegin = $old->tell;
450 90         599 $message->write($old);
451 90         360 $message->moveLocation($newbegin - $oldbegin);
452             }
453              
454 3 50       25 $old->close or return 0;
455 3         2401 trace "Folder $self updated in-place ($kept, $printed)";
456 3         221 1;
457             }
458              
459              
460             sub folderToFilename($$;$)
461 0     0 1 0 { my ($thing, $name, $folderdir) = @_;
462              
463 0 0       0 substr $name, 0, 1, $folderdir
464             if substr $name, 0, 1 eq '=';
465              
466 0         0 $name;
467             }
468              
469 10     10 0 206 sub tmpNewFolder($) { $_[0]->filename . '.tmp' }
470              
471             #--------------------
472              
473             1;