Before moving to qpsmtpd I was using the (now-defunct) greylite
package. This package had two good features that I miss in qpsmtpd:

1. Tracking by IP/sender/recip but subsequent whitelisting by IP only.

What this means is that when we first hear from a server, we expect it
to retry the same message (all parts of connection triple). Once the
server has jumped through this hoop, however, we can assume it will do
the same for any subsequent pair of sender and recipient. Further
greylisting mail at this point will only annoy other local users who
must wait for the inevitable black_timeout to pass before their mail
gets through. I have added a new parameter (ip_only_whitelist) that
implements the greylite model for this (default off). 

2. Database Cleanup

As bots spam us, we will tend to get more and more useless entries
that bog down the system and take up filesystem space. Every 800 times
through (on average) greylite would make a pass through its database
to remove stale keys. I have added this functionality to the plugin,
basing the "staleness" on the grey_timeout for non-whitelisted entries
and the white_timeout for the rest. In my code the default is set to
800 (like greylite).

3. Logging

For those of us using the syslog logging the log lines were set much
too high, with regular messages being logged at CRIT. I have
rationalized that.

Let me know if there's anything else you need to commit this patch.

Cheers,
Colin













--- greylisting.dist    2010-09-09 08:40:21.000000000 -0400
+++ greylisting         2010-09-09 14:02:58.000000000 -0400
@@ -43,6 +43,13 @@
 
 Whether to include the recipient in tracking connections. Default: 0.
 
+=item ip_only_whitelist <bool>
+
+Once a server has proven itself using the connection elements you have 
selected 
+(remote_ip, sender and/or recipient), further messages from that server will 
be 
+accepted immediately (whitelisted) on the basis of remote_ip alone. (The 
white_timeout
+parameter is still honoured.)
+
 =item deny_late <bool>
 
 Whether to defer denials during the 'mail' hook until 'data_post'
@@ -78,6 +85,14 @@
 greylisting off globally if using per_recipient configs). 
 Default: denysoft.
 
+=item cleanup <interval>
+
+Every <interval> recipients processed (on average) the database 
+will be cleaned. Connection triplets older than grey_timeout or 
+white_timeout (as appropriate) will be deleted to keep the database 
+from growing forever. Default: 800. 
+(To disable cleanup, specify an interval of 0.)
+
 =item db_dir <path>
 
 Path to a directory in which the greylisting DB will be stored.  This
@@ -129,7 +144,7 @@
 my ($QPHOME) = ($0 =~ m!(.*?)/([^/]+)$!);
 my $DB = "denysoft_greylist.dbm";
 my %PERMITTED_ARGS = map { $_ => 1 } qw(per_recipient remote_ip sender 
recipient 
-  black_timeout grey_timeout white_timeout deny_late mode db_dir);
+  black_timeout grey_timeout white_timeout deny_late mode db_dir 
ip_only_whitelist cleanup);
 
 my %DEFAULTS = (
   remote_ip => 1,
@@ -139,6 +154,8 @@
   grey_timeout =>  3 * 3600 + 20 * 60,
   white_timeout => 36 * 24 * 3600,
   mode => 'denysoft',
+  ip_only_whitelist => 0,
+  cleanup => 800,
 );
 
 sub register {
@@ -147,7 +164,7 @@
     map { split /\s+/, $_, 2 } $self->qp->config('denysoft_greylist'), 
     %arg };
   if (my @bad = grep { ! exists $PERMITTED_ARGS{$_} } sort keys %$config) {
-    $self->log(LOGALERT, "invalid parameter(s): " . join(',',@bad));
+    $self->log(LOGERROR, "invalid parameter(s): " . join(',',@bad));
   }
   $self->{_greylist_config} = $config;
   unless ($config->{recipient} || $config->{per_recipient}) {
@@ -190,7 +207,7 @@
   return DECLINED unless $note;
   # Decline if ALL recipients are whitelisted
   if (($transaction->notes('whitelistrcpt')||0) == 
scalar($transaction->recipients)) {
-    $self->log(LOGWARN,"all recipients whitelisted - skipping");
+    $self->log(LOGINFO,"all recipients whitelisted - skipping");
     return DECLINED;
   }
   return DENYSOFT, $note;
@@ -218,75 +235,100 @@
     last if $dbdir ||= $d && -d $d && $d;
   }
   my $db = "$dbdir/$DB";
-  $self->log(LOGINFO,"using $db as greylisting database");
+  $self->log(LOGDEBUG,"using $db as greylisting database");
 
   my $remote_ip = $self->qp->connection->remote_ip;
   my $fmt = "%s:%d:%d:%d";
 
   # Check denysoft db
   unless (open LOCK, ">$db.lock") {
-    $self->log(LOGCRIT, "opening lockfile failed: $!");
+    $self->log(LOGERROR, "opening lockfile failed: $!");
     return DECLINED;
   }
   unless (flock LOCK, LOCK_EX) {
-    $self->log(LOGCRIT, "flock of lockfile failed: $!");
+    $self->log(LOGERROR, "flock of lockfile failed: $!");
     close LOCK;
     return DECLINED;
   }
   my %db = ();
   unless (tie %db, 'AnyDBM_File', $db, O_CREAT|O_RDWR, 0600) {
-    $self->log(LOGCRIT, "tie to database $db failed: $!");
+    $self->log(LOGERROR, "tie to database $db failed: $!");
     close LOCK;
     return DECLINED;
   }
