# SPDX-License-Identifier: GPL-2.0-or-later

package Amavis::Conf;
use strict;
use re 'taint';

# constants;  intentionally leave value -1 unassigned for compatibility
use constant D_TEMPFAIL => -4;
use constant D_REJECT   => -3;
use constant D_BOUNCE   => -2;
use constant D_DISCARD  =>  0;
use constant D_PASS     =>  1;

# major contents_category constants, in increasing order of importance
use constant CC_CATCHALL  => 0;
use constant CC_CLEAN     => 1;  # tag_level = "CC_CLEAN,1"
use constant CC_MTA       => 2;  # trouble passing mail back to MTA
use constant CC_OVERSIZED => 3;
use constant CC_BADH      => 4;
use constant CC_SPAMMY    => 5;  # tag2_level  (and: tag3_level = CC_SPAMMY,1)
use constant CC_SPAM      => 6;  # kill_level
use constant CC_UNCHECKED => 7;
use constant CC_BANNED    => 8;
use constant CC_VIRUS     => 9;
#
#  in other words:              major_ccat minor_ccat %subject_tag_maps_by_ccat
## if    score >= kill level  =>  CC_SPAM    0
## elsif score >= tag3 level  =>  CC_SPAMMY  1        @spam_subject_tag3_maps
## elsif score >= tag2 level  =>  CC_SPAMMY  0        @spam_subject_tag2_maps
## elsif score >= tag  level  =>  CC_CLEAN   1        @spam_subject_tag_maps
## else                       =>  CC_CLEAN   0

BEGIN {
  require Exporter;
  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  $VERSION = '2.412';
  @ISA = qw(Exporter);
  %EXPORT_TAGS = (
    'dynamic_confvars' =>  # per- policy bank settings
    [qw(
      $child_timeout $smtpd_timeout
      $policy_bank_name $protocol $haproxy_target_enabled @inet_acl
      $myhostname $myauthservid $snmp_contact $snmp_location
      $myprogram_name $syslog_ident $syslog_facility
      $log_level $log_templ $log_recip_templ $enable_log_capture_dump
      $forward_method $notify_method $resend_method $report_format
      $release_method $requeue_method $release_format
      $attachment_password $attachment_email_name $attachment_outer_name
      $mail_digest_algorithm $mail_part_digest_algorithm
      $os_fingerprint_method $os_fingerprint_dst_ip_and_port
      $originating @smtpd_discard_ehlo_keywords $soft_bounce
      $propagate_dsn_if_possible $terminate_dsn_on_notify_success
      $amavis_auth_user $amavis_auth_pass $auth_reauthenticate_forwarded
      $auth_required_out $auth_required_inp $auth_required_release
      @auth_mech_avail $tls_security_level_in $tls_security_level_out
      $local_client_bind_address $smtpd_message_size_limit
      $localhost_name $smtpd_greeting_banner $smtpd_quit_banner
      $mailfrom_to_quarantine $warn_offsite $bypass_decode_parts @decoders
      @av_scanners @av_scanners_backup @spam_scanners
      $first_infected_stops_scan $virus_scanners_failure_is_fatal
      $sa_spam_level_char $sa_mail_body_size_limit
      $penpals_bonus_score $penpals_halflife $bounce_killer_score
      $reputation_factor
      $undecipherable_subject_tag $localpart_is_case_sensitive
      $recipient_delimiter $replace_existing_extension
      $hdr_encoding $bdy_encoding $hdr_encoding_qb
      $allow_disclaimers $outbound_disclaimers_only
      $prepend_header_fields_hdridx
      $allow_fixing_improper_header
      $allow_fixing_improper_header_folding $allow_fixing_long_header_lines
      %allowed_added_header_fields %prefer_our_added_header_fields
      %allowed_header_tests
      $X_HEADER_TAG $X_HEADER_LINE
      $remove_existing_x_scanned_headers $remove_existing_spam_headers
      %sql_clause $partition_tag
      %local_delivery_aliases $banned_namepath_re
      $per_recip_whitelist_sender_lookup_tables
      $per_recip_blacklist_sender_lookup_tables
      @anomy_sanitizer_args @altermime_args_defang
      @altermime_args_disclaimer @disclaimer_options_bysender_maps
      %signed_header_fields @dkim_signature_options_bysender_maps
      $enable_dkim_verification $enable_dkim_signing $dkim_signing_service
      $dkim_minimum_key_bits $enable_ldap $enable_ip_repu $redis_logging_key
      $ip_repu_score_limit

      @local_domains_maps
      @mynetworks_maps @client_ipaddr_policy @ip_repu_ignore_maps
      @forward_method_maps @newvirus_admin_maps @banned_filename_maps
      @spam_quarantine_bysender_to_maps
      @spam_tag_level_maps @spam_tag2_level_maps @spam_tag3_level_maps
      @spam_kill_level_maps
      @spam_subject_tag_maps @spam_subject_tag2_maps @spam_subject_tag3_maps
      @spam_dsn_cutoff_level_maps @spam_dsn_cutoff_level_bysender_maps
      @spam_crediblefrom_dsn_cutoff_level_maps
      @spam_crediblefrom_dsn_cutoff_level_bysender_maps
      @spam_quarantine_cutoff_level_maps @spam_notifyadmin_cutoff_level_maps
      @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps
      @author_to_policy_bank_maps @signer_reputation_maps
      @message_size_limit_maps @debug_sender_maps @debug_recipient_maps
      @bypass_virus_checks_maps @bypass_spam_checks_maps
      @bypass_banned_checks_maps @bypass_header_checks_maps
      @viruses_that_fake_sender_maps
      @virus_name_to_spam_score_maps @virus_name_to_policy_bank_maps
      @remove_existing_spam_headers_maps
      @sa_userconf_maps @sa_username_maps

      %final_destiny_maps_by_ccat %forward_method_maps_by_ccat
      %lovers_maps_by_ccat %defang_maps_by_ccat %subject_tag_maps_by_ccat
      %quarantine_method_by_ccat %quarantine_to_maps_by_ccat
      %notify_admin_templ_by_ccat %notify_recips_templ_by_ccat
      %notify_sender_templ_by_ccat %notify_autoresp_templ_by_ccat
      %notify_release_templ_by_ccat %notify_report_templ_by_ccat
      %warnsender_by_ccat
      %hdrfrom_notify_admin_by_ccat %mailfrom_notify_admin_by_ccat
      %hdrfrom_notify_recip_by_ccat %mailfrom_notify_recip_by_ccat
      %hdrfrom_notify_sender_by_ccat
      %hdrfrom_notify_release_by_ccat %hdrfrom_notify_report_by_ccat
      %admin_maps_by_ccat %warnrecip_maps_by_ccat
      %always_bcc_by_ccat %dsn_bcc_by_ccat
      %addr_extension_maps_by_ccat %addr_rewrite_maps_by_ccat
      %smtp_reason_by_ccat
    )],
    'confvars' =>  # global settings (not per-policy, not per-recipient)
    [qw(
      $myproduct_name $myversion_id $myversion_id_numeric $myversion_date
      $myversion $instance_name @additional_perl_modules
      $MYHOME $TEMPBASE $QUARANTINEDIR $quarantine_subdir_levels
      $daemonize $courierfilter_shutdown $pid_file $lock_file $db_home
      $enable_db $enable_zmq @zmq_sockets $mail_id_size_bits
      $daemon_user @daemon_groups $daemon_chroot_dir $path
      $DEBUG %i_know_what_i_am_doing
      $do_syslog $logfile $allow_preserving_evidence $enable_log_capture
      $log_short_templ $log_verbose_templ $logline_maxlen
      $nanny_details_level $max_servers $max_requests
      $min_servers $min_spare_servers $max_spare_servers
      %current_policy_bank %policy_bank %interface_policy
      @listen_sockets $inet_socket_port $inet_socket_bind $listen_queue_size
      $smtpd_recipient_limit $unix_socketname $unix_socket_mode
      $smtp_connection_cache_on_demand $smtp_connection_cache_enable
      %smtp_tls_client_verifycn_name_maps
      %smtp_tls_client_options %smtpd_tls_server_options
      $smtpd_tls_cert_file $smtpd_tls_key_file $macro_tests_sanity_limit
      $enforce_smtpd_message_size_limit_64kb_min
      $MAXLEVELS $MAXFILES
      $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR
      $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR
      $database_sessions_persistent $lookup_maps_imply_sql_and_ldap
      @lookup_sql_dsn @storage_sql_dsn @storage_redis_dsn
      $storage_redis_ttl $redis_logging_queue_size_limit
      $sql_schema_version $timestamp_fmt_mysql
      $sql_quarantine_chunksize_max $sql_allow_8bit_address
      $sql_lookups_no_at_means_domain $ldap_lookups_no_at_means_domain
      $sql_store_info_for_all_msgs $default_ldap
      $trim_trailing_space_in_lookup_result_fields
      @keep_decoded_original_maps @map_full_type_to_short_type_maps
      %banned_rules $penpals_threshold_low $penpals_threshold_high
      %dkim_signing_keys_by_domain
      @dkim_signing_keys_list @dkim_signing_keys_storage
      $file $altermime $enable_anomy_sanitizer
    )],
    'sa' =>  # global SpamAssassin settings
    [qw(
      $spamcontrol_obj $sa_num_instances
      $helpers_home $sa_configpath $sa_siteconfigpath $sa_userprefs_file
      $sa_local_tests_only $sa_timeout $sa_debug
      $dspam $sa_spawned
    )],
    'platform' => [qw(
      $profiling $can_truncate $my_pid
      $AF_INET6 $have_inet4 $have_inet6 $io_socket_module_name
      &D_TEMPFAIL &D_REJECT &D_BOUNCE &D_DISCARD &D_PASS
      &CC_CATCHALL &CC_CLEAN &CC_MTA &CC_OVERSIZED &CC_BADH
      &CC_SPAMMY &CC_SPAM &CC_UNCHECKED &CC_BANNED &CC_VIRUS
      %ccat_display_names %ccat_display_names_major
    )],
    # other variables settable by user in amavisd.conf,
    # but not directly accessible to the program
    'hidden_confvars' => [qw(
      $mydomain
    )],
    'legacy_dynamic_confvars' =>
      # the rest of the program does not use these settings directly and they
      # should not be visible in, or imported to other modules, but may be
      # referenced indirectly through *_by_ccat variables for compatibility
    [qw(
      $final_virus_destiny $final_banned_destiny $final_unchecked_destiny
      $final_spam_destiny $final_bad_header_destiny
      @virus_lovers_maps @spam_lovers_maps @unchecked_lovers_maps
      @banned_files_lovers_maps @bad_header_lovers_maps
      $always_bcc $dsn_bcc
      $mailfrom_notify_sender $mailfrom_notify_recip
      $mailfrom_notify_admin  $mailfrom_notify_spamadmin
      $hdrfrom_notify_sender  $hdrfrom_notify_recip
      $hdrfrom_notify_admin   $hdrfrom_notify_spamadmin
      $hdrfrom_notify_release $hdrfrom_notify_report
      $notify_virus_admin_templ  $notify_spam_admin_templ
      $notify_virus_recips_templ $notify_spam_recips_templ
      $notify_virus_sender_templ $notify_spam_sender_templ
      $notify_sender_templ $notify_release_templ
      $notify_report_templ $notify_autoresp_templ
      $warnbannedsender $warnbadhsender
      $defang_virus $defang_banned $defang_spam
      $defang_bad_header $defang_undecipherable $defang_all
      $virus_quarantine_method $banned_files_quarantine_method
      $unchecked_quarantine_method $spam_quarantine_method
      $bad_header_quarantine_method $clean_quarantine_method
      $archive_quarantine_method
      @virus_quarantine_to_maps @banned_quarantine_to_maps
      @unchecked_quarantine_to_maps @spam_quarantine_to_maps
      @bad_header_quarantine_to_maps @clean_quarantine_to_maps
      @archive_quarantine_to_maps
      @virus_admin_maps @banned_admin_maps
      @spam_admin_maps @bad_header_admin_maps @spam_modifies_subj_maps
      @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps
      @addr_extension_virus_maps  @addr_extension_spam_maps
      @addr_extension_banned_maps @addr_extension_bad_header_maps
    )],
    'legacy_confvars' =>
      # legacy variables, predeclared for compatibility of amavisd.conf
      # The rest of the program does not use them directly and they should
      # not be visible in other modules, but may be referenced through
      # @*_maps variables for backward compatibility
    [qw(
      %local_domains @local_domains_acl $local_domains_re
      @mynetworks @ip_repu_ignore_networks
      %bypass_virus_checks @bypass_virus_checks_acl $bypass_virus_checks_re
      %bypass_spam_checks @bypass_spam_checks_acl $bypass_spam_checks_re
      %bypass_banned_checks @bypass_banned_checks_acl $bypass_banned_checks_re
      %bypass_header_checks @bypass_header_checks_acl $bypass_header_checks_re
      %virus_lovers @virus_lovers_acl $virus_lovers_re
      %spam_lovers @spam_lovers_acl $spam_lovers_re
      %banned_files_lovers @banned_files_lovers_acl $banned_files_lovers_re
      %bad_header_lovers @bad_header_lovers_acl $bad_header_lovers_re
      %virus_admin %spam_admin
      $newvirus_admin $virus_admin $banned_admin $bad_header_admin $spam_admin
      $warnvirusrecip $warnbannedrecip $warnbadhrecip
      $virus_quarantine_to $banned_quarantine_to $unchecked_quarantine_to
      $spam_quarantine_to $spam_quarantine_bysender_to
      $bad_header_quarantine_to $clean_quarantine_to $archive_quarantine_to
      $keep_decoded_original_re $map_full_type_to_short_type_re
      $banned_filename_re $viruses_that_fake_sender_re
      $sa_tag_level_deflt $sa_tag2_level_deflt $sa_tag3_level_deflt
      $sa_kill_level_deflt
      $sa_quarantine_cutoff_level @spam_notifyadmin_cutoff_level_maps
      $sa_dsn_cutoff_level $sa_crediblefrom_dsn_cutoff_level
      $sa_spam_modifies_subj $sa_spam_subject_tag1 $sa_spam_subject_tag
      %whitelist_sender @whitelist_sender_acl $whitelist_sender_re
      %blacklist_sender @blacklist_sender_acl $blacklist_sender_re
      $addr_extension_virus $addr_extension_spam
      $addr_extension_banned $addr_extension_bad_header
      $sql_select_policy $sql_select_white_black_list
      $gets_addr_in_quoted_form @debug_sender_acl
      $arc $bzip2 $lzop $lha $unarj $gzip $uncompress $unfreeze
      $unrar $zoo $pax $cpio $ar $rpm2cpio $cabextract $ripole $tnef
      $gunzip $bunzip2 $unlzop $unstuff
      $SYSLOG_LEVEL $syslog_priority $append_header_fields_to_bottom
      $insert_received_line $notify_xmailer_header $relayhost_is_client
      $sa_spam_report_header $sa_auto_whitelist
      $warnvirussender $warnspamsender
      $enable_global_cache
      $virus_check_negative_ttl $virus_check_positive_ttl
      $spam_check_negative_ttl $spam_check_positive_ttl
      $daemon_group
    )],
  );
  Exporter::export_tags qw(dynamic_confvars confvars sa platform
                      hidden_confvars legacy_dynamic_confvars legacy_confvars);
  1;
} # BEGIN

