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              
43             use Mail::SpamAssassin::Plugin;
44 22     22   166 use Mail::SpamAssassin::Logger;
  22         50  
  22         672  
45 22     22   121 use IO::Socket;
  22         50  
  22         1431  
46 22     22   159 use strict;
  22         39  
  22         441  
47 22     22   17725 use warnings;
  22         59  
  22         673  
48 22     22   128 # use bytes;
  22         37  
  22         660  
49             use re 'taint';
50 22     22   114  
  22         44  
  22         1144  
51             use constant HAS_NET_DNS => eval { require Net::DNS; };
52 22     22   206 use constant HAS_NET_SMTP => eval { require Net::SMTP; };
  22         39  
  22         43  
  22         1859  
53 22     22   117  
  22         33  
  22         40  
  22         11530  
54             our @ISA = qw(Mail::SpamAssassin::Plugin);
55              
56             my $class = shift;
57             my $mailsaobject = shift;
58 63     63 1 217  
59 63         151 $class = ref($class) || $class;
60             my $self = $class->SUPER::new($mailsaobject);
61 63   33     394 bless ($self, $class);
62 63         418  
63 63         180 # are network tests enabled?
64             if (!$mailsaobject->{local_tests_only} && HAS_NET_DNS && HAS_NET_SMTP) {
65             $self->{spamcop_available} = 1;
66 63 100 100     418 dbg("reporter: network tests on, attempting SpamCop");
      100        
67 1         5 }
68 1         3 else {
69             $self->{spamcop_available} = 0;
70             dbg("reporter: local tests only, disabling SpamCop");
71 62         292 }
72 62         244  
73             $self->set_config($mailsaobject->{conf});
74              
75 63         368 return $self;
76             }
77 63         558  
78             my($self, $conf) = @_;
79             my @cmds;
80              
81 63     63 0 257 =head1 USER OPTIONS
82 63         127  
83             =over 4
84              
85             =item spamcop_from_address user@example.com (default: none)
86              
87             This address is used during manual reports to SpamCop as the From:
88             address. You can use your normal email address. If this is not set, a
89             guess will be used as the From: address in SpamCop reports.
90              
91             =cut
92              
93             push (@cmds, {
94             setting => 'spamcop_from_address',
95             default => '',
96             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
97             code => sub {
98             my ($self, $key, $value, $line) = @_;
99             if ($value =~ /([^<\s]+\@[^>\s]+)/) {
100             $self->{spamcop_from_address} = $1;
101 0     0   0 }
102 0 0       0 elsif ($value =~ /^$/) {
    0          
103 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
104             }
105             else {
106 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
107             }
108             },
109 0         0 });
110              
111             =item spamcop_to_address user@example.com (default: generic reporting address)
112 63         779  
113             Your customized SpamCop report submission address. You need to obtain
114             this address by registering at C<http://www.spamcop.net/>. If this is
115             not set, SpamCop reports will go to a generic reporting address for
116             SpamAssassin users and your reports will probably have less weight in
117             the SpamCop system.
118              
119             =cut
120              
121             push (@cmds, {
122             setting => 'spamcop_to_address',
123             default => 'spamassassin-submit@spam.spamcop.net',
124             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
125             code => sub {
126             my ($self, $key, $value, $line) = @_;
127             if ($value =~ /([^<\s]+\@[^>\s]+)/) {
128             $self->{spamcop_to_address} = $1;
129 0     0   0 }
130 0 0       0 elsif ($value =~ /^$/) {
    0          
131 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
132             }
133             else {
134 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
135             }
136             },
137 0         0 });
138              
139             =item spamcop_max_report_size (default: 50)
140 63         570  
141             Messages larger than this size (in kilobytes) will be truncated in
142             report messages sent to SpamCop. The default setting is the maximum
143             size that SpamCop will accept at the time of release.
144              
145             =cut
146              
147             push (@cmds, {
148             setting => 'spamcop_max_report_size',
149             default => 50,
150 63         341 type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
151             });
152              
153             $conf->{parser}->register_commands(\@cmds);
154             }
155              
156 63         440 my ($self, $options) = @_;
157              
158             return unless $self->{spamcop_available};
159              
160 0     0 1   if (!$options->{report}->{options}->{dont_report_to_spamcop}) {
161             if ($self->spamcop_report($options)) {
162 0 0         $options->{report}->{report_available} = 1;
163             info("reporter: spam reported to SpamCop");
164 0 0         $options->{report}->{report_return} = 1;
165 0 0         }
166 0           else {
167 0           info("reporter: could not report spam to SpamCop");
168 0           }
169             }
170             }
171 0            
172             my ($command, $smtp) = @_;
173              
174             dbg("reporter: SpamCop sent $command");
175             my $code = $smtp->code();
176             my $message = $smtp->message();
177 0     0 0   my $debug;
178             $debug .= $code if $code;
179 0           $debug .= ($code ? " " : "") . $message if $message;
180 0           chomp $debug;
181 0           dbg("reporter: SpamCop received $debug");
182 0           return 1;
183 0 0         }
184 0 0          
    0          
