File Coverage

lib/Sisimai/Lhost/EZweb.pm
Criterion Covered Total %
statement 87 92 94.5
branch 48 64 75.0
condition 16 22 72.7
subroutine 6 6 100.0
pod 2 2 100.0
total 159 186 85.4


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::EZweb;
2 14     14   6046 use parent 'Sisimai::Lhost';
  14         26  
  14         74  
3 14     14   739 use feature ':5.10';
  14         20  
  14         869  
4 14     14   62 use strict;
  14         25  
  14         248  
5 14     14   55 use warnings;
  14         18  
  14         17821  
6              
7 2     2 1 1073 sub description { 'au EZweb: http://www.au.kddi.com/mobile/' }
8             sub make {
9             # Detect an error from EZweb
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.0.0
15 222     222 1 569 my $class = shift;
16 222   100     588 my $mhead = shift // return undef;
17 221   50     439 my $mbody = shift // return undef;
18 221         268 my $match = 0;
19              
20             # Pre-process email headers of NON-STANDARD bounce message au by EZweb, as
21             # known as ezweb.ne.jp.
22             # Subject: Mail System Error - Returned Mail
23             # From:
24             # Received: from ezweb.ne.jp (wmflb12na02.ezweb.ne.jp [222.15.69.197])
25             # Received: from nmomta.auone-net.jp ([aaa.bbb.ccc.ddd]) by ...
26             #
27 221 100       590 $match++ if rindex($mhead->{'from'}, 'Postmaster@ezweb.ne.jp') > -1;
28 221 50       639 $match++ if rindex($mhead->{'from'}, 'Postmaster@au.com') > -1;
29 221 100       524 $match++ if $mhead->{'subject'} eq 'Mail System Error - Returned Mail';
30 221 100       280 $match++ if grep { rindex($_, 'ezweb.ne.jp (EZweb Mail) with') > -1 } @{ $mhead->{'received'} };
  326         955  
  221         578  
31 221 50       324 $match++ if grep { rindex($_, '.au.com (') > -1 } @{ $mhead->{'received'} };
  326         799  
  221         629  
32 221 100       567 if( defined $mhead->{'message-id'} ) {
33 201 100       637 $match++ if substr($mhead->{'message-id'}, -13, 13) eq '.ezweb.ne.jp>';
34 201 50       544 $match++ if substr($mhead->{'message-id'}, -8, 8) eq '.au.com>';
35             }
36 221 100       548 return undef if $match < 2;
37              
38 40         87 state $indicators = __PACKAGE__->INDICATORS;
39 40         78 state $rebackbone = qr<^(?:[-]{50}|Content-Type:[ ]*message/rfc822)>m;
40 40         231 my $markingsof = {
41             'message' => qr{\A(?:
42             The[ ]user[(]s[)][ ]
43             |Your[ ]message[ ]
44             |Each[ ]of[ ]the[ ]following
45             |[<][^ ]+[@][^ ]+[>]\z
46             )
47             }x,
48             'boundary' => qr/\A__SISIMAI_PSEUDO_BOUNDARY__\z/,
49             };
50 40         114 state $refailures = {
51             #'notaccept' => [qr/The following recipients did not receive this message:/],
52             'mailboxfull' => [qr/The user[(]s[)] account is temporarily over quota/],
53             'suspend' => [
54             # http://www.naruhodo-au.kddi.com/qa3429203.html
55             # The recipient may be unpaid user...?
56             qr/The user[(]s[)] account is disabled[.]/,
57             qr/The user[(]s[)] account is temporarily limited[.]/,
58             ],
59             'expired' => [
60             # Your message was not delivered within 0 days and 1 hours.
61             # Remote host is not responding.
62             qr/Your message was not delivered within /,
63             ],
64             'onhold' => [qr/Each of the following recipients was rejected by a remote mail server/],
65             };
66              
67 40         540 require Sisimai::RFC1894;
68 40         203 my $fieldtable = Sisimai::RFC1894->FIELDTABLE;
69 40         161 my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
70 40         225 my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
71 40         61 my $readcursor = 0; # (Integer) Points the current cursor position
72 40         57 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
73 40         58 my $v = undef;
74              
75 40 50       115 if( $mhead->{'content-type'} ) {
76             # Get the boundary string and set regular expression for matching with
77             # the boundary string.
78 40         167 my $b0 = Sisimai::MIME->boundary($mhead->{'content-type'}, 1);
79 40 100       638 $markingsof->{'boundary'} = qr/\A\Q$b0\E\z/ if $b0; # Convert to regular expression
80             }
81 40         71 my @rxmessages; push @rxmessages, @{ $refailures->{ $_ } } for keys %$refailures;
  40         162  
  160         266  
82              
83 40         277 for my $e ( split("\n", $emailsteak->[0]) ) {
84             # Read error messages and delivery status lines from the head of the email
85             # to the previous line of the beginning of the original message.
86 555 100       660 unless( $readcursor ) {
87             # Beginning of the bounce message or message/delivery-status part
88 293 100       795 $readcursor |= $indicators->{'deliverystatus'} if $e =~ $markingsof->{'message'};
89             }
90 555 100       805 next unless $readcursor & $indicators->{'deliverystatus'};
91 302 100       433 next unless length $e;
92              
93             # The user(s) account is disabled.
94             #
95             # <***@ezweb.ne.jp>: 550 user unknown (in reply to RCPT TO command)
96             #
97             # -- OR --
98             # Each of the following recipients was rejected by a remote
99             # mail server.
100             #
101             # Recipient: <******@ezweb.ne.jp>
102             # >>> RCPT TO:<******@ezweb.ne.jp>
103             # <<< 550 <******@ezweb.ne.jp>: User unknown
104 214         218 $v = $dscontents->[-1];
105              
106 214 100 100     1198 if( $e =~ /\A[<]([^ ]+[@][^ ]+)[>]\z/ ||
    100 100        
107             $e =~ /\A[<]([^ ]+[@][^ ]+)[>]:?(.*)\z/ ||
108             $e =~ /\A[ \t]+Recipient: [<]([^ ]+[@][^ ]+)[>]/ ) {
109              
110 40 50       102 if( $v->{'recipient'} ) {
111             # There are multiple recipient addresses in the message body.
112 0         0 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
113 0         0 $v = $dscontents->[-1];
114             }
115              
116 40         232 my $r = Sisimai::Address->s3s4($1);
117 40         65 $v->{'recipient'} = $r;
118 40         71 $recipients++;
119              
120             } elsif( my $f = Sisimai::RFC1894->match($e) ) {
121             # $e matched with any field defined in RFC3464
122 45 50       80 next unless my $o = Sisimai::RFC1894->field($e);
123 45 50       89 next unless exists $fieldtable->{ $o->[0] };
124 45         115 $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
125              
126             } else {
127             # The line does not begin with a DSN field defined in RFC3464
128 129 50       348 next if Sisimai::String->is_8bit(\$e);
129 129 100       286 if( $e =~ /\A[ \t]+[>]{3}[ \t]+([A-Z]{4})/ ) {
130             # >>> RCPT TO:<******@ezweb.ne.jp>
131 15         53 $v->{'command'} = $1;
132              
133             } else {
134             # Check error message
135 114 100       167 if( grep { $e =~ $_ } @rxmessages ) {
  570         1215  
136             # Check with regular expressions of each error
137 25         97 $v->{'diagnosis'} .= ' '.$e;
138             } else {
139             # >>> 550
140 89         313 $v->{'alterrors'} .= ' '.$e;
141             }
142             }
143             } # End of error message part
144             }
145 40 50       147 return undef unless $recipients;
146              
147 40         81 for my $e ( @$dscontents ) {
148 40 50 66     157 if( exists $e->{'alterrors'} && $e->{'alterrors'} ) {
149             # Copy alternative error message
150 34   66     115 $e->{'diagnosis'} ||= $e->{'alterrors'};
151 34 50 33     168 if( index($e->{'diagnosis'}, '-') == 0 || substr($e->{'diagnosis'}, -2, 2) eq '__' ) {
152             # Override the value of diagnostic code message
153 0 0       0 $e->{'diagnosis'} = $e->{'alterrors'} if $e->{'alterrors'};
154             }
155 34         60 delete $e->{'alterrors'};
156             }
157 40         140 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
158              
159 40 100 66     183 if( defined $mhead->{'x-spasign'} && $mhead->{'x-spasign'} eq 'NG' ) {
160             # Content-Type: text/plain; ..., X-SPASIGN: NG (spamghetti, au by EZweb)
161             # Filtered recipient returns message that include 'X-SPASIGN' header
162 6         14 $e->{'reason'} = 'filtered';
163              
164             } else {
165 34 100       73 if( $e->{'command'} eq 'RCPT' ) {
166             # set "userunknown" when the remote server rejected after RCPT
167             # command.
168 10         25 $e->{'reason'} = 'userunknown';
169              
170             } else {
171             # SMTP command is not RCPT
172 24         65 SESSION: for my $r ( keys %$refailures ) {
173             # Verify each regular expression of session errors
174 60         85 PATTERN: for my $rr ( @{ $refailures->{ $r } } ) {
  60         94  
175             # Check each regular expression
176 61 100       229 next(PATTERN) unless $e->{'diagnosis'} =~ $rr;
177 24         47 $e->{'reason'} = $r;
178 24         47 last(SESSION);
179             }
180             }
181             }
182             }
183 40 50       109 next if $e->{'reason'};
184 0 0       0 next if $e->{'recipient'} =~ /[@](?:ezweb[.]ne[.]jp|au[.]com)\z/;
185 0         0 $e->{'reason'} = 'userunknown';
186             }
187 40         311 return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
188             }
189              
190             1;
191             __END__