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