package Sisimai::Reason;
use feature ':5.10';
use strict;
use warnings;

my $ModulePath = __PACKAGE__->path;
my $GetRetried = __PACKAGE__->retry;
my $ClassOrder = [
    [qw/MailboxFull MesgTooBig ExceedLimit Suspend HasMoved NoRelaying AuthFailure UserUnknown
        Filtered RequirePTR NotCompliantRFC Rejected HostUnknown SpamDetected Speeding TooManyConn
        Blocked/
    ],
    [qw/MailboxFull SpamDetected PolicyViolation VirusDetected NoRelaying AuthFailure BadReputation
        SecurityError SystemError NetworkError Speeding Suspend Expired ContentError SystemFull
        NotAccept MailerError/
    ],
    [qw/MailboxFull MesgTooBig ExceedLimit Suspend UserUnknown Filtered Rejected HostUnknown
        SpamDetected Speeding TooManyConn Blocked SpamDetected AuthFailure SecurityError SystemError
        NetworkError Suspend Expired ContentError HasMoved SystemFull NotAccept MailerError
        NoRelaying SyntaxError OnHold/
    ],
];

sub retry {
    # Reason list better to retry detecting an error reason
    # @return   [Hash] Reason list
    return {
        'undefined' => 1, 'onhold' => 1, 'systemerror' => 1, 'securityerror' => 1, 'expired' => 1,
        'suspend' => 1, 'networkerror' => 1, 'hostunknown' => 1, 'userunknown'=> 1
    };
}

sub index {
    # All the error reason list Sisimai support
    # @return   [Array] Reason list
    return [qw/
        AuthFailure BadReputation Blocked ContentError ExceedLimit Expired Filtered HasMoved
        HostUnknown MailboxFull MailerError MesgTooBig NetworkError NotAccept NotCompliantRFC
        OnHold Rejected NoRelaying SpamDetected VirusDetected PolicyViolation SecurityError
        Speeding Suspend RequirePTR SystemError SystemFull TooManyConn UserUnknown SyntaxError/
    ];
}

sub path {
    # Returns Sisimai::Reason::* module path table
    # @return   [Hash] Module path table
    # @since    v4.25.6
    my $class = shift;
    my $index = __PACKAGE__->index;
    my $table = {};
    $table->{ __PACKAGE__.'::'.$_ } = 'Sisimai/Reason/'.$_.'.pm' for @$index;
    return $table;
}

sub get {
    # Detect the bounce reason
    # @param    [Hash]   argvs  Parsed email object
    # @return   [String]        Bounce reason or undef if the argument is missing or not HASH
    # @see anotherone
    my $class = shift;
    my $argvs = shift // return undef;

    unless( exists $GetRetried->{ $argvs->{'reason'} } ) {
        # Return a reason text already decided except a reason matched with the regular expression
        # of ->retry() method.
        return $argvs->{'reason'} if $argvs->{'reason'};
    }
    return 'delivered' if substr($argvs->{'deliverystatus'}, 0, 2) eq '2.';

    my $reasontext = '';
    my $issuedcode = $argvs->{'diagnosticcode'} || '';
    my $codeformat = $argvs->{'diagnostictype'} || '';
    if( $codeformat eq 'SMTP' || $codeformat eq '' ) {
        # Diagnostic-Code: SMTP; ... or empty value
        for my $e ( $ClassOrder->[0]->@* ) {
            # Check the values of Diagnostic-Code: and Status: fields using true() method of each
            # child class in Sisimai::Reason
            my $p = 'Sisimai::Reason::'.$e;
            require $ModulePath->{ $p };

            next unless $p->true($argvs);
            $reasontext = $p->text;
            last;
        }
    }

    if( not $reasontext || $reasontext eq 'undefined' ) {
        # Bounce reason is not detected yet.
        $reasontext   = __PACKAGE__->anotherone($argvs);
        $reasontext   = '' if $reasontext eq 'undefined';
        $reasontext ||= 'expired' if $argvs->{'action'} eq 'delayed';
        return $reasontext if $reasontext;

        # Try to match with message patterns in Sisimai::Reason::Vacation
        require Sisimai::Reason::Vacation;
        $reasontext   = 'vacation' if Sisimai::Reason::Vacation->match(lc $issuedcode);
        $reasontext ||= 'onhold'   if $issuedcode;
        $reasontext ||= 'undefined';
    }
    return $reasontext;
}

