File Coverage

blib/lib/Mail/SpamAssassin/Plugin/GoogleSafeBrowsing.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             # Copyright 2007 Daniel Born
2             #
3             # Licensed under the Apache License, Version 2.0 (the "License");
4             # you may not use this file except in compliance with the License.
5             # You may obtain a copy of the License at
6             #
7             # http://www.apache.org/licenses/LICENSE-2.0
8             #
9             # Unless required by applicable law or agreed to in writing, software
10             # distributed under the License is distributed on an "AS IS" BASIS,
11             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12             # See the License for the specific language governing permissions and
13             # limitations under the License.
14              
15             =head1 NAME
16              
17             Mail::SpamAssassin::Plugin::GoogleSafeBrowsing - SpamAssassin plugin to score
18             mail based on Google blocklists.
19              
20             =head1 SYNOPSIS
21              
22             loadplugin Mail::SpamAssassin::Plugin::GoogleSafeBrowsing
23             body GOOGLE_SAFEBROWSING eval:check_google_safebrowsing_blocklists()
24              
25             =head1 DESCRIPTION
26              
27             Score messages by checking the URIs they contain against Google's safebrowsing
28             tables. See L
29              
30             =head1 CONFIGURATION
31              
32             The GoogleSafeBrowsing SpamAssassin plugin relies on a local cache of the
33             URI tables to scan messages. The local cache should be updated at least once
34             every 30 minutes. The recommended setup looks something like:
35              
36             =over
37              
38             =item Install the required Perl modules:
39              
40             Net::Google::SafeBrowsing::Blocklist
41             Net::Google::SafeBrowsing::UpdateRequest
42             Mail::SpamAssassin::Plugin::GoogleSafeBrowsing
43              
44             =item Get an API key from Google
45              
46             L
47              
48             =item Use the blocklist_updater Perl script to keep the local cache up to date.
49              
50             Install a cron job that, every 25 minutes or so, runs something like:
51              
52             APIKEY=ABCD...
53             for LIST in goog-black-hash goog-malware-hash; do
54             blocklist_updater --apikey "$APIKEY" --blocklist $LIST --dbfile /var/cache/spamassassin/${LIST}-db
55             done
56              
57             "goog-black-hash" and "goog-malware-hash" are the only lists Google has for now.
58             goog-black-hash seems to be a list for the worst sites.
59              
60             =item Configure spamassassin
61              
62             Typically in local.cf, include lines:
63             loadplugin Mail::SpamAssassin::Plugin::GoogleSafeBrowsing
64             body GOOGLE_SAFEBROWSING eval:check_google_safebrowsing_blocklists()
65              
66             google_safebrowsing_dir /var/cache/spamassassin
67             google_safebrowsing_apikey ABCD...
68             google_safebrowsing_blocklist goog-black-hash 0.2
69             google_safebrowsing_blocklist goog-malware-hash 0.1
70              
71             In this example, for each URI in a message that has a match in goog-black-hash,
72             add 0.2 to the message's spam score.
73              
74             =back
75              
76             =cut
77              
78             package Mail::SpamAssassin::Plugin::GoogleSafeBrowsing;
79 1     1   22912 use strict;
  1         2  
  1         36  
80 1     1   7 use warnings;
  1         1  
  1         29  
81 1     1   528 use Net::Google::SafeBrowsing::Blocklist;
  0            
  0            
