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