package Qpsmtpd::SMTP;
use strict;

use base 'Qpsmtpd';

use Carp;

#use Data::Dumper;
use POSIX qw(strftime);
use Mail::Header;
use Net::DNS;

use Qpsmtpd;
use Qpsmtpd::Connection;
use Qpsmtpd::Transaction;
use Qpsmtpd::Plugin;
use Qpsmtpd::Constants;
use Qpsmtpd::Auth;
use Qpsmtpd::Command;

my %auth_mechanisms = ();

# this is only good for forkserver
# can't set these here, cause forkserver resets them
#$SIG{ALRM} = sub { respond(421, "timeout; I can't wait that long..."); exit };
#$SIG{ALRM} = sub { warn "Connection Timed Out\n"; exit; };

sub new {
    my ($proto, %args) = @_;
    my $class = ref($proto) || $proto;

    my $self = bless({args => \%args}, $class);

   # this list of valid commands should probably be a method or a set of methods
    $self->{_commands} =
      {map { $_ => '' } qw(ehlo helo rset mail rcpt data help vrfy noop quit)};

    $self->SUPER::_restart(%args) if $args{restart}; # calls Qpsmtpd::_restart()
    $self;
}

sub command_counter {
    my $self = shift;
    $self->{_counter} || 0;
}

sub dispatch {
    my $self = shift;
    my ($cmd) = shift;
    if (!$cmd) {
        $self->run_hooks("unrecognized_command", '', @_);
        return 1;
    }
    $cmd = lc $cmd;

    $self->{_counter}++;

    if ($cmd !~ /^(\w{1,12})$/ or !exists $self->{_commands}->{$1}) {
        $self->run_hooks("unrecognized_command", $cmd, @_);
        return 1;
    }
    $cmd = $1;

    my ($result) = eval { $self->$cmd(@_) };
    $self->log(LOGERROR, "XX: $@") if $@;
    return $result if defined $result;
    return $self->fault("command '$cmd' failed unexpectedly");
}

sub unrecognized_command_respond {
    my ($self, $rc, $msg) = @_;
    if ($rc == DENY_DISCONNECT) {
        $self->respond(521, @$msg);
        $self->disconnect;
    }
    elsif ($rc == DENY) {
        $self->respond(500, @$msg);
    }
    elsif ($rc != DONE) {
        $self->respond(500, "Unrecognized command");
    }
}

sub fault {
    my $self = shift;
    my ($msg) = shift || "program fault - command not performed";
    my ($name) = split /\s+/, $0, 2;
    print STDERR $name, "[$$]: $msg\n";
    print STDERR $name, "[$$]: Last system error: $!"
      . " (Likely irelevant--debug the crashed plugin to ensure it handles \$! properly)";
    return $self->respond(451, "Internal error - try again later - " . $msg);
}

sub start_conversation {
    my $self = shift;

    # this should maybe be called something else than "connect", see
    # lib/Qpsmtpd/TcpServer.pm for more confusion.
    $self->run_hooks("connect");
    return DONE;
}

sub connect_respond {
    my ($self, $rc, $msg) = @_;
    if ($rc == DENY || $rc == DENY_DISCONNECT) {
        $msg->[0] ||= 'Connection from you denied, bye bye.';
        $self->respond(550, @$msg);
        $self->disconnect;
    }
    elsif ($rc == DENYSOFT || $rc == DENYSOFT_DISCONNECT) {
        $msg->[0] ||= 'Connection from you temporarily denied, bye bye.';
        $self->respond(450, @$msg);
        $self->disconnect;
    }
    elsif ($rc != DONE) {
        my $greets = $self->config('smtpgreeting');
        if ($greets) {
            $greets .= " ESMTP" unless $greets =~ /(^|\W)ESMTP(\W|$)/;
        }
        else {
            $greets =
                $self->config('me')
              . " ESMTP qpsmtpd "
              . $self->version
              . " ready; send us your mail, but not your spam.";
        }

        $self->respond(220, $greets);
    }
}

