File Coverage

lib/Sisimai/Lhost/Exchange2007.pm
Criterion Covered Total %
statement 83 85 97.6
branch 37 44 84.0
condition 18 20 90.0
subroutine 6 6 100.0
pod 2 2 100.0
total 146 157 92.9


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::Exchange2007;
2 46     46   3650 use parent 'Sisimai::Lhost';
  46         63  
  46         275  
3 46     46   2976 use v5.26;
  46         122  
4 46     46   164 use strict;
  46         73  
  46         938  
5 46     46   173 use warnings;
  46         64  
  46         39831  
6              
7 1     1 1 3 sub description { 'Microsoft Exchange Server 2007: https://www.microsoft.com/microsoft-365/exchange/email' }
8             sub inquire {
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 decode or the arguments are missing
14             # @since v4.1.1
15 1060     1060 1 4499 my $class = shift;
16 1060   100     2684 my $mhead = shift // return undef;
17 1058   100     2028 my $mbody = shift // return undef;
18              
19 1056         1947 my $mailsender = ['postmaster@outlook.com', ".onmicrosoft.com"];
20 1056         2236 my $emailtitle = [
21             # "Subject:" "Content-Language:"
22             "Undeliverable", # en-US
23             "Non_remis_", # fr-FR
24             "Non remis ", # fr-FR
25             "Non recapitabile", # it-CH
26             "Olevererbart", # sv-SE
27             ];
28 1056         1344 state $boundaries = [
29             "Original Message Headers",
30             "Original message headers:", # en-US
31             "tes de message d'origine :", # fr-FR/En-têtes de message d'origine
32             "Intestazioni originali del messaggio:", # it-CH
33             "Ursprungshuvuden:", # sv-SE
34             ];
35 1056         1817 state $startingof = {
36             "error" => [" RESOLVER.", " QUEUE."],
37             "message" => [
38             "Error Details",
39             "Diagnostic information for administrators:", # en-US
40             "Informations de diagnostic pour les administrateurs", # fr-FR
41             "Informazioni di diagnostica per gli amministratori", # it-CH
42             "Diagnostisk information f", # sv-SE
43             ],
44             "rhost" => [
45             "DSN generated by:",
46             "Generating server", # en-US
47             "Serveur de g", # fr-FR/Serveur de gènèration
48             "Server di generazione", # it-CH
49             "Genererande server", # sv-SE
50             ],
51             };
52 1056         1654 state $ndrsubject = {
53             "SMTPSEND.DNS.NonExistentDomain" => "hostunknown", # 554 5.4.4 SMTPSEND.DNS.NonExistentDomain
54             "SMTPSEND.DNS.MxLoopback" => "networkerror", # 554 5.4.4 SMTPSEND.DNS.MxLoopback
55             "RESOLVER.ADR.BadPrimary" => "systemerror", # 550 5.2.0 RESOLVER.ADR.BadPrimary
56             "RESOLVER.ADR.RecipNotFound" => "userunknown", # 550 5.1.1 RESOLVER.ADR.RecipNotFound
57             "RESOLVER.ADR.RecipientNotFound" => "userunknown", # 550 5.1.1 RESOLVER.ADR.RecipientNotFound
58             "RESOLVER.ADR.ExRecipNotFound" => "userunknown", # 550 5.1.1 RESOLVER.ADR.ExRecipNotFound
59             "RESOLVER.ADR.RecipLimit" => "ratelimited", # 550 5.5.3 RESOLVER.ADR.RecipLimit
60             "RESOLVER.ADR.InvalidInSmtp" => "systemerror", # 550 5.1.0 RESOLVER.ADR.InvalidInSmtp
61             "RESOLVER.ADR.Ambiguous" => "systemerror", # 550 5.1.4 RESOLVER.ADR.Ambiguous, 420 4.2.0 RESOLVER.ADR.Ambiguous
62             "RESOLVER.RST.AuthRequired" => "securityerror", # 550 5.7.1 RESOLVER.RST.AuthRequired
63             "RESOLVER.RST.NotAuthorized" => "rejected", # 550 5.7.1 RESOLVER.RST.NotAuthorized
64             "RESOLVER.RST.RecipSizeLimit" => "emailtoolarge", # 550 5.2.3 RESOLVER.RST.RecipSizeLimit
65             "QUEUE.Expired" => "expired", # 550 4.4.7 QUEUE.Expired
66             };
67 1056 100       1288 my $proceedsto = 0; $proceedsto++ if grep { index($mhead->{"subject"}, $_) > -1 } @$emailtitle;
  1056         1579  
  5280         8033  
68 1056 100       1476 $proceedsto++ if grep { index($mhead->{"from"}, $_) > 1 } @$mailsender;
  2112         3852  
69 1056 100       2347 $proceedsto++ if grep { index($$mbody, $_) > 1 } $startingof->{"error"}->@*;
  2112         6037  
70 1056 100       2185 $proceedsto++ if grep { index($$mbody, $_) > 1 } $startingof->{"message"}->@*;
  5280         9523  
71 1056 100       2048 $proceedsto++ if defined $mhead->{"content-language"};
72 1056 100       3048 return undef if $proceedsto < 2;
73              
74 108         427 require Sisimai::RFC1123;
75 108         170 state $indicators = __PACKAGE__->INDICATORS;
76              
77 108         423 my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $v = undef;
  108         211  
78 108         467 my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries);
79 108         130 my $readcursor = 0; # (Integer) Points the current cursor position
80 108         153 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
81              
82 108         998 for my $e ( split("\n", $emailparts->[0]) ) {
83             # Read error messages and delivery status lines from the head of the email to the previous
84             # line of the beginning of the original message.
85 3131 100       3240 unless( $readcursor ) {
86             # Beginning of the bounce message or message/delivery-status part
87 2436 100       2212 $readcursor |= $indicators->{"deliverystatus"} if grep { index($e, $_) == 0 } $startingof->{"message"}->@*;
  12180         12456  
88 2436         2004 next;
89             }
90 695 100 66     1793 next if ($readcursor & $indicators->{'deliverystatus'}) == 0 || $e eq "";
91              
92             # Diagnostic information for administrators:
93             #
94             # Generating server: mta2.neko.example.jp
95             #
96             # kijitora@example.jp
97             # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ##
98             #
99             # Original message headers:
100 474         527 $v = $dscontents->[-1];
101              
102 474 100 100     1180 if( index($e, " ") < 0 && index($e, '@') > 1 ) {
103             # kijitora@example.jp
104 97 50       196 if( $v->{"recipient"} ) {
105             # There are multiple recipient addresses in the message body.
106 0         0 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
107 0         0 $v = $dscontents->[-1];
108             }
109 97         486 $v->{"recipient"} = Sisimai::Address->s3s4($e);
110 97         162 $recipients++;
111              
112             } else {
113             # Try to pick the remote hostname and status code, reply code from the error message
114 377 100       576 if( grep { index($e, $_) == 0 } $startingof->{"rhost"}->@* ) {
  1885         2405  
115             # Generating server: SG2APC01HT234.mail.protection.outlook.com
116             # DSN generated by: NEKONYAAN0022.apcprd01.prod.exchangelabs.com
117 108         714 my $cv = Sisimai::RFC1123->find($e);
118 108 50       225 $v->{"rhost"} = $cv if Sisimai::RFC1123->is_internethost($cv);
119              
120             } else {
121             # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ##
122             # #550 5.2.3 RESOLVER.RST.RecipSizeLimit; message too large for this recipient ##
123 269   100     971 my $cr = Sisimai::SMTP::Reply->find($e) || "";
124 269   100     864 my $cs = Sisimai::SMTP::Status->find($e) || "";
125 269 100 100     927 if( $cr ne "" || $cs ne "" || index($e, "Remote Server ") > -1 ) {
      66        
126             # Remote Server returned '550 5.1.1 RESOLVER.ADR.RecipNotFound; not found'
127             # 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'
128 123         221 $v->{"replycode"} = $cr;
129 123         171 $v->{"status"} = $cs;
130 123         345 $v->{"diagnosis"} .= $e." ";
131             }
132             }
133             }
134             }
135              
136 108         578 while( $recipients == 0 ) {
137             # Try to pick the recipient address from the following formatted bounce message:
138             #
139             # Original Message Details
140             # Created Date: 4/29/2017 11:23:34 PM
141             # Sender Address: neko@example.com
142             # Recipient Address: kijitora-nyaan@neko.kyoto.example.jp
143             # Subject: Nyaan?
144 11 50       63 my $p1 = index($emailparts->[0], "Original Message Details"); last if $p1 < 0;
  11         34  
145 11 50       73 my $p2 = index($emailparts->[0], "\nRecipient Address: "); last if $p2 < 0;
  11         31  
146 11 50       33 my $p3 = index($emailparts->[0], "\n", $p2 + 20); last if $p3 < 0;
  11         23  
147 11         98 my $cv = Sisimai::Address->s3s4(substr($emailparts->[0], $p2 + 20, $p3 - $p2 - 20));
148              
149 11 50       44 last unless Sisimai::Address->is_emailaddress($cv);
150 11         56 $dscontents->[0]->{"recipient"} = $cv;
151 11         44 $recipients++;
152             }
153 108 50       185 return undef unless $recipients;
154              
155 108         143 for my $e ( @$dscontents ) {
156 108         111 my $p0 = -1; for my $r ( $startingof->{"error"}->@* ) {
  108         189  
157             # Try to find the NDR subject string such as "RESOLVER.ADR.RecipientNotFound" from the
158             # error message
159 168 100       267 $p0 = index($e->{"diagnosis"}, $r); last if $p0 > -1;
  168         280  
160             }
161 108 100       204 next if $p0 < 0;
162              
163 53         152 my $cv = substr($e->{"diagnosis"}, $p0 + 1, index($e->{"diagnosis"}, ";") - $p0 - 1);
164 53         209 for my $r ( keys %$ndrsubject ) {
165             # Try to match with error subject strings such as "RESOLVER.ADR.RecipNotFound"
166 315 100       425 next unless $cv eq $r;
167 53         93 $e->{"reason"} = $ndrsubject->{ $r };
168 53         119 last;
169             }
170             }
171 108         658 return {"ds" => $dscontents, "rfc822" => $emailparts->[1]};
172             }
173              
174             1;
175             __END__