(spam-summary-prepare-exit): use spam-process-ham-in-spam-groups
[gnus] / lisp / spam.el
1 ;;; spam.el --- Identifying spam
2 ;; Copyright (C) 2002, 2003 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
5 ;; Keywords: network
6
7 ;; This file is part of GNU Emacs.
8
9 ;; GNU Emacs is free software; you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation; either version 2, or (at your option)
12 ;; any later version.
13
14 ;; GNU Emacs is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 ;; GNU General Public License for more details.
18
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
21 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
22 ;; Boston, MA 02111-1307, USA.
23
24 ;;; Commentary:
25
26 ;;; This module addresses a few aspects of spam control under Gnus.  Page
27 ;;; breaks are used for grouping declarations and documentation relating to
28 ;;; each particular aspect.
29
30 ;;; The integration with Gnus is not yet complete.  See various `FIXME'
31 ;;; comments, below, for supplementary explanations or discussions.
32
33 ;;; Several TODO items are marked as such
34
35 ;;; Code:
36
37 (eval-when-compile (require 'cl))
38
39 (require 'gnus-sum)
40
41 (require 'gnus-uu)                      ; because of key prefix issues
42 (require 'gnus) ; for the definitions of group content classification and spam processors
43 (require 'message)                      ;for the message-fetch-field functions
44
45 ;; for nnimap-split-download-body-default
46 (eval-when-compile (require 'nnimap))
47
48 ;; autoload executable-find
49 (eval-and-compile
50   ;; executable-find is not autoloaded in Emacs 20
51   (autoload 'executable-find "executable"))
52
53 ;; autoload query-dig
54 (eval-and-compile
55   (autoload 'query-dig "dig"))
56
57 ;; autoload spam-report
58 (eval-and-compile
59   (autoload 'spam-report-gmane "spam-report"))
60
61 ;; autoload query-dns
62 (eval-and-compile
63   (autoload 'query-dns "dns"))
64
65 ;;; Main parameters.
66
67 (defgroup spam nil
68   "Spam configuration.")
69
70 (defcustom spam-directory "~/News/spam/"
71   "Directory for spam whitelists and blacklists."
72   :type 'directory
73   :group 'spam)
74
75 (defcustom spam-move-spam-nonspam-groups-only t
76   "Whether spam should be moved in non-spam groups only.
77 When nil, only ham and unclassified groups will have their spam moved
78 to the spam-process-destination.  When t, spam will also be moved from
79 spam groups."
80   :type 'boolean
81   :group 'spam)
82
83 (defcustom spam-process-ham-in-nonham-groups nil
84   "Whether ham should be processed in non-ham groups."
85   :type 'boolean
86   :group 'spam)
87
88 (defcustom spam-process-ham-in-spam-groups nil
89   "Whether ham should be processed in spam groups."
90   :type 'boolean
91   :group 'spam)
92
93 (defcustom spam-mark-only-unseen-as-spam t
94   "Whether only unseen articles should be marked as spam in spam
95 groups.  When nil, all unread articles in a spam group are marked as
96 spam.  Set this if you want to leave an article unread in a spam group
97 without losing it to the automatic spam-marking process."
98   :type 'boolean
99   :group 'spam)
100
101 (defcustom spam-mark-ham-unread-before-move-from-spam-group nil
102   "Whether ham should be marked unread before it's moved out of a spam
103 group according to ham-process-destination.  This variable is an
104 official entry in the international Longest Variable Name
105 Competition."
106   :type 'boolean
107   :group 'spam)
108
109 (defcustom spam-whitelist (expand-file-name "whitelist" spam-directory)
110   "The location of the whitelist.
111 The file format is one regular expression per line.
112 The regular expression is matched against the address."
113   :type 'file
114   :group 'spam)
115
116 (defcustom spam-blacklist (expand-file-name "blacklist" spam-directory)
117   "The location of the blacklist.
118 The file format is one regular expression per line.
119 The regular expression is matched against the address."
120   :type 'file
121   :group 'spam)
122
123 (defcustom spam-use-dig t
124   "Whether query-dig should be used instead of query-dns."
125   :type 'boolean
126   :group 'spam)
127
128 (defcustom spam-use-blacklist nil
129   "Whether the blacklist should be used by spam-split."
130   :type 'boolean
131   :group 'spam)
132
133 (defcustom spam-use-whitelist nil
134   "Whether the whitelist should be used by spam-split."
135   :type 'boolean
136   :group 'spam)
137
138 (defcustom spam-use-whitelist-exclusive nil
139   "Whether whitelist-exclusive should be used by spam-split.
140 Exclusive whitelisting means that all messages from senders not in the whitelist
141 are considered spam."
142   :type 'boolean
143   :group 'spam)
144
145 (defcustom spam-use-blackholes nil
146   "Whether blackholes should be used by spam-split."
147   :type 'boolean
148   :group 'spam)
149
150 (defcustom spam-use-hashcash nil
151   "Whether hashcash payments should be detected by spam-split."
152   :type 'boolean
153   :group 'spam)
154
155 (defcustom spam-use-regex-headers nil
156   "Whether a header regular expression match should be used by spam-split.
157 Also see the variables `spam-regex-headers-spam' and `spam-regex-headers-ham'."
158   :type 'boolean
159   :group 'spam)
160
161 (defcustom spam-use-bogofilter-headers nil
162   "Whether bogofilter headers should be used by spam-split.
163 Enable this if you pre-process messages with Bogofilter BEFORE Gnus sees them."
164   :type 'boolean
165   :group 'spam)
166
167 (defcustom spam-use-bogofilter nil
168   "Whether bogofilter should be invoked by spam-split.
169 Enable this if you want Gnus to invoke Bogofilter on new messages."
170   :type 'boolean
171   :group 'spam)
172
173 (defcustom spam-use-BBDB nil
174   "Whether BBDB should be used by spam-split."
175   :type 'boolean
176   :group 'spam)
177
178 (defcustom spam-use-BBDB-exclusive nil
179   "Whether BBDB-exclusive should be used by spam-split.
180 Exclusive BBDB means that all messages from senders not in the BBDB are 
181 considered spam."
182   :type 'boolean
183   :group 'spam)
184
185 (defcustom spam-use-ifile nil
186   "Whether ifile should be used by spam-split."
187   :type 'boolean
188   :group 'spam)
189
190 (defcustom spam-use-stat nil
191   "Whether spam-stat should be used by spam-split."
192   :type 'boolean
193   :group 'spam)
194
195 (defcustom spam-use-spamoracle nil
196   "Whether spamoracle should be used by spam-split."
197   :type 'boolean
198   :group 'spam)
199
200 (defcustom spam-install-hooks (or
201                                spam-use-dig
202                                spam-use-blacklist
203                                spam-use-whitelist 
204                                spam-use-whitelist-exclusive 
205                                spam-use-blackholes 
206                                spam-use-hashcash 
207                                spam-use-regex-headers 
208                                spam-use-bogofilter-headers 
209                                spam-use-bogofilter 
210                                spam-use-BBDB 
211                                spam-use-BBDB-exclusive 
212                                spam-use-ifile 
213                                spam-use-stat
214                                spam-use-spamoracle)
215   "Whether the spam hooks should be installed, default to t if one of
216 the spam-use-* variables is set."
217   :group 'gnus-registry
218   :type 'boolean)
219
220 (defcustom spam-split-group "spam"
221   "Group name where incoming spam should be put by spam-split."
222   :type 'string
223   :group 'spam)
224
225 ;;; TODO: deprecate this variable, it's confusing since it's a list of strings, not regular expressions
226 (defcustom spam-junk-mailgroups (cons spam-split-group '("mail.junk" "poste.pourriel"))
227   "Mailgroups with spam contents.
228 All unmarked article in such group receive the spam mark on group entry."
229   :type '(repeat (string :tag "Group"))
230   :group 'spam)
231
232 (defcustom spam-blackhole-servers '("bl.spamcop.net" "relays.ordb.org" 
233                                     "dev.null.dk" "relays.visi.com")
234   "List of blackhole servers."
235   :type '(repeat (string :tag "Server"))
236   :group 'spam)
237
238 (defcustom spam-blackhole-good-server-regex nil
239   "String matching IP addresses that should not be checked in the blackholes"
240   :type 'regexp
241   :group 'spam)
242
243 (defcustom spam-face 'gnus-splash-face
244   "Face for spam-marked articles"
245   :type 'face
246   :group 'spam)
247
248 (defcustom spam-regex-headers-spam '("^X-Spam-Flag: YES")
249   "Regular expression for positive header spam matches"
250   :type '(repeat (regexp :tag "Regular expression to match spam header"))
251   :group 'spam)
252
253 (defcustom spam-regex-headers-ham '("^X-Spam-Flag: NO")
254   "Regular expression for positive header ham matches"
255   :type '(repeat (regexp :tag "Regular expression to match ham header"))
256   :group 'spam)
257
258 (defgroup spam-ifile nil
259   "Spam ifile configuration."
260   :group 'spam)
261
262 (defcustom spam-ifile-path (executable-find "ifile")
263   "File path of the ifile executable program."
264   :type '(choice (file :tag "Location of ifile")
265                  (const :tag "ifile is not installed"))
266   :group 'spam-ifile)
267
268 (defcustom spam-ifile-database-path nil
269   "File path of the ifile database."
270   :type '(choice (file :tag "Location of the ifile database")
271                  (const :tag "Use the default"))
272   :group 'spam-ifile)
273
274 (defcustom spam-ifile-spam-category "spam"
275   "Name of the spam ifile category."  
276   :type 'string
277   :group 'spam-ifile)
278
279 (defcustom spam-ifile-ham-category nil
280   "Name of the ham ifile category.  If nil, the current group name will
281 be used."
282   :type '(choice (string :tag "Use a fixed category")
283                 (const :tag "Use the current group name"))
284   :group 'spam-ifile)
285
286 (defcustom spam-ifile-all-categories nil
287   "Whether the ifile check will return all categories, or just spam.
288 Set this to t if you want to use the spam-split invocation of ifile as
289 your main source of newsgroup names."
290   :type 'boolean
291   :group 'spam-ifile)
292
293 (defgroup spam-bogofilter nil
294   "Spam bogofilter configuration."
295   :group 'spam)
296
297 (defcustom spam-bogofilter-path (executable-find "bogofilter")
298   "File path of the Bogofilter executable program."
299   :type '(choice (file :tag "Location of bogofilter")
300                  (const :tag "Bogofilter is not installed"))
301   :group 'spam-bogofilter)
302
303 (defcustom spam-bogofilter-header "X-Bogosity"
304   "The header that Bogofilter inserts in messages."
305   :type 'string
306   :group 'spam-bogofilter)
307
308 (defcustom spam-bogofilter-spam-switch "-s"
309   "The switch that Bogofilter uses to register spam messages."
310   :type 'string
311   :group 'spam-bogofilter)
312
313 (defcustom spam-bogofilter-ham-switch "-n"
314   "The switch that Bogofilter uses to register ham messages."
315   :type 'string
316   :group 'spam-bogofilter)
317
318 (defcustom spam-bogofilter-bogosity-positive-spam-header "^\\(Yes\\|Spam\\)"
319   "The regex on `spam-bogofilter-header' for positive spam identification."
320   :type 'regexp
321   :group 'spam-bogofilter)
322
323 (defcustom spam-bogofilter-database-directory nil
324   "Directory path of the Bogofilter databases."
325   :type '(choice (directory :tag "Location of the Bogofilter database directory")
326                  (const :tag "Use the default"))
327   :group 'spam-ifile)
328
329 (defgroup spam-spamoracle nil
330   "Spam ifile configuration."
331   :group 'spam)
332
333 (defcustom spam-spamoracle-database nil 
334   "Location of spamoracle database file. When nil, use the default
335 spamoracle database."
336   :type '(choice (directory :tag "Location of spamoracle database file.")
337                  (const :tag "Use the default"))
338   :group 'spam-spamoracle)
339
340 (defcustom spam-spamoracle-binary (executable-find "spamoracle")
341   "Location of the spamoracle binary."
342   :type '(choice (directory :tag "Location of the spamoracle binary")
343                  (const :tag "Use the default"))
344   :group 'spam-spamoracle)
345
346 ;;; Key bindings for spam control.
347
348 (gnus-define-keys gnus-summary-mode-map
349   "St" spam-bogofilter-score
350   "Sx" gnus-summary-mark-as-spam
351   "Mst" spam-bogofilter-score
352   "Msx" gnus-summary-mark-as-spam
353   "\M-d" gnus-summary-mark-as-spam)
354
355 ;;; How to highlight a spam summary line.
356
357 ;; TODO: How do we redo this every time spam-face is customized?
358
359 (push '((eq mark gnus-spam-mark) . spam-face)
360       gnus-summary-highlight)
361
362 ;; convenience functions
363 (defun spam-group-ham-mark-p (group mark &optional spam)
364   (when (stringp group)
365     (let* ((marks (spam-group-ham-marks group spam))
366            (marks (if (symbolp mark) 
367                       marks 
368                     (mapcar 'symbol-value marks))))
369       (memq mark marks))))
370
371 (defun spam-group-spam-mark-p (group mark)
372   (spam-group-ham-mark-p group mark t))
373
374 (defun spam-group-ham-marks (group &optional spam)
375   (when (stringp group)
376     (let* ((marks (if spam
377                      (gnus-parameter-spam-marks group)
378                    (gnus-parameter-ham-marks group)))
379            (marks (car marks))
380            (marks (if (listp (car marks)) (car marks) marks)))
381       marks)))
382
383 (defun spam-group-spam-marks (group)
384   (spam-group-ham-marks group t))
385
386 (defun spam-group-spam-contents-p (group)
387   (if (stringp group)
388       (or (member group spam-junk-mailgroups)
389           (memq 'gnus-group-spam-classification-spam 
390                 (gnus-parameter-spam-contents group)))
391     nil))
392   
393 (defun spam-group-ham-contents-p (group)
394   (if (stringp group)
395       (memq 'gnus-group-spam-classification-ham 
396             (gnus-parameter-spam-contents group))
397     nil))
398
399 (defun spam-group-processor-p (group processor)
400   (if (and (stringp group)
401            (symbolp processor))
402       (member processor (car (gnus-parameter-spam-process group)))
403     nil))
404
405 (defun spam-group-spam-processor-report-gmane-p (group)
406   (spam-group-processor-p group 'gnus-group-spam-exit-processor-report-gmane))
407
408 (defun spam-group-spam-processor-bogofilter-p (group)
409   (spam-group-processor-p group 'gnus-group-spam-exit-processor-bogofilter))
410
411 (defun spam-group-spam-processor-blacklist-p (group)
412   (spam-group-processor-p group 'gnus-group-spam-exit-processor-blacklist))
413
414 (defun spam-group-spam-processor-ifile-p (group)
415   (spam-group-processor-p group 'gnus-group-spam-exit-processor-ifile))
416
417 (defun spam-group-ham-processor-ifile-p (group)
418   (spam-group-processor-p group 'gnus-group-ham-exit-processor-ifile))
419
420 (defun spam-group-spam-processor-spamoracle-p (group)
421   (spam-group-processor-p group 'gnus-group-spam-exit-processor-spamoracle))
422
423 (defun spam-group-ham-processor-bogofilter-p (group)
424   (spam-group-processor-p group 'gnus-group-ham-exit-processor-bogofilter))
425
426 (defun spam-group-spam-processor-stat-p (group)
427   (spam-group-processor-p group 'gnus-group-spam-exit-processor-stat))
428
429 (defun spam-group-ham-processor-stat-p (group)
430   (spam-group-processor-p group 'gnus-group-ham-exit-processor-stat))
431
432 (defun spam-group-ham-processor-whitelist-p (group)
433   (spam-group-processor-p group 'gnus-group-ham-exit-processor-whitelist))
434
435 (defun spam-group-ham-processor-BBDB-p (group)
436   (spam-group-processor-p group 'gnus-group-ham-exit-processor-BBDB))
437
438 (defun spam-group-ham-processor-copy-p (group)
439   (spam-group-processor-p group 'gnus-group-ham-exit-processor-copy))
440
441 (defun spam-group-ham-processor-spamoracle-p (group)
442   (spam-group-processor-p group 'gnus-group-ham-exit-processor-spamoracle))
443
444 ;;; Summary entry and exit processing.
445
446 (defun spam-summary-prepare ()
447   (spam-mark-junk-as-spam-routine))
448
449 ;; The spam processors are invoked for any group, spam or ham or neither
450 (defun spam-summary-prepare-exit ()
451   (unless gnus-group-is-exiting-without-update-p
452     (gnus-message 6 "Exiting summary buffer and applying spam rules")
453     (when (and spam-bogofilter-path
454                (spam-group-spam-processor-bogofilter-p gnus-newsgroup-name))
455       (gnus-message 5 "Registering spam with bogofilter")
456       (spam-bogofilter-register-spam-routine))
457   
458     (when (and spam-ifile-path
459                (spam-group-spam-processor-ifile-p gnus-newsgroup-name))
460       (gnus-message 5 "Registering spam with ifile")
461       (spam-ifile-register-spam-routine))
462   
463     (when (spam-group-spam-processor-spamoracle-p gnus-newsgroup-name)
464       (gnus-message 5 "Registering spam with spamoracle")
465       (spam-spamoracle-learn-spam))
466
467     (when (spam-group-spam-processor-stat-p gnus-newsgroup-name)
468       (gnus-message 5 "Registering spam with spam-stat")
469       (spam-stat-register-spam-routine))
470
471     (when (spam-group-spam-processor-blacklist-p gnus-newsgroup-name)
472       (gnus-message 5 "Registering spam with the blacklist")
473       (spam-blacklist-register-routine))
474
475     (when (spam-group-spam-processor-report-gmane-p gnus-newsgroup-name)
476       (gnus-message 5 "Registering spam with the Gmane report")
477       (spam-report-gmane-register-routine))
478
479     (if spam-move-spam-nonspam-groups-only      
480         (when (not (spam-group-spam-contents-p gnus-newsgroup-name))
481           (spam-mark-spam-as-expired-and-move-routine
482            (gnus-parameter-spam-process-destination gnus-newsgroup-name)))
483       (gnus-message 5 "Marking spam as expired and moving it to %s" gnus-newsgroup-name)
484       (spam-mark-spam-as-expired-and-move-routine 
485        (gnus-parameter-spam-process-destination gnus-newsgroup-name)))
486
487     ;; now we redo spam-mark-spam-as-expired-and-move-routine to only
488     ;; expire spam, in case the above did not expire them
489     (gnus-message 5 "Marking spam as expired without moving it")
490     (spam-mark-spam-as-expired-and-move-routine nil)
491
492     (when (or (spam-group-ham-contents-p gnus-newsgroup-name)
493               (and (spam-group-spam-contents-p gnus-newsgroup-name)
494                    spam-process-ham-in-spam-groups)
495               spam-process-ham-in-nonham-groups)
496       (when (spam-group-ham-processor-whitelist-p gnus-newsgroup-name)
497         (gnus-message 5 "Registering ham with the whitelist")
498         (spam-whitelist-register-routine))
499       (when (spam-group-ham-processor-ifile-p gnus-newsgroup-name)
500         (gnus-message 5 "Registering ham with ifile")
501         (spam-ifile-register-ham-routine))
502       (when (spam-group-ham-processor-bogofilter-p gnus-newsgroup-name)
503         (gnus-message 5 "Registering ham with Bogofilter")
504         (spam-bogofilter-register-ham-routine))
505       (when (spam-group-ham-processor-stat-p gnus-newsgroup-name)
506         (gnus-message 5 "Registering ham with spam-stat")
507         (spam-stat-register-ham-routine))
508       (when (spam-group-ham-processor-BBDB-p gnus-newsgroup-name)
509         (gnus-message 5 "Registering ham with the BBDB")
510         (spam-BBDB-register-routine))
511       (when (spam-group-ham-processor-spamoracle-p gnus-newsgroup-name)
512         (gnus-message 5 "Registering ham with spamoracle")
513         (spam-spamoracle-learn-ham)))
514
515     (when (spam-group-ham-processor-copy-p gnus-newsgroup-name)
516       (gnus-message 5 "Copying ham")
517       (spam-ham-move-routine
518        (gnus-parameter-ham-process-destination gnus-newsgroup-name) t))
519
520     ;; now move all ham articles out of spam groups
521     (when (spam-group-spam-contents-p gnus-newsgroup-name)
522       (gnus-message 5 "Moving ham messages from spam group")
523       (spam-ham-move-routine
524        (gnus-parameter-ham-process-destination gnus-newsgroup-name)))))
525
526 (defun spam-mark-junk-as-spam-routine ()
527   ;; check the global list of group names spam-junk-mailgroups and the
528   ;; group parameters
529   (when (spam-group-spam-contents-p gnus-newsgroup-name)
530     (gnus-message 5 "Marking %s articles as spam"
531                   (if spam-mark-only-unseen-as-spam 
532                       "unseen"
533                     "unread"))
534     (let ((articles (if spam-mark-only-unseen-as-spam 
535                         gnus-newsgroup-unseen
536                       gnus-newsgroup-unreads)))
537       (dolist (article articles)
538         (gnus-summary-mark-article article gnus-spam-mark)))))
539
540 (defun spam-mark-spam-as-expired-and-move-routine (&optional group)
541   (gnus-summary-kill-process-mark)
542   (let ((articles gnus-newsgroup-articles)
543         article tomove)
544     (dolist (article articles)
545       (when (eq (gnus-summary-article-mark article) gnus-spam-mark)
546         (gnus-summary-mark-article article gnus-expirable-mark)
547         (push article tomove)))
548
549     ;; now do the actual move
550     (when (and tomove
551                (stringp group))
552       (dolist (article tomove)
553         (gnus-summary-set-process-mark article))
554       (when tomove (gnus-summary-move-article nil group))))
555   (gnus-summary-yank-process-mark))
556  
557 (defun spam-ham-move-routine (&optional group copy)
558   (gnus-summary-kill-process-mark)
559   (let ((articles gnus-newsgroup-articles)
560         article mark tomove)
561     (when (stringp group)               ; this routine will do nothing
562                                         ; without a valid group
563       (dolist (article articles)
564         (when (spam-group-ham-mark-p gnus-newsgroup-name
565                                      (gnus-summary-article-mark article))
566           (push article tomove)))
567
568       ;; now do the actual move
569       (when tomove
570         (dolist (article tomove)
571           (when spam-mark-ham-unread-before-move-from-spam-group
572             (gnus-summary-mark-article article gnus-unread-mark))           
573           (gnus-summary-set-process-mark article))
574         (if copy
575             (gnus-summary-copy-article nil group)
576           (gnus-summary-move-article nil group)))))
577   (gnus-summary-yank-process-mark))
578  
579 (defun spam-generic-register-routine (spam-func ham-func)
580   (let ((articles gnus-newsgroup-articles)
581         article mark ham-articles spam-articles)
582
583     (while articles
584       (setq article (pop articles)
585             mark (gnus-summary-article-mark article))
586       (cond ((spam-group-spam-mark-p gnus-newsgroup-name mark) 
587              (push article spam-articles))
588             ((memq article gnus-newsgroup-saved))
589             ((spam-group-ham-mark-p gnus-newsgroup-name mark)
590              (push article ham-articles))))
591
592     (when (and ham-articles ham-func)
593       (mapc ham-func ham-articles))     ; we use mapc because unlike
594                                         ; mapcar it discards the
595                                         ; return values
596     (when (and spam-articles spam-func)
597       (mapc spam-func spam-articles)))) ; we use mapc because unlike
598                                         ; mapcar it discards the
599                                         ; return values
600
601 (eval-and-compile
602   (defalias 'spam-point-at-eol (if (fboundp 'point-at-eol)
603                                    'point-at-eol
604                                  'line-end-position)))
605
606 (defun spam-get-article-as-string (article)
607   (let ((article-buffer (spam-get-article-as-buffer article))
608                         article-string)
609     (when article-buffer
610       (save-window-excursion
611         (set-buffer article-buffer)
612         (setq article-string (buffer-string))))
613   article-string))
614
615 (defun spam-get-article-as-buffer (article)
616   (let ((article-buffer))
617     (when (numberp article)
618       (save-window-excursion
619         (gnus-summary-goto-subject article)
620         (gnus-summary-show-article t)
621         (setq article-buffer (get-buffer gnus-article-buffer))))
622     article-buffer))
623
624 ;; disabled for now
625 ;; (defun spam-get-article-as-filename (article)
626 ;;   (let ((article-filename))
627 ;;     (when (numberp article)
628 ;;       (nnml-possibly-change-directory (gnus-group-real-name gnus-newsgroup-name))
629 ;;       (setq article-filename (expand-file-name (int-to-string article) nnml-current-directory)))
630 ;;     (if (file-exists-p article-filename)
631 ;;      article-filename
632 ;;       nil)))
633
634 (defun spam-fetch-field-from-fast (article)
635   "Fetch the `from' field quickly, using the internal gnus-data-list function"
636   (if (and (numberp article)
637            (assoc article (gnus-data-list nil)))
638       (mail-header-from (gnus-data-header (assoc article (gnus-data-list nil))))
639     nil))
640
641 (defun spam-fetch-field-subject-fast (article)
642   "Fetch the `subject' field quickly, using the internal gnus-data-list function"
643   (if (and (numberp article)
644            (assoc article (gnus-data-list nil)))
645       (mail-header-subject (gnus-data-header (assoc article (gnus-data-list nil))))
646     nil))
647
648 \f
649 ;;;; Spam determination.
650
651 (defvar spam-list-of-checks
652   '((spam-use-blacklist                 .       spam-check-blacklist)
653     (spam-use-regex-headers             .       spam-check-regex-headers)
654     (spam-use-whitelist                 .       spam-check-whitelist)
655     (spam-use-BBDB                      .       spam-check-BBDB)
656     (spam-use-ifile                     .       spam-check-ifile)
657     (spam-use-spamoracle                .       spam-check-spamoracle)
658     (spam-use-stat                      .       spam-check-stat)
659     (spam-use-blackholes                .       spam-check-blackholes)
660     (spam-use-hashcash                  .       spam-check-hashcash)
661     (spam-use-bogofilter-headers        .       spam-check-bogofilter-headers)
662     (spam-use-bogofilter                .       spam-check-bogofilter))
663 "The spam-list-of-checks list contains pairs associating a parameter
664 variable with a spam checking function.  If the parameter variable is
665 true, then the checking function is called, and its value decides what
666 happens.  Each individual check may return nil, t, or a mailgroup
667 name.  The value nil means that the check does not yield a decision,
668 and so, that further checks are needed.  The value t means that the
669 message is definitely not spam, and that further spam checks should be
670 inhibited.  Otherwise, a mailgroup name is returned where the mail
671 should go, and further checks are also inhibited.  The usual mailgroup
672 name is the value of `spam-split-group', meaning that the message is
673 definitely a spam.")
674
675 (defvar spam-list-of-statistical-checks
676   '(spam-use-ifile spam-use-stat spam-use-bogofilter spam-use-spamoracle)
677 "The spam-list-of-statistical-checks list contains all the mail
678 splitters that need to have the full message body available.")
679
680 (defun spam-split (&rest specific-checks)
681   "Split this message into the `spam' group if it is spam.
682 This function can be used as an entry in `nnmail-split-fancy', for
683 example like this: (: spam-split).  It can take checks as parameters.
684
685 See the Info node `(gnus)Fancy Mail Splitting' for more details."
686   (interactive)
687   (save-excursion
688     (save-restriction
689       (dolist (check spam-list-of-statistical-checks)
690         (when (symbol-value check)
691           (widen)
692           (gnus-message 8 "spam-split: widening the buffer (%s requires it)"
693                         (symbol-name check))
694           (return)))
695       ;;   (progn (widen) (debug (buffer-string)))
696       (let ((list-of-checks spam-list-of-checks)
697             decision)
698         (while (and list-of-checks (not decision))
699           (let ((pair (pop list-of-checks)))
700             (when (and (symbol-value (car pair))
701                        (or (null specific-checks)
702                            (memq (car pair) specific-checks)))
703               (gnus-message 5 "spam-split: calling the %s function" (symbol-name (cdr pair)))
704               (setq decision (funcall (cdr pair))))))
705         (if (eq decision t)
706             nil
707           decision)))))
708   
709 (defun spam-setup-widening ()
710   (dolist (check spam-list-of-statistical-checks)
711     (when (symbol-value check)
712       (setq nnimap-split-download-body-default t))))
713
714 \f
715 ;;;; Regex headers
716
717 (defun spam-check-regex-headers ()
718   (let (ret found)
719     (dolist (h-regex spam-regex-headers-ham)
720       (unless found
721         (goto-char (point-min))
722         (when (re-search-forward h-regex nil t)
723           (message "Ham regex header search positive.")
724           (setq found t))))
725     (dolist (s-regex spam-regex-headers-spam)
726       (unless found
727         (goto-char (point-min))
728         (when (re-search-forward s-regex nil t)
729           (message "Spam regex header search positive." (match-string 1))
730           (setq found t)
731           (setq ret spam-split-group))))
732     ret))
733
734 \f
735 ;;;; Blackholes.
736
737 (defun spam-check-blackholes ()
738   "Check the Received headers for blackholed relays."
739   (let ((headers (message-fetch-field "received"))
740         ips matches)
741     (when headers
742       (with-temp-buffer
743         (insert headers)
744         (goto-char (point-min))
745         (gnus-message 5 "Checking headers for relay addresses")
746         (while (re-search-forward
747                 "\\[\\([0-9]+.[0-9]+.[0-9]+.[0-9]+\\)\\]" nil t)
748           (gnus-message 9 "Blackhole search found host IP %s." (match-string 1))
749           (push (mapconcat 'identity
750                            (nreverse (split-string (match-string 1) "\\."))
751                            ".")
752                 ips)))
753       (dolist (server spam-blackhole-servers)
754         (dolist (ip ips)
755           (unless (and spam-blackhole-good-server-regex
756                        (string-match spam-blackhole-good-server-regex ip))
757             (let ((query-string (concat ip "." server)))
758               (if spam-use-dig
759                   (let ((query-result (query-dig query-string)))
760                     (when query-result
761                       (gnus-message 5 "(DIG): positive blackhole check '%s'" 
762                                     query-result)
763                       (push (list ip server query-result)
764                             matches)))
765                 ;; else, if not using dig.el
766                 (when (query-dns query-string)
767                   (gnus-message 5 "positive blackhole check")
768                   (push (list ip server (query-dns query-string 'TXT))
769                         matches))))))))
770     (when matches
771       spam-split-group)))
772 \f
773 ;;;; Hashcash.
774
775 (condition-case nil
776     (progn
777       (require 'hashcash)
778       
779       (defun spam-check-hashcash ()
780         "Check the headers for hashcash payments."
781         (mail-check-payment)))          ;mail-check-payment returns a boolean
782
783   (file-error (progn
784                 (defalias 'mail-check-payment 'ignore)
785                 (defalias 'spam-check-hashcash 'ignore))))
786 \f
787 ;;;; BBDB 
788
789 ;;; original idea for spam-check-BBDB from Alexander Kotelnikov
790 ;;; <sacha@giotto.sj.ru>
791
792 ;; all this is done inside a condition-case to trap errors
793
794 (condition-case nil
795     (progn
796       (require 'bbdb)
797       (require 'bbdb-com)
798       
799   (defun spam-enter-ham-BBDB (from)
800     "Enter an address into the BBDB; implies ham (non-spam) sender"
801     (when (stringp from)
802       (let* ((parsed-address (gnus-extract-address-components from))
803              (name (or (car parsed-address) "Ham Sender"))
804              (net-address (car (cdr parsed-address))))
805         (gnus-message 5 "Adding address %s to BBDB" from)
806         (when (and net-address
807                    (not (bbdb-search-simple nil net-address)))
808           (bbdb-create-internal name nil net-address nil nil 
809                                 "ham sender added by spam.el")))))
810
811   (defun spam-BBDB-register-routine ()
812     (spam-generic-register-routine 
813      ;; spam function
814      nil
815      ;; ham function
816      (lambda (article)
817        (spam-enter-ham-BBDB (spam-fetch-field-from-fast article)))))
818
819   (defun spam-check-BBDB ()
820     "Mail from people in the BBDB is classified as ham or non-spam"
821     (let ((who (message-fetch-field "from")))
822       (when who
823         (setq who (cadr (gnus-extract-address-components who)))
824         (if (bbdb-search-simple nil who)
825             t 
826           (if spam-use-BBDB-exclusive
827               spam-split-group
828             nil))))))
829
830   (file-error (progn
831                 (defalias 'bbdb-search-simple 'ignore)
832                 (defalias 'spam-check-BBDB 'ignore)
833                 (defalias 'spam-BBDB-register-routine 'ignore)
834                 (defalias 'spam-enter-ham-BBDB 'ignore)
835                 (defalias 'bbdb-create-internal 'ignore)
836                 (defalias 'bbdb-records 'ignore))))
837
838 \f
839 ;;;; ifile
840
841 ;;; check the ifile backend; return nil if the mail was NOT classified
842 ;;; as spam
843
844 (defun spam-get-ifile-database-parameter ()
845   "Get the command-line parameter for ifile's database from spam-ifile-database-path."
846   (if spam-ifile-database-path
847       (format "--db-file=%s" spam-ifile-database-path)
848     nil))
849     
850 (defun spam-check-ifile ()
851   "Check the ifile backend for the classification of this message"
852   (let ((article-buffer-name (buffer-name)) 
853         category return)
854     (with-temp-buffer
855       (let ((temp-buffer-name (buffer-name))
856             (db-param (spam-get-ifile-database-parameter)))
857         (save-excursion
858           (set-buffer article-buffer-name)
859           (if db-param
860               (call-process-region (point-min) (point-max) spam-ifile-path
861                                    nil temp-buffer-name nil "-q" "-c" db-param)
862             (call-process-region (point-min) (point-max) spam-ifile-path
863                                  nil temp-buffer-name nil "-q" "-c")))
864         (goto-char (point-min))
865         (if (not (eobp))
866             (setq category (buffer-substring (point) (spam-point-at-eol))))
867         (when (not (zerop (length category))) ; we need a category here
868           (if spam-ifile-all-categories
869               (setq return category)
870             ;; else, if spam-ifile-all-categories is not set...
871             (when (string-equal spam-ifile-spam-category category)
872               (setq return spam-split-group))))))
873     return))
874
875 (defun spam-ifile-register-with-ifile (article-string category)
876   "Register an article, given as a string, with a category.
877 Uses `gnus-newsgroup-name' if category is nil (for ham registration)."
878   (when (stringp article-string)
879     (let ((category (or category gnus-newsgroup-name))
880           (db-param (spam-get-ifile-database-parameter)))
881       (with-temp-buffer
882         (insert article-string)
883         (if db-param
884             (call-process-region (point-min) (point-max) spam-ifile-path 
885                                  nil nil nil 
886                                  "-h" "-i" category db-param)
887           (call-process-region (point-min) (point-max) spam-ifile-path 
888                                nil nil nil 
889                                "-h" "-i" category))))))
890
891 (defun spam-ifile-register-spam-routine ()
892   (spam-generic-register-routine 
893    (lambda (article)
894      (spam-ifile-register-with-ifile 
895       (spam-get-article-as-string article) spam-ifile-spam-category))
896    nil))
897
898 (defun spam-ifile-register-ham-routine ()
899   (spam-generic-register-routine 
900    nil
901    (lambda (article)
902      (spam-ifile-register-with-ifile 
903       (spam-get-article-as-string article) spam-ifile-ham-category))))
904
905 \f
906 ;;;; spam-stat
907
908 (condition-case nil
909     (progn
910       (let ((spam-stat-install-hooks nil))
911         (require 'spam-stat))
912       
913       (defun spam-check-stat ()
914         "Check the spam-stat backend for the classification of this message"
915         (let ((spam-stat-split-fancy-spam-group spam-split-group) ; override
916               (spam-stat-buffer (buffer-name)) ; stat the current buffer
917               category return)
918           (spam-stat-split-fancy)))
919
920       (defun spam-stat-register-spam-routine ()
921         (spam-generic-register-routine 
922          (lambda (article)
923            (let ((article-string (spam-get-article-as-string article)))
924              (with-temp-buffer
925                (insert article-string)
926                (spam-stat-buffer-is-spam))))
927          nil))
928
929       (defun spam-stat-register-ham-routine ()
930         (spam-generic-register-routine 
931          nil
932          (lambda (article)
933            (let ((article-string (spam-get-article-as-string article)))
934              (with-temp-buffer
935                (insert article-string)
936                (spam-stat-buffer-is-non-spam))))))
937
938       (defun spam-maybe-spam-stat-load ()
939         (when spam-use-stat (spam-stat-load)))
940       
941       (defun spam-maybe-spam-stat-save ()
942         (when spam-use-stat (spam-stat-save))))
943
944   (file-error (progn
945                 (defalias 'spam-maybe-spam-stat-load 'ignore)
946                 (defalias 'spam-maybe-spam-stat-save 'ignore)
947                 (defalias 'spam-stat-register-ham-routine 'ignore)
948                 (defalias 'spam-stat-register-spam-routine 'ignore)
949                 (defalias 'spam-stat-buffer-is-spam 'ignore)
950                 (defalias 'spam-stat-buffer-is-non-spam 'ignore)
951                 (defalias 'spam-stat-split-fancy 'ignore)
952                 (defalias 'spam-stat-load 'ignore)
953                 (defalias 'spam-stat-save 'ignore)
954                 (defalias 'spam-check-stat 'ignore))))
955
956 \f
957
958 ;;;; Blacklists and whitelists.
959
960 (defvar spam-whitelist-cache nil)
961 (defvar spam-blacklist-cache nil)
962
963 (defun spam-enter-whitelist (address)
964   "Enter ADDRESS into the whitelist."
965   (interactive "sAddress: ")
966   (spam-enter-list address spam-whitelist)
967   (setq spam-whitelist-cache nil))
968
969 (defun spam-enter-blacklist (address)
970   "Enter ADDRESS into the blacklist."
971   (interactive "sAddress: ")
972   (spam-enter-list address spam-blacklist)
973   (setq spam-blacklist-cache nil))
974
975 (defun spam-enter-list (address file)
976   "Enter ADDRESS into the given FILE, either the whitelist or the blacklist."
977   (unless (file-exists-p (file-name-directory file))
978     (make-directory (file-name-directory file) t))
979   (save-excursion
980     (set-buffer
981      (find-file-noselect file))
982     (goto-char (point-min))
983     (unless (re-search-forward (regexp-quote address) nil t)
984       (goto-char (point-max))
985       (unless (bobp)
986         (insert "\n"))
987       (insert address "\n")
988       (save-buffer))))
989
990 ;;; returns t if the sender is in the whitelist, nil or spam-split-group otherwise
991 (defun spam-check-whitelist ()
992   ;; FIXME!  Should it detect when file timestamps change?
993   (unless spam-whitelist-cache
994     (setq spam-whitelist-cache (spam-parse-list spam-whitelist)))
995   (if (spam-from-listed-p spam-whitelist-cache) 
996       t
997     (if spam-use-whitelist-exclusive
998         spam-split-group
999       nil)))
1000
1001 (defun spam-check-blacklist ()
1002   ;; FIXME!  Should it detect when file timestamps change?
1003   (unless spam-blacklist-cache
1004     (setq spam-blacklist-cache (spam-parse-list spam-blacklist)))
1005   (and (spam-from-listed-p spam-blacklist-cache) spam-split-group))
1006
1007 (defun spam-parse-list (file)
1008   (when (file-readable-p file)
1009     (let (contents address)
1010       (with-temp-buffer
1011         (insert-file-contents file)
1012         (while (not (eobp))
1013           (setq address (buffer-substring (point) (spam-point-at-eol)))
1014           (forward-line 1)
1015           ;; insert the e-mail address if detected, otherwise the raw data
1016           (unless (zerop (length address))
1017             (let ((pure-address (cadr (gnus-extract-address-components address))))
1018               (push (or pure-address address) contents)))))
1019       (nreverse contents))))
1020
1021 (defun spam-from-listed-p (cache)
1022   (let ((from (message-fetch-field "from"))
1023         found)
1024     (while cache
1025       (let ((address (pop cache)))
1026         (unless (zerop (length address)) ; 0 for a nil address too
1027           (setq address (regexp-quote address))
1028           ;; fix regexp-quote's treatment of user-intended regexes
1029           (while (string-match "\\\\\\*" address)
1030             (setq address (replace-match ".*" t t address))))
1031         (when (and address (string-match address from))
1032           (setq found t
1033                 cache nil))))
1034     found))
1035
1036 (defun spam-blacklist-register-routine ()
1037   (spam-generic-register-routine 
1038    ;; the spam function
1039    (lambda (article)
1040      (let ((from (spam-fetch-field-from-fast article)))
1041        (when (stringp from)
1042            (spam-enter-blacklist from))))
1043    ;; the ham function
1044    nil))
1045
1046 (defun spam-whitelist-register-routine ()
1047   (spam-generic-register-routine 
1048    ;; the spam function
1049    nil 
1050    ;; the ham function
1051    (lambda (article)
1052      (let ((from (spam-fetch-field-from-fast article)))
1053        (when (stringp from)
1054            (spam-enter-whitelist from))))))
1055
1056 \f
1057 ;;;; Spam-report glue
1058 (defun spam-report-gmane-register-routine ()
1059   (spam-generic-register-routine
1060    'spam-report-gmane
1061    nil))
1062
1063 \f
1064 ;;;; Bogofilter
1065 (defun spam-check-bogofilter-headers (&optional score)
1066   (let ((header (message-fetch-field spam-bogofilter-header)))
1067     (when header                        ; return nil when no header
1068       (if score                         ; scoring mode
1069           (if (string-match "spamicity=\\([0-9.]+\\)" header)
1070               (match-string 1 header)
1071             "0")
1072         ;; spam detection mode
1073         (when (string-match spam-bogofilter-bogosity-positive-spam-header
1074                             header)
1075           spam-split-group)))))
1076
1077 ;; return something sensible if the score can't be determined
1078 (defun spam-bogofilter-score ()
1079   "Get the Bogofilter spamicity score"
1080   (interactive)
1081   (save-window-excursion
1082     (gnus-summary-show-article t)
1083     (set-buffer gnus-article-buffer)
1084     (let ((score (or (spam-check-bogofilter-headers t)
1085                      (spam-check-bogofilter t))))
1086       (message "Spamicity score %s" score)
1087       (or score "0"))
1088     (gnus-summary-show-article)))
1089
1090 (defun spam-check-bogofilter (&optional score)
1091   "Check the Bogofilter backend for the classification of this message"
1092   (let ((article-buffer-name (buffer-name)) 
1093         return)
1094     (with-temp-buffer
1095       (let ((temp-buffer-name (buffer-name)))
1096         (save-excursion
1097           (set-buffer article-buffer-name)
1098           (if spam-bogofilter-database-directory
1099               (call-process-region (point-min) (point-max) 
1100                                    spam-bogofilter-path
1101                                    nil temp-buffer-name nil "-v"
1102                                    "-d" spam-bogofilter-database-directory)
1103             (call-process-region (point-min) (point-max) spam-bogofilter-path
1104                                  nil temp-buffer-name nil "-v")))
1105         (setq return (spam-check-bogofilter-headers score))))
1106     return))
1107
1108 (defun spam-bogofilter-register-with-bogofilter (article-string spam)
1109   "Register an article, given as a string, as spam or non-spam."
1110   (when (stringp article-string)
1111     (let ((switch (if spam spam-bogofilter-spam-switch 
1112                     spam-bogofilter-ham-switch)))
1113       (with-temp-buffer
1114         (insert article-string)
1115         (if spam-bogofilter-database-directory
1116             (call-process-region (point-min) (point-max) 
1117                                  spam-bogofilter-path
1118                                  nil nil nil "-v" switch
1119                                  "-d" spam-bogofilter-database-directory)
1120           (call-process-region (point-min) (point-max) spam-bogofilter-path
1121                                nil nil nil "-v" switch))))))
1122
1123 (defun spam-bogofilter-register-spam-routine ()
1124   (spam-generic-register-routine 
1125    (lambda (article)
1126      (spam-bogofilter-register-with-bogofilter
1127       (spam-get-article-as-string article) t))
1128    nil))
1129
1130 (defun spam-bogofilter-register-ham-routine ()
1131   (spam-generic-register-routine 
1132    nil
1133    (lambda (article)
1134      (spam-bogofilter-register-with-bogofilter
1135       (spam-get-article-as-string article) nil))))
1136
1137 \f
1138 ;;;; spamoracle
1139 (defun spam-check-spamoracle ()
1140   "Run spamoracle on an article to determine whether it's spam."
1141   (let ((article-buffer-name (buffer-name)))
1142     (with-temp-buffer
1143       (let ((temp-buffer-name (buffer-name)))
1144         (save-excursion
1145           (set-buffer article-buffer-name)
1146           (let ((status 
1147                  (apply 'call-process-region 
1148                         (point-min) (point-max)
1149                         spam-spamoracle-binary 
1150                         nil temp-buffer-name nil
1151                         (if spam-spamoracle-database
1152                             `("-f" ,spam-spamoracle-database "mark")
1153                           '("mark")))))
1154             (if (zerop status)
1155                 (progn
1156                   (set-buffer temp-buffer-name)
1157                   (goto-char (point-min))
1158                   (when (re-search-forward "^X-Spam: yes;" nil t)
1159                     spam-split-group))
1160               (error "Error running spamoracle" status))))))))
1161
1162 (defun spam-spamoracle-learn (article article-is-spam-p)
1163   "Run spamoracle in training mode."
1164   (with-temp-buffer
1165     (let ((temp-buffer-name (buffer-name)))
1166       (save-excursion
1167         (goto-char (point-min))
1168         (insert (spam-get-article-as-string article))
1169         (let* ((arg (if article-is-spam-p "-spam" "-good"))
1170                (status 
1171                 (apply 'call-process-region
1172                        (point-min) (point-max)
1173                        spam-spamoracle-binary
1174                        nil temp-buffer-name nil
1175                        (if spam-spamoracle-database
1176                            `("-f" ,spam-spamoracle-database 
1177                              "add" ,arg)
1178                          `("add" ,arg)))))
1179           (when (not (zerop status))
1180             (error "Error running spamoracle" status)))))))
1181   
1182 (defun spam-spamoracle-learn-ham ()
1183   (spam-generic-register-routine 
1184    nil
1185    (lambda (article)
1186      (spam-spamoracle-learn article nil))))
1187
1188 (defun spam-spamoracle-learn-spam ()
1189   (spam-generic-register-routine 
1190    (lambda (article)
1191      (spam-spamoracle-learn article t))
1192    nil))
1193 \f
1194 ;;;; Hooks
1195
1196 (defun spam-install-hooks-function ()
1197   "Install the spam.el hooks"
1198   (interactive)
1199   ;; Add hooks for loading and saving the spam stats
1200   (when spam-use-stat
1201     (add-hook 'gnus-save-newsrc-hook 'spam-maybe-spam-stat-save)
1202     (add-hook 'gnus-get-top-new-news-hook 'spam-maybe-spam-stat-load)
1203     (add-hook 'gnus-startup-hook 'spam-maybe-spam-stat-load))
1204   (add-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
1205   (add-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
1206   (add-hook 'gnus-get-new-news-hook 'spam-setup-widening))
1207
1208 (defun spam-unload-hook ()
1209   "Uninstall the spam.el hooks"
1210   (interactive)
1211   (remove-hook 'gnus-save-newsrc-hook 'spam-maybe-spam-stat-save)
1212   (remove-hook 'gnus-get-top-new-news-hook 'spam-maybe-spam-stat-load)
1213   (remove-hook 'gnus-startup-hook 'spam-maybe-spam-stat-load)
1214   (remove-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
1215   (remove-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
1216   (remove-hook 'gnus-get-new-news-hook 'spam-setup-widening))
1217
1218 (when spam-install-hooks
1219   (spam-install-hooks-function))
1220
1221 (provide 'spam)
1222
1223 ;;; spam.el ends here.