use POSIX ();
use Carp ();
use Errno qw(ENOENT EACCES EBADF);

use vars @EXPORT;

sub c($); sub cr($); sub ca($); sub dkim_key($$$;@);  # prototypes
use subs qw(c cr ca dkim_key);  # access subroutines to config vars and keys
BEGIN { push(@EXPORT,qw(c cr ca dkim_key)) }

# access to dynamic config variables, returns a scalar config variable value;
# one level of indirection is allowed
#
sub c($) {
  my $var = $current_policy_bank{$_[0]};
  if (!defined $var) {
    my $name = $_[0];
    if (!exists $current_policy_bank{$name}) {
      Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
                          $name, $current_policy_bank{'policy_bank_name'}));
    }
  }
  my $r = ref $var;
  !$r ? $var : $r eq 'SCALAR' || $r eq 'REF' ? $$var : $var;
}

# return a ref to a config variable value, or undef if var is undefined
#
sub cr($) {
  my $var = $current_policy_bank{$_[0]};
  if (!defined $var) {
    my $name = $_[0];
    if (!exists $current_policy_bank{$name}) {
      Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
                          $name, $current_policy_bank{'policy_bank_name'}));
    }
  }
  ref $var ? $var : defined $var ? \$var : undef;
}

# return a ref to a config variable value (which is supposed to be an array),
# converting undef to an empty array, and a scalar to a one-element array
# if necessary
#
sub ca($) {
  my $var = $current_policy_bank{$_[0]};
  if (!defined $var) {
    my $name = $_[0];
    if (!exists $current_policy_bank{$name}) {
      Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
                          $name, $current_policy_bank{'policy_bank_name'}));
    }
  }
  ref $var ? $var : defined $var ? [$var] : [];
}

sub deprecate_var($$$) {
  my($data_type, $var_name, $init_value) = @_;
  my $code = <<'EOD';
    tie(%n, '%p', %v)  or die 'Tieing a variable %n failed';
    package %p;
    use strict; use Carp ();
    sub TIESCALAR { my($class,$val) = @_; bless \$val, $class }
    sub FETCH { my $self = shift; $$self }
    sub STORE { my($self,$newv) = @_; my $oldv = $$self;
      if ((defined $oldv || defined $newv) && (%t)) {
        Carp::carp('Variable %n was retired, changing its value has no effect.'
                   . " See release notes.\n");
      }
      $$self = $newv;
    }
    1;
EOD
  if ($data_type eq 'bool') {
    $code =~ s{%t}'($oldv ? 1 : 0) != ($newv ? 1 : 0)'g;
  } elsif ($data_type eq 'num') {
    $code =~ s{%t}'!defined $oldv || !defined $newv || $oldv != $newv'g;
  } elsif ($data_type eq 'str') {
    $code =~ s{%t}'!defined $oldv || !defined $newv || $oldv ne $newv'g;
  } else {
    die "Error deprecating a variable $var_name: bad type $data_type";
  }
  $code =~ s/%n/$var_name/g;
  $code =~ s/%v/\$init_value/g;
  my $barename = $var_name;
  $barename =~ s/^[\$\@%&]//; $code =~ s/%p/Amavis::Deprecate::$barename/g;
  eval $code
    or do { chomp $@; die "Error deprecating a variable $var_name: $@" };
}

# Store a private DKIM signing key for a given domain and selector.
# The argument $key can be a Mail::DKIM::PrivateKey object or a file
# name containing a key in a PEM format (e.g. as generated by openssl).
# For compatibility with dkim_milter the signing domain can include a '*'
# as a wildcard - this is not recommended as this way amavisd could produce
# signatures which have no corresponding public key published in DNS.
# The proper way is to have one dkim_key entry for each published DNS RR.
# Optional arguments can provide additional information about the resource
# record (RR) of a public key, i.e. its options according to RFC 6376.
# The subroutine is typically called from a configuration file, once for
# each signing key available.
#
sub dkim_key($$$;@) {
  my($domain,$selector,$key) = @_;  shift; shift; shift;
  @_%2 == 0 or die "dkim_key: a list of key/value pairs expected as options\n";
  my(%key_options) = @_;  # remaining args are options from a public key RR
  defined $domain && $domain ne ''
    or die "dkim_key: domain must not be empty: ($domain,$selector,$key)";
  defined $selector && $selector ne ''
    or die "dkim_key: selector must not be empty: ($domain,$selector,$key)";
  my $key_storage_ind;
  if (ref $key) {  # key already preprocessed and provided as an object
    push(@dkim_signing_keys_storage, [$key]);
    $key_storage_ind = $#dkim_signing_keys_storage;
  } else {  # assume a name of a file containing a private key in PEM format
    my $fname = $key;
    my $pem_fh = IO::File->new;  # open a file with a private key
    $pem_fh->open($fname,'<') or die "Can't open PEM file $fname: $!";
    my(@stat_list) = stat($pem_fh);  # soft-link friendly
    @stat_list or warn "Error accessing $fname: $!";
    my($dev,$inode) = @stat_list;
    # perl 5.28: On platforms where inode numbers are of a type larger than
    # perl's native integer numerical types, stat will preserve the full
    # content of large inode numbers by returning them in the form of strings
    # of decimal digits. Use eq rather than == for exact comparison of inode.
    if (defined $dev && defined $inode) {
      for my $j (0..$#dkim_signing_keys_storage) {  # same file reused?
        my($k,$dv,$in,$fn) = @{$dkim_signing_keys_storage[$j]};
        if ($dv == $dev && $in eq $inode) { $key_storage_ind = $j; last }
      }
    }
    if (!defined($key_storage_ind)) {
      # read file and store its contents as a new entry
      $key = ''; Amavis::Util::read_file($pem_fh,\$key);
      my $key_fit = $key;  # shrink allocated storage size to actual size
      undef $key;  # release storage
      push(@dkim_signing_keys_storage, [$key_fit, $dev, $inode, $fname]);
      $key_storage_ind = $#dkim_signing_keys_storage;
    }
    $pem_fh->close or die "Error closing file $fname: $!";
    $key_options{k} = 'rsa'  if defined $key_options{k};  # force RSA
  }
  # possibly the $domain is a regexp
  $domain   = Amavis::Util::idn_to_ascii($domain)  if !ref $domain;
  $selector = Amavis::Util::idn_to_ascii($selector);
  $key_options{domain} = $domain; $key_options{selector} = $selector;
  $key_options{key_storage_ind} = $key_storage_ind;
  if (@dkim_signing_keys_list > 100) {
    # sorry, skip the test to avoid slow O(n^2) searches
  } else {
    !grep($_->{domain} eq $domain && $_->{selector} eq $selector,
          @dkim_signing_keys_list)
     or die "dkim_key: selector $selector for domain $domain already in use\n";
  }
  $key_options{key_ind} = $#dkim_signing_keys_list + 1;
  push(@dkim_signing_keys_list, \%key_options);  # using a list preserves order
}

# essential initializations, right at the program start time, may run as root!
#
use vars qw($read_config_files_depth @actual_config_files);
BEGIN {  # init_primary: version, base policy bank
  $myprogram_name = $0;  # typically 'amavisd'
  local $1; $myprogram_name =~ s{([^/]*)\z}{$1}s;
  $myproduct_name = 'amavis';
  $myversion_id = '2.14.0'; $myversion_date = '20250711';

  $myversion = "$myproduct_name-$myversion_id ($myversion_date)";
  $myversion_id_numeric =  # x.yyyzzz, allows numerical compare, like Perl $]
    sprintf('%8.6f', $1 + ($2 + $3/1000)/1000)
    if $myversion_id =~ /^(\d+)(?:\.(\d*)(?:\.(\d*))?)?(.*)$/s;
  $sql_schema_version = $myversion_id_numeric;

  $read_config_files_depth = 0;
  # initialize policy bank hash to contain dynamic config settings
  for my $tag (@EXPORT_TAGS{'dynamic_confvars', 'legacy_dynamic_confvars'}) {
    for my $v (@$tag) {
      local($1,$2);
      if ($v !~ /^([%\$\@])(.*)\z/s) { die "Unsupported variable type: $v" }
      else {
        no strict 'refs'; my($type,$name) = ($1,$2);
        $current_policy_bank{$name} = $type eq '$' ? \${"Amavis::Conf::$name"}
                                    : $type eq '@' ? \@{"Amavis::Conf::$name"}
                                    : $type eq '%' ? \%{"Amavis::Conf::$name"}
                                    : undef;
      }
    }
  }
  $current_policy_bank{'policy_bank_name'} = '';  # builtin policy
  $current_policy_bank{'policy_bank_path'} = '';
  $policy_bank{''} = { %current_policy_bank };    # copy
  1;
} # end BEGIN - init_primary


