File Coverage

blib/lib/Mail/SpamAssassin/Plugin/VBounce.pm
Criterion Covered Total %
statement 28 71 39.4
branch 0 30 0.0
condition 1 6 16.6
subroutine 7 11 63.6
pod 1 4 25.0
total 37 122 30.3


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::VBounce - aid in rescuing genuine bounces
21              
22             =head1 SYNOPSIS
23              
24             loadplugin Mail::SpamAssassin::Plugin::VBounce [/path/to/VBounce.pm]
25              
26             =cut
27              
28              
29             use Mail::SpamAssassin::Plugin;
30 22     22   160 use Mail::SpamAssassin::Logger;
  22         42  
  22         637  
31 22     22   124 use strict;
  22         45  
  22         1141  
32 22     22   132 use warnings;
  22         44  
  22         493  
33 22     22   107 use re 'taint';
  22         62  
  22         723  
34 22     22   139  
  22         41  
  22         15891  
35             our @ISA = qw(Mail::SpamAssassin::Plugin);
36              
37             my $class = shift;
38             my $mailsaobject = shift;
39 63     63 1 194  
40 63         120 $class = ref($class) || $class;
41             my $self = $class->SUPER::new($mailsaobject);
42 63   33     442 bless ($self, $class);
43 63         328  
44 63         145 $self->register_eval_rule("have_any_bounce_relays");
45             $self->register_eval_rule("check_whitelist_bounce_relays");
46 63         249  
47 63         187 $self->set_config($mailsaobject->{conf});
48              
49 63         260 return $self;
50             }
51 63         522  
52             my($self, $conf) = @_;
53             my @cmds;
54              
55 63     63 0 152 =head1 USER PREFERENCES
56 63         110  
57             The following options can be used in both site-wide (C<local.cf>) and
58             user-specific (C<user_prefs>) configuration files to customize how
59             SpamAssassin handles incoming email messages.
60              
61             =over 4
62              
63             =item whitelist_bounce_relays hostname [hostname2 ...]
64              
65             This is used to 'rescue' legitimate bounce messages that were generated in
66             response to mail you really *did* send. List the MTA relay hostnames that
67             your outbound mail is delivered through. If a bounce message is found, and
68             it contains one of these hostnames in a 'Received' header found the in the
69             message body, it will not be marked as a blowback virus-bounce.
70              
71             The hostnames can be file-glob-style patterns, so C<relay*.isp.com> will work.
72             Specifically, C<*> and C<?> are allowed, but all other metacharacters are not.
73             Regular expressions are not used for security reasons.
74              
75             Multiple addresses per line, separated by spaces, is OK. Multiple
76             C<whitelist_bounce_relays> lines are also OK.
77              
78             =back
79              
80             =cut
81              
82             push (@cmds, {
83             setting => 'whitelist_bounce_relays',
84             type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
85 63         270 });
86              
87             $conf->{parser}->register_commands(\@cmds);
88             }
89              
90 63         302 my ($self, $pms) = @_;
91             return $pms->{conf}->{whitelist_bounce_relays} &&
92             %{$pms->{conf}->{whitelist_bounce_relays}} ? 1 : 0;
93             }
94 0     0 0    
95             my ($self, $pms) = @_;
96 0 0 0        
97             return 0 if !$self->have_any_bounce_relays($pms);
98              
99             my $body = $pms->get_decoded_stripped_body_text_array();
100 0     0 0   my $res;
101              
102 0 0         # catch lines like:
103             # Received: by dogma.boxhost.net (Postfix, from userid 1007)
104 0            
105 0           # check the plain-text body, first
106             foreach my $line (@{$body}) {
107             next unless ($line =~ /^[> ]*Received:/i);
108             while ($line =~ / (\S+\.\S+) /g) {
109             return 1 if $self->_relay_is_in_whitelist_bounce_relays($pms, $1);
110             }
111 0           }
  0            
112 0 0          
113 0           # now check any "message/anything" attachment MIME parts, too.
114 0 0         # don't use the more efficient find_parts() method until bug 5331 is
115             # fixed, otherwise we'll miss some messages due to their MIME structure
116              
117             my $pristine = $pms->{msg}->get_pristine_body();
118              
119             # triage, avoids expensive loop through large mail with attachments
120             return 0 if $pristine !~ /Received:/i;
121              
122 0           my $found_received = 0;
123             my $fullhdr = '';
124             foreach my $line ($pristine =~ /^(.*)$/gm) {
125 0 0         if (!defined $line) { return 0; }
126              
127 0           # don't bother until we see a line with "Received:" in it
128 0           if (!$found_received) {
129 0           next unless ($line =~ /^[> ]*Received:/i);
130 0 0         $found_received = 1;
  0            
131             }
132              
133 0 0         if ($line =~ /^\s/) { # bug 5912, deal with multiline
134 0 0         $fullhdr .= $line;
135 0           } else {
136             $fullhdr = $line;
137             }
138 0 0          
139 0           next unless ($fullhdr =~ /^[> ]*Received:/i);
140             while ($fullhdr =~ /\s(\S+\.\S+)\s/gs) {
141 0           return 1 if $self->_relay_is_in_whitelist_bounce_relays($pms, $1);
142             }
143             }
144 0 0          
145 0           return 0;
146 0 0         }
147              
148             my ($self, $pms, $relay) = @_;
149             return 1 if $self->_relay_is_in_list(
150 0           $pms->{conf}->{whitelist_bounce_relays}, $pms, $relay);
151             dbg("rules: relay $relay doesn't match any whitelist");
152              
153             return 0;
154 0     0     }
155              
156 0 0         my ($self, $list, $pms, $relay) = @_;
157 0           $relay = lc $relay;
158             utf8::encode($relay) if utf8::is_utf8($relay); # encode chars to UTF-8
159 0            
160             if (defined $list->{$relay}) { return 1; }
161              
162             foreach my $regexp (values %{$list}) {
163 0     0     if ($relay =~ qr/$regexp/i) {
164 0           dbg("rules: relay $relay matches regexp: $regexp");
165 0 0         return 1;
166             }
167 0 0         }
  0            
168              
169 0           return 0;
  0            
170 0 0         }
171 0            
172 0           1;