File Coverage

blib/lib/Mail/IspMailGate.pm
Criterion Covered Total %
statement 138 231 59.7
branch 36 114 31.5
condition 10 48 20.8
subroutine 16 20 80.0
pod 0 10 0.0
total 200 423 47.2


line stmt bran cond sub pod time code
1             # -*- perl -*-
2              
3             require 5.005;
4 12     12   5672907 use strict;
  12         52  
  12         515  
5              
6 12     12   10534 use IO::File ();
  12         113549  
  12         243  
7 12     12   14358 use IO::Tee ();
  12         60665  
  12         276  
8 12     12   11022 use Mail::IspMailGate::Parser ();
  12         44  
  12         353  
9 12     12   14768 use Net::SMTP ();
  12         6710918  
  12         395  
10 12     12   19958 use Sys::Syslog ();
  12         16321900  
  12         419  
11 12     12   127 use File::Path ();
  12         26  
  12         65783  
12              
13              
14             package Mail::IspMailGate;
15              
16             $Mail::IspMailGate::VERSION = '1.102';
17              
18              
19             package Mail::IspMailGate::SMTP;
20              
21             # Simple wrapper for Net::SMTP to make it usable for
22             # MIME::Entity->print()
23             #
24             # This relies on the assumption that the MIME-tools use only
25             # print() for output purposes!
26             #
27             @Mail::IspMailGate::SMTP::ISA = qw(Net::SMTP);
28              
29             sub print {
30 0     0   0 my($self) = shift;
31 0         0 foreach (@_) {
32 0 0       0 if (!$self->datasend($_)) {
33 0         0 return undef;
34             }
35             }
36 0         0 return 1;
37             }
38              
39              
40             package Mail::IspMailGate;
41              
42              
43             ############################################################################
44             #
45             # Name: Debug (Instance method)
46             # Error (Instance method)
47             # Fatal (Instance method)
48             #
49             # Purpose: Create logfile entries with different severity levels.
50             # The Debug method supresses output, unless the 'debug'
51             # attribute is set. The Fatal method terminates the
52             # current thread after logging the message.
53             #
54             # Inputs: $self - This instance
55             # $fmt - printf-like format string
56             # @args - arguments
57             #
58             # Result: Nothing
59             #
60             ############################################################################
61              
62             sub Debug ($$;@) {
63 195     195 0 1480 my($self, $fmt, @args) = @_;
64 195 50       959 return unless $self->{'debug'};
65 195         2829 &Sys::Syslog::syslog('debug', $fmt, @args);
66 195 50       62820 printf STDERR ("$fmt\n", @args) if ($self->{'stderr'});
67             }
68              
69             sub Error ($$;@) {
70 0     0 0 0 my($self, $fmt, @args) = @_;
71 0         0 &Sys::Syslog::syslog('err', $fmt, @args);
72 0         0 printf STDERR ("$fmt\n", @args);
73             }
74              
75             sub Fatal ($$;@) {
76 0     0 0 0 my($self, $fmt, @args) = @_;
77 0         0 Error($self, $fmt, @args);
78 0         0 exit 1;
79             }
80              
81              
82             ############################################################################
83             #
84             # Name: GetUniqueId (Instance method)
85             #
86             # Purpose: Returns a unique ID for this mail
87             #
88             # Inputs: $self - This instance
89             #
90             # Returns: ID (decimal)
91             #
92             ############################################################################
93              
94             sub TmpDir {
95 16     16 0 31 my $self = shift;
96 16 50       118 return $self->{'tmpDir'} if exists $self->{'tmpDir'};
97 0         0 $self->{'tmpDir'} = $Mail::IspMailGate::Config::config->{'tmp_dir'};
98             }
99              
100             sub GetUniqueId ($) {
101             # XXX: use attrs 'locked';
102 8     8 0 23 my $self = shift;
103              
104 8         46 my $idFile = $self->TmpDir() . "/.id";
105              
106             # Generate a unique ID for this mail
107 8         32 my $fh = Symbol::gensym();
108 8 50       636 sysopen($fh, $idFile, Fcntl::O_RDWR()|Fcntl::O_CREAT())
109             or $self->Fatal("Cannot open lock file $idFile: $!");
110 8 50       99 flock($fh, 2) or $self->Fatal("Cannot lock file $idFile: $!");
111 8         126 my $id = <$fh>;
112 8 100       33 if (!defined($id)) { $id = 0 }
  2         7  
113 8 50       46 if (++$id < 0) { $id = 1 }
  0         0  
114 8 50       62 seek($fh, 0, 0)
115             or $self->Fatal("Error while seeking to top of lock file $idFile: $!");
116 8 50       423 truncate($fh, 0)
117             or $self->Fatal("Error while truncating lock file $idFile: $!");
118 8 50       72 printf $fh ("%d\n", $id)
119             or $self->Fatal("Error while writing lock file $idFile: $!");
120 8 50       300 close($fh) or $self->Fatal("Error while closing lock file $idFile: $!");
121 8         44 $id;
122             }
123              
124              
125             ############################################################################
126             #
127             # Name: SendMimeMail (Instance method)
128             #
129             # Purpose: Send a MIME entity
130             #
131             # Inputs: $self - This instance
132             # $entity - MIME entity to send
133             # $sender - Mail sender
134             # $recipients - List of recipients
135             # $host - Delivery host
136             #
137             # Returns: Nothing
138             #
139             ############################################################################
140              
141             sub SendMimeMail ($$$$$) {
142 8     8 0 37 my($self, $entity, $sender, $recipients, $host) = @_;
143 8         21 my $cfg = $Mail::IspMailGate::Config::config;
144              
145 8 50       42 if ($self->{'noMails'}) {
146 8 50       40 if (ref($self->{'noMails'}) eq 'SCALAR') {
147 8         16 ${$self->{'noMails'}} .= $entity->stringify();
  8         120  
148             } else {
149 0         0 $entity->print(\*STDOUT);
150             }
151 8         902072 return;
152             }
153              
154 0         0 my $mailHost = $cfg->{'mail_host'};
155 0         0 my $addIspMailGate = 1;
156 0 0       0 if ($host) {
157 0         0 $addIspMailGate = 0;
158 0         0 $mailHost = $host;
159             }
160              
161 0         0 my($smtp) = Mail::IspMailGate::SMTP->new($mailHost);
162 0 0       0 if (!$smtp) {
163 0         0 $self->Fatal("Failed to connect to mail server $mailHost: $!");
164             }
165             #$smtp->debug(1);
166 0         0 my $msender = $sender;
167 0 0 0     0 if ($msender !~ /\@/ && $cfg->{'unqualified_domain'}) {
168 0         0 $msender .= $cfg->{'unqualified_domain'};
169             }
170 0 0       0 if (!$smtp->mail($sender)) {
171 0         0 $self->Fatal("Failed to pass sender to mail server $mailHost: $!");
172             }
173 0         0 my($r);
174 0         0 foreach $r (@$recipients) {
175 0 0       0 if (!$smtp->to($addIspMailGate ? "$r.ispmailgate" : $r)) {
    0          
176 0         0 $self->Fatal("Failed to pass recipient $r to mail server"
177             . " $mailHost: $!");
178             }
179             }
180 0 0       0 if (!$smtp->data()) {
181 0         0 $self->Fatal("Failed to request data mode from mail server"
182             . " $mailHost: $!");
183             }
184 0 0       0 if (!$entity->print($smtp)) {
185 0         0 $self->Fatal("Failed to write mail to mail server $mailHost");
186             }
187 0 0       0 if (!$smtp->dataend()) {
188 0         0 $self->Fatal("Failed to terminate data connection: $!");
189             }
190             }
191              
192              
193             ############################################################################
194             #
195             # Name: SendBackupFile (Instance method)
196             #
197             # Purpose: If something went wrong while parsing the mail, we do the
198             # following: Move the mail to a folder where it will be
199             # saved, send it to the recipients and tell the postmaster
200             # about the problem.
201             #
202             # Inputs: $self - This instance
203             # $id - Mail id
204             # $ifh - Backup file's file handle
205             # $fileName - Backup file's file name
206             # $sender - Sender's email address
207             # $recipients - Recipient list
208             #
209             # Returns: Nothing, exits
210             #
211             ############################################################################
212              
213             sub SendBackupFile ($$$$$$) {
214 0     0 0 0 my($self, $id, $ifh, $fileName, $sender, $recipients) = @_;
215              
216 0         0 my $cfg = $Mail::IspMailGate::Config::config;
217 0         0 my $mailHost = $cfg->{'mail_host'};
218              
219 0 0       0 if (!$ifh->seek(0, 0)) {
220 0         0 $self->Fatal("Failed to rewind backup file $fileName: $!");
221             }
222              
223 0 0       0 if ($self->{'noMails'}) {
224 0         0 my($line);
225 0         0 while (defined($line = $ifh->getline())) {
226 0 0       0 if (ref($self->{'noMails'}) eq 'SCALAR') {
227 0         0 ${$self->{'noMails'}} .= $line;
  0         0  
228             } else {
229 0         0 print $line;
230             }
231             }
232 0         0 exit 0;
233             }
234              
235 0         0 my($smtp) = Net::SMTP->new($mailHost);
236 0 0       0 if (!$smtp) {
237 0         0 $self->Fatal("Failed to connect to mail server $mailHost: $!");
238             }
239 0 0       0 if (!$smtp->mail($sender)) {
240 0         0 $self->Fatal("Failed to pass sender to mail server $mailHost: $!");
241             }
242 0         0 my($r);
243 0         0 foreach $r (@$recipients) {
244 0 0       0 if (!$smtp->to($r . ".ispmailgate")) {
245 0         0 $self->Fatal("Failed to pass recipient $r to mail server"
246             . " $mailHost: $!");
247             }
248             }
249 0 0       0 if (!$smtp->data()) {
250 0         0 $self->Fatal("Failed to request data mode from mail server"
251             . " $mailHost: $!");
252             }
253 0         0 my($line);
254 0         0 while (defined($line = $ifh->getline())) {
255 0 0       0 if (!$smtp->datasend($line)) {
256 0         0 $self->Fatal("Failed to send data to mail server $mailHost: $!");
257             }
258             }
259 0 0 0     0 if (!$smtp->dataend() || !$smtp->quit()) {
260 0         0 $self->Fatal("Failed to end data on $mailHost: $!");
261             }
262 0 0 0     0 if ($ifh->error() || !$ifh->close()) {
263 0         0 $self->Fatal("Failed to read from backup file $fileName: $!");
264             }
265              
266 0         0 my($keepDir) = $self->TmpDir() . "/keep";
267 0         0 my($keepFile) = $keepDir . "/mail$id";
268 0 0 0     0 if (! -d $keepDir && ! mkdir $keepDir, 0770) {
269 0         0 $self->Fatal("Failed to create directory $keepDir: $!");
270             }
271 0 0       0 if (!rename $fileName, $keepFile) {
272 0         0 $self->Fatal("Failed to rename backup file $fileName as",
273             " $keepFile: $!");
274             }
275              
276 0 0 0     0 $smtp->mail($sender) &&
      0        
      0        
      0        
277             $smtp->to($cfg->{'postmaster'}) &&
278             $smtp->data() &&
279             $smtp->datasend("Failed to parse mail, kept in $keepFile\n") &&
280             $smtp->dataend() &&
281             $smtp->quit();
282 0         0 exit 0;
283             }
284              
285              
286             ############################################################################
287             #
288             # Name: MakeFilterList (Instance method)
289             #
290             # Purpose: Given a recipient, find the list of filters to apply for
291             # him.
292             #
293             # Inputs: $self - This instance
294             # $sender
295             # $recipient
296             #
297             # Returns: List of filter instances
298             #
299             ############################################################################
300              
301             #
302             # Sender and Recipient may be "Joe User " or
303             # "joe.user@my.domain (Joe User)"
304             #
305             sub _CanonicAddress($) {
306 16     16   26 my($address) = @_;
307 16         62 $address =~ s/^\s+//;
308 16         37 $address =~ s/\s+$//;
309 16 50       79 if ($address =~ /\<(.*)\>/) {
    50          
310 0         0 $address = $1;
311             } elsif ($address =~ /(.*?)\s*\(.*\)/) {
312 0         0 $address = $1;
313             }
314 16         34 $address;
315             }
316              
317             sub MakeFilterList ($$) {
318 8     8 0 26 my($self, $sender, $recipient) = @_;
319 8         24 my $cfg = $Mail::IspMailGate::Config::config;
320              
321 8         33 $sender = _CanonicAddress($sender);
322 8         23 $recipient = _CanonicAddress($recipient);
323              
324 8         16 my $filters;
325              
326             my($r);
327 8         14 foreach $r (@{$cfg->{'recipients'}}) {
  8         80  
328 9         48 my($rec) = $r->{'recipient'};
329 9         21 my($sen) = $r->{'sender'};
330 9 50 66     229 if ((!$rec || $recipient =~ /$rec/) &&
      33        
      66        
331             (!$sen || $sender =~ /$sen/)) {
332 8         23 $filters = $r->{'filters'};
333 8         22 last;
334             }
335             }
336 8   33     26 $filters ||= $cfg->{'default_filter'};
337              
338             map {
339 8 50       30 if (!ref($_)) {
  8         43  
340 0         0 my $proto = $_;
341 0         0 my $c = "$_.pm";
342 0         0 $c =~ s/\:\:/\//g;
343 0         0 require $c;
344 0         0 $proto->new({});
345             } else {
346 8         39 $_
347             }
348             } @$filters;
349             }
350              
351              
352             ############################################################################
353             #
354             # Name: Main (Instance method)
355             #
356             # Purpose: Process a single mail.
357             #
358             # Inputs: $self - This instance
359             # $sender - Mail sender
360             # $recipients - Array ref to list of recipients
361             # $host - The delivery host
362             #
363             # Returns: Nothing; exits in case of error
364             #
365             ############################################################################
366              
367             sub Main($$$$) {
368 8     8 0 1670 my($self, $infh, $sender, $recipients, $host) = @_;
369 8         148 my $id = $self->GetUniqueId();
370 8         31 my $td = $self->TmpDir();
371 8         37 my $tmpDir = $self->{'tmpDir'} = "$td/$id";
372 8         41 my($backupFile) = $self->{'backupFile'} = "$td/mail$id";
373 8         20 my $cfg = $Mail::IspMailGate::Config::config;
374              
375 8 50 33     1704 if (! -d $tmpDir && !mkdir $tmpDir, 0770) {
376 0         0 $self->Fatal("Error while creating directory $tmpDir");
377             }
378 8         64 $self->Debug("Using tmpdir $tmpDir");
379              
380             # Create a new parser and let it read a mail from STDIN.
381 8         85 my($ofh) = IO::File->new($backupFile, "w+");
382 8 50       1681 if (!$ofh) {
383 0         0 $self->Fatal("Error while creating backup file $backupFile: $!");
384             }
385              
386 8         120 my($ifh) = IO::Tee->new($infh, $ofh);
387 8 50       478 if (!$ifh) {
388 0         0 $self->Fatal("Error while creating input file handle: $!");
389             }
390 8         41 $self->Debug("Using backup file $backupFile");
391              
392 8 50       35 if (!$sender) {
393 0 0       0 if (defined(my $line = $ifh->getline())) {
394 0 0       0 if ($line =~ /^\s*from\s+(\S+)\s+/i) {
395 0         0 $sender = $1;
396             } else {
397 0         0 $self->Fatal("Cannot parse From line: $line\n");
398             }
399             } else {
400 0         0 $self->Fatal("Failed to read From line from mail: $!");
401             }
402             }
403 8         44 $self->Debug("Received mail from $sender");
404              
405 8         17 $@ = '';
406 8         20 my($parser, $entity);
407 8         17 eval {
408 8         104 $parser = Mail::IspMailGate::Parser->new('output_dir' => $tmpDir);
409 8         1992 $entity = $parser->read($ifh);
410             };
411 8 50 33     15555 if ($@ || !$entity) {
412 0         0 $self->SendBackupFile($id, $ofh, $backupFile, $sender, $recipients);
413             }
414              
415             #
416             # For any recipient: Build his filter list
417             #
418 8         23 my @rFilters;
419 8         30 foreach my $r (@$recipients) {
420 8         76 $self->Debug("Making filter list for recipient $r");
421 8         65 my(@filters) = $self->MakeFilterList($sender, $r);
422 8         29 push(@rFilters, [$r, $entity, @filters]);
423 8         46 $self->Debug("Filter list is: @filters");
424             }
425              
426             #
427             # As long as there are filters in the filter lists: Find the
428             # first recipient with a filter. Pipe his entity into the filter.
429             # Replace his entity and that of all recipients with the same
430             # entity and filter with the result.
431             #
432             # This is somewhat complicated, but this way we are guaranteed,
433             # that we call any filter only once, regardless of the number
434             # of recipients.
435             #
436 8         18 my $done;
437 8         21 do {
438 13         31 $done = 1;
439 13         24 my($eOrig, $fOrig, $eNew, @rList);
440 13         25 undef $eOrig;
441 13         36 foreach my $r (@rFilters) {
442 13 100       64 if (@$r > 2) {
443 8 50       28 if (!$eOrig) {
444 8         17 $eOrig = $r->[1];
445 8         14 $fOrig = $r->[2];
446 8         36 $self->Debug("Filtering entity %s for recipient %s via"
447             . " Filter %s", $eOrig, $r->[0], $fOrig);
448 8         124 $eNew = $eOrig->dup();
449 8         13036 my $msg = eval { $fOrig->doFilter({'entity' => $eNew,
  8         111  
450             'parser' => $parser,
451             'main' => $self });
452             };
453 8 50       87 $self->Fatal($@) if $@;
454 8 100       57 if (length($msg)) {
455             # The filter returned an error. Let the postmaster
456             # know about it.
457 3         89 $eNew = MIME::Entity->build
458             ('Type' => 'multipart/mixed',
459             'From' => $cfg->{'my-mail'},
460             'To' => $cfg->{'postmaster'},
461             'Reply-To' => join(",", $sender, @rList),
462             'Subject' => 'IspMailGate error report'
463             );
464 3         20381 $eNew->attach
465             ('Data' =>
466             [ "An error occurred while processing the",
467             " attached mail. The error\n",
468             "message is:\n",
469             "\n",
470             $msg,
471             "\n",
472             "This report was created by IspMailGate,",
473             " version $cfg->{'VERSION'}.\n"
474             ]);
475 3 50       5094 $eOrig->mime_type("message/rfc822") unless
476             $eOrig->mime_type();
477 3         712 $eNew->add_part($eOrig);
478 3         47 $sender = $cfg->{'my-mail'};
479 3         17 @rList = $cfg->{'postmaster'};
480 3         20 last;
481             }
482 5         21 $done = 0;
483             }
484 5 50 33     140 if ($r->[1] eq $eOrig && $fOrig->IsEq($r->[2])) {
485 5         14 $r->[1] = $eNew;
486 5         15 splice(@$r, 2, 1);
487 5         57 $self->Debug("Replacing entity %s, recipient %s with %s",
488             $eOrig, $r->[0], $eNew);
489 5 50       33 if (@$r == 2) {
490             # No more filters, send this mail
491 5         28 $self->Debug("Delivering entity %s for recipient %s",
492             $eNew, $r->[0]);
493 5         44 push(@rList, $r->[0]);
494             }
495             }
496             }
497             }
498 13 100       1661 if (@rList) {
499 8         43 $self->Debug("Array of parts while delivering: " . ($eNew->parts()));
500 8         94 $self->SendMimeMail($eNew, $sender, \@rList, $host);
501             }
502             } until ($done);
503             }
504              
505              
506             ############################################################################
507             #
508             # Name: new
509             #
510             # Purpose: IspMailGate constructor; not yet clear for what this
511             # will be used, but it can be used (for example) to create
512             # a new thread.
513             #
514             # Inputs: $class - This class
515             # $attr - Constructor attributes
516             #
517             # Returns: IspMailGate object or undef
518             #
519             ############################################################################
520              
521             sub new ($$) {
522 8     8 0 1190090 my($class, $attr) = @_;
523 8 50       79 my($self) = $attr ? { %$attr } : {};
524 8   33     98 bless($self, (ref($class) || $class));
525 8         35 $self;
526             }
527              
528             sub DESTROY ($) {
529 7     7   197092 my($self) = @_;
530 7 50       63 if ($self->{'tmpDir'}) {
531 7         179 $self->Debug("Removing directory %s", $self->{'tmpDir'});
532 7         81154 &File::Path::rmtree($self->{'tmpDir'});
533             }
534             # if ( $self->{'backupFile'} ) {
535             # $self->Debug("Removing backup file %s", $self->{'backupFile'} );
536             # unlink $self->{'backupFile'} ;
537             # }
538             }
539              
540             1;