(spam-check-bogofilter-headers, spam-check-blackholes, spam-check-BBDB)
[gnus] / lisp / spam.el
1 ;;; spam.el --- Identifying spam
2 ;; Copyright (C) 2002, 2003 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 (eval-when-compile (require 'cl))
38
39 (require 'gnus-sum)
40
41 (require 'gnus-uu)                      ; because of key prefix issues
42 (require 'gnus) ; for the definitions of group content classification and spam processors
43 (require 'message)                      ;for the message-fetch-field functions
44
45 ;; for nnimap-split-download-body-default
46 (eval-when-compile (require 'nnimap))
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 query-dig
54 (eval-and-compile
55   (autoload 'query-dig "dig"))
56
57 ;; autoload spam-report
58 (eval-and-compile
59   (autoload 'spam-report-gmane "spam-report"))
60
61 ;; autoload query-dns
62 (eval-and-compile
63   (autoload 'query-dns "dns"))
64
65 ;;; Main parameters.
66
67 (defgroup spam nil
68   "Spam configuration.")
69
70 (defcustom spam-directory "~/News/spam/"
71   "Directory for spam whitelists and blacklists."
72   :type 'directory
73   :group 'spam)
74
75 (defcustom spam-move-spam-nonspam-groups-only t
76   "Whether spam should be moved in non-spam groups only.
77 When nil, only ham and unclassified groups will have their spam moved
78 to the spam-process-destination.  When t, spam will also be moved from
79 spam groups."
80   :type 'boolean
81   :group 'spam)
82
83 (defcustom spam-process-ham-in-nonham-groups nil
84   "Whether ham should be processed in non-ham groups."
85   :type 'boolean
86   :group 'spam)
87
88 (defcustom spam-process-ham-in-spam-groups nil
89   "Whether ham should be processed in spam groups."
90   :type 'boolean
91   :group 'spam)
92
93 (defcustom spam-mark-only-unseen-as-spam t
94   "Whether only unseen articles should be marked as spam in spam
95 groups.  When nil, all unread articles in a spam group are marked as
96 spam.  Set this if you want to leave an article unread in a spam group
97 without losing it to the automatic spam-marking process."
98   :type 'boolean
99   :group 'spam)
100
101 (defcustom spam-mark-ham-unread-before-move-from-spam-group nil
102   "Whether ham should be marked unread before it's moved out of a spam
103 group according to ham-process-destination.  This variable is an
104 official entry in the international Longest Variable Name
105 Competition."
106   :type 'boolean
107   :group 'spam)
108
109 (defcustom spam-whitelist (expand-file-name "whitelist" spam-directory)
110   "The location of the whitelist.
111 The file format is one regular expression per line.
112 The regular expression is matched against the address."
113   :type 'file
114   :group 'spam)
115
116 (defcustom spam-blacklist (expand-file-name "blacklist" spam-directory)
117   "The location of the blacklist.
118 The file format is one regular expression per line.
119 The regular expression is matched against the address."
120   :type 'file
121   :group 'spam)
122
123 (defcustom spam-use-dig t
124   "Whether query-dig should be used instead of query-dns."
125   :type 'boolean
126   :group 'spam)
127
128 (defcustom spam-use-blacklist nil
129   "Whether the blacklist should be used by spam-split."
130   :type 'boolean
131   :group 'spam)
132
133 (defcustom spam-use-whitelist nil
134   "Whether the whitelist should be used by spam-split."
135   :type 'boolean
136   :group 'spam)
137
138 (defcustom spam-use-whitelist-exclusive nil
139   "Whether whitelist-exclusive should be used by spam-split.
140 Exclusive whitelisting means that all messages from senders not in the whitelist
141 are considered spam."
142   :type 'boolean
143   :group 'spam)
144
145 (defcustom spam-use-blackholes nil
146   "Whether blackholes should be used by spam-split."
147   :type 'boolean
148   :group 'spam)
149
150 (defcustom spam-use-hashcash nil
151   "Whether hashcash payments should be detected by spam-split."
152   :type 'boolean
153   :group 'spam)
154
155 (defcustom spam-use-regex-headers nil
156   "Whether a header regular expression match should be used by spam-split.
157 Also see the variables `spam-regex-headers-spam' and `spam-regex-headers-ham'."
158   :type 'boolean
159   :group 'spam)
160
161 (defcustom spam-use-regex-body nil
162   "Whether a body regular expression match should be used by spam-split.
163 Also see the variables `spam-regex-body-spam' and `spam-regex-body-ham'."
164   :type 'boolean
165   :group 'spam)
166
167 (defcustom spam-use-bogofilter-headers nil
168   "Whether bogofilter headers should be used by spam-split.
169 Enable this if you pre-process messages with Bogofilter BEFORE Gnus sees them."
170   :type 'boolean
171   :group 'spam)
172
173 (defcustom spam-use-bogofilter nil
174   "Whether bogofilter should be invoked by spam-split.
175 Enable this if you want Gnus to invoke Bogofilter on new messages."
176   :type 'boolean
177   :group 'spam)
178
179 (defcustom spam-use-BBDB nil
180   "Whether BBDB should be used by spam-split."
181   :type 'boolean
182   :group 'spam)
183
184 (defcustom spam-use-BBDB-exclusive nil
185   "Whether BBDB-exclusive should be used by spam-split.
186 Exclusive BBDB means that all messages from senders not in the BBDB are 
187 considered spam."
188   :type 'boolean
189   :group 'spam)
190
191 (defcustom spam-use-ifile nil
192   "Whether ifile should be used by spam-split."
193   :type 'boolean
194   :group 'spam)
195
196 (defcustom spam-use-stat nil
197   "Whether spam-stat should be used by spam-split."
198   :type 'boolean
199   :group 'spam)
200
201 (defcustom spam-use-spamoracle nil
202   "Whether spamoracle should be used by spam-split."
203   :type 'boolean
204   :group 'spam)
205
206 (defcustom spam-install-hooks (or
207                                spam-use-dig
208                                spam-use-blacklist
209                                spam-use-whitelist 
210                                spam-use-whitelist-exclusive 
211                                spam-use-blackholes 
212                                spam-use-hashcash 
213                                spam-use-regex-headers 
214                                spam-use-regex-body 
215                                spam-use-bogofilter-headers 
216                                spam-use-bogofilter 
217                                spam-use-BBDB 
218                                spam-use-BBDB-exclusive 
219                                spam-use-ifile 
220                                spam-use-stat
221                                spam-use-spamoracle)
222   "Whether the spam hooks should be installed, default to t if one of
223 the spam-use-* variables is set."
224   :group 'gnus-registry
225   :type 'boolean)
226
227 (defcustom spam-split-group "spam"
228   "Group name where incoming spam should be put by spam-split."
229   :type 'string
230   :group 'spam)
231
232 ;;; TODO: deprecate this variable, it's confusing since it's a list of strings, not regular expressions
233 (defcustom spam-junk-mailgroups (cons spam-split-group '("mail.junk" "poste.pourriel"))
234   "Mailgroups with spam contents.
235 All unmarked article in such group receive the spam mark on group entry."
236   :type '(repeat (string :tag "Group"))
237   :group 'spam)
238
239 (defcustom spam-blackhole-servers '("bl.spamcop.net" "relays.ordb.org" 
240                                     "dev.null.dk" "relays.visi.com")
241   "List of blackhole servers."
242   :type '(repeat (string :tag "Server"))
243   :group 'spam)
244
245 (defcustom spam-blackhole-good-server-regex nil
246   "String matching IP addresses that should not be checked in the blackholes"
247   :type '(radio (const nil)
248                 (regexp :format "%t: %v\n" :size 0))
249   :group 'spam)
250
251 (defcustom spam-face 'gnus-splash-face
252   "Face for spam-marked articles"
253   :type 'face
254   :group 'spam)
255
256 (defcustom spam-regex-headers-spam '("^X-Spam-Flag: YES")
257   "Regular expression for positive header spam matches"
258   :type '(repeat (regexp :tag "Regular expression to match spam header"))
259   :group 'spam)
260
261 (defcustom spam-regex-headers-ham '("^X-Spam-Flag: NO")
262   "Regular expression for positive header ham matches"
263   :type '(repeat (regexp :tag "Regular expression to match ham header"))
264   :group 'spam)
265
266 (defcustom spam-regex-body-spam '()
267   "Regular expression for positive body spam matches"
268   :type '(repeat (regexp :tag "Regular expression to match spam body"))
269   :group 'spam)
270
271 (defcustom spam-regex-body-ham '()
272   "Regular expression for positive body ham matches"
273   :type '(repeat (regexp :tag "Regular expression to match ham body"))
274   :group 'spam)
275
276 (defgroup spam-ifile nil
277   "Spam ifile configuration."
278   :group 'spam)
279
280 (defcustom spam-ifile-path (executable-find "ifile")
281   "File path of the ifile executable program."
282   :type '(choice (file :tag "Location of ifile")
283                  (const :tag "ifile is not installed"))
284   :group 'spam-ifile)
285
286 (defcustom spam-ifile-database-path nil
287   "File path of the ifile database."
288   :type '(choice (file :tag "Location of the ifile database")
289                  (const :tag "Use the default"))
290   :group 'spam-ifile)
291
292 (defcustom spam-ifile-spam-category "spam"
293   "Name of the spam ifile category."  
294   :type 'string
295   :group 'spam-ifile)
296
297 (defcustom spam-ifile-ham-category nil
298   "Name of the ham ifile category.  If nil, the current group name will
299 be used."
300   :type '(choice (string :tag "Use a fixed category")
301                 (const :tag "Use the current group name"))
302   :group 'spam-ifile)
303
304 (defcustom spam-ifile-all-categories nil
305   "Whether the ifile check will return all categories, or just spam.
306 Set this to t if you want to use the spam-split invocation of ifile as
307 your main source of newsgroup names."
308   :type 'boolean
309   :group 'spam-ifile)
310
311 (defgroup spam-bogofilter nil
312   "Spam bogofilter configuration."
313   :group 'spam)
314
315 (defcustom spam-bogofilter-path (executable-find "bogofilter")
316   "File path of the Bogofilter executable program."
317   :type '(choice (file :tag "Location of bogofilter")
318                  (const :tag "Bogofilter is not installed"))
319   :group 'spam-bogofilter)
320
321 (defcustom spam-bogofilter-header "X-Bogosity"
322   "The header that Bogofilter inserts in messages."
323   :type 'string
324   :group 'spam-bogofilter)
325
326 (defcustom spam-bogofilter-spam-switch "-s"
327   "The switch that Bogofilter uses to register spam messages."
328   :type 'string
329   :group 'spam-bogofilter)
330
331 (defcustom spam-bogofilter-ham-switch "-n"
332   "The switch that Bogofilter uses to register ham messages."
333   :type 'string
334   :group 'spam-bogofilter)
335
336 (defcustom spam-bogofilter-bogosity-positive-spam-header "^\\(Yes\\|Spam\\)"
337   "The regex on `spam-bogofilter-header' for positive spam identification."
338   :type 'regexp
339   :group 'spam-bogofilter)
340
341 (defcustom spam-bogofilter-database-directory nil
342   "Directory path of the Bogofilter databases."
343   :type '(choice (directory :tag "Location of the Bogofilter database directory")
344                  (const :tag "Use the default"))
345   :group 'spam-ifile)
346
347 (defgroup spam-spamoracle nil
348   "Spam ifile configuration."
349   :group 'spam)
350
351 (defcustom spam-spamoracle-database nil 
352   "Location of spamoracle database file. When nil, use the default
353 spamoracle database."
354   :type '(choice (directory :tag "Location of spamoracle database file.")
355                  (const :tag "Use the default"))
356   :group 'spam-spamoracle)
357
358 (defcustom spam-spamoracle-binary (executable-find "spamoracle")
359   "Location of the spamoracle binary."
360   :type '(choice (directory :tag "Location of the spamoracle binary")
361                  (const :tag "Use the default"))
362   :group 'spam-spamoracle)
363
364 ;;; Key bindings for spam control.
365
366 (gnus-define-keys gnus-summary-mode-map
367   "St" spam-bogofilter-score
368   "Sx" gnus-summary-mark-as-spam
369   "Mst" spam-bogofilter-score
370   "Msx" gnus-summary-mark-as-spam
371   "\M-d" gnus-summary-mark-as-spam)
372
373 ;;; How to highlight a spam summary line.
374
375 ;; TODO: How do we redo this every time spam-face is customized?
376
377 (push '((eq mark gnus-spam-mark) . spam-face)
378       gnus-summary-highlight)
379
380 ;; convenience functions
381 (defun spam-group-ham-mark-p (group mark &optional spam)
382   (when (stringp group)
383     (let* ((marks (spam-group-ham-marks group spam))
384            (marks (if (symbolp mark) 
385                       marks 
386                     (mapcar 'symbol-value marks))))
387       (memq mark marks))))
388
389 (defun spam-group-spam-mark-p (group mark)
390   (spam-group-ham-mark-p group mark t))
391
392 (defun spam-group-ham-marks (group &optional spam)
393   (when (stringp group)
394     (let* ((marks (if spam
395                      (gnus-parameter-spam-marks group)
396                    (gnus-parameter-ham-marks group)))
397            (marks (car marks))
398            (marks (if (listp (car marks)) (car marks) marks)))
399       marks)))
400
401 (defun spam-group-spam-marks (group)
402   (spam-group-ham-marks group t))
403
404 (defun spam-group-spam-contents-p (group)
405   (if (stringp group)
406       (or (member group spam-junk-mailgroups)
407           (memq 'gnus-group-spam-classification-spam 
408                 (gnus-parameter-spam-contents group)))
409     nil))
410   
411 (defun spam-group-ham-contents-p (group)
412   (if (stringp group)
413       (memq 'gnus-group-spam-classification-ham 
414             (gnus-parameter-spam-contents group))
415     nil))
416
417 (defun spam-group-processor-p (group processor)
418   (if (and (stringp group)
419            (symbolp processor))
420       (member processor (car (gnus-parameter-spam-process group)))
421     nil))
422
423 (defun spam-group-spam-processor-report-gmane-p (group)
424   (spam-group-processor-p group 'gnus-group-spam-exit-processor-report-gmane))
425
426 (defun spam-group-spam-processor-bogofilter-p (group)
427   (spam-group-processor-p group 'gnus-group-spam-exit-processor-bogofilter))
428
429 (defun spam-group-spam-processor-blacklist-p (group)
430   (spam-group-processor-p group 'gnus-group-spam-exit-processor-blacklist))
431
432 (defun spam-group-spam-processor-ifile-p (group)
433   (spam-group-processor-p group 'gnus-group-spam-exit-processor-ifile))
434
435 (defun spam-group-ham-processor-ifile-p (group)
436   (spam-group-processor-p group 'gnus-group-ham-exit-processor-ifile))
437
438 (defun spam-group-spam-processor-spamoracle-p (group)
439   (spam-group-processor-p group 'gnus-group-spam-exit-processor-spamoracle))
440
441 (defun spam-group-ham-processor-bogofilter-p (group)
442   (spam-group-processor-p group 'gnus-group-ham-exit-processor-bogofilter))
443
444 (defun spam-group-spam-processor-stat-p (group)
445   (spam-group-processor-p group 'gnus-group-spam-exit-processor-stat))
446
447 (defun spam-group-ham-processor-stat-p (group)
448   (spam-group-processor-p group 'gnus-group-ham-exit-processor-stat))
449
450 (defun spam-group-ham-processor-whitelist-p (group)
451   (spam-group-processor-p group 'gnus-group-ham-exit-processor-whitelist))
452
453 (defun spam-group-ham-processor-BBDB-p (group)
454   (spam-group-processor-p group 'gnus-group-ham-exit-processor-BBDB))
455
456 (defun spam-group-ham-processor-copy-p (group)
457   (spam-group-processor-p group 'gnus-group-ham-exit-processor-copy))
458
459 (defun spam-group-ham-processor-spamoracle-p (group)
460   (spam-group-processor-p group 'gnus-group-ham-exit-processor-spamoracle))
461
462 ;;; Summary entry and exit processing.
463
464 (defun spam-summary-prepare ()
465   (spam-mark-junk-as-spam-routine))
466
467 ;; The spam processors are invoked for any group, spam or ham or neither
468 (defun spam-summary-prepare-exit ()
469   (unless gnus-group-is-exiting-without-update-p
470     (gnus-message 6 "Exiting summary buffer and applying spam rules")
471     (when (and spam-bogofilter-path
472                (spam-group-spam-processor-bogofilter-p gnus-newsgroup-name))
473       (gnus-message 5 "Registering spam with bogofilter")
474       (spam-bogofilter-register-spam-routine))
475   
476     (when (and spam-ifile-path
477                (spam-group-spam-processor-ifile-p gnus-newsgroup-name))
478       (gnus-message 5 "Registering spam with ifile")
479       (spam-ifile-register-spam-routine))
480   
481     (when (spam-group-spam-processor-spamoracle-p gnus-newsgroup-name)
482       (gnus-message 5 "Registering spam with spamoracle")
483       (spam-spamoracle-learn-spam))
484
485     (when (spam-group-spam-processor-stat-p gnus-newsgroup-name)
486       (gnus-message 5 "Registering spam with spam-stat")
487       (spam-stat-register-spam-routine))
488
489     (when (spam-group-spam-processor-blacklist-p gnus-newsgroup-name)
490       (gnus-message 5 "Registering spam with the blacklist")
491       (spam-blacklist-register-routine))
492
493     (when (spam-group-spam-processor-report-gmane-p gnus-newsgroup-name)
494       (gnus-message 5 "Registering spam with the Gmane report")
495       (spam-report-gmane-register-routine))
496
497     (if spam-move-spam-nonspam-groups-only      
498         (when (not (spam-group-spam-contents-p gnus-newsgroup-name))
499           (spam-mark-spam-as-expired-and-move-routine
500            (gnus-parameter-spam-process-destination gnus-newsgroup-name)))
501       (gnus-message 5 "Marking spam as expired and moving it to %s" gnus-newsgroup-name)
502       (spam-mark-spam-as-expired-and-move-routine 
503        (gnus-parameter-spam-process-destination gnus-newsgroup-name)))
504
505     ;; now we redo spam-mark-spam-as-expired-and-move-routine to only
506     ;; expire spam, in case the above did not expire them
507     (gnus-message 5 "Marking spam as expired without moving it")
508     (spam-mark-spam-as-expired-and-move-routine nil)
509
510     (when (or (spam-group-ham-contents-p gnus-newsgroup-name)
511               (and (spam-group-spam-contents-p gnus-newsgroup-name)
512                    spam-process-ham-in-spam-groups)
513               spam-process-ham-in-nonham-groups)
514       (when (spam-group-ham-processor-whitelist-p gnus-newsgroup-name)
515         (gnus-message 5 "Registering ham with the whitelist")
516         (spam-whitelist-register-routine))
517       (when (spam-group-ham-processor-ifile-p gnus-newsgroup-name)
518         (gnus-message 5 "Registering ham with ifile")
519         (spam-ifile-register-ham-routine))
520       (when (spam-group-ham-processor-bogofilter-p gnus-newsgroup-name)
521         (gnus-message 5 "Registering ham with Bogofilter")
522         (spam-bogofilter-register-ham-routine))
523       (when (spam-group-ham-processor-stat-p gnus-newsgroup-name)
524         (gnus-message 5 "Registering ham with spam-stat")
525         (spam-stat-register-ham-routine))
526       (when (spam-group-ham-processor-BBDB-p gnus-newsgroup-name)
527         (gnus-message 5 "Registering ham with the BBDB")
528         (spam-BBDB-register-routine))
529       (when (spam-group-ham-processor-spamoracle-p gnus-newsgroup-name)
530         (gnus-message 5 "Registering ham with spamoracle")
531         (spam-spamoracle-learn-ham)))
532
533     (when (spam-group-ham-processor-copy-p gnus-newsgroup-name)
534       (gnus-message 5 "Copying ham")
535       (spam-ham-move-routine
536        (gnus-parameter-ham-process-destination gnus-newsgroup-name) t))
537
538     ;; now move all ham articles out of spam groups
539     (when (spam-group-spam-contents-p gnus-newsgroup-name)
540       (gnus-message 5 "Moving ham messages from spam group")
541       (spam-ham-move-routine
542        (gnus-parameter-ham-process-destination gnus-newsgroup-name)))))
543
544 (defun spam-mark-junk-as-spam-routine ()
545   ;; check the global list of group names spam-junk-mailgroups and the
546   ;; group parameters
547   (when (spam-group-spam-contents-p gnus-newsgroup-name)
548     (gnus-message 5 "Marking %s articles as spam"
549                   (if spam-mark-only-unseen-as-spam 
550                       "unseen"
551                     "unread"))
552     (let ((articles (if spam-mark-only-unseen-as-spam 
553                         gnus-newsgroup-unseen
554                       gnus-newsgroup-unreads)))
555       (dolist (article articles)
556         (gnus-summary-mark-article article gnus-spam-mark)))))
557
558 (defun spam-mark-spam-as-expired-and-move-routine (&optional group)
559   (gnus-summary-kill-process-mark)
560   (let ((articles gnus-newsgroup-articles)
561         article tomove)
562     (dolist (article articles)
563       (when (eq (gnus-summary-article-mark article) gnus-spam-mark)
564         (gnus-summary-mark-article article gnus-expirable-mark)
565         (push article tomove)))
566
567     ;; now do the actual move
568     (when (and tomove
569                (stringp group))
570       (dolist (article tomove)
571         (gnus-summary-set-process-mark article))
572       (when tomove (gnus-summary-move-article nil group))))
573   (gnus-summary-yank-process-mark))
574  
575 (defun spam-ham-move-routine (&optional group copy)
576   (gnus-summary-kill-process-mark)
577   (let ((articles gnus-newsgroup-articles)
578         article mark tomove)
579     (when (stringp group)               ; this routine will do nothing
580                                         ; without a valid group
581       (dolist (article articles)
582         (when (spam-group-ham-mark-p gnus-newsgroup-name
583                                      (gnus-summary-article-mark article))
584           (push article tomove)))
585
586       ;; now do the actual move
587       (when tomove
588         (dolist (article tomove)
589           (when spam-mark-ham-unread-before-move-from-spam-group
590             (gnus-summary-mark-article article gnus-unread-mark))           
591           (gnus-summary-set-process-mark article))
592         (if copy
593             (gnus-summary-copy-article nil group)
594           (gnus-summary-move-article nil group)))))
595   (gnus-summary-yank-process-mark))
596  
597 (defun spam-generic-register-routine (spam-func ham-func)
598   (let ((articles gnus-newsgroup-articles)
599         article mark ham-articles spam-articles)
600
601     (while articles
602       (setq article (pop articles)
603             mark (gnus-summary-article-mark article))
604       (cond ((spam-group-spam-mark-p gnus-newsgroup-name mark) 
605              (push article spam-articles))
606             ((memq article gnus-newsgroup-saved))
607             ((spam-group-ham-mark-p gnus-newsgroup-name mark)
608              (push article ham-articles))))
609
610     (when (and ham-articles ham-func)
611       (mapc ham-func ham-articles))     ; we use mapc because unlike
612                                         ; mapcar it discards the
613                                         ; return values
614     (when (and spam-articles spam-func)
615       (mapc spam-func spam-articles)))) ; we use mapc because unlike
616                                         ; mapcar it discards the
617                                         ; return values
618
619 (eval-and-compile
620   (defalias 'spam-point-at-eol (if (fboundp 'point-at-eol)
621                                    'point-at-eol
622                                  'line-end-position)))
623
624 (defun spam-get-article-as-string (article)
625   (let ((article-buffer (spam-get-article-as-buffer article))
626                         article-string)
627     (when article-buffer
628       (save-window-excursion
629         (set-buffer article-buffer)
630         (setq article-string (buffer-string))))
631   article-string))
632
633 (defun spam-get-article-as-buffer (article)
634   (let ((article-buffer))
635     (when (numberp article)
636       (save-window-excursion
637         (gnus-summary-goto-subject article)
638         (gnus-summary-show-article t)
639         (setq article-buffer (get-buffer gnus-article-buffer))))
640     article-buffer))
641
642 ;; disabled for now
643 ;; (defun spam-get-article-as-filename (article)
644 ;;   (let ((article-filename))
645 ;;     (when (numberp article)
646 ;;       (nnml-possibly-change-directory (gnus-group-real-name gnus-newsgroup-name))
647 ;;       (setq article-filename (expand-file-name (int-to-string article) nnml-current-directory)))
648 ;;     (if (file-exists-p article-filename)
649 ;;      article-filename
650 ;;       nil)))
651
652 (defun spam-fetch-field-from-fast (article)
653   "Fetch the `from' field quickly, using the internal gnus-data-list function"
654   (if (and (numberp article)
655            (assoc article (gnus-data-list nil)))
656       (mail-header-from (gnus-data-header (assoc article (gnus-data-list nil))))
657     nil))
658
659 (defun spam-fetch-field-subject-fast (article)
660   "Fetch the `subject' field quickly, using the internal gnus-data-list function"
661   (if (and (numberp article)
662            (assoc article (gnus-data-list nil)))
663       (mail-header-subject (gnus-data-header (assoc article (gnus-data-list nil))))
664     nil))
665
666 \f
667 ;;;; Spam determination.
668
669 (defvar spam-list-of-checks
670   '((spam-use-blacklist                 .       spam-check-blacklist)
671     (spam-use-regex-headers             .       spam-check-regex-headers)
672     (spam-use-regex-body                .       spam-check-regex-body)
673     (spam-use-whitelist                 .       spam-check-whitelist)
674     (spam-use-BBDB                      .       spam-check-BBDB)
675     (spam-use-ifile                     .       spam-check-ifile)
676     (spam-use-spamoracle                .       spam-check-spamoracle)
677     (spam-use-stat                      .       spam-check-stat)
678     (spam-use-blackholes                .       spam-check-blackholes)
679     (spam-use-hashcash                  .       spam-check-hashcash)
680     (spam-use-bogofilter-headers        .       spam-check-bogofilter-headers)
681     (spam-use-bogofilter                .       spam-check-bogofilter))
682 "The spam-list-of-checks list contains pairs associating a parameter
683 variable with a spam checking function.  If the parameter variable is
684 true, then the checking function is called, and its value decides what
685 happens.  Each individual check may return nil, t, or a mailgroup
686 name.  The value nil means that the check does not yield a decision,
687 and so, that further checks are needed.  The value t means that the
688 message is definitely not spam, and that further spam checks should be
689 inhibited.  Otherwise, a mailgroup name is returned where the mail
690 should go, and further checks are also inhibited.  The usual mailgroup
691 name is the value of `spam-split-group', meaning that the message is
692 definitely a spam.")
693
694 (defvar spam-list-of-statistical-checks
695   '(spam-use-ifile spam-use-regex-body spam-use-stat spam-use-bogofilter spam-use-spamoracle)
696 "The spam-list-of-statistical-checks list contains all the mail
697 splitters that need to have the full message body available.")
698
699 ;;;TODO: modify to invoke self with each specific check if invoked without specific checks
700 (defun spam-split (&rest specific-checks)
701   "Split this message into the `spam' group if it is spam.
702 This function can be used as an entry in `nnmail-split-fancy', for
703 example like this: (: spam-split).  It can take checks as parameters.
704
705 See the Info node `(gnus)Fancy Mail Splitting' for more details."
706   (interactive)
707   (save-excursion
708     (save-restriction
709       (dolist (check spam-list-of-statistical-checks)
710         (when (symbol-value check)
711           (widen)
712           (gnus-message 8 "spam-split: widening the buffer (%s requires it)"
713                         (symbol-name check))
714           (return)))
715       ;;   (progn (widen) (debug (buffer-string)))
716       (let ((list-of-checks spam-list-of-checks)
717             decision)
718         (while (and list-of-checks (not decision))
719           (let ((pair (pop list-of-checks)))
720             (when (and (symbol-value (car pair))
721                        (or (null specific-checks)
722                            (memq (car pair) specific-checks)))
723               (gnus-message 5 "spam-split: calling the %s function" (symbol-name (cdr pair)))
724               (setq decision (funcall (cdr pair))))))
725         (if (eq decision t)
726             nil
727           decision)))))
728   
729 (defun spam-setup-widening ()
730   (dolist (check spam-list-of-statistical-checks)
731     (when (symbol-value check)
732       (setq nnimap-split-download-body-default t))))
733
734 \f
735 ;;;; Regex body
736
737 (defun spam-check-regex-body ()
738   (let ((spam-regex-headers-ham spam-regex-body-ham)
739         (spam-regex-headers-spam spam-regex-body-spam))
740     (spam-check-regex-headers t)))
741
742 \f
743 ;;;; Regex headers
744
745 (defun spam-check-regex-headers (&optional body)
746   (let ((type (if body "body" "header"))
747          ret found)
748     (dolist (h-regex spam-regex-headers-ham)
749       (unless found
750         (goto-char (point-min))
751         (when (re-search-forward h-regex nil t)
752           (message "Ham regex %s search positive." type)
753           (setq found t))))
754     (dolist (s-regex spam-regex-headers-spam)
755       (unless found
756         (goto-char (point-min))
757         (when (re-search-forward s-regex nil t)
758           (message "Spam regex %s search positive." type)
759           (setq found t)
760           (setq ret spam-split-group))))
761     ret))
762
763 \f
764 ;;;; Blackholes.
765
766 (defun spam-check-blackholes ()
767   "Check the Received headers for blackholed relays."
768   (let ((headers (nnmail-fetch-field "received"))
769         ips matches)
770     (when headers
771       (with-temp-buffer
772         (insert headers)
773         (goto-char (point-min))
774         (gnus-message 5 "Checking headers for relay addresses")
775         (while (re-search-forward
776                 "\\[\\([0-9]+.[0-9]+.[0-9]+.[0-9]+\\)\\]" nil t)
777           (gnus-message 9 "Blackhole search found host IP %s." (match-string 1))
778           (push (mapconcat 'identity
779                            (nreverse (split-string (match-string 1) "\\."))
780                            ".")
781                 ips)))
782       (dolist (server spam-blackhole-servers)
783         (dolist (ip ips)
784           (unless (and spam-blackhole-good-server-regex
785                        (string-match spam-blackhole-good-server-regex ip))
786             (unless matches
787               (let ((query-string (concat ip "." server)))
788                 (if spam-use-dig
789                     (let ((query-result (query-dig query-string)))
790                       (when query-result
791                         (gnus-message 5 "(DIG): positive blackhole check '%s'" 
792                                       query-result)
793                         (push (list ip server query-result)
794                               matches)))
795                   ;; else, if not using dig.el
796                   (when (query-dns query-string)
797                     (gnus-message 5 "positive blackhole check")
798                     (push (list ip server (query-dns query-string 'TXT))
799                           matches)))))))))
800     (when matches
801       spam-split-group)))
802 \f
803 ;;;; Hashcash.
804
805 (condition-case nil
806     (progn
807       (require 'hashcash)
808       
809       (defun spam-check-hashcash ()
810         "Check the headers for hashcash payments."
811         (mail-check-payment)))          ;mail-check-payment returns a boolean
812
813   (file-error (progn
814                 (defalias 'mail-check-payment 'ignore)
815                 (defalias 'spam-check-hashcash 'ignore))))
816 \f
817 ;;;; BBDB 
818
819 ;;; original idea for spam-check-BBDB from Alexander Kotelnikov
820 ;;; <sacha@giotto.sj.ru>
821
822 ;; all this is done inside a condition-case to trap errors
823
824 (condition-case nil
825     (progn
826       (require 'bbdb)
827       (require 'bbdb-com)
828       
829   (defun spam-enter-ham-BBDB (from)
830     "Enter an address into the BBDB; implies ham (non-spam) sender"
831     (when (stringp from)
832       (let* ((parsed-address (gnus-extract-address-components from))
833              (name (or (car parsed-address) "Ham Sender"))
834              (net-address (car (cdr parsed-address))))
835         (gnus-message 5 "Adding address %s to BBDB" from)
836         (when (and net-address
837                    (not (bbdb-search-simple nil net-address)))
838           (bbdb-create-internal name nil net-address nil nil 
839                                 "ham sender added by spam.el")))))
840
841   (defun spam-BBDB-register-routine ()
842     (spam-generic-register-routine 
843      ;; spam function
844      nil
845      ;; ham function
846      (lambda (article)
847        (spam-enter-ham-BBDB (spam-fetch-field-from-fast article)))))
848
849   (defun spam-check-BBDB ()
850     "Mail from people in the BBDB is classified as ham or non-spam"
851     (let ((who (nnmail-fetch-field "from")))
852       (when who
853         (setq who (cadr (gnus-extract-address-components who)))
854         (if (bbdb-search-simple nil who)
855             t 
856           (if spam-use-BBDB-exclusive
857               spam-split-group
858             nil))))))
859
860   (file-error (progn
861                 (defalias 'bbdb-search-simple 'ignore)
862                 (defalias 'spam-check-BBDB 'ignore)
863                 (defalias 'spam-BBDB-register-routine 'ignore)
864                 (defalias 'spam-enter-ham-BBDB 'ignore)
865                 (defalias 'bbdb-create-internal 'ignore)
866                 (defalias 'bbdb-records 'ignore))))
867
868 \f
869 ;;;; ifile
870
871 ;;; check the ifile backend; return nil if the mail was NOT classified
872 ;;; as spam
873
874 (defun spam-get-ifile-database-parameter ()
875   "Get the command-line parameter for ifile's database from spam-ifile-database-path."
876   (if spam-ifile-database-path
877       (format "--db-file=%s" spam-ifile-database-path)
878     nil))
879     
880 (defun spam-check-ifile ()
881   "Check the ifile backend for the classification of this message"
882   (let ((article-buffer-name (buffer-name)) 
883         category return)
884     (with-temp-buffer
885       (let ((temp-buffer-name (buffer-name))
886             (db-param (spam-get-ifile-database-parameter)))
887         (save-excursion
888           (set-buffer article-buffer-name)
889           (if db-param
890               (call-process-region (point-min) (point-max) spam-ifile-path
891                                    nil temp-buffer-name nil "-q" "-c" db-param)
892             (call-process-region (point-min) (point-max) spam-ifile-path
893                                  nil temp-buffer-name nil "-q" "-c")))
894         (goto-char (point-min))
895         (if (not (eobp))
896             (setq category (buffer-substring (point) (spam-point-at-eol))))
897         (when (not (zerop (length category))) ; we need a category here
898           (if spam-ifile-all-categories
899               (setq return category)
900             ;; else, if spam-ifile-all-categories is not set...
901             (when (string-equal spam-ifile-spam-category category)
902               (setq return spam-split-group))))))
903     return))
904
905 (defun spam-ifile-register-with-ifile (article-string category)
906   "Register an article, given as a string, with a category.
907 Uses `gnus-newsgroup-name' if category is nil (for ham registration)."
908   (when (stringp article-string)
909     (let ((category (or category gnus-newsgroup-name))
910           (db-param (spam-get-ifile-database-parameter)))
911       (with-temp-buffer
912         (insert article-string)
913         (if db-param
914             (call-process-region (point-min) (point-max) spam-ifile-path 
915                                  nil nil nil 
916                                  "-h" "-i" category db-param)
917           (call-process-region (point-min) (point-max) spam-ifile-path 
918                                nil nil nil 
919                                "-h" "-i" category))))))
920
921 (defun spam-ifile-register-spam-routine ()
922   (spam-generic-register-routine 
923    (lambda (article)
924      (spam-ifile-register-with-ifile 
925       (spam-get-article-as-string article) spam-ifile-spam-category))
926    nil))
927
928 (defun spam-ifile-register-ham-routine ()
929   (spam-generic-register-routine 
930    nil
931    (lambda (article)
932      (spam-ifile-register-with-ifile 
933       (spam-get-article-as-string article) spam-ifile-ham-category))))
934
935 \f
936 ;;;; spam-stat
937
938 (condition-case nil
939     (progn
940       (let ((spam-stat-install-hooks nil))
941         (require 'spam-stat))
942       
943       (defun spam-check-stat ()
944         "Check the spam-stat backend for the classification of this message"
945         (let ((spam-stat-split-fancy-spam-group spam-split-group) ; override
946               (spam-stat-buffer (buffer-name)) ; stat the current buffer
947               category return)
948           (spam-stat-split-fancy)))
949
950       (defun spam-stat-register-spam-routine ()
951         (spam-generic-register-routine 
952          (lambda (article)
953            (let ((article-string (spam-get-article-as-string article)))
954              (with-temp-buffer
955                (insert article-string)
956                (spam-stat-buffer-is-spam))))
957          nil))
958
959       (defun spam-stat-register-ham-routine ()
960         (spam-generic-register-routine 
961          nil
962          (lambda (article)
963            (let ((article-string (spam-get-article-as-string article)))
964              (with-temp-buffer
965                (insert article-string)
966                (spam-stat-buffer-is-non-spam))))))
967
968       (defun spam-maybe-spam-stat-load ()
969         (when spam-use-stat (spam-stat-load)))
970       
971       (defun spam-maybe-spam-stat-save ()
972         (when spam-use-stat (spam-stat-save))))
973
974   (file-error (progn
975                 (defalias 'spam-maybe-spam-stat-load 'ignore)
976                 (defalias 'spam-maybe-spam-stat-save 'ignore)
977                 (defalias 'spam-stat-register-ham-routine 'ignore)
978                 (defalias 'spam-stat-register-spam-routine 'ignore)
979                 (defalias 'spam-stat-buffer-is-spam 'ignore)
980                 (defalias 'spam-stat-buffer-is-non-spam 'ignore)
981                 (defalias 'spam-stat-split-fancy 'ignore)
982                 (defalias 'spam-stat-load 'ignore)
983                 (defalias 'spam-stat-save 'ignore)
984                 (defalias 'spam-check-stat 'ignore))))
985
986 \f
987
988 ;;;; Blacklists and whitelists.
989
990 (defvar spam-whitelist-cache nil)
991 (defvar spam-blacklist-cache nil)
992
993 (defun spam-enter-whitelist (address)
994   "Enter ADDRESS into the whitelist."
995   (interactive "sAddress: ")
996   (spam-enter-list address spam-whitelist)
997   (setq spam-whitelist-cache nil))
998
999 (defun spam-enter-blacklist (address)
1000   "Enter ADDRESS into the blacklist."
1001   (interactive "sAddress: ")
1002   (spam-enter-list address spam-blacklist)
1003   (setq spam-blacklist-cache nil))
1004
1005 (defun spam-enter-list (address file)
1006   "Enter ADDRESS into the given FILE, either the whitelist or the blacklist."
1007   (unless (file-exists-p (file-name-directory file))
1008     (make-directory (file-name-directory file) t))
1009   (save-excursion
1010     (set-buffer
1011      (find-file-noselect file))
1012     (goto-char (point-min))
1013     (unless (re-search-forward (regexp-quote address) nil t)
1014       (goto-char (point-max))
1015       (unless (bobp)
1016         (insert "\n"))
1017       (insert address "\n")
1018       (save-buffer))))
1019
1020 ;;; returns t if the sender is in the whitelist, nil or spam-split-group otherwise
1021 (defun spam-check-whitelist ()
1022   ;; FIXME!  Should it detect when file timestamps change?
1023   (unless spam-whitelist-cache
1024     (setq spam-whitelist-cache (spam-parse-list spam-whitelist)))
1025   (if (spam-from-listed-p spam-whitelist-cache) 
1026       t
1027     (if spam-use-whitelist-exclusive
1028         spam-split-group
1029       nil)))
1030
1031 (defun spam-check-blacklist ()
1032   ;; FIXME!  Should it detect when file timestamps change?
1033   (unless spam-blacklist-cache
1034     (setq spam-blacklist-cache (spam-parse-list spam-blacklist)))
1035   (and (spam-from-listed-p spam-blacklist-cache) spam-split-group))
1036
1037 (defun spam-parse-list (file)
1038   (when (file-readable-p file)
1039     (let (contents address)
1040       (with-temp-buffer
1041         (insert-file-contents file)
1042         (while (not (eobp))
1043           (setq address (buffer-substring (point) (spam-point-at-eol)))
1044           (forward-line 1)
1045           ;; insert the e-mail address if detected, otherwise the raw data
1046           (unless (zerop (length address))
1047             (let ((pure-address (cadr (gnus-extract-address-components address))))
1048               (push (or pure-address address) contents)))))
1049       (nreverse contents))))
1050
1051 (defun spam-from-listed-p (cache)
1052   (let ((from (nnmail-fetch-field "from"))
1053         found)
1054     (while cache
1055       (let ((address (pop cache)))
1056         (unless (zerop (length address)) ; 0 for a nil address too
1057           (setq address (regexp-quote address))
1058           ;; fix regexp-quote's treatment of user-intended regexes
1059           (while (string-match "\\\\\\*" address)
1060             (setq address (replace-match ".*" t t address))))
1061         (when (and address (string-match address from))
1062           (setq found t
1063                 cache nil))))
1064     found))
1065
1066 (defun spam-blacklist-register-routine ()
1067   (spam-generic-register-routine 
1068    ;; the spam function
1069    (lambda (article)
1070      (let ((from (spam-fetch-field-from-fast article)))
1071        (when (stringp from)
1072            (spam-enter-blacklist from))))
1073    ;; the ham function
1074    nil))
1075
1076 (defun spam-whitelist-register-routine ()
1077   (spam-generic-register-routine 
1078    ;; the spam function
1079    nil 
1080    ;; the ham function
1081    (lambda (article)
1082      (let ((from (spam-fetch-field-from-fast article)))
1083        (when (stringp from)
1084            (spam-enter-whitelist from))))))
1085
1086 \f
1087 ;;;; Spam-report glue
1088 (defun spam-report-gmane-register-routine ()
1089   (spam-generic-register-routine
1090    'spam-report-gmane
1091    nil))
1092
1093 \f
1094 ;;;; Bogofilter
1095 (defun spam-check-bogofilter-headers (&optional score)
1096   (let ((header (nnmail-fetch-field spam-bogofilter-header)))
1097     (when header                        ; return nil when no header
1098       (if score                         ; scoring mode
1099           (if (string-match "spamicity=\\([0-9.]+\\)" header)
1100               (match-string 1 header)
1101             "0")
1102         ;; spam detection mode
1103         (when (string-match spam-bogofilter-bogosity-positive-spam-header
1104                             header)
1105           spam-split-group)))))
1106
1107 ;; return something sensible if the score can't be determined
1108 (defun spam-bogofilter-score ()
1109   "Get the Bogofilter spamicity score"
1110   (interactive)
1111   (save-window-excursion
1112     (gnus-summary-show-article t)
1113     (set-buffer gnus-article-buffer)
1114     (let ((score (or (spam-check-bogofilter-headers t)
1115                      (spam-check-bogofilter t))))
1116       (message "Spamicity score %s" score)
1117       (or score "0"))
1118     (gnus-summary-show-article)))
1119
1120 (defun spam-check-bogofilter (&optional score)
1121   "Check the Bogofilter backend for the classification of this message"
1122   (let ((article-buffer-name (buffer-name)) 
1123         return)
1124     (with-temp-buffer
1125       (let ((temp-buffer-name (buffer-name)))
1126         (save-excursion
1127           (set-buffer article-buffer-name)
1128           (if spam-bogofilter-database-directory
1129               (call-process-region (point-min) (point-max) 
1130                                    spam-bogofilter-path
1131                                    nil temp-buffer-name nil "-v"
1132                                    "-d" spam-bogofilter-database-directory)
1133             (call-process-region (point-min) (point-max) spam-bogofilter-path
1134                                  nil temp-buffer-name nil "-v")))
1135         (setq return (spam-check-bogofilter-headers score))))
1136     return))
1137
1138 (defun spam-bogofilter-register-with-bogofilter (article-string spam)
1139   "Register an article, given as a string, as spam or non-spam."
1140   (when (stringp article-string)
1141     (let ((switch (if spam spam-bogofilter-spam-switch 
1142                     spam-bogofilter-ham-switch)))
1143       (with-temp-buffer
1144         (insert article-string)
1145         (if spam-bogofilter-database-directory
1146             (call-process-region (point-min) (point-max) 
1147                                  spam-bogofilter-path
1148                                  nil nil nil "-v" switch
1149                                  "-d" spam-bogofilter-database-directory)
1150           (call-process-region (point-min) (point-max) spam-bogofilter-path
1151                                nil nil nil "-v" switch))))))
1152
1153 (defun spam-bogofilter-register-spam-routine ()
1154   (spam-generic-register-routine 
1155    (lambda (article)
1156      (spam-bogofilter-register-with-bogofilter
1157       (spam-get-article-as-string article) t))
1158    nil))
1159
1160 (defun spam-bogofilter-register-ham-routine ()
1161   (spam-generic-register-routine 
1162    nil
1163    (lambda (article)
1164      (spam-bogofilter-register-with-bogofilter
1165       (spam-get-article-as-string article) nil))))
1166
1167 \f
1168 ;;;; spamoracle
1169 (defun spam-check-spamoracle ()
1170   "Run spamoracle on an article to determine whether it's spam."
1171   (let ((article-buffer-name (buffer-name)))
1172     (with-temp-buffer
1173       (let ((temp-buffer-name (buffer-name)))
1174         (save-excursion
1175           (set-buffer article-buffer-name)
1176           (let ((status 
1177                  (apply 'call-process-region 
1178                         (point-min) (point-max)
1179                         spam-spamoracle-binary 
1180                         nil temp-buffer-name nil
1181                         (if spam-spamoracle-database
1182                             `("-f" ,spam-spamoracle-database "mark")
1183                           '("mark")))))
1184             (if (zerop status)
1185                 (progn
1186                   (set-buffer temp-buffer-name)
1187                   (goto-char (point-min))
1188                   (when (re-search-forward "^X-Spam: yes;" nil t)
1189                     spam-split-group))
1190               (error "Error running spamoracle" status))))))))
1191
1192 (defun spam-spamoracle-learn (article article-is-spam-p)
1193   "Run spamoracle in training mode."
1194   (with-temp-buffer
1195     (let ((temp-buffer-name (buffer-name)))
1196       (save-excursion
1197         (goto-char (point-min))
1198         (insert (spam-get-article-as-string article))
1199         (let* ((arg (if article-is-spam-p "-spam" "-good"))
1200                (status 
1201                 (apply 'call-process-region
1202                        (point-min) (point-max)
1203                        spam-spamoracle-binary
1204                        nil temp-buffer-name nil
1205                        (if spam-spamoracle-database
1206                            `("-f" ,spam-spamoracle-database 
1207                              "add" ,arg)
1208                          `("add" ,arg)))))
1209           (when (not (zerop status))
1210             (error "Error running spamoracle" status)))))))
1211   
1212 (defun spam-spamoracle-learn-ham ()
1213   (spam-generic-register-routine 
1214    nil
1215    (lambda (article)
1216      (spam-spamoracle-learn article nil))))
1217
1218 (defun spam-spamoracle-learn-spam ()
1219   (spam-generic-register-routine 
1220    (lambda (article)
1221      (spam-spamoracle-learn article t))
1222    nil))
1223 \f
1224 ;;;; Hooks
1225
1226 ;;;###autoload
1227 (defun spam-install-hooks-function ()
1228   "Install the spam.el hooks"
1229   (interactive)
1230   ;; Add hooks for loading and saving the spam stats
1231   (when spam-use-stat
1232     (add-hook 'gnus-save-newsrc-hook 'spam-maybe-spam-stat-save)
1233     (add-hook 'gnus-get-top-new-news-hook 'spam-maybe-spam-stat-load)
1234     (add-hook 'gnus-startup-hook 'spam-maybe-spam-stat-load))
1235   (add-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
1236   (add-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
1237   (add-hook 'gnus-get-new-news-hook 'spam-setup-widening))
1238
1239 (defun spam-unload-hook ()
1240   "Uninstall the spam.el hooks"
1241   (interactive)
1242   (remove-hook 'gnus-save-newsrc-hook 'spam-maybe-spam-stat-save)
1243   (remove-hook 'gnus-get-top-new-news-hook 'spam-maybe-spam-stat-load)
1244   (remove-hook 'gnus-startup-hook 'spam-maybe-spam-stat-load)
1245   (remove-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
1246   (remove-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
1247   (remove-hook 'gnus-get-new-news-hook 'spam-setup-widening))
1248
1249 (when spam-install-hooks
1250   (spam-install-hooks-function))
1251
1252 (provide 'spam)
1253
1254 ;;; spam.el ends here.