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

package Amavis::In::Message;
# this class keeps information about the message being processed

use strict;
use re 'taint';

BEGIN {
  require Exporter;
  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  $VERSION = '2.412';
  @ISA = qw(Exporter);
}

use Amavis::Conf qw(:platform);
use Amavis::In::Message::PerRecip;
use Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp quote_rfc2821_local
                                  qquote_rfc2821_local);
use Amavis::Util qw(ll do_log orcpt_decode);

sub new
  { my $class = $_[0];
    my $self = bless({},$class); $self->skip_bytes(0); $self }

sub conn_obj        # ref to a connection object Amavis::In::Connection
  { @_<2 ? shift->{conn}       : ($_[0]->{conn} = $_[1]) }
sub rx_time         # Unix time (s since epoch) of message reception by amavisd
  { @_<2 ? shift->{rx_time}    : ($_[0]->{rx_time} = $_[1]) }
sub partition_tag   # SQL partition tag (e.g. an ISO week number 1..53, or 0)
  { @_<2 ? shift->{partition}  : ($_[0]->{partition} = $_[1]) }
sub client_proto    # orig. client protocol, obtained from XFORWARD or milter
  { @_<2 ? shift->{cli_proto}  : ($_[0]->{cli_proto} = $_[1]) }
sub client_addr     # original client IP addr, obtained from XFORWARD or milter
  { @_<2 ? shift->{cli_ip}     : ($_[0]->{cli_ip} = $_[1]) }
sub client_name     # orig. client DNS name, obtained from XFORWARD or milter
  { @_<2 ? shift->{cli_name}   : (shift->{cli_name} = $_[1]) }
sub client_port    # orig client src port num, obtained from XFORWARD or milter
  { @_<2 ? shift->{cli_port}   : ($_[0]->{cli_port} = $_[1]) }
sub client_source   # LOCAL/REMOTE/undef, local_header_rewrite_clients/XFORWARD
  { @_<2 ? shift->{cli_source} : ($_[0]->{cli_source} = $_[1]) }
sub client_helo     # orig. client EHLO name, obtained from XFORWARD or milter
  { @_<2 ? shift->{cli_helo}   : ($_[0]->{cli_helo} = $_[1]) }
sub client_os_fingerprint  # SMTP client's OS fingerprint, obtained from p0f
  { @_<2 ? shift->{cli_p0f}    : ($_[0]->{cli_p0f} = $_[1]) }
sub originating     # originating from our users, copied from c('originating')
  { @_<2 ? shift->{originating}: ($_[0]->{originating} = $_[1]) }
sub queue_id # MTA queue ID of message if known (Courier, milter/AM.PDP, XFORW)
  { @_<2 ? shift->{queue_id}   : ($_[0]->{queue_id} = $_[1]) }
sub log_id          # task id as shown in the log, also known as am_id
  { @_<2 ? shift->{log_id}     : ($_[0]->{log_id} = $_[1]) }
sub mail_id         # long-term unique id of the message on this system
  { @_<2 ? shift->{mail_id}    : ($_[0]->{mail_id} = $_[1]) }
sub secret_id       # secret string to grant access to a message with mail_id
  { @_<2 ? $_[0]->{secret_id}  : ($_[0]->{secret_id} = $_[1]) }
sub parent_mail_id  # original mail_id for msgs generated by amavis (DSN,notif)
  { @_<2 ? shift->{parent_mail_id} : ($_[0]->{parent_mail_id} = $_[1]) }
sub attachment_password # scrambles a potentially dangerous released mail
  { @_<2 ? shift->{release_pwd}: ($_[0]->{release_pwd} = $_[1]) }
sub msg_size        # ESMTP SIZE value, later corrected to actual size,RFC 1870
  { @_<2 ? shift->{msg_size}   : ($_[0]->{msg_size} = $_[1]) }
sub auth_user       # ESMTP AUTH username
  { @_<2 ? shift->{auth_user}  : ($_[0]->{auth_user} = $_[1]) }
sub auth_pass       # ESMTP AUTH password
  { @_<2 ? shift->{auth_pass}  : ($_[0]->{auth_pass} = $_[1]) }
sub auth_submitter  # ESMTP MAIL command AUTH option value (addr-spec or "<>")
  { @_<2 ? shift->{auth_subm}  : (shift->{auth_subm} = $_[1]) }