sub transaction {
    my $self = shift;
    return $self->{_transaction} || $self->reset_transaction();
}

sub reset_transaction {
    my $self = shift;
    $self->run_hooks("reset_transaction") if $self->{_transaction};
    return $self->{_transaction} = Qpsmtpd::Transaction->new();
}

sub connection {
    my $self = shift;
    if (@_) { $self->{_connection} = shift; }
    return $self->{_connection} if $self->{_connection};
    return $self->{_connection} = Qpsmtpd::Connection->new();
}

sub helo {
    my ($self, $line) = @_;
    my ($rc,   @msg)  = $self->run_hooks('helo_parse');
    my ($ok, $hello_host, @stuff) =
      Qpsmtpd::Command->parse('helo', $line, $msg[0]);

    return $self->respond(501,
                          "helo requires domain/address - see RFC-2821 4.1.1.1")
      unless $hello_host;
    my $conn = $self->connection;
    return $self->respond(503, "but you already said HELO ...") if $conn->hello;

    $self->run_hooks("helo", $hello_host, @stuff);
}

sub helo_respond {
    my ($self, $rc, $msg, $args) = @_;
    my ($hello_host) = @$args;
    if ($rc == DONE) {

        # do nothing:
        1;
    }
    elsif ($rc == DENY) {
        $self->respond(550, @$msg);
    }
    elsif ($rc == DENYSOFT) {
        $self->respond(450, @$msg);
    }
    elsif ($rc == DENY_DISCONNECT) {
        $self->respond(550, @$msg);
        $self->disconnect;
    }
    elsif ($rc == DENYSOFT_DISCONNECT) {
        $self->respond(450, @$msg);
        $self->disconnect;
    }
    else {
        my $conn = $self->connection;
        $conn->hello("helo");
        $conn->hello_host($hello_host);
        $self->transaction;
        $self->respond(
                       250,
                       $self->config('me') . " Hi "
                         . $conn->remote_info . " ["
                         . $conn->remote_ip
                         . "]; I am so happy to meet you."
                      );
    }
}

sub ehlo {
    my ($self, $line) = @_;
    my ($rc,   @msg)  = $self->run_hooks('ehlo_parse');
    my ($ok, $hello_host, @stuff) =
      Qpsmtpd::Command->parse('ehlo', $line, $msg[0]);
    return $self->respond(501,
                          "ehlo requires domain/address - see RFC-2821 4.1.1.1")
      unless $hello_host;
    my $conn = $self->connection;
    return $self->respond(503, "but you already said HELO ...") if $conn->hello;

    $self->run_hooks("ehlo", $hello_host, @stuff);
}

sub ehlo_respond {
    my ($self, $rc, $msg, $args) = @_;
    my ($hello_host) = @$args;
    if ($rc == DONE) {

        # do nothing:
        1;
    }
    elsif ($rc == DENY) {
        $self->respond(550, @$msg);
    }
    elsif ($rc == DENYSOFT) {
        $self->respond(450, @$msg);
    }
    elsif ($rc == DENY_DISCONNECT) {
        $self->respond(550, @$msg);
        $self->disconnect;
    }
    elsif ($rc == DENYSOFT_DISCONNECT) {
        $self->respond(450, @$msg);
        $self->disconnect;
    }
    else {
        my $conn = $self->connection;
        $conn->hello("ehlo");
        $conn->hello_host($hello_host);
        $self->transaction;

        my @capabilities =
          $self->transaction->notes('capabilities')
          ? @{$self->transaction->notes('capabilities')}
          : ();

        # Check for possible AUTH mechanisms
      HOOK: foreach my $hook (keys %{$self->hooks}) {
            if ($hook =~ m/^auth-?(.+)?$/) {
                if (defined $1) {
                    $auth_mechanisms{uc($1)} = 1;
                }
                else {    # at least one polymorphous auth provider
                    %auth_mechanisms = map { $_, 1 } qw(PLAIN CRAM-MD5 LOGIN);
                    last HOOK;
                }
            }
        }

        # Check if we should only offer AUTH after TLS is completed
        my $tls_before_auth = (
                               $self->config('tls_before_auth')
                               ? ($self->config('tls_before_auth'))[0]
                                 && $self->transaction->notes('tls_enabled')
                               : 0
                              );
        if (%auth_mechanisms && !$tls_before_auth) {
            push @capabilities, 'AUTH ' . join(" ", keys(%auth_mechanisms));
            $self->{_commands}->{'auth'} = "";
        }

        $self->respond(
                       250,
                       $self->config("me") . " Hi "
                         . $conn->remote_info . " ["
                         . $conn->remote_ip . "]",
                       "PIPELINING",
                       "8BITMIME",
                       (
                        $self->config('databytes')
                        ? "SIZE " . ($self->config('databytes'))[0]
                        : ()
                       ),
                       @capabilities,
                      );
    }
}

