| #!/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($_); |
| $ibx->{indexlevel} = $ibx->detect_indexlevel; |
| 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); |