my
$VERSION
= 0.2;
our
@ISA
=
qw(Mail::SpamAssassin::Plugin)
;
sub
dbg {
my
$msg
=
shift
; Mail::SpamAssassin::Logger::dbg(
"DMARC: $msg"
,
@_
); }
sub
info {
my
$msg
=
shift
; Mail::SpamAssassin::Logger::info(
"DMARC: $msg"
,
@_
); }
sub
new {
my
(
$class
,
$mailsa
) =
@_
;
$class
=
ref
(
$class
) ||
$class
;
my
$self
=
$class
->SUPER::new(
$mailsa
);
bless
(
$self
,
$class
);
$self
->set_config(
$mailsa
->{conf});
$self
->register_eval_rule(
"check_dmarc_pass"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_dmarc_reject"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_dmarc_quarantine"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_dmarc_none"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_dmarc_missing"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
return
$self
;
}
sub
set_config {
my
(
$self
,
$conf
) =
@_
;
my
@cmds
;
push
(
@cmds
, {
setting
=>
'dmarc_save_reports'
,
default
=> 0,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_BOOL
,
});
$conf
->{parser}->register_commands(\
@cmds
);
}
sub
parsed_metadata {
my
(
$self
,
$opts
) =
@_
;
my
$pms
=
$opts
->{permsgstatus};
$pms
->{dmarc_async_queue} = [];
}
sub
_check_eval {
my
(
$self
,
$pms
,
$result
) =
@_
;
if
(
exists
$pms
->{dmarc_async_queue}) {
my
$rulename
=
$pms
->get_current_eval_rule_name();
push
@{
$pms
->{dmarc_async_queue}},
sub
{
if
(
$result
->()) {
$pms
->got_hit(
$rulename
,
''
,
ruletype
=>
'header'
);
}
else
{
$pms
->rule_ready(
$rulename
);
}
};
return
;
}
$self
->_check_dmarc(
$pms
);
return
$result
->() || 0;
}
sub
check_dmarc_pass {
my
(
$self
,
$pms
,
$name
) =
@_
;
my
$result
=
sub
{
defined
$pms
->{dmarc_result} &&
$pms
->{dmarc_result} eq
'pass'
&&
$pms
->{dmarc_policy} ne
'no policy available'
;
};
return
$self
->_check_eval(
$pms
,
$result
);
}
sub
check_dmarc_reject {
my
(
$self
,
$pms
,
$name
) =
@_
;
my
$result
=
sub
{
defined
$pms
->{dmarc_result} &&
$pms
->{dmarc_result} eq
'fail'
&&
$pms
->{dmarc_policy} eq
'reject'
;
};
return
$self
->_check_eval(
$pms
,
$result
);
}
sub
check_dmarc_quarantine {
my
(
$self
,
$pms
,
$name
) =
@_
;
my
$result
=
sub
{
defined
$pms
->{dmarc_result} &&
$pms
->{dmarc_result} eq
'fail'
&&
$pms
->{dmarc_policy} eq
'quarantine'
;
};
return
$self
->_check_eval(
$pms
,
$result
);
}
sub
check_dmarc_none {
my
(
$self
,
$pms
,
$name
) =
@_
;
my
$result
=
sub
{
defined
$pms
->{dmarc_result} &&
$pms
->{dmarc_result} eq
'fail'
&&
$pms
->{dmarc_policy} eq
'none'
;
};
return
$self
->_check_eval(
$pms
,
$result
);
}
sub
check_dmarc_missing {
my
(
$self
,
$pms
,
$name
) =
@_
;
my
$result
=
sub
{
defined
$pms
->{dmarc_result} &&
$pms
->{dmarc_policy} eq
'no policy available'
;
};
return
$self
->_check_eval(
$pms
,
$result
);
}
sub
check_tick {
my
(
$self
,
$opts
) =
@_
;
$self
->_check_async_queue(
$opts
->{permsgstatus});
}
sub
check_cleanup {
my
(
$self
,
$opts
) =
@_
;
$self
->_check_async_queue(
$opts
->{permsgstatus}, 1);
}
sub
_check_async_queue {
my
(
$self
,
$pms
,
$finish
) =
@_
;
return
unless
exists
$pms
->{dmarc_async_queue};
if
(
$finish
|| (
$pms
->{spf_checked} &&
$pms
->{dkim_checked_signature})) {
$self
->_check_dmarc(
$pms
);
$_
->()
foreach
(@{
$pms
->{dmarc_async_queue}});
delete
$pms
->{dmarc_async_queue};
}
}
sub
_check_dmarc {
my
(
$self
,
$pms
,
$name
) =
@_
;
if
(!
exists
$self
->{has_mail_dmarc}) {
my
$eval_stat
;
eval
{
} 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
;
}
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 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
->published->p;
}
else
{
dbg(
"result: no policy available"
);
$pms
->{dmarc_policy} =
'no policy available'
;
}
}
}
1;