sub auth {
    my ($self, $line) = @_;
    $self->run_hooks('auth_parse', $line);
}

sub auth_parse_respond {
    my ($self, $rc, $msg, $args) = @_;
    my ($line) = @$args;

    my ($ok, $mechanism, @stuff) =
      Qpsmtpd::Command->parse('auth', $line, $msg->[0]);
    return $self->respond(501, $mechanism || "Syntax error in command")
      unless ($ok == OK);

    $mechanism = lc($mechanism);

    #they AUTH'd once already
    return $self->respond(503, "but you already said AUTH ...")
      if (defined $self->{_auth} && $self->{_auth} == OK);

    return $self->respond(503, "AUTH not defined for HELO")
      if ($self->connection->hello eq "helo");

    return $self->respond(503, "SSL/TLS required before AUTH")
      if (($self->config('tls_before_auth'))[0]
          && $self->transaction->notes('tls_enabled'));

    # we don't have a plugin implementing this auth mechanism, 504
    if (exists $auth_mechanisms{uc($mechanism)}) {
        return $self->{_auth} = Qpsmtpd::Auth::SASL($self, $mechanism, @stuff);
    }

    $self->respond(504, "Unimplemented authentification mechanism: $mechanism");
    return DENY;
}

sub mail {
    my ($self, $line) = @_;

    # -> from RFC2821
    # The MAIL command (or the obsolete SEND, SOML, or SAML commands)
    # begins a mail transaction.  Once started, a mail transaction
    # consists of a transaction beginning command, one or more RCPT
    # commands, and a DATA command, in that order.  A mail transaction
    # may be aborted by the RSET (or a new EHLO) command.  There may be
    # zero or more transactions in a session.  MAIL (or SEND, SOML, or
    # SAML) MUST NOT be sent if a mail transaction is already open,
    # i.e., it should be sent only if no mail transaction had been
    # started in the session, or it the previous one successfully
    # concluded with a successful DATA command, or if the previous one
    # was aborted with a RSET.

    # sendmail (8.11) rejects a second MAIL command.

    # qmail-smtpd (1.03) accepts it and just starts a new transaction.
    # Since we are a qmail-smtpd thing we will do the same.

    $self->reset_transaction;

    if (!$self->connection->hello) {
        return $self->respond(503, "please say hello first ...");
    }

    $self->log(LOGDEBUG, "full from_parameter: $line");
    $self->connection->notes('envelope_from', $line);
    $self->run_hooks("mail_parse", $line);
}

