--- spf-milter	2004-04-30 11:06:20.000000000 +0100
+++ spf-milter	2004-05-04 17:35:09.000000000 +0100
@@ -89,6 +89,7 @@
 
 use POSIX qw (:sys_wait_h);
 use Sendmail::Milter;
+use Sendmail::AccessDB;
 use Socket;
 use Net::CIDR;
 use Mail::SPF::Query;
@@ -100,7 +101,7 @@
 require 5.8.0;
 use strict;
 
-use vars qw/$opt_k $opt_l $opt_t $opt_m $opt_S $opt_r $opt_h $opt_v $opt_T/;
+use vars qw/$opt_A $opt_k $opt_l $opt_D $opt_t $opt_m $opt_S $opt_r $opt_h $opt_v $opt_T/;
 
 my $pidFile = $basedir . '/spf-milter.pid';
 my $sock = $basedir . '/spf-milter.sock';
@@ -109,6 +110,10 @@
 my @extraParams : shared = ();
 
 my $mx_mode : shared = 0;
+my $delay_checks : shared = 0;
+my $delay_checks_friend_mode : shared = 0;
+my $delay_checks_hater_mode : shared = 0;
+my $access_db_whitelisting : shared = 0;
 my $our_hostname : shared = 0;
 my $trust : shared = 1;
 my $require_srs_dsn : shared = 0;
@@ -285,20 +290,24 @@
     my $ctx = shift;
     my $priv_data = $ctx -> getpriv ();
     $priv_data->{'helo'} = shift;
+    $priv_data->{'is_authenticated'} = 0;
+    $priv_data->{'is_whitelisted'} = 0;
+    $priv_data->{'mechanism'} = undef;
 
     # We first allow a bypass for STARTTLS authenticated users.
-    # In all other cases, where $use_whitelist indicates the use of a
-    # whitelist, we set 'is_authenticated' to the "eval" of the
-    # CIDR lookup in our IP whitelist. Else, we reset to 0;
+    #
+    # Then, where $use_whitelist indicates the use of a
+    # whitelist, we set 'is_whitelisted' to the "eval" of the
+    # CIDR lookup in our IP whitelist.
     #
     # @cidr_list is a global array, read from "$basedir/whitelist".
 
     if ($ctx -> getsymval ('{verify}') eq 'OK') {
         $priv_data->{'is_authenticated'} = 1;
-    } elsif (not $use_whitelist) {
-        $priv_data->{'is_authenticated'} = 0;
-    } else {
-        $priv_data->{'is_authenticated'} = eval {Net::CIDR::cidrlookup ($priv_data->{'ipaddr'}, @cidr_list)};
+        $priv_data->{'mechanism'}        = "STARTTLS";
+    } elsif ($use_whitelist && eval {Net::CIDR::cidrlookup ($priv_data->{'ipaddr'}, @cidr_list)}) {
+        $priv_data->{'is_whitelisted'} = 1;
+        $priv_data->{'mechanism'}      = "SPF-milter whitelist entry";
     }
 
     $ctx -> setpriv ($priv_data);
@@ -324,7 +333,10 @@
     # Are we authenticated via SASL? Do not set if we're
     # already STARTTLS authenticated (or whitelisted!).
 
-    $priv_data->{'is_authenticated'} ||= $ctx -> getsymval ('{auth_authen}');
+    if (not ($priv_data->{'is_authenticated'} || $priv_data->{'is_whitelisted'}) && $ctx -> getsymval ('{auth_authen}')) {
+        $priv_data->{'is_authenticated'} = 1;
+        $priv_data->{'mechanism'}        = "SASL";
+    }
 
     # envfrom_callback can be called more than once within the same connection;
     # delete $priv_data->{'spf_result'} on entry!