# boot-time initializations of simple global settings, may run as root!
#
BEGIN {
  # serves only as a quick default for other configuration settings
  $MYHOME = '/var/amavisd';
  $mydomain = '!change-mydomain-variable!.example.com';#intentionally bad deflt

  # Create debugging output - true: log to stderr; false: log to syslog/file
  $DEBUG = 0;

  # Is Devel::NYTProf profiler loaded?
  $profiling = 1  if DB->UNIVERSAL::can('enable_profile');

  # In case of trouble, allow preserving temporary files for forensics
  $allow_preserving_evidence = 1;

  # Cause Net::Server parameters 'background' and 'setsid' to be set,
  # resulting in the process to detach itself from the terminal
  $daemonize = 1;

  # Net::Server pre-forking settings - defaults, overruled by amavisd.conf
  $max_servers  = 2;   # number of pre-forked children
  $max_requests = 20;  # retire a child after that many accepts, 0=unlimited

  # timeout for our processing:
  $child_timeout = 8*60; # abort child if it does not complete a task in n sec

  # timeout for waiting on client input:
  $smtpd_timeout = 8*60; # disconnect session if client is idle for too long;
  #  $smtpd_timeout should be higher than Postfix's max_idle (default 100s)

  # Assume STDIN is a courierfilter pipe and shutdown when it becomes readable
  $courierfilter_shutdown = 0;

  # Can file be truncated?
  # Set to 1 if 'truncate' works (it is XPG4-UNIX standard feature,
  #                               not required by Posix).
  # Things will go faster with SMTP-in, otherwise (e.g. with milter)
  # it makes no difference as file truncation will not be used.
  $can_truncate = 1;

  # Customizable notification messages, logging

  $syslog_ident = 'amavis';
  $syslog_facility = 'mail';

  $log_level = 0;

  # should be less than (1023 - prefix), i.e. 980,
  # to avoid syslog truncating lines; see sub write_log
  $logline_maxlen = 980;

  $nanny_details_level = 1;  # register_proc verbosity: 0, 1, 2

# $inner_sock_specs in amavis-services should match one of the sockets
# in the @zmq_sockets list
# @zmq_sockets = ( "ipc://$MYHOME/amavisd-zmq.sock" );  # after-default

# $enable_zmq = undef;  # load optional module Amavis::ZMQ
#                       #   (interface to 0MQ or Crossroads I/O)
# $enable_db = undef;   # load optional modules Amavis::DB & Amavis::DB::SNMP
# $enable_dkim_signing = undef;
# $enable_dkim_verification = undef;

  $enable_ip_repu = 1;  # ignored when @storage_redis_dsn is empty

  # a key (string) for a redis list serving as a queue of json events
  # for logstash / elasticsearch use;  undef or empty or '0' disables
  # logging of events to redis
  $redis_logging_key = undef;  # e.g. "amavis-log";

  # a limit on the length of a redis list - new log events will be dropped
  # while the queue size limit is exceeded; undef or 0 disables logging;
  # reasonable value: 100000, takes about 250 MB of memory in a redis server
  # when noone is pulling events from the list
  $redis_logging_queue_size_limit = undef;

  $reputation_factor = 0.2;  # DKIM reputation: a value between 0 and 1,
    # controlling the amount of 'bending' of a calculated spam score
    # towards a fixed score assigned to a signing domain (its 'reputation')
    # through @signer_reputation_maps;  the formula is:
    #   adjusted_spam_score = f*reputation + (1-f)*spam_score
    # which has the same semantics as auto_whitelist_factor in SpamAssassin AWL

  # keep SQL, LDAP and Redis sessions open when idle
  $database_sessions_persistent = 1;

  $lookup_maps_imply_sql_and_ldap = 1;  # set to 0 to disable

  # Algorithm name for generating a mail header digest and a mail body digest:
  # either 'MD5' (will use Digest::MD5, fastest and smallest digest), or
  # anything else accepted by Digest::SHA->new(), e.g. 'SHA-1' or 'SHA-256'.
  # The generated digest may end up as part of a quarantine file name
  # or via macro %b in log or notification templates.
  #
  $mail_digest_algorithm = 'MD5';  # or 'SHA-1' or 'SHA-256', ...

  # Algorithm name for generating digests of decoded MIME parts of a message.
  # The value is an algorithm name as accepted by Digest::SHA->new(),
  # e.g. 'SHA-1' or 'SHA-256' or 'sha256', or a string 'MD5' which implies
  # the MD5 algorithm as implemented by a module Digest::MD5.
  # For compatibility with SpamAssassin the chosen algorithm should be SHA1,
  # otherwise bayes tokens won't match those generated by sa-learn.
  # Undefined value disables generating digests of MIME parts.
  #
  $mail_part_digest_algorithm = 'SHA1';

  # Where to find SQL server(s) and database to support SQL lookups?
  # A list of triples: (dsn,user,passw). Specify more than one
  # for multiple (backup) SQL servers.
  #
  #@storage_sql_dsn =
  #@lookup_sql_dsn =
  #   ( ['DBI:mysql:mail:host1', 'some-username1', 'some-password1'],
  #     ['DBI:mysql:mail:host2', 'some-username2', 'some-password2'] );

  # Does a database mail address field with no '@' character represent a
  # local username or a domain name?  By default it implies a username in
  # SQL and LDAP lookups (but represents a domain in hash and acl lookups),
  # so domain names in SQL and LDAP should be specified as '@domain'.
  # Setting these to true will cause 'xxx' to be interpreted as a domain
  # name, just like in hash or acl lookups.
  #
  $sql_lookups_no_at_means_domain  = 0;
  $ldap_lookups_no_at_means_domain = 0;

  # Maximum size (in bytes) for data written to a field 'quarantine.mail_text'
  # when quarantining to SQL. Must not exceed size allowed for a data type
  # on a given SQL server. It also determines a buffer size in amavisd.
  # Too large a value may exceed process virtual memory limits or just waste
  # memory, too small a value splits large mail into too many chunks, which
  # may be less efficient to process.
  #
  $sql_quarantine_chunksize_max = 16384;
  $sql_allow_8bit_address = 0;

  # the length of mail_id in bits, must be an integral multiple of 24
  # (i.e. divisible by 6 and 8);  the mail_id is represented externally
  # as a base64url-encoded string of size $mail_id_size_bits / 6
  #
  $mail_id_size_bits = 72;  # 24, 48, 72, 96

  # redis data (penpals) expiration - time-to-live in seconds of stored items
  $storage_redis_ttl = 16*24*60*60;  # 16 days (only affects penpals data)

  $sql_store_info_for_all_msgs = 1;
  $penpals_bonus_score = undef;  # maximal (positive) score value by which spam
       # score is lowered when sender is known to have previously received mail
       # from our local user from this mail system. Zero or undef disables
       # pen pals lookups in Redis or in SQL tables msgs and msgrcpt, and
       # is a default.
  $penpals_halflife = 7*24*60*60; # exponential decay time constant in seconds;
       # pen pal bonus is halved for each halflife period since the last mail
       # sent by a local user to a current message's sender
  $penpals_threshold_low = 1.0;   # SA score below which pen pals lookups are
       # not performed to save time; undef lets the threshold be ignored;
  $penpals_threshold_high = undef;
       # when (SA_score - $penpals_bonus_score > $penpals_threshold_high)
       # pen pals lookup will not be performed to save time, as it could not
       # influence blocking of spam even at maximal penpals bonus (age=0);
       # usual choice for value would be a kill level or other reasonably high
       # value; undef lets the threshold be ignored and is a default (useful
       # for testing and statistics gathering);

  $bounce_killer_score = 0;

  #
  # Receiving mail related

  # $unix_socketname = '/var/amavis/amavisd.sock';  # e.g. milter or release
  # $inet_socket_port = 10024;      # accept SMTP on this TCP port
  # $inet_socket_port = [10024,10026,10027];  # ...possibly on more than one

  $AF_INET6 = eval { require Socket;  Socket::AF_INET6()  } ||
              eval { require Socket6; Socket6::AF_INET6() };

  # prefer using module IO::Socket::IP if available,
  # otherwise fall back to IO::Socket::INET6 or to IO::Socket::INET
  #
  if (eval { require IO::Socket::IP }) {
    $io_socket_module_name = 'IO::Socket::IP';
  } elsif (eval { require IO::Socket::INET6 }) {
    $io_socket_module_name = 'IO::Socket::INET6';
  } elsif (eval { require IO::Socket::INET }) {
    $io_socket_module_name = 'IO::Socket::INET';
  }

  $have_inet4 =  # can we create a PF_INET socket?
    defined $io_socket_module_name && eval {
      my $sock =
        $io_socket_module_name->new(LocalAddr => '0.0.0.0', Proto => 'tcp');
      $sock->close or die "error closing socket: $!"  if $sock;
      $sock ? 1 : undef;
    };

  $have_inet6 =  # can we create a PF_INET6 socket?
    defined $io_socket_module_name &&
    $io_socket_module_name ne 'IO::Socket::INET' &&
    eval {
      my $sock =
        $io_socket_module_name->new(LocalAddr => '::', Proto => 'tcp');
      $sock->close or die "error closing socket: $!"  if $sock;
      $sock ? 1 : undef;
    };

# if (!$have_inet6 && $io_socket_module_name ne 'IO::Socket::INET') {
#   # ok, let's stay on proven grounds, use the IO::Socket::INET anyway
#   if (eval { require IO::Socket::INET }) {
#     $io_socket_module_name = 'IO::Socket::INET';
#   }
# }

  # bind socket to a loopback interface
  if (Net::Server->VERSION < 2) {
    $inet_socket_bind = '127.0.0.1';
  } else {  # requires Net::Server 2 or a patched 0.99 with IPv6 support)
    $inet_socket_bind = $have_inet4 && $have_inet6 ? ['127.0.0.1', '[::1]']
                      : $have_inet6 ? '[::1]' : '127.0.0.1';
  }
  @inet_acl   = qw( 127.0.0.1 [::1] );  # allow SMTP access only from localhost
  @mynetworks = qw( 127.0.0.0/8 [::1] 169.254.0.0/16 [fe80::]/10
                    10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
                    [fc00::]/7 );  # consider also RFC 6598: 100.64.0.0/10
  $originating = 0;  # a boolean, initially reflects @mynetworks match,
                     # but may be modified later through a policy bank

  $forward_method = $have_inet6 && !$have_inet4 ? 'smtp:[::1]:10025'
                                                : 'smtp:[127.0.0.1]:10025';
  $notify_method = $forward_method;

  $resend_method  = undef; # overrides $forward_method on defanging if nonempty
  $release_method = undef; # overrides $notify_method on releasing
                           #   from quarantine if nonempty
  $requeue_method =        # requeuing release from a quarantine
    $have_inet6 && !$have_inet4 ? 'smtp:[::1]:25' : 'smtp:[127.0.0.1]:25';

  $release_format = 'resend';  # (dsn), (arf), attach,  plain,  resend
  $report_format  = 'arf';     # (dsn),  arf,  attach,  plain,  resend

  # when $release_format is 'attach', the following control the attachment:
  $attachment_password = ''; # '': no pwd; undef: PIN; code ref; or static str
  $attachment_email_name = 'msg-%m.eml';
  $attachment_outer_name = 'msg-%m.zip';

  $virus_quarantine_method              = 'local:virus-%m';
  $banned_files_quarantine_method       = 'local:banned-%m';
  $spam_quarantine_method               = 'local:spam-%m.gz';
  $bad_header_quarantine_method         = 'local:badh-%m';
  $unchecked_quarantine_method = undef; # 'local:unchecked-%m';
  $clean_quarantine_method     = undef; # 'local:clean-%m';
  $archive_quarantine_method   = undef; # 'local:archive-%m.gz';

  $prepend_header_fields_hdridx      = 0;  # normally 0, use 1 for co-existence
                                           # with signing DK and DKIM milters
  $remove_existing_x_scanned_headers = 0;
  $remove_existing_spam_headers      = 1;

  # fix improper header fields in passed or released mail - this setting
  # is a pre-condition for $allow_fixing_improper_header_folding and similar
  # (future) fixups; (desirable, but may break DKIM validation of messages
  # with illegal header section)
  $allow_fixing_improper_header = 1;

  # fix improper folded header fields made up entirely of whitespace, by
  # removing all-whitespace lines ($allow_fixing_improper_header must be true)
  $allow_fixing_improper_header_folding = 1;

  # truncate header section lines longer than 998 characters as limited
  # by the RFC 5322 ($allow_fixing_improper_header must be true)
  $allow_fixing_long_header_lines = 1;

  # encoding (charset in MIME terminology)
  # to be used in RFC 2047-encoded ...
  $hdr_encoding = 'UTF-8';       # ... header field bodies
  $bdy_encoding = 'UTF-8';       # ... notification body text

  # encoding (encoding in MIME terminology)
  $hdr_encoding_qb = 'Q';        # quoted-printable (default)
# $hdr_encoding_qb = 'B';        # base64

  $smtpd_recipient_limit = 1100; # max recipients (RCPT TO) - sanity limit
  $macro_tests_sanity_limit = 50; # truncation of Tests: [BAYES_999=9,BAYES_99=7,...]

  # $myhostname is used by SMTP server module in the initial SMTP welcome line,
  # in inserted Received: lines, Message-ID in notifications, log entries, ...
  $myhostname = (POSIX::uname)[1];  # should be a FQDN !

  $snmp_contact  = '';  # a value of sysContact OID
  $snmp_location = '';  # a value of sysLocation OID

  $smtpd_greeting_banner = '${helo-name} ${protocol} ${product} service ready';
  $smtpd_quit_banner = '${helo-name} ${product} closing transmission channel';
  $enforce_smtpd_message_size_limit_64kb_min = 1;

  # $localhost_name is the name of THIS host running amavisd
  # (often just 'localhost'). It is used in HELO SMTP command
  # when reinjecting mail back to MTA via SMTP for final delivery,
  # and in inserted Received header field
  $localhost_name = 'localhost';

  $propagate_dsn_if_possible = 1;  # pass on DSN if MTA announces this
            # capability; useful to be turned off globally but enabled in
            # MYNETS policy bank to hide internal mail routing from outsiders
  $terminate_dsn_on_notify_success = 0;  # when true=>handle DSN NOTIFY=SUCCESS
            # locally, do not let NOTIFY=SUCCESS propagate to MTA (but allow
            # other DSN options like NOTIFY=NEVER/FAILURE/DELAY, ORCPT, RET,
            # and ENVID to propagate if possible)

  #@auth_mech_avail = ('PLAIN','LOGIN');   # empty list disables incoming AUTH
  #$auth_required_inp = 1;  # incoming SMTP authentication required by amavisd?
  #$auth_required_out = 1;  # SMTP authentication required by MTA
  $auth_required_release = 1;  # secret_id is required for a quarantine release

  $tls_security_level_in  = undef;  # undef, 'may', 'encrypt', ...
  $tls_security_level_out = undef;  # undef, 'may', 'encrypt', ...

  # Server side certificate and key: $smtpd_tls_cert_file, $smtpd_tls_key_file.
  # These two settings are now deprecated, set fields 'SSL_key_file'
  # and 'SSL_cert_file' directly in %smtpd_tls_server_options instead.
  # For compatibility with 2.10 the values of $smtpd_tls_cert_file
  # and $smtpd_tls_key_file are fed into %smtpd_tls_server_options
  # if fields 'SSL_key_file' and 'SSL_cert_file' are not provided.
  #
  # $smtpd_tls_cert_file = undef;   # e.g. "$MYHOME/cert/amavisd-cert.pem"
  # $smtpd_tls_key_file  = undef;   # e.g. "$MYHOME/cert/amavisd-key.pem"

  # The following options are passed to IO::Socket::SSL::start_SSL() when
  # setting up a server side of a TLS session (from MTA). The only options
  # passed implicitly are SSL_server, SSL_hostname, and SSL_error_trap.
  # See IO::Socket::SSL documentation.
  #
  %smtpd_tls_server_options = (
    SSL_verifycn_scheme => 'smtp',
    SSL_session_cache => 2,
#   SSL_key_file    => $smtpd_tls_key_file,
#   SSL_cert_file   => $smtpd_tls_cert_file,
#   SSL_dh_file     => ... ,
#   SSL_ca_file     => ... ,
#   SSL_version     => '!SSLv2,!SSLv3',
#   SSL_cipher_list => 'HIGH:!MD5:!DSS:!aNULL',
#   SSL_passwd_cb => sub { 'example' },
#   ...
  );

  # The following options are passed to IO::Socket::SSL::start_SSL() when
  # setting up a client side of a TLS session back to MTA. The only options
  # passed implicitly are SSL_session_cache and SSL_error_trap.
  # See IO::Socket::SSL documentation.
  #
  %smtp_tls_client_options = (
    SSL_verifycn_scheme => 'smtp',
#   SSL_version     => '!SSLv2,!SSLv3',
#   SSL_cipher_list => 'HIGH:!MD5:!DSS:!aNULL',
#   SSL_client_ca_file => ... ,
  );

  $dkim_minimum_key_bits = 1024;    # min acceptable DKIM key size (in bits)
                                    # for whitelisting

  # SMTP AUTH username and password for notification submissions
  # (and reauthentication of forwarded mail if requested)
  #$amavis_auth_user = undef;  # perhaps: 'amavisd'
  #$amavis_auth_pass = undef;
  #$auth_reauthenticate_forwarded = undef;  # supply our own credentials also
                                            # for forwarded (passed) mail
  $smtp_connection_cache_on_demand = 1;
  $smtp_connection_cache_enable = 1;

  # whom quarantined messages appear to be sent from (envelope sender)
  # $mailfrom_to_quarantine = undef; # orig. sender if undef, or set explicitly

  # where to send quarantined malware - specify undef to disable, or an
  # e-mail address containing '@', or just a local part, which will be
  # mapped by %local_delivery_aliases into local mailbox name or directory.
  # The lookup key is a recipient address
  $virus_quarantine_to      = 'virus-quarantine';
  $banned_quarantine_to     = 'banned-quarantine';
  $unchecked_quarantine_to  = 'unchecked-quarantine';
  $spam_quarantine_to       = 'spam-quarantine';
  $bad_header_quarantine_to = 'bad-header-quarantine';
  $clean_quarantine_to      = 'clean-quarantine';
  $archive_quarantine_to    = 'archive-quarantine';

  # similar to $spam_quarantine_to, but the lookup key is the sender address:
  $spam_quarantine_bysender_to = undef;  # dflt: no by-sender spam quarantine

  # quarantine directory or mailbox file or empty
  #   (only used if $*_quarantine_to specifies direct local delivery)
  $QUARANTINEDIR = undef;  # no quarantine unless overridden by config

  $undecipherable_subject_tag = '***UNCHECKED*** ';

  # NOTE: all entries can accept mail_body_size_limit and score_factor options
  @spam_scanners = (
    ['SpamAssassin', 'Amavis::SpamControl::SpamAssassin' ],
  # ['SpamdClient',  'Amavis::SpamControl::SpamdClient',
  #   mail_body_size_limit => 65000, score_factor => 1.0,
  # ],
  # ['DSPAM', 'Amavis::SpamControl::ExtProg', $dspam,
  #   [ qw(--stdout --classify --deliver=innocent,spam
  #        --mode=toe --feature noise
  #        --user), $daemon_user ],
  #   mail_body_size_limit => 65000, score_factor => 1.0,
  # ],
  # ['CRM114', 'Amavis::SpamControl::ExtProg', 'crm',
  #   [ qw(-u /var/amavis/home/.crm114 mailreaver.crm
  #        --dontstore --report_only --stats_only
  #        --good_threshold=10 --spam_threshold=-10) ],
  #   mail_body_size_limit => 65000, score_factor => -0.20,
  #   lock_file => '/var/amavis/crm114.lock',
  #   lock_type => 'shared', learner_lock_type => 'exclusive',
  # ],
  # ['Bogofilter', 'Amavis::SpamControl::ExtProg', 'bogofilter',
  #   [ qw(-e -v)],  # -u
  #   mail_body_size_limit => 65000, score_factor => 1.0,
  # ],
  # ['Rspamd', 'Amavis::SpamControl::RspamdClient',
  #   score_factor => $sa_tag2_level_deflt / 15.0,
  #   mta_name => 'mail.example.com',
  # ],
  );

  $sa_spawned = 0;  # true: run SA in a subprocess;  false: call SA directly

  # string to prepend to Subject header field when message qualifies as spam
  # $sa_spam_subject_tag1 = undef;  # example: '***Possible Spam*** '
  # $sa_spam_subject_tag  = undef;  # example: '***Spam*** '
  $sa_spam_level_char = '*'; # character to be used in X-Spam-Level bar;
                             # empty or undef disables adding this header field
  $sa_num_instances = 1;  # number of SA instances,
                          # usually 1, memory-expensive, keep small
  $sa_local_tests_only = 0;
  $sa_debug = undef;
  $sa_timeout = 30;  # no longer used since 2.6.5

  $file = 'file';  # path to the file(1) utility for classifying contents
  $altermime = 'altermime';  # path to the altermime utility (optional)
  @altermime_args_defang     = qw(--verbose --removeall);
  @altermime_args_disclaimer = qw(--disclaimer=/etc/altermime-disclaimer.txt);
  # @altermime_args_disclaimer =
  #  qw(--disclaimer=/etc/_OPTION_.txt --disclaimer-html=/etc/_OPTION_.html);
  # @disclaimer_options_bysender_maps = ( 'altermime-disclaimer' );

  $MIN_EXPANSION_FACTOR =   5;  # times original mail size
  $MAX_EXPANSION_FACTOR = 500;  # times original mail size
# $MIN_EXPANSION_QUOTA  = ...   # bytes, undef=not enforced
# $MAX_EXPANSION_QUOTA  = ...   # bytes, undef=not enforced

  # See amavisd.conf and README.lookups for details.

  # What to do with the message (this is independent of quarantining):
  #   Reject:  tell MTA to generate a non-delivery notification,  MTA gets 5xx
  #   Bounce:  generate a non-delivery notification by ourselves, MTA gets 250
  #   Discard: drop the message and pretend it was delivered,     MTA gets 250
  #   Pass:    accept/forward a message,                          MTA gets 250
  #   TempFail: temporary failure, client should retry,           MTA gets 4xx
  #
  # COMPATIBILITY NOTE: the separation of *_destiny values into
  #   D_BOUNCE, D_REJECT, D_DISCARD and D_PASS made settings $warn*sender only
  #   still useful with D_PASS. The combination of D_DISCARD + $warn*sender=1
  #   is mapped into D_BOUNCE for compatibility.

  # The following symbolic constants can be used in *destiny settings:
  #
  # D_PASS     mail will pass to recipients, regardless of contents;
  #
  # D_DISCARD  mail will not be delivered to its recipients, sender will NOT be
  #            notified. Effectively we lose mail (but it will be quarantined
  #            unless disabled).
  #
  # D_BOUNCE   mail will not be delivered to its recipients, a non-delivery
  #            notification (bounce) will be sent to the sender by amavis
  #            (unless suppressed). Bounce (DSN) will not be sent if a virus
  #            name matches $viruses_that_fake_sender_maps, or to messages
  #            from mailing lists (Precedence: bulk|list|junk), or for spam
  #            exceeding spam_dsn_cutoff_level
  #
  # D_REJECT   mail will not be delivered to its recipients, amavisd will
  #            return a 5xx status response. Depending on an MTA/amavisd setup
  #            this will result either in a reject status passed back to a
  #            connecting SMTP client (in a pre-queue setup: proxy or milter),
  #            or an MTA will generate a bounce in a post-queue setup.
  #            If not all recipients agree on rejecting a message (like when
  #            different recipients have different thresholds on bad mail
  #            contents and LMTP is not used) amavisd sends a bounce by itself
  #            (same as D_BOUNCE).
  #
  # D_TEMPFAIL indicates a temporary failure, mail will not be delivered to
  #            its recipients, sender should retry the operation later.
  #
  # Notes:
  #   D_REJECT and D_BOUNCE are similar,the difference is in who is responsible
  #            for informing the sender about non-delivery, and how informative
  #            the notification can be (amavis knows more than MTA);
  #   With D_REJECT, MTA may reject original SMTP, or send DSN (delivery status
  #            notification, colloquially called 'bounce') - depending on MTA
  #            and its interface to a content checker; best suited for sendmail
  #            milter or other pre-queue filtering setups
  #   With D_BOUNCE, amavis (not MTA) sends DSN (can better explain the
  #            reason for mail non-delivery but unable to reject the original
  #            SMTP session, and is in position to suppress DSN if considered
  #            unsuitable). Best suited for Postfix and other dual-MTA setups.
  #            Exceeded spam cutoff limit or faked virus sender implicitly
  #            turns D_BOUNCE into a D_DISCARD;

  # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS, D_TEMPFAIL
  $final_virus_destiny      = D_DISCARD;
  $final_banned_destiny     = D_DISCARD;
  $final_unchecked_destiny  = D_PASS;
  $final_spam_destiny       = D_PASS;
  $final_bad_header_destiny = D_PASS;

  # If decided to pass viruses (or spam) to certain recipients
  # by %final_destiny_maps_by_ccat yielding a D_PASS, or %lovers_maps_by_ccat
  # yielding a true, one may set the corresponding %addr_extension_maps_by_ccat
  # to some string, and the recipient address will have this string appended
  # as an address extension to a local-part (mailbox part) of the address.
  # This extension can be used by a final local delivery agent for example
  # to place such mail in different folder. Leaving this variable undefined
  # or an empty string prevents appending address extension. Recipients
  # which do not match @local_domains_maps are not affected (i.e. non-local
  # recipients (=outbound mail) do not get address extension appended).
  #
  # LDAs usually default to stripping away address extension if no special
  # handling for it is specified, so having this option enabled normally
  # does no harm, provided the $recipients_delimiter character matches
  # the setting at the final MTA's local delivery agent (LDA).
  #
  # $addr_extension_virus  = 'virus';  # for example
  # $addr_extension_spam   = 'spam';
  # $addr_extension_banned = 'banned';
  # $addr_extension_bad_header = 'badh';

  # Delimiter between local part of the recipient address and address extension
  # (which can optionally be added, see variable %addr_extension_maps_by_ccat.
  # E.g. recipient address <user@domain.example> gets
  # changed to <user+virus@domain.example>.
  #
  # Delimiter should match an equivalent (final) MTA delimiter setting.
  # (e.g. for Postfix add 'recipient_delimiter = +' to main.cf).
  # Setting it to an empty string or to undef disables this feature
  # regardless of %addr_extension_maps_by_ccat setting.

  # $recipient_delimiter = '+';
  $replace_existing_extension = 1;   # true: replace ext; false: append ext

  # Affects matching of localpart of e-mail addresses (left of '@')
  # in lookups: true = case sensitive, false = case insensitive
  $localpart_is_case_sensitive = 0;

  # Trim trailing whitespace from SQL fields, LDAP attribute values
  # and hash righthand-sides as read by read_hash(); disabled by default;
  # turn it on for compatibility with pre-2.4.0 versions.
  $trim_trailing_space_in_lookup_result_fields = 0;

  # since 2.7.0: deprecated some old variables:
  #
  deprecate_var('bool', '$insert_received_line',  1);
  deprecate_var('bool', '$relayhost_is_client',   undef);
  deprecate_var('bool', '$warnvirussender',       undef);
  deprecate_var('bool', '$warnspamsender',        undef);
  deprecate_var('bool', '$sa_spam_report_header', undef);
  deprecate_var('bool', '$sa_spam_modifies_subj', 1);
  deprecate_var('bool', '$sa_auto_whitelist',     undef);
  deprecate_var('num',  '$sa_timeout',            30);
  deprecate_var('str',  '$syslog_priority',       'debug');
  deprecate_var('str',  '$SYSLOG_LEVEL',          'mail.debug');
  deprecate_var('str',  '$notify_xmailer_header', undef);
# deprecate_var('array','@spam_modifies_subj_maps');
  1;
} # end BEGIN - init_secondary


