[PATCH] merged check_badmailfrom_patterns into check_badmailfrom

[email protected] (Matt Simerson)
Newsgroups perl.qpsmtpd
Message-ID <[email protected]>
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 <[email protected]>

-  return (DECLINED) unless ($sender->format ne "<>"
-                            and $sender->host && $sender->user);
+merging of the two and plugin tests - Matt Simerson <[email protected]>

-  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 <[email protected]> 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 = '[email protected]';
+    my $address = Qpsmtpd::Address->new( "<$test_email>" );
+    $transaction->sender($address);
+
+    $self->{_badmailfrom_config} = ['[email protected]','[email protected]'];
+    $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} = ['[email protected]','[email protected] 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( '[email protected]', '[email protected]', 'test.net' );
+    ok($r, "check_badmailfrom match");
+    
+    ok( ! $self->is_match( '[email protected]', '[email protected]', 'tnpi.net' ),
+        "check_badmailfrom non-match");
+
+    ok( $self->is_match( '[email protected]', '@test.net', 'test.net' ),
+        "check_badmailfrom match host");
+
+    ok( ! $self->is_match( '[email protected]', '@test.not', 'test.net' ),
+        "check_badmailfrom non-match host");
+
+    ok( ! $self->is_match( '[email protected]', '@test.net', 'test.not' ),
+        "check_badmailfrom non-match host");
+
+    ok( $self->is_match( '[email protected]', 'test.net$', 'tnpi.net' ),
+        "check_badmailfrom pattern match");
+
+    ok( ! $self->is_match( '[email protected]', 'test.not$', 'tnpi.net' ),
+        "check_badmailfrom pattern non-match");
+};
+
+
-- 
1.7.9.6
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.