sub mail_parse_respond {
    my ($self, $rc, $msg, $args) = @_;
    my ($line) = @$args;
    my ($ok, $from, @params) =
      Qpsmtpd::Command->parse('mail', $line, $msg->[0]);
    return $self->respond(501, $from || "Syntax error in command")
      unless ($ok == OK);
    my %param;
    foreach (@params) {
        my ($k, $v) = split /=/, $_, 2;
        $param{lc $k} = $v;
    }

    # to support addresses without <> we now require a plugin
    # hooking "mail_pre" to
    #   return (OK, "<$from>");
    # (...or anything else parseable by Qpsmtpd::Address ;-))
    # see also comment in sub rcpt()
    $self->run_hooks("mail_pre", $from, \%param);
}

sub mail_pre_respond {
    my ($self, $rc, $msg, $args) = @_;
    my ($from, $param) = @$args;
    if ($rc == OK) {
        $from = shift @$msg;
    }

    $self->log(LOGDEBUG, "from email address : [$from]");
    return $self->respond(501, "could not parse your mail from command")
      unless $from =~ /^<.*>$/;

    if ($from eq "<>" or $from =~ m/\[undefined\]/ or $from eq "<#@[]>") {
        $from = $self->address("<>");
    }
    else {
        $from = $self->address($from);
    }
    return $self->respond(501, "could not parse your mail from command")
      unless $from;

    $self->run_hooks("mail", $from, %$param);
}

sub mail_respond {
    my ($self, $rc, $msg, $args) = @_;
    my ($from, $param) = @$args;
    if ($rc == DONE) {
        return 1;
    }
    elsif ($rc == DENY) {
        $msg->[0] ||= $from->format . ', denied';
        $self->log(LOGINFO, "deny mail from " . $from->format . " (@$msg)");
        $self->respond(550, @$msg);
    }
    elsif ($rc == DENYSOFT) {
        $msg->[0] ||= $from->format . ', temporarily denied';
        $self->log(LOGINFO, "denysoft mail from " . $from->format . " (@$msg)");
        $self->respond(450, @$msg);
    }
    elsif ($rc == DENY_DISCONNECT) {
        $msg->[0] ||= $from->format . ', denied';
        $self->log(LOGINFO, "deny mail from " . $from->format . " (@$msg)");
        $self->respond(550, @$msg);
        $self->disconnect;
    }
    elsif ($rc == DENYSOFT_DISCONNECT) {
        $msg->[0] ||= $from->format . ', temporarily denied';
        $self->log(LOGINFO, "denysoft mail from " . $from->format . " (@$msg)");
        $self->respond(421, @$msg);
        $self->disconnect;
    }
    else {    # includes OK
        $self->log(LOGDEBUG, "getting mail from " . $from->format);
        $self->respond(
                       250,
                       $from->format
                         . ", sender OK - how exciting to get mail from you!"
                      );
        $self->transaction->sender($from);
    }
}

sub rcpt {
    my ($self, $line) = @_;
    $self->connection->notes('envelope_rcpt', $line);
    $self->run_hooks("rcpt_parse", $line);
}

sub rcpt_parse_respond {
    my ($self, $rc, $msg, $args) = @_;
    my ($line) = @$args;
    my ($ok, $rcpt, @param) = Qpsmtpd::Command->parse("rcpt", $line, $msg->[0]);
    return $self->respond(501, $rcpt || "Syntax error in command")
      unless ($ok == OK);
    return $self->respond(503, "Use MAIL before RCPT")
      unless $self->transaction->sender;

    my %param;
    foreach (@param) {
        my ($k, $v) = split /=/, $_, 2;
        $param{lc $k} = $v;
    }

    # to support addresses without <> we now require a plugin
    # hooking "rcpt_pre" to
    #   return (OK, "<$rcpt>");
    # (... or anything else parseable by Qpsmtpd::Address ;-))
    # this means, a plugin can decide to (pre-)accept
    # addresses like <user@example.com.> or <user@example.com >
    # by removing the trailing dot or space from this example.
    $self->run_hooks("rcpt_pre", $rcpt, \%param);
}

