our
@ISA
=
qw()
;
my
$ARITH_EXPRESSION_LEXER
= ARITH_EXPRESSION_LEXER;
my
$META_RULES_MATCHING_RE
= META_RULES_MATCHING_RE;
sub
new {
my
$class
=
shift
;
$class
=
ref
(
$class
) ||
$class
;
my
(
$conf
) =
@_
;
my
$self
= {
'conf'
=>
$conf
};
$self
->{command_luts} = { };
bless
(
$self
,
$class
);
$self
;
}
sub
register_commands {
my
(
$self
,
$arrref
) =
@_
;
my
$conf
=
$self
->{conf};
$self
->set_defaults_from_command_list(
$arrref
);
$self
->build_command_luts(
$arrref
);
push
(@{
$conf
->{registered_commands}}, @{
$arrref
});
}
sub
set_defaults_from_command_list {
my
(
$self
,
$arrref
) =
@_
;
my
$conf
=
$self
->{conf};
foreach
my
$cmd
(@{
$arrref
}) {
if
(
exists
(
$cmd
->{
default
})) {
$conf
->{
$cmd
->{setting}} =
$cmd
->{
default
};
}
}
}
sub
build_command_luts {
my
(
$self
,
$arrref
) =
@_
;
my
$conf
=
$self
->{conf};
foreach
my
$cmd
(@{
$arrref
}) {
my
$cmdname
=
$cmd
->{command} ||
$cmd
->{setting};
$self
->{command_luts}->{
$cmdname
} =
$cmd
;
if
(
$cmd
->{aliases} &&
scalar
@{
$cmd
->{aliases}} > 0) {
foreach
my
$name
(@{
$cmd
->{aliases}}) {
$self
->{command_luts}->{
$name
} =
$cmd
;
}
}
}
}
sub
parse {
my
(
$self
,
undef
,
$scoresonly
) =
@_
;
my
$conf
=
$self
->{conf};
$self
->{scoresonly} =
$scoresonly
;
my
$lang
=
$ENV
{
'LANGUAGE'
};
if
(
$lang
) {
$lang
=~ s/:.*$//;
}
$lang
||=
$ENV
{
'LC_ALL'
};
$lang
||=
$ENV
{
'LC_MESSAGES'
};
$lang
||=
$ENV
{
'LANG'
};
$lang
||=
'C'
;
if
(
$lang
=~ /^(C|POSIX)$/) {
$lang
=
'en_US'
;
}
else
{
$lang
=~ s/[@.+,].*$//;
}
my
$lut
=
$self
->{command_luts};
my
%migrated_keys
=
map
{
$_
=> 1 }
@Mail::SpamAssassin::Conf::MIGRATED_SETTINGS
;
$self
->{currentfile} =
'(no file)'
;
$self
->{linenum} = ();
my
$skip_parsing
= 0;
my
@curfile_stack
;
my
@if_stack
;
my
@conf_lines
=
split
(/\n/,
$_
[1]);
my
$line
;
$self
->{if_stack} = \
@if_stack
;
$self
->{cond_cache} = { };
$self
->{file_scoped_attrs} = { };
my
$keepmetadata
=
$conf
->{main}->{keep_config_parsing_metadata};
while
(
defined
(
$line
=
shift
@conf_lines
)) {
local
($1);
my
$parse_error
;
$self
->{linenum}{
$self
->{currentfile}}++
if
index
(
$line
,
'file '
) != 0;
if
(
index
(
$line
,
'#'
) > -1) {
if
(
$keepmetadata
&&
$line
=~ /^\
$self
->{file_scoped_attrs}->{testrules}++;
next
;
}
if
(
index
(
$line
,
'# UPD'
) == 0 &&
$line
=~ /^\# UPDATE version (\d+)$/) {
for
(
$self
->{currentfile}) {
$conf
->{update_version}{
$_
} = $1
if
defined
$_
&&
$_
ne
'(no file)'
;
}
}
$line
=~ s/(?<!\\)
$line
=~ s/\\
}
$line
=~ s/^\s+//;
$line
=~ s/\s+$//;
next
unless
(
$line
);
if
(
index
(
$line
,
'lang'
) == 0 &&
$line
=~ s/^lang\s+(\S+)\s+//) {
next
if
$lang
!~ /^$1/i;
}
my
(
$key
,
$value
) =
split
(/\s+/,
$line
, 2);
$key
=
lc
$key
;
$key
=~
tr
/-/_/;
$value
=
''
unless
defined
(
$value
);
if
(
$key
eq
'endif'
) {
if
(
$value
ne
''
) {
$parse_error
=
"config: '$key' must be standalone"
;
goto
failed_line;
}
my
$lastcond
=
pop
@if_stack
;
if
(!
defined
$lastcond
) {
$parse_error
=
"config: missing starting 'if' for '$key'"
;
goto
failed_line;
}
$skip_parsing
=
$lastcond
->{skip_parsing};
next
;
}
elsif
(
$key
eq
'ifplugin'
) {
if
(
$value
eq
''
) {
$parse_error
=
"config: missing '$key' condition"
;
goto
failed_line;
}
$self
->handle_conditional (
$key
,
"plugin ($value)"
,
\
@if_stack
, \
$skip_parsing
);
next
;
}
elsif
(
$key
eq
'if'
) {
if
(
$value
eq
''
) {
$parse_error
=
"config: missing '$key' condition"
;
goto
failed_line;
}
$self
->handle_conditional (
$key
,
$value
,
\
@if_stack
, \
$skip_parsing
);
next
;
}
elsif
(
$key
eq
'file'
) {
if
(
$value
=~ /^start\s+(.+)$/) {
dbg(
"config: parsing file $1"
);
push
(
@curfile_stack
,
$self
->{currentfile});
$self
->{currentfile} = $1;
next
;
}
elsif
(
$value
=~ /^end\s/) {
foreach
(
@if_stack
) {
my
$msg
=
"config: unclosed '$_->{type}' found "
.
"in $self->{currentfile} (line $_->{linenum})"
;
$self
->lint_warn(
$msg
,
undef
);
}
$self
->{file_scoped_attrs} = { };
@if_stack
= ();
$skip_parsing
= 0;
$self
->{currentfile} =
pop
@curfile_stack
;
next
;
}
else
{
$parse_error
=
"config: missing '$key' value"
;
goto
failed_line;
}
}
elsif
(
$key
eq
'include'
) {
if
(
$value
eq
''
) {
$parse_error
=
"config: missing '$key' value"
;
goto
failed_line;
}
$value
=
$self
->fix_path_relative_to_current_file(
$value
);
my
$text
=
$conf
->{main}->read_cf(
$value
,
'included file'
);
unshift
(
@conf_lines
,
"file end $self->{currentfile}"
,
split
(/\n/,
$text
),
"file start $self->{currentfile}"
);
next
;
}
elsif
(
$key
eq
'else'
) {
if
(
$value
ne
''
) {
$parse_error
=
"config: '$key' must be standalone"
;
goto
failed_line;
}
if
(!
@if_stack
) {
$parse_error
=
"config: '$key' missing starting if"
;
goto
failed_line;
}
if
(
grep
{
$_
->{skip_parsing} }
@if_stack
) {
$skip_parsing
= 1;
}
else
{
$skip_parsing
= !
$skip_parsing
;
}
next
;
}
next
if
$skip_parsing
;
if
(
$key
eq
'require_version'
) {
if
(
$value
eq
''
) {
$parse_error
=
"config: missing '$key' value"
;
goto
failed_line;
}
next
if
(
$value
eq
"\@\@VERSION\@\@"
);
my
$ver
=
$Mail::SpamAssassin::VERSION
;
if
(
$ver
ne
$value
) {
my
$msg
=
"config: configuration file '$self->{currentfile}' requires "
.
"version $value of SpamAssassin, but this is code version "
.
"$ver. Maybe you need to use "
.
"the -C switch, or remove the old config files? "
.
"Skipping this file."
;
warn
$msg
;
$self
->lint_warn(
$msg
,
undef
);
$skip_parsing
= 1;
}
next
;
}
my
$cmd
=
$lut
->{
$key
};
if
(
$cmd
) {
if
(
$self
->{scoresonly}) {
if
(
$cmd
->{is_priv} && !
$conf
->{allow_user_rules}) {
info(
"config: not parsing, 'allow_user_rules' is 0: $line"
);
goto
failed_line;
}
if
(
$cmd
->{is_admin}) {
info(
"config: not parsing, administrator setting: $line"
);
goto
failed_line;
}
}
if
(!
$cmd
->{code}) {
if
(!
$self
->setup_default_code_cb(
$cmd
)) {
goto
failed_line;
}
}
my
$ret
= &{
$cmd
->{code}} (
$conf
,
$cmd
->{setting},
$value
,
$line
);
next
if
!
$ret
;
if
(
$ret
eq
$Mail::SpamAssassin::Conf::INVALID_VALUE
) {
$parse_error
=
"config: invalid '$key' value"
;
goto
failed_line;
}
elsif
(
$ret
eq
$Mail::SpamAssassin::Conf::INVALID_HEADER_FIELD_NAME
) {
$parse_error
=
"config: invalid header field name"
;
goto
failed_line;
}
elsif
(
$ret
eq
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
) {
$parse_error
=
"config: missing '$key' value"
;
goto
failed_line;
}
else
{
next
;
}
}
if
(
$conf
->{main}->call_plugins(
"parse_config"
, {
key
=>
$key
,
value
=>
$value
,
line
=>
$line
,
conf
=>
$conf
,
user_config
=>
$self
->{scoresonly}
}))
{
next
;
}
failed_line:
my
$msg
=
$parse_error
;
my
$is_error
= 1;
if
(!
$msg
) {
if
(
$migrated_keys
{
$key
}) {
$is_error
= 0;
$msg
=
"config: failed to parse line, now a plugin"
;
}
else
{
$msg
=
"config: failed to parse line"
;
}
}
if
(
$self
->{currentfile} eq
'(no file)'
) {
$msg
.=
" in $self->{currentfile}: $line"
;
}
else
{
$msg
.=
" in $self->{currentfile} "
.
"(line $self->{linenum}{$self->{currentfile}}): $line"
;
}
$self
->lint_warn(
$msg
,
undef
,
$is_error
);
}
delete
$self
->{if_stack};
delete
$self
->{cond_cache};
delete
$self
->{linenum};
$self
->lint_check();
$self
->fix_tests();
delete
$self
->{scoresonly};
}
sub
handle_conditional {
my
(
$self
,
$key
,
$value
,
$if_stack_ref
,
$skip_parsing_ref
) =
@_
;
my
$conf
=
$self
->{conf};
if
(
exists
$self
->{cond_cache}{
"$key $value"
}) {
push
(@{
$if_stack_ref
}, {
'type'
=>
$key
,
'conditional'
=>
$value
,
'skip_parsing'
=>
$$skip_parsing_ref
,
'linenum'
=>
$self
->{linenum}{
$self
->{currentfile}}
});
if
(
$self
->{cond_cache}{
"$key $value"
} == 0) {
$$skip_parsing_ref
= 1;
}
return
;
}
my
@tokens
= (
$value
=~ /(
$ARITH_EXPRESSION_LEXER
)/og);
my
$eval
=
''
;
foreach
my
$token
(
@tokens
) {
if
(
$token
eq
'('
||
$token
eq
')'
||
$token
eq
'!'
) {
my
$u
= untaint_var(
$token
);
$eval
.=
$u
.
" "
;
}
elsif
(
$token
eq
'plugin'
) {
$eval
.=
'$self->cond_clause_plugin_loaded'
;
}
elsif
(
$token
eq
'can'
) {
$eval
.=
'$self->cond_clause_can'
;
}
elsif
(
$token
eq
'has'
) {
$eval
.=
'$self->cond_clause_has'
;
}
elsif
(
$token
eq
'version'
) {
$eval
.=
$Mail::SpamAssassin::VERSION
.
" "
;
}
elsif
(
$token
eq
'perl_version'
) {
$eval
.= $].
" "
;
}
elsif
(
$token
eq
'local_tests_only'
) {
$eval
.=
'($self->{conf}->{main}->{local_tests_only}?1:0) '
}
elsif
(
$token
=~ /^(?:\W{1,5}|[+-]?\d+(?:\.\d+)?)$/) {
my
$u
= untaint_var(
$token
);
$eval
.=
$u
.
" "
;
}
elsif
(
$token
=~ /^\w[\w\:]+$/) {
if
(
$token
=~ /^(?:\w+::){0,10}\w+$/) {
if
(
$token
eq
'Mail::SpamAssassin::Plugin::Dmarc'
) {
$token
=
'Mail::SpamAssassin::Plugin::DMARC'
;
}
elsif
(
$token
eq
'Mail::SpamAssassin::Plugin::WhiteListSubject'
) {
$token
=
'Mail::SpamAssassin::Plugin::WelcomeListSubject'
;
}
my
$u
= untaint_var(
$token
);
$eval
.=
"'$u'"
;
}
else
{
my
$msg
=
"config: not allowed value '$token' "
.
"in $self->{currentfile} (line $self->{linenum}{$self->{currentfile}})"
;
$self
->lint_warn(
$msg
,
undef
);
return
;
}
}
else
{
my
$msg
=
"config: unparseable value '$token' "
.
"in $self->{currentfile} (line $self->{linenum}{$self->{currentfile}})"
;
$self
->lint_warn(
$msg
,
undef
);
return
;
}
}
push
(@{
$if_stack_ref
}, {
'type'
=>
$key
,
'conditional'
=>
$value
,
'skip_parsing'
=>
$$skip_parsing_ref
,
'linenum'
=>
$self
->{linenum}{
$self
->{currentfile}}
});
if
(
eval
$eval
) {
$self
->{cond_cache}{
"$key $value"
} = 1;
}
else
{
if
($@) {
my
$msg
=
"config: error parsing conditional "
.
"in $self->{currentfile} (line $self->{linenum}{$self->{currentfile}}): $eval ($@)"
;
warn
$msg
;
$self
->lint_warn(
$msg
,
undef
, 0);
}
$self
->{cond_cache}{
"$key $value"
} = 0;
$$skip_parsing_ref
= 1;
}
}
sub
cond_clause_plugin_loaded {
return
1
if
$_
[1] eq
'Mail::SpamAssassin::Plugin::RaciallyCharged'
;
return
$_
[0]->{conf}->{plugins_loaded}->{
$_
[1]};
}
sub
cond_clause_can {
my
(
$self
,
$method
) =
@_
;
if
(
$self
->{currentfile} =~
q!\buser_prefs$!
) {
warn
"config: 'if can $method' not available in user_prefs"
;
return
0
}
$self
->cond_clause_can_or_has(
'can'
,
$method
);
}
sub
cond_clause_has {
my
(
$self
,
$method
) =
@_
;
$self
->cond_clause_can_or_has(
'has'
,
$method
);
}
sub
cond_clause_can_or_has {
my
(
$self
,
$fn_name
,
$method
) =
@_
;
local
($1,$2);
if
(!
defined
$method
) {
my
$msg
=
"config: bad 'if' line, no argument to $fn_name() "
.
"in $self->{currentfile} (line $self->{linenum}{$self->{currentfile}})"
;
$self
->lint_warn(
$msg
,
undef
);
}
elsif
(
$method
=~ /^(.*)::([^:]+)$/) {
no
strict
"refs"
;
my
(
$module
,
$meth
) = ($1, $2);
return
1
if
$module
->can(
$meth
) &&
(
$fn_name
eq
'has'
|| &{
$method
}() );
}
else
{
my
$msg
=
"config: bad 'if' line, cannot find '::' in $fn_name($method) "
.
"in $self->{currentfile} (line $self->{linenum}{$self->{currentfile}})"
;
$self
->lint_warn(
$msg
,
undef
);
}
return
;
}
sub
lint_check {
my
(
$self
) =
@_
;
my
$conf
=
$self
->{conf};
if
(
$conf
->{lint_rules}) {
while
(
my
$k
=
each
%{
$conf
->{descriptions}} ) {
if
(!
exists
$conf
->{tests}->{
$k
}) {
dbg(
"config: description exists for non-existent rule $k"
);
}
}
while
(
my
(
$sk
) =
each
%{
$conf
->{scores}} ) {
if
(!
exists
$conf
->{tests}->{
$sk
}) {
dbg(
"config: score set for non-existent rule $sk"
);
}
}
}
}
sub
fix_tests {
my
(
$self
) =
@_
;
my
$conf
=
$self
->{conf};
my
$would_log_dbg
= would_log(
'dbg'
);
while
(
my
$k
=
each
%{
$conf
->{tests}} ) {
if
( !
exists
$conf
->{scores}->{
$k
} ) {
my
$set_score
=
index
(
$k
,
'T_'
) == 0 ? 0.01 : 1.0;
$set_score
= -
$set_score
if
( (
$conf
->{tflags}->{
$k
}||
''
) =~ /\bnice\b/ );
for
my
$index
(0..3) {
$conf
->{scoreset}->[
$index
]->{
$k
} =
$set_score
;
}
}
if
(
$would_log_dbg
&&
$k
!~ m/^(?:T_|__)/i) {
if
( !
exists
$conf
->{descriptions}->{
$k
} ) {
dbg(
"config: no description set for rule $k"
);
}
}
}
}
sub
setup_default_code_cb {
my
(
$self
,
$cmd
) =
@_
;
my
$type
=
$cmd
->{type};
if
(
$type
==
$Mail::SpamAssassin::Conf::CONF_TYPE_STRING
) {
$cmd
->{code} = \
&set_string_value
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::CONF_TYPE_BOOL
) {
$cmd
->{code} = \
&set_bool_value
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
) {
$cmd
->{code} = \
&set_numeric_value
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE
) {
$cmd
->{code} = \
&set_hash_key_value
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
) {
$cmd
->{code} = \
&set_addrlist_value
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::CONF_TYPE_TEMPLATE
) {
$cmd
->{code} = \
&set_template_append
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::CONF_TYPE_NOARGS
) {
$cmd
->{code} = \
&set_no_value
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::CONF_TYPE_STRINGLIST
) {
$cmd
->{code} = \
&set_string_list
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::CONF_TYPE_IPADDRLIST
) {
$cmd
->{code} = \
&set_ipaddr_list
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::CONF_TYPE_DURATION
) {
$cmd
->{code} = \
&set_duration_value
;
}
else
{
warn
"config: unknown conf type $type!"
;
return
0;
}
return
1;
}
sub
set_no_value {
my
(
$conf
,
$key
,
$value
,
$line
) =
@_
;
unless
(!
defined
$value
||
$value
eq
''
) {
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
}
sub
set_numeric_value {
my
(
$conf
,
$key
,
$value
,
$line
) =
@_
;
unless
(
defined
$value
&&
$value
!~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
unless
(
$value
=~ /^ [+-]? \d+ (?: \. \d* )? \z/sx) {
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
$conf
->{
$key
} = untaint_var(
$value
) + 0;
}
sub
set_duration_value {
my
(
$conf
,
$key
,
$value
,
$line
) =
@_
;
local
($1,$2);
unless
(
defined
$value
&&
$value
!~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
unless
(
$value
=~ /^( \+? \d+ (?: \. \d* )? ) (?: \s* ([smhdw]))? \z/sxi) {
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
$value
= $1;
$value
*= {
s
=> 1,
m
=> 60,
h
=> 3600,
d
=> 24*3600,
w
=> 7*24*3600 }->{
lc
$2}
if
defined
$2;
$conf
->{
$key
} = untaint_var(
$value
) + 0;
}
sub
set_bool_value {
my
(
$conf
,
$key
,
$value
,
$line
) =
@_
;
unless
(
defined
$value
&&
$value
!~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
$value
=
lc
$value
;
if
(
$value
eq
'yes'
||
$value
eq
'1'
) {
$value
= 1;
}
elsif
(
$value
eq
'no'
||
$value
eq
'0'
) {
$value
= 0;
}
else
{
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
$conf
->{
$key
} =
$value
;
}
sub
set_string_value {
my
(
$conf
,
$key
,
$value
,
$line
) =
@_
;
unless
(
defined
$value
&&
$value
!~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
$conf
->{
$key
} =
$value
;
}
sub
set_string_list {
my
(
$conf
,
$key
,
$value
,
$line
) =
@_
;
unless
(
defined
$value
&&
$value
!~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
push
(@{
$conf
->{
$key
}},
split
(/\s+/,
$value
));
}
sub
set_ipaddr_list {
my
(
$conf
,
$key
,
$value
,
$line
) =
@_
;
unless
(
defined
$value
&&
$value
!~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
foreach
my
$net
(
split
(/\s+/,
$value
)) {
$conf
->{
$key
}->add_cidr(
$net
);
}
$conf
->{
$key
.
'_configured'
} = 1;
}
sub
set_hash_key_value {
my
(
$conf
,
$key
,
$value
,
$line
) =
@_
;
my
(
$k
,
$v
) =
split
(/\s+/,
$value
, 2);
unless
(
defined
$v
&&
$v
ne
''
) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
$conf
->{
$key
}->{
$k
} =
$v
;
}
sub
set_addrlist_value {
my
(
$conf
,
$key
,
$value
,
$line
) =
@_
;
unless
(
defined
$value
&&
$value
!~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
$conf
->{parser}->add_to_addrlist (
$key
,
split
(/\s+/,
$value
));
}
sub
remove_addrlist_value {
my
(
$conf
,
$key
,
$value
,
$line
) =
@_
;
unless
(
defined
$value
&&
$value
!~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
$conf
->{parser}->remove_from_addrlist (
$key
,
split
(/\s+/,
$value
));
}
sub
set_template_append {
my
(
$conf
,
$key
,
$value
,
$line
) =
@_
;
if
(
$value
=~ /^
"(.*?)"
$/ ) {
$value
= $1; }
$conf
->{
$key
} .=
$value
.
"\n"
;
}
sub
set_template_clear {
my
(
$conf
,
$key
,
$value
,
$line
) =
@_
;
unless
(!
defined
$value
||
$value
eq
''
) {
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
$conf
->{
$key
} =
''
;
}
sub
finish_parsing {
my
(
$self
,
$isuserconf
) =
@_
;
my
$conf
=
$self
->{conf};
if
(!
$isuserconf
) {
$conf
->{main}->call_plugins(
"finish_parsing_start"
, {
conf
=>
$conf
});
}
else
{
$conf
->{main}->call_plugins(
"user_conf_parsing_start"
, {
conf
=>
$conf
});
}
$self
->compile_meta_rules();
$self
->fix_priorities();
$self
->fix_tflags();
dbg(
"config: finish parsing"
);
while
(
my
(
$name
,
$text
) =
each
%{
$conf
->{tests}}) {
my
$type
=
$conf
->{test_types}->{
$name
};
my
$priority
=
$conf
->{priority}->{
$name
} ?
$conf
->{priority}->{
$name
} :
(
$conf
->{tflags}->{
$name
}||
''
) =~ /\bnet\b/ ? -100 : 0;
$conf
->{priorities}->{
$priority
}++;
if
((
$type
& 1) == 1) {
if
(
my
(
$function
,
$args
) = (
$text
=~ /^(\w+)\((.*?)\)$/)) {
my
$argsref
=
$self
->pack_eval_args(
$args
);
if
(!
defined
$argsref
) {
$self
->lint_warn(
"syntax error for eval function $name: $text"
);
next
;
}
my
$expected_type
=
$conf
->{eval_plugins_types}->{
$function
};
if
(
defined
$expected_type
&&
$expected_type
!=
$type
) {
if
(!(
$expected_type
==
$Mail::SpamAssassin::Conf::TYPE_BODY_EVALS
&&
$type
==
$Mail::SpamAssassin::Conf::TYPE_RAWBODY_EVALS
))
{
my
$estr
=
$Mail::SpamAssassin::Conf::TYPE_AS_STRING
{
$expected_type
};
$self
->lint_warn(
"wrong rule type defined for $name, expected '$estr'"
);
next
;
}
}
if
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_BODY_EVALS
) {
$conf
->{body_evals}->{
$priority
}->{
$name
} = [
$function
, [
@$argsref
] ];
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
) {
$conf
->{head_evals}->{
$priority
}->{
$name
} = [
$function
, [
@$argsref
] ];
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_RBL_EVALS
) {
$conf
->{rbl_evals}->{
$name
} = [
$function
, [
@$argsref
] ];
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_RAWBODY_EVALS
) {
$conf
->{rawbody_evals}->{
$priority
}->{
$name
} = [
$function
, [
@$argsref
] ];
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_FULL_EVALS
) {
$conf
->{full_evals}->{
$priority
}->{
$name
} = [
$function
, [
@$argsref
] ];
}
else
{
$self
->lint_warn(
"unknown type $type for $name: $text"
,
$name
);
next
;
}
}
else
{
$self
->lint_warn(
"syntax error for eval function $name: $text"
,
$name
);
next
;
}
}
else
{
if
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_BODY_TESTS
) {
$conf
->{body_tests}->{
$priority
}->{
$name
} =
$text
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_HEAD_TESTS
) {
$conf
->{head_tests}->{
$priority
}->{
$name
} =
$text
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_META_TESTS
) {
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_URI_TESTS
) {
$conf
->{uri_tests}->{
$priority
}->{
$name
} =
$text
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_RAWBODY_TESTS
) {
$conf
->{rawbody_tests}->{
$priority
}->{
$name
} =
$text
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_FULL_TESTS
) {
$conf
->{full_tests}->{
$priority
}->{
$name
} =
$text
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_EMPTY_TESTS
) {
}
else
{
$self
->lint_warn(
"unknown type $type for $name: $text"
,
$name
);
next
;
}
}
}
$self
->lint_trusted_networks();
if
(!
$isuserconf
) {
$conf
->{main}->call_plugins(
"finish_parsing_end"
, {
conf
=>
$conf
});
}
else
{
$conf
->{main}->call_plugins(
"user_conf_parsing_end"
, {
conf
=>
$conf
});
}
$conf
->found_any_rules();
if
(!
$conf
->{allow_user_rules}) {
delete
$conf
->{tests};
delete
$conf
->{priority};
}
}
sub
expand_ruleglob {
my
(
$self
,
$ruleglob
,
$rulename
) =
@_
;
my
$expanded
;
if
(
exists
$self
->{ruleglob_cache}{
$ruleglob
}) {
$expanded
=
$self
->{ruleglob_cache}{
$ruleglob
};
}
else
{
my
$reglob
=
$ruleglob
;
$reglob
=~ s/\?/./g;
$reglob
=~ s/\*/.*?/g;
my
@rules
=
grep
{/^${reglob}$/ &&
$_
ne
$rulename
}
keys
%{
$self
->{conf}->{scores}};
if
(
@rules
) {
$expanded
=
join
(
'+'
,
sort
@rules
);
}
else
{
$expanded
=
'0'
;
}
}
my
$logstr
=
$expanded
eq
'0'
?
'no matches'
:
$expanded
;
dbg(
"rules: meta $rulename rules_matching($ruleglob) expanded: $logstr"
);
$self
->{ruleglob_cache}{
$ruleglob
} =
$expanded
;
return
" ($expanded) "
;
}
sub
compile_meta_rules {
my
(
$self
) =
@_
;
my
(
%meta
,
%meta_deps
,
%rule_deps
);
my
$conf
=
$self
->{conf};
foreach
my
$name
(
keys
%{
$conf
->{tests}}) {
next
unless
$conf
->{test_types}->{
$name
} ==
$Mail::SpamAssassin::Conf::TYPE_META_TESTS
;
my
$rule
=
$conf
->{tests}->{
$name
};
$rule
=~ s/${META_RULES_MATCHING_RE}/
$self
->expand_ruleglob($1,
$name
)/ge;
my
@tokens
= (
$rule
=~ /
$ARITH_EXPRESSION_LEXER
/og);
$meta
{
$name
} =
''
;
$meta_deps
{
$name
} = [ ];
$rule_deps
{
$name
} = [ ];
foreach
my
$token
(
@tokens
) {
if
(
$token
!~
tr
/+&|()!<>=//c) {
$meta
{
$name
} .=
"$token "
;
}
elsif
(
$token
eq
'local_tests_only'
) {
$meta
{
$name
} .=
'($_[0]->{main}->{local_tests_only}||0) '
;
}
elsif
(
$token
=~ IS_RULENAME) {
$meta
{
$name
} .=
"(\$_[1]->{'$token'}||0) "
;
if
(!
exists
$conf
->{test_types}->{
$token
}) {
dbg(
"rules: meta test $name has undefined dependency '$token'"
);
push
@{
$rule_deps
{
$name
}},
$token
;
next
;
}
if
(
$conf
->{scores}->{
$token
} == 0) {
unless
(((
$conf
->get_score_set()) & 1) == 0 &&
(
$conf
->{tflags}->{
$token
}||
''
) =~ /\bnet\b/)
{
dbg(
"rules: meta test $name has dependency '$token' with a zero score"
);
}
}
if
(
$conf
->{test_types}->{
$token
} ==
$Mail::SpamAssassin::Conf::TYPE_META_TESTS
) {
push
@{
$meta_deps
{
$name
}},
$token
;
}
push
@{
$rule_deps
{
$name
}},
$token
;
}
else
{
$meta
{
$name
} .=
"$token "
;
}
}
}
my
@metas
=
sort
{ @{
$meta_deps
{
$a
}} <=> @{
$meta_deps
{
$b
}} }
keys
%meta
;
my
$count
;
do
{
$count
=
$#metas
;
my
%metas
=
map
{
$_
=> 1 }
@metas
;
for
(
my
$i
= 0 ;
$i
<=
$#metas
;
$i
++) {
next
if
(
grep
(
$metas
{
$_
}, @{
$meta_deps
{
$metas
[
$i
] } }));
splice
@metas
,
$i
--, 1;
}
}
while
(
$#metas
!=
$count
&&
$#metas
> -1); # run
until
we can't go anymore
my
%unsolved_metas
=
map
{
$_
=> 1 }
@metas
;
foreach
my
$rulename_t
(
@metas
) {
my
$msg
=
"rules: excluding meta test $rulename_t, unsolved meta dependencies: "
.
join
(
", "
,
grep
(
$unsolved_metas
{
$_
}, @{
$meta_deps
{
$rulename_t
} }));
$self
->lint_warn(
$msg
);
}
foreach
my
$name
(
keys
%meta
) {
if
(
$unsolved_metas
{
$name
}) {
$conf
->{meta_tests}->{
$name
} =
sub
{ 0 };
$rule_deps
{
$name
} = [ ];
}
if
(
$meta
{
$name
} eq
'( ) '
) {
$meta
{
$name
} =
'0'
;
}
elsif
(@{
$rule_deps
{
$name
}}) {
$conf
->{meta_dependencies}->{
$name
} =
$rule_deps
{
$name
};
foreach
my
$deprule
(@{
$rule_deps
{
$name
}}) {
$conf
->{meta_deprules}->{
$deprule
}->{
$name
} = 1;
}
}
else
{
$conf
->{meta_nodeps}->{
$name
} = 1;
}
eval
'$conf->{meta_tests}->{$name} = sub { '
.
$meta
{
$name
}.
'};'
;
die
"rules: meta compilation failed for $name: '$meta{$name}': $@"
if
($@);
}
}
sub
fix_priorities {
my
(
$self
) =
@_
;
my
$conf
=
$self
->{conf};
return
unless
$conf
->{meta_dependencies};
my
$pri
=
$conf
->{priority};
my
$tflags
=
$conf
->{tflags};
foreach
my
$rule
(
sort
{
$pri
->{
$a
} <=>
$pri
->{
$b
} }
keys
%{
$pri
}) {
my
$deps
=
$conf
->{meta_dependencies}->{
$rule
};
next
unless
(
defined
$deps
);
my
$basepri
=
$pri
->{
$rule
};
foreach
my
$dep
(
@$deps
) {
my
$deppri
=
$pri
->{
$dep
};
if
(
defined
$deppri
&&
$deppri
>
$basepri
) {
if
(
$basepri
< -100 && (
$tflags
->{
$dep
}||
''
) =~ /\bnet\b/) {
dbg(
"rules: $rule (pri $basepri) requires $dep (pri $deppri): fixed to -100 (net rule)"
);
$pri
->{
$dep
} = -100;
$conf
->{priorities}->{-100}++;
}
else
{
dbg(
"rules: $rule (pri $basepri) requires $dep (pri $deppri): fixed"
);
$pri
->{
$dep
} =
$basepri
;
}
}
}
}
}
sub
fix_tflags {
my
(
$self
) =
@_
;
my
$conf
=
$self
->{conf};
my
$tflags
=
$conf
->{tflags};
while
(
my
(
$rulename
,
$deps
) =
each
%{
$conf
->{meta_dependencies}}) {
my
$tfl
=
$tflags
->{
$rulename
}||
''
;
next
if
$tfl
=~ /\bnet\b/;
foreach
my
$deprule
(
@$deps
) {
if
((
$tflags
->{
$deprule
}||
''
) =~ /\bnet\b/) {
dbg(
"rules: meta $rulename inherits tflag net, depends on $deprule"
);
$tflags
->{
$rulename
} =
$tfl
eq
''
?
'net'
:
"$tfl net"
;
last
;
}
}
}
}
sub
pack_eval_method {
warn
"deprecated function pack_eval_method() used\n"
;
return
(
''
,
undef
);
}
sub
pack_eval_args {
my
(
$self
,
$args
) =
@_
;
return
[]
if
$args
=~ /^\s+$/;
my
@args
;
local
($1,$2,$3);
while
(
$args
=~ s/^\s* (?: (['"]) (.*?) \1 | ( [\d\.:A-Za-z-]+? ) )
\s* (?: , \s* | $ )//x) {
push
@args
,
defined
$2 ? $2 : $3;
}
if
(
$args
ne
''
) {
return
undef
;
}
return
\
@args
;
}
sub
lint_trusted_networks {
my
(
$self
) =
@_
;
my
$conf
=
$self
->{conf};
my
(
$nt
,
$matching_against
);
if
(
$conf
->{trusted_networks_configured}) {
$nt
=
$conf
->{trusted_networks};
$matching_against
=
'trusted_networks'
;
}
elsif
(
$conf
->{internal_networks_configured}) {
$nt
=
$conf
->{internal_networks};
$matching_against
=
'internal_networks'
;
}
else
{
return
;
}
foreach
my
$net_type
(
'internal_networks'
,
'msa_networks'
) {
next
unless
$conf
->{
"${net_type}_configured"
};
next
if
$net_type
eq
$matching_against
;
my
$replace_nets
;
my
@valid_net_list
;
my
$net_list
=
$conf
->{
$net_type
};
foreach
my
$net
(@{
$net_list
->{nets}}) {
if
(!
$net
->{exclude} && !
$nt
->contains_net(
$net
)) {
my
$msg
=
"$matching_against doesn't contain $net_type entry '"
.
(
$net
->{as_string}).
"'"
;
$self
->lint_warn(
$msg
,
undef
);
$replace_nets
= 1;
}
else
{
push
@valid_net_list
,
$net
;
}
}
if
(
$replace_nets
) {
$net_list
->{nets} = \
@valid_net_list
;
}
}
}
sub
add_test {
my
(
$self
,
$name
,
$text
,
$type
) =
@_
;
my
$conf
=
$self
->{conf};
if
(
$name
!~ IS_RULENAME) {
$self
->lint_warn(
"config: error: rule '$name' has invalid characters "
.
"(not Alphanumeric + Underscore + starting with a non-digit)\n"
,
$name
);
return
;
}
if
(
length
$name
> 100) {
$self
->lint_warn(
"config: error: rule '$name' is too long "
.
"(recommended maximum length is 22 characters)\n"
,
$name
);
return
;
}
if
(
$conf
->{lint_rules}) {
if
(
length
(
$name
) > 40 &&
$name
!~ /^__/ &&
$name
!~ /^T_/) {
$self
->lint_warn(
"config: warning: rule name '$name' is over 40 chars "
.
"(recommended maximum length is 22 characters)\n"
,
$name
);
}
}
my
$ignore_amre
=
$self
->{conf}->{lint_rules} ||
$self
->{conf}->{ignore_always_matching_regexps};
if
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_BODY_TESTS
||
$type
==
$Mail::SpamAssassin::Conf::TYPE_URI_TESTS
||
$type
==
$Mail::SpamAssassin::Conf::TYPE_RAWBODY_TESTS
||
$type
==
$Mail::SpamAssassin::Conf::TYPE_FULL_TESTS
)
{
$self
->parse_captures(
$name
, \
$text
);
my
(
$rec
,
$err
) = compile_regexp(
$text
, 1,
$ignore_amre
);
if
(!
$rec
) {
$self
->lint_warn(
"config: invalid regexp for $name '$text': $err"
,
$name
);
return
;
}
$conf
->{test_qrs}->{
$name
} =
$rec
;
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_HEAD_TESTS
)
{
if
(
exists
$conf
->{tests}->{
$name
}) {
delete
$conf
->{test_opt_exists}->{
$name
};
delete
$conf
->{test_opt_unset}->{
$name
};
delete
$conf
->{test_opt_neg}->{
$name
};
}
local
($1,$2,$3);
if
(
$text
=~ /^
exists
:(.*)/) {
my
$hdr
= $1;
if
(
$hdr
!~ /^[\w.-]+:?$/) {
$self
->lint_warn(
"config: invalid head test $name header: $hdr"
);
return
;
}
$hdr
=~ s/:$//;
$conf
->{test_opt_header}->{
$name
} =
$hdr
;
$conf
->{test_opt_exists}->{
$name
} = 1;
}
else
{
if
(
$text
!~ /^([\w.-]+(?:\:|(?:\:[a-z]+){1,2})?)\s*([=!]~)\s*(.+)$/) {
$self
->lint_warn(
"config: invalid head test $name: $text"
);
return
;
}
my
(
$hdr
,
$op
,
$pat
) = ($1, $2, $3);
$hdr
=~ s/:$//;
if
(
$hdr
=~ /:(?!(?:raw|addr|name|host|domain|ip|revip|first|
last
)\b)/i) {
$self
->lint_warn(
"config: invalid header modifier for $name: $hdr"
,
$name
);
return
;
}
if
(
$pat
=~ s/\s+\[
if
-unset:\s+(.+)\]$//) {
$conf
->{test_opt_unset}->{
$name
} = $1;
}
$self
->parse_captures(
$name
, \
$pat
);
my
(
$rec
,
$err
) = compile_regexp(
$pat
, 1,
$ignore_amre
);
if
(!
$rec
) {
$self
->lint_warn(
"config: invalid regexp for $name '$pat': $err"
,
$name
);
return
;
}
$conf
->{test_qrs}->{
$name
} =
$rec
;
$conf
->{test_opt_header}->{
$name
} =
$hdr
;
$conf
->{test_opt_neg}->{
$name
} = 1
if
$op
eq
'!~'
;
}
}
elsif
(
$type
==
$Mail::SpamAssassin::Conf::TYPE_META_TESTS
)
{
if
(
$self
->is_meta_valid(
$name
,
$text
)) {
$text
= untaint_var(
$text
);
}
else
{
return
;
}
}
elsif
((
$type
& 1) == 1) {
if
(
my
(
$function
) = (
$text
=~ m/(.*?)\s*\(.*?\)\s*$/)) {
push
@{
$conf
->{eval_to_rule}->{
$function
}},
$name
;
}
}
$conf
->{tests}->{
$name
} =
$text
;
$conf
->{test_types}->{
$name
} =
$type
;
if
(
$name
=~ /^AUTOLEARNTEST/) {
dbg(
"config: auto-learn: $name has type $type = $conf->{test_types}->{$name} during add_test\n"
);
}
$conf
->{priority}->{
$name
} ||= 0;
if
(
$conf
->{main}->{keep_config_parsing_metadata}) {
$conf
->{source_file}->{
$name
} =
$self
->{currentfile};
$conf
->{if_stack}->{
$name
} =
$self
->get_if_stack_as_string();
if
(
$self
->{file_scoped_attrs}->{testrules}) {
$conf
->{testrules}->{
$name
} = 1;
}
}
if
(
$self
->{scoresonly}) {
$conf
->{want_rebuild_for_type}->{
$type
} = 2;
$conf
->{user_defined_rules}->{
$name
} = 1;
}
}
sub
add_regression_test {
my
(
$self
,
$name
,
$ok_or_fail
,
$string
) =
@_
;
my
$conf
=
$self
->{conf};
if
(
$conf
->{regression_tests}->{
$name
}) {
push
@{
$conf
->{regression_tests}->{
$name
}}, [
$ok_or_fail
,
$string
];
}
else
{
$conf
->{regression_tests}->{
$name
} = [ [
$ok_or_fail
,
$string
] ];
}
}
sub
is_meta_valid {
my
(
$self
,
$name
,
$rule
) =
@_
;
my
$meta
=
''
;
if
(
$rule
=~ /(?:\:\:|->|[\$\@\%\;\{\}])/) {
warn
(
"config: invalid meta $name rule: $rule\n"
);
return
0;
}
$rule
=~ s/${META_RULES_MATCHING_RE}/ 0 /g;
my
@tokens
= (
$rule
=~ /(
$ARITH_EXPRESSION_LEXER
)/og);
if
(
length
(
$name
) == 1) {
for
(
@tokens
) {
print
"$name $_\n "
or
die
"Error writing token: $!"
;
}
}
foreach
my
$token
(
@tokens
) {
if
(
$token
=~ IS_RULENAME) {
$meta
.=
"0 "
;
}
elsif
(
$token
=~ /^(\d+(?:\.\d+)?|[[:punct:]]{1,2})\z/s) {
$meta
.=
"$token "
;
}
else
{
$self
->lint_warn(
"config: invalid meta $name token: $token"
,
$name
);
return
0;
}
}
$meta
= untaint_var(
$meta
);
my
$evalstr
=
'my $x = '
.
$meta
.
'; 1;'
;
if
(
eval
$evalstr
) {
return
1;
}
my
$err
= $@ ne
''
? $@ :
"errno=$!"
;
chomp
$err
;
$err
=~ s/\s+(?:at|near)\b.*//s;
$err
=~ s/Illegal division by zero/division by zero possible/i;
$self
->lint_warn(
"config: invalid expression for rule $name: \"$rule\": $err\n"
,
$name
);
return
0;
}
sub
parse_captures {
my
(
$self
,
$name
,
$re
) =
@_
;
if
(
index
(
$$re
,
'%{'
) >= 0) {
local
($1);
while
(
$$re
=~ s/(?<!\\)\%\{([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*(?:\([^\)\}]*\))?)\}/%\\{$1\\}/g) {
dbg(
"config: found named capture for rule $name: $1"
);
$self
->{conf}->{capture_template_rules}->{
$name
}->{$1} = 1;
}
}
if
(
$$re
=~ /\(\?P?[<'][A-Z]/) {
dbg(
"config: adjusting regex capture rule $name priority to -10000"
);
$self
->{conf}->{priority}->{
$name
} = -10000;
$self
->{conf}->{capture_rules}->{
$name
} = 1;
}
}
sub
is_delimited_regexp_valid {
my
(
$self
,
$rule
,
$re
) =
@_
;
warn
"deprecated is_delimited_regexp_valid() called, use compile_regexp()\n"
;
my
(
$rec
,
$err
) = compile_regexp(
$re
, 1, 1);
return
$rec
;
}
sub
is_regexp_valid {
my
(
$self
,
$rule
,
$re
) =
@_
;
warn
"deprecated is_regexp_valid() called, use compile_regexp()\n"
;
my
(
$rec
,
$err
) = compile_regexp(
$re
, 1, 1);
return
$rec
;
}
sub
is_always_matching_regexp {
warn
"deprecated is_always_matching_regexp() called\n"
;
return
;
}
sub
add_to_addrlist {
my
(
$self
,
$singlelist
,
@addrs
) =
@_
;
my
$conf
=
$self
->{conf};
foreach
my
$addr
(
@addrs
) {
$addr
=
lc
$addr
;
my
$re
=
$addr
;
$re
=~ s/[\000\\\(]/_/gs;
$re
=~ s/([^\*\?_a-zA-Z0-9])/\\$1/g;
$re
=~
tr
/?/./;
$re
=~ s/\*+/\.\*/g;
my
(
$rec
,
$err
) = compile_regexp(
"^${re}\$"
, 0);
if
(!
$rec
) {
warn
"could not compile $singlelist '$addr': $err"
;
return
;
}
$conf
->{
$singlelist
}->{
$addr
} =
$rec
;
}
}
sub
add_to_addrlist_rcvd {
my
(
$self
,
$listname
,
$addr
,
$domain
) =
@_
;
my
$conf
=
$self
->{conf};
$domain
=
lc
$domain
;
$addr
=
lc
$addr
;
if
(
$conf
->{
$listname
}->{
$addr
}) {
push
@{
$conf
->{
$listname
}->{
$addr
}{domain}},
$domain
;
}
else
{
my
$re
=
$addr
;
$re
=~ s/[\000\\\(]/_/gs;
$re
=~ s/([^\*\?_a-zA-Z0-9])/\\$1/g;
$re
=~
tr
/?/./;
$re
=~ s/\*+/\.\*/g;
my
(
$rec
,
$err
) = compile_regexp(
"^${re}\$"
, 0);
if
(!
$rec
) {
warn
"could not compile $listname '$addr': $err"
;
return
;
}
$conf
->{
$listname
}->{
$addr
}{re} =
$rec
;
$conf
->{
$listname
}->{
$addr
}{domain} = [
$domain
];
}
}
sub
remove_from_addrlist {
my
(
$self
,
$singlelist
,
@addrs
) =
@_
;
my
$conf
=
$self
->{conf};
foreach
my
$addr
(
@addrs
) {
delete
(
$conf
->{
$singlelist
}->{
lc
$addr
});
}
}
sub
remove_from_addrlist_rcvd {
my
(
$self
,
$listname
,
@addrs
) =
@_
;
my
$conf
=
$self
->{conf};
foreach
my
$addr
(
@addrs
) {
delete
(
$conf
->{
$listname
}->{
lc
$addr
});
}
}
sub
add_to_addrlist_dkim {
add_to_addrlist_rcvd(
@_
);
}
sub
remove_from_addrlist_dkim {
my
(
$self
,
$listname
,
$addr
,
$domain
) =
@_
;
my
$conf
=
$self
->{conf};
my
$conf_lname
=
$conf
->{
$listname
};
$addr
=
lc
$addr
;
if
(
$conf_lname
->{
$addr
}) {
$domain
=
lc
$domain
;
my
$domains_listref
=
$conf_lname
->{
$addr
}{domain};
my
@replacement
=
grep
{
lc
$_
ne
$domain
}
@$domains_listref
;
if
(!
@replacement
) {
delete
(
$conf_lname
->{
$addr
});
}
elsif
(
@replacement
!=
@$domains_listref
) {
$conf_lname
->{
$addr
}{domain} = \
@replacement
;
}
}
}
sub
fix_path_relative_to_current_file {
my
(
$self
,
$path
) =
@_
;
$path
=
$self
->{conf}->{main}->sed_path(
$path
);
if
(!File::Spec->file_name_is_absolute (
$path
)) {
my
(
$vol
,
$dirs
,
$file
) = File::Spec->splitpath (
$self
->{currentfile});
$path
= File::Spec->catpath (
$vol
,
$dirs
,
$path
);
dbg(
"config: fixed relative path: $path"
);
}
return
$path
;
}
sub
lint_warn {
my
(
$self
,
$msg
,
$rule
,
$iserror
) =
@_
;
if
(!
defined
$iserror
) {
$iserror
= 1; }
if
(
$self
->{conf}->{main}->{lint_callback}) {
$self
->{conf}->{main}->{lint_callback}->(
msg
=>
$msg
,
rule
=>
$rule
,
iserror
=>
$iserror
);
}
elsif
(
$self
->{conf}->{lint_rules}) {
warn
$msg
.
"\n"
;
}
else
{
info(
$msg
);
}
if
(
$iserror
) {
$self
->{conf}->{errors}++;
}
}
sub
get_if_stack_as_string {
my
(
$self
) =
@_
;
return
join
' '
,
map
{
$_
->{conditional}
} @{
$self
->{if_stack}};
}
1;