sub anotherone {
    # Detect the other bounce reason, fall back method for get()
    # @param    [Hash] argvs    Parsed email structure
    # @return   [String]        Bounce reason or undef if the argument is missing or not HASH
    # @see get
    my $class = shift;
    my $argvs = shift // return undef;
    return $argvs->{'reason'} if $argvs->{'reason'};

    require Sisimai::SMTP::Status;
    my $issuedcode = lc $argvs->{'diagnosticcode'} // '';
    my $codeformat = $argvs->{'diagnostictype'}    // '';
    my $actiontext = $argvs->{'action'}            // '';
    my $statuscode = $argvs->{'deliverystatus'}    // '';
    my $reasontext = Sisimai::SMTP::Status->name($statuscode) || '';

    TRY_TO_MATCH: while(1) {
        my $trytomatch   = $reasontext eq '' ? 1 : 0;
           $trytomatch ||= 1 if exists $GetRetried->{ $reasontext };
           $trytomatch ||= 1 if $codeformat ne 'SMTP';
        last unless $trytomatch;

        # Could not decide the reason by the value of Status:
        for my $e ( $ClassOrder->[1]->@* ) {
            # Trying to match with other patterns in Sisimai::Reason::* classes
            my $p = 'Sisimai::Reason::'.$e;
            require $ModulePath->{ $p };

            next unless $p->match($issuedcode);
            $reasontext = lc $e;
            last;
        }
        last(TRY_TO_MATCH) if $reasontext;

        # Check the value of Status:
        my $code2digit = substr($statuscode, 0, 3) || '';
        if( $code2digit eq '5.6' || $code2digit eq '4.6' ) {
            #  X.6.0   Other or undefined media error
            $reasontext = 'contenterror';

        } elsif( $code2digit eq '5.7' || $code2digit eq '4.7' ) {
            #  X.7.0   Other or undefined security status
            $reasontext = 'securityerror';

        } elsif( CORE::index($codeformat, 'X-UNIX') == 0 ) {
            # Diagnostic-Code: X-UNIX; ..., X-Postfix, or other X-*
            $reasontext = 'mailererror';

        } else {
            # 50X Syntax Error?
            require Sisimai::Reason::SyntaxError;
            $reasontext = 'syntaxerror' if Sisimai::Reason::SyntaxError->true($argvs);
        }
        last(TRY_TO_MATCH) if $reasontext;

        # Check the value of Action: field, first
        if( CORE::index($actiontext, 'delayed') == 0 || CORE::index($actiontext, 'expired') == 0 ) {
            # Action: delayed, expired
            $reasontext = 'expired';

        } else {
            # Check the value of SMTP command
            my $thecommand = $argvs->{'smtpcommand'} // '';
            if( $thecommand eq 'EHLO' || $thecommand eq 'HELO' ) {
                # Rejected at connection or after EHLO|HELO
                $reasontext = 'blocked';
            }
        }
        last(TRY_TO_MATCH);
    }
    return $reasontext;
}

