Subversion Repositories

?revision_form?Rev ?revision_input??revision_submit??revision_endform?

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 magnus 1
package Greylisting;
2
#
3
# $Id: Greylisting.pm,v 1.4 2006/01/11 17:17:28 marcmerlin Exp $
4
#
5
 
6
# General Greylisting Plugin, written by Marc MERLIN <marc_soft@merlins.org>
7
# (Kristopher Austin gets the credit for the original port to an SA 3.0 plugin)
8
#
9
# This was originally written to implement greylisting in SA-Exim, although
10
# I have tried to make it more general and allow for reuse in other MTAs
11
# (although they will need to
12
# 1) be running SA at SMTP time
13
# 2) Provide the list of rcpt to and env from in some headers for SA to read
14
# 3) Provide the IP of the connecting host )
15
#
16
# This rule should get a negative score so that if we've already seen the
17
# greylisting tuplet before, we lower the score, which hopefully brings us from
18
# a tempreject to an accept (at least that's how sa-exim does it)
19
# 
20
# -- Marc 2004/01/19
21
 
22
use strict;
23
use Mail::SpamAssassin::Plugin;
24
our @ISA = qw(Mail::SpamAssassin::Plugin);
25
 
26
sub new
27
{
28
    my ($class, $mailsa) = @_;
29
    $class = ref($class) || $class;
30
    my $self = $class->SUPER::new($mailsa);
31
    bless ($self, $class);
32
    $self->register_eval_rule ("greylisting");
33
    return $self;
34
}
35
 
36
 
37
sub check_end
38
{
39
    my ($self, $permsgstatus) = @_;
40
 
41
    if (not $self->{'rangreylisting'})
42
    {
43
        Mail::SpamAssassin::Plugin::dbg("GREYLISTING: greylisting didn't run since the configuration wasn't setup to call us");
44
    }
45
}
46
 
