File Coverage

lib/Sisimai/Lhost/V5sendmail.pm
Criterion Covered Total %
statement 88 95 92.6
branch 35 46 76.0
condition 25 33 75.7
subroutine 6 6 100.0
pod 2 2 100.0
total 156 182 85.7


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::V5sendmail;
2 35     35   3631 use parent 'Sisimai::Lhost';
  35         52  
  35         188  
3 35     35   2374 use v5.26;
  35         95  
4 35     35   116 use strict;
  35         55  
  35         617  
5 35     35   113 use warnings;
  35         62  
  35         33719  
6              
7 1     1 1 2 sub description { 'Sendmail version 5' }
8             sub inquire {
9             # Detect an error from V5sendmail
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.1.2
15 906     906 1 2714 my $class = shift;
16 906   100     1718 my $mhead = shift // return undef;
17 905   100     1596 my $mbody = shift // return undef;
18              
19 904 100       2326 return undef unless $mhead->{'subject'};
20 886 100       2402 return undef if index($mhead->{'subject'}, 'Returned mail: ') != 0;
21              
22 81         150 state $indicators = __PACKAGE__->INDICATORS;
23 81         127 state $boundaries = [' ----- Unsent message follows -----', ' ----- No message was collected -----'];
24 81         151 state $startingof = {
25             # Error text regular expressions which defined in src/savemail.c
26             # savemail.c:485| (void) fflush(stdout);
27             # savemail.c:486| p = queuename(e->e_parent, 'x');
28             # savemail.c:487| if ((xfile = fopen(p, "r")) == NULL)
29             # savemail.c:488| {
30             # savemail.c:489| syserr("Cannot open %s", p);
31             # savemail.c:490| fprintf(fp, " ----- Transcript of session is unavailable -----\n");
32             # savemail.c:491| }
33             # savemail.c:492| else
34             # savemail.c:493| {
35             # savemail.c:494| fprintf(fp, " ----- Transcript of session follows -----\n");
36             # savemail.c:495| if (e->e_xfp != NULL)
37             # savemail.c:496| (void) fflush(e->e_xfp);
38             # savemail.c:497| while (fgets(buf, sizeof buf, xfile) != NULL)
39             # savemail.c:498| putline(buf, fp, m);
40             # savemail.c:499| (void) fclose(xfile);
41             'error' => ['While talking to '],
42             'message' => ['----- Transcript of session follows -----'],
43             };
44              
45 81         313 my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries);
46 81 100       273 return undef unless length $emailparts->[1] > 0;
47              
48 36         142 require Sisimai::RFC1123;
49 36         83 require Sisimai::SMTP::Command;
50 36         140 my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $v = undef;
  36         53  
