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