=head1 NAME

bcc

=head1 DESCRIPTION

Forwards a copy of all mail (or just incoming or outgoing mail) to the 
given email address (useful for archiving etc.). bcc addresses may be
passed as parameters to bcc, or may be defined in a 'bcc' config file.

bcc also checks config files 'bcc_ignore_mailfrom' and 
'bcc_ignore_rcptto' for exceptions - these contain lists of email 
addresses bcc should ignore as senders or recipients. They also support 
bare username and '@domain' entries as wildcards.

bcc will use per_recipient configs if passed a true 'per_recipient' 
argument, but it cannot handle multiple different configs for different
recipients (so use denysoft_multi or similar to prevent those).

bcc should generally be called before the 'check_relay' plugin, since 
incoming processing uses a RCPT hook.

=head1 CONFIG

The following parameters can be passed to bcc, or set via a 'bcc' config
file.

=over 4

=item mode [ bcc | cc | off ]

bcc mode. Defaults to 'bcc' if any of the all/incoming/outgoing parameters
are set, and 'off' otherwise. 'cc' mode is identical to 'bcc' mode, except
that an 'X-Copied-To' header is added to the mail headers for each 
all/incoming/outgoing recipient.

=item all <email>

Send a copy of all email to the given address.

=item incoming <email>

Send a copy of all incoming email to the given address. Mail is considered
incoming if it is going to a recipient with a domain in 'rcpthosts'.

=item outgoing <email>

Send a copy of all outgoing email to the given address. Mail is considered
outgoing if it is sent from a relay client i.e. the RELAYCLIENT environment 
variable is set.

=back

The following parameter can be passed to bcc (but not set via a config 
file):

=over 4

=item per_recipient 1

Allow per-recipient configs to be used (using the per_user_config plugin).

=back


=head1 BUGS

Note that the definitions of incoming and outgoing mail mean that relayed
mail to a local domain is considered both incoming and outgoing, and will
be copied twice. This is a feature, not a bug. :-)

=head1 AUTHOR

Written by Gavin Carr <gavin@openfusion.com.au>.

=cut

use Qpsmtpd::Address;

my $VERSION = 0.04;

sub register {
  my ($self, $qp, %arg) = @_;
  $self->{_bcc_mode} = $arg{mode};
  $self->{_bcc_all} = $arg{all};
  $self->{_bcc_incoming} = $arg{incoming};
  $self->{_bcc_outgoing} = $arg{outgoing};
  $self->{_bcc_per_recipient} = $arg{per_recipient};
  if ($self->{_bcc_per_recipient}) {
    $self->register_hook("rcpt", "check_sender");
    $self->register_hook("rcpt", "check_rcpt");
    $self->register_hook("data_post", "bcc");
  }
  else {
    $self->load_bcc_config;
    unless ($self->{_bcc_mode} eq 'off') {
      $self->register_hook("mail", "check_sender");
      $self->register_hook("rcpt", "check_rcpt");
      $self->register_hook("data_post", "bcc");
    }
  }
}

sub load_bcc_config
{
  my ($self, $rcpt) = @_;
  my $arg = $rcpt ? { user => $rcpt } : {};
  for ($self->qp->config("bcc", $arg)) {
    my ($key, $value) = split /\s+/, $_, 2;
    next unless $key =~ m/(mode|all|incoming|outgoing)/;
    $self->{"_bcc_$1"} = $value;
  }
  # Default bcc mode
  $self->{_bcc_mode} ||= 
    $self->{_bcc_all} || $self->{_bcc_incoming} || $self->{_bcc_outgoing} ?
      'bcc' : 'off';
}

sub check_sender {
  my ($self, $transaction, $sender) = @_;
  my $rcpt;
  if ($self->{_bcc_per_recipient}) {
    $rcpt = $sender;
    $sender = $transaction->sender;
  }

  my $ignore = 0;
  my $config_arg = $rcpt ? { user => $rcpt } : {};
  for ($self->qp->config("bcc_ignore_mailfrom", $config_arg)) {
    $_ = lc;
    $ignore = 1,last if $_ eq lc $sender->address;
    $ignore = 1,last if substr($_,0,1) eq '@' && substr($_,1) eq lc $sender->host;
    $ignore = 1,last if index($_,'@') < 0 && $_ eq lc $sender->user;
  }
  $transaction->notes("bcc_ignore",1) if $ignore;

  return (DECLINED);
}