51 36         40 my $readcursor = 0; # (Integer) Points the current cursor position
52 36         46 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
53 36         39 my $anotherone = {}; # (Ref->Hash) Another error information
54 36         56 my $remotehost = ""; # (String) The last remote hostname
55 36         45 my $curcommand = ""; # (String) The last SMTP command
56              
57 36         138 for my $e ( split("\n", $emailparts->[0]) ) {
58             # Read error messages and delivery status lines from the head of the email to the previous
59             # line of the beginning of the original message.
60 262 100       360 unless( $readcursor ) {
61             # Beginning of the bounce message or message/delivery-status part
62 36 50       134 $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) > -1;
63 36         47 next;
64             }
65 226 50 33     572 next if ($readcursor & $indicators->{'deliverystatus'}) == 0 || $e eq "";
66              
67             # ----- Transcript of session follows -----
68             # While talking to smtp.example.com:
69             # >>> RCPT To:
70             # <<< 550 , User Unknown
71             # 550 ... User unknown
72             # 421 example.org (smtp)... Deferred: Connection timed out during user open with example.org
73 226         250 $v = $dscontents->[-1];
74 226 100       553 $curcommand = Sisimai::SMTP::Command->find(substr($e, 4,)) if index($e, ">>> ") == 0;
75              
76 226 100 100     701 if( Sisimai::String->aligned(\$e, [' <', '@', '>...']) || index(uc $e, ">>> RCPT TO:") > -1 ) {
77             # 550 ... User unknown
78             # >>> RCPT To:
79 85         119 my $p0 = index($e, " ");
80 85         127 my $p1 = index($e, "<", $p0);
81 85         104 my $p2 = index($e, ">", $p1);
82 85         319 my $cv = Sisimai::Address->s3s4(substr($e, $p1, $p2 - $p1 + 1));
83              
84 85 100       180 if( $remotehost eq "" ) {
85             # Keep error messages before "While talking to ..." line
86 15         30 $anotherone->{ $recipients } .= " ".$e;
87 15         28 next;
88             }
89              
90 70 100 100     276 if( $cv eq $v->{"recipient"} || ($curcommand eq "MAIL" && index($e, "<<< ") == 0) ) {
      100        
91             # The recipient address is the same address with the last appeared address
92             # like "550 ... User unknown"
93             # Append this line to the string which is keeping error messages
94 25         51 $v->{"diagnosis"} .= " ".$e;
95 25         95 $v->{"replycode"} = Sisimai::SMTP::Reply->find($e);
96 25         56 $curcommand = "";
97              
98             } else {
99             # The recipient address in this line differs from the last appeared address
100             # or is the first recipient address in this bounce message
101 45 100       84 if( $v->{'recipient'} ) {
102             # There are multiple recipient addresses in the message body.
103 25         79 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
104 25         39 $v = $dscontents->[-1];
105             }
106 45         57 $v->{"recipient"} = $cv;
107 45         60 $v->{"rhost"} = $remotehost;
108 45         153 $v->{"replycode"} = Sisimai::SMTP::Reply->find($e);
109 45         92 $v->{"diagnosis"} .= " ".$e;
110 45   66     140 $v->{"command"} ||= $curcommand;
111 45         85 $recipients++
112             }
113             } else {
114             # This line does not include a recipient address
115 141 100       266 if( index($e, $startingof->{"error"}->[0]) > -1 ) {
116             # ... while talking to mta.example.org.:
117 35         162 my $cv = Sisimai::RFC1123->find($e);
118 35 50       77 $remotehost = $cv if Sisimai::RFC1123->is_internethost($cv);
119              
120             } else {
121             # Append this line into the error message string
122 106 100 100     267 if( index($e, ">>> ") == 0 || index($e, "<<< ") == 0 ) {
123             # >>> DATA
124             # <<< 550 Your E-Mail is redundant. You cannot send E-Mail to yourself (shironeko@example.jp).
125             # >>> QUIT
126             # <<< 421 dns.example.org Sorry, unable to contact destination SMTP daemon.
127             # <<< 550 Requested User Mailbox not found. No such user here.
128 45         107 $v->{"diagnosis"} .= " ".$e
129              
130             } else {
131             # 421 Other error message
132 61         218 $anotherone->{ $recipients } .= " ".$e;
133             }
134             }
135             }
136             }
137              
138 36 100       97 if( $recipients == 0 ) {
139             # There is no recipient address in the error message
140 16         35 for my $e ( keys %$anotherone ) {
141             # Try to pick an recipient address, a reply code, and error messages
142 16 100       48 my $cv = Sisimai::Address->s3s4($anotherone->{ $e }); next unless Sisimai::Address->is_emailaddress($cv);
  16         64  
143 10   50     82 my $cr = Sisimai::SMTP::Reply->find($anotherone->{ $e }) || "";
144              
145 10         28 $dscontents->[ $e ]->{"recipient"} = $cv;
146 10         18 $dscontents->[ $e ]->{"replycode"} = $cr;
147 10         23 $dscontents->[ $e ]->{"diagnosis"} = $anotherone->{ $e };
148 10         18 $recipients++;
149             }
150              
151 16 100       28 if( $recipients == 0 ) {
152             # Try to pick an recipient address from the original message
153 6         18 my $p1 = index($emailparts->[1], "\nTo: ");
154 6         11 my $p2 = index($emailparts->[1], "\n", $p1 + 6);
155              
156 6 50       12 if( $p1 > 0 ) {
157             # Get the recipient address from "To:" header at the original message
158 6         22 my $cv = Sisimai::Address->s3s4(substr($emailparts->[1], $p1, $p2 - $p1 - 5));
159 6 50       19 return undef unless Sisimai::Address->is_emailaddress($cv);
160 0         0 $dscontents->[0]->{'recipient'} = $cv;
161 0         0 $recipients++;
162             }
163             }
164             }
165 30 50       45 return undef unless $recipients;
166              
167 30         52 my $j = 0; for my $e ( @$dscontents ) {
  30         43  
168             # Tidy up the error message in e.Diagnosis
169 55   33     88 $e->{"diagnosis"} ||= $anotherone->{ $j };
170 55   100     154 $e->{"command"} ||= Sisimai::SMTP::Command->find($e->{"diagnosis"});
171 55   33     98 $e->{'replycode'} = Sisimai::SMTP::Reply->find($e->{'diagnosis'}) || $anotherone->{'replycode'};
172              
173             # @example.jp, no local part
174             # Get email address from the value of Diagnostic-Code header
175 55 50       139 next if index($e->{'recipient'}, '@') > 0;
176 0 0       0 my $p1 = index($e->{'diagnosis'}, '<'); next if $p1 == -1;
  0         0  
177 0 0       0 my $p2 = index($e->{'diagnosis'}, '>'); next if $p2 == -1;
  0         0  
178 0         0 $e->{'recipient'} = Sisimai::Address->s3s4(substr($e->{'diagnosis'}, $p1, $p2 - $p1));
179             }
180 30         170 return {"ds" => $dscontents, "rfc822" => $emailparts->[1]};
181             }
182              
183             1;
184             __END__