--- 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] [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] [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 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);