+  if ($config->{cleanup} && ! int(rand $config->{cleanup})) {
+    # every $config->{cleanup} times (on average) clean out stale DB entries
+    my $now = time;
+    while (my ($key,$val) = each %db) {
+      my ($ts, $new, $black, $white) = split /:/, $db{$key};
+      if (!$white && (time - $ts > $config->{grey_timeout})) {
+        $self->log(LOGDEBUG,"Cleaning up expired black or grey key $key (ts: " 
. localtime($ts) . ", now: " . localtime($now).")");
+        delete $db{$key};
+      }
+      if ($white && (time - $ts > $config->{white_timeout})) {
+        $self->log(LOGDEBUG,"Cleaning up expired white key $key (ts: " . 
localtime($ts) . ", now: " . localtime($now).")");
+        delete $db{$key};
+      }
+    }
+  }
   my @key;
   push @key, $remote_ip             if $config->{remote_ip};
   push @key, $sender->address || '' if $config->{sender};
   push @key, $rcpt->address         if $rcpt && $config->{recipient};
-  my $key = join ':', @key;
+  my $fullkey = join ':', @key;
+  my $ipowl_key = $remote_ip if $config->{ip_only_whitelist};
+  my $key = ($ipowl_key && $db{$ipowl_key})?$ipowl_key:$fullkey;
   my ($ts, $new, $black, $white) = (0,0,0,0);
   if ($db{$key}) {
     ($ts, $new, $black, $white) = split /:/, $db{$key};
-    $self->log(LOGERROR, "ts: " . localtime($ts) . ", now: " . localtime);
+    $self->log(LOGDEBUG, "ts: " . localtime($ts) . ", now: " . localtime);
     if (! $white) {
       # Black IP - deny, but don't update timestamp
       if (time - $ts < $config->{black_timeout}) {
         $db{$key} = sprintf $fmt, $ts, $new, ++$black, 0;
-        $self->log(LOGCRIT, "key $key black DENYSOFT - $black failed 
connections");
+        $self->log(LOGINFO, "key $key black DENYSOFT - $black failed 
connections");
         untie %db;
         close LOCK;
         return $config->{mode} eq 'testonly' ? DECLINED : DENYSOFT, $DENYMSG;
       }
       # Grey IP - accept unless timed out
       elsif (time - $ts < $config->{grey_timeout}) {
-        $db{$key} = sprintf $fmt, time, $new, $black, 1;
-        $self->log(LOGCRIT, "key $key updated grey->white");
+        if ($ipowl_key) {
+          # whitelist based on IP address only. Tidy up full key.
+          delete $db{$fullkey};
+          $db{$ipowl_key} = sprintf $fmt, time, $new, $black, 1;
+          $self->log(LOGINFO, "host $remote_ip updated grey->white (matched 
$key)");
+        } else {
+          $db{$key} = sprintf $fmt, time, $new, $black, 1;
+          $self->log(LOGINFO, "key $key updated grey->white");
+        }
         untie %db;
         close LOCK;
         return DECLINED;
       }
       else {
-        $self->log(LOGERROR, "key $key has timed out (grey)");
+        $self->log(LOGINFO, "key $key has timed out (grey)");
       }
     }
     # White IP - accept unless timed out
     else {
       if (time - $ts < $config->{white_timeout}) {
         $db{$key} = sprintf $fmt, time, $new, $black, ++$white;
-        $self->log(LOGCRIT, "key $key is white, $white deliveries");
+        $self->log(LOGINFO, "key $key is white, $white deliveries");
         untie %db;
         close LOCK;
         return DECLINED;
       }
       else {
-        $self->log(LOGERROR, "key $key has timed out (white)");
+        $self->log(LOGINFO, "key $key has timed out (white)");
       }
     }
   }
 
   # New ip or entry timed out - record new and return DENYSOFT
-  $db{$key} = sprintf $fmt, time, ++$new, $black, 0;
-  $self->log(LOGCRIT, "key $key initial DENYSOFT, unknown");
+  delete $db{$ipowl_key} if $ipowl_key;
+  $db{$fullkey} = sprintf $fmt, time, ++$new, $black, 0;
+  $self->log(LOGINFO, "key $key initial DENYSOFT, unknown");
   untie %db;
   close LOCK;
   return $config->{mode} eq 'testonly' ? DECLINED : DENYSOFT, $DENYMSG;

Reply via email to