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 23     23   5651 use parent 'Sisimai::Lhost';
  23         39  
  23         130  
3 23     23   1363 use feature ':5.10';
  23         33  
  23         1645  
4 23     23   117 use strict;
  23         37  
  23         435  
5 23     23   88 use warnings;
  23         38  
  23         34365  
6              
7 2     2 1 949 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 647     647 1 1299 my $class = shift;
16 647   100     1564 my $mhead = shift // return undef;
17 646   50     1317 my $mbody = shift // return undef;
18              
19 646 100       1585 return undef unless $mhead->{'subject'} eq 'Undelivered Mail Returned to Sender';
20 467 100       1353 return undef if $mhead->{'x-aol-ip'};
21              
22 457         737 state $indicators = __PACKAGE__->INDICATORS;
23 457         594 state $rebackbone = qr<^Content-Type:[ ](?:message/rfc822|text/rfc822-headers)>m;
24 457         627 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 457         4731 require Sisimai::RFC1894;
48 457         1485 require Sisimai::Address;
49 457         1971 my $fieldtable = Sisimai::RFC1894->FIELDTABLE;
50 457         791 my $permessage = {}; # (Hash) Store values of each Per-Message field
51              
52 457         1564 my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
53 457         2450 my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
54 457         749 my $readcursor = 0; # (Integer) Points the current cursor position
55 457         615 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
56 457         754 my $anotherset = {}; # (Hash) Another error information
57 457         640 my $nomessages = 0; # (Integer) Delivery report unavailable
58 457         603 my @commandset; # (Array) ``in reply to * command'' list
59 457         695 my $v = undef;
60 457         1030 my $p = '';
61              
62 457         4595 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 14418 100       16771 unless( $readcursor ) {
66             # Beginning of the bounce message or message/delivery-status part
67 2611 100       7970 $readcursor |= $indicators->{'deliverystatus'} if $e =~ $markingsof->{'message'};
68 2611         2705 next;
69             }
70 11807 50       15482 next unless $readcursor & $indicators->{'deliverystatus'};
71 11807 100       13552 next unless length $e;
72              
73 8842 100       13115 if( my $f = Sisimai::RFC1894->match($e) ) {
74             # $e matched with any field defined in RFC3464
75 3106 50       4761 next unless my $o = Sisimai::RFC1894->field($e);
76 3106         3737 $v = $dscontents->[-1];
77              
78 3106 100       4891 if( $o->[-1] eq 'addr' ) {
    100          
79             # Final-Recipient: rfc822; kijitora@example.jp
80             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
81 754 100       1403 if( $o->[0] eq 'final-recipient' ) {
82             # Final-Recipient: rfc822; kijitora@example.jp
83 408 100       1332 if( $v->{'recipient'} ) {
84             # There are multiple recipient addresses in the message body.
85 15         79 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
86 15         48 $v = $dscontents->[-1];
87             }
88 408         684 $v->{'recipient'} = $o->[2];
89 408         783 $recipients++;
90              
91             } else {
92             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
93 346         989 $v->{'alias'} = $o->[2];
94             }
95             } elsif( $o->[-1] eq 'code' ) {
96             # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown
97 408         1042 $v->{'spec'} = $o->[1];
98 408 100       1048 $v->{'spec'} = 'SMTP' if $v->{'spec'} eq 'X-POSTFIX';
99 408         967 $v->{'diagnosis'} = $o->[2];
100              
101             } else {
102             # Other DSN fields defined in RFC3464
103 1944 50       3485 next unless exists $fieldtable->{ $o->[0] };
104 1944         3341 $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
105              
106 1944 100       3465 next unless $f == 1;
107 796         2415 $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 5736 100 66     14195 if( index($p, 'Diagnostic-Code:') == 0 && $e =~ /\A[ \t]+(.+)\z/ ) {
    100          
118             # Continued line of the value of Diagnostic-Code header
119 504         1483 $v->{'diagnosis'} .= ' '.$1;
120 504         994 $e = 'Diagnostic-Code: '.$e;
121              
122             } elsif( $e =~ /\A(X-Postfix-Sender):[ ]*rfc822;[ ]*(.+)\z/ ) {
123             # X-Postfix-Sender: rfc822; shironeko@example.org
124 393         2752 $emailsteak->[1] .= sprintf("%s: %s\n", $1, $2);
125              
126             } else {
127             # Alternative error message and recipient
128 4839 100 100     20731 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 313         753 push @commandset, $1;
132 313 100       1113 $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         14 $anotherset->{'alias'} = $2;
138 6         24 $anotherset->{'diagnosis'} = $3;
139              
140             } elsif( $e =~ /\A[<]([^ ]+[@][^ ]+)[>]:(.*)\z/ ) {
141             # : ...
142 397         1345 $anotherset->{'recipient'} = $1;
143 397         935 $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         15 $nomessages = 1;
156              
157             } else {
158             # Get error message continued from the previous line
159 4118 100       6645 next unless $anotherset->{'diagnosis'};
160 1403 100       4329 $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 14418         18417 $p = $e;
167             }
168              
169 457 100       1938 unless( $recipients ) {
170             # Fallback: get a recipient address from error messages
171 64 100 66     260 if( defined $anotherset->{'recipient'} && $anotherset->{'recipient'} ) {
172             # Set a recipient address
173 10         23 $dscontents->[-1]->{'recipient'} = $anotherset->{'recipient'};
174 10         17 $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     191 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         29 $dscontents->[-1]->{'recipient'} = Sisimai::Address->s3s4($1);
183 5         23 $recipients++;
184             }
185             }
186             }
187 457 100       1391 return undef unless $recipients;
188              
189 408         834 for my $e ( @$dscontents ) {
190             # Set default values if each value is empty.
191 423   66     1603 $e->{'lhost'} ||= $permessage->{'rhost'};
192 423   50     2097 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage;
      66        
193              
194 423 50 66     1832 if( exists $anotherset->{'diagnosis'} && $anotherset->{'diagnosis'} ) {
195             # Copy alternative error message
196 403   66     923 $e->{'diagnosis'} ||= $anotherset->{'diagnosis'};
197 403 50       1285 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 403         684 my $as = undef; # status
204 403         432 my $ar = undef; # replycode
205              
206 403 100 100     2051 if( $e->{'status'} eq '' || substr($e->{'status'}, -4, 4) eq '.0.0' ) {
207             # Check the value of D.S.N. in $anotherset
208 148   100     937 $as = Sisimai::SMTP::Status->find($anotherset->{'diagnosis'}) || '';
209 148 100 66     526 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         35 $e->{'status'} = $as;
212             }
213             }
214              
215 403 50 33     1297 if( $e->{'replycode'} eq '' || substr($e->{'replycode'}, -2, 2) eq '00' ) {
216             # Check the value of SMTP reply code in $anotherset
217 403   100     2226 $ar = Sisimai::SMTP::Reply->find($anotherset->{'diagnosis'}) || '';
218 403 100 66     1806 if( length($ar) > 0 && substr($ar, -2, 2) ne '00' ) {
219             # The SMTP reply code is neither an empty nor *00
220 347         738 $e->{'replycode'} = $ar;
221             }
222             }
223              
224 403 100 100     2055 if( $as || $ar && ( length($anotherset->{'diagnosis'}) > length($e->{'diagnosis'}) ) ) {
      100        
225             # Update the error message in $e->{'diagnosis'}
226 342         722 $e->{'diagnosis'} = $anotherset->{'diagnosis'};
227             }
228             }
229             }
230 423         2639 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
231 423   100     1288 $e->{'command'} = shift @commandset || '';
232 423 100 50     1333 $e->{'command'} ||= 'HELO' if $e->{'diagnosis'} =~ /refused to talk to me:/;
233 423 100 100     2371 $e->{'spec'} ||= 'SMTP' if $e->{'diagnosis'} =~ /host .+ said:/;
234             }
235 408         3269 return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
236             }
237              
238             1;
239             __END__