File Coverage

lib/Sisimai/Reason.pm
Criterion Covered Total %
statement 91 94 96.8
branch 46 52 88.4
condition 49 66 74.2
subroutine 10 10 100.0
pod 1 7 14.2
total 197 229 86.0


line stmt bran cond sub pod time code
1             package Sisimai::Reason;
2 85     85   1888 use v5.26;
  85         350  
3 85     85   480 use strict;
  85         159  
  85         2169  
4 85     85   402 use warnings;
  85         199  
  85         122339  
5              
6             my $ModulePath = __PACKAGE__->path;
7             my $GetRetried = __PACKAGE__->retry;
8             my $ClassOrder = [
9             [qw/MailboxFull EmailTooLarge Suspend HasMoved NoRelaying AuthFailure UserUnknown Filtered
10             RequirePTR NotCompliantRFC BadReputation ContentError Rejected HostUnknown SpamDetected
11             RateLimited Blocked/
12             ],
13             [qw/MailboxFull AuthFailure BadReputation RateLimited SpamDetected VirusDetected PolicyViolation
14             NoRelaying SystemError NetworkError Suspend ContentError SystemFull NotAccept Expired
15             FailedSTARTTLS SecurityError Suppressed MailerError/
16             ],
17             [qw/MailboxFull EmailTooLarge Suspend UserUnknown Filtered Rejected HostUnknown SpamDetected
18             RateLimited Blocked SpamDetected AuthFailure FailedSTARTTLS SecurityError SystemError
19             NetworkError Suspend Expired ContentError HasMoved SystemFull NotAccept MailerError
20             NoRelaying Suppressed SyntaxError OnHold/
21             ],
22             ];
23              
24             sub retry {
25             # Reason list better to retry detecting an error reason
26             # @return [Hash] Reason list
27             return {
28 170     170 0 48809 'undefined' => 1, 'onhold' => 1, 'systemerror' => 1, 'securityerror' => 1, 'expired' => 1,
29             'networkerror' => 1, 'hostunknown' => 1, 'userunknown'=> 1
30             };
31             }
32              
33             sub is_explicit {
34             # is_explicit() returns 0 when the argument is empty or is "undefined" or is "onhold"
35             # @param string argv1 Reason name
36             # @return bool false: The reaosn is not explicit
37 9278     9278 0 56397 my $class = shift;
38 9278   100     29363 my $argv1 = shift || return 0;
39              
40 3651 100 100     28458 return 0 if $argv1 eq "undefined" || $argv1 eq "onhold" || $argv1 eq "";
      66        
41 3598         13205 return 1;
42             }
43              
44             sub index {
45             # All the error reason list Sisimai support
46             # @return [Array] Reason list
47 169     169 0 15154 return [qw/
48             AuthFailure BadReputation Blocked ContentError Expired FailedSTARTTLS Filtered HasMoved
49             HostUnknown MailboxFull MailerError EmailTooLarge NetworkError NotAccept NotCompliantRFC
50             OnHold Rejected NoRelaying SpamDetected VirusDetected PolicyViolation SecurityError
51             Suspend RequirePTR SystemError SystemFull RateLimited Suppressed UserUnknown SyntaxError/
52             ];
53             }
54              
55             sub path {
56             # Returns Sisimai::Reason::* module path table
57             # @return [Hash] Module path table
58             # @since v4.25.6
59 87     87 0 264 my $class = shift;
60 87         268 my $index = __PACKAGE__->index;
61 87         179 my $table = {}; $table->{ __PACKAGE__.'::'.$_ } = 'Sisimai/Reason/'.$_.'.pm' for @$index;
  87         3510  
62 87         443 return $table;
63             }
64              
65             sub find {
66             # Detect the bounce reason
67             # @param [Hash] argvs Decoded email object
68             # @return [String] Bounce reason or an empty string if the argument is missing or not HASH
69             # @see anotherone
70 2398     2398 0 298557 my $class = shift;
71 2398   100     6099 my $argvs = shift // return "";
72              
73             # Return a reason text already decided except a reason matched with the regular expression of
74             # Sisimai::Reason->retry() method.
75 2397 50 66     15836 return $argvs->{'reason'} if( (not exists $GetRetried->{ $argvs->{'reason'} }) && $argvs->{'reason'} );
76 2397 100       9522 return 'delivered' if substr($argvs->{'deliverystatus'}, 0, 2) eq '2.';
77              
78 2395         4058 my $reasontext = '';
79 2395   100     8402 my $issuedcode = $argvs->{'diagnosticcode'} || '';
80 2395   50     7694 my $codeformat = $argvs->{'diagnostictype'} || '';
81 2395 100 66     8853 if( $codeformat eq 'SMTP' || $codeformat eq '' ) {
82             # Diagnostic-Code: SMTP; ... or empty value
83 2329         7353 for my $e ( $ClassOrder->[0]->@* ) {
84             # Check the values of Diagnostic-Code: and Status: fields using true() method of each
85             # child class in Sisimai::Reason
86 23520         44280 my $p = 'Sisimai::Reason::'.$e;
87 23520         556137 require $ModulePath->{ $p };
88              
89 23520 100       195033 next unless $p->true($argvs);
90 1764         7742 $reasontext = $p->text;
91 1764         3789 last;
92             }
93             }
94              
95 2395 100 66     10864 if( not $reasontext || $reasontext eq 'undefined' ) {
96             # Bounce reason is not detected yet.
97 631         2793 $reasontext = __PACKAGE__->anotherone($argvs);
98 631 50       1903 $reasontext = '' if $reasontext eq 'undefined';
99 631 100 50     2176 $reasontext ||= 'expired' if $argvs->{'action'} eq 'delayed';
100 631 100       3389 return $reasontext if $reasontext;
101              
102             # Try to match with message patterns in Sisimai::Reason::Vacation
103 37         5847 require Sisimai::Reason::Vacation;
104 37 50       382 $reasontext = 'vacation' if Sisimai::Reason::Vacation->match(lc $issuedcode);
105 37 100 50     222 $reasontext ||= 'onhold' if $issuedcode;
106 37   100     143 $reasontext ||= 'undefined';
107             }
108 1801         7847 return $reasontext;
109             }
110              
111             sub anotherone {
112             # Detect the other bounce reason, fall back method for find()
113             # @param [Hash] argvs Decoded email structure
114             # @return [String] Bounce reason or an empty string if the argument is missing or not HASH
115             # @see find()
116 632     632 0 1236 my $class = shift;
117 632 100 100     1985 my $argvs = shift // return ""; return $argvs->{'reason'} if $argvs->{'reason'};
  631         2337  
118              
119 486         2921 require Sisimai::SMTP::Status;
120 486   50     2479 my $issuedcode = lc $argvs->{'diagnosticcode'} // '';
121 486   50     1677 my $codeformat = $argvs->{'diagnostictype'} // '';
122 486   50     1723 my $actiontext = $argvs->{'action'} // '';
123 486   50     1664 my $statuscode = $argvs->{'deliverystatus'} // '';
124 486   100     2398 my $reasontext = Sisimai::SMTP::Status->name($statuscode) || '';
125 486 100       2220 my $trytomatch = $reasontext eq '' ? 1 : 0;
126 486 100 100     4182 $trytomatch = 1 if exists $GetRetried->{ $reasontext } || $codeformat ne 'SMTP';
127              
128 486         1374 while($trytomatch) {
129             # Could not decide the reason by the value of Status:
130 441         1582 for my $e ( $ClassOrder->[1]->@* ) {
131             # Trying to match with other patterns in Sisimai::Reason::* classes
132 6154         9348 my $p = 'Sisimai::Reason::'.$e;
133 6154         150096 require $ModulePath->{ $p };
134              
135 6154 100       42814 next unless $p->match($issuedcode);
136 269         997 $reasontext = lc $e;
137 269         542 last;
138             }
139 441 100       1671 last if $reasontext;
140              
141             # Check the value of Status:
142 88   100     463 my $code2digit = substr($statuscode, 0, 3) || '';
143 88 50 33     1110 if( $code2digit eq '5.6' || $code2digit eq '4.6' ) {
    50 33        
    100          
144             # X.6.0 Other or undefined media error
145 0         0 $reasontext = 'contenterror';
146              
147             } elsif( $code2digit eq '5.7' || $code2digit eq '4.7' ) {
148             # X.7.0 Other or undefined security status
149 0         0 $reasontext = 'securityerror';
150              
151             } elsif( CORE::index($codeformat, 'X-UNIX') == 0 ) {
152             # Diagnostic-Code: X-UNIX; ..., X-Postfix, or other X-*
153 15         40 $reasontext = 'mailererror';
154              
155             } else {
156             # 50X Syntax Error?
157 73         6992 require Sisimai::Reason::SyntaxError;
158 73 100       750 $reasontext = 'syntaxerror' if Sisimai::Reason::SyntaxError->true($argvs);
159             }
160 88 100       268 last if $reasontext;
161              
162             # Check the value of Action: field, first
163 68 100 66     571 if( CORE::index($actiontext, 'delayed') == 0 || CORE::index($actiontext, 'expired') == 0 ) {
164             # Action: delayed, expired
165 10         27 $reasontext = 'expired';
166              
167             } else {
168             # Check the value of SMTP command
169 58   50     241 my $thecommand = $argvs->{'command'} // '';
170 58 100 100     294 if( $thecommand eq 'EHLO' || $thecommand eq 'HELO' ) {
171             # Rejected at connection or after EHLO|HELO
172 21         47 $reasontext = 'blocked';
173             }
174             }
175 68         149 last;
176             }
177 486         1685 return $reasontext;
178             }
179              
180             sub match {
181             # Detect the bounce reason from given text
182             # @param [String] argv1 Error message
183             # @return [String] Bounce reason
184 189     189 1 245998 my $class = shift;
185 189   100     529 my $argv1 = shift // return "";
186              
187 188         266 my $reasontext = '';
188 188         355 my $issuedcode = lc $argv1;
189              
190             # Diagnostic-Code: SMTP; ... or empty value
191 188         406 for my $e ( $ClassOrder->[2]->@* ) {
192             # Check the values of Diagnostic-Code: and Status: fields using true() method of each child
193             # class in Sisimai::Reason
194 2291         3300 my $p = 'Sisimai::Reason::'.$e;
195 2291         11378 require $ModulePath->{ $p };
196              
197 2291 100       7617 next unless $p->match($issuedcode);
198 144         421 $reasontext = $p->text;
199 144         273 last;
200             }
201 188 100       722 return $reasontext if $reasontext;
202              
203 44 50       175 if( CORE::index(uc $issuedcode, 'X-UNIX; ') > -1 ) {
204             # X-Unix; ...
205 0         0 $reasontext = 'mailererror';
206              
207             } else {
208             # Detect the bounce reason from "Status:" code
209 44         744 require Sisimai::SMTP::Status;
210 44   100     177 my $cv = Sisimai::SMTP::Status->find($argv1) || '';
211 44   100     142 $reasontext = Sisimai::SMTP::Status->name($cv) || 'undefined';
212             }
213 44         188 return $reasontext;
214             }
215              
216             1;
217             __END__