185 0           my ($self, $options) = @_;
186 0            
187 0           # original text
188             my $original = ${$options->{text}};
189              
190             # check date
191 0     0 0   my $header = $original;
192             $header =~ s/\r?\n\r?\n.*//s;
193             my $date = Mail::SpamAssassin::Util::receive_date($header);
194 0           if ($date && $date < time - 2*86400) {
  0            
195             warn("reporter: SpamCop message older than 2 days, not reporting\n");
196             return 0;
197 0           }
198 0            
199 0           # message variables
200 0 0 0       my $boundary = "----------=_" . sprintf("%08X.%08X",time,int(rand(2**32)));
201 0           while ($original =~ /^\Q${boundary}\E$/m) {
202 0           $boundary .= "/".sprintf("%08X",int(rand(2**32)));
203             }
204             my $description = "spam report via " . Mail::SpamAssassin::Version();
205             my $trusted = $options->{msg}->{metadata}->{relays_trusted_str};
206 0           my $untrusted = $options->{msg}->{metadata}->{relays_untrusted_str};
207 0           my $user = $options->{report}->{main}->{'username'} || 'unknown';
208 0           my $host = Mail::SpamAssassin::Util::fq_hostname() || 'unknown';
209             my $from = $options->{report}->{conf}->{spamcop_from_address} || "$user\@$host";
210 0            
211 0           # message data
212 0           my %head = (
213 0   0       'To' => $options->{report}->{conf}->{spamcop_to_address},
214 0   0       'From' => $from,
215 0   0       'Subject' => 'report spam',
216             'Date' => Mail::SpamAssassin::Util::time_to_rfc822_date(),
217             'Message-Id' =>
218             sprintf("<%08X.%08X@%s>",time,int(rand(2**32)),$host),
219             'MIME-Version' => '1.0',
220 0           'Content-Type' => "multipart/mixed; boundary=\"$boundary\"",
221             );
222              
223             # truncate message
224             if (length($original) > $self->{main}->{conf}->{spamcop_max_report_size} * 1024) {
225             substr($original, ($self->{main}->{conf}->{spamcop_max_report_size} * 1024)) =
226             "\n[truncated by SpamAssassin]\n";
227             }
228              
229             my $body = <<"EOM";
230 0 0         This is a multi-part message in MIME format.
231 0            
232             --$boundary
233             Content-Type: message/rfc822; x-spam-type=report
234             Content-Description: $description
235 0           Content-Disposition: attachment
236             Content-Transfer-Encoding: 8bit
237             X-Spam-Relays-Trusted: $trusted
238             X-Spam-Relays-Untrusted: $untrusted
239              
240             $original
241             --$boundary--
242              
243             EOM
244              
245             # compose message
246             my $message;
247             while (my ($k, $v) = each %head) {
248             $message .= "$k: $v\n";
249             }
250             $message .= "\n" . $body;
251              
252 0           # send message
253 0           my $failure;
254 0           my $mx = $head{To};
255             my $hello = Mail::SpamAssassin::Util::fq_hostname() || $from;
256 0           $mx =~ s/.*\@//;
257             $hello =~ s/.*\@//;
258             for my $rr (Net::DNS::mx($mx)) {
259 0           my $exchange = Mail::SpamAssassin::Util::untaint_hostname($rr->exchange);
260 0           next unless $exchange;
261 0   0       my $smtp;
262 0           if ($smtp = Net::SMTP->new($exchange,
263 0           Hello => $hello,
264 0           Port => 587,
265 0           Timeout => 10))
266 0 0         {
267 0           if ($smtp->mail($from) && smtp_dbg("FROM $from", $smtp) &&
268 0 0         $smtp->recipient($head{To}) && smtp_dbg("TO $head{To}", $smtp) &&
269             $smtp->data($message) && smtp_dbg("DATA", $smtp) &&
270             $smtp->quit() && smtp_dbg("QUIT", $smtp))
271             {
272             # tell user we succeeded after first attempt if we previously failed
273 0 0 0       warn("reporter: SpamCop report to $exchange succeeded\n") if defined $failure;
      0        
      0        
      0        
      0        
      0        
      0        
274             return 1;
275             }
276             my $code = $smtp->code();
277             my $text = $smtp->message();
278             $failure = "$code $text" if ($code && $text);
279 0 0         }
280 0           $failure ||= "Net::SMTP error";
281             chomp $failure;
282 0           warn("reporter: SpamCop report to $exchange failed: $failure\n");
283 0           }
284 0 0 0        
285             return 0;
286 0   0       }
287 0            
288 0           1;
289              
290             =back
291 0            
292             =cut