in lib/Mail/SpamAssassin/Plugin/DMARC.pm [234:442]
sub _check_dmarc {
my ($self, $pms, $name) = @_;
return unless $pms->is_dns_available();
# Load DMARC module
if (!exists $self->{has_mail_dmarc}) {
my $eval_stat;
eval {
require Mail::DMARC::PurePerl;
} or do {
$eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
};
if (!defined($eval_stat)) {
dbg("using Mail::DMARC::PurePerl for DMARC checks");
$self->{has_mail_dmarc} = 1;
} else {
dbg("cannot load Mail::DMARC::PurePerl: module: $eval_stat");
dbg("Mail::DMARC::PurePerl is required for DMARC checks, DMARC checks disabled");
$self->{has_mail_dmarc} = undef;
}
}
return if !$self->{has_mail_dmarc};
return if $pms->{dmarc_checked};
$pms->{dmarc_checked} = 1;
my $lasthop = $pms->{relays_external}->[0];
if (!defined $lasthop) {
dbg("no external relay found, skipping DMARC check");
return;
}
my $from_addr = ($pms->get('From:first:addr'))[0];
return if not defined $from_addr;
return if index($from_addr, '@') == -1;
my $mfrom_domain = ($pms->get('EnvelopeFrom:first:addr:host'))[0];
if (!defined $mfrom_domain) {
$mfrom_domain = ($pms->get('From:first:addr:domain'))[0];
return if !defined $mfrom_domain;
dbg("EnvelopeFrom header not found, using From");
}
my $spf_status = 'none';
if ($pms->{spf_pass}) { $spf_status = 'pass'; }
elsif ($pms->{spf_fail}) { $spf_status = 'fail'; }
elsif ($pms->{spf_permerror}) { $spf_status = 'fail'; }
elsif ($pms->{spf_none}) { $spf_status = 'fail'; }
elsif ($pms->{spf_neutral}) { $spf_status = 'neutral'; }
elsif ($pms->{spf_softfail}) { $spf_status = 'softfail'; }
my $spf_helo_status = 'none';
if ($pms->{spf_helo_pass}) { $spf_helo_status = 'pass'; }
elsif ($pms->{spf_helo_fail}) { $spf_helo_status = 'fail'; }
elsif ($pms->{spf_helo_permerror}) { $spf_helo_status = 'fail'; }
elsif ($pms->{spf_helo_none}) { $spf_helo_status = 'fail'; }
elsif ($pms->{spf_helo_neutral}) { $spf_helo_status = 'neutral'; }
elsif ($pms->{spf_helo_softfail}) { $spf_helo_status = 'softfail'; }
my $dmarc = Mail::DMARC::PurePerl->new();
$dmarc->source_ip($lasthop->{ip});
$dmarc->header_from_raw($from_addr);
my $suppl_attrib = $pms->{msg}->{suppl_attrib};
if (defined $suppl_attrib && exists $suppl_attrib->{dkim_signatures}) {
my $dkim_signatures = $suppl_attrib->{dkim_signatures};
foreach my $signature ( @$dkim_signatures ) {
$dmarc->dkim( domain => $signature->domain, result => $signature->result );
dbg("DKIM result for domain " . $signature->domain . ": " . $signature->result);
}
} else {
$dmarc->dkim($pms->{dkim_verifier}) if (ref($pms->{dkim_verifier}));
}
my $result;
eval {
$dmarc->spf([
{
scope => 'mfrom',
domain => $mfrom_domain,
result => $spf_status,
},
{
scope => 'helo',
domain => $lasthop->{lc_helo},
result => $spf_helo_status,
},
]);
$result = $dmarc->validate();
};
if ($@) {
dbg("error while evaluating domain $mfrom_domain: $@");
return;
}
my $dmarc_arc_verified = 0;
if (($result->result ne 'pass') and (ref($pms->{arc_verifier}) and ($pms->{arc_verifier}->result eq 'pass'))) {
undef $result;
$dmarc_arc_verified = 1;
# if DMARC fails retry by reading data from AAR headers
# use Mail::SpamAssassin::Plugin::AuthRes if available to read ARC signature details
my @spf_parsed = sort { ( $a->{authres_parsed}{spf}{arc_index} // 0 ) <=> ( $b->{authres_parsed}{spf}{arc_index} // 0 ) } @{$pms->{authres_parsed}{spf} // []};
my $old_arc_index = 0;
foreach my $spf_parse ( @spf_parsed ) {
last if not defined $spf_parse->{arc_index};
last if $old_arc_index > $spf_parse->{arc_index};
dbg("Evaluate DMARC using AAR spf information for index $spf_parse->{arc_index}");
if(exists $spf_parse->{properties}{smtp}{mailfrom}) {
my $mfrom_dom = $spf_parse->{properties}{smtp}{mailfrom};
if($mfrom_dom =~ /\@(.*)/) {
$mfrom_dom = $1;
} else {
$mfrom_dom = $mfrom_domain
}
$dmarc->spf([
{
scope => 'mfrom',
domain => $mfrom_dom,
result => $spf_parse->{result},
}
]);
}
if(exists $spf_parse->{properties}{smtp}{helo}) {
$dmarc->spf([
{
scope => 'helo',
domain => $spf_parse->{properties}{smtp}{helo},
result => $spf_parse->{result},
}
]);
}
$old_arc_index = $spf_parse->{arc_index};
}
my @tmp_arc_seals;
my @arc_seals;
if(defined $pms->{arc_verifier}{seals}) {
@tmp_arc_seals = @{$pms->{arc_verifier}{seals}};
@arc_seals = sort { ( $a->{arc_verifier}{seals}{tags_by_name}{i}{value} // 0 ) <=> ( $b->{arc_verifier}{seals}{tags_by_name}{i}{value} // 0 ) } @tmp_arc_seals;
foreach my $seals ( @arc_seals ) {
if(exists($seals->{tags_by_name}{d}) and exists($pms->{arc_author_domains}->{$mfrom_domain})) {
dbg("Evaluate DMARC using AAR dkim information for index $seals->{tags_by_name}{i}{value} on domain $mfrom_domain and selector $seals->{tags_by_name}{s}{value}. Result is $seals->{verify_result}");
my $arc_result = $seals->{verify_result};
if($seals->{verify_result} eq 'invalid') {
$arc_result = 'permerror';
}
$dmarc->dkim(domain => $mfrom_domain, selector => $seals->{tags_by_name}{s}{value}, result => $arc_result);
last;
}
}
}
eval { $result = $dmarc->validate(); };
if ($@) {
dbg("error while validating domain $mfrom_domain: $@");
return;
}
}
if(defined $result and ($result->result ne 'none') and ($result->published->can('stringify'))) {
dbg("Found DMARC record \"" . $result->published->stringify . "\" for domain $mfrom_domain");
}
# Report that DMARC failed but it has been overridden because of AAR headers
if(ref($pms->{arc_verifier}) and ($pms->{arc_verifier}->result) and ($dmarc_arc_verified)) {
$result->reason->[0]{type} = 'local_policy';
$result->reason->[0]{comment} = "arc=" . $pms->{arc_verifier}->result;
my $cnt = 1;
foreach my $seals ( @{$pms->{arc_verifier}{seals}} ) {
if(exists($seals->{tags_by_name}{d}) and exists($seals->{tags_by_name}{s})) {
$result->reason->[0]{comment} .= " as[$cnt].d=$seals->{tags_by_name}{d}{value} as[$cnt].s=$seals->{tags_by_name}{s}{value}";
$cnt++;
}
}
if($cnt gt 1) {
$result->reason->[0]{comment} .= " remote-ip[1]=$lasthop->{ip}";
}
}
if (defined($pms->{dmarc_result} = $result->result)) {
if ($pms->{conf}->{dmarc_save_reports}) {
my $rua = eval { $result->published()->rua(); };
if (defined $rua && index($rua, 'mailto:') >= 0) {
eval { $dmarc->save_aggregate(); };
if ($@) {
info("report could not be saved: $@");
} else {
dbg("report will be sent to $rua");
}
}
}
if (defined $result->reason->[0]{comment} &&
$result->reason->[0]{comment} eq 'too many policies') {
dbg("result: no policy available (too many policies)");
$pms->{dmarc_policy} = 'no policy available';
} elsif ($result->result eq 'pass') {
dbg("result: pass");
$pms->{dmarc_policy} = $result->published->p;
} elsif ($result->result ne 'none') {
dbg("result: $result->{result}, disposition: $result->{disposition}, dkim: $result->{dkim}, spf: $result->{spf} (spf: $spf_status, spf_helo: $spf_helo_status)");
$pms->{dmarc_policy} = $result->disposition;
} else {
dbg("result: no policy available");
$pms->{dmarc_policy} = 'no policy available';
}
}
}