<NME> SMTP.pm
<BEF> package Qpsmtpd::SMTP;
use strict;

use parent 'Qpsmtpd';

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

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

my %auth_mechanisms = ();
my %loaded = (
    auth_mechanisms => 0,
);

# 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);
  my (%commands); @commands{@commands} = ('') x @commands;
  # this list of valid commands should probably be a method or a set of methods
  $self->{_commands} = \%commands;

  $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;
    if ($self->{_transaction}) {
        $self->run_hooks('reset_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->helo_no_host('helo') if !$hello_host;
    return $self->helo_repeat_host() if $self->connection->hello;

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

sub helo_no_host {
    my ($self, $cmd) = @_;
    return $self->respond(501, "$cmd requires domain/address - see RFC-2821 4.1.1.1");
}

sub helo_repeat_host {
    my ($self) = @_;
    return $self->respond(503, 'but you already said HELO ...');
}

sub helo_hi_msg {
    my $self = shift;
    return $self->config('me') . ' Hi '
        . $self->connection->remote_info . ' ['
        . $self->connection->remote_ip
        . ']';
}

sub is_deny_response {
    my ($self, $rc, $msg) = @_;
    $self->respond(550, @$msg) if $rc == DENY     || $rc == DENY_DISCONNECT;
    $self->respond(450, @$msg) if $rc == DENYSOFT || $rc == DENYSOFT_DISCONNECT;
    if ($rc == DENY || $rc == DENYSOFT) {
        return 1;
    }
    if ($rc == DENY_DISCONNECT || $rc == DENYSOFT_DISCONNECT) {
        $self->disconnect;
        return 1;
    }
    return;
}

sub helo_respond {
    my ($self, $rc, $msg, $args) = @_;
    return if $rc == DONE;   # do nothing

    $self->is_deny_response($rc, $msg) and return;

    my $conn = $self->connection;
    $conn->hello('helo');
    $conn->hello_host($args->[0]);  # store helo hostname
    $self->transaction;

    $self->respond( 250, $self->helo_hi_msg . '; 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->helo_no_host('ehlo') if !$hello_host;
    return $self->helo_repeat_host() if $self->connection->hello;

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

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

    return if $rc == DONE;
    $self->is_deny_response($rc, $msg) and return;

    my $conn = $self->connection;
    $conn->hello('ehlo');
    $conn->hello_host($args->[0]);  # store helo hostname
    $self->transaction;

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

    # Check for possible AUTH mechanisms
    if (!$loaded{auth_mechanisms}) {
        $self->auth_load_mechanisms();
    }

    my $offer_auth = 1;
    if ($self->transaction->notes('tls_enabled') && ($self->config('tls_before_auth'))[0]) {
        $offer_auth = 0;
    }

    if (%auth_mechanisms && $offer_auth) {
        push @capabilities, 'AUTH ' . join(' ', keys(%auth_mechanisms));
        $self->{_commands}{auth} = '';
    }

    $self->respond( 250, $self->helo_hi_msg,
                    'PIPELINING',
                    '8BITMIME',
                    $self->ehlo_size(),
                    @capabilities,
                    );
}

sub ehlo_size {
    my $self = shift;
    return () if ! $self->config('databytes');
    return 'SIZE ' . ($self->config('databytes'))[0];
};

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

sub auth_load_mechanisms {
    my $self = shift;
    foreach my $hook (keys %{$self->hooks}) {
        next if $hook !~ m/^auth-?(.+)?$/;
        if (defined $1) {
            $auth_mechanisms{uc($1)} = 1;
            next;
        }
        # at least one polymorphous auth provider
        %auth_mechanisms = map { $_, 1 } qw(PLAIN CRAM-MD5 LOGIN);
        last;
    }
    $loaded{auth_mechanisms} = 1;
}

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")
      if $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'));

    if (exists $auth_mechanisms{uc($mechanism)}) {
        return $self->{_auth} = Qpsmtpd::Auth::SASL($self, $mechanism, @stuff);
    }

    # no plugin implementing this auth mechanism
    $self->respond(504, "Unimplemented authentication 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")
      if $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->run_hooks('reset_transaction') if $self->{_transaction};
    return;
}

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

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

    return 1 if $rc == DONE;   # do nothing

    if ($rc == DENY || $rc == DENY_DISCONNECT) {
        $msg->[0] ||= "Message denied";
        $self->respond(554, @$msg);
    }
    if ($rc == DENYSOFT || $rc == DENYSOFT_DISCONNECT) {
        $msg->[0] ||= "Message denied temporarily";
        $self->respond(451, @$msg);
    }

    if ($rc == DENY || $rc == DENYSOFT) {
        $self->reset_transaction;
        return 1;
    }

    if ($rc == DENY_DISCONNECT || $rc == DENYSOFT_DISCONNECT) {
        $self->disconnect;
        return 1;
    }

    if (!$self->transaction->sender) {
        $self->respond(503, 'MAIL first');
        return 1;
    };
    if (! $self->transaction->recipients) {
        $self->respond(503, 'RCPT first');
        return 1;
    };
    $self->respond(354, 'go ahead');

    my $buffer = '';
    my $size   = 0;
    my $i      = 0;
    my $max_size = ($self->config('databytes'))[0] || 0;
    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++;

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

        if ($_ eq ".\n" || $_ eq ".\r") {
            $self->respond(421, 'See http://smtpd.develooper.com/barelf.html');
            $self->disconnect;
            return 1;
        }

        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;

        # ... 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.github.io/qpsmtpd/");

                $buffer = '';

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

                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 dead.
    if (!$complete) {
        $self->respond(451, 'Incomplete DATA');
        $self->reset_transaction;
        return 1;
    }

    if ($max_size && $size > $max_size) {
        $self->log(LOGALERT,
                   "Message too big: size: $size (max size: $max_size)");
        $self->respond(552, 'Message too big!');
        $self->reset_transaction;
        return 1;
    }

    $self->run_hooks('data_post_headers');
    $self->clean_authentication_results();
    $self->received_line();
    $self->authentication_results();
    $self->run_hooks('data_post');
}

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

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

    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;

    # 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: was A-R header added by a Trusted MTA? Remove otherwise. Maybe.
    # See Also: RFC 5451, Removing The Header Field:
    #   http://tools.ietf.org/html/rfc5451#section-5
    #   http://tools.ietf.org/html/draft-kucherawy-original-authres-00.html

    # Do not rename A-R header if DKIM signed
    my $dkim_sig = $self->transaction->header->get('DKIM-Signature');
    if ($dkim_sig) {
        my %fields = map { split /=/, $_, 2 } split /;\s+/, $dkim_sig;
        # print Data::Dumper::Dumper(\%fields);
        if ($fields{h} && $fields{h} =~ /Authentication-Results/i) {
            return;
        }
    }

    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 == 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 $smtp $sslheader; "
          . (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;

<MSG> Fix for hooks not running with previous patch, caused by qpsmtpd objects not
asking each plugin to register. There is slightly more overhead this way,
but it feels more correct, and we can fix the overhead later in a more clean
way.


git-svn-id: 5bd49132847c26fb0bf09d36ee8823e0080b2740@300 958fd67b-6ff1-0310-b445-bb7760255be9

<DFF> @@ -33,6 +33,8 @@ sub new {
   my (%commands); @commands{@commands} = ('') x @commands;
   # this list of valid commands should probably be a method or a set of methods
   $self->{_commands} = \%commands;
+  
+  $self->load_plugins;
 
   $self;
 }

{'commit': 'diff --git a/lib/Qpsmtpd.pm b/lib/Qpsmtpd.pm\nindex bcd9e2e..ed826f6 100644\n--- a/lib/Qpsmtpd.pm\n+++ b/lib/Qpsmtpd.pm\n@@ -111,6 +111,40 @@ sub _config_from_file {\n   return wantarray ? @config : $config[0];\n }\n \n+sub _compile {\n+    my ($plugin, $package, $file) = @_;\n+    \n+    my $sub;\n+    open F, $file or die "could not open $file: $!";\n+    { \n+      local $/ = undef;\n+      $sub = <F>;\n+    }\n+    close F;\n+\n+    my $line = "\\n#line 1 $file\\n";\n+\n+    my $eval = join(\n+\t\t    "\\n",\n+\t\t    "package $package;",\n+\t\t    \'use Qpsmtpd::Constants;\',\n+\t\t    "require Qpsmtpd::Plugin;",\n+\t\t    \'use vars qw(@ISA);\',\n+\t\t    \'@ISA = qw(Qpsmtpd::Plugin);\',\n+\t\t    "sub plugin_name { qq[$plugin] }",\n+\t\t    $line,\n+\t\t    $sub,\n+\t\t    "\\n", # last line comment without newline?\n+\t\t   );\n+\n+    #warn "eval: $eval";\n+\n+    $eval =~ m/(.*)/s;\n+    $eval = $1;\n+\n+    eval $eval;\n+    die "eval $@" if $@;\n+}\n \n sub load_plugins {\n   my $self = shift;\n@@ -174,39 +208,9 @@ sub _load_plugins {\n     my $package = "Qpsmtpd::Plugin::$plugin_name";\n \n     # don\'t reload plugins if they are already loaded\n-    next if defined &{"${package}::register"};\n+    _compile($plugin_name, $package, "$dir/$plugin") unless\n+        defined &{"${package}::register"};\n     \n-    my $sub;\n-    open F, "$dir/$plugin" or die "could not open $dir/$plugin: $!";\n-    { \n-      local $/ = undef;\n-      $sub = <F>;\n-    }\n-    close F;\n-\n-    my $line = "\\n#line 1 $dir/$plugin\\n";\n-\n-    my $eval = join(\n-\t\t    "\\n",\n-\t\t    "package $package;",\n-\t\t    \'use Qpsmtpd::Constants;\',\n-\t\t    "require Qpsmtpd::Plugin;",\n-\t\t    \'use vars qw(@ISA);\',\n-\t\t    \'@ISA = qw(Qpsmtpd::Plugin);\',\n-\t\t    "sub plugin_name { qq[$plugin_name] }",\n-\t\t    $line,\n-\t\t    $sub,\n-\t\t    "\\n", # last line comment without newline?\n-\t\t   );\n-\n-    #warn "eval: $eval";\n-\n-    $eval =~ m/(.*)/s;\n-    $eval = $1;\n-\n-    eval $eval;\n-    die "eval $@" if $@;\n-\n     my $plug = $package->new();\n     $plug->_register($self, @args);\n \ndiff --git a/lib/Qpsmtpd/SMTP.pm b/lib/Qpsmtpd/SMTP.pm\nindex 1f4f2d0..3e7bdbc 100644\n--- a/lib/Qpsmtpd/SMTP.pm\n+++ b/lib/Qpsmtpd/SMTP.pm\n@@ -33,6 +33,8 @@ sub new {\n   my (%commands); @commands{@commands} = (\'\') x @commands;\n   # this list of valid commands should probably be a method or a set of methods\n   $self->{_commands} = \\%commands;\n+  \n+  $self->load_plugins;\n \n   $self;\n }\n', 'commit_subject': 'Fix for hooks not running with previous patch, caused by qpsmtpd objects not asking each plugin to register. There is slightly more overhead this way, but it feels more correct, and we can fix the overhead later in a more clean way.', 'repo_name': 'smtpd/qpsmtpd', 'committer': {'name': 'Matt Sergeant', 'email': '1fa2ef4755a9226cb9a0a4840bd89b158ac71391@sergeant.org', 'time_sec': '1094401688', 'tz_offset': '0', 'date': {'seconds': '1094401688'}}, 'commit_tree': '5705cb09fae686db78436c6ef3c91ba4fc432698', 'commit_message': 'Fix for hooks not running with previous patch, caused by qpsmtpd objects not\nasking each plugin to register. There is slightly more overhead this way,\nbut it feels more correct, and we can fix the overhead later in a more clean\nway.\n\n\ngit-svn-id: 5bd49132847c26fb0bf09d36ee8823e0080b2740@300 958fd67b-6ff1-0310-b445-bb7760255be9\n', 'language_name': 'Shell', 'license': 'mit', 'hunks': "@@ -33,6 +33,8 @@ sub new {\n   my (%commands); @commands{@commands} = ('') x @commands;\n   # this list of valid commands should probably be a method or a set of methods\n   $self->{_commands} = \\%commands;\n+  \n+  $self->load_plugins;\n \n   $self;\n }\n", 'addition_count': 2, 'deletion_count': 0, 'src_file': 'a/lib/Qpsmtpd/SMTP.pm', 'tgt_file': 'b/lib/Qpsmtpd/SMTP.pm', 'file_extension': '.pm', 'before_file': 'package Qpsmtpd::SMTP;\nuse strict;\n\nuse parent \'Qpsmtpd\';\n\n#use Data::Dumper;\nuse POSIX qw(strftime);\nuse Mail::Header;\n\nuse Qpsmtpd;\nuse Qpsmtpd::Connection;\nuse Qpsmtpd::Transaction;\nuse Qpsmtpd::Plugin;\nuse Qpsmtpd::Constants;\nuse Qpsmtpd::Auth;\nuse Qpsmtpd::Command;\n\nmy %auth_mechanisms = ();\nmy %loaded = (\n    auth_mechanisms => 0,\n);\n\n# this is only good for forkserver\n# can\'t set these here, cause forkserver resets them\n#$SIG{ALRM} = sub { respond(421, "timeout; I can\'t wait that long..."); exit };\n#$SIG{ALRM} = sub { warn "Connection Timed Out\\n"; exit; };\n\nsub new {\n    my ($proto, %args) = @_;\n    my $class = ref($proto) || $proto;\n\n    my $self = bless({args => \\%args}, $class);\n  my (%commands); @commands{@commands} = (\'\') x @commands;\n  # this list of valid commands should probably be a method or a set of methods\n  $self->{_commands} = \\%commands;\n\n  $self;\n}\n\nsub command_counter {\n    my $self = shift;\n    $self->{_counter} || 0;\n}\n\nsub dispatch {\n    my $self = shift;\n    my ($cmd) = shift;\n    if (!$cmd) {\n        $self->run_hooks("unrecognized_command", \'\', @_);\n        return 1;\n    }\n    $cmd = lc $cmd;\n\n    $self->{_counter}++;\n\n    if ($cmd !~ /^(\\w{1,12})$/ or !exists $self->{_commands}->{$1}) {\n        $self->run_hooks("unrecognized_command", $cmd, @_);\n        return 1;\n    }\n    $cmd = $1;\n\n    my ($result) = eval { $self->$cmd(@_) };\n    $self->log(LOGERROR, "XX: $@") if $@;\n    return $result if defined $result;\n    return $self->fault("command \'$cmd\' failed unexpectedly");\n}\n\nsub unrecognized_command_respond {\n    my ($self, $rc, $msg) = @_;\n    if ($rc == DENY_DISCONNECT) {\n        $self->respond(521, @$msg);\n        $self->disconnect;\n    }\n    elsif ($rc == DENY) {\n        $self->respond(500, @$msg);\n    }\n    elsif ($rc != DONE) {\n        $self->respond(500, "Unrecognized command");\n    }\n}\n\nsub fault {\n    my $self = shift;\n    my ($msg) = shift || "program fault - command not performed";\n    my ($name) = split /\\s+/, $0, 2;\n    print STDERR $name, "[$$]: $msg\\n";\n    print STDERR $name, "[$$]: Last system error: $!"\n      . " (Likely irelevant--debug the crashed plugin to ensure it handles \\$! properly)";\n    return $self->respond(451, "Internal error - try again later - " . $msg);\n}\n\nsub start_conversation {\n    my $self = shift;\n\n    # this should maybe be called something else than "connect", see\n    # lib/Qpsmtpd/TcpServer.pm for more confusion.\n    $self->run_hooks("connect");\n    return DONE;\n}\n\nsub connect_respond {\n    my ($self, $rc, $msg) = @_;\n    if ($rc == DENY || $rc == DENY_DISCONNECT) {\n        $msg->[0] ||= \'Connection from you denied, bye bye.\';\n        $self->respond(550, @$msg);\n        $self->disconnect;\n    }\n    elsif ($rc == DENYSOFT || $rc == DENYSOFT_DISCONNECT) {\n        $msg->[0] ||= \'Connection from you temporarily denied, bye bye.\';\n        $self->respond(450, @$msg);\n        $self->disconnect;\n    }\n    elsif ($rc != DONE) {\n        my $greets = $self->config(\'smtpgreeting\');\n        if ($greets) {\n            $greets .= " ESMTP" unless $greets =~ /(^|\\W)ESMTP(\\W|$)/;\n        }\n        else {\n            $greets =\n                $self->config(\'me\')\n              . " ESMTP qpsmtpd "\n              . $self->version\n              . " ready; send us your mail, but not your spam.";\n        }\n\n        $self->respond(220, $greets);\n    }\n}\n\nsub transaction {\n    my $self = shift;\n    return $self->{_transaction} || $self->reset_transaction;\n}\n\nsub reset_transaction {\n    my $self = shift;\n    if ($self->{_transaction}) {\n        $self->run_hooks(\'reset_transaction\');\n    };\n    return $self->{_transaction} = Qpsmtpd::Transaction->new();\n}\n\nsub connection {\n    my $self = shift;\n    if (@_) { $self->{_connection} = shift; }\n    return $self->{_connection} if $self->{_connection};\n    return $self->{_connection} = Qpsmtpd::Connection->new();\n}\n\nsub helo {\n    my ($self, $line) = @_;\n    my ($rc,   @msg)  = $self->run_hooks(\'helo_parse\');\n    my ($ok, $hello_host, @stuff) =\n      Qpsmtpd::Command->parse(\'helo\', $line, $msg[0]);\n\n    return $self->helo_no_host(\'helo\') if !$hello_host;\n    return $self->helo_repeat_host() if $self->connection->hello;\n\n    $self->run_hooks(\'helo\', $hello_host, @stuff);\n}\n\nsub helo_no_host {\n    my ($self, $cmd) = @_;\n    return $self->respond(501, "$cmd requires domain/address - see RFC-2821 4.1.1.1");\n}\n\nsub helo_repeat_host {\n    my ($self) = @_;\n    return $self->respond(503, \'but you already said HELO ...\');\n}\n\nsub helo_hi_msg {\n    my $self = shift;\n    return $self->config(\'me\') . \' Hi \'\n        . $self->connection->remote_info . \' [\'\n        . $self->connection->remote_ip\n        . \']\';\n}\n\nsub is_deny_response {\n    my ($self, $rc, $msg) = @_;\n    $self->respond(550, @$msg) if $rc == DENY     || $rc == DENY_DISCONNECT;\n    $self->respond(450, @$msg) if $rc == DENYSOFT || $rc == DENYSOFT_DISCONNECT;\n    if ($rc == DENY || $rc == DENYSOFT) {\n        return 1;\n    }\n    if ($rc == DENY_DISCONNECT || $rc == DENYSOFT_DISCONNECT) {\n        $self->disconnect;\n        return 1;\n    }\n    return;\n}\n\nsub helo_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    return if $rc == DONE;   # do nothing\n\n    $self->is_deny_response($rc, $msg) and return;\n\n    my $conn = $self->connection;\n    $conn->hello(\'helo\');\n    $conn->hello_host($args->[0]);  # store helo hostname\n    $self->transaction;\n\n    $self->respond( 250, $self->helo_hi_msg . \'; I am so happy to meet you.\');\n}\n\nsub ehlo {\n    my ($self, $line) = @_;\n    my ($rc,   @msg ) = $self->run_hooks(\'ehlo_parse\');\n    my ($ok, $hello_host, @stuff) =\n      Qpsmtpd::Command->parse(\'ehlo\', $line, $msg[0]);\n\n    return $self->helo_no_host(\'ehlo\') if !$hello_host;\n    return $self->helo_repeat_host() if $self->connection->hello;\n\n    $self->run_hooks(\'ehlo\', $hello_host, @stuff);\n}\n\nsub ehlo_respond {\n    my ($self, $rc, $msg, $args) = @_;\n\n    return if $rc == DONE;\n    $self->is_deny_response($rc, $msg) and return;\n\n    my $conn = $self->connection;\n    $conn->hello(\'ehlo\');\n    $conn->hello_host($args->[0]);  # store helo hostname\n    $self->transaction;\n\n    my @capabilities = ();\n    if ($self->transaction->notes(\'capabilities\')) {\n        @capabilities = @{$self->transaction->notes(\'capabilities\')};\n    }\n\n    # Check for possible AUTH mechanisms\n    if (!$loaded{auth_mechanisms}) {\n        $self->auth_load_mechanisms();\n    }\n\n    my $offer_auth = 1;\n    if ($self->transaction->notes(\'tls_enabled\') && ($self->config(\'tls_before_auth\'))[0]) {\n        $offer_auth = 0;\n    }\n\n    if (%auth_mechanisms && $offer_auth) {\n        push @capabilities, \'AUTH \' . join(\' \', keys(%auth_mechanisms));\n        $self->{_commands}{auth} = \'\';\n    }\n\n    $self->respond( 250, $self->helo_hi_msg,\n                    \'PIPELINING\',\n                    \'8BITMIME\',\n                    $self->ehlo_size(),\n                    @capabilities,\n                    );\n}\n\nsub ehlo_size {\n    my $self = shift;\n    return () if ! $self->config(\'databytes\');\n    return \'SIZE \' . ($self->config(\'databytes\'))[0];\n};\n\nsub auth {\n    my ($self, $line) = @_;\n    $self->run_hooks(\'auth_parse\', $line);\n}\n\nsub auth_load_mechanisms {\n    my $self = shift;\n    foreach my $hook (keys %{$self->hooks}) {\n        next if $hook !~ m/^auth-?(.+)?$/;\n        if (defined $1) {\n            $auth_mechanisms{uc($1)} = 1;\n            next;\n        }\n        # at least one polymorphous auth provider\n        %auth_mechanisms = map { $_, 1 } qw(PLAIN CRAM-MD5 LOGIN);\n        last;\n    }\n    $loaded{auth_mechanisms} = 1;\n}\n\nsub auth_parse_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    my ($line) = @$args;\n\n    my ($ok, $mechanism, @stuff) =\n      Qpsmtpd::Command->parse(\'auth\', $line, $msg->[0]);\n\n    return $self->respond(501, $mechanism || "Syntax error in command")\n      if $ok != OK;\n\n    $mechanism = lc $mechanism;\n\n    #they AUTH\'d once already\n    return $self->respond(503, "but you already said AUTH ...")\n      if (defined $self->{_auth} && $self->{_auth} == OK);\n\n    return $self->respond(503, "AUTH not defined for HELO")\n      if $self->connection->hello eq \'helo\';\n\n    return $self->respond(503, "SSL/TLS required before AUTH")\n      if (($self->config(\'tls_before_auth\'))[0]\n          && $self->transaction->notes(\'tls_enabled\'));\n\n    if (exists $auth_mechanisms{uc($mechanism)}) {\n        return $self->{_auth} = Qpsmtpd::Auth::SASL($self, $mechanism, @stuff);\n    }\n\n    # no plugin implementing this auth mechanism\n    $self->respond(504, "Unimplemented authentication mechanism: $mechanism");\n    return DENY;\n}\n\nsub mail {\n    my ($self, $line) = @_;\n\n    # -> from RFC2821\n    # The MAIL command (or the obsolete SEND, SOML, or SAML commands)\n    # begins a mail transaction.  Once started, a mail transaction\n    # consists of a transaction beginning command, one or more RCPT\n    # commands, and a DATA command, in that order.  A mail transaction\n    # may be aborted by the RSET (or a new EHLO) command.  There may be\n    # zero or more transactions in a session.  MAIL (or SEND, SOML, or\n    # SAML) MUST NOT be sent if a mail transaction is already open,\n    # i.e., it should be sent only if no mail transaction had been\n    # started in the session, or it the previous one successfully\n    # concluded with a successful DATA command, or if the previous one\n    # was aborted with a RSET.\n\n    # sendmail (8.11) rejects a second MAIL command.\n\n    # qmail-smtpd (1.03) accepts it and just starts a new transaction.\n    # Since we are a qmail-smtpd thing we will do the same.\n\n    $self->reset_transaction;\n\n    if (!$self->connection->hello) {\n        return $self->respond(503, "please say hello first ...");\n    }\n\n    $self->log(LOGDEBUG, "full from_parameter: $line");\n    $self->connection->notes(\'envelope_from\', $line);\n    $self->run_hooks("mail_parse", $line);\n}\n\nsub mail_parse_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    my ($line) = @$args;\n    my ($ok, $from, @params) =\n      Qpsmtpd::Command->parse(\'mail\', $line, $msg->[0]);\n    return $self->respond(501, $from || "Syntax error in command")\n      unless ($ok == OK);\n    my %param;\n    foreach (@params) {\n        my ($k, $v) = split /=/, $_, 2;\n        $param{lc $k} = $v;\n    }\n\n    # to support addresses without <> we now require a plugin\n    # hooking "mail_pre" to\n    #   return OK, "<$from>";\n    # (...or anything else parseable by Qpsmtpd::Address ;-))\n    # see also comment in sub rcpt()\n    $self->run_hooks("mail_pre", $from, \\%param);\n}\n\nsub mail_pre_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    my ($from, $param) = @$args;\n    if ($rc == OK) {\n        $from = shift @$msg;\n    }\n\n    $self->log(LOGDEBUG, "from email address : [$from]");\n    return $self->respond(501, "could not parse your mail from command")\n      unless $from =~ /^<.*>$/;\n\n    if ($from eq "<>" or $from =~ m/\\[undefined\\]/ or $from eq "<#@[]>") {\n        $from = $self->address("<>");\n    }\n    else {\n        $from = $self->address($from);\n    }\n    return $self->respond(501, "could not parse your mail from command")\n      unless $from;\n\n    $self->run_hooks("mail", $from, %$param);\n}\n\nsub mail_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    my ($from, $param) = @$args;\n    if ($rc == DONE) {\n        return 1;\n    }\n    elsif ($rc == DENY) {\n        $msg->[0] ||= $from->format . \', denied\';\n        $self->log(LOGINFO, "deny mail from " . $from->format . " (@$msg)");\n        $self->respond(550, @$msg);\n    }\n    elsif ($rc == DENYSOFT) {\n        $msg->[0] ||= $from->format . \', temporarily denied\';\n        $self->log(LOGINFO, "denysoft mail from " . $from->format . " (@$msg)");\n        $self->respond(450, @$msg);\n    }\n    elsif ($rc == DENY_DISCONNECT) {\n        $msg->[0] ||= $from->format . \', denied\';\n        $self->log(LOGINFO, "deny mail from " . $from->format . " (@$msg)");\n        $self->respond(550, @$msg);\n        $self->disconnect;\n    }\n    elsif ($rc == DENYSOFT_DISCONNECT) {\n        $msg->[0] ||= $from->format . \', temporarily denied\';\n        $self->log(LOGINFO, "denysoft mail from " . $from->format . " (@$msg)");\n        $self->respond(421, @$msg);\n        $self->disconnect;\n    }\n    else {    # includes OK\n        $self->log(LOGDEBUG, "getting mail from " . $from->format);\n        $self->respond(\n                       250,\n                       $from->format\n                         . ", sender OK - how exciting to get mail from you!"\n                      );\n        $self->transaction->sender($from);\n    }\n}\n\nsub rcpt {\n    my ($self, $line) = @_;\n    $self->connection->notes(\'envelope_rcpt\', $line);\n    $self->run_hooks("rcpt_parse", $line);\n}\n\nsub rcpt_parse_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    my ($line) = @$args;\n    my ($ok, $rcpt, @param) = Qpsmtpd::Command->parse("rcpt", $line, $msg->[0]);\n    return $self->respond(501, $rcpt || "Syntax error in command")\n      unless ($ok == OK);\n    return $self->respond(503, "Use MAIL before RCPT")\n      unless $self->transaction->sender;\n\n    my %param;\n    foreach (@param) {\n        my ($k, $v) = split /=/, $_, 2;\n        $param{lc $k} = $v;\n    }\n\n    # to support addresses without <> we now require a plugin\n    # hooking "rcpt_pre" to\n    #   return OK, "<$rcpt>";\n    # (... or anything else parseable by Qpsmtpd::Address ;-))\n    # this means, a plugin can decide to (pre-)accept\n    # addresses like <user@example.com.> or <user@example.com >\n    # by removing the trailing dot or space from this example.\n    $self->run_hooks("rcpt_pre", $rcpt, \\%param);\n}\n\nsub rcpt_pre_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    my ($rcpt, $param) = @$args;\n    if ($rc == OK) {\n        $rcpt = shift @$msg;\n    }\n    $self->log(LOGDEBUG, "to email address : [$rcpt]");\n    return $self->respond(501, "could not parse recipient")\n      if $rcpt !~ /^<.*>$/;\n\n    $rcpt = $self->address($rcpt);\n\n    return $self->respond(501, "could not parse recipient")\n      if (!$rcpt or ($rcpt->format eq \'<>\'));\n\n    $self->run_hooks("rcpt", $rcpt, %$param);\n}\n\nsub rcpt_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    my ($rcpt, $param) = @$args;\n    if ($rc == DONE) {\n        return 1;\n    }\n    elsif ($rc == DENY) {\n        $msg->[0] ||= \'relaying denied\';\n        $self->respond(550, @$msg);\n    }\n    elsif ($rc == DENYSOFT) {\n        $msg->[0] ||= \'relaying denied\';\n        return $self->respond(450, @$msg);\n    }\n    elsif ($rc == DENY_DISCONNECT) {\n        $msg->[0] ||= \'delivery denied\';\n        $self->log(LOGDEBUG, "delivery denied (@$msg)");\n        $self->respond(550, @$msg);\n        $self->disconnect;\n    }\n    elsif ($rc == DENYSOFT_DISCONNECT) {\n        $msg->[0] ||= \'relaying denied\';\n        $self->log(LOGDEBUG, "delivery denied (@$msg)");\n        $self->respond(421, @$msg);\n        $self->disconnect;\n    }\n    elsif ($rc == OK) {\n        $self->respond(250, $rcpt->format . ", recipient ok");\n        return $self->transaction->add_recipient($rcpt);\n    }\n    else {\n        return $self->respond(450, "No plugin decided if relaying is allowed");\n    }\n    return 0;\n}\n\nsub help {\n    my ($self, @args) = @_;\n    $self->run_hooks("help", @args);\n}\n\nsub help_respond {\n    my ($self, $rc, $msg, $args) = @_;\n\n    return 1\n      if $rc == DONE;\n\n    if ($rc == DENY) {\n        $msg->[0] ||= "Syntax error, command not recognized";\n        $self->respond(500, @$msg);\n    }\n    else {\n        unless ($msg->[0]) {\n            @$msg = (\n                     "This is qpsmtpd "\n                       . ($self->config(\'smtpgreeting\') ? \'\' : $self->version),\n                     "See http://smtpd.develooper.com/",\n\'To report bugs or send comments, mail to <ask@develooper.com>.\'\n                    );\n        }\n        $self->respond(214, @$msg);\n    }\n    return 1;\n}\n\nsub noop {\n    my $self = shift;\n    $self->run_hooks("noop");\n}\n\nsub noop_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    return 1 if $rc == DONE;\n\n    if ($rc == DENY || $rc == DENY_DISCONNECT) {\n        $msg->[0] ||= "Stop wasting my time.";  # FIXME: better default message?\n        $self->respond(500, @$msg);\n        $self->disconnect if $rc == DENY_DISCONNECT;\n        return 1;\n    }\n\n    $self->respond(250, "OK");\n    return 1;\n}\n\nsub vrfy {\n    my $self = shift;\n\n    # Note, this doesn\'t support the multiple ambiguous results\n    # documented in RFC2821#3.5.1\n    # I also don\'t think it provides all the proper result codes.\n\n    $self->run_hooks("vrfy");\n}\n\nsub vrfy_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    if ($rc == DONE) {\n        return 1;\n    }\n    elsif ($rc == DENY) {\n        $msg->[0] ||= "Access Denied";\n        $self->respond(554, @$msg);\n        $self->reset_transaction;\n        return 1;\n    }\n    elsif ($rc == OK) {\n        $msg->[0] ||= "User OK";\n        $self->respond(250, @$msg);\n        return 1;\n    }\n    else {    # $rc == DECLINED or anything else\n        $self->respond(252,\n                  "Just try sending a mail and we\'ll see how it turns out ...");\n        return 1;\n    }\n}\n\nsub rset {\n    my $self = shift;\n    $self->reset_transaction;\n    $self->respond(250, "OK");\n}\n\nsub quit {\n    my $self = shift;\n    $self->run_hooks("quit");\n}\n\nsub quit_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    if ($rc != DONE) {\n        $msg->[0] ||=\n          $self->config(\'me\') . " closing connection. Have a wonderful day.";\n        $self->respond(221, @$msg);\n    }\n    $self->disconnect();\n}\n\nsub disconnect {\n    my $self = shift;\n    $self->run_hooks("disconnect");\n    $self->connection->notes(disconnected => 1);\n    $self->run_hooks(\'reset_transaction\') if $self->{_transaction};\n    return;\n}\n\nsub data {\n    my $self = shift;\n    $self->run_hooks("data");\n}\n\nsub data_respond {\n    my ($self, $rc, $msg, $args) = @_;\n\n    return 1 if $rc == DONE;   # do nothing\n\n    if ($rc == DENY || $rc == DENY_DISCONNECT) {\n        $msg->[0] ||= "Message denied";\n        $self->respond(554, @$msg);\n    }\n    if ($rc == DENYSOFT || $rc == DENYSOFT_DISCONNECT) {\n        $msg->[0] ||= "Message denied temporarily";\n        $self->respond(451, @$msg);\n    }\n\n    if ($rc == DENY || $rc == DENYSOFT) {\n        $self->reset_transaction;\n        return 1;\n    }\n\n    if ($rc == DENY_DISCONNECT || $rc == DENYSOFT_DISCONNECT) {\n        $self->disconnect;\n        return 1;\n    }\n\n    if (!$self->transaction->sender) {\n        $self->respond(503, \'MAIL first\');\n        return 1;\n    };\n    if (! $self->transaction->recipients) {\n        $self->respond(503, \'RCPT first\');\n        return 1;\n    };\n    $self->respond(354, \'go ahead\');\n\n    my $buffer = \'\';\n    my $size   = 0;\n    my $i      = 0;\n    my $max_size = ($self->config(\'databytes\'))[0] || 0;\n    my $blocked = \'\';\n    my %matches;\n    my $in_header = 1;\n    my $complete  = 0;\n\n    $self->log(LOGDEBUG, "max_size: $max_size / size: $size");\n\n    my $header = Mail::Header->new(Modify => 0, MailFrom => \'COERCE\');\n\n    my $timeout = $self->config(\'timeout\');\n    while (defined($_ = $self->getline($timeout))) {\n        if ($_ eq ".\\r\\n") {\n            $complete++;\n            $_ = \'\';\n        }\n        $i++;\n\n        # Reject messages that have either bare LF or CR. rjkaes noticed a\n        # lot of spam that is malformed in the header.\n\n        if ($_ eq ".\\n" || $_ eq ".\\r") {\n            $self->respond(421, \'See http://smtpd.develooper.com/barelf.html\');\n            $self->disconnect;\n            return 1;\n        }\n\n        unless (($max_size and $size > $max_size)) {\n            s/\\r\\n$/\\n/;\n            s/^\\.\\./\\./;\n            if ($in_header && (m/^$/ || $complete > 0)) {\n                $in_header = 0;\n                my @headers = split /^/m, $buffer;\n\n        # ... don\'t reformat any of the received lines.\n        #\n        # 3.8.2 Received Lines in Gatewaying\n        #   When forwarding a message into or out of the Internet environment, a\n        #   gateway MUST prepend a Received: line, but it MUST NOT alter in any\n        #   way a Received: line that is already in the header.\n\n                $header->extract(\\@headers);\n\n#$header->add("X-SMTPD", "qpsmtpd/".$self->version.", http://smtpd.github.io/qpsmtpd/");\n\n                $buffer = \'\';\n\n                $self->transaction->header($header);\n\n                my ($rc, $msg) = $self->run_hooks(\'data_headers_end\');\n                if ($rc == DENY_DISCONNECT) {\n                    $self->respond(554, $msg || \'Message denied\');\n                    $self->disconnect;\n                    return 1;\n                }\n                elsif ($rc == DENYSOFT_DISCONNECT) {\n                    $self->respond(421, $msg || \'Message denied temporarily\');\n                    $self->disconnect;\n                    return 1;\n                }\n\n                # Save the start of just the body itself\n                $self->transaction->set_body_start();\n            }\n\n            # grab a copy of all of the header lines\n            if ($in_header) {\n                $buffer .= $_;\n            }\n\n            # copy all lines into the spool file, including the headers\n            # we will create a new header later before sending onwards\n            $self->transaction->body_write($_) if !$complete;\n            $size += length $_;\n        }\n        last if $complete > 0;\n\n        #$self->log(LOGDEBUG, "size is at $size\\n") unless ($i % 300);\n    }\n\n    $self->log(LOGDEBUG, "max_size: $max_size / size: $size");\n\n    # if we get here without seeing a terminator, the connection is dead.\n    if (!$complete) {\n        $self->respond(451, \'Incomplete DATA\');\n        $self->reset_transaction;\n        return 1;\n    }\n\n    if ($max_size && $size > $max_size) {\n        $self->log(LOGALERT,\n                   "Message too big: size: $size (max size: $max_size)");\n        $self->respond(552, \'Message too big!\');\n        $self->reset_transaction;\n        return 1;\n    }\n\n    $self->run_hooks(\'data_post_headers\');\n    $self->clean_authentication_results();\n    $self->received_line();\n    $self->authentication_results();\n    $self->run_hooks(\'data_post\');\n}\n\nsub authentication_results {\n    my ($self) = @_;\n\n    my @auth_list = $self->config(\'me\');\n\n    if (!defined $self->{_auth}) {\n        push @auth_list, \'auth=none\';\n    }\n    else {\n        my $mechanism = "(" . $self->{_auth_mechanism} . ")";\n        my $user      = "smtp.auth=" . $self->{_auth_user};\n        if ($self->{_auth} == OK) {\n            push @auth_list, "auth=pass $mechanism $user";\n        }\n        else {\n            push @auth_list, "auth=fail $mechanism $user";\n        }\n    }\n\n    # RFC 5451: used in AUTH, DKIM, DOMAINKEYS, SENDERID, SPF\n    if ($self->connection->notes(\'authentication_results\')) {\n        push @auth_list, $self->connection->notes(\'authentication_results\');\n    }\n\n    $self->log(LOGDEBUG, "adding auth results header");\n    $self->transaction->header->add(\'Authentication-Results\',\n                                    join(\'; \', @auth_list), 0);\n}\n\nsub clean_authentication_results {\n    my $self = shift;\n\n    # On messages received from the internet, move Authentication-Results headers\n    # to Original-AR, so our downstream can trust the A-R header we insert.\n\n    # TODO: was A-R header added by a Trusted MTA? Remove otherwise. Maybe.\n    # See Also: RFC 5451, Removing The Header Field:\n    #   http://tools.ietf.org/html/rfc5451#section-5\n    #   http://tools.ietf.org/html/draft-kucherawy-original-authres-00.html\n\n    # Do not rename A-R header if DKIM signed\n    my $dkim_sig = $self->transaction->header->get(\'DKIM-Signature\');\n    if ($dkim_sig) {\n        my %fields = map { split /=/, $_, 2 } split /;\\s+/, $dkim_sig;\n        # print Data::Dumper::Dumper(\\%fields);\n        if ($fields{h} && $fields{h} =~ /Authentication-Results/i) {\n            return;\n        }\n    }\n\n    my @ar_headers = $self->transaction->header->get(\'Authentication-Results\');\n    for (my $i = 0 ; $i < scalar @ar_headers ; $i++) {\n        $self->transaction->header->delete(\'Authentication-Results\', $i);\n        $self->transaction->header->add(\'Original-Authentication-Results\',\n                                        $ar_headers[$i]);\n    }\n\n    $self->log(LOGDEBUG,\n             "Authentication-Results moved to Original-Authentication-Results");\n}\n\nsub received_line {\n    my ($self) = @_;\n\n    my $smtp = $self->connection->hello eq "ehlo" ? "ESMTP" : "SMTP";\n    my $esmtp      = substr($smtp, 0, 1) eq "E";\n    my $authheader = \'\';\n    my $sslheader  = \'\';\n\n    if (defined $self->connection->notes(\'tls_enabled\')\n        and $self->connection->notes(\'tls_enabled\'))\n    {\n        $smtp .= "S" if $esmtp;    # RFC3848\n        $sslheader = "("\n          . $self->connection->notes(\'tls_socket\')->get_cipher()\n          . " encrypted)";\n    }\n    if (defined $self->{_auth} && $self->{_auth} == OK) {\n        my $mech = $self->{_auth_mechanism};\n        my $user = $self->{_auth_user};\n        $smtp .= "A" if $esmtp;    # RFC3848\n        $authheader = "(smtp-auth username $user, mechanism $mech)\\n";\n    }\n\n    my $header_str;\n    my ($rc, @received) =\n      $self->run_hooks("received_line", $smtp, $authheader, $sslheader);\n    if ($rc == OK) {\n        return join("\\n", @received);\n    }\n    else {    # assume $rc == DECLINED\n        $header_str =\n            "from "\n          . $self->connection->remote_info\n          . " (HELO "\n          . $self->connection->hello_host . ") ("\n          . $self->connection->remote_ip\n          . ")\\n by "\n          . $self->config(\'me\')\n          . " (qpsmtpd/"\n          . $self->version\n          . ") with $smtp $sslheader; "\n          . (strftime(\'%a, %d %b %Y %H:%M:%S %z\', localtime));\n    }\n    $self->transaction->header->add(\'Received\', $header_str, 0);\n}\n\nsub data_post_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    if ($rc == DONE) {\n        return 1;\n    }\n    elsif ($rc == DENY) {\n        $msg->[0] ||= "Message denied";\n        $self->respond(552, @$msg);\n\n        # DATA is always the end of a "transaction"\n        return $self->reset_transaction;\n    }\n    elsif ($rc == DENYSOFT) {\n        $msg->[0] ||= "Message denied temporarily";\n        $self->respond(452, @$msg);\n\n        # DATA is always the end of a "transaction"\n        return $self->reset_transaction;\n    }\n    elsif ($rc == DENY_DISCONNECT) {\n        $msg->[0] ||= "Message denied";\n        $self->respond(552, @$msg);\n        $self->disconnect;\n        return 1;\n    }\n    elsif ($rc == DENYSOFT_DISCONNECT) {\n        $msg->[0] ||= "Message denied temporarily";\n        $self->respond(452, @$msg);\n        $self->disconnect;\n        return 1;\n    }\n    else {\n        $self->queue($self->transaction);\n    }\n}\n\nsub getline {\n    my ($self, $timeout) = @_;\n\n    alarm $timeout;\n    my $line = <STDIN>;    # default implementation\n    alarm 0;\n    return $line;\n}\n\nsub queue {\n    my ($self, $transaction) = @_;\n\n    # First fire any queue_pre hooks\n    $self->run_hooks("queue_pre");\n}\n\nsub queue_pre_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    if ($rc == DONE) {\n        return 1;\n    }\n    elsif ($rc != OK and $rc != DECLINED and $rc != 0) {\n        return $self->log(LOGERROR, "pre plugin returned illegal value");\n        return 0;\n    }\n\n    # If we got this far, run the queue hooks\n    $self->run_hooks("queue");\n}\n\nsub queue_respond {\n    my ($self, $rc, $msg, $args) = @_;\n\n    # reset transaction if we queued the mail\n    $self->reset_transaction;\n\n    if ($rc == DONE) {\n        return 1;\n    }\n    elsif ($rc == OK) {\n        $msg->[0] ||= \'Queued\';\n        $self->respond(250, @$msg);\n    }\n    elsif ($rc == DENY) {\n        $msg->[0] ||= \'Message denied\';\n        $self->respond(552, @$msg);\n    }\n    elsif ($rc == DENYSOFT) {\n        $msg->[0] ||= \'Message denied temporarily\';\n        $self->respond(452, @$msg);\n    }\n    else {\n        $msg->[0] ||= \'Queuing declined or disabled; try again later\';\n        $self->respond(451, @$msg);\n    }\n\n    # And finally run any queue_post hooks\n    $self->run_hooks("queue_post");\n}\n\nsub queue_post_respond {\n    my ($self, $rc, $msg, $args) = @_;\n    $self->log(LOGERROR, @$msg) unless ($rc == OK or $rc == 0);\n}\n\n1;\n', 'message': 'Fix for hooks not running with previous patch, caused by qpsmtpd objects not\nasking each plugin to register. There is slightly more overhead this way,\nbut it feels more correct, and we can fix the overhead later in a more clean\nway.\n\n\ngit-svn-id: 5bd49132847c26fb0bf09d36ee8823e0080b2740@300 958fd67b-6ff1-0310-b445-bb7760255be9\n', 'author_name': 'Matt Sergeant', 'lang': 'pm'}