Merge pull request #196 from msimerson/dmarc

dmarc: add error handling and tests
This commit is contained in:
Matt Simerson 2015-01-27 10:30:28 -08:00
commit 178c5f6884
2 changed files with 32 additions and 53 deletions

View File

@ -72,6 +72,7 @@ https://github.com/smtpd/qpsmtpd/wiki/DMARC-FAQ
use strict; use strict;
use warnings; use warnings;
use English qw/-no_match_vars/;
use Qpsmtpd::Constants; use Qpsmtpd::Constants;
sub register { sub register {
@ -90,16 +91,16 @@ sub register {
} }
else { else {
$self->{_dmarc} = Mail::DMARC::PurePerl->new(); $self->{_dmarc} = Mail::DMARC::PurePerl->new();
$self->register_hook('data_post_headers', 'data_post_handler'); $self->register_hook('data_post_headers', 'check_dmarc');
}; };
} }
sub data_post_handler { sub check_dmarc {
my ($self, $transaction) = @_; my ($self, $transaction) = @_;
if ( $self->qp->connection->relay_client() ) { if ( $self->qp->connection->relay_client() ) {
$self->log(LOGINFO, "skip, relay client" ); $self->log(LOGINFO, "skip, relay client" );
return DECLINED; # disable reporting to ourself return DECLINED; # don't report to ourself
}; };
my $dmarc = $self->{_dmarc}; my $dmarc = $self->{_dmarc};
@ -117,19 +118,15 @@ sub data_post_handler {
my @recipients = $transaction->recipients; my @recipients = $transaction->recipients;
eval { $dmarc->envelope_to( lc $recipients[0]->host ); }; # optional eval { $dmarc->envelope_to( lc $recipients[0]->host ); }; # optional
eval { $dmarc->envelope_from( $transaction->sender->host ); }; # may be <> eval { $dmarc->envelope_from( $transaction->sender->host ); }; # may be <>
$dmarc->spf( $transaction->notes('dmarc_spf') ); eval { $dmarc->spf( $transaction->notes('dmarc_spf') ); };
my $dkim = $self->connection->notes('dkim_verifier'); my $dkim = $self->connection->notes('dkim_verifier');
if ( $dkim ) { if ( $dkim ) { eval { $dmarc->dkim( $dkim ); }; };
eval { $dmarc->dkim( $dkim ); }
};
$dmarc->source_ip( $self->qp->connection->remote_ip ); $dmarc->source_ip( $self->qp->connection->remote_ip );
eval { $dmarc->validate(); }; eval { $dmarc->validate(); };
if ( $@ ) { if ( $EVAL_ERROR ) {
$self->log(LOGERROR, $@ ); $self->log(LOGERROR, $@ );
return DECLINED if $self->is_immune; return DECLINED if $self->is_immune;
$self->log(LOGINFO, "TODO: handle this validation failure"); return $self->get_reject( $@ );
return DECLINED;
return $self->get_reject( $@, $@ );
}; };
#$self->log(LOGINFO, "result: " . Dumper( $dmarc ) ); #$self->log(LOGINFO, "result: " . Dumper( $dmarc ) );

View File

@ -1,64 +1,46 @@
#!perl -w #!perl -w
use strict; use strict;
use English qw/-no_match_vars/;
use POSIX qw(strftime); use POSIX qw(strftime);
use Qpsmtpd::Address; use Qpsmtpd::Address;
use Qpsmtpd::Constants; use Qpsmtpd::Constants;
my $remote_ip = '66.128.51.165';
my $test_email = 'matt@tnpi.net'; my $test_email = 'matt@tnpi.net';
sub register_tests { sub register_tests {
my $self = shift; my $self = shift;
# TODO: test against newer DMARC plugin that uses Mail::DMARC eval 'use Mail::DMARC';
} if ($EVAL_ERROR) {
warn 'unable to load Mail::DMARC';
sub setup_test_headers { return;
my $self = shift;
my $transaction = $self->qp->transaction;
my $address = Qpsmtpd::Address->new( "<$test_email>" );
my $header = Mail::Header->new(Modify => 0, MailFrom => "COERCE");
my $now = strftime "%a %b %e %H:%M:%S %Y", localtime time;
$transaction->sender($address);
$transaction->header($header);
$transaction->header->add('From', "<$test_email>");
$transaction->header->add('Date', $now );
$transaction->body_write( "test message body " );
$self->qp->connection->relay_client(0);
}
sub test_fetch_dmarc_record {
my $self = shift;
foreach ( qw/ tnpi.net nictool.com / ) {
my @matches = $self->fetch_dmarc_record($_);
cmp_ok( scalar @matches, '==', 1, 'fetch_dmarc_record');
}
foreach ( qw/ example.com / ) {
my @matches = $self->fetch_dmarc_record($_);
cmp_ok( scalar @matches, '==', 0, 'fetch_dmarc_record');
} }
$self->register_test('_check_dmarc');
} }
sub test_get_organizational_domain { sub _check_dmarc {
my $self = shift; my $self = shift;
$self->setup_test_headers(); $self->qp->connection->remote_ip($remote_ip);
my $transaction = $self->qp->transaction; my $t = $self->qp->transaction;
$t->header(Mail::Header->new(Modify => 0, MailFrom => "COERCE"));
$t->sender(Qpsmtpd::Address->new( "<$test_email>" ));
$t->header->add('Date', strftime "%a %b %e %H:%M:%S %Y", localtime time);
$t->body_write( "test message body " );
cmp_ok( $self->get_organizational_domain('test.www.tnpi.net'), 'eq', 'tnpi.net' ); # no From header, reject as invalid message
cmp_ok( $self->get_organizational_domain('www.example.co.uk'), 'eq', 'example.co.uk' ); my ($rc, $msg) = $self->check_dmarc($t);
cmp_ok( $self->get_organizational_domain('plus.google.com'), 'eq', 'google.com' ); cmp_ok($rc, '==', DENY, "no From header, $msg");
}
sub test_discover_policy {
my $self = shift;
$self->setup_test_headers(); $t->header->add('From', "<$test_email>");
($rc, $msg) = $self->check_dmarc($t);
cmp_ok($rc, '==', DENY, "$msg");
cmp_ok($msg, 'eq', 'failed DMARC policy', 'check_dmarc, no SPF');
ok( $self->discover_policy( 'tnpi.net' ), 'discover_policy' ); #warn $self->qp->connection->notes('authentication_results');
} }