I'm now running the traps.SpamAssassin.org spamtrap using qpsmtpd. Here's the configuration and plugins I use to do this.

First off, I used SVN trunk as of Apr 11 2007, so that I could use the super-efficient Danga::Socket asynchronous sockets support, which scales insanely well. (This was the main reason for switching from Postfix.)

Setup

Here is the config/plugins file:

dont_require_anglebrackets
spamtrap
rcpt_regexp

queue/ipc-dirqueue /tmpfs/trapperqueue/hamincoming ham
queue/ipc-dirqueue /tmpfs/trapperqueue/incoming

'dont_require_anglebrackets' is a standard qpsmtpd plugin. 'spamtrap' is a custom plugin (below). 'rcpt_regexp' is a contributed plugin, modified (below). 'queue/ipc-dirqueue' is the IPC::DirQueue queueing plugin in QpsmtpdIpcDirqueue.

plugins/spamtrap file:

sub register {
  my ($self, $qp, @args) = @_;
  $self->register_hook('received_line', 'received_line');
  1;
}

sub hook_rcpt {
  my ($self, $transaction, $addr, %param) = @_;
  $transaction->notes("rcpt_to", $addr);
  return (DECLINED);        # just record it
}

sub hook_mail {
  my ($self, $transaction, $addr, %param) = @_;
  $transaction->notes("mail_from", $addr);
  return (DECLINED)
}


sub received_line {
  my ($self, $transaction, $smtp, $authheader, $sslheader) = @_;
  use POSIX qw(strftime);
  return (OK, "from ".$self->connection->hello_host." (".

    $self->connection->remote_host." [".$self->connection->remote_ip."]) ".
    " $authheader by ".$self->transaction->config('me').
    " (qpsmtpd/".$self->transaction->version.")".
    " with $sslheader$smtp ident=".$self->connection->remote_info.
    " MAIL_FROM=".$self->transaction->notes("mail_from").
    " RCPT_TO=".$self->transaction->notes("rcpt_to").
    "; ".(strftime('%a, %d %b %Y %H:%M:%S %z', localtime)));

}

plugins/rcpt_regexp file:

use Qpsmtpd::Constants;

sub register {
    my ($self, $qp, @args) = @_;
    $self->register_hook("rcpt", "check_regexp_rcpt");
}

sub check_regexp_rcpt {
    my ($self, $transaction, $recipient) = @_;
    return (DECLINED)
      unless $recipient->host && $recipient->user;

    my $rcpt = lc $recipient->user . '@' . $recipient->host;
    my ($re, $const, $comment, $str, $ok, $err);

    foreach ($self->qp->config("rcpt_regexp")) {
        s/^\s*//;
        ($re, $const, $comment) = split /\s+/, $_, 3;
        $str = undef;
        if ($re =~ m#^/(.*)/$#) {
            $re = $1;
            $ok = eval { $re = qr/$re/i; };
            if ([email protected]) {
                ($err = [email protected]) =~ s/\s*at \S+ line \d+\.\s*$//;
                $self->log(LOGWARN, "REGEXP '$re' not valid: $err");
                next;
            }
            $re = $ok;
        }
        else {
            $str = lc $re;
        }

        unless (defined $const) {
            $self->(LOGWARN, "rcpt_regexp - no return code");
            next;
        }

        $ok    = $const;
        $const = Qpsmtpd::Constants::return_code($const);
        unless (defined $const) {
            $self->log(LOGWARN,
                           "rcpt_regexp - '$ok' is not a valid "
                         . "constant, ignoring this line"
                      );
            next;
        }

        if (defined $str) {
            next unless $str eq $rcpt;
            $self->log(LOGDEBUG, "String $str matched $rcpt, returning $ok");
        }
        else {
            next unless $rcpt =~ $re;
            $self->log(LOGDEBUG, "RE $re matched $rcpt, returning $ok");
        }

        if ($comment =~ s/\[note=(\S+)\]//) {
            $transaction->notes("rcpt_regexp_note", $1);
        }

        return ($const, $comment);
    }
    return (DECLINED);
}

=head1 NAME

rcpt_regexp - check recipients against a list of regular expressions

=head1 DESCRIPTION

B<rcpt_regexp> reads a list of regular expressions, return codes and comments
from the I<rcpt_regexp> config file. If the regular expression does NOT match
I<m#^(/.*/)$#>, it is used as a string which is compared with I<eq lc($rcpt)>.
The recipient addresses are checked against this list, and if the first
matches, the return code from that line and the comment are returned to
qpsmtpd. Return code can be any valid plugin return code from
Qpsmtpd::Constants. Matching is always done case insenstive.

=head1 CONFIG FILE

The config file I<rcpt_regexp> contains lines with a perl RE, including the
"/"s, a return code and a comment, which will be returned to the sender, if
the code is not OK or DECLINED. Example:

  # rcpt_regexp - config for rcpt_regexp plugin
  [email protected]           OK       Accepting mail
  /^user\d+\@doma\.in$/   OK       Accepting mail
  [email protected]         DENY     User not found.
  /^unused\@.*/           DENY     User not found.
  /^.*$/                  DECLINED Fall through to next rcpt plugin

If the comment contains /\[note=(\S+)\]/, that string will be added to the
transaction as a new "note" called "rcpt_regexp_note", which later plugins
can examine and use for their own purposes.

=head1 COPYRIGHT AND LICENSE

Copyright (c) 2005 Hanno Hecker

This plugin is licensed under the same terms as the qpsmtpd package itself.
Please see the LICENSE file included with qpsmtpd for details.

=cut

# vim: ts=4 sw=4 expandtab syn=perl

config/rcpt_regexp file (paraphrased):

# a hamtrap address
/^address\@traps.spamassassin.org$/    OK Sure [note=ham]

# old spamtraps that are longer usable
/^[email protected]$/  DENY Sorry, too much nonspam - please turn off the forward

# all others: trap
/^.*$/                                 OK Sure

I then run it as

/path/to/qpsmtpd/qpsmtpd-async -u trapper -p 25 -l 
64.142.115.130

, run a Postfix on 127.0.0.1, and hey presto -- internal andoutbound email is handled by Postfix, while incoming email goes straight into qpsmtpd and never gets near Postfix.

Results

It's extremely low in resource consumption so far:

trapper   7781  0.0  1.1 12128 10492 pts12   SN   14:14  10:55 /usr/bin/perl /home/trapscripts/qpsmtpd/qpsmtpd-async -u trapper -p 25 -l 64.142.115.130

There seems to be a slight memory leak; after 3 days, it's now up to:

trapper   7781 11.3  7.9 74544 71816 pts12   SN   Apr11 281:22 /usr/bin/perl /home/trapscripts/qpsmtpd/qpsmtpd-async -u trapper -p 25 -l 64.142.115.130

Throughput is, of course, up quite a bit from the old postfix + procmail + perl script setup:

KB per day of spam, postfix version:
134428  cor/spam.2
134204  cor/spam.3

KB per day, qpsmtpd:
175492  cor/spam.1
152192  cor/spam.2

Load average is down below 1.0, compared to around 8.0 before, and interactive performance is similarly speedy.

It remains to be seen if a really heavy spam storm could cause qpsmtpd to refuse to accept SMTP connections, as intermittently happened with postfix.

TODO

- I'd like to capture root, postmaster, jm etc. and forward those into the internal Postfix instead of into the dirqueue.

- It'd be nice to have a way to do conditionals on the notes in the config/plugins file, so that different destination queues can be chosen without having to hack it into the queue plugin code.

QpsmtpdSpamtrap (last edited 2007-04-13 14:45:20 by 212-2-169-61)