File Coverage

blib/lib/Mail/SpamAssassin/Plugin/SpamCop.pm
Criterion Covered Total %
statement 44 118 37.2
branch 2 36 5.5
condition 5 46 10.8
subroutine 10 15 66.6
pod 2 5 40.0
total 63 220 28.6


line stmt bran cond sub pod time code
1             # <@LICENSE>
2             # Licensed to the Apache Software Foundation (ASF) under one or more
3             # contributor license agreements. See the NOTICE file distributed with
4             # this work for additional information regarding copyright ownership.
5             # The ASF licenses this file to you under the Apache License, Version 2.0
6             # (the "License"); you may not use this file except in compliance with
7             # the License. You may obtain a copy of the License at:
8             #
9             # http://www.apache.org/licenses/LICENSE-2.0
10             #
11             # Unless required by applicable law or agreed to in writing, software
12             # distributed under the License is distributed on an "AS IS" BASIS,
13             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14             # See the License for the specific language governing permissions and
15             # limitations under the License.
16             # </@LICENSE>
17              
18             =head1 NAME
19              
20             Mail::SpamAssassin::Plugin::SpamCop - perform SpamCop reporting of messages
21              
22             =head1 SYNOPSIS
23              
24             loadplugin Mail::SpamAssassin::Plugin::SpamCop
25              
26             =head1 DESCRIPTION
27              
28             SpamCop is a service for reporting spam. SpamCop determines the origin
29             of unwanted email and reports it to the relevant Internet service
30             providers. By reporting spam, you have a positive impact on the
31             problem. Reporting unsolicited email also helps feed spam filtering
32             systems, including, but not limited to, the SpamCop blacklist used in
33             SpamAssassin as a DNSBL.
34              
35             Note that spam reports sent by this plugin to SpamCop each include the
36             entire spam message.
37              
38             See http://www.spamcop.net/ for more information about SpamCop.
39              
40             =cut
41              
42             package Mail::SpamAssassin::Plugin::SpamCop;
43              
44 21     21   165 use Mail::SpamAssassin::Plugin;
  21         52  
  21         734  
45 21     21   143 use Mail::SpamAssassin::Logger;
  21         40  
  21         1505  
46 21     21   156 use IO::Socket;
  21         47  
  21         548  
47 21     21   19733 use strict;
  21         55  
  21         630  
48 21     21   364 use warnings;
  21         48  
  21         646  
49             # use bytes;
50 21     21   119 use re 'taint';
  21         55  
  21         1094  
51              
52 21     21   185 use constant HAS_NET_DNS => eval { require Net::DNS; };
  21         43  
  21         60  
  21         2026  
53 21     21   148 use constant HAS_NET_SMTP => eval { require Net::SMTP; };
  21         42  
  21         42  
  21         13059  
