From 92dc6cbd4ff72ad8bde5f9216e2c219be8f2c954 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Tue, 10 Jan 2017 17:19:17 -0500 Subject: [PATCH 01/13] * Added mention of the requirement for Sys::Syslog to INSTALL * Add references to the current upstream location on Launchpad --- CHANGES | 4 ++++ INSTALL | 1 + README | 1 + postfix-policyd-spf-perl | 5 +++-- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index a4262b4..1fe6493 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,10 @@ # ! = Changed something significant, or removed a feature # * = Fixed a bug, or made a minor improvement +--- 2.011 UNRELEASED + * Added mention of the requirement for Sys::Syslog to INSTALL + * Add references to the current upstream location on Launchpad + --- 2.010 2012-06-17 * Fixed incorrect use of != instead of ne for string comparison (LP: #1014243) diff --git a/INSTALL b/INSTALL index 3e13787..7912567 100644 --- a/INSTALL +++ b/INSTALL @@ -9,6 +9,7 @@ postfix-policyd-spf-perl: NetAddr-IP 4 Mail::SPF (not Mail-SPF-Query) version 2.006 or later Sys::Hostname::Long + Sys::Syslog Installing ---------- diff --git a/README b/README index 9d8fc13..85adea8 100644 --- a/README +++ b/README @@ -6,6 +6,7 @@ A Postfix SMTPd policy server for SPF checking (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. diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index 06c9a51..18941b4 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -1,8 +1,9 @@ #!/usr/bin/perl # postfix-policyd-spf-perl +# https://launchpad.net/postfix-policyd-spf-perl # http://www.openspf.org/Software -# version 2.010 +# version 2.011 # # (C) 2007-2008,2012 Scott Kitterman # (C) 2012 Allison Randal @@ -23,7 +24,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.010'); +use version; our $VERSION = qv('2.011'); use strict; From b916c542c6c963cbea49473de3f74a7ecfdb0d8e Mon Sep 17 00:00:00 2001 From: Scott Savarese Date: Thu, 26 Jul 2018 00:42:40 -0400 Subject: [PATCH 02/13] Add option to skip SPF checks on exempt domains based on /etc/postfix/exempt_spf_domains --- README | 6 +++++ postfix-policyd-spf-perl | 51 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/README b/README index 85adea8..ecd954b 100644 --- a/README +++ b/README @@ -36,6 +36,12 @@ 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. +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. + Error conditions within the policy server (that don't result in a crash) or from Mail::SPF will return DUNNO. diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index 18941b4..b5f36a9 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -64,6 +64,10 @@ my @HANDLERS = ( code => \&exempt_relay }, { + name => 'exempt_domains', + code => \&exempt_domains + }, + { name => 'sender_policy_framework', code => \&sender_policy_framework } @@ -73,6 +77,9 @@ my $VERBOSE = 0; my $DEFAULT_RESPONSE = 'DUNNO'; +# Read in exempt domains list +my $exempt_domains = get_exempt_domains( "/etc/postfix/exempt_spf_domains" ); + # # Syslogging options for verbose mode and for fatal errors. # NOTE: comment out the $syslog_socktype line if syslogging does not @@ -184,6 +191,50 @@ while () { %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 # ---------------------------------------------------------- From 341d0a775d0ba0c3533d2e139f50ff975afbd114 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Thu, 26 Jul 2018 00:51:58 -0400 Subject: [PATCH 03/13] CHANGES and titivation of last commit --- CHANGES | 2 ++ postfix-policyd-spf-perl | 13 +++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index 1fe6493..e777128 100644 --- a/CHANGES +++ b/CHANGES @@ -7,6 +7,8 @@ --- 2.011 UNRELEASED * 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 --- 2.010 2012-06-17 * Fixed incorrect use of != instead of ne for string comparison diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index b5f36a9..3491f00 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -5,10 +5,11 @@ # http://www.openspf.org/Software # version 2.011 # -# (C) 2007-2008,2012 Scott Kitterman -# (C) 2012 Allison Randal -# (C) 2007 Julian Mehnle -# (C) 2003-2004 Meng Weng Wong +# (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 @@ -64,8 +65,8 @@ my @HANDLERS = ( code => \&exempt_relay }, { - name => 'exempt_domains', - code => \&exempt_domains + name => 'exempt_domains', + code => \&exempt_domains }, { name => 'sender_policy_framework', From 652e07ced7cfbd8bc895384bdd8dacabeb25d2b6 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Thu, 26 Jul 2018 03:06:18 -0400 Subject: [PATCH 04/13] Non-working attempt at config file for relay exemption --- postfix-policyd-spf-perl | 41 ++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index 3491f00..0e2da49 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -78,8 +78,9 @@ my $VERBOSE = 0; my $DEFAULT_RESPONSE = 'DUNNO'; -# Read in exempt domains list +# 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. @@ -97,11 +98,6 @@ 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:"? -use constant relay_addresses => map( - NetAddr::IP->new($_), - qw( ) -); # add addresses to qw ( ) above separated by spaces using CIDR notation. - # Fully qualified hostname, if available, for use in authentication results # headers now provided by the localhost and whitelist checks. my $host = hostname_long; @@ -195,6 +191,7 @@ while () { # ---------------------------------------------------------- # handler: domain exemption # ---------------------------------------------------------- + sub get_exempt_domains { my ( $file ) = @_; @@ -255,6 +252,38 @@ 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 ); + + #$list => map( + # NetAddr::IP->new($_), + # qw( $text ) + #); # add addresses to qw ( ) above separated by spaces using CIDR notation. + + foreach my $addr ( split( /[\s,]+/, $text ) ) { + map( + NetAddr::IP->new($list), + qw( $addr ) + ); + } + return $list; +} + sub exempt_relay { my %options = @_; my $attr = $options{attr}; From eae14d029dd0712d71664c39f664d316667a2dc7 Mon Sep 17 00:00:00 2001 From: Scott Savarese Date: Sun, 29 Jul 2018 13:02:49 -0400 Subject: [PATCH 05/13] Fix switch to use /etc/postfix/exempt_spf_addresses for skipped relays --- postfix-policyd-spf-perl | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index 0e2da49..cb1c47f 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -255,7 +255,7 @@ sub exempt_localhost { sub get_exempt_address { my ( $file ) = @_; - my $list = {}; + my $list = []; # Return nothing if file not found if ( ! -r $file ) { @@ -270,16 +270,8 @@ sub get_exempt_address { } close( FILE ); - #$list => map( - # NetAddr::IP->new($_), - # qw( $text ) - #); # add addresses to qw ( ) above separated by spaces using CIDR notation. - foreach my $addr ( split( /[\s,]+/, $text ) ) { - map( - NetAddr::IP->new($list), - qw( $addr ) - ); + push( @$list, NetAddr::IP->new($addr) ); } return $list; } @@ -290,7 +282,7 @@ sub exempt_relay { 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); + if grep($_->contains($client_address), @$relay_addresses); }; return 'DUNNO'; } From 8530b85957f921fe3f9c3f9edafd4ba21ad47dfd Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Sun, 29 Jul 2018 13:15:42 -0400 Subject: [PATCH 06/13] Add CHANGES for exempt_spf_addresses changes --- CHANGES | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index e777128..e8d31e0 100644 --- a/CHANGES +++ b/CHANGES @@ -8,7 +8,10 @@ * 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 + 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) --- 2.010 2012-06-17 * Fixed incorrect use of != instead of ne for string comparison From 5d8ffda27bd8d523adb0871936901f882140cbe8 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Sun, 29 Jul 2018 14:57:27 -0400 Subject: [PATCH 07/13] Add config.sh and update README/INSTALL so we can configure the script for non-standard configuration directories --- INSTALL | 2 + README | 15 +- config.sh | 7 + postfix-policyd-spf-perl | 0 postfix-policyd-spf-perl.in | 469 ++++++++++++++++++++++++++++++++++++ 5 files changed, 489 insertions(+), 4 deletions(-) create mode 100755 config.sh mode change 100755 => 100644 postfix-policyd-spf-perl create mode 100755 postfix-policyd-spf-perl.in diff --git a/INSTALL b/INSTALL index 7912567..01911c7 100644 --- a/INSTALL +++ b/INSTALL @@ -13,6 +13,8 @@ postfix-policyd-spf-perl: 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 ecd954b..1e039fc 100644 --- a/README +++ b/README @@ -31,10 +31,11 @@ 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, 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. +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 @@ -42,6 +43,12 @@ 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. +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. + 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 new file mode 100755 index 0000000..9a39b5d --- /dev/null +++ b/config.sh @@ -0,0 +1,7 @@ +#!/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 old mode 100755 new mode 100644 diff --git a/postfix-policyd-spf-perl.in b/postfix-policyd-spf-perl.in new file mode 100755 index 0000000..60df32e --- /dev/null +++ b/postfix-policyd-spf-perl.in @@ -0,0 +1,469 @@ +#!/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.net/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} || '' + ); + }; + + # 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 "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 "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} || '' + ); + }; + 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 "550 $mfrom_authority_exp"; + } + elsif ($mfrom_result->is_code('temperror')) { + return "DEFER_IF_PERMIT SPF-Result=$mfrom_local_exp"; + } + 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; +} From 570e9836d71f0b9801d601bdb4e5a04af5b83e83 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Sun, 29 Jul 2018 15:00:03 -0400 Subject: [PATCH 08/13] Fixup CHANGES for release --- CHANGES | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/CHANGES b/CHANGES index e8d31e0..bb7dc04 100644 --- a/CHANGES +++ b/CHANGES @@ -4,22 +4,24 @@ # ! = Changed something significant, or removed a feature # * = Fixed a bug, or made a minor improvement ---- 2.011 UNRELEASED - * 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) +--- 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) + + Add config.sh (see README for details) to support different postfix + configuration directories --- 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 + * 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 From ad8e2849cb5ec066d94f28cd3addff4b9a7cc87a Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Sun, 29 Jul 2018 15:00:57 -0400 Subject: [PATCH 09/13] Fixup execture permissions --- postfix-policyd-spf-perl | 0 postfix-policyd-spf-perl.in | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 postfix-policyd-spf-perl mode change 100755 => 100644 postfix-policyd-spf-perl.in diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl old mode 100644 new mode 100755 diff --git a/postfix-policyd-spf-perl.in b/postfix-policyd-spf-perl.in old mode 100755 new mode 100644 From 151caf449bda9973b88da74403ec98d8230a2d87 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Sun, 29 Jul 2018 15:03:39 -0400 Subject: [PATCH 10/13] More CHANGES --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index bb7dc04..b1cc6d9 100644 --- a/CHANGES +++ b/CHANGES @@ -11,7 +11,7 @@ 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) + the finish line) (Debian #902801) + Add config.sh (see README for details) to support different postfix configuration directories From ce6fbdf659e04b61446cc872f75b867bc34794f4 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Sun, 29 Jul 2018 15:10:04 -0400 Subject: [PATCH 11/13] * Change domain back to openspf.org, it has been back for a long time (Debian #900512) --- CHANGES | 2 ++ README | 2 +- postfix-policyd-spf-perl | 2 +- postfix-policyd-spf-perl.in | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index b1cc6d9..c260f5b 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,8 @@ 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 diff --git a/README b/README index 1e039fc..8c7f3f6 100644 --- a/README +++ b/README @@ -5,7 +5,7 @@ A Postfix SMTPd policy server for SPF checking (C) 2007 Julian Mehnle (C) 2003-2004 Meng Weng Wong Thanks for contributions by various members of the SPF project - + ============================================================================== diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index cb1c47f..29d5f3f 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -51,7 +51,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.net/Why?s=%{_scope};id=%{S};ip=%{C};r=%{R}' + 'Please see http://www.openspf.org/Why?s=%{_scope};id=%{S};ip=%{C};r=%{R}' ); # Adding more handlers is easy: diff --git a/postfix-policyd-spf-perl.in b/postfix-policyd-spf-perl.in index 60df32e..83dbf78 100644 --- a/postfix-policyd-spf-perl.in +++ b/postfix-policyd-spf-perl.in @@ -51,7 +51,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.net/Why?s=%{_scope};id=%{S};ip=%{C};r=%{R}' + 'Please see http://www.openspf.org/Why?s=%{_scope};id=%{S};ip=%{C};r=%{R}' ); # Adding more handlers is easy: From c9757b4635c856c09436948765fcd5119324e34f Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Sun, 29 Jul 2018 15:17:23 -0400 Subject: [PATCH 12/13] Mention file permissions in README --- README | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README b/README index 8c7f3f6..17c334a 100644 --- a/README +++ b/README @@ -43,6 +43,9 @@ 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 From 5db125ae861f2e8455d417460eb3473ca810815f Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 15 Oct 2025 21:15:05 +0700 Subject: [PATCH 13/13] The script now operates in monitoring/observation mode, where SPF results are recorded in headers but never cause email rejection or deferral. --- postfix-policyd-spf-perl | 16 ++++++++++------ postfix-policyd-spf-perl.in | 16 ++++++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index 29d5f3f..322111e 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -348,8 +348,8 @@ sub sender_policy_framework { ); }; - # Reject on HELO fail. Defer on HELO temperror if message would otherwise - # be accepted. Use the HELO result and return for null sender. + # 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( @@ -358,7 +358,8 @@ sub sender_policy_framework { $attr->{helo_name} || '' ); }; - return "550 $helo_authority_exp"; + return "PREPEND $helo_spf_header" + unless $cache->{added_spf_header}++; } elsif ($helo_result->is_code('temperror')) { if ($VERBOSE) { @@ -368,7 +369,8 @@ sub sender_policy_framework { $attr->{helo_name} || '' ); }; - return "DEFER_IF_PERMIT SPF-Result=$helo_local_exp"; + return "PREPEND $helo_spf_header" + unless $cache->{added_spf_header}++; } elsif ($attr->{sender} eq '') { if ($VERBOSE) { @@ -442,10 +444,12 @@ sub sender_policy_framework { ); }; if ($mfrom_result->is_code('fail')) { - return "550 $mfrom_authority_exp"; + return "PREPEND $mfrom_spf_header" + unless $cache->{added_spf_header}++; } elsif ($mfrom_result->is_code('temperror')) { - return "DEFER_IF_PERMIT SPF-Result=$mfrom_local_exp"; + return "PREPEND $mfrom_spf_header" + unless $cache->{added_spf_header}++; } else { return "PREPEND $mfrom_spf_header" diff --git a/postfix-policyd-spf-perl.in b/postfix-policyd-spf-perl.in index 83dbf78..a626c89 100644 --- a/postfix-policyd-spf-perl.in +++ b/postfix-policyd-spf-perl.in @@ -348,8 +348,8 @@ sub sender_policy_framework { ); }; - # Reject on HELO fail. Defer on HELO temperror if message would otherwise - # be accepted. Use the HELO result and return for null sender. + # 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( @@ -358,7 +358,8 @@ sub sender_policy_framework { $attr->{helo_name} || '' ); }; - return "550 $helo_authority_exp"; + return "PREPEND $helo_spf_header" + unless $cache->{added_spf_header}++; } elsif ($helo_result->is_code('temperror')) { if ($VERBOSE) { @@ -368,7 +369,8 @@ sub sender_policy_framework { $attr->{helo_name} || '' ); }; - return "DEFER_IF_PERMIT SPF-Result=$helo_local_exp"; + return "PREPEND $helo_spf_header" + unless $cache->{added_spf_header}++; } elsif ($attr->{sender} eq '') { if ($VERBOSE) { @@ -442,10 +444,12 @@ sub sender_policy_framework { ); }; if ($mfrom_result->is_code('fail')) { - return "550 $mfrom_authority_exp"; + return "PREPEND $mfrom_spf_header" + unless $cache->{added_spf_header}++; } elsif ($mfrom_result->is_code('temperror')) { - return "DEFER_IF_PERMIT SPF-Result=$mfrom_local_exp"; + return "PREPEND $mfrom_spf_header" + unless $cache->{added_spf_header}++; } else { return "PREPEND $mfrom_spf_header"