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-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::File;
10 25     25   910 use vars '$VERSION';
  25         41  
  25         1093  
11             $VERSION = '3.008';
12              
13 25     25   124 use base 'Mail::Box';
  25         42  
  25         5580  
14              
15 25     25   146 use strict;
  25         40  
  25         455  
16 25     25   102 use warnings;
  25         68  
  25         691  
17              
18 25     25   9864 use filetest 'access';
  25         288  
  25         129  
19              
20 25     25   9416 use Mail::Box::File::Message;
  25         54  
  25         690  
21              
22 25     25   144 use Mail::Message::Body::Lines;
  25         53  
  25         503  
23 25     25   279 use Mail::Message::Body::File;
  25         42  
  25         501  
24 25     25   7569 use Mail::Message::Body::Delayed;
  25         58  
  25         592  
25 25     25   134 use Mail::Message::Body::Multipart;
  25         46  
  25         532  
26              
27 25     25   107 use Mail::Message::Head;
  25         44  
  25         415  
28              
29 25     25   95 use Carp;
  25         46  
  25         1226  
30 25     25   122 use File::Copy;
  25         43  
  25         1094  
31 25     25   141 use File::Spec;
  25         44  
  25         496  
32 25     25   109 use File::Basename;
  25         105  
  25         1633  
33 25     25   11079 use POSIX ':unistd_h';
  25         133281  
  25         119  
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   36968 use IO::File;
  25         53  
  25         3736  
