File Coverage

blib/lib/Mail/Audit.pm
Criterion Covered Total %
statement 267 435 61.3
branch 91 200 45.5
condition 31 91 34.0
subroutine 35 56 62.5
pod 25 25 100.0
total 449 807 55.6


line stmt bran cond sub pod time code
1 9     9   351805 use 5.006;
  9         34  
  9         393  
2 9     9   47 use strict;
  9         17  
  9         648  
3             package Mail::Audit;
4             {
5             $Mail::Audit::VERSION = '2.228';
6             }
7             # ABSTRACT: library for creating easy mail filters
8              
9 9     9   64 use Carp ();
  9         18  
  9         128  
10 9     9   53 use File::Basename ();
  9         16  
  9         204  
11 9     9   8620 use File::HomeDir 0.61 ();
  9         73555  
  9         216  
12 9     9   67 use File::Spec ();
  9         17  
  9         135  
13 9     9   4589 use Mail::Audit::MailInternet ();
  9         28  
  9         207  
14 9     9   53 use Mail::Internet ();
  9         15  
  9         217  
15 9     9   2700 use Symbol ();
  9         2816  
  9         201  
16              
17 9     9   8822 use Sys::Hostname ();
  9         12920  
  9         246  
18              
19 9     9   61 use Fcntl ':flock';
  9         20  
  9         1424  
20              
21 9     9   54 use constant REJECTED => 100;
  9         17  
  9         603  
22 9     9   47 use constant DEFERRED => 75;
  9         19  
  9         404  
23 9     9   44 use constant DELIVERED => 0;
  9         16  
  9         77752  
