File Coverage

lib/Sisimai/RFC3464.pm
Criterion Covered Total %
statement 172 173 99.4
branch 102 110 92.7
condition 31 44 70.4
subroutine 8 8 100.0
pod 2 2 100.0
total 315 337 93.4


line stmt bran cond sub pod time code
1             package Sisimai::RFC3464;
2 33     33   13721 use v5.26;
  33         97  
3 33     33   126 use strict;
  33         46  
  33         629  
4 33     33   98 use warnings;
  33         156  
  33         1381  
5 33     33   153 use Sisimai::Lhost;
  33         42  
  33         761  
6 33     33   121 use Sisimai::RFC1123;
  33         56  
  33         577  
7 33     33   12733 use Sisimai::RFC3464::ThirdParty;
  33         75  
  33         55744  
8              
9             # http://tools.ietf.org/html/rfc3464
10 1     1 1 2 sub description { 'RFC3464' };
11             sub inquire {
12             # Decode a bounce mail which have fields defined in RFC3464
13             # @param [Hash] mhead Message headers of a bounce email
14             # @param [String] mbody Message body of a bounce email
15             # @return [Hash] Bounce data list and message/rfc822 part
16             # @return [undef] failed to decode or the arguments are missing
17 891     891 1 22245 my $class = shift;
18 891 100 100     1786 my $mhead = shift // return undef; return undef unless keys %$mhead;
  873         2584  
19 854 100 50     1413 my $mbody = shift // return undef; return undef unless ref $mbody eq 'SCALAR';
  854         2009  
20              
21 853         1991 require Sisimai::RFC1894;
22 853         1932 require Sisimai::RFC2045;
23 853         1865 require Sisimai::RFC5322;
24 853         2032 require Sisimai::Address;
25 853         2303 require Sisimai::String;
26              
27 853         1019 state $indicators = Sisimai::Lhost->INDICATORS;
28 853         1005 state $boundaries = [
29             # When the new value added, the part of the value should be listed in $delimiters variable
30             # defined at Sisimai::RFC2045->makeFlat() method
31             "Content-Type: message/rfc822",
32             "Content-Type: text/rfc822-headers",
33             "Content-Type: message/partial",
34             "Content-Disposition: inline", # See lhost-amavis-*.eml, lhost-facebook-*.eml
35             ];
36 853         931 state $startingof = {"message" => ["Content-Type: message/delivery-status"]};
37 853         1471 state $fieldtable = Sisimai::RFC1894->FIELDTABLE;
38              
39 853 100       1357 unless( grep { index($$mbody, $_) > 0 } @$boundaries ) {
  3412         7208  
40             # There is no "Content-Type: message/rfc822" line in the message body
41             # Insert "Content-Type: message/rfc822" before "Return-Path:" of the original message
42 121         277 my $p0 = index($$mbody, "\n\nReturn-Path:");
43 121 100       14922 $$mbody = sprintf("%s%s%s", substr($$mbody, 0, $p0), $boundaries->[0], substr($$mbody, $p0 + 1,)) if $p0 > 0;
44             }
45              
46 853         1616 my $permessage = {};
47 853         3437 my $dscontents = [Sisimai::Lhost->DELIVERYSTATUS]; my $v = undef;
  853         1210  
48 853         1665 my $alternates = Sisimai::Lhost->DELIVERYSTATUS;
49 853         3401 my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries);
50 853         1018 my $readcursor = 0; # (Integer) Points the current cursor position
51 853         1009 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
52 853         1061 my $beforemesg = ""; # (String) String before $startingof->{"message"}
53 853         971 my $goestonext = 0; # (Bool) Flag: do not append the line into $beforemesg
54 853   100     3168 my $isboundary = [Sisimai::RFC2045->boundary($mhead->{"content-type"}, 0)]; $isboundary->[0] ||= "";
  853         1961  
