File Coverage

lib/Sisimai/RFC3464.pm
Criterion Covered Total %
statement 172 174 98.8
branch 101 110 91.8
condition 31 44 70.4
subroutine 8 8 100.0
pod 2 2 100.0
total 314 338 92.9


line stmt bran cond sub pod time code
1             package Sisimai::RFC3464;
2 32     32   18827 use v5.26;
  32         126  
3 32     32   213 use strict;
  32         60  
  32         817  
4 32     32   153 use warnings;
  32         77  
  32         1772  
5 32     32   165 use Sisimai::Lhost;
  32         57  
  32         910  
6 32     32   432 use Sisimai::RFC1123;
  32         54  
  32         925  
7 32     32   18913 use Sisimai::RFC3464::ThirdParty;
  32         275  
  32         77785  
8              
9             # http://tools.ietf.org/html/rfc3464
10 1     1 1 21 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 893     893 1 30251 my $class = shift;
18 893 100 100     3072 my $mhead = shift // return undef; return undef unless keys %$mhead;
  875         4154  
19 856 100 50     2500 my $mbody = shift // return undef; return undef unless ref $mbody eq 'SCALAR';
  856         2915  
20              
21 855         3275 require Sisimai::RFC1894;
22 855         3169 require Sisimai::RFC2045;
23 855         2572 require Sisimai::RFC5322;
24 855         3885 require Sisimai::Address;
25 855         2730 require Sisimai::String;
26              
27 855         1562 state $indicators = Sisimai::Lhost->INDICATORS;
28 855         1331 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 855         1389 state $startingof = {"message" => ["Content-Type: message/delivery-status"]};
37 855         1921 state $fieldtable = Sisimai::RFC1894->FIELDTABLE;
38              
39 855 100       2240 unless( grep { index($$mbody, $_) > 0 } @$boundaries ) {
  3420         11452  
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 124         450 my $p0 = index($$mbody, "\n\nReturn-Path:");
43 124 100       695 $$mbody = sprintf("%s%s%s", substr($$mbody, 0, $p0), $boundaries->[0], substr($$mbody, $p0 + 1,)) if $p0 > 0;
44             }
45              
46 855         1627 my $permessage = {};
47 855         5312 my $dscontents = [Sisimai::Lhost->DELIVERYSTATUS]; my $v = undef;
  855         1999  
48 855         2295 my $alternates = Sisimai::Lhost->DELIVERYSTATUS;
49 855         4630 my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries);
50 855         1731 my $readcursor = 0; # (Integer) Points the current cursor position
51 855         1278 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
52 855         1616 my $beforemesg = ""; # (String) String before $startingof->{"message"}
53 855         1404 my $goestonext = 0; # (Bool) Flag: do not append the line into $beforemesg
54 855   100     5051 my $isboundary = [Sisimai::RFC2045->boundary($mhead->{"content-type"}, 0)]; $isboundary->[0] ||= "";
  855         3076  
