software/postfix-policyd-spf-perl/trunk/postfix-policyd-spf-perl

* Added myself to copyright statement after all.
* Implemented results cache in order to prevent redundant SPF checks in
  multiple invocations per message instance.  This also enables us to prepend
  a "Received-SPF" header only once per message instance (as opposed to once
  per recipient address).
* Minor code and comments clean-up.
This commit is contained in:
Julian Mehnle 2007-02-19 23:44:05 +00:00
commit e041d210e3

View file

@ -5,6 +5,7 @@
# version 2.002 # version 2.002
# #
# (C) 2007 Scott Kitterman <scott@kitterman.com> # (C) 2007 Scott Kitterman <scott@kitterman.com>
# (C) 2007 Julian Mehnle <julian@mehnle.net>
# (C) 2003-2004 Meng Weng Wong <mengwong@pobox.com> # (C) 2003-2004 Meng Weng Wong <mengwong@pobox.com>
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
@ -36,7 +37,7 @@ use Mail::SPF;
my $spf_server = Mail::SPF::Server->new(); 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 = ( my @HANDLERS = (
{ {
name => 'exempt_localhost', name => 'exempt_localhost',
@ -51,7 +52,6 @@ my @HANDLERS = (
my $VERBOSE = 0; my $VERBOSE = 0;
my $DEFAULT_RESPONSE = 'DUNNO'; my $DEFAULT_RESPONSE = 'DUNNO';
my $accepted = "UNDEF";
# #
# Syslogging options for verbose mode and for fatal errors. # 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 ) qw( 127.0.0.0/8 ::ffff:127.0.0.0/104 ::1 )
); # Does Postfix ever say "client_address=::ffff:<ipv4-address>"? ); # Does Postfix ever say "client_address=::ffff:<ipv4-address>"?
my %results_cache; # by message instance
# ---------------------------------------------------------- # ----------------------------------------------------------
# initialization # initialization
# ---------------------------------------------------------- # ----------------------------------------------------------
@ -121,27 +123,23 @@ while (<STDIN>) {
syslog(debug => "Attribute: %s=%s", $_, $attr{$_}); 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 $action = $DEFAULT_RESPONSE;
my %responses;
# Skip SPF check for local connections
foreach my $handler (@HANDLERS) { foreach my $handler (@HANDLERS) {
my $handler_name = $handler->{name}; my $handler_name = $handler->{name};
my $handler_code = $handler->{code}; my $handler_code = $handler->{code};
my $response = $handler_code->(attr => \%attr); my $response = $handler_code->(attr => \%attr, cache => $cache);
if($instance && $instance eq $accepted) {
$response = 'DUNNO';
}
if ($VERBOSE) { if ($VERBOSE) {
syslog(debug => "handler %s: %s", $handler_name, $response); 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) { if ($response and $response !~ /^DUNNO/i) {
syslog(info => "handler %s: is decisive.", $handler_name); syslog(info => "handler %s: is decisive.", $handler_name);
$action = $response; $action = $response;
@ -152,7 +150,6 @@ while (<STDIN>) {
syslog(info => "%s: Policy action=%s", $attr{queue_id}, $action); syslog(info => "%s: Policy action=%s", $attr{queue_id}, $action);
STDOUT->print("action=$action\n\n"); STDOUT->print("action=$action\n\n");
$accepted = $instance;
%attr = (); %attr = ();
} }
@ -178,9 +175,18 @@ sub exempt_localhost {
sub sender_policy_framework { sub sender_policy_framework {
my %options = @_; my %options = @_;
my $attr = $options{attr}; 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.
# 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 { my $helo_request = eval {
Mail::SPF::Request->new( Mail::SPF::Request->new(
scope => 'helo', scope => 'helo',
@ -189,24 +195,27 @@ sub sender_policy_framework {
); );
}; };
# If initializing helo_request throws an error, don't use it.
if ($@) { if ($@) {
# An unexpected error occurred during request creation,
# probably due to invalid input data!
my $errmsg = $@; my $errmsg = $@;
$errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception');
syslog( syslog(
info => "%s:HELO check failed - Mail::SPF->new(%s, %s, %s) failed: %s", 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 $attr->{queue_id}, $attr->{client_address}, $attr->{sender}, $attr->{helo_name}, $errmsg
); );
return 'DUNNO'; return;
}
$helo_result = $cache->{helo_result} = $spf_server->process($helo_request);
} }
else {
my $helo_result = $spf_server->process($helo_request);
my $helo_result_code = $helo_result->code; # 'pass', 'fail', etc. my $helo_result_code = $helo_result->code; # 'pass', 'fail', etc.
my $helo_local_exp = $helo_result->local_explanation; my $helo_local_exp = $helo_result->local_explanation;
my $helo_authority_exp = $helo_result->authority_explanation my $helo_authority_exp = $helo_result->authority_explanation
if $helo_result->is_code('fail'); if $helo_result->is_code('fail');
my $helo_spf_header = $helo_result->received_spf_header; my $helo_spf_header = $helo_result->received_spf_header;
if ($VERBOSE) { if ($VERBOSE) {
syslog( syslog(
info => "%s: SPF %s: HELO/EHLO: %s, IP Address: %s, Recipient: %s", info => "%s: SPF %s: HELO/EHLO: %s, IP Address: %s, Recipient: %s",
@ -224,11 +233,19 @@ sub sender_policy_framework {
return "DEFER_IF_PERMIT SPF-Result=$helo_local_exp"; return "DEFER_IF_PERMIT SPF-Result=$helo_local_exp";
} }
elsif ($attr->{sender} eq '') { elsif ($attr->{sender} eq '') {
return "PREPEND $helo_spf_header"; return "PREPEND $helo_spf_header"
} unless $cache->{added_spf_header}++;
} }
# Do mail from is HELO doesn't give a definitive result. # -------------------------------------------------------------------------
# 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 { my $mfrom_request = eval {
Mail::SPF::Request->new( Mail::SPF::Request->new(
scope => 'mfrom', scope => 'mfrom',
@ -239,16 +256,19 @@ sub sender_policy_framework {
}; };
if ($@) { if ($@) {
# An unexpected error occurred during request creation,
# probably due to invalid input data!
my $errmsg = $@; my $errmsg = $@;
$errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception');
syslog( syslog(
info => "%s: Mail From (sender) check failed - Mail::SPF->new(%s, %s, %s) failed: %s", 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 $attr->{queue_id}, $attr->{client_address}, $attr->{sender}, $attr->{helo_name}, $errmsg
); );
return 'DUNNO'; return;
}
$mfrom_result = $cache->{mfrom_result} = $spf_server->process($mfrom_request);
} }
else {
my $mfrom_result = $spf_server->process($mfrom_request);
my $mfrom_result_code = $mfrom_result->code; # 'pass', 'fail', etc. my $mfrom_result_code = $mfrom_result->code; # 'pass', 'fail', etc.
my $mfrom_local_exp = $mfrom_result->local_explanation; my $mfrom_local_exp = $mfrom_result->local_explanation;
@ -272,7 +292,9 @@ sub sender_policy_framework {
return "DEFER_IF_PERMIT SPF-Result=$mfrom_local_exp"; return "DEFER_IF_PERMIT SPF-Result=$mfrom_local_exp";
} }
else { else {
return "PREPEND $mfrom_spf_header"; return "PREPEND $mfrom_spf_header"
} unless $cache->{added_spf_header}++;
} }
return;
} }