our
@ISA
=
qw(Mail::SpamAssassin::Plugin)
;
my
%method_result
= (
'arc'
=> {
'fail'
=>1,
'none'
=>1,
'pass'
=>1},
'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'
=> {
'bestguesspass'
=>1,
'fail'
=>1,
'none'
=>1,
'pass'
=>1,
'permerror'
=>1,
'temperror'
=>1},
'dnswl'
=> {
'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
= (
'arc'
=> {
'smtp'
=> {
'remote-ip'
=>1},
'header'
=> {
'oldest-pass'
=>1},
'arc'
=> {
'chain'
=>1}},
'auth'
=> {
'smtp'
=> {
'auth'
=>1,
'mailfrom'
=>1}},
'dkim'
=> {
'header'
=> {
'd'
=>1,
'i'
=>1,
'b'
=>1,
'a'
=>1,
's'
=>1}},
'dkim-adsp'
=> {
'header'
=> {
'from'
=>1}},
'dkim-atps'
=> {
'header'
=> {
'from'
=>1}},
'dmarc'
=> {
'header'
=> {
'from'
=>1},
'policy'
=> {
'dmarc'
=>1}},
'dnswl'
=> {
'dns'
=> {
'zone'
=>1,
'sec'
=>1},
'policy'
=> {
'ip'
=>1,
'txt'
=>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,
'mfrom'
=>1,
'helo'
=>1,
'rcpttodomain'
=>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
0
unless
defined
$result
;
return
(
$wanted_result
eq
$result
) ? 1 : 0;
}
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,$2];
}
}
if
(!
@authres
) {
dbg(
"authres: no Authentication-Results headers found from %s"
,
$pms
->{conf}->{authres_networks});
return
0;
}
foreach
(
@authres
) {
eval
{
$self
->parse_authres(
$pms
,
$_
->[0],
$_
->[1]);
} 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
,
$hdrname
,
$hdr
) =
@_
;
dbg(
"authres: parsing $hdrname: $hdr"
);
my
$authserv
;
my
$version
= 1;
my
@methods
;
my
$arc_index
;
local
$_
=
$hdr
;
if
(
$hdrname
=~ /^ARC-/i) {
if
(!/\Gi\b/gcs) {
die
(
"missing arc index: $hdr"
);
}
skip_cfws();
if
(!/\G=/gcs) {
die
(
"invalid arc index: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
skip_cfws();
if
(!/\G(\d+)/gcs) {
die
(
"invalid arc index: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
$arc_index
= $1;
if
(
$arc_index
< 1 ||
$arc_index
> 50) {
die
(
"invalid arc index: $arc_index\n"
);
}
skip_cfws();
if
(!/\G;/gcs) {
die
(
"missing delimiter: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
skip_cfws();
}
if
(!/\G(
$TOKEN
)/gcs) {
die
(
"invalid authserv: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
$authserv
=
lc
($1);
if
(/\G=/gcs) {
die
(
"missing authserv: $hdr\n"
);
}
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: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
skip_cfws();
while
(
pos
() <
length
()) {
my
(
$method
,
$result
);
my
$reason
=
''
;
my
$props
= {};
if
(/\G\Q${authserv}\E\s*;/gcs) {
skip_cfws();
}
if
(/\Gnone\b/igcs) {
die
(
"method none\n"
);
}
if
(!/\G([\w-]+)/gcs) {
die
(
"invalid method: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
$method
=
lc
($1);
if
(!
exists
$method_result
{
$method
}) {
die
(
"unknown method: $method: $hdr\n"
);
}
skip_cfws();
if
(/\G\//gcs) {
skip_cfws();
if
(!/\G\d+/gcs) {
die
(
"invalid $method version: "
.
substr
(
$_
,
pos
()).
"\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: "
.
substr
(
$_
,
pos
()).
"\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: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
skip_cfws();
if
(!/\G
$QUOTED_STRING
|(
$TOKEN
)/gcs) {
die
(
"invalid reason: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
$reason
=
defined
$1 ? $1 : $2;
skip_cfws();
}
if
(/\Gaction\b/igcs) {
skip_cfws();
if
(!/\G=/gcs) {
die
(
"invalid action: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
skip_cfws();
if
(!/\G
$QUOTED_STRING
|
$TOKEN
/gcs) {
die
(
"invalid action: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
skip_cfws();
}
while
(
pos
() <
length
()) {
my
(
$ptype
,
$property
,
$value
);
if
(/\G(?:;|$)/gcs) {
skip_cfws();
last
;
}
if
(!/\G([\w-]+)/gcs) {
die
(
"invalid ptype: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
$ptype
=
lc
($1);
if
(!
exists
$method_ptype_prop
{
$method
}{
$ptype
}) {
die
(
"unknown ptype: $method/$ptype\n"
);
}
skip_cfws();
if
(!/\G\./gcs) {
die
(
"missing property: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
skip_cfws();
if
(!/\G([\w-]+)/gcs) {
die
(
"invalid property: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
$property
=
lc
($1);
if
(!
exists
$method_ptype_prop
{
$method
}{
$ptype
}{
$property
} &&
!
exists
$method_ptype_prop
{
$method
}{
$ptype
}{
'*'
}) {
die
(
"unknown property for $method/$ptype: $property\n"
);
}
skip_cfws();
if
(!/\G=/gcs) {
die
(
"missing property value: "
.
substr
(
$_
,
pos
()).
"\n"
);
}
skip_cfws();
if
(!/\G
$QUOTED_STRING
|(
$ATOM
(?:\.
$ATOM
)*|
$TOKEN
)(?=(?:[\s;]|$))/gcs) {
die
(
"invalid $method/$ptype.$property value: "
.
substr
(
$_
,
pos
()).
"\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
,
'arc_index'
=>
$arc_index
,
}];
}
if
(
pos
() <
length
()) {
die
(
"parse ended prematurely? "
.
substr
(
$_
,
pos
()).
"\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;