The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

# Copyright 2005-2007 Messiah College.
# Jason Long <jlong@messiah.edu>

# Copyright (c) 2004 Anthony D. Urso. All rights reserved.
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.

use strict;
use warnings;

package Mail::DKIM::Policy;

use Mail::DKIM::DNS;

=head1 NAME

Mail::DKIM::Policy - abstract base class for originator "signing" policies

=head1 SYNOPSIS

  # get all policies that apply to a verified message
  foreach my $policy ($dkim->policies)
  {

      # the name of this policy
      my $name = $policy->name;

      # the location in DNS where this policy was found
      my $location = $policy->location;

      # apply this policy to the message being verified
      my $result = $policy->apply($dkim);

  }

=head1 DESCRIPTION

Between the various versions of the DomainKeys/DKIM standards, several
different forms of sender "signing" policies have been defined.
In order for the L<Mail::DKIM> library to support these different
policies, it uses several different subclasses. All subclasses support
this general interface, so that a program using L<Mail::DKIM> can
support any and all policies found for a message.

=cut

sub fetch {
    my $class  = shift;
    my $waiter = $class->fetch_async(@_);
    return $waiter->();
}

sub fetch_async {
    my $class = shift;
    my %prms  = @_;

    ( $prms{'Protocol'} eq 'dns' )
      or die "invalid protocol '$prms{Protocol}'\n";

    my $host       = $class->get_lookup_name( \%prms );
    my %callbacks  = %{ $prms{Callbacks} || {} };
    my $on_success = $callbacks{Success} || sub { $_[0] };
    $callbacks{Success} = sub {
        my @resp = @_;
        unless (@resp) {

            # no requested resource records or NXDOMAIN,
            # use default policy
            return $on_success->( $class->default );
        }

        my $strn;
        foreach my $rr (@resp) {
            next unless $rr->type eq 'TXT';

            # join with no intervening spaces, RFC 5617
            if ( Net::DNS->VERSION >= 0.69 ) {

                # must call txtdata() in a list context
                $strn = join '', $rr->txtdata;
            }
            else {
                # char_str_list method is 'historical'
                $strn = join '', $rr->char_str_list;
            }
        }

        unless ($strn) {

            # empty record found in DNS, use default policy
            return $on_success->( $class->default );
        }

        my $self = $class->parse(
            String => $strn,
            Domain => $prms{Domain},
        );
        return $on_success->($self);
    };

    #
    # perform DNS query for domain policy...
    #
    my $waiter =
      Mail::DKIM::DNS::query_async( $host, 'TXT', Callbacks => \%callbacks, );
    return $waiter;
}

sub parse {
    my $class = shift;
    my %prms  = @_;

    my $text = $prms{'String'};
    my %tags;
    foreach my $tag ( split /;/, $text ) {

        # strip whitespace
        $tag =~ s/^\s+|\s+$//g;

        my ( $tagname, $value ) = split /=/, $tag, 2;
        unless ( defined $value ) {
            die "policy syntax error\n";
        }

        $tagname =~ s/\s+$//;
        $value =~ s/^\s+//;
        $tags{$tagname} = $value;
    }

    $prms{tags} = \%tags;
    return bless \%prms, $class;
}

=head1 METHODS

These methods are supported by all classes implementing the
L<Mail::DKIM::Policy> interface.

=head2 apply()

Apply the policy to the results of a DKIM verifier.

  my $result = $policy->apply($dkim_verifier);

The caller must provide an instance of L<Mail::DKIM::Verifier>, one which
has already been fed the message being verified.

Possible results are:

=over

=item accept

The message is approved by the sender signing policy.

=item reject

The message is rejected by the sender signing policy.

=item neutral

The message is neither approved nor rejected by the sender signing
policy. It can be considered suspicious.

=back

=cut

sub apply {
    my $self = shift;
    my ($dkim) = @_;

    my $first_party;
    foreach my $signature ( $dkim->signatures ) {
        next if $signature->result ne 'pass';

        my $oa = $dkim->message_sender->address;
        if ( $signature->identity_matches($oa) ) {

            # found a first party signature
            $first_party = 1;
            last;
        }
    }

    return 'accept' if $first_party;
    return 'reject' if ( $self->signall && !$self->testing );
    return 'neutral';
}

=head2 as_string()

The policy as a string.

Note that the string returned by this method will not necessarily have
the tags ordered the same as the text record found in DNS.

=cut

sub as_string {
    my $self = shift;

    return join(
        '; ', map { "$_=" . $self->{tags}->{$_} }
          keys %{ $self->{tags} }
    );
}

=head2 is_implied_default_policy()

Is this policy implied?

  my $is_implied = $policy->is_implied_default_policy;

If you fetch the policy for a particular domain, but that domain
does not have a policy published, then the "default policy" is
in effect. Use this method to detect when that happens.

=cut

sub is_implied_default_policy {
    my $self           = shift;
    my $default_policy = ref($self)->default;
    return ( $self == $default_policy );
}

=head2 location()

Where the policy was fetched from.

This is generally a domain name, the domain name where the policy
was published.

If nothing is published for the domain, and the default policy
was returned instead, the location will be C<undef>.

=cut

sub location {
    my $self = shift;
    return $self->{Domain};
}

=head2 name()

Identify what type of policy this is.

This currently returns strings like "sender", "author", and "ADSP".
It is subject to change in the next version of Mail::DKIM.

=cut

1;

=head1 SEE ALSO

L<Mail::DKIM::DkPolicy> - for RFC4870(historical) DomainKeys
sender signing policies

L<Mail::DKIM::DkimPolicy> - for early draft DKIM sender signing policies

L<Mail::DKIM::AuthorDomainPolicy> - for Author Domain Signing Practices
(ADSP)

=head1 AUTHOR

Jason Long, E<lt>jlong@messiah.eduE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2006-2009 by Messiah College

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.6 or,
at your option, any later version of Perl 5 you may have available.

=cut