@ISA
=
qw(Mail::SpamAssassin::Plugin)
;
my
$VERSION
= 1.0;
sub
dbg {
my
$msg
=
shift
; Mail::SpamAssassin::Plugin::dbg(
"FromNameSpoof: $msg"
,
@_
); }
sub
new {
my
$class
=
shift
;
my
$mailsaobject
=
shift
;
$class
=
ref
(
$class
) ||
$class
;
my
$self
=
$class
->SUPER::new(
$mailsaobject
);
bless
(
$self
,
$class
);
$self
->set_config(
$mailsaobject
->{conf});
$self
->register_eval_rule(
"check_fromname_spoof"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_fromname_different"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_fromname_domain_differ"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_fromname_contains_email"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_fromname_equals_to"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_fromname_owners_differ"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_fromname_equals_replyto"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
return
$self
;
}
sub
set_config {
my
(
$self
,
$conf
) =
@_
;
my
@cmds
= ();
push
(
@cmds
, {
setting
=>
'fns_add_addrlist'
,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
local
($1,$2);
if
(
$value
!~ /^ \( (.+?) \) \s+ (.+) \z/sx) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
my
$listname
=
"FNS_"
.
lc
($1);
$self
->{parser}->add_to_addrlist(
$listname
,
split
(/\s+/,
lc
$2));
$self
->{fns_addrlists}{
$listname
} = 1;
}
});
push
(
@cmds
, {
setting
=>
'fns_remove_addrlist'
,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
local
($1,$2);
if
(
$value
!~ /^ \( (.+?) \) \s+ (.+) \z/sx) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
my
$listname
=
"FNS_"
.
lc
($1);
$self
->{parser}->remove_from_addrlist(
$listname
,
split
(/\s+/,
lc
$2));
}
});
push
(
@cmds
, {
setting
=>
'fns_extrachars'
,
default
=> 50,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
,
});
push
(
@cmds
, {
setting
=>
'fns_ignore_dkim'
,
default
=> {},
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
if
(
$value
eq
''
) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
$self
->{fns_ignore_dkim}->{
$_
} = 1
foreach
(
split
(/\s+/,
lc
$value
));
}
});
push
(
@cmds
, {
setting
=>
'fns_ignore_headers'
,
default
=> {},
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
if
(
$value
eq
''
) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
$self
->{fns_ignore_header}->{
$_
} = 1
foreach
(
split
(/\s+/,
$value
));
}
});
push
(
@cmds
, {
setting
=>
'fns_check'
,
default
=> 1,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
if
(
$value
eq
''
) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
if
(
$value
!~ /^[012]$/) {
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
$self
->{fns_check} =
$value
;
}
});
$conf
->{parser}->register_commands(\
@cmds
);
}
sub
parsed_metadata {
my
(
$self
,
$opts
) =
@_
;
my
$pms
=
$opts
->{permsgstatus};
if
(%{
$pms
->{conf}->{fns_ignore_dkim}}) {
if
(
$self
->{main}->{local_tests_only}) {
dbg(
"local tests only, ignoring fns_ignore_dkim setting"
);
}
elsif
(
exists
$pms
->{conf}->{dkim_timeout}) {
$pms
->{fromname_async_queue} = [];
$pms
->action_depends_on_tags(
'DKIMDOMAIN'
,
sub
{
$self
->_check_async_queue(
$pms
);
});
}
else
{
dbg(
"DKIM plugin not loaded, ignoring fns_ignore_dkim setting"
);
}
}
}
sub
_check_eval {
my
(
$self
,
$pms
,
$result
) =
@_
;
if
(
exists
$pms
->{fromname_async_queue}) {
my
$rulename
=
$pms
->get_current_eval_rule_name();
push
@{
$pms
->{fromname_async_queue}},
sub
{
if
(
$result
->()) {
$pms
->got_hit(
$rulename
,
''
,
ruletype
=>
'header'
);
}
else
{
$pms
->rule_ready(
$rulename
);
}
};
return
;
}
$self
->_check_fromnamespoof(
$pms
);
return
$result
->() || 0;
}
sub
check_fromname_spoof {
my
(
$self
,
$pms
,
$check_lvl
) =
@_
;
if
(!
defined
$check_lvl
||
$check_lvl
!~ /^[012]$/) {
$check_lvl
=
$pms
->{conf}->{fns_check};
}
my
$result
=
sub
{
my
@array
= (
(
$pms
->{fromname_address_different}),
(
$pms
->{fromname_address_different} &&
$pms
->{fromname_owner_different}),
(
$pms
->{fromname_address_different} &&
$pms
->{fromname_domain_different})
);
$array
[
$check_lvl
];
};
return
$self
->_check_eval(
$pms
,
$result
);
}
sub
check_fromname_different {
my
(
$self
,
$pms
) =
@_
;
my
$result
=
sub
{
$pms
->{fromname_address_different};
};
return
$self
->_check_eval(
$pms
,
$result
);
}
sub
check_fromname_domain_differ {
my
(
$self
,
$pms
) =
@_
;
my
$result
=
sub
{
$pms
->{fromname_domain_different};
};
return
$self
->_check_eval(
$pms
,
$result
);
}
sub
check_fromname_contains_email {
my
(
$self
,
$pms
) =
@_
;
my
$result
=
sub
{
$pms
->{fromname_contains_email};
};
return
$self
->_check_eval(
$pms
,
$result
);
}
sub
check_fromname_equals_to {
my
(
$self
,
$pms
) =
@_
;
my
$result
=
sub
{
$pms
->{fromname_equals_to_addr};
};
return
$self
->_check_eval(
$pms
,
$result
);
}
sub
check_fromname_owners_differ {
my
(
$self
,
$pms
) =
@_
;
my
$result
=
sub
{
$pms
->{fromname_owner_different};
};
return
$self
->_check_eval(
$pms
,
$result
);
}
sub
check_fromname_equals_replyto {
my
(
$self
,
$pms
) =
@_
;
my
$result
=
sub
{
$pms
->{fromname_equals_replyto};
};
return
$self
->_check_eval(
$pms
,
$result
);
}
sub
check_cleanup {
my
(
$self
,
$opts
) =
@_
;
$self
->_check_async_queue(
$opts
->{permsgstatus});
}
sub
_check_async_queue {
my
(
$self
,
$pms
) =
@_
;
if
(
exists
$pms
->{fromname_async_queue}) {
$self
->_check_fromnamespoof(
$pms
);
$_
->()
foreach
(@{
$pms
->{fromname_async_queue}});
delete
$pms
->{fromname_async_queue};
}
}
sub
_check_fromnamespoof {
my
(
$self
,
$pms
) =
@_
;
return
if
$pms
->{fromname_checked};
$pms
->{fromname_checked} = 1;
my
$conf
=
$pms
->{conf};
foreach
my
$addr
(
split
(/\s+/,
$pms
->get_tag(
'DKIMDOMAIN'
)||
''
)) {
if
(
$conf
->{fns_ignore_dkim}->{
lc
$addr
}) {
dbg(
"ignoring, DKIM signed: $addr"
);
return
;
}
}
foreach
my
$iheader
(
keys
%{
$conf
->{fns_ignore_header}}) {
if
(
$pms
->get(
$iheader
)) {
dbg(
"ignoring, header $iheader found"
);
return
;
}
}
my
$from_addr
=
lc
$pms
->get(
'From:addr'
);
my
$from_domain
=
$self
->{main}->{registryboundaries}->uri_to_domain(
"mailto:$from_addr"
);
return
unless
defined
$from_domain
;
my
$fromname
=
lc
$pms
->get(
'From:name'
);
my
(
$fromname_addr
,
$fromname_domain
);
if
(
$fromname
=~ /\b([\w\.\!\
$fromname_addr
= $1;
$fromname_domain
=
$self
->{main}->{registryboundaries}->uri_to_domain(
"mailto:$fromname_addr"
);
if
(!
defined
$fromname_domain
) {
dbg(
"no From-name addr found"
);
return
;
}
$pms
->{fromname_contains_email} = 1;
my
$nochar
= (
$fromname
=~ y/a-z0-9//c);
$nochar
-= (
$fromname_addr
=~ y/a-z0-9//c);
my
$len
=
length
(
$fromname
) +
$nochar
-
length
(
$fromname_addr
);
unless
(
$len
<=
$conf
->{fns_extrachars}) {
dbg(
"not enough closeness for From-name/addr: $fromname <=> $fromname_addr ($len <= $conf->{fns_extrachars})"
);
return
;
}
}
else
{
dbg(
"no From-name addr found"
);
return
;
}
my
$list_refs
= {};
if
(
$conf
->{fns_addrlists}) {
my
@lists
=
keys
%{
$conf
->{fns_addrlists}};
foreach
my
$list
(
@lists
) {
$list_refs
->{
$list
} =
$conf
->{
$list
};
}
dbg(
"using addrlists for owner aliases: "
.
join
(
', '
,
map
{ s/^FNS_//r; }
@lists
));
}
my
$fromname_owner
=
$self
->_find_address_owner(
$fromname_addr
,
$fromname_domain
,
$list_refs
);
my
$from_owner
=
$self
->_find_address_owner(
$from_addr
,
$from_domain
,
$list_refs
);
dbg(
"Parsed From-name addr/domain/owner: $fromname_addr/$fromname_domain/$fromname_owner"
);
dbg(
"Parsed From-addr addr/domain/owner: $from_addr/$from_domain/$from_owner"
);
if
(
$fromname_addr
ne
$from_addr
) {
dbg(
"From-name addr differs from From addr: $fromname_addr != $from_addr"
);
$pms
->{fromname_address_different} = 1;
}
if
(
$fromname_domain
ne
$from_domain
) {
dbg(
"From-name domain differs from From domain: $fromname_domain != $from_domain"
);
$pms
->{fromname_domain_different} = 1;
}
if
(
$fromname_owner
ne
$from_owner
) {
dbg(
"From-name owner differs from From owner: $fromname_owner != $from_owner"
);
$pms
->{fromname_owner_different} = 1;
}
my
$replyto_addr
=
lc
$pms
->get(
'Reply-To:addr'
);
if
(
$fromname_addr
eq
$replyto_addr
) {
dbg(
"From-name addr is same as Reply-To addr: $fromname_addr"
);
$pms
->{fromname_equals_replyto} = 1;
}
foreach
my
$to_addr
(
$pms
->all_to_addrs()) {
if
(
$fromname_addr
eq
$to_addr
) {
dbg(
"From-name addr is same as To addr: $fromname_addr"
);
$pms
->{fromname_equals_to_addr} = 1;
last
;
}
}
if
(
$pms
->{fromname_address_different} ||
$pms
->{fromname_owner_different}) {
$pms
->set_tag(
"FNSFNAMEADDR"
,
$fromname_addr
);
$pms
->set_tag(
"FNSFNAMEDOMAIN"
,
$fromname_domain
);
$pms
->set_tag(
"FNSFNAMEOWNER"
,
$fromname_owner
);
$pms
->set_tag(
"FNSFADDRADDR"
,
$from_addr
);
$pms
->set_tag(
"FNSFADDRDOMAIN"
,
$from_domain
);
$pms
->set_tag(
"FNSFADDROWNER"
,
$from_owner
);
}
}
sub
_find_address_owner {
my
(
$self
,
$addr
,
$addr_domain
,
$list_refs
) =
@_
;
foreach
my
$owner
(
keys
%{
$list_refs
}) {
foreach
my
$listaddr
(
keys
%{
$list_refs
->{
$owner
}}) {
if
(
$addr
=~
$list_refs
->{
$owner
}{
$listaddr
}) {
$owner
=~ s/^FNS_//;
return
lc
$owner
;
}
}
}
local
($1,$2);
if
(
$addr
=~ /^([^\@]+)\@(.+)$/) {
if
($2 ne
$addr_domain
) {
return
$self
->_find_address_owner(
"$1\@$addr_domain"
,
$addr_domain
,
$list_refs
);
}
}
if
(
$addr_domain
=~ /^([^.]+)\./) {
return
$1;
}
else
{
return
$addr_domain
;
}
}
1;