Skip to content
This repository

Dspam #26

Closed
wants to merge 3 commits into from

1 participant

Matt Simerson
Matt Simerson
Collaborator

dspam: work around DESTROY bug

worked around qpsmtpd bug that DESTROYs the parent when a forked child exits
expanded learning support. Now learns from spamassassin, naughty, and karma (both good and bad).
better logging for 'reject agree'
improved POD
abstracted new subs: log_and_return, attach_headers

added some commits June 03, 2012
Matt Simerson reject and reject_type handling for plugins cbd1be7
Matt Simerson dspam: work around DESTROY bug
worked around qpsmtpd bug that DESTROYs the parent when a forked child exits
expanded learning support. Now learns from spamassassin, naughty, and karma (both good and bad).
better logging for 'reject agree'
improved POD
abstracted new subs: log_and_return, attach_headers
4f00da4
Matt Simerson dspam: replaced header->replace with ->delete & ->add
the replace feature doesn't insert the header in the correct position.

added POD note for dspam maintenance script
d156262
Matt Simerson msimerson closed this August 05, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 3 unique commits by 1 author.

Jun 04, 2012
Matt Simerson reject and reject_type handling for plugins cbd1be7
Matt Simerson dspam: work around DESTROY bug
worked around qpsmtpd bug that DESTROYs the parent when a forked child exits
expanded learning support. Now learns from spamassassin, naughty, and karma (both good and bad).
better logging for 'reject agree'
improved POD
abstracted new subs: log_and_return, attach_headers
4f00da4
Jun 06, 2012
Matt Simerson dspam: replaced header->replace with ->delete & ->add
the replace feature doesn't insert the header in the correct position.

added POD note for dspam maintenance script
d156262
This page is out of date. Refresh to see the latest.
45  docs/config.pod
Source Rendered
@@ -89,7 +89,7 @@ connection before any auth succeeds, defaults to C<0>.
89 89
 
90 90
 =back
91 91
 
92  
-=head2 Plugin settings
  92
+=head2 Plugin settings files
93 93
 
94 94
 =over 4
95 95
 
@@ -153,5 +153,48 @@ only currenlty.
153 153
 
154 154
 =back
155 155
 
  156
+=head2 Plugin settings arguments
  157
+
  158
+These are arguments that can be set on the config/plugins line, after the name
  159
+of the plugin. These config options are available to all plugins.
  160
+
  161
+=over 4
  162
+
  163
+=item loglevel
  164
+
  165
+Adjust the quantity of logging for the plugin. See docs/logging.pod
  166
+
  167
+=item reject
  168
+
  169
+ plugin reject [ 0 | 1 | naughty ]
  170
+
  171
+Should the plugin reject mail?
  172
+
  173
+The special 'naughty' case will mark the connection as a naughty. Most plugins
  174
+skip processing naughty connections. Filtering plugins can learn from them.
  175
+Naughty connections are terminated up by the B<naughty> plugin.
  176
+
  177
+Plugins that use $self->get_reject() or $self->get_reject_type() will
  178
+automatically honor this setting.
  179
+
  180
+=item reject_type
  181
+
  182
+ plugin reject_type [ perm | temp | disconnect | temp_disconnect ]
  183
+
  184
+Default: perm
  185
+
  186
+Values with temp in the name return a 4xx code and the others return a 5xx
  187
+code.
  188
+
  189
+The I<reject_type> argument and the corresponding get_reject_type() method
  190
+provides a standard way for plugins to automatically return the selected
  191
+rejection type, as chosen by the config setting, the plugin author, or the
  192
+get_reject_type() method.
  193
+
  194
+Plugins that are updated to use the $self->get_reject() or
  195
+$self->get_reject_type() methods will automatically honor this setting.
  196
+
  197
+=back
  198
+
156 199
 =cut
157 200
 
34  lib/Qpsmtpd/Plugin.pm
@@ -210,6 +210,40 @@ sub compile {
210 210
     die "eval $@" if $@;
211 211
 }
212 212
 
  213
+sub get_reject {
  214
+    my $self = shift;
  215
+    my $message = shift || "why didn't you pass an error message?";
  216
+
  217
+    my $reject = $self->{_args}{reject};
  218
+    if ( defined $reject && ! $reject ) {
  219
+        $self->log(LOGINFO, 'fail, reject disabled');
  220
+        return DECLINED;
  221
+    };
  222
+
  223
+    # the naughty plugin will reject later
  224
+    if ( $reject eq 'naughty' ) {
  225
+        $self->log(LOGINFO, 'fail, NAUGHTY');
  226
+        $self->connection->notes('naughty', $message);
  227
+        return (DECLINED);
  228
+    };
  229
+
  230
+    # they asked for reject, we give them reject
  231
+    $self->log(LOGINFO, 'fail');
  232
+    return ( $self->get_reject_type(), $message);
  233
+};
  234
