Merge branch 'master' of github.com:msimerson/qpsmtpd-dev

This commit is contained in:
Matt Simerson 2012-06-30 16:14:28 -04:00
commit 28798aa02e
14 changed files with 321 additions and 22 deletions

View File

@ -56,7 +56,7 @@ auth/authdeny
rcpt_ok rcpt_ok
headers days 5 reject_type temp require From,Date headers days 5 reject_type temp require From,Date
domainkeys dkim
# content filters # content filters
#uribl #uribl

View File

@ -282,6 +282,15 @@ sub is_immune {
return; return;
}; };
sub adjust_karma {
my ( $self, $value ) = @_;
my $karma = $self->connection->notes('karma') || 0
$karma += $value;
$self->connection->notes('karma', $value);
return $value;
};
sub _register_standard_hooks { sub _register_standard_hooks {
my ($plugin, $qp) = @_; my ($plugin, $qp) = @_;

72
log/show_message Executable file
View File

@ -0,0 +1,72 @@
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
my $QPDIR = '/usr/home/qpsmtpd/smtpd';
my $logfile = "$QPDIR/log/main/current";
my $is_ip = 0;
my $search = $ARGV[0];
if ( ! $search ) {
die "\nusage: $0 [ ip_address | PID ]\n\n";
};
if ( $search =~ /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/ ) {
#print "it's an IP\n";
$is_ip++;
};
open my $LOG, '<', $logfile;
if ( $is_ip ) { # look for the connection start message for the IP
my $ip_matches;
while ( defined (my $line = <$LOG>) ) {
next if ! $line;
my ( $tai, $pid, $mess ) = split /\s/, $line, 3;
if ( 'Connection from ' eq substr( $mess, 0, 16 ) ) {
my ( $ip ) = (split /\s+/, $mess)[-1]; # IP is last word
$ip = substr $ip, 1, -1; # trim off brackets
if ( $ip eq $search ) {
$ip_matches++;
$search = $pid;
$is_ip = 0;
};
};
};
seek $LOG, 0, 0;
die "no pid found for ip $search\n" if $is_ip;
print "showing the last of $ip_matches connnections from $ARGV[0]\n";
};
print "showing QP message PID $search\n";
while ( defined (my $line = <$LOG>) ) {
next if ! $line;
my ( $tai, $pid, $mess ) = split /\s/, $line, 3;
next if ! $pid;
print $mess if ( $pid eq $search );
};
close $LOG;
sub get_qp_dir {
foreach my $user ( qw/ qpsmtpd smtpd / ) {
my ($homedir) = (getpwnam( $user ))[7] or next;
if ( -d "$homedir/plugins" ) {
return "$homedir";
};
foreach my $s ( qw/ smtpd qpsmtpd qpsmtpd-dev / ) {
if ( -d "$homedir/$s/plugins" ) {
return "$homedir/$s";
};
};
};
if ( -d "./plugins" ) {
return Cwd::getcwd();
};
};

View File

@ -85,7 +85,7 @@ sub hook_mail {
next unless $bad; next unless $bad;
next unless $self->is_match( $from, $bad, $host ); next unless $self->is_match( $from, $bad, $host );
$reason ||= "Your envelope sender is in my badmailfrom list"; $reason ||= "Your envelope sender is in my badmailfrom list";
$self->connection->notes('karma', ($self->connection->notes('karma') || 0) - 1); $self->adjust_karma( -1 );
return $self->get_reject( $reason ); return $self->get_reject( $reason );
} }

View File

@ -478,9 +478,7 @@ sub reject_agree {
if ( $d->{class} eq 'Spam' ) { if ( $d->{class} eq 'Spam' ) {
if ( $sa->{is_spam} eq 'Yes' ) { if ( $sa->{is_spam} eq 'Yes' ) {
if ( defined $self->connection->notes('karma') ) { $self->adjust_karma( -2 );
$self->connection->notes('karma', $self->connection->notes('karma') - 2);
};
$self->log(LOGINFO, "fail, agree, $status"); $self->log(LOGINFO, "fail, agree, $status");
my $reject = $self->get_reject_type(); my $reject = $self->get_reject_type();
return ($reject, 'we agree, no spam please'); return ($reject, 'we agree, no spam please');
@ -493,9 +491,7 @@ sub reject_agree {
if ( $d->{class} eq 'Innocent' ) { if ( $d->{class} eq 'Innocent' ) {
if ( $sa->{is_spam} eq 'No' ) { if ( $sa->{is_spam} eq 'No' ) {
if ( $d->{confidence} > .9 ) { if ( $d->{confidence} > .9 ) {
if ( defined $self->connection->notes('karma') ) { $self->adjust_karma( 2 );
$self->connection->notes('karma', ( $self->connection->notes('karma') + 2) );
};
}; };
$self->log(LOGINFO, "pass, agree, $status"); $self->log(LOGINFO, "pass, agree, $status");
return DECLINED; return DECLINED;
@ -591,6 +587,7 @@ sub autolearn {
defined $self->{_args}{autolearn} or return; defined $self->{_args}{autolearn} or return;
# only train once.
$self->autolearn_naughty( $response, $transaction ) and return; $self->autolearn_naughty( $response, $transaction ) and return;
$self->autolearn_karma( $response, $transaction ) and return; $self->autolearn_karma( $response, $transaction ) and return;
$self->autolearn_spamassassin( $response, $transaction ) and return; $self->autolearn_spamassassin( $response, $transaction ) and return;

View File

@ -173,7 +173,7 @@ sub connect_handler {
}; };
$self->connection->notes('earlytalker', 1); $self->connection->notes('earlytalker', 1);
$self->connection->notes('karma', -1); $self->adjust_karma( -1 );
return DECLINED; return DECLINED;
} }

View File

@ -430,7 +430,7 @@ sub no_matching_dns {
if ( $self->connection->notes('helo_forward_match') && if ( $self->connection->notes('helo_forward_match') &&
$self->connection->notes('helo_reverse_match') ) { $self->connection->notes('helo_reverse_match') ) {
$self->log( LOGDEBUG, "foward and reverse match" ); $self->log( LOGDEBUG, "foward and reverse match" );
# TODO: consider adding some karma here $self->adjust_karma( 1 ); # whoppee, a match!
return; return;
}; };

View File

@ -177,14 +177,14 @@ those senders haven't sent us any ham. As such, it's much safer to use.
This plugin sets the connection note I<karma_history>. Your plugin can This plugin sets the connection note I<karma_history>. Your plugin can
use the senders karma to be more gracious or rude to senders. The value of use the senders karma to be more gracious or rude to senders. The value of
I<karma_history> is the number the nice connections minus naughty I<karma_history> is the number of nice connections minus naughty
ones. The higher the number, the better you should treat the sender. ones. The higher the number, the better you should treat the sender.
When I<reject naughty> is set and a naughty sender is encountered, most To alter a connections karma based on its behavior, do this:
plugins should skip processing. However, if you wish to toy with spammers by
teergrubing, extending banner delays, limiting connections, limiting $self->adjust_karma( -1 ); # lower karma (naughty)
recipients, random disconnects, handoffs to rblsmtpd, and other fun tricks, $self->adjust_karma( 1 ); # raise karma (good)
then connections with the I<naughty> note set are for you!
=head1 EFFECTIVENESS =head1 EFFECTIVENESS
@ -194,7 +194,7 @@ connections.
This plugins effectiveness results from the propensity of naughty senders This plugins effectiveness results from the propensity of naughty senders
to be repeat offenders. Limiting them to a single offense per day(s) greatly to be repeat offenders. Limiting them to a single offense per day(s) greatly
reduces the number of useless tokens miscreants add to our Bayes databases. reduces the resources they can waste.
Of the connections that had previously passed all other checks and were caught Of the connections that had previously passed all other checks and were caught
only by spamassassin and/or dspam, B<karma> rejected 31 percent. Since only by spamassassin and/or dspam, B<karma> rejected 31 percent. Since
@ -207,7 +207,7 @@ Connection summaries are stored in a database. The database key is the int
form of the remote IP. The value is a : delimited list containing a penalty form of the remote IP. The value is a : delimited list containing a penalty
box start time (if the server is/was on timeout) and the count of naughty, box start time (if the server is/was on timeout) and the count of naughty,
nice, and total connections. The database can be listed and searched with the nice, and total connections. The database can be listed and searched with the
karma_dump.pl script. karma_tool script.
=head1 BUGS & LIMITATIONS =head1 BUGS & LIMITATIONS

View File

@ -138,9 +138,7 @@ sub rcpt_handler {
return DECLINED if $rv; return DECLINED if $rv;
if ( defined $self->connection->notes('karma') ) { $self->adjust_karma( -1 );
$self->connection->notes('karma', ($self->connection->notes('karma') - 1));
};
return (DENY, "fail, no mailbox by that name. qd (#5.1.1)" ); return (DENY, "fail, no mailbox by that name. qd (#5.1.1)" );
} }

View File

@ -401,7 +401,7 @@ sub reject {
} }
} }
$self->connection->notes('karma', ($self->connection->notes('karma') - 1)); $self->adjust_karma( -1 );
# default of media_unsupported is DENY, so just change the message # default of media_unsupported is DENY, so just change the message
$self->log(LOGINFO, "fail, $status, > $reject, $learn"); $self->log(LOGINFO, "fail, $status, > $reject, $learn");
return ($self->get_reject_type(), "spam score exceeded threshold"); return ($self->get_reject_type(), "spam score exceeded threshold");

223
plugins/whitelist Normal file
View File

@ -0,0 +1,223 @@
=head1 NAME
whitelist - whitelist override for other qpsmtpd plugins
=head1 DESCRIPTION
The B<whitelist> plugin allows selected hosts or senders or recipients
to be whitelisted as exceptions to later plugin processing. It is a more
conservative variant of Devin Carraway's 'whitelist' plugin.
=head1 CONFIGURATION
To enable the plugin, add it to the qpsmtpd/config/plugins file as usual.
It should precede any plugins you might wish to whitelist for.
Several configuration files are supported, corresponding to different
parts of the SMTP conversation:
=over 4
=item whitelisthosts
Any IP address (or start-anchored fragment thereof) listed in the
whitelisthosts file is exempted from any further validation during
'connect', and can be selectively exempted at other stages by
plugins testing for a 'whitelisthost' connection note.
Similarly, if the environment variable $WHITELISTCLIENT is set
(which can be done by tcpserver), the connection will be exempt from
further 'connect' validation, and the host can be selectively
exempted by other plugins testing for a 'whitelistclient' connection
note.
=item whitelisthelo
Any host that issues a HELO matching an entry in whitelisthelo will
be exempted from further validation at the 'helo' stage. Subsequent
plugins can test for a 'whitelisthelo' connection note. Note that
this does not actually amount to an authentication in any meaningful
sense.
=item whitelistsenders
If the envelope sender of a mail (that which is sent as the MAIL FROM)
matches an entry in whitelistsenders, or if the hostname component
matches, the mail will be exempted from any further validation within
the 'mail' stage. Subsequent plugins can test for this exemption as a
'whitelistsender' transaction note.
=item whitelistrcpt
If any recipient of a mail (that sent as the RCPT TO) matches an
entry from whitelistrcpt, or if the hostname component matches, no
further validation will be required for this recipient. Subsequent
plugins can test for this exemption using a 'whitelistrcpt'
transaction note, which holds the count of whitelisted recipients.
=back
whitelist_soft also supports per-recipient whitelisting when using
the per_user_config plugin. To enable the per-recipient behaviour
(delaying all whitelisting until the rcpt part of the smtp
conversation, and using per-recipient whitelist configs, if
available), pass a true 'per_recipient' argument in the
config/plugins invocation i.e.
whitelist_soft per_recipient 1
By default global and per-recipient whitelists are merged; to turn off
the merge behaviour pass a false 'merge' argument in the config/plugins
invocation i.e.
whitelist_soft per_recipient 1 merge 0
=head1 BUGS
Whitelist lookups are all O(n) linear scans of configuration files, even
though they're all associative lookups. Something should be done about
this when CDB/DB/GDBM configs are supported.
=head1 AUTHOR
Based on the 'whitelist' plugin by Devin Carraway <qpsmtpd@devin.com>.
Modified by Gavin Carr <gavin@openfusion.com.au> to not inherit
whitelisting across hooks, but use per-hook whitelist notes instead.
This is a more conservative approach e.g. whitelisting an IP will not
automatically allow relaying from that IP.
=cut
my $VERSION = 0.02;
# Default is to merge whitelists in per_recipient mode
my %MERGE = (merge => 1);
sub register {
my ($self, $qp, %arg) = @_;
$self->{_per_recipient} = 1 if $arg{per_recipient};
$MERGE{merge} = $arg{merge} if defined $arg{merge};
# Normal mode - whitelist per hook
unless ($arg{per_recipient}) {
$self->register_hook("connect", "check_host");
$self->register_hook("helo", "check_helo");
$self->register_hook("ehlo", "check_helo");
$self->register_hook("mail", "check_sender");
$self->register_hook("rcpt", "check_rcpt");
}
# Per recipient mode - defer all whitelisting to rcpt hook
else {
$self->register_hook("rcpt", "check_host");
$self->register_hook("helo", "helo_helper");
$self->register_hook("ehlo", "helo_helper");
$self->register_hook("rcpt", "check_helo");
$self->register_hook("rcpt", "check_sender");
$self->register_hook("rcpt", "check_rcpt");
}
}
sub check_host {
my ($self, $transaction, $rcpt) = @_;
my $ip = $self->qp->connection->remote_ip || return (DECLINED);
# From tcpserver
if (exists $ENV{WHITELISTCLIENT}) {
$self->qp->connection->notes('whitelistclient', 1);
$self->log(2, "host $ip is a whitelisted client");
return OK;
}
my $config_arg = $self->{_per_recipient} ? {rcpt => $rcpt, %MERGE} : {};
for my $h ($self->qp->config('whitelisthosts', $config_arg)) {
if ($h eq $ip or $ip =~ /^\Q$h\E/) {
$self->qp->connection->notes('whitelisthost', 1);
$self->log(2, "host $ip is a whitelisted host");
return OK;
}
}
return DECLINED;
}
sub helo_helper {
my ($self, $transaction, $helo) = @_;
$self->{_whitelist_soft_helo} = $helo;
return DECLINED;
}
sub check_helo {
my ($self, $transaction, $helo) = @_;
# If per_recipient will be rcpt hook, and helo actually rcpt
my $config_arg = {};
if ($self->{_per_recipient}) {
$config_arg = {rcpt => $helo, %MERGE};
$helo = $self->{_whitelist_soft_helo};
}
for my $h ($self->qp->config('whitelisthelo', $config_arg)) {
if ($helo and lc $h eq lc $helo) {
$self->qp->connection->notes('whitelisthelo', 1);
$self->log(2, "helo host $helo in whitelisthelo");
return OK;
}
}
return DECLINED;
}
sub check_sender {
my ($self, $transaction, $sender) = @_;
# If per_recipient will be rcpt hook, and sender actually rcpt
my $config_arg = {};
if ($self->{_per_recipient}) {
$config_arg = {rcpt => $sender, %MERGE};
$sender = $transaction->sender;
}
return DECLINED if $sender->format eq '<>';
my $addr = lc $sender->address or return DECLINED;
my $host = lc $sender->host or return DECLINED;
for my $h ($self->qp->config('whitelistsenders', $config_arg)) {
next unless $h;
$h = lc $h;
if ($addr eq $h or $host eq $h) {
$transaction->notes('whitelistsender', 1);
$self->log(2, "envelope sender $addr in whitelistsenders");
return OK;
}
}
return DECLINED;
}
sub check_rcpt {
my ($self, $transaction, $rcpt) = @_;
my $addr = lc $rcpt->address or return DECLINED;
my $host = lc $rcpt->host or return DECLINED;
my $config_arg = $self->{_per_recipient} ? {rcpt => $rcpt, %MERGE} : {};
for my $h ($self->qp->config('whitelistrcpt', $config_arg)) {
next unless $h;
$h = lc $h;
if ($addr eq $h or $host eq $h) {
my $note = $transaction->notes('whitelistrcpt');
$transaction->notes('whitelistrcpt', ++$note);
$self->log(2, "recipient $addr in whitelistrcpt");
return OK;
}
}
return DECLINED;
}