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