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