sub rcpt_pre_respond {
    my ($self, $rc, $msg, $args) = @_;
    my ($rcpt, $param) = @$args;
    if ($rc == OK) {
        $rcpt = shift @$msg;
    }
    $self->log(LOGDEBUG, "to email address : [$rcpt]");
    return $self->respond(501, "could not parse recipient")
      unless $rcpt =~ /^<.*>$/;

    $rcpt = $self->address($rcpt);

    return $self->respond(501, "could not parse recipient")
      if (!$rcpt or ($rcpt->format eq '<>'));

    $self->run_hooks("rcpt", $rcpt, %$param);
}

sub rcpt_respond {
    my ($self, $rc, $msg, $args) = @_;
    my ($rcpt, $param) = @$args;
    if ($rc == DONE) {
        return 1;
    }
    elsif ($rc == DENY) {
        $msg->[0] ||= 'relaying denied';
        $self->respond(550, @$msg);
    }
    elsif ($rc == DENYSOFT) {
        $msg->[0] ||= 'relaying denied';
        return $self->respond(450, @$msg);
    }
    elsif ($rc == DENY_DISCONNECT) {
        $msg->[0] ||= 'delivery denied';
        $self->log(LOGDEBUG, "delivery denied (@$msg)");
        $self->respond(550, @$msg);
        $self->disconnect;
    }
    elsif ($rc == DENYSOFT_DISCONNECT) {
        $msg->[0] ||= 'relaying denied';
        $self->log(LOGDEBUG, "delivery denied (@$msg)");
        $self->respond(421, @$msg);
        $self->disconnect;
    }
    elsif ($rc == OK) {
        $self->respond(250, $rcpt->format . ", recipient ok");
        return $self->transaction->add_recipient($rcpt);
    }
    else {
        return $self->respond(450, "No plugin decided if relaying is allowed");
    }
    return 0;
}

sub help {
    my ($self, @args) = @_;
    $self->run_hooks("help", @args);
}

sub help_respond {
    my ($self, $rc, $msg, $args) = @_;

    return 1
      if $rc == DONE;

    if ($rc == DENY) {
        $msg->[0] ||= "Syntax error, command not recognized";
        $self->respond(500, @$msg);
    }
    else {
        unless ($msg->[0]) {
            @$msg = (
                     "This is qpsmtpd "
                       . ($self->config('smtpgreeting') ? '' : $self->version),
                     "See http://smtpd.develooper.com/",
'To report bugs or send comments, mail to <ask@develooper.com>.'
                    );
        }
        $self->respond(214, @$msg);
    }
    return 1;
}

sub noop {
    my $self = shift;
    $self->run_hooks("noop");
}

sub noop_respond {
    my ($self, $rc, $msg, $args) = @_;
    return 1 if $rc == DONE;

    if ($rc == DENY || $rc == DENY_DISCONNECT) {
        $msg->[0] ||= "Stop wasting my time.";  # FIXME: better default message?
        $self->respond(500, @$msg);
        $self->disconnect if $rc == DENY_DISCONNECT;
        return 1;
    }

    $self->respond(250, "OK");
    return 1;
}

sub vrfy {
    my $self = shift;

    # Note, this doesn't support the multiple ambiguous results
    # documented in RFC2821#3.5.1
    # I also don't think it provides all the proper result codes.

    $self->run_hooks("vrfy");
}

sub vrfy_respond {
    my ($self, $rc, $msg, $args) = @_;
    if ($rc == DONE) {
        return 1;
    }
    elsif ($rc == DENY) {
        $msg->[0] ||= "Access Denied";
        $self->respond(554, @$msg);
        $self->reset_transaction();
        return 1;
    }
    elsif ($rc == OK) {
        $msg->[0] ||= "User OK";
        $self->respond(250, @$msg);
        return 1;
    }
    else {    # $rc == DECLINED or anything else
        $self->respond(252,
                  "Just try sending a mail and we'll see how it turns out ...");
        return 1;
    }
}