82             use Mail::SpamAssassin::Plugin;
83             use Mail::SpamAssassin::Logger;
84             use URI;
85             use File::Spec;
86             use base qw(Mail::SpamAssassin::Plugin);
87             our $VERSION = '1.03';
88              
89             our $CONFIG_DIR = 'google_safebrowsing_dir';
90             our $CONFIG_APIKEY = 'google_safebrowsing_apikey';
91             our $CONFIG_BLOCKLIST = 'google_safebrowsing_blocklist';
92             # Map config key to number of args.
93             our %CONFIGKEYS = ($CONFIG_DIR => 1,
94             $CONFIG_APIKEY => 1,
95             $CONFIG_BLOCKLIST => 1);
96              
97             our $RULENAME = 'GOOGLE_SAFEBROWSING';
98             our $LOG_FACILITY = 'GoogleSafeBrowsing';
99              
100             # Fields:
101             # mailsa - Mail::SpamAssassin instance
102             # blocklists - {$name => {bl => Net::Google::SafeBrowsing::Blocklist, score => },
103             # ...,}
104             sub new {
105             my ($class, $mailsa) = @_;
106             $class = ref($class) || $class;
107             my $self = $class->SUPER::new($mailsa);
108             bless($self, $class);
109             $self->{mailsa} = $mailsa;
110             Mail::SpamAssassin::Logger::add_facilities($LOG_FACILITY);
111             $self->register_eval_rule('check_google_safebrowsing_blocklists');
112             $self->set_config($mailsa->{conf});
113             return $self;
114             }
115              
116             sub set_config {
117             my Mail::SpamAssassin::Plugin::GoogleSafeBrowsing $self = shift;
118             my ($conf) = @_;
119              
120             sub config_log {
121             my ($config, @msg) = @_;
122             my $msg = join('', "$LOG_FACILITY: ", @msg);
123             if ($config->{lint_rules}) {
124             warn $msg, "\n";
125             } else {
126             Mail::SpamAssassin::Logger::info($msg);
127             }
128             }
129              
130             sub required_dir {
131             my ($config, $key, $value, $line) = @_;
132             if (not defined($value) or length($value) == 0) {
133             return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
134             }
135             $value = Mail::SpamAssassin::Util::untaint_file_path($value);
136             if (not (-d $value and -x _)) {
137             config_log($config,
138             "config: $key '$value' isn't a readable directory");
139             return $Mail::SpamAssassin::Conf::INVALID_VALUE;
140             }
141             $config->{$key} = $value;
142             }
143              
144             sub blocklist_config {
145             my ($config, $key, $value, $line) = @_;
146             my ($name, $score) = split(/\s+/, $value, 2);
147             if (not (defined($name) and defined($score) and $score =~ /^(:?\d*\.)?\d+$/)) {
148             config_log($config, "config: $key ");
149             return $Mail::SpamAssassin::Conf::INVALID_VALUE;
150             }
151             $config->{$key}->{$name} = $score;
152             }
153              
154             my @cmds;
155             push(@cmds,
156             {setting => $CONFIG_DIR,
157             code => \&required_dir,},
158             {setting => $CONFIG_APIKEY,
159             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,},
160             {setting => $CONFIG_BLOCKLIST,
161             code => \&blocklist_config,},);
162             $conf->{parser}->register_commands(\@cmds);
163             }
164              
165             sub finish_parsing_end {
166             my Mail::SpamAssassin::Plugin::GoogleSafeBrowsing $self = shift;
167             my ($opts) = @_;
168             if (not ($opts->{conf}->{$CONFIG_BLOCKLIST} and
169             $opts->{conf}->{$CONFIG_DIR} and
170             $opts->{conf}->{$CONFIG_APIKEY})) {
171             Mail::SpamAssassin::Logger::info("$LOG_FACILITY: Incomplete config, " .
172             "need all of $CONFIG_BLOCKLIST, $CONFIG_DIR, $CONFIG_APIKEY");
173             return;
174             }
175             while (my ($name, $score) = each(%{$opts->{conf}->{$CONFIG_BLOCKLIST}})) {
176             $self->{blocklists}->{$name}->{bl} = Net::Google::SafeBrowsing::Blocklist->new(
177             $name, File::Spec->join($opts->{conf}->{$CONFIG_DIR}, $name . '-db'),
178             $opts->{conf}->{$CONFIG_APIKEY});
179             $self->{blocklists}->{$name}->{score} = $score;
180             }
181             }
182              
183             sub l {
184             Mail::SpamAssassin::Logger::dbg("$LOG_FACILITY: " . join('', @_));
185             }
186              
187             sub check_google_safebrowsing_blocklists {
188             my Mail::SpamAssassin::Plugin::GoogleSafeBrowsing $self = shift;
189             my ($pms, $msg_ary_ref) = @_;
190             my %uris;
191             while (my($raw_uri, $urifields) = each(%{$pms->get_uri_detail_list})) {
192             my $cleaned = $urifields->{cleaned};
193             my $uristr;
194             if (@{$cleaned} > 1) {
195             $uristr = $cleaned->[1];
196             } elsif (@{$cleaned} > 0) {
197             $uristr = $cleaned->[0];
198             } else {
199             $uristr = $raw_uri;
200             }
201             if (defined($uristr)) {
202             $uris{$uristr} = 1;
203             }
204             }
205             my $spamscore = 0.0;
206             while (my ($uristr, $unused) = each(%uris)) {
207             while (my ($name, $fields) = each(%{$self->{blocklists}})) {
208             my $matched_uri = $fields->{bl}->suffix_prefix_match($uristr);
209             if (defined($matched_uri)) {
210             $spamscore += $fields->{score};
211             }
212             l("URI: '", $uristr, "', blocklist: ", $name, ", match: '",
213             defined($matched_uri) ? $matched_uri : "(none)", "'");
214             }
215             }
216             l("Spam score for message: ", $spamscore);
217             if ($spamscore > 0.0) {
218             $pms->got_hit($RULENAME, 'BODY: ', score => $spamscore);
219             for my $set (0..3) {
220             $pms->{conf}->{scoreset}->[$set]->{$RULENAME} =
221             sprintf("%0.3f", $spamscore);
222             }
223             }
224             return 0;
225             }
226              
227              
228             1;