sub tls_cipher      # defined if TLS was on, e.g. contains cipher alg.,RFC 3207
  { @_<2 ? shift->{auth_tlscif}: ($_[0]->{auth_tlscif} = $_[1]) }
sub dsn_ret         # ESMTP MAIL command RET option   (DSN-RFC 3461)
  { @_<2 ? shift->{dsn_ret}    : ($_[0]->{dsn_ret} = $_[1]) }
sub dsn_envid       # ESMTP MAIL command ENVID option (DSN-RFC 3461) xtext enc.
  { @_<2 ? shift->{dsn_envid}  : ($_[0]->{dsn_envid} = $_[1]) }
sub dsn_passed_on   # obligation to send notification on SUCCESS was relayed
  { @_<2 ? shift->{dsn_pass_on}: ($_[0]->{dsn_pass_on} = $_[1]) }
sub requested_by    # Resent-From addr who requested release from a quarantine
  { @_<2 ? shift->{requested_by}:($_[0]->{requested_by} = $_[1])}
sub body_type       # ESMTP BODY param (RFC 6152: 7BIT, 8BITMIME) or BINARYMIME
  { @_<2 ? shift->{body_type}  : ($_[0]->{body_type} = $_[1]) }
sub smtputf8        # ESMTP SMTPUTF8 param, boolean (RFC 6531)
  { @_<2 ? shift->{smtputf8}   : ($_[0]->{smtputf8} = $_[1]) }
sub header_8bit     # true if header contains non-ASCII characters
  { @_<2 ? shift->{header_8bit}: ($_[0]->{header_8bit} = $_[1]) }
sub body_8bit       # true if body contains non-ASCII characters
  { @_<2 ? shift->{body_8bit}  : ($_[0]->{body_8bit} = $_[1]) }
sub sender          # envelope sender, internal form, e.g.: j doe@example.com
  { @_<2 ? $_[0]->{sender}     : ($_[0]->{sender} = $_[1]) }
sub sender_smtp     # env sender, SMTP form in <>, e.g.: <"j doe"@example.com>
  { @_<2 ? shift->{sender_smtp}: ($_[0]->{sender_smtp} = $_[1]) }
sub sender_credible # envelope sender is believed to be valid
  { @_<2 ? shift->{sender_cred}: ($_[0]->{sender_cred} = $_[1]) }
sub sender_source   # unmangled sender addr. or info from the trace (log/notif)
  { @_<2 ? shift->{sender_src} : ($_[0]->{sender_src} = $_[1]) }
sub sender_maddr_id # maddr.id field from SQL if logging to SQL is enabled
  { @_<2 ? shift->{maddr_id}   : ($_[0]->{maddr_id} = $_[1]) }
sub mime_entity     # MIME::Parser entity holding the parsed message
  { @_<2 ? shift->{mime_entity}: (shift->{mime_entity} = $_[1])}
sub parts_root      # Amavis::Unpackers::Part root object
  { @_<2 ? shift->{parts_root} : ($_[0]->{parts_root} = $_[1])}
sub skip_bytes      # file offset where mail starts, useful for quar. release
  { @_<2 ? shift->{file_ofs}   : ($_[0]->{file_ofs} = $_[1]) }
sub mail_text       # RFC 5322 msg: open file handle, or MIME::Entity object
  { @_<2 ? shift->{mail_text}  : ($_[0]->{mail_text} = $_[1]) }
sub mail_text_str   # RFC 5322 msg: small messages as a stringref, else undef
  { @_<2 ? shift->{mailtextstr}: ($_[0]->{mailtextstr} = $_[1]) }
sub mail_text_fn    # orig. mail filename or undef, e.g. mail_tempdir/email.txt
  { @_<2 ? shift->{mailtextfn} : ($_[0]->{mailtextfn} = $_[1]) }
sub mail_tempdir    # work directory, under $TEMPBASE or supplied by client
  { @_<2 ? shift->{mailtempdir}: ($_[0]->{mailtempdir} = $_[1])}
sub mail_tempdir_obj # Amavis::TempDir obj when non-persistent (quar.release)
  { @_<2 ? shift->{tempdirobj}: ($_[0]->{tempdirobj} = $_[1])}
sub header_edits    # Amavis::Out::EditHeader object or undef
  { @_<2 ? shift->{hdr_edits}  : ($_[0]->{hdr_edits} = $_[1]) }
