Revision: emacs@sv.gnu.org/gnus--devo--0--patch-25
[gnus] / lisp / spam.el
1 ;;; spam.el --- Identifying spam
2
3 ;; Copyright (C) 2002, 2003, 2004, 2005, 2006 Free Software Foundation, Inc.
4
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6 ;; Maintainer: Ted Zlatanov <tzz@lifelogs.com>
7 ;; Keywords: network, spam, mail, bogofilter, BBDB, dspam, dig, whitelist, blacklist, gmane, hashcash, spamassassin, bsfilter, ifile, stat, crm114, spamoracle
8
9 ;; This file is part of GNU Emacs.
10
11 ;; GNU Emacs is free software; you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation; either version 2, or (at your option)
14 ;; any later version.
15
16 ;; GNU Emacs is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 ;; GNU General Public License for more details.
20
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
23 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24 ;; Boston, MA 02110-1301, USA.
25
26 ;;; Commentary:
27
28 ;;; This module addresses a few aspects of spam control under Gnus.  Page
29 ;;; breaks are used for grouping declarations and documentation relating to
30 ;;; each particular aspect.
31
32 ;;; The integration with Gnus is not yet complete.  See various `FIXME'
33 ;;; comments, below, for supplementary explanations or discussions.
34
35 ;;; Several TODO items are marked as such
36
37 ;; TODO: cross-server splitting, remote processing, training through files
38
39 ;;; Code:
40
41 ;;{{{ compilation directives and autoloads/requires
42
43 (eval-when-compile (require 'cl))
44 (eval-when-compile (require 'spam-report))
45 (eval-when-compile (require 'hashcash))
46
47 (require 'gnus-sum)
48
49 (require 'gnus-uu)                      ; because of key prefix issues
50 ;;; for the definitions of group content classification and spam processors
51 (require 'gnus)
52 (require 'message)              ;for the message-fetch-field functions
53
54 ;; for nnimap-split-download-body-default
55 (eval-when-compile (require 'nnimap))
56
57 ;; autoload query-dig
58 (eval-and-compile
59   (autoload 'query-dig "dig"))
60
61 ;; autoload spam-report
62 (eval-and-compile
63   (autoload 'spam-report-gmane "spam-report")
64   (autoload 'spam-report-gmane-spam "spam-report")
65   (autoload 'spam-report-gmane-ham "spam-report")
66   (autoload 'spam-report-resend "spam-report"))
67
68 ;; autoload gnus-registry
69 (eval-and-compile
70   (autoload 'gnus-registry-group-count "gnus-registry")
71   (autoload 'gnus-registry-add-group "gnus-registry")
72   (autoload 'gnus-registry-store-extra-entry "gnus-registry")
73   (autoload 'gnus-registry-fetch-extra "gnus-registry"))
74
75 ;; autoload query-dns
76 (eval-and-compile
77   (autoload 'query-dns "dns"))
78
79 ;;}}}
80
81 ;;{{{ Main parameters.
82 (defvar spam-backends nil
83   "List of spam.el backends with all the pertinent data.
84 Populated by spam-install-backend-super.")
85
86 (defgroup spam nil
87   "Spam configuration."
88   :version "22.1"
89   :group 'mail
90   :group 'news)
91
92 (defcustom spam-summary-exit-behavior 'default
93   "Exit behavior at the time of summary exit.
94 Note that setting the spam-use-move or spam-use-copy backends on
95 a group through group/topic parameters overrides this mechanism."
96   :type '(choice (const 'default :tag 
97                         "Move spam out of all groups.  Move ham out of spam groups.")
98                  (const 'move-all :tag 
99                         "Move spam out of all groups.  Move ham out of all groups.")
100                  (const 'move-none :tag 
101                         "Never move spam or ham out of any groups."))
102   :group 'spam)
103
104 (defcustom spam-directory (nnheader-concat gnus-directory "spam/")
105   "Directory for spam whitelists and blacklists."
106   :type 'directory
107   :group 'spam)
108
109 (defcustom spam-mark-new-messages-in-spam-group-as-spam t
110   "Whether new messages in a spam group should get the spam-mark."
111   :type 'boolean
112   :group 'spam)
113
114 (defcustom spam-log-to-registry nil
115   "Whether spam/ham processing should be logged in the registry."
116   :type 'boolean
117   :group 'spam)
118
119 (defcustom spam-split-symbolic-return nil
120   "Whether `spam-split' should work with symbols or group names."
121   :type 'boolean
122   :group 'spam)
123
124 (defcustom spam-split-symbolic-return-positive nil
125   "Whether `spam-split' should ALWAYS work with symbols or group names.
126 Do not set this if you use `spam-split' in a fancy split
127   method."
128   :type 'boolean
129   :group 'spam)
130
131 (defcustom spam-mark-only-unseen-as-spam t
132   "Whether only unseen articles should be marked as spam in spam groups.
133 When nil, all unread articles in a spam group are marked as
134 spam.  Set this if you want to leave an article unread in a spam group
135 without losing it to the automatic spam-marking process."
136   :type 'boolean
137   :group 'spam)
138
139 (defcustom spam-mark-ham-unread-before-move-from-spam-group nil
140   "Whether ham should be marked unread before it's moved.
141 The article is moved out of a spam group according to ham-process-destination.
142 This variable is an official entry in the international Longest Variable Name
143 Competition."
144   :type 'boolean
145   :group 'spam)
146
147 (defcustom spam-disable-spam-split-during-ham-respool nil
148   "Whether `spam-split' should be ignored while resplitting ham.
149 This is useful to prevent ham from ending up in the same spam
150 group after the resplit.  Don't set this to t if you have `spam-split' as the
151 last rule in your split configuration."
152   :type 'boolean
153   :group 'spam)
154
155 (defcustom spam-autodetect-recheck-messages nil
156   "Should spam.el recheck all meessages when autodetecting?
157 Normally this is nil, so only unseen messages will be checked."
158   :type 'boolean
159   :group 'spam)
160
161 (defcustom spam-whitelist (expand-file-name "whitelist" spam-directory)
162   "The location of the whitelist.
163 The file format is one regular expression per line.
164 The regular expression is matched against the address."
165   :type 'file
166   :group 'spam)
167
168 (defcustom spam-blacklist (expand-file-name "blacklist" spam-directory)
169   "The location of the blacklist.
170 The file format is one regular expression per line.
171 The regular expression is matched against the address."
172   :type 'file
173   :group 'spam)
174
175 (defcustom spam-use-dig t
176   "Whether `query-dig' should be used instead of `query-dns'."
177   :type 'boolean
178   :group 'spam)
179
180 (defcustom spam-use-gmane-xref nil
181   "Whether the Gmane spam xref should be used by `spam-split'."
182   :type 'boolean
183   :group 'spam)
184
185 (defcustom spam-use-blacklist nil
186   "Whether the blacklist should be used by `spam-split'."
187   :type 'boolean
188   :group 'spam)
189
190 (defcustom spam-blacklist-ignored-regexes nil
191   "Regular expressions that the blacklist should ignore."
192   :type '(repeat (regexp :tag "Regular expression to ignore when blacklisting"))
193   :group 'spam)
194
195 (defcustom spam-use-whitelist nil
196   "Whether the whitelist should be used by `spam-split'."
197   :type 'boolean
198   :group 'spam)
199
200 (defcustom spam-use-whitelist-exclusive nil
201   "Whether whitelist-exclusive should be used by `spam-split'.
202 Exclusive whitelisting means that all messages from senders not in the whitelist
203 are considered spam."
204   :type 'boolean
205   :group 'spam)
206
207 (defcustom spam-use-blackholes nil
208   "Whether blackholes should be used by `spam-split'."
209   :type 'boolean
210   :group 'spam)
211
212 (defcustom spam-use-hashcash nil
213   "Whether hashcash payments should be detected by `spam-split'."
214   :type 'boolean
215   :group 'spam)
216
217 (defcustom spam-use-regex-headers nil
218   "Whether a header regular expression match should be used by `spam-split'.
219 Also see the variables `spam-regex-headers-spam' and `spam-regex-headers-ham'."
220   :type 'boolean
221   :group 'spam)
222
223 (defcustom spam-use-regex-body nil
224   "Whether a body regular expression match should be used by `spam-split'.
225 Also see the variables `spam-regex-body-spam' and `spam-regex-body-ham'."
226   :type 'boolean
227   :group 'spam)
228
229 (defcustom spam-use-bogofilter-headers nil
230   "Whether bogofilter headers should be used by `spam-split'.
231 Enable this if you pre-process messages with Bogofilter BEFORE Gnus sees them."
232   :type 'boolean
233   :group 'spam)
234
235 (defcustom spam-use-bogofilter nil
236   "Whether bogofilter should be invoked by `spam-split'.
237 Enable this if you want Gnus to invoke Bogofilter on new messages."
238   :type 'boolean
239   :group 'spam)
240
241 (defcustom spam-use-bsfilter-headers nil
242   "Whether bsfilter headers should be used by `spam-split'.
243 Enable this if you pre-process messages with Bsfilter BEFORE Gnus sees them."
244   :type 'boolean
245   :group 'spam)
246
247 (defcustom spam-use-bsfilter nil
248   "Whether bsfilter should be invoked by `spam-split'.
249 Enable this if you want Gnus to invoke Bsfilter on new messages."
250   :type 'boolean
251   :group 'spam)
252
253 (defcustom spam-use-BBDB nil
254   "Whether BBDB should be used by `spam-split'."
255   :type 'boolean
256   :group 'spam)
257
258 (defcustom spam-use-BBDB-exclusive nil
259   "Whether BBDB-exclusive should be used by `spam-split'.
260 Exclusive BBDB means that all messages from senders not in the BBDB are
261 considered spam."
262   :type 'boolean
263   :group 'spam)
264
265 (defcustom spam-use-ifile nil
266   "Whether ifile should be used by `spam-split'."
267   :type 'boolean
268   :group 'spam)
269
270 (defcustom spam-use-stat nil
271   "Whether `spam-stat' should be used by `spam-split'."
272   :type 'boolean
273   :group 'spam)
274
275 (defcustom spam-use-spamoracle nil
276   "Whether spamoracle should be used by `spam-split'."
277   :type 'boolean
278   :group 'spam)
279
280 (defcustom spam-use-spamassassin nil
281   "Whether spamassassin should be invoked by `spam-split'.
282 Enable this if you want Gnus to invoke SpamAssassin on new messages."
283   :type 'boolean
284   :group 'spam)
285
286 (defcustom spam-use-spamassassin-headers nil
287   "Whether spamassassin headers should be checked by `spam-split'.
288 Enable this if you pre-process messages with SpamAssassin BEFORE Gnus sees
289 them."
290   :type 'boolean
291   :group 'spam)
292
293 (defcustom spam-use-crm114 nil
294   "Whether the CRM114 Mailfilter should be used by `spam-split'."
295   :type 'boolean
296   :group 'spam)
297
298 (defcustom spam-install-hooks (or
299                                spam-use-dig
300                                spam-use-gmane-xref
301                                spam-use-blacklist
302                                spam-use-whitelist
303                                spam-use-whitelist-exclusive
304                                spam-use-blackholes
305                                spam-use-hashcash
306                                spam-use-regex-headers
307                                spam-use-regex-body
308                                spam-use-bogofilter
309                                spam-use-bogofilter-headers
310                                spam-use-spamassassin
311                                spam-use-spamassassin-headers
312                                spam-use-bsfilter
313                                spam-use-bsfilter-headers
314                                spam-use-BBDB
315                                spam-use-BBDB-exclusive
316                                spam-use-ifile
317                                spam-use-stat
318                                spam-use-spamoracle
319                                spam-use-crm114)
320   "Whether the spam hooks should be installed.
321 Default to t if one of the spam-use-* variables is set."
322   :group 'spam
323   :type 'boolean)
324
325 (defcustom spam-split-group "spam"
326   "Group name where incoming spam should be put by `spam-split'."
327   :type 'string
328   :group 'spam)
329
330 ;;; TODO: deprecate this variable, it's confusing since it's a list of strings,
331 ;;; not regular expressions
332 (defcustom spam-junk-mailgroups (cons
333                                  spam-split-group
334                                  '("mail.junk" "poste.pourriel"))
335   "Mailgroups with spam contents.
336 All unmarked article in such group receive the spam mark on group entry."
337   :type '(repeat (string :tag "Group"))
338   :group 'spam)
339
340
341 (defcustom spam-gmane-xref-spam-group "gmane.spam.detected"
342   "The group where spam xrefs can be found on Gmane.
343 Only meaningful if you enable `spam-use-gmane-xref'."
344   :type 'string
345   :group 'spam)
346
347 (defcustom spam-blackhole-servers '("bl.spamcop.net" "relays.ordb.org"
348                                     "dev.null.dk" "relays.visi.com")
349   "List of blackhole servers.
350 Only meaningful if you enable `spam-use-blackholes'."
351   :type '(repeat (string :tag "Server"))
352   :group 'spam)
353
354 (defcustom spam-blackhole-good-server-regex nil
355   "String matching IP addresses that should not be checked in the blackholes.
356 Only meaningful if you enable `spam-use-blackholes'."
357   :type '(radio (const nil) regexp)
358   :group 'spam)
359
360 (defface spam
361   '((((class color) (type tty) (background dark))
362      (:foreground "gray80" :background "gray50"))
363     (((class color) (type tty) (background light))
364      (:foreground "gray50" :background "gray80"))
365     (((class color) (background dark))
366      (:foreground "ivory2"))
367     (((class color) (background light))
368      (:foreground "ivory4"))
369     (t :inverse-video t))
370   "Face for spam-marked articles."
371   :group 'spam)
372 ;; backward-compatibility alias
373 (put 'spam-face 'face-alias 'spam)
374
375 (defcustom spam-face 'spam
376   "Face for spam-marked articles."
377   :type 'face
378   :group 'spam)
379
380 (defcustom spam-regex-headers-spam '("^X-Spam-Flag: YES")
381   "Regular expression for positive header spam matches.
382 Only meaningful if you enable `spam-use-regex-headers'."
383   :type '(repeat (regexp :tag "Regular expression to match spam header"))
384   :group 'spam)
385
386 (defcustom spam-regex-headers-ham '("^X-Spam-Flag: NO")
387   "Regular expression for positive header ham matches.
388 Only meaningful if you enable `spam-use-regex-headers'."
389   :type '(repeat (regexp :tag "Regular expression to match ham header"))
390   :group 'spam)
391
392 (defcustom spam-regex-body-spam '()
393   "Regular expression for positive body spam matches.
394 Only meaningful if you enable `spam-use-regex-body'."
395   :type '(repeat (regexp :tag "Regular expression to match spam body"))
396   :group 'spam)
397
398 (defcustom spam-regex-body-ham '()
399   "Regular expression for positive body ham matches.
400 Only meaningful if you enable `spam-use-regex-body'."
401   :type '(repeat (regexp :tag "Regular expression to match ham body"))
402   :group 'spam)
403
404 (defcustom spam-summary-score-preferred-header nil
405   "Preferred header to use for spam-summary-score."
406   :type '(choice :tag "Header name"
407           (symbol :tag "SpamAssassin etc" X-Spam-Status)
408           (symbol :tag "Bogofilter"       X-Bogosity)
409           (const  :tag "No preference, take best guess." nil))
410   :group 'spam)
411
412 (defgroup spam-ifile nil
413   "Spam ifile configuration."
414   :group 'spam)
415
416 (defcustom spam-ifile-path (executable-find "ifile")
417   "File path of the ifile executable program."
418   :type '(choice (file :tag "Location of ifile")
419                  (const :tag "ifile is not installed"))
420   :group 'spam-ifile)
421
422 (defcustom spam-ifile-database-path nil
423   "File path of the ifile database."
424   :type '(choice (file :tag "Location of the ifile database")
425                  (const :tag "Use the default"))
426   :group 'spam-ifile)
427
428 (defcustom spam-ifile-spam-category "spam"
429   "Name of the spam ifile category."
430   :type 'string
431   :group 'spam-ifile)
432
433 (defcustom spam-ifile-ham-category nil
434   "Name of the ham ifile category.
435 If nil, the current group name will be used."
436   :type '(choice (string :tag "Use a fixed category")
437                  (const :tag "Use the current group name"))
438   :group 'spam-ifile)
439
440 (defcustom spam-ifile-all-categories nil
441   "Whether the ifile check will return all categories, or just spam.
442 Set this to t if you want to use the `spam-split' invocation of ifile as
443 your main source of newsgroup names."
444   :type 'boolean
445   :group 'spam-ifile)
446
447 (defgroup spam-bogofilter nil
448   "Spam bogofilter configuration."
449   :group 'spam)
450
451 (defcustom spam-bogofilter-path (executable-find "bogofilter")
452   "File path of the Bogofilter executable program."
453   :type '(choice (file :tag "Location of bogofilter")
454                  (const :tag "Bogofilter is not installed"))
455   :group 'spam-bogofilter)
456
457 (defvar spam-bogofilter-valid 'unknown "Is the bogofilter version valid?")
458
459 (defcustom spam-bogofilter-header "X-Bogosity"
460   "The header that Bogofilter inserts in messages."
461   :type 'string
462   :group 'spam-bogofilter)
463
464 (defcustom spam-bogofilter-spam-switch "-s"
465   "The switch that Bogofilter uses to register spam messages."
466   :type 'string
467   :group 'spam-bogofilter)
468
469 (defcustom spam-bogofilter-ham-switch "-n"
470   "The switch that Bogofilter uses to register ham messages."
471   :type 'string
472   :group 'spam-bogofilter)
473
474 (defcustom spam-bogofilter-spam-strong-switch "-S"
475   "The switch that Bogofilter uses to unregister ham messages."
476   :type 'string
477   :group 'spam-bogofilter)
478
479 (defcustom spam-bogofilter-ham-strong-switch "-N"
480   "The switch that Bogofilter uses to unregister spam messages."
481   :type 'string
482   :group 'spam-bogofilter)
483
484 (defcustom spam-bogofilter-bogosity-positive-spam-header "^\\(Yes\\|Spam\\)"
485   "The regex on `spam-bogofilter-header' for positive spam identification."
486   :type 'regexp
487   :group 'spam-bogofilter)
488
489 (defcustom spam-bogofilter-database-directory nil
490   "Directory path of the Bogofilter databases."
491   :type '(choice (directory
492                   :tag "Location of the Bogofilter database directory")
493                  (const :tag "Use the default"))
494   :group 'spam-bogofilter)
495
496 (defgroup spam-bsfilter nil
497   "Spam bsfilter configuration."
498   :group 'spam)
499
500 (defcustom spam-bsfilter-path (executable-find "bsfilter")
501   "File path of the Bsfilter executable program."
502   :type '(choice (file :tag "Location of bsfilter")
503                  (const :tag "Bsfilter is not installed"))
504   :group 'spam-bsfilter)
505
506 (defcustom spam-bsfilter-header "X-Spam-Flag"
507   "The header inserted by Bsfilter to flag spam."
508   :type 'string
509   :group 'spam-bsfilter)
510
511 (defcustom spam-bsfilter-probability-header "X-Spam-Probability"
512   "The header that Bsfilter inserts in messages."
513   :type 'string
514   :group 'spam-bsfilter)
515
516 (defcustom spam-bsfilter-spam-switch "--add-spam"
517   "The switch that Bsfilter uses to register spam messages."
518   :type 'string
519   :group 'spam-bsfilter)
520
521 (defcustom spam-bsfilter-ham-switch "--add-ham"
522   "The switch that Bsfilter uses to register ham messages."
523   :type 'string
524   :group 'spam-bsfilter)
525
526 (defcustom spam-bsfilter-spam-strong-switch "--sub-spam"
527   "The switch that Bsfilter uses to unregister ham messages."
528   :type 'string
529   :group 'spam-bsfilter)
530
531 (defcustom spam-bsfilter-ham-strong-switch "--sub-clean"
532   "The switch that Bsfilter uses to unregister spam messages."
533   :type 'string
534   :group 'spam-bsfilter)
535
536 (defcustom spam-bsfilter-database-directory nil
537   "Directory path of the Bsfilter databases."
538   :type '(choice (directory
539                   :tag "Location of the Bsfilter database directory")
540                  (const :tag "Use the default"))
541   :group 'spam-bsfilter)
542
543 (defgroup spam-spamoracle nil
544   "Spam spamoracle configuration."
545   :group 'spam)
546
547 (defcustom spam-spamoracle-database nil
548   "Location of spamoracle database file.
549 When nil, use the default spamoracle database."
550   :type '(choice (directory :tag "Location of spamoracle database file.")
551                  (const :tag "Use the default"))
552   :group 'spam-spamoracle)
553
554 (defcustom spam-spamoracle-binary (executable-find "spamoracle")
555   "Location of the spamoracle binary."
556   :type '(choice (directory :tag "Location of the spamoracle binary")
557                  (const :tag "Use the default"))
558   :group 'spam-spamoracle)
559
560 (defgroup spam-spamassassin nil
561   "Spam SpamAssassin configuration."
562   :group 'spam)
563
564 (defcustom spam-spamassassin-path (executable-find "spamassassin")
565   "File path of the spamassassin executable program.
566 Hint: set this to \"spamc\" if you have spamd running.  See the spamc and
567 spamd man pages for more information on these programs."
568   :type '(choice (file :tag "Location of spamc")
569                  (const :tag "spamassassin is not installed"))
570   :group 'spam-spamassassin)
571
572 (defcustom spam-spamassassin-arguments ()
573   "Arguments to pass to the spamassassin executable.
574 This must be a list.  For example, `(\"-C\" \"configfile\")'."
575   :type '(restricted-sexp :match-alternatives (listp))
576   :group 'spam-spamassassin)
577
578 (defcustom spam-spamassassin-spam-flag-header "X-Spam-Flag"
579   "The header inserted by SpamAssassin to flag spam."
580   :type 'string
581   :group 'spam-spamassassin)
582
583 (defcustom spam-spamassassin-positive-spam-flag-header "YES"
584   "The regex on `spam-spamassassin-spam-flag-header' for positive spam
585 identification"
586   :type 'string
587   :group 'spam-spamassassin)
588
589 (defcustom spam-spamassassin-spam-status-header "X-Spam-Status"
590   "The header inserted by SpamAssassin, giving extended scoring information"
591   :type 'string
592   :group 'spam-spamassassin)
593
594 (defcustom spam-sa-learn-path (executable-find "sa-learn")
595   "File path of the sa-learn executable program."
596   :type '(choice (file :tag "Location of spamassassin")
597                  (const :tag "spamassassin is not installed"))
598   :group 'spam-spamassassin)
599
600 (defcustom spam-sa-learn-rebuild t
601   "Whether sa-learn should rebuild the database every time it is called
602 Enable this if you want sa-learn to rebuild the database automatically.  Doing
603 this will slightly increase the running time of the spam registration process.
604 If you choose not to do this, you will have to run \"sa-learn --rebuild\" in
605 order for SpamAssassin to recognize the new registered spam."
606   :type 'boolean
607   :group 'spam-spamassassin)
608
609 (defcustom spam-sa-learn-spam-switch "--spam"
610   "The switch that sa-learn uses to register spam messages"
611   :type 'string
612   :group 'spam-spamassassin)
613
614 (defcustom spam-sa-learn-ham-switch "--ham"
615   "The switch that sa-learn uses to register ham messages"
616   :type 'string
617   :group 'spam-spamassassin)
618
619 (defcustom spam-sa-learn-unregister-switch "--forget"
620   "The switch that sa-learn uses to unregister messages messages"
621   :type 'string
622   :group 'spam-spamassassin)
623
624 (defgroup spam-crm114 nil
625   "Spam CRM114 Mailfilter configuration."
626   :group 'spam)
627
628 (defcustom spam-crm114-program (executable-find "mailfilter.crm")
629   "File path of the CRM114 Mailfilter executable program."
630   :type '(choice (file :tag "Location of CRM114 Mailfilter")
631          (const :tag "CRM114 Mailfilter is not installed"))
632   :group 'spam-crm114)
633
634 (defcustom spam-crm114-header "X-CRM114-Status"
635   "The header that CRM114 Mailfilter inserts in messages."
636   :type 'string
637   :group 'spam-crm114)
638
639 (defcustom spam-crm114-spam-switch "--learnspam"
640   "The switch that CRM114 Mailfilter uses to register spam messages."
641   :type 'string
642   :group 'spam-crm114)
643
644 (defcustom spam-crm114-ham-switch "--learnnonspam"
645   "The switch that CRM114 Mailfilter uses to register ham messages."
646   :type 'string
647   :group 'spam-crm114)
648
649 (defcustom spam-crm114-spam-strong-switch "--UNKNOWN"
650   "The switch that CRM114 Mailfilter uses to unregister ham messages."
651   :type 'string
652   :group 'spam-crm114)
653
654 (defcustom spam-crm114-ham-strong-switch "--UNKNOWN"
655   "The switch that CRM114 Mailfilter uses to unregister spam messages."
656   :type 'string
657   :group 'spam-crm114)
658
659 (defcustom spam-crm114-positive-spam-header "^SPAM"
660   "The regex on `spam-crm114-header' for positive spam identification."
661   :type 'regexp
662   :group 'spam-crm114)
663
664 (defcustom spam-crm114-database-directory nil
665   "Directory path of the CRM114 Mailfilter databases."
666   :type '(choice (directory
667           :tag "Location of the CRM114 Mailfilter database directory")
668          (const :tag "Use the default"))
669   :group 'spam-crm114)
670
671 ;;; Key bindings for spam control.
672
673 (gnus-define-keys gnus-summary-mode-map
674   "St" spam-generic-score
675   "Sx" gnus-summary-mark-as-spam
676   "Mst" spam-generic-score
677   "Msx" gnus-summary-mark-as-spam
678   "\M-d" gnus-summary-mark-as-spam)
679
680 (defvar spam-cache-lookups t
681   "Whether spam.el will try to cache lookups using `spam-caches'.")
682
683 (defvar spam-caches (make-hash-table
684                      :size 10
685                      :test 'equal)
686   "Cache of spam detection entries.")
687
688 (defvar spam-old-articles nil
689   "List of old ham and spam articles, generated when a group is entered.")
690
691 (defvar spam-split-disabled nil
692   "If non-nil, `spam-split' is disabled, and always returns nil.")
693
694 (defvar spam-split-last-successful-check nil
695   "Internal variable.
696 `spam-split' will set this to nil or a spam-use-XYZ check if it
697 finds ham or spam.")
698
699 ;; internal variables for backends
700 ;; TODO: find a way to create these on the fly in spam-install-backend-super
701 (defvar spam-use-copy nil)
702 (defvar spam-use-move nil)
703 (defvar spam-use-gmane nil)
704 (defvar spam-use-resend nil)
705
706 ;;}}}
707
708 ;;{{{ convenience functions
709
710 (defun spam-clear-cache (symbol)
711   "Clear the spam-caches entry for a check."
712   (remhash symbol spam-caches))
713
714 (defun spam-xor (a b)
715   "Logical A xor B."
716   (and (or a b) (not (and a b))))
717
718 (defun spam-set-difference (list1 list2)
719   "Return a set difference of LIST1 and LIST2.  
720 When either list is nil, the other is returned."
721   (if (and list1 list2)
722       ;; we have two non-nil lists
723       (progn
724         (dolist (item (append list1 list2))
725           (when (and (memq item list1) (memq item list2))
726             (setq list1 (delq item list1))
727             (setq list2 (delq item list2))))
728         (append list1 list2))
729     ;; if either of the lists was nil, return the other one
730     (if list1 list1 list2)))
731
732 (defun spam-group-ham-mark-p (group mark &optional spam)
733   "Checks if MARK is considered a ham mark in GROUP."
734   (when (stringp group)
735     (let* ((marks (spam-group-ham-marks group spam))
736            (marks (if (symbolp mark)
737                       marks
738                     (mapcar 'symbol-value marks))))
739       (memq mark marks))))
740
741 (defun spam-group-spam-mark-p (group mark)
742   "Checks if MARK is considered a spam mark in GROUP."
743   (spam-group-ham-mark-p group mark t))
744
745 (defun spam-group-ham-marks (group &optional spam)
746   "In GROUP, get all the ham marks."
747   (when (stringp group)
748     (let* ((marks (if spam
749                       (gnus-parameter-spam-marks group)
750                     (gnus-parameter-ham-marks group)))
751            (marks (car marks))
752            (marks (if (listp (car marks)) (car marks) marks)))
753       marks)))
754
755 (defun spam-group-spam-marks (group)
756   "In GROUP, get all the spam marks."
757   (spam-group-ham-marks group t))
758
759 (defun spam-group-spam-contents-p (group)
760   "Is GROUP a spam group?"
761   (if (and (stringp group) (< 0 (length group)))
762       (or (member group spam-junk-mailgroups)
763           (memq 'gnus-group-spam-classification-spam
764                 (gnus-parameter-spam-contents group)))
765     nil))
766
767 (defun spam-group-ham-contents-p (group)
768   "Is GROUP a ham group?"
769   (if (stringp group)
770       (memq 'gnus-group-spam-classification-ham
771             (gnus-parameter-spam-contents group))
772     nil))
773
774 (defun spam-classifications ()
775   "Return list of valid classifications"
776   '(spam ham))
777
778 (defun spam-classification-valid-p (classification)
779   "Is CLASSIFICATION a valid spam/ham classification?"
780   (memq classification (spam-classifications)))
781
782 (defun spam-backend-properties ()
783   "Return list of valid classifications."
784   '(statistical mover check hrf srf huf suf))
785
786 (defun spam-backend-property-valid-p (property)
787   "Is PROPERTY a valid backend property?"
788   (memq property (spam-backend-properties)))
789
790 (defun spam-backend-function-type-valid-p (type)
791   (or (eq type 'registration)
792       (eq type 'unregistration)))
793
794 (defun spam-process-type-valid-p (process-type)
795   (or (eq process-type 'incoming)
796       (eq process-type 'process)))
797
798 (defun spam-list-articles (articles classification)
799   (let ((mark-check (if (eq classification 'spam)
800                         'spam-group-spam-mark-p
801                       'spam-group-ham-mark-p))
802         alist mark-cache-yes mark-cache-no)
803     (dolist (article articles)
804       (let ((mark (gnus-summary-article-mark article)))
805         (unless (or (memq mark mark-cache-yes)
806                     (memq mark mark-cache-no))
807           (if (funcall mark-check
808                        gnus-newsgroup-name
809                        mark)
810               (push mark mark-cache-yes)
811             (push mark mark-cache-no)))
812         (when (memq mark mark-cache-yes)
813           (push article alist))))
814     alist))
815
816 ;;}}}
817
818 ;;{{{ backend installation functions and procedures
819
820 (defun spam-install-backend-super (backend &rest properties)
821   "Install BACKEND for spam.el.
822 Accepts incoming CHECK, ham registration function HRF, spam
823 registration function SRF, ham unregistration function HUF, spam
824 unregistration function SUF, and an indication whether the
825 backend is STATISTICAL."
826
827   (setq spam-backends (add-to-list 'spam-backends backend))
828   (while properties
829     (let ((property (pop properties))
830           (value (pop properties)))
831       (if (spam-backend-property-valid-p property)
832           (put backend property value)
833         (gnus-error 
834          5 
835          "spam-install-backend-super got an invalid property %s"
836          property)))))
837
838 (defun spam-backend-list (&optional type)
839   "Return a list of all the backend symbols, constrained by TYPE.
840 When TYPE is 'non-mover, only non-mover backends are returned.
841 When TYPE is 'mover, only mover backends are returned."
842   (let (list)
843     (dolist (backend spam-backends)
844       (when (or
845              (null type)                ;either no type was requested
846              ;; or the type is 'mover and the backend is a mover
847              (and
848               (eq type 'mover)
849               (spam-backend-mover-p backend))
850              ;; or the type is 'non-mover and the backend is not a mover
851              (and
852               (eq type 'non-mover)
853               (not (spam-backend-mover-p backend))))
854         (push backend list)))
855       list))
856
857 (defun spam-backend-check (backend)
858   "Get the check function for BACKEND.
859 Each individual check may return nil, t, or a mailgroup name.
860 The value nil means that the check does not yield a decision, and
861 so, that further checks are needed.  The value t means that the
862 message is definitely not spam, and that further spam checks
863 should be inhibited.  Otherwise, a mailgroup name or the symbol
864 'spam (depending on spam-split-symbolic-return) is returned where
865 the mail should go, and further checks are also inhibited.  The
866 usual mailgroup name is the value of `spam-split-group', meaning
867 that the message is definitely a spam."
868   (get backend 'check))
869
870 (defun spam-backend-valid-p (backend)
871   "Is BACKEND valid?"
872   (member backend (spam-backend-list)))
873
874 (defun spam-backend-info (backend)
875   "Return information about BACKEND."
876   (if (spam-backend-valid-p backend)
877       (let (info)
878         (setq info (format "Backend %s has the following properties:\n"
879                            backend))
880         (dolist (property (spam-backend-properties))
881           (setq info (format "%s%s=%s\n" 
882                              info
883                              property
884                              (get backend property))))
885         info)
886     (gnus-error 5 "spam-backend-info was asked about an invalid backend %s"
887                 backend)))
888
889 (defun spam-backend-function (backend classification type)
890   "Get the BACKEND function for CLASSIFICATION and TYPE.
891 TYPE is 'registration or 'unregistration.
892 CLASSIFICATION is 'ham or 'spam."
893   (if (and
894        (spam-classification-valid-p classification)
895        (spam-backend-function-type-valid-p type))
896       (let ((retrieval 
897              (intern 
898               (format "spam-backend-%s-%s-function"
899                       classification
900                       type))))
901         (funcall retrieval backend))
902     (gnus-error 
903      5
904      "%s was passed invalid backend %s, classification %s, or type %s"
905      "spam-backend-function"
906      backend
907      classification
908      type)))
909
910 (defun spam-backend-article-list-property (classification 
911                                            &optional unregister)
912   "Property name of article list with CLASSIFICATION and UNREGISTER."
913   (let* ((r (if unregister "unregister" "register"))
914          (prop (format "%s-%s" classification r)))
915     prop))
916
917 (defun spam-backend-get-article-todo-list (backend 
918                                            classification 
919                                            &optional unregister)
920   "Get the articles to be processed for BACKEND and CLASSIFICATION.  
921 With UNREGISTER, get articles to be unregistered.
922 This is a temporary storage function - nothing here persists."
923   (get
924    backend 
925    (intern (spam-backend-article-list-property classification unregister))))
926
927 (defun spam-backend-put-article-todo-list (backend classification list &optional unregister)
928   "Set the LIST of articles to be processed for BACKEND and CLASSIFICATION.
929 With UNREGISTER, set articles to be unregistered.
930 This is a temporary storage function - nothing here persists."
931   (put
932    backend
933    (intern (spam-backend-article-list-property classification unregister))
934    list))
935
936 (defun spam-backend-ham-registration-function (backend)
937   "Get the ham registration function for BACKEND."
938   (get backend 'hrf))
939
940 (defun spam-backend-spam-registration-function (backend)
941   "Get the spam registration function for BACKEND."
942   (get backend 'srf))
943
944 (defun spam-backend-ham-unregistration-function (backend)
945   "Get the ham unregistration function for BACKEND."
946   (get backend 'huf))
947
948 (defun spam-backend-spam-unregistration-function (backend)
949   "Get the spam unregistration function for BACKEND."
950   (get backend 'suf))
951
952 (defun spam-backend-statistical-p (backend)
953   "Is BACKEND statistical?"
954   (get backend 'statistical))
955
956 (defun spam-backend-mover-p (backend)
957   "Is BACKEND a mover?"
958   (get backend 'mover))
959
960 (defun spam-install-backend-alias (backend alias)
961   "Add ALIAS to an existing BACKEND.
962 The previous backend settings for ALIAS are erased."
963
964   ;; install alias with no properties at first
965   (spam-install-backend-super alias)
966   
967   (dolist (property (spam-backend-properties))
968     (put alias property (get backend property))))
969
970 (defun spam-install-checkonly-backend (backend check)
971   "Install a BACKEND than can only CHECK for spam."
972   (spam-install-backend-super backend 'check check))
973
974 (defun spam-install-mover-backend (backend hrf srf huf suf)
975   "Install a BACKEND than can move articles at summary exit.
976 Accepts ham registration function HRF, spam registration function
977 SRF, ham unregistration function HUF, spam unregistration
978 function SUF.  The backend has no incoming check and can't be
979 statistical."
980   (spam-install-backend-super 
981    backend 
982    'hrf hrf 'srf srf 'huf huf 'suf suf 'mover t))
983
984 (defun spam-install-nocheck-backend (backend hrf srf huf suf)
985   "Install a BACKEND than has no check.
986 Accepts ham registration function HRF, spam registration function
987 SRF, ham unregistration function HUF, spam unregistration
988 function SUF.  The backend has no incoming check and can't be
989 statistical (it could be, but in practice that doesn't happen)."
990   (spam-install-backend-super 
991    backend
992    'hrf hrf 'srf srf 'huf huf 'suf suf))
993
994 (defun spam-install-backend (backend check hrf srf huf suf)
995   "Install a BACKEND.
996 Accepts incoming CHECK, ham registration function HRF, spam
997 registration function SRF, ham unregistration function HUF, spam
998 unregistration function SUF.  The backend won't be
999 statistical (use spam-install-statistical-backend for that)."
1000   (spam-install-backend-super 
1001    backend
1002    'check check 'hrf hrf 'srf srf 'huf huf 'suf suf))
1003
1004 (defun spam-install-statistical-backend (backend check hrf srf huf suf)
1005   "Install a BACKEND.
1006 Accepts incoming CHECK, ham registration function HRF, spam
1007 registration function SRF, ham unregistration function HUF, spam
1008 unregistration function SUF.  The backend will be
1009 statistical (use spam-install-backend for non-statistical
1010 backends)."
1011   (spam-install-backend-super 
1012    backend
1013    'check check 'statistical t 'hrf hrf 'srf srf 'huf huf 'suf suf))
1014
1015 (defun spam-install-statistical-checkonly-backend (backend check)
1016   "Install a statistical BACKEND than can only CHECK for spam."
1017   (spam-install-backend-super 
1018    backend
1019    'check check 'statistical t))
1020
1021 ;;}}}
1022
1023 ;;{{{ backend installations
1024 (spam-install-checkonly-backend 'spam-use-blackholes
1025                                 'spam-check-blackholes)
1026
1027 (spam-install-checkonly-backend 'spam-use-hashcash
1028                                 'spam-check-hashcash)
1029
1030 (spam-install-checkonly-backend 'spam-use-spamassassin-headers
1031                                 'spam-check-spamassassin-headers)
1032
1033 (spam-install-checkonly-backend 'spam-use-bogofilter-headers
1034                                 'spam-check-bogofilter-headers)
1035
1036 (spam-install-checkonly-backend 'spam-use-bsfilter-headers
1037                                 'spam-check-bsfilter-headers)
1038
1039 (spam-install-checkonly-backend 'spam-use-gmane-xref
1040                                 'spam-check-gmane-xref)
1041
1042 (spam-install-checkonly-backend 'spam-use-regex-headers
1043                                 'spam-check-regex-headers)
1044
1045 (spam-install-statistical-checkonly-backend 'spam-use-regex-body
1046                                             'spam-check-regex-body)
1047
1048 ;; TODO: NOTE: spam-use-ham-copy is now obsolete, use (ham spam-use-copy) instead
1049 (spam-install-mover-backend 'spam-use-move
1050                             'spam-move-ham-routine
1051                             'spam-move-spam-routine
1052                             nil
1053                             nil)
1054
1055 (spam-install-nocheck-backend 'spam-use-copy
1056                               'spam-copy-ham-routine
1057                               'spam-copy-spam-routine
1058                               nil
1059                               nil)
1060
1061 (spam-install-nocheck-backend 'spam-use-gmane
1062                               'spam-report-gmane-unregister-routine
1063                               'spam-report-gmane-register-routine
1064                               'spam-report-gmane-register-routine
1065                               'spam-report-gmane-unregister-routine)
1066
1067 (spam-install-nocheck-backend 'spam-use-resend
1068                               'spam-report-resend-register-ham-routine
1069                               'spam-report-resend-register-routine
1070                               nil
1071                               nil)
1072
1073 (spam-install-backend 'spam-use-BBDB     
1074                       'spam-check-BBDB
1075                       'spam-BBDB-register-routine
1076                       nil
1077                       'spam-BBDB-unregister-routine
1078                       nil)
1079
1080 (spam-install-backend-alias 'spam-use-BBDB 'spam-use-BBDB-exclusive)
1081
1082 (spam-install-backend 'spam-use-blacklist
1083                       'spam-check-blacklist
1084                       nil
1085                       'spam-blacklist-register-routine
1086                       nil
1087                       'spam-blacklist-unregister-routine)
1088
1089 (spam-install-backend 'spam-use-whitelist
1090                       'spam-check-whitelist
1091                       'spam-whitelist-register-routine
1092                       nil
1093                       'spam-whitelist-unregister-routine
1094                       nil)
1095
1096 (spam-install-statistical-backend 'spam-use-ifile
1097                                   'spam-check-ifile
1098                                   'spam-ifile-register-ham-routine
1099                                   'spam-ifile-register-spam-routine
1100                                   'spam-ifile-unregister-ham-routine
1101                                   'spam-ifile-unregister-spam-routine)
1102
1103 (spam-install-statistical-backend 'spam-use-spamoracle
1104                                   'spam-check-spamoracle
1105                                   'spam-spamoracle-learn-ham
1106                                   'spam-spamoracle-learn-spam
1107                                   'spam-spamoracle-unlearn-ham
1108                                   'spam-spamoracle-unlearn-spam)
1109
1110 (spam-install-statistical-backend 'spam-use-stat
1111                                   'spam-check-stat
1112                                   'spam-stat-register-ham-routine
1113                                   'spam-stat-register-spam-routine
1114                                   'spam-stat-unregister-ham-routine
1115                                   'spam-stat-unregister-spam-routine)
1116
1117 (spam-install-statistical-backend 'spam-use-spamassassin 
1118                                   'spam-check-spamassassin
1119                                   'spam-spamassassin-register-ham-routine
1120                                   'spam-spamassassin-register-spam-routine
1121                                   'spam-spamassassin-unregister-ham-routine
1122                                   'spam-spamassassin-unregister-spam-routine)
1123
1124 (spam-install-statistical-backend 'spam-use-bogofilter
1125                                   'spam-check-bogofilter
1126                                   'spam-bogofilter-register-ham-routine
1127                                   'spam-bogofilter-register-spam-routine
1128                                   'spam-bogofilter-unregister-ham-routine
1129                                   'spam-bogofilter-unregister-spam-routine)
1130
1131 (spam-install-statistical-backend 'spam-use-bsfilter
1132                                   'spam-check-bsfilter
1133                                   'spam-bsfilter-register-ham-routine
1134                                   'spam-bsfilter-register-spam-routine
1135                                   'spam-bsfilter-unregister-ham-routine
1136                                   'spam-bsfilter-unregister-spam-routine)
1137
1138 (spam-install-statistical-backend 'spam-use-crm114
1139                                   'spam-check-crm114
1140                                   'spam-crm114-register-ham-routine
1141                                   'spam-crm114-register-spam-routine
1142                                   ;; does CRM114 Mailfilter support unregistration?
1143                                   nil
1144                                   nil)
1145
1146 ;;}}}
1147
1148 ;;{{{ scoring and summary formatting
1149 (defun spam-necessary-extra-headers ()
1150   "Return the extra headers spam.el thinks are necessary."
1151   (let (list)
1152     (when (or spam-use-spamassassin
1153               spam-use-spamassassin-headers
1154               spam-use-regex-headers)
1155       (push 'X-Spam-Status list))
1156     (when spam-use-bogofilter
1157       (push 'X-Bogosity list))
1158     list))
1159
1160 (defun spam-user-format-function-S (headers)
1161   (when headers
1162     (format "%3.2f"
1163             (spam-summary-score headers spam-summary-score-preferred-header))))
1164
1165 (defun spam-article-sort-by-spam-status (h1 h2)
1166   "Sort articles by score."
1167   (let (result)
1168     (dolist (header (spam-necessary-extra-headers))
1169       (let ((s1 (spam-summary-score h1 header))
1170             (s2 (spam-summary-score h2 header)))
1171       (unless (= s1 s2)
1172         (setq result (< s1 s2))
1173         (return))))
1174     result))
1175
1176 (defun spam-extra-header-to-number (header headers)
1177   "Transform an extra HEADER to a number, using list of HEADERS.
1178 Note this has to be fast."
1179   (if (gnus-extra-header header headers)
1180       (cond
1181        ((eq header 'X-Spam-Status)
1182         (string-to-number (gnus-replace-in-string
1183                            (gnus-extra-header header headers)
1184                            ".*hits=" "")))
1185        ;; for CRM checking, it's probably faster to just do the string match
1186        ((and spam-use-crm114 (string-match "( pR: \\([0-9.-]+\\)" header))
1187         (match-string 1 header))
1188        ((eq header 'X-Bogosity)
1189         (string-to-number (gnus-replace-in-string
1190                            (gnus-replace-in-string
1191                             (gnus-extra-header header headers)
1192                             ".*spamicity=" "")
1193                            ",.*" "")))
1194        (t nil))
1195     nil))
1196
1197 (defun spam-summary-score (headers &optional specific-header)
1198   "Score an article for the summary buffer, as fast as possible.
1199 With SPECIFIC-HEADER, returns only that header's score.
1200 Will not return a nil score."
1201   (let (score)
1202     (dolist (header 
1203              (if specific-header
1204                  (list specific-header)
1205                (spam-necessary-extra-headers)))
1206       (setq score 
1207             (spam-extra-header-to-number header headers))
1208       (when score 
1209         (return)))
1210     (or score 0)))
1211
1212 (defun spam-generic-score (&optional recheck)
1213   "Invoke whatever scoring method we can."
1214   (interactive "P")
1215   (cond
1216    ((or spam-use-spamassassin spam-use-spamassassin-headers)
1217     (spam-spamassassin-score recheck))
1218    ((or spam-use-bsfilter spam-use-bsfilter-headers)
1219     (spam-bsfilter-score recheck))
1220    (spam-use-crm114
1221     (spam-crm114-score))
1222    (t (spam-bogofilter-score recheck))))
1223 ;;}}}
1224
1225 ;;{{{ set up widening, processor checks
1226
1227 ;;; set up IMAP widening if it's necessary
1228 (defun spam-setup-widening ()
1229   (when (spam-widening-needed-p)
1230     (setq nnimap-split-download-body-default t)))
1231
1232 (defun spam-widening-needed-p (&optional force-symbols)
1233   (let (found)
1234     (dolist (backend (spam-backend-list))
1235       (when (and (spam-backend-statistical-p backend)
1236                  (or (symbol-value backend) 
1237                      (memq backend force-symbols)))
1238         (setq found backend)))
1239     found))
1240
1241 (defvar spam-list-of-processors
1242   ;; note the nil processors are not defined in gnus.el
1243   '((gnus-group-spam-exit-processor-bogofilter   spam spam-use-bogofilter)
1244     (gnus-group-spam-exit-processor-bsfilter     spam spam-use-bsfilter)
1245     (gnus-group-spam-exit-processor-blacklist    spam spam-use-blacklist)
1246     (gnus-group-spam-exit-processor-ifile        spam spam-use-ifile)
1247     (gnus-group-spam-exit-processor-stat         spam spam-use-stat)
1248     (gnus-group-spam-exit-processor-spamoracle   spam spam-use-spamoracle)
1249     (gnus-group-spam-exit-processor-spamassassin spam spam-use-spamassassin)
1250     (gnus-group-ham-exit-processor-ifile         ham spam-use-ifile)
1251     (gnus-group-ham-exit-processor-bogofilter    ham spam-use-bogofilter)
1252     (gnus-group-ham-exit-processor-bsfilter      ham spam-use-bsfilter)
1253     (gnus-group-ham-exit-processor-stat          ham spam-use-stat)
1254     (gnus-group-ham-exit-processor-whitelist     ham spam-use-whitelist)
1255     (gnus-group-ham-exit-processor-BBDB          ham spam-use-BBDB)
1256     (gnus-group-ham-exit-processor-copy          ham spam-use-ham-copy)
1257     (gnus-group-ham-exit-processor-spamassassin  ham spam-use-spamassassin)
1258     (gnus-group-ham-exit-processor-spamoracle    ham spam-use-spamoracle))
1259   "The OBSOLETE `spam-list-of-processors' list.
1260 This list contains pairs associating the obsolete ham/spam exit
1261 processor variables with a classification and a spam-use-*
1262 variable.  When the processor variable is nil, just the
1263 classification and spam-use-* check variable are used.  This is
1264 superceded by the new spam backend code, so it's only consulted
1265 for backwards compatibility.")
1266
1267 (defun spam-group-processor-p (group backend &optional classification)
1268   "Checks if GROUP has a BACKEND with CLASSIFICATION registered.
1269 Also accepts the obsolete processors, which can be found in
1270 gnus.el and in spam-list-of-processors.  In the case of mover
1271 backends, checks the setting of spam-summary-exit-behavior in
1272 addition to the set values for the group."
1273   (if (and (stringp group)
1274            (symbolp backend))
1275       (let ((old-style (assq backend spam-list-of-processors))
1276             (parameters (nth 0 (gnus-parameter-spam-process group)))
1277             found)
1278         (if old-style  ; old-style processor
1279             (spam-group-processor-p group (nth 2 old-style) (nth 1 old-style))
1280           ;; now search for the parameter
1281           (dolist (parameter parameters)
1282             (when (and (null found)
1283                        (listp parameter)
1284                        (eq classification (nth 0 parameter))
1285                        (eq backend (nth 1 parameter)))
1286               (setq found t)))
1287
1288           ;; now, if the parameter was not found, do the
1289           ;; spam-summary-exit-behavior-logic for mover backends
1290           (unless found
1291             (when (spam-backend-mover-p backend)
1292               (setq 
1293                found
1294                (cond
1295                 ((eq spam-summary-exit-behavior 'move-all) t)
1296                 ((eq spam-summary-exit-behavior 'move-none) nil)
1297                 ((eq spam-summary-exit-behavior 'default)
1298                  (or (eq classification 'spam) ;move spam out of all groups
1299                      ;; move ham out of spam groups
1300                      (and (eq classification 'ham)
1301                           (spam-group-spam-contents-p group))))
1302                 (t (gnus-error 5 "Unknown spam-summary-exit-behavior: %s" 
1303                                spam-summary-exit-behavior))))))
1304
1305           found))
1306     nil))
1307
1308 ;;}}}
1309
1310 ;;{{{ Summary entry and exit processing.
1311
1312 (defun spam-mark-junk-as-spam-routine ()
1313   ;; check the global list of group names spam-junk-mailgroups and the
1314   ;; group parameters
1315   (when (spam-group-spam-contents-p gnus-newsgroup-name)
1316     (gnus-message 6 "Marking %s articles as spam"
1317                   (if spam-mark-only-unseen-as-spam
1318                       "unseen"
1319                     "unread"))
1320     (let ((articles (if spam-mark-only-unseen-as-spam
1321                         gnus-newsgroup-unseen
1322                       gnus-newsgroup-unreads)))
1323       (if spam-mark-new-messages-in-spam-group-as-spam
1324           (dolist (article articles)
1325             (gnus-summary-mark-article article gnus-spam-mark))
1326         (gnus-message 9 "Did not mark new messages as spam.")))))
1327
1328 (defun spam-summary-prepare ()
1329   (setq spam-old-articles
1330         (list (cons 'ham (spam-list-articles gnus-newsgroup-articles 'ham))
1331               (cons 'spam (spam-list-articles gnus-newsgroup-articles 'spam))))
1332   (spam-mark-junk-as-spam-routine))
1333
1334 ;; The spam processors are invoked for any group, spam or ham or neither
1335 (defun spam-summary-prepare-exit ()
1336   (unless gnus-group-is-exiting-without-update-p
1337     (gnus-message 6 "Exiting summary buffer and applying spam rules")
1338
1339     ;; before we begin, remove any article limits
1340 ;    (ignore-errors
1341 ;      (gnus-summary-pop-limit t))
1342
1343     ;; first of all, unregister any articles that are no longer ham or spam
1344     ;; we have to iterate over the processors, or else we'll be too slow
1345     (dolist (classification (spam-classifications))
1346       (let* ((old-articles (cdr-safe (assq classification spam-old-articles)))
1347              (new-articles (spam-list-articles
1348                             gnus-newsgroup-articles
1349                             classification))
1350              (changed-articles (spam-set-difference new-articles old-articles)))
1351         ;; now that we have the changed articles, we go through the processors
1352         (dolist (backend (spam-backend-list))
1353           (let (unregister-list)
1354             (dolist (article changed-articles)
1355               (let ((id (spam-fetch-field-message-id-fast article)))
1356                 (when (spam-log-unregistration-needed-p
1357                        id 'process classification backend)
1358                   (push article unregister-list))))
1359             ;; call spam-register-routine with specific articles to unregister,
1360             ;; when there are articles to unregister and the check is enabled
1361             (when (and unregister-list (symbol-value backend))
1362               (spam-backend-put-article-todo-list backend 
1363                                                   classification 
1364                                                   unregister-list
1365                                                   t))))))
1366
1367     ;; do the non-moving backends first, then the moving ones
1368     (dolist (backend-type '(non-mover mover))
1369       (dolist (classification (spam-classifications))
1370         (dolist (backend (spam-backend-list backend-type))
1371           (when (spam-group-processor-p
1372                  gnus-newsgroup-name
1373                  backend
1374                  classification)
1375             (spam-backend-put-article-todo-list backend 
1376                                                 classification
1377                                                 (spam-list-articles
1378                                                  gnus-newsgroup-articles
1379                                                  classification))))))
1380
1381     (spam-resolve-registrations-routine) ; do the registrations now
1382
1383     ;; we mark all the leftover spam articles as expired at the end
1384     (dolist (article (spam-list-articles
1385                       gnus-newsgroup-articles
1386                       'spam))
1387       (gnus-summary-mark-article article gnus-expirable-mark)))
1388
1389   (setq spam-old-articles nil))
1390
1391 ;;}}}
1392
1393 ;;{{{ spam-use-move and spam-use-copy backend support functions
1394
1395 (defun spam-copy-or-move-routine (copy groups articles classification)
1396
1397   (when (and (car-safe groups) (listp (car-safe groups)))
1398     (setq groups (pop groups)))
1399
1400   (unless (listp groups)
1401     (setq groups (list groups)))
1402
1403     ;; remove the current process mark
1404   (gnus-summary-kill-process-mark)
1405
1406   (let ((backend-supports-deletions
1407          (gnus-check-backend-function
1408           'request-move-article gnus-newsgroup-name))
1409         (respool-method (gnus-find-method-for-group gnus-newsgroup-name))
1410         article mark deletep respool)
1411
1412     (when (member 'respool groups)
1413       (setq respool t)                  ; boolean for later
1414       (setq groups '("fake"))) ; when respooling, groups are dynamic so fake it
1415
1416     ;; now do the actual move
1417     (dolist (group groups)
1418       (when (and articles (stringp group))
1419
1420         ;; first, mark the article with the process mark and, if needed,
1421         ;; the unread or expired mark (for ham and spam respectively)
1422         (dolist (article articles)
1423           (when (and (eq classification 'ham)
1424                      spam-mark-ham-unread-before-move-from-spam-group)
1425             (gnus-message 9 "Marking ham article %d unread before move"
1426                           article)
1427             (gnus-summary-mark-article article gnus-unread-mark))
1428           (when (and (eq classification 'spam)
1429                      (not copy))
1430             (gnus-message 9 "Marking spam article %d expirable before move"
1431                           article)
1432             (gnus-summary-mark-article article gnus-expirable-mark))
1433           (gnus-summary-set-process-mark article)
1434             
1435           (if respool              ; respooling is with a "fake" group
1436               (let ((spam-split-disabled
1437                      (or spam-split-disabled
1438                          (and (eq classification 'ham) 
1439                               spam-disable-spam-split-during-ham-respool))))
1440                 (gnus-message 9 "Respooling article %d with method %s"
1441                               article respool-method)
1442                 (gnus-summary-respool-article nil respool-method))
1443             (if (or (not backend-supports-deletions) ; else, we are not respooling
1444                     (> (length groups) 1))
1445                 (progn              ; if copying, copy and set deletep
1446                   (gnus-message 9 "Copying article %d to group %s"
1447                                 article group)
1448                   (gnus-summary-copy-article nil group)
1449                   (setq deletep t))
1450               (gnus-message 9 "Moving article %d to group %s"
1451                             article group)
1452               (gnus-summary-move-article nil group))))) ; else move articles
1453         
1454       ;; now delete the articles, unless a) copy is t, and there was a copy done
1455       ;;                                 b) a move was done to a single group
1456       ;;                                 c) backend-supports-deletions is nil
1457       (unless copy
1458         (when (and deletep backend-supports-deletions)
1459           (dolist (article articles)
1460               (gnus-summary-set-process-mark article)
1461               (gnus-message 9 "Deleting article %d" article))
1462           (when articles
1463             (let ((gnus-novice-user nil)) ; don't ask me if I'm sure
1464               (gnus-summary-delete-article nil)))))
1465         
1466       (gnus-summary-yank-process-mark)
1467       (length articles))))
1468
1469 (defun spam-copy-spam-routine (articles)
1470   (spam-copy-or-move-routine 
1471    t 
1472    (gnus-parameter-spam-process-destination gnus-newsgroup-name)
1473    articles
1474    'spam))
1475
1476 (defun spam-move-spam-routine (articles)
1477   (spam-copy-or-move-routine 
1478    nil
1479    (gnus-parameter-spam-process-destination gnus-newsgroup-name)
1480    articles
1481    'spam))
1482
1483 (defun spam-copy-ham-routine (articles)
1484   (spam-copy-or-move-routine 
1485    t 
1486    (gnus-parameter-ham-process-destination gnus-newsgroup-name)
1487    articles
1488    'ham))
1489
1490 (defun spam-move-ham-routine (articles)
1491   (spam-copy-or-move-routine 
1492    nil
1493    (gnus-parameter-ham-process-destination gnus-newsgroup-name)
1494    articles
1495    'ham))
1496
1497 ;;}}}
1498
1499 ;;{{{ article and field retrieval code
1500 (defun spam-get-article-as-string (article)
1501   (when (numberp article)
1502     (with-temp-buffer
1503       (gnus-request-article-this-buffer
1504        article
1505        gnus-newsgroup-name)
1506       (buffer-string))))
1507
1508 ;; disabled for now
1509 ;; (defun spam-get-article-as-filename (article)
1510 ;;   (let ((article-filename))
1511 ;;     (when (numberp article)
1512 ;;       (nnml-possibly-change-directory
1513 ;;        (gnus-group-real-name gnus-newsgroup-name))
1514 ;;       (setq article-filename (expand-file-name
1515 ;;                              (int-to-string article) nnml-current-directory)))
1516 ;;     (if (file-exists-p article-filename)
1517 ;;      article-filename
1518 ;;       nil)))
1519
1520 (defun spam-fetch-field-fast (article field &optional prepared-data-header)
1521   "Fetch a FIELD for ARTICLE quickly, using the internal gnus-data-list function.
1522 When PREPARED-DATA-HEADER is given, don't look in the Gnus data.
1523 When FIELD is 'number, ARTICLE can be any number (since we want
1524 to find it out)."
1525   (when (numberp article)
1526     (let* ((data-header (or prepared-data-header
1527                             (spam-fetch-article-header article))))
1528       (if (arrayp data-header)
1529         (cond
1530          ((equal field 'number)
1531           (mail-header-number data-header))
1532          ((equal field 'from)
1533           (mail-header-from data-header))
1534          ((equal field 'message-id)
1535           (mail-header-message-id data-header))
1536          ((equal field 'subject)
1537           (mail-header-subject data-header))
1538          ((equal field 'references)
1539           (mail-header-references data-header))
1540          ((equal field 'date)
1541           (mail-header-date data-header))
1542          ((equal field 'xref)
1543           (mail-header-xref data-header))
1544          ((equal field 'extra)
1545           (mail-header-extra data-header))
1546          (t
1547           (gnus-error 
1548            5 
1549            "spam-fetch-field-fast: unknown field %s requested" 
1550            field)
1551           nil))
1552         (gnus-message 6 "Article %d has a nil data header" article)))))
1553
1554 (defun spam-fetch-field-from-fast (article &optional prepared-data-header)
1555   (spam-fetch-field-fast article 'from prepared-data-header))
1556
1557 (defun spam-fetch-field-subject-fast (article &optional prepared-data-header)
1558   (spam-fetch-field-fast article 'subject prepared-data-header))
1559
1560 (defun spam-fetch-field-message-id-fast (article &optional prepared-data-header)
1561   (spam-fetch-field-fast article 'message-id prepared-data-header))
1562
1563 (defun spam-generate-fake-headers (article)
1564   (let ((dh (spam-fetch-article-header article)))
1565     (if dh
1566         (concat
1567          (format
1568           ;; 80-character limit makes for strange constructs
1569           (concat "From: %s\nSubject: %s\nMessage-ID: %s\n"
1570                   "Date: %s\nReferences: %s\nXref: %s\n")
1571           (spam-fetch-field-fast article 'from dh)
1572           (spam-fetch-field-fast article 'subject dh)
1573           (spam-fetch-field-fast article 'message-id dh)
1574           (spam-fetch-field-fast article 'date dh)
1575           (spam-fetch-field-fast article 'references dh)
1576           (spam-fetch-field-fast article 'xref dh))
1577          (when (spam-fetch-field-fast article 'extra dh)
1578            (format "%s\n" (spam-fetch-field-fast article 'extra dh))))
1579       (gnus-message
1580        5
1581        "spam-generate-fake-headers: article %d didn't have a valid header"
1582        article))))
1583
1584 (defun spam-fetch-article-header (article)
1585   (save-excursion
1586     (set-buffer gnus-summary-buffer)
1587     (gnus-read-header article)
1588     (nth 3 (assq article gnus-newsgroup-data))))
1589 ;;}}}
1590
1591 ;;{{{ Spam determination.
1592
1593 (defun spam-split (&rest specific-checks)
1594   "Split this message into the `spam' group if it is spam.
1595 This function can be used as an entry in the variable `nnmail-split-fancy',
1596 for example like this: (: spam-split).  It can take checks as
1597 parameters.  A string as a parameter will set the
1598 spam-split-group to that string.
1599
1600 See the Info node `(gnus)Fancy Mail Splitting' for more details."
1601   (interactive)
1602   (setq spam-split-last-successful-check nil)
1603   (unless spam-split-disabled
1604     (let ((spam-split-group-choice spam-split-group))
1605       (dolist (check specific-checks)
1606         (when (stringp check)
1607           (setq spam-split-group-choice check)
1608           (setq specific-checks (delq check specific-checks))))
1609
1610       (let ((spam-split-group spam-split-group-choice)
1611             (widening-needed-check (spam-widening-needed-p specific-checks)))
1612         (save-excursion
1613           (save-restriction
1614             (when widening-needed-check
1615               (widen)
1616               (gnus-message 8 "spam-split: widening the buffer (%s requires it)"
1617                             widening-needed-check))
1618             (let ((backends (spam-backend-list))
1619                   decision)
1620               (while (and backends (not decision))
1621                 (let* ((backend (pop backends))
1622                        (check-function (spam-backend-check backend))
1623                        (spam-split-group (if spam-split-symbolic-return
1624                                              'spam
1625                                            spam-split-group)))
1626                   (when (or
1627                          ;; either, given specific checks, this is one of them
1628                          (memq backend specific-checks)
1629                          ;; or, given no specific checks, spam-use-CHECK is set
1630                          (and (null specific-checks) (symbol-value backend)))
1631                     (gnus-message 6 "spam-split: calling the %s function"
1632                                   check-function)
1633                     (setq decision (funcall check-function))
1634                     ;; if we got a decision at all, save the current check
1635                     (when decision
1636                       (setq spam-split-last-successful-check backend))
1637
1638                     (when (eq decision 'spam)
1639                       (unless spam-split-symbolic-return
1640                         (gnus-error
1641                          5
1642                          (format "spam-split got %s but %s is nil"
1643                                  decision
1644                                  spam-split-symbolic-return)))))))
1645               (if (eq decision t)
1646                   (if spam-split-symbolic-return-positive 'ham nil)
1647                 decision))))))))
1648
1649 (defun spam-find-spam ()
1650   "This function will detect spam in the current newsgroup using spam-split."
1651   (interactive)
1652
1653   (let* ((group gnus-newsgroup-name)
1654          (autodetect (gnus-parameter-spam-autodetect group))
1655          (methods (gnus-parameter-spam-autodetect-methods group))
1656          (first-method (nth 0 methods))
1657          (articles (if spam-autodetect-recheck-messages
1658                        gnus-newsgroup-articles
1659                      gnus-newsgroup-unseen))
1660          article-cannot-be-faked)
1661
1662     
1663     (dolist (backend methods)
1664       (when (spam-backend-statistical-p backend)
1665         (setq article-cannot-be-faked t)
1666         (return)))
1667
1668     (when (memq 'default methods)
1669       (setq article-cannot-be-faked t))
1670
1671     (when (and autodetect
1672                (not (equal first-method 'none)))
1673       (mapcar
1674        (lambda (article)
1675          (let ((id (spam-fetch-field-message-id-fast article))
1676                (subject (spam-fetch-field-subject-fast article))
1677                (sender (spam-fetch-field-from-fast article))
1678                registry-lookup)
1679            
1680            (unless id
1681              (gnus-message 6 "Article %d has no message ID!" article))
1682          
1683            (when (and id spam-log-to-registry)
1684              (setq registry-lookup (spam-log-registration-type id 'incoming))
1685              (when registry-lookup
1686                (gnus-message
1687                 9
1688                 "spam-find-spam: message %s was already registered incoming"
1689                 id)))
1690
1691            (let* ((spam-split-symbolic-return t)
1692                   (spam-split-symbolic-return-positive t)
1693                   (fake-headers (spam-generate-fake-headers article))
1694                   (split-return
1695                    (or registry-lookup
1696                        (with-temp-buffer
1697                          (if article-cannot-be-faked
1698                              (gnus-request-article-this-buffer
1699                               article
1700                               group)
1701                            ;; else, we fake the article
1702                            (when fake-headers (insert fake-headers)))
1703                          (if (or (null first-method)
1704                                  (equal first-method 'default))
1705                              (spam-split)
1706                            (apply 'spam-split methods))))))
1707              (if (equal split-return 'spam)
1708                  (gnus-summary-mark-article article gnus-spam-mark))
1709            
1710              (when (and id split-return spam-log-to-registry)
1711                (when (zerop (gnus-registry-group-count id))
1712                  (gnus-registry-add-group
1713                   id group subject sender))
1714                
1715                (unless registry-lookup
1716                  (spam-log-processing-to-registry
1717                   id
1718                   'incoming
1719                   split-return
1720                   spam-split-last-successful-check
1721                   group))))))
1722        articles))))
1723
1724 ;;}}}
1725
1726 ;;{{{ registration/unregistration functions
1727
1728 (defun spam-resolve-registrations-routine ()
1729   "Go through the backends and register or unregister articles as needed."
1730   (dolist (backend-type '(non-mover mover))
1731     (dolist (classification (spam-classifications))
1732       (dolist (backend (spam-backend-list backend-type))
1733         (let ((rlist (spam-backend-get-article-todo-list
1734                       backend classification))
1735               (ulist (spam-backend-get-article-todo-list
1736                       backend classification t))
1737               (delcount 0))
1738
1739           ;; clear the old lists right away
1740           (spam-backend-put-article-todo-list backend 
1741                                               classification
1742                                               nil
1743                                               nil)
1744           (spam-backend-put-article-todo-list backend 
1745                                               classification
1746                                               nil
1747                                               t)
1748
1749           ;; eliminate duplicates
1750           (dolist (article (copy-sequence ulist))
1751             (when (memq article rlist)
1752               (incf delcount)
1753               (setq rlist (delq article rlist))
1754               (setq ulist (delq article ulist))))
1755           
1756           (unless (zerop delcount)
1757             (gnus-message 
1758              9 
1759              "%d messages were saved the trouble of unregistering and then registering"
1760              delcount))
1761           
1762           ;; unregister articles
1763           (unless (zerop (length ulist))
1764             (let ((num (spam-unregister-routine classification backend ulist)))
1765               (when (> num 0)
1766                 (gnus-message 
1767                  6
1768                  "%d %s messages were unregistered by backend %s."
1769                  num
1770                  classification
1771                  backend))))
1772             
1773             ;; register articles
1774             (unless (zerop (length rlist))
1775               (let ((num (spam-register-routine classification backend rlist)))
1776                 (when (> num 0)
1777                   (gnus-message 
1778                    6
1779                    "%d %s messages were registered by backend %s."
1780                    num
1781                    classification
1782                    backend)))))))))
1783
1784 (defun spam-unregister-routine (classification
1785                                 backend 
1786                                 specific-articles)
1787   (spam-register-routine classification backend specific-articles t))
1788
1789 (defun spam-register-routine (classification
1790                               backend 
1791                               specific-articles
1792                               &optional unregister)
1793   (when (and (spam-classification-valid-p classification)
1794              (spam-backend-valid-p backend))
1795     (let* ((register-function
1796             (spam-backend-function backend classification 'registration))
1797            (unregister-function
1798             (spam-backend-function backend classification 'unregistration))
1799            (run-function (if unregister
1800                              unregister-function
1801                            register-function))
1802            (log-function (if unregister
1803                              'spam-log-undo-registration
1804                            'spam-log-processing-to-registry))
1805            article articles)
1806
1807       (when run-function
1808         ;; make list of articles, using specific-articles if given
1809         (setq articles (or specific-articles
1810                            (spam-list-articles
1811                             gnus-newsgroup-articles
1812                             classification)))
1813         ;; process them
1814         (when (> (length articles) 0)
1815           (gnus-message 5 "%s %d %s articles as %s using backend %s"
1816                         (if unregister "Unregistering" "Registering")
1817                         (length articles)
1818                         (if specific-articles "specific" "")
1819                         classification
1820                         backend)
1821           (funcall run-function articles)
1822           ;; now log all the registrations (or undo them, depending on
1823           ;; unregister)
1824           (dolist (article articles)
1825             (funcall log-function
1826                      (spam-fetch-field-message-id-fast article)
1827                      'process
1828                      classification
1829                      backend
1830                      gnus-newsgroup-name))))
1831       ;; return the number of articles processed
1832       (length articles))))
1833
1834 ;;; log a ham- or spam-processor invocation to the registry
1835 (defun spam-log-processing-to-registry (id type classification backend group)
1836   (when spam-log-to-registry
1837     (if (and (stringp id)
1838              (stringp group)
1839              (spam-process-type-valid-p type)
1840              (spam-classification-valid-p classification)
1841              (spam-backend-valid-p backend))
1842         (let ((cell-list (cdr-safe (gnus-registry-fetch-extra id type)))
1843               (cell (list classification backend group)))
1844           (push cell cell-list)
1845           (gnus-registry-store-extra-entry
1846            id
1847            type
1848            cell-list))
1849
1850       (gnus-error
1851        7
1852        (format "%s call with bad ID, type, classification, spam-backend, or group"
1853                "spam-log-processing-to-registry")))))
1854
1855 ;;; check if a ham- or spam-processor registration has been done
1856 (defun spam-log-registered-p (id type)
1857   (when spam-log-to-registry
1858     (if (and (stringp id)
1859              (spam-process-type-valid-p type))
1860         (cdr-safe (gnus-registry-fetch-extra id type))
1861       (progn
1862         (gnus-error
1863          7
1864          (format "%s called with bad ID, type, classification, or spam-backend"
1865                  "spam-log-registered-p"))
1866         nil))))
1867
1868 ;;; check what a ham- or spam-processor registration says
1869 ;;; returns nil if conflicting registrations are found
1870 (defun spam-log-registration-type (id type)
1871   (let ((count 0)
1872         decision)
1873     (dolist (reg (spam-log-registered-p id type))
1874       (let ((classification (nth 0 reg)))
1875         (when (spam-classification-valid-p classification)
1876           (when (and decision
1877                      (not (eq classification decision)))
1878             (setq count (+ 1 count)))
1879           (setq decision classification))))
1880     (if (< 0 count)
1881         nil
1882       decision)))
1883
1884
1885 ;;; check if a ham- or spam-processor registration needs to be undone
1886 (defun spam-log-unregistration-needed-p (id type classification backend)
1887   (when spam-log-to-registry
1888     (if (and (stringp id)
1889              (spam-process-type-valid-p type)
1890              (spam-classification-valid-p classification)
1891              (spam-backend-valid-p backend))
1892         (let ((cell-list (cdr-safe (gnus-registry-fetch-extra id type)))
1893               found)
1894           (dolist (cell cell-list)
1895             (unless found
1896               (when (and (eq classification (nth 0 cell))
1897                          (eq backend (nth 1 cell)))
1898                 (setq found t))))
1899           found)
1900       (progn
1901         (gnus-error
1902          7
1903          (format "%s called with bad ID, type, classification, or spam-backend"
1904                  "spam-log-unregistration-needed-p"))
1905         nil))))
1906
1907
1908 ;;; undo a ham- or spam-processor registration (the group is not used)
1909 (defun spam-log-undo-registration (id type classification backend &optional group)
1910   (when (and spam-log-to-registry
1911              (spam-log-unregistration-needed-p id type classification backend))
1912     (if (and (stringp id)
1913              (spam-process-type-valid-p type)
1914              (spam-classification-valid-p classification)
1915              (spam-backend-valid-p backend))
1916         (let ((cell-list (cdr-safe (gnus-registry-fetch-extra id type)))
1917               new-cell-list found)
1918           (dolist (cell cell-list)
1919             (unless (and (eq classification (nth 0 cell))
1920                          (eq backend (nth 1 cell)))
1921               (push cell new-cell-list)))
1922           (gnus-registry-store-extra-entry
1923            id
1924            type
1925            new-cell-list))
1926       (progn
1927         (gnus-error 7 (format "%s call with bad ID, type, spam-backend, or group"
1928                               "spam-log-undo-registration"))
1929         nil))))
1930
1931 ;;}}}
1932
1933 ;;{{{ backend functions
1934
1935 ;;{{{ Gmane xrefs
1936 (defun spam-check-gmane-xref ()
1937   (let ((header (or
1938                  (message-fetch-field "Xref")
1939                  (message-fetch-field "Newsgroups"))))
1940     (when header                        ; return nil when no header
1941       (when (string-match spam-gmane-xref-spam-group
1942                           header)
1943           spam-split-group))))
1944
1945 ;;}}}
1946
1947 ;;{{{ Regex body
1948
1949 (defun spam-check-regex-body ()
1950   (let ((spam-regex-headers-ham spam-regex-body-ham)
1951         (spam-regex-headers-spam spam-regex-body-spam))
1952     (spam-check-regex-headers t)))
1953
1954 ;;}}}
1955
1956 ;;{{{ Regex headers
1957
1958 (defun spam-check-regex-headers (&optional body)
1959   (let ((type (if body "body" "header"))
1960         ret found)
1961     (dolist (h-regex spam-regex-headers-ham)
1962       (unless found
1963         (goto-char (point-min))
1964         (when (re-search-forward h-regex nil t)
1965           (message "Ham regex %s search positive." type)
1966           (setq found t))))
1967     (dolist (s-regex spam-regex-headers-spam)
1968       (unless found
1969         (goto-char (point-min))
1970         (when (re-search-forward s-regex nil t)
1971           (message "Spam regex %s search positive." type)
1972           (setq found t)
1973           (setq ret spam-split-group))))
1974     ret))
1975
1976 ;;}}}
1977
1978 ;;{{{ Blackholes.
1979
1980 (defun spam-reverse-ip-string (ip)
1981   (when (stringp ip)
1982     (mapconcat 'identity
1983                (nreverse (split-string ip "\\."))
1984                ".")))
1985
1986 (defun spam-check-blackholes ()
1987   "Check the Received headers for blackholed relays."
1988   (let ((headers (message-fetch-field "received"))
1989         ips matches)
1990     (when headers
1991       (with-temp-buffer
1992         (insert headers)
1993         (goto-char (point-min))
1994         (gnus-message 6 "Checking headers for relay addresses")
1995         (while (re-search-forward
1996                 "\\([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+\\)" nil t)
1997           (gnus-message 9 "Blackhole search found host IP %s." (match-string 1))
1998           (push (spam-reverse-ip-string (match-string 1))
1999                 ips)))
2000       (dolist (server spam-blackhole-servers)
2001         (dolist (ip ips)
2002           (unless (and spam-blackhole-good-server-regex
2003                        ;; match the good-server-regex against the reversed (again) IP string
2004                        (string-match
2005                         spam-blackhole-good-server-regex
2006                         (spam-reverse-ip-string ip)))
2007             (unless matches
2008               (let ((query-string (concat ip "." server)))
2009                 (if spam-use-dig
2010                     (let ((query-result (query-dig query-string)))
2011                       (when query-result
2012                         (gnus-message 6 "(DIG): positive blackhole check '%s'"
2013                                       query-result)
2014                         (push (list ip server query-result)
2015                               matches)))
2016                   ;; else, if not using dig.el
2017                   (when (query-dns query-string)
2018                     (gnus-message 6 "positive blackhole check")
2019                     (push (list ip server (query-dns query-string 'TXT))
2020                           matches)))))))))
2021     (when matches
2022       spam-split-group)))
2023 ;;}}}
2024
2025 ;;{{{ Hashcash.
2026
2027 (defun spam-check-hashcash ()
2028   "Check the headers for hashcash payments."
2029   (ignore-errors (mail-check-payment)))  ;mail-check-payment returns a boolean
2030
2031 ;;}}}
2032
2033 ;;{{{ BBDB
2034
2035 ;;; original idea for spam-check-BBDB from Alexander Kotelnikov
2036 ;;; <sacha@giotto.sj.ru>
2037
2038 ;; all this is done inside a condition-case to trap errors
2039
2040 (eval-when-compile
2041   (autoload 'bbdb-buffer "bbdb")
2042   (autoload 'bbdb-create-internal "bbdb")
2043   (autoload 'bbdb-search-simple "bbdb"))
2044
2045 (eval-and-compile
2046   (when (condition-case nil
2047             (progn
2048               (require 'bbdb)
2049               (require 'bbdb-com))
2050           (file-error
2051            ;; `bbdb-records' should not be bound as an autoload function
2052            ;; before loading bbdb because of `bbdb-hashtable-size'.
2053            (defalias 'bbdb-records 'ignore)
2054            (defalias 'spam-BBDB-register-routine 'ignore)
2055            (defalias 'spam-enter-ham-BBDB 'ignore)
2056            nil))
2057
2058     ;; when the BBDB changes, we want to clear out our cache
2059     (defun spam-clear-cache-BBDB (&rest immaterial)
2060       (spam-clear-cache 'spam-use-BBDB))
2061
2062     (add-hook 'bbdb-change-hook 'spam-clear-cache-BBDB)
2063
2064     (defun spam-enter-ham-BBDB (addresses &optional remove)
2065       "Enter an address into the BBDB; implies ham (non-spam) sender"
2066       (dolist (from addresses)
2067         (when (stringp from)
2068           (let* ((parsed-address (gnus-extract-address-components from))
2069                  (name (or (nth 0 parsed-address) "Ham Sender"))
2070                  (remove-function (if remove
2071                                       'bbdb-delete-record-internal
2072                                     'ignore))
2073                  (net-address (nth 1 parsed-address))
2074                  (record (and net-address
2075                               (bbdb-search-simple nil net-address))))
2076             (when net-address
2077               (gnus-message 6 "%s address %s %s BBDB"
2078                             (if remove "Deleting" "Adding")
2079                             from
2080                             (if remove "from" "to"))
2081               (if record
2082                   (funcall remove-function record)
2083                 (bbdb-create-internal name nil net-address nil nil
2084                                       "ham sender added by spam.el")))))))
2085
2086     (defun spam-BBDB-register-routine (articles &optional unregister)
2087       (let (addresses)
2088         (dolist (article articles)
2089           (when (stringp (spam-fetch-field-from-fast article))
2090             (push (spam-fetch-field-from-fast article) addresses)))
2091         ;; now do the register/unregister action
2092         (spam-enter-ham-BBDB addresses unregister)))
2093
2094     (defun spam-BBDB-unregister-routine (articles)
2095       (spam-BBDB-register-routine articles t))
2096
2097     (defun spam-check-BBDB ()
2098       "Mail from people in the BBDB is classified as ham or non-spam"
2099       (let ((who (message-fetch-field "from"))
2100             bbdb-cache bbdb-hashtable)
2101         (when spam-cache-lookups
2102           (setq bbdb-cache (gethash 'spam-use-BBDB spam-caches))
2103           (unless bbdb-cache
2104             (setq bbdb-cache (make-vector 17 0)) ; a good starting hash value
2105             ;; this is based on the expanded (bbdb-hashtable) macro
2106             ;; without the debugging support
2107             (with-current-buffer (bbdb-buffer)
2108               (save-excursion
2109                 (save-window-excursion
2110                   (bbdb-records nil t)
2111                   (mapatoms 
2112                    (lambda (symbol)
2113                      (intern (downcase (symbol-name symbol)) bbdb-cache))
2114                    bbdb-hashtable))))
2115             (puthash 'spam-use-BBDB bbdb-cache spam-caches)))
2116         (when who
2117           (setq who (nth 1 (gnus-extract-address-components who)))
2118           (if
2119               (if spam-cache-lookups
2120                   (intern-soft (downcase who) bbdb-cache)
2121                 (bbdb-search-simple nil who))
2122               t
2123             (if spam-use-BBDB-exclusive
2124                 spam-split-group
2125               nil)))))))
2126
2127 ;;}}}
2128
2129 ;;{{{ ifile
2130
2131 ;;; check the ifile backend; return nil if the mail was NOT classified
2132 ;;; as spam
2133
2134 (defun spam-get-ifile-database-parameter ()
2135   "Get the command-line parameter for ifile's database from
2136   spam-ifile-database-path."
2137   (if spam-ifile-database-path
2138       (format "--db-file=%s" spam-ifile-database-path)
2139     nil))
2140
2141 (defun spam-check-ifile ()
2142   "Check the ifile backend for the classification of this message."
2143   (let ((article-buffer-name (buffer-name))
2144         category return)
2145     (with-temp-buffer
2146       (let ((temp-buffer-name (buffer-name))
2147             (db-param (spam-get-ifile-database-parameter)))
2148         (save-excursion
2149           (set-buffer article-buffer-name)
2150           (apply 'call-process-region
2151                  (point-min) (point-max) spam-ifile-path
2152                  nil temp-buffer-name nil "-c"
2153                  (if db-param `(,db-param "-q") `("-q"))))
2154         ;; check the return now (we're back in the temp buffer)
2155         (goto-char (point-min))
2156         (if (not (eobp))
2157             (setq category (buffer-substring (point) (point-at-eol))))
2158         (when (not (zerop (length category))) ; we need a category here
2159           (if spam-ifile-all-categories
2160               (setq return category)
2161             ;; else, if spam-ifile-all-categories is not set...
2162             (when (string-equal spam-ifile-spam-category category)
2163               (setq return spam-split-group)))))) ; note return is nil otherwise
2164     return))
2165
2166 (defun spam-ifile-register-with-ifile (articles category &optional unregister)
2167   "Register an article, given as a string, with a category.
2168 Uses `gnus-newsgroup-name' if category is nil (for ham registration)."
2169   (let ((category (or category gnus-newsgroup-name))
2170         (add-or-delete-option (if unregister "-d" "-i"))
2171         (db (spam-get-ifile-database-parameter))
2172         parameters)
2173     (with-temp-buffer
2174       (dolist (article articles)
2175         (let ((article-string (spam-get-article-as-string article)))
2176           (when (stringp article-string)
2177             (insert article-string))))
2178       (apply 'call-process-region
2179              (point-min) (point-max) spam-ifile-path
2180              nil nil nil
2181              add-or-delete-option category
2182              (if db `(,db "-h") `("-h"))))))
2183
2184 (defun spam-ifile-register-spam-routine (articles &optional unregister)
2185   (spam-ifile-register-with-ifile articles spam-ifile-spam-category unregister))
2186
2187 (defun spam-ifile-unregister-spam-routine (articles)
2188   (spam-ifile-register-spam-routine articles t))
2189
2190 (defun spam-ifile-register-ham-routine (articles &optional unregister)
2191   (spam-ifile-register-with-ifile articles spam-ifile-ham-category unregister))
2192
2193 (defun spam-ifile-unregister-ham-routine (articles)
2194   (spam-ifile-register-ham-routine articles t))
2195
2196 ;;}}}
2197
2198 ;;{{{ spam-stat
2199
2200 (eval-when-compile
2201   (autoload 'spam-stat-buffer-change-to-non-spam "spam-stat")
2202   (autoload 'spam-stat-buffer-change-to-spam "spam-stat")
2203   (autoload 'spam-stat-buffer-is-non-spam "spam-stat")
2204   (autoload 'spam-stat-buffer-is-spam "spam-stat")
2205   (autoload 'spam-stat-load "spam-stat")
2206   (autoload 'spam-stat-save "spam-stat")
2207   (autoload 'spam-stat-split-fancy "spam-stat"))
2208
2209 (eval-and-compile
2210   (when (condition-case nil
2211             (let ((spam-stat-install-hooks nil))
2212               (require 'spam-stat))
2213           (file-error
2214            (defalias 'spam-stat-register-ham-routine 'ignore)
2215            (defalias 'spam-stat-register-spam-routine 'ignore)
2216            nil))
2217
2218     (defun spam-check-stat ()
2219       "Check the spam-stat backend for the classification of this message"
2220       (let ((spam-stat-split-fancy-spam-group spam-split-group) ; override
2221             (spam-stat-buffer (buffer-name)) ; stat the current buffer
2222             category return)
2223         (spam-stat-split-fancy)))
2224
2225     (defun spam-stat-register-spam-routine (articles &optional unregister)
2226       (dolist (article articles)
2227         (let ((article-string (spam-get-article-as-string article)))
2228           (with-temp-buffer
2229             (insert article-string)
2230             (if unregister
2231                 (spam-stat-buffer-change-to-non-spam)
2232               (spam-stat-buffer-is-spam))))))
2233
2234     (defun spam-stat-unregister-spam-routine (articles)
2235       (spam-stat-register-spam-routine articles t))
2236
2237     (defun spam-stat-register-ham-routine (articles &optional unregister)
2238       (dolist (article articles)
2239         (let ((article-string (spam-get-article-as-string article)))
2240           (with-temp-buffer
2241             (insert article-string)
2242             (if unregister
2243                 (spam-stat-buffer-change-to-spam)
2244               (spam-stat-buffer-is-non-spam))))))
2245
2246     (defun spam-stat-unregister-ham-routine (articles)
2247       (spam-stat-register-ham-routine articles t))
2248
2249     (defun spam-maybe-spam-stat-load ()
2250       (when spam-use-stat (spam-stat-load)))
2251
2252     (defun spam-maybe-spam-stat-save ()
2253       (when spam-use-stat (spam-stat-save)))))
2254
2255 ;;}}}
2256
2257 ;;{{{ Blacklists and whitelists.
2258
2259 (defvar spam-whitelist-cache nil)
2260 (defvar spam-blacklist-cache nil)
2261
2262 (defun spam-kill-whole-line ()
2263   (beginning-of-line)
2264   (let ((kill-whole-line t))
2265     (kill-line)))
2266
2267 ;;; address can be a list, too
2268 (defun spam-enter-whitelist (address &optional remove)
2269   "Enter ADDRESS (list or single) into the whitelist.
2270 With a non-nil REMOVE, remove them."
2271   (interactive "sAddress: ")
2272   (spam-enter-list address spam-whitelist remove)
2273   (setq spam-whitelist-cache nil)
2274   (spam-clear-cache 'spam-use-whitelist))
2275
2276 ;;; address can be a list, too
2277 (defun spam-enter-blacklist (address &optional remove)
2278   "Enter ADDRESS (list or single) into the blacklist.
2279 With a non-nil REMOVE, remove them."
2280   (interactive "sAddress: ")
2281   (spam-enter-list address spam-blacklist remove)
2282   (setq spam-blacklist-cache nil)
2283   (spam-clear-cache 'spam-use-whitelist))
2284
2285 (defun spam-enter-list (addresses file &optional remove)
2286   "Enter ADDRESSES into the given FILE.
2287 Either the whitelist or the blacklist files can be used.  With
2288 REMOVE not nil, remove the ADDRESSES."
2289   (if (stringp addresses)
2290       (spam-enter-list (list addresses) file remove)
2291     ;; else, we have a list of addresses here
2292     (unless (file-exists-p (file-name-directory file))
2293       (make-directory (file-name-directory file) t))
2294     (save-excursion
2295       (set-buffer
2296        (find-file-noselect file))
2297       (dolist (a addresses)
2298         (when (stringp a)
2299           (goto-char (point-min))
2300           (if (re-search-forward (regexp-quote a) nil t)
2301               ;; found the address
2302               (when remove
2303                 (spam-kill-whole-line))
2304             ;; else, the address was not found
2305             (unless remove
2306               (goto-char (point-max))
2307               (unless (bobp)
2308                 (insert "\n"))
2309               (insert a "\n")))))
2310       (save-buffer))))
2311
2312 (defun spam-filelist-build-cache (type)
2313   (let ((cache (if (eq type 'spam-use-blacklist)
2314                    spam-blacklist-cache
2315                  spam-whitelist-cache))
2316         parsed-cache)
2317     (unless (gethash type spam-caches)
2318       (while cache
2319         (let ((address (pop cache)))
2320           (unless (zerop (length address)) ; 0 for a nil address too
2321             (setq address (regexp-quote address))
2322             ;; fix regexp-quote's treatment of user-intended regexes
2323             (while (string-match "\\\\\\*" address)
2324               (setq address (replace-match ".*" t t address))))
2325           (push address parsed-cache)))
2326       (puthash type parsed-cache spam-caches))))
2327
2328 (defun spam-filelist-check-cache (type from)
2329   (when (stringp from)
2330     (spam-filelist-build-cache type)
2331     (let (found)
2332       (dolist (address (gethash type spam-caches))
2333         (when (and address (string-match address from))
2334           (setq found t)
2335           (return)))
2336       found)))
2337
2338 ;;; returns t if the sender is in the whitelist, nil or
2339 ;;; spam-split-group otherwise
2340 (defun spam-check-whitelist ()
2341   ;; FIXME!  Should it detect when file timestamps change?
2342   (unless spam-whitelist-cache
2343     (setq spam-whitelist-cache (spam-parse-list spam-whitelist)))
2344   (if (spam-from-listed-p 'spam-use-whitelist)
2345       t
2346     (if spam-use-whitelist-exclusive
2347         spam-split-group
2348       nil)))
2349
2350 (defun spam-check-blacklist ()
2351   ;; FIXME!  Should it detect when file timestamps change?
2352   (unless spam-blacklist-cache
2353     (setq spam-blacklist-cache (spam-parse-list spam-blacklist)))
2354   (and (spam-from-listed-p 'spam-use-blacklist)
2355        spam-split-group))
2356
2357 (defun spam-parse-list (file)
2358   (when (file-readable-p file)
2359     (let (contents address)
2360       (with-temp-buffer
2361         (insert-file-contents file)
2362         (while (not (eobp))
2363           (setq address (buffer-substring (point) (point-at-eol)))
2364           (forward-line 1)
2365           ;; insert the e-mail address if detected, otherwise the raw data
2366           (unless (zerop (length address))
2367             (let ((pure-address (nth 1 (gnus-extract-address-components address))))
2368               (push (or pure-address address) contents)))))
2369       (nreverse contents))))
2370
2371 (defun spam-from-listed-p (type)
2372   (let ((from (message-fetch-field "from"))
2373         found)
2374     (spam-filelist-check-cache type from)))
2375
2376 (defun spam-filelist-register-routine (articles blacklist &optional unregister)
2377   (let ((de-symbol (if blacklist 'spam-use-whitelist 'spam-use-blacklist))
2378         (declassification (if blacklist 'ham 'spam))
2379         (enter-function
2380          (if blacklist 'spam-enter-blacklist 'spam-enter-whitelist))
2381         (remove-function
2382          (if blacklist 'spam-enter-whitelist 'spam-enter-blacklist))
2383         from addresses unregister-list article-unregister-list)
2384     (dolist (article articles)
2385       (let ((from (spam-fetch-field-from-fast article))
2386             (id (spam-fetch-field-message-id-fast article))
2387             sender-ignored)
2388         (when (stringp from)
2389           (dolist (ignore-regex spam-blacklist-ignored-regexes)
2390             (when (and (not sender-ignored)
2391                        (stringp ignore-regex)
2392                        (string-match ignore-regex from))
2393               (setq sender-ignored t)))
2394           ;; remember the messages we need to unregister, unless remove is set
2395           (when (and
2396                  (null unregister)
2397                  (spam-log-unregistration-needed-p
2398                   id 'process declassification de-symbol))
2399             (push article article-unregister-list)
2400             (push from unregister-list))
2401           (unless sender-ignored
2402             (push from addresses)))))
2403
2404     (if unregister
2405         (funcall enter-function addresses t) ; unregister all these addresses
2406       ;; else, register normally and unregister what we need to
2407       (funcall remove-function unregister-list t)
2408       (dolist (article article-unregister-list)
2409         (spam-log-undo-registration
2410          (spam-fetch-field-message-id-fast article)
2411          'process
2412          declassification
2413          de-symbol))
2414       (funcall enter-function addresses nil))))
2415
2416 (defun spam-blacklist-unregister-routine (articles)
2417   (spam-blacklist-register-routine articles t))
2418
2419 (defun spam-blacklist-register-routine (articles &optional unregister)
2420   (spam-filelist-register-routine articles t unregister))
2421
2422 (defun spam-whitelist-unregister-routine (articles)
2423   (spam-whitelist-register-routine articles t))
2424
2425 (defun spam-whitelist-register-routine (articles &optional unregister)
2426   (spam-filelist-register-routine articles nil unregister))
2427
2428 ;;}}}
2429
2430 ;;{{{ Spam-report glue (gmane and resend reporting)
2431 (defun spam-report-gmane-register-routine (articles)
2432   (when articles
2433     (apply 'spam-report-gmane-spam articles)))
2434
2435 (defun spam-report-gmane-unregister-routine (articles)
2436   (when articles
2437     (apply 'spam-report-gmane-ham articles)))
2438
2439 (defun spam-report-resend-register-ham-routine (articles)
2440   (spam-report-resend-register-routine articles t))
2441
2442 (defun spam-report-resend-register-routine (articles &optional ham)
2443   (let* ((resend-to-gp 
2444           (if ham
2445               (gnus-parameter-ham-resend-to gnus-newsgroup-name)
2446             (gnus-parameter-spam-resend-to gnus-newsgroup-name)))
2447          (spam-report-resend-to (or (car-safe resend-to-gp)
2448                                     spam-report-resend-to)))
2449     (spam-report-resend articles ham)))
2450
2451 ;;}}}
2452
2453 ;;{{{ Bogofilter
2454 (defun spam-check-bogofilter-headers (&optional score)
2455   (let ((header (message-fetch-field spam-bogofilter-header)))
2456     (when header                        ; return nil when no header
2457       (if score                         ; scoring mode
2458           (if (string-match "spamicity=\\([0-9.]+\\)" header)
2459               (match-string 1 header)
2460             "0")
2461         ;; spam detection mode
2462         (when (string-match spam-bogofilter-bogosity-positive-spam-header
2463                             header)
2464           spam-split-group)))))
2465
2466 ;; return something sensible if the score can't be determined
2467 (defun spam-bogofilter-score (&optional recheck)
2468   "Get the Bogofilter spamicity score"
2469   (interactive "P")
2470   (save-window-excursion
2471     (gnus-summary-show-article t)
2472     (set-buffer gnus-article-buffer)
2473     (let ((score (or (unless recheck
2474                        (spam-check-bogofilter-headers t))
2475                      (spam-check-bogofilter t))))
2476       (gnus-summary-show-article)
2477       (message "Spamicity score %s" score)
2478       (or score "0"))))
2479
2480 (defun spam-verify-bogofilter ()
2481   "Verify the Bogofilter version is sufficient."
2482   (when (eq spam-bogofilter-valid 'unknown)
2483     (setq spam-bogofilter-valid
2484           (not (string-match "^bogofilter version 0\\.\\([0-9]\\|1[01]\\)\\."
2485                              (shell-command-to-string 
2486                               (format "%s -V" spam-bogofilter-path))))))
2487   spam-bogofilter-valid)
2488   
2489 (defun spam-check-bogofilter (&optional score)
2490   "Check the Bogofilter backend for the classification of this message."
2491   (if (spam-verify-bogofilter)
2492       (let ((article-buffer-name (buffer-name))
2493             (db spam-bogofilter-database-directory)
2494             return)
2495         (with-temp-buffer
2496           (let ((temp-buffer-name (buffer-name)))
2497             (save-excursion
2498               (set-buffer article-buffer-name)
2499               (apply 'call-process-region
2500                      (point-min) (point-max)
2501                      spam-bogofilter-path
2502                      nil temp-buffer-name nil
2503                      (if db `("-d" ,db "-v") `("-v"))))
2504             (setq return (spam-check-bogofilter-headers score))))
2505         return)
2506     (gnus-error "`spam.el' doesnt support obsolete bogofilter versions")))
2507
2508 (defun spam-bogofilter-register-with-bogofilter (articles
2509                                                  spam
2510                                                  &optional unregister)
2511   "Register an article, given as a string, as spam or non-spam."
2512   (if (spam-verify-bogofilter)
2513       (dolist (article articles)
2514         (let ((article-string (spam-get-article-as-string article))
2515               (db spam-bogofilter-database-directory)
2516               (switch (if unregister
2517                           (if spam
2518                               spam-bogofilter-spam-strong-switch
2519                             spam-bogofilter-ham-strong-switch)
2520                         (if spam
2521                             spam-bogofilter-spam-switch
2522                           spam-bogofilter-ham-switch))))
2523           (when (stringp article-string)
2524             (with-temp-buffer
2525               (insert article-string)
2526               
2527               (apply 'call-process-region
2528                      (point-min) (point-max)
2529                      spam-bogofilter-path
2530                      nil nil nil switch
2531                      (if db `("-d" ,db "-v") `("-v")))))))
2532     (gnus-error "`spam.el' doesnt support obsolete bogofilter versions")))
2533
2534 (defun spam-bogofilter-register-spam-routine (articles &optional unregister)
2535   (spam-bogofilter-register-with-bogofilter articles t unregister))
2536
2537 (defun spam-bogofilter-unregister-spam-routine (articles)
2538   (spam-bogofilter-register-spam-routine articles t))
2539
2540 (defun spam-bogofilter-register-ham-routine (articles &optional unregister)
2541   (spam-bogofilter-register-with-bogofilter articles nil unregister))
2542
2543 (defun spam-bogofilter-unregister-ham-routine (articles)
2544   (spam-bogofilter-register-ham-routine articles t))
2545
2546
2547 ;;}}}
2548
2549 ;;{{{ spamoracle
2550 (defun spam-check-spamoracle ()
2551   "Run spamoracle on an article to determine whether it's spam."
2552   (let ((article-buffer-name (buffer-name)))
2553     (with-temp-buffer
2554       (let ((temp-buffer-name (buffer-name)))
2555         (save-excursion
2556           (set-buffer article-buffer-name)
2557           (let ((status
2558                  (apply 'call-process-region
2559                         (point-min) (point-max)
2560                         spam-spamoracle-binary
2561                         nil temp-buffer-name nil
2562                         (if spam-spamoracle-database
2563                             `("-f" ,spam-spamoracle-database "mark")
2564                           '("mark")))))
2565             (if (eq 0 status)
2566                 (progn
2567                   (set-buffer temp-buffer-name)
2568                   (goto-char (point-min))
2569                   (when (re-search-forward "^X-Spam: yes;" nil t)
2570                     spam-split-group))
2571               (error "Error running spamoracle: %s" status))))))))
2572
2573 (defun spam-spamoracle-learn (articles article-is-spam-p &optional unregister)
2574   "Run spamoracle in training mode."
2575   (with-temp-buffer
2576     (let ((temp-buffer-name (buffer-name)))
2577       (save-excursion
2578         (goto-char (point-min))
2579         (dolist (article articles)
2580           (insert (spam-get-article-as-string article)))
2581         (let* ((arg (if (spam-xor unregister article-is-spam-p)
2582                         "-spam"
2583                       "-good"))
2584                (status
2585                 (apply 'call-process-region
2586                        (point-min) (point-max)
2587                        spam-spamoracle-binary
2588                        nil temp-buffer-name nil
2589                        (if spam-spamoracle-database
2590                            `("-f" ,spam-spamoracle-database
2591                              "add" ,arg)
2592                          `("add" ,arg)))))
2593           (unless (eq 0 status)
2594             (error "Error running spamoracle: %s" status)))))))
2595
2596 (defun spam-spamoracle-learn-ham (articles &optional unregister)
2597   (spam-spamoracle-learn articles nil unregister))
2598
2599 (defun spam-spamoracle-unlearn-ham (articles &optional unregister)
2600   (spam-spamoracle-learn-ham articles t))
2601
2602 (defun spam-spamoracle-learn-spam (articles &optional unregister)
2603   (spam-spamoracle-learn articles t unregister))
2604
2605 (defun spam-spamoracle-unlearn-spam (articles &optional unregister)
2606   (spam-spamoracle-learn-spam articles t))
2607
2608 ;;}}}
2609
2610 ;;{{{ SpamAssassin
2611 ;;; based mostly on the bogofilter code
2612 (defun spam-check-spamassassin-headers (&optional score)
2613   "Check the SpamAssassin headers for the classification of this message."
2614   (if score                             ; scoring mode
2615       (let ((header (message-fetch-field spam-spamassassin-spam-status-header)))
2616         (when header
2617           (if (string-match "hits=\\(-?[0-9.]+\\)" header)
2618               (match-string 1 header)
2619             "0")))
2620     ;; spam detection mode
2621     (let ((header (message-fetch-field spam-spamassassin-spam-flag-header)))
2622           (when header                  ; return nil when no header
2623             (when (string-match spam-spamassassin-positive-spam-flag-header
2624                                 header)
2625               spam-split-group)))))
2626
2627 (defun spam-check-spamassassin (&optional score)
2628   "Check the SpamAssassin backend for the classification of this message."
2629   (let ((article-buffer-name (buffer-name)))
2630     (with-temp-buffer
2631       (let ((temp-buffer-name (buffer-name)))
2632         (save-excursion
2633           (set-buffer article-buffer-name)
2634           (apply 'call-process-region
2635                  (point-min) (point-max) spam-spamassassin-path
2636                  nil temp-buffer-name nil spam-spamassassin-arguments))
2637         ;; check the return now (we're back in the temp buffer)
2638         (goto-char (point-min))
2639         (spam-check-spamassassin-headers score)))))
2640
2641 ;; return something sensible if the score can't be determined
2642 (defun spam-spamassassin-score (&optional recheck)
2643   "Get the SpamAssassin score"
2644   (interactive "P")
2645   (save-window-excursion
2646     (gnus-summary-show-article t)
2647     (set-buffer gnus-article-buffer)
2648     (let ((score (or (unless recheck
2649                        (spam-check-spamassassin-headers t))
2650                      (spam-check-spamassassin t))))
2651       (gnus-summary-show-article)
2652       (message "SpamAssassin score %s" score)
2653       (or score "0"))))
2654
2655 (defun spam-spamassassin-register-with-sa-learn (articles spam
2656                                                  &optional unregister)
2657   "Register articles with spamassassin's sa-learn as spam or non-spam."
2658   (if articles
2659       (let ((action (if unregister spam-sa-learn-unregister-switch
2660                       (if spam spam-sa-learn-spam-switch
2661                         spam-sa-learn-ham-switch)))
2662             (summary-buffer-name (buffer-name)))
2663         (with-temp-buffer
2664           ;; group the articles into mbox format
2665           (dolist (article articles)
2666             (let (article-string)
2667               (save-excursion
2668                 (set-buffer summary-buffer-name)
2669                 (setq article-string (spam-get-article-as-string article)))
2670               (when (stringp article-string)
2671                 (insert "From \n") ; mbox separator (sa-learn only checks the
2672                                    ; first five chars, so we can get away with
2673                                    ; a bogus line))
2674                 (insert article-string)
2675                 (insert "\n"))))
2676           ;; call sa-learn on all messages at the same time
2677           (apply 'call-process-region
2678                  (point-min) (point-max)
2679                  spam-sa-learn-path
2680                  nil nil nil "--mbox"
2681                  (if spam-sa-learn-rebuild
2682                      (list action)
2683                    `("--no-rebuild" ,action)))))))
2684
2685 (defun spam-spamassassin-register-spam-routine (articles &optional unregister)
2686   (spam-spamassassin-register-with-sa-learn articles t unregister))
2687
2688 (defun spam-spamassassin-register-ham-routine (articles &optional unregister)
2689   (spam-spamassassin-register-with-sa-learn articles nil unregister))
2690
2691 (defun spam-spamassassin-unregister-spam-routine (articles)
2692   (spam-spamassassin-register-with-sa-learn articles t t))
2693
2694 (defun spam-spamassassin-unregister-ham-routine (articles)
2695   (spam-spamassassin-register-with-sa-learn articles nil t))
2696
2697 ;;}}}
2698
2699 ;;{{{ Bsfilter
2700 ;;; based mostly on the bogofilter code
2701 (defun spam-check-bsfilter-headers (&optional score)
2702   (if score
2703       (or (nnmail-fetch-field spam-bsfilter-probability-header)
2704           "0")
2705     (let ((header (nnmail-fetch-field spam-bsfilter-header)))
2706       (when header ; return nil when no header
2707         (when (string-match "YES" header)
2708           spam-split-group)))))
2709
2710 ;; return something sensible if the score can't be determined
2711 (defun spam-bsfilter-score (&optional recheck)
2712   "Get the Bsfilter spamicity score"
2713   (interactive "P")
2714   (save-window-excursion
2715     (gnus-summary-show-article t)
2716     (set-buffer gnus-article-buffer)
2717     (let ((score (or (unless recheck
2718                        (spam-check-bsfilter-headers t))
2719                      (spam-check-bsfilter t))))
2720       (gnus-summary-show-article)
2721       (message "Spamicity score %s" score)
2722       (or score "0"))))
2723
2724 (defun spam-check-bsfilter (&optional score)
2725   "Check the Bsfilter backend for the classification of this message"
2726   (let ((article-buffer-name (buffer-name))
2727         (dir spam-bsfilter-database-directory)
2728         return)
2729     (with-temp-buffer
2730       (let ((temp-buffer-name (buffer-name)))
2731         (save-excursion
2732           (set-buffer article-buffer-name)
2733           (apply 'call-process-region
2734                  (point-min) (point-max)
2735                  spam-bsfilter-path
2736                  nil temp-buffer-name nil
2737                  "--pipe"
2738                  "--insert-flag"
2739                  "--insert-probability"
2740                  (when dir
2741                    (list "--homedir" dir))))
2742         (setq return (spam-check-bsfilter-headers score))))
2743     return))
2744
2745 (defun spam-bsfilter-register-with-bsfilter (articles
2746                                              spam
2747                                              &optional unregister)
2748   "Register an article, given as a string, as spam or non-spam."
2749   (dolist (article articles)
2750     (let ((article-string (spam-get-article-as-string article))
2751           (switch (if unregister
2752                       (if spam
2753                           spam-bsfilter-spam-strong-switch
2754                         spam-bsfilter-ham-strong-switch)
2755                     (if spam
2756                         spam-bsfilter-spam-switch
2757                       spam-bsfilter-ham-switch))))
2758       (when (stringp article-string)
2759         (with-temp-buffer
2760           (insert article-string)
2761           (apply 'call-process-region
2762                  (point-min) (point-max)
2763                  spam-bsfilter-path
2764                  nil nil nil switch
2765                  "--update"
2766                  (when spam-bsfilter-database-directory
2767                    (list "--homedir"
2768                          spam-bsfilter-database-directory))))))))
2769
2770 (defun spam-bsfilter-register-spam-routine (articles &optional unregister)
2771   (spam-bsfilter-register-with-bsfilter articles t unregister))
2772
2773 (defun spam-bsfilter-unregister-spam-routine (articles)
2774   (spam-bsfilter-register-spam-routine articles t))
2775
2776 (defun spam-bsfilter-register-ham-routine (articles &optional unregister)
2777   (spam-bsfilter-register-with-bsfilter articles nil unregister))
2778
2779 (defun spam-bsfilter-unregister-ham-routine (articles)
2780   (spam-bsfilter-register-ham-routine articles t))
2781
2782 ;;}}}
2783
2784 ;;{{{ CRM114 Mailfilter
2785 (defun spam-check-crm114-headers (&optional score)
2786   (let ((header (message-fetch-field spam-crm114-header)))
2787     (when header                        ; return nil when no header
2788       (if score                         ; scoring mode
2789           (if (string-match "( pR: \\([0-9.-]+\\)" header)
2790               (match-string 1 header)
2791             "0")
2792         ;; spam detection mode
2793         (when (string-match spam-crm114-positive-spam-header
2794                             header)
2795           spam-split-group)))))
2796
2797 ;; return something sensible if the score can't be determined
2798 (defun spam-crm114-score ()
2799   "Get the CRM114 Mailfilter pR"
2800   (interactive)
2801   (save-window-excursion
2802     (gnus-summary-show-article t)
2803     (set-buffer gnus-article-buffer)
2804     (let ((score (or (spam-check-crm114-headers t)
2805                      (spam-check-crm114 t))))
2806       (gnus-summary-show-article)
2807       (message "pR: %s" score)
2808       (or score "0"))))
2809
2810 (defun spam-check-crm114 (&optional score)
2811   "Check the CRM114 Mailfilter backend for the classification of this message"
2812   (let ((article-buffer-name (buffer-name))
2813         (db spam-crm114-database-directory)
2814         return)
2815     (with-temp-buffer
2816       (let ((temp-buffer-name (buffer-name)))
2817         (save-excursion
2818           (set-buffer article-buffer-name)
2819           (apply 'call-process-region
2820                  (point-min) (point-max)
2821                  spam-crm114-program
2822                  nil temp-buffer-name nil
2823                  (when db (list (concat "--fileprefix=" db)))))
2824         (setq return (spam-check-crm114-headers score))))
2825     return))
2826
2827 (defun spam-crm114-register-with-crm114 (articles
2828                                          spam
2829                                          &optional unregister)
2830   "Register an article, given as a string, as spam or non-spam."
2831   (dolist (article articles)
2832     (let ((article-string (spam-get-article-as-string article))
2833           (db spam-crm114-database-directory)
2834           (switch (if unregister
2835                       (if spam
2836                           spam-crm114-spam-strong-switch
2837                         spam-crm114-ham-strong-switch)
2838                     (if spam
2839                         spam-crm114-spam-switch
2840                       spam-crm114-ham-switch))))
2841       (when (stringp article-string)
2842         (with-temp-buffer
2843           (insert article-string)
2844
2845           (apply 'call-process-region
2846                  (point-min) (point-max)
2847                  spam-crm114-program
2848                  nil nil nil
2849                  (when db (list switch (concat "--fileprefix=" db)))))))))
2850
2851 (defun spam-crm114-register-spam-routine (articles &optional unregister)
2852   (spam-crm114-register-with-crm114 articles t unregister))
2853
2854 (defun spam-crm114-unregister-spam-routine (articles)
2855   (spam-crm114-register-spam-routine articles t))
2856
2857 (defun spam-crm114-register-ham-routine (articles &optional unregister)
2858   (spam-crm114-register-with-crm114 articles nil unregister))
2859
2860 (defun spam-crm114-unregister-ham-routine (articles)
2861   (spam-crm114-register-ham-routine articles t))
2862
2863 ;;}}}
2864
2865 ;;}}}
2866
2867 ;;{{{ Hooks
2868
2869 ;;;###autoload
2870 (defun spam-initialize (&rest symbols)
2871   "Install the spam.el hooks and do other initialization.
2872 When SYMBOLS is given, set those variables to t.  This is so you
2873 can call spam-initialize before you set spam-use-* variables on
2874 explicitly, and matters only if you need the extra headers
2875 installed through spam-necessary-extra-headers."
2876   (interactive)
2877
2878   (dolist (var symbols)
2879     (set var t))
2880
2881   (dolist (header (spam-necessary-extra-headers))
2882     (add-to-list 'nnmail-extra-headers header)
2883     (add-to-list 'gnus-extra-headers header))
2884
2885   (setq spam-install-hooks t)
2886   ;; TODO: How do we redo this every time the `spam' face is customized?
2887   (push '((eq mark gnus-spam-mark) . spam)
2888         gnus-summary-highlight)
2889   ;; Add hooks for loading and saving the spam stats
2890   (add-hook 'gnus-save-newsrc-hook 'spam-maybe-spam-stat-save)
2891   (add-hook 'gnus-get-top-new-news-hook 'spam-maybe-spam-stat-load)
2892   (add-hook 'gnus-startup-hook 'spam-maybe-spam-stat-load)
2893   (add-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
2894   (add-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
2895   (add-hook 'gnus-get-new-news-hook 'spam-setup-widening)
2896   (add-hook 'gnus-summary-prepared-hook 'spam-find-spam))
2897
2898 (defun spam-unload-hook ()
2899   "Uninstall the spam.el hooks"
2900   (interactive)
2901   (remove-hook 'gnus-save-newsrc-hook 'spam-maybe-spam-stat-save)
2902   (remove-hook 'gnus-get-top-new-news-hook 'spam-maybe-spam-stat-load)
2903   (remove-hook 'gnus-startup-hook 'spam-maybe-spam-stat-load)
2904   (remove-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
2905   (remove-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
2906   (remove-hook 'gnus-get-new-news-hook 'spam-setup-widening)
2907   (remove-hook 'gnus-summary-prepare-hook 'spam-find-spam))
2908
2909 (add-hook 'spam-unload-hook 'spam-unload-hook)
2910
2911 (when spam-install-hooks
2912   (spam-initialize))
2913 ;;}}}
2914
2915 (provide 'spam)
2916
2917 ;;; arch-tag: 07e6e0ca-ab0a-4412-b445-1f6c72a4f27f
2918 ;;; spam.el ends here