(spam-group-spam-contents-p, spam-group-ham-contents-p)
[gnus] / lisp / spam.el
1 ;;; spam.el --- Identifying spam
2 ;; Copyright (C) 2002 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 (require 'gnus-sum)
38
39 (require 'gnus-uu)                      ; because of key prefix issues
40 (require 'gnus) ; for the definitions of group content classification and spam processors
41
42 ;; FIXME!  We should not require `message' until we actually need
43 ;; them.  Best would be to declare needed functions as auto-loadable.
44 (require 'message)
45
46 ;; Attempt to load BBDB macros
47 (eval-when-compile
48   (condition-case nil
49       (require 'bbdb-com)
50     (file-error (defalias 'bbdb-search 'ignore))))
51
52 ;; autoload executable-find
53 (eval-and-compile
54   ;; executable-find is not autoloaded in Emacs 20
55   (autoload 'executable-find "executable"))
56
57 ;; autoload ifile-spam-filter
58 (eval-and-compile
59   (autoload 'ifile-spam-filter "ifile-gnus"))
60
61 ;; autoload query-dig
62 (eval-and-compile
63   (autoload 'query-dig "dig"))
64
65 ;; autoload query-dns
66 (eval-and-compile
67   (autoload 'query-dns "dns"))
68
69 ;;; Main parameters.
70
71 (defgroup spam nil
72   "Spam configuration.")
73
74 (defcustom spam-directory "~/News/spam/"
75   "Directory for spam whitelists and blacklists."
76   :type 'directory
77   :group 'spam)
78
79 (defcustom spam-whitelist (expand-file-name "whitelist" spam-directory)
80   "The location of the whitelist.
81 The file format is one regular expression per line.
82 The regular expression is matched against the address."
83   :type 'file
84   :group 'spam)
85
86 (defcustom spam-blacklist (expand-file-name "blacklist" spam-directory)
87   "The location of the blacklist.
88 The file format is one regular expression per line.
89 The regular expression is matched against the address."
90   :type 'file
91   :group 'spam)
92
93 (defcustom spam-use-dig t
94   "Whether query-dig should be used instead of query-dns."
95   :type 'boolean
96   :group 'spam)
97
98 (defcustom spam-use-blacklist t
99   "Whether the blacklist should be used by spam-split."
100   :type 'boolean
101   :group 'spam)
102
103 (defcustom spam-use-whitelist nil
104   "Whether the whitelist should be used by spam-split."
105   :type 'boolean
106   :group 'spam)
107
108 (defcustom spam-use-blackholes nil
109   "Whether blackholes should be used by spam-split."
110   :type 'boolean
111   :group 'spam)
112
113 (defcustom spam-use-bogofilter nil
114   "Whether bogofilter should be used by spam-split."
115   :type 'boolean
116   :group 'spam)
117
118 (defcustom spam-use-bbdb nil
119   "Whether BBDB should be used by spam-split."
120   :type 'boolean
121   :group 'spam)
122
123 (defcustom spam-use-ifile nil
124   "Whether ifile should be used by spam-split."
125   :type 'boolean
126   :group 'spam)
127
128 (defcustom spam-split-group "spam"
129   "Group name where incoming spam should be put by spam-split."
130   :type 'string
131   :group 'spam)
132
133 ;; FIXME!  The mailgroup list evidently depends on other choices made by the
134 ;; user, so the built-in default below is not likely to be appropriate.
135 (defcustom spam-junk-mailgroups (cons spam-split-group '("mail.junk" "poste.pourriel"))
136   "Mailgroups with spam contents.
137 All unmarked article in such group receive the spam mark on group entry."
138   :type '(repeat (string :tag "Group"))
139   :group 'spam)
140
141 (defcustom spam-blackhole-servers '("bl.spamcop.net" "relays.ordb.org" "dev.null.dk" "relays.visi.com")
142   "List of blackhole servers."
143   :type '(repeat (string :tag "Server"))
144   :group 'spam)
145
146 (defcustom spam-ham-marks (list 'gnus-del-mark 'gnus-read-mark 'gnus-killed-mark 'gnus-kill-file-mark 'gnus-low-score-mark)
147   "Marks considered as being ham (positively not spam).
148 Such articles will be processed as ham (non-spam) on group exit."
149   :type '(set
150           (variable-item gnus-del-mark)
151           (variable-item gnus-read-mark)
152           (variable-item gnus-killed-mark)
153           (variable-item gnus-kill-file-mark)
154           (variable-item gnus-low-score-mark))
155   :group 'spam)
156
157 (defcustom spam-spam-marks (list 'gnus-spam-mark)
158   "Marks considered as being spam (positively spam).
159 Such articles will be transmitted to `bogofilter -s' on group exit."
160   :type '(set 
161           (variable-item gnus-spam-mark)
162           (variable-item gnus-killed-mark)
163           (variable-item gnus-kill-file-mark)
164           (variable-item gnus-low-score-mark))
165   :group 'spam)
166
167 (defcustom spam-face 'gnus-splash-face
168   "Face for spam-marked articles"
169   :type 'face
170   :group 'spam)
171
172 (defgroup spam-bogofilter nil
173   "Spam bogofilter configuration."
174   :group 'spam)
175
176 (defcustom spam-bogofilter-output-buffer-name "*Bogofilter Output*"
177   "Name of buffer when displaying `bogofilter -v' output."  
178   :type 'string
179   :group 'spam-bogofilter)
180
181 (defcustom spam-bogofilter-initial-timeout 40
182   "Timeout in seconds for the initial reply from the `bogofilter' program."
183   :type 'integer
184   :group 'spam-bogofilter)
185
186 (defcustom spam-bogofilter-subsequent-timeout 15
187   "Timeout in seconds for any subsequent reply from the `bogofilter' program."
188   :type 'integer
189   :group 'spam-bogofilter)
190
191 (defcustom spam-bogofilter-path (executable-find "bogofilter")
192   "File path of the Bogofilter executable program."
193   :type '(choice (file :tag "Location of bogofilter")
194                  (const :tag "Bogofilter is not installed"))
195   :group 'spam-bogofilter)
196
197 ;; FIXME!  In the following regexp, we should explain which tool produces
198 ;; which kind of header.  I do not even remember them all by now.  X-Junk
199 ;; (and previously X-NoSpam) are produced by the `NoSpam' tool, which has
200 ;; never been published, so it might not be reasonable leaving it in the
201 ;; list.
202 (defcustom spam-bogofilter-spaminfo-header-regexp "^X-\\(jf\\|Junk\\|NoSpam\\|Spam\\|SB\\)[^:]*:"
203   "Regexp for spam markups in headers.
204 Markup from spam recognisers, as well as `Xref', are to be removed from
205 articles before they get registered by Bogofilter."
206   :type 'regexp
207   :group 'spam-bogofilter)
208
209 ;;; Key bindings for spam control.
210
211 (gnus-define-keys gnus-summary-mode-map
212   "St" spam-bogofilter-score
213   "Sx" gnus-summary-mark-as-spam
214   "Mst" spam-bogofilter-score
215   "Msx" gnus-summary-mark-as-spam
216   "\M-d" gnus-summary-mark-as-spam)
217
218 ;;; How to highlight a spam summary line.
219
220 ;; TODO: How do we redo this every time spam-face is customized?
221
222 (push '((eq mark gnus-spam-mark) . spam-face)
223       gnus-summary-highlight)
224
225 ;; convenience functions
226 (defun spam-group-spam-contents-p (group)
227   (if (stringp group)
228       (or (member group spam-junk-mailgroups)
229           (memq 'gnus-group-spam-classification-spam (gnus-parameter-spam-contents group)))
230     nil))
231   
232 (defun spam-group-ham-contents-p (group)
233   (if (stringp group)
234       (memq 'gnus-group-spam-classification-ham (gnus-parameter-spam-contents group))
235     nil))
236
237 (defun spam-group-processor-p (group processor)
238   (if (and (stringp group)
239            (symbolp processor))
240       (member processor (car (gnus-parameter-spam-process group)))
241     nil))
242
243 (defun spam-group-processor-bogofilter-p (group)
244   (spam-group-processor-p group 'gnus-group-spam-exit-processor-bogofilter))
245
246 (defun spam-group-processor-ifile-p (group)
247   (spam-group-processor-p group 'gnus-group-spam-exit-processor-ifile))
248
249 (defun spam-group-processor-blacklist-p (group)
250   (spam-group-processor-p group 'gnus-group-spam-exit-processor-blacklist))
251
252 (defun spam-group-processor-whitelist-p (group)
253   (spam-group-processor-p group 'gnus-group-ham-exit-processor-whitelist))
254
255 (defun spam-group-processor-BBDB-p (group)
256   (spam-group-processor-p group 'gnus-group-ham-exit-processor-BBDB))
257
258 ;;; Hooks dispatching.  A bit raw for now.
259
260 (defun spam-summary-prepare ()
261   (spam-mark-junk-as-spam-routine))
262
263 (defun spam-summary-prepare-exit ()
264   ;; The spam processors are invoked for any group, spam or ham or neither
265   (when (or (and spam-use-bogofilter spam-bogofilter-path)
266             (spam-group-processor-bogofilter-p gnus-newsgroup-name))
267     (spam-bogofilter-register-routine))
268   
269   (when (or spam-use-ifile
270             (spam-group-processor-ifile-p gnus-newsgroup-name))
271     (spam-ifile-register-routine))
272   
273   (when (or spam-use-blacklist
274             (spam-group-processor-bogofilter-p gnus-newsgroup-name))
275     (spam-blacklist-register-routine))
276
277   ;; Only for spam groups, we expire and maybe move articles
278   (when (spam-group-spam-contents-p gnus-newsgroup-name)
279     (spam-mark-spam-as-expired-and-move-routine (gnus-parameter-spam-process-destination gnus-newsgroup-name)))
280
281   (when (spam-group-ham-contents-p gnus-newsgroup-name)
282     ;; TODO: the ham processors here
283     (when (or spam-use-whitelist
284               (spam-group-processor-whitelist-p gnus-newsgroup-name))
285       (spam-whitelist-register-routine))
286     (when (or spam-use-BBDB
287               (spam-group-processor-BBDB-p gnus-newsgroup-name))
288       (spam-BBDB-register-routine))))
289
290 (add-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
291 (add-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
292
293 (defun spam-mark-junk-as-spam-routine ()
294   ;; check the global list of group names spam-junk-mailgroups and the group parameters
295   (when (spam-group-spam-contents-p gnus-newsgroup-name)
296     (let ((articles gnus-newsgroup-articles)
297           article)
298       (while articles
299         (setq article (pop articles))
300         (when (eq (gnus-summary-article-mark article) gnus-unread-mark)
301           (gnus-summary-mark-article article gnus-spam-mark))))))
302
303 (defun spam-mark-spam-as-expired-and-move-routine (&optional group)
304   (let ((articles gnus-newsgroup-articles)
305         article)
306     (while articles
307       (setq article (pop articles))
308       (when (eq (gnus-summary-article-mark article) gnus-spam-mark)
309         (gnus-summary-mark-article article gnus-expirable-mark)
310         (when (stringp group)
311           (let ((gnus-current-article article))
312             (gnus-summary-move-article nil group)))))))
313  
314 (defun spam-generic-register-routine (spam-func ham-func)
315   (let ((articles gnus-newsgroup-articles)
316         article mark ham-articles spam-articles spam-mark-values ham-mark-values)
317
318     ;; marks are stored as symbolic values, so we have to dereference them for memq to work
319     ;; we wouldn't have to do this if gnus-summary-article-mark returned a symbol.
320     (dolist (mark spam-ham-marks)
321       (push (symbol-value mark) ham-mark-values))
322
323     (dolist (mark spam-spam-marks)
324       (push (symbol-value mark) spam-mark-values))
325
326     (while articles
327       (setq article (pop articles)
328             mark (gnus-summary-article-mark article))
329       (cond ((memq mark spam-mark-values) (push article spam-articles))
330             ((memq article gnus-newsgroup-saved))
331             ((memq mark ham-mark-values) (push article ham-articles))))
332     (when (and ham-articles ham-func)
333       (funcall ham-func ham-articles))
334     (when (and spam-articles spam-func)
335       (funcall spam-func spam-articles))))
336
337 \f
338 ;;;; Spam determination.
339
340
341 (defvar spam-list-of-checks
342   '((spam-use-blacklist  . spam-check-blacklist)
343     (spam-use-whitelist  . spam-check-whitelist)
344     (spam-use-bbdb       . spam-check-bbdb)
345     (spam-use-ifile      . spam-check-ifile)
346     (spam-use-blackholes . spam-check-blackholes)
347     (spam-use-bogofilter . spam-check-bogofilter))
348 "The spam-list-of-checks list contains pairs associating a parameter
349 variable with a spam checking function.  If the parameter variable is
350 true, then the checking function is called, and its value decides what
351 happens.  Each individual check may return `nil', `t', or a mailgroup
352 name.  The value `nil' means that the check does not yield a decision,
353 and so, that further checks are needed.  The value `t' means that the
354 message is definitely not spam, and that further spam checks should be
355 inhibited.  Otherwise, a mailgroup name is returned where the mail
356 should go, and further checks are also inhibited.  The usual mailgroup
357 name is the value of `spam-split-group', meaning that the message is
358 definitely a spam.")
359
360 (defun spam-split ()
361   "Split this message into the `spam' group if it is spam.
362 This function can be used as an entry in `nnmail-split-fancy', for
363 example like this: (: spam-split)
364
365 See the Info node `(gnus)Fancy Mail Splitting' for more details."
366   (interactive)
367
368   (let ((list-of-checks spam-list-of-checks)
369         decision)
370     (while (and list-of-checks (not decision))
371       (let ((pair (pop list-of-checks)))
372         (when (symbol-value (car pair))
373           (setq decision (funcall (cdr pair))))))
374     (if (eq decision t)
375         nil
376       decision)))
377 \f
378 ;;;; Blackholes.
379
380 (defun spam-check-blackholes ()
381   "Check the Received headers for blackholed relays."
382   (let ((headers (message-fetch-field "received"))
383         ips matches)
384     (when headers
385       (with-temp-buffer
386         (insert headers)
387         (goto-char (point-min))
388         (while (re-search-forward
389                 "\\[\\([0-9]+.[0-9]+.[0-9]+.[0-9]+\\)\\]" nil t)
390           (message "Blackhole search found host IP %s." (match-string 1))
391           (push (mapconcat 'identity
392                            (nreverse (split-string (match-string 1) "\\."))
393                            ".")
394                 ips)))
395       (dolist (server spam-blackhole-servers)
396         (dolist (ip ips)
397           (let ((query-string (concat ip "." server)))
398             (if spam-use-dig
399                 (let ((query-result (query-dig query-string)))
400                   (when query-result
401                     (message "spam detected with blackhole check of relay %s (dig query result '%s')" query-string query-result)
402                     (push (list ip server query-result)
403                           matches)))
404               ;; else, if not using dig.el
405               (when (query-dns query-string)
406                 (push (list ip server (query-dns query-string 'TXT))
407                       matches)))))))
408     (when matches
409       spam-split-group)))
410 \f
411 ;;;; Blacklists and whitelists.
412
413 (defvar spam-whitelist-cache nil)
414 (defvar spam-blacklist-cache nil)
415
416 (defun spam-enter-whitelist (address)
417   "Enter ADDRESS into the whitelist."
418   (interactive "sAddress: ")
419   (spam-enter-list address spam-whitelist)
420   (setq spam-whitelist-cache nil))
421
422 (defun spam-enter-blacklist (address)
423   "Enter ADDRESS into the blacklist."
424   (interactive "sAddress: ")
425   (spam-enter-list address spam-blacklist)
426   (setq spam-blacklist-cache nil))
427
428 (defun spam-enter-list (address file)
429   "Enter ADDRESS into the given FILE, either the whitelist or the blacklist."
430   (unless (file-exists-p (file-name-directory file))
431     (make-directory (file-name-directory file) t))
432   (save-excursion
433     (set-buffer
434      (find-file-noselect file))
435     (goto-char (point-max))
436     (unless (bobp)
437       (insert "\n"))
438     (insert address "\n")
439     (save-buffer)))
440
441 ;;; returns nil if the sender is in the whitelist, spam-split-group otherwise
442 (defun spam-check-whitelist ()
443   ;; FIXME!  Should it detect when file timestamps change?
444   (unless spam-whitelist-cache
445     (setq spam-whitelist-cache (spam-parse-list spam-whitelist)))
446   (if (spam-from-listed-p spam-whitelist-cache) nil spam-split-group))
447
448 ;;; original idea from Alexander Kotelnikov <sacha@giotto.sj.ru>
449 (condition-case nil
450     (progn
451       (require 'bbdb-com)
452       (defun spam-check-bbdb ()
453         "We want messages from people who are in the BBDB not to be split to spam"
454         (let ((who (message-fetch-field "from")))
455           (when who
456             (setq who (regexp-quote (cadr (gnus-extract-address-components who))))
457             (if (bbdb-search (bbdb-records) nil nil who) nil spam-split-group)))))
458   (file-error (setq spam-list-of-checks
459                     (delete (assoc 'spam-use-bbdb spam-list-of-checks)
460                             spam-list-of-checks))))
461
462 ;; TODO: add BBDB registration
463 (defun spam-BBDB-register-routine
464   (spam-generic-register-routine nil nil))
465
466 ;;; check the ifile backend; return nil if the mail was NOT classified as spam
467 ;;; TODO: we can't (require) ifile, because it will insinuate itself automatically
468 (defun spam-check-ifile ()
469   (let ((ifile-primary-spam-group spam-split-group))
470     (ifile-spam-filter nil)))
471
472 ;; TODO: add ifile registration
473 (defun spam-ifile-register-routine
474   (spam-generic-register-routine nil nil))
475
476 (defun spam-check-blacklist ()
477   ;; FIXME!  Should it detect when file timestamps change?
478   (unless spam-blacklist-cache
479     (setq spam-blacklist-cache (spam-parse-list spam-blacklist)))
480   (and (spam-from-listed-p spam-blacklist-cache) spam-split-group))
481
482 (eval-and-compile
483   (defalias 'spam-point-at-eol (if (fboundp 'point-at-eol)
484                                    'point-at-eol
485                                  'line-end-position)))
486
487 (defun spam-parse-list (file)
488   (when (file-readable-p file)
489     (let (contents address)
490       (with-temp-buffer
491         (insert-file-contents file)
492         (while (not (eobp))
493           (setq address (buffer-substring (point) (spam-point-at-eol)))
494           (forward-line 1)
495           (unless (zerop (length address))
496             (setq address (regexp-quote address))
497             (while (string-match "\\\\\\*" address)
498               (setq address (replace-match ".*" t t address)))
499             (push address contents))))
500       (nreverse contents))))
501
502 (defun spam-from-listed-p (cache)
503   (let ((from (message-fetch-field "from"))
504         found)
505     (while cache
506       (when (string-match (pop cache) from)
507         (setq found t
508               cache nil)))
509     found))
510
511 ;; TODO: add blacklist and whitelist registrations
512 (defun spam-blacklist-register-routine
513   (spam-generic-register-routine nil nil))
514 (defun spam-whitelist-register-routine
515   (spam-generic-register-routine nil nil))
516
517 \f
518 ;;;; Training via Bogofilter.   Last updated 2002-09-02.
519
520 ;;; See Paul Graham article, at `http://www.paulgraham.com/spam.html'.
521
522 ;;; This page is for those wanting to control spam with the help of Eric
523 ;;; Raymond's speedy Bogofilter, see http://www.tuxedo.org/~esr/bogofilter.
524 ;;; This has been tested with a locally patched copy of version 0.4.
525
526 ;;; Make sure Bogofilter is installed.  Bogofilter internally uses Judy fast
527 ;;; associative arrays, so you need to install Judy first, and Bogofilter
528 ;;; next.  Fetch both distributions by visiting the following links and
529 ;;; downloading the latest version of each:
530 ;;;
531 ;;;     http://sourceforge.net/projects/judy/
532 ;;;     http://www.tuxedo.org/~esr/bogofilter/
533 ;;;
534 ;;; Unpack the Judy distribution and enter its main directory.  Then do:
535 ;;;
536 ;;;     ./configure
537 ;;;     make
538 ;;;     make install
539 ;;;
540 ;;; You will likely need to become super-user for the last step.  Then, unpack
541 ;;; the Bogofilter distribution and enter its main directory:
542 ;;;
543 ;;;     make
544 ;;;     make install
545 ;;;
546 ;;; Here as well, you need to become super-user for the last step.  Now,
547 ;;; initialize your word lists by doing, under your own identity:
548 ;;;
549 ;;;     mkdir ~/.bogofilter
550 ;;;     touch ~/.bogofilter/badlist
551 ;;;     touch ~/.bogofilter/goodlist
552 ;;;
553 ;;; These two files are text files you may edit, but you normally don't!
554
555 ;;; The `M-d' command gets added to Gnus summary mode, marking current article
556 ;;; as spam, showing it with the `H' mark.  Whenever you see a spam article,
557 ;;; make sure to mark its summary line with `M-d' before leaving the group.
558 ;;; Some groups, as per variable `spam-junk-mailgroups' below, receive articles
559 ;;; from Gnus splitting on clues added by spam recognisers, so for these
560 ;;; groups, we tack an `H' mark at group entry for all summary lines which
561 ;;; would otherwise have no other mark.  Make sure to _remove_ `H' marks for
562 ;;; any article which is _not_ genuine spam, before leaving such groups: you
563 ;;; may use `M-u' to "unread" the article, or `d' for declaring it read the
564 ;;; non-spam way.  When you leave a group, all `H' marked articles, saved or
565 ;;; unsaved, are sent to Bogofilter which will study them as spam samples.
566
567 ;;; Messages may also be deleted in various other ways, and unless
568 ;;; `spam-ham-marks-form' gets overridden below, marks `R' and `r' for default
569 ;;; read or explicit delete, marks `X' and 'K' for automatic or explicit
570 ;;; kills, as well as mark `Y' for low scores, are all considered to be
571 ;;; associated with articles which are not spam.  This assumption might be
572 ;;; false, in particular if you use kill files or score files as means for
573 ;;; detecting genuine spam, you should then adjust `spam-ham-marks-form'.  When
574 ;;; you leave a group, all _unsaved_ articles bearing any the above marks are
575 ;;; sent to Bogofilter which will study these as not-spam samples.  If you
576 ;;; explicit kill a lot, you might sometimes end up with articles marked `K'
577 ;;; which you never saw, and which might accidentally contain spam.  Best is
578 ;;; to make sure that real spam is marked with `H', and nothing else.
579
580 ;;; All other marks do not contribute to Bogofilter pre-conditioning.  In
581 ;;; particular, ticked, dormant or souped articles are likely to contribute
582 ;;; later, when they will get deleted for real, so there is no need to use
583 ;;; them prematurely.  Explicitly expired articles do not contribute, command
584 ;;; `E' is a way to get rid of an article without Bogofilter ever seeing it.
585
586 ;;; In a word, with a minimum of care for associating the `H' mark for spam
587 ;;; articles only, Bogofilter training all gets fairly automatic.  You should
588 ;;; do this until you get a few hundreds of articles in each category, spam
589 ;;; or not.  The shell command `head -1 ~/.bogofilter/*' shows both article
590 ;;; counts.  The command `S S' in summary mode, either for debugging or for
591 ;;; curiosity, triggers Bogofilter into displaying in another buffer the
592 ;;; "spamicity" score of the current article (between 0.0 and 1.0), together
593 ;;; with the article words which most significantly contribute to the score.
594
595 ;;; The real way for using Bogofilter, however, is to have some use tool like
596 ;;; `procmail' for invoking it on message reception, then adding some
597 ;;; recognisable header in case of detected spam.  Gnus splitting rules might
598 ;;; later trip on these added headers and react by sorting such articles into
599 ;;; specific junk folders as per `spam-junk-mailgroups'.  Here is a possible
600 ;;; `.procmailrc' contents (still untested -- please tell me how it goes):
601 ;;;
602 ;;; :0HBf:
603 ;;; * ? bogofilter
604 ;;; | formail -bfI "X-Spam-Status: Yes"
605
606 (defun spam-check-bogofilter ()
607   ;; Dynamic spam check.  I do not know how to check the exit status,
608   ;; so instead, read `bogofilter -v' output.
609   (when (and spam-use-bogofilter spam-bogofilter-path)
610     (spam-bogofilter-articles nil "-v" (list (gnus-summary-article-number)))
611     (when (save-excursion
612             (set-buffer spam-bogofilter-output-buffer-name)
613             (goto-char (point-min))
614             (re-search-forward "Spamicity: \\(0\\.9\\|1\\.0\\)" nil t))
615       spam-split-group)))
616
617 (defun spam-bogofilter-score ()
618   "Use `bogofilter -v' on the current article.
619 This yields the 15 most discriminant words for this article and the
620 spamicity coefficient of each, and the overall article spamicity."
621   (interactive)
622   (when (and spam-use-bogofilter spam-bogofilter-path)
623     (spam-bogofilter-articles nil "-v" (list (gnus-summary-article-number)))
624     (with-current-buffer spam-bogofilter-output-buffer-name
625       (unless (zerop (buffer-size))
626         (if (<= (count-lines (point-min) (point-max)) 1)
627             (progn
628               (goto-char (point-max))
629               (when (bolp)
630                 (backward-char 1))
631               (message "%s" (buffer-substring (point-min) (point))))
632           (goto-char (point-min))
633           (display-buffer (current-buffer)))))))
634
635 (defun spam-bogofilter-register-routine ()
636   (let ((articles gnus-newsgroup-articles)
637         article mark ham-articles spam-articles spam-mark-values ham-mark-values)
638
639     ;; marks are stored as symbolic values, so we have to dereference them for memq to work
640     ;; we wouldn't have to do this if gnus-summary-article-mark returned a symbol.
641     (dolist (mark spam-ham-marks)
642       (push (symbol-value mark) ham-mark-values))
643
644     (dolist (mark spam-spam-marks)
645       (push (symbol-value mark) spam-mark-values))
646
647     (while articles
648       (setq article (pop articles)
649             mark (gnus-summary-article-mark article))
650       (cond ((memq mark spam-mark-values) (push article spam-articles))
651             ((memq article gnus-newsgroup-saved))
652             ((memq mark ham-mark-values) (push article ham-articles))))
653     (when ham-articles
654       (spam-bogofilter-articles "ham" "-n" ham-articles))
655     (when spam-articles
656       (spam-bogofilter-articles "SPAM" "-s" spam-articles))))
657
658 (defun spam-bogofilter-articles (type option articles)
659   (let ((output-buffer (get-buffer-create spam-bogofilter-output-buffer-name))
660         (article-copy (get-buffer-create " *Bogofilter Article Copy*"))
661         (remove-regexp (concat spam-bogofilter-spaminfo-header-regexp "\\|Xref:"))
662         (counter 0)
663         prefix process article)
664     (when type
665       (setq prefix (format "Studying %d articles as %s..." (length articles)
666                            type))
667       (message "%s" prefix))
668     (save-excursion (set-buffer output-buffer) (erase-buffer))
669     (setq process (start-process "bogofilter" output-buffer
670                                  spam-bogofilter-path "-F" option))
671     (process-kill-without-query process t)
672     (unwind-protect
673         (save-window-excursion
674           (while articles
675             (setq counter (1+ counter))
676             (when prefix
677               (message "%s %d" prefix counter))
678             (setq article (pop articles))
679             (gnus-summary-goto-subject article)
680             (gnus-summary-show-article t)
681             (gnus-eval-in-buffer-window article-copy
682               (insert-buffer-substring gnus-original-article-buffer)
683               ;; Remove spam classification redundant headers: they may induce
684               ;; unwanted biases in later analysis.
685               (message-remove-header remove-regexp t)
686               ;; Bogofilter really wants From envelopes for counting articles.
687               ;; Fake one at the beginning, make sure there will be no other.
688               (goto-char (point-min))
689               (if (looking-at "From ")
690                   (forward-line 1)
691                 (insert "From nobody " (current-time-string) "\n"))
692               (let (case-fold-search)
693                 (while (re-search-forward "^From " nil t)
694                   (beginning-of-line)
695                   (insert ">")))
696               (process-send-region process (point-min) (point-max))
697               (erase-buffer))))
698       ;; Sending the EOF is unwind-protected.  This is to prevent lost copies
699       ;; of `bogofilter', hung on reading their standard input, in case the
700       ;; whole registering process gets interrupted by the user.
701       (process-send-eof process))
702     (kill-buffer article-copy)
703     ;; Receive process output.  It sadly seems that we still have to protect
704     ;; ourselves against hung `bogofilter' processes.
705     (let ((status (process-status process))
706           (timeout (* 1000 spam-bogofilter-initial-timeout))
707           (quanta 200))                 ; also counted in milliseconds
708       (while (and (not (eq status 'exit)) (> timeout 0))
709         ;; `accept-process-output' timeout is counted in microseconds.
710         (setq timeout (if (accept-process-output process 0 (* 1000 quanta))
711                           (* 1000 spam-bogofilter-subsequent-timeout)
712                         (- timeout quanta))
713               status (process-status process)))
714       (if (eq status 'exit)
715           (when prefix
716             (message "%s done!" prefix))
717         ;; Sigh!  The process did time out...  Become brutal!
718         (interrupt-process process)
719         (message "%s %d INTERRUPTED!  (Article %d, status %s)"
720                  (or prefix "Bogofilter process...")
721                  counter article status)
722         ;; Give some time for user to read.  Sitting redisplays but gives up
723         ;; if input is pending.  Sleeping does not give up, but it does not
724         ;; redisplay either.  Mix both: let's redisplay and not give up.
725         (sit-for 1)
726         (sleep-for 3)))))
727
728 (provide 'spam)
729
730 ;;; spam.el ends here.