The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

#!/usr/bin/perl
use strict;
$|++;
my %command_line_options = (
'author:s' => \my $author,
'from_dom:s' => \my $from,
'begin:s' => \my $begin,
'end:s' => \my $end,
'disposition:s' => \my $disposition,
'dkim:s' => \my $dkim,
'spf:s' => \my $spf,
'dns' => \my $dns_opt,
'geoip:s' => \my $geoip_opt,
'help' => \my $help,
'verbose' => \my $verbose,
);
GetOptions (%command_line_options);
use lib 'lib';
my $report = Mail::DMARC::Report->new;
my $gip;
pod2usage if $help;
my $reports = $report->store->retrieve(
(defined $from ? (from_domain => $from ) : () ),
(defined $author ? (author => $author ) : () ),
(defined $begin ? (begin => $begin ) : () ),
(defined $end ? (end => $end ) : () ),
);
print_header();
foreach my $report_ref ( reverse @$reports ) {
my $rows = $report->store->backend->get_rr( rid => $report_ref->{rid} )->{rows};
next if $disposition && ! grep { $_->{disposition} eq $disposition } @$rows;
next if $dkim && ! grep { $_->{dkim} eq $dkim } @$rows;
next if $spf && ! grep { $_->{spf } eq $spf } @$rows;
print_record($report_ref);
print_rows( $rows );
print "\n";
}
sub print_record {
my $rec = shift;
printf "%3s %26s %15s\n", @$rec{qw/ rid author begin /};
return;
};
sub print_rows {
my $rows = shift;
foreach my $row ( @$rows ) {
no warnings; ## no critic (NoWarn)
next if $disposition && $disposition ne $row->{disposition};
next if $dkim && $dkim ne $row->{dkim};
next if $spf && $spf ne $row->{spf};
printf " | -- %3s %20s %39s %13s %7s %7s", @$row{qw/ count header_from source_ip disposition dkim spf /};
foreach my $r ( @{ $row->{reasons} } ) {
print ' ' . $r->{type};
print "( $r->{comment} )" if $r->{comment};
};
my $geoip_details = get_geoip_details( $row->{source_ip} );
print " $geoip_details";
my $dns_hostname = get_dns_hostname( $row->{source_ip} );
print " $dns_hostname\n";
}
return;
};
sub print_header {
printf "%3s %26s %15s\n", qw[ ID Author Report-Start ];
printf " | -- %3s %20s %39s %13s %7s %7s\n", 'Qty','From','IP','Disposition','DKIM','SPF';
return;
};
sub get_geoip_details {
my $ip = shift;
return if ! defined $geoip_opt;
$geoip_opt ||= 'city,country_code,continent_code';
$gip ||= get_geoip_db();
return if ! $gip;
if ($ip =~ /^\d+\.\d+\.\d+\.\d+$/) {
$ip = '::ffff:'.$ip;
}
my $r = $gip->record_by_addr_v6($ip) or return '';
my @result;
my @fields = split(',', $geoip_opt);
my @allowed = qw(
country_code
country_code3
country_name
region
region_name
city
postal_code
latitude
longitude
time_zone
area_code
continent_code
metro_code
);
foreach my $f (@fields) {
next if ! grep {$_ eq $f} @allowed;
next if ! $r->${f}();
push @result, $r->${f}();
}
return join(', ', @result);
}
sub get_geoip_db {
return $gip if $gip;
eval "require Geo::IP"; ## no critic (Eval)
if ($@) {
warn "unable to load Geo::IP\n";
return;
};
foreach my $local ( '/usr/local', '/opt/local', '/usr' ) {
my $db_dir = "$local/share/GeoIP";
foreach my $db (qw/ GeoIPCityv6 GeoLiteCityv6 /) {
if (-f "$db_dir/$db.dat") {
print "using db $db" if $verbose;
$gip = Geo::IP->open("$db_dir/$db.dat");
}
last if $gip;
}
last if $gip;
};
return $gip;
}
sub get_dns_hostname {
my $ip = shift;
return if ! $dns_opt;
my @answers = $report->has_dns_rr('PTR', $ip);
return '' if 0 == scalar @answers;
return $answers[0] if scalar @answers >= 1;
print Dumper(\@answers);
return;
};
exit;
__END__
=head1 SYNOPSIS
dmarc_view_reports [ --option=value ]
Dumps the contents of the DMARC data store to your terminal. The most recent records are show first.
=head2 Search Options
author - report author (Yahoo! Inc, google.com, etc..)
from_dom - message sender domain
begin - epoch start time to display messages after
end - epoch end time to display messages before
disposition - DMARC disposition (none,quarantine,reject)
dkim - DKIM alignment result (pass/fail)
spf - SPF alignment result (pass/fail)
=head2 Other Options
dmarc_view_reports [ --geoip --dns --help --verbose ]
geoip - do GeoIP lookups (requires the free Maxmind GeoCityLitev6 database).
dns - do reverse DNS lookups and display hostnames
help - print this syntax guide
verbose - print additional debug info
=head1 EXAMPLES
To search for all reports from google.com that failed DMARC alignment:
dmarc_view_reports --author=google.com --dkim=fail --spf=fail
Note that we don't use --disposition. That would only tell us the result of applying DMARC policy, not necessarily if the messages failed DMARC alignment.
To display GeoIP lookup data for the source ip:
dmarc_view_reports --geoip
By default; city, country_code & continent_code are shown. You can optionally pass a comma delimited string to --geoip= with any of the following fields:
country_code
country_code3
country_name
region
region_name
city
postal_code
latitude
longitude
time_zone
area_code
continent_code
metro_code
dmarc_view_reports --geoip=country_name,continent_code
dmarc_view_reports --geoip=continent_code,country_name # keep order
dmarc_view_reports --geoip=city,city,city # repeat
=head1 SAMPLE OUTPUT
ID Recipient From/Sender Report-Start
| -- Qty Source IP Disposition DKIM SPF
570 theartfarm.com simerson.net 2013-05-20 09:40:50
| -- 1 75.126.200.152 quarantine fail fail
568 yeah.net tnpi.net 2013-05-21 09:00:00
| -- 1 111.176.77.138 reject fail fail
567 126.com tnpi.net 2013-05-21 09:00:00
| -- 1 49.73.135.125 reject fail fail
565 google.com mesick.us 2013-05-20 17:00:00
| -- 88 208.75.177.101 none pass pass
564 google.com theartfarm.com 2013-05-20 17:00:00
| -- 3 208.75.177.101 none pass pass
563 google.com lynboyer.com 2013-05-20 17:00:00
| -- 1 2a00:1450:4010:c03::235 none pass fail forwarded
| -- 12 208.75.177.101 none pass pass
| -- 1 209.85.217.174 none pass fail forwarded
561 google.com simerson.net 2013-05-20 17:00:00
| -- 1 208.75.177.101 none pass pass
560 google.com tnpi.net 2013-05-20 17:00:00
| -- 1 208.75.177.101 none pass pass
| -- 1 27.20.110.240 reject fail fail
559 hotmail.com lynboyer.com 2013-05-20 20:00:00
| -- 6 208.75.177.101 none pass pass
=head1 AUTHORS
=over 4
=item *
Matt Simerson <msimerson@cpan.org>
=item *
Davide Migliavacca <shari@cpan.org>
=item *
Marc Bradshaw <marc@marcbradshaw.net>
=back
=cut