File Coverage

blib/lib/Mail/LocalDelivery.pm
Criterion Covered Total %
statement 189 253 74.7
branch 60 116 51.7
condition 15 31 48.3
subroutine 21 21 100.0
pod 2 2 100.0
total 287 423 67.8


line stmt bran cond sub pod time code
1 1     1   49317 use strict;
  1         2  
  1         47  
2             package Mail::LocalDelivery;
3             {
4             $Mail::LocalDelivery::VERSION = '0.305';
5             }
6             # ABSTRACT: Deliver mail to a local mailbox
7              
8 1     1   5 use Carp;
  1         3  
  1         54  
9 1     1   759 use Email::Abstract;
  1         54020  
  1         30  
10 1     1   9 use File::Basename;
  1         3  
  1         75  
11 1     1   6 use Fcntl ':flock';
  1         2  
  1         150  
12 1     1   4 use Mail::Internet;
  1         2  
  1         20  
13 1     1   2091 use Sys::Hostname; (my $HOSTNAME = hostname) =~ s/\..*//;
  1         1188  
  1         93  
14              
15             my $debuglevel = 0;
16             our $ASSUME_MSGPREFIX = 0;
17              
18 1     1   7 use constant DEFERRED => 75;
  1         2  
  1         63  
19 1     1   4 use constant REJECTED => 100;
  1         2  
  1         40  
20 1     1   17 use constant DELIVERED => 0;
  1         3  
  1         3234  