sub match {
    # Detect the bounce reason from given text
    # @param    [String] argv1  Error message
    # @return   [String]        Bounce reason
    my $class = shift;
    my $argv1 = shift // return undef;

    my $reasontext = '';
    my $issuedcode = lc $argv1;

    # Diagnostic-Code: SMTP; ... or empty value
    for my $e ( $ClassOrder->[2]->@* ) {
        # Check the values of Diagnostic-Code: and Status: fields using true() method of each child
        # class in Sisimai::Reason
        my $p = 'Sisimai::Reason::'.$e;
        require $ModulePath->{ $p };

        next unless $p->match($issuedcode);
        $reasontext = $p->text;
        last;
    }
    return $reasontext if $reasontext;

    if( uc $issuedcode eq 'X-UNIX' ) {
        # X-Unix; ...
        $reasontext = 'mailererror';

    } else {
        # Detect the bounce reason from "Status:" code
        require Sisimai::SMTP::Status;
        my $cv = Sisimai::SMTP::Status->find($argv1)   || '';
        $reasontext = Sisimai::SMTP::Status->name($cv) || 'undefined';
    }
    return $reasontext;
}

1;
__END__

=encoding utf-8

=head1 NAME

Sisimai::Reason - Detect the bounce reason

=head1 SYNOPSIS

    use Sisimai::Reason;

=head1 DESCRIPTION

Sisimai::Reason detects the bounce reason from the content of Sisimai::Fact object as an argument
of get() method. This class is called only Sisimai::Fact class.

=head1 CLASS METHODS

=head2 C<B<get(I<Sisimai::Fact Object>)>>

C<get()> detects the bounce reason.

=head2 C<B<anotherone(I<Sisimai::Fact object>)>>

C<anotherone()> is a method for detecting the bounce reason, it works as a fall back method of get()
and called only from get() method.

C<match()> detects the bounce reason from given text as a error message.

=head2 C<B<match(I<String>)>>

C<match()> is a method for detecting the bounce reason from the string given as an argument of the
method. However, this method is low analytical precision.

=head1 LIST OF BOUNCE REASONS

C<Sisimai::Reason->get()> detects the reason of bounce with parsing the bounced messages. The following
reasons will be set in the value of C<reason> property of Sisimai::Fact instance.

=head2 C<blocked>

This is the error that SMTP connection was rejected due to a client IP address or a hostname, or the
parameter of C<HELO/EHLO> command. This reason has added in Sisimai 4.0.0.

    <kijitora@example.net>:
    Connected to 192.0.2.112 but my name was rejected.
    Remote host said: 501 5.0.0 Invalid domain name

=head2 C<contenterror>

This is the error that a destination mail server has rejected email due to header format of the email
like the following. Sisimai will set C<contenterror> to the reason of email bounce if the value of
Status: field in a bounce email
is C<5.6.*>.

=over

=item - 8 bit data in message header

=item - Too many “Received” headers

=item - Invalid MIME headers

=back

    ... while talking to g5.example.net.:
    >>> DATA
    <<< 550 5.6.9 improper use of 8-bit data in message header
    554 5.0.0 Service unavailable

=head2 C<delivered>

This is NOT AN ERROR and means the message you sent has delivered to recipients successfully.

    Final-Recipient: rfc822; kijitora@neko.nyaan.jp
    Action: delivered
    Status: 2.1.5
    Remote-MTA: dns; home.neko.nyaan.jp
    Diagnostic-Code: SMTP; 250 2.1.5 OK

=head2 C<exceedlimit>

This is the error that a message was rejected due to an email exceeded the limit. The value of D.S.N.
is C<5.2.3>. This reason is almost the same as C<MesgTooBig>, we think.

    ... while talking to mx.example.org.:
    >>> MAIL From:<kijitora@example.co.jp> SIZE=16600348
    <<< 552 5.2.3 Message size exceeds fixed maximum message size (10485760)
    554 5.0.0 Service unavailable

=head2 C<expired>

This is the error that delivery time has expired due to connection failure or network error and the
message you sent has been in the queue for long time.

=head2 C<feedback>

The message you sent was forwarded to the sender as a complaint message from your mailbox provider.
When Sismai has set C<feedback> to the reason, the value of C<feedbacktype> is also set like the
following parsed data.

=head2 C<filtered>

