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