From 0632517790a1afcb5cd7a098c1dfe59d3a7df068 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Thu, 19 Jan 2012 15:08:55 -0500 Subject: [PATCH 01/24] Urgh! Set verbose back to 0 - cowboyed into 2.008 too. --- postfix-policyd-spf-perl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index b36447b..d464ddd 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -66,7 +66,7 @@ my @HANDLERS = ( } ); -my $VERBOSE = 1; +my $VERBOSE = 0; my $DEFAULT_RESPONSE = 'DUNNO'; From 101de98f4f2e6f599be8e5fb1330a079311c9412 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Wed, 25 Jan 2012 22:44:27 -0500 Subject: [PATCH 02/24] * Bump versions to start version 2.009. * Set verbose for the development period again. --- CHANGES | 1 + README | 2 +- postfix-policyd-spf-perl | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index c41e949..d5269bb 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,7 @@ # ! = Changed something significant, or removed a feature # * = Fixed a bug, or made a minor improvement +--- 2.009 UNRELEASED --- 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) diff --git a/README b/README index 86993a4..0f60ae9 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -postfix-policyd-spf-perl 2.008 +postfix-policyd-spf-perl 2.009 A Postfix SMTPd policy server for SPF checking (C) 2007-2008,2012 Scott Kitterman (C) 2007 Julian Mehnle diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index d464ddd..6b43403 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -2,7 +2,7 @@ # postfix-policyd-spf-perl # http://www.openspf.org/Software -# version 2.008 +# version 2.009 # # (C) 2007-2008,2012 Scott Kitterman # (C) 2007 Julian Mehnle @@ -22,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.008'); +use version; our $VERSION = qv('2.009'); use strict; @@ -66,7 +66,7 @@ my @HANDLERS = ( } ); -my $VERBOSE = 0; +my $VERBOSE = 1; my $DEFAULT_RESPONSE = 'DUNNO'; From 39a6d9bcd6fde6e1dd492c1b2e7c0f55eb1ce7bd Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Wed, 25 Jan 2012 22:54:02 -0500 Subject: [PATCH 03/24] * 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) - Patch thanks to Allison Randal --- CHANGES | 3 +++ postfix-policyd-spf-perl | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index d5269bb..f68d5c4 100644 --- a/CHANGES +++ b/CHANGES @@ -5,6 +5,9 @@ # * = Fixed a bug, or made a minor improvement --- 2.009 UNRELEASED + * 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) + - Patch thanks to Allison Randal --- 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) diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index 6b43403..4293394 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -250,8 +250,8 @@ sub sender_policy_framework { } my $helo_result_code = $helo_result->code; # 'pass', 'fail', etc. - my $helo_local_exp = $helo_result->local_explanation; - my $helo_authority_exp = $helo_result->authority_explanation + 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; @@ -327,8 +327,8 @@ sub sender_policy_framework { } my $mfrom_result_code = $mfrom_result->code; # 'pass', 'fail', etc. - my $mfrom_local_exp = $mfrom_result->local_explanation; - my $mfrom_authority_exp = $mfrom_result->authority_explanation + 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; @@ -360,3 +360,16 @@ 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; +} From b2d1889dfaf52f3d1f5aebc9c1153279c53b66db Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Wed, 25 Jan 2012 23:00:43 -0500 Subject: [PATCH 04/24] Add copyright information for Allison Randal --- README | 5 +++-- postfix-policyd-spf-perl | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README b/README index 0f60ae9..b6952f3 100644 --- a/README +++ b/README @@ -1,8 +1,9 @@ postfix-policyd-spf-perl 2.009 A Postfix SMTPd policy server for SPF checking (C) 2007-2008,2012 Scott Kitterman -(C) 2007 Julian Mehnle -(C) 2003-2004 Meng Weng Wong +(C) 2012 Allison Randal +(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 4293394..e77ded7 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -5,8 +5,9 @@ # version 2.009 # # (C) 2007-2008,2012 Scott Kitterman -# (C) 2007 Julian Mehnle -# (C) 2003-2004 Meng Weng Wong +# (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 From 291238fe731ae8ec8ba7ae2a0efdbe8eee8f9a1f Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Fri, 3 Feb 2012 22:27:11 -0500 Subject: [PATCH 05/24] Remove test_cases file since these are maintained in the pypolicyd-spf repository now. --- test_cases | 147 ----------------------------------------------------- 1 file changed, 147 deletions(-) delete mode 100644 test_cases diff --git a/test_cases b/test_cases deleted file mode 100644 index cae5156..0000000 --- a/test_cases +++ /dev/null @@ -1,147 +0,0 @@ -#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 From 2f5a4ff1ba8e6a981839d2390d66fb542ae5b0a8 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Fri, 3 Feb 2012 22:42:16 -0500 Subject: [PATCH 06/24] Remove logging of queue ID - It's virtually never available and just clutters logs. --- postfix-policyd-spf-perl | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index e77ded7..2357b13 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -170,7 +170,7 @@ while () { } } - syslog(info => "%s: Policy action=%s", $attr{queue_id} || '', $action || ''); + syslog(info => "Policy action=%s", $action || ''); STDOUT->print("action=$action\n\n"); %attr = (); @@ -239,8 +239,8 @@ sub sender_policy_framework { my $errmsg = $@; $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); syslog( - info => "%s:HELO check failed - Mail::SPF->new(%s, %s, %s) failed: %s", - $attr->{queue_id} || '', $attr->{client_address} || '', + info => "HELO check failed - Mail::SPF->new(%s, %s, %s) failed: %s", + $attr->{client_address} || '', $attr->{sender} || '', $attr->{helo_name} || '', $errmsg || '' ); @@ -258,8 +258,8 @@ sub sender_policy_framework { if ($VERBOSE) { syslog( - info => "%s: SPF %s: HELO/EHLO: %s, IP Address: %s, Recipient: %s", - $attr->{queue_id} || '', $helo_result || '', + info => "SPF %s: HELO/EHLO: %s, IP Address: %s, Recipient: %s", + $helo_result || '', $attr->{helo_name} || '', $attr->{client_address} || '', $attr->{recipient} || '' ); @@ -269,24 +269,24 @@ sub sender_policy_framework { # be accepted. Use the HELO result and return for null sender. if ($helo_result->is_code('fail')) { syslog( - info => "%s: SPF %s: HELO/EHLO: %s", - $attr->{queue_id} || '', $helo_result || '', + info => "SPF %s: HELO/EHLO: %s", + $helo_result || '', $attr->{helo_name} || '' ); return "550 $helo_authority_exp"; } elsif ($helo_result->is_code('temperror')) { syslog( - info => "%s: SPF %s: HELO/EHLO: %s", - $attr->{queue_id} || '', $helo_result || '', + info => "SPF %s: HELO/EHLO: %s", + $helo_result || '', $attr->{helo_name} || '' ); return "DEFER_IF_PERMIT SPF-Result=$helo_local_exp"; } elsif ($attr->{sender} eq '') { syslog( - info => "%s: SPF %s: HELO/EHLO (Null Sender): %s", - $attr->{queue_id} || '', $helo_result || '', + info => "SPF %s: HELO/EHLO (Null Sender): %s", + $helo_result || '', $attr->{helo_name} || '' ); return "PREPEND $helo_spf_header" @@ -317,8 +317,8 @@ sub sender_policy_framework { my $errmsg = $@; $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); syslog( - info => "%s: Mail From (sender) check failed - Mail::SPF->new(%s, %s, %s) failed: %s", - $attr->{queue_id} || '', $attr->{client_address} || '', + info => "Mail From (sender) check failed - Mail::SPF->new(%s, %s, %s) failed: %s", + $attr->{client_address} || '', $attr->{sender} || '', $attr->{helo_name} || '', $errmsg || '' ); return; @@ -335,8 +335,8 @@ sub sender_policy_framework { if ($VERBOSE) { syslog( - info => "%s: SPF %s: Envelope-from: %s, IP Address: %s, Recipient: %s", - $attr->{queue_id} || '', $mfrom_result || '', + info => "SPF %s: Envelope-from: %s, IP Address: %s, Recipient: %s", + $mfrom_result || '', $attr->{sender} || '', $attr->{client_address} || '', $attr->{recipient} || '' ); @@ -344,8 +344,8 @@ sub sender_policy_framework { # Same approach as HELO.... syslog( - info => "%s: SPF %s: Envelope-from: %s", - $attr->{queue_id} || '', $mfrom_result || '', + info => "SPF %s: Envelope-from: %s", + $mfrom_result || '', $attr->{sender} || '' ); if ($mfrom_result->is_code('fail')) { From 3156020666ba99db68e3c0c62e817ae90ec6b5a5 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Fri, 3 Feb 2012 23:13:46 -0500 Subject: [PATCH 07/24] Reduce non-verbose logging to a single line per message. --- CHANGES | 3 ++ postfix-policyd-spf-perl | 82 +++++++++++++++++++++++----------------- 2 files changed, 51 insertions(+), 34 deletions(-) diff --git a/CHANGES b/CHANGES index f68d5c4..a0b4a0a 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,9 @@ * 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) - 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) diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index 2357b13..ec8f9a3 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -145,7 +145,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} ||= {} : {}; @@ -160,11 +160,13 @@ while () { if ($VERBOSE) { syslog(debug => "handler %s: %s", $handler_name || '', $response || ''); - } + }; # Pick whatever response is not 'DUNNO' if ($response and $response !~ /^DUNNO/i) { - syslog(info => "handler %s: is decisive.", $handler_name || ''); + if ($VERBOSE) { + syslog(info => "handler %s: is decisive.", $handler_name || ''); + } $action = $response; last; } @@ -238,12 +240,14 @@ sub sender_policy_framework { # probably due to invalid input data! my $errmsg = $@; $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); - syslog( - info => "HELO check failed - Mail::SPF->new(%s, %s, %s) failed: %s", - $attr->{client_address} || '', - $attr->{sender} || '', $attr->{helo_name} || '', - $errmsg || '' - ); + 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; } @@ -268,27 +272,33 @@ 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. if ($helo_result->is_code('fail')) { - syslog( - info => "SPF %s: HELO/EHLO: %s", - $helo_result || '', - $attr->{helo_name} || '' - ); + 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')) { - syslog( - info => "SPF %s: HELO/EHLO: %s", - $helo_result || '', - $attr->{helo_name} || '' - ); + 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 '') { - syslog( - info => "SPF %s: HELO/EHLO (Null Sender): %s", - $helo_result || '', - $attr->{helo_name} || '' - ); + 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}++; } @@ -316,11 +326,13 @@ sub sender_policy_framework { # probably due to invalid input data! my $errmsg = $@; $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); - syslog( - info => "Mail From (sender) check failed - Mail::SPF->new(%s, %s, %s) failed: %s", - $attr->{client_address} || '', - $attr->{sender} || '', $attr->{helo_name} || '', $errmsg || '' - ); + 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; } @@ -343,11 +355,13 @@ sub sender_policy_framework { }; # Same approach as HELO.... - syslog( - info => "SPF %s: Envelope-from: %s", - $mfrom_result || '', - $attr->{sender} || '' - ); + if ($VERBOSE) { + syslog( + info => "SPF %s: Envelope-from: %s", + $mfrom_result || '', + $attr->{sender} || '' + ); + }; if ($mfrom_result->is_code('fail')) { return "550 $mfrom_authority_exp"; } From c05c21182231cf9a880fd96bc22c3b33a3089c9c Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Fri, 3 Feb 2012 23:23:59 -0500 Subject: [PATCH 08/24] Changelog cleanups in preparation for release. --- CHANGES | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index a0b4a0a..ebeb02c 100644 --- a/CHANGES +++ b/CHANGES @@ -4,13 +4,15 @@ # ! = Changed something significant, or removed a feature # * = Fixed a bug, or made a minor improvement ---- 2.009 UNRELEASED +--- 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 + * 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) @@ -18,7 +20,6 @@ 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 From 5970525467ff70afb5fee5cd4cef1c84341efbdd Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Fri, 3 Feb 2012 23:40:07 -0500 Subject: [PATCH 09/24] Set logging back to non-verbose by default for release. --- postfix-policyd-spf-perl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index ec8f9a3..1cf8d5e 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -67,7 +67,7 @@ my @HANDLERS = ( } ); -my $VERBOSE = 1; +my $VERBOSE = 0; my $DEFAULT_RESPONSE = 'DUNNO'; From 2aa104973fca2f363a527c63442a2eb3b0497cb9 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Sun, 17 Jun 2012 22:26:46 -0400 Subject: [PATCH 10/24] * Start 2.010 * Fixed incorrect use of != instead of ne for string comparison --- CHANGES | 4 ++++ README | 2 +- postfix-policyd-spf-perl | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index ebeb02c..949f62e 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.010 2012-06-17 + * Fixed incorrect use of != instead of ne for string comparison + (LP: #1014243) + --- 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) diff --git a/README b/README index b6952f3..9d8fc13 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -postfix-policyd-spf-perl 2.009 +postfix-policyd-spf-perl 2.010 A Postfix SMTPd policy server for SPF checking (C) 2007-2008,2012 Scott Kitterman (C) 2012 Allison Randal diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index 1cf8d5e..cd31cf4 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -2,7 +2,7 @@ # postfix-policyd-spf-perl # http://www.openspf.org/Software -# version 2.009 +# version 2.010 # # (C) 2007-2008,2012 Scott Kitterman # (C) 2012 Allison Randal @@ -23,7 +23,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.009'); +use version; our $VERSION = qv('2.010'); use strict; @@ -185,7 +185,7 @@ while () { sub exempt_localhost { my %options = @_; my $attr = $options{attr}; - if ($attr->{client_address} != '') { + if ($attr->{client_address} ne '') { my $client_address = NetAddr::IP->new($attr->{client_address}); return 'PREPEND X-Comment: SPF not applicable to localhost connection - skipped check' if grep($_->contains($client_address), localhost_addresses); @@ -200,7 +200,7 @@ sub exempt_localhost { sub exempt_relay { my %options = @_; my $attr = $options{attr}; - if ($attr->{client_address} != '') { + if ($attr->{client_address} ne '') { my $client_address = NetAddr::IP->new($attr->{client_address}); return 'PREPEND X-Comment: SPF skipped for whitelisted relay' if grep($_->contains($client_address), relay_addresses); From 8c2ec2083f7ca295f6f216e7e7379ae781d83564 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Sun, 17 Jun 2012 23:49:11 -0400 Subject: [PATCH 11/24] ! 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 --- CHANGES | 4 ++++ INSTALL | 3 ++- postfix-policyd-spf-perl | 9 +++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 949f62e..a4262b4 100644 --- a/CHANGES +++ b/CHANGES @@ -7,6 +7,10 @@ --- 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 diff --git a/INSTALL b/INSTALL index edc3cec..3e13787 100644 --- a/INSTALL +++ b/INSTALL @@ -7,7 +7,8 @@ postfix-policyd-spf-perl: Perl 5.6 version NetAddr-IP 4 - Mail-SPF (not Mail-SPF-Query) version 2.006 or later + Mail::SPF (not Mail-SPF-Query) version 2.006 or later + Sys::Hostname::Long Installing ---------- diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index cd31cf4..06c9a51 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -31,6 +31,7 @@ use IO::Handle; use Sys::Syslog qw(:DEFAULT setlogsock); use NetAddr::IP; use Mail::SPF; +use Sys::Hostname::Long 'hostname_long'; # ---------------------------------------------------------- # configuration @@ -92,6 +93,10 @@ use constant relay_addresses => map( 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; + my %results_cache; # by message instance # ---------------------------------------------------------- @@ -187,7 +192,7 @@ sub exempt_localhost { my $attr = $options{attr}; if ($attr->{client_address} ne '') { my $client_address = NetAddr::IP->new($attr->{client_address}); - return 'PREPEND X-Comment: SPF not applicable to localhost connection - skipped check' + return "PREPEND Authentication-Results: $host; none (SPF not checked for localhost)" if grep($_->contains($client_address), localhost_addresses); }; return 'DUNNO'; @@ -202,7 +207,7 @@ sub exempt_relay { my $attr = $options{attr}; if ($attr->{client_address} ne '') { my $client_address = NetAddr::IP->new($attr->{client_address}); - return 'PREPEND X-Comment: SPF skipped for whitelisted relay' + return "PREPEND Authentication-Results: $host; none (SPF not checked for whitelisted relay)" if grep($_->contains($client_address), relay_addresses); }; return 'DUNNO'; From 92dc6cbd4ff72ad8bde5f9216e2c219be8f2c954 Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Tue, 10 Jan 2017 17:19:17 -0500 Subject: [PATCH 12/24] * 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 13/24] 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 14/24] 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 15/24] 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 16/24] 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 17/24] 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 18/24] 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 19/24] 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 20/24] 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 21/24] 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 22/24] * 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 23/24] 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 24/24] 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"