This is the error that an email has been rejected by a header content after SMTP DATA command. In
Japanese cellular phones, the error will incur that a sender's email address or a domain is rejected
by recipient's email configuration. Sisimai will set C<filtered> to the reason of email bounce if
the value of Status: field in a bounce email is C<5.2.0> or C<5.2.1>.

This error reason is almost the same as UserUnknown.

    ... while talking to mfsmax.ntt.example.ne.jp.:
    >>> DATA
    <<< 550 Unknown user kijitora@ntt.example.ne.jp
    554 5.0.0 Service unavailable

=head2 C<hasmoved>

This is the error that a user's mailbox has moved (and is not forwarded automatically). Sisimai will
set C<hasmoved> to the reason of email bounce if the value of Status: field in a bounce email is C<5.1.6>.

    <kijitora@example.go.jp>: host mx1.example.go.jp[192.0.2.127] said: 550 5.1.6 recipient
        no longer on server: kijitora@example.go.jp (in reply to RCPT TO command)

=head2 C<hostunknown>

This is the error that a domain part (Right hand side of @ sign) of a recipient's email address does
not exist. In many case, the domain part is misspelled, or the domain name has been expired. Sisimai
will set C<hostunknown> to the reason of email bounce if the value of Status: field in a bounce mail
is C<5.1.2>.

    Your message to the following recipients cannot be delivered:

    <kijitora@example.cat>:
    <<< No such domain.

=head2 C<mailboxfull>

This is the error that a recipient's mailbox is full. Sisimai will set C<mailboxfull> to the reason
of email bounce if the value of Status: field in a bounce email is C<4.2.2> or C<5.2.2>.

    Action: failed
    Status: 5.2.2
    Diagnostic-Code: smtp;550 5.2.2 <kijitora@example.jp>... Mailbox Full

=head2 C<mailererror>

This is the error that a mailer program has not exited successfully or exited unexpectedly on a
destination mail server.

    X-Actual-Recipient: X-Unix; |/home/kijitora/mail/catch.php
    Diagnostic-Code: X-Unix; 255

=head2 C<mesgtoobig>

This is the error that a sent email size is too big for a destination mail server. In many case,
There are many attachment files with email, or the file size is too large. Sisimai will set C<mesgtoobig>
to the reason of email bounce if the value of Status: field in a bounce email is C<5.3.4>.

    Action: failed
    Status: 553 Exceeded maximum inbound message size

=head2 C<networkerror>

This is the error that SMTP connection failed due to DNS look up failure or other network problems.
This reason has added in Sisimai 4.1.12.

    A message is delayed for more than 10 minutes for the following
    list of recipients:

    kijitora@neko.example.jp: Network error on destination MXs

=head2 C<norelaying>

This is the error that SMTP connection rejected with error message C<Relaying Denied>.

    ... while talking to mailin-01.mx.example.com.:
    >>> RCPT To:<kijitora@example.org>
    <<< 554 5.7.1 <kijitora@example.org>: Relay access denied
    554 5.0.0 Service unavailable

=head2 C<notaccept>

This is the error that a destination mail server does ( or can ) not accept any email. In many case,
the server is high load or under the maintenance. Sisimai will set C<notaccept> to the reason of
email bounce if the value of Status: field in a bounce email is C<5.3.2> or the value of SMTP reply
code is 556.

=head2 C<onhold>

Sisimai will set C<onhold> to the reason of email bounce if there is no (or less) detailed information
about email bounce for judging the reason.

=head2 C<policyviolation>

This is the error that a policy violation was detected on a destination mail server. This reason has
been divided from C<securityerror> at Sisimai 4.22.0.

    570 5.7.7 Email not accepted for policy reasons

=head2 C<rejected>

This is the error that a connection to destination server was rejected by a sender's email address
(envelope from). Sisimai set C<rejected> to the reason of email bounce if the value of Status: field
in a bounce email is C<5.1.8> or the connection has been rejected due to the argument of SMTP MAIL
command.

    <kijitora@example.org>:
    Connected to 192.0.2.225 but sender was rejected.
    Remote host said: 550 5.7.1 <root@nijo.example.jp>... Access denied

