Mail::SPF::Record - Abstract base class for SPF records


Mail-SPF documentation Contained in the Mail-SPF distribution.

Index


Code Index:

NAME

Top

Mail::SPF::Record - Abstract base class for SPF records

SYNOPSIS

Top

Creating a record from a string

    use Mail::SPF::v1::Record;

    my $record = Mail::SPF::v1::Record->new_from_string("v=spf1 a mx -all");

Creating a record synthetically

    use Mail::SPF::v2::Record;

    my $record = Mail::SPF::v2::Record->new(
        scopes      => ['mfrom', 'pra'],
        terms       => [
            Mail::SPF::Mech::A->new(),
            Mail::SPF::Mech::MX->new(),
            Mail::SPF::Mech::All->new(qualifier => '-')
        ],
        global_mods => [
            Mail::SPF::Mod::Exp->new(domain_spec => 'spf-exp.example.com')
        ]
    );

DESCRIPTION

Top

Mail::SPF::Record is an abstract base class for SPF records. It cannot be instantiated directly. Create an instance of a concrete sub-class instead.

Constructor

The following constructors are provided:

new(%options): returns Mail::SPF::Record

Creates a new SPF record object.

%options is a list of key/value pairs representing any of the following options:

text

A string denoting the unparsed text of the record.

scopes

