File Coverage

blib/lib/Mail/SpamFilter.pm
Criterion Covered Total %
statement 12 81 14.8
branch 0 32 0.0
condition 0 3 0.0
subroutine 4 9 44.4
pod n/a
total 16 125 12.8


line stmt bran cond sub pod time code
1             package Mail::SpamFilter;
2              
3 1     1   29887 use 5.008001;
  1         3  
  1         40  
4 1     1   8 use strict;
  1         2  
  1         36  
5 1     1   4 use warnings;
  1         7  
  1         42  
6 1     1   5 use Carp;
  1         2  
  1         2044  
7              
8             require Exporter;
9              
10             our @ISA = qw(Exporter);
11              
12             # Items to export into callers namespace by default. Note: do not export
13             # names by default without a very good reason. Use EXPORT_OK instead.
14             # Do not simply export all your public functions/methods/constants.
15              
16             # This allows declaration use Mail::SpamFilter ':all';
17             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
18             # will save memory.
19             our %EXPORT_TAGS = ( 'all' => [ qw(
20              
21             extract_header
22             extract_spam_headers
23             filter_message
24             report_message
25             count_votes
26              
27             ) ] );
28              
29             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
30              
31             our @EXPORT = qw(
32            
33             );
34              
35             our $VERSION = '0.08';
36              
37             my $HOME = $ENV{'HOME'} || $ENV{'LOGDIR'} || $ENV{'USERPROFILE'} ||
38             die "You're homeless!\n";
39             my $USER = $ENV{'USER'} || $ENV{'LOGNAME'} || $ENV{'USERNAME'} ||
40             getlogin || die "You're nameless!\n";
41              
42              
43             # Table of spam filters and the tags they produce that we are interested in:
44              
45             our %FILTER_TAGS = (
46              
47             spamassassin => [qw(X-Spam-Level
48             X-Spam-Status)],
49              
50             crm114 => [qw(X-CRM114-Status
51             X-CRM114-Version
52             X-CRM114-CacheID
53             X-CRM114-Notice)],
54              
55             wpbl => [qw(X-WPBL)],
56              
57             dspam => [qw(X-DSPAM-Result
58             X-DSPAM-Confidence
59             X-DSPAM-Probability
60             X-DSPAM-Signature
61             X-DSPAM-User)],
62              
63             spamprobe => [qw(X-SpamProbe)],
64              
65             bogofilter => [qw(X-Bogosity)],
66              
67             spamhaus_zen => [qw(X-SPAMHOUSE-ZEN)],
68              
69             );
70              
71              
72             our %FILTER_SPAM_TAG = (
73              
74             spamassassin => qr/X-Spam-Status: Yes/,
75              
76             crm114 => qr/X-CRM114-Status: SPAM/,
77              
78             wpbl => qr/X-WPBL: BLOCK/,
79              
80             dspam => qr/X-DSPAM-Result: Spam/,
81              
82             spamprobe => qr/X-SpamProbe: SPAM/,
83              
84             bogofilter => qr/X-Bogosity: Spam/,
85              
86             spamhaus_zen => qr/X-SPAMHOUSE-ZEN: BLOCK/,
87              
88             );
89              
90              
91             our %FILTER_GOOD_TAG = (
92              
93             spamassassin => qr/X-Spam-Status: No/,
94              
95             crm114 => qr/X-CRM114-Status: (Good|UNSURE)/,
96              
97             wpbl => qr/X-WPBL: OK/,
98              
99             dspam => qr/X-DSPAM-Result: Innocent/,
100              
101             spamprobe => qr/X-SpamProbe: GOOD/,
102              
103             bogofilter => qr/X-Bogosity: (Ham|Unsure)/,
104              
105             spamhaus_zen => qr/X-SPAMHOUSE-ZEN: OK/,
106              
107             );
108              
109              
110             # The command required to run each filter.
111             # Ensure that these are in your PATH: you may need to set
112             # the PATH variable in your .procmailrc for example.
113              
114             our %FILTER_CMD = (
115              
116             spamassassin => "spamassassin",
117              
118             crm114 => "crm -u $HOME/.crm114 mailreaver.crm",
119              
120             wpbl => "wpbl_check",
121              
122             dspam => "dspam --user $USER --mode=teft --stdout --deliver=innocent,spam",
123              
124             spamprobe => "spamprobe_check",
125              
126             bogofilter => "bogofilter -pe",
127              
128             spamhaus_zen => "spamhaus_zen_check",
129              
130             );
131              
132              
133             # Command to tell the scanner that this message is spam:
134              
135             our %ISSPAM_CMD = (
136              
137             spamassassin => "spamassassin --report",
138              
139             crm114 => "crm -u $HOME/.crm114 mailreaver.crm --spam",
140              
141             wpbl => "wpbl spam",
142              
143             dspam => "dspam-train spam",
144              
145             spamprobe => "spamprobe spam",
146              
147             bogofilter => "bogofilter -Ns",
148              
149             );
150              
151              
152             # Command to tell the scanner that this message is NOT spam:
153              
154             our %NOTSPAM_CMD = (
155              
156             spamassassin => "spamassassin --revoke",
157              
158             crm114 => "crm -u $HOME/.crm114 mailreaver.crm --good",
159              
160             wpbl => "wpbl good",
161              
162             dspam => "dspam-train good",
163              
164             spamprobe => "spamprobe good",
165              
166             bogofilter => "bogofilter -Sn",
167              
168             );
169              
170              
171              
172             # Headers that we want to remove before passing through our filters:
173             our @EXTRA_TAGS = (
174              
175             qr/X-(\w+)-MailScanner/,
176              
177             qr/X-(\w+)-MailScanner-SpamScore/,
178              
179             qr/X-(\w+)-MailScanner-SpamCheck/,
180              
181             qr/Status/,
182              
183             qr/X-Status/,
184              
185             qr/X-KMail-(\S+)/,
186              
187             );
188              
189              
190              
191             our @FILTER_LIST = keys %FILTER_TAGS;
192              
193             # Some filters want to see the existing headers when reporting as spam/notspam:
194              
195             our @KEEP_TAG_CMDS = qw(dspam);
196              
197              
198              
199             # Preloaded methods go here.
200              
201              
202             # Split a message into header and body:
203              
204             sub extract_header($) {
205 0     0     my ($msg) = @_;
206 0 0         if ($msg =~ s/\n(\n.*)/\n/s) {
207 0           return($msg, $1);
208             } else {
209             # Assume we were just given a header
210 0           return($msg, "");
211             }
212             }
213              
214              
215             # Extract the spam headers relating to the given list of filters
216             # Return a hash table: key->headers
217              
218             sub extract_spam_headers($@) {
219 0     0     my ($msg, @filters) = @_;
220 0 0         @filters = @FILTER_LIST unless @filters;
221 0           my ($header, $body) = extract_header($msg);
222 0           my %tags = ();
223 0           foreach my $filter (@filters) {
224 0           $tags{$filter} = "";
225 0           foreach my $tag (@{$FILTER_TAGS{$filter}}) {
  0            
226 0           $tags{$filter} .= "$1\n" while $header =~ s/\n($tag:.*(\n[ \t].*)*)//;
227             }
228             }
229             # Always remove the extra tags:
230 0           $tags{"*extra*"} = "";
231 0           foreach my $tag (@EXTRA_TAGS) {
232 0           $tags{"*extra*"} .= "$1\n" while $header =~ s/\n($tag:.*(\n[ \t].*)*)//;
233             }
234 0           return(\%tags, $header, $body);
235             }
236              
237              
238             # Pass the message through the given list of filters,
239             # collect the results and return the tags:
240              
241             sub filter_message($@) {
242 0     0     my ($msg, @filters) = @_;
243 0 0         @filters = @FILTER_LIST unless @filters;
244             # Remove all tags before testing the message:
245 0           my ($orig_tags, $header, $body) = extract_spam_headers($msg, @FILTER_LIST);
246 0           my %tags;
247 0           foreach my $filter (@filters) {
248 0 0         next unless $FILTER_CMD{$filter};
249 0           $tags{$filter} = "";
250 0           my ($in_fh, $out_fh);
251 0 0         unless(open($in_fh, "-|")) {
252 0 0         open($out_fh, "|$FILTER_CMD{$filter}")
253             or croak "Can't run $FILTER_CMD{$filter}: $!\n";
254 0           print $out_fh $header, $body;
255 0           close($out_fh);
256 0           exit(0);
257             }
258 0           my $output = join("", <$in_fh>);
259 0           close($in_fh);
260 0 0         next if $output eq "";
261 0           my ($output_tags) = extract_spam_headers($output, $filter);
262 0           $tags{$filter} = $$output_tags{$filter};
263             }
264             # Copy any extra tags:
265 0           $tags{"*extra*"} = $$orig_tags{"*extra*"};
266 0           return(\%tags, $header, $body);
267             }
268              
269              
270             # Report this message as spam or good to the given list of filters:
271              
272             sub report_message($$@) {
273 0     0     my ($type, $msg, @filters) = @_;
274 0 0 0       croak qq[report_message: type must be "spam" or "good", not "$type"]
275             unless ($type eq "spam") || ($type eq "good");
276             # Remove all tags before reporting:
277 0           my ($orig_tags, $header, $body) = extract_spam_headers($msg, @FILTER_LIST);
278 0           foreach my $filter (@filters) {
279 0           my $cmd = "";
280 0 0         if ($type eq "spam") {
281 0           $cmd = $ISSPAM_CMD{$filter};
282             } else {
283 0           $cmd = $NOTSPAM_CMD{$filter};
284             }
285 0 0         next unless $cmd;
286 0 0         open(my $out_fh, "|$cmd") or croak "Can't run $cmd: $!\n";
287 0 0         if (grep { $filter eq $_ } @KEEP_TAG_CMDS) {
  0            
288 0           print $out_fh $header, $$orig_tags{$filter}, $body;
289             } else {
290 0           print $out_fh $header, $body;
291             }
292 0 0         close($out_fh) or die "close failed on pipe to $cmd";
293             }
294             }
295              
296              
297             # Count how many of the given filters have marked this message
298             # as either spam or good.
299             # Returns ($spam_count, $good_count, \@spam_voters, \@good_voters)
300              
301             sub count_votes($@) {
302 0     0     my ($msg, @filters) = @_;
303             # If we are given a message or header, then extract the tags
304 0           my $tags;
305 0 0         if (ref $msg eq "HASH") {
306 0           $tags = $msg;
307             } else {
308 0           ($tags) = extract_spam_headers($msg, @FILTER_LIST);
309             }
310 0           my $spam_count = 0;
311 0           my $good_count = 0;
312 0           my @spam_voters = ();
313 0           my @good_voters = ();
314 0           foreach my $filter (@filters) {
315 0 0         if ($$tags{$filter} =~ /$FILTER_SPAM_TAG{$filter}/) {
316 0           $spam_count++;
317 0           push(@spam_voters, $filter);
318             }
319 0 0         if ($$tags{$filter} =~ /$FILTER_GOOD_TAG{$filter}/) {
320 0           $good_count++;
321 0           push(@good_voters, $filter);
322             }
323             }
324 0           return($spam_count, $good_count, \@spam_voters, \@good_voters);
325             }
326              
327              
328              
329              
330             1;
331             __END__