File Coverage

blib/lib/Mail/Box/File.pm
Criterion Covered Total %
statement 230 276 83.3
branch 77 134 57.4
condition 35 66 53.0
subroutine 34 38 89.4
pod 14 16 87.5
total 390 530 73.5


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::File;
10 25     25   1141 use vars '$VERSION';
  25         57  
  25         1262  
11             $VERSION = '3.010';
12              
13 25     25   139 use base 'Mail::Box';
  25         63  
  25         6649  
14              
15 25     25   175 use strict;
  25         62  
  25         514  
16 25     25   134 use warnings;
  25         54  
  25         732  
17              
18 25     25   12447 use filetest 'access';
  25         359  
  25         167  
19              
20 25     25   11444 use Mail::Box::File::Message;
  25         76  
  25         885  
21              
22 25     25   185 use Mail::Message::Body::Lines;
  25         48  
  25         608  
23 25     25   306 use Mail::Message::Body::File;
  25         53  
  25         605  
24 25     25   9209 use Mail::Message::Body::Delayed;
  25         59  
  25         728  
25 25     25   168 use Mail::Message::Body::Multipart;
  25         59  
  25         651  
26              
27 25     25   136 use Mail::Message::Head;
  25         80  
  25         547  
28              
29 25     25   134 use Carp;
  25         47  
  25         1356  
30 25     25   152 use File::Copy;
  25         58  
  25         1339  
31 25     25   154 use File::Spec;
  25         52  
  25         843  
32 25     25   167 use File::Basename;
  25         132  
  25         2038  
33 25     25   15440 use POSIX ':unistd_h';
  25         170562  
  25         160  
34              
35             # tell() is not available for open(my $fh) on perl versions <= 5.10 So,
36             # we need to stick to IO::File syntax.
37 25     25   46656 use IO::File;
  25         61  
  25         5179  
