our
@ISA
=
qw(Mail::SpamAssassin::Plugin)
;
my
%method_result
= (
'auth'
=> {
'fail'
=>1,
'none'
=>1,
'pass'
=>1,
'permerror'
=>1,
'temperror'
=>1},
'dkim'
=> {
'fail'
=>1,
'neutral'
=>1,
'none'
=>1,
'pass'
=>1,
'permerror'
=>1,
'policy'
=>1,
'temperror'
=>1},
'dkim-adsp'
=> {
'discard'
=>1,
'fail'
=>1,
'none'
=>1,
'nxdomain'
=>1,
'pass'
=>1,
'permerror'
=>1,
'temperror'
=>1,
'unknown'
=>1},
'dkim-atps'
=> {
'fail'
=>1,
'none'
=>1,
'pass'
=>1,
'permerror'
=>1,
'temperror'
=>1,
'neutral'
=>1},
'dmarc'
=> {
'fail'
=>1,
'none'
=>1,
'pass'
=>1,
'permerror'
=>1,
'temperror'
=>1},
'domainkeys'
=> {
'fail'
=>1,
'neutral'
=>1,
'none'
=>1,
'permerror'
=>1,
'policy'
=>1,
'pass'
=>1,
'temperror'
=>1},
'iprev'
=> {
'fail'
=>1,
'pass'
=>1,
'permerror'
=>1,
'temperror'
=>1},
'rrvs'
=> {
'fail'
=>1,
'none'
=>1,
'pass'
=>1,
'permerror'
=>1,
'temperror'
=>1,
'unknown'
=>1},
'sender-id'
=> {
'fail'
=>1,
'hardfail'
=>1,
'neutral'
=>1,
'none'
=>1,
'pass'
=>1,
'permerror'
=>1,
'policy'
=>1,
'softfail'
=>1,
'temperror'
=>1},
'smime'
=> {
'fail'
=>1,
'neutral'
=>1,
'none'
=>1,
'pass'
=>1,
'permerror'
=>1,
'policy'
=>1,
'temperror'
=>1},
'spf'
=> {
'fail'
=>1,
'hardfail'
=>1,
'neutral'
=>1,
'none'
=>1,
'pass'
=>1,
'permerror'
=>1,
'policy'
=>1,
'softfail'
=>1,
'temperror'
=>1},
'vbr'
=> {
'fail'
=>1,
'none'
=>1,
'pass'
=>1,
'permerror'
=>1,
'temperror'
=>1},
);
my
%method_ptype_prop
= (
'auth'
=> {
'smtp'
=> {
'auth'
=>1,
'mailfrom'
=>1}},
'dkim'
=> {
'header'
=> {
'd'
=>1,
'i'
=>1,
'b'
=>1}},
'dkim-adsp'
=> {
'header'
=> {
'from'
=>1}},
'dkim-atps'
=> {
'header'
=> {
'from'
=>1}},
'dmarc'
=> {
'header'
=> {
'from'
=>1}},
'domainkeys'
=> {
'header'
=> {
'd'
=>1,
'from'
=>1,
'sender'
=>1}},
'iprev'
=> {
'policy'
=> {
'iprev'
=>1}},
'rrvs'
=> {
'smtp'
=> {
'rcptto'
=>1}},
'sender-id'
=> {
'header'
=> {
'*'
=>1}},
'smime'
=> {
'body'
=> {
'smime-part'
=>1,
'smime-identifer'
=>1,
'smime-serial'
=>1,
'smime-issuer'
=>1}},
'spf'
=> {
'smtp'
=> {
'mailfrom'
=>1,
'helo'
=>1}},
'vbr'
=> {
'header'
=> {
'md'
=>1,
'mv'
=>1}},
);
my
$QUOTED_STRING
=
qr/"((?:[^"\\]++|\\.)*+)"?/
;
my
$TOKEN
=
qr/[^\s\x00-\x1f\x80-\xff\(\)\<\>\@\,\;\:\/
\[\]\?\=\"]+/;
my
$ATOM
=
qr/[a-zA-Z0-9\@\!\#\$\%\&\\\'\*\+\-\/
\=\?\^\_\`\{\|\}\~]+/;
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_method_priority(
"parsed_metadata"
, -10);
$self
->register_eval_rule(
"check_authres_result"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
return
$self
;
}
sub
set_config {
my
(
$self
,
$conf
) =
@_
;
my
@cmds
;
push
(
@cmds
, {
setting
=>
'authres_networks'
,
is_admin
=> 1,
default
=>
'internal'
,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_STRING
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
if
(!
defined
$value
||
$value
=~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
$value
=
lc
(
$value
);
if
(
$value
=~ /^(?:internal|trusted|all)$/) {
$self
->{authres_networks} =
$value
;
}
else
{
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
}
});
push
(
@cmds
, {
setting
=>
'authres_trusted_authserv'
,
is_admin
=> 1,
default
=> {},
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
if
(!
defined
$value
||
$value
=~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
foreach
my
$id
(
split
(/\s+/,
lc
$value
)) {
$self
->{authres_trusted_authserv}->{
$id
} = 1;
}
}
});
push
(
@cmds
, {
setting
=>
'authres_ignored_authserv'
,
is_admin
=> 1,
default
=> {},
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
if
(!
defined
$value
||
$value
=~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
foreach
my
$id
(
split
(/\s+/,
lc
$value
)) {
$self
->{authres_ignored_authserv}->{
$id
} = 1;
}
}
});
$conf
->{parser}->register_commands(\
@cmds
);
}
sub
check_authres_result {
my
(
$self
,
$pms
,
$method
,
$wanted_result
) =
@_
;
my
$result
=
$pms
->{authres_result}->{
$method
};
$wanted_result
=
lc
(
$wanted_result
);
if
(
$wanted_result
eq
'missing'
) {
return
!
defined
(
$result
) ? 1 : 0;
}
return
(
$wanted_result
eq
$result
);
}
sub
parsed_metadata {
my
(
$self
,
$opts
) =
@_
;
my
$pms
=
$opts
->{permsgstatus};
my
@authres
;
my
$nethdr
;
if
(
$pms
->{conf}->{authres_networks} eq
'internal'
) {
$nethdr
=
'ALL-INTERNAL'
;
}
elsif
(
$pms
->{conf}->{authres_networks} eq
'trusted'
) {
$nethdr
=
'ALL-TRUSTED'
;
}
else
{
$nethdr
=
'ALL'
;
}
foreach
my
$hdr
(
split
(/^/m,
$pms
->get(
$nethdr
))) {
if
(
$hdr
=~ /^(?:Arc\-)?Authentication-Results:\s*(.+)/i) {
push
@authres
, $1;
}
}
if
(!
@authres
) {
dbg(
"authres: no Authentication-Results headers found from %s"
,
$pms
->{conf}->{authres_networks});
return
0;
}
foreach
(
@authres
) {
eval
{
$self
->parse_authres(
$pms
,
$_
);
} or
do
{
dbg(
"authres: skipping header, $@"
);
}
}
$pms
->{authres_result} = {};
foreach
my
$method
(
keys
%method_result
) {
my
$parsed
=
$pms
->{authres_parsed}->{
$method
};
next
if
!
$parsed
;
foreach
my
$pref
(
@$parsed
) {
if
(!
$pms
->{authres_result}->{
$method
} ||
$pref
->{result} eq
'pass'
)
{
$pms
->{authres_result}->{
$method
} =
$pref
->{result};
}
}
}
if
(%{
$pms
->{authres_result}}) {
dbg(
"authres: results: %s"
,
join
(
' '
,
map
{
$_
.
'='
.
$pms
->{authres_result}->{
$_
} }
sort
keys
%{
$pms
->{authres_result}}));
}
else
{
dbg(
"authres: no results"
);
}
}
sub
parse_authres {
my
(
$self
,
$pms
,
$hdr
) =
@_
;
dbg(
"authres: parsing Authentication-Results: $hdr"
);
my
$authserv
;
my
$version
= 1;
my
@methods
;
local
$_
=
$hdr
;
if
(!/\G(
$TOKEN
)/gcs) {
die
(
"invalid authserv\n"
);
}
$authserv
=
lc
($1);
if
(%{
$pms
->{conf}->{authres_trusted_authserv}}) {
if
(!
$pms
->{conf}->{authres_trusted_authserv}->{
$authserv
}) {
die
(
"authserv not trusted: $authserv\n"
);
}
}
if
(
$pms
->{conf}->{authres_ignored_authserv}->{
$authserv
}) {
die
(
"ignored authserv: $authserv\n"
);
}
skip_cfws();
if
(/\G\d+/gcs) {
skip_cfws();
}
if
(!/\G;/gcs) {
die
(
"missing delimiter\n"
);
}
skip_cfws();
while
(
pos
() <
length
()) {
my
(
$method
,
$result
);
my
$reason
=
''
;
my
$props
= {};
if
(/\Gnone\b/igcs) {
die
(
"method none\n"
);
}
if
(!/\G([\w-]+)/gcs) {
die
(
"invalid method\n"
);
}
$method
=
lc
($1);
if
(!
exists
$method_result
{
$method
}) {
die
(
"unknown method: $method\n"
);
}
skip_cfws();
if
(/\G\//gcs) {
skip_cfws();
if
(!/\G\d+/gcs) {
die
(
"invalid $method version\n"
);
}
$version
= $1;
skip_cfws();
}
if
(!/\G=/gcs) {
die
(
"missing result for $method: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
skip_cfws();
if
(!/\G(\w+)/gcs) {
die
(
"invalid result for $method\n"
);
}
$result
= $1;
if
(!
exists
$method_result
{
$method
}{
$result
}) {
die
(
"unknown result for $method: $result\n"
);
}
skip_cfws();
if
(/\Greason\b/igcs) {
skip_cfws();
if
(!/\G=/gcs) {
die
(
"invalid reason\n"
);
}
skip_cfws();
if
(!/\G
$QUOTED_STRING
|(
$TOKEN
)/gcs) {
die
(
"invalid reason\n"
);
}
$reason
=
defined
$1 ? $1 : $2;
skip_cfws();
}
while
(
pos
() <
length
()) {
my
(
$ptype
,
$property
,
$value
);
if
(!/\G(\w+)/gcs) {
die
(
"invalid ptype: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
$ptype
=
lc
($1);
if
(!
exists
$method_ptype_prop
{
$method
}{
$ptype
}) {
die
(
"unknown ptype: $ptype\n"
);
}
skip_cfws();
if
(!/\G\./gcs) {
die
(
"missing property\n"
);
}
skip_cfws();
if
(!/\G(\w+)/gcs) {
die
(
"invalid property\n"
);
}
$property
=
lc
($1);
if
(!
exists
$method_ptype_prop
{
$method
}{
$ptype
}{
$property
} &&
!
exists
$method_ptype_prop
{
$method
}{
$ptype
}{
'*'
}) {
die
(
"unknown property for $ptype: $property\n"
);
}
skip_cfws();
if
(!/\G=/gcs) {
die
(
"missing property value\n"
);
}
skip_cfws();
if
(!/\G
$QUOTED_STRING
|(
$ATOM
(?:\.
$ATOM
)*|
$TOKEN
)(?=(?:[\s;]|$))/gcs) {
die
(
"invalid $ptype.$property value\n"
);
}
$value
=
defined
$1 ? $1 : $2;
skip_cfws();
$props
->{
$ptype
}->{
$property
} =
$value
;
if
(/\G(?:;|$)/gcs) {
skip_cfws();
last
;
}
}
push
@methods
, [
$method
, {
'authserv'
=>
$authserv
,
'version'
=>
$version
,
'result'
=>
$result
,
'reason'
=>
$reason
,
'properties'
=>
$props
,
}];
}
if
(
pos
() <
length
()) {
die
(
"parse ended prematurely?\n"
);
}
foreach
my
$marr
(
@methods
) {
push
@{
$pms
->{authres_parsed}->{
$marr
->[0]}},
$marr
->[1];
}
return
1;
}
sub
skip_cfws {
/\G\s*/gcs;
if
(/\G\(/gcs) {
my
$i
= 1;
while
(/\G.*?([()]|\z)/gcs) {
$1 eq
')'
?
$i
-- :
$i
++;
last
if
!
$i
;
}
die
(
"comment not ended\n"
)
if
$i
;
/\G\s*/gcs;
}
}
1;