55 855         2140 my $p = "";
56              
57 855         3827 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         60 my $p0 = -1; # The index of the boundary string found first
61 28         51 my $p1 = 0; # Offset position of the message body after the boundary string
62 28         92 my $ct = ""; # Boundary string found first such as "Content-Type: message/rfc822"
63              
64 28         93 for my $e ( @$boundaries ) {
65             # Look for a boundary string from the message body
66 73 100       150 $p0 = index($$mbody, $e."\n"); next if $p0 < 0;
  73         179  
67 13         58 $p1 = $p0 + length($e) + 2;
68 13         26 $ct = $e; last;
  13         28  
69             }
70 28 100       90 last if $p0 < 0;
71              
72 13         156 my $cx = substr($$mbody, $p1,);
73 13         41 my $p2 = index($cx,, "\n\n");
74 13         82 my $cv = substr($cx, $p2 + 2,);
75 13         80 $emailparts = Sisimai::RFC5322->part(\$cv, [$ct], 0);
76 13         38 last;
77             }
78              
79 855 100       4260 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 169         332 my $cv = "\n\nReporting-MTA:";
83 169         361 my $e0 = $emailparts->[0];
84 169         352 my $p0 = index($e0, $cv);
85 169 100       933 $emailparts->[0] = sprintf("%s\n\n%s%s", substr($e0, 0, $p0), $startingof->{"message"}->[0], substr($e0, $p0,)) if $p0 > 0;
86             }
87              
88 855         2273 for my $e ("Final-Recipient", "Original-Recipient") {
89             # Fix the malformed field "Final-Recipient: "
90 1710         2895 my $cv = "\n".$e.": ";
91 1710         2425 my $cx = $cv."<";
92 1710 100       3650 my $p0 = index($emailparts->[0], $cx); next if $p0 < 0;
  1710         4159  
93              
94 26         126 substr($emailparts->[0], $p0, length($cv) + 1, $cv."rfc822; ");
95 26         66 my $p1 = index($emailparts->[0], ">\n", $p0 + 2); substr($emailparts->[0], $p1, 1, "");
  26         116  
96             }
97              
98 855         9376 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 18016 100       30522 if( $readcursor == 0 ) {
102             # Beginning of the bounce message or message/delivery-status part
103 11169 100       29515 $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0;
104              
105 11169         12233 while(1) {
106             # Append each string before startingof["message"][0] except the following patterns
107             # for the later reference
108 11169 100 66     29850 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 7369 100       10634 if( grep { index($e, $_) == 0 } @$isboundary ) { $goestonext = 0; last }
  7369         21561  
  988         2620  
  988         1293  
113 6381 100       13323 if( index($e, "Content-Type:") == 0 ) {
114             # Content-Type: field in multipart/*
115 825 50       3717 if( index($e, "multipart/") > 0 ) {
    50          
116             # Content-Type: multipart/alternative; boundary=aa00220022222222ffeebb
117             # Pick the boundary string and store it into "isboucdary"
118 0         0 push @$isboundary, Sisimai::RFC2045->boundary($e, 0);
119              
120             } elsif( index($e, "text/plain") ) {
121             # Content-Type: "text/plain"
122 825         1737 $goestonext = 0;
123              
124             } else {
125             # Other types: for example, text/html, image/jpg, and so on
126 0         0 $goestonext = 1;
127             }
128 825         1436 last;
129             }
130              
131 5556 100       9955 last if index($e, "Content-") == 0; # Content-Disposition, ...
132 5531 100       10122 last if index($e, "This is a MIME") == 0; # This is a MIME-formatted message.
133 5351 100       10008 last if index($e, "This is a multi") == 0; # This is a multipart message in MIME format
134 5263 100       9821 last if index($e, "This is an auto") == 0; # This is an automatically generated ...
135 5143 100       11904 last if index($e, "This multi-part") == 0; # This multi-part MIME message contains...
136 5138 100       11072 last if index($e, "###") == 0; # A frame like #####
137 5128 50       9204 last if index($e, "***") == 0; # A frame like *****
138 5128 100       12188 last if index($e, "--") == 0; # Boundary string
139 5095 100       8635 last if index($e, "--- The follow") > -1; # ----- The following addresses had delivery problems -----
140 5030 100       9088 last if index($e, "--- Transcript") > -1; # ----- Transcript of session follows -----
141 5007         8111 $beforemesg .= $e." "; last;
  5007         6669  
142             }
143 11169         13235 next;
144             }
145 6847 100 66     28859 next if ($readcursor & $indicators->{'deliverystatus'}) == 0 || $e eq "";
146              
147 5968 100       24033 if( my $f = Sisimai::RFC1894->match($e) ) {
148             # $e matched with any field defined in RFC3464
149 5247 100       14002 next unless my $o = Sisimai::RFC1894->field($e);
150 5082         10096 $v = $dscontents->[-1];
151              
152 5082 100       12342 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       2530 if( $o->[0] eq "final-recipient" ) {
156             # Final-Recipient: rfc822; kijitora@example.jp
157             # Final-Recipient: x400; /PN=...
158 745 50       6024 my $cv = Sisimai::Address->s3s4($o->[2]); next unless Sisimai::Address->is_emailaddress($cv);
  745         3316  
159 745 50 33     2210 my $cw = scalar @$dscontents; next if $cw > 0 && $cv eq $dscontents->[$cw - 1]->{'recipient'};
  745         5125  
160              
161 745 100       1993 if( $v->{'recipient'} ) {
162             # There are multiple recipient addresses in the message body.
163 32         177 push @$dscontents, Sisimai::Lhost->DELIVERYSTATUS;
164 32         94 $v = $dscontents->[-1];
165             }
166 745         1840 $v->{'recipient'} = $cv;
167 745         2625 $recipients++;
168              
169             } else {
170             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
171 212         866 $v->{'alias'} = $o->[2];
172             }
173             } elsif( $o->[3] eq "code" ) {
174             # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown
175 670         1923 $v->{'spec'} = $o->[1];
176 670         2732 $v->{'diagnosis'} .= $o->[2]." ";
177              
178             } else {
179             # Other DSN fields defined in RFC3464
180 3455 100       7382 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         1851 $v->{'diagnosis'} .= " ".$o->[4]." ";
185             }
186 3455 50       8871 next unless exists $fieldtable->{ $o->[0] };
187 3455 100 100     13939 next if $o->[3] eq "host" && Sisimai::RFC1123->is_internethost($o->[2]) == 0;
188              
189 3215         9553 $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
190 3215 100       8728 next unless $f == 1;
191 1292         5390 $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 721 100 66     3377 if( index($e, "X-") == 0 && index($e, ": ") > 1 ) {
196             # This line is a MTA-Specific fields begins with "X-"
197 238 100       1381 next unless Sisimai::RFC3464::ThirdParty->is3rdparty($e);
198              
199 32         89 my $cv = Sisimai::RFC3464::ThirdParty->xfield($e);
200 32 100 66     142 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       86 $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         151  
207 16   33     55 $v->{ $z } ||= $cv->[2];
208             }
209             } else {
210             # The line may be a continued line of the value of the Diagnostic-Code: field
211 483 100       1570 if( index($p, 'Diagnostic-Code:') < 0 ) {
212             # In the case of multiple "message/delivery-status" line
213 274 100       833 next if index($e, "Content-") == 0; # Content-Disposition:, ...
214 254 100       878 next if index($e, "--") == 0; # Boundary string
215 207         512 $beforemesg .= $e." "; next
216 207         416 }
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       857 next unless index($e, " ") == 0;
221 176         817 $v->{'diagnosis'} .= " ".Sisimai::String->sweep($e);
222             }
223             }
224             } continue {
225             # Save the current line for the next loop
226 18016         31411 $p = $e;
227             }
228              
229 855         5070 while( $recipients == 0 ) {
230             # There is no valid recipient address, Try to use the alias addaress as a final recipient
231 142 100       590 last unless length $dscontents->[0]->{'alias'} > 0;
232 26 50       242 last unless Sisimai::Address->is_emailaddress($dscontents->[0]->{'alias'});
233 26         114 $dscontents->[0]->{'recipient'} = $dscontents->[0]->{'alias'};
234 26         399 $recipients++;
235             }
236 855 100       3746 return undef unless $recipients;
237              
238 739         5511 require Sisimai::SMTP::Reply;
239 739         3093 require Sisimai::SMTP::Status;
240 739         3000 require Sisimai::SMTP::Command;
241              
242 739 100       2417 if( $beforemesg ne "" ) {
243             # Pick some values of $dscontents from the string before $startingof->{'message'}
244 729         2314 $beforemesg = Sisimai::String->sweep($beforemesg);
245 729         7429 $alternates->{'command'} = Sisimai::SMTP::Command->find($beforemesg);
246 729         6291 $alternates->{'replycode'} = Sisimai::SMTP::Reply->find($beforemesg, $dscontents->[0]->{'status'});
247 729         6146 $alternates->{'status'} = Sisimai::SMTP::Status->find($beforemesg, $alternates->{'replycode'});
248             }
249 739         3028 my $issuedcode = lc $beforemesg;
250              
251 739         1794 for my $e ( @$dscontents ) {
252             # Set default values stored in "permessage" if each value in "dscontents" is empty.
253 771   50     4790 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage;
      66        
254 771         3696 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
255 771         2496 my $lowercased = lc $e->{'diagnosis'};
256              
257 771 100       2089 if( $recipients == 1 ) {
258             # Do not mix the error message of each recipient with "beforemesg" when there is
259             # multiple recipient addresses in the bounce message
260 712 100       2375 if( index($issuedcode, $lowercased) > -1 ) {
261             # $beforemesg contains the entire strings of $e->{'diagnosis'}
262 115         300 $e->{'diagnosis'} = $beforemesg;
263              
264             } else {
265             # The value of $e->{'diagnosis'} is not contained in $beforemesg
266             # There may be an important error message in $beforemesg
267 597         3092 $e->{'diagnosis'} = Sisimai::String->sweep(sprintf("%s %s", $beforemesg, $e->{'diagnosis'}))
268             }
269             }
270 771   100     3177 $e->{'command'} = Sisimai::SMTP::Command->find($e->{'diagnosis'}) || $alternates->{'command'};
271 771   66     3475 $e->{'replycode'} = Sisimai::SMTP::Reply->find($e->{'diagnosis'}, $e->{'status'}) || $alternates->{'replycode'};
272 771   66     2880 $e->{'status'} ||= Sisimai::SMTP::Status->find($e->{'diagnosis'}, $e->{'replycode'}) || $alternates->{'status'};
      100        
273             }
274              
275             # Set the recipient address as To: header in the original message part
276 739 100       2530 $emailparts->[1] = sprintf("To: <%s>\n", $dscontents->[0]->{'recipient'}) unless $emailparts->[1];
277            
278 739         9547 return {"ds" => $dscontents, "rfc822" => $emailparts->[1]};
279             }
280              
281             1;
282              
283             __END__