1package Sisimai::Lhost::Exchange2007;
2use parent 'Sisimai::Lhost';
3use feature ':5.10';
4use strict;
5use warnings;
6
7sub description { 'Microsoft Exchange Server 2007' }
8sub make {
9    # Detect an error from Microsoft Exchange Server 2007
10    # @param    [Hash] mhead    Message headers of a bounce email
11    # @param    [String] mbody  Message body of a bounce email
12    # @return   [Hash]          Bounce data list and message/rfc822 part
13    # @return   [Undef]         failed to parse or the arguments are missing
14    # @since v4.1.1
15    my $class = shift;
16    my $mhead = shift // return undef;
17    my $mbody = shift // return undef;
18
19    # Content-Language: en-US, fr-FR
20    return undef unless $mhead->{'subject'} =~ /\A(?:Undeliverable|Non_remis_|Non[ ]recapitabile):/;
21    return undef unless defined $mhead->{'content-language'};
22    return undef unless $mhead->{'content-language'} =~ /\A[a-z]{2}(?:[-][A-Z]{2})?\z/;
23
24    # These headers exist only a bounce mail from Office365
25    return undef if $mhead->{'x-ms-exchange-crosstenant-originalarrivaltime'};
26    return undef if $mhead->{'x-ms-exchange-crosstenant-fromentityheader'};
27
28    state $indicators = __PACKAGE__->INDICATORS;
29    state $rebackbone = qr{^(?:
30         Original[ ]message[ ]headers:                  # en-US
31        |En-t.tes[ ]de[ ]message[ ]d'origine[ ]:        # fr-FR/En-têtes de message d'origine
32        |Intestazioni[ ]originali[ ]del[ ]messaggio:    # it-CH
33        )
34    }mx;
35    state $markingsof = {
36        'message' => qr{\A(?:
37             Diagnostic[ ]information[ ]for[ ]administrators:               # en-US
38            |Informations[ ]de[ ]diagnostic[ ]pour[ ]les[ ]administrateurs  # fr-FR
39            |Informazioni[ ]di[ ]diagnostica[ ]per[ ]gli[ ]amministratori   # it-CH
40            )
41        }x,
42        'error'   => qr/[ ]((?:RESOLVER|QUEUE)[.][A-Za-z]+(?:[.]\w+)?);/,
43        'rhost'   => qr{\A(?:
44             Generating[ ]server            # en-US
45            |Serveur[ ]de[ ]g[^ ]+ration[ ] # fr-FR/Serveur de génération
46            |Server[ ]di[ ]generazione      # it-CH
47            ):[ ]?(.*)
48        }x,
49    };
50    state $ndrsubject = {
51        'SMTPSEND.DNS.NonExistentDomain'=> 'hostunknown',   # 554 5.4.4 SMTPSEND.DNS.NonExistentDomain
52        'SMTPSEND.DNS.MxLoopback'       => 'networkerror',  # 554 5.4.4 SMTPSEND.DNS.MxLoopback
53        'RESOLVER.ADR.BadPrimary'       => 'systemerror',   # 550 5.2.0 RESOLVER.ADR.BadPrimary
54        'RESOLVER.ADR.RecipNotFound'    => 'userunknown',   # 550 5.1.1 RESOLVER.ADR.RecipNotFound
55        'RESOLVER.ADR.ExRecipNotFound'  => 'userunknown',   # 550 5.1.1 RESOLVER.ADR.ExRecipNotFound
56        'RESOLVER.ADR.RecipLimit'       => 'toomanyconn',   # 550 5.5.3 RESOLVER.ADR.RecipLimit
57        'RESOLVER.ADR.InvalidInSmtp'    => 'systemerror',   # 550 5.1.0 RESOLVER.ADR.InvalidInSmtp
58        'RESOLVER.ADR.Ambiguous'        => 'systemerror',   # 550 5.1.4 RESOLVER.ADR.Ambiguous, 420 4.2.0 RESOLVER.ADR.Ambiguous
59        'RESOLVER.RST.AuthRequired'     => 'securityerror', # 550 5.7.1 RESOLVER.RST.AuthRequired
60        'RESOLVER.RST.NotAuthorized'    => 'rejected',      # 550 5.7.1 RESOLVER.RST.NotAuthorized
61        'RESOLVER.RST.RecipSizeLimit'   => 'mesgtoobig',    # 550 5.2.3 RESOLVER.RST.RecipSizeLimit
62        'QUEUE.Expired'                 => 'expired',       # 550 4.4.7 QUEUE.Expired
63    };
64
65    my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
66    my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
67    my $readcursor = 0;     # (Integer) Points the current cursor position
68    my $recipients = 0;     # (Integer) The number of 'Final-Recipient' header
69    my $connvalues = 0;     # (Integer) Flag, 1 if all the value of $connheader have been set
70    my $connheader = {
71        'rhost' => '',      # The value of Reporting-MTA header or "Generating Server:"
72    };
73    my $v = undef;
74
75    for my $e ( split("\n", $emailsteak->[0]) ) {
76        # Read error messages and delivery status lines from the head of the email
77        # to the previous line of the beginning of the original message.
78        unless( $readcursor ) {
79            # Beginning of the bounce message or message/delivery-status part
80            $readcursor |= $indicators->{'deliverystatus'} if $e =~ $markingsof->{'message'};
81            next;
82        }
83        next unless $readcursor & $indicators->{'deliverystatus'};
84
85        if( $connvalues == scalar(keys %$connheader) ) {
86            # Diagnostic information for administrators:
87            #
88            # Generating server: mta2.neko.example.jp
89            #
90            # kijitora@example.jp
91            # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ##
92            #
93            # Original message headers:
94            $v = $dscontents->[-1];
95
96            if( $e =~ /\A([^ @]+[@][^ @]+)\z/ ) {
97                # kijitora@example.jp
98                if( $v->{'recipient'} ) {
99                    # There are multiple recipient addresses in the message body.
100                    push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
101                    $v = $dscontents->[-1];
102                }
103                $v->{'recipient'} = $1;
104                $recipients++;
105
106            } elsif( $e =~ /([45]\d{2})[ ]([45][.]\d[.]\d+)[ ].+\z/ ) {
107                # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ##
108                # #550 5.2.3 RESOLVER.RST.RecipSizeLimit; message too large for this recipient ##
109                # Remote Server returned '550 5.1.1 RESOLVER.ADR.RecipNotFound; not found'
110                # 3/09/2016 8:05:56 PM - Remote Server at mydomain.com (10.1.1.3) returned '550 4.4.7 QUEUE.Expired; message expired'
111                $v->{'replycode'} = int $1;
112                $v->{'status'}    = $2;
113                $v->{'diagnosis'} = $e;
114
115            } else {
116                # Continued line of error messages
117                next unless $v->{'diagnosis'};
118                next unless substr($v->{'diagnosis'}, -1, 1) eq '=';
119                substr($v->{'diagnosis'}, -1, 1, $e);
120            }
121        } else {
122            # Diagnostic information for administrators:
123            #
124            # Generating server: mta22.neko.example.org
125            next unless $e =~ $markingsof->{'rhost'};
126            next if $connheader->{'rhost'};
127            $connheader->{'rhost'} = $1;
128            $connvalues++;
129        }
130    }
131    return undef unless $recipients;
132
133    for my $e ( @$dscontents ) {
134        if( $e->{'diagnosis'} =~ $markingsof->{'error'} ) {
135            # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ##
136            my $f = $1;
137            for my $r ( keys %$ndrsubject ) {
138                # Try to match with error subject strings
139                next unless $f eq $r;
140                $e->{'reason'} = $ndrsubject->{ $r };
141                last;
142            }
143        }
144        $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
145    }
146    return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
147}
148
1491;
150__END__
151
152=encoding utf-8
153
154=head1 NAME
155
156Sisimai::Lhost::Exchange2007 - bounce mail parser class for C<Microsft Exchange
157Server 2007>.
158
159=head1 SYNOPSIS
160
161    use Sisimai::Lhost::Exchange2007;
162
163=head1 DESCRIPTION
164
165Sisimai::Lhost::Exchange2007 parses a bounce email which created by C<Microsoft
166Exchange Server 2007>.
167Methods in the module are called from only Sisimai::Message.
168
169=head1 CLASS METHODS
170
171=head2 C<B<description()>>
172
173C<description()> returns description string of this module.
174
175    print Sisimai::Lhost::Exchange2007->description;
176
177=head2 C<B<make(I<header data>, I<reference to body string>)>>
178
179C<make()> method parses a bounced email and return results as a array reference.
180See Sisimai::Message for more details.
181
182=head1 AUTHOR
183
184azumakuniyuki
185
186=head1 COPYRIGHT
187
188Copyright (C) 2016-2020 azumakuniyuki, All rights reserved.
189
190=head1 LICENSE
191
192This software is distributed under The BSD 2-Clause License.
193
194=cut
195