21              
22              
23             sub _debug {
24 34     34   51 my ($self, $priority, $what) = @_;
25 34 50       119 return $self->{logger}->($priority, $what) if $self->{logger};
26 0 0       0 return if $debuglevel < $priority;
27 0         0 chomp $what;
28 0         0 chomp $what;
29 0         0 my ($subroutine) = (caller(1))[3];
30 0         0 $subroutine =~ s/(.*):://;
31 0         0 my ($line) = (caller(0))[2];
32              
33 0         0 warn "$line($subroutine): $what\n";
34             }
35              
36              
37             sub new {
38 1     1 1 2194 my $class = shift;
39 1         2 my $stuff = shift;
40              
41 1         5 my %opts = @_;
42              
43 1         6 my $self = {
44             interpolate_strftime => 0,
45             one_for_all => 0,
46             %opts,
47             };
48              
49             # What sort of stuff do we have?
50 1 50 33     2 if (eval { $stuff->isa('Mail::Internet') }) {
  1 50       13  
    50          
51 0         0 $self->{email} = $stuff;
52 1         10 } elsif (my $email = eval { Email::Abstract->new($self); }) {
53 0         0 $self->{email} = $email->cast('Mail::Internet');
54             } elsif (ref $stuff eq "ARRAY" or ref $stuff eq "GLOB") {
55 1         55 $self->{email} = Mail::Internet->new($stuff);
56             } else {
57 0         0 croak
58             "Data was neither a mail object or a reference to something I understand";
59             }
60              
61 1         1645 my $default_maildir = ((getpwuid($>))[7]) . "/Maildir/";
62              
63             my $default_mbox = $ENV{MAIL}
64             || (-d File::Spec->catdir($default_maildir, 'new') ? $default_maildir : ())
65 1   0     7 || ((grep { -d $_ } qw(/var/spool/mail/ /var/mail/))[0] . getpwuid($>));
66              
67 1         3 $self->{default_mbox} = $default_mbox;
68 1   33     6 $self->{emergency} ||= $default_mbox;
69              
70 1         7 return bless $self => $class;
71             }
72              
73              
74             sub _nifty_interpolate {
75             # perform ~user and %Y%m%d strftime interpolation
76 3     3   3 my $self = shift;
77 3         6 my @out = @_;
78 3         148 my @localtime = localtime;
79              
80 3 50 33     17 if ($self->{interpolate_strftime} and grep { /%/ } @out) {
  0         0  
81 0         0 require POSIX;
82 0         0 POSIX->import(qw(strftime));
83 0         0 @out = map { strftime($_, @localtime) } @out;
  0         0  
84             }
85              
86 2         24 @out = map {
87 3         8 s{^~/} {((getpwuid($>))[7])."/"}e;
  0         0  
88 2         5 s{^~(\w+)/}{((getpwnam($1))[7])."/"}e;
  0         0  
89 2         6 $_
90             } @out;
91              
92 3         7 return @out;
93             }
94              
95             sub deliver {
96 3     3 1 3233 my $self = shift;
97              
98 3         10 my @files = $self->_nifty_interpolate(@_);
99 3 100       8 @files = ($self->{default_mbox}) if not @files;
100              
101 3         4 my @actually_saved_to_files;
102              
103 3         14 $self->_debug(2, "delivering to @files");
104              
105             # from man procmailrc:
106             # If it is a directory, the mail will be delivered to a newly created,
107             # guaranteed to be unique file named $MSGPRE- FIX* in the specified
108             # directory. If the mailbox name ends in "/.", then this directory is
109             # presumed to be an MH folder; i.e., procmail will use the next
110             # number it finds available. If the mailbox name ends in "/", then
111             # this directory is presumed to be a maildir folder; i.e., proc- mail will
112             # deliver the message to a file in a subdirectory named "tmp" and rename
113             # it to be inside a subdirectory named "new". If the mailbox is
114             # specified to be an MH folder or maildir folder, procmail will create
115             # the neces- sary directories if they don't exist, rather than treat the
116             # mailbox as a non-existent filename. When procmail is delivering to
117             # directories, you can specify multiple direc- tories to deliver to
118             # (procmail will do so utilising hardlinks).
119             #
120             # for now we will support maildir and mbox delivery.
121             # MH delivery and MSGPREFIX delivery remain todo.
122              
123 3         20 my %deliver_types = (
124             mbox => [],
125             maildir => [],
126             mh => [],
127             msgprefix => [],
128             );
129              
130 3         5 for my $file (@files) {
131 3         8 my $mailbox_type = $self->_mailbox_type($file);
132 3         3 push @{ $deliver_types{$mailbox_type} }, $file;
  3         7  
133 3         37 $self->_debug(3, "$file is of type $mailbox_type");
134             }
135              
136 3         25 foreach my $deliver_type (sort keys %deliver_types) {
137 12 100       13 next if not @{ $deliver_types{$deliver_type} };
  12         30  
138 3         14 my $deliver_handler = "_deliver_to_$deliver_type";
139 3         5 $self->_debug(3,
140             "calling deliver handler "
141 3         14 . "$deliver_handler(@{$deliver_types{$deliver_type}})"
142             );
143              
144             # Don't try to deliver to things for which we have no delivery method.
145 3 50       21 next unless $self->can($deliver_handler);
146              
147 3         11 push @actually_saved_to_files,
148 3         4 $self->$deliver_handler(@{ $deliver_types{$deliver_type} });
149             }
150              
151 3 100       9 if (@actually_saved_to_files == 0) {
152             # in this section you will often see
153             # $!=DEFERRED; die("unable to write to @files or to $emergency");
154             # we say this instead of
155             # exit DEFERRED;
156             # because we want to be able to trap the die message inside an eval {}
157             # for testing purposes.
158              
159 1         3 my $emergency = $self->{emergency};
160 1 50       2 if (not defined $emergency) {
161 0         0 return;
162             } else {
163 1 50       4 if (grep ($emergency eq $_, @files)) { # already tried that mailbox
164 0         0 return;
165             } else {
166 1         3 my $deliver_type = $self->_mailbox_type($emergency);
167 1         4 my $deliver_handler = "_deliver_to_$deliver_type";
168 1         15 @actually_saved_to_files = $self->$deliver_handler($emergency);
169 1 50       5 return if not @actually_saved_to_files;
170             }
171             }
172             }
173 3         14 return @actually_saved_to_files;
174             }
175              
176             sub _mailbox_type {
177 4     4   6 my $self = shift;
178 4         5 my $file = shift;
179              
180 4 50       10 return 'maildir' if $file =~ /\/$/;
181 4 50       10 return 'mh' if $file =~ /\/\.$/;
182              
183 4 100       63 if (-d $file) {
184 2 50 33     45 return 'maildir' if (-d "$file/tmp" and -d "$file/new");
185 2 50       7 if (exists($self->{ASSUME_MSGPREFIX})) {
186 0 0       0 return 'msgprefix' if $self->{ASSUME_MSGPREFIX};
187 0         0 return "maildir";
188             }
189 2 50       5 return 'msgprefix' if $ASSUME_MSGPREFIX;
190 2         6 return 'maildir';
191             }
192 2         5 return 'mbox';
193             }
194              
195             sub _get_opt {
196 2     2   3 my ($self, $arg) = @_;
197              
198 2         3 my $opt;
199              
200 2 50       20 if (ref $arg->[0] eq 'HASH') {
    50          
201 0 0       0 Carp::carp "prepending arguments is deprecated; append them instead"
202             unless @$arg == 1;
203 0         0 $opt = shift @$arg;
204             } elsif (ref $arg->[-1] eq 'HASH') {
205 0         0 $opt = pop @$arg;
206             }
207              
208 2   50     12 return $opt || {};
209             }
210              
211             sub _deliver_to_mbox {
212 2     2   3 my $self = shift;
213 2         6 my $opt = $self->_get_opt(\@_);
214              
215 2         4 my @saved_to = ();
216              
217 2         4 foreach my $file (@_) {
218             # auto-create the parent dir.
219 2 100       68 if (my $mkdir_error = $self->_mkdir_p(dirname($file))) {
220 1         3 $self->_debug(0, $mkdir_error);
221 1         4 next;
222             }
223              
224 1         6 my $error = $self->_write_message(
225             $file,
226             {
227             need_lock => 1,
228             need_from => 1,
229             extra_newline => 1
230             }
231             );
232              
233 1 50       5 if (not $error) {
234 1         4 push @saved_to, $file;
235             } else {
236 0         0 $self->_debug(1, $error);
237             }
238             }
239              
240 2         8 return @saved_to;
241             }
242              
243             sub _write_message {
244 3     3   5 my $self = shift;
245 3         5 my $file = shift;
246 3   50     7 my $write_opts = shift || {};
247              
248 3 50       10 $write_opts->{need_from} = 1 if not defined $write_opts->{need_from};
249 3 50       7 $write_opts->{need_lock} = 1 if not defined $write_opts->{need_lock};
250 3 100       9 $write_opts->{extra_newline} = 0 if not defined $write_opts->{extra_newline};
251              
252 3         7 $self->_debug(3, "writing to $file; options @{[%$write_opts]}");
  3         20  
253              
254 3 50       556 unless (open(FH, ">>$file")) {
255 0         0 return "Couldn't open $file: $!";
256             }
257              
258 3 100       10 if ($write_opts->{need_lock}) {
259 1         5 my $lock_error = $self->_audit_get_lock(\*FH, $file);
260 1 50       3 return $lock_error if $lock_error;
261             }
262              
263 3         21 seek FH, 0, 2;
264              
265 3 50 66     23 if (
266             not $write_opts->{need_from}
267             and $self->{email}->head->header->[0] =~ /^From\s/
268             ) {
269 0         0 $self->_debug(3, "mbox From line found, stripping because we're maildir");
270 0         0 $self->{email}->head->delete("From ");
271 0         0 $self->{email}->unescape_from;
272             }
273              
274 3 100 66     272 if (
275             $write_opts->{need_from}
276             and $self->{email}->head->header->[0] !~ /^From\s/
277             ) {
278 1         128 $self->_debug(3, "No mbox From line, making one up.");
279 1 50       5 if (exists $ENV{UFLINE}) {
280 0         0 $self->_debug(
281             3,
282             "Looks qmail, but preline not run, prepending UFLINE, RPLINE, DTLINE"
283             );
284 0         0 print FH $ENV{UFLINE};
285 0         0 print FH $ENV{RPLINE};
286 0         0 print FH $ENV{DTLINE};
287             } else {
288 1   50     5 my $from = (
289             $self->{email}->get('Return-path')
290             || $self->{email}->get('Sender')
291             || $self->{email}->get('Reply-To')
292             ||
293              
294             # It might not be insane to include From header here.
295             # -- rjbs, 2006-07-25
296             'root@localhost'
297             );
298 1         86 chomp $from;
299              
300             # comment -> email@address
301 1 50       4 $from = $1 if $from =~ /<(.*?)>/;
302              
303             # email@address (comment) -> email@address
304 1         35 $from =~ s/\s*\(.*\)\s*//;
305              
306             # if any whitespace remains, get rid of it.
307 1         3 $from =~ s/\s+//g;
308              
309             # strip timezone.
310 1         29 (my $fromtime = localtime) =~ s/(:\d\d) \S+ (\d{4})$/$1 $2/;
311              
312 1         14 print FH "From $from $fromtime\n";
313             }
314             }
315              
316 3         8 $self->_debug(4, "printing self as mbox string.");
317 3 50       16 print FH $self->{email}->as_string or return "couldn't write to $file: $!";
318              
319             # extra \n added because mutt seems to like a "\n\nFrom " in mbox files
320 3 50 100     97 print FH "\n"
321             if $write_opts->{extra_newline}
322             or return "couldn't write to $file: $!";
323              
324 1 50       3 if ($write_opts->{need_lock}) {
325 1 50       38 flock(FH, LOCK_UN) or return "Couldn't unlock $file";
326             }
327              
328 1 50       19 close FH or return "Couldn't close $file after writing: $!";
329 1         4 $self->_debug(4, "returning success.");
330 1         4 return 0; # success
331             }
332              
333             # ----------------------------------------------------------
334             # NOT IMPLEMENTED
335             # ----------------------------------------------------------
336              
337             #sub _deliver_to_mh { my $self = shift; my @saved_to=(); }
338             #sub _deliver_to_msgprefix { my $self = shift; my @saved_to=(); }
339              
340             # variables for _deliver_to_maildir
341              
342             my $maildir_time = 0;
343             my $maildir_counter = 0;
344              
345             sub _deliver_to_maildir {
346 2     2   4 my $self = shift;
347 2         4 my @saved_to = ();
348              
349 2         9 $self->_debug(3, "will write to @_");
350              
351             # since mutt won't add a lines tag to maildir messages, we'll add it here
352 2 100 100     16 unless (length($self->{email}->get("Lines") || '')) {
353 1         46 my $num_lines = @{ $self->{email}->body };
  1         4  
354 1         16 $self->{email}->head->add("Lines", $num_lines);
355 1         126 $self->_debug(4, "Adding Lines: $num_lines header");
356             }
357              
358 2 100       70 if ($maildir_time != time) {
359 1         2 $maildir_time = time;
360 1         1 $maildir_counter = 0;
361             } else {
362 1         2 $maildir_counter++;
363             }
364              
365             # write the tmp file.
366             # hardlink to all the new files.
367             # unlink the temp file.
368              
369             # write the tmp file in the first writable maildir directory.
370              
371 2         3 my $tmp_path;
372 2         6 foreach my $file (my @maildirs = @_) {
373              
374 2         4 $file =~ s/\/$//;
375 2 50       7 my $tmpdir = $self->{one_for_all} ? $file : "$file/tmp";
376              
377 2         3 my $msg_file;
378 2         3 do {
379 2         9 $msg_file = join ".",
380             ($maildir_time, $$ . "_$maildir_counter", $HOSTNAME);
381 2         42 $maildir_counter++;
382             } while (-e "$tmpdir/$msg_file");
383              
384 2         5 $tmp_path = "$tmpdir/$msg_file";
385 2         8 $self->_debug(3, "writing to $tmp_path");
386              
387             # auto-create the maildir.
388 2 50       10 if (
    50          
389 6         20 my $mkdir_error = $self->_mkdir_p(
390             $self->{one_for_all}
391             ? ($file)
392             : map { "$file/$_" } qw(tmp new cur)
393             )
394             )
395             {
396 0         0 $self->_debug(0, $mkdir_error);
397 0         0 next;
398             }
399              
400 2         12 my $error = $self->_write_message(
401             $tmp_path,
402             {
403             need_from => 0,
404             need_lock => 0
405             }
406             );
407              
408 2 50       8 last unless $error; # only write to the first writeable maildir
409              
410 2         4 $self->_debug(1, $error);
411 2         98 unlink $tmp_path;
412 2         3 $tmp_path = undef;
413 2         6 next;
414             }
415              
416 2 50       6 if (not $tmp_path) {
417 2         7 return 0;
418             } # unable to write to any of the specified maildirs.
419              
420             #
421             # it is now in tmp/. hardlink to all the new/ destinations.
422             #
423              
424 0         0 foreach my $file (my @maildirs = @_) {
425 0         0 $file =~ s/\/$//;
426              
427 0         0 my $msg_file;
428 0 0       0 my $newdir = $self->{one_for_all} ? $file : "$file/new";
429 0         0 $maildir_counter = 0;
430 0         0 do {
431 0         0 $msg_file = join ".",
432             ($maildir_time = time, $$ . "_$maildir_counter", $HOSTNAME);
433 0         0 $maildir_counter++;
434             } while (-e "$newdir/$msg_file");
435              
436             # auto-create the maildir.
437 0 0       0 if (
    0          
438 0         0 my $mkdir_error = $self->_mkdir_p(
439             $self->{one_for_all}
440             ? ($file)
441             : map { "$file/$_" } qw(tmp new cur)
442             )
443             )
444             {
445 0         0 $self->_debug(0, $mkdir_error);
446 0         0 next;
447             }
448              
449 0         0 my $new_path = "$newdir/$msg_file";
450 0         0 $self->_debug(3, "maildir: hardlinking to $new_path");
451              
452 0 0       0 if (link $tmp_path, $new_path) { push @saved_to, $new_path; }
  0         0  
453             else {
454 0         0 require Errno;
455 0         0 import Errno qw(EXDEV);
456 0 0       0 if ($! == &EXDEV)
457             { # Invalid cross-device link, see /usr/**/include/*/errno.h
458 0         0 $self->_debug(0, "Couldn't link $tmp_path to $new_path: $!");
459 0         0 $self->_debug(0, "attempting direct maildir delivery to $new_path...");
460 0         0 push @saved_to, $self->_deliver_to_maildir($file);
461 0         0 next;
462             } else {
463 0         0 $self->_debug(0, "Couldn't link $tmp_path to $new_path: $!");
464             }
465             }
466             }
467              
468             # unlink the temp file
469 0 0       0 unlink $tmp_path or $self->_debug(1, "Couldn't unlink $tmp_path: $!");
470 0         0 return @saved_to;
471             }
472              
473             sub _audit_get_lock {
474 1     1   2 my $self = shift;
475 1         2 my $FH = shift;
476 1         2 my $file = shift;
477 1         4 $self->_debug(4, " attempting to lock file $file");
478              
479 1         4 for (1 .. 10) {
480 1 50       12 if (flock($FH, LOCK_EX)) {
481 1         4 $self->_debug(4, " successfully locked file $file");
482 1         4 return;
483             } else {
484 0 0       0 sleep $_ and next;
485             }
486             }
487              
488 0         0 $self->_debug(1, my $errstr = "Couldn't get exclusive lock on $file");
489 0         0 return $errstr;
490             }
491              
492             sub _mkdir_p {
493 4     4   7 my $self = shift;
494 4 50       11 return unless @_;
495              
496 4         6 foreach my $path (@_) {
497 8 100       140 next if -d $path;
498 7         28 $self->_debug(4, "$path doesn't exist, creating.");
499 7         23 eval { File::Path::mkpath($path, 0, 0755) };
  7         1193  
500 7 100       23 return $@ if $@;
501             }
502              
503 3         12 return;
504             }
505              
506             1;
507              
508             __END__