File Coverage

lib/Sisimai/Lhost/Sendmail.pm
Criterion Covered Total %
statement 96 97 98.9
branch 62 72 86.1
condition 23 34 67.6
subroutine 6 6 100.0
pod 2 2 100.0
total 189 211 89.5


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::Sendmail;
2 37     37   5846 use parent 'Sisimai::Lhost';
  37         103  
  37         201  
3 37     37   2239 use feature ':5.10';
  37         76  
  37         2651  
4 37     37   194 use strict;
  37         72  
  37         754  
5 37     37   164 use warnings;
  37         52  
  37         46931  
6              
7 2     2 1 1110 sub description { 'V8Sendmail: /usr/sbin/sendmail' }
8             sub make {
9             # Parse bounce messages from Sendmail
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 793     793 1 1412 my $class = shift;
16 793   100     1948 my $mhead = shift // return undef;
17 792   50     1405 my $mbody = shift // return undef;
18              
19 792 100       4234 return undef unless $mhead->{'subject'} =~ /(?:see transcript for details\z|\AWarning: )/;
20 485 50       1080 return undef if $mhead->{'x-aol-ip'}; # X-AOL-IP is a header defined in AOL
21              
22 485         761 state $indicators = __PACKAGE__->INDICATORS;
23 485         661 state $rebackbone = qr<^Content-Type:[ ](?:message/rfc822|text/rfc822-headers)>m;
24 485         588 state $startingof = {
25             # savemail.c:1040|if (printheader && !putline(" ----- Transcript of session follows -----\n",
26             # savemail.c:1041| mci))
27             # savemail.c:1042| goto writeerr;
28             # savemail.c:1360|if (!putline(
29             # savemail.c:1361| sendbody
30             # savemail.c:1362| ? " ----- Original message follows -----\n"
31             # savemail.c:1363| : " ----- Message header follows -----\n",
32             'message' => [' ----- Transcript of session follows -----'],
33             'error' => ['... while talking to '],
34             };
35              
36 485         5466 require Sisimai::RFC1894;
37 485         1856 my $fieldtable = Sisimai::RFC1894->FIELDTABLE;
38 485         786 my $permessage = {}; # (Hash) Store values of each Per-Message field
39              
40 485         1549 my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
41 485         2164 my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
42 485         704 my $readcursor = 0; # (Integer) Points the current cursor position
43 485         608 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
44 485         724 my $commandtxt = ''; # (String) SMTP Command name begin with the string '>>>'
45 485         745 my $esmtpreply = []; # (Array) Reply from remote server on SMTP session
46 485         580 my $sessionerr = 0; # (Integer) Flag, 1 if it is SMTP session error
47 485         604 my $anotherset = {}; # (Hash) Another error information
48 485         563 my $v = undef;
49 485         605 my $p = '';
50              
51 485         3926 for my $e ( split("\n", $emailsteak->[0]) ) {
52             # Read error messages and delivery status lines from the head of the email
53             # to the previous line of the beginning of the original message.
54 13243 100       14997 unless( $readcursor ) {
55             # Beginning of the bounce message or message/delivery-status part
56 5241 100       8445 $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0;
57 5241         5307 next;
58             }
59 8002 50       10537 next unless $readcursor & $indicators->{'deliverystatus'};
60 8002 100       9158 next unless length $e;
61              
62 7029 100       10931 if( my $f = Sisimai::RFC1894->match($e) ) {
63             # $e matched with any field defined in RFC3464
64 4107 50       6522 next unless my $o = Sisimai::RFC1894->field($e);
65 4107         4676 $v = $dscontents->[-1];
66              
67 4107 100       6142 if( $o->[-1] eq 'addr' ) {
    100          
68             # Final-Recipient: rfc822; kijitora@example.jp
69             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
70 524 100       958 if( $o->[0] eq 'final-recipient' ) {
71             # Final-Recipient: rfc822; kijitora@example.jp
72 479 100       877 if( $v->{'recipient'} ) {
73             # There are multiple recipient addresses in the message body.
74 11         40 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
75 11         27 $v = $dscontents->[-1];
76             }
77 479         907 $v->{'recipient'} = $o->[2];
78 479         867 $recipients++;
79              
80             } else {
81             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
82 45         133 $v->{'alias'} = $o->[2];
83             }
84             } elsif( $o->[-1] eq 'code' ) {
85             # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown
86 449         788 $v->{'spec'} = $o->[1];
87 449         888 $v->{'diagnosis'} = $o->[2];
88              
89             } else {
90             # Other DSN fields defined in RFC3464
91 3134 50       4716 next unless exists $fieldtable->{ $o->[0] };
92 3134         5030 $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
93              
94 3134 100       5291 next unless $f == 1;
95 1330         3067 $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2];
96             }
97             } else {
98             # The line does not begin with a DSN field defined in RFC3464
99             #
100             # ----- Transcript of session follows -----
101             # ... while talking to mta.example.org.:
102             # >>> DATA
103             # <<< 550 Unknown user recipient@example.jp
104             # 554 5.0.0 Service unavailable
105 2922 100       4617 if( substr($e, 0, 1) ne ' ') {
106             # Other error messages
107 2911 100       6794 if( $e =~ /\A[>]{3}[ ]+([A-Z]{4})[ ]?/ ) {
    100          
108             # >>> DATA
109 457         973 $commandtxt = $1;
110              
111             } elsif( $e =~ /\A[<]{3}[ ]+(.+)\z/ ) {
112             # <<< Response
113 799 100       2070 push @$esmtpreply, $1 unless grep { $1 eq $_ } @$esmtpreply;
  709         1726  
114              
115             } else {
116             # Detect SMTP session error or connection error
117 1655 100       2657 next if $sessionerr;
118 613 100       1718 if( index($e, $startingof->{'error'}->[0]) == 0 ) {
119             # ----- Transcript of session follows -----
120             # ... while talking to mta.example.org.:
121 398         512 $sessionerr = 1;
122 398         542 next;
123             }
124              
125 215 100       395 if( $e =~ /\A[<](.+)[>][.]+ (.+)\z/ ) {
126             # ... Deferred: Name server: example.co.jp.: host name lookup failure
127 10         37 $anotherset->{'recipient'} = $1;
128 10         27 $anotherset->{'diagnosis'} = $2;
129              
130             } else {
131             # ----- Transcript of session follows -----
132             # Message could not be delivered for too long
133             # Message will be deleted from queue
134 205 100 66     838 if( $e =~ /\A[45]\d\d[ \t]([45][.]\d[.]\d)[ \t].+/ ) {
    100          
135             # 550 5.1.2 ... Message
136             #
137             # DBI connect('dbname=...')
138             # 554 5.3.0 unknown mailer error 255
139 46         137 $anotherset->{'status'} = $1;
140 46         145 $anotherset->{'diagnosis'} .= ' '.$e;
141              
142             } elsif( index($e, 'Message: ') == 0 || index($e, 'Warning: ') == 0 ) {
143             # Message could not be delivered for too long
144             # Warning: message still undelivered after 4 hours
145 5         18 $anotherset->{'diagnosis'} .= ' '.$e;
146             }
147             }
148             }
149             } else {
150             # Continued line of the value of Diagnostic-Code field
151 11 100       51 next unless index($p, 'Diagnostic-Code:') == 0;
152 6 50       28 next unless $e =~ /\A[ \t]+(.+)\z/;
153 6         23 $v->{'diagnosis'} .= ' '.$1;
154             }
155             }
156             } continue {
157             # Save the current line for the next loop
158 13243         15356 $p = $e;
159             }
160 485 100       1878 return undef unless $recipients;
161              
162 468         768 for my $e ( @$dscontents ) {
163             # Set default values if each value is empty.
164 479   66     1275 $e->{'lhost'} ||= $permessage->{'rhost'};
165 479   50     2449 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage;
      66        
166 479   100     2400 $e->{'command'} ||= $commandtxt || '';
      66        
167 479 100 100     1385 $e->{'command'} ||= 'EHLO' if scalar @$esmtpreply;
168              
169 479 50 66     1034 if( exists $anotherset->{'diagnosis'} && $anotherset->{'diagnosis'} ) {
170             # Copy alternative error message
171 56 50       180 $e->{'diagnosis'} = $anotherset->{'diagnosis'} if $e->{'diagnosis'} =~ /\A[ \t]+\z/;
172 56   66     182 $e->{'diagnosis'} ||= $anotherset->{'diagnosis'};
173 56 100       223 $e->{'diagnosis'} = $anotherset->{'diagnosis'} if $e->{'diagnosis'} =~ /\A\d+\z/;
174             }
175 479 100       889 if( scalar @$esmtpreply ) {
176             # Replace the error message in "diagnosis" with the ESMTP Reply
177 409         1090 my $r = join(' ', @$esmtpreply);
178 409 100       1114 $e->{'diagnosis'} = $r if length($r) > length($e->{'diagnosis'});
179             }
180 479         2508 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
181              
182 479 50 66     1286 if( exists $anotherset->{'status'} && $anotherset->{'status'} ) {
183             # Check alternative status code
184 46 50 33     310 if( ! $e->{'status'} || $e->{'status'} !~ /\A[45][.]\d[.]\d{1,3}\z/ ) {
185             # Override alternative status code
186 0         0 $e->{'status'} = $anotherset->{'status'};
187             }
188             }
189              
190             # @example.jp, no local part
191             # Get email address from the value of Diagnostic-Code header
192 479 100       2351 next if $e->{'recipient'} =~ /\A[^ ]+[@][^ ]+\z/;
193 5 50       39 $e->{'recipient'} = $1 if $e->{'diagnosis'} =~ /[<]([^ ]+[@][^ ]+)[>]/;
194             }
195 468         3365 return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
196             }
197              
198             1;
199             __END__