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 37     37   3410 use parent 'Sisimai::Lhost';
  37         78  
  37         201  
3 37     37   2443 use v5.26;
  37         105  
4 37     37   168 use strict;
  37         86  
  37         824  
5 37     37   287 use warnings;
  37         70  
  37         28574  
6              
7 1     1 1 3 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 891     891 1 2576 my $class = shift;
16 891   100     2113 my $mhead = shift // return undef;
17 890   100     1785 my $mbody = shift // return undef;
18 889 50 0     1252 my $match = 0; $match ||= 1 if index($mhead->{'from'}, 'Courier mail server at ') > -1;
  889         2168  
19 889 100 50     2004 $match ||= 1 if index($mhead->{'subject'}, 'NOTICE: mail delivery status.') > -1;
20 889 50 0     2198 $match ||= 1 if index($mhead->{'subject'}, 'WARNING: delayed mail.') > -1;
21 889 100       1810 if( defined $mhead->{'message-id'} ) {
22             # Message-ID:
23 827 100 50     2926 $match ||= 1 if index($mhead->{'message-id'}, '
24             }
25 889 100       2107 return undef unless $match;
26              
27 21         93 require Sisimai::RFC1123;
28 21         47 require Sisimai::SMTP::Command;
29 21         43 state $indicators = __PACKAGE__->INDICATORS;
30 21         27 state $boundaries = ['Content-Type: :message/rfc822', 'Content-Type: text/rfc822-headers'];
31 21         33 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         40 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         107 my $fieldtable = Sisimai::RFC1894->FIELDTABLE;
47 21         25 my $permessage = {}; # (Hash) Store values of each Per-Message field
48 21         72 my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $v = undef;
  21         27  
49 21         79 my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries);
50 21         29 my $readcursor = 0; # (Integer) Points the current cursor position
51 21         26 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
52 21         20 my $thecommand = ''; # (String) SMTP Command name begin with the string '>>>'
53 21         25 my $p = '';
54              
55 21         292 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       1110 unless( $readcursor ) {
59             # Beginning of the bounce message or message/delivery-status part
60 252 100 66     565 if( rindex($e, $startingof->{'message'}->[0]) > -1 || rindex($e, $startingof->{'message'}->[1]) > -1 ) {
61 21         51 $readcursor |= $indicators->{'deliverystatus'};
62 21         28 next;
63             }
64             }
65 1039 100 100     1932 next if ($readcursor & $indicators->{'deliverystatus'}) == 0 || $e eq "";
66              
67 609 100       854 if( my $f = Sisimai::RFC1894->match($e) ) {
68             # $e matched with any field defined in RFC3464
69 163 50       251 next unless my $o = Sisimai::RFC1894->field($e);
70 163         177 $v = $dscontents->[-1];
71              
72 163 100       284 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       44 if( $o->[0] eq 'final-recipient' ) {
76             # Final-Recipient: rfc822; kijitora@example.jp
77 21 50       53 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         37 $v->{'recipient'} = $o->[2];
83 21         34 $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         55 $v->{'spec'} = $o->[1];
92 21         42 $v->{'diagnosis'} = $o->[2];
93              
94             } else {
95             # Other DSN fields defined in RFC3464
96 121 50       222 next unless exists $fieldtable->{ $o->[0] };
97 121 100 100     332 next if $o->[3] eq "host" && Sisimai::RFC1123->is_internethost($o->[2]) == 0;
98 99         196 $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
99              
100 99 100       163 next unless $f == 1;
101 57         164 $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       600 if( index($e, '>>> ') == 0 ) {
125             # >>> DATA
126 16         119 $thecommand = Sisimai::SMTP::Command->find($e);
127              
128             } else {
129             # Continued line of the value of Diagnostic-Code field
130 430 100       582 next unless index($p, 'Diagnostic-Code:') == 0;
131 5 50       17 next unless index($e, ' ') == 0;
132 5         15 $v->{'diagnosis'} .= ' '.$e;
133             }
134             }
135             } continue {
136             # Save the current line for the next loop
137 1060         1135 $p = $e;
138             }
139 21 50       151 return undef unless $recipients;
140              
141 21         34 for my $e ( @$dscontents ) {
142             # Set default values if each value is empty.
143 21   0     84 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage;
      33        
144 21         67 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
145              
146 21         57 for my $r ( keys %$messagesof ) {
147             # Verify each regular expression of session errors
148 54 100       92 next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*;
  54         127  
149 5         6 $e->{'reason'} = $r;
150 5         9 last;
151             }
152 21   100     104 $e->{'command'} ||= $thecommand || '';
      66        
153             }
154 21         128 return {"ds" => $dscontents, "rfc822" => $emailparts->[1]};
155             }
156              
157             1;
158             __END__