]> git.donarmstrong.com Git - debbugs.git/blob - bin/debbugs-spam
fix $is_match of mark_it
[debbugs.git] / bin / debbugs-spam
1 #! /usr/bin/perl
2 # debbugs-spam is part of debbugs, and is released
3 # under the terms of the GPL version 2, or any later version, at your
4 # option. See the file README and COPYING for more information.
5 # Copyright 2012 by Don Armstrong <don@donarmstrong.com>.
6
7
8 use warnings;
9 use strict;
10
11 use Getopt::Long qw(:config no_ignore_case);
12 use Pod::Usage;
13
14 =head1 NAME
15
16 debbugs-spam -- Scan log files for spam and populate nnn.log.spam
17
18 =head1 SYNOPSIS
19
20 debbugs-spam [options] bugnumber [[bugnumber2]..]
21
22  Options:
23   --spool-dir debbugs spool directory
24   --debug, -d debugging level (Default 0)
25   --help, -h display this help
26   --man, -m display manual
27
28 =head1 OPTIONS
29
30 =over
31
32 =item B<--spool-dir>
33
34 Debbugs spool directory; defaults to the value configured in the
35 debbugs configuration file.
36
37 =item B<--debug, -d>
38
39 Debug verbosity.
40
41 =item B<--help, -h>
42
43 Display brief useage information.
44
45 =item B<--man, -m>
46
47 Display this manual.
48
49 =back
50
51 =head1 SUBCOMMANDS
52
53 =over
54
55 =item B<auto-scan>
56
57 Automatically scan messages using spamassassin and mark messages as
58 spam which hit the threshold, and those that are highly negative as
59 ham.
60
61 =item B<score>
62
63 Output the score of all of the messages in a bug
64
65 =over
66
67 =item B<--skip-seen> Skip messages which have previously been classified
68
69 =back
70
71 =item B<mark-spam>
72
73 Mark messages as spam if there is a regex match to subject or message
74 id
75
76 =item B<mark-ham>
77
78 Mark messages as ham if there is a regex match to subject or message
79 id
80
81 =back
82
83
84 =head1 EXAMPLES
85
86
87 =cut
88
89
90 use vars qw($DEBUG);
91
92 use Debbugs::Log qw(record_regex);
93 use Debbugs::Log::Spam;
94 use Debbugs::Config qw(:config);
95 use Debbugs::Command qw(:all);
96 use IPC::Open3 qw(open3);
97 use Carp;
98
99 my %options =
100     (debug   => 0,
101      help    => 0,
102      man     => 0,
103      verbose => 0,
104      quiet   => 0,
105      quick   => 0,
106      spamc   => 'spamc',
107      spamc_opts => [],
108     );
109
110 handle_main_arguments(\%options,
111                       'quick|q',
112                       'service|s',
113                       'sysconfdir|c',
114                       'spamc=s' => 0,
115                       'spamc_opts|spamc-opts=s@' => 0,
116                       'spool_dir|spool-dir=s',
117                       'debug|d+','help|h|?','man|m');
118
119 my %subcommands =
120     ('auto-scan' => {function => \&auto_spamscan,
121                      arguments => {'ham_threshold|ham-threshold=s' => 0,
122                                   },
123                      defaults => {ham_threshold => -5},
124                     },
125      'score' => {function => \&score_bug,
126                  arguments => {'skip_seen|skip-seen!' => 0
127                               },
128                 },
129      'mark-spam' => {function => \&mark_spam,
130                     },
131      'mark-ham' => {function => \&mark_ham,
132                    },
133      'help' => {function => sub {pod2usage({verbose => 2});}}
134     );
135
136 pod2usage() if $options{help};
137 pod2usage({verbose=>2}) if $options{man};
138
139 $DEBUG = $options{debug};
140
141 my @USAGE_ERRORS;
142 $options{verbose} = $options{verbose} - $options{quiet};
143
144 pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS;
145
146 my ($subcommand) = shift @ARGV;
147 if (not defined $subcommand) {
148     $subcommand = 'help';
149     print STDERR "You must provide a subcommand; displaying usage.\n";
150     pod2usage();
151 } elsif (not exists $subcommands{$subcommand}) {
152     print STDERR "$subcommand is not a valid subcommand; displaying usage.\n";
153     pod2usage();
154 }
155
156 if (exists $options{spool_dir} and defined $options{spool_dir}) {
157     $config{spool_dir} = $options{spool_dir};
158 }
159 if ($subcommand ne 'help') {
160     chdir($config{spool_dir}) or die "chdir $config{spool_dir} failed: $!";
161 }
162 my $opts =
163     handle_subcommand_arguments(\@ARGV,
164                                 $subcommands{$subcommand}{arguments},
165                                 $subcommands{$subcommand}{defaults},
166                                );
167 $subcommands{$subcommand}{function}->(\%options,$opts,\%config,\@ARGV);
168
169
170 sub mark_ham {
171     mark_it('ham',@_);
172 }
173
174 sub mark_spam {
175     mark_it('spam',@_);
176 }
177
178 sub mark_it {
179     my ($spam_ham,$options,$opts,$config,$argv) = @_;
180     my $regex = shift @{$argv};
181     for my $bug_num (@{$argv}) {
182         my $spam = Debbugs::Log::Spam->new(bug_num => $bug_num) or
183             die "Unable to open bug log spam for $bug_num";
184         foreachmsg(sub {
185                        my ($bn,$rec,$mid) = @_;
186                        my $body = $rec->{text};
187                        my ($subject) = $body =~ /^Subject: *(.+)$/mi;
188                        my $is_match = 0;
189                        if ($subject =~ /\Q$regex\E/) {
190                            $is_match = 1;
191                        }
192                        if ($mid =~ /\Q$regex\E/) {
193                            $is_match = 1;
194                        }
195                        if ($is_match) {
196                            print STDERR "it's a match" if $DEBUG;
197                            if ($spam_ham eq 'spam') {
198                                $spam->add_spam($mid);
199                            } else {
200                                $spam->add_ham($mid);
201                            }
202                        }
203                    },
204                    $bug_num
205                   );
206     }
207 }
208
209
210 sub score_bug {
211     my ($options,$opts,$config,$argv) = @_;
212     for my $bug_num (@{$argv}) {
213         my @bug_score =
214             spam_score_bug($bug_num,
215                            $options->{spamc},
216                            $options->{spamc_opts},
217                            $opts->{skip_seen},
218                           );
219         print "$_->{score} $_->{message_id} $_->{subject}\n"
220             foreach @bug_score;
221     }
222 }
223
224 sub auto_spamscan {
225     my ($options,$opts,$config,$argv) = @_;
226
227     for my $bug_num (@{$argv}) {
228         my $spam = Debbugs::Log::Spam->new(bug_num => $bug_num) or
229             die "Unable to open bug log spam for $bug_num";
230         foreachmsg(sub {
231                        my ($bn,$rec,$mid) = @_;
232                        if ($spam->is_spam($mid)) {
233                            print STDERR "already spam\n" if $DEBUG;
234                            return;
235                        }
236                        if ($spam->is_ham($mid)) {
237                            print STDERR "already ham\n" if $DEBUG;
238                            return;
239                        }
240                        my ($score,$is_spam,$report,$threshold) =
241                            spam_score($rec,
242                                       $options->{spamc},
243                                       $options->{spamc_opts},
244                                      );
245                        if ($is_spam) {
246                            print STDERR "it's spam ($score)\n" if $DEBUG;
247                            $spam->add_spam($mid);
248                        } elsif ($score < $opts->{ham_threshold}) {
249                            print STDERR "it's really ham ($score)\n" if $DEBUG;
250                            $spam->add_ham($mid);
251                        }
252                        else {
253                            print STDERR "it's ham ($score)\n" if $DEBUG;
254                        }
255                    },
256                    $bug_num,
257                   );
258         $spam->save();
259     }
260 }
261
262 sub spam_score_bug {
263     my ($bug,$spamc,$spamc_opts,$skip_seen) = @_;
264
265     my $spam;
266     if ($skip_seen) {
267         $spam = Debbugs::Log::Spam->new(bug_num => $bug) or
268             die "Unable to open bug log spam for $bug";
269     }
270     my @records;
271     foreachmsg(sub {
272                    my ($bn,$rec,$mid) = @_;
273                    my $score;
274                    if ($skip_seen) {
275                        if ($spam->is_spam($mid)) {
276                            $score = 999;
277                        } elsif ($spam->is_ham($mid)) {
278                            $score = -999;
279                        }
280                    }
281                    $score //=
282                        spam_score($rec,$spamc,$spamc_opts);
283                    my ($subject) = $rec->{text} =~ /^Subject: *(.+)$/mi;
284                    push @records,
285                       {message_id => $mid,
286                        score => $score,
287                        subject => $subject,
288                       };
289                },
290                $bug
291               );
292     return @records;
293 }
294
295 sub spam_score {
296     my ($record,$spamc,$spamc_opts) = @_;
297     my ($score,$threshold,$report);
298     my $is_spam = 0;
299     eval {
300         my ($spamc_in,$spamc_out);
301         my $old_sig = $SIG{"PIPE"};
302         $SIG{"PIPE"} = sub {
303             die "SIGPIPE in child for some reason";
304         };
305         my $childpid =
306             open3($spamc_in,$spamc_out,0,
307                   $spamc,'-E',@{$spamc_opts}) or
308                       die "Unable to fork spamc: $!";
309         if (not $childpid) {
310             die "Unable to fork spamc";
311         }
312         print {$spamc_in} $record->{text};
313         close($spamc_in) or die "Unable to close spamc_in: $!";
314         waitpid($childpid,0);
315         if ($? >> 8) {
316             $is_spam = 1;
317         }
318         my ($first_line,@report) = <$spamc_out>;
319         if ($DEBUG) {
320             print STDERR "[$?;".($? >> 8)."] ";
321             print STDERR $first_line,@report;
322             print STDERR " ";
323         }
324         if (defined $first_line) {
325             chomp $first_line;
326             ($score,$threshold) = $first_line =~ m{^(-?[\d\.]+)/(-?[\d\.]+)$};
327             $report = join('',@report);
328         }
329         close($spamc_out);
330         $SIG{"PIPE"} = $old_sig;
331     };
332     if ($@) {
333         carp "processing of message failed [$@]\n";
334         return undef;
335     }
336     return wantarray?($score,$is_spam,$report):$score;
337 }
338
339 sub foreachmsg {
340     my ($sub,$bug_num) = @_;
341     my $log = Debbugs::Log->new(bug_num => $bug_num) or
342         die "Unable to open bug log for $bug_num";
343     my %seen_msgids;
344     while (my $record = $log->read_record()) {
345         next if $record->{type} eq 'html';
346         next if $record->{type} eq 'autocheck';
347         my ($msg_id) = record_regex($record,
348                                     qr/^Message-Id:\s+<(.+)>/mi);
349         next unless defined $msg_id;
350         print STDERR "examining $msg_id: " if $DEBUG;
351         if ($msg_id =~ /$config{email_domain}$/) {
352             print STDERR "skipping\n" if $DEBUG;
353             next;
354         }
355         if ($seen_msgids{$msg_id}) {
356             print STDERR "already seen\n" if $DEBUG;
357             next;
358         }
359         $seen_msgids{$msg_id}=1;
360         $sub->($bug_num,$record,$msg_id);
361         print STDERR "\n" if $DEBUG;
362     }
363 }
364
365
366 __END__
367
368 # Local Variables:
369 # cperl-indent-level: 4
370 # indent-tabs-mode: nil
371 # End: