1package Sisimai::Rhost::ExchangeOnline; 2use feature ':5.10'; 3use strict; 4use warnings; 5 6# https://technet.microsoft.com/en-us/library/bb232118 7sub 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 my $class = shift; 13 my $argvs = shift // return undef; 14 return $argvs->reason if $argvs->reason; 15 16 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 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 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 my $statuscode = $argvs->deliverystatus; 129 my $statusmesg = $argvs->diagnosticcode; 130 my $reasontext = ''; 131 132 for my $e ( keys %$statuslist ) { 133 # Try to compare with each status code as a key 134 next unless $statuscode eq $e; 135 for my $f ( @{ $statuslist->{ $e } } ) { 136 # Try to compare with each string of error messages 137 next if index($statusmesg, $f->{'string'}) == -1; 138 $reasontext = $f->{'reason'}; 139 last; 140 } 141 last if $reasontext; 142 } 143 return $reasontext if $reasontext; 144 145 for my $e ( keys %$restatuses ) { 146 # Try to compare with each string of delivery status codes 147 next unless $statuscode =~ $e; 148 for my $f ( @{ $restatuses->{ $e } } ) { 149 # Try to compare with each string of error messages 150 for my $g ( @{ $f->{'string'} } ) { 151 next if index($statusmesg, $g) == -1; 152 $reasontext = $f->{'reason'}; 153 last; 154 } 155 last if $reasontext; 156 } 157 last if $reasontext; 158 } 159 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 for my $e ( keys %$messagesof ) { 164 # Try to compare with error messages defined in MessagesOf 165 for my $f ( @{ $messagesof->{ $e } } ) { 166 next if index($statusmesg, $f) == -1; 167 $reasontext = $e; 168 last; 169 } 170 last if $reasontext; 171 } 172 return $reasontext; 173} 174 1751; 176__END__ 177 178=encoding utf-8 179 180=head1 NAME 181 182Sisimai::Rhost::ExchangeOnline - Detect the bounce reason returned from on-premises 183Exchange 2013 and Office 365. 184 185=head1 SYNOPSIS 186 187 use Sisimai::Rhost; 188 189=head1 DESCRIPTION 190 191Sisimai::Rhost detects the bounce reason from the content of Sisimai::Data 192object as an argument of get() method when the value of C<rhost> of the object 193is "*.protection.outlook.com". This class is called only Sisimai::Data class. 194 195=head1 CLASS METHODS 196 197=head2 C<B<get(I<Sisimai::Data Object>)>> 198 199C<get()> detects the bounce reason. 200 201=head1 AUTHOR 202 203azumakuniyuki 204 205=head1 COPYRIGHT 206 207Copyright (C) 2016-2021 azumakuniyuki, All rights reserved. 208 209=head1 LICENSE 210 211This software is distributed under The BSD 2-Clause License. 212 213=cut 214 215