File Coverage

lib/Sisimai/Lhost/PowerMTA.pm
Criterion Covered Total %
statement 58 61 95.0
branch 25 30 83.3
condition 5 11 45.4
subroutine 6 6 100.0
pod 2 2 100.0
total 96 110 87.2


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::PowerMTA;
2 14     14   5961 use parent 'Sisimai::Lhost';
  14         29  
  14         85  
3 14     14   865 use feature ':5.10';
  14         30  
  14         972  
4 14     14   81 use strict;
  14         23  
  14         256  
5 14     14   311 use warnings;
  14         175  
  14         10692  
6              
7 2     2 1 1195 sub description { 'PowerMTA: https://www.sparkpost.com/powermta/' }
8             sub make {
9             # Detect an error from PowerMTA
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.25.6
15 198     198 1 714 my $class = shift;
16 198   100     473 my $mhead = shift // return undef;
17 197   50     551 my $mbody = shift // return undef;
18 197 100       824 return undef unless index($mhead->{'subject'}, 'Delivery report') > -1;
19              
20 16         79 state $indicators = __PACKAGE__->INDICATORS;
21 16         41 state $rebackbone = qr|^Content-Type:[ ]text/rfc822-headers|m;
22 16         32 state $startingof = { 'message' => ['Hello, this is the mail server on '] };
23 16         60 state $categories = {
24             'bad-domain' => 'hostunknown',
25             'bad-mailbox' => 'userunknown',
26             'inactive-mailbox' => 'disabled',
27             'message-expired' => 'expired',
28             'no-answer-from-host' => 'networkerror',
29             'policy-related' => 'policyviolation',
30             'quota-issues' => 'mailboxfull',
31             'routing-errors' => 'systemerror',
32             'spam-related' => 'spamdetected',
33             };
34              
35 16         481 require Sisimai::RFC1894;
36 16         97 my $fieldtable = Sisimai::RFC1894->FIELDTABLE;
37 16         33 my $permessage = {}; # (Hash) Store values of each Per-Message field
38              
39 16         74 my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
40 16         92 my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
41 16         43 my $readcursor = 0; # (Integer) Points the current cursor position
42 16         41 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
43 16         37 my $v = undef;
44              
45 16         150 for my $e ( split("\n", $emailsteak->[0]) ) {
46             # Read error messages and delivery status lines from the head of the email
47             # to the previous line of the beginning of the original message.
48 368 100       549 unless( $readcursor ) {
49             # Beginning of the bounce message or message/delivery-status part
50 32 100       122 if( rindex($e, $startingof->{'message'}->[0]) > -1 ) {
51 16         40 $readcursor |= $indicators->{'deliverystatus'};
52 16         28 next;
53             }
54             }
55 352 100       595 next unless $readcursor & $indicators->{'deliverystatus'};
56 336 100       487 next unless length $e;
57              
58 272 100       487 if( my $f = Sisimai::RFC1894->match($e) ) {
59             # $e matched with any field defined in RFC3464
60 128 50       238 next unless my $o = Sisimai::RFC1894->field($e);
61 128         199 $v = $dscontents->[-1];
62              
63 128 100       287 if( $o->[-1] eq 'addr' ) {
    100          
64             # Final-Recipient: rfc822; kijitora@example.jp
65             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
66 16 50       69 if( $o->[0] eq 'final-recipient' ) {
67             # Final-Recipient: rfc822; kijitora@example.jp
68 16 50       68 if( $v->{'recipient'} ) {
69             # There are multiple recipient addresses in the message body.
70 0         0 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
71 0         0 $v = $dscontents->[-1];
72             }
73 16         35 $v->{'recipient'} = $o->[2];
74 16         44 $recipients++;
75              
76             } else {
77             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
78 0         0 $v->{'alias'} = $o->[2];
79             }
80             } elsif( $o->[-1] eq 'code' ) {
81             # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown
82 16         45 $v->{'spec'} = $o->[1];
83 16         52 $v->{'diagnosis'} = $o->[2];
84              
85             } else {
86             # Other DSN fields defined in RFC3464
87 96 50       266 next unless exists $fieldtable->{ $o->[0] };
88 96         214 $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
89              
90 96 100       226 next unless $f == 1;
91 48         160 $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2];
92             }
93             } else {
94             # Hello, this is the mail server on neko2.example.org.
95             #
96             # I am sending you this message to inform you on the delivery status of a
97             # message you previously sent. Immediately below you will find a list of
98             # the affected recipients; also attached is a Delivery Status Notification
99             # (DSN) report in standard format, as well as the headers of the original
100             # message.
101             #
102             # delivery failed; will not continue trying
103             #
104 144 100       334 if( $e =~ /\AX-PowerMTA-BounceCategory:[ ]*(.+)\z/ ) {
105             # X-PowerMTA-BounceCategory: bad-mailbox
106 16         88 $v->{'category'} = $1;
107             }
108             }
109             }
110 16 50       82 return undef unless $recipients;
111              
112 16         41 for my $e ( @$dscontents ) {
113             # Set default values if each value is empty.
114 16   0     112 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage;
      33        
115 16         114 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
116 16   50     98 $e->{'reason'} = $categories->{ $e->{'category'} } || '';
117             }
118 16         122 return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
119             }
120              
121             1;
122             __END__