sub rfc2822_from #author addresses list (rfc allows one or more), parsed 'From'
  { @_<2 ? $_[0]->{hdr_from}   : ($_[0]->{hdr_from} = $_[1]) }
sub rfc2822_sender  # sender address (rfc allows none or one), parsed 'Sender'
  { @_<2 ? shift->{hdr_sender} : ($_[0]->{hdr_sender} = $_[1]) }
sub rfc2822_resent_from # resending author addresses list, parsed 'Resent-From'
  { @_<2 ? shift->{hdr_rfrom}  : ($_[0]->{hdr_rfrom} = $_[1]) }
sub rfc2822_resent_sender  # resending sender addresses, parsed 'Resent-Sender'
  { @_<2 ? shift->{hdr_rsender}: ($_[0]->{hdr_rsender} = $_[1]) }
sub rfc2822_to      # parsed 'To' header field: a list of recipients
  { @_<2 ? shift->{hdr_to}     : ($_[0]->{hdr_to} = $_[1]) }
sub rfc2822_cc      # parsed 'Cc' header field: a list of Cc recipients
  { @_<2 ? shift->{hdr_cc}     : (shift->{hdr_cc} = $_[1]) }
sub orig_header_fields # header field indices by h.f. name, hashref of arrays
  { @_<2 ? shift->{orig_hdr_f} : ($_[0]->{orig_hdr_f} = $_[1]) }
sub orig_header # orig.h.sect, arrayref of h.fields, with folding & trailing LF
  { @_<2 ? shift->{orig_header}: ($_[0]->{orig_header} = $_[1]) }
sub orig_header_size # size of original header, incl. a separator line,RFC 1870
  { @_<2 ? shift->{orig_hdr_s} : ($_[0]->{orig_hdr_s} = $_[1]) }
sub references  # References & In-Reply-To message IDs, array
  { @_<2 ? shift->{refs}       : ($_[0]->{refs} = $_[1]) }
sub orig_body_size  # size of original body (in bytes), RFC 1870
  { @_<2 ? shift->{orig_bdy_s} : ($_[0]->{orig_bdy_s} = $_[1]) }
sub body_start_pos  # byte offset into a msg where mail body starts (if known)
  { @_<2 ? shift->{body_pos}: ($_[0]->{body_pos} = $_[1]) }
sub body_digest     # digest of a message body (e.g. MD5, SHA1, SHA256), hex
  { @_<2 ? shift->{body_digest}: ($_[0]->{body_digest} = $_[1]) }
sub trace  # info from Received header fields, top-down, array of hashrefs
  { @_<2 ? shift->{trace}      : ($_[0]->{trace} = $_[1]) }
sub ip_addr_trace_public  # public IP addresses in 'Received from' hdr flds
  { @_<2 ? shift->{iptracepub} : ($_[0]->{iptracepub} = $_[1]) }
sub is_mlist        # mail is from a mailing list (boolean/string)
  { @_<2 ? shift->{is_mlist}   : ($_[0]->{is_mlist} = $_[1]) }
sub is_auto         # mail is an auto-response (boolean/string)
  { @_<2 ? shift->{is_auto}    : ($_[0]->{is_auto} = $_[1]) }
sub is_bulk         # mail from a m.list or bulk or auto-response (bool/string)
  { @_<2 ? $_[0]->{is_bulk}    : ($_[0]->{is_bulk} = $_[1]) }
sub dkim_signatures_all # a ref to a list of DKIM signature objects, or undef
  { @_<2 ? shift->{dkim_sall}  : ($_[0]->{dkim_sall} = $_[1]) }
sub dkim_signatures_valid # a ref to a list of valid DKIM signature objects
  { @_<2 ? shift->{dkim_sval}  : ($_[0]->{dkim_sval} = $_[1]) }
sub dkim_author_sig # author domain signature present and valid (bool/domain)
  { @_<2 ? shift->{dkim_auth_s}: ($_[0]->{dkim_auth_s} = $_[1]) }
sub dkim_thirdparty_sig # third-party signature present and valid (bool/domain)
  { @_<2 ? shift->{dkim_3rdp_s}: ($_[0]->{dkim_3rdp_s} = $_[1]) }
sub dkim_sender_sig # a sender signature is present and is valid (bool/domain)
  { @_<2 ? shift->{dkim_sndr_s}: (shift->{dkim_sndr_s} = $_[1]) }
