line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Sisimai::Lhost::MXLogic; |
2
|
15
|
|
|
15
|
|
6186
|
use parent 'Sisimai::Lhost'; |
|
15
|
|
|
|
|
30
|
|
|
15
|
|
|
|
|
78
|
|
3
|
15
|
|
|
15
|
|
906
|
use feature ':5.10'; |
|
15
|
|
|
|
|
30
|
|
|
15
|
|
|
|
|
1000
|
|
4
|
15
|
|
|
15
|
|
80
|
use strict; |
|
15
|
|
|
|
|
51
|
|
|
15
|
|
|
|
|
299
|
|
5
|
15
|
|
|
15
|
|
86
|
use warnings; |
|
15
|
|
|
|
|
25
|
|
|
15
|
|
|
|
|
19992
|
|
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
# Based on Sisimai::Lhost::Exim |
8
|
2
|
|
|
2
|
1
|
1269
|
sub description { 'McAfee SaaS' } |
9
|
|
|
|
|
|
|
sub make { |
10
|
|
|
|
|
|
|
# Detect an error from MXLogic |
11
|
|
|
|
|
|
|
# @param [Hash] mhead Message headers of a bounce email |
12
|
|
|
|
|
|
|
# @param [String] mbody Message body of a bounce email |
13
|
|
|
|
|
|
|
# @return [Hash] Bounce data list and message/rfc822 part |
14
|
|
|
|
|
|
|
# @return [Undef] failed to parse or the arguments are missing |
15
|
|
|
|
|
|
|
# @since v4.1.1 |
16
|
203
|
|
|
203
|
1
|
686
|
my $class = shift; |
17
|
203
|
|
100
|
|
|
502
|
my $mhead = shift // return undef; |
18
|
202
|
|
50
|
|
|
502
|
my $mbody = shift // return undef; |
19
|
202
|
|
|
|
|
318
|
my $match = 0; |
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
# X-MX-Bounce: mta/src/queue/bounce |
22
|
|
|
|
|
|
|
# X-MXL-NoteHash: ffffffffffffffff-0000000000000000000000000000000000000000 |
23
|
|
|
|
|
|
|
# X-MXL-Hash: 4c9d4d411993da17-bbd4212b6c887f6c23bab7db4bd87ef5edc00758 |
24
|
202
|
100
|
50
|
|
|
682
|
$match ||= 1 if defined $mhead->{'x-mx-bounce'}; |
25
|
202
|
50
|
0
|
|
|
441
|
$match ||= 1 if defined $mhead->{'x-mxl-hash'}; |
26
|
202
|
100
|
50
|
|
|
449
|
$match ||= 1 if defined $mhead->{'x-mxl-notehash'}; |
27
|
202
|
100
|
50
|
|
|
601
|
$match ||= 1 if index($mhead->{'from'}, 'Mail Delivery System') == 0; |
28
|
202
|
100
|
100
|
|
|
1803
|
$match ||= 1 if $mhead->{'subject'} =~ qr{(?: |
29
|
|
|
|
|
|
|
Mail[ ]delivery[ ]failed(:[ ]returning[ ]message[ ]to[ ]sender)? |
30
|
|
|
|
|
|
|
|Warning:[ ]message[ ][^ ]+[ ]delayed[ ]+ |
31
|
|
|
|
|
|
|
|Delivery[ ]Status[ ]Notification |
32
|
|
|
|
|
|
|
) |
33
|
|
|
|
|
|
|
}x; |
34
|
202
|
100
|
|
|
|
796
|
return undef unless $match; |
35
|
|
|
|
|
|
|
|
36
|
40
|
|
|
|
|
149
|
state $indicators = __PACKAGE__->INDICATORS; |
37
|
40
|
|
|
|
|
85
|
state $rebackbone = qr|^Included is a copy of the message header:|m; |
38
|
40
|
|
|
|
|
69
|
state $startingof = { 'message' => ['This message was created automatically by mail delivery software.'] }; |
39
|
40
|
|
|
|
|
72
|
state $recommands = [ |
40
|
|
|
|
|
|
|
qr/SMTP error from remote (?:mail server|mailer) after ([A-Za-z]{4})/, |
41
|
|
|
|
|
|
|
qr/SMTP error from remote (?:mail server|mailer) after end of ([A-Za-z]{4})/, |
42
|
|
|
|
|
|
|
]; |
43
|
40
|
|
|
|
|
148
|
state $messagesof = { |
44
|
|
|
|
|
|
|
'userunknown' => ['user not found'], |
45
|
|
|
|
|
|
|
'hostunknown' => [ |
46
|
|
|
|
|
|
|
'all host address lookups failed permanently', |
47
|
|
|
|
|
|
|
'all relevant MX records point to non-existent hosts', |
48
|
|
|
|
|
|
|
'Unrouteable address', |
49
|
|
|
|
|
|
|
], |
50
|
|
|
|
|
|
|
'mailboxfull' => [ |
51
|
|
|
|
|
|
|
'mailbox is full', |
52
|
|
|
|
|
|
|
'error: quota exceed', |
53
|
|
|
|
|
|
|
], |
54
|
|
|
|
|
|
|
'notaccept' => [ |
55
|
|
|
|
|
|
|
'an MX or SRV record indicated no SMTP service', |
56
|
|
|
|
|
|
|
'no host found for existing SMTP connection', |
57
|
|
|
|
|
|
|
], |
58
|
|
|
|
|
|
|
'syntaxerror' => [ |
59
|
|
|
|
|
|
|
'angle-brackets nested too deep', |
60
|
|
|
|
|
|
|
'expected word or "<"', |
61
|
|
|
|
|
|
|
'domain missing in source-routed address', |
62
|
|
|
|
|
|
|
'malformed address:', |
63
|
|
|
|
|
|
|
], |
64
|
|
|
|
|
|
|
'systemerror' => [ |
65
|
|
|
|
|
|
|
'delivery to file forbidden', |
66
|
|
|
|
|
|
|
'delivery to pipe forbidden', |
67
|
|
|
|
|
|
|
'local delivery failed', |
68
|
|
|
|
|
|
|
'LMTP error after ', |
69
|
|
|
|
|
|
|
], |
70
|
|
|
|
|
|
|
'contenterror' => ['Too many "Received" headers'], |
71
|
|
|
|
|
|
|
}; |
72
|
40
|
|
|
|
|
68
|
state $delayedfor = [ |
73
|
|
|
|
|
|
|
'retry timeout exceeded', |
74
|
|
|
|
|
|
|
'No action is required on your part', |
75
|
|
|
|
|
|
|
'retry time not reached for any host after a long failure period', |
76
|
|
|
|
|
|
|
'all hosts have been failing for a long time and were last tried', |
77
|
|
|
|
|
|
|
'Delay reason: ', |
78
|
|
|
|
|
|
|
'has been frozen', |
79
|
|
|
|
|
|
|
'was frozen on arrival by ', |
80
|
|
|
|
|
|
|
]; |
81
|
|
|
|
|
|
|
|
82
|
40
|
|
|
|
|
143
|
my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; |
83
|
40
|
|
|
|
|
195
|
my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone); |
84
|
40
|
|
|
|
|
120
|
my $readcursor = 0; # (Integer) Points the current cursor position |
85
|
40
|
|
|
|
|
72
|
my $recipients = 0; # (Integer) The number of 'Final-Recipient' header |
86
|
40
|
|
|
|
|
86
|
my $localhost0 = ''; # (String) Local MTA |
87
|
40
|
|
|
|
|
83
|
my $v = undef; |
88
|
|
|
|
|
|
|
|
89
|
40
|
|
|
|
|
607
|
for my $e ( split("\n", $emailsteak->[0]) ) { |
90
|
|
|
|
|
|
|
# Read error messages and delivery status lines from the head of the email |
91
|
|
|
|
|
|
|
# to the previous line of the beginning of the original message. |
92
|
1512
|
100
|
|
|
|
1960
|
unless( $readcursor ) { |
93
|
|
|
|
|
|
|
# Beginning of the bounce message or message/delivery-status part |
94
|
1427
|
100
|
|
|
|
2281
|
$readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; |
95
|
1427
|
|
|
|
|
1453
|
next; |
96
|
|
|
|
|
|
|
} |
97
|
85
|
50
|
|
|
|
152
|
next unless $readcursor & $indicators->{'deliverystatus'}; |
98
|
85
|
100
|
|
|
|
151
|
next unless length $e; |
99
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
# This message was created automatically by mail delivery software. |
101
|
|
|
|
|
|
|
# |
102
|
|
|
|
|
|
|
# A message that you sent could not be delivered to one or more of its |
103
|
|
|
|
|
|
|
# recipients. This is a permanent error. The following address(es) failed: |
104
|
|
|
|
|
|
|
# |
105
|
|
|
|
|
|
|
# kijitora@example.jp |
106
|
|
|
|
|
|
|
# SMTP error from remote mail server after RCPT TO:: |
107
|
|
|
|
|
|
|
# host neko.example.jp [192.0.2.222]: 550 5.1.1 ... User Unknown |
108
|
53
|
|
|
|
|
72
|
$v = $dscontents->[-1]; |
109
|
|
|
|
|
|
|
|
110
|
53
|
100
|
|
|
|
263
|
if( $e =~ /\A[ \t]*[<]([^ ]+[@][^ ]+)[>]:(.+)\z/ ) { |
|
|
100
|
|
|
|
|
|
111
|
|
|
|
|
|
|
# A message that you have sent could not be delivered to one or more |
112
|
|
|
|
|
|
|
# recipients. This is a permanent error. The following address failed: |
113
|
|
|
|
|
|
|
# |
114
|
|
|
|
|
|
|
# : 550 5.1.1 ... |
115
|
16
|
50
|
|
|
|
99
|
if( $v->{'recipient'} ) { |
116
|
|
|
|
|
|
|
# There are multiple recipient addresses in the message body. |
117
|
0
|
|
|
|
|
0
|
push @$dscontents, __PACKAGE__->DELIVERYSTATUS; |
118
|
0
|
|
|
|
|
0
|
$v = $dscontents->[-1]; |
119
|
|
|
|
|
|
|
} |
120
|
16
|
|
|
|
|
57
|
$v->{'recipient'} = $1; |
121
|
16
|
|
|
|
|
49
|
$v->{'diagnosis'} = $2; |
122
|
16
|
|
|
|
|
35
|
$recipients++; |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
} elsif( scalar @$dscontents == $recipients ) { |
125
|
|
|
|
|
|
|
# Error message |
126
|
5
|
50
|
|
|
|
30
|
next unless length $e; |
127
|
5
|
|
|
|
|
21
|
$v->{'diagnosis'} .= $e.' '; |
128
|
|
|
|
|
|
|
} |
129
|
|
|
|
|
|
|
} |
130
|
40
|
100
|
|
|
|
312
|
return undef unless $recipients; |
131
|
|
|
|
|
|
|
|
132
|
16
|
50
|
|
|
|
25
|
if( scalar @{ $mhead->{'received'} } ) { |
|
16
|
|
|
|
|
57
|
|
133
|
|
|
|
|
|
|
# Get the name of local MTA |
134
|
|
|
|
|
|
|
# Received: from marutamachi.example.org (c192128.example.net [192.0.2.128]) |
135
|
16
|
100
|
|
|
|
87
|
$localhost0 = $1 if $mhead->{'received'}->[-1] =~ /from[ \t]([^ ]+) /; |
136
|
|
|
|
|
|
|
} |
137
|
|
|
|
|
|
|
|
138
|
16
|
|
|
|
|
56
|
for my $e ( @$dscontents ) { |
139
|
|
|
|
|
|
|
# Set default values if each value is empty. |
140
|
16
|
|
66
|
|
|
99
|
$e->{'lhost'} ||= $localhost0; |
141
|
16
|
|
|
|
|
45
|
$e->{'diagnosis'} =~ s/[-]{2}.*\z//g; |
142
|
16
|
|
|
|
|
99
|
$e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); |
143
|
|
|
|
|
|
|
|
144
|
16
|
50
|
|
|
|
75
|
unless( $e->{'rhost'} ) { |
145
|
|
|
|
|
|
|
# Get the remote host name |
146
|
|
|
|
|
|
|
# host neko.example.jp [192.0.2.222]: 550 5.1.1 ... User Unknown |
147
|
16
|
50
|
|
|
|
96
|
$e->{'rhost'} = $1 if $e->{'diagnosis'} =~ /host[ \t]+([^ \t]+)[ \t]\[.+\]:[ \t]/; |
148
|
|
|
|
|
|
|
|
149
|
16
|
50
|
|
|
|
45
|
unless( $e->{'rhost'} ) { |
150
|
16
|
50
|
|
|
|
33
|
if( scalar @{ $mhead->{'received'} } ) { |
|
16
|
|
|
|
|
97
|
|
151
|
|
|
|
|
|
|
# Get localhost and remote host name from Received header. |
152
|
16
|
|
|
|
|
20
|
$e->{'rhost'} = pop @{ Sisimai::RFC5322->received($mhead->{'received'}->[-1]) }; |
|
16
|
|
|
|
|
63
|
|
153
|
|
|
|
|
|
|
} |
154
|
|
|
|
|
|
|
} |
155
|
|
|
|
|
|
|
} |
156
|
|
|
|
|
|
|
|
157
|
16
|
50
|
|
|
|
117
|
unless( $e->{'command'} ) { |
158
|
|
|
|
|
|
|
# Get the SMTP command name for the session |
159
|
16
|
|
|
|
|
55
|
SMTP: for my $r ( @$recommands ) { |
160
|
|
|
|
|
|
|
# Verify each regular expression of SMTP commands |
161
|
32
|
50
|
|
|
|
207
|
next unless $e->{'diagnosis'} =~ $r; |
162
|
0
|
|
|
|
|
0
|
$e->{'command'} = uc $1; |
163
|
0
|
|
|
|
|
0
|
last; |
164
|
|
|
|
|
|
|
} |
165
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
# Detect the reason of bounce |
167
|
16
|
50
|
33
|
|
|
133
|
if( $e->{'command'} eq 'MAIL' ) { |
|
|
50
|
|
|
|
|
|
168
|
|
|
|
|
|
|
# MAIL | Connected to 192.0.2.135 but sender was rejected. |
169
|
0
|
|
|
|
|
0
|
$e->{'reason'} = 'rejected'; |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
} elsif( $e->{'command'} eq 'HELO' || $e->{'command'} eq 'EHLO' ) { |
172
|
|
|
|
|
|
|
# HELO | Connected to 192.0.2.135 but my name was rejected. |
173
|
0
|
|
|
|
|
0
|
$e->{'reason'} = 'blocked'; |
174
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
} else { |
176
|
|
|
|
|
|
|
# Verify each regular expression of session errors |
177
|
16
|
|
|
|
|
68
|
SESSION: for my $r ( keys %$messagesof ) { |
178
|
|
|
|
|
|
|
# Check each regular expression |
179
|
112
|
50
|
|
|
|
133
|
next unless grep { index($e->{'diagnosis'}, $_) > -1 } @{ $messagesof->{ $r } }; |
|
272
|
|
|
|
|
640
|
|
|
112
|
|
|
|
|
196
|
|
180
|
0
|
|
|
|
|
0
|
$e->{'reason'} = $r; |
181
|
0
|
|
|
|
|
0
|
last; |
182
|
|
|
|
|
|
|
} |
183
|
|
|
|
|
|
|
|
184
|
16
|
50
|
|
|
|
85
|
unless( $e->{'reason'} ) { |
185
|
|
|
|
|
|
|
# The reason "expired" |
186
|
16
|
50
|
|
|
|
36
|
$e->{'reason'} = 'expired' if grep { index($e->{'diagnosis'}, $_) > -1 } @$delayedfor; |
|
112
|
|
|
|
|
243
|
|
187
|
|
|
|
|
|
|
} |
188
|
|
|
|
|
|
|
} |
189
|
|
|
|
|
|
|
} |
190
|
16
|
|
50
|
|
|
125
|
$e->{'command'} ||= ''; |
191
|
|
|
|
|
|
|
} |
192
|
16
|
|
|
|
|
104
|
return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] }; |
193
|
|
|
|
|
|
|
} |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
1; |
196
|
|
|
|
|
|
|
__END__ |