diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index 5793ac9..dbc61ec 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -4,8 +4,9 @@ # http://www.openspf.org/Software # version 2.002 # -#(C) 2007 Scott Kitterman -#(C) 2003-2004 Meng Weng Wong +# (C) 2007 Scott Kitterman +# (C) 2007 Julian Mehnle +# (C) 2003-2004 Meng Weng Wong # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -36,7 +37,7 @@ use Mail::SPF; my $spf_server = Mail::SPF::Server->new(); -# Leaving this to make it easier to add more handlers later: +# Adding more handlers is easy: my @HANDLERS = ( { name => 'exempt_localhost', @@ -51,7 +52,6 @@ my @HANDLERS = ( my $VERBOSE = 0; my $DEFAULT_RESPONSE = 'DUNNO'; -my $accepted = "UNDEF"; # # Syslogging options for verbose mode and for fatal errors. @@ -69,6 +69,8 @@ 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:"? +my %results_cache; # by message instance + # ---------------------------------------------------------- # initialization # ---------------------------------------------------------- @@ -121,38 +123,33 @@ while () { syslog(debug => "Attribute: %s=%s", $_, $attr{$_}); } } - my $instance = $attr{instance}; + + my $message_instance = $attr{instance}; + my $cache = defined($message_instance) ? $results_cache{$message_instance} ||= {} : {}; + my $action = $DEFAULT_RESPONSE; - my %responses; - # Skip SPF check for local connections - + foreach my $handler (@HANDLERS) { my $handler_name = $handler->{name}; my $handler_code = $handler->{code}; - - my $response = $handler_code->(attr => \%attr); - - if($instance && $instance eq $accepted) { - $response = 'DUNNO'; - } - - + + my $response = $handler_code->(attr => \%attr, cache => $cache); + if ($VERBOSE) { syslog(debug => "handler %s: %s", $handler_name, $response); } - - # Picks whatever response is not 'DUNNO' + + # Pick whatever response is not 'DUNNO' if ($response and $response !~ /^DUNNO/i) { syslog(info => "handler %s: is decisive.", $handler_name); $action = $response; last; } } - + syslog(info => "%s: Policy action=%s", $attr{queue_id}, $action); - + STDOUT->print("action=$action\n\n"); - $accepted = $instance; %attr = (); } @@ -177,102 +174,127 @@ sub exempt_localhost { sub sender_policy_framework { my %options = @_; - my $attr = $options{attr}; - - # Always do HELO check first. If no HELO policy it's only one lookup. - # Avoids the need to do any Mail From processing for null sender. - my $helo_request = eval { - Mail::SPF::Request->new( - scope => 'helo', - identity => $attr->{helo_name}, - ip_address => $attr->{client_address} - ); - }; + my $attr = $options{attr}; + my $cache = $options{cache}; - # If initializing helo_request throws an error, don't use it. - if ($@) { - 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}, $attr->{sender}, $attr->{helo_name}, $errmsg - ); - return 'DUNNO'; - } - else { - my $helo_result = $spf_server->process($helo_request); + # ------------------------------------------------------------------------- + # 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_result_code = $helo_result->code; # 'pass', 'fail', etc. - my $helo_local_exp = $helo_result->local_explanation; - my $helo_authority_exp = $helo_result->authority_explanation - if $helo_result->is_code('fail'); - my $helo_spf_header = $helo_result->received_spf_header; - if ($VERBOSE) { - syslog( - info => "%s: SPF %s: HELO/EHLO: %s, IP Address: %s, Recipient: %s", - $attr->{queue_id}, $helo_result, $attr->{helo_name}, $attr->{client_address}, - $attr->{recipient} + my $helo_request = eval { + Mail::SPF::Request->new( + scope => 'helo', + identity => $attr->{helo_name}, + ip_address => $attr->{client_address} ); }; - # 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')) { - return "550 $helo_authority_exp"; - } - elsif ($helo_result->is_code('temperror')) { - return "DEFER_IF_PERMIT SPF-Result=$helo_local_exp"; - } - elsif ($attr->{sender} eq '') { - return "PREPEND $helo_spf_header"; + 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'); + syslog( + info => "%s:HELO check failed - Mail::SPF->new(%s, %s, %s) failed: %s", + $attr->{queue_id}, $attr->{client_address}, $attr->{sender}, $attr->{helo_name}, $errmsg + ); + return; } + + $helo_result = $cache->{helo_result} = $spf_server->process($helo_request); } - # Do mail from is HELO doesn't give a definitive result. - 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 + 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 + if $helo_result->is_code('fail'); + my $helo_spf_header = $helo_result->received_spf_header; + + if ($VERBOSE) { + syslog( + info => "%s: SPF %s: HELO/EHLO: %s, IP Address: %s, Recipient: %s", + $attr->{queue_id}, $helo_result, $attr->{helo_name}, $attr->{client_address}, + $attr->{recipient} ); }; - if ($@) { - 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}, $attr->{sender}, $attr->{helo_name}, $errmsg - ); - return 'DUNNO'; - } - else { - my $mfrom_result = $spf_server->process($mfrom_request); + # 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')) { + return "550 $helo_authority_exp"; + } + elsif ($helo_result->is_code('temperror')) { + return "DEFER_IF_PERMIT SPF-Result=$helo_local_exp"; + } + elsif ($attr->{sender} eq '') { + 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_result_code = $mfrom_result->code; # 'pass', 'fail', etc. - my $mfrom_local_exp = $mfrom_result->local_explanation; - my $mfrom_authority_exp = $mfrom_result->authority_explanation - if $mfrom_result->is_code('fail'); - my $mfrom_spf_header = $mfrom_result->received_spf_header; - - if ($VERBOSE) { - syslog( - info => "%s: SPF %s: Envelope-from: %s, IP Address: %s, Recipient: %s", - $attr->{queue_id}, $mfrom_result, $attr->{sender}, $attr->{client_address}, - $attr->{recipient} + 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 ); }; - - # Same approach as HELO.... - 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"; - } + + 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'); + syslog( + info => "%s: Mail From (sender) check failed - Mail::SPF->new(%s, %s, %s) failed: %s", + $attr->{queue_id}, $attr->{client_address}, $attr->{sender}, $attr->{helo_name}, $errmsg + ); + return; + } + + $mfrom_result = $cache->{mfrom_result} = $spf_server->process($mfrom_request); } + + 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 + if $mfrom_result->is_code('fail'); + my $mfrom_spf_header = $mfrom_result->received_spf_header; + + if ($VERBOSE) { + syslog( + info => "%s: SPF %s: Envelope-from: %s, IP Address: %s, Recipient: %s", + $attr->{queue_id}, $mfrom_result, $attr->{sender}, $attr->{client_address}, + $attr->{recipient} + ); + }; + + # Same approach as HELO.... + if ($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; }