File Coverage

lib/Sisimai/Lhost/Postfix.pm
Criterion Covered Total %
statement 105 106 99.0
branch 68 74 91.8
condition 44 58 75.8
subroutine 6 6 100.0
pod 2 2 100.0
total 225 246 91.4


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::Postfix;
2 25     25   5783 use parent 'Sisimai::Lhost';
  25         46  
  25         181  
3 25     25   1724 use feature ':5.10';
  25         104  
  25         2071  
4 25     25   145 use strict;
  25         44  
  25         527  
5 25     25   116 use warnings;
  25         55  
  25         42962  
6              
7 2     2 1 999 sub description { 'Postfix' }
8             sub make {
9             # Parse bounce messages from Postfix
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             # @since v4.0.0
15 671     671 1 1354 my $class = shift;
16 671   100     1871 my $mhead = shift // return undef;
17 670   50     1437 my $mbody = shift // return undef;
18              
19 670 100       2253 return undef unless $mhead->{'subject'} eq 'Undelivered Mail Returned to Sender';
20 491 100       1504 return undef if $mhead->{'x-aol-ip'};
21              
22 481         978 state $indicators = __PACKAGE__->INDICATORS;
23 481         680 state $rebackbone = qr<^Content-Type:[ ](?:message/rfc822|text/rfc822-headers)>m;
24 481         663 state $markingsof = {
25             # Postfix manual - bounce(5) - http://www.postfix.org/bounce.5.html
26             'message' => qr{\A(?>
27             [ ]+The[ ](?:
28             Postfix[ ](?:
29             program\z # The Postfix program
30             |on[ ].+[ ]program\z # The Postfix on program
31             )
32             |\w+[ ]Postfix[ ]program\z # The Postfix program
33             |mail[ \t]system\z # The mail system
34             |\w+[ \t]program\z # The program
35             )
36             |This[ ]is[ ]the[ ](?:
37             Postfix[ ]program # This is the Postfix program
38             |\w+[ ]Postfix[ ]program # This is the Postfix program
39             |\w+[ ]program # This is the Postfix program
40             |mail[ ]system[ ]at[ ]host # This is the mail system at host .
41             )
42             )
43             }x,
44             # 'from'=> qr/ [(]Mail Delivery System[)]\z/,
45             };
46              
47 481         6092 require Sisimai::RFC1894;
48 481         1597 require Sisimai::Address;
49 481         2352 my $fieldtable = Sisimai::RFC1894->FIELDTABLE;
50 481         1007 my $permessage = {}; # (Hash) Store values of each Per-Message field
51              
52 481         1626 my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
53 481         2407 my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
54 481         883 my $readcursor = 0; # (Integer) Points the current cursor position
55 481         730 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
56 481         848 my $anotherset = {}; # (Hash) Another error information
57 481         722 my $nomessages = 0; # (Integer) Delivery report unavailable
58 481         759 my @commandset; # (Array) ``in reply to * command'' list
59 481         674 my $v = undef;
60 481         744 my $p = '';
61              
62 481         5555 for my $e ( split("\n", $emailsteak->[0]) ) {
63             # Read error messages and delivery status lines from the head of the email
64             # to the previous line of the beginning of the original message.
65 15156 100       20298 unless( $readcursor ) {
66             # Beginning of the bounce message or message/delivery-status part
67 2683 100       9886 $readcursor |= $indicators->{'deliverystatus'} if $e =~ $markingsof->{'message'};
68 2683         3467 next;
69             }
70 12473 50       18863 next unless $readcursor & $indicators->{'deliverystatus'};
71 12473 100       17077 next unless length $e;
72              
73 9340 100       16845 if( my $f = Sisimai::RFC1894->match($e) ) {
74             # $e matched with any field defined in RFC3464
75 3292 50       6165 next unless my $o = Sisimai::RFC1894->field($e);
76 3292         4776 $v = $dscontents->[-1];
77              
78 3292 100       7019 if( $o->[-1] eq 'addr' ) {
    100          
79             # Final-Recipient: rfc822; kijitora@example.jp
80             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
81 796 100       1803 if( $o->[0] eq 'final-recipient' ) {
82             # Final-Recipient: rfc822; kijitora@example.jp
83 432 100       1267 if( $v->{'recipient'} ) {
84             # There are multiple recipient addresses in the message body.
85 15         69 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
86 15         36 $v = $dscontents->[-1];
87             }
88 432         850 $v->{'recipient'} = $o->[2];
89 432         1104 $recipients++;
90              
91             } else {
92             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
93 364         996 $v->{'alias'} = $o->[2];
94             }
95             } elsif( $o->[-1] eq 'code' ) {
96             # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown
97 432         966 $v->{'spec'} = $o->[1];
98 432 100       1103 $v->{'spec'} = 'SMTP' if $v->{'spec'} eq 'X-POSTFIX';
99 432         1071 $v->{'diagnosis'} = $o->[2];
100              
101             } else {
102             # Other DSN fields defined in RFC3464
103 2064 50       4185 next unless exists $fieldtable->{ $o->[0] };
104 2064         4139 $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
105              
106 2064 100       4653 next unless $f == 1;
107 844         2651 $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2];
108             }
109             } else {
110             # If you do so, please include this problem report. You can
111             # delete your own text from the attached returned message.
112             #
113             # The mail system
114             #
115             # : host mx.example.co.jp[192.0.2.153] said: 550
116             # 5.1.1 ... User Unknown (in reply to RCPT TO command)
117 6048 100 66     18170 if( index($p, 'Diagnostic-Code:') == 0 && $e =~ /\A[ \t]+(.+)\z/ ) {
    100          
118             # Continued line of the value of Diagnostic-Code header
119 522         2012 $v->{'diagnosis'} .= ' '.$1;
120 522         1202 $e = 'Diagnostic-Code: '.$e;
121              
122             } elsif( $e =~ /\A(X-Postfix-Sender):[ ]*rfc822;[ ]*(.+)\z/ ) {
123             # X-Postfix-Sender: rfc822; shironeko@example.org
124 417         3056 $emailsteak->[1] .= sprintf("%s: %s\n", $1, $2);
125              
126             } else {
127             # Alternative error message and recipient
128 5109 100 100     24274 if( $e =~ /[ \t][(]in reply to (?:end of )?([A-Z]{4}).*/ ||
    100          
    100          
    100          
129             $e =~ /([A-Z]{4})[ \t]*.*command[)]\z/ ) {
130             # 5.1.1 ... User Unknown (in reply to RCPT TO
131 337         1040 push @commandset, $1;
132 337 100       1557 $anotherset->{'diagnosis'} .= ' '.$e if $anotherset->{'diagnosis'};
133              
134             } elsif( $e =~ /\A[<]([^ ]+[@][^ ]+)[>] [(]expanded from [<](.+)[>][)]:[ \t]*(.+)\z/ ) {
135             # (expanded from ): user ...
136 6         26 $anotherset->{'recipient'} = $1;
137 6         18 $anotherset->{'alias'} = $2;
138 6         21 $anotherset->{'diagnosis'} = $3;
139              
140             } elsif( $e =~ /\A[<]([^ ]+[@][^ ]+)[>]:(.*)\z/ ) {
141             # : ...
142 421         1653 $anotherset->{'recipient'} = $1;
143 421         1104 $anotherset->{'diagnosis'} = $2;
144              
145             } elsif( index($e, '--- Delivery report unavailable ---') > -1 ) {
146             # postfix-3.1.4/src/bounce/bounce_notify_util.c
147             # bounce_notify_util.c:602|if (bounce_info->log_handle == 0
148             # bounce_notify_util.c:602||| bounce_log_rewind(bounce_info->log_handle)) {
149             # bounce_notify_util.c:602|if (IS_FAILURE_TEMPLATE(bounce_info->template)) {
150             # bounce_notify_util.c:602| post_mail_fputs(bounce, "");
151             # bounce_notify_util.c:602| post_mail_fputs(bounce, "\t--- delivery report unavailable ---");
152             # bounce_notify_util.c:602| count = 1; /* xxx don't abort */
153             # bounce_notify_util.c:602|}
154             # bounce_notify_util.c:602|} else {
155 5         13 $nomessages = 1;
156              
157             } else {
158             # Get error message continued from the previous line
159 4340 100       8548 next unless $anotherset->{'diagnosis'};
160 1481 100       5355 $anotherset->{'diagnosis'} .= ' '.$e if $e =~ /\A[ \t]{4}(.+)\z/;
161             }
162             }
163             } # End of message/delivery-status
164             } continue {
165             # Save the current line for the next loop
166 15156         22154 $p = $e;
167             }
168              
169 481 100       2469 unless( $recipients ) {
170             # Fallback: get a recipient address from error messages
171 64 100 66     295 if( defined $anotherset->{'recipient'} && $anotherset->{'recipient'} ) {
172             # Set a recipient address
173 10         26 $dscontents->[-1]->{'recipient'} = $anotherset->{'recipient'};
174 10         24 $recipients++;
175              
176             } else {
177             # Get a recipient address from message/rfc822 part if the delivery
178             # report was unavailable: '--- Delivery report unavailable ---'
179 54 100 66     263 if( $nomessages && $emailsteak->[1] =~ /^To:[ ]*(.+)/m ) {
180             # Try to get a recipient address from To: field in the original
181             # message at message/rfc822 part
182 5         35 $dscontents->[-1]->{'recipient'} = Sisimai::Address->s3s4($1);
183 5         14 $recipients++;
184             }
185             }
186             }
187 481 100       1562 return undef unless $recipients;
188              
189 432         965 for my $e ( @$dscontents ) {
190             # Set default values if each value is empty.
191 447   66     2086 $e->{'lhost'} ||= $permessage->{'rhost'};
192 447   50     2780 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage;
      66        
193              
194 447 50 66     2077 if( exists $anotherset->{'diagnosis'} && $anotherset->{'diagnosis'} ) {
195             # Copy alternative error message
196 427   66     923 $e->{'diagnosis'} ||= $anotherset->{'diagnosis'};
197 427 50       1599 if( $e->{'diagnosis'} =~ /\A\d+\z/ ) {
198             # Override the value of diagnostic code message
199 0         0 $e->{'diagnosis'} = $anotherset->{'diagnosis'};
200              
201             } else {
202             # More detailed error message is in "$anotherset"
203 427         671 my $as = undef; # status
204 427         603 my $ar = undef; # replycode
205              
206 427 100 100     2200 if( $e->{'status'} eq '' || substr($e->{'status'}, -4, 4) eq '.0.0' ) {
207             # Check the value of D.S.N. in $anotherset
208 172   100     1319 $as = Sisimai::SMTP::Status->find($anotherset->{'diagnosis'}) || '';
209 172 100 66     790 if( length($as) > 0 && substr($as, -4, 4) ne '.0.0' ) {
210             # The D.S.N. is neither an empty nor *.0.0
211 10         30 $e->{'status'} = $as;
212             }
213             }
214              
215 427 50 33     1652 if( $e->{'replycode'} eq '' || substr($e->{'replycode'}, -2, 2) eq '00' ) {
216             # Check the value of SMTP reply code in $anotherset
217 427   100     2513 $ar = Sisimai::SMTP::Reply->find($anotherset->{'diagnosis'}) || '';
218 427 100 66     2241 if( length($ar) > 0 && substr($ar, -2, 2) ne '00' ) {
219             # The SMTP reply code is neither an empty nor *00
220 371         827 $e->{'replycode'} = $ar;
221             }
222             }
223              
224 427 100 100     2193 if( $as || $ar && ( length($anotherset->{'diagnosis'}) > length($e->{'diagnosis'}) ) ) {
      100        
225             # Update the error message in $e->{'diagnosis'}
226 366         995 $e->{'diagnosis'} = $anotherset->{'diagnosis'};
227             }
228             }
229             }
230 447         2460 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
231 447   100     1662 $e->{'command'} = shift @commandset || '';
232 447 100 50     1706 $e->{'command'} ||= 'HELO' if $e->{'diagnosis'} =~ /refused to talk to me:/;
233 447 100 100     3082 $e->{'spec'} ||= 'SMTP' if $e->{'diagnosis'} =~ /host .+ said:/;
234             }
235 432         4173 return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
236             }
237              
238             1;
239             __END__