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