1package Sisimai::Lhost::Zoho;
2use parent 'Sisimai::Lhost';
3use feature ':5.10';
4use strict;
5use warnings;
6
7sub description { 'Zoho Mail: https://www.zoho.com' }
8sub make {
9    # Detect an error from Zoho Mail
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.1.7
15    my $class = shift;
16    my $mhead = shift // return undef;
17    my $mbody = shift // return undef;
18
19    # X-ZohoMail: Si CHF_MF_NL SS_10 UW48 UB48 FMWL UW48 UB48 SGR3_1_09124_42
20    # X-Zoho-Virus-Status: 2
21    # X-Mailer: Zoho Mail
22    return undef unless $mhead->{'x-zohomail'};
23
24    state $indicators = __PACKAGE__->INDICATORS;
25    state $rebackbone = qr|^Received:[ ]*from[ ]mail[.]zoho[.]com[ ]by[ ]mx[.]zohomail[.]com|m;
26    state $startingof = { 'message' => ['This message was created automatically by mail delivery'] };
27    state $messagesof = { 'expired' => ['Host not reachable'] };
28
29    my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
30    my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
31    my $readcursor = 0;     # (Integer) Points the current cursor position
32    my $recipients = 0;     # (Integer) The number of 'Final-Recipient' header
33    my $qprintable = 0;
34    my $v = undef;
35
36    for my $e ( split("\n", $emailsteak->[0]) ) {
37        # Read error messages and delivery status lines from the head of the email
38        # to the previous line of the beginning of the original message.
39        unless( $readcursor ) {
40            # Beginning of the bounce message or message/delivery-status part
41            $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0;
42            next;
43        }
44        next unless $readcursor & $indicators->{'deliverystatus'};
45        next unless length $e;
46
47        # This message was created automatically by mail delivery software.
48        # A message that you sent could not be delivered to one or more of its recip=
49        # ients. This is a permanent error.=20
50        #
51        # kijitora@example.co.jp Invalid Address, ERROR_CODE :550, ERROR_CODE :5.1.=
52        # 1 <kijitora@example.co.jp>... User Unknown
53
54        # This message was created automatically by mail delivery software.
55        # A message that you sent could not be delivered to one or more of its recipients. This is a permanent error.
56        #
57        # shironeko@example.org Invalid Address, ERROR_CODE :550, ERROR_CODE :Requested action not taken: mailbox unavailable
58        $v = $dscontents->[-1];
59
60        if( $e =~ /\A([^ ]+[@][^ ]+)[ \t]+(.+)\z/ ) {
61            # kijitora@example.co.jp Invalid Address, ERROR_CODE :550, ERROR_CODE :5.1.=
62            if( $v->{'recipient'} ) {
63                # There are multiple recipient addresses in the message body.
64                push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
65                $v = $dscontents->[-1];
66            }
67            $v->{'recipient'} = $1;
68            $v->{'diagnosis'} = $2;
69
70            if( substr($v->{'diagnosis'}, -1, 1) eq '=' ) {
71                # Quoted printable
72                substr($v->{'diagnosis'}, -1, 1, '');
73                $qprintable = 1;
74            }
75            $recipients++;
76
77        } elsif( $e =~ /\A\[Status: .+[<]([^ ]+[@][^ ]+)[>],/ ) {
78            # Expired
79            # [Status: Error, Address: <kijitora@6kaku.example.co.jp>, ResponseCode 421, , Host not reachable.]
80            if( $v->{'recipient'} ) {
81                # There are multiple recipient addresses in the message body.
82                push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
83                $v = $dscontents->[-1];
84            }
85            $v->{'recipient'} = $1;
86            $v->{'diagnosis'} = $e;
87            $recipients++;
88
89        } else {
90            # Continued line
91            next unless $qprintable;
92            $v->{'diagnosis'} .= $e;
93        }
94    }
95    return undef unless $recipients;
96
97    for my $e ( @$dscontents ) {
98        $e->{'diagnosis'} =~ y/\n/ /;
99        $e->{'diagnosis'} =  Sisimai::String->sweep($e->{'diagnosis'});
100
101        SESSION: for my $r ( keys %$messagesof ) {
102            # Verify each regular expression of session errors
103            next unless grep { index($e->{'diagnosis'}, $_) > -1 } @{ $messagesof->{ $r } };
104            $e->{'reason'} = $r;
105            last;
106        }
107    }
108    return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
109}
110
1111;
112__END__
113
114=encoding utf-8
115
116=head1 NAME
117
118Sisimai::Lhost::Zoho - bounce mail parser class for C<Zoho Mail>.
119
120=head1 SYNOPSIS
121
122    use Sisimai::Lhost::Zoho;
123
124=head1 DESCRIPTION
125
126Sisimai::Lhost::Zoho parses a bounce email which created by C<Zoho! MAIL>.
127Methods in the module are called from only Sisimai::Message.
128
129=head1 CLASS METHODS
130
131=head2 C<B<description()>>
132
133C<description()> returns description string of this module.
134
135    print Sisimai::Lhost::Zoho->description;
136
137=head2 C<B<make(I<header data>, I<reference to body string>)>>
138
139C<make()> method parses a bounced email and return results as a array reference.
140See Sisimai::Message for more details.
141
142=head1 AUTHOR
143
144azumakuniyuki
145
146=head1 COPYRIGHT
147
148Copyright (C) 2014-2020 azumakuniyuki, All rights reserved.
149
150=head1 LICENSE
151
152This software is distributed under The BSD 2-Clause License.
153
154=cut
155
156