A reference to an array of strings denoting the scopes that are covered by the record (see the description of the scope option of Mail::SPF::Request's new constructor|Mail::SPF::Request/new).

terms

A reference to an array of Mail::SPF::Term (i.e. Mail::SPF::Mech or Mail::SPF::Mod) objects that make up the record. Mail::SPF::GlobalMod objects must not be included here, but should be specified using the global_mods option instead.

global_mods

A reference to an array of Mail::SPF::GlobalMod objects that are global modifiers of the record.

new_from_string($text, %options): returns Mail::SPF::Record; throws Mail::SPF::ENothingToParse, Mail::SPF::EInvalidRecordVersion, Mail::SPF::ESyntaxError

Creates a new SPF record object by parsing the string and any options given.

Class methods

The following class methods are provided:

version_tag_pattern: returns Regexp

Abstract. Returns a regular expression that matches a legal version tag.

This method is abstract and must be implemented by sub-classes of Mail::SPF::Record.

default_qualifier: returns string

Returns the default qualifier, i.e. '+'.

results_by_qualifier: returns hash of string

Returns a reference to a hash that maps qualifiers to result codes as follows:

     Qualifier | Result code
    -----------+-------------
         +     | pass
         -     | fail
         ~     | softfail
         ?     | neutral

Instance methods

The following instance methods are provided:

text: returns string; throws Mail::SPF::ENoUnparsedText

Returns the unparsed text of the record. Throws a Mail::SPF::ENoUnparsedText exception if the record was created synthetically instead of being parsed, and no text was provided.

version_tag: returns string

Abstract. Returns the version tag of the record.

This method is abstract and must be implemented by sub-classes of Mail::SPF::Record.

scopes: returns list of string

Returns a list of the scopes that are covered by the record. See the description of the new constructor's scopes option.

terms: returns list of Mail::SPF::Term

Returns a list of the terms that make up the record, excluding any global modifiers, which are returned by the global_mods method. See the description of the new constructor's terms option.

global_mods: returns list of Mail::SPF::GlobalMod

Returns a list of the global modifiers of the record, ordered ascending by modifier precedence. See the description of the new constructor's global_mods option.

global_mod($mod_name): returns Mail::SPF::GlobalMod

Returns the global modifier of the given name if it is present in the record. Returns undef otherwise. Use this method if you wish to retrieve a specific global modifier as opposed to getting all of them.

stringify: returns string

Returns the record's version tag and terms (including the global modifiers) formatted as a string. You can simply use a Mail::SPF::Record object as a string for the same effect, see "OVERLOADING".

eval($server, $request): throws Mail::SPF::Result

Evaluates the SPF record in the context of the request parameters represented by the given Mail::SPF::Request object. The given Mail::SPF::Server object is used for performing DNS look-ups. Throws a Mail::SPF::Result object matching the outcome of the evaluation; see Mail::SPF::Result. See RFC 4408, 4.6 and 4.7, for the exact algorithm used.

OVERLOADING

Top

If a Mail::SPF::Record object is used as a string, the stringify method is used to convert the object into a string.

SEE ALSO

Top

Mail::SPF, Mail::SPF::v1::Record, Mail::SPF::v2::Record, Mail::SPF::Term, Mail::SPF::Mech, Mail::SPF::Mod

http://www.ietf.org/rfc/rfc4408.txt

For availability, support, and license information, see the README file included with Mail::SPF.

AUTHORS

Top

Julian Mehnle <julian@mehnle.net>, Shevek <cpan@anarres.org>


Mail-SPF documentation Contained in the Mail-SPF distribution.
#
# Mail::SPF::Record
# Abstract base class for SPF records.
#
# (C) 2005-2008 Julian Mehnle <julian@mehnle.net>
#     2005      Shevek <cpan@anarres.org>
# $Id: Record.pm 50 2008-08-17 21:28:15Z Julian Mehnle $
#
##############################################################################

package Mail::SPF::Record;

use warnings;
use strict;

use utf8;  # Hack to keep Perl 5.6 from whining about /[\p{}]/.

use base 'Mail::SPF::Base';

use overload
    '""'        => 'stringify',
    fallback    => 1;

use Error ':try';

use constant TRUE   => (0 == 0);
use constant FALSE  => not TRUE;

use constant default_qualifier      => '+';

use constant results_by_qualifier   => {
    ''  => 'pass',
    '+' => 'pass',
    '-' => 'fail',
    '~' => 'softfail',
    '?' => 'neutral'
};

# Interface:
##############################################################################

# Implementation:
##############################################################################

sub new {
    my ($self, %options) = @_;
    $self->class ne __PACKAGE__
        or throw Mail::SPF::EAbstractClass;
    $self = $self->SUPER::new(%options);
    $self->{parse_text} = $self->{text} if not defined($self->{parse_text});
    $self->{terms}       ||= [];
    $self->{global_mods} ||= {};
    return $self;
}

sub new_from_string {
    my ($self, $text, %options) = @_;
    $self = $self->new(%options, text => $text);
    $self->parse();
    return $self;
}

sub parse {
    my ($self) = @_;
    defined($self->{parse_text})
        or throw Mail::SPF::ENothingToParse('Nothing to parse for record');
    $self->parse_version_tag();
    $self->parse_term() while length($self->{parse_text});
    $self->parse_end();
    return;
}

sub parse_version_tag {
    my ($self) = @_;
    if (not $self->{parse_text} =~ s/^${\$self->version_tag_pattern}(?:\x20+|$)//) {
        throw Mail::SPF::EInvalidRecordVersion(
            "Not a '" . $self->version_tag . "' record: '" . $self->text . "'");
    }
}

sub parse_term {
    my ($self) = @_;
    if (
        $self->{parse_text} =~ s/
                        ^
                        (
                                ${\Mail::SPF::Mech->qualifier_pattern}?
                              (${\Mail::SPF::Mech->name_pattern})
                                [^\x20]*
                        )
                        (?: \x20+ | $ )
                //x
    ) {
        # Looks like a mechanism:
        my ($mech_text, $mech_name) = ($1, lc($2));
        my $mech_class = $self->mech_classes->{$mech_name};
        throw Mail::SPF::EInvalidMech("Unknown mechanism type '$mech_name' in '" . $self->version_tag . "' record")
            if not defined($mech_class);
        my $mech = $mech_class->new_from_string($mech_text);
        push(@{$self->{terms}}, $mech);
    }
    elsif (
        $self->{parse_text} =~ s/
                        ^
                        (
                              (${\Mail::SPF::Mod->name_pattern}) =
                                [^\x20]*
                        ) 
                        (?: \x20+ | $ )
                //x
    ) {
        # Looks like a modifier:
        my ($mod_text, $mod_name) = ($1, lc($2));
        my $mod_class = $self->mod_classes->{$mod_name};
        if (defined($mod_class)) {
            # Known modifier.
            my $mod = $mod_class->new_from_string($mod_text);
            if ($mod->isa('Mail::SPF::GlobalMod')) {
                # Global modifier.
                not defined($self->{global_mods}->{$mod_name}) or
                    throw Mail::SPF::EDuplicateGlobalMod("Duplicate global modifier '$mod_name' encountered");
                $self->{global_mods}->{$mod_name} = $mod;
            }
            elsif ($mod->isa('Mail::SPF::PositionalMod')) {
                # Positional modifier, queue normally:
                push(@{$self->{terms}}, $mod);
            }
            else {
                # Huh?  This should not happen.
            }
        }
        else {
            # Unknown modifier.
            my $mod = Mail::SPF::UnknownMod->new_from_string($mod_text);
            push(@{$self->{terms}}, $mod);
        }
    }
    else {
        throw Mail::SPF::EJunkInRecord("Junk encountered in record '" . $self->text . "'");
    }
    return;
}

sub parse_end {
    my ($self) = @_;
    throw Mail::SPF::EJunkInRecord("Junk encountered in record '" . $self->text . "'")
        if $self->{parse_text} ne '';
    delete($self->{parse_text});
    return;
}

sub text {
    my ($self) = @_;
    defined($self->{text})
        or throw Mail::SPF::ENoUnparsedText;
    return $self->{text};
}

sub scopes {
    my ($self) = @_;
    return @{$self->{scopes}};
}

sub terms {
    my ($self) = @_;
    return @{$self->{terms}};
}

sub global_mods {
    my ($self) = @_;
    return sort { $a->precedence <=> $b->precedence } values(%{$self->{global_mods}});
}

sub global_mod {
    my ($self, $mod_name) = @_;
    return $self->{global_mods}->{$mod_name};
}

sub stringify {
    my ($self) = @_;
    return join(' ', $self->version_tag, $self->terms, $self->global_mods);
}

sub eval {
    my ($self, $server, $request) = @_;
    
    defined($server)
        or throw Mail::SPF::EOptionRequired('Mail::SPF server object required for record evaluation');
    defined($request)
        or throw Mail::SPF::EOptionRequired('Request object required for record evaluation');
    
    try {
        foreach my $term ($self->terms) {
            if ($term->isa('Mail::SPF::Mech')) {
                # Term is a mechanism.
                my $mech = $term;
                if ($mech->match($server, $request)) {
                    my $result_name  = $self->results_by_qualifier->{$mech->qualifier};
                    my $result_class = $server->result_class($result_name);
                    my $result = $result_class->new($server, $request, "Mechanism '$term' matched");
                    $mech->explain($server, $request, $result);
                    $result->throw();
                }
            }
            elsif ($term->isa('Mail::SPF::PositionalMod')) {
                # Term is a positional modifier.
                my $mod = $term;
                $mod->process($server, $request);
            }
            elsif ($term->isa('Mail::SPF::UnknownMod')) {
                # Term is an unknown modifier.  Ignore it (RFC 4408, 6/3).
            }
            else {
                # Invalid term object encountered:
                throw Mail::SPF::EUnexpectedTermObject(
                    "Unexpected term object '$term' encountered");
            }
        }
        
        # Default result when "falling off" the end of the record (RFC 4408, 4.7/1):
        $server->throw_result('neutral-by-default', $request,
            'Default neutral result due to no mechanism matches');
    }
    catch Mail::SPF::Result with {
        my ($result) = @_;
        
        # Process global modifiers in ascending order of precedence:
        foreach my $global_mod ($self->global_mods) {
            $global_mod->process($server, $request, $result);
        }
        
        $result->throw();
    };
}

TRUE;