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-2020 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   1128 use vars '$VERSION';
  25         51  
  25         1383  
11             $VERSION = '3.009';
12              
13 25     25   187 use base 'Mail::Box';
  25         47  
  25         7113  
14              
15 25     25   185 use strict;
  25         51  
  25         512  
16 25     25   120 use warnings;
  25         52  
  25         919  
17              
18 25     25   12299 use filetest 'access';
  25         355  
  25         148  
19              
20 25     25   11756 use Mail::Box::File::Message;
  25         66  
  25         844  
21              
22 25     25   166 use Mail::Message::Body::Lines;
  25         52  
  25         611  
23 25     25   304 use Mail::Message::Body::File;
  25         48  
  25         608  
24 25     25   9369 use Mail::Message::Body::Delayed;
  25         61  
  25         902  
25 25     25   171 use Mail::Message::Body::Multipart;
  25         50  
  25         620  
26              
27 25     25   130 use Mail::Message::Head;
  25         51  
  25         558  
28              
29 25     25   122 use Carp;
  25         47  
  25         1289  
30 25     25   158 use File::Copy;
  25         59  
  25         1284  
31 25     25   170 use File::Spec;
  25         47  
  25         619  
32 25     25   189 use File::Basename;
  25         126  
  25         1958  
33 25     25   13886 use POSIX ':unistd_h';
  25         166223  
  25         147  
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   45681 use IO::File;
  25         62  
  25         4982  
