diff --git a/CHANGES b/CHANGES index c260f5b..c41e949 100644 --- a/CHANGES +++ b/CHANGES @@ -4,36 +4,6 @@ # ! = Changed something significant, or removed a feature # * = Fixed a bug, or made a minor improvement ---- 2.011 2018-07-29 - * Added mention of the requirement for Sys::Syslog to INSTALL - * Add references to the current upstream location on Launchpad - + Add option to skip SPF checks on exempt domains based on /etc/postfix/ - exempt_spf_domains (Thanks to Scott Savarese for the patch) - ! Switch relay_addresses to use /etc/postfix/exempt_spf_addresses instead - of hand editing the code (Thanks to Scott Savarese for pushing this over - the finish line) (Debian #902801) - + Add config.sh (see README for details) to support different postfix - configuration directories - * Change domain back to openspf.org, it has been back for a long time - (Debian #900512) - ---- 2.010 2012-06-17 - * Fixed incorrect use of != instead of ne for string comparison - (LP: #1014243) - ! Changed non-standard X-Comment header fields for localhost and - whitelisted addresses to use RFC 5451 Authentication Results header - fields - ! Added depenency on Sys::Hostname::Long for local hostname determination - ---- 2.009 2012-02-03 - * Chomp erroneus NULLs off the end of local and authority explanations to - work around a Mail::SPF bug (in Mail::SPF versions prior to 2.008) - (LP: #806926) - - Patch thanks to Allison Randal - * Stop logging queue ID since it is virtually never available and clutters - the logs - * Reduced non-verbose logging to a single line per message - --- 2.008 (2012-01-19 13:46 -0500) ! Query only TXT and not DNS RR Type SPF records to reduce unnecessary DNS lookups (LP: #161133) @@ -41,6 +11,7 @@ openspf.net instead of openspf.org due to extended outage * Fix incorrect version string * Ensure all variables are initialized prior to being passed to syslog + (LP: #806926) --- 2.007 (2008-07-25 22:24 -0400) * Update documentation and examples, see Debian bugs 492420 and 492421 for diff --git a/INSTALL b/INSTALL index 01911c7..edc3cec 100644 --- a/INSTALL +++ b/INSTALL @@ -7,14 +7,10 @@ postfix-policyd-spf-perl: Perl 5.6 version NetAddr-IP 4 - Mail::SPF (not Mail-SPF-Query) version 2.006 or later - Sys::Hostname::Long - Sys::Syslog + Mail-SPF (not Mail-SPF-Query) version 2.006 or later Installing ---------- - 0. If your postfix config directory is not /etc/postifx, see the README for - additional instructions and adjust the path in step 2 accordingly. 1. Copy postfix-policyd-spf-perl to /usr/local/lib/policyd-spf-perl diff --git a/README b/README index 17c334a..86993a4 100644 --- a/README +++ b/README @@ -1,12 +1,10 @@ -postfix-policyd-spf-perl 2.010 +postfix-policyd-spf-perl 2.008 A Postfix SMTPd policy server for SPF checking (C) 2007-2008,2012 Scott Kitterman -(C) 2012 Allison Randal -(C) 2007 Julian Mehnle -(C) 2003-2004 Meng Weng Wong +(C) 2007 Julian Mehnle +(C) 2003-2004 Meng Weng Wong Thanks for contributions by various members of the SPF project - - + ============================================================================== postfix-policyd-spf-perl is a Postfix SMTPd policy server for SPF checking. @@ -31,26 +29,10 @@ Mail From None even if HELO is Pass). The policy server skips SPF checks for connections from the localhost (127.) and instead prepends and logs 'SPF skipped - localhost is always allowed.' If you -have relays that you want to skip SPF checks for, create a configuration file, -/etc/postfix/exempt_spf_addresses and add them on one using standard CIDR -notation in a space separated list. For these addresses, 'X-Comment: SPF -skipped for whitelisted relay' is prepended and logged. IPv6 localhost is also -skipped. - -A configuration file, /etc/postfix/exempt_spf_domains, can be used to -ignore domains that have broken SPF configurations that would normally -fail. For those domains, add the domain to the file (one per line), and -restart postfix so that the policy server can reload its configuration. -The policy server will ignore the domain going forward. - -If defined, the configuration files described above need to have permissions -to allow the policy server to read the files. - -The standard build for the policy server assumes that the postfix config file -directory is /etc/postfix. If this is not correct for your operating systemn, -run the provided config.sh file from the package directory and it will update -the config file directory based on the output of postconf -h config_directory. -This needs to be done before package installation. +have relays that you want to skip SPF checks for, you can add them to +relay_addresses on line 78 using standard CIDR notation in a space separated +list. For these addresses, 'X-Comment: SPF skipped for whitelisted relay' is +prepended and logged. IPv6 localhost is also skipped. Error conditions within the policy server (that don't result in a crash) or from Mail::SPF will return DUNNO. diff --git a/config.sh b/config.sh deleted file mode 100755 index 9a39b5d..0000000 --- a/config.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -set -e - -CDIR=$(postconf -h config_directory) -echo "Config directory: ${CDIR}" -sed -e 's|@@CDIR@@|'"${CDIR}"'|g' postfix-policyd-spf-perl.in > postfix-policyd-spf-perl diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index 322111e..b36447b 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -1,15 +1,12 @@ #!/usr/bin/perl # postfix-policyd-spf-perl -# https://launchpad.net/postfix-policyd-spf-perl # http://www.openspf.org/Software -# version 2.011 +# version 2.008 # -# (C) 2007-2008,2012,2018 Scott Kitterman -# (C) 2018 Scott Savarese -# (C) 2012 Allison Randal -# (C) 2007 Julian Mehnle -# (C) 2003-2004 Meng Weng Wong +# (C) 2007-2008,2012 Scott Kitterman +# (C) 2007 Julian Mehnle +# (C) 2003-2004 Meng Weng Wong # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,7 +22,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -use version; our $VERSION = qv('2.011'); +use version; our $VERSION = qv('2.008'); use strict; @@ -33,7 +30,6 @@ use IO::Handle; use Sys::Syslog qw(:DEFAULT setlogsock); use NetAddr::IP; use Mail::SPF; -use Sys::Hostname::Long 'hostname_long'; # ---------------------------------------------------------- # configuration @@ -51,7 +47,7 @@ my $spf_server = Mail::SPF::Server->new( dns_resolver => $resolver, query_rr_types => Mail::SPF::Server->query_rr_type_txt, default_authority_explanation => - 'Please see http://www.openspf.org/Why?s=%{_scope};id=%{S};ip=%{C};r=%{R}' + 'Please see http://www.openspf.net/Why?s=%{_scope};id=%{S};ip=%{C};r=%{R}' ); # Adding more handlers is easy: @@ -65,23 +61,15 @@ my @HANDLERS = ( code => \&exempt_relay }, { - name => 'exempt_domains', - code => \&exempt_domains - }, - { name => 'sender_policy_framework', code => \&sender_policy_framework } ); -my $VERBOSE = 0; +my $VERBOSE = 1; my $DEFAULT_RESPONSE = 'DUNNO'; -# Read in exemption lists -my $exempt_domains = get_exempt_domains( "/etc/postfix/exempt_spf_domains" ); -my $relay_addresses = get_exempt_address("/etc/postfix/exempt_spf_addresses"); - # # Syslogging options for verbose mode and for fatal errors. # NOTE: comment out the $syslog_socktype line if syslogging does not @@ -98,9 +86,10 @@ use constant localhost_addresses => map( qw( 127.0.0.0/8 ::ffff:127.0.0.0/104 ::1 ) ); # Does Postfix ever say "client_address=::ffff:"? -# Fully qualified hostname, if available, for use in authentication results -# headers now provided by the localhost and whitelist checks. -my $host = hostname_long; +use constant relay_addresses => map( + NetAddr::IP->new($_), + qw( ) +); # add addresses to qw ( ) above separated by spaces using CIDR notation. my %results_cache; # by message instance @@ -155,7 +144,7 @@ while () { for (sort keys %attr) { syslog(debug => "Attribute: %s=%s", $_ || '', $attr{$_} || ''); } - }; + } my $message_instance = $attr{instance}; my $cache = defined($message_instance) ? $results_cache{$message_instance} ||= {} : {}; @@ -170,69 +159,22 @@ while () { if ($VERBOSE) { syslog(debug => "handler %s: %s", $handler_name || '', $response || ''); - }; + } # Pick whatever response is not 'DUNNO' if ($response and $response !~ /^DUNNO/i) { - if ($VERBOSE) { - syslog(info => "handler %s: is decisive.", $handler_name || ''); - } + syslog(info => "handler %s: is decisive.", $handler_name || ''); $action = $response; last; } } - syslog(info => "Policy action=%s", $action || ''); + syslog(info => "%s: Policy action=%s", $attr{queue_id} || '', $action || ''); STDOUT->print("action=$action\n\n"); %attr = (); } -# ---------------------------------------------------------- -# handler: domain exemption -# ---------------------------------------------------------- - -sub get_exempt_domains { - my ( $file ) = @_; - - my $list = {}; - - # Return nothing if file not found - if ( ! -r $file ) { - return $list; - } - - # Read the file into one variable, split on space or comma (or all) - open ( FILE, $file ) or die "Can't open $file: $!\n"; - my $text = ""; - while ( my $tmp = ) { - $text .= $tmp; - } - close( FILE ); - - foreach my $domain ( split( /[\s,]+/, $text ) ) { - $list->{$domain} = 1; - } - - return $list; -} - -sub exempt_domains { - my %options = @_; - my $attr = $options{attr}; - - my $domain = ( split( /\@/, $attr->{sender} ) )[1]; - return 'DUNNO' if ( ( ! defined( $domain ) ) or ( $domain eq '' ) ); - - # Check the domain against our list of ignored domains - if ( defined( $exempt_domains->{$domain} ) ) { - return "PREPEND Authentication-Results: $host; none " . - "(SPF exempted by policy)"; - } - - return 'DUNNO'; -} - # ---------------------------------------------------------- # handler: localhost exemption # ---------------------------------------------------------- @@ -240,9 +182,9 @@ sub exempt_domains { sub exempt_localhost { my %options = @_; my $attr = $options{attr}; - if ($attr->{client_address} ne '') { + if ($attr->{client_address} != '') { my $client_address = NetAddr::IP->new($attr->{client_address}); - return "PREPEND Authentication-Results: $host; none (SPF not checked for localhost)" + return 'PREPEND X-Comment: SPF not applicable to localhost connection - skipped check' if grep($_->contains($client_address), localhost_addresses); }; return 'DUNNO'; @@ -252,37 +194,13 @@ sub exempt_localhost { # handler: relay exemption # ---------------------------------------------------------- -sub get_exempt_address { - my ( $file ) = @_; - - my $list = []; - - # Return nothing if file not found - if ( ! -r $file ) { - return $list; - } - - # Read the file into one variable, split on space or comma (or all) - open ( FILE, $file ) or die "Can't open $file: $!\n"; - my $text = ""; - while ( my $tmp = ) { - $text .= $tmp; - } - close( FILE ); - - foreach my $addr ( split( /[\s,]+/, $text ) ) { - push( @$list, NetAddr::IP->new($addr) ); - } - return $list; -} - sub exempt_relay { my %options = @_; my $attr = $options{attr}; - if ($attr->{client_address} ne '') { + if ($attr->{client_address} != '') { my $client_address = NetAddr::IP->new($attr->{client_address}); - return "PREPEND Authentication-Results: $host; none (SPF not checked for whitelisted relay)" - if grep($_->contains($client_address), @$relay_addresses); + return 'PREPEND X-Comment: SPF skipped for whitelisted relay' + if grep($_->contains($client_address), relay_addresses); }; return 'DUNNO'; } @@ -319,14 +237,12 @@ sub sender_policy_framework { # probably due to invalid input data! my $errmsg = $@; $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); - if ($VERBOSE) { - syslog( - info => "HELO check failed - Mail::SPF->new(%s, %s, %s) failed: %s", - $attr->{client_address} || '', - $attr->{sender} || '', $attr->{helo_name} || '', - $errmsg || '' - ); - }; + syslog( + info => "%s:HELO check failed - Mail::SPF->new(%s, %s, %s) failed: %s", + $attr->{queue_id} || '', $attr->{client_address} || '', + $attr->{sender} || '', $attr->{helo_name} || '', + $errmsg || '' + ); return; } @@ -334,52 +250,44 @@ sub sender_policy_framework { } my $helo_result_code = $helo_result->code; # 'pass', 'fail', etc. - my $helo_local_exp = nullchomp($helo_result->local_explanation); - my $helo_authority_exp = nullchomp($helo_result->authority_explanation) + my $helo_local_exp = $helo_result->local_explanation; + my $helo_authority_exp = $helo_result->authority_explanation if $helo_result->is_code('fail'); my $helo_spf_header = $helo_result->received_spf_header; if ($VERBOSE) { syslog( - info => "SPF %s: HELO/EHLO: %s, IP Address: %s, Recipient: %s", - $helo_result || '', + info => "%s: SPF %s: HELO/EHLO: %s, IP Address: %s, Recipient: %s", + $attr->{queue_id} || '', $helo_result || '', $attr->{helo_name} || '', $attr->{client_address} || '', $attr->{recipient} || '' ); }; - # Prepend header on HELO fail instead of rejecting. - # Use the HELO result and return for null sender. + # Reject on HELO fail. Defer on HELO temperror if message would otherwise + # be accepted. Use the HELO result and return for null sender. if ($helo_result->is_code('fail')) { - if ($VERBOSE) { - syslog( - info => "SPF %s: HELO/EHLO: %s", - $helo_result || '', - $attr->{helo_name} || '' - ); - }; - return "PREPEND $helo_spf_header" - unless $cache->{added_spf_header}++; + syslog( + info => "%s: SPF %s: HELO/EHLO: %s", + $attr->{queue_id} || '', $helo_result || '', + $attr->{helo_name} || '' + ); + return "550 $helo_authority_exp"; } elsif ($helo_result->is_code('temperror')) { - if ($VERBOSE) { - syslog( - info => "SPF %s: HELO/EHLO: %s", - $helo_result || '', - $attr->{helo_name} || '' - ); - }; - return "PREPEND $helo_spf_header" - unless $cache->{added_spf_header}++; + syslog( + info => "%s: SPF %s: HELO/EHLO: %s", + $attr->{queue_id} || '', $helo_result || '', + $attr->{helo_name} || '' + ); + return "DEFER_IF_PERMIT SPF-Result=$helo_local_exp"; } elsif ($attr->{sender} eq '') { - if ($VERBOSE) { - syslog( - info => "SPF %s: HELO/EHLO (Null Sender): %s", - $helo_result || '', - $attr->{helo_name} || '' - ); - }; + syslog( + info => "%s: SPF %s: HELO/EHLO (Null Sender): %s", + $attr->{queue_id} || '', $helo_result || '', + $attr->{helo_name} || '' + ); return "PREPEND $helo_spf_header" unless $cache->{added_spf_header}++; } @@ -407,13 +315,11 @@ sub sender_policy_framework { # probably due to invalid input data! my $errmsg = $@; $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); - if ($VERBOSE) { - syslog( - info => "Mail From (sender) check failed - Mail::SPF->new(%s, %s, %s) failed: %s", - $attr->{client_address} || '', - $attr->{sender} || '', $attr->{helo_name} || '', $errmsg || '' - ); - }; + syslog( + info => "%s: Mail From (sender) check failed - Mail::SPF->new(%s, %s, %s) failed: %s", + $attr->{queue_id} || '', $attr->{client_address} || '', + $attr->{sender} || '', $attr->{helo_name} || '', $errmsg || '' + ); return; } @@ -421,35 +327,31 @@ sub sender_policy_framework { } my $mfrom_result_code = $mfrom_result->code; # 'pass', 'fail', etc. - my $mfrom_local_exp = nullchomp($mfrom_result->local_explanation); - my $mfrom_authority_exp = nullchomp($mfrom_result->authority_explanation) + my $mfrom_local_exp = $mfrom_result->local_explanation; + my $mfrom_authority_exp = $mfrom_result->authority_explanation if $mfrom_result->is_code('fail'); my $mfrom_spf_header = $mfrom_result->received_spf_header; if ($VERBOSE) { syslog( - info => "SPF %s: Envelope-from: %s, IP Address: %s, Recipient: %s", - $mfrom_result || '', + info => "%s: SPF %s: Envelope-from: %s, IP Address: %s, Recipient: %s", + $attr->{queue_id} || '', $mfrom_result || '', $attr->{sender} || '', $attr->{client_address} || '', $attr->{recipient} || '' ); }; # Same approach as HELO.... - if ($VERBOSE) { - syslog( - info => "SPF %s: Envelope-from: %s", - $mfrom_result || '', - $attr->{sender} || '' - ); - }; + syslog( + info => "%s: SPF %s: Envelope-from: %s", + $attr->{queue_id} || '', $mfrom_result || '', + $attr->{sender} || '' + ); if ($mfrom_result->is_code('fail')) { - return "PREPEND $mfrom_spf_header" - unless $cache->{added_spf_header}++; + return "550 $mfrom_authority_exp"; } elsif ($mfrom_result->is_code('temperror')) { - return "PREPEND $mfrom_spf_header" - unless $cache->{added_spf_header}++; + return "DEFER_IF_PERMIT SPF-Result=$mfrom_local_exp"; } else { return "PREPEND $mfrom_spf_header" @@ -458,16 +360,3 @@ sub sender_policy_framework { return; } - -# ---------------------------------------------------------- -# utility, string cleaning -# ---------------------------------------------------------- - -sub nullchomp { - my $value = shift; - - # Remove one or more null characters from the - # end of the input. - $value =~ s/\0+$//; - return $value; -} diff --git a/postfix-policyd-spf-perl.in b/postfix-policyd-spf-perl.in deleted file mode 100644 index a626c89..0000000 --- a/postfix-policyd-spf-perl.in +++ /dev/null @@ -1,473 +0,0 @@ -#!/usr/bin/perl - -# postfix-policyd-spf-perl -# https://launchpad.net/postfix-policyd-spf-perl -# http://www.openspf.org/Software -# version 2.011 -# -# (C) 2007-2008,2012,2018 Scott Kitterman -# (C) 2018 Scott Savarese -# (C) 2012 Allison Randal -# (C) 2007 Julian Mehnle -# (C) 2003-2004 Meng Weng Wong -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -use version; our $VERSION = qv('2.011'); - -use strict; - -use IO::Handle; -use Sys::Syslog qw(:DEFAULT setlogsock); -use NetAddr::IP; -use Mail::SPF; -use Sys::Hostname::Long 'hostname_long'; - -# ---------------------------------------------------------- -# configuration -# ---------------------------------------------------------- - -my $resolver = Net::DNS::Resolver->new( - retrans => 5, # Net::DNS::Resolver default: 5 - retry => 2, # Net::DNS::Resolver default: 4 - # Makes for a total timeout for UDP queries of 5s * 2 = 10s. -); - -# query_rr_type_all will query both type TXT and type SPF. This upstream -# default is changed due to there being essentiall no type SPF deployment. -my $spf_server = Mail::SPF::Server->new( - dns_resolver => $resolver, - query_rr_types => Mail::SPF::Server->query_rr_type_txt, - default_authority_explanation => - 'Please see http://www.openspf.org/Why?s=%{_scope};id=%{S};ip=%{C};r=%{R}' -); - -# Adding more handlers is easy: -my @HANDLERS = ( - { - name => 'exempt_localhost', - code => \&exempt_localhost - }, - { - name => 'exempt_relay', - code => \&exempt_relay - }, - { - name => 'exempt_domains', - code => \&exempt_domains - }, - { - name => 'sender_policy_framework', - code => \&sender_policy_framework - } -); - -my $VERBOSE = 0; - -my $DEFAULT_RESPONSE = 'DUNNO'; - -# Read in exemption lists -my $exempt_domains = get_exempt_domains( "@@CDIR@@/exempt_spf_domains" ); -my $relay_addresses = get_exempt_address("@@CDIR@@/exempt_spf_addresses"); - -# -# Syslogging options for verbose mode and for fatal errors. -# NOTE: comment out the $syslog_socktype line if syslogging does not -# work on your system. -# - -my $syslog_socktype = 'unix'; # inet, unix, stream, console -my $syslog_facility = 'mail'; -my $syslog_options = 'pid'; -my $syslog_ident = 'postfix/policy-spf'; - -use constant localhost_addresses => map( - NetAddr::IP->new($_), - qw( 127.0.0.0/8 ::ffff:127.0.0.0/104 ::1 ) -); # Does Postfix ever say "client_address=::ffff:"? - -# Fully qualified hostname, if available, for use in authentication results -# headers now provided by the localhost and whitelist checks. -my $host = hostname_long; - -my %results_cache; # by message instance - -# ---------------------------------------------------------- -# initialization -# ---------------------------------------------------------- - -# -# Log an error and abort. -# -sub fatal_exit { - syslog(err => "fatal_exit: @_"); - syslog(warning => "fatal_exit: @_"); - syslog(info => "fatal_exit: @_"); - die("fatal: @_"); -} - -# -# Unbuffer standard output. -# -STDOUT->autoflush(1); - -# -# This process runs as a daemon, so it can't log to a terminal. Use -# syslog so that people can actually see our messages. -# -setlogsock($syslog_socktype); -openlog($syslog_ident, $syslog_options, $syslog_facility); - -# ---------------------------------------------------------- -# main -# ---------------------------------------------------------- - -# -# Receive a bunch of attributes, evaluate the policy, send the result. -# -my %attr; -while () { - chomp; - - if (/=/) { - my ($key, $value) =split (/=/, $_, 2); - $attr{$key} = $value; - next; - } - elsif (length) { - syslog(warning => sprintf("warning: ignoring garbage: %.100s", $_)); - next; - } - - if ($VERBOSE) { - for (sort keys %attr) { - syslog(debug => "Attribute: %s=%s", $_ || '', $attr{$_} || ''); - } - }; - - my $message_instance = $attr{instance}; - my $cache = defined($message_instance) ? $results_cache{$message_instance} ||= {} : {}; - - my $action = $DEFAULT_RESPONSE; - - foreach my $handler (@HANDLERS) { - my $handler_name = $handler->{name}; - my $handler_code = $handler->{code}; - - my $response = $handler_code->(attr => \%attr, cache => $cache); - - if ($VERBOSE) { - syslog(debug => "handler %s: %s", $handler_name || '', $response || ''); - }; - - # Pick whatever response is not 'DUNNO' - if ($response and $response !~ /^DUNNO/i) { - if ($VERBOSE) { - syslog(info => "handler %s: is decisive.", $handler_name || ''); - } - $action = $response; - last; - } - } - - syslog(info => "Policy action=%s", $action || ''); - - STDOUT->print("action=$action\n\n"); - %attr = (); -} - -# ---------------------------------------------------------- -# handler: domain exemption -# ---------------------------------------------------------- - -sub get_exempt_domains { - my ( $file ) = @_; - - my $list = {}; - - # Return nothing if file not found - if ( ! -r $file ) { - return $list; - } - - # Read the file into one variable, split on space or comma (or all) - open ( FILE, $file ) or die "Can't open $file: $!\n"; - my $text = ""; - while ( my $tmp = ) { - $text .= $tmp; - } - close( FILE ); - - foreach my $domain ( split( /[\s,]+/, $text ) ) { - $list->{$domain} = 1; - } - - return $list; -} - -sub exempt_domains { - my %options = @_; - my $attr = $options{attr}; - - my $domain = ( split( /\@/, $attr->{sender} ) )[1]; - return 'DUNNO' if ( ( ! defined( $domain ) ) or ( $domain eq '' ) ); - - # Check the domain against our list of ignored domains - if ( defined( $exempt_domains->{$domain} ) ) { - return "PREPEND Authentication-Results: $host; none " . - "(SPF exempted by policy)"; - } - - return 'DUNNO'; -} - -# ---------------------------------------------------------- -# handler: localhost exemption -# ---------------------------------------------------------- - -sub exempt_localhost { - my %options = @_; - my $attr = $options{attr}; - if ($attr->{client_address} ne '') { - my $client_address = NetAddr::IP->new($attr->{client_address}); - return "PREPEND Authentication-Results: $host; none (SPF not checked for localhost)" - if grep($_->contains($client_address), localhost_addresses); - }; - return 'DUNNO'; -} - -# ---------------------------------------------------------- -# handler: relay exemption -# ---------------------------------------------------------- - -sub get_exempt_address { - my ( $file ) = @_; - - my $list = []; - - # Return nothing if file not found - if ( ! -r $file ) { - return $list; - } - - # Read the file into one variable, split on space or comma (or all) - open ( FILE, $file ) or die "Can't open $file: $!\n"; - my $text = ""; - while ( my $tmp = ) { - $text .= $tmp; - } - close( FILE ); - - foreach my $addr ( split( /[\s,]+/, $text ) ) { - push( @$list, NetAddr::IP->new($addr) ); - } - return $list; -} - -sub exempt_relay { - my %options = @_; - my $attr = $options{attr}; - if ($attr->{client_address} ne '') { - my $client_address = NetAddr::IP->new($attr->{client_address}); - return "PREPEND Authentication-Results: $host; none (SPF not checked for whitelisted relay)" - if grep($_->contains($client_address), @$relay_addresses); - }; - return 'DUNNO'; -} - -# ---------------------------------------------------------- -# handler: SPF -# ---------------------------------------------------------- - -sub sender_policy_framework { - my %options = @_; - my $attr = $options{attr}; - my $cache = $options{cache}; - - # ------------------------------------------------------------------------- - # Always do HELO check first. If no HELO policy, it's only one lookup. - # This avoids the need to do any MAIL FROM processing for null sender. - # ------------------------------------------------------------------------- - - my $helo_result = $cache->{helo_result}; - - if (not defined($helo_result)) { - # No HELO result has been cached from earlier checks on this message. - - my $helo_request = eval { - Mail::SPF::Request->new( - scope => 'helo', - identity => $attr->{helo_name}, - ip_address => $attr->{client_address} - ); - }; - - if ($@) { - # An unexpected error occurred during request creation, - # probably due to invalid input data! - my $errmsg = $@; - $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); - if ($VERBOSE) { - syslog( - info => "HELO check failed - Mail::SPF->new(%s, %s, %s) failed: %s", - $attr->{client_address} || '', - $attr->{sender} || '', $attr->{helo_name} || '', - $errmsg || '' - ); - }; - return; - } - - $helo_result = $cache->{helo_result} = $spf_server->process($helo_request); - } - - my $helo_result_code = $helo_result->code; # 'pass', 'fail', etc. - my $helo_local_exp = nullchomp($helo_result->local_explanation); - my $helo_authority_exp = nullchomp($helo_result->authority_explanation) - if $helo_result->is_code('fail'); - my $helo_spf_header = $helo_result->received_spf_header; - - if ($VERBOSE) { - syslog( - info => "SPF %s: HELO/EHLO: %s, IP Address: %s, Recipient: %s", - $helo_result || '', - $attr->{helo_name} || '', $attr->{client_address} || '', - $attr->{recipient} || '' - ); - }; - - # Prepend header on HELO fail instead of rejecting. - # Use the HELO result and return for null sender. - if ($helo_result->is_code('fail')) { - if ($VERBOSE) { - syslog( - info => "SPF %s: HELO/EHLO: %s", - $helo_result || '', - $attr->{helo_name} || '' - ); - }; - return "PREPEND $helo_spf_header" - unless $cache->{added_spf_header}++; - } - elsif ($helo_result->is_code('temperror')) { - if ($VERBOSE) { - syslog( - info => "SPF %s: HELO/EHLO: %s", - $helo_result || '', - $attr->{helo_name} || '' - ); - }; - return "PREPEND $helo_spf_header" - unless $cache->{added_spf_header}++; - } - elsif ($attr->{sender} eq '') { - if ($VERBOSE) { - syslog( - info => "SPF %s: HELO/EHLO (Null Sender): %s", - $helo_result || '', - $attr->{helo_name} || '' - ); - }; - return "PREPEND $helo_spf_header" - unless $cache->{added_spf_header}++; - } - - # ------------------------------------------------------------------------- - # Do MAIL FROM check (as HELO did not give a definitive result) - # ------------------------------------------------------------------------- - - my $mfrom_result = $cache->{mfrom_result}; - - if (not defined($mfrom_result)) { - # No MAIL FROM result has been cached from earlier checks on this message. - - my $mfrom_request = eval { - Mail::SPF::Request->new( - scope => 'mfrom', - identity => $attr->{sender}, - ip_address => $attr->{client_address}, - helo_identity => $attr->{helo_name} # for %{h} macro expansion - ); - }; - - if ($@) { - # An unexpected error occurred during request creation, - # probably due to invalid input data! - my $errmsg = $@; - $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); - if ($VERBOSE) { - syslog( - info => "Mail From (sender) check failed - Mail::SPF->new(%s, %s, %s) failed: %s", - $attr->{client_address} || '', - $attr->{sender} || '', $attr->{helo_name} || '', $errmsg || '' - ); - }; - return; - } - - $mfrom_result = $cache->{mfrom_result} = $spf_server->process($mfrom_request); - } - - my $mfrom_result_code = $mfrom_result->code; # 'pass', 'fail', etc. - my $mfrom_local_exp = nullchomp($mfrom_result->local_explanation); - my $mfrom_authority_exp = nullchomp($mfrom_result->authority_explanation) - if $mfrom_result->is_code('fail'); - my $mfrom_spf_header = $mfrom_result->received_spf_header; - - if ($VERBOSE) { - syslog( - info => "SPF %s: Envelope-from: %s, IP Address: %s, Recipient: %s", - $mfrom_result || '', - $attr->{sender} || '', $attr->{client_address} || '', - $attr->{recipient} || '' - ); - }; - - # Same approach as HELO.... - if ($VERBOSE) { - syslog( - info => "SPF %s: Envelope-from: %s", - $mfrom_result || '', - $attr->{sender} || '' - ); - }; - if ($mfrom_result->is_code('fail')) { - return "PREPEND $mfrom_spf_header" - unless $cache->{added_spf_header}++; - } - elsif ($mfrom_result->is_code('temperror')) { - return "PREPEND $mfrom_spf_header" - unless $cache->{added_spf_header}++; - } - else { - return "PREPEND $mfrom_spf_header" - unless $cache->{added_spf_header}++; - } - - return; -} - -# ---------------------------------------------------------- -# utility, string cleaning -# ---------------------------------------------------------- - -sub nullchomp { - my $value = shift; - - # Remove one or more null characters from the - # end of the input. - $value =~ s/\0+$//; - return $value; -} diff --git a/test_cases b/test_cases new file mode 100644 index 0000000..cae5156 --- /dev/null +++ b/test_cases @@ -0,0 +1,147 @@ +#HELO and mfrom pass +request=smtpd_access_policy +client_address=72.81.252.18 +helo_name=mailout02.controlledmail.com +sender=scott@kitterman.com +recipient=bogus@kitterman.org +queue_id=q1234 +instance=1 + +#HELO fail and mfrom pass +request=smtpd_access_policy +client_address=72.81.252.18 +helo_name=mailout00.controlledmail.com +sender=scott@kitterman.com +recipient=bogus@kitterman.org +queue_id=q1234 +instance=2 + +#no HELO and mfrom pass +request=smtpd_access_policy +client_address=72.81.252.18 +helo_name=72.81.252.18 +sender=scott@kitterman.com +recipient=bogus@kitterman.org +queue_id=q1234 +instance=3 + +#helo pass and mfrom pass +request=smtpd_access_policy +client_address=72.81.252.19 +helo_name=mailout00.controlledmail.com +sender=scott@kitterman.org +recipient=bogus@kitterman.org +queue_id=q1234 +instance=4 + +#helo pass and mfrom none +request=smtpd_access_policy +client_address=72.81.252.18 +helo_name=mailout02.controlledmail.com +sender=scott@yahoo.com +recipient=bogus@kitterman.org +queue_id=q1234 +instance=5 + +#helo pass and mfrom null +request=smtpd_access_policy +client_address=72.81.252.18 +helo_name=mailout02.controlledmail.com +sender= +recipient=bogus@kitterman.org +queue_id=q1234 +instance=6 + +#helo fail and mfrom null +request=smtpd_access_policy +client_address=72.81.252.19 +helo_name=mailout02.controlledmail.com +sender= +recipient=bogus@kitterman.org +queue_id=q1234 +instance=7 + +#Multi-recipient dunno +request=smtpd_access_policy +client_address=72.81.252.18 +helo_name=mailout03.controlledmail.com +sender=scott@kitterman.com +recipient=bogus2@kitterman.org +queue_id=q1234 +instance=3 + +#localhost bypass +request=smtpd_access_policy +client_address=127.0.0.1 +helo_name=mailout03.controlledmail.com +sender=scott@kitterman.com +recipient=bogus2@kitterman.org +queue_id=q1234 +instance=12 + +#Helo Neutral and mfrom pass +request=smtpd_access_policy +client_address=72.81.252.18 +helo_name=aol.com +sender=scott@kitterman.com +recipient=bogus2@kitterman.org +queue_id=q1234 +instance=13 + +#Whitelist +request=smtpd_access_policy +client_address=192.168.0.1 +helo_name=mailout03.controlledmail.com +sender=scott@kitterman.com +recipient=bogus2@kitterman.org +queue_id=q1234 +instance=14 + +# multi-recipient mfrom fail +request=smtpd_access_policy +client_address=72.81.252.19 +helo_name=mailout00.controlledmail.com +sender=scott@kitterman.org +recipient=bogus2@kitterman.org +queue_id=q1234 +instance=4 + +# multi-recipient HELO fail and mfrom pass +request=smtpd_access_policy +client_address=72.81.252.18 +helo_name=mailout00.controlledmail.com +sender=scott@kitterman.com +recipient=bogus2@kitterman.org +queue_id=q1234 +instance=2 + +# No SPF +request=smtpd_access_policy +client_address=72.81.252.18 +helo_name=mailout00.yahoo.com +sender=scott@yahoo.com +recipient=bogus2@kitterman.org +queue_id=q1234 +instance=15 + +# Permerror reject +request=smtpd_access_policy +client_address=72.81.252.18 +helo_name=elvey.com +sender=scott@elvey.com +recipient=bogus2@kitterman.org +queue_id=q1234 +instance=16 + +# None and None +request=smtpd_access_policy +client_address=71.17.127.27 +helo_name=71-17-127-27.estv.hsdb.sasknet.sk.ca +sender=dalbecbhoj@accessamericatransport.com +recipient=hostmaster@jamux.com + +request=smtpd_access_policy +client_address=200.120.31.84 +helo_name=autohaus-knabe.de +sender=daniel.hahnomjy@autohaus-knabe.de +recipient=jam@jamux.com \ No newline at end of file