=head2 C<securityerror>

This is the error that a security violation was detected on a destination mail server. Depends on the
security policy on the server, a sender's email address is camouflaged address. Sisimai will set
C<securityerror> to the reason of email bounce if the value of Status: field in a bounce email is C<5.7.*>.

    Status: 5.7.0
    Remote-MTA: DNS; gmail-smtp-in.l.google.com
    Diagnostic-Code: SMTP; 552-5.7.0 Our system detected an illegal attachment on your message. Please

=head2 C<spamdetected>

This is the error that the message you sent was rejected by C<spam> filter which is running on the
remote host. This reason has added in Sisimai 4.1.25.

    Action: failed
    Status: 5.7.1
    Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, UBE, id=00000-00-000
    Last-Attempt-Date: Thu, 9 Apr 2008 23:34:45 +0900 (JST)

=head2 C<suspend>

This is the error that a recipient account is being suspended due to unpaid or other reasons.

=head2 C<syntaxerror>

This is the error that a destination mail server could not recognize SMTP command which is sent from
a sender's MTA. This reason has been added at Sisimai v4.17.0.

=over

=item - 503 Improper sequence of commands

=item - 504 Command parameter not implemented

=back

=head2 C<systemerror>

This is the error that an email has bounced due to system error on the remote host such as LDAP
connection failure or other internal system error.

    <kijitora@example.net>:
    Unable to contact LDAP server. (#4.4.3)I'm not going to try again; this
    message has been in the queue too long.

=head2 C<systemfull>

This is the error that a destination mail server's disk (or spool) is full.  Sisimai will set C<systemfull>
to the reason of email bounce if the value of Status: field in a bounce email is C<4.3.1> or C<5.3.1>.

=head2 C<toomanyconn>

This is the error that SMTP connection was rejected temporarily due to too many concurrency connections
to the remote server. This reason has added in Sisimai 4.1.26.

    <kijitora@example.ne.jp>: host mx02.example.ne.jp[192.0.1.20] said:
        452 4.3.2 Connection rate limit exceeded. (in reply to MAIL FROM command)

=head2 C<userunknown>

This is the error that a local part (Left hand side of @ sign) of a recipient's email address does
not exist. In many case, a user has changed internet service provider, or has quit company, or the
local part is misspelled. Sisimai will set C<userunknown> to the reason of email bounce if the value
of Status: field in a bounce email is C<5.1.1>, or connection was refused at SMTP RCPT command, or
the contents of Diagnostic-Code: field represents that it is unknown user.

    <kijitora@example.co.jp>: host mx01.example.co.jp[192.0.2.8] said:
      550 5.1.1 Address rejected kijitora@example.co.jp (in reply to
      RCPT TO command)

=head2 C<undefined>

Sisimai could not detect the error reason. In many case, error message is written in non-English or
there are no enough error message in a bounce email to decide the reason.

=head2 C<vacation>

This is the reason that the recipient is out of office. The bounce message is generated and returned
from auto responder program. This reason has added in Sisimai 4.1.28

=head2 C<virusdetected>

This is the error that an email was rejected by a virus scanner at a destination mail server. This
reason has been divided from C<securityerror> at Sisimai 4.22.

    Your message was infected with a virus. You should download a virus
    scanner and check your computer for viruses.

    Sender:    <sironeko@libsisimai.org>
    Recipient: <kijitora@example.jp>

=head1 SEE ALSO

L<Sisimai::ARF>
L<http://tools.ietf.org/html/rfc5965>
L<https://libsisimai.org/en/reason/>

=head1 AUTHOR

azumakuniyuki

=head1 COPYRIGHT

Copyright (C) 2014-2023 azumakuniyuki, All rights reserved.

=head1 LICENSE

This software is distributed under The BSD 2-Clause License.

=cut

