File Coverage

lib/Sisimai/Rhost/ExchangeOnline.pm
Criterion Covered Total %
statement 47 47 100.0
branch 19 24 79.1
condition 2 2 100.0
subroutine 4 4 100.0
pod 0 1 0.0
total 72 78 92.3


line stmt bran cond sub pod time code
1             package Sisimai::Rhost::ExchangeOnline;
2 7     7   1307 use feature ':5.10';
  7         11  
  7         597  
3 7     7   38 use strict;
  7         12  
  7         119  
4 7     7   28 use warnings;
  7         9  
  7         5918  
5              
6             # https://technet.microsoft.com/en-us/library/bb232118
7             sub get {
8             # Detect bounce reason from Exchange 2013 and Office 365
9             # @param [Sisimai::Data] argvs Parsed email object
10             # @return [String] The bounce reason for Exchange Online
11             # @see https://technet.microsoft.com/en-us/library/bb232118
12 30     30 0 839 my $class = shift;
13 30   100     91 my $argvs = shift // return undef;
14 29 100       90 return $argvs->reason if $argvs->reason;
15              
16 24         354 state $statuslist = {
17             '4.3.1' => [{ 'reason' => 'systemfull', 'string' => 'Insufficient system resources' }],
18             '4.3.2' => [{ 'reason' => 'notaccept', 'string' => 'System not accepting network messages' }],
19             '4.4.2' => [{ 'reason' => 'blocked', 'string' => 'Connection dropped' }],
20             '4.7.26' => [{
21             'reason' => 'securityerror',
22             'string' => 'must pass either SPF or DKIM validation, this message is not signed'
23             }],
24             '5.0.0' => [{ 'reason' => 'blocked', 'string' => 'HELO / EHLO requires domain address' }],
25             '5.1.4' => [{ 'reason' => 'systemerror', 'string' => 'Destination mailbox address ambiguous' }],
26             '5.2.1' => [{ 'reason' => 'suspend', 'string' => 'Mailbox cannot be accessed' }],
27             '5.2.2' => [{ 'reason' => 'mailboxfull', 'string' => 'Mailbox full' }],
28             '5.2.3' => [{ 'reason' => 'exceedlimit', 'string' => 'Message too large' }],
29             '5.2.4' => [{ 'reason' => 'systemerror', 'string' => 'Mailing list expansion problem' }],
30             '5.2.14' => [{ 'reason' => 'systemerror', 'string' => 'misconfigured forwarding address' }],
31             '5.2.122' => [{ 'reason' => 'toomanyconn', 'string' => 'The recipient has exceeded their limit for' }],
32             '5.3.3' => [{ 'reason' => 'systemfull', 'string' => 'Unrecognized command' }],
33             '5.3.4' => [{ 'reason' => 'mesgtoobig', 'string' => 'Message too big for system' }],
34             '5.3.5' => [{ 'reason' => 'systemerror', 'string' => 'System incorrectly configured' }],
35             '5.4.1' => [{ 'reason' => 'rejected', 'string' => 'Recipient address rejected: Access denied' }],
36             '5.4.11' => [{ 'reason' => 'contenterror','string' => 'Agent generated message depth exceeded' }],
37             '5.4.14' => [{ 'reason' => 'networkerror','string' => 'Hop count exceeded' }],
38             '5.4.310' => [{ 'reason' => 'systemerror', 'string' => 'does not exist'}], # DNS domain * does not exist
39             '5.5.2' => [{ 'reason' => 'syntaxerror', 'string' => 'Send hello first' }],
40             '5.5.3' => [{ 'reason' => 'syntaxerror', 'string' => 'Too many recipients' }],
41             '5.5.4' => [{ 'reason' => 'filtered', 'string' => 'Invalid domain name' }],
42             '5.5.6' => [{ 'reason' => 'contenterror','string' => 'Invalid message content' }],
43             '5.7.1' => [
44             { 'reason' => 'securityerror', 'string' => 'Delivery not authorized' },
45             { 'reason' => 'securityerror', 'string' => 'Client was not authenticated' },
46             { 'reason' => 'norelaying', 'string' => 'Unable to relay' },
47             ],
48             '5.7.25' => [{ 'reason' => 'blocked', 'string' => 'must have a reverse DNS record' }],
49             '5.7.51' => [{ 'reason' => 'blocked', 'string' => 'RestrictDomainsToIPAddresses or RestrictDomainsToCertificate' }],
50             '5.7.506' => [{ 'reason' => 'blocked', 'string' => 'Bad HELO' }],
51             '5.7.508' => [{ 'reason' => 'toomanyconn', 'string' => 'has exceeded permitted limits within ' }],
52             '5.7.509' => [{ 'reason' => 'rejected', 'string' => 'does not pass DMARC verification' }],
53             '5.7.510' => [{ 'reason' => 'notaccept', 'string' => 'does not accept email over IPv6' }],
54             '5.7.511' => [{ 'reason' => 'blocked', 'string' => 'banned sender' }],
55             '5.7.512' => [{ 'reason' => 'contenterror', 'string' => 'message must be RFC 5322' }],
56             };
57 24         182 state $restatuses = {
58             qr/\A4[.]4[.][17]\z/ => [
59             { 'reason' => 'expired', 'string' => ['Connection timed out', 'Message expired'] }
60             ],
61             qr/\A4[.]7[.][568]\d\d\z/ => [
62             { 'reason' => 'securityerror', 'string' => ['Access denied, please try again later'] }
63             ],
64             qr/\A5[.]1[.][07]\z/ => [
65             { 'reason' => 'rejected', 'string' => ['Sender denied', 'Invalid address'] }
66             ],
67             qr/\A5[.]1[.][123]\z/ => [{
68             'reason' => 'userunknown',
69             'string' => [
70             'Bad destination mailbox address',
71             'Invalid X.400 address',
72             'Invalid recipient address',
73             ]
74             }],
75             qr/\A5[.]4[.][46]\z/ => [{
76             'reason' => 'networkerror',
77             'string' => ['Invalid arguments', 'Routing loop detected'],
78             }],
79             qr/\A5[.]7[.][13]\z/ => [{
80             'reason' => 'securityerror',
81             'string' => ['Delivery not authorized', 'Not Authorized'],
82             }],
83             qr/\A5[.]7[.]50[1-3]\z/ => [{
84             'reason' => 'spamdetected',
85             'string' => [
86             'Access denied, spam abuse detected',
87             'Access denied, banned sender'
88             ],
89             }],
90             qr/\A5[.]7[.]50[457]\z/ => [{
91             'reason' => 'filtered',
92             'string' => [
93             'Recipient address rejected: Access denied',
94             'Access denied, banned recipient',
95             'Access denied, rejected by recipient'
96             ]
97             }],
98             qr/\A5[.]7[.]6\d\d\z/ => [
99             { 'reason' => 'blocked', 'string' => ['Access denied, banned sending IP '] }
100             ],
101             qr/\A5[.]7[.]7\d\d\z/ => [
102             { 'reason' => 'toomanyconn', 'string' => ['Access denied, tenant has exceeded threshold'] }
103             ],
104             };
105 24         109 state $messagesof = {
106             # Copied and converted from Sisimai::Lhost::Exchange2007
107             'expired' => ['QUEUE.Expired'],
108             'hostunknown' => ['SMTPSEND.DNS.NonExistentDomain'],
109             'mesgtoobig' => ['RESOLVER.RST.RecipSizeLimit', 'RESOLVER.RST.RecipientSizeLimit'],
110             'networkerror' => ['SMTPSEND.DNS.MxLoopback'],
111             'rejected' => ['RESOLVER.RST.NotAuthorized'],
112             'securityerror' => ['RESOLVER.RST.AuthRequired'],
113             'systemerror' => [
114             'RESOLVER.ADR.Ambiguous',
115             'RESOLVER.ADR.BadPrimary',
116             'RESOLVER.ADR.InvalidInSmtp',
117             'RESOLVER.FWD.NotFound',
118             ],
119             'toomanyconn' => ['RESOLVER.ADR.RecipLimit', 'RESOLVER.ADR.RecipientLimit'],
120             'userunknown' => [
121             'RESOLVER.ADR.RecipNotFound',
122             'RESOLVER.ADR.RecipientNotFound',
123             'RESOLVER.ADR.ExRecipNotFound',
124             'RESOLVER.ADR.ExRecipientNotFound',
125             ],
126             };
127              
128 24         90 my $statuscode = $argvs->deliverystatus;
129 24         156 my $statusmesg = $argvs->diagnosticcode;
130 24         107 my $reasontext = '';
131              
132 24         228 for my $e ( keys %$statuslist ) {
133             # Try to compare with each status code as a key
134 657 100       847 next unless $statuscode eq $e;
135 11         18 for my $f ( @{ $statuslist->{ $e } } ) {
  11         34  
136             # Try to compare with each string of error messages
137 11 50       58 next if index($statusmesg, $f->{'string'}) == -1;
138 11         22 $reasontext = $f->{'reason'};
139 11         21 last;
140             }
141 11 50       30 last if $reasontext;
142             }
143 24 100       111 return $reasontext if $reasontext;
144              
145 13         53 for my $e ( keys %$restatuses ) {
146             # Try to compare with each string of delivery status codes
147 84 100       1074 next unless $statuscode =~ $e;
148 7         20 for my $f ( @{ $restatuses->{ $e } } ) {
  7         18  
149             # Try to compare with each string of error messages
150 7         12 for my $g ( @{ $f->{'string'} } ) {
  7         17  
151 7 50       33 next if index($statusmesg, $g) == -1;
152 7         13 $reasontext = $f->{'reason'};
153 7         13 last;
154             }
155 7 50       18 last if $reasontext;
156             }
157 7 50       26 last if $reasontext;
158             }
159 13 100       72 return $reasontext if $reasontext;
160              
161             # D.S.N. included in the error message did not matched with any key
162             # in statuslist, restatuses
163 6         28 for my $e ( keys %$messagesof ) {
164             # Try to compare with error messages defined in MessagesOf
165 35         36 for my $f ( @{ $messagesof->{ $e } } ) {
  35         68  
166 59 100       131 next if index($statusmesg, $f) == -1;
167 6         11 $reasontext = $e;
168 6         10 last;
169             }
170 35 100       52 last if $reasontext;
171             }
172 6         22 return $reasontext;
173             }
174              
175             1;
176             __END__