38              
39             my $windows;
40 25     25   75687 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   2709 { my $size = shift->guessBodySize || 0;
47 946 50       41942 'Mail::Message::Body::'.($size > 100000 ? 'File' : 'Lines');
48             }
49              
50             sub init($)
51 49     49 0 140 { my ($self, $args) = @_;
52 49   66     184 $args->{folderdir} ||= $default_folder_dir;
53 49   50     389 $args->{body_type} ||= \&_default_body_type;
54 49   50     291 $args->{lock_file} ||= '--'; # to be resolved later
55              
56 49 50       275 defined $self->SUPER::init($args)
57             or return;
58              
59 49         152 my $class = ref $self;
60              
61             my $filename = $self->{MBF_filename}
62 49         168 = $self->folderToFilename
63             ( $self->name
64             , $self->folderdir
65             );
66              
67 49 100 66     888 if(-e $filename) {;} # Folder already exists
    100          
68             elsif($args->{create} && $class->create($args->{folder}, %$args)) {;}
69             else
70 1         8 { $self->log(PROGRESS =>
71             "File $filename for folder $self does not exist.");
72 1         30 return;
73             }
74              
75 48         229 $self->{MBF_policy} = $args->{write_policy};
76              
77             # Lock the folder.
78              
79 48         279 my $locker = $self->locker;
80              
81 48         287 my $lockfile = $locker->filename;
82 48 50       202 if($lockfile eq '--') # filename to be used not resolved yet
83 48         99 { my $lockdir = $filename;
84 48         423 $lockdir =~ s!/([^/]*)$!!;
85 48   50     278 my $extension = $args->{lock_extension} || '.lock';
86              
87 48 50       652 $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       208 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     193 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       536 $self->{MB_access} !~ m/r/ ? $self
    100          
109             : $self->parser ? $self
110             : undef;
111             }
112              
113              
114             sub create($@)
115 11     11 1 66 { my ($thingy, $name, %args) = @_;
116 11   33     57 my $class = ref $thingy || $thingy;
117 11   33     44 my $folderdir = $args{folderdir} || $default_folder_dir;
118 11         26 my $subext = $args{subfolder_extension}; # not always available
119 11         41 my $filename = $class->folderToFilename($name, $folderdir, $subext);
120              
121 11 50       162 return $class if -f $filename;
122              
123 11         715 my $dir = dirname $filename;
124 11 50 33     191 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     325 $class->log(ERROR => "Cannot create directory $dir for folder $name: $!"),return
130             unless -d $dir || mkdir $dir, 0755;
131              
132 11 100 66     164 $class->moveAwaySubFolder($filename, $subext)
133             if -d $filename && defined $subext;
134              
135 11         124 my $create = IO::File->new($filename, 'w');
136 11 50       1904 unless($create)
137 0         0 { $class->log(WARNING => "Cannot create folder file $name: $!");
138 0         0 return;
139             }
140              
141 11         122 $class->log(PROGRESS => "Created folder $name.");
142 11 50       170 $create->close or return;
143 11         338 $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 2464 { my $self = $_[0]; # be careful, we want to set the calling
167 49         113 undef $_[0]; # ref to undef, as the SUPER does.
168 49         91 shift;
169              
170 49         371 my $rc = $self->SUPER::close(@_);
171              
172 49 100       254 if(my $parser = delete $self->{MBF_parser}) { $parser->stop }
  38         329  
173              
174 49         3975 $rc;
175             }
176              
177              
178             sub appendMessages(@)
179 1     1 1 3 { my $class = shift;
180 1         6 my %args = @_;
181              
182             my @messages
183             = exists $args{message} ? $args{message}
184 1 50       7 : exists $args{messages} ? @{$args{messages}}
  1 50       6  
185             : return ();
186              
187 1 50       8 my $folder = $class->new(lock_type => 'NONE', @_, access => 'w+')
188             or return ();
189            
190 1         4 my $filename = $folder->filename;
191              
192 1         9 my $out = IO::File->new($filename, 'a');
193 1 50       132 unless($out)
194 0         0 { $class->log(ERROR => "Cannot append messages to folder file $filename: $!");
195 0         0 return ();
196             }
197              
198 1         5 my $msgtype = $class.'::Message';
199 1         2 my @coerced;
200              
201 1         4 foreach my $msg (@messages)
202 1 50       19 { my $coerced
    50          
203             = $msg->isa($msgtype) ? $msg
204             : $msg->can('clone') ? $msgtype->coerce($msg->clone)
205             : $msgtype->coerce($msg);
206              
207 1         40 $coerced->write($out);
208 1         5 push @coerced, $coerced;
209             }
210              
211 1         6 my $ok = $folder->close;
212 1 50 33     5 $out->close && $ok
213             or return 0;
214              
215 1         64 @coerced;
216             }
217              
218             #-------------------------------------------
219              
220              
221 175     175 1 86623 sub filename() { shift->{MBF_filename} }
222              
223             #-------------------------------------------
224              
225              
226             sub parser()
227 100     100 1 214 { my $self = shift;
228              
229             return $self->{MBF_parser}
230 100 100       460 if defined $self->{MBF_parser};
231              
232 39         223 my $source = $self->filename;
233              
234 39   50     174 my $mode = $self->{MB_access} || 'r';
235 39 100 66     278 $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       225 , $self->logSettings
244             ) or return;
245              
246 39         13328 $parser->pushSeparator('From ');
247 39         789 $parser;
248             }
249              
250             sub readMessages(@)
251 39     39 1 259 { 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         236 );
261              
262 39         255 $self->updateMessages;
263             }
264            
265              
266             sub updateMessages(@)
267 40     40 1 124 { my ($self, %args) = @_;
268 40 50       123 my $parser = $self->parser or return;
269              
270             # On a directory, simulate an empty folder with only subfolders.
271 40         178 my $filename = $self->filename;
272 40 50       743 return $self if -d $filename;
273              
274 40 100       388 if(my $last = $self->message(-1))
275 1         16 { (undef, my $end) = $last->fileLocation;
276 1         8 $parser->filePosition($end);
277             }
278              
279 40         186 my ($type, @msgopts) = $self->messageCreateOptions;
280 40         106 my $count = 0;
281              
282 40         87 while(1)
283 1304         5027 { my $message = $type->new(@msgopts);
284 1304 100       4743 last unless $message->readFromParser($parser);
285 1264         3978 $self->storeMessage($message);
286 1264         1902 $count++;
287             }
288              
289 40 100       777 $self->log(PROGRESS => "Found $count new messages in $filename")
290             if $count;
291              
292 40         1078 $self;
293             }
294              
295              
296             sub messageCreateOptions(@)
297 79     79 1 602 { my ($self, @options) = @_;
298 79 100       289 if(@options)
299 39   66     875 { ref($_) && ref($_) =~ m/^Mail::/ && weaken $_ for @options;
      66        
300 39         203 $self->{MBF_create_options} = \@options;
301             }
302            
303 79         248 @{$self->{MBF_create_options}};
  79         349  
304             }
305              
306              
307             sub moveAwaySubFolder($$)
308 1     1 1 4 { 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         105 $self;
312             }
313              
314             sub delete(@)
315 12     12 1 22 { my $self = shift;
316 12         56 $self->SUPER::delete(@_);
317 12         31 unlink $self->filename;
318             }
319              
320              
321             sub writeMessages($)
322 14     14 1 44 { my ($self, $args) = @_;
323              
324 14         61 my $filename = $self->filename;
325 14 0 33     29 if( ! @{$args->{messages}} && $self->{MB_remove_empty})
  14         60  
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       71 my $policy = exists $args->{policy} ? $args->{policy} : $self->{MBF_policy};
332 14   100     74 $policy ||= '';
333              
334 14 50       582 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       56 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         75 $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   44 { my ($self, $args) = @_;
374              
375 10         28 my $filename = $self->filename;
376 10         54 my $tmpnew = $self->tmpNewFolder($filename);
377              
378 10 50       99 my $new = IO::File->new($tmpnew, 'w') or return 0;
379 10         1953 $new->binmode;
380              
381 10 50       144 my $old = IO::File->new($filename, 'r') or return 0;
382 10         986 $old->binmode;
383              
384 10         90 my ($reprint, $kept) = (0,0);
385              
386 10         23 foreach my $message ( @{$args->{messages}} )
  10         39  
387             {
388 216         555 my $newbegin = $new->tell;
389 216         1206 my $oldbegin = $message->fileLocation;
390              
391 216 100       475 if($message->isModified)
392 35         261 { $message->write($new);
393 35 50       98 $message->moveLocation($newbegin - $oldbegin)
394             if defined $oldbegin;
395 35         52 $reprint++;
396 35         84 next;
397             }
398              
399 181         1329 my ($begin, $end) = $message->fileLocation;
400 181         624 my $need = $end-$begin;
401              
402 181         518 $old->seek($begin, 0);
403 181         2826 my $whole;
404 181         498 my $size = $old->read($whole, $need);
405              
406 181 50       2911 $self->log(ERROR => "File too short to get write message "
407             . $message->seqnr. " ($size, $need)")
408             unless $size == $need;
409              
410 181         561 $new->print($whole);
411 181 50       3077 $new->print($Mail::Message::crlf_platform ? "\r\n" : "\n");
412              
413 181         1298 $message->moveLocation($newbegin - $oldbegin);
414 181         357 $kept++;
415             }
416              
417 10         66 my $ok = $new->close;
418 10 50 33     542 $old->close && $ok
419             or return 0;
420              
421 10 50       253 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       68 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         2138 $self->log(PROGRESS => "Folder $self replaced ($kept, $reprint)");
438 10         318 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   17 { my ($self, $args) = @_;
446              
447 4         8 my @messages = @{$args->{messages}};
  4         24  
448 4         7 my $last;
449              
450 4         11 my ($msgnr, $kept) = (0, 0);
451 4         20 while(@messages)
452 87         115 { my $next = $messages[0];
453 87 100 100     152 last if $next->isModified || $next->seqnr!=$msgnr++;
454 84         118 $last = shift @messages;
455 84         147 $kept++;
456             }
457              
458 4 50 66     24 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         19 $_->body->load foreach @messages;
464              
465 4 50       410 my $mode = $^O eq 'MSWin32' ? 'a' : 'r+';
466 4         17 my $filename = $self->filename;
467 4 50       39 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       690 my $end = defined $last ? ($last->fileLocation)[1] : 0;
472              
473 4         66 $end =~ m/(.*)/; # untaint, only required by perl5.6.1
474 4         26 $end = $1;
475              
476 4 50       31 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       497 unless(@messages)
483             { # All further messages only are flagged to be deleted
484 1 50       5 $old->close or return 0;
485 1         24 $self->log(PROGRESS => "Folder $self shortened in-place ($kept kept)");
486 1         29 return 1;
487             }
488              
489             # go to the end of the truncated output file.
490 3         22 $old->seek(0, 2);
491              
492             # Print the messages which have to move.
493 3         45 my $printed = @messages;
494 3         10 foreach my $message (@messages)
495 90         184 { my $oldbegin = $message->fileLocation;
496 90         238 my $newbegin = $old->tell;
497 90         532 $message->write($old);
498 90         240 $message->moveLocation($newbegin - $oldbegin);
499             }
500              
501 3 50       37 $old->close or return 0;
502 3         300 $self->log(PROGRESS => "Folder $self updated in-place ($kept, $printed)");
503 3         111 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 32 sub tmpNewFolder($) { shift->filename . '.tmp' }
519              
520             #-------------------------------------------
521              
522              
523             1;