# init structured variables like %sql_clause, $map_full_type_to_short_type_re,
# %ccat_display_names, @decoders, build default maps;  may run as root!
#
BEGIN {
  $allowed_added_header_fields{lc($_)} = 1  for qw(
    Received DKIM-Signature Authentication-Results VBR-Info
    X-Quarantine-ID X-Amavis-Alert X-Amavis-Hold X-Amavis-Modified
    X-Amavis-PenPals X-Amavis-OS-Fingerprint X-Amavis-PolicyBank X-Amavis-Category
    X-Spam-Status X-Spam-Level X-Spam-Flag X-Spam-Score
    X-Spam-Report X-Spam-Checker-Version X-Spam-Tests
    X-CRM114-Status X-CRM114-CacheID X-CRM114-Notice X-CRM114-Action
    X-DSPAM-Result X-DSPAM-Class X-DSPAM-Signature X-DSPAM-Processed
    X-DSPAM-Confidence X-DSPAM-Probability X-DSPAM-User X-DSPAM-Factors
    X-Bogosity
  );
  $allowed_added_header_fields{lc('X-Spam-Report')} = 0;
  $allowed_added_header_fields{lc('X-Spam-Checker-Version')} = 0;
  $allowed_added_header_fields{lc('X-Amavis-Category')} = 0;
  # $allowed_added_header_fields{lc(c(lc $X_HEADER_TAG))}=1; #later:read_config

  # even though SpamAssassin does provide the following header fields, we
  # prefer to provide our own version (per-recipient scores, version hiding);
  # our own non-"X-Spam" header fields are always preferred and need not
  # be listed here
  $prefer_our_added_header_fields{lc($_)} = 1  for qw(
    X-Spam-Status X-Spam-Level X-Spam-Flag X-Spam-Score X-Spam-Report
    X-Spam-Checker-Version
    X-CRM114-Status X-CRM114-CacheID X-DSPAM-Result X-DSPAM-Signature
  );

  # controls which header section tests are performed in check_header_validity,
  # keys correspond to minor contents categories for CC_BADH
  $allowed_header_tests{lc($_)} = 1  for qw(
              other mime syntax empty long control 8bit utf8 missing multiple);
  $allowed_header_tests{'utf8'} = 0;  # turn this test off by default

  # RFC 6376 standard set of header fields to be signed:
  my(@sign_headers) = qw(From Sender Reply-To Subject Date Message-ID To Cc
    In-Reply-To References MIME-Version Content-Type Content-Transfer-Encoding
    Content-ID Content-Description Resent-Date Resent-From Resent-Sender
    Resent-To Resent-Cc Resent-Message-ID List-Id List-Post List-Owner
    List-Subscribe List-Unsubscribe List-Help List-Archive);
  # additional header fields considered appropriate, see also RFC 4021
  # and IANA registry "Permanent Message Header Field Names";
  # see RFC 3834 for Auto-Submitted; RFC 5518 for VBR-Info (Vouch By Reference)
  push(@sign_headers, qw(Received Precedence
    Original-Message-ID Message-Context PICS-Label Sensitivity Solicitation
    Content-Location Content-Features Content-Disposition Content-Language
    Content-Alternative Content-Base Content-MD5 Content-Duration Content-Class
    Accept-Language Auto-Submitted Archived-At VBR-Info));
  # note that we are signing Received despite the advise in RFC 6376;
  # some additional nonstandard header fields:
  push(@sign_headers, qw(Organization Organisation User-Agent X-Mailer));
  $signed_header_fields{lc($_)} = 1  for @sign_headers;
  # Excluded:
  #   DKIM-Signature DomainKey-Signature Authentication-Results
  #   Keywords Comments Errors-To X-Virus-Scanned X-Archived-At X-No-Archive
  # Some MTAs are dropping Disposition-Notification-To, exclude:
  #   Disposition-Notification-To Disposition-Notification-Options
  # Some mail scanners are dropping Return-Receipt-To, exclude it.
  # Signing a 'Sender' may not be a good idea because when such mail is sent
  # through a mailing list, this header field is usually replaced by a new one,
  # invalidating a signature. Long To and Cc address lists are often mangled,
  # especially when containing non-encoded display names.
  # Off: Sender - conflicts with mailing lists which must replace a Sender
  # Off: To, Cc, Resent-To, Resent-Cc - too often get garbled by mailers
  $signed_header_fields{lc($_)} = 0  for qw(Sender To Cc Resent-To Resent-Cc);
  #
  # a value greater than 1 causes signing of one additional null instance of
  # a header field, thus prohibiting prepending additional occurrences of such
  # header field without breaking a signature
  $signed_header_fields{lc($_)} = 2  for qw(From Date Subject Content-Type);

  # provide names for content categories - to be used only for logging,
  # SNMP counter names, and display purposes
  %ccat_display_names = (
    CC_CATCHALL,   'CatchAll',   # last resort, should not normally appear
    CC_CLEAN,      'Clean',
    CC_CLEAN.',1', 'CleanTag',   # tag_level
    CC_MTA,        'MtaFailed',  # unable to forward (general)
    CC_MTA.',1',   'MtaTempFailed',  # MTA response was 4xx
    CC_MTA.',2',   'MtaRejected',    # MTA response was 5xx
    CC_OVERSIZED,  'Oversized',
    CC_BADH,       'BadHdr',
    CC_BADH.',1',  'BadHdrMime',
    CC_BADH.',2',  'BadHdr8bit',
    CC_BADH.',3',  'BadHdrChar',
    CC_BADH.',4',  'BadHdrSpace',
    CC_BADH.',5',  'BadHdrLong',
    CC_BADH.',6',  'BadHdrSyntax',
    CC_BADH.',7',  'BadHdrMissing',
    CC_BADH.',8',  'BadHdrDupl',
    CC_SPAMMY,     'Spammy',     # tag2_level
    CC_SPAMMY.',1','Spammy3',    # tag3_level
    CC_SPAM,       'Spam',       # kill_level
    CC_UNCHECKED,      'Unchecked',
    CC_UNCHECKED.',1', 'UncheckedEncrypted',
    CC_UNCHECKED.',2', 'UncheckedOverLimits',
    CC_UNCHECKED.',3', 'UncheckedAmbiguousContent',
    CC_BANNED,     'Banned',
    CC_VIRUS,      'Virus',
  );

  # provide names for content categories - to be used only for logging,
  # SNMP counter names, and display purposes, similar to %ccat_display_names
  # but only major contents category names are listed
  %ccat_display_names_major = (
    CC_CATCHALL,   'CatchAll',   # last resort, should not normally appear
    CC_CLEAN,      'Clean',
    CC_MTA,        'MtaFailed',  # unable to forward
    CC_OVERSIZED,  'Oversized',
    CC_BADH,       'BadHdr',
    CC_SPAMMY,     'Spammy',     # tag2_level
    CC_SPAM,       'Spam',       # kill_level
    CC_UNCHECKED,  'Unchecked',
    CC_BANNED,     'Banned',
    CC_VIRUS,      'Virus',
  );

  # $partition_tag is a user-specified SQL field value in tables maddr, msgs,
  # msgrcpt and quarantine, inserted into new records, but can be useful even
  # without SQL, accessible through a macro %P and in quarantine templates.
  # It is usually an integer, but depending on a schema may be of other data
  # type e.g. a string. May be used to speed up purging of old records by using
  # partitioned tables (MySQL 5.1+, PostgreSQL 8.1+). A possible usage can
  # be a week-of-a-year, or some other slowly changing value, allowing to
  # quickly drop old table partitions without wasting time on deleting
  # individual records. Mail addresses in table maddr are self-contained
  # within a partition tag, which means that the same mail address may
  # appear in more than one maddr partition (using different 'id's), and
  # that tables msgs and msgrcpt are guaranteed to reference a maddr.id
  # within their own partition tag. The $partition_tag may be a scalar
  # (an integer or a string), or a reference to a subroutine, which will be
  # called with an object of type Amavis::In::Message as argument, and its
  # result will be used as a partition tag value. Possible usage:
  #
  #  $partition_tag =
  #    sub { my($msginfo)=@_; iso8601_week($msginfo->rx_time) };
  #or:
  #  $partition_tag =
  #    sub { my($msginfo)=@_; iso8601_yearweek($msginfo->rx_time) };
  #
  #or based on a day of a week for short-term cycling (Mo=1, Tu=2,... Su=7):
  #  $partition_tag =
  #    sub { my($msginfo)=@_; iso8601_weekday($msginfo->rx_time) };
  #
  #  $spam_quarantine_method = 'local:W%P/spam/%m.gz';  # quar dir by week num

  # The SQL select clause to fetch per-recipient policy settings.
  # The %k will be replaced by a comma-separated list of query addresses
  # for a recipient (e.g. a full address, domain only, catchall), %a will be
  # replaced by an exact recipient address (same as the first entry in %k,
  # suitable for pattern matching), %l by a full unmodified localpart, %u by
  # a lowercased username (a localpart without extension), %e by lowercased
  # addr extension (which includes a delimiter), and %d for lowercased domain.
  # Use ORDER if there is a chance that multiple records will match - the
  # first match wins (i.e. the first returned record). If field names are
  # not unique (e.g. 'id'), the later field overwrites the earlier in a hash
  # returned by lookup, which is why we use 'users.*, policy.*, users.id',
  # i.e. the id is repeated at the end.
  # This is a legacy variable for upwards compatibility, now only referenced
  # by the program through a %sql_clause entry 'sel_policy' - newer config
  # files may assign directly to $sql_clause{'sel_policy'} if preferred.
  #
  $sql_select_policy =
    'SELECT users.*, policy.*, users.id'.
    ' FROM users LEFT JOIN policy ON users.policy_id=policy.id'.
    ' WHERE users.email IN (%k) ORDER BY users.priority DESC';

  # Btw, MySQL and PostgreSQL are happy with 'SELECT *, users.id',
  # but Oracle wants 'SELECT users.*, policy.*, users.id', which is
  # also acceptable to MySQL and PostgreSQL.

  # The SQL select clause to check sender in per-recipient whitelist/blacklist.
  # The first SELECT argument '?' will be users.id from recipient SQL lookup,
  # the %k will be replaced by a comma-separated list of query addresses
  # for a sender (e.g. a full address, domain only, catchall), %a will be
  # replaced by an exact sender address (same as the first entry in %k,
  # suitable for pattern matching), %l by a full unmodified localpart, %u by
  # a lowercased username (a localpart without extension), %e by lowercased
  # addr extension (which includes a delimiter), and %d for lowercased domain.
  # Only the first occurrence of '?' will be replaced by users.id,
  # subsequent occurrences of '?' will see empty string as an argument.
  # There can be zero or more occurrences of each %k, %a, %l, %u, %e, %d,
  # lookup keys will be replicated accordingly.
  # This is a separate legacy variable for upwards compatibility, now only
  # referenced by the program through %sql_clause entry 'sel_wblist' - newer
  # config files may assign directly to $sql_clause{'sel_wblist'} if preferred.
  #
  $sql_select_white_black_list =
    'SELECT wb FROM wblist JOIN mailaddr ON wblist.sid=mailaddr.id'.
    ' WHERE wblist.rid=? AND mailaddr.email IN (%k)'.
    ' ORDER BY mailaddr.priority DESC';

  %sql_clause = (
    'sel_policy' => \$sql_select_policy,
    'sel_wblist' => \$sql_select_white_black_list,
    'sel_adr' =>
      'SELECT id FROM maddr WHERE partition_tag=? AND email=?',
    'ins_adr' =>
      'INSERT INTO maddr (partition_tag, email, domain) VALUES (?,?,?)',
    'ins_msg' =>
      'INSERT INTO msgs (partition_tag, mail_id, secret_id, am_id,'.
      ' time_num, time_iso, sid, policy, client_addr, size, host)'.
      ' VALUES (?,?,?,?,?,?,?,?,?,?,?)',
    'upd_msg' =>
      'UPDATE msgs SET content=?, quar_type=?, quar_loc=?, dsn_sent=?,'.
      ' spam_level=?, message_id=?, from_addr=?, subject=?, client_addr=?,'.
      ' originating=?'.
      ' WHERE partition_tag=? AND mail_id=?',
    'ins_rcp' =>
      'INSERT INTO msgrcpt (partition_tag, mail_id, rseqnum, rid, is_local,'.
      ' content, ds, rs, bl, wl, bspam_level, smtp_resp)'.
      ' VALUES (?,?,?,?,?,?,?,?,?,?,?,?)',
    'ins_quar' =>
      'INSERT INTO quarantine (partition_tag, mail_id, chunk_ind, mail_text)'.
      ' VALUES (?,?,?,?)',
    'sel_msg' =>  # obtains partition_tag if missing in a release request
      'SELECT partition_tag FROM msgs WHERE mail_id=?',
    'sel_quar' =>
      'SELECT mail_text FROM quarantine'.
      ' WHERE partition_tag=? AND mail_id=?'.
      ' ORDER BY chunk_ind',
    'sel_penpals' =>  # no message-id references list
      "SELECT msgs.time_num, msgs.mail_id, subject".
      " FROM msgs JOIN msgrcpt USING (partition_tag,mail_id)".
      " WHERE sid=? AND rid=? AND msgs.content!='V' AND ds='P'".
      " ORDER BY msgs.time_num DESC",  # LIMIT 1
    'sel_penpals_msgid' =>  # with a nonempty list of message-id references
      "SELECT msgs.time_num, msgs.mail_id, subject, message_id, rid".
      " FROM msgs JOIN msgrcpt USING (partition_tag,mail_id)".
      " WHERE sid=? AND msgs.content!='V' AND ds='P' AND message_id IN (%m)".
        " AND rid!=sid".
      " ORDER BY rid=? DESC, msgs.time_num DESC",  # LIMIT 1
  );
  # NOTE on $sql_clause{'upd_msg'}: MySQL clobbers timestamp on update
  # (unless DEFAULT 0 is used) setting it to a current local time and
  # losing the cherishly preserved and prepared timestamp of mail reception.
  # From the MySQL 4.1 documentation:
  # * With neither DEFAULT nor ON UPDATE clauses, it is the same as
  #   DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP.
  # * suppress the automatic initialization and update behaviors for the first
  #   TIMESTAMP column by explicitly assigning it a constant DEFAULT value
  #   (for example, DEFAULT 0)
  # * The first TIMESTAMP column in table row automatically is updated to
  #   the current timestamp when the value of any other column in the row is
  #   changed, unless the TIMESTAMP column explicitly is assigned a value
  #   other than NULL.

  # maps full string as returned by a file(1) utility into a short string;
  # the first match wins, more specific entries should precede general ones!
  # the result may be a string or a ref to a list of strings;
  # see also sub decompose_part()

  # prepare an arrayref, later to be converted to an Amavis::Lookup::RE object
  $map_full_type_to_short_type_re = [
    [qr/^empty\z/                          => 'empty'],
    [qr/^directory\z/                      => 'dir'],
    [qr/^can't (stat|read)\b/              => 'dat'],  # file(1) diagnostics
    [qr/^cannot open\b/                    => 'dat'],  # file(1) diagnostics
    [qr/^ERROR:/                           => 'dat'],  # file(1) diagnostics
    [qr/can't read magic file|couldn't find any magic files/ => 'dat'],
    [qr/^data\z/                           => 'dat'],

    [qr/^ISO-8859.*\btext\b/               => 'txt'],
    [qr/^Non-ISO.*ASCII\b.*\btext\b/       => 'txt'],
    [qr/^Unicode\b.*\btext\b/i             => 'txt'],
    [qr/^UTF.* Unicode text\b/i            => 'txt'],
    [qr/^'diff' output text\b/             => 'txt'],
    [qr/^GNU message catalog\b/            => 'mo'],

    [qr/^PGP message [Ss]ignature\b/       => ['pgp','pgp.asc'] ],
    [qr/^PGP message.*[Ee]ncrypted\b/      => ['pgp','pgp.enc'] ],
    [qr/^PGP message\z/                    => ['pgp','pgp.enc'] ],
    [qr/^(?:PGP|GPG) encrypted data\b/     => ['pgp','pgp.enc'] ],
    [qr/^PGP public key\b/                 => ['pgp','pgp.asc'] ],
    [qr/^PGP armored data( signed)? message\b/ => ['pgp','pgp.asc'] ],
    [qr/^PGP armored\b/                    => ['pgp','pgp.asc'] ],
    [qr/^PGP\b/                            => 'pgp' ],

  ### 'file' is a bit too trigger happy to claim something is 'mail text'
  # [qr/^RFC 822 mail text\b/              => 'mail'],
    [qr/^(ASCII|smtp|RFC 822) mail text\b/ => 'txt'],

    [qr/^JPEG image data\b/                => ['image','jpg'] ],
    [qr/^GIF image data\b/                 => ['image','gif'] ],
    [qr/^PNG image data\b/                 => ['image','png'] ],
    [qr/^TIFF image data\b/                => ['image','tif'] ],
    [qr/^PCX\b.*\bimage data\b/            => ['image','pcx'] ],
    [qr/^PC bitmap data\b/                 => ['image','bmp'] ],
    [qr/^SVG Scalable Vector Graphics image\b/ => ['image','svg'] ],

    [qr/^MP2\b/                            => ['audio','mpa','mp2'] ],
    [qr/^MP3\b/                            => ['audio','mpa','mp3'] ],
    [qr/\bMPEG ADTS, layer III\b/          => ['audio','mpa','mp3'] ],
    [qr/^ISO Media, MPEG v4 system, 3GPP\b/=> ['audio','mpa','3gpp'] ],
    [qr/^ISO Media, MPEG v4 system\b/      => ['audio','mpa','m4a','m4b'] ],
    [qr/^FLAC audio bitstream data\b/      => ['audio','flac'] ],
    [qr/^Ogg data, FLAC audio\b/           => ['audio','oga'] ],
    [qr/^Ogg data\b/                       => ['audio','ogg'] ],

    [qr/^MPEG video stream data\b/         => ['movie','mpv'] ],
    [qr/^MPEG system stream data\b/        => ['movie','mpg'] ],
    [qr/^MPEG\b/                           => ['movie','mpg'] ],
    [qr/^Matroska data\b/                  => ['movie','mkv'] ],
    [qr/^Microsoft ASF\b/                  => ['movie','wmv'] ],
    [qr/^RIFF\b.*\bAVI\b/                  => ['movie','avi'] ],
    [qr/^RIFF\b.*\banimated cursor\b/      => ['movie','ani'] ],
    [qr/^RIFF\b.*\bWAVE audio\b/           => ['audio','wav'] ],

    [qr/^Macromedia Flash data\b/          => 'swf'],
    [qr/^HTML document text\b/             => 'html'],
    [qr/^XML document text\b/              => 'xml'],
    [qr/^exported SGML document text\b/    => 'sgml'],
    [qr/^PostScript document text\b/       => 'ps'],
    [qr/^PDF document\b/                   => 'pdf'],
    [qr/^Rich Text Format data\b/          => 'rtf'],
    [qr/^Microsoft Office Document\b/i     => 'doc'], # OLE2: doc, ppt, xls,...
    [qr/^Microsoft Word\b/i                => 'doc'],
    [qr/^Microsoft Installer\b/i           => 'doc'], # file(1) may misclassify
    [qr/^Composite Document File V2 Document\b/i => 'cdf-ms'], # Microsoft Office document files (doc, xls, ...)
    [qr/^ms-windows meta(file|font)\b/i    => 'wmf'],
    [qr/^LaTeX\b.*\bdocument text\b/       => 'lat'],
    [qr/^TeX DVI file\b/                   => 'dvi'],
    [qr/\bdocument text\b/                 => 'txt'],
    [qr/^compiled Java class data\b/       => 'java'],
    [qr/^MS Windows 95 Internet shortcut text\b/ => 'url'],
    [qr/^Compressed Google KML Document\b/ => 'kmz'],

    [qr/^frozen\b/                         => 'F'],
    [qr/^gzip compressed\b/                => 'gz'],
    [qr/^bzip compressed\b/                => 'bz'],
    [qr/^bzip2 compressed\b/               => 'bz2'],
    [qr/^(?i:xz) compressed\b/             => 'xz'],
    [qr/^lzip compressed\b/                => 'lzip'],
    [qr/^lzma compressed\b/                => 'lzma'],
    [qr/^lrz compressed\b/                 => 'lrz'],  #***(untested)
    [qr/^lzop compressed\b/                => 'lzo'],
    [qr/^LZ4 compressed\b/                 => 'lz4'],
    [qr/^compress'd/                       => 'Z'],
    [qr/^Zip archive\b/i                   => 'zip'],
    [qr/^7-zip archive\b/i                 => '7z'],
    [qr/^RAR archive\b/i                   => 'rar'],
    [qr/^LHa.*\barchive\b/i                => 'lha'],  # (also known as .lzh)
    [qr/^ARC archive\b/i                   => 'arc'],
    [qr/^ARJ archive\b/i                   => 'arj'],
    [qr/^ACE archive\b/i                   => 'ace'],
    [qr/^Zoo archive\b/i                   => 'zoo'],
    [qr/^(\S+\s+)?tar archive\b/i          => 'tar'],
    [qr/^(\S+\s+)?cpio archive\b/i         => 'cpio'],
    [qr/^StuffIt Archive\b/i               => 'sit'],
    [qr/^Debian binary package\b/i         => 'deb'],  # std. Unix archive (ar)
    [qr/^current ar archive\b/i            => 'a'],    # std. Unix archive (ar)
    [qr/^RPM\b/                            => 'rpm'],
    [qr/^(Transport Neutral Encapsulation Format|TNEF)\b/i => 'tnef'],
    [qr/^Microsoft Cabinet (file|archive)\b/i => 'cab'],
    [qr/^InstallShield Cabinet file\b/     => 'installshield'],
    [qr/^ISO 9660 CD-ROM filesystem\b/i    => 'iso'],

    [qr/^(uuencoded|xxencoded)\b/i         => 'uue'],
    [qr/^binhex\b/i                        => 'hqx'],
    [qr/^(ASCII|text)\b/i                  => 'asc'],
    [qr/^Emacs.*byte-compiled Lisp data/i  => 'asc'],  # BinHex with empty line
    [qr/\bscript\b.* text executable\b/    => 'txt'],

    [qr/^MS Windows\b.*\bDLL\b/                 => ['exe','dll'] ],
    [qr/\bexecutable for MS Windows\b.*\bDLL\b/ => ['exe','dll'] ],
    [qr/^MS-DOS executable \(built-in\)/        => 'asc'],  # starts with LZ
    [qr/^(MS-)?DOS executable\b.*\bDLL\b/       => ['exe','dll'] ],
    [qr/^MS Windows\b.*\bexecutable\b/          => ['exe','exe-ms'] ],
    [qr/\bexecutable\b.*\bfor MS Windows\b/     => ['exe','exe-ms'] ],
    [qr/^COM executable for DOS\b/              => 'asc'],  # misclassified?
    [qr/^DOS executable \(COM\)/                => 'asc'],  # misclassified?
    [qr/^(MS-)?DOS executable\b(?!.*\(COM\))/   => ['exe','exe-ms'] ],
    [qr/^PA-RISC.*\bexecutable\b/          => ['exe','exe-unix'] ],
    [qr/^ELF .*\bexecutable\b/             => ['exe','exe-unix'] ],
    [qr/^COFF format .*\bexecutable\b/     => ['exe','exe-unix'] ],
    [qr/^executable \(RISC System\b/       => ['exe','exe-unix'] ],
    [qr/^VMS\b.*\bexecutable\b/            => ['exe','exe-vms'] ],
    [qr/\bexecutable\b/i                   => 'exe'],

    [qr/\bshared object, /i                => 'so'],
    [qr/\brelocatable, /i                  => 'o'],
    [qr/\btext\b/i                         => 'asc'],
    [qr/^Zstandard compressed\b/           => 'zst'],
    [qr/^/                                 => 'dat'],  # catchall
  ];

  # MS Windows PE 32-bit Intel 80386 GUI executable not relocatable
  # MS-DOS executable (EXE), OS/2 or MS Windows
  # MS-DOS executable PE  for MS Windows (DLL) (GUI) Intel 80386 32-bit
  # MS-DOS executable PE  for MS Windows (DLL) (GUI) Alpha 32-bit
  # MS-DOS executable, NE for MS Windows 3.x (driver)
  # MS-DOS executable (built-in)  (any file starting with LZ!)
  # PE executable for MS Windows (DLL) (GUI) Intel 80386 32-bit
  # PE executable for MS Windows (GUI) Intel 80386 32-bit
  # NE executable for MS Windows 3.x
  # PA-RISC1.1 executable dynamically linked
  # PA-RISC1.1 shared executable dynamically linked
  # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (FreeBSD),
  #   for FreeBSD 5.0.1, dynamically linked (uses shared libs), stripped
  # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (SYSV),
  #   for GNU/Linux 2.2.5, dynamically linked (uses shared libs), stripped
  # ELF 64-bit MSB executable, SPARC V9, version 1 (FreeBSD),
  #   for FreeBSD 5.0, dynamically linked (uses shared libs), stripped
  # ELF 64-bit MSB shared object, SPARC V9, version 1 (FreeBSD), stripped
  # ELF 32-bit LSB executable, Intel 80386, version 1, dynamically`
  # ELF 32-bit MSB executable, SPARC, version 1, dynamically linke`
  # COFF format alpha executable paged stripped - version 3.11-10
  # COFF format alpha executable paged dynamically linked stripped`
  # COFF format alpha demand paged executable or object module
  #   stripped - version 3.11-10
  # COFF format alpha paged dynamically linked not stripped shared`
  # executable (RISC System/6000 V3.1) or obj module
  # VMS VAX executable


  # A list of pairs or n-tuples: [short-type, code_ref, optional-args...].
  # Maps short types to a decoding routine, the first match wins.
  # Arguments beyond the first two can be a program path string (or a listref
  # of paths to be searched) or a reference to a variable containing such
  # path - which allows for lazy evaluation, making possible to assign values
  # to legacy configuration variables even after the assignment to @decoders.
  #
  @decoders = (
    ['mail', \&Amavis::Unpackers::do_mime_decode],
#   [[qw(asc uue hqx ync)], \&Amavis::Unpackers::do_ascii],  # not safe
    ['F',    \&Amavis::Unpackers::do_uncompress, \$unfreeze],
             # ['unfreeze', 'freeze -d', 'melt', 'fcat'] ],
    ['Z',    \&Amavis::Unpackers::do_uncompress, \$uncompress],
             # ['uncompress', 'gzip -d', 'zcat'] ],
    ['gz',   \&Amavis::Unpackers::do_uncompress, \$gunzip],
    ['gz',   \&Amavis::Unpackers::do_gunzip],
    ['bz2',  \&Amavis::Unpackers::do_uncompress, \$bunzip2],
    ['xz',   \&Amavis::Unpackers::do_uncompress,
             ['xzdec', 'xz -dc', 'unxz -c', 'xzcat'] ],
    ['lzma', \&Amavis::Unpackers::do_uncompress,
             ['lzmadec', 'xz -dc --format=lzma',
              'lzma -dc', 'unlzma -c', 'lzcat', 'lzmadec'] ],
    ['lrz',  \&Amavis::Unpackers::do_uncompress,
             ['lrzip -q -k -d -o -', 'lrzcat -q -k'] ],
    ['lzo',  \&Amavis::Unpackers::do_uncompress, \$unlzop],
    ['lzip', \&Amavis::Unpackers::do_uncompress, ['lzip -d'] ],
    ['lz4',  \&Amavis::Unpackers::do_uncompress, ['lz4c -d'] ],
    ['rpm',  \&Amavis::Unpackers::do_uncompress, \$rpm2cpio],
             # ['rpm2cpio.pl', 'rpm2cpio'] ],
    [['cpio','tar'], \&Amavis::Unpackers::do_pax_cpio, \$pax],
             # ['/usr/local/heirloom/usr/5bin/pax', 'pax', 'gcpio', 'cpio'] ],
#   ['tar',  \&Amavis::Unpackers::do_tar],  # no longer supported
    ['deb',  \&Amavis::Unpackers::do_ar, \$ar],
#   ['a',    \&Amavis::Unpackers::do_ar, \$ar], #unpacking .a seems an overkill
    ['rar',  \&Amavis::Unpackers::do_unrar, \$unrar],  # ['unrar', 'rar']
    ['arj',  \&Amavis::Unpackers::do_unarj, \$unarj],  # ['unarj', 'arj']
    ['arc',  \&Amavis::Unpackers::do_arc,   \$arc],    # ['nomarch', 'arc']
    ['zoo',  \&Amavis::Unpackers::do_zoo,   \$zoo],    # ['zoo', 'unzoo']
    ['doc',  \&Amavis::Unpackers::do_ole,   \$ripole],
    ['cab',  \&Amavis::Unpackers::do_cabextract, \$cabextract],
    ['tnef', \&Amavis::Unpackers::do_tnef_ext, \$tnef],
    ['tnef', \&Amavis::Unpackers::do_tnef],
#   ['lha',  \&Amavis::Unpackers::do_lha,   \$lha],  # not safe, use 7z instead
#   ['sit',  \&Amavis::Unpackers::do_unstuff, \$unstuff],  # not safe
    [['zip','kmz'], \&Amavis::Unpackers::do_7zip,  ['7za', '7zz', '7z'] ],
    [['zip','kmz'], \&Amavis::Unpackers::do_unzip],
    ['zst',  \&Amavis::Unpackers::do_uncompress, ['unzstd'] ],
    [[qw(7z lzma xz)],
             \&Amavis::Unpackers::do_7zip,  ['7zr', '7za', '7zz', '7z'] ],
    [[qw(bz2 Z cab gz tar zst)],
             \&Amavis::Unpackers::do_7zip,  ['7za', '7zz', '7z'] ],
    [[qw(arj cpio deb iso jar lha rar rpm swf)],
             \&Amavis::Unpackers::do_7zip,  ['7zz', '7z'] ],
    ['exe',  \&Amavis::Unpackers::do_executable, \$unrar, \$lha, \$unarj],
  );

  # build_default_maps

  @local_domains_maps = (
    \%local_domains, \@local_domains_acl, \$local_domains_re);
  @mynetworks_maps = (\@mynetworks);
  @client_ipaddr_policy = map(($_,'MYNETS'), @mynetworks_maps);
  @ip_repu_ignore_maps = (\@ip_repu_ignore_networks);

  @bypass_virus_checks_maps = (
    \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);
  @bypass_spam_checks_maps = (
    \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);
  @bypass_banned_checks_maps = (
    \%bypass_banned_checks, \@bypass_banned_checks_acl, \$bypass_banned_checks_re);
  @bypass_header_checks_maps = (
    \%bypass_header_checks, \@bypass_header_checks_acl, \$bypass_header_checks_re);
  @virus_lovers_maps = (
    \%virus_lovers, \@virus_lovers_acl, \$virus_lovers_re);
  @spam_lovers_maps = (
    \%spam_lovers, \@spam_lovers_acl, \$spam_lovers_re);
  @banned_files_lovers_maps = (
    \%banned_files_lovers, \@banned_files_lovers_acl, \$banned_files_lovers_re);
  @bad_header_lovers_maps = (
    \%bad_header_lovers, \@bad_header_lovers_acl, \$bad_header_lovers_re);
# @unchecked_lovers_maps = ();  # empty, new setting, no need for backw compat.
  @warnvirusrecip_maps  = (\$warnvirusrecip);
  @warnbannedrecip_maps = (\$warnbannedrecip);
  @warnbadhrecip_maps   = (\$warnbadhrecip);
  @newvirus_admin_maps  = (\$newvirus_admin);
  @virus_admin_maps     = (\%virus_admin, \$virus_admin);
  @banned_admin_maps    = (\$banned_admin, \%virus_admin, \$virus_admin);
  @bad_header_admin_maps= (\$bad_header_admin);
  @spam_admin_maps      = (\%spam_admin, \$spam_admin);
  @virus_quarantine_to_maps = (\$virus_quarantine_to);
  @banned_quarantine_to_maps = (\$banned_quarantine_to);
  @unchecked_quarantine_to_maps = (\$unchecked_quarantine_to);
  @spam_quarantine_to_maps = (\$spam_quarantine_to);
  @spam_quarantine_bysender_to_maps = (\$spam_quarantine_bysender_to);
  @bad_header_quarantine_to_maps = (\$bad_header_quarantine_to);
  @clean_quarantine_to_maps = (\$clean_quarantine_to);
  @archive_quarantine_to_maps = (\$archive_quarantine_to);
  @keep_decoded_original_maps = (\$keep_decoded_original_re);
  @map_full_type_to_short_type_maps = (\$map_full_type_to_short_type_re);
# @banned_filename_maps = ( {'.' => [$banned_filename_re]} );
# @banned_filename_maps = ( {'.' => 'DEFAULT'} );#names mapped by %banned_rules
  @banned_filename_maps = ( 'DEFAULT' );  # same as above, but shorter
  @viruses_that_fake_sender_maps = (\$viruses_that_fake_sender_re, 1);
  @spam_tag_level_maps  = (\$sa_tag_level_deflt);     # CC_CLEAN,1
  @spam_tag2_level_maps = (\$sa_tag2_level_deflt);    # CC_SPAMMY
  @spam_tag3_level_maps = (\$sa_tag3_level_deflt);    # CC_SPAMMY,1
  @spam_kill_level_maps = (\$sa_kill_level_deflt);    # CC_SPAM
  @spam_dsn_cutoff_level_maps = (\$sa_dsn_cutoff_level);
  @spam_dsn_cutoff_level_bysender_maps = (\$sa_dsn_cutoff_level);
  @spam_crediblefrom_dsn_cutoff_level_maps =
    (\$sa_crediblefrom_dsn_cutoff_level);
  @spam_crediblefrom_dsn_cutoff_level_bysender_maps =
    (\$sa_crediblefrom_dsn_cutoff_level);
  @spam_quarantine_cutoff_level_maps = (\$sa_quarantine_cutoff_level);
  @spam_subject_tag_maps  = (\$sa_spam_subject_tag1); # note: inconsistent name
  @spam_subject_tag2_maps = (\$sa_spam_subject_tag);  # note: inconsistent name
# @spam_subject_tag3_maps = ();    # new variable, no backward compatib. needed
  @whitelist_sender_maps = (
    \%whitelist_sender, \@whitelist_sender_acl, \$whitelist_sender_re);
  @blacklist_sender_maps = (
    \%blacklist_sender, \@blacklist_sender_acl, \$blacklist_sender_re);
  @addr_extension_virus_maps  = (\$addr_extension_virus);
  @addr_extension_spam_maps   = (\$addr_extension_spam);
  @addr_extension_banned_maps = (\$addr_extension_banned);
  @addr_extension_bad_header_maps = (\$addr_extension_bad_header);
  @debug_sender_maps = (\@debug_sender_acl);
# @debug_recipient_maps = ();
  @remove_existing_spam_headers_maps = (\$remove_existing_spam_headers);

  # new variables, no backward compatibility needed, empty by default:
  # @score_sender_maps, @author_to_policy_bank_maps, @signer_reputation_maps,
  # @message_size_limit_maps

  # build backward-compatible settings hashes
  #
  %final_destiny_maps_by_ccat = (
    # value is normally a list of by-recipient lookup tables, but for compa-
    # tibility with old %final_destiny_by_ccat a value may also be a scalar
    CC_VIRUS,       sub { c('final_virus_destiny') },
    CC_BANNED,      sub { c('final_banned_destiny') },
    CC_UNCHECKED,   sub { c('final_unchecked_destiny') },
    CC_SPAM,        sub { c('final_spam_destiny') },
    CC_BADH,        sub { c('final_bad_header_destiny') },
    CC_MTA.',1',    D_TEMPFAIL,  # MTA response was 4xx
    CC_MTA.',2',    D_REJECT,    # MTA response was 5xx
    CC_MTA,         D_TEMPFAIL,
    CC_OVERSIZED,   D_BOUNCE,
    CC_CATCHALL,    D_PASS,
  );
  %forward_method_maps_by_ccat = (
    CC_CATCHALL,    sub { ca('forward_method_maps') },
  );
  %smtp_reason_by_ccat = (
    # currently only used for blocked messages only, status 5xx
    # a multiline message will produce a valid multiline SMTP response
    CC_VIRUS,       'id=%n - INFECTED: %V',
    CC_BANNED,      'id=%n - BANNED: %F',
    CC_UNCHECKED.',1', 'id=%n - UNCHECKED: encrypted',
    CC_UNCHECKED.',2', 'id=%n - UNCHECKED: over limits',
    CC_UNCHECKED.',3', 'id=%n - UNCHECKED: ambiguous content',
    CC_UNCHECKED,      'id=%n - UNCHECKED',
    CC_SPAM,        'id=%n - spam',
    CC_SPAMMY.',1', 'id=%n - spammy (tag3)',
    CC_SPAMMY,      'id=%n - spammy',
    CC_BADH.',1',   'id=%n - BAD HEADER: MIME error',
    CC_BADH.',2',   'id=%n - BAD HEADER: nonencoded 8-bit character',
    CC_BADH.',3',   'id=%n - BAD HEADER: contains invalid control character',
    CC_BADH.',4',   'id=%n - BAD HEADER: line made up entirely of whitespace',
    CC_BADH.',5',   'id=%n - BAD HEADER: line longer than RFC 5322 limit',
    CC_BADH.',6',   'id=%n - BAD HEADER: syntax error',
    CC_BADH.',7',   'id=%n - BAD HEADER: missing required header field',
    CC_BADH.',8',   'id=%n - BAD HEADER: duplicate header field',
    CC_BADH,        'id=%n - BAD HEADER',
    CC_OVERSIZED,   'id=%n - Message size exceeds recipient\'s size limit',
    CC_MTA.',1',    'id=%n - Temporary MTA failure on relaying',
    CC_MTA.',2',    'id=%n - Rejected by next-hop MTA on relaying',
    CC_MTA,         'id=%n - Unable to relay message back to MTA',
    CC_CLEAN,       'id=%n - CLEAN',
    CC_CATCHALL,    'id=%n - OTHER',  # should not happen
  );
  %lovers_maps_by_ccat = (
    CC_VIRUS,       sub { ca('virus_lovers_maps') },
    CC_BANNED,      sub { ca('banned_files_lovers_maps') },
    CC_UNCHECKED,   sub { ca('unchecked_lovers_maps') },
    CC_SPAM,        sub { ca('spam_lovers_maps') },
    CC_SPAMMY,      sub { ca('spam_lovers_maps') },
    CC_BADH,        sub { ca('bad_header_lovers_maps') },
  );
  %defang_maps_by_ccat = (
    # compatible with legacy %defang_by_ccat: value may be a scalar
    CC_VIRUS,       sub { c('defang_virus') },
    CC_BANNED,      sub { c('defang_banned') },
    CC_UNCHECKED,   sub { c('defang_undecipherable') },
    CC_SPAM,        sub { c('defang_spam') },
    CC_SPAMMY,      sub { c('defang_spam') },
  # CC_BADH.',3',   1,  # NUL or CR character in header section
  # CC_BADH.',5',   1,  # header line longer than 998 characters
  # CC_BADH.',6',   1,  # header field syntax error
    CC_BADH,        sub { c('defang_bad_header') },
  );
  %subject_tag_maps_by_ccat = (
    CC_VIRUS,       [ '***INFECTED*** ' ],
    CC_BANNED,      undef,
    CC_UNCHECKED,   sub { [ c('undecipherable_subject_tag') ] }, # not by-recip
    CC_SPAM,        undef,
    CC_SPAMMY.',1', sub { ca('spam_subject_tag3_maps') },
    CC_SPAMMY,      sub { ca('spam_subject_tag2_maps') },
    CC_CLEAN.',1',  sub { ca('spam_subject_tag_maps') },
  );
  %quarantine_method_by_ccat = (
    CC_VIRUS,       sub { c('virus_quarantine_method') },
    CC_BANNED,      sub { c('banned_files_quarantine_method') },
    CC_UNCHECKED,   sub { c('unchecked_quarantine_method') },
    CC_SPAM,        sub { c('spam_quarantine_method') },
    CC_BADH,        sub { c('bad_header_quarantine_method') },
    CC_CLEAN,       sub { c('clean_quarantine_method') },
  );
  %quarantine_to_maps_by_ccat = (
    CC_VIRUS,       sub { ca('virus_quarantine_to_maps') },
    CC_BANNED,      sub { ca('banned_quarantine_to_maps') },
    CC_UNCHECKED,   sub { ca('unchecked_quarantine_to_maps') },
    CC_SPAM,        sub { ca('spam_quarantine_to_maps') },
    CC_BADH,        sub { ca('bad_header_quarantine_to_maps') },
    CC_CLEAN,       sub { ca('clean_quarantine_to_maps') },
  );
  %admin_maps_by_ccat = (
    CC_VIRUS,       sub { ca('virus_admin_maps') },
    CC_BANNED,      sub { ca('banned_admin_maps') },
    CC_UNCHECKED,   sub { ca('virus_admin_maps') },
    CC_SPAM,        sub { ca('spam_admin_maps') },
    CC_BADH,        sub { ca('bad_header_admin_maps') },
  );
  %always_bcc_by_ccat = (
    CC_CATCHALL,    sub { c('always_bcc') },
  );
  %dsn_bcc_by_ccat = (
    CC_CATCHALL,    sub { c('dsn_bcc') },
  );
  %mailfrom_notify_admin_by_ccat = (
    CC_SPAM,        sub { c('mailfrom_notify_spamadmin') },
    CC_CATCHALL,    sub { c('mailfrom_notify_admin') },
  );
  %hdrfrom_notify_admin_by_ccat = (
    CC_SPAM,        sub { c('hdrfrom_notify_spamadmin') },
    CC_CATCHALL,    sub { c('hdrfrom_notify_admin') },
  );
  %mailfrom_notify_recip_by_ccat = (
    CC_CATCHALL,    sub { c('mailfrom_notify_recip') },
  );
  %hdrfrom_notify_recip_by_ccat = (
    CC_CATCHALL,    sub { c('hdrfrom_notify_recip') },
  );
  %hdrfrom_notify_sender_by_ccat = (
    CC_CATCHALL,    sub { c('hdrfrom_notify_sender') },
  );
  %hdrfrom_notify_release_by_ccat = (
    CC_CATCHALL,    sub { c('hdrfrom_notify_release') },
  );
  %hdrfrom_notify_report_by_ccat = (
    CC_CATCHALL,    sub { c('hdrfrom_notify_report') },
  );
  %notify_admin_templ_by_ccat = (
    CC_SPAM,        sub { cr('notify_spam_admin_templ') },
    CC_CATCHALL,    sub { cr('notify_virus_admin_templ') },
  );
  %notify_recips_templ_by_ccat = (
    CC_SPAM,        sub { cr('notify_spam_recips_templ') },  #usually empty
    CC_CATCHALL,    sub { cr('notify_virus_recips_templ') },
  );
  %notify_sender_templ_by_ccat = (  # bounce templates
    CC_VIRUS,       sub { cr('notify_virus_sender_templ') },
    CC_BANNED,      sub { cr('notify_virus_sender_templ') }, #historical reason
    CC_SPAM,        sub { cr('notify_spam_sender_templ') },
    CC_CATCHALL,    sub { cr('notify_sender_templ') },
  );
  %notify_release_templ_by_ccat = (
    CC_CATCHALL,    sub { cr('notify_release_templ') },
  );
  %notify_report_templ_by_ccat = (
    CC_CATCHALL,    sub { cr('notify_report_templ') },
  );
  %notify_autoresp_templ_by_ccat = (
    CC_CATCHALL,    sub { cr('notify_autoresp_templ') },
  );
  %warnsender_by_ccat = (  # deprecated use, except perhaps for CC_BADH
    CC_VIRUS,       undef,
    CC_BANNED,      sub { c('warnbannedsender') },
    CC_SPAM,        undef,
    CC_BADH,        sub { c('warnbadhsender') },
  );
  %warnrecip_maps_by_ccat = (
    CC_VIRUS,       sub { ca('warnvirusrecip_maps') },
    CC_BANNED,      sub { ca('warnbannedrecip_maps') },
    CC_SPAM,        undef,
    CC_BADH,        sub { ca('warnbadhrecip_maps') },
  );
  %addr_extension_maps_by_ccat = (
    CC_VIRUS,       sub { ca('addr_extension_virus_maps') },
    CC_BANNED,      sub { ca('addr_extension_banned_maps') },
    CC_SPAM,        sub { ca('addr_extension_spam_maps') },
    CC_SPAMMY,      sub { ca('addr_extension_spam_maps') },
    CC_BADH,        sub { ca('addr_extension_bad_header_maps') },
  # CC_OVERSIZED,   'oversized';
  );
  %addr_rewrite_maps_by_ccat = ( );
  1;
} # end BEGIN - init_tertiary


# prototypes
sub Amavis::Unpackers::do_mime_decode($$);
sub Amavis::Unpackers::do_ascii($$);
sub Amavis::Unpackers::do_uncompress($$$);
sub Amavis::Unpackers::do_gunzip($$);
sub Amavis::Unpackers::do_pax_cpio($$$);
#sub Amavis::Unpackers::do_tar($$);  # no longer supported
sub Amavis::Unpackers::do_ar($$$);
sub Amavis::Unpackers::do_unzip($$;$$);
sub Amavis::Unpackers::do_7zip($$$;$);
sub Amavis::Unpackers::do_unrar($$$;$);
sub Amavis::Unpackers::do_unarj($$$;$);
sub Amavis::Unpackers::do_arc($$$);
sub Amavis::Unpackers::do_zoo($$$);
sub Amavis::Unpackers::do_lha($$$;$);
sub Amavis::Unpackers::do_ole($$$);
sub Amavis::Unpackers::do_cabextract($$$);
sub Amavis::Unpackers::do_tnef($$);
sub Amavis::Unpackers::do_tnef_ext($$$);
sub Amavis::Unpackers::do_unstuff($$$);
sub Amavis::Unpackers::do_executable($$@);

no warnings 'once';
# Define alias names or shortcuts in this module to make it simpler
# to call these routines from amavisd.conf
*read_l10n_templates = \&Amavis::Util::read_l10n_templates;
*read_text       = \&Amavis::Util::read_text;
*read_hash       = \&Amavis::Util::read_hash;
*read_array      = \&Amavis::Util::read_array;
*read_cidr       = \&Amavis::Util::read_cidr;
*idn_to_ascii    = \&Amavis::Util::idn_to_ascii;  # RFC 3490: ToASCII
*idn_to_utf8     = \&Amavis::Util::idn_to_utf8;   # RFC 3490: ToUnicode
*mail_idn_to_ascii = \&Amavis::Util::mail_addr_idn_to_ascii;
*dump_hash       = \&Amavis::Util::dump_hash;
*dump_array      = \&Amavis::Util::dump_array;
*ask_daemon      = \&Amavis::AV::ask_daemon;
*ask_clamav      = \&Amavis::AV::ask_clamav;  # deprecated, use ask_daemon
*do_mime_decode  = \&Amavis::Unpackers::do_mime_decode;
*do_ascii        = \&Amavis::Unpackers::do_ascii;
*do_uncompress   = \&Amavis::Unpackers::do_uncompress;
*do_gunzip       = \&Amavis::Unpackers::do_gunzip;
*do_pax_cpio     = \&Amavis::Unpackers::do_pax_cpio;
*do_tar          = \&Amavis::Unpackers::do_tar;  # no longer supported
*do_ar           = \&Amavis::Unpackers::do_ar;
*do_unzip        = \&Amavis::Unpackers::do_unzip;
*do_unrar        = \&Amavis::Unpackers::do_unrar;
*do_7zip         = \&Amavis::Unpackers::do_7zip;
*do_unarj        = \&Amavis::Unpackers::do_unarj;
*do_arc          = \&Amavis::Unpackers::do_arc;
*do_zoo          = \&Amavis::Unpackers::do_zoo;
*do_lha          = \&Amavis::Unpackers::do_lha;
*do_ole          = \&Amavis::Unpackers::do_ole;
*do_cabextract   = \&Amavis::Unpackers::do_cabextract;
*do_tnef_ext     = \&Amavis::Unpackers::do_tnef_ext;
*do_tnef         = \&Amavis::Unpackers::do_tnef;
*do_unstuff      = \&Amavis::Unpackers::do_unstuff;
*do_executable   = \&Amavis::Unpackers::do_executable;

*iso8601_week          = \&Amavis::rfc2821_2822_Tools::iso8601_week;
*iso8601_yearweek      = \&Amavis::rfc2821_2822_Tools::iso8601_yearweek;
*iso8601_year_and_week = \&Amavis::rfc2821_2822_Tools::iso8601_year_and_week;
*iso8601_weekday       = \&Amavis::rfc2821_2822_Tools::iso8601_weekday;
*iso8601_timestamp     = \&Amavis::rfc2821_2822_Tools::iso8601_timestamp;
*iso8601_utc_timestamp = \&Amavis::rfc2821_2822_Tools::iso8601_utc_timestamp;

# a shorthand for creating a regexp-based lookup table
sub new_RE    { require Amavis::Lookup::RE; Amavis::Lookup::RE->new(@_) }

# shorthand: construct a query object for a DNSxL query on an IP address
sub q_dns_a   { require Amavis::Lookup::DNSxL; Amavis::Lookup::DNSxL->new(@_) }  # dns zone, expect, resolver

# shorthand: construct a query object for an SQL field
sub q_sql_s   { require Amavis::Lookup::SQLfield; Amavis::Lookup::SQLfield->new(undef, $_[0], 'S-') }  # string
sub q_sql_n   { require Amavis::Lookup::SQLfield; Amavis::Lookup::SQLfield->new(undef, $_[0], 'N-') }  # numeric
sub q_sql_b   { require Amavis::Lookup::SQLfield; Amavis::Lookup::SQLfield->new(undef, $_[0], 'B-') }  # boolean

# shorthand: construct a query object for an LDAP attribute
sub q_ldap_s  { require Amavis::Lookup::LDAPattr; Amavis::Lookup::LDAPattr->new(undef, $_[0], 'S-') }  # string
sub q_ldap_n  { require Amavis::Lookup::LDAPattr; Amavis::Lookup::LDAPattr->new(undef, $_[0], 'N-') }  # numeric
sub q_ldap_b  { require Amavis::Lookup::LDAPattr; Amavis::Lookup::LDAPattr->new(undef, $_[0], 'B-') }  # boolean

sub Opaque    { require Amavis::Lookup::Opaque; Amavis::Lookup::Opaque->new(@_) }
sub OpaqueRef { require Amavis::Lookup::OpaqueRef; Amavis::Lookup::OpaqueRef->new(@_) }
#
# Opaque provides a wrapper to arbitrary data structures, allowing them to be
# treated as 'constant' pseudo-lookups, i.e. preventing arrays and hashes from
# being interpreted as lookup lists/tables. In case of $forward_method this
# allows for a listref of failover methods. Without the protection of Opaque
# the listref would be interpreted by a lookup() as an acl lookup type instead
# of a match-always data structure. The Opaque subroutine is not yet available
# during a BEGIN phase, so this assignment must come after compiling the rest
# of the code.
#
# This is the only case where both an array @*_maps as well as its default
# element are members of a policy bank. Use lazy evaluation through a sub
# to make this work as expected.
#
# @forward_method_maps = ( OpaqueRef(\$forward_method) );
@forward_method_maps = ( sub { Opaque(c('forward_method')) } );

# retain compatibility with old names
use vars qw(%final_destiny_by_ccat %defang_by_ccat
            $sql_partition_tag $DO_SYSLOG $LOGFILE);
*final_destiny_by_ccat = \%final_destiny_maps_by_ccat;
*defang_by_ccat = \%defang_maps_by_ccat;
*sql_partition_tag = \$partition_tag;
*DO_SYSLOG = \$do_syslog;
*LOGFILE = \$logfile;

@virus_name_to_spam_score_maps =
  (new_RE(  # the order matters, first match wins
    [ qr'^Structured\.(SSN|CreditCardNumber)\b'            => 0.1 ],
    [ qr'^(Heuristics\.)?Phishing\.'                       => 0.1 ],
    [ qr'^(Email|HTML)\.Phishing\.(?!.*Sanesecurity)'      => 0.1 ],
    [ qr'^Sanesecurity\.(Malware|Rogue|Trojan)\.' => undef ],# keep as infected
    [ qr'^Sanesecurity\.Foxhole\.Zip_exe'                  => 0.1 ], # F.P.
    [ qr'^Sanesecurity\.Foxhole\.Zip_bat'                  => 0.1 ], # F.P.
    [ qr'^Sanesecurity\.Foxhole\.Mail_gz'                  => 0.1 ], # F.P.
    [ qr'^Sanesecurity\.Foxhole\.Mail_ace'                 => 0.1 ], # F.P.
    [ qr'^Sanesecurity\.Foxhole\.Mail_tar'                 => 0   ], # F.P.
    [ qr'^Sanesecurity\.Foxhole\.'                => undef ],# keep as infected
    [ qr'^Sanesecurity\.'                                  => 0.1 ],
    [ qr'^Sanesecurity_PhishBar_'                          => 0   ],
    [ qr'^Sanesecurity.TestSig_'                           => 0   ],
    [ qr'^Email\.Spam\.Bounce(\.[^., ]*)*\.Sanesecurity\.' => 0   ],
    [ qr'^Email\.Spammail\b'                               => 0.1 ],
    [ qr'^MSRBL-(Images|SPAM)\b'                           => 0.1 ],
    [ qr'^VX\.Honeypot-SecuriteInfo\.com\.Joke'            => 0.1 ],
    [ qr'^VX\.not-virus_(Hoax|Joke)\..*-SecuriteInfo\.com(\.|\z)' => 0.1 ],
    [ qr'^Email\.Spam.*-SecuriteInfo\.com(\.|\z)'          => 0.1 ],
    [ qr'^Safebrowsing\.'                                  => 0.1 ],
    [ qr'^winnow\.(phish|spam)\.'                          => 0.1 ],
    [ qr'^INetMsg\.SpamDomain'                             => 0.1 ],
    [ qr'^Doppelstern\.(Spam|Scam|Phishing|Junk|Lott|Loan)'=> 0.1 ],
    [ qr'^Bofhland\.Phishing'                              => 0.1 ],
    [ qr'^ScamNailer\.'                                    => 0.1 ],
    [ qr'^HTML/Bankish'                                    => 0.1 ],  # F-Prot
    [ qr'^PORCUPINE_JUNK'                                  => 0.1 ],
    [ qr'^PORCUPINE_PHISHING'                              => 0.1 ],
    [ qr'^Porcupine\.Junk'                                 => 0.1 ],
    [ qr'^PhishTank\.Phishing\.'                           => 0.1 ],
    [ qr'-SecuriteInfo\.com(\.|\z)'         => undef ],  # keep as infected
    [ qr'^MBL_NA\.UNOFFICIAL'               => 0.1 ],    # false positives
    [ qr'^MBL_'                             => undef ],  # keep as infected
  ));
# Sanesecurity       http://www.sanesecurity.co.uk/
# MSRBL-             http://www.msrbl.com/site/contact
# MBL                http://www.malware.com.br/index.shtml
# -SecuriteInfo.com  http://clamav.securiteinfo.com/malwares.html

# prepend a lookup table label object for logging purposes
#
sub label_default_maps() {
  for my $varname (qw(
    @disclaimer_options_bysender_maps @dkim_signature_options_bysender_maps
    @local_domains_maps @mynetworks_maps @ip_repu_ignore_maps
    @forward_method_maps @newvirus_admin_maps @banned_filename_maps
    @spam_quarantine_bysender_to_maps
    @spam_tag_level_maps @spam_tag2_level_maps @spam_tag3_level_maps
    @spam_kill_level_maps
    @spam_subject_tag_maps @spam_subject_tag2_maps @spam_subject_tag3_maps
    @spam_dsn_cutoff_level_maps @spam_dsn_cutoff_level_bysender_maps
    @spam_crediblefrom_dsn_cutoff_level_maps
    @spam_crediblefrom_dsn_cutoff_level_bysender_maps
    @spam_quarantine_cutoff_level_maps @spam_notifyadmin_cutoff_level_maps
    @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps
    @author_to_policy_bank_maps @signer_reputation_maps
    @message_size_limit_maps @debug_sender_maps @debug_recipient_maps
    @bypass_virus_checks_maps @bypass_spam_checks_maps
    @bypass_banned_checks_maps @bypass_header_checks_maps
    @viruses_that_fake_sender_maps
    @virus_name_to_spam_score_maps @virus_name_to_policy_bank_maps
    @remove_existing_spam_headers_maps
    @sa_userconf_maps @sa_username_maps

    @keep_decoded_original_maps @map_full_type_to_short_type_maps
    @virus_lovers_maps @spam_lovers_maps @unchecked_lovers_maps
    @banned_files_lovers_maps @bad_header_lovers_maps
    @virus_quarantine_to_maps @banned_quarantine_to_maps
    @unchecked_quarantine_to_maps @spam_quarantine_to_maps
    @bad_header_quarantine_to_maps @clean_quarantine_to_maps
    @archive_quarantine_to_maps
    @virus_admin_maps @banned_admin_maps
    @spam_admin_maps @bad_header_admin_maps @spam_modifies_subj_maps
    @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps
    @addr_extension_virus_maps  @addr_extension_spam_maps
    @addr_extension_banned_maps @addr_extension_bad_header_maps
    ))
  {
    my $g = $varname; $g =~ s{\@}{Amavis::Conf::};  # qualified variable name
    my $label = $varname; $label=~s/^\@//; $label=~s/_maps$//;
    { no strict 'refs';
      require Amavis::Lookup::Label;
      unshift(@$g,  # NOTE: a symbolic reference
              Amavis::Lookup::Label->new($label))  if @$g;  # no label if empty
    }
  }
}

# return a list of actually read&evaluated configuration files
sub get_config_files_read() { @actual_config_files }

# read and evaluate a configuration file, some sanity checking and housekeeping
#
sub read_config_file($$) {
  my($config_file,$is_optional) = @_;
  my(@stat_list) = stat($config_file);  # symlinks-friendly
  my $errn = @stat_list ? 0 : 0+$!;
  if ($errn == ENOENT && $is_optional) {
    # don't complain if missing
  } else {
    my $owner_uid = $stat_list[4];
    my $msg;
    if ($errn == ENOENT) { $msg = "does not exist" }
    elsif ($errn)        { $msg = "is inaccessible: $!" }
    elsif (-d _)         { $msg = "is a directory" }
    elsif (-S _ || -b _ || -c _) { $msg = "is not a regular file or pipe" }
    elsif (!$i_know_what_i_am_doing{no_conf_file_writable_check}) {
      if    ($> && -o _) { $msg = "should not be owned by EUID $>"}
      elsif ($> && -w _) { $msg = "is writable by EUID $>, EGID $)" }
      elsif ($owner_uid) { $msg = "should be owned by root (uid 0)" }
    }
    if (defined $msg)    { die "Config file \"$config_file\" $msg," }
    $read_config_files_depth++;  push(@actual_config_files, $config_file);
    if ($read_config_files_depth >= 100) {
      print STDERR "read_config_files: recursion depth limit exceeded\n";
      exit 1;  # avoid unwinding deep recursion, abort right away
    }
    # avoid magic of searching @INC in do() and reporting unrelated errors
    $config_file = './'.$config_file  if $config_file !~ m{^\.{0,2}/};
    local($1,$2,$3,$4,$5,$6,$7,$8,$9);
    local $/ = $/;  # protect us from a potential change in a config file
    $! = 0;
    if (defined(do $config_file)) {}
    elsif ($@ ne '') { die "Error in config file \"$config_file\": $@" }
    elsif ($! != 0)  { die "Error reading config file \"$config_file\": $!" }
    $read_config_files_depth--  if $read_config_files_depth > 0;
  }
  1;
}

sub include_config_files(@)          { read_config_file($_,0)  for @_;  1 }
sub include_optional_config_files(@) { read_config_file($_,1)  for @_;  1 }

# supply remaining defaults after config files have already been read/evaluated
#
sub supply_after_defaults() {
  $daemon_chroot_dir = ''
    if !defined $daemon_chroot_dir || $daemon_chroot_dir eq '/';
  unshift @daemon_groups, $daemon_group if defined $daemon_group;
  @daemon_groups = Amavis::Util::get_user_groups($daemon_user) if not @daemon_groups;
  # provide some sensible defaults for essential settings (post-defaults)
  $TEMPBASE     = $MYHOME                   if !defined $TEMPBASE;
  $helpers_home = $MYHOME                   if !defined $helpers_home;
  $db_home      = "$MYHOME/db"              if !defined $db_home;
  @zmq_sockets  = ( "ipc://$MYHOME/amavisd-zmq.sock" )  if !@zmq_sockets;
  $pid_file     = "$MYHOME/amavisd.pid"     if !defined $pid_file && $daemonize;
# just keep $lock_file undefined by default, a temp file (File::Temp::tmpnam)
# will be provided by Net::Server for 'flock' serialization on a socket accept()
# $lock_file    = "$MYHOME/amavisd.lock"    if !defined $lock_file;
  local($1,$2);
  $X_HEADER_LINE = $myproduct_name . ' at ' .
    Amavis::Util::idn_to_ascii($mydomain)  if !defined $X_HEADER_LINE;
  $X_HEADER_TAG = 'X-Virus-Scanned' if !defined $X_HEADER_TAG;
  if ($X_HEADER_TAG =~ /^[!-9;-\176]+\z/) {
    # implicitly add to %allowed_added_header_fields for compatibility,
    # unless the hash entry already exists
    my $allowed_hdrs = cr('allowed_added_header_fields');
    $allowed_hdrs->{lc($X_HEADER_TAG)} = 1
      if $allowed_hdrs && !exists($allowed_hdrs->{lc($X_HEADER_TAG)});
  }
  $gunzip  = "$gzip -d"   if !defined $gunzip  && $gzip  ne '';
  $bunzip2 = "$bzip2 -d"  if !defined $bunzip2 && $bzip2 ne '';
  $unlzop  = "$lzop -d"   if !defined $unlzop  && $lzop  ne '';

  # substring "${myhostname}" will be expanded later, just before use
  my $pname = '"Content-filter at ${myhostname_utf8}"';
  $hdrfrom_notify_sender = $pname . ' <postmaster@${myhostname_ascii}>'
    if !defined $hdrfrom_notify_sender;
  $hdrfrom_notify_recip = $mailfrom_notify_recip eq ''
    ? $hdrfrom_notify_sender
    : sprintf("%s <%s>", $pname,
              Amavis::Util::mail_addr_idn_to_ascii($mailfrom_notify_recip))
    if !defined $hdrfrom_notify_recip;
  $hdrfrom_notify_admin = $mailfrom_notify_admin eq ''
    ? $hdrfrom_notify_sender
    : sprintf("%s <%s>", $pname,
              Amavis::Util::mail_addr_idn_to_ascii($mailfrom_notify_admin))
    if !defined $hdrfrom_notify_admin;
  $hdrfrom_notify_spamadmin = $mailfrom_notify_spamadmin eq ''
    ? $hdrfrom_notify_sender
    : sprintf("%s <%s>", $pname,
              Amavis::Util::mail_addr_idn_to_ascii($mailfrom_notify_spamadmin))
    if !defined $hdrfrom_notify_spamadmin;
  $hdrfrom_notify_release = $hdrfrom_notify_sender
    if !defined $hdrfrom_notify_release;
  $hdrfrom_notify_report = $hdrfrom_notify_sender
    if !defined $hdrfrom_notify_report;

  if ($final_banned_destiny == D_DISCARD && c('warnbannedsender') )
    { $final_banned_destiny = D_BOUNCE }
  if ($final_bad_header_destiny == D_DISCARD && c('warnbadhsender') )
    { $final_bad_header_destiny = D_BOUNCE }
  if (!%banned_rules) {
    # an associative array mapping a rule name
    # to a single 'banned names/types' lookup table
    %banned_rules = ('DEFAULT'=>$banned_filename_re);  # backward compatible
  }
  1;
}

1;
