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