File Coverage

lib/Sisimai/Reason.pm
Criterion Covered Total %
statement 91 94 96.8
branch 45 52 86.5
condition 49 66 74.2
subroutine 10 10 100.0
pod 1 7 14.2
total 196 229 85.5


line stmt bran cond sub pod time code
1             package Sisimai::Reason;
2 87     87   1615 use v5.26;
  87         232  
3 87     87   335 use strict;
  87         134  
  87         1592  
4 87     87   308 use warnings;
  87         165  
  87         86890  
5              
6             my $ModulePath = __PACKAGE__->path;
7             my $GetRetried = __PACKAGE__->retry;
8             my $ClassOrder = [
9             # 0. true() meethod in the following reasons are called from Reason->find()
10             [qw/MailboxFull EmailTooLarge Suspend HasMoved NoRelaying AuthFailure UserUnknown Filtered RequirePTR
11             NotCompliantRFC BadReputation ContentError Rejected HostUnknown SpamDetected RateLimited Blocked
12             FailedSTARTTLS NotAccept VirusDetected PolicyViolation/
13             ],
14             # 1. match() method in the following reasons are called from Reason->find()
15             [qw/MailboxFull SpamDetected VirusDetected NoRelaying SystemError NetworkError Suspend SystemFull
16             Suppressed MailerError SecurityError PolicyViolation SyntaxError Expired/
17             ],
18             [qw/MailboxFull EmailTooLarge Suspend UserUnknown Filtered Rejected HostUnknown SpamDetected
19             RateLimited Blocked SpamDetected AuthFailure FailedSTARTTLS SecurityError SystemError
20             NetworkError Suspend Expired ContentError HasMoved SystemFull NotAccept MailerError
21             NoRelaying Suppressed SyntaxError OnHold/
22             ],
23             ];
24              
25             sub retry {
26             # Reason list better to retry detecting an error reason
27             # @return [Hash] Reason list
28             return {
29 173     173 0 28455 'undefined' => 1, 'onhold' => 1, 'systemerror' => 1, 'securityerror' => 1, 'expired' => 1,
30             'networkerror' => 1, 'hostunknown' => 1, 'userunknown'=> 1
31             };
32             }
33              
34             sub is_explicit {
35             # is_explicit() returns 0 when the argument is empty or is "undefined" or is "onhold"
36             # @param string argv1 Reason name
37             # @return bool false: The reaosn is not explicit
38 9557     9557 0 27886 my $class = shift;
39 9557   100     19144 my $argv1 = shift || return 0;
40              
41 3734 100 100     18446 return 0 if $argv1 eq "undefined" || $argv1 eq "onhold" || $argv1 eq "";
      66        
42 3671         8888 return 1;
43             }
44              
45             sub index {
46             # All the error reason list Sisimai support
47             # @return [Array] Reason list
48 172     172 0 13242 return [qw/
49             AuthFailure BadReputation Blocked ContentError Expired FailedSTARTTLS Filtered HasMoved
50             HostUnknown MailboxFull MailerError EmailTooLarge NetworkError NotAccept NotCompliantRFC
51             OnHold Rejected NoRelaying SpamDetected VirusDetected PolicyViolation SecurityError
52             Suspend RequirePTR SystemError SystemFull RateLimited Suppressed UserUnknown SyntaxError/
53             ];
54             }
55              
56             sub path {
57             # Returns Sisimai::Reason::* module path table
58             # @return [Hash] Module path table
59             # @since v4.25.6
60 89     89 0 194 my $class = shift;
61 89         209 my $index = __PACKAGE__->index;
62 89         173 my $table = {}; $table->{ __PACKAGE__.'::'.$_ } = 'Sisimai/Reason/'.$_.'.pm' for @$index;
  89         2626  
63 89         342 return $table;
64             }
65              
66             sub find {
67             # Detect the bounce reason
68             # @param [Hash] argvs Decoded email object
69             # @return [String] Bounce reason or an empty string if the argument is missing or not HASH
70             # @see anotherone
71 2496     2496 0 226121 my $class = shift;
72 2496   100     4358 my $argvs = shift // return "";
73              
74             # Return a reason text already decided except a reason matched with the regular expression of
75             # Sisimai::Reason->retry() method.
76 2495 50 66     10482 return $argvs->{'reason'} if( (not exists $GetRetried->{ $argvs->{'reason'} }) && $argvs->{'reason'} );
77 2495 100       6054 return 'delivered' if substr($argvs->{'deliverystatus'}, 0, 2) eq '2.';
78              
79 2493         3115 my $reasontext = '';
80 2493   100     4430 my $issuedcode = $argvs->{'diagnosticcode'} || '';
81 2493   50     5340 my $codeformat = $argvs->{'diagnostictype'} || '';
82 2493 100 66     5574 if( $codeformat eq 'SMTP' || $codeformat eq '' ) {
83             # Diagnostic-Code: SMTP; ... or empty value
84 2422         4540 for my $e ( $ClassOrder->[0]->@* ) {
85             # Check the values of Diagnostic-Code: and Status: fields using true() method of each
86             # child class in Sisimai::Reason
87 27304         28146 my $p = 'Sisimai::Reason::'.$e;
88 27304         435127 require $ModulePath->{ $p };
89              
90 27304 100       133368 next unless $p->true($argvs);
91 1835         5414 $reasontext = $p->text;
92 1835         2630 last;
93             }
94             }
95              
96 2493 100 66     6042 if( not $reasontext || $reasontext eq 'undefined' ) {
97             # Bounce reason is not detected yet.
98 658         2027 $reasontext = __PACKAGE__->anotherone($argvs);
99 658 50       1366 $reasontext = '' if $reasontext eq 'undefined';
100 658 100 50     1610 $reasontext ||= 'expired' if $argvs->{'action'} eq 'delayed';
101 658 100       2916 return $reasontext if $reasontext;
102              
103             # Try to match with message patterns in Sisimai::Reason::Vacation
104 42         5014 require Sisimai::Reason::Vacation;
105 42 50       402 $reasontext = 'vacation' if Sisimai::Reason::Vacation->match(lc $issuedcode);
106 42 100 50     218 $reasontext ||= 'onhold' if $issuedcode;
107 42   100     104 $reasontext ||= 'undefined';
108             }
109 1877         5233 return $reasontext;
110             }
111              
112             sub anotherone {
113             # Detect the other bounce reason, fall back method for find()
114             # @param [Hash] argvs Decoded email structure
115             # @return [String] Bounce reason or an empty string if the argument is missing or not HASH
116             # @see find()
117 659     659 0 826 my $class = shift;
118 659 100 100     1182 my $argvs = shift // return ""; return $argvs->{'reason'} if $argvs->{'reason'};
  658         1528  
119              
120 628         1799 require Sisimai::SMTP::Status;
121 628   50     1941 my $issuedcode = lc $argvs->{'diagnosticcode'} // '';
122 628   50     1766 my $codeformat = $argvs->{'diagnostictype'} // '';
123 628   50     1403 my $actiontext = $argvs->{'action'} // '';
124 628   50     1529 my $statuscode = $argvs->{'deliverystatus'} // '';
125 628   100     2005 my $reasontext = Sisimai::SMTP::Status->name($statuscode) || '';
126 628 100       1386 my $trytomatch = $reasontext eq '' ? 1 : 0;
127 628 100 100     2471 $trytomatch = 1 if exists $GetRetried->{ $reasontext } || $codeformat ne 'SMTP';
128              
129 628         1149 while($trytomatch) {
130             # Could not decide the reason by the value of Status:
131 593         1461 for my $e ( $ClassOrder->[1]->@* ) {
132             # Trying to match with other patterns in Sisimai::Reason::* classes
133 6122         6196 my $p = 'Sisimai::Reason::'.$e;
134 6122         118397 require $ModulePath->{ $p };
135              
136 6122 100       23445 next unless $p->match($issuedcode);
137 448         1128 $reasontext = lc $e;
138 448         641 last;
139             }
140 593 100       1454 last if $reasontext;
141              
142             # Check the value of Status:
143 83   100     305 my $code2digit = substr($statuscode, 0, 3) || '';
144 83 50 33     834 if( $code2digit eq '5.6' || $code2digit eq '4.6' ) {
    50 33        
    100          
145             # X.6.0 Other or undefined media error
146 0         0 $reasontext = 'contenterror';
147              
148             } elsif( $code2digit eq '5.7' || $code2digit eq '4.7' ) {
149             # X.7.0 Other or undefined security status
150 0         0 $reasontext = 'securityerror';
151              
152             } elsif( CORE::index($codeformat, 'X-UNIX') == 0 ) {
153             # Diagnostic-Code: X-UNIX; ..., X-Postfix, or other X-*
154 15         27 $reasontext = 'mailererror';
155              
156             } else {
157             # 50X Syntax Error?
158 68         231 require Sisimai::Reason::SyntaxError;
159 68 50       214 $reasontext = 'syntaxerror' if Sisimai::Reason::SyntaxError->true($argvs);
160             }
161 83 100       245 last if $reasontext;
162              
163             # Check the value of Action: field, first
164 68 100 66     328 if( CORE::index($actiontext, 'delayed') == 0 || CORE::index($actiontext, 'expired') == 0 ) {
165             # Action: delayed, expired
166 5         8 $reasontext = 'expired';
167              
168             } else {
169             # Check the value of SMTP command
170 63   50     232 my $thecommand = $argvs->{'command'} // '';
171 63 100 100     298 if( $thecommand eq 'EHLO' || $thecommand eq 'HELO' ) {
172             # Rejected at connection or after EHLO|HELO
173 21         36 $reasontext = 'blocked';
174             }
175             }
176 68         107 last;
177             }
178 628         1489 return $reasontext;
179             }
180              
181             sub match {
182             # Detect the bounce reason from given text
183             # @param [String] argv1 Error message
184             # @return [String] Bounce reason
185 189     189 1 193061 my $class = shift;
186 189   100     438 my $argv1 = shift // return "";
187              
188 188         246 my $reasontext = '';
189 188         276 my $issuedcode = lc $argv1;
190              
191             # Diagnostic-Code: SMTP; ... or empty value
192 188         335 for my $e ( $ClassOrder->[2]->@* ) {
193             # Check the values of Diagnostic-Code: and Status: fields using true() method of each child
194             # class in Sisimai::Reason
195 2291         2154 my $p = 'Sisimai::Reason::'.$e;
196 2291         6303 require $ModulePath->{ $p };
197              
198 2291 100       5352 next unless $p->match($issuedcode);
199 144         294 $reasontext = $p->text;
200 144         150 last;
201             }
202 188 100       507 return $reasontext if $reasontext;
203              
204 44 50       114 if( CORE::index(uc $issuedcode, 'X-UNIX; ') > -1 ) {
205             # X-Unix; ...
206 0         0 $reasontext = 'mailererror';
207              
208             } else {
209             # Detect the bounce reason from "Status:" code
210 44         642 require Sisimai::SMTP::Status;
211 44   100     138 my $cv = Sisimai::SMTP::Status->find($argv1) || '';
212 44   100     91 $reasontext = Sisimai::SMTP::Status->name($cv) || 'undefined';
213             }
214 44         115 return $reasontext;
215             }
216              
217             1;
218             __END__