1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
#!/usr/bin/perl -w
# Copyright (C) 2013-2021 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;
GetOptions('precheck!' => \$precheck, 'help|h' => \$show_help) or
do { print STDERR $help; exit 1 };
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 setup the emergency path without bailing due to
# user error, we really want to setup 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 = do { local $/; <STDIN> };
$str =~ s/\A[\r\n]*From [^\r\n]*\r?\n//s;
$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 = [];
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)) {
die "ORIGINAL_RECIPIENT not defined in ENV\n";
}
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);
|