File Coverage

lib/Sisimai/Reason.pm
Criterion Covered Total %
statement 94 96 97.9
branch 57 64 89.0
condition 30 45 66.6
subroutine 9 9 100.0
pod 1 6 16.6
total 191 220 86.8


line stmt bran cond sub pod time code
1             package Sisimai::Reason;
2 78     78   1287 use feature ':5.10';
  78         149  
  78         6015  
3 78     78   425 use strict;
  78         138  
  78         1467  
4 78     78   327 use warnings;
  78         142  
  78         103631  
5              
6             my $ModulePath = __PACKAGE__->path;
7             my $GetRetried = __PACKAGE__->retry;
8             my $ClassOrder = [
9             [qw|MailboxFull MesgTooBig ExceedLimit Suspend HasMoved NoRelaying UserUnknown
10             Filtered Rejected HostUnknown SpamDetected TooManyConn Blocked
11             |],
12             [qw|MailboxFull SpamDetected PolicyViolation VirusDetected NoRelaying
13             SecurityError SystemError NetworkError Suspend Expired ContentError
14             SystemFull NotAccept MailerError
15             |],
16             [qw|MailboxFull MesgTooBig ExceedLimit Suspend UserUnknown Filtered Rejected
17             HostUnknown SpamDetected TooManyConn Blocked SpamDetected SecurityError
18             SystemError NetworkError Suspend Expired ContentError HasMoved SystemFull
19             NotAccept MailerError NoRelaying SyntaxError OnHold
20             |],
21             ];
22              
23             sub retry {
24             # Reason list better to retry detecting an error reason
25             # @return [Array] Reason list
26             return {
27 155     155 0 977 'undefined' => 1, 'onhold' => 1, 'systemerror' => 1, 'securityerror' => 1,
28             'networkerror' => 1, 'hostunknown' => 1, 'userunknown'=> 1
29             };
30             }
31              
32             sub index {
33             # All the error reason list Sisimai support
34             # @return [Array] Reason list
35 83     83 0 712 return [qw|
36             Blocked ContentError ExceedLimit Expired Filtered HasMoved HostUnknown
37             MailboxFull MailerError MesgTooBig NetworkError NotAccept OnHold
38             Rejected NoRelaying SpamDetected VirusDetected PolicyViolation SecurityError
39             Suspend SystemError SystemFull TooManyConn UserUnknown SyntaxError
40             |];
41             }
42              
43             sub path {
44             # Returns Sisimai::Reason::* module path table
45             # @return [Hash] Module path table
46             # @since v4.25.6
47 79     79 0 213 my $class = shift;
48 79         211 my $index = __PACKAGE__->index;
49 79         155 my $table = {};
50 79         2719 $table->{ __PACKAGE__.'::'.$_ } = 'Sisimai/Reason/'.$_.'.pm' for @$index;
51 79         278 return $table;
52             }
53              
54             sub get {
55             # Detect the bounce reason
56             # @param [Sisimai::Data] argvs Parsed email object
57             # @return [String, Undef] Bounce reason or Undef if the argument
58             # is missing or invalid object
59             # @see anotherone
60 2064     2064 0 5372 my $class = shift;
61 2064   100     4734 my $argvs = shift // return undef;
62 2063 50       6337 return undef unless ref $argvs eq 'Sisimai::Data';
63              
64 2063 100       5587 unless( exists $GetRetried->{ $argvs->reason } ) {
65             # Return reason text already decided except reason match with the
66             # regular expression of ->retry() method.
67 1674 50       10639 return $argvs->reason if $argvs->reason;
68             }
69 2063 100       11759 return 'delivered' if substr($argvs->deliverystatus, 0, 2) eq '2.';
70              
71 2061         10864 my $reasontext = '';
72 2061 100 66     4083 if( $argvs->diagnostictype eq 'SMTP' || $argvs->diagnostictype eq '' ) {
73             # Diagnostic-Code: SMTP; ... or empty value
74 1980         10276 for my $e ( @{ $ClassOrder->[0] } ) {
  1980         4756  
75             # Check the value of Diagnostic-Code: and the value of Status:, it is a
76             # deliverystats, with true() method in each Sisimai::Reason::* class.
77 17680         34878 my $p = 'Sisimai::Reason::'.$e;
78 17680         278072 require $ModulePath->{ $p };
79              
80 17680 100       78141 next unless $p->true($argvs);
81 1491         6552 $reasontext = $p->text;
82 1491         3110 last;
83             }
84             }
85              
86 2061 100 66     7519 if( not $reasontext || $reasontext eq 'undefined' ) {
87             # Bounce reason is not detected yet.
88 570         1745 $reasontext = __PACKAGE__->anotherone($argvs);
89 570 50       2326 $reasontext = '' if $reasontext eq 'undefined';
90 570 100 50     1553 $reasontext ||= 'expired' if $argvs->action eq 'delayed';
91 570 100       5183 return $reasontext if $reasontext;
92              
93             # Try to match with message patterns in Sisimai::Reason::Vacation
94 31         2968 require Sisimai::Reason::Vacation;
95 31 50       113 $reasontext = 'vacation' if Sisimai::Reason::Vacation->match(lc $argvs->diagnosticcode);
96 31 100 50     88 $reasontext ||= 'onhold' if $argvs->diagnosticcode;
97 31   100     267 $reasontext ||= 'undefined';
98             }
99 1522         5567 return $reasontext;
100             }
101              
102             sub anotherone {
103             # Detect the other bounce reason, fall back method for get()
104             # @param [Sisimai::Data] argvs Parsed email object
105             # @return [String, Undef] Bounce reason or Undef if the argument
106             # is missing or invalid object
107             # @see get
108 571     571 0 898 my $class = shift;
109 571   100     1537 my $argvs = shift // return undef;
110              
111 570 50       1561 return undef unless ref $argvs eq 'Sisimai::Data';
112 570 100       1207 return $argvs->reason if $argvs->reason;
113              
114 483         3522 require Sisimai::SMTP::Status;
115 483   50     1240 my $statuscode = $argvs->deliverystatus // '';
116 483   100     3089 my $reasontext = Sisimai::SMTP::Status->name($statuscode) || '';
117              
118 483         774 TRY_TO_MATCH: while(1) {
119 483   50     1145 my $diagnostic = lc $argvs->diagnosticcode // '';
120 483 100       3101 my $trytomatch = $reasontext eq '' ? 1 : 0;
121 483 100 50     1421 $trytomatch ||= 1 if $reasontext eq 'expired';
122 483 100 50     1587 $trytomatch ||= 1 if exists $GetRetried->{ $reasontext };
123 483 100 50     985 $trytomatch ||= 1 if $argvs->diagnostictype ne 'SMTP';
124 483 100       2937 last unless $trytomatch;
125              
126             # Could not decide the reason by the value of Status:
127 434         734 for my $e ( @{ $ClassOrder->[1] } ) {
  434         1249  
128             # Trying to match with other patterns in Sisimai::Reason::* classes
129 4045         6293 my $p = 'Sisimai::Reason::'.$e;
130 4045         76890 require $ModulePath->{ $p };
131              
132 4045 100       19559 next unless $p->match($diagnostic);
133 288         819 $reasontext = lc $e;
134 288         538 last;
135             }
136 434 100       1334 last(TRY_TO_MATCH) if $reasontext;
137              
138             # Check the value of Status:
139 70 50 33     741 if( (my $v = substr($statuscode, 0, 3)) =~ /\A[45][.]6\z/ ) {
    50 66        
    100          
140             # X.6.0 Other or undefined media error
141 0         0 $reasontext = 'contenterror';
142              
143             } elsif( $v eq '5.7' || $v eq '4.7' ) {
144             # X.7.0 Other or undefined security status
145 0         0 $reasontext = 'securityerror';
146              
147             } elsif( $argvs->diagnostictype eq 'X-UNIX' || $argvs->diagnostictype eq 'X-POSTFIX' ) {
148             # Diagnostic-Code: X-UNIX; ...
149 14         142 $reasontext = 'mailererror';
150              
151             } else {
152             # 50X Syntax Error?
153 56         3801 require Sisimai::Reason::SyntaxError;
154 56 100       416 $reasontext = 'syntaxerror' if Sisimai::Reason::SyntaxError->true($argvs);
155             }
156 70 100       264 last(TRY_TO_MATCH) if $reasontext;
157              
158             # Check the value of Action: field, first
159 51 100       130 if( $argvs->action =~ /\A(?:delayed|expired)/ ) {
160             # Action: delayed, expired
161 15         138 $reasontext = 'expired';
162             } else {
163             # Check the value of SMTP command
164 36   50     218 my $commandtxt = $argvs->smtpcommand // '';
165 36 100 66     362 if( $commandtxt eq 'EHLO' || $commandtxt eq 'HELO' ) {
166             # Rejected at connection or after EHLO|HELO
167 5         12 $reasontext = 'blocked';
168             }
169             }
170 51         124 last(TRY_TO_MATCH);
171             }
172 483         1100 return $reasontext;
173             }
174              
175             sub match {
176             # Detect the bounce reason from given text
177             # @param [String] argv1 Error message
178             # @return [String] Bounce reason
179 186     186 1 110524 my $class = shift;
180 186   50     306 my $argv1 = shift // return undef;
181              
182 186         208 my $reasontext = '';
183 186         273 my $diagnostic = lc $argv1;
184              
185             # Diagnostic-Code: SMTP; ... or empty value
186 186         159 for my $e ( @{ $ClassOrder->[2] } ) {
  186         301  
187             # Check the value of Diagnostic-Code: and the value of Status:, it is a
188             # deliverystats, with true() method in each Sisimai::Reason::* class.
189 2144         2457 my $p = 'Sisimai::Reason::'.$e;
190 2144         5521 require $ModulePath->{ $p };
191              
192 2144 100       3905 next unless $p->match($diagnostic);
193 142         297 $reasontext = $p->text;
194 142         159 last;
195             }
196 186 100       416 return $reasontext if $reasontext;
197              
198             # Check the value of $typestring
199 44 100       241 my $typestring = uc($argv1) =~ /\A(SMTP|X-.+);/ ? uc($1) : '';
200 44 100       71 if( $typestring eq 'X-UNIX' ) {
201             # X-Unix; ...
202 2         3 $reasontext = 'mailererror';
203             } else {
204             # Detect the bounce reason from "Status:" code
205 42         133 require Sisimai::SMTP::Status;
206 42   100     121 my $statuscode = Sisimai::SMTP::Status->find($argv1) || '';
207 42   100     92 $reasontext = Sisimai::SMTP::Status->name($statuscode) || 'undefined';
208             }
209 44         99 return $reasontext;
210             }
211              
212             1;
213             __END__