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