File Coverage

lib/Sisimai/Lhost/OpenSMTPD.pm
Criterion Covered Total %
statement 54 54 100.0
branch 19 22 86.3
condition 3 4 75.0
subroutine 6 6 100.0
pod 2 2 100.0
total 84 88 95.4


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::OpenSMTPD;
2 21     21   5405 use parent 'Sisimai::Lhost';
  21         45  
  21         109  
3 21     21   1284 use feature ':5.10';
  21         38  
  21         1741  
4 21     21   130 use strict;
  21         41  
  21         461  
5 21     21   103 use warnings;
  21         38  
  21         14312  
6              
7 2     2 1 987 sub description { 'OpenSMTPD' }
8             sub make {
9             # Detect an error from OpenSMTPD
10             # @param [Hash] mhead Message headers of a bounce email
11             # @param [String] mbody Message body of a bounce email
12             # @return [Hash] Bounce data list and message/rfc822 part
13             # @return [Undef] failed to parse or the arguments are missing
14              
15             # @since v4.0.0
16 474     474 1 1137 my $class = shift;
17 474   100     1347 my $mhead = shift // return undef;
18 473   50     1278 my $mbody = shift // return undef;
19              
20 473 100       1861 return undef unless index($mhead->{'subject'}, 'Delivery status notification') > -1;
21 57 100       266 return undef unless index($mhead->{'from'}, 'Mailer Daemon <') > -1;
22 31 50       63 return undef unless grep { rindex($_, ' (OpenSMTPD) with ') > -1 } @{ $mhead->{'received'} };
  57         214  
  31         99  
23              
24 31         102 state $indicators = __PACKAGE__->INDICATORS;
25 31         79 state $rebackbone = qr|^[ ]+Below is a copy of the original message:|m;
26 31         67 state $startingof = {
27             # http://www.openbsd.org/cgi-bin/man.cgi?query=smtpd&sektion=8
28             # opensmtpd-5.4.2p1/smtpd/
29             # bounce.c/317:#define NOTICE_INTRO \
30             # bounce.c/318: " Hi!\n\n" \
31             # bounce.c/319: " This is the MAILER-DAEMON, please DO NOT REPLY to this e-mail.\n"
32             # bounce.c/320:
33             # bounce.c/321:const char *notice_error =
34             # bounce.c/322: " An error has occurred while attempting to deliver a message for\n"
35             # bounce.c/323: " the following list of recipients:\n\n";
36             # bounce.c/324:
37             # bounce.c/325:const char *notice_warning =
38             # bounce.c/326: " A message is delayed for more than %s for the following\n"
39             # bounce.c/327: " list of recipients:\n\n";
40             # bounce.c/328:
41             # bounce.c/329:const char *notice_warning2 =
42             # bounce.c/330: " Please note that this is only a temporary failure report.\n"
43             # bounce.c/331: " The message is kept in the queue for up to %s.\n"
44             # bounce.c/332: " You DO NOT NEED to re-send the message to these recipients.\n\n";
45             # bounce.c/333:
46             # bounce.c/334:const char *notice_success =
47             # bounce.c/335: " Your message was successfully delivered to these recipients.\n\n";
48             # bounce.c/336:
49             # bounce.c/337:const char *notice_relay =
50             # bounce.c/338: " Your message was relayed to these recipients.\n\n";
51             # bounce.c/339:
52             'message' => [' This is the MAILER-DAEMON, please DO NOT REPLY to this'],
53             };
54 31         117 state $messagesof = {
55             # smtpd/queue.c:221| envelope_set_errormsg(&evp, "Envelope expired");
56             'expired' => ['Envelope expired'],
57             'hostunknown' => [
58             # smtpd/mta.c:976| relay->failstr = "Invalid domain name";
59             # smtpd/mta.c:980| relay->failstr = "Domain does not exist";
60             'Invalid domain name',
61             'Domain does not exist',
62             ],
63             # smtp/mta.c:1085| relay->failstr = "Destination seem to reject all mails";
64             'notaccept' => ['Destination seem to reject all mails'],
65             'networkerror'=> [
66             # smtpd/mta.c:972| relay->failstr = "Temporary failure in MX lookup";
67             'Address family mismatch on destination MXs',
68             'All routes to destination blocked',
69             'bad DNS lookup error code',
70             'Could not retrieve source address',
71             'Loop detected',
72             'Network error on destination MXs',
73             'No MX found for domain',
74             'No MX found for destination',
75             'No valid route to remote MX',
76             'No valid route to destination',
77             'Temporary failure in MX lookup',
78             ],
79             # smtpd/mta.c:1013| relay->failstr = "Could not retrieve credentials";
80             'securityerror' => ['Could not retrieve credentials'],
81             };
82              
83 31         133 my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
84 31         183 my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
85 31         70 my $readcursor = 0; # (Integer) Points the current cursor position
86 31         62 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
87 31         48 my $v = undef;
88              
89 31         201 for my $e ( split("\n", $emailsteak->[0]) ) {
90             # Read error messages and delivery status lines from the head of the email
91             # to the previous line of the beginning of the original message.
92 303 100       449 unless( $readcursor ) {
93             # Beginning of the bounce message or message/delivery-status part
94 103 100       345 $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0;
95 103         138 next;
96             }
97 200 50       328 next unless $readcursor & $indicators->{'deliverystatus'};
98 200 100       293 next unless length $e;
99              
100             # Hi!
101             #
102             # This is the MAILER-DAEMON, please DO NOT REPLY to this e-mail.
103             #
104             # An error has occurred while attempting to deliver a message for
105             # the following list of recipients:
106             #
107             # kijitora@example.jp: 550 5.2.2 ... Mailbox Full
108             #
109             # Below is a copy of the original message:
110 128         173 $v = $dscontents->[-1];
111              
112 128 100       440 if( $e =~ /\A([^ ]+?[@][^ ]+?):?[ ](.+)\z/ ) {
113             # kijitora@example.jp: 550 5.2.2 ... Mailbox Full
114 36 100       113 if( $v->{'recipient'} ) {
115             # There are multiple recipient addresses in the message body.
116 5         19 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
117 5         13 $v = $dscontents->[-1];
118             }
119 36         118 $v->{'recipient'} = $1;
120 36         96 $v->{'diagnosis'} = $2;
121 36         73 $recipients++;
122             }
123             }
124 31 50       106 return undef unless $recipients;
125              
126 31         71 for my $e ( @$dscontents ) {
127 36         191 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
128              
129 36         156 SESSION: for my $r ( keys %$messagesof ) {
130             # Verify each regular expression of session errors
131 144 100       174 next unless grep { index($e->{'diagnosis'}, $_) > -1 } @{ $messagesof->{ $r } };
  525         1496  
  144         273  
132 20         53 $e->{'reason'} = $r;
133 20         53 last;
134             }
135             }
136 31         172 return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
137             }
138              
139             1;
140             __END__