1package Sisimai::Lhost::FML;
2use parent 'Sisimai::Lhost';
3use feature ':5.10';
4use strict;
5use warnings;
6
7sub description { 'fml mailing list server/manager' };
8sub make {
9    # Detect an error from fml mailing list server/manager
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.22.3
15    my $class = shift;
16    my $mhead = shift // return undef;
17    my $mbody = shift // return undef;
18
19    return undef unless defined $mhead->{'x-mlserver'};
20    return undef unless $mhead->{'from'} =~ /.+[-]admin[@].+/;
21    return undef unless $mhead->{'message-id'} =~ /\A[<]\d+[.]FML.+[@].+[>]\z/;
22
23    state $rebackbone = qr|^Original[ ]mail[ ]as[ ]follows:|m;
24    state $errortitle = {
25        'rejected' => qr{(?>
26             (?:Ignored[ ])*NOT[ ]MEMBER[ ]article[ ]from[ ]
27            |reject[ ]mail[ ](?:.+:|from)[ ],
28            |Spam[ ]mail[ ]from[ ]a[ ]spammer[ ]is[ ]rejected
29            |You[ ].+[ ]are[ ]not[ ]member
30            )
31        }x,
32        'systemerror' => qr{(?:
33             fml[ ]system[ ]error[ ]message
34            |Loop[ ]Alert:[ ]
35            |Loop[ ]Back[ ]Warning:[ ]
36            |WARNING:[ ]UNIX[ ]FROM[ ]Loop
37            )
38        }x,
39        'securityerror' => qr/Security Alert/,
40    };
41    state $errortable = {
42        'rejected' => qr{(?>
43            (?:Ignored[ ])*NOT[ ]MEMBER[ ]article[ ]from[ ]
44            |reject[ ](?:
45                 mail[ ]from[ ].+[@].+
46                |since[ ].+[ ]header[ ]may[ ]cause[ ]mail[ ]loop
47                |spammers:
48                )
49            |You[ ]are[ ]not[ ]a[ ]member[ ]of[ ]this[ ]mailing[ ]list
50            )
51        }x,
52        'systemerror' => qr{(?:
53             Duplicated[ ]Message-ID
54            |fml[ ].+[ ]has[ ]detected[ ]a[ ]loop[ ]condition[ ]so[ ]that
55            |Loop[ ]Back[ ]Warning:
56            )
57        }x,
58        'securityerror' => qr/Security alert:/,
59    };
60
61    my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
62    my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
63    my $readcursor = 0;     # (Integer) Points the current cursor position
64    my $recipients = 0;     # (Integer) The number of 'Final-Recipient' header
65    my $v = undef;
66
67    for my $e ( split("\n", $emailsteak->[0]) ) {
68        # Read error messages and delivery status lines from the head of the email
69        # to the previous line of the beginning of the original message.
70        next unless length $e;
71
72        # Duplicated Message-ID in <2ndml@example.com>.
73        # Original mail as follows:
74        $v = $dscontents->[-1];
75
76        if( $e =~ /[<]([^ ]+?[@][^ ]+?)[>][.]\z/ ) {
77            # Duplicated Message-ID in <2ndml@example.com>.
78            if( $v->{'recipient'} ) {
79                # There are multiple recipient addresses in the message body.
80                push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
81                $v = $dscontents->[-1];
82            }
83            $v->{'recipient'} = $1;
84            $v->{'diagnosis'} = $e;
85            $recipients++;
86
87        } else {
88            # If you know the general guide of this list, please send mail with
89            # the mail body
90            $v->{'diagnosis'} .= $e;
91        }
92    }
93    return undef unless $recipients;
94
95    for my $e ( @$dscontents ) {
96        $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
97
98        for my $f ( keys %$errortable ) {
99            # Try to match with error messages defined in $errortable
100            next unless $e->{'diagnosis'} =~ $errortable->{ $f };
101            $e->{'reason'} = $f;
102            last;
103        }
104        next if $e->{'reason'};
105
106        # Error messages in the message body did not matched
107        for my $f ( keys %$errortitle ) {
108            # Try to match with the Subject string
109            next unless $mhead->{'subject'} =~ $errortitle->{ $f };
110            $e->{'reason'} = $f;
111            last;
112        }
113    }
114    return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
115}
116
1171;
118__END__
119
120=encoding utf-8
121
122=head1 NAME
123
124Sisimai::Lhost::FML - bounce mail parser class for FML (fml.org).
125
126=head1 SYNOPSIS
127
128    use Sisimai::Lhost::FML;
129
130=head1 DESCRIPTION
131
132Sisimai::Lhost::FML parses a bounce email which created by C<fml mailing
133list server/manager>. Methods in the module are called from only Sisimai::Message.
134
135=head1 CLASS METHODS
136
137=head2 C<B<description()>>
138
139C<description()> returns description string of this module.
140
141    print Sisimai::Lhost::FML->description;
142
143=head2 C<B<make(I<header data>, I<reference to body string>)>>
144
145C<make()> method parses a bounced email and return results as a array reference.
146See Sisimai::Message for more details.
147
148=head1 AUTHOR
149
150azumakuniyuki
151
152=head1 COPYRIGHT
153
154Copyright (C) 2017-2020 azumakuniyuki, All rights reserved.
155
156=head1 LICENSE
157
158This software is distributed under The BSD 2-Clause License.
159
160=cut
161
162