sub rset {
    my $self = shift;
    $self->reset_transaction;
    $self->respond(250, "OK");
}

sub quit {
    my $self = shift;
    $self->run_hooks("quit");
}

sub quit_respond {
    my ($self, $rc, $msg, $args) = @_;
    if ($rc != DONE) {
        $msg->[0] ||=
          $self->config('me') . " closing connection. Have a wonderful day.";
        $self->respond(221, @$msg);
    }
    $self->disconnect();
}

sub disconnect {
    my $self = shift;
    $self->run_hooks("disconnect");
    $self->connection->notes(disconnected => 1);
    $self->reset_transaction;
}

sub data {
    my $self = shift;
    $self->run_hooks("data");
}

sub data_respond {
    my ($self, $rc, $msg, $args) = @_;
    if ($rc == DONE) {
        return 1;
    }
    elsif ($rc == DENY) {
        $msg->[0] ||= "Message denied";
        $self->respond(554, @$msg);
        $self->reset_transaction();
        return 1;
    }
    elsif ($rc == DENYSOFT) {
        $msg->[0] ||= "Message denied temporarily";
        $self->respond(451, @$msg);
        $self->reset_transaction();
        return 1;
    }
    elsif ($rc == DENY_DISCONNECT) {
        $msg->[0] ||= "Message denied";
        $self->respond(554, @$msg);
        $self->disconnect;
        return 1;
    }
    elsif ($rc == DENYSOFT_DISCONNECT) {
        $msg->[0] ||= "Message denied temporarily";
        $self->respond(421, @$msg);
        $self->disconnect;
        return 1;
    }
    $self->respond(503, "MAIL first"), return 1
      unless $self->transaction->sender;
    $self->respond(503, "RCPT first"), return 1
      unless $self->transaction->recipients;
    $self->respond(354, "go ahead");

    my $buffer = '';
    my $size   = 0;
    my $i      = 0;
    my $max_size =
      ($self->config('databytes'))[0] || 0; # this should work in scalar context
    my $blocked = "";
    my %matches;
    my $in_header = 1;
    my $complete  = 0;

    $self->log(LOGDEBUG, "max_size: $max_size / size: $size");

    my $header = Mail::Header->new(Modify => 0, MailFrom => "COERCE");

    my $timeout = $self->config('timeout');
    while (defined($_ = $self->getline($timeout))) {
        if ($_ eq ".\r\n") {
            $complete++;
            $_ = '';
        }
        $i++;

    # should probably use \012 and \015 in these checks instead of \r and \n ...

        # Reject messages that have either bare LF or CR. rjkaes noticed a
        # lot of spam that is malformed in the header.

        ($_ eq ".\n" or $_ eq ".\r")
          and $self->respond(421, "See http://smtpd.develooper.com/barelf.html")
          and return $self->disconnect;

# add a transaction->blocked check back here when we have line by line plugin access...
        unless (($max_size and $size > $max_size)) {
            s/\r\n$/\n/;
            s/^\.\./\./;
            if ($in_header && (m/^$/ || $complete > 0)) {
                $in_header = 0;
                my @headers = split /^/m, $buffer;

        # ... need to check that we don't reformat any of the received lines.
        #
        # 3.8.2 Received Lines in Gatewaying
        #   When forwarding a message into or out of the Internet environment, a
        #   gateway MUST prepend a Received: line, but it MUST NOT alter in any
        #   way a Received: line that is already in the header.

                $header->extract(\@headers);

#$header->add("X-SMTPD", "qpsmtpd/".$self->version.", http://smtpd.develooper.com/");

                $buffer = "";

                $self->transaction->header($header);

                # NOTE: This will not work properly under async.  A
                # data_headers_end_respond needs to be created.
                my ($rc, $msg) = $self->run_hooks('data_headers_end');
                if ($rc == DENY_DISCONNECT) {
                    $self->respond(554, $msg || "Message denied");
                    $self->disconnect;
                    return 1;
                }
                elsif ($rc == DENYSOFT_DISCONNECT) {
                    $self->respond(421, $msg || "Message denied temporarily");
                    $self->disconnect;
                    return 1;
                }

                # Save the start of just the body itself
                $self->transaction->set_body_start();

            }

            # grab a copy of all of the header lines
            if ($in_header) {
                $buffer .= $_;
            }

            # copy all lines into the spool file, including the headers
            # we will create a new header later before sending onwards
            $self->transaction->body_write($_) if !$complete;
            $size += length $_;
        }
        last if $complete > 0;

        #$self->log(LOGDEBUG, "size is at $size\n") unless ($i % 300);
    }

    $self->log(LOGDEBUG, "max_size: $max_size / size: $size");

    # if we get here without seeing a terminator, the connection is
    # probably dead.
    unless ($complete) {
        $self->respond(451, "Incomplete DATA");
        $self->reset_transaction;    # clean up after ourselves
        return 1;
    }

#$self->respond(550, $self->transaction->blocked),return 1 if ($self->transaction->blocked);
    if ($max_size and $size > $max_size) {
        $self->log(LOGALERT,
                   "Message too big: size: $size (max size: $max_size)");
        $self->respond(552, "Message too big!");
        $self->reset_transaction;    # clean up after ourselves
        return 1;
    }

    $self->authentication_results();
    $self->received_line();
    $self->run_hooks("data_post");
}