sub check_rcpt {
  my ($self, $transaction, $rcpt) = @_;
  # Only copy once for multiple recipients
  return (DECLINED) if $transaction->notes("bcc_ignore");
  return (DECLINED) if $transaction->notes('bcc_incoming');

  my $ignore = 0;
  my $config_arg = $rcpt ? { rcpt => $rcpt } : {};
  for ($self->qp->config("bcc_ignore_rcptto", $config_arg)) {
    $_ = lc;
    $ignore = 1,last if $_ eq lc $rcpt->address;
    $ignore = 1,last if substr($_,0,1) eq '@' && substr($_,1) eq lc $rcpt->host;
    $ignore = 1,last if index($_,'@') < 0 && $_ eq lc $rcpt->user;
  }
  if ($ignore) {
    my $ignore_rcpt_cnt = $transaction->notes("bcc_ignore_rcpt");
    $transaction->notes("bcc_ignore_rcpt",$ignore_rcpt_cnt+1);
    return (DECLINED);
  }

  # Load per-recipient config, if per_recipient
  if ($self->{_bcc_per_recipient}) {
    $self->load_bcc_config($rcpt);
    return (DECLINED) if $self->{_bcc_mode} eq 'off';
  }

  # For 'incoming', at least one recipient must be on a rcpthosts domain
  return (DECLINED) unless $self->{_bcc_incoming};
  my $host = lc $rcpt->host;
  my @rcpthosts = ($self->qp->config("me"),$self->qp->config("rcpthosts"));
  for my $allowed (@rcpthosts) {
    ($allowed) =~ s/^\s*(\S+)/\L$1\E/;
    if ($host eq $allowed || 
       (substr($allowed,0,1) eq '.' && $host =~ m/\Q$allowed\E$/)) {
      $transaction->notes('bcc_incoming',1);
      last;
    }
  }

  return (DECLINED);
}

# Actual copying is deferred to data_post so as not to mess up the rcpt list
sub bcc
{
  my ($self, $transaction) = @_;
  return (DECLINED) if $self->{_bcc_mode} eq 'off';
  return (DECLINED) if $transaction->notes("bcc_ignore");
  return (DECLINED) 
    unless ($transaction->notes("bcc_ignore_rcpt") || 0) < $transaction->recipients;

  if ($self->{_bcc_all}) {
    my $rcpt = (Qpsmtpd::Address->parse($self->{_bcc_all}))[0];
    $transaction->add_recipient($rcpt); 
    $transaction->header->add('X-Copied-To', $self->{_bcc_all}) 
      if $self->{_bcc_mode} eq 'cc';
    $self->log(3,"message copied to " . $self->{_bcc_all});
  }

  if ($self->{_bcc_outgoing} && exists $ENV{RELAYCLIENT}) {
    my $rcpt = (Qpsmtpd::Address->parse($self->{_bcc_outgoing}))[0];
    $transaction->add_recipient($rcpt); 
    $transaction->header->add('X-Copied-To', $self->{_bcc_outgoing}) 
      if $self->{_bcc_mode} eq 'cc';
    $self->log(3,"outgoing message copied to " . $self->{_bcc_outgoing});
  }

  if ($transaction->notes('bcc_incoming')) {
    my $rcpt = (Qpsmtpd::Address->parse($self->{_bcc_incoming}))[0];
    $transaction->add_recipient($rcpt);
    $transaction->header->add('X-Copied-To', $self->{_bcc_incoming}) 
      if $self->{_bcc_mode} eq 'cc';
    $self->log(3,"incoming message copied to " . $self->{_bcc_incoming});
  }

  return (DECLINED); 
}

# arch-tag: a1f2db91-5652-4040-9a4e-1747b7fbe4d8