47
# Greylisting happens depending on the SA score, so we want to run it last,
48
# which is why we give it a high priority
49
sub greylisting
50
{
51
    my ($self, $permsgstatus, $optionhash) = @_;
52
 
53
    my $connectip;
54
    my $envfrom;
55
    my $rcptto;
56
    my @rcptto;
57
    my $iswhitelisted=0;
58
    my $err;
59
    my $mesgid = $permsgstatus->get('Message-Id')."\n";
60
    my $mesgidfn;
61
    my $tuplet;
62
    my $sascore = $permsgstatus->get_score();
63
    my $dontcheckscore;
64
    my %option;
65
 
66
    Mail::SpamAssassin::Plugin::dbg("GREYLISTING: called function");
67
 
68
    $optionhash  =~ s/;/,/g;
69
    # This is safe, right? (users shouldn't be able to set it in their config)
70
    %option=eval $optionhash;
71
    $self->{'rangreylisting'}=1;
72
 
73
    foreach my $reqoption (qw ( method greylistsecs dontgreylistthreshold
74
        connectiphdr envfromhdr rcpttohdr greylistnullfrom greylistfourthbyte ))
75
    {
76
        die "Greylist option $reqoption missing from SA config" unless (defined $option{$reqoption});
77
    }
78
 
79
    $dontcheckscore = $option{'dontgreylistthreshold'};
80
 
81
 
82
    # No newlines, thank you (yes, you need this twice apparently)
83
    chomp ($mesgid);
84
    chomp ($mesgid);
85
    # Newline in the middle mesgids, are you serious? Get rid of them here
86
    $mesgid =~ s/\012/|/g;
87
 
88
    # For stuff that we know is spam, don't greylist the host
89
    # (that might help later spam with a lower score to come in)
90
    if ($sascore >= $dontcheckscore)
91
    {
92
        Mail::SpamAssassin::Plugin::dbg("GREYLISTING: skipping greylisting on $mesgid, since score is already $sascore and you configured greylisting not to bother with anything above $dontcheckscore");
93
        return 0;
94
    }
95
    else
96
    {
97
        Mail::SpamAssassin::Plugin::dbg("GREYLISTING: running greylisting on $mesgid, since score is too low ($sascore) and you configured greylisting to greylist anything under $dontcheckscore");
98
    }
99
 
100
    if (not $connectip = $permsgstatus->get($option{'connectiphdr'}))
101
    {
102
        warn "Couldn't get Connecting IP header $option{'connectiphdr'} for message $mesgid, skipping greylisting call\n";
103
        return 0;
104
    }
105
    chomp($connectip);
106
    # Clean up input (for security, if you use files/dirs)
107
    $connectip =~ /([\d.:]+)/;
108
    $connectip = ($1 or "");
109
 
110
    # Account for a null envelope from
111
    if (not defined ($envfrom = $permsgstatus->get($option{'envfromhdr'})))
112
    {
113
        warn "Couldn't get Envelope From header $option{'envfromhdr'} for message $mesgid, skipping greylisting call\n";
114
        return 0;
115
    }
116
    chomp($envfrom);
117
    # Clean up input (for security, if you use files/dirs)
118
    $envfrom =~ s#/#-#g;
119
    if (not $envfrom)
120
    {
121
        $envfrom="<>";
122
        return 0 if (not $option{'greylistnullfrom'});
123
    }
124
 
125
    if (not $rcptto = $permsgstatus->get($option{'rcpttohdr'}))
126
    {
127
        warn "Couldn't get Rcpt To header $option{'rcpttohdr'} for message $mesgid, skipping greylisting call\n";
128
        return 0;
129
    }
130
    chomp($rcptto);
131
    # Clean up input (for security, if you use files/dirs)
132
    $rcptto =~ s#/#-#g;
133
    @rcptto = split(/, /, $rcptto);
134
 
135
 
136
    umask 0007;
137
 
138
    foreach $rcptto (@rcptto)
139
    {
140
        # The dir method is easy to fiddle with and expire records in (with
141
        # a find | rm) but it's probably more I/O extensive than a real DB
142
        # and suffers from directory size problems if a specific IP is sending
143
        # generating tens of thousands of tuplets. -- Marc
144
        # That said, I prefer formats I can easily tinker with, and not having
145
        # to worry about buggy locking and so forth
146
 
147
        if ($option{'method'} eq "dir")
148
        {
149
            my $tmpvar;
150
 
151
            # The clean strings are hardcoded because it's hard to do a variable
152
            # substitution within a tr (and using the eval solution is too
153
            # resource expensive)
154
            # envfrom could be cleaned outside of the loop, but the other method 
155
            # options might now want that
156
            $envfrom =~ tr/!#%()*+,-.0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~/_/c;
157
            # clean variables to run properly under -T
158
            $envfrom =~ /(.+)/;
159
            $tmpvar = ($1 or "");
160
            # work around bug in perl untaint in perl 5.8
161
            $envfrom=undef;
162
            $envfrom=$tmpvar;
163
            $rcptto  =~ tr/!#%()*+,-.0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~/_/c;
164
            $rcptto =~ /(.+)/;
165
            $tmpvar = ($1 or "");
166
            $rcptto=undef;
167
            $rcptto=$tmpvar;
168
 
169
            die "greylist option dir not passed, even though method was set to dir" unless ($option{'dir'});
170
 
171
            # connectip is supposed to be untainted now, but I was still getting
172
            # some insecure dependecy error messages sometimes (perl 5.8 problem apparently)
173
            $connectip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/;
174
            my ($ipbyte1, $ipbyte2, $ipbyte3, $ipbyte4) = ($1, $2, $3, $4);
175
            my $ipdir1 = "$option{'dir'}/$ipbyte1";
176
            my $ipdir2 = "$ipdir1/$ipbyte2";
177
            my $ipdir3 = "$ipdir2/$ipbyte3";
178
            my $ipdir4;
179
            my $tupletdir;
180
 
181
            $ipdir4 = "$ipdir3";
182
            $ipdir4 .= "/$ipbyte4" if ($option{'greylistfourthbyte'});
183
            $tupletdir = "$ipdir4/$envfrom";
184
 
185
            $tuplet = "$tupletdir/$rcptto";
186
 
187
            # make directory whether it's there or not (faster than test and set)
188
            mkdir $ipdir1;
189
            mkdir $ipdir2;
190
            mkdir $ipdir3;
191
            mkdir $ipdir4;
192
            mkdir $tupletdir;
193
 
194
            if (not -e $tuplet)
195
            {
196
                # If the tuplets aren't there, we create them and continue in
197
                # case there are other ones (one of them might be whitelisted
198
                # already)
199
                $err="creating $tuplet";
200
                open (TUPLET, ">$tuplet") or goto greylisterror;
201
                print TUPLET time."\n";
202
                print TUPLET "Status: Greylisted\n";
203
                print TUPLET "Last Message-Id: $mesgid\n";
204
                print TUPLET "Whitelisted Count: 0\n";
205
                print TUPLET "Query Count: 1\n";
206
                print TUPLET "SA Score: $sascore\n";
207
                $err="closing first-written $tuplet";
208
                close TUPLET or goto greylisterror;
209
            }
210
            else
211
            {
212
                my $time;
213
                my $status;
214
                my $whitelistcount;
215
                my $querycount;
216
 
217
                # Take into account race condition of expiring deletes and us
218
                # running
219
                $err="reading $tuplet";
220
                open (TUPLET, "<$tuplet") or goto greylisterror;
221
                $err="Couldn't read time";
222
                defined ($time=<TUPLET>) or goto greylisterror;
223
                chomp ($time);
224
 
225
                $err="Couldn't read status";
226
                defined ($status=<TUPLET>) or goto greylisterror;
227
                chomp ($status);
228
                $err="Couldn't extract Status from $status";
229
                $status =~ s/^Status: // or goto greylisterror;
230
 
231
                # Skip Mesg-Id
232
                $err="Couldn't skip Mesg-Id";
233
                defined ($_=<TUPLET>) or goto greylisterror;
234
 
235
                $err="Couldn't read whitelistcount";
236
                defined ($whitelistcount=<TUPLET>) or goto greylisterror;
237
                chomp ($whitelistcount);
238
                $err="Couldn't extract Whitelisted Count from $whitelistcount";
239
                $whitelistcount =~ s/^Whitelisted Count: // or goto greylisterror;
240
 
241
                $err="Couldn't read querycount";
242
                defined ($querycount=<TUPLET>) or goto greylisterror;
243
                chomp ($querycount);
244
                $err="Couldn't extract Query Count from $querycount";
245
                $querycount =~ s/^Query Count: // or goto greylisterror;
246
                close (TUPLET);
247
 
248
                $querycount++;
249
                if ((time - $time) > $option{'greylistsecs'})
250
                {
251
                    $status="Whitelisted";
252
                    $whitelistcount++;
253
                }
254
 
255
                $err="re-writing $tuplet";
256
                open (TUPLET, ">$tuplet") or goto greylisterror;
257
                print TUPLET "$time\n";
258
                print TUPLET "Status: $status\n";
259
                print TUPLET "Last Message-Id: $mesgid\n";
260
                print TUPLET "Whitelisted Count: $whitelistcount\n";
261
                print TUPLET "Query Count: $querycount\n";
262
                print TUPLET "SA Score: $sascore\n";
263
                $err="closing re-written $tuplet";
264
                close TUPLET or goto greylisterror;
265
 
266
                # We continue processing the other recipients, to setup or
267
                # update their counters
268
                if ($status eq "Whitelisted")
269
                {
270
                    $iswhitelisted=1;
271
                }
272
            }
273
        }
274
        elsif ($option{'method'} eq "file")
275
        {
276
            warn "codeme (file greylisting)\n";
277
        }
278
        elsif ($option{'method'} eq "db")
279
        {
280
            warn "codeme (db greylisting)\n";
281
        }
282
    }
283
 
284
    Mail::SpamAssassin::Plugin::dbg("GREYLISTING: computed greylisting on tuplet, saved info in $tuplet and whitelist status is $iswhitelisted");
285
    return $iswhitelisted;
286
 
287
    greylisterror:
288
    warn "Reached greylisterror: $err / $!";
289
    # delete tuplet since it apparently had issues but don't check for errors
290
    # in case it was a permission denied on write
291
    unlink ($tuplet);
292
    return $iswhitelisted;
293
}
294
 
295
 
296
1;