sub authentication_results {
    my ($self) = @_;

    my @auth_list = $self->config('me');

    #   $self->clean_authentication_results();

    if (!defined $self->{_auth}) {
        push @auth_list, 'auth=none';
    }
    else {
        my $mechanism = "(" . $self->{_auth_mechanism} . ")";
        my $user      = "smtp.auth=" . $self->{_auth_user};
        if ($self->{_auth} == OK) {
            push @auth_list, "auth=pass $mechanism $user";
        }
        else {
            push @auth_list, "auth=fail $mechanism $user";
        }
    }

    # RFC 5451: used in AUTH, DKIM, DOMAINKEYS, SENDERID, SPF
    if ($self->connection->notes('authentication_results')) {
        push @auth_list, $self->connection->notes('authentication_results');
    }

    $self->log(LOGDEBUG, "adding auth results header");
    $self->transaction->header->add('Authentication-Results',
                                    join('; ', @auth_list), 0);
}

sub clean_authentication_results {
    my $self = shift;

    # http://tools.ietf.org/html/draft-kucherawy-original-authres-00.html

   # On messages received from the internet, move Authentication-Results headers
   # to Original-AR, so our downstream can trust the A-R header we insert.

    # TODO: Do not invalidate DKIM signatures.
    #   if $self->transaction->header->get('DKIM-Signature')
    #       Parse the DKIM signature(s)
    #       return if A-R header is signed;
    #   }

    my @ar_headers = $self->transaction->header->get('Authentication-Results');
    for (my $i = 0 ; $i < scalar @ar_headers ; $i++) {
        $self->transaction->header->delete('Authentication-Results', $i);
        $self->transaction->header->add('Original-Authentication-Results',
                                        $ar_headers[$i]);
    }

    $self->log(LOGDEBUG,
             "Authentication-Results moved to Original-Authentication-Results");
}