+
  235
+sub get_reject_type {
  236
+    my $self = shift;
  237
+    my $default = shift || DENY;
  238
+    my $deny = $self->{_args}{reject_type} or return $default;
  239
+
  240
+    return $deny =~ /^(temp|soft)$/i  ? DENYSOFT
  241
+         : $deny =~ /^(perm|hard)$/i  ? DENY
  242
+         : $deny eq 'disconnect'      ? DENY_DISCONNECT
  243
+         : $deny eq 'temp_disconnect' ? DENYSOFT_DISCONNECT
  244
+         : $default;
  245
+};
  246
+
213 247
 sub _register_standard_hooks {
214 248
   my ($plugin, $qp) = @_;
215 249
 
335  plugins/dspam
@@ -6,15 +6,15 @@ dspam - dspam integration for qpsmtpd
6 6
 
7 7
 =head1 DESCRIPTION
8 8
 
9  
-qpsmtpd plugin that uses dspam to classify messages. Can use SpamAssassin to
10  
-train dspam.
  9
+Uses dspam to classify messages. Use B<spamassassin>, B<karma>, and B<naughty>
  10
+to train dspam.
11 11
 
12 12
 Adds the X-DSPAM-Result and X-DSPAM-Signature headers to messages. The latter is essential for
13 13
 training dspam and the former is useful to MDAs, MUAs, and humans.
14 14
 
15  
-Adds a transaction note to the qpsmtpd transaction. The notes is a hashref
  15
+Adds a transaction note to the qpsmtpd transaction. The note is a hashref
16 16
 with at least the 'class' field (Spam,Innocent,Whitelisted). It will normally
17  
-contain a probability and confidence ratings as well.
  17
+contain a probability and confidence rating.
18 18
 
19 19
 =head1 TRAINING DSPAM
20 20
 
@@ -30,7 +30,7 @@ dspam as follows:
30 30
 
31 31
 =item learn from SpamAssassin
32 32
 
33  
-See the docs on the learn_from_sa feature in the CONFIG section.
  33
+See the SPAMASSASSIN section.
34 34
 
35 35
 =item periodic training
36 36
 
@@ -54,41 +54,58 @@ messages are moved to/from the Spam folder.
54 54
 =head2 dspam_bin
55 55
 
56 56
 The path to the dspam binary. If yours is installed somewhere other
57  
-than /usr/local/bin/dspam, you'll need to set this.
  57
+than /usr/local/bin/dspam, set this.
58 58
 
59  
-=head2 learn_from_sa
60  
-
61  
-Dspam can be trained by SpamAssassin. This relationship between them requires
62  
-attention to several important details:
  59
+=head2 autolearn [ naughty | karma | spamassassin | any ]
63 60
 
64 61
 =over 4
65 62
 
66  
-=item 1
  63
+=item naughty
67 64
 
68  
-dspam must be listed B<after> spamassassin in the config/plugins file.
69  
-Because SA runs first, I crank the SA reject_threshold up above 100 so that
70  
-all spam messages will be used to train dspam.
  65
+learn naughty messages as spam (see plugins/naughty)
71 66
 
72  
-Once dspam is trained and errors are rare, I plan to run dspam first and
73  
-reduce the SA load.
  67
+=item karma
74 68
 
75  
-=item 2
  69
+learn messages with negative karma as spam (see plugins/karma)
  70
+
  71
+=item spamassassin
  72
+
  73
+learn from spamassassins messages with autolearn=(ham|spam)
  74
+
  75
+=item any
  76
+
  77
+all of the above, and any future tests too!
  78
+
  79
+=back
  80
+
  81
+=head2 reject
  82
+
  83
+Set to a floating point value between 0 and 1.00 where 0 is no confidence
  84
+and 1.0 is 100% confidence.
  85
+
  86
+If dspam's confidence is greater than or equal to this threshold, the
  87
+message will be rejected. The default is 1.00.
  88
+
  89
+  dspam reject .95
  90
+
  91
+To only reject mail if dspam and spamassassin both think the message is spam,
  92
+set I<reject agree>.
  93
+
  94
+=head2 reject_type
  95
+
  96
+ reject_type [ perm | temp | disconnect ]
76 97
 
77  
-Autolearn must be enabled and configured in SpamAssassin. SA autolearn
78  
-preferences will determine whether a message is learned as spam or innocent
79  
-by dspam. The settings to pay careful attention to in your SA local.cf file
80  
-are bayes_auto_learn_threshold_spam and bayes_auto_learn_threshold_nonspam.
81  
-Make sure they are both set to conservative values that are certain to
82  
-yield no false positives.
  98
+By default, rejects are permanent (5xx). Set I<reject_type temp> to
  99
+defer mail instead of rejecting it.
83 100
 
84  
-If you are using learn_from_sa and reject, then messages that exceed the SA
85  
-threshholds will cause dspam to reject them. Again I say, make sure them SA
86  
-autolearn threshholds are set high enough to avoid false positives.
  101
+Set I<reject_type disconnect> if you'd prefer to immediately disconnect
  102
+the connection when a spam is encountered. This prevents the remote server
  103
+from issuing a reset and attempting numerous times in a single connection.
87 104
 
88  
-=item 3
  105
+=head1 dspam.conf
89 106
 
90  
-dspam must be configured and working properly. I have modified the following
91  
-dspam values on my system:
  107
+dspam must be configured and working properly. I had to modify the following
  108
+settings on my system:
92 109
 
93 110
 =over 4
94 111
 
@@ -117,26 +134,47 @@ only supports storing the signature in the headers. If you want to train dspam
117 134
 after delivery (ie, users moving messages to/from spam folders), then the
118 135
 dspam signature must be in the headers.
119 136
 
120  
-When using the dspam MySQL backend, use InnoDB tables. Dspam training
  137
+When using the dspam MySQL backend, use InnoDB tables. DSPAM training
121 138
 is dramatically slowed by MyISAM table locks and dspam requires lots
122 139
 of training. InnoDB has row level locking and updates are much faster.
123 140
 
124  
-=back
  141
+=head1 DSPAM periodic maintenance
125 142
 
126  
-=head2 reject
  143
+Install this cron job to clean up your DSPAM database.
127 144
 
128  
-Set to a floating point value between 0 and 1.00 where 0 is no confidence
129  
-and 1.0 is 100% confidence.
  145
+http://dspam.git.sourceforge.net/git/gitweb.cgi?p=dspam/dspam;a=tree;f=contrib/dspam_maintenance;hb=HEAD
130 146
 
131  
-If dspam's confidence is greater than or equal to this threshold, the
132  
-message will be rejected. The default is 1.00.
133 147
 
134  
-=head2 reject_type
135 148
 
136  
- reject_type [ temp | perm ]
  149
+=head1 SPAMASSASSIN
137 150
 
138  
-By default, rejects are permanent (5xx). Set this to temp if you want to
139  
-defer mail instead of rejecting it with dspam.
  151
+DSPAM can be trained by SpamAssassin. This relationship between them requires
  152
+attention to several important details:
  153
+
  154
+=over 4
  155
+
  156
+=item 1
  157
+
  158
+dspam must be listed B<after> spamassassin in the config/plugins file.
  159
+Because SA runs first, I set the SA reject_threshold up above 100 so that
  160
+all spam messages will be used to train dspam.
  161
+
  162
+Once dspam is trained and errors are rare, I plan to run dspam first and
  163
+reduce the SA load.
  164
+
  165
+=item 2
  166
+
  167
+Autolearn must be enabled and configured in SpamAssassin. SA autolearn will
  168
+determine if a message is learned by dspam. The settings to pay careful
  169
+attention to in your SA local.cf file are I<bayes_auto_learn_threshold_spam>
  170
+and I<bayes_auto_learn_threshold_nonspam>. Make sure they are set to
  171
+conservative values that will yield no false positives.
  172
+
  173
+If you are using I<autolearn spamassassin> and reject, messages that exceed
  174
+the SA threshholds will cause dspam to reject them. Again I say, make sure
  175
+the SA autolearn threshholds are set high enough to avoid false positives.
  176
+
  177
+=back
140 178
 
141 179
 =head1 MULTIPLE RECIPIENT BEHAVIOR
142 180
 
@@ -151,9 +189,12 @@ ie, (Trust smtpd).
151 189
 
152 190
 =head1 CHANGES
153 191
 
  192
+2012-06 - Matt Simerson - added karma & naughty learning support
  193
+                        - worked around the DESTROY bug in dspam_process
  194
+
154 195
 =head1 AUTHOR
155 196
 
156  
- Matt Simerson - 2012
  197
+2012 - Matt Simerson
157 198
 
158 199
 =cut
159 200
 
@@ -166,49 +207,42 @@ use IO::Handle;
166 207
 use Socket qw(:DEFAULT :crlf);
167 208
 
168 209
 sub register {
169  
-    my ($self, $qp, %args) = @_;
  210
+    my ($self, $qp) = shift, shift;
170 211
 
171 212
     $self->log(LOGERROR, "Bad parameters for the dspam plugin") if @_ % 2;
172 213
 
173  
-    $self->{_args} = { %args };
174  
-    $self->{_args}{reject} = defined $args{reject} ? $args{reject} : 1;
175  
-    $self->{_args}{reject_type} = $args{reject_type} || 'perm';
  214
+    $self->{_args} = { @_ };
  215
+    $self->{_args}{reject} = 1 if ! defined $self->{_args}{reject};
  216
+    $self->{_args}{reject_type} ||= 'perm';
176 217
 
177  
-    $self->register_hook('data_post', 'dspam_reject');
  218
+    $self->register_hook('data_post', 'data_post_handler');
178 219
 }
179 220
 
180  
-sub hook_data_post {
181  
-    my ($self, $transaction) = @_;
  221
+sub data_post_handler {
  222
+    my $self = shift;
  223
+    my $transaction = shift || $self->qp->transaction;
  224
+
  225
+    $self->autolearn( $transaction );
  226
+    return (DECLINED) if $self->is_immune();
182 227
 
183  
-    $self->log(LOGDEBUG, "check_dspam");
184 228
     if ( $transaction->data_size > 500_000 ) {
185  
-        $self->log(LOGINFO, "skip: message too large (" . $transaction->data_size . ")" );
  229
+        $self->log(LOGINFO, "skip, too big (" . $transaction->data_size . ")" );
186 230
         return (DECLINED);
187 231
     };
188 232
 
189 233
     my $username = $self->select_username( $transaction );
190  
-    my $message  = $self->assemble_message($transaction);
191 234
     my $filtercmd = $self->get_filter_cmd( $transaction, $username );
192 235
     $self->log(LOGDEBUG, $filtercmd);
193 236
 
194  
-    my $response = $self->dspam_process( $filtercmd, $message );
  237
+    my $response = $self->dspam_process( $filtercmd, $transaction );
195 238
     if ( ! $response ) {
196  
-        $self->log(LOGWARN, "skip: no response from dspam. Check logs for errors.");
  239
+        $self->log(LOGWARN, "skip, no dspam response. Check logs for errors.");
197 240
         return (DECLINED);
198 241
     };
199 242
 
200  
-    # X-DSPAM-Result: user@example.com; result="Spam"; class="Spam"; probability=1.0000; confidence=1.00; signature=N/A
201  
-    # X-DSPAM-Result: smtpd; result="Innocent"; class="Innocent"; probability=0.0023; confidence=1.00; signature=4f8dae6a446008399211546
202  
-    my ($result,$prob,$conf,$sig) = $response =~ /result=\"(Spam|Innocent)\";.*?probability=([\d\.]+); confidence=([\d\.]+); signature=(.*)/;
203  
-    my $header_str = "$result, probability=$prob, confidence=$conf";
204  
-    $self->log(LOGDEBUG, $header_str);
205  
-    $transaction->header->replace('X-DSPAM-Result', $header_str, 0);
206  
-
207  
-    # the signature header is required if you intend to train dspam later.
208  
-    # In dspam.conf, set: Preference "signatureLocation=headers"
209  
-    $transaction->header->add('X-DSPAM-Signature', $sig, 0);
  243
+    $self->attach_headers( $response, $transaction );
210 244
 
211  
-    return (DECLINED);
  245
+    return $self->log_and_return( $transaction );
212 246
 };
213 247
 
214 248
 sub select_username {
@@ -243,18 +277,23 @@ sub assemble_message {
243 277
 };
244 278
 
245 279
 sub dspam_process {
246  
-    my ( $self, $filtercmd, $message ) = @_;
247  
-
248  
-    #return $self->dspam_process_open2( $filtercmd, $message );
249  
-
250  
-    my ($in_fh, $out_fh);
251  
-    if (! open($in_fh, '-|')) {
252  
-        open($out_fh, "|$filtercmd") or die "Can't run $filtercmd: $!\n";
  280
+    my ( $self, $filtercmd, $transaction ) = @_;
  281
+
  282
+    return $self->dspam_process_backticks( $filtercmd );
  283
+    #return $self->dspam_process_open2( $filtercmd, $transaction );
  284
+
  285
+    # yucky. This method (which forks) exercises a bug in qpsmtpd. When the
  286
+    # child exits, the Transaction::DESTROY method is called, which deletes
  287
+    # the spooled file from disk. The contents of $self->qp->transaction
  288
+    # needed to spool it again are also destroyed. Don't use this.
  289
+    my $message  = $self->assemble_message( $transaction );
  290
+    my $in_fh;
  291
+    if (! open($in_fh, '-|')) {    # forks child for writing
  292
+        open(my $out_fh, "|$filtercmd") or die "Can't run $filtercmd: $!\n";
253 293
         print $out_fh $message;
254 294
         close $out_fh;
255 295
         exit(0);
256 296
     };
257  
-    #my $response = join('', <$in_fh>);
258 297
     my $response = <$in_fh>;
259 298
     close $in_fh;
260 299
     chomp $response;
@@ -262,8 +301,20 @@ sub dspam_process {
262 301
     return $response;
263 302
 };
264 303
 
  304
+sub dspam_process_backticks {
  305
+    my ( $self, $filtercmd ) = @_;
  306
+
  307
+    my $filename = $self->qp->transaction->body_filename;
  308
+    #my $response = `cat $filename | $filtercmd`; chomp $response;
  309
+    my $response = `$filtercmd < $filename`; chomp $response;
  310
+    $self->log(LOGDEBUG, $response);
  311
+    return $response;
  312
+};
  313
+
265 314
 sub dspam_process_open2 {
266  
-    my ( $self, $filtercmd, $message ) = @_;
  315
+    my ( $self, $filtercmd, $transaction ) = @_;
  316
+
  317
+    my $message  = $self->assemble_message( $transaction );
267 318
 
268 319
 # not sure why, but this is not as reliable as I'd like. What's a dspam
269 320
 # error -5 mean anyway?
@@ -281,31 +332,33 @@ sub dspam_process_open2 {
281 332
     return $response;
282 333
 };
283 334
 
284  
-sub dspam_reject {
285  
-    my ($self, $transaction) = @_;
  335
+sub log_and_return {
  336
+    my $self = shift;
  337
+    my $transaction = shift || $self->qp->transaction;
286 338
 
287 339
     my $d = $self->get_dspam_results( $transaction ) or return DECLINED;
288 340
 
289 341
     if ( ! $d->{class} ) {
290  
-        $self->log(LOGWARN, "skip: no dspam class detected");
  342
+        $self->log(LOGWARN, "skip, no dspam class detected");
291 343
         return DECLINED;
292 344
     };
293 345
 
294 346
     my $status = "$d->{class}, $d->{confidence} c.";
295 347
     my $reject = $self->{_args}{reject} or do {
296  
-        $self->log(LOGINFO, "skip: reject disabled ($status)");
  348
+        $self->log(LOGINFO, "skip, reject disabled ($status)");
297 349
         return DECLINED;
298 350
     };
299 351
 
300 352
     if ( $reject eq 'agree' ) {
301  
-        return $self->dspam_reject_agree( $transaction, $d );
  353
+        return $self->reject_agree( $transaction, $d );
302 354
     };
  355
+
303 356
     if ( $d->{class} eq 'Innocent' ) {
304  
-        $self->log(LOGINFO, "pass: $status");
  357
+        $self->log(LOGINFO, "pass, $status");
305 358
         return DECLINED;
306 359
     };
307 360
     if ( $self->qp->connection->relay_client ) {
308  
-        $self->log(LOGINFO, "skip: allowing spam, user authenticated ($status)");
  361
+        $self->log(LOGINFO, "skip, allowing spam, user authenticated ($status)");
309 362
         return DECLINED;
310 363
     };
311 364
     if ( $d->{probability} <= $reject ) {
@@ -313,17 +366,17 @@ sub dspam_reject {
313 366
         return DECLINED;
314 367
     };
315 368
     if ( $d->{confidence} != 1 ) {
316  
-        $self->log(LOGINFO, "pass: $d->{class} confidence is too low ($d->{confidence})");
  369
+        $self->log(LOGINFO, "pass, $d->{class} confidence is too low ($d->{confidence})");
317 370
         return DECLINED;
318 371
     };
319 372
 
320 373
     # dspam is more than $reject percent sure this message is spam
321  
-    $self->log(LOGINFO, "fail: $d->{class}, ($d->{confidence} confident)");
322  
-    my $deny = $self->{_args}{reject_type} eq 'temp' ? DENYSOFT : DENY;
323  
-    return Qpsmtpd::DSN->media_unsupported($deny,'dspam says, no spam please');
  374
+    $self->log(LOGINFO, "fail, $d->{class}, ($d->{confidence} confident)");
  375
+    my $deny = $self->get_reject_type();
  376
+    return Qpsmtpd::DSN->media_unsupported($deny, 'dspam says, no spam please');
324 377
 }
325 378
 
326  
-sub dspam_reject_agree {
  379
+sub reject_agree {
327 380
     my ($self, $transaction, $d ) = @_;
328 381
 
329 382
     my $sa = $transaction->notes('spamassassin' );
@@ -331,21 +384,44 @@ sub dspam_reject_agree {
331 384
     my $status = "$d->{class}, $d->{confidence} c";
332 385
 
333 386
     if ( ! $sa->{is_spam} ) {
334  
-        $self->log(LOGINFO, "pass: cannot agree, SA results missing ($status)");
  387
+        $self->log(LOGINFO, "pass, cannot agree, SA results missing ($status)");
335 388
         return DECLINED;
336 389
     };
337 390
 
338  
-    if ( $d->{class} eq 'Spam' && $sa->{is_spam} eq 'Yes' ) {
339  
-        $self->log(LOGINFO, "fail: agree, $status");
340  
-        return Qpsmtpd::DSN->media_unsupported(DENY,'we agree, no spam please');
  391
+    if ( $d->{class} eq 'Spam' ) {
  392
+        if ( $sa->{is_spam} eq 'Yes' ) {
  393
+            if ( defined $self->connection->notes('karma') ) {
  394
+                $self->connection->notes('karma', $self->connection->notes('karma') - 2);
  395
+            };
  396
+            $self->log(LOGINFO, "fail, agree, $status");
  397
+            my $reject = $self->get_reject_type();
  398
+            return ($reject, 'we agree, no spam please');
  399
+        };
  400
+
  401
+        $self->log(LOGINFO, "fail, disagree, $status");
  402
+        return DECLINED;
341 403
     };
342 404
 
343  
-    $self->log(LOGINFO, "pass: agree, $status");
  405
+    if ( $d->{class} eq 'Innocent' ) {
  406
+        if ( $sa->{is_spam} eq 'No' ) {
  407
+            if ( $d->{confidence} > .9 ) {
  408
+                if ( defined $self->connection->notes('karma') ) {
  409
+                    $self->connection->notes('karma', $self->connection->notes('karma') + 2);
  410
+                };
  411
+            };
  412
+            $self->log(LOGINFO, "pass, agree, $status");
  413
+            return DECLINED;
  414
+        };
  415
+        $self->log(LOGINFO, "pass, disagree, $status");
  416
+    };
  417
+
  418
+    $self->log(LOGINFO, "pass, other $status");
344 419
     return DECLINED;
345 420
 };
346 421
 
347 422
 sub get_dspam_results {
348  
-    my ( $self, $transaction ) = @_;
  423
+    my $self = shift;
  424
+    my $transaction = shift || $self->qp->transaction;
349 425
 
350 426
     if ( $transaction->notes('dspam') ) {
351 427
         return $transaction->notes('dspam');
@@ -379,19 +455,22 @@ sub get_filter_cmd {
379 455
 
380 456
     my $dspam_bin = $self->{_args}{dspam_bin} || '/usr/local/bin/dspam';
381 457
     my $default = "$dspam_bin --user $user --mode=tum --process --deliver=summary --stdout";
382  
-    my $min_score = $self->{_args}{learn_from_sa} or return $default;
383 458
 
384  
-    #$self->log(LOGDEBUG, "attempting to learn from SA");
  459
+    my $learn = $self->{_args}{autolearn} or return $default;
  460
+    return $default if ( $learn ne 'spamassassin' && $learn ne 'any' );
385 461
 
386  
-    my $sa = $transaction->notes('spamassassin' );
387  
-    return $default if ! $sa || ! $sa->{is_spam};
  462
+    $self->log(LOGDEBUG, "attempting to learn from SA");
388 463
 
389  
-    if ( $sa->{is_spam} eq 'Yes' && $sa->{score} < $min_score ) {
390  
-        $self->log(LOGNOTICE, "SA score $sa->{score} < $min_score, skip autolearn");
  464
+    my $sa = $transaction->notes('spamassassin' );
  465
+    if ( ! $sa || ! $sa->{is_spam} ) {
  466
+        $self->log(LOGERROR, "SA results missing");
391 467
         return $default;
392 468
     };
393 469
 
394  
-    return $default if ! $sa->{autolearn};
  470
+    if ( ! $sa->{autolearn} ) {
  471
+        $self->log(LOGERROR, "SA autolearn unset");
  472
+        return $default;
  473
+    };
395 474
 
396 475
     if ( $sa->{is_spam} eq 'Yes' && $sa->{autolearn} eq 'spam' ) {
397 476
         return "$dspam_bin --user $user --mode=tum --source=corpus --class=spam --deliver=summary --stdout";
@@ -403,4 +482,64 @@ sub get_filter_cmd {
403 482
     return $default;
404 483
 };
405 484
 
  485
+sub attach_headers {
  486
+    my ($self, $response, $transaction) = @_;
  487
+    $transaction ||= $self->qp->transaction;
  488
+
  489
+    # X-DSPAM-Result: user@example.com; result="Spam"; class="Spam"; probability=1.0000; confidence=1.00; signature=N/A
  490
+    # X-DSPAM-Result: smtpd; result="Innocent"; class="Innocent"; probability=0.0023; confidence=1.00; signature=4f8dae6a446008399211546
  491
+    my ($result,$prob,$conf,$sig) = $response =~ /result=\"(Spam|Innocent)\";.*?probability=([\d\.]+); confidence=([\d\.]+); signature=(.*)/;
  492
+    my $header_str = "$result, probability=$prob, confidence=$conf";
  493
+    $self->log(LOGDEBUG, $header_str);
  494
+    my $name = 'X-DSPAM-Result';
  495
+    $transaction->header->delete($name) if $transaction->header->get($name);
  496
+    $transaction->header->add($name, $header_str, 0);
  497
+
  498
+    # the signature header is required if you intend to train dspam later.
  499
+    # In dspam.conf, set: Preference "signatureLocation=headers"
  500
+    $transaction->header->add('X-DSPAM-Signature', $sig, 0);
  501
+};
  502
+
  503
+sub learn_as_ham {
  504
+    my $self = shift;
  505
+    my $transaction = shift;
406 506
 
  507
+    my $user = $self->select_username( $transaction );
  508
+    my $dspam_bin = $self->{_args}{dspam_bin} || '/usr/local/bin/dspam';
  509
+    my $cmd = "$dspam_bin --user $user --mode=tum --source=corpus --class=innocent --deliver=summary --stdout";
  510
+    $self->dspam_process( $cmd, $transaction );
  511
+};
  512
+
  513
+sub learn_as_spam {
  514
+    my $self = shift;
  515
+    my $transaction = shift;
  516
+
  517
+    my $user = $self->select_username( $transaction );
  518
+    my $dspam_bin = $self->{_args}{dspam_bin} || '/usr/local/bin/dspam';
  519
+    my $cmd = "$dspam_bin --user $user --mode=tum --source=corpus --class=spam --deliver=summary --stdout";
  520
+    $self->dspam_process( $cmd, $transaction );
  521
+};
  522
+
  523
+sub autolearn {
  524
+    my ( $self, $transaction ) = @_;
  525
+
  526
+    my $learn = $self->{_args}{autolearn} or return;
  527
+
  528
+    if ( $learn eq 'naughty' || $learn eq 'any' ) {
  529
+        if ( $self->connection->notes('naughty') ) {
  530
+            $self->log(LOGINFO, "training naughty as spam");
  531
+            $self->learn_as_spam( $transaction );
  532
+        };
  533
+    };
  534
+    if ( $learn eq 'karma' || $learn eq 'any' ) {
  535
+        my $karma = $self->connection->notes('karma');
  536
+        if ( defined $karma && $karma <= -1 ) {
  537
+            $self->log(LOGINFO, "training poor karma as spam");
  538
+            $self->learn_as_spam( $transaction );
  539
+        };
  540
+        if ( defined $karma && $karma >= 1 ) {
  541
+            $self->log(LOGINFO, "training good karma as ham");
  542
+            $self->learn_as_ham( $transaction );
  543
+        };
  544
+    };
  545
+};
56  t/plugin_tests/dspam
@@ -13,48 +13,49 @@ sub register_tests {
13 13
 
14 14
     $self->register_test('test_get_filter_cmd', 5);
15 15
     $self->register_test('test_get_dspam_results', 6);
16  
-    $self->register_test('test_dspam_reject', 6);
  16
+    $self->register_test('test_log_and_return', 6);
  17
+    $self->register_test('test_reject_type', 3);
17 18
 }
18 19
 
19  
-sub test_dspam_reject {
  20
+sub test_log_and_return {
20 21
     my $self = shift;
21 22
 
22 23
     my $transaction = $self->qp->transaction;
23 24
 
24 25
     # reject not set
25 26
     $transaction->notes('dspam', { class=> 'Spam', probability => .99, confidence=>1 } );
26  
-    ($r) = $self->dspam_reject( $transaction );
27  
-    cmp_ok( $r, '==', DECLINED, "dspam_reject ($r)");
  27
+    ($r) = $self->log_and_return( $transaction );
  28
+    cmp_ok( $r, '==', DECLINED, "($r)");
28 29
 
29 30
     # reject exceeded
30  
-    $self->{_args}->{reject} = .95;
  31
+    $self->{_args}{reject} = .95;
31 32
     $transaction->notes('dspam', { class=> 'Spam', probability => .99, confidence=>1 } );
32  
-    ($r) = $self->dspam_reject( $transaction );
33  
-    cmp_ok( $r, '==', DENY, "dspam_reject ($r)");
  33
+    ($r) = $self->log_and_return( $transaction );
  34
+    cmp_ok( $r, '==', DENY, "($r)");
34 35
 
35 36
     # below reject threshold
36 37
     $transaction->notes('dspam', { class=> 'Spam', probability => .94, confidence=>1 } );
37  
-    ($r) = $self->dspam_reject( $transaction );
38  
-    cmp_ok( $r, '==', DECLINED, "dspam_reject ($r)");
  38
+    ($r) = $self->log_and_return( $transaction );
  39
+    cmp_ok( $r, '==', DECLINED, "($r)");
39 40
 
40 41
     # requires agreement
41  
-    $self->{_args}->{reject} = 'agree';
  42
+    $self->{_args}{reject} = 'agree';
42 43
     $transaction->notes('spamassassin', { is_spam => 'Yes', score => 25 } );
43 44
     $transaction->notes('dspam', { class=> 'Spam', probability => .90, confidence=>1 } );
44  
-    ($r) = $self->dspam_reject( $transaction );
45  
-    cmp_ok( $r, '==', DENY, "dspam_reject ($r)");
  45
+    ($r) = $self->log_and_return( $transaction );
  46
+    cmp_ok( $r, '==', DENY, "($r)");
46 47
 
47 48
     # requires agreement
48 49
     $transaction->notes('spamassassin', { is_spam => 'No', score => 15 } );
49 50
     $transaction->notes('dspam', { class=> 'Spam', probability => .96, confidence=>1 } );
50  
-    ($r) = $self->dspam_reject( $transaction );
51  
-    cmp_ok( $r, '==', DECLINED, "dspam_reject ($r)");
  51
+    ($r) = $self->log_and_return( $transaction );
  52
+    cmp_ok( $r, '==', DECLINED, "($r)");
52 53
 
53 54
     # requires agreement
54 55
     $transaction->notes('spamassassin', { is_spam => 'Yes', score => 10 } );
55 56
     $transaction->notes('dspam', { class=> 'Innocent', probability => .96, confidence=>1 } );
56  
-    ($r) = $self->dspam_reject( $transaction );
57  
-    cmp_ok( $r, '==', DECLINED, "dspam_reject ($r)");
  57
+    ($r) = $self->log_and_return( $transaction );
  58
+    cmp_ok( $r, '==', DECLINED, "($r)");
58 59
 };
59 60
 
60 61
 sub test_get_dspam_results {
@@ -77,7 +78,7 @@ sub test_get_dspam_results {
77 78
         $transaction->header->delete('X-DSPAM-Result');
78 79
         $transaction->header->add('X-DSPAM-Result', $header);
79 80
         my $r = $self->get_dspam_results($transaction);
80  
-        ok( ref $r, "get_dspam_results ($header)" );
  81
+        ok( ref $r, "r: ($header)" );
81 82
         #warn Data::Dumper::Dumper($r);
82 83
     };
83 84
 };
@@ -88,26 +89,39 @@ sub test_get_filter_cmd {
88 89
     my $transaction = $self->qp->transaction;
89 90
     my $dspam = "/usr/local/bin/dspam";
90 91
     $self->{_args}{dspam_bin} = $dspam;
  92
+    $self->{_args}{autolearn} = 'spamassassin';
91 93
 
92 94
     foreach my $user ( qw/ smtpd matt@example.com / ) {
93 95
         my $answer = "$dspam --user smtpd --mode=tum --process --deliver=summary --stdout";
94 96
         my $r = $self->get_filter_cmd($transaction, 'smtpd');
95  
-        cmp_ok( $r, 'eq', $answer, "get_filter_cmd $user" );
  97
+        cmp_ok( $r, 'eq', $answer, "$user" );
96 98
     };
97 99
 
98 100
     $transaction->notes('spamassassin', { is_spam => 'No', autolearn => 'ham' } );
99 101
     my $r = $self->get_filter_cmd($transaction, 'smtpd');
100 102
     cmp_ok( $r, 'eq', "$dspam --user smtpd --mode=tum --source=corpus --class=innocent --deliver=summary --stdout",
101  
-        "get_filter_cmd smtpd, ham" );
  103
+        "smtpd, ham" );
102 104
 
103 105
     $transaction->notes('spamassassin', { is_spam => 'Yes', autolearn => 'spam', score => 110 } );
104 106
     $r = $self->get_filter_cmd($transaction, 'smtpd');
105 107
     cmp_ok( $r, 'eq', "$dspam --user smtpd --mode=tum --source=corpus --class=spam --deliver=summary --stdout",
106  
-        "get_filter_cmd smtpd, spam" );
  108
+        "smtpd, spam" );
107 109
 
108 110
     $transaction->notes('spamassassin', { is_spam => 'No', autolearn => 'spam' } );
109 111
     $r = $self->get_filter_cmd($transaction, 'smtpd');
110 112
     cmp_ok( $r, 'eq', "$dspam --user smtpd --mode=tum --process --deliver=summary --stdout",
111  
-        "get_filter_cmd smtpd, spam" );
  113
+        "smtpd, spam" );
112 114
 };
113 115
 
  116
+sub test_reject_type {
  117
+    my $self = shift;
  118
+
  119
+    $self->{_args}{reject_type} = undef;
  120
+    cmp_ok( $self->get_reject_type(), '==', DENY, "default");
  121
+
  122
+    $self->{_args}{reject_type} = 'temp';
  123
+    cmp_ok( $self->get_reject_type(), '==', DENYSOFT, "defer");
  124
+
  125
+    $self->{_args}{reject_type} = 'disconnect';
  126
+    cmp_ok( $self->get_reject_type(), '==', DENY_DISCONNECT, "disconnect");
  127
+};
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.