@@ -335,7 +347,14 @@
 
     if ($priv_data->{'is_authenticated'}) {
         $priv_data->{'spf_result'} = "pass";
-        $priv_data->{'spf_header_comment'} = "$our_hostname: $priv_data->{'ipaddr'} is authenticated by a trusted mechanism";
+        $priv_data->{'spf_header_comment'} =
+            "$our_hostname: $priv_data->{'ipaddr'} is authenticated by $priv_data->{'mechanism'}";
+        $ctx -> setpriv ($priv_data);
+        return SMFIS_CONTINUE;
+    } elsif ($priv_data->{'is_whitelisted'}) {
+        $priv_data->{'spf_result'} = "pass";
+        $priv_data->{'spf_header_comment'} =
+            "$our_hostname: $priv_data->{'ipaddr'} is whitelisted by $priv_data->{'mechanism'}";
         $ctx -> setpriv ($priv_data);
         return SMFIS_CONTINUE;
     }
@@ -349,10 +368,10 @@
         return SMFIS_REJECT;
     }
 
-    # Did we start in "mx" mode? If so, we will delay SPF checks until
-    # envrcpt_callback.
+    # Are we doing delayed checks for recipient-based checking?
+    # If so, we will delay SPF checks until envrcpt_callback.
 
-    return SMFIS_CONTINUE if ($mx_mode);
+    return SMFIS_CONTINUE if ($delay_checks);
 
     # Make the SPF query, and immediately store the result in our private hash;
     # we may also need it later, at eom_callback.
@@ -368,8 +387,14 @@
                 return SMFIS_REJECT;
             }
         } elsif ($priv_data->{'spf_result'} eq 'error') {
-            $ctx -> setreply ('451', '4.7.1', "$priv_data->{'spf_smtp_comment'}");
-            return SMFIS_TEMPFAIL;
+            if ($tagOnly) {
+                write_log ("SPF \"error\" from ip = ".$priv_data->{'ipaddr'}.
+                           " helo = ".$priv_data->{'helo'}.
+                           " from = ".$priv_data->{'from'});
+            } else {
+                $ctx -> setreply ('451', '4.7.1', "$priv_data->{'spf_smtp_comment'}");
+                return SMFIS_TEMPFAIL;
+            }
         }
     }
 