54              
55             our @ISA = qw(Mail::SpamAssassin::Plugin);
56              
57             sub new {
58 62     62 1 241 my $class = shift;
59 62         189 my $mailsaobject = shift;
60              
61 62   33     484 $class = ref($class) || $class;
62 62         391 my $self = $class->SUPER::new($mailsaobject);
63 62         223 bless ($self, $class);
64              
65             # are network tests enabled?
66 62 100 100     465 if (!$mailsaobject->{local_tests_only} && HAS_NET_DNS && HAS_NET_SMTP) {
      100        
67 1         7 $self->{spamcop_available} = 1;
68 1         7 dbg("reporter: network tests on, attempting SpamCop");
69             }
70             else {
71 61         317 $self->{spamcop_available} = 0;
72 61         317 dbg("reporter: local tests only, disabling SpamCop");
73             }
74              
75 62         460 $self->set_config($mailsaobject->{conf});
76              
77 62         697 return $self;
78             }
79              
80             sub set_config {
81 62     62 0 247 my($self, $conf) = @_;
82 62         159 my @cmds;
83              
84             =head1 USER OPTIONS
85              
86             =over 4
87              
88             =item spamcop_from_address user@example.com (default: none)
89              
90             This address is used during manual reports to SpamCop as the From:
91             address. You can use your normal email address. If this is not set, a
92             guess will be used as the From: address in SpamCop reports.
93              
94             =cut
95              
96             push (@cmds, {
97             setting => 'spamcop_from_address',
98             default => '',
99             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
100             code => sub {
101 0     0   0 my ($self, $key, $value, $line) = @_;
102 0 0       0 if ($value =~ /([^<\s]+\@[^>\s]+)/) {
    0          
103 0         0 $self->{spamcop_from_address} = $1;
104             }
105             elsif ($value =~ /^$/) {
106 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
107             }
108             else {
109 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
110             }
111             },
112 62         890 });
113              
114             =item spamcop_to_address user@example.com (default: generic reporting address)
115              
116             Your customized SpamCop report submission address. You need to obtain
117             this address by registering at C<http://www.spamcop.net/>. If this is
118             not set, SpamCop reports will go to a generic reporting address for
119             SpamAssassin users and your reports will probably have less weight in
120             the SpamCop system.
121              
122             =cut
123              
124             push (@cmds, {
125             setting => 'spamcop_to_address',
126             default => 'spamassassin-submit@spam.spamcop.net',
127             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
128             code => sub {
129 0     0   0 my ($self, $key, $value, $line) = @_;
130 0 0       0 if ($value =~ /([^<\s]+\@[^>\s]+)/) {
    0          
131 0         0 $self->{spamcop_to_address} = $1;
132             }
133             elsif ($value =~ /^$/) {
134 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
135             }
136             else {
137 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
138             }
139             },
140 62         728 });
141              
142             =item spamcop_max_report_size (default: 50)
143              
144             Messages larger than this size (in kilobytes) will be truncated in
145             report messages sent to SpamCop. The default setting is the maximum
146             size that SpamCop will accept at the time of release.
147              
148             =cut
149              
150 62         367 push (@cmds, {
151             setting => 'spamcop_max_report_size',
152             default => 50,
153             type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
154             });
155              
156 62         399 $conf->{parser}->register_commands(\@cmds);
157             }
158              
159             sub plugin_report {
160 0     0 1   my ($self, $options) = @_;
161              
162 0 0         return unless $self->{spamcop_available};
163              
164 0 0         if (!$options->{report}->{options}->{dont_report_to_spamcop}) {
165 0 0         if ($self->spamcop_report($options)) {
166 0           $options->{report}->{report_available} = 1;
167 0           info("reporter: spam reported to SpamCop");
168 0           $options->{report}->{report_return} = 1;
169             }
170             else {
171 0           info("reporter: could not report spam to SpamCop");
172             }
173             }
174             }
175              
176             sub smtp_dbg {
177 0     0 0   my ($command, $smtp) = @_;
178              
179 0           dbg("reporter: SpamCop sent $command");
180 0           my $code = $smtp->code();
181 0           my $message = $smtp->message();
182 0           my $debug;
183 0 0         $debug .= $code if $code;
184 0 0         $debug .= ($code ? " " : "") . $message if $message;
    0          
185 0           chomp $debug;
186 0           dbg("reporter: SpamCop received $debug");
187 0           return 1;
188             }
189              
190             sub spamcop_report {
191 0     0 0   my ($self, $options) = @_;
192              
193             # original text
194 0           my $original = ${$options->{text}};
  0            
195              
196             # check date
197 0           my $header = $original;
198 0           $header =~ s/\r?\n\r?\n.*//s;
199 0           my $date = Mail::SpamAssassin::Util::receive_date($header);
200 0 0 0       if ($date && $date < time - 2*86400) {
201 0           warn("reporter: SpamCop message older than 2 days, not reporting\n");
202 0           return 0;
203             }
204              
205             # message variables
206 0           my $boundary = "----------=_" . sprintf("%08X.%08X",time,int(rand(2**32)));
207 0           while ($original =~ /^\Q${boundary}\E$/m) {
208 0           $boundary .= "/".sprintf("%08X",int(rand(2**32)));
209             }
210 0           my $description = "spam report via " . Mail::SpamAssassin::Version();
211 0           my $trusted = $options->{msg}->{metadata}->{relays_trusted_str};
212 0           my $untrusted = $options->{msg}->{metadata}->{relays_untrusted_str};
213 0   0       my $user = $options->{report}->{main}->{'username'} || 'unknown';
214 0   0       my $host = Mail::SpamAssassin::Util::fq_hostname() || 'unknown';
215 0   0       my $from = $options->{report}->{conf}->{spamcop_from_address} || "$user\@$host";
216              
217             # message data
218             my %head = (
219             'To' => $options->{report}->{conf}->{spamcop_to_address},
220 0           'From' => $from,
221             'Subject' => 'report spam',
222             'Date' => Mail::SpamAssassin::Util::time_to_rfc822_date(),
223             'Message-Id' =>
224             sprintf("<%08X.%08X@%s>",time,int(rand(2**32)),$host),
225             'MIME-Version' => '1.0',
226             'Content-Type' => "multipart/mixed; boundary=\"$boundary\"",
227             );
228              
229             # truncate message
230 0 0         if (length($original) > $self->{main}->{conf}->{spamcop_max_report_size} * 1024) {
231 0           substr($original, ($self->{main}->{conf}->{spamcop_max_report_size} * 1024)) =
232             "\n[truncated by SpamAssassin]\n";
233             }
234              
235 0           my $body = <<"EOM";
236             This is a multi-part message in MIME format.
237              
238             --$boundary
239             Content-Type: message/rfc822; x-spam-type=report
240             Content-Description: $description
241             Content-Disposition: attachment
242             Content-Transfer-Encoding: 8bit
243             X-Spam-Relays-Trusted: $trusted
244             X-Spam-Relays-Untrusted: $untrusted
245              
246             $original
247             --$boundary--
248              
249             EOM
250              
251             # compose message
252 0           my $message;
253 0           while (my ($k, $v) = each %head) {
254 0           $message .= "$k: $v\n";
255             }
256 0           $message .= "\n" . $body;
257              
258             # send message
259 0           my $failure;
260 0           my $mx = $head{To};
261 0   0       my $hello = Mail::SpamAssassin::Util::fq_hostname() || $from;
262 0           $mx =~ s/.*\@//;
263 0           $hello =~ s/.*\@//;
264 0           for my $rr (Net::DNS::mx($mx)) {
265 0           my $exchange = Mail::SpamAssassin::Util::untaint_hostname($rr->exchange);
266 0 0         next unless $exchange;
267 0           my $smtp;
268 0 0         if ($smtp = Net::SMTP->new($exchange,
269             Hello => $hello,
270             Port => 587,
271             Timeout => 10))
272             {
273 0 0 0       if ($smtp->mail($from) && smtp_dbg("FROM $from", $smtp) &&
      0        
      0        
      0        
      0        
      0        
      0        
274             $smtp->recipient($head{To}) && smtp_dbg("TO $head{To}", $smtp) &&
275             $smtp->data($message) && smtp_dbg("DATA", $smtp) &&
276             $smtp->quit() && smtp_dbg("QUIT", $smtp))
277             {
278             # tell user we succeeded after first attempt if we previously failed
279 0 0         warn("reporter: SpamCop report to $exchange succeeded\n") if defined $failure;
280 0           return 1;
281             }
282 0           my $code = $smtp->code();
283 0           my $text = $smtp->message();
284 0 0 0       $failure = "$code $text" if ($code && $text);
285             }
286 0   0       $failure ||= "Net::SMTP error";
287 0           chomp $failure;
288 0           warn("reporter: SpamCop report to $exchange failed: $failure\n");
289             }
290              
291 0           return 0;
292             }
293              
294             1;
295              
296             =back
297              
298             =cut