File Coverage

lib/Sisimai/Lhost/Amavis.pm
Criterion Covered Total %
statement 63 65 96.9
branch 26 32 81.2
condition 5 12 41.6
subroutine 6 6 100.0
pod 2 2 100.0
total 102 117 87.1


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::Amavis;
2 16     16   4887 use parent 'Sisimai::Lhost';
  16         30  
  16         90  
3 16     16   850 use feature ':5.10';
  16         31  
  16         951  
4 16     16   75 use strict;
  16         22  
  16         248  
5 16     16   54 use warnings;
  16         22  
  16         10897  
6              
7             # https://www.amavis.org
8 2     2 1 1020 sub description { 'amavisd-new: https://www.amavis.org/' }
9             sub make {
10             # Detect an error from amavisd-new
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.25.0
16 229     229 1 625 my $class = shift;
17 229   100     499 my $mhead = shift // return undef;
18 228   50     510 my $mbody = shift // return undef;
19              
20             # From: "Content-filter at neko1.example.jp"
21             # Subject: Undeliverable mail, MTA-BLOCKED
22 228 100       800 return undef unless index($mhead->{'from'}, '"Content-filter at ') == 0;
23              
24 16         52 state $indicators = __PACKAGE__->INDICATORS;
25 16         33 state $rebackbone = qr|^Content-Type:[ ]text/rfc822-headers|m;
26 16         30 state $startingof = { 'message' => ['The message '] };
27 16         37 state $messagesof = {
28             # amavisd-new-2.11.1/amavisd:1840|%smtp_reason_by_ccat = (
29             # amavisd-new-2.11.1/amavisd:1840| # currently only used for blocked messages only, status 5xx
30             # amavisd-new-2.11.1/amavisd:1840| # a multiline message will produce a valid multiline SMTP response
31             # amavisd-new-2.11.1/amavisd:1840| CC_VIRUS, 'id=%n - INFECTED: %V',
32             # amavisd-new-2.11.1/amavisd:1840| CC_BANNED, 'id=%n - BANNED: %F',
33             # amavisd-new-2.11.1/amavisd:1840| CC_UNCHECKED.',1', 'id=%n - UNCHECKED: encrypted',
34             # amavisd-new-2.11.1/amavisd:1840| CC_UNCHECKED.',2', 'id=%n - UNCHECKED: over limits',
35             # amavisd-new-2.11.1/amavisd:1840| CC_UNCHECKED, 'id=%n - UNCHECKED',
36             # amavisd-new-2.11.1/amavisd:1840| CC_SPAM, 'id=%n - spam',
37             # amavisd-new-2.11.1/amavisd:1840| CC_SPAMMY.',1', 'id=%n - spammy (tag3)',
38             # amavisd-new-2.11.1/amavisd:1840| CC_SPAMMY, 'id=%n - spammy',
39             # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',1', 'id=%n - BAD HEADER: MIME error',
40             # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',2', 'id=%n - BAD HEADER: nonencoded 8-bit character',
41             # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',3', 'id=%n - BAD HEADER: contains invalid control character',
42             # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',4', 'id=%n - BAD HEADER: line made up entirely of whitespace',
43             # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',5', 'id=%n - BAD HEADER: line longer than RFC 5322 limit',
44             # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',6', 'id=%n - BAD HEADER: syntax error',
45             # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',7', 'id=%n - BAD HEADER: missing required header field',
46             # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',8', 'id=%n - BAD HEADER: duplicate header field',
47             # amavisd-new-2.11.1/amavisd:1840| CC_BADH, 'id=%n - BAD HEADER',
48             # amavisd-new-2.11.1/amavisd:1840| CC_OVERSIZED, 'id=%n - Message size exceeds recipient\'s size limit',
49             # amavisd-new-2.11.1/amavisd:1840| CC_MTA.',1', 'id=%n - Temporary MTA failure on relaying',
50             # amavisd-new-2.11.1/amavisd:1840| CC_MTA.',2', 'id=%n - Rejected by next-hop MTA on relaying',
51             # amavisd-new-2.11.1/amavisd:1840| CC_MTA, 'id=%n - Unable to relay message back to MTA',
52             # amavisd-new-2.11.1/amavisd:1840| CC_CLEAN, 'id=%n - CLEAN',
53             # amavisd-new-2.11.1/amavisd:1840| CC_CATCHALL, 'id=%n - OTHER', # should not happen
54             # ...
55             # amavisd-new-2.11.1/amavisd:15289|my $status = setting_by_given_contents_category(
56             # amavisd-new-2.11.1/amavisd:15289| $blocking_ccat,
57             # amavisd-new-2.11.1/amavisd:15289| { CC_VIRUS, "554 5.7.0",
58             # amavisd-new-2.11.1/amavisd:15289| CC_BANNED, "554 5.7.0",
59             # amavisd-new-2.11.1/amavisd:15289| CC_UNCHECKED, "554 5.7.0",
60             # amavisd-new-2.11.1/amavisd:15289| CC_SPAM, "554 5.7.0",
61             # amavisd-new-2.11.1/amavisd:15289| CC_SPAMMY, "554 5.7.0",
62             # amavisd-new-2.11.1/amavisd:15289| CC_BADH.",2", "554 5.6.3", # nonencoded 8-bit character
63             # amavisd-new-2.11.1/amavisd:15289| CC_BADH, "554 5.6.0",
64             # amavisd-new-2.11.1/amavisd:15289| CC_OVERSIZED, "552 5.3.4",
65             # amavisd-new-2.11.1/amavisd:15289| CC_MTA, "550 5.3.5",
66             # amavisd-new-2.11.1/amavisd:15289| CC_CATCHALL, "554 5.7.0",
67             # amavisd-new-2.11.1/amavisd:15289| });
68             # ...
69             # amavisd-new-2.11.1/amavisd:15332|my $response = sprintf("%s %s%s%s", $status,
70             # amavisd-new-2.11.1/amavisd:15333| ($final_destiny == D_PASS ? "Ok" :
71             # amavisd-new-2.11.1/amavisd:15334| $final_destiny == D_DISCARD ? "Ok, discarded" :
72             # amavisd-new-2.11.1/amavisd:15335| $final_destiny == D_REJECT ? "Reject" :
73             # amavisd-new-2.11.1/amavisd:15336| $final_destiny == D_BOUNCE ? "Bounce" :
74             # amavisd-new-2.11.1/amavisd:15337| $final_destiny == D_TEMPFAIL ? "Temporary failure" :
75             # amavisd-new-2.11.1/amavisd:15338| "Not ok ($final_destiny)" ),
76             'spamdetected' => [' - spam'],
77             'virusdetected' => [' - infected'],
78             'contenterror' => [' - bad header:'],
79             'exceedlimit' => [' - message size exceeds recipient'],
80             'systemerror' => [
81             ' - temporary mta failure on relaying',
82             ' - rejected by next-hop mta on relaying',
83             ' - unable to relay message back to mta',
84             ],
85             };
86              
87 16         401 require Sisimai::RFC1894;
88 16         89 my $fieldtable = Sisimai::RFC1894->FIELDTABLE;
89 16         42 my $permessage = {}; # (Hash) Store values of each Per-Message field
90              
91 16         60 my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
92 16         86 my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
93 16         39 my $readcursor = 0; # (Integer) Points the current cursor position
94 16         30 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
95 16         23 my $v = undef;
96              
97 16         152 for my $e ( split("\n", $emailsteak->[0]) ) {
98             # Read error messages and delivery status lines from the head of the email
99             # to the previous line of the beginning of the original message.
100 524 100       595 unless( $readcursor ) {
101             # Beginning of the bounce message or message/delivery-status part
102 83 100       190 $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0;
103 83         89 next;
104             }
105 441 50       592 next unless $readcursor & $indicators->{'deliverystatus'};
106 441 100       526 next unless length $e;
107 362 100       501 next unless my $f = Sisimai::RFC1894->match($e);
108              
109             # $e matched with any field defined in RFC3464
110 150 50       241 next unless my $o = Sisimai::RFC1894->field($e);
111 150         173 $v = $dscontents->[-1];
112              
113 150 100       254 if( $o->[-1] eq 'addr' ) {
    100          
114             # Final-Recipient: rfc822; kijitora@example.jp
115             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
116 27 100       68 if( $o->[0] eq 'final-recipient' ) {
117             # Final-Recipient: rfc822; kijitora@example.jp
118 16 50       62 if( $v->{'recipient'} ) {
119             # There are multiple recipient addresses in the message body.
120 0         0 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
121 0         0 $v = $dscontents->[-1];
122             }
123 16         28 $v->{'recipient'} = $o->[2];
124 16         32 $recipients++;
125             } else {
126             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
127 11         45 $v->{'alias'} = $o->[2];
128             }
129             } elsif( $o->[-1] eq 'code' ) {
130             # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown
131 16         42 $v->{'spec'} = $o->[1];
132 16 50       54 $v->{'spec'} = 'SMTP' if $v->{'spec'} eq 'X-POSTFIX';
133 16         37 $v->{'diagnosis'} = $o->[2];
134              
135             } else {
136             # Other DSN fields defined in RFC3464
137 107 50       184 next unless exists $fieldtable->{ $o->[0] };
138 107         161 $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
139              
140 107 100       204 next unless $f == 1;
141 48         123 $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2];
142             }
143             }
144 16 50       69 return undef unless $recipients;
145              
146 16         31 for my $e ( @$dscontents ) {
147             # Set default values if each value is empty.
148 16   0     113 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage;
      33        
149 16   33     57 $e->{'diagnosis'} ||= Sisimai::String->sweep($e->{'diagnosis'});
150 16         38 my $q = lc $e->{'diagnosis'};
151 16         53 DETECT_REASON: for my $p ( keys %$messagesof ) {
152             # Try to detect an error reason
153 74         63 for my $r ( @{ $messagesof->{ $p } } ) {
  74         97  
154             # Try to find an error message including lower-cased string
155             # defined in $messagesof
156 98 100       210 next unless index($q, $r) > -1;
157 5         7 $e->{'reason'} = $p;
158 5         19 last(DETECT_REASON)
159             }
160             }
161             }
162 16         114 return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
163             }
164              
165             1;
166             __END__