]> Sergey Matveev's repositories - public-inbox.git/blob - script/public-inbox-learn
9352c8ff7dd41088796eb60607452cea3b98cc0d
[public-inbox.git] / script / public-inbox-learn
1 #!/usr/bin/perl -w
2 # Copyright (C) 2014-2020 all contributors <meta@public-inbox.org>
3 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4 #
5 # Used for training spam (via SpamAssassin) and removing messages from a
6 # public-inbox
7 my $help = <<EOF;
8 usage: public-inbox-learn [OPTIONS] [spam|ham|rm] </path/to/RFC2822_message
9
10 required action argument:
11
12    spam  unindex the message and train as spam
13      rm  remove the message without training as spam
14     ham  index the message (based on To:/Cc: headers) and train as ham
15
16 options:
17
18   --all  scan all inboxes on `rm'
19
20 See public-inbox-learn(1) man page for full documentation.
21 EOF
22 use strict;
23 use PublicInbox::Config;
24 use PublicInbox::InboxWritable;
25 use PublicInbox::Eml;
26 use PublicInbox::Address;
27 use PublicInbox::Spamcheck::Spamc;
28 use Getopt::Long qw(:config gnu_getopt no_ignore_case auto_abbrev);
29 my %opt = (all => 0);
30 GetOptions(\%opt, qw(all help|h)) or die $help;
31
32 my $train = shift or die $help;
33 if ($train !~ /\A(?:ham|spam|rm)\z/) {
34         die "`$train' not recognized.\n$help";
35 }
36 die "--all only works with `rm'\n" if $opt{all} && $train ne 'rm';
37
38 my $spamc = PublicInbox::Spamcheck::Spamc->new;
39 my $pi_cfg = PublicInbox::Config->new;
40 my $err;
41 my $mime = PublicInbox::Eml->new(do{
42         local $/;
43         my $data = <STDIN>;
44         $data =~ s/\A[\r\n]*From [^\r\n]*\r?\n//s;
45
46         if ($train ne 'rm') {
47                 eval {
48                         if ($train eq 'ham') {
49                                 $spamc->hamlearn(\$data);
50                         } elsif ($train eq 'spam') {
51                                 $spamc->spamlearn(\$data);
52                         }
53                         die "spamc failed with: $?\n" if $?;
54                 };
55                 $err = $@;
56         }
57         \$data
58 });
59
60 sub remove_or_add ($$$$) {
61         my ($ibx, $train, $mime, $addr) = @_;
62
63         # We do not touch GIT_COMMITTER_* env here so we can track
64         # who trained the message.
65         $ibx->{name} = $ENV{GIT_COMMITTER_NAME} // $ibx->{name};
66         $ibx->{-primary_address} = $ENV{GIT_COMMITTER_EMAIL} // $addr;
67         $ibx = PublicInbox::InboxWritable->new($ibx);
68         my $im = $ibx->importer(0);
69
70         if ($train eq "rm") {
71                 # This needs to be idempotent, as my inotify trainer
72                 # may train for each cross-posted message, and this
73                 # script already learns for every list in
74                 # ~/.public-inbox/config
75                 $im->remove($mime, $train);
76         } elsif ($train eq "ham") {
77                 # no checking for spam here, we assume the message has
78                 # been reviewed by a human at this point:
79                 PublicInbox::MDA->set_list_headers($mime, $ibx);
80
81                 # Ham messages are trained when they're marked into
82                 # a SEEN state, so this is idempotent:
83                 $im->add($mime);
84         }
85         $im->done;
86 }
87
88 # spam is removed from all known inboxes since it is often Bcc:-ed
89 if ($train eq 'spam' || ($train eq 'rm' && $opt{all})) {
90         $pi_cfg->each_inbox(sub {
91                 my ($ibx) = @_;
92                 $ibx = PublicInbox::InboxWritable->new($ibx);
93                 my $im = $ibx->importer(0);
94                 $im->remove($mime, $train);
95                 $im->done;
96         });
97 } else {
98         require PublicInbox::MDA;
99
100         # get all recipients
101         my %dests; # address => <PublicInbox::Inbox|0(false)>
102         for ($mime->header('Cc'), $mime->header('To')) {
103                 foreach my $addr (PublicInbox::Address::emails($_)) {
104                         $addr = lc($addr);
105                         $dests{$addr} //= $pi_cfg->lookup($addr) // 0;
106                 }
107         }
108
109         # n.b. message may be cross-posted to multiple public-inboxes
110         my %seen;
111         while (my ($addr, $ibx) = each %dests) {
112                 next unless ref($ibx); # $ibx may be 0
113                 next if $seen{"$ibx"}++;
114                 remove_or_add($ibx, $train, $mime, $addr);
115         }
116         my $dests = PublicInbox::MDA->inboxes_for_list_id($pi_cfg, $mime);
117         for my $ibx (@$dests) {
118                 next if $seen{"$ibx"}++;
119                 remove_or_add($ibx, $train, $mime, $ibx->{-primary_address});
120         }
121 }
122
123 if ($err) {
124         warn $err;
125         exit 1;
126 }