38              
39             my $windows;
40 25     25   78429 BEGIN { $windows = $^O =~ m/mswin32/i }
41              
42              
43             my $default_folder_dir = exists $ENV{HOME} ? $ENV{HOME} . '/Mail' : '.';
44              
45             sub _default_body_type($$)
46 946   100 946   2501 { my $size = shift->guessBodySize || 0;
47 946 50       41570 'Mail::Message::Body::'.($size > 100000 ? 'File' : 'Lines');
48             }
49              
50             sub init($)
51 49     49 0 151 { my ($self, $args) = @_;
52 49   66     202 $args->{folderdir} ||= $default_folder_dir;
53 49   50     373 $args->{body_type} ||= \&_default_body_type;
54 49   50     303 $args->{lock_file} ||= '--'; # to be resolved later
55              
56 49 50       265 defined $self->SUPER::init($args)
57             or return;
58              
59 49         209 my $class = ref $self;
60              
61             my $filename = $self->{MBF_filename}
62 49         241 = $self->folderToFilename
63             ( $self->name
64             , $self->folderdir
65             );
66              
67 49 100 66     924 if(-e $filename) {;} # Folder already exists
    100          
68             elsif($args->{create} && $class->create($args->{folder}, %$args)) {;}
69             else
70 1         11 { $self->log(PROGRESS =>
71             "File $filename for folder $self does not exist.");
72 1         33 return;
73             }
74              
75 48         277 $self->{MBF_policy} = $args->{write_policy};
76              
77             # Lock the folder.
78              
79 48         311 my $locker = $self->locker;
80              
81 48         283 my $lockfile = $locker->filename;
82 48 50       203 if($lockfile eq '--') # filename to be used not resolved yet
83 48         96 { my $lockdir = $filename;
84 48         460 $lockdir =~ s!/([^/]*)$!!;
85 48   50     330 my $extension = $args->{lock_extension} || '.lock';
86              
87 48 50       770 $locker->filename
    50          
88             ( File::Spec->file_name_is_absolute($extension) ? $extension
89             : $extension =~ m!^\.! ? "$filename$extension"
90             : File::Spec->catfile($lockdir, $extension)
91             );
92             }
93              
94 48 50       226 unless($locker->lock)
95 0         0 { $self->log(ERROR => "Cannot get a lock on $class folder $self.");
96 0         0 return;
97             }
98              
99             # Check if we can write to the folder, if we need to.
100              
101 48 50 66     239 if($self->writable && ! -w $filename)
102 0         0 { $self->log(WARNING => "Folder $self file $filename is write-protected.");
103 0         0 $self->{MB_access} = 'r';
104             }
105              
106             # Start parser if reading is required.
107              
108 48 50       614 $self->{MB_access} !~ m/r/ ? $self
    100          
109             : $self->parser ? $self
110             : undef;
111             }
112              
113              
114             sub create($@)
115 11     11 1 71 { my ($thingy, $name, %args) = @_;
116 11   33     55 my $class = ref $thingy || $thingy;
117 11   33     46 my $folderdir = $args{folderdir} || $default_folder_dir;
118 11         25 my $subext = $args{subfolder_extension}; # not always available
119 11         45 my $filename = $class->folderToFilename($name, $folderdir, $subext);
120              
121 11 50       162 return $class if -f $filename;
122              
123 11         1063 my $dir = dirname $filename;
124 11 50 33     192 if(-f $dir && defined $subext)
125 0         0 { $dir .= $subext;
126 0         0 $filename = File::Spec->catfile($dir, basename $filename);
127             }
128              
129 11 50 66     379 $class->log(ERROR => "Cannot create directory $dir for folder $name: $!"),return
130             unless -d $dir || mkdir $dir, 0755;
131              
132 11 100 66     206 $class->moveAwaySubFolder($filename, $subext)
133             if -d $filename && defined $subext;
134              
135 11         130 my $create = IO::File->new($filename, 'w');
136 11 50       1993 unless($create)
137 0         0 { $class->log(WARNING => "Cannot create folder file $name: $!");
138 0         0 return;
139             }
140              
141 11         131 $class->log(PROGRESS => "Created folder $name.");
142 11 50       180 $create->close or return;
143 11         387 $class;
144             }
145              
146             sub foundIn($@)
147 0     0 1 0 { my $class = shift;
148 0 0       0 my $name = @_ % 2 ? shift : undef;
149 0         0 my %args = @_;
150 0 0 0     0 $name ||= $args{folder} or return;
151              
152 0   0     0 my $folderdir = $args{folderdir} || $default_folder_dir;
153 0         0 my $filename = $class->folderToFilename($name, $folderdir);
154              
155 0         0 -f $filename;
156             }
157              
158             sub organization() { 'FILE' }
159              
160             sub size()
161 0     0 1 0 { my $self = shift;
162 0 0       0 $self->isModified ? $self->SUPER::size : -s $self->filename;
163             }
164              
165             sub close(@)
166 49     49 1 2835 { my $self = $_[0]; # be careful, we want to set the calling
167 49         115 undef $_[0]; # ref to undef, as the SUPER does.
168 49         99 shift;
169              
170 49         403 my $rc = $self->SUPER::close(@_);
171              
172 49 100       317 if(my $parser = delete $self->{MBF_parser}) { $parser->stop }
  38         583  
173              
174 49         4128 $rc;
175             }
176              
177              
178             sub appendMessages(@)
179 1     1 1 3 { my $class = shift;
180 1         7 my %args = @_;
181              
182             my @messages
183             = exists $args{message} ? $args{message}
184 1 50       7 : exists $args{messages} ? @{$args{messages}}
  1 50       16  
185             : return ();
186              
187 1 50       10 my $folder = $class->new(lock_type => 'NONE', @_, access => 'w+')
188             or return ();
189            
190 1         7 my $filename = $folder->filename;
191              
192 1         9 my $out = IO::File->new($filename, 'a');
193 1 50       115 unless($out)
194 0         0 { $class->log(ERROR => "Cannot append messages to folder file $filename: $!");
195 0         0 return ();
196             }
197              
198 1         7 my $msgtype = $class.'::Message';
199 1         2 my @coerced;
200              
201 1         4 foreach my $msg (@messages)
202 1 50       20 { my $coerced
    50          
203             = $msg->isa($msgtype) ? $msg
204             : $msg->can('clone') ? $msgtype->coerce($msg->clone)
205             : $msgtype->coerce($msg);
206              
207 1         36 $coerced->write($out);
208 1         3 push @coerced, $coerced;
209             }
210              
211 1         5 my $ok = $folder->close;
212 1 50 33     5 $out->close && $ok
213             or return 0;
214              
215 1         100 @coerced;
216             }
217              
218             #-------------------------------------------
219              
220              
221 175     175 1 88554 sub filename() { shift->{MBF_filename} }
222              
223             #-------------------------------------------
224              
225              
226             sub parser()
227 100     100 1 192 { my $self = shift;
228              
229             return $self->{MBF_parser}
230 100 100       428 if defined $self->{MBF_parser};
231              
232 39         206 my $source = $self->filename;
233              
234 39   50     209 my $mode = $self->{MB_access} || 'r';
235 39 100 66     209 $mode = 'r+' if $mode eq 'rw' || $mode eq 'a';
236              
237             my $parser = $self->{MBF_parser}
238             = Mail::Box::Parser->new
239             ( filename => $source
240             , mode => $mode
241             , trusted => $self->{MB_trusted}
242             , fix_header_errors => $self->{MB_fix_headers}
243 39 50       217 , $self->logSettings
244             ) or return;
245              
246 39         13810 $parser->pushSeparator('From ');
247 39         761 $parser;
248             }
249              
250             sub readMessages(@)
251 39     39 1 294 { my ($self, %args) = @_;
252              
253             $self->messageCreateOptions
254             ( $args{message_type}
255             , $self->logSettings
256             , folder => $self
257             , head_type => $args{head_type}
258             , field_type => $args{field_type}
259             , trusted => $args{trusted}
260 39         212 );
261              
262 39         260 $self->updateMessages;
263             }
264            
265              
266             sub updateMessages(@)
267 40     40 1 141 { my ($self, %args) = @_;
268 40 50       112 my $parser = $self->parser or return;
269              
270             # On a directory, simulate an empty folder with only subfolders.
271 40         227 my $filename = $self->filename;
272 40 50       727 return $self if -d $filename;
273              
274 40 100       422 if(my $last = $self->message(-1))
275 1         17 { (undef, my $end) = $last->fileLocation;
276 1         9 $parser->filePosition($end);
277             }
278              
279 40         172 my ($type, @msgopts) = $self->messageCreateOptions;
280 40         123 my $count = 0;
281              
282 40         69 while(1)
283 1304         4793 { my $message = $type->new(@msgopts);
284 1304 100       4723 last unless $message->readFromParser($parser);
285 1264         3877 $self->storeMessage($message);
286 1264         1845 $count++;
287             }
288              
289 40 100       802 $self->log(PROGRESS => "Found $count new messages in $filename")
290             if $count;
291              
292 40         1140 $self;
293             }
294              
295              
296             sub messageCreateOptions(@)
297 79     79 1 650 { my ($self, @options) = @_;
298 79 100       298 if(@options)
299 39   66     937 { ref($_) && ref($_) =~ m/^Mail::/ && weaken $_ for @options;
      66        
300 39         248 $self->{MBF_create_options} = \@options;
301             }
302            
303 79         252 @{$self->{MBF_create_options}};
  79         376  
304             }
305              
306              
307             sub moveAwaySubFolder($$)
308 1     1 1 4 { my ($self, $dir, $extension) = @_;
309 1 50       11 $self->log(ERROR => "Cannot move away sub-folder $dir")
310             unless move $dir, $dir.$extension;
311 1         114 $self;
312             }
313              
314             sub delete(@)
315 12     12 1 23 { my $self = shift;
316 12         53 $self->SUPER::delete(@_);
317 12         33 unlink $self->filename;
318             }
319              
320              
321             sub writeMessages($)
322 14     14 1 51 { my ($self, $args) = @_;
323              
324 14         67 my $filename = $self->filename;
325 14 0 33     41 if( ! @{$args->{messages}} && $self->{MB_remove_empty})
  14         67  
326 0 0       0 { $self->log(WARNING => "Cannot remove folder $self file $filename: $!")
327             unless unlink $filename;
328 0         0 return $self;
329             }
330              
331 14 100       58 my $policy = exists $args->{policy} ? $args->{policy} : $self->{MBF_policy};
332 14   100     76 $policy ||= '';
333              
334 14 50       514 my $success
    100          
    100          
    50          
335             = ! -e $filename ? $self->_write_new($args)
336             : $policy eq 'INPLACE' ? $self->_write_inplace($args)
337             : $policy eq 'REPLACE' ? $self->_write_replace($args)
338             : $self->_write_replace($args) ? 1
339             : $self->_write_inplace($args);
340              
341 14 50       67 unless($success)
342 0         0 { $self->log(ERROR => "Unable to update folder $self.");
343 0         0 return;
344             }
345              
346             # $self->parser->restart;
347 14         59 $self;
348             }
349              
350             sub _write_new($)
351 0     0   0 { my ($self, $args) = @_;
352              
353 0         0 my $filename = $self->filename;
354 0         0 my $new = IO::File->new($filename, 'w');
355 0 0       0 return 0 unless defined $new;
356              
357 0         0 $new->binmode;
358 0         0 $_->write($new) foreach @{$args->{messages}};
  0         0  
359              
360 0 0       0 $new->close or return 0;
361              
362             $self->log(PROGRESS =>
363 0         0 "Wrote new folder $self with ".@{$args->{messages}}."msgs.");
  0         0  
364 0         0 1;
365             }
366              
367             # First write to a new file, then replace the source folder in one
368             # move. This is much slower than inplace update, but it is safer,
369             # The folder is always in the right shape, even if the program is
370             # interrupted.
371              
372             sub _write_replace($)
373 10     10   50 { my ($self, $args) = @_;
374              
375 10         39 my $filename = $self->filename;
376 10         65 my $tmpnew = $self->tmpNewFolder($filename);
377              
378 10 50       109 my $new = IO::File->new($tmpnew, 'w') or return 0;
379 10         1999 $new->binmode;
380              
381 10 50       151 my $old = IO::File->new($filename, 'r') or return 0;
382 10         995 $old->binmode;
383              
384 10         108 my ($reprint, $kept) = (0,0);
385              
386 10         31 foreach my $message ( @{$args->{messages}} )
  10         40  
387             {
388 216         566 my $newbegin = $new->tell;
389 216         1169 my $oldbegin = $message->fileLocation;
390              
391 216 100       478 if($message->isModified)
392 35         285 { $message->write($new);
393 35 50       100 $message->moveLocation($newbegin - $oldbegin)
394             if defined $oldbegin;
395 35         54 $reprint++;
396 35         97 next;
397             }
398              
399 181         1327 my ($begin, $end) = $message->fileLocation;
400 181         647 my $need = $end-$begin;
401              
402 181         494 $old->seek($begin, 0);
403 181         2847 my $whole;
404 181         529 my $size = $old->read($whole, $need);
405              
406 181 50       3687 $self->log(ERROR => "File too short to get write message "
407             . $message->seqnr. " ($size, $need)")
408             unless $size == $need;
409              
410 181         548 $new->print($whole);
411 181 50       3465 $new->print($Mail::Message::crlf_platform ? "\r\n" : "\n");
412              
413 181         1276 $message->moveLocation($newbegin - $oldbegin);
414 181         349 $kept++;
415             }
416              
417 10         72 my $ok = $new->close;
418 10 50 33     590 $old->close && $ok
419             or return 0;
420              
421 10 50       269 if($windows)
422             { # Windows does not like to move to existing filenames
423 0         0 unlink $filename;
424              
425             # Windows cannot move to files which are opened.
426 0         0 $self->parser->closeFile;
427             }
428              
429 10 50       62 unless(move $tmpnew, $filename)
430 0         0 { $self->log(WARNING =>
431             "Cannot replace $filename by $tmpnew, to update folder $self: $!");
432              
433 0         0 unlink $tmpnew;
434 0         0 return 0;
435             }
436              
437 10         2146 $self->log(PROGRESS => "Folder $self replaced ($kept, $reprint)");
438 10         264 1;
439             }
440              
441             # Inplace is currently very poorly implemented. From the first
442             # location where changes appear, all messages are rewritten.
443              
444             sub _write_inplace($)
445 4     4   20 { my ($self, $args) = @_;
446              
447 4         9 my @messages = @{$args->{messages}};
  4         31  
448 4         12 my $last;
449              
450 4         10 my ($msgnr, $kept) = (0, 0);
451 4         13 while(@messages)
452 87         109 { my $next = $messages[0];
453 87 100 100     150 last if $next->isModified || $next->seqnr!=$msgnr++;
454 84         126 $last = shift @messages;
455 84         147 $kept++;
456             }
457              
458 4 50 66     39 if(@messages==0 && $msgnr==$self->messages)
459 0         0 { $self->log(PROGRESS => "No changes to be written to $self.");
460 0         0 return 1;
461             }
462              
463 4         28 $_->body->load foreach @messages;
464              
465 4 50       404 my $mode = $^O eq 'MSWin32' ? 'a' : 'r+';
466 4         13 my $filename = $self->filename;
467 4 50       48 my $old = IO::File->new($filename, $mode) or return 0;
468              
469             # Chop the folder after the messages which does not have to change.
470              
471 4 100       757 my $end = defined $last ? ($last->fileLocation)[1] : 0;
472              
473 4         41 $end =~ m/(.*)/; # untaint, only required by perl5.6.1
474 4         14 $end = $1;
475              
476 4 50       29 unless($old->truncate($end))
477             { # truncate impossible: try replace writing
478 0         0 $old->close;
479 0         0 return 0;
480             }
481              
482 4 100       503 unless(@messages)
483             { # All further messages only are flagged to be deleted
484 1 50       7 $old->close or return 0;
485 1         28 $self->log(PROGRESS => "Folder $self shortened in-place ($kept kept)");
486 1         28 return 1;
487             }
488              
489             # go to the end of the truncated output file.
490 3         29 $old->seek(0, 2);
491              
492             # Print the messages which have to move.
493 3         49 my $printed = @messages;
494 3         15 foreach my $message (@messages)
495 90         188 { my $oldbegin = $message->fileLocation;
496 90         228 my $newbegin = $old->tell;
497 90         545 $message->write($old);
498 90         240 $message->moveLocation($newbegin - $oldbegin);
499             }
500              
501 3 50       29 $old->close or return 0;
502 3         312 $self->log(PROGRESS => "Folder $self updated in-place ($kept, $printed)");
503 3         80 1;
504             }
505              
506             #-------------------------------------------
507              
508              
509             sub folderToFilename($$;$)
510 0     0 1 0 { my ($thing, $name, $folderdir) = @_;
511              
512 0 0       0 substr $name, 0, 1, $folderdir
513             if substr $name, 0, 1 eq '=';
514              
515 0         0 $name;
516             }
517              
518 10     10 0 31 sub tmpNewFolder($) { shift->filename . '.tmp' }
519              
520             #-------------------------------------------
521              
522              
523             1;