=head1 NAME
Mail::SpamAssassin::Conf::Parser - parse SpamAssassin configuration
=head1 SYNOPSIS
(see Mail::SpamAssassin)
=head1 DESCRIPTION
Mail::SpamAssassin is a module to identify spam using text analysis and
several internet-based realtime blacklists.
This class is used internally by SpamAssassin to parse its configuration files.
Please refer to the C<Mail::SpamAssassin> documentation for public interfaces.
=head1 STRUCTURE OF A CONFIG BLOCK
This is the structure of a config-setting block. Each is a hashref which may
contain these keys:
=over 4
=item setting
the name of the setting it modifies, e.g. "required_score". this also doubles
as the default for 'command' (below). THIS IS REQUIRED.
=item command
The command string used in the config file for this setting. Optional;
'setting' will be used for the command if this is omitted.
=item aliases
An [aryref] of other aliases for the same command. optional.
=item type
The type of this setting:
- $CONF_TYPE_STRING: string
- $CONF_TYPE_NUMERIC: numeric value (float or int)
- $CONF_TYPE_BOOL: boolean (0/no or 1/yes)
- $CONF_TYPE_TEMPLATE: template, like "report"
- $CONF_TYPE_ADDRLIST: address list, like "whitelist_from"
- $CONF_TYPE_HASH_KEY_VALUE: hash key/value pair,
like "describe" or tflags
If this is set, a 'code' block is assigned based on the type.
Note that C<$CONF_TYPE_HASH_KEY_VALUE>-type settings require that the
value be non-empty, otherwise they'll produce a warning message.
=item code
A subroutine to deal with the setting. Only used if B<type> is not set. ONE OF
B<code> OR B<type> IS REQUIRED. The arguments passed to the function are
C<($self, $key, $value, $line)>, where $key is the setting (*not* the command),
$value is the value string, and $line is the entire line.
There are two special return values that the B<code> subroutine may return
to signal that there is an error in the configuration:
C<$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE> -- this setting requires
that a value be set, but one was not provided.
C<$Mail::SpamAssassin::Conf::INVALID_VALUE> -- this setting requires a value
from a set of 'valid' values, but the user provided an invalid one.
Any other values -- including C<undef> -- returned from the subroutine are
considered to mean 'success'.
=item default
The default value for the setting. may be omitted if the default value is a
non-scalar type, which should be set in the Conf ctor. note for path types:
using "__userstate__" is recommended for defaults, as it allows
Mail::SpamAssassin module users who set that configuration setting, to receive
the correct values.
=item is_priv
Set to 1 if this setting requires 'allow_user_rules' when run from spamd.
=item is_admin
Set to 1 if this setting can only be set in the system-wide config when run
from spamd. (All settings can be used by local programs run directly by the
user.)
=item is_frequent
Set to 1 if this value occurs frequently in the config. this means it's looked
up first for speed.
=back
=head1 METHODS
=over 4
=cut
package Mail::SpamAssassin::Conf::Parser;
use Mail::SpamAssassin::Conf;
use Mail::SpamAssassin::Constants qw(:sa);
use Mail::SpamAssassin::Logger;
use strict;
use warnings;
use bytes;
use vars qw{
@ISA
};
@ISA = qw();
sub new {
my $class = shift;
$class = ref($class) || $class;
my ($conf) = @_;
my $self = {
'conf' => $conf
};
$self->{command_luts} = { };
$self->{command_luts}->{frequent} = { };
$self->{command_luts}->{remaining} = { };
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};
my $set;
foreach my $cmd (@{$arrref}) {
if ($cmd->{is_frequent}) { $set = 'frequent'; }
else { $set = 'remaining'; }
my $cmdname = $cmd->{command} || $cmd->{setting};
$self->{command_luts}->{$set}->{$cmdname} = $cmd;
if ($cmd->{aliases} && scalar @{$cmd->{aliases}} > 0) {
foreach my $name (@{$cmd->{aliases}}) {
$self->{command_luts}->{$set}->{$name} = $cmd;
}
}
}
}
sub parse {
my ($self, undef, $scoresonly) = @_;
$self->{scoresonly} = $scoresonly;
my $conf = $self->{conf};
my $lang = $ENV{'LANGUAGE'}; if ($lang) { $lang =~ s/:.*$//; # one language here, colon separated. We use the
} $lang ||= $ENV{'LC_ALL'};
$lang ||= $ENV{'LC_MESSAGES'};
$lang ||= $ENV{'LANG'};
$lang ||= 'C';
if ($lang =~ /^(C|POSIX)$/) {
$lang = 'en_US'; } else {
$lang =~ s/[@.+,].*$//; # Strip codeset, modifier/audience, etc.
}
my $lut_frequent = $self->{command_luts}->{frequent};
my $lut_remaining = $self->{command_luts}->{remaining};
my %migrated_keys = map { $_ => 1 }
@Mail::SpamAssassin::Conf::MIGRATED_SETTINGS;
$self->{currentfile} = '(no file)';
my $skip_parsing = 0;
my @curfile_stack = ();
my @if_stack = ();
my @conf_lines = split (/\n/, $_[1]);
my $line;
$self->{if_stack} = \@if_stack;
while (defined ($line = shift @conf_lines)) {
local ($1);
$line =~ s/(?<!\\) $line =~ s/\\ $line =~ s/^\s+//; # remove leading whitespace
$line =~ s/\s+$//; # remove tailing whitespace
next unless($line);
if ($line =~ s/^lang\s+(\S+)\s+//) { next if ($lang !~ /^$1/i); }
my($key, $value) = split(/\s+/, $line, 2);
$key = lc $key;
$key =~ s/-/_/g;
$value = '' unless defined($value);
$value =~ /^(.*)$/;
$value = $1;
my $parse_error;
if ($key eq 'file') {
if ($value =~ /^start\s+(.+)$/) {
push (@curfile_stack, $self->{currentfile});
$self->{currentfile} = $1;
next;
}
if ($value =~ /^end\s/) {
if (scalar @if_stack > 0) {
my $cond = pop @if_stack;
if ($cond->{type} eq 'if') {
my $msg = "config: unclosed 'if' in ".
$self->{currentfile}.": if ".$cond->{conditional}."\n";
warn $msg;
$self->lint_warn($msg, undef);
}
else {
die "config: unknown 'if' type: ".$cond->{type}."\n";
}
@if_stack = ();
}
$skip_parsing = 0;
my $curfile = pop @curfile_stack;
if (defined $curfile) {
$self->{currentfile} = $curfile;
} else {
$self->{currentfile} = '(no file)';
}
next;
}
}
if ($key eq 'include') {
$value = $self->fix_path_relative_to_current_file($value);
my $text = $conf->{main}->read_cf($value, 'included file');
unshift (@conf_lines, split (/\n/, $text));
next;
}
if ($key eq 'ifplugin') {
$self->handle_conditional ($key, "plugin ($value)",
\@if_stack, \$skip_parsing);
next;
}
if ($key eq 'if') {
$self->handle_conditional ($key, $value,
\@if_stack, \$skip_parsing);
next;
}
if ($key eq 'else') {
if (!@if_stack) {
$parse_error = "config: found else without matching conditional";
goto failed_line;
}
$skip_parsing = !$skip_parsing;
next;
}
if ($key eq 'endif') {
my $lastcond = pop @if_stack;
if (!defined $lastcond) {
$parse_error = "config: found endif without matching conditional";
goto failed_line;
}
$skip_parsing = $lastcond->{skip_parsing};
next;
}
next if $skip_parsing;
if ($key eq 'require_version') {
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_frequent->{$key}; if (!$cmd) {
$cmd = $lut_remaining->{$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);
if ($ret && $ret eq $Mail::SpamAssassin::Conf::INVALID_VALUE)
{
$parse_error = "config: SpamAssassin failed to parse line, ".
"\"$value\" is not valid for \"$key\", ".
"skipping: $line";
goto failed_line;
}
elsif ($ret && $ret eq $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE)
{
$parse_error = "config: SpamAssassin failed to parse line, ".
"no value provided for \"$key\", ".
"skipping: $line";
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, now a plugin, skipping, in \"$self->{currentfile}\": $line";
} else {
$msg = "config: failed to parse line, skipping, in \"$self->{currentfile}\": $line";
}
}
$self->lint_warn($msg, undef, $is_error);
}
delete $self->{if_stack};
$self->lint_check();
$self->set_default_scores();
delete $self->{scoresonly};
}
sub handle_conditional {
my ($self, $key, $value, $if_stack_ref, $skip_parsing_ref) = @_;
my $conf = $self->{conf};
my $lexer = ARITH_EXPRESSION_LEXER;
my @tokens = ($value =~ m/($lexer)/g);
my $eval = '';
my $bad = 0;
foreach my $token (@tokens) {
if ($token =~ /^(\W+|[+-]?\d+(?:\.\d+)?)$/) {
$eval .= $1." "; }
elsif ($token eq 'plugin') {
$eval .= "\$self->cond_clause_plugin_loaded";
}
elsif ($token eq 'version') {
$eval .= $Mail::SpamAssassin::VERSION." ";
}
elsif ($token =~ /^(\w[\w\:]+)$/) { $eval .= "\"$1\" "; }
else {
$bad++;
warn "config: unparseable chars in 'if $value': '$token'\n";
}
}
if ($bad) {
$self->lint_warn("bad 'if' line, in \"$self->{currentfile}\"", undef);
return -1;
}
push (@{$if_stack_ref}, {
type => 'if',
conditional => $value,
skip_parsing => $$skip_parsing_ref
});
if (eval $eval) {
} else {
$$skip_parsing_ref = 1;
}
}
sub cond_clause_plugin_loaded {
return $_[0]->{conf}->{plugins_loaded}->{$_[1]};
}
sub lint_check {
my ($self) = @_;
my $conf = $self->{conf};
if ($conf->{lint_rules}) {
while ( my $k = each %{$conf->{descriptions}} ) {
if (!exists $conf->{tests}->{$k}) {
$self->lint_warn("config: warning: description exists for non-existent rule $k\n", $k);
}
}
while ( my($sk) = each %{$conf->{scores}} ) {
if (!exists $conf->{tests}->{$sk}) {
$self->lint_warn("config: warning: score set for non-existent rule $sk\n", $sk);
}
}
}
}
sub set_default_scores {
my ($self) = @_;
my $conf = $self->{conf};
while ( my $k = each %{$conf->{tests}} ) {
if ( ! exists $conf->{scores}->{$k} ) {
my $set_score = ($k =~/^T_/) ? 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;
}
}
}
}
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;
}
else {
warn "config: unknown conf type $type!";
return 0;
}
return 1;
}
sub set_numeric_value {
my ($conf, $key, $value, $line) = @_;
unless (defined $value && $value !~ /^$/) {
return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
}
unless ($value =~ /^-?\d+(?:\.\d+)?$/) {
return $Mail::SpamAssassin::Conf::INVALID_VALUE;
}
$conf->{$key} = $value+0.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 = 1;
}
elsif ($value eq 'no') {
$value = 0;
}
unless ($value =~ /^[01]$/) {
return $Mail::SpamAssassin::Conf::INVALID_VALUE;
}
$conf->{$key} = $value+0;
}
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_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 (' ', $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 (' ', $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) = @_;
$conf->{$key} = '';
}
sub finish_parsing {
my ($self) = @_;
my $conf = $self->{conf};
$self->trace_meta_dependencies();
$self->fix_priorities();
$self->find_dup_rules();
dbg("conf: finish parsing");
while (my ($name, $text) = each %{$conf->{tests}}) {
my $type = $conf->{test_types}->{$name};
my $priority = $conf->{priority}->{$name} || 0;
$conf->{priorities}->{$priority}++;
if (($type & 1) == 1) {
if (my ($function, $args) = ($text =~ m/(.*?)\s*\((.*?)\)\s*$/)) {
my ($packed, $argsref) =
$self->pack_eval_method($function, $args, $name, $text);
if (!$packed) {
}
elsif ($type == $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS) {
$conf->{body_evals}->{$priority}->{$name} = $packed;
}
elsif ($type == $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS) {
$conf->{head_evals}->{$priority}->{$name} = $packed;
}
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} = $packed;
}
elsif ($type == $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS) {
$conf->{full_evals}->{$priority}->{$name} = $packed;
}
else {
$self->lint_warn("unknown type $type for $name: $text", $name);
}
}
else {
$self->lint_warn("syntax error for eval function $name: $text", $name);
}
}
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) {
$conf->{meta_tests}->{$priority}->{$name} = $text;
}
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;
}
else {
$self->lint_warn("unknown type $type for $name: $text", $name);
}
}
}
$self->lint_trusted_networks();
$conf->{main}->call_plugins("finish_parsing_end", { conf => $conf });
delete $conf->{tests};
delete $conf->{priority};
delete $conf->{test_types};
}
sub trace_meta_dependencies {
my ($self) = @_;
my $conf = $self->{conf};
$conf->{meta_dependencies} = { };
foreach my $name (keys %{$conf->{tests}}) {
next unless ($conf->{test_types}->{$name}
== $Mail::SpamAssassin::Conf::TYPE_META_TESTS);
my $deps = [ ];
my $alreadydone = { };
$self->_meta_deps_recurse($conf, $name, $name, $deps, $alreadydone);
$conf->{meta_dependencies}->{$name} = join (' ', @{$deps});
}
}
sub _meta_deps_recurse {
my ($self, $conf, $toprule, $name, $deps, $alreadydone) = @_;
return if $alreadydone->{$name};
$alreadydone->{$name} = 1;
my $rule = $conf->{tests}->{$name};
return unless $rule;
my $lexer = ARITH_EXPRESSION_LEXER;
my @tokens = ($rule =~ m/$lexer/g);
foreach my $token (@tokens) {
next if ($token =~ /^(?:\W+|[+-]?\d+(?:\.\d+)?)$/);
next unless exists $conf->{tests}->{$token};
push @{$deps}, $token;
$self->_meta_deps_recurse($conf, $toprule, $token, $deps, $alreadydone);
}
}
sub fix_priorities {
my ($self) = @_;
my $conf = $self->{conf};
die unless $conf->{meta_dependencies}; my $pri = $conf->{priority};
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 (split ' ', $deps) {
my $deppri = $pri->{$dep};
if ($deppri > $basepri) {
dbg("rules: $rule (pri $basepri) requires $dep (pri $deppri): fixed");
$pri->{$dep} = $basepri;
}
}
}
}
sub find_dup_rules {
my ($self) = @_;
my $conf = $self->{conf};
my %names_for_text = ();
my %dups = ();
while (my ($name, $text) = each %{$conf->{tests}}) {
my $type = $conf->{test_types}->{$name};
my $tf = ($conf->{tflags}->{$name}||''); $tf =~ s/\s+/ /gs;
$text = "$type\t$text\t$tf";
if (defined $names_for_text{$text}) {
$names_for_text{$text} .= " ".$name;
$dups{$text} = undef; } else {
$names_for_text{$text} = $name;
}
}
foreach my $text (keys %dups) {
my $first;
my $first_pri;
my @names = sort {$a cmp $b} split(' ', $names_for_text{$text});
foreach my $name (@names) {
my $priority = $conf->{priority}->{$name} || 0;
if (!defined $first || $priority < $first_pri) {
$first_pri = $priority;
$first = $name;
}
}
my @dups = ();
foreach my $name (@names) {
next if $name eq $first;
push @dups, $name;
delete $conf->{tests}->{$name};
}
dbg("rules: $first merged duplicates: ".join(' ', @dups));
$conf->{duplicate_rules}->{$first} = \@dups;
}
}
sub pack_eval_method {
my ($self, $function, $args, $name, $text) = @_;
my @args;
if ($args) {
while ($args =~ s/^\s*(?:['"](.*?)['"]|([\d\.:A-Za-z]+?))\s*(?:,\s*|$)//) {
if (defined $1) {
push @args, $1;
}
else {
push @args, $2;
}
}
}
if ($args) {
$self->lint_warn("syntax error (unparsable argument: $args) for eval function: $name: $text", $name);
return;
}
my $argstr = $function;
$argstr =~ s/\s+//gs;
if (scalar @args > 0) {
$argstr .= ',' . join (', ', map {
s/\#/[HASH]/gs; "q } @args);
}
return ($argstr, \@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 !~ /^\D\w*$/) {
$self->lint_warn("config: error: rule '$name' has invalid characters ".
"(not Alphanumeric + Underscore + starting with a non-digit)\n", $name);
return;
}
if (length $name > 200) {
$self->lint_warn("config: error: rule '$name' is way too long ".
"(recommended maximum length is 22 characters)\n", $name);
return;
}
if ($conf->{lint_rules}) {
if (length($name) > 50 && $name !~ /^__/ && $name !~ /^T_/) {
$self->lint_warn("config: warning: rule name '$name' is over 50 chars ".
"(recommended maximum length is 22 characters)\n", $name);
}
}
if ($type == $Mail::SpamAssassin::Conf::TYPE_BODY_TESTS ||
$type == $Mail::SpamAssassin::Conf::TYPE_FULL_TESTS ||
$type == $Mail::SpamAssassin::Conf::TYPE_RAWBODY_TESTS ||
$type == $Mail::SpamAssassin::Conf::TYPE_URI_TESTS)
{
return unless $self->is_delimited_regexp_valid($name, $text);
}
if ($type == $Mail::SpamAssassin::Conf::TYPE_HEAD_TESTS)
{
my ($pat) = ($text =~ /^\s*\S+\s*(?:\=|\!)\~\s*(\S.*?\S)\s*$/);
if ($pat) { $pat =~ s/\s+\[if-unset:\s+(.+)\]\s*$//; }
return unless $self->is_delimited_regexp_valid($name, $pat);
}
elsif ($type == $Mail::SpamAssassin::Conf::TYPE_META_TESTS)
{
return unless $self->is_meta_valid($name, $text);
}
$conf->{tests}->{$name} = $text;
$conf->{test_types}->{$name} = $type;
if ($type == $Mail::SpamAssassin::Conf::TYPE_META_TESTS) {
$conf->{priority}->{$name} ||= 500;
}
else {
$conf->{priority}->{$name} ||= 0;
}
$conf->{priority}->{$name} ||= 0;
$conf->{source_file}->{$name} = $self->{currentfile};
if ($self->{main}->{keep_config_parsing_metadata}) {
$conf->{if_stack}->{$name} = $self->get_if_stack_as_string();
}
if ($self->{scoresonly}) {
$conf->{user_rules_to_compile}->{$type} = 1;
$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 = '';
my $lexer = ARITH_EXPRESSION_LEXER;
my @tokens = ($rule =~ m/$lexer/g);
if (length($name) == 1) {
print "$name $_\n " for @tokens;
}
foreach my $token (@tokens) {
if ($token =~ /^(?:\W+|[+-]?\d+(?:\.\d+)?)$/) {
$meta .= "$token ";
}
else {
$meta .= "0 ";
}
}
my $evalstr = 'my $x = ' . $meta . '; 1;';
if (eval $evalstr) {
return 1;
}
if ($@) {
my $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 is_delimited_regexp_valid {
my ($self, $name, $re) = @_;
if (!$re || $re !~ /^\s*m?(\W).*(?:\1|>|}|\)|\])[a-z]*\s*$/) {
$re ||= '';
$self->lint_warn("config: invalid regexp for rule $name: $re: missing or invalid delimiters\n", $name);
return 0;
}
return $self->is_regexp_valid($name, $re);
}
sub is_regexp_valid {
my ($self, $name, $re) = @_;
my $origre = $re;
my $safere = $re;
my $mods = '';
if ($re =~ s/^m{//) {
$re =~ s/}([a-z]*)$//; $mods = $1;
}
elsif ($re =~ s/^m\(//) {
$re =~ s/\)([a-z]*)$//; $mods = $1;
}
elsif ($re =~ s/^m<//) {
$re =~ s/>([a-z]*)$//; $mods = $1;
}
elsif ($re =~ s/^m(\W)//) {
$re =~ s/\Q$1\E([a-z]*)$//; $mods = $1;
}
elsif ($re =~ s/^\/(.*)\/([a-z]*)$/$1/) {
$mods = $2;
}
else {
$safere = "m#".$re."#";
}
if ($mods) {
$re = "(?".$mods.")".$re;
}
if (eval { ("" =~ m return 1;
}
my $err = $@;
$err =~ s/ at .*? line \d.*$//;
$self->lint_warn("config: invalid regexp for rule $name: $origre: $err\n", $name);
return 0;
}
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; $conf->{$singlelist}->{$addr} = "^${re}\$";
}
}
sub add_to_addrlist_rcvd {
my ($self, $listname, $addr, $domain) = @_;
my $conf = $self->{conf};
$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; $conf->{$listname}->{$addr}{re} = "^${re}\$";
$conf->{$listname}->{$addr}{domain} = [ $domain ];
}
}
sub remove_from_addrlist {
my ($self, $singlelist, @addrs) = @_;
my $conf = $self->{conf};
foreach my $addr (@addrs) {
delete($conf->{$singlelist}->{$addr});
}
}
sub remove_from_addrlist_rcvd {
my ($self, $listname, @addrs) = @_;
my $conf = $self->{conf};
foreach my $addr (@addrs) {
delete($conf->{$listname}->{$addr});
}
}
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;