38              
39             my $windows;
40 25     25   60738 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   2265 { my $size = shift->guessBodySize || 0;
47 946 50       34717 'Mail::Message::Body::'.($size > 100000 ? 'File' : 'Lines');
48             }
49              
50             sub init($)
51 49     49 0 118 { my ($self, $args) = @_;
52 49   66     153 $args->{folderdir} ||= $default_folder_dir;
53 49   50     272 $args->{body_type} ||= \&_default_body_type;
54 49   50     256 $args->{lock_file} ||= '--'; # to be resolved later
55              
56 49 50       201 defined $self->SUPER::init($args)
57             or return;
58              
59 49         110 my $class = ref $self;
60              
61             my $filename = $self->{MBF_filename}
62 49         147 = $self->folderToFilename
63             ( $self->name
64             , $self->folderdir
65             );
66              
67 49 100 66     689 if(-e $filename) {;} # Folder already exists
    100          
68             elsif($args->{create} && $class->create($args->{folder}, %$args)) {;}
69             else
70 1         6 { $self->log(PROGRESS =>
71             "File $filename for folder $self does not exist.");
72 1         25 return;
73             }
74              
75 48         173 $self->{MBF_policy} = $args->{write_policy};
76              
77             # Lock the folder.
78              
79 48         245 my $locker = $self->locker;
80              
81 48         212 my $lockfile = $locker->filename;
82 48 50       138 if($lockfile eq '--') # filename to be used not resolved yet
83 48         83 { my $lockdir = $filename;
84 48         374 $lockdir =~ s!/([^/]*)$!!;
85 48   50     296 my $extension = $args->{lock_extension} || '.lock';
86              
87 48 50       553 $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       156 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     151 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       437 $self->{MB_access} !~ m/r/ ? $self
    100          
109             : $self->parser ? $self
110             : undef;
111             }
112              
113              
114             sub create($@)
115 11     11 1 51 { my ($thingy, $name, %args) = @_;
116 11   33     43 my $class = ref $thingy || $thingy;
117 11   33     33 my $folderdir = $args{folderdir} || $default_folder_dir;
118 11         18 my $subext = $args{subfolder_extension}; # not always available
119 11         33 my $filename = $class->folderToFilename($name, $folderdir, $subext);
120              
121 11 50       104 return $class if -f $filename;
122              
123 11         506 my $dir = dirname $filename;
124 11 50 33     140 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     219 $class->log(ERROR => "Cannot create directory $dir for folder $name: $!"),return
130             unless -d $dir || mkdir $dir, 0755;
131              
132 11 100 66     118 $class->moveAwaySubFolder($filename, $subext)
133             if -d $filename && defined $subext;
134              
135 11         88 my $create = IO::File->new($filename, 'w');
136 11 50       1381 unless($create)
137 0         0 { $class->log(WARNING => "Cannot create folder file $name: $!");
138 0         0 return;
139             }
140              
141 11         124 $class->log(PROGRESS => "Created folder $name.");
142 11 50       136 $create->close or return;
143 11         256 $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 2070 { my $self = $_[0]; # be careful, we want to set the calling
167 49         96 undef $_[0]; # ref to undef, as the SUPER does.
168 49         81 shift;
169              
170 49         238 my $rc = $self->SUPER::close(@_);
171              
172 49 100       213 if(my $parser = delete $self->{MBF_parser}) { $parser->stop }
  38         265  
173              
174 49         3345 $rc;
175             }
176              
177              
178             sub appendMessages(@)
179 1     1 1 2 { my $class = shift;
180 1         6 my %args = @_;
181              
182             my @messages
183             = exists $args{message} ? $args{message}
184 1 50       6 : exists $args{messages} ? @{$args{messages}}
  1 50       4  
185             : return ();
186              
187 1 50       7 my $folder = $class->new(lock_type => 'NONE', @_, access => 'w+')
188             or return ();
189            
190 1         3 my $filename = $folder->filename;
191              
192 1         8 my $out = IO::File->new($filename, 'a');
193 1 50       103 unless($out)
194 0         0 { $class->log(ERROR => "Cannot append messages to folder file $filename: $!");
195 0         0 return ();
196             }
197              
198 1         4 my $msgtype = $class.'::Message';
199 1         3 my @coerced;
200              
201 1         2 foreach my $msg (@messages)
202 1 50       16 { my $coerced
    50          
203             = $msg->isa($msgtype) ? $msg
204             : $msg->can('clone') ? $msgtype->coerce($msg->clone)
205             : $msgtype->coerce($msg);
206              
207 1         33 $coerced->write($out);
208 1         3 push @coerced, $coerced;
209             }
210              
211 1         4 my $ok = $folder->close;
212 1 50 33     4 $out->close && $ok
213             or return 0;
214              
215 1         46 @coerced;
216             }
217              
218             #-------------------------------------------
219              
220              
221 175     175 1 82486 sub filename() { shift->{MBF_filename} }
222              
223             #-------------------------------------------
224              
225              
226             sub parser()
227 100     100 1 169 { my $self = shift;
228              
229             return $self->{MBF_parser}
230 100 100       400 if defined $self->{MBF_parser};
231              
232 39         160 my $source = $self->filename;
233              
234 39   50     132 my $mode = $self->{MB_access} || 'r';
235 39 100 66     173 $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       183 , $self->logSettings
244             ) or return;
245              
246 39         10515 $parser->pushSeparator('From ');
247 39         598 $parser;
248             }
249              
250             sub readMessages(@)
251 39     39 1 223 { 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         223 );
261              
262 39         201 $self->updateMessages;
263             }
264            
265              
266             sub updateMessages(@)
267 40     40 1 100 { my ($self, %args) = @_;
268 40 50       114 my $parser = $self->parser or return;
269              
270             # On a directory, simulate an empty folder with only subfolders.
271 40         184 my $filename = $self->filename;
272 40 50       588 return $self if -d $filename;
273              
274 40 100       308 if(my $last = $self->message(-1))
275 1         11 { (undef, my $end) = $last->fileLocation;
276 1         6 $parser->filePosition($end);
277             }
278              
279 40         144 my ($type, @msgopts) = $self->messageCreateOptions;
280 40         90 my $count = 0;
281              
282 40         59 while(1)
283 1304         4300 { my $message = $type->new(@msgopts);
284 1304 100       3924 last unless $message->readFromParser($parser);
285 1264         3521 $self->storeMessage($message);
286 1264         1645 $count++;
287             }
288              
289 40 100       581 $self->log(PROGRESS => "Found $count new messages in $filename")
290             if $count;
291              
292 40         894 $self;
293             }
294              
295              
296             sub messageCreateOptions(@)
297 79     79 1 504 { my ($self, @options) = @_;
298 79 100       235 if(@options)
299 39   66     710 { ref($_) && ref($_) =~ m/^Mail::/ && weaken $_ for @options;
      66        
300 39         162 $self->{MBF_create_options} = \@options;
301             }
302            
303 79         213 @{$self->{MBF_create_options}};
  79         283  
304             }
305              
306              
307             sub moveAwaySubFolder($$)
308 1     1 1 3 { my ($self, $dir, $extension) = @_;
309 1 50       7 $self->log(ERROR => "Cannot move away sub-folder $dir")
310             unless move $dir, $dir.$extension;
311 1         83 $self;
312             }
313              
314             sub delete(@)
315 12     12 1 24 { my $self = shift;
316 12         51 $self->SUPER::delete(@_);
317 12         27 unlink $self->filename;
318             }
319              
320              
321             sub writeMessages($)
322 14     14 1 45 { my ($self, $args) = @_;
323              
324 14         51 my $filename = $self->filename;
325 14 0 33     27 if( ! @{$args->{messages}} && $self->{MB_remove_empty})
  14         73  
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       53 my $policy = exists $args->{policy} ? $args->{policy} : $self->{MBF_policy};
332 14   100     75 $policy ||= '';
333              
334 14 50       382 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       48 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         61 $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   33 { my ($self, $args) = @_;
374              
375 10         23 my $filename = $self->filename;
376 10         45 my $tmpnew = $self->tmpNewFolder($filename);
377              
378 10 50       72 my $new = IO::File->new($tmpnew, 'w') or return 0;
379 10         9761 $new->binmode;
380              
381 10 50       109 my $old = IO::File->new($filename, 'r') or return 0;
382 10         788 $old->binmode;
383              
384 10         73 my ($reprint, $kept) = (0,0);
385              
386 10         17 foreach my $message ( @{$args->{messages}} )
  10         32  
387             {
388 216         471 my $newbegin = $new->tell;
389 216         964 my $oldbegin = $message->fileLocation;
390              
391 216 100       389 if($message->isModified)
392 35         208 { $message->write($new);
393 35 50       76 $message->moveLocation($newbegin - $oldbegin)
394             if defined $oldbegin;
395 35         42 $reprint++;
396 35         75 next;
397             }
398              
399 181         1180 my ($begin, $end) = $message->fileLocation;
400 181         817 my $need = $end-$begin;
401              
402 181         410 $old->seek($begin, 0);
403 181         2193 my $whole;
404 181         395 my $size = $old->read($whole, $need);
405              
406 181 50       2286 $self->log(ERROR => "File too short to get write message "
407             . $message->seqnr. " ($size, $need)")
408             unless $size == $need;
409              
410 181         457 $new->print($whole);
411 181 50       17025 $new->print($Mail::Message::crlf_platform ? "\r\n" : "\n");
412              
413 181         1036 $message->moveLocation($newbegin - $oldbegin);
414 181         311 $kept++;
415             }
416              
417 10         54 my $ok = $new->close;
418 10 50 33     394 $old->close && $ok
419             or return 0;
420              
421 10 50       190 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       51 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         18672 $self->log(PROGRESS => "Folder $self replaced ($kept, $reprint)");
438 10         271 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   13 { my ($self, $args) = @_;
446              
447 4         7 my @messages = @{$args->{messages}};
  4         25  
448 4         8 my $last;
449              
450 4         9 my ($msgnr, $kept) = (0, 0);
451 4         13 while(@messages)
452 87         91 { my $next = $messages[0];
453 87 100 100     135 last if $next->isModified || $next->seqnr!=$msgnr++;
454 84         100 $last = shift @messages;
455 84         123 $kept++;
456             }
457              
458 4 50 66     31 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         14 $_->body->load foreach @messages;
464              
465 4 50       341 my $mode = $^O eq 'MSWin32' ? 'a' : 'r+';
466 4         13 my $filename = $self->filename;
467 4 50       35 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       486 my $end = defined $last ? ($last->fileLocation)[1] : 0;
472              
473 4         29 $end =~ m/(.*)/; # untaint, only required by perl5.6.1
474 4         11 $end = $1;
475              
476 4 50       33 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       278 unless(@messages)
483             { # All further messages only are flagged to be deleted
484 1 50       5 $old->close or return 0;
485 1         20 $self->log(PROGRESS => "Folder $self shortened in-place ($kept kept)");
486 1         21 return 1;
487             }
488              
489             # go to the end of the truncated output file.
490 3         17 $old->seek(0, 2);
491              
492             # Print the messages which have to move.
493 3         38 my $printed = @messages;
494 3         10 foreach my $message (@messages)
495 90         181 { my $oldbegin = $message->fileLocation;
496 90         191 my $newbegin = $old->tell;
497 90         442 $message->write($old);
498 90         491 $message->moveLocation($newbegin - $oldbegin);
499             }
500              
501 3 50       27 $old->close or return 0;
502 3         441 $self->log(PROGRESS => "Folder $self updated in-place ($kept, $printed)");
503 3         88 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 24 sub tmpNewFolder($) { shift->filename . '.tmp' }
519              
520             #-------------------------------------------
521              
522              
523             1;