File Coverage

blib/lib/Mail/SpamAssassin/Plugin/DNSEval.pm
Criterion Covered Total %
statement 35 178 19.6
branch 0 80 0.0
condition 1 33 3.0
subroutine 9 22 40.9
pod 3 14 21.4
total 48 327 14.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             DNSEVAL - look up URLs against DNS blocklists
21              
22             =cut
23              
24              
25             package Mail::SpamAssassin::Plugin::DNSEval;
26              
27 22     22   150 use Mail::SpamAssassin::Plugin;
  22         50  
  22         662  
28 22     22   128 use Mail::SpamAssassin::Logger;
  22         47  
  22         1300  
29 22     22   151 use Mail::SpamAssassin::Constants qw(:ip);
  22         69  
  22         2527  
30 22     22   156 use Mail::SpamAssassin::Util qw(reverse_ip_address);
  22         65  
  22         1177  
31              
32 22     22   172 use strict;
  22         56  
  22         566  
33 22     22   118 use warnings;
  22         60  
  22         680  
34             # use bytes;
35 22     22   126 use re 'taint';
  22         74  
  22         49118  
36              
37             our @ISA = qw(Mail::SpamAssassin::Plugin);
38              
39             # constructor: register the eval rule
40             sub new {
41 63     63 1 220 my $class = shift;
42 63         167 my $mailsaobject = shift;
43              
44             # some boilerplate...
45 63   33     511 $class = ref($class) || $class;
46 63         393 my $self = $class->SUPER::new($mailsaobject);
47 63         167 bless ($self, $class);
48              
49             # this is done this way so that the same list can be used here and in
50             # check_start()
51 63         417 $self->{'evalrules'} = [
52             'check_rbl_accreditor',
53             'check_rbl',
54             'check_rbl_txt',
55             'check_rbl_sub',
56             'check_rbl_results_for',
57             'check_rbl_from_host',
58             'check_rbl_from_domain',
59             'check_rbl_envfrom',
60             'check_dns_sender',
61             ];
62              
63 63         141 foreach(@{$self->{'evalrules'}}) {
  63         230  
64 567         1107 $self->register_eval_rule($_);
65             }
66              
67 63         581 return $self;
68             }
69              
70             # this is necessary because PMS::run_rbl_eval_tests() calls these functions
71             # directly as part of PMS
72             sub check_start {
73 81     81 1 270 my ($self, $opts) = @_;
74              
75 81         171 foreach(@{$self->{'evalrules'}}) {
  81         294  
76 729         1668 $opts->{'permsgstatus'}->register_plugin_eval_glue($_);
77             }
78             }
79              
80             sub ip_list_uniq_and_strip_private {
81 0     0 0   my ($self, @origips) = @_;
82 0           my @ips;
83             my %seen;
84 0           my $IP_PRIVATE = IP_PRIVATE;
85 0           foreach my $ip (@origips) {
86 0 0         next unless $ip;
87 0 0         next if (exists ($seen{$ip})); $seen{$ip} = 1;
  0            
88 0 0         next if ($ip =~ /$IP_PRIVATE/o);
89 0           push(@ips, $ip);
90             }
91 0           return @ips;
92             }
93              
94             # check an RBL if the message contains an "accreditor assertion,"
95             # that is, the message contains the name of a service that will vouch
96             # for their practices.
97             #
98             sub check_rbl_accreditor {
99 0     0 0   my ($self, $pms, $rule, $set, $rbl_server, $subtest, $accreditor) = @_;
100              
101 0 0         if (!defined $pms->{accreditor_tag}) {
102 0           $self->message_accreditor_tag($pms);
103             }
104 0 0         if ($pms->{accreditor_tag}->{$accreditor}) {
105 0           $self->check_rbl_backend($pms, $rule, $set, $rbl_server, 'A', $subtest);
106             }
107 0           return 0;
108             }
109              
110             # Check for an Accreditor Assertion within the message, that is, the name of
111             # a third-party who will vouch for the sender's practices. The accreditor
112             # can be asserted in the EnvelopeFrom like this:
113             #
114             # listowner@a--accreditor.mail.example.com
115             #
116             # or in an 'Accreditor" Header field, like this:
117             #
118             # Accreditor: accreditor1, parm=value; accreditor2, parm-value
119             #
120             # This implementation supports multiple accreditors, but ignores any
121             # parameters in the header field.
122             #
123             sub message_accreditor_tag {
124 0     0 0   my ($self, $pms) = @_;
125 0           my %acctags;
126              
127 0 0         if ($pms->get('EnvelopeFrom:addr') =~ /[@.]a--([a-z0-9]{3,})\./i) {
128 0           (my $tag = $1) =~ tr/A-Z/a-z/;
129 0           $acctags{$tag} = -1;
130             }
131 0           my $accreditor_field = $pms->get('Accreditor',undef);
132 0 0         if (defined $accreditor_field) {
133 0           my @accreditors = split(/,/, $accreditor_field);
134 0           foreach my $accreditor (@accreditors) {
135 0           my @terms = split(' ', $accreditor);
136 0 0         if ($#terms >= 0) {
137 0           my $tag = $terms[0];
138 0           $tag =~ tr/A-Z/a-z/;
139 0           $acctags{$tag} = -1;
140             }
141             }
142             }
143 0           $pms->{accreditor_tag} = \%acctags;
144             }
145              
146             sub check_rbl_backend {
147 0     0 0   my ($self, $pms, $rule, $set, $rbl_server, $type, $subtest) = @_;
148 0           local ($_);
149              
150             # First check that DNS is available, if not do not perform this check
151 0 0         return 0 if $self->{main}->{conf}->{skip_rbl_checks};
152 0 0         return 0 unless $pms->is_dns_available();
153 0           $pms->load_resolver();
154              
155 0 0 0       if (($rbl_server !~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) &&
      0        
156             (index($rbl_server, '.') >= 0) &&
157             ($rbl_server !~ /\.$/)) {
158 0           $rbl_server .= ".";
159             }
160              
161 0           dbg("dns: checking RBL $rbl_server, set $set");
162              
163             # ok, make a list of all the IPs in the untrusted set
164 0           my @fullips = map { $_->{ip} } @{$pms->{relays_untrusted}};
  0            
  0            
165              
166             # now, make a list of all the IPs in the external set, for use in
167             # notfirsthop testing. This will often be more IPs than found
168             # in @fullips. It includes the IPs that are trusted, but
169             # not in internal_networks.
170             my @fullexternal = map {
171 0 0         (!$_->{internal}) ? ($_->{ip}) : ()
172 0           } @{$pms->{relays_trusted}};
  0            
173 0           push (@fullexternal, @fullips); # add untrusted set too
174              
175             # Make sure a header significantly improves results before adding here
176             # X-Sender-Ip: could be worth using (very low occurance for me)
177             # X-Sender: has a very low bang-for-buck for me
178 0           my $IP_ADDRESS = IP_ADDRESS;
179 0           my @originating;
180 0           for my $header (@{$pms->{conf}->{originating_ip_headers}}) {
  0            
181 0           my $str = $pms->get($header,undef);
182 0 0 0       next unless defined $str && $str ne '';
183 0           push (@originating, ($str =~ m/($IP_ADDRESS)/g));
184             }
185              
186             # Let's go ahead and trim away all private ips (KLC)
187             # also uniq the list and strip dups. (jm)
188 0           my @ips = $self->ip_list_uniq_and_strip_private(@fullips);
189              
190             # if there's no untrusted IPs, it means we trust all the open-internet
191             # relays, so we can return right now.
192 0 0         return 0 unless (scalar @ips + scalar @originating > 0);
193              
194 0           dbg("dns: IPs found: full-external: ".join(", ", @fullexternal).
195             " untrusted: ".join(", ", @ips).
196             " originating: ".join(", ", @originating));
197              
198 0           my $trusted = $self->{main}->{conf}->{trusted_networks};
199              
200             # If name is foo-notfirsthop, check all addresses except for
201             # the originating one. Suitable for use with dialup lists, like the PDL.
202             # note that if there's only 1 IP in the untrusted set, do NOT pop the
203             # list, since it'd remove that one, and a legit user is supposed to
204             # use their SMTP server (ie. have at least 1 more hop)!
205             # If name is foo-lastexternal, check only the Received header just before
206             # it enters our internal networks; we can trust it and it's the one that
207             # passed mail between networks
208 0 0         if ($set =~ /-(notfirsthop|lastexternal)$/)
    0          
209             {
210             # use the external IP set, instead of the trusted set; the user may have
211             # specified some third-party relays as trusted. Also, don't use
212             # @originating; those headers are added by a phase of relaying through
213             # a server like Hotmail, which is not going to be in dialup lists anyway.
214 0           @ips = $self->ip_list_uniq_and_strip_private(@fullexternal);
215 0 0         if ($1 eq "lastexternal") {
216 0 0         @ips = (defined $ips[0]) ? ($ips[0]) : ();
217             } else {
218 0 0         pop @ips if (scalar @ips > 1);
219             }
220             }
221             # If name is foo-firsttrusted, check only the Received header just
222             # after it enters our trusted networks; that's the only one we can
223             # trust the IP address from (since our relay added that header).
224             # And if name is foo-untrusted, check any untrusted IP address.
225             elsif ($set =~ /-(first|un)trusted$/)
226             {
227 0           my @tips;
228 0           foreach my $ip (@originating) {
229 0 0 0       if ($ip && !$trusted->contains_ip($ip)) {
230 0           push(@tips, $ip);
231             }
232             }
233 0           @ips = $self->ip_list_uniq_and_strip_private (@ips, @tips);
234 0 0         if ($1 eq "first") {
235 0 0         @ips = (defined $ips[0]) ? ($ips[0]) : ();
236             } else {
237 0           shift @ips;
238             }
239             }
240             else
241             {
242 0           my @tips;
243 0           foreach my $ip (@originating) {
244 0 0 0       if ($ip && !$trusted->contains_ip($ip)) {
245 0           push(@tips, $ip);
246             }
247             }
248              
249             # add originating IPs as untrusted IPs (if they are untrusted)
250 0           @ips = reverse $self->ip_list_uniq_and_strip_private (@ips, @tips);
251             }
252              
253             # How many IPs max you check in the received lines
254 0           my $checklast=$self->{main}->{conf}->{num_check_received};
255              
256 0 0         if (scalar @ips > $checklast) {
257 0           splice (@ips, $checklast); # remove all others
258             }
259              
260 0           my $tflags = $pms->{conf}->{tflags}->{$rule};
261              
262             # Trusted relays should only be checked against nice rules (dnswls)
263 0 0 0       if (defined $tflags && $tflags !~ /\bnice\b/) {
264             # remove trusted hosts from beginning
265 0   0       while (@ips && $trusted->contains_ip($ips[0])) { shift @ips }
  0            
266             }
267              
268 0 0         unless (scalar @ips > 0) {
269 0           dbg("dns: no untrusted IPs to check");
270 0           return 0;
271             }
272              
273 0           dbg("dns: only inspecting the following IPs: ".join(", ", @ips));
274              
275 0           eval {
276 0           foreach my $ip (@ips) {
277 0           my $revip = reverse_ip_address($ip);
278 0 0         $pms->do_rbl_lookup($rule, $set, $type,
279             $revip.'.'.$rbl_server, $subtest) if defined $revip;
280             }
281             };
282              
283             # note that results are not handled here, hits are handled directly
284             # as DNS responses are harvested
285 0           return 0;
286             }
287              
288             sub check_rbl {
289 0     0 0   my ($self, $pms, $rule, $set, $rbl_server, $subtest) = @_;
290 0           $self->check_rbl_backend($pms, $rule, $set, $rbl_server, 'A', $subtest);
291             }
292              
293             sub check_rbl_txt {
294 0     0 0   my ($self, $pms, $rule, $set, $rbl_server, $subtest) = @_;
295 0           $self->check_rbl_backend($pms, $rule, $set, $rbl_server, 'TXT', $subtest);
296             }
297              
298             # run for first message
299             sub check_rbl_sub {
300 0     0 0   my ($self, $pms, $rule, $set, $subtest) = @_;
301              
302 0 0         return 0 if $self->{main}->{conf}->{skip_rbl_checks};
303 0 0         return 0 unless $pms->is_dns_available();
304              
305 0           $pms->register_rbl_subtest($rule, $set, $subtest);
306             }
307              
308             # backward compatibility
309             sub check_rbl_results_for {
310             #warn "dns: check_rbl_results_for() is deprecated, use check_rbl_sub()\n";
311 0     0 0   check_rbl_sub(@_);
312             }
313              
314             # this only checks the address host name and not the domain name because
315             # using the domain name had much worse results for dsn.rfc-ignorant.org
316             sub check_rbl_from_host {
317 0     0 0   my ($self, $pms, $rule, $set, $rbl_server, $subtest) = @_;
318 0           _check_rbl_addresses($self, $pms, $rule, $set, $rbl_server, $subtest, $_[1]->all_from_addrs_domains());
319             }
320              
321             =over 4
322              
323             =item check_rbl_from_domain
324              
325             This checks all the from addrs domain names as an alternate to check_rbl_from_host. As of v3.4.1, it has been improved to include a subtest for a specific octet.
326              
327             =back
328              
329             =cut
330              
331             sub check_rbl_from_domain {
332 0     0 1   my ($self, $pms, $rule, $set, $rbl_server, $subtest) = @_;
333 0           _check_rbl_addresses($self, $pms, $rule, $set, $rbl_server, $subtest, $_[1]->all_from_addrs_domains());
334             }
335              
336             # this only checks the address host name and not the domain name because
337             # using the domain name had much worse results for dsn.rfc-ignorant.org
338             sub check_rbl_envfrom {
339 0     0 0   my ($self, $pms, $rule, $set, $rbl_server, $subtest) = @_;
340 0           _check_rbl_addresses($self, $pms, $rule, $set, $rbl_server, $subtest, $_[1]->get('EnvelopeFrom:addr',undef));
341             }
342              
343             sub _check_rbl_addresses {
344 0     0     my ($self, $pms, $rule, $set, $rbl_server, $subtest, @addresses) = @_;
345            
346 0 0         return 0 if $self->{main}->{conf}->{skip_rbl_checks};
347 0 0         return 0 unless $pms->is_dns_available();
348              
349 0           my %hosts;
350 0           for (@addresses) {
351 0 0 0       next if !defined($_) || !/ \@ ( [^\@\s]+ )/x;
352 0           my $address = $1;
353             # strip leading & trailing dots (as seen in some e-mail addresses)
354 0           $address =~ s/^\.+//; $address =~ s/\.+\z//;
  0            
355             # squash duplicate dots to avoid an invalid DNS query with a null label
356 0           $address =~ tr/.//s;
357 0 0         $hosts{lc($address)} = 1 if $address =~ /\./; # must by a FQDN
358             }
359 0 0         return unless scalar keys %hosts;
360              
361 0           $pms->load_resolver();
362              
363 0 0 0       if (($rbl_server !~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) &&
      0        
364             (index($rbl_server, '.') >= 0) &&
365             ($rbl_server !~ /\.$/)) {
366 0           $rbl_server .= ".";
367             }
368 0           dbg("dns: _check_rbl_addresses RBL $rbl_server, set $set");
369              
370 0           for my $host (keys %hosts) {
371 0           dbg("dns: checking [$host] / $rule / $set / $rbl_server");
372 0           $pms->do_rbl_lookup($rule, $set, 'A', "$host.$rbl_server", $subtest);
373             }
374             }
375              
376             sub check_dns_sender {
377 0     0 0   my ($self, $pms, $rule) = @_;
378              
379 0           my $host;
380 0           for my $from ($pms->get('EnvelopeFrom:addr',undef)) {
381 0 0         next unless defined $from;
382              
383 0           $from =~ tr/././s; # bug 3366
384 0 0         if ($from =~ m/ \@ ( [^\@\s]+ \. [^\@\s]+ )/x ) {
385 0           $host = lc($1);
386 0           last;
387             }
388             }
389 0 0         return 0 unless defined $host;
390              
391             # First check that DNS is available, if not do not perform this check
392             # TODO: need a way to skip DNS checks as a whole in configuration
393 0 0         return 0 unless $pms->is_dns_available();
394 0           $pms->load_resolver();
395              
396 0 0         if ($host eq 'compiling.spamassassin.taint.org') {
397             # only used when compiling
398 0           return 0;
399             }
400              
401 0           dbg("dns: checking A and MX for host $host");
402              
403 0           $pms->do_dns_lookup($rule, 'A', $host);
404 0           $pms->do_dns_lookup($rule, 'MX', $host);
405              
406             # cache name of host for later checking
407 0           $pms->{sender_host} = $host;
408              
409 0           return 0;
410             }
411              
412             1;