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 UserUnknown
        Filtered Rejected HostUnknown SpamDetected TooManyConn Blocked
    |],
    [qw|MailboxFull SpamDetected PolicyViolation VirusDetected NoRelaying
        SecurityError SystemError NetworkError Suspend Expired ContentError
        SystemFull NotAccept MailerError
    |],
    [qw|MailboxFull MesgTooBig ExceedLimit Suspend UserUnknown Filtered Rejected
        HostUnknown SpamDetected TooManyConn Blocked SpamDetected 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   [Array] Reason list
    return {
        'undefined' => 1, 'onhold' => 1, 'systemerror' => 1, 'securityerror' => 1,
        'networkerror' => 1, 'hostunknown' => 1, 'userunknown'=> 1
    };
}

sub index {
    # All the error reason list Sisimai support
    # @return   [Array] Reason list
    return [qw|
        Blocked ContentError ExceedLimit Expired Filtered HasMoved HostUnknown
        MailboxFull MailerError MesgTooBig NetworkError NotAccept OnHold
        Rejected NoRelaying SpamDetected VirusDetected PolicyViolation SecurityError
        Suspend 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    [Sisimai::Data] argvs   Parsed email object
    # @return   [String, Undef]         Bounce reason or Undef if the argument
    #                                   is missing or invalid object
    # @see anotherone
    my $class = shift;
    my $argvs = shift // return undef;
    return undef unless ref $argvs eq 'Sisimai::Data';

    unless( exists $GetRetried->{ $argvs->reason } ) {
        # Return reason text already decided except reason match 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 = '';
    if( $argvs->diagnostictype eq 'SMTP' || $argvs->diagnostictype eq '' ) {
        # Diagnostic-Code: SMTP; ... or empty value
        for my $e ( @{ $ClassOrder->[0] } ) {
            # Check the value of Diagnostic-Code: and the value of Status:, it is a
            # deliverystats, with true() method in each Sisimai::Reason::* class.
            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 $argvs->diagnosticcode);
        $reasontext ||= 'onhold'   if $argvs->diagnosticcode;
        $reasontext ||= 'undefined';
    }
    return $reasontext;
}

sub anotherone {
    # Detect the other bounce reason, fall back method for get()
    # @param    [Sisimai::Data] argvs   Parsed email object
    # @return   [String, Undef]         Bounce reason or Undef if the argument
    #                                   is missing or invalid object
    # @see get
    my $class = shift;
    my $argvs = shift // return undef;

    return undef unless ref $argvs eq 'Sisimai::Data';
    return $argvs->reason if $argvs->reason;

    require Sisimai::SMTP::Status;
    my $statuscode = $argvs->deliverystatus // '';
    my $reasontext = Sisimai::SMTP::Status->name($statuscode) || '';

    TRY_TO_MATCH: while(1) {
        my $diagnostic   = lc $argvs->diagnosticcode // '';
        my $trytomatch   = $reasontext eq '' ? 1 : 0;
           $trytomatch ||= 1 if $reasontext eq 'expired';
           $trytomatch ||= 1 if exists $GetRetried->{ $reasontext };
           $trytomatch ||= 1 if $argvs->diagnostictype 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($diagnostic);
            $reasontext = lc $e;
            last;
        }
        last(TRY_TO_MATCH) if $reasontext;

        # Check the value of Status:
        if( (my $v = substr($statuscode, 0, 3)) =~ /\A[45][.]6\z/ ) {
            #  X.6.0   Other or undefined media error
            $reasontext = 'contenterror';

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

        } elsif( $argvs->diagnostictype eq 'X-UNIX' || $argvs->diagnostictype eq 'X-POSTFIX' ) {
            # Diagnostic-Code: X-UNIX; ...
            $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( $argvs->action =~ /\A(?:delayed|expired)/ ) {
            # Action: delayed, expired
            $reasontext = 'expired';
        } else {
            # Check the value of SMTP command
            my $commandtxt = $argvs->smtpcommand // '';
            if( $commandtxt eq 'EHLO' || $commandtxt 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 $diagnostic = lc $argv1;

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

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

    # Check the value of $typestring
    my $typestring = uc($argv1) =~ /\A(SMTP|X-.+);/ ? uc($1) : '';
    if( $typestring eq 'X-UNIX' ) {
        # X-Unix; ...
        $reasontext = 'mailererror';
    } else {
        # Detect the bounce reason from "Status:" code
        require Sisimai::SMTP::Status;
        my $statuscode = Sisimai::SMTP::Status->find($argv1) || '';
        $reasontext = Sisimai::SMTP::Status->name($statuscode) || '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::Data
object as an argument of get() method. This class is called only Sisimai::Data
class.

=head1 CLASS METHODS

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

C<get()> detects the bounce reason.

=head2 C<B<anotherone(I<Sisimai::Data 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::Data 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 and does not exist in any version of bounceHammer.

    <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: deliverable
    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: failure
    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 and does not
exist in any version of bounceHammer.

    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>. This reason does not exist in any version of bounceHammer.

    ... 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 and does
not exist in any version of bounceHammer.

    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 and does not exist in any version of bounceHammer.

    <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 and does not exist in any version of bounceHammer.

=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-2020,2021 azumakuniyuki, All rights reserved.

=head1 LICENSE

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

=cut