24              
25              
26             sub import {
27 9     9   109 my ($pkg, @plugins) = @_;
28 9         159 for (@plugins) {
29 0         0 eval "use $pkg\::$_";
30 0 0       0 die $@ if $@;
31             }
32             }
33              
34 99     99   315 sub _log { shift->log(@_) }
35              
36             sub _get_opt {
37 23     23   37 my ($self, $arg) = @_;
38              
39 23         26 my $opt;
40              
41 23 50       113 if (ref $arg->[0] eq 'HASH') {
    100          
42 0 0       0 Carp::carp "prepending arguments is deprecated; append them instead"
43             unless @$arg == 1;
44 0         0 $opt = shift @$arg;
45             } elsif (ref $arg->[-1] eq 'HASH') {
46 13         37 $opt = pop @$arg;
47             }
48              
49 23   100     110 return $opt || {};
50             }
51              
52              
53             my $default_mime_test = sub { $_[0]->get("MIME-Version") };
54              
55             sub new {
56 9     9 1 232183 my $class = shift;
57 9         62 my %opts = @_;
58 9   33     76 my $type = ref($class) || $class;
59              
60 9   33     70 my $mime_test = (delete $opts{mime_test}) || $default_mime_test;
61              
62 9 50       125 my $self = Mail::Audit::MailInternet->new(
63             (exists $opts{data} ? $opts{data} : \*STDIN),
64             Modify => 0
65             );
66              
67             # set up logging
68 9 50       39 unless ($opts{no_log}) {
69 9         19 my $log = {};
70 9 100       53 $log->{level} = exists $opts{loglevel} ? $opts{loglevel} : 3;
71              
72 9 50       46 $log->{file} = exists $opts{log}
73             ? $opts{log}
74             : File::Spec->catfile(
75             File::HomeDir->my_home,
76             "mail-audit.log"
77             );
78              
79 9         17 my $output_fh;
80 9 100 66     517 if ($log->{file} and open $output_fh, '>>', $log->{file}) {
81 7         24 $log->{fh} = $output_fh;
82             } else {
83 2 50       8 warn "couldn't open $log->{file} to log: $!" if $log->{file};
84 2         8 $log->{fh} = \*STDERR;
85             }
86              
87             # This sucks, but the gut-construction order does, too. We need to make it
88             # saner in general. -- rjbs, 2006-06-04
89 9         82 $self->{_log} = $log;
90             }
91              
92 9         1519 $self->_log(1, "------------------------------ new run at " . localtime);
93              
94 9         72 $self->_log(2, " From: " . ($self->get("from")));
95 9         39 $self->_log(2, " To: " . ($self->get("to")));
96 9         47 $self->_log(2, "Subject: " . ($self->get("subject")));
97              
98             # do we have a MIME-Version header?
99             # if so, we subclass MIME::Entity.
100             # if not, we remain Mail::Internet, and, presumably, diminish, and go
101             # into the West.
102 9 100 66     65 if ($opts{alwaysmime} or $mime_test->($self)) {
103 1 50       7 unless ($opts{nomime}) {
104 1         3 $self->_log(3,
105             "message is MIME. MIME-Version is " . ($self->get("MIME-Version"))
106             );
107 1         3 eval {
108 1         839 require Mail::Audit::MimeEntity;
109 1         13 Mail::Audit::MimeEntity->import;
110             };
111 1 50       6 die "$@" if $@;
112 1         8 $self = Mail::Audit::MimeEntity->_autotype_new($self, $opts{mimeoptions});
113             } else {
114 0         0 $self->_log(3, "message is MIME, but 'nomime' option was set.");
115             }
116             }
117              
118 9         78 ($self->{_hostname} = Sys::Hostname::hostname) =~ s/\..*//;
119              
120 9         156 $self->{_audit_opts} = \%opts;
121 9   50     89 $self->{_audit_opts}->{noexit} ||= 0;
122 9   50     60 $self->{_audit_opts}->{interpolate_strftime} ||= 0;
123 9   50     61 $self->{_audit_opts}->{one_for_all} ||= 0;
124              
125 9         37 return $self;
126             }
127              
128             sub _emergency_mbox {
129 1     1   3 my ($self) = @_;
130              
131 1 50       6 return $self->{_audit_opts}->{emergency}
132             if exists $self->{_audit_opts}->{emergency};
133              
134 0         0 return $self->{_audit_opts}->{emergency} = $self->_default_mbox;
135             }
136              
137             sub _default_mbox {
138 0     0   0 my ($self) = @_;
139 0 0       0 return $self->{_default_mbox} if exists $self->{_default_mbox};
140              
141             # XXX: How very unixocentric of us; how can we fix this? -- rjbs, 2006-06-04
142             # It's not really broken, but it's also not very awesome.
143 0         0 my $default_mbox = $ENV{MAIL};
144              
145 0 0       0 return $default_mbox if $default_mbox;
146              
147 0         0 my $default_maildir = File::Spec->catdir(
148             File::HomeDir->my_home,
149             'Maildir'
150             );
151              
152             $default_mbox =
153             (-d File::Spec->catdir($default_maildir, 'new') ? $default_maildir : ())
154 0   0     0 || ((grep { -d $_ } qw(/var/spool/mail/ /var/mail/))[0] . getpwuid($>));
155              
156 0         0 return $self->{_default_mbox} = $default_mbox;
157             }
158              
159             # XXX: This is a test case until I have a better interface. This will make
160             # testing simpler! -- rjbs, 2006-06-04
161             sub _exit {
162 5     5   10 my ($self, $exit) = @_;
163              
164 5 50       31 return $self->{_audit_opts}->{_exit}->(@_)
165             if exists $self->{_audit_opts}->{_exit};
166              
167 0         0 exit $exit;
168             }
169              
170              
171             sub _shorthand_expand {
172             # perform ~user and %Y%m%d strftime expansion
173 6     6   89 my $self = shift;
174 6         21 my $local_opts = $self->_get_opt(\@_);
175 6         14 my @out = @_;
176              
177 6         9 my $opt = 'interpolate_strftime';
178 6 100 66     59 if (
      66        
179 1         7 ((exists $local_opts->{$opt} and $local_opts->{$opt})
180             or $self->{_audit_opts}->{$opt})
181             and grep { index($_, '%') >= 0 } @out
182             ) {
183 1         34 my @localtime = localtime;
184 1         905 require POSIX;
185 1         6620 import POSIX qw(strftime);
186 1         988 @out = map { strftime($_, @localtime) } @out;
  1         77  
187             }
188              
189 6         16 return @out = map { $self->_expand_homedir($_) } @out;
  6         37  
190             }
191              
192             sub _expand_homedir {
193 6     6   14 my ($self, $path) = @_;
194              
195 6         23 my ($user, $rest) = $path =~ m!^~(\w*)((?:[/\\]).+)?$!;
196              
197 6 100 66     57 return $path unless defined $user and defined $rest;
198 2 50       19 my $base = (length $user) ? File::HomeDir->users_home($user)
199             : File::HomeDir->my_home;
200              
201 2         92 return "$base$rest";
202             }
203              
204             sub accept {
205 5     5 1 2526 my $self = shift;
206              
207 5         31 my $local_opts = $self->_get_opt(\@_);
208              
209 5 100       28 return $self->{_audit_opts}->{accept}->(@_, $local_opts)
210             if exists $self->{_audit_opts}->{accept};
211              
212 4         21 my @files = $self->_shorthand_expand(@_, $local_opts);
213              
214 4 50       13 @files = $self->_default_mbox unless @files;
215              
216 4         7 my @actually_saved_to_files = ();
217              
218 4         19 $self->_log(2, "accepting to @files");
219              
220             # from man procmailrc:
221             # If it is a directory, the mail will be delivered to a newly created,
222             # guaranteed to be unique file named $MSGPREFIX* in the specified
223             # directory. If the mailbox name ends in "/.", then this directory is
224             # presumed to be an MH folder; i.e., procmail will use the next
225             # number it finds available. If the mailbox name ends in "/", then
226             # this directory is presumed to be a maildir folder; i.e., procmail will
227             # deliver the message to a file in a subdirectory named "tmp" and
228             # rename it to be inside a subdirectory named "new". If the mailbox is
229             # specified to be an MH folder or maildir folder, procmail will create
230             # the necessary directories if they don't exist, rather than treat the
231             # mailbox as a non-existent filename. When procmail is delivering to
232             # directories, you can specify multiple directories to deliver to
233             # (procmail will do so utilising hardlinks).
234             #
235             # for now we will support maildir and mbox delivery.
236             # MH delivery remains TODO.
237 4         20 my %accept_types = (
238             mbox => [],
239             maildir => [],
240             mh => [],
241             );
242              
243 4         8 for my $file (@files) {
244 4         22 my $mailbox_type = $self->_mailbox_type($file);
245 4         6 push @{ $accept_types{$mailbox_type} }, $file;
  4         10  
246 4         24 $self->_log(3, "$file is of type $mailbox_type");
247             }
248              
249 4         25 foreach my $accept_type (sort keys %accept_types) {
250 12 100       16 next if not @{ $accept_types{$accept_type} };
  12         36  
251 4         8 my $accept_handler = "_accept_to_$accept_type";
252 4         13 $self->_log(3,
253 4         28 "calling accept handler $accept_handler(@{$accept_types{$accept_type}})"
254             );
255 4         38 push @actually_saved_to_files,
256 4         7 $self->$accept_handler(@{ $accept_types{$accept_type} }, $local_opts);
257             }
258              
259 4 100       15 if ((my $success_count = @actually_saved_to_files) > 0) {
260 3         109 $self->_log(3,
261             "delivered successfully to $success_count destinations at " . localtime
262             );
263 3 100 33     36 unless ((exists $local_opts->{noexit} and $local_opts->{noexit})
      66        
264             or $self->{_audit_opts}->{noexit}
265             ) {
266 1         4 $self->_log(2, "Exiting with status DELIVERED = " . DELIVERED);
267 1         11 $self->_exit(DELIVERED);
268             }
269             } else {
270             # nothing got delivered, take emergency action.
271              
272 1         7 my $emergency = $self->_emergency_mbox;
273 1 50       4 if (not defined $emergency) {
274 0         0 $self->_log(0,
275             "unable to write to @files and no emergency mailbox defined; "
276             . "exiting DEFERRED"
277             );
278 0         0 warn "unable to write to @files";
279 0         0 $self->_exit(DEFERRED);
280             } else {
281 1 50       1 if (grep { $emergency eq $_ } @files) { # already tried that mailbox
  1         6  
282 0 0       0 if (@files == 1) {
283 0         0 $self->_log(0, "unable to write to @files; exiting DEFERRED");
284             } else {
285 0         0 $self->_log(0,
286             "unable to write to any of (@files), which includes the emergency mailbox; exiting DEFERRED"
287             );
288             }
289 0         0 warn "unable to write to @files";
290 0         0 $self->_exit(DEFERRED);
291             } else {
292 1         3 my $accept_type = $self->_mailbox_type($emergency);
293 1         4 my $accept_handler = "_accept_to_$accept_type";
294 1         4 @actually_saved_to_files = $self->$accept_handler($emergency);
295 1 50       5 if (not @actually_saved_to_files) {
296 0         0 $self->_log(0,
297             "unable to write to @files or to emergency mailbox $emergency either; exiting DEFERRED"
298             );
299 0         0 warn "unable to write to @files" ;
300 0         0 $self->_exit(DEFERRED);
301             } else {
302 1         7 $self->_log(0,
303             "unable to write to @files; wrote to emergency mailbox $emergency."
304             );
305             }
306             }
307             }
308             }
309 4         28 return @actually_saved_to_files;
310             }
311              
312             sub _mailbox_type {
313 5     5   8 my $self = shift;
314 5         7 my $file = shift;
315              
316 5 50       15 return 'maildir' if $file =~ m{/\z};
317 5 50       13 return 'mh' if $file =~ m{/\.\z};
318 5 100       105 return 'maildir' if -d $file;
319              
320 1         3 return 'mbox';
321             }
322              
323             sub _accept_to_mbox {
324 1     1   3 my $self = shift;
325 1         2 my @saved_to = ();
326 1         5 my $local_opts = $self->_get_opt(\@_);
327              
328 1         11 foreach my $file (@_) {
329             # auto-create the parent dir.
330 1 50       72 if (my $mkdir_error = $self->_mkdir_p(File::Basename::dirname($file))) {
331 1         4 $self->_log(0, $mkdir_error);
332 1         3 next;
333             }
334 0         0 my $error = $self->_write_message($file,
335             { need_lock => 1, need_from => 1, extra_newline => 1 });
336 0 0       0 if (not $error) { push @saved_to, $file; }
  0         0  
337 0         0 else { $self->_log(1, $error); }
338             }
339 1         4 return @saved_to;
340             }
341              
342             sub _write_message {
343 4     4   7 my $self = shift;
344 4         8 my $file = shift;
345 4   50     14 my $write_opts = shift || {};
346              
347 4 50       17 $write_opts->{need_from} = 1 if not defined $write_opts->{need_from};
348 4 50       13 $write_opts->{need_lock} = 1 if not defined $write_opts->{need_lock};
349 4 50       57 $write_opts->{extra_newline} = 0
350             if not defined $write_opts->{extra_newline};
351              
352 4         48 $self->_log(3, "writing to $file; options @{[%$write_opts]}");
  4         41  
353              
354 4         24 my $fh = Symbol::gensym;
355 4 50       377 unless (open($fh, ">>$file")) { return "Couldn't open $file: $!"; }
  0         0  
356              
357 4 50       16 if ($write_opts->{need_lock}) {
358 0         0 my $lock_error = $self->_audit_get_lock($fh, $file);
359 0 0       0 return $lock_error if $lock_error;
360             }
361 4         26 seek $fh, 0, 2;
362              
363 4 50 33     26 if (not $write_opts->{need_from} and $self->head->header->[0] =~ /^From\s/)
364             {
365 0         0 $self->_log(3, "mbox From line found, stripping because we're maildir");
366 0         0 $self->delete_header("From ");
367 0         0 $self->unescape_from();
368             }
369              
370 4 50 33     82 if ($write_opts->{need_from} and $self->head->header->[0] !~ /^From\s/) {
371 0         0 $self->_log(3, "No mbox From line, making one up.");
372 0 0       0 if (exists $ENV{UFLINE}) {
373 0         0 $self->_log(3,
374             "Looks qmail, but preline not run, prepending UFLINE, RPLINE, DTLINE");
375 0         0 print $fh $ENV{UFLINE};
376 0         0 print $fh $ENV{RPLINE};
377 0         0 print $fh $ENV{DTLINE};
378             } else {
379 0   0     0 my $from = (
380             $self->get('Return-path')
381             || $self->get('Sender')
382             || $self->get('Reply-To')
383             || 'root@localhost'
384             );
385 0         0 chomp $from;
386 0 0       0 $from = $1 if $from =~ /<(.*?)>/; # comment -> name@domain
387 0         0 $from =~ s/\s*\(.*\)\s*//; # name@domain (comment) -> name@domain
388 0         0 $from =~ s/\s+//g; # if any whitespace remains, get rid of it.
389              
390             # strip timezone.
391 0         0 (my $fromtime = localtime) =~ s/(:\d\d) \S+ (\d{4})$/$1 $2/;
392              
393 0         0 print $fh "From $from $fromtime\n";
394             }
395             }
396              
397 4         11 $self->_log(4, "printing self as mbox string.");
398 4 50       11 if ($write_opts->{need_from}) {
399 0         0 my $content = $self->as_string;
400 0         0 $content =~ s/\nFrom /\n>From /g;
401 0         0 print $fh $content;
402             } else {
403 4         27 print $fh $self->as_string;
404             }
405              
406             # extra \n added because mutt seems to like a "\n\nFrom " in mbox files
407 4 50       238 print $fh "\n" if $write_opts->{extra_newline};
408              
409 4 50       15 if ($write_opts->{need_lock}) {
410 0 0       0 flock($fh, LOCK_UN) or return "Couldn't unlock $file";
411             }
412              
413 4 50       195 close $fh or return "Couldn't close $file after writing: $!";
414 4         14 $self->_log(4, "returning success.");
415 4         17 return 0; # success
416             }
417              
418             sub _accept_to_mh {
419 0     0   0 my $self = shift;
420 0         0 my @saved_to = ();
421 0         0 my $local_opts = $self->_get_opt(\@_);
422              
423 0         0 die "_accept_to_mh not implemented";
424 0         0 return @saved_to;
425             }
426              
427             # variables for accept_to_maildir
428              
429             my $maildir_time = 0;
430             my $maildir_counter = 0;
431              
432             sub _accept_to_maildir {
433 4     4   8 my $self = shift;
434 4         8 my @saved_to = ();
435 4         12 my $local_opts = $self->_get_opt(\@_);
436              
437 4 50       21 $local_opts->{one_for_all} = exists $local_opts->{one_for_all}
438             ? $local_opts->{one_for_all}
439             : $self->{_audit_opts}->{one_for_all};
440              
441 4         17 $self->_log(3, "will write to @_");
442              
443             # since mutt won't add a lines tag to maildir messages, we'll add it here
444             # XXX: Why the nuts is this here? This should be another method, or a
445             # plugin! -- rjbs, 2006-05-30
446 4 100       11 unless (length $self->get("Lines")) {
447 2         16 my @lines = $self->body;
448 2 50 33     97 @lines = @{ $lines[0] } if @lines == 1 and ref $lines[0] eq 'ARRAY';
  2         10  
449 2         6 my $num_lines = @lines;
450 2         8 $self->head->add("Lines", $num_lines);
451 2         339 $self->_log(4, "Adding Lines: $num_lines header");
452             }
453              
454 4 100       25 if ($maildir_time != time) {
455 2         4 $maildir_time = time;
456 2         4 $maildir_counter = 0;
457             } else {
458 2         4 $maildir_counter++;
459             }
460              
461             # write the tmp file.
462             # hardlink to all the new files.
463             # unlink the temp file.
464              
465             # write the tmp file in the first writable maildir directory.
466              
467 4         6 my $tmp_path;
468 4         11 foreach my $file (my @maildirs = @_) {
469 4         10 $file =~ s/\/$//;
470 4 50       21 my $tmpdir = $local_opts->{one_for_all} ? $file : "$file/tmp";
471              
472 4         4 my $msg_file;
473 4         12 do {
474 4         19 $msg_file = join ".",
475             ($maildir_time, $$ . "_$maildir_counter", $self->{_hostname});
476 4         111 $maildir_counter++;
477             } while (-e "$tmpdir/$msg_file");
478              
479 4         11 $tmp_path = "$tmpdir/$msg_file";
480 4         15 $self->_log(3, "writing to $tmp_path");
481              
482             # auto-create the maildir.
483 4 50       12 if (
    50          
484 12         42 my $mkdir_error = $self->_mkdir_p(
485             $local_opts->{one_for_all}
486             ? ($file)
487             : map { "$file/$_" } qw(tmp new cur)
488             )
489             ) {
490 0         0 $self->_log(0, $mkdir_error);
491 0         0 next;
492             }
493              
494 4         39 my $error
495             = $self->_write_message($tmp_path, { need_from => 0, need_lock => 0 });
496              
497             # only write to the first writeable maildir
498 4 50       19 last unless $error;
499              
500 0         0 $self->_log(1, $error);
501 0         0 unlink $tmp_path;
502 0         0 $tmp_path = undef;
503             }
504              
505             # unable to write to any of the specified maildirs.
506 4 50       12 if (not $tmp_path) {
507 0         0 return 0;
508             }
509              
510             # it is now in tmp/. hardlink to all the new/ destinations.
511 4         13 foreach my $file (my @maildirs = @_) {
512 4         9 $file =~ s/\/$//;
513              
514 4         6 my $msg_file;
515 4 50       15 my $newdir = $local_opts->{one_for_all} ? $file : "$file/new";
516 4         7 $maildir_counter = 0;
517              
518 4         6 do {
519 5         24 $msg_file = join ".",
520             ($maildir_time = time, $$ . "_$maildir_counter", $self->{_hostname});
521 5         171 $maildir_counter++;
522             } while (-e File::Spec->catdir($newdir, $msg_file));
523              
524             # auto-create the maildir.
525 4 50       19 if (
    50          
526 12         71 my $mkdir_error = $self->_mkdir_p(
527             $local_opts->{one_for_all}
528             ? ($file)
529             : map { File::Spec->catdir($file, $_) } qw(tmp new cur)
530             )
531             ) {
532 0         0 $self->_log(0, $mkdir_error);
533 0         0 next;
534             }
535              
536 4         45 my $new_path = File::Spec->catfile($newdir, $msg_file);
537 4         20 $self->_log(3, "maildir: hardlinking to $new_path");
538              
539 4 50       216 if (link $tmp_path, $new_path) {
540 4         15 push @saved_to, $new_path;
541             } else {
542 0         0 require Errno;
543 0 0       0 if ($! == Errno::EXDEV()) {
544             # Invalid cross-device link, see /usr/**/include/*/errno.h
545 0         0 $self->_log(0, "Couldn't link $tmp_path to $new_path: $!");
546 0         0 $self->_log(0, "attempting direct maildir delivery to $new_path...");
547 0         0 push @saved_to, $self->_accept_to_maildir($file);
548 0         0 next;
549             } else {
550 0         0 $self->_log(0, "Couldn't link $tmp_path to $new_path: $!");
551             }
552             }
553             }
554              
555             # unlink the temp file
556 4 50       207 unlink $tmp_path or $self->_log(1, "Couldn't unlink $tmp_path: $!");
557 4         22 return @saved_to;
558             }
559              
560              
561             sub reject {
562 4     4 1 525 my $self = shift;
563              
564 4         15 my $local_opt = $self->_get_opt(\@_);
565              
566 4 100       25 return $self->{_audit_opts}->{reject}->(@_, $local_opt)
567             if exists $self->{_audit_opts}->{reject};
568              
569 3         13 $self->_log(1, "Rejecting with exitcode " . REJECTED . " and reason $_[0]");
570              
571 3         13 $self->_exit(REJECTED);
572             }
573              
574              
575             sub resend {
576 0     0 1 0 my $self = shift;
577 0         0 my $local_opts = $self->_get_opt(\@_);
578 0         0 my $rcpt = shift;
579              
580 0 0       0 $self->smtpsend(
    0          
    0          
581             To => $rcpt,
582             (exists $local_opts->{host} ? (Host => $local_opts->{host}) : ()),
583             (exists $local_opts->{port} ? (Port => $local_opts->{port}) : ()),
584             (exists $local_opts->{debug} ? (Debug => $local_opts->{debug}) : ()),
585             );
586              
587 0 0 0     0 unless (
      0        
588             (exists $local_opts->{noexit} and $local_opts->{noexit})
589             or $self->{_audit_opts}->{noexit}
590             ) {
591 0         0 $self->_log(2, "Exiting with status DELIVERED = " . DELIVERED);
592 0         0 $self->_exit(DELIVERED);
593             }
594             }
595              
596              
597             sub pipe {
598 0     0 1 0 my $self = shift;
599 0 0       0 return $self->{_audit_opts}->{pipe}->(@_)
600             if exists $self->{_audit_opts}->{pipe};
601              
602 0         0 my $local_opts = $self->_get_opt(\@_);
603 0         0 my ($command) = @_;
604              
605 0         0 my ($file) = $self->_shorthand_expand($command, $local_opts);
606 0         0 $self->_log(1, "Piping to $file");
607              
608 0         0 my $pipe = Symbol::gensym;
609 0 0       0 unless (open($pipe, "|$file")) {
610 0         0 $self->_log(0, "Couldn't open pipe $file: $!");
611 0         0 $self->accept();
612             }
613              
614 0         0 $self->print($pipe);
615 0         0 close $pipe;
616 0         0 my $status = $? >> 8;
617 0         0 $self->_log(3, "Pipe closed with status $status");
618              
619 0 0 0     0 unless ((exists $local_opts->{noexit} and $local_opts->{noexit})
      0        
620             or $self->{_audit_opts}->{noexit}
621             ) {
622 0         0 $self->_log(2, "Exiting with status DELIVERED = " . DELIVERED);
623 0         0 $self->_exit(DELIVERED);
624             }
625              
626 0         0 return $status;
627             }
628              
629              
630             sub ignore {
631 3     3 1 1052 my ($self, $reason) = @_;
632              
633 3 100       18 $self->_log(
634             1,
635             "Ignoring: " . (defined $reason ? $reason : '(no reason given)')
636             );
637              
638 3         15 my $local_opts = $self->_get_opt(\@_);
639              
640 3 100 66     49 $self->_exit(DELIVERED)
      100        
641             unless ((exists $local_opts->{noexit} and $local_opts->{noexit})
642             or $self->{_audit_opts}->{noexit});
643             }
644              
645              
646             sub _reply_recipient {
647 0     0   0 my $self = shift;
648              
649             # TODO: clean this up with Mail::Address. right now if From: <> we barf.
650 0   0     0 return ($self->get("Resent-From")
651             || $self->get("Reply-To")
652             || $self->get("From")
653             || $self->get("Sender")
654             || $self->get("Return-Path"));
655             }
656              
657             sub reply {
658 0     0 1 0 my $self = shift;
659 0         0 my %reply_opts = @_;
660 0         0 foreach my $k (keys %reply_opts) {
661 0         0 $reply_opts{ lc $k } = delete $reply_opts{$k};
662             } # lowercase option names
663              
664             # thanks to man procmailrc(1), this is ^FROM_DAEMON
665 0 0       0 if ($self->from_daemon) {
666 0 0 0     0 unless (defined $reply_opts{even_if_from_daemon}
667             and $reply_opts{even_if_from_daemon}
668             ) {
669 0         0 $self->_log(2, "message is ^FROM_DAEMON, skipping reply");
670 0         0 return "(^FROM_DAEMON, no reply)";
671             }
672             }
673              
674 0 0 0     0 if ( length $self->get("X-Loop")
675             or length $self->get("X-Loop-Detect")
676             ) {
677 0         0 return "(X-Loop header found, not replying)";
678             }
679              
680 0         0 require Mail::Mailer;
681              
682 0   0     0 my $rcpt = ($reply_opts{"to"} || $self->_reply_recipient);
683              
684 0 0       0 return if not $rcpt;
685              
686 0   0     0 my $subject = (
687             $reply_opts{"subject"}
688             || (
689             defined $self->subject
690             && length $self->subject
691             ? (
692             $self->subject !~ /\bRe:/i
693             ? "Re: " . $self->subject
694             : $self->subject
695             )
696             : "your mail"
697             )
698             );
699              
700 0         0 chomp($rcpt, $subject);
701              
702 0         0 my @references;
703 0         0 @references = (
704             defined $reply_opts{"references"}
705             ? (
706             ref($reply_opts{"references"})
707 0         0 ? map { split ' ', $_ } @{ $reply_opts{"references"} }
  0         0  
708             : split ' ', $reply_opts{"references"}
709             )
710 0 0       0 : grep { length $_ } (
    0          
711             split(' ', $self->get("References")),
712             split(' ', $self->get("Message-ID"))
713             )
714             );
715 0         0 @references = grep { /^<.*>$/ } @references;
  0         0  
716              
717 0         0 my %headers = (
718             To => $rcpt,
719             Subject => $subject,
720             );
721 0 0       0 $headers{From} = $reply_opts{from} if defined $reply_opts{from};
722 0 0       0 $headers{CC} = $reply_opts{cc} if defined $reply_opts{cc};
723 0 0       0 $headers{BCC} = $reply_opts{bcc} if defined $reply_opts{bcc};
724 0 0       0 $headers{References} = "@references" if @references;
725 0   0     0 $headers{"X-Loop"} = $reply_opts{"x-loop"} || $self->get("X-Loop") || "1";
726 0   0     0 $headers{"X-Loop-Detect"} = $self->get("X-Loop-Detect") || "1";
727              
728 0         0 my $reply = Mail::Mailer->new(qw(sendmail));
729              
730 0         0 $reply->open(\%headers);
731              
732 0 0       0 print $reply (
733             defined $reply_opts{body}
734             ? $reply_opts{body}
735             : "Your message has been received.\n");
736 0         0 $reply->close; # complete the message and send it
737              
738 0         0 $self->_log(1, "reply sent to $rcpt");
739 0         0 return $rcpt;
740             }
741              
742              
743             sub log {
744 99     99 1 160 my ($self, $priority, $what) = @_;
745 99 50       393 return unless $self->{_log};
746 99 100       308 return if $self->{_log}{level} < $priority;
747 35         59 chomp $what;
748 35         45 chomp $what;
749 35         173 my ($subroutine) = (caller(1))[3];
750 35         192 $subroutine =~ s/(.*):://;
751 35         120 my ($line) = (caller(0))[2];
752 35 50       60 print { $self->{_log}{fh} } "$line($subroutine): $what\n"
  35         259  
753             or die "couldn't write to log file: $!";
754             }
755              
756              
757             # ----------------------------------------------------------
758              
759 0     0 1 0 sub header { $_[0]->head->as_string() }
760 0     0 1 0 sub add_header { $_[0]->head->add($_[1], $_[2]); }
761 0     0 1 0 sub put_header { &add_header }
762 0     0 1 0 sub get_header { &get }
763 0     0 1 0 sub replace_header { $_[0]->head->replace($_[1], $_[2]); }
764 0     0 1 0 sub delete_header { $_[0]->head->delete($_[1]); }
765              
766             sub get {
767 47     47 1 79 my ($self, $header) = @_;
768              
769 47 100       100 if (wantarray) {
770 2         7 my @strings = $self->head->get($header);
771 2         61 chomp @strings;
772 2         8 return @strings;
773             } else {
774 45         168 my $string = $self->head->get($header);
775 45 100 66     1653 chomp($string = (defined $string && length $string) ? $string : "");
776 45         226 return $string;
777             }
778             }
779              
780             # inheriting from MIME::Entity breaks this. mengwong 20020112
781             sub tidy {
782 0     0 1 0 $_[0]->tidy_body();
783             }
784              
785             sub noexit {
786 8 100   8 1 5174 $_[0]->{_audit_opts}->{noexit} = $_[1] ? 1 : 0;
787             }
788              
789              
790             # ----------------------------------------------------------
791 0     0 1 0 sub from { $_[0]->get("From") }
792 0     0 1 0 sub to { $_[0]->get("To") }
793 2     2 1 13 sub subject { $_[0]->get("Subject") }
794 0     0 1 0 sub bcc { $_[0]->get("BCC") }
795 0     0 1 0 sub cc { $_[0]->get("CC") }
796 0     0 1 0 sub received { $_[0]->get("Received") }
797              
798             # from_mailer and from_daemon inspired by procmailrc
799             sub from_daemon {
800 0     0 1 0 my $message = shift;
801 0         0 my $head = $message->head->dup;
802 0         0 $head->unfold;
803 0 0       0 if (
804             $head->as_string =~ /(^(Mailing-List:
805             |List-ID:
806             |Precedence:.*(junk|bulk|list)
807             |To:.*Multiple recipients of
808             |(((Resent-)?(From|Sender)|X-Envelope-From):|>?From )
809             .*?\b
810             (Post(ma?(st(e?r)?|n)|office)
811             |(?-i)Mailer?(?i)
812             |sendmail
813             |daemon
814             |m(mdf|ajordomo)
815             |n?uucp
816             |LIST(SERV|proc)
817             |NETSERV
818             |o(wner|ps)
819             |(?-i)r(e(quest|sponse)|oot)(?i)
820             |b(ounce|bs\.smtp)
821             |mirror
822             |s(erv(ices?|er)|mtp(error)?|ystem)
823             |A(dmin(istrator)?|MMGR|utoanswer)
824             )\@
825             ))/imx
826             ) {
827 0         0 return $1;
828             }
829 0         0 return;
830             }
831              
832             sub from_mailer {
833 0     0 1 0 my $message = shift;
834 0         0 my $head = $message->head->dup;
835 0         0 $head->unfold;
836 0         0 __from_mailer($head->as_string);
837             }
838              
839             sub __from_mailer {
840 5     5   2250 my $header = shift;
841              
842 5 100       104 if (
843             $header =~ /
844             (^(((Resent-)?(From|Sender)
845             |X-Envelope-From):|>?From )
846             .*?\b
847             (Post(ma(st(er)?|n)|office)
848             |(?-i)Mailer?(?i)
849             |sendmail
850             |daemon
851             |mmdf
852             |n?uucp
853             |ops
854             |(?-i)r(esponse|oot)(?i)
855             |(bbs\.)?smtp(error)?
856             |s(erv(ices?|er)|ystem)|A(dmin(istrator)?|MMGR)
857             )\@
858             )/imx
859             ) {
860 3         16 return $1;
861             }
862              
863 2         6 return;
864             }
865              
866             # ----------------------------------------------------------
867             # utility functions
868             # ----------------------------------------------------------
869              
870             sub _audit_get_lock {
871 0     0   0 my $self = shift;
872 0         0 my $fh = shift;
873 0         0 my $file = shift;
874 0         0 $self->_log(4, " attempting to lock file $file");
875 0         0 for (1 .. 10) {
876 0 0       0 if (flock($fh, LOCK_EX)) {
877 0         0 $self->_log(4, " successfully locked file $file");
878 0         0 return;
879             } else {
880 0 0       0 sleep $_ and next;
881             }
882             }
883 0         0 $self->_log(1, my $errstr = "Couldn't get exclusive lock on $file");
884 0         0 return $errstr;
885             }
886              
887             sub _mkdir_p { # mkdir -p (also create parents if necessary)
888 19     19   25 my $self = shift;
889 19 50       42 return if not @_;
890 19 50       33 return if not length $_[0];
891 19         36 foreach (@_) {
892 35 100       654 next if -d $_;
893 10         31 chop while m{/$};
894 10         33 $self->_log(4, "$_ doesn't exist, creating.");
895 10 50       606 if (my $error = $self->_mkdir_p(File::Basename::dirname($_))) {
896 0         0 return $error
897             }
898 10 100       575 mkdir($_, 0755) or return "unable to mkdir $_: $!";
899             }
900 18         62 return;
901             }
902              
903              
904              
905             1;
906              
907             __END__