merged check_badmailfrom_patterns into check_badmailfrom 90% of the code was identical.
refactored it a bit to facilitate testing. added tests. --- plugins/check_badmailfrom | 104 ++++++++++++++++++++++++++---------- plugins/check_badmailfrom_patterns | 62 --------------------- t/plugin_tests/check_badmailfrom | 80 +++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 91 deletions(-) delete mode 100644 plugins/check_badmailfrom_patterns create mode 100644 t/plugin_tests/check_badmailfrom diff --git a/plugins/check_badmailfrom b/plugins/check_badmailfrom index 20c7679..6968bbe 100644 --- a/plugins/check_badmailfrom +++ b/plugins/check_badmailfrom @@ -9,53 +9,99 @@ check_badmailfrom - checks the badmailfrom config, with per-line reasons Reads the "badmailfrom" configuration like qmail-smtpd does. From the qmail-smtpd docs: -"Unacceptable envelope sender addresses. qmail-smtpd will reject every +"Unacceptable envelope sender addresses. qmail-smtpd will reject every recipient address for a message if the envelope sender address is -listed in badmailfrom. A line in badmailfrom may be of the form +listed in badmailfrom. A line in badmailfrom may be of the form @host, meaning every address at host." -You may optionally include a message after the sender address (leave a space), -which is used when rejecting the sender. +You may include an optional message after the sender address (leave a space), +to be used when rejecting the sender. + + +=head1 PATTERNS + +This plugin also supports regular expression matches. This allows +special patterns to be denied (e.g. FQDN-VERP, percent hack, bangs, +double ats). + +Patterns are stored in the format pattern(\s+)response, where pattern +is a Perl pattern expression. Don't forget to anchor the pattern +(front ^ and back $) if you want to restrict it from matching +anywhere in the string. + + ^streamsendbouncer@.*\.mailengine1\.com$ Your right-hand side VERP doesn't fool me + ^return.*@.*\.pidplate\.biz$ I don' want it regardless of subdomain + ^admin.*\.ppoonn400\.com$ + =head1 NOTES According to the SMTP protocol, we can't reject until after the RCPT stage, so store it until later. -=cut -# TODO: add the ability to provide a custom default rejection reason +=head1 AUTHORS -sub hook_mail { - my ($self, $transaction, $sender, %param) = @_; +initial author of badmailfrom - Jim Winstead - my @badmailfrom = $self->qp->config("badmailfrom") - or return (DECLINED); +pattern matching plugin - Johan Almqvist <johan-qpsm...@almqvist.net> - return (DECLINED) unless ($sender->format ne "<>" - and $sender->host && $sender->user); +merging of the two and plugin tests - Matt Simerson <m...@tnpi.net> - my $host = lc $sender->host; - my $from = lc($sender->user) . '@' . $host; +=cut - for my $config (@badmailfrom) { - my ($bad, $reason) = $config =~ /^\s*(\S+)(?:\s*(.*))?$/; - $reason = "sorry, your envelope sender is in my badmailfrom list" unless $reason; - next unless $bad; - $bad = lc $bad; - $self->log(LOGWARN, "Bad badmailfrom config: No \@ sign in $bad") and next unless $bad =~ m/\@/; - $transaction->notes('badmailfrom', $reason) - if ($bad eq $from) || (substr($bad,0,1) eq '@' && $bad eq "\@$host"); - } - return (DECLINED); +sub hook_mail { + my ($self, $transaction, $sender, %param) = @_; + + my @badmailfrom = $self->qp->config('badmailfrom'); + if ( defined $self->{_badmailfrom_config} ) { # testing + @badmailfrom = @{$self->{_badmailfrom_config}}; + }; + + return DECLINED if ! scalar @badmailfrom; + return DECLINED if $sender->format eq '<>'; + return DECLINED if ! $sender->host || ! $sender->user; + + my $host = lc $sender->host; + my $from = lc($sender->user) . '@' . $host; + + for my $config (@badmailfrom) { + $config =~ s/^\s+//g; # trim any leading whitespace + my ($bad, $reason) = split /\s+/, $config, 2; + next unless $bad; + next unless $self->is_match( $from, $bad, $host ); + $reason ||= "Your envelope sender is in my badmailfrom list"; + $transaction->notes('badmailfrom', $reason); + } + return DECLINED; } +sub is_match { + my ( $self, $from, $bad, $host ) = @_; + + if ( $bad =~ /[\/\^\$\*\+]/ ) { # it's a regexp + $self->log(LOGDEBUG, "badmailfrom pattern ($bad) match for $from"); + return 1 if $from =~ /$bad/; + return; + }; + + $bad = lc $bad; + if ( $bad !~ m/\@/ ) { + $self->log(LOGWARN, "badmailfrom: bad config: no \@ sign in $bad"); + return; + }; + if ( substr($bad,0,1) eq '@' ) { + return 1 if $bad eq "\@$host"; + return; + }; + return if $bad ne $from; + return 1; +}; + sub hook_rcpt { - my ($self, $transaction, $rcpt, %param) = @_; - my $note = $transaction->notes('badmailfrom'); - if ($note) { + my ($self, $transaction, $rcpt, %param) = @_; + my $note = $transaction->notes('badmailfrom') or return (DECLINED); + $self->log(LOGINFO, $note); return (DENY, $note); - } - return (DECLINED); } diff --git a/plugins/check_badmailfrom_patterns b/plugins/check_badmailfrom_patterns deleted file mode 100644 index 2def1a4..0000000 --- a/plugins/check_badmailfrom_patterns +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/perl - -=head1 SYNOPSIS - -This plugin checks the badmailfrom_patterns config. This allows -special patterns to be denied (e.g. FQDN-VERP, percent hack, bangs, -double ats). - -=head1 CONFIG - -Configuration is placed in the following file: - -F<config/badmailfrom_patterns> - -Patterns are stored in the format pattern\sresponse, where pattern -is a Perl pattern expression. Don't forget to anchor the pattern -(front ^ and back $) if you want to restrict it from matching -anywhere in the string. - - ^streamsendbouncer@.*\.mailengine1\.com$ Your right-hand side VERP doesn't fool me - ^return.*@.*\.pidplate\.biz$ I don' want it regardless of subdomain - ^admin.*\.ppoonn400\.com$ - -=head1 AUTHOR - -Johan Almqvist <johan-qpsm...@almqvist.net> based on L<check_badmailfrom> - -This software is free software and may be distributed under the same -terms as qpsmtpd itself. - -=cut - -sub hook_mail { - my ($self, $transaction, $sender, %param) = @_; - - my @badmailfrom = $self->qp->config("badmailfrom_patterns") - or return (DECLINED); - - return (DECLINED) if ($sender->format eq "<>"); - - my $host = lc $sender->host; - my $from = lc($sender->user) . '@' . $host; - - for (@badmailfrom) { - my ($pattern, $response) = split /\s+/, $_, 2; - next unless $from =~ /$pattern/; - $response = "Your envelope sender is in my badmailfrom_patterns list" - unless $response; - $transaction->notes('badmailfrom_patterns', $response); - } - return (DECLINED); -} - -sub hook_rcpt { - my ($self, $transaction, $rcpt, %param) = @_; - my $note = $transaction->notes('badmailfrom_patterns'); - if ($note) { - $self->log(LOGINFO, $note); - return (DENY, $note); - } - return (DECLINED); -} diff --git a/t/plugin_tests/check_badmailfrom b/t/plugin_tests/check_badmailfrom new file mode 100644 index 0000000..7d447ae --- /dev/null +++ b/t/plugin_tests/check_badmailfrom @@ -0,0 +1,80 @@ + +use strict; +use Data::Dumper; + +use Qpsmtpd::Address; + +sub register_tests { + my $self = shift; + + $self->register_test("test_badmailfrom_match", 1); + $self->register_test("test_badmailfrom_hook_mail", 1); + $self->register_test("test_badmailfrom_hook_rcpt", 1); +} + +sub test_badmailfrom_hook_mail { + my $self = shift; + + my $transaction = $self->qp->transaction; + + my $test_email = 'm...@test.com'; + my $address = Qpsmtpd::Address->new( "<$test_email>" ); + $transaction->sender($address); + + $self->{_badmailfrom_config} = ['m...@test.net','m...@test.com']; + $transaction->notes('badmailfrom', ''); + my ($r) = $self->hook_mail( $transaction, $address ); + ok( $r == 909, "badmailfrom hook_mail"); + ok( $transaction->notes('badmailfrom') eq 'Your envelope sender is in my badmailfrom list', + "badmailfrom hook_mail: default reason"); + + $self->{_badmailfrom_config} = ['m...@test.net','m...@test.com Yer a spammin bastert']; + $transaction->notes('badmailfrom', ''); + my ($r) = $self->hook_mail( $transaction, $address ); + ok( $r == 909, "badmailfrom hook_mail"); + ok( $transaction->notes('badmailfrom') eq 'Yer a spammin bastert', + "badmailfrom hook_mail: custom reason"); + +}; + +sub test_badmailfrom_hook_rcpt { + my $self = shift; + + my $transaction = $self->qp->transaction; + + $transaction->notes('badmailfrom', 'Yer a spammin bastart. Be gon wit yuh.' ); + + my ($code,$note) = $self->hook_rcpt( $transaction ); + + ok( $code == 901, 'badmailfrom hook hit'); + ok( $note, $note ); +} + +sub test_badmailfrom_match { + my $self = shift; + +# is_match receives ( $from, $bad, $host ) + + my $r = $self->is_match( 'm...@test.net', 'm...@test.net', 'test.net' ); + ok($r, "check_badmailfrom match"); + + ok( ! $self->is_match( 'm...@test.net', 'm...@test.com', 'tnpi.net' ), + "check_badmailfrom non-match"); + + ok( $self->is_match( 'm...@test.net', '@test.net', 'test.net' ), + "check_badmailfrom match host"); + + ok( ! $self->is_match( 'm...@test.net', '@test.not', 'test.net' ), + "check_badmailfrom non-match host"); + + ok( ! $self->is_match( 'm...@test.net', '@test.net', 'test.not' ), + "check_badmailfrom non-match host"); + + ok( $self->is_match( 'm...@test.net', 'test.net$', 'tnpi.net' ), + "check_badmailfrom pattern match"); + + ok( ! $self->is_match( 'm...@test.net', 'test.not$', 'tnpi.net' ), + "check_badmailfrom pattern non-match"); +}; + + -- 1.7.9.6