diff --git a/postfix-policyd-spf-perl b/postfix-policyd-spf-perl index 61bbb29..d1a1925 100755 --- a/postfix-policyd-spf-perl +++ b/postfix-policyd-spf-perl @@ -25,21 +25,23 @@ use version; our $VERSION = qv('1.990'); use strict; -use Fcntl; +use IO::Handle; use Sys::Syslog qw(:DEFAULT setlogsock); use Mail::SPF; # ---------------------------------------------------------- # configuration # ---------------------------------------------------------- + my $spf_server = Mail::SPF::Server->new(); + my @HANDLERS; -push @HANDLERS, "sender_policy_framework"; -#Leaving this to make it easier to add others later. +push(@HANDLERS, \&sender_policy_framework); + # Leaving this to make it easier to add others later. my $VERBOSE = 0; -my $DEFAULT_RESPONSE = "DUNNO"; +my $DEFAULT_RESPONSE = 'DUNNO'; # # Syslogging options for verbose mode and for fatal errors. @@ -48,9 +50,9 @@ my $DEFAULT_RESPONSE = "DUNNO"; # my $syslog_socktype = 'unix'; # inet, unix, stream, console -my $syslog_facility = "mail"; -my $syslog_options = "pid"; -my $syslog_ident = "postfix/policy-spf"; +my $syslog_facility = 'mail'; +my $syslog_options = 'pid'; +my $syslog_ident = 'postfix/policy-spf'; # ---------------------------------------------------------- # initialization @@ -63,20 +65,20 @@ sub fatal_exit { syslog(err => "fatal_exit: @_"); syslog(warning => "fatal_exit: @_"); syslog(info => "fatal_exit: @_"); - die "fatal: @_"; + die("fatal: @_"); } # # Unbuffer standard output. # -select((select(STDOUT), $| = 1)[0]); +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; +setlogsock($syslog_socktype); +openlog($syslog_ident, $syslog_options, $syslog_facility); # ---------------------------------------------------------- # main @@ -85,118 +87,144 @@ openlog $syslog_ident, $syslog_options, $syslog_facility; # # Receive a bunch of attributes, evaluate the policy, send the result. # -my %attr; while () { chomp; - if (/=/) { my ($k, $v) = split (/=/, $_, 2); $attr{$k} = $v; next } - elsif (length) { syslog(warning => sprintf("warning: ignoring garbage: %.100s", $_)); next; } + + my %attr; + 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 $action = $DEFAULT_RESPONSE; my %responses; foreach my $handler (@HANDLERS) { - no strict 'refs'; - my $response = $handler->(attr=>\%attr); + my $response = $handler->(attr => \%attr); + if ($VERBOSE) { syslog(debug => "handler %s: %s", $handler, $response); } + # Picks whatever response is not dunno if ($response and $response !~ /^dunno/i) { syslog(info => "handler %s: is decisive.", $handler); - $action = $response; last; + $action = $response; + last; } } syslog(info => "Policy action=%s", $action); - print STDOUT "action=$action\n\n"; - %attr = (); + STDOUT->print("action=$action\n\n"); } # ---------------------------------------------------------- # plugin: SPF # ---------------------------------------------------------- sub sender_policy_framework { - local %_ = @_; - my %attr = %{ $_{attr} }; + 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', # 'mfrom' or 'helo', 'pra' - identity => $attr{helo_name}, - ip_address => $attr{client_address}, - helo_identity # optional, - => $attr{helo_name} + scope => 'helo', + identity => $attr->{helo_name}, + ip_address => $attr->{client_address} ); }; + # 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: Mail::SPF->new(%s, %s, %s) failed: %s", - $attr{queue_id}, $attr{client_address}, $attr{sender}, $attr{helo_name}, $@ - ); - return "DUNNO"; - } - else { - my $helo_result = $spf_server->process($helo_request); - - 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; - - 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} - ); - - # Reject on HELO fail. Defer on HELO temperror if message would otherwis - # be accepted. Use the HELO result and return for null sender. - if ($helo_result_code eq "fail") { return "REJECT $helo_authority_exp"; } - elsif ($helo_result_code eq "temperror") { return "DEFER_IF_PERMIT SPF-Result=$helo_local_exp"; } - elsif ($attr{sender} eq '') { return "PREPEND $helo_spf_header"; } - }; - # Do mail from is HELO doesn't give a definitive result. - my $mfrom_request = eval { - Mail::SPF::Request->new( - scope => 'mfrom', # 'mfrom' or 'helo', 'pra' - identity => $attr{sender}, - ip_address => $attr{client_address}, - helo_identity # optional, - => $attr{helo_name} # for %{h} macro expansion - ); - }; - if ($@) { - syslog( - info => "%s: Mail::SPF->new(%s, %s, %s) failed: %s", - $attr{queue_id}, $attr{client_address}, $attr{sender}, $attr{helo_name}, $@ + $attr->{queue_id}, $attr->{client_address}, $attr->{sender}, $attr->{helo_name}, $errmsg ); return "DUNNO"; } else { - my $mfrom_result = $spf_server->process($mfrom_request); + my $helo_result = $spf_server->process($helo_request); + + 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; - 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 + 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} + ); + + # 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 "REJECT $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"; + } + } + + # 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 + ); + }; + + if ($@) { + my $errmsg = $@; + $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); + syslog( + info => "%s: 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); + + 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; + my $mfrom_spf_header = $mfrom_result->received_spf_header; + 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} + $attr->{queue_id}, $mfrom_result, $attr->{sender}, $attr->{client_address}, $attr->{recipient} ); # Same approach as HELO.... - if ($mfrom_result_code eq "fail") { return "REJECT $mfrom_authority_exp"; } - elsif ($mfrom_result_code eq "mfrom_temperror") { return "DEFER_IF_PERMIT SPF-Result=$mfrom_local_exp"; } - else { return "PREPEND $mfrom_spf_header"; } + if ($mfrom_result->is_code('fail')) { + return "REJECT $mfrom_authority_exp"; + } + elsif ($mfrom_result->is_code('temperror')) { + return "DEFER_IF_PERMIT SPF-Result=$mfrom_local_exp"; + } + else { + return "PREPEND $mfrom_spf_header"; + } } }