# # Courier::Filter::Module::SPF class # # (C) 2004-2008 Julian Mehnle # $Id: SPF.pm 211 2008-03-23 01:25:20Z julian $ # ############################################################################### =head1 NAME Courier::Filter::Module::SPF - SPF filter module for the Courier::Filter framework =cut package Courier::Filter::Module::SPF; use warnings; use strict; use base 'Courier::Filter::Module'; use Courier::Filter::Util qw( ipv4_address_pattern ipv6_address_pattern loopback_address_pattern ); use Mail::SPF; use constant TRUE => (0 == 0); use constant FALSE => not TRUE; use constant match_on_default => ['fail', 'permerror', 'temperror']; =head1 SYNOPSIS use Courier::Filter::Module::SPF; my $module = Courier::Filter::Module::SPF->new( scope => 'mfrom' || 'helo', match_on => ['fail', 'permerror', 'temperror'], default_response => $default_response_text, spf_options => { # any Mail::SPF::Server options }, logger => $logger, inverse => 0, trusting => 0, testing => 0, debugging => 0 ); my $filter = Courier::Filter->new( ... modules => [ $module ], ... ); =head1 DESCRIPTION This class is a filter module class for use with Courier::Filter. By default, it matches a message if the sending machine's IP address is I authorized to send mail from the envelope sender's (MAIL FROM) domain according to that domain's SPF (Sender Policy Framework) DNS record. This is classic I SPF checking. The point of inbound SPF checking is for receivers to protect I against forged envelope sender addresses in messages sent by others. =cut # Implementation: ############################################################################### =head2 Constructor The following constructor is provided: =over =item B: returns I Creates a new B filter module. %options is a list of key/value pairs representing any of the following options: =over =item B A string denoting the authorization scope, i.e., identity, on which the SPF check is to be performed. Defaults to C<'mfrom'>. See L for a detailed explanation. =item B A reference to an array containing the set of SPF result codes which should cause the filter module to match a message. Possible result codes are C, C, C, C, C, C, and C. See the SPF specification for details on the meaning of those. If C is listed, a C result will by definition never cause a I rejection, but only a I one. Defaults to B<['fail', 'permerror', 'temperror']>. I: With early SPF specification drafts as well as the obsolete Mail::SPF::Query module, the C and C result codes were known as C and C, respectively; the old codes are now deprecated but still supported for the time being. =item B A string that is to be returned as the module's match result in case of a match, that is when the C option includes the result code of the SPF check (by default when a message fails the SPF check). However, this default response is used only if the (claimed) MAIL FROM or HELO domain does not provide a result explanation of its own. See L for more information. SPF macro substitution is performed on the default response, just like on explanations provided by domain owners. If B, L will be used. Defaults to B. =item B A hash-ref specifying options for the Mail::SPF server object used by this filter module. See L for the supported options. If any of L, such as C (best-guess) or C (C accreditation checking), are specified, a black-magic SPF server object will be created instead. =item B =item B I. These options should now be specified as C and C keys of the L option instead, although these legacy options will continue to work for the time being. Furthermore, due to the move from the obsolete L module to the L reference implementation, the L extension module is now required when using these non-standard options. =back All options of the B constructor are also supported. Please see L for their descriptions. =cut sub new { my ($class, %options) = @_; $options{scope} ||= 'mfrom'; if (defined($options{reject_on})) { $class->warn('"reject_on" option is deprecated! Use "match_on" option instead.'); $options{match_on} ||= $options{reject_on}; } $options{match_on} ||= $class->match_on_default; my $spf_options = $options{spf_options} || {}; if (defined($options{fallback_guess})) { $class->warn('"fallback_guess" option is deprecated! Use "default_policy" key in "spf_options" option instead.'); $spf_options->{default_policy} ||= $options{fallback_guess}; } if (defined($options{trusted_forwarders})) { $class->warn('"trusted_forwarders" option is deprecated! Use "tfwl" key in "spf_options" option instead.'); $spf_options->{tfwl} ||= $options{trusted_forwarders}; } my $spf_server_class = 'Mail::SPF::Server'; foreach my $spf_option (keys(%$spf_options)) { if (not Mail::SPF->can($spf_option)) { $spf_server_class = 'Mail::SPF::BlackMagic::Server'; eval { require Mail::SPF::BlackMagic }; if ($@) { $class->warn("Mail::SPF::BlackMagic not installed. Ignoring unsupported \"$spf_option\" SPF option."); } elsif (not $spf_server_class->can($spf_option)) { $class->warn("Ignoring unsupported \"$spf_option\" SPF option. Perhaps newer Mail::SPF or Mail::SPF::BlackMagic required?"); } } } my $spf_server = $spf_server_class->new( default_authority_explanation => $options{default_response}, %$spf_options ); my $self = $class->SUPER::new( %options, spf_server => $spf_server ); return $self; } =back =head2 Instance methods See L for a description of the provided instance methods. =cut sub match { my ($self, $message) = @_; return undef if $message->remote_host =~ / ^ ${\loopback_address_pattern} $ /x; # Exempt IPv4/IPv6 loopback addresses. my $scope = $self->{scope}; my $identities = { helo => $message->remote_host_helo, mfrom => $message->sender }; my $identity = $identities->{$scope}; return undef if $identity eq ''; # Exempt empty identities (esp. empty MAIL FROM, i.e., bounces). return undef if $identity =~ / ^ \[ (?: ${\ipv4_address_pattern} | ${\ipv6_address_pattern} ) \] $ /x; # Exempt IP address literals ("[]"). my $request = Mail::SPF::Request->new( scope => $scope, identity => $identity, ip_address => $message->remote_host, helo_identity => $message->remote_host_helo ); my $result = $self->{spf_server}->process($request); my $result_code = $result->code; my $response = $result->can('authority_explanation') ? $result->authority_explanation : $result->local_explanation; my %match_on; @match_on{ @{$self->{match_on}} } = (); # Hash-ify match-on result codes list. return "SPF: $response", ($result_code eq 'temperror' ? 451 : ()) if exists($match_on{$result_code}); return undef; } =head1 SEE ALSO L, L, L. For AVAILABILITY, SUPPORT, and LICENSE information, see L. =head1 REFERENCES =over =item B (Sender Policy Framework) L =item B L =back =head1 AUTHOR Julian Mehnle =cut TRUE;