sub dkim_envsender_sig # boolean: envelope sender signature present and valid
  { @_<2 ? shift->{dkim_envs_s}: ($_[0]->{dkim_envs_s} = $_[1]) }
sub dkim_signatures_new # ref to a list of DKIM signature objects, our signing
  { @_<2 ? shift->{dkim_snew}  : ($_[0]->{dkim_snew} = $_[1]) }
sub dkim_signwith_sd # ref to a pair [selector,domain] to force signing with
  { @_<2 ? shift->{dkim_signsd}: ($_[0]->{dkim_signsd} = $_[1]) }
sub quarantined_to  # list of quar mailbox names or addresses if quarantined
  { @_<2 ? shift->{quarantine} : ($_[0]->{quarantine} = $_[1]) }
sub quar_type  # list of quar types: F/Z/B/Q/M (file/zipfile/bsmtp/sql/mailbox)
  { @_<2 ? shift->{quar_type}  : ($_[0]->{quar_type} = $_[1]) }
sub dsn_sent        # delivery status notification was sent(1) or suppressed(2)
  { @_<2 ? shift->{dsn_sent}   : ($_[0]->{dsn_sent} = $_[1]) }
sub client_delete   # don't delete the tempdir, it is a client's responsibility
  { @_<2 ? shift->{client_del} :($_[0]->{client_del} = $_[1])}
sub contents_category # sorted arrayref CC_VIRUS/CC_BANNED/CC_SPAM../CC_CLEAN
  { @_<2 ? shift->{category}   : ($_[0]->{category} = $_[1]) }
sub blocking_ccat   # category type most responsible for blocking msg, or undef
  { @_<2 ? $_[0]->{bl_ccat}    : ($_[0]->{bl_ccat} = $_[1]) }
sub checks_performed  # a hashref of checks done on a msg (for statistics/log)
  { @_<2 ? shift->{checks_perf}: ($_[0]->{checks_perf} = $_[1]) }
sub actions_performed  # listref, summarized actions & SMTP status, for logging
  { @_<2 ? shift->{act_perf}   : ($_[0]->{act_perf} = $_[1]) }
sub virusnames      # a ref to a list of virus names detected, or undef
  { @_<2 ? shift->{virusnames} : ($_[0]->{virusnames} = $_[1]) }
sub spam_report     # SA terse report of tests hit (for header section reports)
  { @_<2 ? shift->{spam_report}   : ($_[0]->{spam_report} = $_[1])}
sub spam_summary    # SA summary of tests hit for standard body reports
  { @_<2 ? shift->{spam_summary}  :($_[0]->{spam_summary} = $_[1])}
sub ip_repu_score   # IP reputation score (info, also added to spam_level)
  { @_<2 ? shift->{ip_repu_score} :($_[0]->{ip_repu_score} = $_[1])}
sub time_elapsed    # elapsed times by section - associative array ref
  { @_<2 ? shift->{elapsed}       : ($_[0]->{elapsed} = $_[1])}

# new style of providing additional information from checkers
sub supplementary_info {  # holds a hash of tag/value pairs, such as SA get_tag
  my $self=shift; my $key=shift;
  !@_ ? $self->{info_tag}{$key} : ($self->{info_tag}{$key}=shift);
}

{ no warnings 'once';
# the following methods apply on a per-message level as well, summarizing
# per-recipient information as far as possible
  *add_contents_category =
    \&Amavis::In::Message::PerRecip::add_contents_category;
  *is_in_contents_category =
    \&Amavis::In::Message::PerRecip::is_in_contents_category;
  *setting_by_main_contents_category =
    \&Amavis::In::Message::PerRecip::setting_by_main_contents_category;
  *setting_by_main_contents_category_all =
    \&Amavis::In::Message::PerRecip::setting_by_main_contents_category_all;
  *setting_by_blocking_contents_category =
    \&Amavis::In::Message::PerRecip::setting_by_blocking_contents_category;
  *setting_by_contents_category =
    \&Amavis::In::Message::PerRecip::setting_by_contents_category;
}

