File Coverage

lib/Sisimai/Lhost/Courier.pm
Criterion Covered Total %
statement 75 78 96.1
branch 38 46 82.6
condition 19 31 61.2
subroutine 6 6 100.0
pod 2 2 100.0
total 140 163 85.8


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::Courier;
2 36     36   4047 use parent 'Sisimai::Lhost';
  36         74  
  36         1941  
3 36     36   2890 use v5.26;
  36         121  
4 36     36   168 use strict;
  36         84  
  36         922  
5 36     36   169 use warnings;
  36         102  
  36         35966  
6              
7 1     1 1 4 sub description { 'Courier MTA' }
8             sub inquire {
9             # Detect an error from Courier MTA
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.0.0
15 893     893 1 3515 my $class = shift;
16 893   100     3474 my $mhead = shift // return undef;
17 892   100     3400 my $mbody = shift // return undef;
18 891 50 0     1709 my $match = 0; $match ||= 1 if index($mhead->{'from'}, 'Courier mail server at ') > -1;
  891         3812  
19 891 100 50     3255 $match ||= 1 if index($mhead->{'subject'}, 'NOTICE: mail delivery status.') > -1;
20 891 50 0     2826 $match ||= 1 if index($mhead->{'subject'}, 'WARNING: delayed mail.') > -1;
21 891 100       2821 if( defined $mhead->{'message-id'} ) {
22             # Message-ID:
23 830 100 50     4070 $match ||= 1 if index($mhead->{'message-id'}, '
24             }
25 891 100       3147 return undef unless $match;
26              
27 21         99 require Sisimai::RFC1123;
28 21         63 require Sisimai::SMTP::Command;
29 21         74 state $indicators = __PACKAGE__->INDICATORS;
30 21         39 state $boundaries = ['Content-Type: :message/rfc822', 'Content-Type: text/rfc822-headers'];
31 21         36 state $startingof = {
32             # https://www.courier-mta.org/courierdsn.html
33             # courier/module.dsn/dsn*.txt
34             'message' => ['DELAYS IN DELIVERING YOUR MESSAGE', 'UNDELIVERABLE MAIL'],
35             };
36 21         43 state $messagesof = {
37             # courier/module.esmtp/esmtpclient.c:526| hard_error(del, ctf, "No such domain.");
38             'hostunknown' => ['No such domain.'],
39             # courier/module.esmtp/esmtpclient.c:531| hard_error(del, ctf,
40             # courier/module.esmtp/esmtpclient.c:532| "This domain's DNS violates RFC 1035.");
41             'systemerror' => ["This domain's DNS violates RFC 1035."],
42             # courier/module.esmtp/esmtpclient.c:535| soft_error(del, ctf, "DNS lookup failed.");
43             'networkerror'=> ['DNS lookup failed.'],
44             };
45              
46 21         149 my $fieldtable = Sisimai::RFC1894->FIELDTABLE;
47 21         32 my $permessage = {}; # (Hash) Store values of each Per-Message field
48 21         228 my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $v = undef;
  21         43  
49 21         85 my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries);
50 21         33 my $readcursor = 0; # (Integer) Points the current cursor position
51 21         25 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
52 21         31 my $thecommand = ''; # (String) SMTP Command name begin with the string '>>>'
53 21         48 my $p = '';
54              
55 21         303 for my $e ( split("\n", $emailparts->[0]) ) {
56             # Read error messages and delivery status lines from the head of the email to the previous
57             # line of the beginning of the original message.
58 1060 100       1435 unless( $readcursor ) {
59             # Beginning of the bounce message or message/delivery-status part
60 252 100 66     716 if( rindex($e, $startingof->{'message'}->[0]) > -1 || rindex($e, $startingof->{'message'}->[1]) > -1 ) {
61 21         55 $readcursor |= $indicators->{'deliverystatus'};
62 21         28 next;
63             }
64             }
65 1039 100 100     2632 next if ($readcursor & $indicators->{'deliverystatus'}) == 0 || $e eq "";
66              
67 609 100       1202 if( my $f = Sisimai::RFC1894->match($e) ) {
68             # $e matched with any field defined in RFC3464
69 163 50       331 next unless my $o = Sisimai::RFC1894->field($e);
70 163         231 $v = $dscontents->[-1];
71              
72 163 100       378 if( $o->[3] eq 'addr' ) {
    100          
73             # Final-Recipient: rfc822; kijitora@example.jp
74             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
75 21 50       46 if( $o->[0] eq 'final-recipient' ) {
76             # Final-Recipient: rfc822; kijitora@example.jp
77 21 50       108 if( $v->{'recipient'} ) {
78             # There are multiple recipient addresses in the message body.
79 0         0 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
80 0         0 $v = $dscontents->[-1];
81             }
82 21         44 $v->{'recipient'} = $o->[2];
83 21         49 $recipients++;
84              
85             } else {
86             # X-Actual-Recipient: rfc822; kijitora@example.co.jp
87 0         0 $v->{'alias'} = $o->[2];
88             }
89             } elsif( $o->[3] eq 'code' ) {
90             # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown
91 21         51 $v->{'spec'} = $o->[1];
92 21         58 $v->{'diagnosis'} = $o->[2];
93              
94             } else {
95             # Other DSN fields defined in RFC3464
96 121 50       268 next unless exists $fieldtable->{ $o->[0] };
97 121 100 100     439 next if $o->[3] eq "host" && Sisimai::RFC1123->is_internethost($o->[2]) == 0;
98 99         253 $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
99              
100 99 100       286 next unless $f == 1;
101 57         190 $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2];
102             }
103             } else {
104             # The line does not begin with a DSN field defined in RFC3464
105             #
106             # This is a delivery status notification from marutamachi.example.org,
107             # running the Courier mail server, version 0.65.2.
108             #
109             # The original message was received on Sat, 11 Dec 2010 12:19:57 +0900
110             # from [127.0.0.1] (c10920.example.com [192.0.2.20])
111             #
112             # ---------------------------------------------------------------------------
113             #
114             # UNDELIVERABLE MAIL
115             #
116             # Your message to the following recipients cannot be delivered:
117             #
118             # :
119             # mx.example.co.jp [74.207.247.95]:
120             # >>> RCPT TO:
121             # <<< 550 5.1.1 ... User Unknown
122             #
123             # ---------------------------------------------------------------------------
124 446 100       776 if( index($e, '>>> ') == 0 ) {
125             # >>> DATA
126 16         144 $thecommand = Sisimai::SMTP::Command->find($e);
127              
128             } else {
129             # Continued line of the value of Diagnostic-Code field
130 430 100       775 next unless index($p, 'Diagnostic-Code:') == 0;
131 5 50       19 next unless index($e, ' ') == 0;
132 5         18 $v->{'diagnosis'} .= ' '.Sisimai::String->sweep($e);
133             }
134             }
135             } continue {
136             # Save the current line for the next loop
137 1060         1447 $p = $e;
138             }
139 21 50       192 return undef unless $recipients;
140              
141 21         49 for my $e ( @$dscontents ) {
142             # Set default values if each value is empty.
143 21   0     116 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage;
      33        
144 21         99 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
145              
146 21         70 for my $r ( keys %$messagesof ) {
147             # Verify each regular expression of session errors
148 59 100       130 next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*;
  59         173  
149 5         11 $e->{'reason'} = $r;
150 5         9 last;
151             }
152 21   100     126 $e->{'command'} ||= $thecommand || '';
      66        
153             }
154 21         177 return {"ds" => $dscontents, "rfc822" => $emailparts->[1]};
155             }
156              
157             1;
158             __END__