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   6655 use parent 'Sisimai::Lhost';
  25         54  
  25         168  
3 25     25   1669 use feature ':5.10';
  25         46  
  25         1870  
4 25     25   316 use strict;
  25         190  
  25         597  
5 25     25   116 use warnings;
  25         47  
  25         43743  
6              
7 2     2 1 1314 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 1575 my $class = shift;
16 671   100     1735 my $mhead = shift // return undef;
17 670   50     1240 my $mbody = shift // return undef;
18              
19 670 100       2152 return undef unless $mhead->{'subject'} eq 'Undelivered Mail Returned to Sender';
20 491 100       1444 return undef if $mhead->{'x-aol-ip'};
21              
22 481         760 state $indicators = __PACKAGE__->INDICATORS;
23 481         689 state $rebackbone = qr<^Content-Type:[ ](?:message/rfc822|text/rfc822-headers)>m;
24 481         668 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         5757 require Sisimai::RFC1894;
48 481         1496 require Sisimai::Address;
49 481         2018 my $fieldtable = Sisimai::RFC1894->FIELDTABLE;
50 481         982 my $permessage = {}; # (Hash) Store values of each Per-Message field
51              
52 481         1587 my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
53 481         2390 my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
54 481         884 my $readcursor = 0; # (Integer) Points the current cursor position
55 481         834 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
56 481         775 my $anotherset = {}; # (Hash) Another error information
57 481         611 my $nomessages = 0; # (Integer) Delivery report unavailable
58 481         780 my @commandset; # (Array) ``in reply to * command'' list
59 481         639 my $v = undef;
60 481         697 my $p = '';
61              
62 481         5595 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       21865 unless( $readcursor ) {
66             # Beginning of the bounce message or message/delivery-status part
67 2683 100       9661 $readcursor |= $indicators->{'deliverystatus'} if $e =~ $markingsof->{'message'};
68 2683         3680 next;
69             }
70 12473 50       19619 next unless $readcursor & $indicators->{'deliverystatus'};
71 12473 100       17957 next unless length $e;
72              
73 9340 100       17431 if( my $f = Sisimai::RFC1894->match($e) ) {
74             # $e matched with any field defined in RFC3464
75 3292 50       6232 next unless my $o = Sisimai::RFC1894->field($e);
76 3292         4675 $v = $dscontents->[-1];
77              
78 3292 100       6014 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       1788 if( $o->[0] eq 'final-recipient' ) {
82             # Final-Recipient: rfc822; kijitora@example.jp
83 432 100       1327 if( $v->{'recipient'} ) {
84             # There are multiple recipient addresses in the message body.
85 15         118 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
86 15         77 $v = $dscontents->[-1];
87             }
88 432         825 $v->{'recipient'} = $o->[2];
89 432         910 $recipients++;
90              
91             } else {
92             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
93 364         974 $v->{'alias'} = $o->[2];
94             }
95             } elsif( $o->[-1] eq 'code' ) {
96             # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown
97 432         1126 $v->{'spec'} = $o->[1];
98 432 100       1025 $v->{'spec'} = 'SMTP' if $v->{'spec'} eq 'X-POSTFIX';
99 432         980 $v->{'diagnosis'} = $o->[2];
100              
101             } else {
102             # Other DSN fields defined in RFC3464
103 2064 50       4313 next unless exists $fieldtable->{ $o->[0] };
104 2064         4168 $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
105              
106 2064 100       4384 next unless $f == 1;
107 844         2650 $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     18086 if( index($p, 'Diagnostic-Code:') == 0 && $e =~ /\A[ \t]+(.+)\z/ ) {
    100          
118             # Continued line of the value of Diagnostic-Code header
119 522         1949 $v->{'diagnosis'} .= ' '.$1;
120 522         1257 $e = 'Diagnostic-Code: '.$e;
121              
122             } elsif( $e =~ /\A(X-Postfix-Sender):[ ]*rfc822;[ ]*(.+)\z/ ) {
123             # X-Postfix-Sender: rfc822; shironeko@example.org
124 417         3151 $emailsteak->[1] .= sprintf("%s: %s\n", $1, $2);
125              
126             } else {
127             # Alternative error message and recipient
128 5109 100 100     24742 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         903 push @commandset, $1;
132 337 100       1404 $anotherset->{'diagnosis'} .= ' '.$e if $anotherset->{'diagnosis'};
133              
134             } elsif( $e =~ /\A[<]([^ ]+[@][^ ]+)[>] [(]expanded from [<](.+)[>][)]:[ \t]*(.+)\z/ ) {
135             # (expanded from ): user ...
136 6         27 $anotherset->{'recipient'} = $1;
137 6         21 $anotherset->{'alias'} = $2;
138 6         21 $anotherset->{'diagnosis'} = $3;
139              
140             } elsif( $e =~ /\A[<]([^ ]+[@][^ ]+)[>]:(.*)\z/ ) {
141             # : ...
142 421         1712 $anotherset->{'recipient'} = $1;
143 421         1115 $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         20 $nomessages = 1;
156              
157             } else {
158             # Get error message continued from the previous line
159 4340 100       8341 next unless $anotherset->{'diagnosis'};
160 1481 100       5507 $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         22597 $p = $e;
167             }
168              
169 481 100       2417 unless( $recipients ) {
170             # Fallback: get a recipient address from error messages
171 64 100 66     463 if( defined $anotherset->{'recipient'} && $anotherset->{'recipient'} ) {
172             # Set a recipient address
173 10         31 $dscontents->[-1]->{'recipient'} = $anotherset->{'recipient'};
174 10         18 $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     215 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         36 $dscontents->[-1]->{'recipient'} = Sisimai::Address->s3s4($1);
183 5         22 $recipients++;
184             }
185             }
186             }
187 481 100       1265 return undef unless $recipients;
188              
189 432         930 for my $e ( @$dscontents ) {
190             # Set default values if each value is empty.
191 447   66     1856 $e->{'lhost'} ||= $permessage->{'rhost'};
192 447   50     2476 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage;
      66        
193              
194 447 50 66     1869 if( exists $anotherset->{'diagnosis'} && $anotherset->{'diagnosis'} ) {
195             # Copy alternative error message
196 427   66     859 $e->{'diagnosis'} ||= $anotherset->{'diagnosis'};
197 427 50       1442 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         658 my $as = undef; # status
204 427         514 my $ar = undef; # replycode
205              
206 427 100 100     2043 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     1173 $as = Sisimai::SMTP::Status->find($anotherset->{'diagnosis'}) || '';
209 172 100 66     830 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         39 $e->{'status'} = $as;
212             }
213             }
214              
215 427 50 33     1192 if( $e->{'replycode'} eq '' || substr($e->{'replycode'}, -2, 2) eq '00' ) {
216             # Check the value of SMTP reply code in $anotherset
217 427   100     2231 $ar = Sisimai::SMTP::Reply->find($anotherset->{'diagnosis'}) || '';
218 427 100 66     2202 if( length($ar) > 0 && substr($ar, -2, 2) ne '00' ) {
219             # The SMTP reply code is neither an empty nor *00
220 371         831 $e->{'replycode'} = $ar;
221             }
222             }
223              
224 427 100 100     2217 if( $as || $ar && ( length($anotherset->{'diagnosis'}) > length($e->{'diagnosis'}) ) ) {
      100        
225             # Update the error message in $e->{'diagnosis'}
226 366         791 $e->{'diagnosis'} = $anotherset->{'diagnosis'};
227             }
228             }
229             }
230 447         2285 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
231 447   100     1790 $e->{'command'} = shift @commandset || '';
232 447 100 50     1691 $e->{'command'} ||= 'HELO' if $e->{'diagnosis'} =~ /refused to talk to me:/;
233 447 100 100     3141 $e->{'spec'} ||= 'SMTP' if $e->{'diagnosis'} =~ /host .+ said:/;
234             }
235 432         4196 return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
236             }
237              
238             1;
239             __END__