File Coverage

lib/Sisimai/Fact.pm
Criterion Covered Total %
statement 288 290 99.3
branch 127 148 85.8
condition 107 140 76.4
subroutine 23 23 100.0
pod 2 4 50.0
total 547 605 90.4


line stmt bran cond sub pod time code
1             package Sisimai::Fact;
2 86     86   362563 use v5.26;
  86         224  
3 86     86   380 use strict;
  86         98  
  86         1397  
4 86     86   222 use warnings;
  86         95  
  86         3089  
5 86     86   39792 use Digest::SHA;
  86         243523  
  86         5234  
6 86     86   34646 use Sisimai::Message;
  86         235  
  86         2899  
7 86     86   448 use Sisimai::RFC791;
  86         118  
  86         1553  
8 86     86   247 use Sisimai::RFC1123;
  86         94  
  86         983  
9 86     86   232 use Sisimai::RFC1894;
  86         91  
  86         864  
10 86     86   209 use Sisimai::RFC5322;
  86         84  
  86         842  
11 86     86   29696 use Sisimai::Reason;
  86         168  
  86         2586  
12 86     86   421 use Sisimai::Address;
  86         97  
  86         1577  
13 86     86   36216 use Sisimai::DateTime;
  86         352  
  86         3135  
14 86     86   29616 use Sisimai::Time;
  86         208  
  86         681  
15 86     86   35337 use Sisimai::SMTP::Command;
  86         311  
  86         2717  
16 86     86   31851 use Sisimai::SMTP::Failure;
  86         218  
  86         2720  
17 86     86   472 use Sisimai::String;
  86         105  
  86         1575  
18 86     86   29123 use Sisimai::Rhost;
  86         228  
  86         2644  
19 86     86   29547 use Sisimai::LDA;
  86         187  
  86         5957  
20 86         878 use Class::Accessor::Lite ('new' => 0, 'rw' => [
21             'action', # [String] The value of Action: header
22             'addresser', # [Sisimai::Address] From address
23             'alias', # [String] Alias of the recipient address
24             'bogus', # [Integer] EXPERIMENTAL
25             'catch', # [?] Results generated by hook method
26             'command', # [String] The last SMTP command
27             'decodedby', # [String] MTA Module name since v5.2.0
28             'deliverystatus', # [String] Delivery Status(DSN)
29             'destination', # [String] The domain part of the "recipient"
30             'diagnosticcode', # [String] Diagnostic-Code: Header
31             'diagnostictype', # [String] The 1st part of Diagnostic-Code: Header
32             'feedbackid', # [String] The value of Feedback-ID: header of the original message
33             'feedbacktype', # [String] Feedback Type
34             'hardbounce', # [Integer] 1 = Hard bounce, 0 = Is not a hard bounce
35             'lhost', # [String] local host name/Local MTA
36             'listid', # [String] List-Id header of each ML
37             'messageid', # [String] Message-Id: header
38             'origin', # [String] Email path as a data source
39             'reason', # [String] Bounce reason
40             'recipient', # [Sisimai::Address] Recipient address which bounced
41             'replycode', # [String] SMTP Reply Code
42             'rhost', # [String] Remote host name/Remote MTA
43             'senderdomain', # [String] The domain part of the "addresser"
44             'subject', # [String] UTF-8 Subject text
45             'timestamp', # [Sisimai::Time] Date: header in the original message
46             'timezoneoffset', # [Integer] Time zone offset(seconds)
47             'token', # [String] Message token/MD5 Hex digest value
48             'toxic', # [Integer] EXPERIMENTAL
49 86     86   445 ]);
  86         127  
