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