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