@@ -380,7 +405,7 @@
 sub envrcpt_callback : locked {
     my $ctx = shift;
     my $priv_data = $ctx -> getpriv ();
-    my ($envelope_to, $reversed_recipient);
+    my ($envelope_to, $reversed_recipient, $access_db_lookup, $rcpt_domain, $rcpt_tag_only);
 
     # Keep the old recipient too, exactly as it appeared
     # in the SMTP dialoge!
@@ -540,14 +565,14 @@
 
     $ctx -> setpriv ($priv_data);
 
-    # We're done if we're already authenticated.
+    # We're done if we're already authenticated or whitelisted.
 
-    return SMFIS_CONTINUE if ($priv_data->{'is_authenticated'});
+    return SMFIS_CONTINUE if ($priv_data->{'is_authenticated'} || $priv_data->{'is_whitelisted'});
 
-    # Here we do the opposite check of envfrom_callback: if not "mx" mode,
-    # we bale rightaway.
+    # Here we do the opposite check of envfrom_callback: if not delayed checks mode,
+    # we bale right away.
 
-    return SMFIS_CONTINUE if (not $mx_mode);
+    return SMFIS_CONTINUE if (not $delay_checks);
 
     # We also need to purge $priv_data->{'spf_result'} for each recipient!
 
@@ -555,20 +580,79 @@
 
     $ctx -> setpriv ($priv_data);
 
+    # Per-recipient checks:
+    # If access.db check for recipient is `OK', treat as `whitelisted'
+    # Else if access.db check for recipient is `TAG', treat as `tag only'
+    # Else if delay_checks in `friend' mode and the recipient is a spam friend
+    #      or delay_checks in `hater' mode and the recipient is not a spam hater
+    #      then treat as `tag only'
+
+    # access.db whitelisting format (checked in order):
+    # SPF:[ip.add.re.ss]user@dom.ain	OK|TAG	whitelist mail for user@dom.ain from ip.add.re.ss
+    # SPF:[ip.add.re.ss]dom.ain		OK|TAG	whitelist mail for anyone at dom.ain from ip.add.re.ss
+    # SPF:user@dom.ain			OK|TAG	whitelist mail for user@dom.ain from anywhere
+    # SPF:dom.ain			OK|TAG	whitelist mail for dom.ain from anywhere
+
+    $rcpt_tag_only = $tagOnly;
+    if ($access_db_whitelisting) {
+        ($rcpt_domain = $priv_data->{'to'}) =~ s/.*@//g;
+        if (eval {$access_db_lookup = Sendmail::AccessDB::lookup( "SPF:[".$priv_data->{'ipaddr'}."]".$priv_data->{'to'})} ||
+            eval {$access_db_lookup = Sendmail::AccessDB::lookup( "SPF:[".$priv_data->{'ipaddr'}."]".$rcpt_domain)}       ||
+            eval {$access_db_lookup = Sendmail::AccessDB::lookup( "SPF:".$priv_data->{'to'})}                             ||
+            eval {$access_db_lookup = Sendmail::AccessDB::lookup( "SPF:".$rcpt_domain)}) {
+            if ($access_db_lookup eq 'OK') {
+                $priv_data->{'spf_result'} = "pass";
+                $priv_data->{'spf_header_comment'} =
+                    "$our_hostname: $priv_data->{'ipaddr'} is whitelisted by access.db entry for $priv_data->{'to'}";
+                $ctx -> setpriv ($priv_data);
+                return SMFIS_CONTINUE;
+            } elsif ($access_db_lookup eq 'TAG') {
+                $rcpt_tag_only = 1;
+            } else {
+                write_log ("Unsupported access.db entry for ip = ".$priv_data->{'ipaddr'}.
+                           " rcpt = ".$priv_data->{'to'}.
+                           " access_db_lookup = ".$access_db_lookup);
+            }
+        }
+    }
+
     if ($priv_data->{'spf_result'} = socket_call (2, $priv_data, $priv_data->{'ipaddr'}, $priv_data->{'from'}, $priv_data->{'helo'}, $priv_data->{'to'})) {
         if ($priv_data->{'spf_result'} eq 'fail') {
-            if ($tagOnly) {
+            if ($delay_checks_friend_mode || $delay_checks_hater_mode) {
+                eval {$access_db_lookup = Sendmail::AccessDB::spam_friend ($priv_data->{'to'})};
+            }
+            if ($rcpt_tag_only ||
+                ($delay_checks_friend_mode && ($access_db_lookup eq 'FRIEND')) ||
+                ($delay_checks_hater_mode  && not ($access_db_lookup eq 'HATER'))
+               ) {
+                $access_db_lookup ||= 'undef';
                 write_log ("SPF \"fail\" from ip = ".$priv_data->{'ipaddr'}.
                            " helo = ".$priv_data->{'helo'}.
                            " from = ".$priv_data->{'from'}.
-                           " to = ".$priv_data->{'to'});
+                           " to = ".$priv_data->{'to'}.
+                           " friend_or_hater = ".$access_db_lookup);
             } else {
                 $ctx -> setreply ('550', '5.7.1', "[RCPT TO: <$priv_data->{'to'}>] $priv_data->{'spf_smtp_comment'}");
                 return SMFIS_REJECT;
             }
         } elsif ($priv_data->{'spf_result'} eq 'error') {
-            $ctx -> setreply ('451', '4.7.1', "[RCPT TO: <$priv_data->{'to'}>] $priv_data->{'spf_smtp_comment'}");
-            return SMFIS_TEMPFAIL;
+            if ($delay_checks_friend_mode || $delay_checks_hater_mode) {
+                eval {$access_db_lookup = Sendmail::AccessDB::spam_friend ($priv_data->{'to'})};
+            }
+            if ($rcpt_tag_only ||
+                ($delay_checks_friend_mode && ($access_db_lookup eq 'FRIEND')) ||
+                ($delay_checks_hater_mode  && not ($access_db_lookup eq 'HATER'))
+               ) {
+                $access_db_lookup ||= 'undef';
+                write_log ("SPF \"error\" from ip = ".$priv_data->{'ipaddr'}.
+                           " helo = ".$priv_data->{'helo'}.
+                           " from = ".$priv_data->{'from'}.
+                           " to = ".$priv_data->{'to'}.
+                           " friend_or_hater = ".$access_db_lookup);
+            } else {
+                $ctx -> setreply ('451', '4.7.1', "[RCPT TO: <$priv_data->{'to'}>] $priv_data->{'spf_smtp_comment'}");
+                return SMFIS_TEMPFAIL;
+            }
         }
     }
 
@@ -670,20 +754,25 @@
 # (or third, if you run in "mx" mode). You need at least Mail::SPF::Query 1.99
 # for this functionality!
 
-getopts("kl:tmSrhvT");
+getopts("kl:D:tmSrhvTA");
 
 sub usage {
     my ($ret) = @_;
-    print STDERR "Usage: $0 [-k] [-l local_trust] [-t] [-m] [-S] [-r] [-h] <user> [mx] [dt]\n";
+    print STDERR "Usage: $0 -h\n";
+    print STDERR "       $0 -k\n";
+    print STDERR "       $0 -v\n";
+    print STDERR "       $0 [-A] [-D friend|hater] [-l local_trust] [-m] [-r] [-S] [-T] [-t] <user> [mx] [dt]\n";
+    print STDERR "        -h        print this help message\n";
     print STDERR "        -k        kill running milter\n";
+    print STDERR "        -v        display version info\n";
+    print STDERR "        -A        support access.db whitelisting\n";
+    print STDERR "        -D        support spam friends/haters from access.db\n";
     print STDERR "        -l        add local trust record\n";
-    print STDERR "        -t        don't add trusted-forwarder.org record\n";
     print STDERR "        -m        trust recipient's MX hosts\n";
-    print STDERR "        -S        only allow SRS signed bounces (see documentation!)\n";
     print STDERR "        -r        will relay SRS1\n";
+    print STDERR "        -S        only allow SRS signed bounces (see documentation!)\n";
     print STDERR "        -T        don't reject failed messages, tag only\n";
-    print STDERR "        -h        print this help message\n";
-    print STDERR "        -v        display version info\n";
+    print STDERR "        -t        don't add trusted-forwarder.org record\n";
     print STDERR "        <user>    user to run this script as\n";
     print STDERR "        mx        trust recipient's MX hosts (same as -m)\n";
     print STDERR "        dt        don't add trusted-forwarder.org (same as -t)\n";
@@ -793,10 +882,20 @@
     push (@extraParams, local => $opt_l);
 }
 
-if ($opt_T) {
-    $tagOnly = 1;
+if ($opt_D) {
+    if (lc ($opt_D) eq 'friend') {
+        $delay_checks_friend_mode = 1;
+    } elsif (lc ($opt_D) eq 'hater') {
+        $delay_checks_hater_mode = 1;
+    } else {
+        print STDERR "-D option requires argument `friend' or `hater'\n";
+        exit 1;
+    }
 }
 
+$access_db_whitelisting = 1 if ($opt_A);
+$delay_checks = 1 if ($delay_checks_friend_mode || $delay_checks_hater_mode || $mx_mode || $access_db_whitelisting);
+$tagOnly = 1 if ($opt_T);
 $require_srs_dsn = 1 if ($opt_S);
 $will_relay_srs1 = 1 if ($opt_r);
 
