#!/usr/bin/perl -w
# Copyright (C) all contributors <meta@public-inbox.org>
# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
#
# Mail delivery agent for public-inbox, run from your MTA upon mail delivery
my $help = <<EOF;
usage: public-inbox-mda [OPTIONS] </path/to/RFC2822_message

options:

  --no-precheck  skip internal checks for spam messages

See public-inbox-mda(1) man page for full documentation.
EOF
use strict;
use Getopt::Long qw(:config gnu_getopt no_ignore_case auto_abbrev);
my ($ems, $emm, $show_help);
my $precheck = 1;
use PublicInbox::Import;
local $PublicInbox::Import::DROP_UNIQUE_UNSUB; # does this need a CLI switch?
GetOptions('precheck!' => \$precheck, 'help|h' => \$show_help) or
	do { print STDERR $help; exit 1 };
if ($show_help) {
	print $help;
	exit;
}

my $do_exit = sub {
	my ($code) = shift;
	$emm = $ems = undef; # trigger DESTROY
	exit $code;
};

use PublicInbox::Eml;
use PublicInbox::MDA;
use PublicInbox::Config;
use PublicInbox::Emergency;
use PublicInbox::Filter::Base;
use PublicInbox::InboxWritable;
use PublicInbox::Spamcheck;

# n.b.: Hopefully we can set up the emergency path without bailing due to
# user error, we really want to set up the emergency destination ASAP
# in case there's bugs in our code or user error.
my $emergency = $ENV{PI_EMERGENCY} || "$ENV{HOME}/.public-inbox/emergency/";
$ems = PublicInbox::Emergency->new($emergency);
my $str = PublicInbox::IO::read_all \*STDIN;
PublicInbox::Eml::strip_from($str);
$ems->prepare(\$str);
my $eml = PublicInbox::Eml->new(\$str);
my $cfg = PublicInbox::Config->new;
my $key = 'publicinboxmda.spamcheck';
my $default = 'PublicInbox::Spamcheck::Spamc';
my $spamc = PublicInbox::Spamcheck::get($cfg, $key, $default);
my $dests = [];
PublicInbox::Import::load_config($cfg, $do_exit);

my $recipient = $ENV{ORIGINAL_RECIPIENT};
if (defined $recipient) {
	my $ibx = $cfg->lookup($recipient); # first check
	push @$dests, $ibx if $ibx;
}
if (!scalar(@$dests)) {
	$dests = PublicInbox::MDA->inboxes_for_list_id($cfg, $eml);
	if (!scalar(@$dests) && !defined($recipient)) {
		warn "ORIGINAL_RECIPIENT not defined in ENV\n";
		$do_exit->(67); # EX_NOUSER
	}
	scalar(@$dests) or $do_exit->(67); # EX_NOUSER 5.1.1 user unknown
}

my $err;
@$dests = grep {
	my $ibx = PublicInbox::InboxWritable->new($_);
	eval { $ibx->assert_usable_dir };
	if ($@) {
		warn $@;
		$err = 1;
		0;
	# pre-check, MDA has stricter rules than an importer might;
	} elsif ($precheck) {
		!!PublicInbox::MDA->precheck($eml, $ibx->{address});
	} else {
		1;
	}
} @$dests;

$do_exit->(67) if $err && scalar(@$dests) == 0;

$eml = undef;
my $spam_ok;
if ($spamc) {
	$str = '';
	$spam_ok = $spamc->spamcheck($ems->fh, \$str);
	# update the emergency dump with the new message:
	$emm = PublicInbox::Emergency->new($emergency);
	$emm->prepare(\$str);
	$ems = $ems->abort;
} else { # no spam checking configured:
	$spam_ok = 1;
	$emm = $ems;
	my $fh = $emm->fh;
	read($fh, $str, -s $fh);
}
$do_exit->(0) unless $spam_ok;

# -mda defaults to the strict base filter which we won't use anywhere else
sub mda_filter_adjust ($) {
	my ($ibx) = @_;
	my $fcfg = $ibx->{filter} || '';
	if ($fcfg eq '') {
		$ibx->{filter} = 'PublicInbox::Filter::Base';
	} elsif ($fcfg eq 'scrub') { # legacy alias, undocumented, remove?
		$ibx->{filter} = 'PublicInbox::Filter::Mirror';
	}
}

my @rejects;
for my $ibx (@$dests) {
	mda_filter_adjust($ibx);
	my $filter = $ibx->filter;
	my $mime = PublicInbox::Eml->new($str);
	my $ret = $filter->delivery($mime);
	if (ref($ret) && ($ret->isa('PublicInbox::Eml') ||
			$ret->isa('Email::MIME'))) { # filter altered message
		$mime = $ret;
	} elsif ($ret == PublicInbox::Filter::Base::IGNORE) {
		next; # nothing, keep looping
	} elsif ($ret == PublicInbox::Filter::Base::REJECT) {
		push @rejects, $filter->err;
		next;
	}

	PublicInbox::MDA->set_list_headers($mime, $ibx);
	my $im = $ibx->importer(0);
	if (defined $im->add($mime)) {
		# ->abort is idempotent, no emergency if a single
		# destination succeeds
		$emm->abort;
	} else { # v1-only
		my $mid = $mime->header_raw('Message-ID');
		# this message is similar to what ssoma-mda shows:
		print STDERR "CONFLICT: Message-ID: $mid exists\n";
	}
	$im->done;
}

if (scalar(@rejects) && scalar(@rejects) == scalar(@$dests)) {
	$! = 65; # EX_DATAERR 5.6.0 data format error
	die join("\n", @rejects, '');
}

$do_exit->(0);
