From 8fd04a2621a1ce085f177ae0018185e4a15c2749 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Wed, 27 Jun 2012 19:36:58 -0400 Subject: [PATCH 01/37] SPF: more logging additions --- plugins/sender_permitted_from | 56 ++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/plugins/sender_permitted_from b/plugins/sender_permitted_from index dabad55..d888701 100644 --- a/plugins/sender_permitted_from +++ b/plugins/sender_permitted_from @@ -143,28 +143,18 @@ sub mail_handler { }; # SPF result codes: pass fail softfail neutral none error permerror temperror + return $self->handle_code_none($reject, $why) if $code eq 'none'; + return $self->handle_code_fail($reject, $why) if $code eq 'fail'; + return $self->handle_code_softfail($reject, $why) if $code eq 'softfail'; + if ( $code eq 'pass' ) { $self->log(LOGINFO, "pass, $code: $why" ); return (DECLINED); } - elsif ( $code eq 'fail' ) { - $self->log(LOGINFO, "fail, $why" ); - return (DENY, "SPF - forgery: $why") if $reject >= 3; - return (DENYSOFT, "SPF - $code: $why") if $reject >= 2; - } - elsif ( $code eq 'softfail' ) { - $self->log(LOGINFO, "fail, $why" ); - return (DENY, "SPF - $code: $why") if $reject >= 4; - return (DENYSOFT, "SPF - $code: $why") if $reject >= 3; - } elsif ( $code eq 'neutral' ) { $self->log(LOGINFO, "fail, $code, $why" ); return (DENY, "SPF - $code: $why") if $reject >= 5; } - elsif ( $code eq 'none' ) { - $self->log(LOGINFO, "fail, $code, $why" ); - return (DENY, "SPF - $code: $why") if $reject >= 6; - } elsif ( $code eq 'error' ) { $self->log(LOGINFO, "fail, $code, $why" ); return (DENY, "SPF - $code: $why") if $reject >= 6; @@ -184,6 +174,44 @@ sub mail_handler { return (DECLINED); } +sub handle_code_none { + my ($self, $reject, $why ) = @_; + + if ( $reject >= 6 ) { + $self->log(LOGINFO, "fail, none, $why" ); + return (DENY, "SPF - none: $why"); + }; + + $self->log(LOGINFO, "pass, none, $why" ); + return DECLINED; +}; + +sub handle_code_fail { + my ($self, $reject, $why ) = @_; + + if ( $reject >= 2 ) { + $self->log(LOGINFO, "fail, $why" ); + return (DENY, "SPF - forgery: $why") if $reject >= 3; + return (DENYSOFT, "SPF - fail: $why") + }; + + $self->log(LOGINFO, "pass, fail tolerated, $why" ); + return DECLINED; +}; + +sub handle_code_softfail { + my ($self, $reject, $why ) = @_; + + if ( $reject >= 3 ) { + $self->log(LOGINFO, "fail, soft, $why" ); + return (DENY, "SPF - fail: $why") if $reject >= 4; + return (DENYSOFT, "SPF - fail: $why") if $reject >= 3; + }; + + $self->log(LOGINFO, "pass, softfail tolerated, $why" ); + return DECLINED; +}; + sub data_post_handler { my ($self, $transaction) = @_; From 0ca16d61a774a83703643eaced3792405e21eb78 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Wed, 27 Jun 2012 20:15:13 -0400 Subject: [PATCH 02/37] summarize: check more locations to discover QP dir --- log/summarize.pl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/log/summarize.pl b/log/summarize.pl index 04784cc..b506d82 100755 --- a/log/summarize.pl +++ b/log/summarize.pl @@ -3,6 +3,7 @@ use strict; use warnings; +use Cwd; use Data::Dumper; use File::Tail; @@ -276,16 +277,20 @@ sub show_symbol { sub get_qp_dir { foreach my $user ( qw/ qpsmtpd smtpd / ) { - my ($homedir) = (getpwnam( $user ))[7] or next; if ( -d "$homedir/plugins" ) { return "$homedir"; }; - if ( -d "$homedir/smtpd/plugins" ) { - return "$homedir/smtpd"; + foreach my $s ( qw/ smtpd qpsmtpd qpsmtpd-dev / ) { + if ( -d "$homedir/smtpd/plugins" ) { + return "$homedir/smtpd"; + }; }; }; + if ( -d "./plugins" ) { + return Cwd::getcwd(); + }; }; sub populate_plugins_from_registry { From ba854c471fca704cfd7c40a62b7e661070070866 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Wed, 27 Jun 2012 20:16:11 -0400 Subject: [PATCH 03/37] log/run: removed spurious space --- log/run | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/log/run b/log/run index 5b3b4b6..e3a630c 100755 --- a/log/run +++ b/log/run @@ -1,4 +1,4 @@ -#! /bin/sh +#!/bin/sh export LOGDIR=./main mkdir -p $LOGDIR exec multilog t s10000000 n20 $LOGDIR From 6f34fbb6cdc34ebfd7aa92d6d9f45511e22724d6 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Wed, 27 Jun 2012 20:16:34 -0400 Subject: [PATCH 04/37] dspam: better error message if dspam_bin is not found --- plugins/dspam | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/dspam b/plugins/dspam index d92da7f..d133dd8 100644 --- a/plugins/dspam +++ b/plugins/dspam @@ -216,7 +216,7 @@ sub register { $self->{_args}{dspam_bin} ||= '/usr/local/bin/dspam'; if ( ! -x $self->{_args}{dspam_bin} ) { - $self->log(LOGERROR, "dspam not found: "); + $self->log(LOGERROR, "dspam CLI binary not found: install dspam and/or set dspam_bin"); return DECLINED; }; From e3fcd08706778a084e39858b30f81e407a444856 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Wed, 27 Jun 2012 20:17:00 -0400 Subject: [PATCH 05/37] qmail_deliverable: test variable if defined before accessing --- plugins/qmail_deliverable | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/qmail_deliverable b/plugins/qmail_deliverable index 0704b06..b22d221 100755 --- a/plugins/qmail_deliverable +++ b/plugins/qmail_deliverable @@ -77,7 +77,7 @@ sub register { $self->log(LOGWARN, "Odd number of arguments, using default config"); } else { my %args = @args; - if ($args{server} =~ /^smtproutes:/) { + if ($args{server} && $args{server} =~ /^smtproutes:/) { my ($fallback, $port) = $args{server} =~ /:(?:(.*?):?)(\d+)/; From c5fb92e64989c733886a8903c79d4f1676135159 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Wed, 27 Jun 2012 20:18:16 -0400 Subject: [PATCH 06/37] spamassassin: further log message refinement --- plugins/spamassassin | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/spamassassin b/plugins/spamassassin index 7070d7f..3c6b0f9 100644 --- a/plugins/spamassassin +++ b/plugins/spamassassin @@ -369,11 +369,12 @@ sub reject { my ($self, $transaction) = @_; my $sa_results = $self->get_spam_results($transaction) or do { - $self->log(LOGNOTICE, "skip, no results"); + $self->log(LOGNOTICE, "error, no results"); return DECLINED; }; - my $score = $sa_results->{score} or do { - $self->log(LOGERROR, "skip, error getting score"); + my $score = $sa_results->{score}; + if ( ! defined $score ) { + $self->log(LOGERROR, "error, error getting score"); return DECLINED; }; @@ -385,7 +386,7 @@ sub reject { }; my $reject = $self->{_args}{reject} or do { - $self->log(LOGERROR, "skip, reject disabled ($status, $learn)"); + $self->log(LOGERROR, "pass, reject disabled ($status, $learn)"); return DECLINED; }; @@ -400,7 +401,7 @@ sub reject { } } - $self->connection->notes('karma', $self->connection->notes('karma') - 1); + $self->connection->notes('karma', ($self->connection->notes('karma') - 1)); # default of media_unsupported is DENY, so just change the message $self->log(LOGINFO, "fail, $status, > $reject, $learn"); return ($self->get_reject_type(), "spam score exceeded threshold"); From db206898875a188c33093a063a71bf11c1ab1e6a Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 29 Jun 2012 20:20:58 -0400 Subject: [PATCH 07/37] registry: added auth_ prefixes, relay aliases --- plugins/registry.txt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/plugins/registry.txt b/plugins/registry.txt index cedcd91..0ecfb3a 100644 --- a/plugins/registry.txt +++ b/plugins/registry.txt @@ -9,7 +9,7 @@ 3 ident::p0f p0f p0f 5 karma krm karma 6 dnsbl dbl dnsbl -7 relay rly relay +7 relay rly relay check_relay,check_norelay,relay_only 9 earlytalker ear early check_earlytalker 15 helo hlo helo check_spamhelo 16 tls tls tls @@ -22,13 +22,14 @@ # # Authentication # -30 auth::vpopmail_sql aut vpsql -31 auth::vpopmaild vpd vpopd -32 auth::vpopmail vpo vpop -33 auth::checkpasswd ckp chkpw -34 auth::cvs_unix_local cvs cvsul -35 auth::flat_file flt aflat -36 auth::ldap_bind ldp aldap +30 auth::auth_vpopmail_sql aut vpsql +31 auth::auth_vpopmaild vpd vpopd +32 auth::auth_vpopmail vpo vpop +33 auth::auth_checkpasswd ckp chkpw +34 auth::auth_cvs_unix_local cvs cvsul +35 auth::auth_flat_file flt aflat +36 auth::auth_ldap_bind ldp aldap +37 auth::authdeny dny adeny # # Sender / From # From d239f394e9983e03062f91f473f056382c7511d7 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 29 Jun 2012 20:28:38 -0400 Subject: [PATCH 08/37] summarize: recognize tcpserver log entries --- log/summarize.pl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/log/summarize.pl b/log/summarize.pl index b506d82..c4616ff 100755 --- a/log/summarize.pl +++ b/log/summarize.pl @@ -70,7 +70,7 @@ while ( defined (my $line = $fh->read) ) { next if ! $line; my ( $type, $pid, $hook, $plugin, $message ) = parse_line( $line ); next if ! $type; - next if $type =~ /info|unknown|response/; + next if $type =~ /^(info|unknown|response|tcpserver)$/; next if $type eq 'init'; # doesn't occur in all deployment models if ( ! $pids{$pid} ) { # haven't seen this pid @@ -151,6 +151,7 @@ sub parse_line { return parse_line_plugin( $line ) if substr($message, 0, 1) eq '('; return ( 'dispatch', $pid, undef, undef, $message ) if substr($message, 0, 12) eq 'dispatching '; return ( 'response', $pid, undef, undef, $message ) if $message =~ /^[2|3]\d\d/; + return ( 'tcpserver', $pid, undef, undef, undef ) if substr($pid, 0, 10) eq 'tcpserver:'; # lines seen about once per connection return ( 'init', $pid, undef, undef, $message ) if substr($message, 0, 19) eq 'Accepted connection'; From 5825c2c3c8c4378af2d181978a943ac0876730aa Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 29 Jun 2012 20:29:04 -0400 Subject: [PATCH 09/37] clamdscan: default is scan always, even authenticated --- plugins/virus/clamdscan | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/virus/clamdscan b/plugins/virus/clamdscan index 0af2929..72e64ea 100644 --- a/plugins/virus/clamdscan +++ b/plugins/virus/clamdscan @@ -140,7 +140,7 @@ sub data_post_handler { my $filename = $self->get_filename( $transaction ) or return DECLINED; - return (DECLINED) if $self->is_immune(); + #return (DECLINED) if $self->is_immune(); return (DECLINED) if $self->is_too_big( $transaction ); return (DECLINED) if $self->is_not_multipart( $transaction ); From c723c40670fbb1de813c5e9914a5e37d36b59e82 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 29 Jun 2012 20:30:06 -0400 Subject: [PATCH 10/37] run: define PORT variable --- run | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/run b/run index 22c6029..0e2ff84 100755 --- a/run +++ b/run @@ -11,6 +11,7 @@ PERL=/usr/bin/perl QMAILDUID=`id -u $QPUSER` NOFILESGID=`id -g $QPUSER` IP=`head -1 config/IP` +PORT=25 LANG=C # Remove the comments between the and tags to choose a @@ -19,7 +20,7 @@ LANG=C # exec $BIN/softlimit -m $MAXRAM \ $BIN/tcpserver -c 10 -v -R -p \ - -u $QMAILDUID -g $NOFILESGID $IP smtp \ + -u $QMAILDUID -g $NOFILESGID $IP $PORT \ ./qpsmtpd 2>&1 # @@ -30,7 +31,7 @@ exec $BIN/softlimit -m $MAXRAM \ # exec $BIN/softlimit -m $MAXRAM \ # $PERL -T ./qpsmtpd-forkserver \ # --listen-address $IP \ -# --port 25 \ +# --port $PORT \ # --limit-connections 15 \ # --max-from-ip 5 \ # --user $QPUSER From 3a50137b34f8a4b72752a6b9073de6faa8f0e049 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 29 Jun 2012 20:39:44 -0400 Subject: [PATCH 11/37] logs: improve ability to find logs --- log/summarize.pl | 4 ++-- log/watch.pl | 22 ++++++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/log/summarize.pl b/log/summarize.pl index c4616ff..1201aa0 100755 --- a/log/summarize.pl +++ b/log/summarize.pl @@ -284,8 +284,8 @@ sub get_qp_dir { return "$homedir"; }; foreach my $s ( qw/ smtpd qpsmtpd qpsmtpd-dev / ) { - if ( -d "$homedir/smtpd/plugins" ) { - return "$homedir/smtpd"; + if ( -d "$homedir/$s/plugins" ) { + return "$homedir/$s"; }; }; }; diff --git a/log/watch.pl b/log/watch.pl index b93ff6e..0514a3d 100755 --- a/log/watch.pl +++ b/log/watch.pl @@ -3,11 +3,12 @@ use strict; use warnings; +use Cwd; use Data::Dumper; use File::Tail; -my $dir = find_qp_log_dir() or die "unable to find QP home dir"; -my $file = "$dir/main/current"; +my $dir = get_qp_dir() or die "unable to find QP home dir"; +my $file = "$dir/log/main/current"; my $fh = File::Tail->new(name=>$file, interval=>1, maxinterval=>1, debug =>1, tail =>100 ); while ( defined (my $line = $fh->read) ) { @@ -15,16 +16,21 @@ while ( defined (my $line = $fh->read) ) { print $line; }; -sub find_qp_log_dir { +sub get_qp_dir { foreach my $user ( qw/ qpsmtpd smtpd / ) { - my ($homedir) = (getpwnam( $user ))[7] or next; - if ( -d "$homedir/log" ) { - return "$homedir/log"; + if ( -d "$homedir/plugins" ) { + return "$homedir"; }; - if ( -d "$homedir/smtpd/log" ) { - return "$homedir/smtpd/log"; + foreach my $s ( qw/ smtpd qpsmtpd qpsmtpd-dev / ) { + if ( -d "$homedir/$s/plugins" ) { + return "$homedir/$s"; + }; }; }; + if ( -d "./plugins" ) { + return Cwd::getcwd(); + }; }; + From 5bc212b890008ac6646148faff7add9ac2bfee0f Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 14:18:16 -0400 Subject: [PATCH 12/37] helo: added is_plain_ip to lenient checks there's no excuse for a client to ever send a raw IP, and I have yet to see a valid client do it --- plugins/helo | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/plugins/helo b/plugins/helo index 58748c7..10ee6b3 100644 --- a/plugins/helo +++ b/plugins/helo @@ -106,25 +106,25 @@ Default: lenient =head3 lenient -Reject failures of the following tests: is_in_badhelo, invalid_localhost, and -is_forged_literal. +Reject failures of the following tests: is_in_badhelo, invalid_localhost, +is_forged_literal, and is_plain_ip. This setting is lenient enough not to cause problems for your Windows users. It is comparable to running check_spamhelo, but with the addition of regexp -support and the prevention of forged localhost and forged IP literals. +support, the prevention of forged localhost, forged IP literals, and plain +IPs. =head3 rfc Per RFC 2821, the HELO hostname is the FQDN of the sending server or an address literal. When I is selected, all the lenient checks and -the following are enforced: is_plain_ip, is_not_fqdn, no_forward_dns, and -no_reverse_dns. +the following are enforced: is_not_fqdn, no_forward_dns, and no_reverse_dns. If you have Windows users that send mail via your server, do not choose -I without I and the B plugin. Windows -users often send unqualified HELO names and will have trouble sending mail. - can defer the rejection, and if the user subsequently authenticates, -the rejection will be cancelled. +I without settings I and using the B +plugin. Windows PCs often send unqualified HELO names and will have trouble +sending mail. The B plugin defers the rejection, and if the user +subsequently authenticates, the rejection is be cancelled. =head3 strict @@ -259,11 +259,10 @@ sub populate_tests { my $self = shift; my $policy = $self->{_args}{policy}; - @{ $self->{_helo_tests} } = qw/ is_in_badhelo invalid_localhost is_forged_literal /; + @{ $self->{_helo_tests} } = qw/ is_in_badhelo invalid_localhost is_forged_literal is_plain_ip /; if ( $policy eq 'rfc' || $policy eq 'strict' ) { - push @{ $self->{_helo_tests} }, qw/ is_plain_ip is_not_fqdn - no_forward_dns no_reverse_dns /; + push @{ $self->{_helo_tests} }, qw/ is_not_fqdn no_forward_dns no_reverse_dns /; }; if ( $policy eq 'strict' ) { From 7d5edacf9be732c79f705d791500b7dfeb75d15d Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 15:37:25 -0400 Subject: [PATCH 13/37] karma: added adjust_karma method makes it easier to set karma in plugins --- lib/Qpsmtpd/Plugin.pm | 9 ++ plugins/badmailfrom | 2 +- plugins/dspam | 9 +- plugins/earlytalker | 2 +- plugins/helo | 2 +- plugins/karma | 16 +-- plugins/qmail_deliverable | 4 +- plugins/spamassassin | 2 +- plugins/whitelist | 223 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 248 insertions(+), 21 deletions(-) create mode 100644 plugins/whitelist diff --git a/lib/Qpsmtpd/Plugin.pm b/lib/Qpsmtpd/Plugin.pm index 6b063b4..3086c20 100644 --- a/lib/Qpsmtpd/Plugin.pm +++ b/lib/Qpsmtpd/Plugin.pm @@ -282,6 +282,15 @@ sub is_immune { 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 { my ($plugin, $qp) = @_; diff --git a/plugins/badmailfrom b/plugins/badmailfrom index 47aa425..1d1f36f 100644 --- a/plugins/badmailfrom +++ b/plugins/badmailfrom @@ -85,7 +85,7 @@ sub hook_mail { next unless $bad; next unless $self->is_match( $from, $bad, $host ); $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 ); } diff --git a/plugins/dspam b/plugins/dspam index d133dd8..72aba48 100644 --- a/plugins/dspam +++ b/plugins/dspam @@ -478,9 +478,7 @@ sub reject_agree { if ( $d->{class} eq 'Spam' ) { if ( $sa->{is_spam} eq 'Yes' ) { - if ( defined $self->connection->notes('karma') ) { - $self->connection->notes('karma', $self->connection->notes('karma') - 2); - }; + $self->adjust_karma( -2 ); $self->log(LOGINFO, "fail, agree, $status"); my $reject = $self->get_reject_type(); return ($reject, 'we agree, no spam please'); @@ -493,9 +491,7 @@ sub reject_agree { if ( $d->{class} eq 'Innocent' ) { if ( $sa->{is_spam} eq 'No' ) { if ( $d->{confidence} > .9 ) { - if ( defined $self->connection->notes('karma') ) { - $self->connection->notes('karma', ( $self->connection->notes('karma') + 2) ); - }; + $self->adjust_karma( 2 ); }; $self->log(LOGINFO, "pass, agree, $status"); return DECLINED; @@ -591,6 +587,7 @@ sub autolearn { defined $self->{_args}{autolearn} or return; + # only train once. $self->autolearn_naughty( $response, $transaction ) and return; $self->autolearn_karma( $response, $transaction ) and return; $self->autolearn_spamassassin( $response, $transaction ) and return; diff --git a/plugins/earlytalker b/plugins/earlytalker index f75c8fe..f7d38b2 100644 --- a/plugins/earlytalker +++ b/plugins/earlytalker @@ -173,7 +173,7 @@ sub connect_handler { }; $self->connection->notes('earlytalker', 1); - $self->connection->notes('karma', -1); + $self->adjust_karma( -1 ); return DECLINED; } diff --git a/plugins/helo b/plugins/helo index 10ee6b3..29a3633 100644 --- a/plugins/helo +++ b/plugins/helo @@ -430,7 +430,7 @@ sub no_matching_dns { if ( $self->connection->notes('helo_forward_match') && $self->connection->notes('helo_reverse_match') ) { $self->log( LOGDEBUG, "foward and reverse match" ); -# TODO: consider adding some karma here + $self->adjust_karma( 1 ); # whoppee, a match! return; }; diff --git a/plugins/karma b/plugins/karma index e46fdfb..18fc768 100644 --- a/plugins/karma +++ b/plugins/karma @@ -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. Your plugin can use the senders karma to be more gracious or rude to senders. The value of -I is the number the nice connections minus naughty +I is the number of nice connections minus naughty ones. The higher the number, the better you should treat the sender. -When I is set and a naughty sender is encountered, most -plugins should skip processing. However, if you wish to toy with spammers by -teergrubing, extending banner delays, limiting connections, limiting -recipients, random disconnects, handoffs to rblsmtpd, and other fun tricks, -then connections with the I note set are for you! +To alter a connections karma based on its behavior, do this: + + $self->adjust_karma( -1 ); # lower karma (naughty) + $self->adjust_karma( 1 ); # raise karma (good) + =head1 EFFECTIVENESS @@ -194,7 +194,7 @@ connections. This plugins effectiveness results from the propensity of naughty senders 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 only by spamassassin and/or dspam, B 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 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 -karma_dump.pl script. +karma_tool script. =head1 BUGS & LIMITATIONS diff --git a/plugins/qmail_deliverable b/plugins/qmail_deliverable index b22d221..04cf5aa 100755 --- a/plugins/qmail_deliverable +++ b/plugins/qmail_deliverable @@ -138,9 +138,7 @@ sub rcpt_handler { return DECLINED if $rv; - if ( defined $self->connection->notes('karma') ) { - $self->connection->notes('karma', ($self->connection->notes('karma') - 1)); - }; + $self->adjust_karma( -1 ); return (DENY, "fail, no mailbox by that name. qd (#5.1.1)" ); } diff --git a/plugins/spamassassin b/plugins/spamassassin index 3c6b0f9..6e81c7e 100644 --- a/plugins/spamassassin +++ b/plugins/spamassassin @@ -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 $self->log(LOGINFO, "fail, $status, > $reject, $learn"); return ($self->get_reject_type(), "spam score exceeded threshold"); diff --git a/plugins/whitelist b/plugins/whitelist new file mode 100644 index 0000000..2e0ccb7 --- /dev/null +++ b/plugins/whitelist @@ -0,0 +1,223 @@ + +=head1 NAME + +whitelist - whitelist override for other qpsmtpd plugins + + +=head1 DESCRIPTION + +The B 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 . + +Modified by Gavin Carr 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; +} + From ad56587e798fc96df0178e4b8a15873e7fa5020b Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 15:40:24 -0400 Subject: [PATCH 14/37] added log/show_message, dropped .pl suffix to be consistent with other QP scripts --- log/{log2sql.pl => log2sql} | 0 log/show_message | 72 +++++++++++++++++++++++++++++++++ log/{summarize.pl => summarize} | 0 log/{watch.pl => watch} | 0 4 files changed, 72 insertions(+) rename log/{log2sql.pl => log2sql} (100%) create mode 100755 log/show_message rename log/{summarize.pl => summarize} (100%) rename log/{watch.pl => watch} (100%) diff --git a/log/log2sql.pl b/log/log2sql similarity index 100% rename from log/log2sql.pl rename to log/log2sql diff --git a/log/show_message b/log/show_message new file mode 100755 index 0000000..932726a --- /dev/null +++ b/log/show_message @@ -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(); + }; +}; diff --git a/log/summarize.pl b/log/summarize similarity index 100% rename from log/summarize.pl rename to log/summarize diff --git a/log/watch.pl b/log/watch similarity index 100% rename from log/watch.pl rename to log/watch From 45da124f9ffd5d371814072f76258b9f8af12de2 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 15:58:21 -0400 Subject: [PATCH 15/37] config: replace domainkeys with dkim dkim is the heir apparent the Mail::DomainKeys perl module is deprecated (per it's author) --- config.sample/plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.sample/plugins b/config.sample/plugins index 5fb03f8..25cf8bb 100644 --- a/config.sample/plugins +++ b/config.sample/plugins @@ -56,7 +56,7 @@ auth/authdeny rcpt_ok headers days 5 reject_type temp require From,Date -domainkeys +dkim # content filters #uribl From 2a85f3b8fbc88395e158ebcb894a2845f63d6a6e Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 16:11:54 -0400 Subject: [PATCH 16/37] karma: added error keyword to error log messages --- plugins/karma | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/karma b/plugins/karma index 18fc768..b5a3a33 100644 --- a/plugins/karma +++ b/plugins/karma @@ -383,7 +383,7 @@ sub get_db_tie { my ( $self, $db, $lock ) = @_; tie( my %db, 'AnyDBM_File', $db, O_CREAT|O_RDWR, 0600) or do { - $self->log(LOGCRIT, "tie to database $db failed: $!"); + $self->log(LOGCRIT, "error, tie to database $db failed: $!"); close $lock; return; }; @@ -416,12 +416,12 @@ sub get_db_lock { # Check denysoft db open( my $lock, ">$db.lock" ) or do { - $self->log(LOGCRIT, "opening lockfile failed: $!"); + $self->log(LOGCRIT, "error, opening lockfile failed: $!"); return; }; flock( $lock, LOCK_EX ) or do { - $self->log(LOGCRIT, "flock of lockfile failed: $!"); + $self->log(LOGCRIT, "error, flock of lockfile failed: $!"); close $lock; return; }; @@ -441,12 +441,12 @@ sub get_db_lock_nfs { blocking_timeout => 10, # 10 sec stale_lock_timeout => 30 * 60, # 30 min } or do { - $self->log(LOGCRIT, "nfs lockfile failed: $!"); + $self->log(LOGCRIT, "error, nfs lockfile failed: $!"); return; }; open( my $lock, "+<$db.lock") or do { - $self->log(LOGCRIT, "opening nfs lockfile failed: $!"); + $self->log(LOGCRIT, "error, opening nfs lockfile failed: $!"); return; }; From 1d2192ba17b965e4f114880731cb69817437805c Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 16:12:42 -0400 Subject: [PATCH 17/37] registry: renamed clamd abb3 from cad to clm --- plugins/registry.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/registry.txt b/plugins/registry.txt index 0ecfb3a..8d6f1ae 100644 --- a/plugins/registry.txt +++ b/plugins/registry.txt @@ -64,7 +64,7 @@ 70 virus::aveclient ave avirs 71 virus::bitdefender bit bitdf 72 virus::clamav cav clamv -73 virus::clamdscan cad clamd +73 virus::clamdscan clm clamd 74 virus::hbedv hbv hbedv 75 virus::kavscanner kav kavsc 76 virus::klez_filter klz vklez From c6b7b504bb98ecdc0273aa479d45623d1fe048f1 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 16:28:54 -0400 Subject: [PATCH 18/37] added missing semicolon --- lib/Qpsmtpd/Plugin.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Qpsmtpd/Plugin.pm b/lib/Qpsmtpd/Plugin.pm index 3086c20..3bb4b73 100644 --- a/lib/Qpsmtpd/Plugin.pm +++ b/lib/Qpsmtpd/Plugin.pm @@ -285,7 +285,7 @@ sub is_immune { sub adjust_karma { my ( $self, $value ) = @_; - my $karma = $self->connection->notes('karma') || 0 + my $karma = $self->connection->notes('karma') || 0; $karma += $value; $self->connection->notes('karma', $value); return $value; From dc02055c96350b03248b08e8ab327b76cc745300 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 16:40:51 -0400 Subject: [PATCH 19/37] log/summarize: added auth formats --- log/summarize | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/log/summarize b/log/summarize index 1201aa0..f1cf174 100755 --- a/log/summarize +++ b/log/summarize @@ -38,6 +38,10 @@ my %formats = ( check_earlytalker => "%-3.3s", helo => "%-3.3s", tls => "%-3.3s", + 'auth::auth_vpopmail' => "%-3.3s", + 'auth::auth_vpopmaild' => "%-3.3s", + 'auth::auth_vpopmail_sql' => "%-3.3s", + 'auth::auth_checkpassword' => "%-3.3s", badmailfrom => "%-3.3s", check_badmailfrom => "%-3.3s", sender_permitted_from => "%-3.3s", From ee7ae800b2eeb60baa24a80cccacedb7522881c5 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 16:43:15 -0400 Subject: [PATCH 20/37] config/plugins: better defaults, additional entries --- config.sample/plugins | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/config.sample/plugins b/config.sample/plugins index 25cf8bb..7f19860 100644 --- a/config.sample/plugins +++ b/config.sample/plugins @@ -28,6 +28,8 @@ dont_require_anglebrackets # parse_addr_withhelo quit_fortune +#karma penalty_box 1 reject naughty + # tls should load before count_unrecognized_commands #tls earlytalker @@ -37,10 +39,10 @@ relay resolvable_fromhost rhsbl -dnsbl +dnsbl reject naughty reject_type disconnect badmailfrom badrcptto -helo +helo policy lenient # sender_permitted_from # greylisting p0f genre,windows @@ -65,18 +67,21 @@ virus/klez_filter # You can run the spamassassin plugin with options. See perldoc # plugins/spamassassin for details. # -spamassassin +spamassassin reject 12 # rejects mails with a SA score higher than 20 and munges the subject # of the score is higher than 10. # -# spamassassin reject_threshold 20 munge_subject_threshold 10 +# spamassassin reject 20 munge_subject_threshold 10 # dspam must run after spamassassin for the learn_from_sa feature to work dspam learn_from_sa 7 reject 1 # run the clamav virus checking plugin # virus/clamav +# virus/clamdscan deny_viruses yes scan_all 1 + +naughty reject data # You must enable a queue plugin - see the options in plugins/queue/ - for example: From 45a2116e8ea8b6ef0f7393b9bde3976301f2b9a5 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 17:51:59 -0400 Subject: [PATCH 21/37] log/summarize: narrower column when no geoip city data present --- log/summarize | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/log/summarize b/log/summarize index f1cf174..b203cca 100755 --- a/log/summarize +++ b/log/summarize @@ -25,7 +25,6 @@ my %formats = ( ip => "%-15.15s", hostname => "%-20.20s", distance => "%5.5s", - 'ident::geoip' => "%-20.20s", 'ident::p0f' => "%-10.10s", count_unrecognized_commands => "%-5.5s", @@ -109,10 +108,16 @@ while ( defined (my $line = $fh->read) ) { }; if ( $plugin eq 'ident::geoip' ) { - my ($gip, $distance) = $message =~ /(.*?),\s+([\d]+)\skm/; - if ( $distance ) { - $pids{$pid}{$plugin} = $gip; - $pids{$pid}{distance} = $distance; + if ( length $message < 3 ) { + $formats{'ident::geoip'} = "%-3.3s"; + $formats3{'ident::geoip'} = "%-3.3s"; + } + else { + my ($gip, $distance) = $message =~ /(.*?),\s+([\d]+)\skm/; + if ( $distance ) { + $pids{$pid}{$plugin} = $gip; + $pids{$pid}{distance} = $distance; + }; }; }; } @@ -234,12 +239,12 @@ sub print_auto_format { if ( defined $pids{$pid}{helo_host} && $plugin =~ /helo/ ) { $format .= " %-18.18s"; - push @values, delete $pids{$pid}{helo_host}; + push @values, substr( delete $pids{$pid}{helo_host}, -18, 18); push @headers, 'HELO'; } elsif ( defined $pids{$pid}{from} && $plugin =~ /from/ ) { $format .= " %-20.20s"; - push @values, delete $pids{$pid}{from}; + push @values, substr( delete $pids{$pid}{from}, -20, 20); push @headers, 'MAIL FROM'; } elsif ( defined $pids{$pid}{to} && $plugin =~ /to|rcpt|recipient/ ) { From d0e9a010da9bc2fa5d7cd6a40aea941362584ae7 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 19:04:42 -0400 Subject: [PATCH 22/37] log/show_message: fixed QP dir detection --- log/show_message | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/log/show_message b/log/show_message index 932726a..9ee2ef1 100755 --- a/log/show_message +++ b/log/show_message @@ -5,7 +5,7 @@ use warnings; use Data::Dumper; -my $QPDIR = '/usr/home/qpsmtpd/smtpd'; +my $QPDIR = get_qp_dir(); my $logfile = "$QPDIR/log/main/current"; my $is_ip = 0; @@ -20,7 +20,7 @@ if ( $search =~ /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/ ) { $is_ip++; }; -open my $LOG, '<', $logfile; +open my $LOG, '<', $logfile or die "unable to open $logfile\n"; if ( $is_ip ) { # look for the connection start message for the IP my $ip_matches; From f5021a6d554ff2a78e2a0b6d9169913670a147f0 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 19:10:14 -0400 Subject: [PATCH 23/37] resolvable_fromhost: additional logging --- plugins/resolvable_fromhost | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/resolvable_fromhost b/plugins/resolvable_fromhost index d65bece..3181470 100644 --- a/plugins/resolvable_fromhost +++ b/plugins/resolvable_fromhost @@ -68,6 +68,7 @@ Default: temp (temporary, aka soft, aka 4xx). use strict; use warnings; +use lib 'lib'; use Qpsmtpd::Constants; use Qpsmtpd::DSN; use Qpsmtpd::TcpServer; @@ -114,13 +115,14 @@ sub hook_mail { }; my $result = $transaction->notes('resolvable_fromhost') or do { + $self->log(LOGINFO, 'error, missing result' ); return Qpsmtpd::DSN->temp_resolver_failed( $self->get_reject_type(), '' ); }; return DECLINED if $result =~ /^(?:a|ip|mx)$/; # success return DECLINED if $result =~ /^(?:whitelist|null|naughty)$/; # immunity - $self->log(LOGINFO, $result ); # log error + $self->log(LOGINFO, "fail, $result" ); # log error return Qpsmtpd::DSN->addr_bad_from_system( $self->get_reject_type(), "FQDN required in the envelope sender"); From a8e793e0af92f889a86dd06b7966198b842dd67b Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 30 Jun 2012 22:35:17 -0400 Subject: [PATCH 24/37] earlytalker: lower karma for earlytalkers --- plugins/earlytalker | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/earlytalker b/plugins/earlytalker index f7d38b2..bcbad95 100644 --- a/plugins/earlytalker +++ b/plugins/earlytalker @@ -205,6 +205,7 @@ sub log_and_deny { my $ip = $self->qp->connection->remote_ip || 'remote host'; $self->connection->notes('earlytalker', 1); + $self->adjust_karma( -1 ); my $log_mess = "$ip started talking before we said hello"; my $smtp_msg = 'Connecting host started transmitting before SMTP greeting'; From b174bb0c4dd865312c264504935549e962004336 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 20 Jul 2012 11:35:20 -0400 Subject: [PATCH 25/37] allow messages with no body: Robin's patch This is Robin's patch from here: http://sources.gentoo.org/cgi-bin/viewvc.cgi/gentoo-x86/mail-mta/qpsmtpd/files/qpsmtpd-0.83-accept-empty-email.patch?view=markup --- lib/Qpsmtpd/SMTP.pm | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/Qpsmtpd/SMTP.pm b/lib/Qpsmtpd/SMTP.pm index 4247503..f1b48db 100644 --- a/lib/Qpsmtpd/SMTP.pm +++ b/lib/Qpsmtpd/SMTP.pm @@ -634,7 +634,10 @@ sub data_respond { my $timeout = $self->config('timeout'); while (defined($_ = $self->getline($timeout))) { - $complete++, last if $_ eq ".\r\n"; + if ( $_ eq ".\r\n" ) { + $complete++; + $_ eq ''; + }; $i++; # should probably use \012 and \015 in these checks instead of \r and \n ... @@ -650,7 +653,7 @@ sub data_respond { unless (($max_size and $size > $max_size)) { s/\r\n$/\n/; s/^\.\./\./; - if ($in_header and m/^$/) { + if ($in_header && (m/^$/ || $complete > 0)) { $in_header = 0; my @headers = split /^/m, $buffer; @@ -693,9 +696,10 @@ sub data_respond { # copy all lines into the spool file, including the headers # we will create a new header later before sending onwards - $self->transaction->body_write($_); + $self->transaction->body_write($_) if ! $complete; $size += length $_; } + last if $complete > 0; #$self->log(LOGDEBUG, "size is at $size\n") unless ($i % 300); } From 6baa6652124466a058613fe9140d2d4e4461d8b0 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 20 Jul 2012 11:41:09 -0400 Subject: [PATCH 26/37] change loglevel from 9 to 6 more appropriate loglevel for users --- config.sample/logging | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.sample/logging b/config.sample/logging index a870643..578467a 100644 --- a/config.sample/logging +++ b/config.sample/logging @@ -5,7 +5,7 @@ # are included below. Just remove the # symbol to enable them. # default logging plugin -logging/warn 9 +logging/warn 6 #logging/adaptive [accept minlevel] [reject maxlevel] [prefix char] #logging/adaptive 4 6 From 8a4cb80d52e8940b467f51176cd6339936cc62e9 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 20 Jul 2012 11:43:20 -0400 Subject: [PATCH 27/37] dnsbl rejections handled by naughty plugin --- config.sample/plugins | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config.sample/plugins b/config.sample/plugins index 887a022..4dc72d8 100644 --- a/config.sample/plugins +++ b/config.sample/plugins @@ -37,7 +37,7 @@ relay resolvable_fromhost rhsbl -dnsbl +dnsbl reject naughty badmailfrom badrcptto helo @@ -78,6 +78,8 @@ dspam learn_from_sa 7 reject 1 # run the clamav virus checking plugin # virus/clamav +naughty + # You must enable a queue plugin - see the options in plugins/queue/ - for example: # queue to a maildir From 0985535c91e96af3e3e6d5091bbd08c5654cd8aa Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 20 Jul 2012 11:43:59 -0400 Subject: [PATCH 28/37] added explicit spamassassin reject level --- config.sample/plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.sample/plugins b/config.sample/plugins index 4dc72d8..bdbdd54 100644 --- a/config.sample/plugins +++ b/config.sample/plugins @@ -65,7 +65,7 @@ virus/klez_filter # You can run the spamassassin plugin with options. See perldoc # plugins/spamassassin for details. # -spamassassin +spamassassin reject 12 # rejects mails with a SA score higher than 20 and munges the subject # of the score is higher than 10. From 3f62ac49beae9aad13bab22c05e5735f2bcdb3d7 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 20 Jul 2012 11:44:29 -0400 Subject: [PATCH 29/37] replaced domainkeys with dkim plugin --- config.sample/plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.sample/plugins b/config.sample/plugins index bdbdd54..50cf04a 100644 --- a/config.sample/plugins +++ b/config.sample/plugins @@ -56,7 +56,7 @@ auth/authdeny rcpt_ok headers days 5 reject_type temp require From,Date -domainkeys +dkim # content filters #uribl From 09a50f020bb132f2871c060b29af4aaa1325cb60 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 20 Jul 2012 11:48:36 -0400 Subject: [PATCH 30/37] decrement karma when detected --- plugins/earlytalker | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/earlytalker b/plugins/earlytalker index f7d38b2..bcbad95 100644 --- a/plugins/earlytalker +++ b/plugins/earlytalker @@ -205,6 +205,7 @@ sub log_and_deny { my $ip = $self->qp->connection->remote_ip || 'remote host'; $self->connection->notes('earlytalker', 1); + $self->adjust_karma( -1 ); my $log_mess = "$ip started talking before we said hello"; my $smtp_msg = 'Connecting host started transmitting before SMTP greeting'; From cfe81cddf8e2286bdded2aeba40eb0498600f296 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 20 Jul 2012 11:49:33 -0400 Subject: [PATCH 31/37] update plugin/headers config entry use future/past instead of days: -headers days 5 reject_type temp require From,Date +headers reject 1 reject_type temp require From,Date future 2 past 15 --- config.sample/plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.sample/plugins b/config.sample/plugins index 7f19860..94bcc4f 100644 --- a/config.sample/plugins +++ b/config.sample/plugins @@ -57,7 +57,7 @@ auth/authdeny # this plugin needs to run after all other "rcpt" plugins rcpt_ok -headers days 5 reject_type temp require From,Date +headers reject 1 reject_type temp require From,Date future 2 past 15 dkim # content filters From 425c25c7ac3419ceb25e76b73b64f47d2b211f5a Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 20 Jul 2012 11:54:56 -0400 Subject: [PATCH 32/37] removed duplicate naughty from config --- config.sample/plugins | 2 -- 1 file changed, 2 deletions(-) diff --git a/config.sample/plugins b/config.sample/plugins index 1e31418..7f19860 100644 --- a/config.sample/plugins +++ b/config.sample/plugins @@ -83,8 +83,6 @@ dspam learn_from_sa 7 reject 1 naughty reject data -naughty - # You must enable a queue plugin - see the options in plugins/queue/ - for example: # queue to a maildir From 3438fed859dcbc742997839e8037d9357a28ceec Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 20 Jul 2012 11:59:54 -0400 Subject: [PATCH 33/37] added vpopmail_ext to qmail_deliverable plugin --- plugins/qmail_deliverable | 89 +++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/plugins/qmail_deliverable b/plugins/qmail_deliverable index 04cf5aa..e4e0263 100755 --- a/plugins/qmail_deliverable +++ b/plugins/qmail_deliverable @@ -38,6 +38,13 @@ Example: Use "smtproutes:8998" (no second colon) to simply skip the deliverability check for domains not listed in smtproutes. +=item vpopmail_ext [ 0 | 1 ] + +Is vpopmail configured with the qmail-ext feature enabled? If so, this config +option must be enabled in order for user-ext@example.org addresses to work. + +Default: 0 + =back =head1 CAVEATS @@ -62,11 +69,57 @@ L, L, L =cut -use Qmail::Deliverable::Client qw(deliverable); +################################# +################################# + +BEGIN { + use FindBin qw($Bin $Script); + if (not $INC{'Qpsmtpd.pm'}) { + my $dir = '$PLUGINS_DIRECTORY'; + -d and $dir = $_ for qw( + /home/qpsmtpd/plugins + /home/smtp/qpsmtpd/plugins + /usr/local/qpsmtpd/plugins + /usr/local/share/qpsmtpd/plugins + /usr/share/qpsmtpd/plugins + ); + + my $file = "the 'plugins' configuration file"; + -f and $file = $_ for qw( + /home/qpsmtpd/config/plugins + /home/smtp/qpsmtpd/config/plugins + /usr/local/qpsmtpd/config/plugins + /usr/local/etc/qpsmtpd/plugins + /etc/qpsmtpd/plugins + ); + + # "die" would print "BEGIN failed" garbage + print STDERR <<"END"; + +This is a plugin for qpsmtpd and should not be run manually. + +To install the plugin: + + ln -s $Bin/$Script $dir/ + +And add "$Script server 127.0.0.1:8998" to $file, before rcpt_ok. +For configuration instructions, read "man $Script" + +(Paths may vary.) + +END + exit 255; + } +} + +################################# +################################# + use strict; use warnings; use Qpsmtpd::Constants; +use Qmail::Deliverable::Client qw(deliverable); my %smtproutes; my $shared_domain; # global variable to be closed over by the SERVER callback @@ -98,14 +151,18 @@ sub register { } elsif ($args{server}) { $Qmail::Deliverable::Client::SERVER = $args{server}; } + + if ( $args{vpopmail_ext} ) { + $Qmail::Deliverable::VPOPMAIL_EXT = $args{vpopmail_ext}; + }; } - $self->register_hook('rcpt', 'rcpt_handler'); + $self->register_hook("rcpt", "rcpt_handler"); } sub rcpt_handler { my ($self, $transaction, $rcpt) = @_; - return DECLINED if $self->is_immune(); + return DECLINED if $self->is_immune(); # requires QP 0.90+ my $address = $rcpt->address; $self->log(LOGDEBUG, "Checking deliverability for recipient '$address'"); @@ -115,31 +172,35 @@ sub rcpt_handler { my $rv = deliverable $address; if (not defined $rv or not length $rv) { - $self->log(LOGWARN, "Unknown error checking deliverability of '$address'"); + $self->log(LOGWARN, "error (unknown) checking '$address'"); return DECLINED; } my $k = 0; # known status code - $self->log(LOGINFO, "Permission failure"), $k++ if $rv == 0x11; + $self->log(LOGINFO, "error, permission failure"), $k++ if $rv == 0x11; $self->log(LOGINFO, "pass, qmail-command in dot-qmail"),$k++ if $rv == 0x12; - $self->log(LOGINFO, "bouncesaying with program"), $k++ if $rv == 0x13; + $self->log(LOGINFO, "pass, bouncesaying with program"), $k++ if $rv == 0x13; $self->log(LOGINFO, "Temporarily undeliverable: group/world writable"), $k++ if $rv == 0x21; $self->log(LOGINFO, "Temporarily undeliverable: sticky home directory"),$k++ if $rv == 0x22; - $self->log(LOGINFO, "error: $Qmail::Deliverable::Client::ERROR"), $k++ + $self->log(LOGINFO, "error, $Qmail::Deliverable::Client::ERROR"), $k++ if $rv == 0x2f; $self->log(LOGINFO, "pass, normal delivery"), $k++ if $rv == 0xf1; - $self->log(LOGINFO, "pass, deliverable through vpopmail"), $k++ if $rv == 0xf2; - $self->log(LOGINFO, "SHOULD NOT HAPPEN"), $k++ if $rv == 0xfe; + $self->log(LOGINFO, "pass, vpopmail dir"), $k++ if $rv == 0xf2; + $self->log(LOGINFO, "pass, vpopmail alias"), $k++ if $rv == 0xf3; + $self->log(LOGINFO, "pass, vpopmail catchall"), $k++ if $rv == 0xf4; + $self->log(LOGINFO, "pass, vpopmail vuser"), $k++ if $rv == 0xf5; + $self->log(LOGINFO, "pass, vpopmail qmail-ext"), $k++ if $rv == 0xf6; + $self->log(LOGINFO, "error, SHOULD NOT HAPPEN"), $k++ if $rv == 0xfe; $self->log(LOGINFO, "fail, address not local"), $k++ if $rv == 0xff; - $self->log(LOGINFO, sprintf("Unknown: 0x%02x", $rv)) if $rv and not $k; + if ( $rv ) { + $self->log(LOGINFO, sprintf("error, unknown: 0x%02x", $rv)) if not $k; + return DECLINED; + }; - return DECLINED if $rv; - - $self->adjust_karma( -1 ); - return (DENY, "fail, no mailbox by that name. qd (#5.1.1)" ); + return (DENY, "Sorry, no mailbox by that name. qd (#5.1.1)" ); } sub _smtproute { From 198625e162b2cef538ef36e02343720e04818786 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Wed, 14 Nov 2012 15:21:44 -0500 Subject: [PATCH 34/37] spamassassin: added 'headers none' option enables suppression of SA header insertion --- plugins/spamassassin | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/plugins/spamassassin b/plugins/spamassassin index 6e81c7e..d3b9710 100644 --- a/plugins/spamassassin +++ b/plugins/spamassassin @@ -18,7 +18,7 @@ These are the common ones: score,required,autolearn,tests,version =head1 CONFIG Configured in the plugins file without any parameters, the -spamassassin plugin will add relevant headers from the spamd +spamassassin plugin will add relevant headers from spamd (X-Spam-Status etc). The format goes like @@ -67,6 +67,11 @@ domain sockets for spamd. This is faster and more secure than using a TCP connection, but if you run spamd on a remote machine, you need to use a TCP connection. +=item headers [none] + +By default, spamassasin headers are added to messages. To suppress header +insertion, use 'headers none'. + =item leave_old_headers [drop|rename|keep] Another mail server before might have checked this mail already and may have @@ -139,6 +144,7 @@ Make the "subject munge string" configurable use strict; use warnings; +use lib 'lib'; use Qpsmtpd::Constants; use Qpsmtpd::DSN; @@ -241,6 +247,12 @@ sub parse_spamd_response { sub insert_spam_headers { my ( $self, $transaction, $new_headers, $username ) = @_; + if ( $self->{_args}{headers} && $self->{_args}{headers} eq 'none' ) { + my $r = $self->parse_spam_header( $new_headers->{'X-Spam-Status'} ); + $transaction->notes('spamassassin', $r); + return; + }; + my $recipient_count = scalar $transaction->recipients; $self->_cleanup_spam_header($transaction, 'X-Spam-User'); # always clean up @@ -410,6 +422,8 @@ sub reject { sub munge_subject { my ($self, $transaction) = @_; + return if ($self->{_args}{headers} && $self->{_args}{headers} eq 'none'); + my $sa = $self->get_spam_results($transaction) or return; my $qp_num = $self->{_args}{munge_subject_threshold}; From 50cb88ba01bb863cd7124f208226dd4d1f7116d4 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Wed, 14 Nov 2012 15:24:06 -0500 Subject: [PATCH 35/37] whitelist: added debug log message & std plugin entries. --- plugins/whitelist | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/whitelist b/plugins/whitelist index 2e0ccb7..43aace4 100644 --- a/plugins/whitelist +++ b/plugins/whitelist @@ -94,6 +94,12 @@ automatically allow relaying from that IP. =cut +use strict; +use warnings; + +use lib 'lib'; +use Qpsmtpd::Constants; + my $VERSION = 0.02; # Default is to merge whitelists in per_recipient mode @@ -144,6 +150,7 @@ sub check_host { return OK; } } + $self->log(LOGDEBUG, "skip: $ip is not whitelisted"); return DECLINED; } From d1bb2d949ba4f570e087941820eb0baddc18ed23 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Wed, 14 Nov 2012 16:58:16 -0500 Subject: [PATCH 36/37] spf: improved support for IPv6 clients --- config.sample/relayclients | 6 ++++++ plugins/sender_permitted_from | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/config.sample/relayclients b/config.sample/relayclients index 792c76b..a0fbc4e 100644 --- a/config.sample/relayclients +++ b/config.sample/relayclients @@ -4,3 +4,9 @@ 127.0.0.1 # leading/trailing whitespace is ignored 192.0. +# +# IPv6 formats ends in a nibble (not a netmask, prefixlen, or colon) +# RFC 3849 example +2001:DB8 +2001:DB8::1 +2001:0DB8:0000:0000:0000:0000:0000:0001 diff --git a/plugins/sender_permitted_from b/plugins/sender_permitted_from index d888701..42f26d8 100644 --- a/plugins/sender_permitted_from +++ b/plugins/sender_permitted_from @@ -59,6 +59,8 @@ use warnings; #use Mail::SPF 2.000; # eval'ed in ->register use Qpsmtpd::Constants; +use Net::IP; + sub register { my ($self, $qp, %args) = @_; eval 'use Mail::SPF'; @@ -237,13 +239,27 @@ sub is_in_relayclients { my $more_relay_clients = $self->qp->config('morerelayclients', 'map'); my %relay_clients = map { $_ => 1 } @relay_clients; + my $ipv6 = $client_ip =~ /:/ ? 1 : 0; + + if ( $ipv6 && $client_ip =~ /::/ ) { # IPv6 compressed notation + $client_ip = Net::IP::ip_expand_address($client_ip,6); + }; + while ($client_ip) { if ( exists $relay_clients{$client_ip} || exists $more_relay_clients->{$client_ip} ) { $self->log( LOGDEBUG, "skip, IP in relayclients" ); return 1; }; - $client_ip =~ s/\d+\.?$// or last; # strip off another 8 bits + + # added IPv6 support (Michael Holzt - 2012-11-14) + if ( $ipv6 ) { + $client_ip =~ s/[0-9a-f]:*$//; # strip off another nibble + chop $client_ip if ':' eq substr($client_ip, -1, 1); + } + else { + $client_ip =~ s/\d+\.?$// or last; # strip off another 8 bits + } } return; }; From 161834f3f355fd7760d72b504b223077fd9d9013 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Wed, 14 Nov 2012 17:15:08 -0500 Subject: [PATCH 37/37] TcpServer, improve IPv6 support, by Michael Holzt --- lib/Qpsmtpd/TcpServer.pm | 2 +- qpsmtpd-forkserver | 4 ---- qpsmtpd-prefork | 4 ---- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/Qpsmtpd/TcpServer.pm b/lib/Qpsmtpd/TcpServer.pm index 42dad62..e4af474 100644 --- a/lib/Qpsmtpd/TcpServer.pm +++ b/lib/Qpsmtpd/TcpServer.pm @@ -14,7 +14,7 @@ if ( # INET6 prior to 2.01 will not work; sorry. eval {require IO::Socket::INET6; IO::Socket::INET6->VERSION("2.00");} ) { - import Socket6; + Socket6->import(qw(inet_ntop)); $has_ipv6=1; } diff --git a/qpsmtpd-forkserver b/qpsmtpd-forkserver index c281a4f..84000f3 100755 --- a/qpsmtpd-forkserver +++ b/qpsmtpd-forkserver @@ -20,10 +20,6 @@ $| = 1; my $has_ipv6 = Qpsmtpd::TcpServer::has_ipv6; -if ($has_ipv6) { - eval 'use Socket6'; -} - # Configuration my $MAXCONN = 15; # max simultaneous connections my @PORT; # port number(s) diff --git a/qpsmtpd-prefork b/qpsmtpd-prefork index c176886..3d018a9 100755 --- a/qpsmtpd-prefork +++ b/qpsmtpd-prefork @@ -31,10 +31,6 @@ defined $Config{sig_name} || die "No signals?"; my $has_ipv6 = Qpsmtpd::TcpServer::has_ipv6; -if ($has_ipv6) { - use Socket6; -} - #use Time::HiRes qw(gettimeofday tv_interval); #get available signals