Hi all,

Attached is a generic 'spamhandle' plugin that handles mail rejection, 
redirection, and/or subject munging based on a spam_score note passed
from an earlier plugin (e.g. spamassassin, spamassassin_spamc, etc.).
This allows the actual spam filtering plugins to be simpler and more
pluggable, and also allows cumulative spam scoring, instead of the
current one-shot approach (e.g. instead of denying in a dnsbl plugin, 
you could just a spam_score to increase the likelihood of the mail 
being treated as spam later on). It also supports the current 
per_user_config stuff, for those who are playing with that 
pre-qpsmtpd-0.30.

Comments welcome.

Cheers,
Gavin

=head1 NAME

spamhandle - generic spam handler/redirector

=head1 DESCRIPTION

Generic plugin to reject, munge, or redirect email according to a spam
score. Must be used in conjunction with one or more spam filtering plugins
that pass the calculated spam score via a numeric 'spam_score' 
transaction note. The intention is to allow spam-handling 'policies' to
be implemented by spamhandle, allowing the actual spam filtering plugins 
to be simpler, more pluggable, and potentially cumulative.

=head1 CONFIG

spamhandle accepts the following parameters either via the config/plugins
line or via a 'spamhandle' config file (one parameter per line, name and
value space-separated):

=over 4

=item reject_threshold <threshold>

The score above which the mail will be rejected outright. This should 
probably be pretty large e.g. for spamassassin, 15 or 20. Rejection
terminates the plugin, so rejected mail is never redirected or munged.
Default: never reject.

=item redirect_threshold <threshold>

The score above which the mail will be redirected instead of delivered
to the specified recipients. Requires the 'redirect_recipient' parameter
below. Default: never redirect.

=item redirect_recipient <recipient_addr>

Email address to which mail scoring above the redirect_threshold is 
redirected (currently requires a one-line patch to Qpsmtpd::Transaction).
If the address is a standard email address it replaces the current 
recipient list. If the address is a bare username (no '@' or domain 
e.g. 'spam'), the existing recipient domains are retained (allowing 
per-domain spam mailboxes); if the address is a bare username ending in a
hyphen (e.g.  'spam-' the username is prefixed to existing recipient 
addresses (allowing per-user spam mailboxes, and a spam-default catchall).

=item munge_threshold <threshold>

The score above which the mail becomes a candidate for munging in 
various ways. See the 'munge_subject' parameter below (TODO: others?). 
Default: never munge.

=item munge_subject <tag>

The spam tag (default: '***SPAM***') to prepend to the Subject header 
(if not already present) if the spam score exceeds the munge_threshold  
above. 

=item per_recipient <boolean>

(only via config/plugins) Allow per-recipient 'spamhandle' config files 
to be used. Note that because spamhandle is a post_data plugin, it is 
required and assumed that all recipients are using the same config 
definitions (see e.g. the denysoft_multi_rcpt plugin).

=back

=head1 AUTHOR

Gavin Carr <[EMAIL PROTECTED]>.

=cut

use Mail::Address;

my $VERSION = 0.01;

my %DEFAULTS = (
  munge_subject => '***SPAM***',
);

sub register {
  my ($self, $qp, %arg) = @_;
  $self->{_defaults} = { %DEFAULTS, %arg };
  $self->register_hook($arg{per_recipient} ? "rcpt" : "mail", "load_config");
  $self->register_hook("data_post", "check_spam");
}

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

  # Setup only once (data plugin: must assume all recipients use the same config!)
  return DECLINED if $self->{_config};

  # Setup config from defaults and spamhandle config
  my $config_arg = $self->{_defaults}->{per_recipient} ? { rcpt => $rcpt } : {};
  my @config = $self->qp->config('spamhandle', $config_arg);
  $self->{_config} = { 
    %{$self->{_defaults}},
    @config ? map { split /\s+/, $_, 2 } @config : ()
  };
  $self->log(6, "spamhandle config: " . join(', ',
    map { $_ . '=' . $self->{_config}->{$_} } sort keys %{$self->{_config}}));
  return (DECLINED);
}

sub check_spam {
  my ($self, $transaction) = @_;
  my $score = $transaction->notes('spam_score') or return DECLINED;
  $self->log(3, "spam_score: $score");

  # Deny if reject threshold exceeded
  return DENY, "spam score too high - message denied"
    if defined $self->{_config}->{reject_threshold} and 
      $score > $self->{_config}->{reject_threshold};

  # Redirect if redirect threshold exceeded
  $self->redirect_spam($transaction)
    if defined $self->{_config}->{redirect_threshold} and 
      $score > $self->{_config}->{redirect_threshold};

  # Munge if munge threshold exceeded
  $self->munge_spam($transaction)
    if defined $self->{_config}->{munge_threshold} and 
      $score > $self->{_config}->{munge_threshold};

  return DECLINED;
}

sub munge_spam {
  my ($self, $transaction) = @_;
  my $tag = $self->{_config}->{munge_subject};
  if ($tag) {
    my $subject = $transaction->header->get('Subject') || '';
    $transaction->header->replace('Subject', "$tag $subject");
  }

  return DECLINED;
}

sub redirect_spam {
  my ($self, $transaction) = @_;
  my $rcpt = $self->{_config}->{redirect_recipient} or return DECLINED;

  # Full email address - replace recipients list
  if ($rcpt =~ m'@') {
    my @rcpt = Mail::Address->parse($rcpt);
    unless (@rcpt) {
      $self->log(1, sprintf("failed to parse any redirect_recipient addresses: '%s'", 
$rcpt));
      return DECLINED;
    }
    # NB: requires non-standard recipients() mutator
    $self->log(4,"redirect_spam: to " . join(',',map { $_->address } @rcpt));
    $transaction->recipients(@rcpt);
  }

  # Recipient is username without domain
  else {
    for my $r ($transaction->recipients) {
      my $new = substr($rcpt,-1) eq '-' ? $rcpt . $r->user : $rcpt;
      $new .= '@' . $r->host if $r->host;
      $self->log(4,sprintf("redirect_spam: '%s' -> '%s'",$r->address,$new));
      $r->address($new);
    }
  }

  $transaction->notes('qpstats_queue_replace', 'spamhandle:redirect');

  return DECLINED;
}

# arch-tag: 09ce6583-fc2e-4b05-b8e4-d38df5810b92

Reply via email to