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