| 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__ |