sub received_line {
    my ($self) = @_;

    my $smtp = $self->connection->hello eq "ehlo" ? "ESMTP" : "SMTP";
    my $esmtp      = substr($smtp, 0, 1) eq "E";
    my $authheader = '';
    my $sslheader  = '';

    if (defined $self->connection->notes('tls_enabled')
        and $self->connection->notes('tls_enabled'))
    {
        $smtp .= "S" if $esmtp;    # RFC3848
        $sslheader = "("
          . $self->connection->notes('tls_socket')->get_cipher()
          . " encrypted) ";
    }
    if (defined $self->{_auth} && $self->{_auth} == OK) {
        my $mech = $self->{_auth_mechanism};
        my $user = $self->{_auth_user};
        $smtp .= "A" if $esmtp;    # RFC3848
        $authheader = "(smtp-auth username $user, mechanism $mech)\n";
    }

    my $header_str;
    my ($rc, @received) =
      $self->run_hooks("received_line", $smtp, $authheader, $sslheader);
    if ($rc == YIELD) {
        die "YIELD not supported for received_line hook";
    }
    elsif ($rc == OK) {
        return join("\n", @received);
    }
    else {    # assume $rc == DECLINED
        $header_str =
            "from "
          . $self->connection->remote_info
          . " (HELO "
          . $self->connection->hello_host . ") ("
          . $self->connection->remote_ip
          . ")\n by "
          . $self->config('me')
          . " (qpsmtpd/"
          . $self->version
          . ") with $sslheader$smtp; "
          . (strftime('%a, %d %b %Y %H:%M:%S %z', localtime));
    }
    $self->transaction->header->add('Received', $header_str, 0);
}

sub data_post_respond {
    my ($self, $rc, $msg, $args) = @_;
    if ($rc == DONE) {
        return 1;
    }
    elsif ($rc == DENY) {
        $msg->[0] ||= "Message denied";
        $self->respond(552, @$msg);

        # DATA is always the end of a "transaction"
        return $self->reset_transaction;
    }
    elsif ($rc == DENYSOFT) {
        $msg->[0] ||= "Message denied temporarily";
        $self->respond(452, @$msg);

        # DATA is always the end of a "transaction"
        return $self->reset_transaction;
    }
    elsif ($rc == DENY_DISCONNECT) {
        $msg->[0] ||= "Message denied";
        $self->respond(552, @$msg);
        $self->disconnect;
        return 1;
    }
    elsif ($rc == DENYSOFT_DISCONNECT) {
        $msg->[0] ||= "Message denied temporarily";
        $self->respond(452, @$msg);
        $self->disconnect;
        return 1;
    }
    else {
        $self->queue($self->transaction);
    }
}

sub getline {
    my ($self, $timeout) = @_;

    alarm $timeout;
    my $line = <STDIN>;    # default implementation
    alarm 0;
    return $line;
}

sub queue {
    my ($self, $transaction) = @_;

    # First fire any queue_pre hooks
    $self->run_hooks("queue_pre");
}

sub queue_pre_respond {
    my ($self, $rc, $msg, $args) = @_;
    if ($rc == DONE) {
        return 1;
    }
    elsif ($rc != OK and $rc != DECLINED and $rc != 0) {
        return $self->log(LOGERROR, "pre plugin returned illegal value");
        return 0;
    }

    # If we got this far, run the queue hooks
    $self->run_hooks("queue");
}

sub queue_respond {
    my ($self, $rc, $msg, $args) = @_;

    # reset transaction if we queued the mail
    $self->reset_transaction;

    if ($rc == DONE) {
        return 1;
    }
    elsif ($rc == OK) {
        $msg->[0] ||= 'Queued';
        $self->respond(250, @$msg);
    }
    elsif ($rc == DENY) {
        $msg->[0] ||= 'Message denied';
        $self->respond(552, @$msg);
    }
    elsif ($rc == DENYSOFT) {
        $msg->[0] ||= 'Message denied temporarily';
        $self->respond(452, @$msg);
    }
    else {
        $msg->[0] ||= 'Queuing declined or disabled; try again later';
        $self->respond(451, @$msg);
    }

    # And finally run any queue_post hooks
    $self->run_hooks("queue_post");
}

sub queue_post_respond {
    my ($self, $rc, $msg, $args) = @_;
    $self->log(LOGERROR, @$msg) unless ($rc == OK or $rc == 0);
}

1;