File Coverage

blib/lib/Mail/SpamAssassin/Plugin/WLBLEval.pm
Criterion Covered Total %
statement 108 289 37.3
branch 26 148 17.5
condition 2 24 8.3
subroutine 19 30 63.3
pod 1 18 5.5
total 156 509 30.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             package Mail::SpamAssassin::Plugin::WLBLEval;
19              
20 22     22   149 use strict;
  22         53  
  22         728  
21 22     22   124 use warnings;
  22         40  
  22         699  
22             # use bytes;
23 22     22   133 use re 'taint';
  22         62  
  22         923  
24              
25 22     22   140 use NetAddr::IP 4.000;
  22         843  
  22         387  
26              
27 22     22   4608 use Mail::SpamAssassin::Plugin;
  22         56  
  22         626  
28 22     22   123 use Mail::SpamAssassin::Logger;
  22         50  
  22         67110  
29              
30             our @ISA = qw(Mail::SpamAssassin::Plugin);
31              
32             # constructor: register the eval rule
33             sub new {
34 63     63 1 232 my $class = shift;
35 63         166 my $mailsaobject = shift;
36              
37             # some boilerplate...
38 63   33     468 $class = ref($class) || $class;
39 63         359 my $self = $class->SUPER::new($mailsaobject);
40 63         416 bless ($self, $class);
41              
42             # the important bit!
43 63         297 $self->register_eval_rule("check_from_in_blacklist");
44 63         224 $self->register_eval_rule("check_to_in_blacklist");
45 63         197 $self->register_eval_rule("check_to_in_whitelist");
46 63         224 $self->register_eval_rule("check_to_in_more_spam");
47 63         233 $self->register_eval_rule("check_to_in_all_spam");
48 63         222 $self->register_eval_rule("check_from_in_list");
49 63         227 $self->register_eval_rule("check_replyto_in_list");
50 63         198 $self->register_eval_rule("check_to_in_list");
51 63         231 $self->register_eval_rule("check_from_in_whitelist");
52 63         235 $self->register_eval_rule("check_forged_in_whitelist");
53 63         266 $self->register_eval_rule("check_from_in_default_whitelist");
54 63         208 $self->register_eval_rule("check_forged_in_default_whitelist");
55 63         247 $self->register_eval_rule("check_mailfrom_matches_rcvd");
56 63         205 $self->register_eval_rule("check_uri_host_listed");
57             # same as: eval:check_uri_host_listed('BLACK') :
58 63         190 $self->register_eval_rule("check_uri_host_in_blacklist");
59             # same as: eval:check_uri_host_listed('WHITE') :
60 63         184 $self->register_eval_rule("check_uri_host_in_whitelist");
61              
62 63         551 return $self;
63             }
64              
65             sub check_from_in_blacklist {
66 77     77 0 209 my ($self, $pms) = @_;
67 77         317 foreach ($pms->all_from_addrs()) {
68 38 50       202 if ($self->_check_whitelist ($self->{main}->{conf}->{blacklist_from}, $_)) {
69 0         0 return 1;
70             }
71             }
72             }
73              
74             sub check_to_in_blacklist {
75 77     77 0 284 my ($self, $pms) = @_;
76 77         400 foreach ($pms->all_to_addrs()) {
77 54 50       84 if ($self->_check_whitelist ($self->{main}->{conf}->{blacklist_to}, $_)) {
78 0         0 return 1;
79             }
80             }
81             }
82              
83             sub check_to_in_whitelist {
84 77     77 0 221 my ($self, $pms) = @_;
85 77         340 foreach ($pms->all_to_addrs()) {
86 54 50       88 if ($self->_check_whitelist ($self->{main}->{conf}->{whitelist_to}, $_)) {
87 0         0 return 1;
88             }
89             }
90             }
91              
92             sub check_to_in_more_spam {
93 0     0 0 0 my ($self, $pms) = @_;
94 0         0 foreach ($pms->all_to_addrs()) {
95 0 0       0 if ($self->_check_whitelist ($self->{main}->{conf}->{more_spam_to}, $_)) {
96 0         0 return 1;
97             }
98             }
99             }
100              
101             sub check_to_in_all_spam {
102 0     0 0 0 my ($self, $pms) = @_;
103 0         0 foreach ($pms->all_to_addrs()) {
104 0 0       0 if ($self->_check_whitelist ($self->{main}->{conf}->{all_spam_to}, $_)) {
105 0         0 return 1;
106             }
107             }
108             }
109              
110             sub check_from_in_list {
111 14     14 0 47 my ($self, $pms, $list) = @_;
112 14         49 my $list_ref = $self->{main}{conf}{$list};
113 14 50       40 unless (defined $list_ref) {
114 0         0 warn "eval: could not find list $list";
115 0         0 return;
116             }
117              
118 14         119 foreach my $addr ($pms->all_from_addrs()) {
119 22 50       103 if ($self->_check_whitelist ($list_ref, $addr)) {
120 0         0 return 1;
121             }
122             }
123              
124 14         50 return 0;
125             }
126              
127             sub check_replyto_in_list {
128 0     0 0 0 my ($self, $pms, $list) = @_;
129 0         0 my $list_ref = $self->{main}{conf}{$list};
130 0 0       0 unless (defined $list_ref) {
131 0         0 warn "eval: could not find list $list";
132 0         0 return;
133             }
134              
135 0         0 my $replyto = $pms->get("Reply-To:addr");
136 0 0       0 return 0 if $replyto eq '';
137              
138 0 0       0 if ($self->_check_whitelist ($list_ref, $replyto)) {
139 0         0 return 1;
140             }
141              
142 0         0 return 0;
143             }
144              
145             # TODO: this should be moved to a utility module off PerMsgStatus,
146             # rather than a plugin API; it's used in Bayes.pm as a utility
147             sub check_wb_list {
148 28     28 0 63 my ($self, $params) = @_;
149              
150 28 50       77 return unless (defined $params->{permsgstatus});
151 28 50       67 return unless (defined $params->{type});
152 28 50       66 return unless (defined $params->{list});
153              
154 28 100       115 if (lc $params->{type} eq "to") {
    50          
155 14         55 return $self->check_to_in_list($params->{permsgstatus}, $params->{list});
156             }
157             elsif (lc $params->{type} eq "from") {
158 14         90 return $self->check_from_in_list($params->{permsgstatus}, $params->{list});
159             }
160              
161 0         0 return;
162             }
163              
164             sub check_to_in_list {
165 14     14 0 43 my ($self,$pms,$list) = @_;
166 14         45 my $list_ref = $self->{main}{conf}{$list};
167 14 50       44 unless (defined $list_ref) {
168 0         0 warn "eval: could not find list $list";
169 0         0 return;
170             }
171              
172 14         109 foreach my $addr ($pms->all_to_addrs()) {
173 24 50       58 if ($self->_check_whitelist ($list_ref, $addr)) {
174 0         0 return 1;
175             }
176             }
177              
178 14         93 return 0;
179             }
180              
181             ###########################################################################
182              
183             sub check_from_in_whitelist {
184 77     77 0 259 my ($self, $pms) = @_;
185 77 50       515 $self->_check_from_in_whitelist($pms) unless exists $pms->{from_in_whitelist};
186 77         1417 return ($pms->{from_in_whitelist} > 0);
187             }
188              
189             sub check_forged_in_whitelist {
190 0     0 0 0 my ($self, $pms) = @_;
191 0 0       0 $self->_check_from_in_whitelist($pms) unless exists $pms->{from_in_whitelist};
192 0 0       0 $self->_check_from_in_default_whitelist($pms) unless exists $pms->{from_in_default_whitelist};
193 0   0     0 return ($pms->{from_in_whitelist} < 0) && ($pms->{from_in_default_whitelist} == 0);
194             }
195              
196             sub check_from_in_default_whitelist {
197 77     77 0 234 my ($self, $pms) = @_;
198 77 50       544 $self->_check_from_in_default_whitelist($pms) unless exists $pms->{from_in_default_whitelist};
199 77         1461 return ($pms->{from_in_default_whitelist} > 0);
200             }
201              
202             sub check_forged_in_default_whitelist {
203 0     0 0 0 my ($self, $pms) = @_;
204 0 0       0 $self->_check_from_in_default_whitelist($pms) unless exists $pms->{from_in_default_whitelist};
205 0 0       0 $self->_check_from_in_whitelist($pms) unless exists $pms->{from_in_whitelist};
206 0   0     0 return ($pms->{from_in_default_whitelist} < 0) && ($pms->{from_in_whitelist} == 0);
207             }
208              
209             ###########################################################################
210              
211             sub _check_from_in_whitelist {
212 77     77   212 my ($self, $pms) = @_;
213 77         182 my $found_match = 0;
214 77         330 foreach ($pms->all_from_addrs()) {
215 38 50       224 if ($self->_check_whitelist ($self->{main}->{conf}->{whitelist_from}, $_)) {
216 0         0 $pms->{from_in_whitelist} = 1;
217 0         0 return;
218             }
219 38         200 my $wh = $self->_check_whitelist_rcvd ($pms, $self->{main}->{conf}->{whitelist_from_rcvd}, $_);
220 38 50       269 if ($wh == 1) {
    50          
221 0         0 $pms->{from_in_whitelist} = 1;
222 0         0 return;
223             }
224             elsif ($wh == -1) {
225 0         0 $found_match = -1;
226             }
227             }
228              
229 77         229 $pms->{from_in_whitelist} = $found_match;
230 77         148 return;
231             }
232              
233             ###########################################################################
234              
235             sub _check_from_in_default_whitelist {
236 77     77   254 my ($self, $pms) = @_;
237 77         181 my $found_match = 0;
238 77         321 foreach ($pms->all_from_addrs()) {
239 38         258 my $wh = $self->_check_whitelist_rcvd ($pms, $self->{main}->{conf}->{def_whitelist_from_rcvd}, $_);
240 38 50       211 if ($wh == 1) {
    50          
241 0         0 $pms->{from_in_default_whitelist} = 1;
242 0         0 return;
243             }
244             elsif ($wh == -1) {
245 0         0 $found_match = -1;
246             }
247             }
248              
249 77         314 $pms->{from_in_default_whitelist} = $found_match;
250 77         144 return;
251             }
252              
253             ###########################################################################
254              
255             # check if domain name of an envelope sender address matches a domain name
256             # of the first untrusted relay (if any), or any trusted relay otherwise
257             sub check_mailfrom_matches_rcvd {
258 0     0 0 0 my ($self, $pms) = @_;
259 0         0 my $sender = $pms->get("EnvelopeFrom:addr");
260 0 0       0 return 0 if $sender eq '';
261 0         0 return $self->_check_addr_matches_rcvd($pms,$sender);
262             }
263              
264             # check if domain name of a supplied e-mail address matches a domain name
265             # of the first untrusted relay (if any), or any trusted relay otherwise
266             sub _check_addr_matches_rcvd {
267 0     0   0 my ($self, $pms, $addr) = @_;
268              
269 0         0 local $1;
270 0 0       0 return 0 if $addr !~ / \@ ( [^\@]+ \. [^\@]+ ) \z/x;
271 0         0 my $addr_domain = lc $1;
272              
273 0         0 my @relays;
274 0 0       0 if ($pms->{num_relays_untrusted} > 0) {
    0          
275             # check against the first untrusted, if present
276 0         0 @relays = $pms->{relays_untrusted}->[0];
277             } elsif ($pms->{num_relays_trusted} > 0) {
278             # otherwise try all trusted ones, but only do so
279             # if there are no untrusted relays to avoid forgery
280 0         0 push(@relays, @{$pms->{relays_trusted}});
  0         0  
281             }
282 0 0       0 return 0 if !@relays;
283              
284             my($adrh,$adrd) =
285 0         0 $self->{main}->{registryboundaries}->split_domain($addr_domain);
286 0         0 my $match = 0;
287 0         0 my $any_tried = 0;
288 0         0 foreach my $rly (@relays) {
289 0         0 my $relay_rdns = $rly->{lc_rdns};
290 0 0 0     0 next if !defined $relay_rdns || $relay_rdns eq '';
291             my($rlyh,$rlyd) =
292 0         0 $self->{main}->{registryboundaries}->split_domain($relay_rdns);
293 0         0 $any_tried = 1;
294 0 0       0 if ($adrd eq $rlyd) {
295 0         0 dbg("rules: $addr MATCHES relay $relay_rdns ($adrd)");
296 0         0 $match = 1; last;
  0         0  
297             }
298             }
299 0 0 0     0 if ($any_tried && !$match) {
300             dbg("rules: %s does NOT match relay(s) %s",
301 0         0 $addr, join(', ', map { $_->{lc_rdns} } @relays));
  0         0  
302             }
303 0         0 return $match;
304             }
305              
306             ###########################################################################
307              
308             # look up $addr and trusted relays in a whitelist with rcvd
309             # note if it appears to be a forgery and $addr is not in any-relay list
310             sub _check_whitelist_rcvd {
311 76     76   218 my ($self, $pms, $list, $addr) = @_;
312              
313             # we can only match this if we have at least 1 trusted or untrusted header
314 76 100       337 return 0 unless ($pms->{num_relays_untrusted}+$pms->{num_relays_trusted} > 0);
315              
316 2         3 my @relays;
317             # try the untrusted one first
318 2 50       5 if ($pms->{num_relays_untrusted} > 0) {
319 2         5 @relays = $pms->{relays_untrusted}->[0];
320             }
321             # then try the trusted ones; the user could have whitelisted a trusted
322             # relay, totally permitted
323             # but do not do this if any untrusted relays, to avoid forgery -- bug 4425
324 2 50 33     6 if ($pms->{num_relays_trusted} > 0 && !$pms->{num_relays_untrusted} ) {
325 0         0 push (@relays, @{$pms->{relays_trusted}});
  0         0  
326             }
327              
328 2         4 $addr = lc $addr;
329 2         10 my $found_forged = 0;
330 2         3 foreach my $white_addr (keys %{$list}) {
  2         5  
331 0         0 my $regexp = qr/$list->{$white_addr}{re}/i;
332 0         0 foreach my $domain (@{$list->{$white_addr}{domain}}) {
  0         0  
333             # $domain is a second param in whitelist_from_rcvd: a domain name or an IP address
334            
335 0 0       0 if ($addr =~ $regexp) {
336             # From or sender address matching the first param in whitelist_from_rcvd
337 0         0 my $match;
338 0         0 foreach my $lastunt (@relays) {
339 0         0 local($1,$2);
340 0 0       0 if ($domain =~ m{^ \[ (.*) \] ( / \d{1,3} )? \z}sx) {
341             # matching by IP address
342 0         0 my($wl_ip, $rly_ip) = ($1, $lastunt->{ip});
343 0 0       0 $wl_ip .= $2 if defined $2; # allow prefix len even after bracket
344              
345 0 0 0     0 if (!defined $rly_ip || $rly_ip eq '') {
    0          
    0          
346             # relay's IP address not provided or unparseable
347              
348             } elsif ($wl_ip =~ /^\d+\.\d+\.\d+\.\d+\z/s) {
349             # an IPv4 whitelist entry can only be matched by an IPv4 relay
350 0 0       0 if ($wl_ip eq $rly_ip) { $match = 1; last } # exact match
  0         0  
  0         0  
351              
352             } elsif ($wl_ip =~ /^[\d\.]+\z/s) { # an IPv4 classful subnet?
353 0         0 $wl_ip =~ s/\.*\z/./; # enforce trailing dot
354 0 0       0 if ($rly_ip =~ /^\Q$wl_ip\E/) { $match = 1; last } # subnet
  0         0  
  0         0  
355              
356             } else { # either an wl entry is an IPv6 addr, or has a prefix len
357 0         0 my $rly_ip_obj = NetAddr::IP->new($rly_ip); # TCP-info field
358 0 0       0 if (!defined $rly_ip_obj) {
359 0         0 dbg("rules: bad IP address in relay: %s, sender: %s",
360             $rly_ip, $addr);
361             } else {
362 0         0 my $wl_ip_obj = NetAddr::IP->new($wl_ip); # whitelist 2nd param
363 0 0       0 if (!defined $wl_ip_obj) {
    0          
364 0         0 info("rules: bad IP address in whitelist: %s", $wl_ip);
365             } elsif ($wl_ip_obj->contains($rly_ip_obj)) {
366             # note: an IPv4-compatible IPv6 address can match an IPv4 addr
367 0         0 dbg("rules: relay addr %s matches whitelist %s, sender: %s",
368             $rly_ip, $wl_ip_obj, $addr);
369 0         0 $match = 1; last;
  0         0  
370             } else {
371 0         0 dbg("rules: relay addr %s does not match wl %s, sender %s",
372             $rly_ip, $wl_ip_obj, $addr);
373             }
374             }
375             }
376              
377             } else { # match by an rdns name
378 0         0 my $rdns = $lastunt->{lc_rdns};
379 0 0       0 if ($rdns =~ /(?:^|\.)\Q${domain}\E$/i) { $match=1; last }
  0         0  
  0         0  
380             }
381             }
382 0 0       0 if ($match) {
383             dbg("rules: address %s matches (def_)whitelist_from_rcvd %s %s",
384 0         0 $addr, $list->{$white_addr}{re}, $domain);
385 0         0 return 1;
386             }
387             # found address match but no relay match. note as possible forgery
388 0         0 $found_forged = -1;
389             }
390             }
391             }
392 2 50       5 if ($found_forged) { # might be forgery. check if in list of exempted
393 0         0 my $wlist = $self->{main}->{conf}->{whitelist_allows_relays};
394 0         0 foreach my $fuzzy_addr (values %{$wlist}) {
  0         0  
395 0 0       0 if ($addr =~ /$fuzzy_addr/i) {
396 0         0 $found_forged = 0;
397 0         0 last;
398             }
399             }
400             }
401 2         5 return $found_forged;
402             }
403              
404             ###########################################################################
405              
406             sub _check_whitelist {
407 230     230   497 my ($self, $list, $addr) = @_;
408 230         394 $addr = lc $addr;
409 230 50       512 if (defined ($list->{$addr})) { return 1; }
  0         0  
410 230         310 study $addr; # study is a no-op since perl 5.16.0, eliminating related bugs
411 230         295 foreach my $regexp (values %{$list}) {
  230         488  
412 0 0       0 if ($addr =~ qr/$regexp/i) {
413 0         0 dbg("rules: address $addr matches whitelist or blacklist regexp: $regexp");
414 0         0 return 1;
415             }
416             }
417              
418 230         1426 return 0;
419             }
420              
421             ###########################################################################
422              
423             sub check_uri_host_in_blacklist {
424 0     0 0   my ($self, $pms) = @_;
425 0           $self->check_uri_host_listed($pms, 'BLACK');
426             }
427              
428             sub check_uri_host_in_whitelist {
429 0     0 0   my ($self, $pms) = @_;
430 0           $self->check_uri_host_listed($pms, 'WHITE');
431             }
432              
433             sub check_uri_host_listed {
434 0     0 0   my ($self, $pms, $subname) = @_;
435 0           my $host_enlisted_ref = $self->_check_uri_host_listed($pms);
436 0 0         if ($host_enlisted_ref) {
437 0           my $matched_host = $host_enlisted_ref->{$subname};
438 0 0         if ($matched_host) {
439 0           dbg("rules: uri host enlisted (%s): %s", $subname, $matched_host);
440 0           $pms->test_log("URI: $matched_host");
441 0           return 1;
442             }
443             }
444 0           return 0;
445             }
446              
447             sub _check_uri_host_listed {
448 0     0     my ($self, $pms) = @_;
449              
450 0 0         if ($pms->{'uri_host_enlisted'}) {
451 0           return $pms->{'uri_host_enlisted'}; # just provide a cached result
452             }
453              
454 0           my $uri_lists_href = $self->{main}{conf}{uri_host_lists};
455 0 0 0       if (!$uri_lists_href || !%$uri_lists_href) {
456 0           $pms->{'uri_host_enlisted'} = {}; # no URI host lists
457 0           return $pms->{'uri_host_enlisted'};
458             }
459              
460 0           my %host_enlisted;
461 0           my @uri_listnames = sort keys %$uri_lists_href;
462 0 0         if (would_log("dbg","rules")) {
463 0           foreach my $nm (@uri_listnames) {
464             dbg("rules: check_uri_host_listed: (%s) %s",
465 0 0         $nm, join(', ', map { $uri_lists_href->{$nm}{$_} ? $_ : '!'.$_ }
466 0           sort keys %{$uri_lists_href->{$nm}}));
  0            
467             }
468             }
469             # obtain a complete list of html-parsed domains
470 0           my $uris = $pms->get_uri_detail_list();
471 0           my %seen;
472 0           while (my($uri,$info) = each %$uris) {
473 0 0         next if $uri =~ /^mailto:/i; # we may want to skip mailto: uris (?)
474 0           while (my($host,$domain) = each( %{$info->{hosts}} )) { # typically one
  0            
475 0 0         next if $seen{$host};
476 0           $seen{$host} = 1;
477 0           local($1,$2);
478 0           my @query_keys;
479 0 0         if ($host =~ /^\[(.*)\]\z/) { # looks like an address literal
    0          
    0          
480 0           @query_keys = ( $1 );
481             } elsif ($host =~ /^\d+\.\d+\.\d+\.\d+\z/) { # IPv4 address
482 0           @query_keys = ( $host );
483             } elsif ($host ne '') {
484 0           my($h) = $host;
485 0           for (;;) {
486 0 0         shift @query_keys if @query_keys > 10; # sanity limit, keep tail
487 0           push(@query_keys, $h); # sub.example.com, example.com, com
488 0 0         last if $h !~ s{^([^.]*)\.(.*)\z}{$2}s;
489             }
490             }
491 0           foreach my $nm (@uri_listnames) {
492 0           my $match;
493             my $verdict;
494 0           my $hash_nm_ref = $uri_lists_href->{$nm};
495 0           foreach my $q (@query_keys) {
496 0           $verdict = $hash_nm_ref->{$q};
497 0 0         if (defined $verdict) {
498 0 0         $match = $q eq $host ? $host : "$host ($q)";
499 0 0         $match = '!' if !$verdict;
500 0           last;
501             }
502             }
503 0 0         if (defined $verdict) {
504 0 0         $host_enlisted{$nm} = $match if $verdict;
505 0           dbg("rules: check_uri_host_listed %s, (%s): %s, search: %s",
506             $uri, $nm, $match, join(', ',@query_keys));
507             }
508             }
509             }
510             }
511 0           $pms->{'uri_host_enlisted'} = \%host_enlisted;
512 0           return $pms->{'uri_host_enlisted'};
513             }
514              
515             1;