50              
51             sub rise {
52             # Constructor of Sisimai::Fact
53             # @param [Hash] argvs
54             # @options argvs [String] data Entire email message
55             # @options argvs [Integer] delivered 1 if the result which has "delivered" reason is included
56             # @options argvs [Integer] vacation 1 if the result which has "vacation" reason is included
57             # @options argvs [Code] hook Code reference to callback method
58             # @options argvs [String] origin Path to the original email file
59             # @return [Array] Array of Sisimai::Fact objects
60 3514     3514 1 222185 my $class = shift;
61 3514   100     7625 my $argvs = shift || return undef;
62 3513 50       8422 die ' ***error: Sisimai::Fact->rise receives only a HASH reference as an argument' unless ref $argvs eq 'HASH';
63              
64 3513   100     7609 my $email = $argvs->{'data'} || return undef;
65 3512         8822 my $args1 = {'data' => $email, 'hook' => $argvs->{'hook'}};
66 3512   100     17258 my $mesg1 = Sisimai::Message->rise($args1) || return undef;
67              
68 3471 50       7760 return undef unless $mesg1->{'ds'};
69 3471 50       7187 return undef unless $mesg1->{'rfc822'};
70              
71 3471         4636 state $retryindex = Sisimai::Reason->retry;
72 3471         3712 state $rfc822head = Sisimai::RFC5322::HEADERTABLE;
73 3471         4062 state $actionlist = {'delayed' => 1, 'delivered' => 1, 'expanded' => 1, 'failed' => 1, 'relayed' => 1};
74 3471         5402 my $rfc822data = $mesg1->{'rfc822'};
75 3471         3767 my $listoffact = [];
76              
77 3471         6865 RISEOF: for my $e ( $mesg1->{'ds'}->@* ) {
78             # Create parameters
79 3658 50       7692 next if length $e->{'recipient'} < 5;
80 3658 100 100     12943 next if ! $argvs->{'delivered'} && index($e->{'status'}, '2.') == 0;
81 3646 100 100     11038 next if ! $argvs->{'vacation'} && $e->{'reason'} eq 'vacation';
82              
83 3625         4353 my $thing = {}; # To be blessed and pushed into the array above at the end of the loop
84             my $piece = {
85             'action' => $e->{'action'} // '',
86             'alias' => $e->{'alias'} // '',
87             'catch' => $mesg1->{'catch'} // undef,
88             'decodedby' => $e->{'agent'} // '',
89             'deliverystatus' => $e->{'status'} // '',
90             'diagnosticcode' => $e->{'diagnosis'} // '',
91             'diagnostictype' => $e->{'spec'} // '',
92             'feedbacktype' => $e->{'feedbacktype'} // '',
93             'hardbounce' => 0,
94             'lhost' => $e->{'lhost'} // '',
95             'origin' => $argvs->{'origin'} // '',
96             'reason' => $e->{'reason'} // '',
97             'recipient' => $e->{'recipient'} // '',
98             'replycode' => $e->{'replycode'} // '',
99             'rhost' => $e->{'rhost'} // '',
100 3625   50     72997 'command' => $e->{'command'} // '',
      50        
      100        
      50        
      50        
      50        
      50        
      50        
      100        
      100        
      50        
      50        
      50        
      50        
      50        
101             };
102              
103             ADDRESSER: {
104             # Detect an email address from message/rfc822 part
105 3625         4429 my $j = [];
  3625         3613  
106 3625         6858 for my $f ( $rfc822head->{'addresser'}->@* ) {
107             # Check each header in message/rfc822 part
108 5472 100       11325 next unless exists $rfc822data->{ $f };
109 3379 50       5918 next unless $rfc822data->{ $f };
110              
111 3379   50     13026 $j = Sisimai::Address->find($rfc822data->{ $f }) || next;
112 3379         6350 $piece->{'addresser'} = shift @$j;
113 3379         6039 last ADDRESSER;
114             }
115              
116             # Fallback: Get the sender address from the header of the bounced email if the address
117             # is not set at the loop above.
118 246   50     1327 $j = Sisimai::Address->find($mesg1->{'header'}->{'to'}) || [];
119 246         598 $piece->{'addresser'} = shift @$j;
120             }
121 3625 50       5975 next RISEOF unless $piece->{'addresser'};
122              
123             TIMESTAMP: {
124             # Convert from a time stamp or a date string to a machine time.
125 3625         4231 my $datestring = undef;
  3625         4634  
126 3625         3501 my $zoneoffset = 0;
127 3625 100       3588 my @datevalues; push @datevalues, $e->{'date'} if $e->{'date'};
  3625         8311  
128              
129             # Date information did not exist in message/delivery-status part,...
130 3625         6849 for my $f ( $rfc822head->{'date'}->@* ) {
131             # Get the value of Date header or other date related header.
132 14500 100       21523 next unless $rfc822data->{ $f };
133 3185         6121 push @datevalues, $rfc822data->{ $f };
134             }
135              
136             # Set "date" getting from the value of "Date" in the bounce message
137 3625 100       8008 push @datevalues, $mesg1->{'header'}->{'date'} if scalar(@datevalues) < 2;
138              
139 3625         8904 while( my $v = shift @datevalues ) {
140             # Decode each date value in the array
141 3625   50     22408 $datestring = Sisimai::DateTime->parse($v) || next;
142              
143 3625 50       16432 if( $datestring =~ /\A(.+)[ ]+([-+]\d{4})\z/ ) {
144             # Get the value of timezone offset from $datestring: Wed, 26 Feb 2014 06:05:48 -0500
145 3625         6416 $datestring = $1;
146 3625         10240 $zoneoffset = Sisimai::DateTime->tz2second($2);
147 3625         7763 $piece->{'timezoneoffset'} = $2;
148             }
149 3625 50       7884 last if $datestring;
150             }
151              
152 3625         4914 eval {
153             # Convert from the date string to an object then calculate time zone offset.
154 3625         22367 my $t = Sisimai::Time->strptime($datestring, '%a, %d %b %Y %T');
155 3625   50     136397 $piece->{'timestamp'} = ($t->epoch - $zoneoffset) // undef;
156             };
157             }
158 3625 50       176746 next RISEOF unless defined $piece->{'timestamp'};
159              
160             RECEIVED: {
161             # Scan "Received:" header of the original message
162 3625   50     5540 my $recv = $mesg1->{'header'}->{'received'} || [];
  3625         10540  
163 3625 100       7215 unless( $piece->{'rhost'} ) {
164             # Try to pick a remote hostname from Received: headers of the bounce message
165 1675         8901 my $ir = Sisimai::RFC1123->find($e->{'diagnosis'});
166 1675 100       3558 $piece->{'rhost'} = $ir if Sisimai::RFC1123->is_internethost($ir);
167              
168 1675 100       3919 unless( $piece->{'rhost'} ) {
169             # The remote hostname in the error message did not exist or is not a valid
170             # internet hostname
171 1259         2411 for my $re ( reverse @$recv ) {
172             # Check the Received: headers backwards and get a remote hostname
173 1973 100       4011 last if $piece->{'rhost'};
174 1529         5003 my $cv = Sisimai::RFC5322->received($re)->[0];
175 1529 100       4852 next unless Sisimai::RFC1123->is_internethost($cv);
176 1111         2692 $piece->{'rhost'} = $cv;
177             }
178             }
179             }
180 3625 100       8274 $piece->{'lhost'} = '' if $piece->{'lhost'} eq $piece->{'rhost'};
181              
182 3625 100       6022 unless( $piece->{'lhost'} ) {
183             # Try to pick a local hostname from Received: headers of the bounce message
184 1588         2733 for my $le ( @$recv ) {
185             # Check the Received: headers forwards and get a local hostname
186 1781         4489 my $cv = Sisimai::RFC5322->received($le)->[0];
187 1781 100       6100 next unless Sisimai::RFC1123->is_internethost($cv);
188 1187         2185 $piece->{'lhost'} = $cv; last;
  1187         1973  
189             }
190             }
191              
192 3625         5135 for my $v ('rhost', 'lhost') {
193             # Check and rewrite each host name
194 7250 100       11348 next unless length $piece->{ $v };
195 6701 50       10283 if( index($piece->{ $v }, '@') > -1 ) {
196             # Use the domain part as a remote/local host when the value is an email address
197 0         0 $piece->{ $v } = (split('@', $piece->{ $v }))[-1];
198             }
199 6701         15736 y/[]()\r//d, s/\A.+=// for $piece->{ $v }; # Remove [], (), \r, and strings before "="
200              
201 6701 100       9554 if( index($piece->{ $v }, ' ') > -1 ) {
202             # Check a space character in each value and get the first hostname
203 77         191 my @ee = split(' ', $piece->{ $v });
204 77         118 for my $w ( @ee ) {
205             # get a hostname from the string like "127.0.0.1 x109-20.example.com 192.0.2.20"
206             # or "mx.sp.example.jp 192.0.2.135"
207 77 50       320 next if Sisimai::RFC791->is_ipv4address($w);
208 77         138 $piece->{ $v } = $w; last;
  77         79  
209             }
210 77 50       187 $piece->{ $v } = $ee[0] if index($piece->{ $v }, ' ') > 0;
211             }
212 6701 100       14173 chop $piece->{ $v } if substr($piece->{ $v }, -1, 1) eq '.'; # Remove "." at the end of the value
213             }
214             }
215              
216             ID_HEADERS: {
217             # Message-ID:, List-ID: headers of the original message
218 3625         4542 my $p0 = 0;
  3625         3742  
219 3625         3945 my $p1 = 0;
220 3625 100       16197 if( Sisimai::String->aligned(\$rfc822data->{'message-id'}, ['<', '@', '>']) ) {
221             # https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4
222             # Leave only string inside of angle brackets(<>)
223 3093         5553 $p0 = index($rfc822data->{'message-id'}, '<') + 1;
224 3093         3930 $p1 = index($rfc822data->{'message-id'}, '>');
225 3093         7727 $piece->{'messageid'} = substr($rfc822data->{'message-id'}, $p0, $p1 - $p0);
226              
227             } else {
228             # Invalid value of the Message-Id: field
229 532         974 $piece->{'messageid'} = '';
230             }
231              
232 3625 100       12840 if( Sisimai::String->aligned(\$rfc822data->{'list-id'}, ['<', '.', '>']) ) {
233             # https://www.rfc-editor.org/rfc/rfc2919
234             # Get the value of List-Id header: "List name "
235 27         59 $p0 = index($rfc822data->{'list-id'}, '<') + 1;
236 27         57 $p1 = index($rfc822data->{'list-id'}, '>');
237 27         85 $piece->{'listid'} = substr($rfc822data->{'list-id'}, $p0, $p1 - $p0);
238              
239             } else {
240             # Invalid value of the List-Id: field
241 3598         6815 $piece->{'listid'} = '';
242             }
243             }
244              
245             DIAGNOSTICCODE: {
246             # Cleanup the value of "Diagnostic-Code:" header
247 3625 100       4433 last unless length $piece->{'diagnosticcode'};
  3625         6577  
248              
249             # Get an SMTP Reply Code and an SMTP Enhanced Status Code
250 3615 50       7296 chop $piece->{'diagnosticcode'} if substr($piece->{'diagnosticcode'}, -1, 1) eq "\r";
251              
252 3615   100     17555 my $cs = Sisimai::SMTP::Status->find($piece->{'diagnosticcode'}) || '';
253 3615   100     18355 my $cr = Sisimai::SMTP::Reply->find( $piece->{'diagnosticcode'}, $cs) || '';
254 3615         12313 $piece->{'deliverystatus'} = Sisimai::SMTP::Status->prefer($piece->{'deliverystatus'}, $cs, $cr);
255              
256 3615 100       8914 if( length $cr == 3 ) {
257             # There is an SMTP reply code in the error message
258 2600   66     7154 $piece->{'replycode'} ||= $cr;
259              
260 2600 100       6824 if( index($piece->{'diagnosticcode'}, $cr.'-') > -1 ) {
261             # 550-5.7.1 [192.0.2.222] Our system has detected that this message is
262             # 550-5.7.1 likely unsolicited mail. To reduce the amount of spam sent to Gmail,
263             # 550-5.7.1 this message has been blocked. Please visit
264             # 550 5.7.1 https://support.google.com/mail/answer/188131 for more information.
265             #
266             # kijitora@example.co.uk
267             # host c.eu.example.com [192.0.2.3]
268             # SMTP error from remote mail server after end of data:
269             # 553-SPF (Sender Policy Framework) domain authentication
270             # 553-fail. Refer to the Troubleshooting page at
271             # 553-http://www.symanteccloud.com/troubleshooting for more
272             # 553 information. (#5.7.1)
273 207         391 for my $q ('-', ' ') {
274             # Remove strings: "550-5.7.1", and "550 5.7.1" from the error message
275 414         574 my $cx = sprintf("%s%s%s", $cr, $q, $cs);
276 414         758 my $p0 = index($piece->{'diagnosticcode'}, $cx);
277 414         889 while( $p0 > -1 ) {
278             # Remove strings like "550-5.7.1"
279 801         1180 substr($piece->{'diagnosticcode'}, $p0, length $cx, '');
280 801         1335 $p0 = index($piece->{'diagnosticcode'}, $cx);
281             }
282              
283             # Remove "553-" and "553 " (SMTP reply code only) from the error message
284 414         462 $cx = sprintf("%s%s", $cr, $q);
285 414         561 $p0 = index($piece->{'diagnosticcode'}, $cx);
286 414         762 while( $p0 > -1 ) {
287             # Remove strings like "553-"
288 42         98 substr($piece->{'diagnosticcode'}, $p0, length $cx, '');
289 42         84 $p0 = index($piece->{'diagnosticcode'}, $cx);
290             }
291             }
292              
293 207 50       617 if( index($piece->{'diagnosticcode'}, $cr) > 1 ) {
294             # Add "550 5.1.1" into the head of the error message when the error
295             # message does not begin with "550"
296 0         0 $piece->{'diagnosticcode'} = sprintf("%s %s %s", $cr, $cs, $piece->{'diagnosticcode'});
297             }
298             }
299             }
300              
301 3615         8288 my $dc = lc $piece->{'diagnosticcode'};
302 3615         5851 my $p1 = index($dc, '');
303 3615         4542 my $p2 = index($dc, '');
304 3615 100 66     7785 substr($piece->{'diagnosticcode'}, $p1, $p2 + 7 - $p1, '') if $p1 > 0 && $p2 > 0;
305 3615         10788 $piece->{'diagnosticcode'} = Sisimai::String->sweep($piece->{'diagnosticcode'});
306             }
307              
308             DIAGNOSTICTYPE: {
309             # Set the value of "diagnostictype" if it is empty
310 3625 50 0     3955 $piece->{'diagnostictype'} ||= 'X-UNIX' if $piece->{'reason'} eq 'mailererror';
  3625         7624  
311 3625 100 100     5084 $piece->{'diagnostictype'} ||= 'SMTP' unless grep { $piece->{'reason'} eq $_ } ('feedback', 'vacation');
  7250         18692  
312             }
313              
314             # Check the Subject field of the original message
315 3625   50     10195 $piece->{'subject'} = $rfc822data->{'subject'} // '';
316 3625 50       8808 chop $piece->{'subject'} if substr($piece->{'subject'}, -1, 1) eq "\r";
317              
318             # When "RCPT first" in the error message, set "RCPT" as the last command.
319             # - <<< 503 RCPT first (#5.5.1)
320             # - <<< 503-5.5.1 RCPT first. A mail transaction protocol command was issued ...
321             # - RCPT first (in reply to DATA command)
322 3625 100       15775 $piece->{'command'} = '' unless Sisimai::SMTP::Command->test($piece->{'command'});
323 3625 100       9377 $piece->{'command'} = 'RCPT' if index($piece->{'diagnosticcode'}, 'RCPT first') > -1;
324              
325             CONSTRUCTOR: {
326             # Create email address object
327 3625   50     3425 my $as = Sisimai::Address->new($piece->{'addresser'}) || next RISEOF;
  3625         11528  
328 3625   50     14098 my $ar = Sisimai::Address->new({'address' => $piece->{'recipient'}}) || next RISEOF;
329 3625         16807 my @ea = (qw|
330             action command decodedby deliverystatus diagnosticcode diagnostictype feedbacktype
331             lhost listid messageid origin reason replycode rhost subject
332             |);
333              
334             $thing = {
335             'addresser' => $as,
336             'recipient' => $ar,
337             'senderdomain' => $as->host,
338             'destination' => $ar->host,
339             'alias' => $piece->{'alias'} || $ar->alias,
340 3625   66     12865 'token' => __PACKAGE__->maketoken($as->address, $ar->address, $piece->{'timestamp'}),
341             };
342 3625   50     112315 $thing->{ $_ } ||= $piece->{ $_ } // '' for @ea;
      66        
343 3625         7417 $thing->{'bogus'} = 0;
344 3625   100     15253 $thing->{'catch'} = $piece->{'catch'} // undef;
345 3625         6411 $thing->{"feedbackid"} = "";
346 3625         6721 $thing->{'hardbounce'} = int $piece->{'hardbounce'};
347 3625   100     9130 $thing->{'replycode'} ||= Sisimai::SMTP::Reply->find($piece->{'diagnosticcode'}) || '';
      100        
348 3625         13702 $thing->{'timestamp'} = Sisimai::Time->new($piece->{'timestamp'});
349 3625   50     268403 $thing->{'timezoneoffset'} = $piece->{'timezoneoffset'} // '+0000';
350 3625         11821 $thing->{'toxic'} = 0;
351             }
352              
353             ALIAS: {
354             # Look up the Envelope-To address from the Received: header in the original message
355             # when the recipient address is same with the value of $o->{'alias'}.
356 3625 100 100     4049 last if length $thing->{'alias'} == 0 || $thing->{'recipient'}->address ne $thing->{'alias'};
  3625         11919  
357 676 50       6051 last unless exists $rfc822data->{'received'};
358 676 100       1644 last unless scalar $rfc822data->{'received'}->@*;
359              
360 603         1304 for my $er ( reverse $rfc822data->{'received'}->@* ) {
361             # Search for the string " for " from the Received: header
362 1093 100       5114 next unless index($er, ' for ') > 1;
363 791         2835 my $or = Sisimai::RFC5322->received($er);
364              
365 791 50 33     3052 next if scalar(@$or) == 0 || length($or->[5]) == 0;
366 791 50       2933 next if Sisimai::Address->is_emailaddress($or->[5]) == 0;
367 791 100       3165 next if $thing->{'recipient'}->address eq $or->[5];
368              
369 33         305 $thing->{'alias'} = $or->[5];
370 33         99 last ALIAS;
371             }
372             }
373 3625 100       13747 $thing->{'alias'} = '' if $thing->{'alias'} eq $thing->{'recipient'}->{'address'};
374              
375             REASON: {
376             # Decide the reason of the email bounce
377 3625 100 100     4831 if( $thing->{'reason'} eq '' || exists $retryindex->{ $thing->{'reason'} } ) {
  3625         11296  
378             # The value of "reason" is empty or is needed to check with other values again
379 3360   100     9023 my $re = $thing->{'reason'} || 'undefined';
380 3360         3773 my $cr = "Sisimai::Reason";
381 3360 100       19429 my $or = Sisimai::LDA->find($thing); if( $cr->is_explicit($or) ){ $thing->{'reason'} = $or; last }
  3360         16375  
  32         50  
  32         60  
382 3328 100       14535 $or = Sisimai::Rhost->find($thing); if( $cr->is_explicit($or) ){ $thing->{'reason'} = $or; last }
  3328         9246  
  833         1345  
  833         1533  
383 2495 100       7978 $or = Sisimai::Reason->find($thing); if( $cr->is_explicit($or) ){ $thing->{'reason'} = $or; last }
  2495         6644  
  2453         4136  
  2453         4112  
384 42 100       173 $thing->{'reason'} = $thing->{'diagnosticcode'} ? "onhold" : $re;
385             }
386             }
387              
388             HARDBOUNCE: {
389             # Set the value of "hardbounce", default value of "bouncebounce" is 0
390 3625 100 100     3933 if( $thing->{'reason'} eq 'delivered' || $thing->{'reason'} eq 'feedback' || $thing->{'reason'} eq 'vacation' ) {
  3625   100     16850  
391             # Delete the value of ReplyCode when the Reason is "feedback" or "vacation"
392 120 100       323 $thing->{'replycode'} = '' unless $thing->{'reason'} eq 'delivered';
393              
394             } else {
395             # The reason is not "delivered", or "feedback", or "vacation"
396 3505         7684 my $smtperrors = $piece->{'deliverystatus'}.' '.$piece->{'diagnosticcode'};
397 3505 100       6658 $smtperrors = '' if length $smtperrors < 4;
398 3505         18994 $thing->{'hardbounce'} = Sisimai::SMTP::Failure->is_hardbounce($thing->{'reason'}, $smtperrors);
399             }
400             }
401              
402             DELIVERYSTATUS: {
403             # Set pseudo status code
404 3625 100       3957 last DELIVERYSTATUS if $thing->{'deliverystatus'};
  3625         7722  
405              
406 796         1850 my $smtperrors = $thing->{'replycode'}.' '.$piece->{'diagnosticcode'};
407 796 100       1397 $smtperrors = '' if length $smtperrors < 4;
408 796         2296 my $permanent0 = Sisimai::SMTP::Failure->is_permanent($smtperrors);
409 796         1973 my $temporary0 = Sisimai::SMTP::Failure->is_temporary($smtperrors);
410 796 100       2229 my $temporary1 = $permanent0.$temporary0 eq "00" ? 0 : $temporary0;
411 796         3049 $thing->{'deliverystatus'} = Sisimai::SMTP::Status->code($thing->{'reason'}, $temporary1);
412             }
413              
414             REPLYCODE: {
415             # Check both of the first digit of "deliverystatus" and "replycode"
416 3625         3464 my $cx = [substr($thing->{'deliverystatus'}, 0, 1), substr($thing->{'replycode'}, 0, 1)];
  3625         11294  
417 3625 100       7678 if( $cx->[0] ne $cx->[1] ) {
418             # The class of the "Status:" is defer with the first digit of the reply code
419 940   100     3036 $cx->[1] = Sisimai::SMTP::Reply->find($piece->{'diagnosticcode'}, $cx->[0]) || '';
420 940 100       2811 $thing->{'replycode'} = index($cx->[1], $cx->[0]) == 0 ? $cx->[1] : '';
421             }
422              
423 3625 100       11204 unless( exists $actionlist->{ $thing->{'action'} } ) {
424             # There is an action value which is not described at RFC1894
425 1685 50       10372 if( my $ox = Sisimai::RFC1894->field('Action: '.$thing->{'action'}) ) {
426             # Rewrite the value of "Action:" field to the valid value
427             #
428             # The syntax for the action-field is:
429             # action-field = "Action" ":" action-value
430             # action-value = "failed" / "delayed" / "delivered" / "relayed" / "expanded"
431 1685         2888 $thing->{'action'} = $ox->[2];
432             }
433             }
434 3625 100       7811 $thing->{'action'} = 'delivered' if $thing->{'reason'} eq 'delivered';
435 3625 100 100     6595 $thing->{'action'} ||= 'delayed' if $thing->{'reason'} eq 'expired';
436 3625 100 100     17137 $thing->{'action'} ||= 'failed' if $cx->[0] eq '4' || $cx->[0] eq '5';
      100        
437 3625   100     8217 $thing->{'action'} ||= "";
438             }
439              
440 3625 100       7118 if( $thing->{'replycode'} ne "" ) {
441             # Fill empty values: ["SMTP Command", "DSN", "Reason"]
442 2579         9535 my $cv = Sisimai::SMTP::Reply->associatedwith($thing->{'replycode'});
443 2579 100       5562 if( scalar @$cv > 0 ) {
444 334 100 100     1237 $thing->{'command'} = $cv->[0] if $cv->[0] ne "" && $thing->{'command'} eq "";
445 334 50 66     918 $thing->{'deliverystatus'} = $cv->[1] if $cv->[1] ne "" && Sisimai::SMTP::Status->is_explicit($thing->{'deliverystatus'}) == 0;
446 334 100 66     1442 $thing->{'reason'} = $cv->[2] if $cv->[2] ne "" && Sisimai::Reason->is_explicit($thing->{'reason'}) == 0;
447             }
448             }
449             # Feedback-ID: 1.us-west-2.QHuyeCQrGtIIMGKQfVdUhP9hCQR2LglVOrRamBc+Prk=:AmazonSES
450 3625   100     12457 $thing->{'feedbackid'} = $rfc822data->{'feedback-id'} || "";
451              
452 3625         31455 push @$listoffact, bless($thing, __PACKAGE__);
453             } # End of for(RISEOF)
454              
455 3471         56504 return $listoffact;
456             }
457              
458             sub maketoken {
459             # Create the message token from an addresser and a recipient
460             # @param [String] addr1 A sender's email address
461             # @param [String] addr2 A recipient's email address
462             # @param [Integer] epoch Machine time of the email bounce
463             # @return [String] Message token(MD5 hex digest) or empty string
464             # if the any argument is missing
465             # @see http://en.wikipedia.org/wiki/ASCII
466             # @see https://metacpan.org/pod/Digest::MD5
467 3631   50 3631 0 82855 my $class = shift || return '';
468 3631   100     5627 my $addr1 = shift || return '';
469 3630   100     5217 my $addr2 = shift || return '';
470 3629   100     5502 my $epoch = shift // return '';
471              
472             # Format: STX(0x02) Sender-Address RS(0x1e) Recipient-Address ETX(0x03)
473 3628         53409 return Digest::SHA::sha1_hex(sprintf("\x02%s\x1e%s\x1e%d\x03", lc $addr1, lc $addr2, $epoch));
474             }
475              
476              
477             sub damn {
478             # Convert from object to hash reference
479             # @return [Hash] Data in Hash reference
480 2949     2949 1 7679220 my $self = shift;
481 2949         6619 my $data = undef;
482 2949         5071 state $stringdata = [qw|
483             action alias catch command decodedby deliverystatus destination diagnosticcode diagnostictype
484             feedbackid feedbacktype lhost listid messageid origin reason replycode rhost senderdomain
485             subject timezoneoffset token
486             |];
487 2949         4483 eval {
488 2949         4763 my $v = {};
489 2949   100     15251 $v->{ $_ } = $self->$_ // '' for @$stringdata;
490 2949         266259 $v->{'hardbounce'} = int $self->hardbounce;
491 2949         15007 $v->{'bogus'} = int $self->bogus;
492 2949         11926 $v->{'toxic'} = int $self->toxic;
493 2949         12417 $v->{'addresser'} = $self->addresser->address;
494 2949         30803 $v->{'recipient'} = $self->recipient->address;
495 2949         20736 $v->{'timestamp'} = $self->timestamp->epoch;
496 2949         34631 $data = $v;
497             };
498 2949         54573 return $data;
499             }
500              
501             sub dump {
502             # Data dumper
503             # @param [String] type Data format: json, yaml
504             # @return [String] Dumped data or an empty string when the argument is neither "json" nor "yaml"
505 1531     1531 0 16194006 my $self = shift;
506 1531 100 100     7205 my $type = shift || 'json'; return "" unless $type =~ /\A(?:json|yaml)\z/;
  1531         9870  
507              
508 1493         4254 my $referclass = 'Sisimai::Fact::'.uc($type);
509 1493         3305 my $modulepath = 'Sisimai/Fact/'.uc($type).'.pm';
510              
511 1493         49312 require $modulepath;
512 1493         10278 return $referclass->dump($self);
513             }
514              
515             1;
516             __END__