File Coverage

lib/Sisimai/Lhost/Postfix.pm
Criterion Covered Total %
statement 141 144 97.9
branch 94 102 92.1
condition 47 66 71.2
subroutine 6 6 100.0
pod 2 2 100.0
total 290 320 90.6


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::Postfix;
2 47     47   5741 use parent 'Sisimai::Lhost';
  47         98  
  47         388  
3 47     47   3992 use v5.26;
  47         249  
4 47     47   282 use strict;
  47         125  
  47         1454  
5 47     47   246 use warnings;
  47         83  
  47         114161  
6              
7 1     1 1 4 sub description { 'Postfix: https://www.postfix.org/' }
8             sub inquire {
9             # Decode 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 decode or the arguments are missing
14             # @since v4.0.0
15 1458     1458 1 4809 my $class = shift;
16 1458   100     5341 my $mhead = shift // return undef;
17 1457   100     4105 my $mbody = shift // return undef;
18 1456         2626 my $match = 0;
19              
20 1456 100       6201 if( index($mhead->{'subject'}, 'SMTP server: errors from ') > 0 ) {
21             # src/smtpd/smtpd_chat.c:|337: post_mail_fprintf(notice, "Subject: %s SMTP server: errors from %s",
22             # src/smtpd/smtpd_chat.c:|338: var_mail_name, state->namaddr);
23 5         10 $match = 2;
24              
25             } else {
26             # Subject: Undelivered Mail Returned to Sender
27 1451 100       6282 $match = 1 if $mhead->{'subject'} eq 'Undelivered Mail Returned to Sender';
28             }
29 1456 100 100     7919 return undef if $match == 0 || $mhead->{'x-aol-ip'};
30              
31 620         3445 require Sisimai::RFC1123;
32 620         2519 require Sisimai::SMTP::Reply;
33 620         5199 require Sisimai::SMTP::Status;
34 620         2249 require Sisimai::SMTP::Command;
35 620         1577 state $indicators = __PACKAGE__->INDICATORS;
36 620         1379 state $boundaries = ['Content-Type: message/rfc822', 'Content-Type: text/rfc822-headers'];
37 620         1652 state $startingof = {
38             # Postfix manual - bounce(5) - http://www.postfix.org/bounce.5.html
39             'message' => [
40             ['The ', 'Postfix '], # The Postfix program, The Postfix on program
41             ['The ', 'mail system'], # The mail system
42             ['The ', 'program'], # The pogram
43             ['This is the', 'Postfix'], # This is the Postfix program
44             ['This is the', 'mail system'], # This is the mail system at host
45             ],
46             };
47              
48 620         1298 my $permessage = {}; # (Hash) Store values of each Per-Message field
49 620         3694 my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $v = undef;
  620         1418  
50 620         4402 my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries);
51 620         1458 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
52 620         1580 my $anotherset = {}; # (Hash) Another error information
53 620         1227 my $nomessages = 0; # (Integer) Delivery report unavailable
54 620         1103 my @commandset; # (Array) ``in reply to * command'' list
55 620         1242 my $p = '';
56              
57 620 100       2485 if( $match == 2 ) {
58             # The message body starts with 'Transcript of session follows.'
59 5         1098 require Sisimai::SMTP::Transcript;
60 5   50     59 my $transcript = Sisimai::SMTP::Transcript->rise($emailparts->[0], 'In:', 'Out:') || return undef;
61 5 50       20 return undef if scalar @$transcript == 0;
62              
63 5         12 for my $e ( @$transcript ) {
64             # Pick email addresses, error messages, and the last SMTP command.
65 40   66     65 $v ||= $dscontents->[-1];
66 40         64 $p = $e->{'response'};
67              
68 40 100 66     138 if( $e->{'command'} eq 'EHLO' || $e->{'command'} eq 'HELO' ) {
    100          
    100          
69             # Use the argument of EHLO/HELO command as a value of "lhost"
70 5         14 $v->{'lhost'} = $e->{'argument'};
71              
72             } elsif( $e->{'command'} eq 'MAIL' ) {
73             # Set the argument of "MAIL" command to pseudo To: header of the original message
74 5 50       49 $emailparts->[1] .= sprintf("To: %s\n", $e->{'argument'}) unless length $emailparts->[1];
75              
76             } elsif( $e->{'command'} eq 'RCPT' ) {
77             # RCPT TO: <...>
78 5 50       39 if( $v->{'recipient'} ) {
79             # There are multiple recipient addresses in the transcript of session
80 0         0 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
81 0         0 $v = $dscontents->[-1];
82             }
83 5         13 $v->{'recipient'} = $e->{'argument'};
84 5         7 $recipients++;
85             }
86 40 100       129 next if int($p->{'reply'}) < 400;
87              
88 5         11 push @commandset, $e->{'command'};
89 5   33     64 $v->{'diagnosis'} ||= join ' ', $p->{'text'}->@*;
90 5   33     53 $v->{'replycode'} ||= $p->{'reply'};
91 5   33     33 $v->{'status'} ||= $p->{'status'};
92             }
93             } else {
94 615         5814 my $fieldtable = Sisimai::RFC1894->FIELDTABLE;
95 615         1217 my $readcursor = 0; # (Integer) Points the current cursor position
96              
97             # The message body is a general bounce mail message of Postfix
98 615         8542 for my $e ( split("\n", $emailparts->[0]) ) {
99             # Read error messages and delivery status lines from the head of the email to the previous
100             # line of the beginning of the original message.
101 19708 100       44143 unless( $readcursor ) {
102             # Beginning of the bounce message or message/delivery-status part
103 3204 100       6435 $readcursor |= $indicators->{'deliverystatus'} if grep { Sisimai::String->aligned(\$e, $_) } $startingof->{'message'}->@*;
  16020         40547  
104 3204         4874 next;
105             }
106 16504 100 66     61739 next if ($readcursor & $indicators->{'deliverystatus'}) == 0 || $e eq "";
107              
108 12463 100       38256 if( my $f = Sisimai::RFC1894->match($e) ) {
109             # $e matched with any field defined in RFC3464
110 4310 50       12464 next unless my $o = Sisimai::RFC1894->field($e);
111 4310         12080 $v = $dscontents->[-1];
112              
113 4310 100       11439 if( $o->[3] eq 'addr' ) {
    100          
114             # Final-Recipient: rfc822; kijitora@example.jp
115             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
116 1052 100       4824 if( Sisimai::Address->is_emailaddress($o->[2]) ) {
117             # The email address is a valid email address, avoid an email address without a
118             # valid domain part such as "neko@mailhost".
119 1047 100       3436 if( $o->[0] eq 'final-recipient' ) {
120             # Final-Recipient: rfc822; kijitora@example.jp
121 555 100       2011 if( $v->{'recipient'} ) {
122             # There are multiple recipient addresses in the message body.
123 15         98 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
124 15         41 $v = $dscontents->[-1];
125             }
126 555         1310 $v->{'recipient'} = $o->[2];
127 555         1753 $recipients++;
128              
129             } else {
130             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
131 492         2148 $v->{'alias'} = $o->[2];
132             }
133             }
134             } elsif( $o->[3] eq 'code' ) {
135             # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown
136 560         1590 $v->{'spec'} = $o->[1];
137 560 100       2137 $v->{'spec'} = 'SMTP' if uc $v->{'spec'} eq 'X-POSTFIX';
138 560         2185 $v->{'diagnosis'} = $o->[2];
139              
140             } else {
141             # Other DSN fields defined in RFC3464
142 2698 50       7015 next unless exists $fieldtable->{ $o->[0] };
143 2698 100 100     12101 next if $o->[3] eq "host" && Sisimai::RFC1123->is_internethost($o->[2]) == 0;
144 2682         9256 $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
145              
146 2682 100       7801 next unless $f == 1;
147 1089         5038 $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2];
148             }
149             } else {
150             # If you do so, please include this problem report. You can
151             # delete your own text from the attached returned message.
152             #
153             # The mail system
154             #
155             # : host mx.example.co.jp[192.0.2.153] said: 550
156             # 5.1.1 ... User Unknown (in reply to RCPT TO command)
157 8153 100 66     39070 if( index($p, 'Diagnostic-Code:') == 0 && index($e, ' ') > -1 ) {
    100          
158             # Continued line of the value of Diagnostic-Code header
159 821         2873 $v->{'diagnosis'} .= ' '.Sisimai::String->sweep($e);
160 821         2246 $e = 'Diagnostic-Code: '.$e;
161              
162             } elsif( Sisimai::String->aligned(\$e, ['X-Postfix-Sender:', 'rfc822;', '@']) ) {
163             # X-Postfix-Sender: rfc822; shironeko@example.org
164 523         4596 $emailparts->[1] .= sprintf("X-Postfix-Sender: %s\n", substr($e, index($e, ';') + 1,));
165              
166             } else {
167             # Alternative error message and recipient
168 6809 100 100     38790 if( index($e, ' (in reply to ') > -1 || index($e, 'command)') > -1 ) {
    100 100        
    100          
    100          
169             # 5.1.1 ... User Unknown (in reply to RCPT TO
170 603 100       5172 my $cv = Sisimai::SMTP::Command->find($e); push @commandset, $cv if $cv;
  603         2194  
171 603 100       3378 $anotherset->{'diagnosis'} .= ' '.$e if $anotherset->{'diagnosis'};
172              
173             } elsif( Sisimai::String->aligned(\$e, ['<', '@', '>', '(expanded from <', '):']) ) {
174             # (expanded from ): user ...
175 40         104 my $p1 = index($e, '> ');
176 40         114 my $p2 = index($e, '(expanded from ', $p1);
177 40         3503 my $p3 = index($e, '>): ', $p2 + 14);
178 40         500 $anotherset->{'recipient'} = Sisimai::Address->s3s4(substr($e, 0, $p1));
179 40         261 $anotherset->{'alias'} = Sisimai::Address->s3s4(substr($e, $p2 + 15, $p3 - $p2 - 15));
180 40         214 $anotherset->{'diagnosis'} = substr($e, $p3 + 3,);
181              
182             } elsif( index($e, '<') == 0 && Sisimai::String->aligned(\$e, ['<', '@', '>:']) ) {
183             # : ...
184 509         5760 $anotherset->{'recipient'} = Sisimai::Address->s3s4(substr($e, 0, index($e, '>')));
185 509         2967 $anotherset->{'diagnosis'} = substr($e, index($e, '>:') + 2,);
186              
187             } elsif( index($e, '--- Delivery report unavailable ---') > -1 ) {
188             # postfix-3.1.4/src/bounce/bounce_notify_util.c
189             # bounce_notify_util.c:602|if (bounce_info->log_handle == 0
190             # bounce_notify_util.c:602||| bounce_log_rewind(bounce_info->log_handle)) {
191             # bounce_notify_util.c:602|if (IS_FAILURE_TEMPLATE(bounce_info->template)) {
192             # bounce_notify_util.c:602| post_mail_fputs(bounce, "");
193             # bounce_notify_util.c:602| post_mail_fputs(bounce, "\t--- delivery report unavailable ---");
194             # bounce_notify_util.c:602| count = 1; /* xxx don't abort */
195             # bounce_notify_util.c:602|}
196             # bounce_notify_util.c:602|} else {
197 5         14 $nomessages = 1;
198              
199             } else {
200             # Get an error message continued from the previous line
201 5652 100       12924 next unless $anotherset->{'diagnosis'};
202 1996 100       7001 $anotherset->{'diagnosis'} .= ' '.substr($e, 4,) if index($e, ' ') == 0;
203             }
204             }
205             } # End of message/delivery-status
206             } continue {
207             # Save the current line for the next loop
208 19708         47076 $p = $e;
209             }
210             } # End of for()
211              
212 620 100       5270 unless( $recipients ) {
213             # Fallback: get a recipient address from error messages
214 75         233 for my $e ( 'recipient', 'alias' ) {
215             # Set a valid recipient address picked from $anotherset
216 140 100       1121 next unless Sisimai::Address->is_emailaddress($anotherset->{ $e });
217 15         82 $dscontents->[-1]->{'recipient'} = $anotherset->{ $e };
218 15         29 $recipients++;
219 15         31 last;
220             }
221              
222 75 100       251 if( $recipients == 0 ) {
223             # Get a recipient address from message/rfc822 part if the delivery report was unavailable:
224             # '--- Delivery report unavailable ---'
225 60         260 my $p1 = index($emailparts->[1], "\nTo: ");
226 60         176 my $p2 = index($emailparts->[1], "\n", $p1 + 6);
227 60 100 66     398 if( $nomessages && $p1 > 0 ) {
228             # Try to get a recipient address from To: field in the original
229             # message at message/rfc822 part
230 5         30 $dscontents->[-1]->{'recipient'} = Sisimai::Address->s3s4(substr($emailparts->[1], $p1 + 5, $p2 - $p1 - 5));
231 5         15 $recipients++;
232             }
233             }
234             }
235 620 100       2652 return undef unless $recipients;
236              
237 565         1494 for my $e ( @$dscontents ) {
238             # Set default values if each value is empty.
239 580   50     3724 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage;
      66        
240              
241 580 100       2273 if( $anotherset->{'diagnosis'} ) {
242             # Copy alternative error message
243 549         2251 $anotherset->{'diagnosis'} = Sisimai::String->sweep($anotherset->{'diagnosis'});
244 549   66     2188 $e->{'diagnosis'} ||= $anotherset->{'diagnosis'};
245              
246 549 50       3097 if( $e->{'diagnosis'} =~ /\A\d+\z/ ) {
247             # Override the value of diagnostic code message
248 0         0 $e->{'diagnosis'} = $anotherset->{'diagnosis'};
249              
250             } else {
251             # More detailed error message is in "$anotherset"
252 549         1224 my $as = ''; # status
253 549         1205 my $ar = ''; # replycode
254              
255 549 100       7073 if( Sisimai::SMTP::Status->is_ambiguous($e->{'status'}) ) {
256             # Check the value of D.S.N. in $anotherset is neither an empty nor *.0.0.
257 187   100     1048 $as = Sisimai::SMTP::Status->find($anotherset->{'diagnosis'}) || '';
258 187 100       863 $e->{'status'} = $as unless Sisimai::SMTP::Status->is_ambiguous($as);
259             }
260              
261 549 50 33     2386 if( $e->{'replycode'} eq '' || substr($e->{'replycode'}, -2, 2) eq '00' ) {
262             # Check the value of SMTP reply code in $anotherset
263 549   100     4932 $ar = Sisimai::SMTP::Reply->find($anotherset->{'diagnosis'}) || '';
264 549 100 66     3600 if( length($ar) > 0 && substr($ar, -2, 2) ne '00' ) {
265             # The SMTP reply code is neither an empty nor *00
266 481         1476 $e->{'replycode'} = $ar;
267             }
268             }
269              
270 549         880 while(1) {
271             # Replace $e->{'diagnosis'} with the value of $anotherset->{'diagnosis'} when
272             # all the following conditions have not matched.
273 549 100       3032 last if length($as.$ar) == 0;
274 481 50       1673 last if length($anotherset->{'diagnosis'}) < length($e->{'diagnosis'});
275 481 100       2203 last if index($anotherset->{'diagnosis'}, $e->{'diagnosis'}) == -1;
276              
277 449         1381 $e->{'diagnosis'} = $anotherset->{'diagnosis'};
278 449         1054 last;
279             }
280             }
281             }
282 580         2874 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
283 580   100     3538 $e->{'command'} = shift @commandset || Sisimai::SMTP::Command->find($e->{'diagnosis'}) || '';
284 580 100 50     2334 $e->{'command'} ||= 'HELO' if index($e->{'diagnosis'}, 'refused to talk to me:') > -1;
285 580 100 100     2961 $e->{'spec'} ||= 'SMTP' if Sisimai::String->aligned(\$e->{'diagnosis'}, ['host ', ' said:']);
286             }
287 565         6978 return {"ds" => $dscontents, "rfc822" => $emailparts->[1]};
288             }
289              
290             1;
291             __END__