# The order of entries in a per-recipient list is the original order
# in which recipient addresses (e.g. obtained via 'MAIL TO:') were received.
# Only the entries that were accepted (via SMTP response code 2xx)
# are placed in the list. The ORDER MUST BE PRESERVED and no recipients
# may be added or removed from the list (without precaution)! This is vital
# to be able to produce correct per-recipient responses to an LMTP client!
#
sub per_recip_data {  # get or set a listref of envelope recipient objects
  my $self = shift;
  # store a copy of the a given listref of recip objects
  if (@_) { $self->{recips} = [@{$_[0]}] }
  # caller may modify data if he knows what he is doing
  $self->{recips};    # return a list of recipient objects
}

sub recips {          # get or set a listref of envelope recipients
  my $self = shift;
  if (@_) {  # store a copy of a given listref of recipient addresses
    my($recips_list_ref, $set_dsn_orcpt_too) = @_;
    $self->per_recip_data([ map {
      my $per_recip_obj = Amavis::In::Message::PerRecip->new;
      $per_recip_obj->recip_addr($_);
      $per_recip_obj->recip_addr_smtp(qquote_rfc2821_local($_));
      $per_recip_obj->dsn_orcpt(
        join(';', orcpt_decode(';'.$per_recip_obj->recip_addr_smtp)))
        if $set_dsn_orcpt_too;
      $per_recip_obj->recip_destiny(D_PASS);  # default is Pass
      $per_recip_obj } @{$recips_list_ref} ]);
  }
  return  if !defined wantarray;  # don't bother
  # return listref of recipient addresses
  [ map($_->recip_addr, @{$self->per_recip_data}) ];
}

# for each header field maintain a list of signature indices which covered it;
# returns a list of signature indices for a given header field position
#
sub header_field_signed_by {
  my($self,$header_field_index) = @_;
  my $h = $self->{hdr_sig_ind};  my $hf;
  if (@_ > 2) {
    $self->{hdr_sig_ind} = $h = []  if !$h;
    $hf = $h->[$header_field_index];
    $h->[$header_field_index] = $hf = []  if !$hf;
    # store signature index(es) at a given header position
    shift; shift; push(@$hf, @_);
  }
  $hf = $h->[$header_field_index]  if $h && !$hf;
  $hf ? @{$hf} : ();
}

# return a j-th header field with a given field name, along with its index
# in the array of all header fields; if a field name is undef then all
# header fields are considered; search proceeds top-down if j >= 0,
# or bottom up for negative values (-1=last, -2=next-to-last, ...)
#
sub get_header_field2 {
  my($self, $field_name, $j) = @_;
  my $orig_hfields = $self->orig_header_fields;
  return if !$orig_hfields;
  my($field_ind, $field, $all_fields, $hfield_indices);
  # arrayref of header field indices for a given h.field name
  $hfield_indices = $orig_hfields->{lc $field_name}  if defined $field_name;
  $all_fields = $self->orig_header;
  if (defined $field_name) {
    if (!defined $hfield_indices) {
      # no header field with such name
    } elsif (ref $hfield_indices) {
      # $hfield_indices is an arrayref
      $j = 0  if !defined $j;
      $field_ind = $hfield_indices->[$j];
    } else {
      # optimized: $hfield_indices is a scalar - the only element
      $field_ind = $hfield_indices  if !defined($j) || $j == 0 || $j == -1;
    }
  } elsif (!ref $all_fields) {
    # no header section
  } elsif ($j >= 0) {  # top-down, 0,1,2,...
    $field_ind = $j  if $j <= $#$all_fields;
  } else {  # bottom-up, -1,-2,-3,...
    $j += @$all_fields;  # turn into an absolute index
    $field_ind = $j  if $j >= 0;
  }
  return $field_ind  if !wantarray;
  ($field_ind, !defined $field_ind ? undef : $all_fields->[$field_ind]);
}

# compatibility wrapper for pre-2.8.0 custom code
#
sub get_header_field {
  my($self, $field_name, $j) = @_;
  my($field_ind, $field) = $self->get_header_field2($field_name,$j);
  if (defined($field_ind) && wantarray) {
    local $1;
    $field_name = lc($1)  if $field =~ /^([^:]*?)[ \t]*:/s;
  }
  !wantarray ? $field_ind : ($field_ind, $field_name, $field);
}

sub get_header_field_body {
  my($self, $field_name, $j) = @_;
  my $k;  my($field_ind, $f) = $self->get_header_field2($field_name,$j);
  defined $f && ($k=index($f,':')) >= 0 ? substr($f,$k+1) : $f;
}

1;