55 853         1155 my $p = "";
56              
57 853         2747 while( index($emailparts->[0], '@') < 0 ) {
58             # There is no email address in the first element of emailparts
59             # There is a bounce message inside of message/rfc822 part at lhost-x5-*
60 28         37 my $p0 = -1; # The index of the boundary string found first
61 28         44 my $p1 = 0; # Offset position of the message body after the boundary string
62 28         47 my $ct = ""; # Boundary string found first such as "Content-Type: message/rfc822"
63              
64 28         41 for my $e ( @$boundaries ) {
65             # Look for a boundary string from the message body
66 73 100       105 $p0 = index($$mbody, $e."\n"); next if $p0 < 0;
  73         131  
67 13         20 $p1 = $p0 + length($e) + 2;
68 13         17 $ct = $e; last;
  13         13  
69             }
70 28 100       59 last if $p0 < 0;
71              
72 13         204 my $cx = substr($$mbody, $p1,);
73 13         27 my $p2 = index($cx,, "\n\n");
74 13         32 my $cv = substr($cx, $p2 + 2,);
75 13         47 $emailparts = Sisimai::RFC5322->part(\$cv, [$ct], 0);
76 13         30 last;
77             }
78              
79 853 100       2840 if( index($emailparts->[0], $startingof->{"message"}->[0]) < 0 ) {
80             # There is no "Content-Type: message/delivery-status" line in the message body
81             # Insert "Content-Type: message/delivery-status" before "Reporting-MTA:" field
82 167         256 my $cv = "\n\nReporting-MTA:";
83 167         275 my $e0 = $emailparts->[0];
84 167         303 my $p0 = index($e0, $cv);
85 167 100       620 $emailparts->[0] = sprintf("%s\n\n%s%s", substr($e0, 0, $p0), $startingof->{"message"}->[0], substr($e0, $p0,)) if $p0 > 0;
86             }
87              
88 853         1440 for my $e ("Final-Recipient", "Original-Recipient") {
89             # Fix the malformed field "Final-Recipient: "
90 1706         1847 my $cv = "\n".$e.": ";
91 1706         1628 my $cx = $cv."<";
92 1706 100       2343 my $p0 = index($emailparts->[0], $cx); next if $p0 < 0;
  1706         2767  
93              
94 26         93 substr($emailparts->[0], $p0, length($cv) + 1, $cv."rfc822; ");
95 26         43 my $p1 = index($emailparts->[0], ">\n", $p0 + 2); substr($emailparts->[0], $p1, 1, "");
  26         54  
96             }
97              
98 853         6737 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 17837 100       19856 if( $readcursor == 0 ) {
102             # Beginning of the bounce message or message/delivery-status part
103 11119 100       15302 $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0;
104              
105 11119         8031 while(1) {
106             # Append each string before startingof["message"][0] except the following patterns
107             # for the later reference
108 11119 100 66     18610 last if $e eq "" || $goestonext; # Blank line or the part is text/html, image/icon, in multipart/*
109              
110             # This line is a boundary kept in "multiparts" as a string, when the end of the boundary
111             # appeared, the condition above also returns true.
112 7322 100       6680 if( grep { index($e, $_) == 0 } @$isboundary ) { $goestonext = 0; last }
  7337         12058  
  976         810  
  976         763  
113 6346 100       7944 if( index($e, "Content-Type:") == 0 ) {
114             # Content-Type: field in multipart/*
115 795 100       2187 if( index($e, "multipart/") > 0 ) {
    50          
116             # Content-Type: multipart/alternative; boundary=aa00220022222222ffeebb
117             # Pick the boundary string and store it into "isboucdary"
118 1         3 push @$isboundary, Sisimai::RFC2045->boundary($e, 0);
119              
120             } elsif( index($e, "text/plain") ) {
121             # Content-Type: "text/plain"
122 794         1016 $goestonext = 0;
123              
124             } else {
125             # Other types: for example, text/html, image/jpg, and so on
126 0         0 $goestonext = 1;
127             }
128 795         828 last;
129             }
130              
131 5551 100       6534 last if index($e, "Content-") == 0; # Content-Disposition, ...
132 5536 100       6396 last if index($e, "This is a MIME") == 0; # This is a MIME-formatted message.
133 5356 100       6495 last if index($e, "This is a multi") == 0; # This is a multipart message in MIME format
134 5268 100       6786 last if index($e, "This is an auto") == 0; # This is an automatically generated ...
135 5148 100       6113 last if index($e, "This multi-part") == 0; # This multi-part MIME message contains...
136 5143 100       6380 last if index($e, "###") == 0; # A frame like #####
137 5133 50       6014 last if index($e, "***") == 0; # A frame like *****
138 5133 100       6235 last if index($e, "--") == 0; # Boundary string
139 5100 100       6524 last if index($e, "--- The follow") > -1; # ----- The following addresses had delivery problems -----
140 5035 100       5965 last if index($e, "--- Transcript") > -1; # ----- Transcript of session follows -----
141 5012         7501 $beforemesg .= $e." "; last;
  5012         3952  
142             }
143 11119         9412 next;
144             }
145 6718 100 66     16991 next if ($readcursor & $indicators->{'deliverystatus'}) == 0 || $e eq "";
146              
147 5927 100       13601 if( my $f = Sisimai::RFC1894->match($e) ) {
148             # $e matched with any field defined in RFC3464
149 5247 100       8147 next unless my $o = Sisimai::RFC1894->field($e);
150 5082         5735 $v = $dscontents->[-1];
151              
152 5082 100       8951 if( $o->[3] eq "addr" ) {
    100          
153             # Final-Recipient: rfc822; kijitora@example.jp
154             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
155 957 100       1550 if( $o->[0] eq "final-recipient" ) {
156             # Final-Recipient: rfc822; kijitora@example.jp
157             # Final-Recipient: x400; /PN=...
158 745 50       4069 my $cv = Sisimai::Address->s3s4($o->[2]); next unless Sisimai::Address->is_emailaddress($cv);
  745         2201  
159 745 50 33     1522 my $cw = scalar @$dscontents; next if $cw > 0 && $cv eq $dscontents->[$cw - 1]->{'recipient'};
  745         3367  
160              
161 745 100       1398 if( $v->{'recipient'} ) {
162             # There are multiple recipient addresses in the message body.
163 32         121 push @$dscontents, Sisimai::Lhost->DELIVERYSTATUS;
164 32         59 $v = $dscontents->[-1];
165             }
166 745         1094 $v->{'recipient'} = $cv;
167 745         1730 $recipients++;
168              
169             } else {
170             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
171 212         463 $v->{'alias'} = $o->[2];
172             }
173             } elsif( $o->[3] eq "code" ) {
174             # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown
175 670         1183 $v->{'spec'} = $o->[1];
176 670         1933 $v->{'diagnosis'} .= $o->[2]." ";
177              
178             } else {
179             # Other DSN fields defined in RFC3464
180 3455 100       4904 if( $o->[4] ne "" ) {
181             # There are other error messages as a comment such as the following:
182             # Status: 5.0.0 (permanent failure)
183             # Status: 4.0.0 (cat.example.net: host name lookup failure)
184 659         1157 $v->{'diagnosis'} .= " ".$o->[4]." ";
185             }
186 3455 50       5760 next unless exists $fieldtable->{ $o->[0] };
187 3455 100 100     9964 next if $o->[3] eq "host" && Sisimai::RFC1123->is_internethost($o->[2]) == 0;
188              
189 3215         5929 $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
190 3215 100       5587 next unless $f == 1;
191 1292         3255 $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2];
192             }
193             } else {
194             # Check that the line is a continued line of the value of Diagnostic-Code: field or not
195 680 100 66     2046 if( index($e, "X-") == 0 && index($e, ": ") > 1 ) {
196             # This line is a MTA-Specific fields begins with "X-"
197 238 100       850 next unless Sisimai::RFC3464::ThirdParty->is3rdparty($e);
198              
199 32         64 my $cv = Sisimai::RFC3464::ThirdParty->xfield($e);
200 32 100 66     137 if( scalar(@$cv) > 0 && not exists $fieldtable->{ lc $cv->[0] } ) {
201             # Check the first element is a field defined in RFC1894 or not
202 16 50       69 $v->{'reason'} = substr($cv->[4], index($cv->[4], ":") + 1,) if index($cv->[4], "reason:") == 0;
203              
204             } else {
205             # Set the value picked from "X-*" field to $dscontents when the current value is empty
206 16 50       32 my $z = $fieldtable->{ lc $cv->[0] }; next unless $z;
  16         22  
207 16   33     42 $v->{ $z } ||= $cv->[2];
208             }
209             } else {
210             # The line may be a continued line of the value of the Diagnostic-Code: field
211 442 100       921 if( index($p, 'Diagnostic-Code:') < 0 ) {
212             # In the case of multiple "message/delivery-status" line
213 233 100       494 next if index($e, "Content-") == 0; # Content-Disposition:, ...
214 223 100       447 next if index($e, "--") == 0; # Boundary string
215 207         289 $beforemesg .= $e." "; next
216 207         227 }
217              
218             # Diagnostic-Code: SMTP; 550-5.7.26 The MAIL FROM domain [email.example.jp]
219             # has an SPF record with a hard fail
220 209 100       590 next unless index($e, " ") == 0;
221 176         422 $v->{'diagnosis'} .= " ".$e;
222             }
223             }
224             } continue {
225             # Save the current line for the next loop
226 17837         18647 $p = $e;
227             }
228              
229 853         3728 while( $recipients == 0 ) {
230             # There is no valid recipient address, Try to use the alias addaress as a final recipient
231 140 100       330 last unless length $dscontents->[0]->{'alias'} > 0;
232 26 50       168 last unless Sisimai::Address->is_emailaddress($dscontents->[0]->{'alias'});
233 26         79 $dscontents->[0]->{'recipient'} = $dscontents->[0]->{'alias'};
234 26         56 $recipients++;
235             }
236 853 100       2018 return undef unless $recipients;
237              
238 739         3839 require Sisimai::SMTP::Reply;
239 739         2184 require Sisimai::SMTP::Status;
240 739         1952 require Sisimai::SMTP::Command;
241              
242 739 100       1424 if( $beforemesg ne "" ) {
243             # Pick some values of $dscontents from the string before $startingof->{'message'}
244 729         1621 $beforemesg = Sisimai::String->sweep($beforemesg);
245 729         5192 $alternates->{'command'} = Sisimai::SMTP::Command->find($beforemesg);
246 729         3944 $alternates->{'replycode'} = Sisimai::SMTP::Reply->find($beforemesg, $dscontents->[0]->{'status'});
247 729         3664 $alternates->{'status'} = Sisimai::SMTP::Status->find($beforemesg, $alternates->{'replycode'});
248             }
249 739         2165 my $issuedcode = lc $beforemesg;
250              
251 739         997 for my $e ( @$dscontents ) {
252             # Set default values stored in "permessage" if each value in "dscontents" is empty.
253 771   50     3045 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage;
      66        
254 771         1696 my $lowercased = lc $e->{'diagnosis'};
255              
256 771 100       1300 if( $recipients == 1 ) {
257             # Do not mix the error message of each recipient with "beforemesg" when there is
258             # multiple recipient addresses in the bounce message
259 712 100       1619 if( index($issuedcode, $lowercased) > -1 ) {
260             # $beforemesg contains the entire strings of $e->{'diagnosis'}
261 83         214 $e->{'diagnosis'} = $beforemesg;
262              
263             } else {
264             # The value of $e->{'diagnosis'} is not contained in $beforemesg
265             # There may be an important error message in $beforemesg
266 629         2640 $e->{'diagnosis'} = sprintf("%s %s", $beforemesg, $e->{'diagnosis'});
267             }
268             }
269 771   100     1965 $e->{'command'} = Sisimai::SMTP::Command->find($e->{'diagnosis'}) || $alternates->{'command'};
270 771   66     2169 $e->{'replycode'} = Sisimai::SMTP::Reply->find($e->{'diagnosis'}, $e->{'status'}) || $alternates->{'replycode'};
271 771   66     1727 $e->{'status'} ||= Sisimai::SMTP::Status->find($e->{'diagnosis'}, $e->{'replycode'}) || $alternates->{'status'};
      100        
272             }
273              
274             # Set the recipient address as To: header in the original message part
275 739 100       1447 $emailparts->[1] = sprintf("To: <%s>\n", $dscontents->[0]->{'recipient'}) unless $emailparts->[1];
276            
277 739         6153 return {"ds" => $dscontents, "rfc822" => $emailparts->[1]};
278             }
279              
280             1;
281              
282             __END__