1 # Copyright (C) 2016-2019 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4 # SpamAssassin rules useful for running a mailing list mirror. We want to:
5 # * ensure Received: headers are really from the list mail server
6 # users expect. This is to prevent malicious users from
7 # injecting spam into mirrors without going through the expected
9 # * flag messages where the mailing list is Bcc:-ed since it is
10 # common for spam to have wrong or non-existent To:/Cc: headers.
12 package PublicInbox::SaPlugin::ListMirror;
15 use base qw(Mail::SpamAssassin::Plugin);
17 # constructor: register the eval rules
19 my ($class, $mail) = @_;
22 $class = ref($class) || $class;
23 my $self = $class->SUPER::new($mail);
25 $mail->{conf}->{list_mirror_check} = [];
26 $self->register_eval_rule('check_list_mirror_received');
27 $self->register_eval_rule('check_list_mirror_bcc');
28 $self->set_config($mail->{conf});
32 sub check_list_mirror_received {
33 my ($self, $pms) = @_;
34 my $recvd = $pms->get('Received') || '';
35 $recvd =~ s/\n.*\z//s;
37 foreach my $cfg (@{$pms->{conf}->{list_mirror_check}}) {
38 my ($hdr, $hval, $host_re, $addr_re) = @$cfg;
39 my $v = $pms->get($hdr) or next;
43 return 1 if $recvd !~ $host_re;
49 sub check_list_mirror_bcc {
50 my ($self, $pms) = @_;
51 my $tocc = $pms->get('ToCc');
53 foreach my $cfg (@{$pms->{conf}->{list_mirror_check}}) {
54 my ($hdr, $hval, $host_re, $addr_re) = @$cfg;
55 defined $addr_re or next;
56 my $v = $pms->get($hdr) or next;
60 return 1 if !$tocc || $tocc !~ $addr_re;
66 # list_mirror HEADER HEADER_VALUE HOSTNAME_GLOB [LIST_ADDRESS]
67 # list_mirror X-Mailing-List git@vger.kernel.org *.kernel.org
68 # list_mirror List-Id <foo.example.org> *.example.org foo@example.org
69 sub config_list_mirror {
70 my ($self, $key, $value, $line) = @_;
73 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
75 my ($hdr, $hval, $host_glob, @extra) = split(/\s+/, $value);
76 my $addr = shift @extra;
80 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
81 $addr = join('|', map { quotemeta } split(/,/, $addr));
82 $addr = qr/\b$addr\b/i;
85 @extra and return $Mail::SpamAssassin::Conf::INVALID_VALUE;
88 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
90 my %patmap = ('*' => '\S+', '?' => '.', '[' => '[', ']' => ']');
91 $host_glob =~ s!(.)!$patmap{$1} || "\Q$1"!ge;
92 my $host_re = qr/\A\s*from\s+$host_glob(?:\s|$)/si;
94 push @{$self->{list_mirror_check}}, [ $hdr, $hval, $host_re, $addr ];
98 my ($self, $conf) = @_;
101 setting => 'list_mirror',
103 type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
104 code => *config_list_mirror,
106 $conf->{parser}->register_commands(\@cmds);