(spam-verify-bogofilter): small fixes
[gnus] / lisp / spam.el
1 ;;; spam.el --- Identifying spam
2 ;; Copyright (C) 2002, 2003, 2004 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
5 ;; Keywords: network
6
7 ;; This file is part of GNU Emacs.
8
9 ;; GNU Emacs is free software; you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation; either version 2, or (at your option)
12 ;; any later version.
13
14 ;; GNU Emacs is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 ;; GNU General Public License for more details.
18
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
21 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
22 ;; Boston, MA 02111-1307, USA.
23
24 ;;; Commentary:
25
26 ;;; This module addresses a few aspects of spam control under Gnus.  Page
27 ;;; breaks are used for grouping declarations and documentation relating to
28 ;;; each particular aspect.
29
30 ;;; The integration with Gnus is not yet complete.  See various `FIXME'
31 ;;; comments, below, for supplementary explanations or discussions.
32
33 ;;; Several TODO items are marked as such
34
35 ;; TODO: cross-server splitting, remote processing, training through files
36
37 ;;; Code:
38
39 ;;{{{ compilation directives and autoloads/requires
40
41 (eval-when-compile (require 'cl))
42 (eval-when-compile (require 'spam-report))
43
44 (require 'gnus-sum)
45
46 (require 'gnus-uu)                      ; because of key prefix issues
47 ;;; for the definitions of group content classification and spam processors
48 (require 'gnus)
49 (require 'message)              ;for the message-fetch-field functions
50
51 ;; for nnimap-split-download-body-default
52 (eval-when-compile (require 'nnimap))
53
54 ;; autoload query-dig
55 (eval-and-compile
56   (autoload 'query-dig "dig"))
57
58 ;; autoload spam-report
59 (eval-and-compile
60   (autoload 'spam-report-gmane "spam-report")
61   (autoload 'spam-report-resend "spam-report"))
62
63 ;; autoload gnus-registry
64 (eval-and-compile
65   (autoload 'gnus-registry-group-count "gnus-registry")
66   (autoload 'gnus-registry-add-group "gnus-registry")
67   (autoload 'gnus-registry-store-extra-entry "gnus-registry")
68   (autoload 'gnus-registry-fetch-extra "gnus-registry"))
69
70 ;; autoload query-dns
71 (eval-and-compile
72   (autoload 'query-dns "dns"))
73
74 ;;}}}
75
76 ;;{{{ Main parameters.
77 (defvar spam-backends nil
78   "List of spam.el backends with all the pertinent data.
79 Populated by spam-install-backend-super.")
80
81 (defgroup spam nil
82   "Spam configuration.")
83
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 (executable-find "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 (executable-find "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-ham-registration-function (backend)
881   "Get the ham registration function for BACKEND."
882   (get backend 'hrf))
883
884 (defun spam-backend-spam-registration-function (backend)
885   "Get the spam registration function for BACKEND."
886   (get backend 'srf))
887
888 (defun spam-backend-ham-unregistration-function (backend)
889   "Get the ham unregistration function for BACKEND."
890   (get backend 'huf))
891
892 (defun spam-backend-spam-unregistration-function (backend)
893   "Get the spam unregistration function for BACKEND."
894   (get backend 'suf))
895
896 (defun spam-backend-statistical-p (backend)
897   "Is BACKEND statistical?"
898   (get backend 'statistical))
899
900 (defun spam-backend-mover-p (backend)
901   "Is BACKEND a mover?"
902   (get backend 'mover))
903
904 (defun spam-install-backend-alias (backend alias)
905   "Add ALIAS to an existing BACKEND.
906 The previous backend settings for ALIAS are erased."
907
908   ;; install alias with no properties at first
909   (spam-install-backend-super alias)
910   
911   (dolist (property (spam-backend-properties))
912     (put alias property (get backend property))))
913
914 (defun spam-install-checkonly-backend (backend check)
915   "Install a BACKEND than can only CHECK for spam."
916   (spam-install-backend-super backend 'check check))
917
918 (defun spam-install-mover-backend (backend hrf srf huf suf)
919   "Install a BACKEND than can move articles at summary exit.
920 Accepts ham registration function HRF, spam registration function
921 SRF, ham unregistration function HUF, spam unregistration
922 function SUF.  The backend has no incoming check and can't be
923 statistical."
924   (spam-install-backend-super 
925    backend 
926    'hrf hrf 'srf srf 'huf huf 'suf suf 'mover t))
927
928 (defun spam-install-nocheck-backend (backend hrf srf huf suf)
929   "Install a BACKEND than has no check.
930 Accepts ham registration function HRF, spam registration function
931 SRF, ham unregistration function HUF, spam unregistration
932 function SUF.  The backend has no incoming check and can't be
933 statistical (it could be, but in practice that doesn't happen)."
934   (spam-install-backend-super 
935    backend
936    'hrf hrf 'srf srf 'huf huf 'suf suf))
937
938 (defun spam-install-backend (backend check hrf srf huf suf)
939   "Install a BACKEND.
940 Accepts incoming CHECK, ham registration function HRF, spam
941 registration function SRF, ham unregistration function HUF, spam
942 unregistration function SUF.  The backend won't be
943 statistical (use spam-install-statistical-backend for that)."
944   (spam-install-backend-super 
945    backend
946    'check check 'hrf hrf 'srf srf 'huf huf 'suf suf))
947
948 (defun spam-install-statistical-backend (backend check hrf srf huf suf)
949   "Install a BACKEND.
950 Accepts incoming CHECK, ham registration function HRF, spam
951 registration function SRF, ham unregistration function HUF, spam
952 unregistration function SUF.  The backend will be
953 statistical (use spam-install-backend for non-statistical
954 backends)."
955   (spam-install-backend-super 
956    backend
957    'check check 'statistical t 'hrf hrf 'srf srf 'huf huf 'suf suf))
958
959 (defun spam-install-statistical-checkonly-backend (backend check)
960   "Install a statistical BACKEND than can only CHECK for spam."
961   (spam-install-backend-super 
962    backend
963    'check check 'statistical t))
964
965 ;;}}}
966
967 ;;{{{ backend installations
968 (spam-install-checkonly-backend 'spam-use-blackholes
969                                 'spam-check-blackholes)
970
971 (spam-install-checkonly-backend 'spam-use-hashcash
972                                 'spam-check-hashcash)
973
974 (spam-install-checkonly-backend 'spam-use-spamassassin-headers
975                                 'spam-check-spamassassin-headers)
976
977 (spam-install-checkonly-backend 'spam-use-bogofilter-headers
978                                 'spam-check-bogofilter-headers)
979
980 (spam-install-checkonly-backend 'spam-use-bsfilter-headers
981                                 'spam-check-bsfilter-headers)
982
983 (spam-install-checkonly-backend 'spam-use-gmane-xref
984                                 'spam-check-gmane-xref)
985
986 (spam-install-checkonly-backend 'spam-use-regex-headers
987                                 'spam-check-regex-headers)
988
989 (spam-install-statistical-checkonly-backend 'spam-use-regex-body
990                                             'spam-check-regex-body)
991
992 ;; TODO: NOTE: spam-use-ham-copy is now obsolete, use (ham spam-use-copy) instead
993 (spam-install-mover-backend 'spam-use-move
994                             'spam-move-ham-routine
995                             'spam-move-spam-routine
996                             nil
997                             nil)
998
999 (spam-install-nocheck-backend 'spam-use-copy
1000                               'spam-copy-ham-routine
1001                               'spam-copy-spam-routine
1002                               nil
1003                               nil)
1004
1005 (spam-install-nocheck-backend 'spam-use-gmane
1006                               nil
1007                               'spam-report-gmane-register-routine
1008                               ;; does Gmane support unregistration?
1009                               nil
1010                               nil)
1011
1012 (spam-install-nocheck-backend 'spam-use-resend
1013                               'spam-report-resend-register-ham-routine
1014                               'spam-report-resend-register-routine
1015                               nil
1016                               nil)
1017
1018 (spam-install-backend 'spam-use-BBDB     
1019                       'spam-check-BBDB
1020                       'spam-BBDB-register-routine
1021                       nil
1022                       'spam-BBDB-unregister-routine
1023                       nil)
1024
1025 (spam-install-backend-alias 'spam-use-BBDB 'spam-use-BBDB-exclusive)
1026
1027 (spam-install-backend 'spam-use-blacklist
1028                       'spam-check-blacklist
1029                       nil
1030                       'spam-blacklist-register-routine
1031                       nil
1032                       'spam-blacklist-unregister-routine)
1033
1034 (spam-install-backend 'spam-use-whitelist
1035                       'spam-check-whitelist
1036                       'spam-whitelist-register-routine
1037                       nil
1038                       'spam-whitelist-unregister-routine
1039                       nil)
1040
1041 (spam-install-statistical-backend 'spam-use-ifile
1042                                   'spam-check-ifile
1043                                   'spam-ifile-register-ham-routine
1044                                   'spam-ifile-register-spam-routine
1045                                   'spam-ifile-unregister-ham-routine
1046                                   'spam-ifile-unregister-spam-routine)
1047
1048 (spam-install-statistical-backend 'spam-use-spamoracle
1049                                   'spam-check-spamoracle
1050                                   'spam-spamoracle-learn-ham
1051                                   'spam-spamoracle-learn-spam
1052                                   'spam-spamoracle-unlearn-ham
1053                                   'spam-spamoracle-unlearn-spam)
1054
1055 (spam-install-statistical-backend 'spam-use-stat
1056                                   'spam-check-stat
1057                                   'spam-stat-register-ham-routine
1058                                   'spam-stat-register-spam-routine
1059                                   'spam-stat-unregister-ham-routine
1060                                   'spam-stat-unregister-spam-routine)
1061
1062 (spam-install-statistical-backend 'spam-use-spamassassin 
1063                                   'spam-check-spamassassin
1064                                   'spam-spamassassin-register-ham-routine
1065                                   'spam-spamassassin-register-spam-routine
1066                                   'spam-spamassassin-unregister-ham-routine
1067                                   'spam-spamassassin-unregister-spam-routine)
1068
1069 (spam-install-statistical-backend 'spam-use-bogofilter
1070                                   'spam-check-bogofilter
1071                                   'spam-bogofilter-register-ham-routine
1072                                   'spam-bogofilter-register-spam-routine
1073                                   'spam-bogofilter-unregister-ham-routine
1074                                   'spam-bogofilter-unregister-spam-routine)
1075
1076 (spam-install-statistical-backend 'spam-use-bsfilter
1077                                   'spam-check-bsfilter
1078                                   'spam-bsfilter-register-ham-routine
1079                                   'spam-bsfilter-register-spam-routine
1080                                   'spam-bsfilter-unregister-ham-routine
1081                                   'spam-bsfilter-unregister-spam-routine)
1082
1083 (spam-install-statistical-backend 'spam-use-crm114
1084                                   'spam-check-crm114
1085                                   'spam-crm114-register-ham-routine
1086                                   'spam-crm114-register-spam-routine
1087                                   ;; does CRM114 Mailfilter support unregistration?
1088                                   nil
1089                                   nil)
1090
1091 ;;}}}
1092
1093 ;;{{{ scoring and summary formatting
1094 (defun spam-necessary-extra-headers ()
1095   "Return the extra headers spam.el thinks are necessary."
1096   (let (list)
1097     (when (or spam-use-spamassassin
1098               spam-use-spamassassin-headers
1099               spam-use-regex-headers)
1100       (push 'X-Spam-Status list))
1101     list))
1102
1103 (defun spam-user-format-function-S (headers)
1104   (when headers
1105     (spam-summary-score headers)))
1106
1107 (defun spam-article-sort-by-spam-status (h1 h2)
1108   "Sort articles by score."
1109   (let (result)
1110     (dolist (header (spam-necessary-extra-headers))
1111       (let ((s1 (spam-summary-score h1 header))
1112             (s2 (spam-summary-score h2 header)))
1113       (unless (= s1 s2)
1114         (setq result (< s1 s2))
1115         (return))))
1116     result))
1117
1118 (defun spam-extra-header-to-number (header headers)
1119   "Transform an extra header to a number."
1120   (if (gnus-extra-header header headers)
1121       (cond
1122        ((eq header 'X-Spam-Status)
1123         (string-to-number (gnus-replace-in-string
1124                            (gnus-extra-header header headers)
1125                            ".*hits=" "")))
1126        ;; for CRM checking, it's probably faster to just do the string match
1127        ((and spam-use-crm114 (string-match "( pR: \\([0-9.-]+\\)" header))
1128         (match-string 1 header))
1129        (t nil))
1130     nil))
1131
1132 (defun spam-summary-score (headers &optional specific-header)
1133   "Score an article for the summary buffer, as fast as possible.
1134 With SPECIFIC-HEADER, returns only that header's score.
1135 Will not return a nil score."
1136   (let (score)
1137     (dolist (header 
1138              (if specific-header
1139                  (list specific-header)
1140                (spam-necessary-extra-headers)))
1141       (setq score 
1142             (spam-extra-header-to-number header headers))
1143       (when score 
1144         (return)))
1145     (or score 0)))
1146
1147 (defun spam-generic-score (&optional recheck)
1148   "Invoke whatever scoring method we can."
1149   (interactive "P")
1150   (cond
1151    ((or spam-use-spamassassin spam-use-spamassassin-headers)
1152     (spam-spamassassin-score recheck))
1153    ((or spam-use-bsfilter spam-use-bsfilter-headers)
1154     (spam-bsfilter-score recheck))
1155    (spam-use-crm114
1156     (spam-crm114-score))
1157    (t (spam-bogofilter-score recheck))))
1158 ;;}}}
1159
1160 ;;{{{ set up widening, processor checks
1161
1162 ;;; set up IMAP widening if it's necessary
1163 (defun spam-setup-widening ()
1164   (when (spam-widening-needed-p)
1165     (setq nnimap-split-download-body-default t)))
1166
1167 (defun spam-widening-needed-p (&optional force-symbols)
1168   (let (found)
1169     (dolist (backend (spam-backend-list))
1170       (when (and (spam-backend-statistical-p backend)
1171                  (or (symbol-value backend) 
1172                      (memq backend force-symbols)))
1173         (setq found backend)))
1174     found))
1175
1176 (defvar spam-list-of-processors
1177   ;; note the nil processors are not defined in gnus.el
1178   '((gnus-group-spam-exit-processor-bogofilter   spam spam-use-bogofilter)
1179     (gnus-group-spam-exit-processor-bsfilter     spam spam-use-bsfilter)
1180     (gnus-group-spam-exit-processor-blacklist    spam spam-use-blacklist)
1181     (gnus-group-spam-exit-processor-ifile        spam spam-use-ifile)
1182     (gnus-group-spam-exit-processor-stat         spam spam-use-stat)
1183     (gnus-group-spam-exit-processor-spamoracle   spam spam-use-spamoracle)
1184     (gnus-group-spam-exit-processor-spamassassin spam spam-use-spamassassin)
1185     (gnus-group-ham-exit-processor-ifile         ham spam-use-ifile)
1186     (gnus-group-ham-exit-processor-bogofilter    ham spam-use-bogofilter)
1187     (gnus-group-ham-exit-processor-bsfilter      ham spam-use-bsfilter)
1188     (gnus-group-ham-exit-processor-stat          ham spam-use-stat)
1189     (gnus-group-ham-exit-processor-whitelist     ham spam-use-whitelist)
1190     (gnus-group-ham-exit-processor-BBDB          ham spam-use-BBDB)
1191     (gnus-group-ham-exit-processor-copy          ham spam-use-ham-copy)
1192     (gnus-group-ham-exit-processor-spamassassin  ham spam-use-spamassassin)
1193     (gnus-group-ham-exit-processor-spamoracle    ham spam-use-spamoracle))
1194   "The OBSOLETE `spam-list-of-processors' list.
1195 This list contains pairs associating the obsolete ham/spam exit
1196 processor variables with a classification and a spam-use-*
1197 variable.  When the processor variable is nil, just the
1198 classification and spam-use-* check variable are used.  This is
1199 superceded by the new spam backend code, so it's only consulted
1200 for backwards compatibility.")
1201
1202 (defun spam-group-processor-p (group backend &optional classification)
1203   "Checks if GROUP has a BACKEND with CLASSIFICATION registered.
1204 Also accepts the obsolete processors, which can be found in
1205 gnus.el and in spam-list-of-processors.  In the case of mover
1206 backends, checks the setting of spam-summary-exit-behavior in
1207 addition to the set values for the group."
1208   (if (and (stringp group)
1209            (symbolp backend))
1210       (let ((old-style (assq backend spam-list-of-processors))
1211             (parameters (nth 0 (gnus-parameter-spam-process group)))
1212             found)
1213         (if old-style  ; old-style processor
1214             (spam-group-processor-p group (nth 2 old-style) (nth 1 old-style))
1215           ;; now search for the parameter
1216           (dolist (parameter parameters)
1217             (when (and (null found)
1218                        (listp parameter)
1219                        (eq classification (nth 0 parameter))
1220                        (eq backend (nth 1 parameter)))
1221               (setq found t)))
1222
1223           ;; now, if the parameter was not found, do the
1224           ;; spam-summary-exit-behavior-logic for mover backends
1225           (unless found
1226             (when (spam-backend-mover-p backend)
1227               (setq 
1228                found
1229                (cond
1230                 ((eq spam-summary-exit-behavior 'move-all) t)
1231                 ((eq spam-summary-exit-behavior 'move-none) nil)
1232                 ((eq spam-summary-exit-behavior 'default)
1233                  (or (eq classification 'spam) ;move spam out of all groups
1234                      ;; move ham out of spam groups
1235                      (and (eq classification 'ham)
1236                           (spam-group-spam-contents-p group))))
1237                 (t (gnus-error 5 "Unknown spam-summary-exit-behavior: %s" 
1238                                spam-summary-exit-behavior))))))
1239
1240           found))
1241     nil))
1242
1243 ;;}}}
1244
1245 ;;{{{ Summary entry and exit processing.
1246
1247 (defun spam-mark-junk-as-spam-routine ()
1248   ;; check the global list of group names spam-junk-mailgroups and the
1249   ;; group parameters
1250   (when (spam-group-spam-contents-p gnus-newsgroup-name)
1251     (gnus-message 6 "Marking %s articles as spam"
1252                   (if spam-mark-only-unseen-as-spam
1253                       "unseen"
1254                     "unread"))
1255     (let ((articles (if spam-mark-only-unseen-as-spam
1256                         gnus-newsgroup-unseen
1257                       gnus-newsgroup-unreads)))
1258       (if spam-mark-new-messages-in-spam-group-as-spam
1259           (dolist (article articles)
1260             (gnus-summary-mark-article article gnus-spam-mark))
1261         (gnus-message 9 "Did not mark new messages as spam.")))))
1262
1263 (defun spam-summary-prepare ()
1264   (setq spam-old-articles
1265         (list (cons 'ham (spam-list-articles gnus-newsgroup-articles 'ham))
1266               (cons 'spam (spam-list-articles gnus-newsgroup-articles 'spam))))
1267   (spam-mark-junk-as-spam-routine))
1268
1269 ;; The spam processors are invoked for any group, spam or ham or neither
1270 (defun spam-summary-prepare-exit ()
1271   (unless gnus-group-is-exiting-without-update-p
1272     (gnus-message 6 "Exiting summary buffer and applying spam rules")
1273
1274     ;; first of all, unregister any articles that are no longer ham or spam
1275     ;; we have to iterate over the processors, or else we'll be too slow
1276     (dolist (classification (spam-classifications))
1277       (let* ((old-articles (cdr-safe (assq classification spam-old-articles)))
1278              (new-articles (spam-list-articles
1279                             gnus-newsgroup-articles
1280                             classification))
1281              (changed-articles (spam-set-difference new-articles old-articles)))
1282         ;; now that we have the changed articles, we go through the processors
1283         (dolist (backend (spam-backend-list))
1284           (let (unregister-list)
1285             (dolist (article changed-articles)
1286               (let ((id (spam-fetch-field-message-id-fast article)))
1287                 (when (spam-log-unregistration-needed-p
1288                        id 'process classification backend)
1289                   (push article unregister-list))))
1290             ;; call spam-register-routine with specific articles to unregister,
1291             ;; when there are articles to unregister and the check is enabled
1292             (when (and unregister-list (symbol-value backend))
1293               (spam-unregister-routine 
1294                classification 
1295                backend 
1296                unregister-list))))))
1297
1298     ;; do the non-moving backends first, then the moving ones
1299     (dolist (backend-type '(non-mover mover))
1300       (dolist (classification '(spam ham))
1301         (dolist (backend (spam-backend-list backend-type))
1302           (when (spam-group-processor-p
1303                  gnus-newsgroup-name
1304                  backend
1305                  classification)
1306             (let ((num (spam-register-routine classification backend)))
1307               (when (> num 0)
1308                 (gnus-message 
1309                  6
1310                  "%d %s messages were processed by backend %s."
1311                  num
1312                  classification
1313                  backend)))))))
1314
1315     ;; we mark all the leftover spam articles as expired at the end
1316     (dolist (article (spam-list-articles
1317                       gnus-newsgroup-articles
1318                       'spam))
1319       (gnus-summary-mark-article article gnus-expirable-mark)))
1320
1321   (setq spam-old-articles nil))
1322
1323 ;;}}}
1324
1325 ;;{{{ spam-use-move and spam-use-copy backend support functions
1326
1327 (defun spam-copy-or-move-routine (copy groups articles classification)
1328
1329   (when (and (car-safe groups) (listp (car-safe groups)))
1330     (setq groups (pop groups)))
1331
1332   (unless (listp groups)
1333     (setq groups (list groups)))
1334
1335     ;; remove the current process mark
1336   (gnus-summary-kill-process-mark)
1337
1338   (let ((backend-supports-deletions
1339          (gnus-check-backend-function
1340           'request-move-article gnus-newsgroup-name))
1341         (respool-method (gnus-find-method-for-group gnus-newsgroup-name))
1342         article mark deletep respool)
1343
1344     (when (member 'respool groups)
1345       (setq respool t)                  ; boolean for later
1346       (setq groups '("fake"))) ; when respooling, groups are dynamic so fake it
1347
1348     ;; now do the actual move
1349     (dolist (group groups)
1350       (when (and articles (stringp group))
1351
1352         ;; first, mark the article with the process mark and, if needed,
1353         ;; the unread or expired mark (for ham and spam respectively)
1354         (dolist (article articles)
1355           (when (and (eq classification 'ham)
1356                      spam-mark-ham-unread-before-move-from-spam-group)
1357             (gnus-message 9 "Marking ham article %d unread before move"
1358                           article)
1359             (gnus-summary-mark-article article gnus-unread-mark))
1360           (when (and (eq classification 'spam)
1361                      (not copy))
1362             (gnus-message 9 "Marking spam article %d expirable before move"
1363                           article)
1364             (gnus-summary-mark-article article gnus-expirable-mark))
1365           (gnus-summary-set-process-mark article)
1366             
1367           (if respool              ; respooling is with a "fake" group
1368               (let ((spam-split-disabled
1369                      (or spam-split-disabled
1370                          (and (eq classification 'ham) 
1371                               spam-disable-spam-split-during-ham-respool))))
1372                 (gnus-message 9 "Respooling article %d with method %s"
1373                               article respool-method)
1374                 (gnus-summary-respool-article nil respool-method))
1375             (if (or (not backend-supports-deletions) ; else, we are not respooling
1376                     (> (length groups) 1))
1377                 (progn              ; if copying, copy and set deletep
1378                   (gnus-message 9 "Copying article %d to group %s"
1379                                 article group)
1380                   (gnus-summary-copy-article nil group)
1381                   (setq deletep t))
1382               (gnus-message 9 "Moving article %d to group %s"
1383                             article group)
1384               (gnus-summary-move-article nil group))))) ; else move articles
1385         
1386       ;; now delete the articles, unless a) copy is t, and there was a copy done
1387       ;;                                 b) a move was done to a single group
1388       ;;                                 c) backend-supports-deletions is nil
1389       (unless copy
1390         (when (and deletep backend-supports-deletions)
1391           (dolist (article articles)
1392               (gnus-summary-set-process-mark article)
1393               (gnus-message 9 "Deleting article %d" article))
1394           (when articles
1395             (let ((gnus-novice-user nil)) ; don't ask me if I'm sure
1396               (gnus-summary-delete-article nil)))))
1397         
1398       (gnus-summary-yank-process-mark)
1399       (length articles))))
1400
1401 (defun spam-copy-spam-routine (articles)
1402   (spam-copy-or-move-routine 
1403    t 
1404    (gnus-parameter-spam-process-destination gnus-newsgroup-name)
1405    articles
1406    'spam))
1407
1408 (defun spam-move-spam-routine (articles)
1409   (spam-copy-or-move-routine 
1410    nil
1411    (gnus-parameter-spam-process-destination gnus-newsgroup-name)
1412    articles
1413    'spam))
1414
1415 (defun spam-copy-ham-routine (articles)
1416   (spam-copy-or-move-routine 
1417    t 
1418    (gnus-parameter-ham-process-destination gnus-newsgroup-name)
1419    articles
1420    'ham))
1421
1422 (defun spam-move-ham-routine (articles)
1423   (spam-copy-or-move-routine 
1424    nil
1425    (gnus-parameter-ham-process-destination gnus-newsgroup-name)
1426    articles
1427    'ham))
1428
1429 ;;}}}
1430
1431 ;;{{{ article and field retrieval code
1432 (defun spam-get-article-as-string (article)
1433   (when (numberp article)
1434     (with-temp-buffer
1435       (gnus-request-article-this-buffer
1436        article
1437        gnus-newsgroup-name)
1438       (buffer-string))))
1439
1440 ;; disabled for now
1441 ;; (defun spam-get-article-as-filename (article)
1442 ;;   (let ((article-filename))
1443 ;;     (when (numberp article)
1444 ;;       (nnml-possibly-change-directory
1445 ;;        (gnus-group-real-name gnus-newsgroup-name))
1446 ;;       (setq article-filename (expand-file-name
1447 ;;                              (int-to-string article) nnml-current-directory)))
1448 ;;     (if (file-exists-p article-filename)
1449 ;;      article-filename
1450 ;;       nil)))
1451
1452 (defun spam-fetch-field-fast (article field &optional prepared-data-header)
1453   "Fetch a FIELD for ARTICLE quickly, using the internal gnus-data-list function.
1454 When PREPARED-DATA-HEADER is given, don't look in the Gnus data.
1455 When FIELD is 'number, ARTICLE can be any number (since we want
1456 to find it out)."
1457   (when (numberp article)
1458     (let* ((data-header (or prepared-data-header
1459                             (spam-fetch-article-header article))))
1460       (if (arrayp data-header)
1461         (cond
1462          ((equal field 'number)
1463           (mail-header-number data-header))
1464          ((equal field 'from)
1465           (mail-header-from data-header))
1466          ((equal field 'message-id)
1467           (mail-header-message-id data-header))
1468          ((equal field 'subject)
1469           (mail-header-subject data-header))
1470          ((equal field 'references)
1471           (mail-header-references data-header))
1472          ((equal field 'date)
1473           (mail-header-date data-header))
1474          ((equal field 'xref)
1475           (mail-header-xref data-header))
1476          ((equal field 'extra)
1477           (mail-header-extra data-header))
1478          (t
1479           (gnus-error 
1480            5 
1481            "spam-fetch-field-fast: unknown field %s requested" 
1482            field)
1483           nil))
1484         (gnus-message 6 "Article %d has a nil data header" article)))))
1485
1486 (defun spam-fetch-field-from-fast (article &optional prepared-data-header)
1487   (spam-fetch-field-fast article 'from prepared-data-header))
1488
1489 (defun spam-fetch-field-subject-fast (article &optional prepared-data-header)
1490   (spam-fetch-field-fast article 'subject prepared-data-header))
1491
1492 (defun spam-fetch-field-message-id-fast (article &optional prepared-data-header)
1493   (spam-fetch-field-fast article 'message-id prepared-data-header))
1494
1495 (defun spam-generate-fake-headers (article)
1496   (let ((dh (spam-fetch-article-header article)))
1497     (if dh
1498         (concat
1499          (format
1500           ;; 80-character limit makes for strange constructs
1501           (concat "From: %s\nSubject: %s\nMessage-ID: %s\n"
1502                   "Date: %s\nReferences: %s\nXref: %s\n")
1503           (spam-fetch-field-fast article 'from dh)
1504           (spam-fetch-field-fast article 'subject dh)
1505           (spam-fetch-field-fast article 'message-id dh)
1506           (spam-fetch-field-fast article 'date dh)
1507           (spam-fetch-field-fast article 'references dh)
1508           (spam-fetch-field-fast article 'xref dh))
1509          (when (spam-fetch-field-fast article 'extra dh)
1510            (format "%s\n" (spam-fetch-field-fast article 'extra dh))))
1511       (gnus-message
1512        5
1513        "spam-generate-fake-headers: article %d didn't have a valid header"
1514        article))))
1515
1516 (defun spam-fetch-article-header (article)
1517   (save-excursion
1518     (set-buffer gnus-summary-buffer)
1519     (gnus-read-header article)
1520     (nth 3 (assq article gnus-newsgroup-data))))
1521 ;;}}}
1522
1523 ;;{{{ Spam determination.
1524
1525 (defun spam-split (&rest specific-checks)
1526   "Split this message into the `spam' group if it is spam.
1527 This function can be used as an entry in the variable `nnmail-split-fancy',
1528 for example like this: (: spam-split).  It can take checks as
1529 parameters.  A string as a parameter will set the
1530 spam-split-group to that string.
1531
1532 See the Info node `(gnus)Fancy Mail Splitting' for more details."
1533   (interactive)
1534   (setq spam-split-last-successful-check nil)
1535   (unless spam-split-disabled
1536     (let ((spam-split-group-choice spam-split-group))
1537       (dolist (check specific-checks)
1538         (when (stringp check)
1539           (setq spam-split-group-choice check)
1540           (setq specific-checks (delq check specific-checks))))
1541
1542       (let ((spam-split-group spam-split-group-choice)
1543             (widening-needed-check (spam-widening-needed-p specific-checks)))
1544         (save-excursion
1545           (save-restriction
1546             (when widening-needed-check
1547               (widen)
1548               (gnus-message 8 "spam-split: widening the buffer (%s requires it)"
1549                             widening-needed-check))
1550             (let ((backends (spam-backend-list))
1551                   decision)
1552               (while (and backends (not decision))
1553                 (let* ((backend (pop backends))
1554                        (check-function (spam-backend-check backend))
1555                        (spam-split-group (if spam-split-symbolic-return
1556                                              'spam
1557                                            spam-split-group)))
1558                   (when (or
1559                          ;; either, given specific checks, this is one of them
1560                          (memq backend specific-checks)
1561                          ;; or, given no specific checks, spam-use-CHECK is set
1562                          (and (null specific-checks) (symbol-value backend)))
1563                     (gnus-message 6 "spam-split: calling the %s function"
1564                                   check-function)
1565                     (setq decision (funcall check-function))
1566                     ;; if we got a decision at all, save the current check
1567                     (when decision
1568                       (setq spam-split-last-successful-check backend))
1569
1570                     (when (eq decision 'spam)
1571                       (unless spam-split-symbolic-return
1572                         (gnus-error
1573                          5
1574                          (format "spam-split got %s but %s is nil"
1575                                  decision
1576                                  spam-split-symbolic-return)))))))
1577               (if (eq decision t)
1578                   (if spam-split-symbolic-return-positive 'ham nil)
1579                 decision))))))))
1580
1581 (defun spam-find-spam ()
1582   "This function will detect spam in the current newsgroup using spam-split."
1583   (interactive)
1584
1585   (let* ((group gnus-newsgroup-name)
1586          (autodetect (gnus-parameter-spam-autodetect group))
1587          (methods (gnus-parameter-spam-autodetect-methods group))
1588          (first-method (nth 0 methods))
1589          (articles (if spam-autodetect-recheck-messages
1590                        gnus-newsgroup-articles
1591                      gnus-newsgroup-unseen))
1592          article-cannot-be-faked)
1593
1594     
1595     (dolist (backend methods)
1596       (when (spam-backend-statistical-p backend)
1597         (setq article-cannot-be-faked t)
1598         (return)))
1599
1600     (when (memq 'default methods)
1601       (setq article-cannot-be-faked t))
1602
1603     (when (and autodetect
1604                (not (equal first-method 'none)))
1605       (mapcar
1606        (lambda (article)
1607          (let ((id (spam-fetch-field-message-id-fast article))
1608                (subject (spam-fetch-field-subject-fast article))
1609                (sender (spam-fetch-field-from-fast article))
1610                registry-lookup)
1611            
1612            (unless id
1613              (gnus-message 6 "Article %d has no message ID!" article))
1614          
1615            (when (and id spam-log-to-registry)
1616              (setq registry-lookup (spam-log-registration-type id 'incoming))
1617              (when registry-lookup
1618                (gnus-message
1619                 9
1620                 "spam-find-spam: message %s was already registered incoming"
1621                 id)))
1622
1623            (let* ((spam-split-symbolic-return t)
1624                   (spam-split-symbolic-return-positive t)
1625                   (fake-headers (spam-generate-fake-headers article))
1626                   (split-return
1627                    (or registry-lookup
1628                        (with-temp-buffer
1629                          (if article-cannot-be-faked
1630                              (gnus-request-article-this-buffer
1631                               article
1632                               group)
1633                            ;; else, we fake the article
1634                            (when fake-headers (insert fake-headers)))
1635                          (if (or (null first-method)
1636                                  (equal first-method 'default))
1637                              (spam-split)
1638                            (apply 'spam-split methods))))))
1639              (if (equal split-return 'spam)
1640                  (gnus-summary-mark-article article gnus-spam-mark))
1641            
1642              (when (and id split-return spam-log-to-registry)
1643                (when (zerop (gnus-registry-group-count id))
1644                  (gnus-registry-add-group
1645                   id group subject sender))
1646                
1647                (unless registry-lookup
1648                  (spam-log-processing-to-registry
1649                   id
1650                   'incoming
1651                   split-return
1652                   spam-split-last-successful-check
1653                   group))))))
1654        articles))))
1655
1656 ;;}}}
1657
1658 ;;{{{ registration/unregistration functions
1659
1660 (defun spam-unregister-routine (classification
1661                                 backend
1662                                 &optional specific-articles)
1663   (spam-register-routine classification backend t specific-articles))
1664
1665 (defun spam-register-routine (classification
1666                               backend
1667                               &optional unregister
1668                               specific-articles)
1669   (when (and (spam-classification-valid-p classification)
1670              (spam-backend-valid-p backend))
1671     (let* ((register-function
1672             (spam-backend-function backend classification 'registration))
1673            (unregister-function
1674             (spam-backend-function backend classification 'unregistration))
1675            (run-function (if unregister
1676                              unregister-function
1677                            register-function))
1678            (log-function (if unregister
1679                              'spam-log-undo-registration
1680                            'spam-log-processing-to-registry))
1681            article articles)
1682
1683       (when run-function
1684         ;; make list of articles, using specific-articles if given
1685         (setq articles (or specific-articles
1686                            (spam-list-articles
1687                             gnus-newsgroup-articles
1688                             classification)))
1689         ;; process them
1690         (when (> (length articles) 0)
1691           (gnus-message 5 "%s %d %s articles as %s using backend %s"
1692                         (if unregister "Unregistering" "Registering")
1693                         (length articles)
1694                         (if specific-articles "specific" "")
1695                         classification
1696                         backend)
1697           (funcall run-function articles)
1698           ;; now log all the registrations (or undo them, depending on unregister)
1699           (dolist (article articles)
1700             (funcall log-function
1701                      (spam-fetch-field-message-id-fast article)
1702                      'process
1703                      classification
1704                      backend
1705                      gnus-newsgroup-name))))
1706     (length articles))))      ;return the number of articles processed
1707
1708 ;;; log a ham- or spam-processor invocation to the registry
1709 (defun spam-log-processing-to-registry (id type classification backend group)
1710   (when spam-log-to-registry
1711     (if (and (stringp id)
1712              (stringp group)
1713              (spam-process-type-valid-p type)
1714              (spam-classification-valid-p classification)
1715              (spam-backend-valid-p backend))
1716         (let ((cell-list (cdr-safe (gnus-registry-fetch-extra id type)))
1717               (cell (list classification backend group)))
1718           (push cell cell-list)
1719           (gnus-registry-store-extra-entry
1720            id
1721            type
1722            cell-list))
1723
1724       (gnus-error
1725        7
1726        (format "%s call with bad ID, type, classification, spam-backend, or group"
1727                "spam-log-processing-to-registry")))))
1728
1729 ;;; check if a ham- or spam-processor registration has been done
1730 (defun spam-log-registered-p (id type)
1731   (when spam-log-to-registry
1732     (if (and (stringp id)
1733              (spam-process-type-valid-p type))
1734         (cdr-safe (gnus-registry-fetch-extra id type))
1735       (progn
1736         (gnus-error
1737          7
1738          (format "%s called with bad ID, type, classification, or spam-backend"
1739                  "spam-log-registered-p"))
1740         nil))))
1741
1742 ;;; check what a ham- or spam-processor registration says
1743 ;;; returns nil if conflicting registrations are found
1744 (defun spam-log-registration-type (id type)
1745   (let ((count 0)
1746         decision)
1747     (dolist (reg (spam-log-registered-p id type))
1748       (let ((classification (nth 0 reg)))
1749         (when (spam-classification-valid-p classification)
1750           (when (and decision
1751                      (not (eq classification decision)))
1752             (setq count (+ 1 count)))
1753           (setq decision classification))))
1754     (if (< 0 count)
1755         nil
1756       decision)))
1757
1758
1759 ;;; check if a ham- or spam-processor registration needs to be undone
1760 (defun spam-log-unregistration-needed-p (id type classification backend)
1761   (when spam-log-to-registry
1762     (if (and (stringp id)
1763              (spam-process-type-valid-p type)
1764              (spam-classification-valid-p classification)
1765              (spam-backend-valid-p backend))
1766         (let ((cell-list (cdr-safe (gnus-registry-fetch-extra id type)))
1767               found)
1768           (dolist (cell cell-list)
1769             (unless found
1770               (when (and (eq classification (nth 0 cell))
1771                          (eq backend (nth 1 cell)))
1772                 (setq found t))))
1773           found)
1774       (progn
1775         (gnus-error
1776          7
1777          (format "%s called with bad ID, type, classification, or spam-backend"
1778                  "spam-log-unregistration-needed-p"))
1779         nil))))
1780
1781
1782 ;;; undo a ham- or spam-processor registration (the group is not used)
1783 (defun spam-log-undo-registration (id type classification backend &optional group)
1784   (when (and spam-log-to-registry
1785              (spam-log-unregistration-needed-p id type classification backend))
1786     (if (and (stringp id)
1787              (spam-process-type-valid-p type)
1788              (spam-classification-valid-p classification)
1789              (spam-backend-valid-p backend))
1790         (let ((cell-list (cdr-safe (gnus-registry-fetch-extra id type)))
1791               new-cell-list found)
1792           (dolist (cell cell-list)
1793             (unless (and (eq classification (nth 0 cell))
1794                          (eq backend (nth 1 cell)))
1795               (push cell new-cell-list)))
1796           (gnus-registry-store-extra-entry
1797            id
1798            type
1799            new-cell-list))
1800       (progn
1801         (gnus-error 7 (format "%s call with bad ID, type, spam-backend, or group"
1802                               "spam-log-undo-registration"))
1803         nil))))
1804
1805 ;;}}}
1806
1807 ;;{{{ backend functions
1808
1809 ;;{{{ Gmane xrefs
1810 (defun spam-check-gmane-xref ()
1811   (let ((header (or
1812                  (message-fetch-field "Xref")
1813                  (message-fetch-field "Newsgroups"))))
1814     (when header                        ; return nil when no header
1815       (when (string-match spam-gmane-xref-spam-group
1816                           header)
1817           spam-split-group))))
1818
1819 ;;}}}
1820
1821 ;;{{{ Regex body
1822
1823 (defun spam-check-regex-body ()
1824   (let ((spam-regex-headers-ham spam-regex-body-ham)
1825         (spam-regex-headers-spam spam-regex-body-spam))
1826     (spam-check-regex-headers t)))
1827
1828 ;;}}}
1829
1830 ;;{{{ Regex headers
1831
1832 (defun spam-check-regex-headers (&optional body)
1833   (let ((type (if body "body" "header"))
1834         ret found)
1835     (dolist (h-regex spam-regex-headers-ham)
1836       (unless found
1837         (goto-char (point-min))
1838         (when (re-search-forward h-regex nil t)
1839           (message "Ham regex %s search positive." type)
1840           (setq found t))))
1841     (dolist (s-regex spam-regex-headers-spam)
1842       (unless found
1843         (goto-char (point-min))
1844         (when (re-search-forward s-regex nil t)
1845           (message "Spam regex %s search positive." type)
1846           (setq found t)
1847           (setq ret spam-split-group))))
1848     ret))
1849
1850 ;;}}}
1851
1852 ;;{{{ Blackholes.
1853
1854 (defun spam-reverse-ip-string (ip)
1855   (when (stringp ip)
1856     (mapconcat 'identity
1857                (nreverse (split-string ip "\\."))
1858                ".")))
1859
1860 (defun spam-check-blackholes ()
1861   "Check the Received headers for blackholed relays."
1862   (let ((headers (message-fetch-field "received"))
1863         ips matches)
1864     (when headers
1865       (with-temp-buffer
1866         (insert headers)
1867         (goto-char (point-min))
1868         (gnus-message 6 "Checking headers for relay addresses")
1869         (while (re-search-forward
1870                 "\\([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+\\)" nil t)
1871           (gnus-message 9 "Blackhole search found host IP %s." (match-string 1))
1872           (push (spam-reverse-ip-string (match-string 1))
1873                 ips)))
1874       (dolist (server spam-blackhole-servers)
1875         (dolist (ip ips)
1876           (unless (and spam-blackhole-good-server-regex
1877                        ;; match the good-server-regex against the reversed (again) IP string
1878                        (string-match
1879                         spam-blackhole-good-server-regex
1880                         (spam-reverse-ip-string ip)))
1881             (unless matches
1882               (let ((query-string (concat ip "." server)))
1883                 (if spam-use-dig
1884                     (let ((query-result (query-dig query-string)))
1885                       (when query-result
1886                         (gnus-message 6 "(DIG): positive blackhole check '%s'"
1887                                       query-result)
1888                         (push (list ip server query-result)
1889                               matches)))
1890                   ;; else, if not using dig.el
1891                   (when (query-dns query-string)
1892                     (gnus-message 6 "positive blackhole check")
1893                     (push (list ip server (query-dns query-string 'TXT))
1894                           matches)))))))))
1895     (when matches
1896       spam-split-group)))
1897 ;;}}}
1898
1899 ;;{{{ Hashcash.
1900
1901 (condition-case nil
1902     (progn
1903       (require 'hashcash)
1904
1905       (defun spam-check-hashcash ()
1906         "Check the headers for hashcash payments."
1907         (mail-check-payment)))   ;mail-check-payment returns a boolean
1908
1909   (file-error (progn
1910                 (defalias 'mail-check-payment 'ignore)
1911                 (defalias 'spam-check-hashcash 'ignore))))
1912 ;;}}}
1913
1914 ;;{{{ BBDB
1915
1916 ;;; original idea for spam-check-BBDB from Alexander Kotelnikov
1917 ;;; <sacha@giotto.sj.ru>
1918
1919 ;; all this is done inside a condition-case to trap errors
1920
1921 (condition-case nil
1922     (progn
1923       (require 'bbdb)
1924       (require 'bbdb-com)
1925
1926       ;; when the BBDB changes, we want to clear out our cache
1927       (defun spam-clear-cache-BBDB (&rest immaterial)
1928         (spam-clear-cache 'spam-use-BBDB))
1929
1930       (add-hook 'bbdb-change-hook 'spam-clear-cache-BBDB)
1931
1932       (defun spam-enter-ham-BBDB (addresses &optional remove)
1933         "Enter an address into the BBDB; implies ham (non-spam) sender"
1934         (dolist (from addresses)
1935           (when (stringp from)
1936             (let* ((parsed-address (gnus-extract-address-components from))
1937                    (name (or (nth 0 parsed-address) "Ham Sender"))
1938                    (remove-function (if remove
1939                                         'bbdb-delete-record-internal
1940                                       'ignore))
1941                    (net-address (nth 1 parsed-address))
1942                    (record (and net-address
1943                                 (bbdb-search-simple nil net-address))))
1944               (when net-address
1945                 (gnus-message 6 "%s address %s %s BBDB"
1946                               (if remove "Deleting" "Adding")
1947                               from
1948                               (if remove "from" "to"))
1949                 (if record
1950                     (funcall remove-function record)
1951                   (bbdb-create-internal name nil net-address nil nil
1952                                         "ham sender added by spam.el")))))))
1953
1954       (defun spam-BBDB-register-routine (articles &optional unregister)
1955         (let (addresses)
1956           (dolist (article articles)
1957             (when (stringp (spam-fetch-field-from-fast article))
1958               (push (spam-fetch-field-from-fast article) addresses)))
1959           ;; now do the register/unregister action
1960           (spam-enter-ham-BBDB addresses unregister)))
1961
1962       (defun spam-BBDB-unregister-routine (articles)
1963         (spam-BBDB-register-routine articles t))
1964
1965       (defun spam-check-BBDB ()
1966         "Mail from people in the BBDB is classified as ham or non-spam"
1967         (let ((who (message-fetch-field "from"))
1968               bbdb-cache bbdb-hashtable)
1969           (when spam-cache-lookups
1970             (setq bbdb-cache (gethash 'spam-use-BBDB spam-caches))
1971             (unless bbdb-cache
1972               (setq bbdb-cache
1973                     ;; this is the expanded (bbdb-hashtable) macro
1974                     ;; without the debugging support
1975                     (with-current-buffer (bbdb-buffer)
1976                       (save-excursion
1977                         (save-window-excursion
1978                           (bbdb-records nil t)
1979                           bbdb-hashtable))))
1980               (puthash 'spam-use-BBDB bbdb-cache spam-caches)))
1981           (when who
1982             (setq who (nth 1 (gnus-extract-address-components who)))
1983             (if
1984                 (if spam-cache-lookups
1985                     (symbol-value
1986                      (intern-soft who bbdb-cache))
1987                   (bbdb-search-simple nil who))
1988                 t
1989               (if spam-use-BBDB-exclusive
1990                   spam-split-group
1991                 nil))))))
1992
1993   (file-error (progn
1994                 (defalias 'bbdb-search-simple 'ignore)
1995                 (defalias 'bbdb-records 'ignore)
1996                 (defalias 'bbdb-buffer 'ignore)
1997                 (defalias 'spam-check-BBDB 'ignore)
1998                 (defalias 'spam-BBDB-register-routine 'ignore)
1999                 (defalias 'spam-enter-ham-BBDB 'ignore)
2000                 (defalias 'bbdb-create-internal 'ignore)
2001                 (defalias 'bbdb-delete-record-internal 'ignore)
2002                 (defalias 'bbdb-records 'ignore))))
2003
2004 ;;}}}
2005
2006 ;;{{{ ifile
2007
2008 ;;; check the ifile backend; return nil if the mail was NOT classified
2009 ;;; as spam
2010
2011 (defun spam-get-ifile-database-parameter ()
2012   "Get the command-line parameter for ifile's database from
2013   spam-ifile-database-path."
2014   (if spam-ifile-database-path
2015       (format "--db-file=%s" spam-ifile-database-path)
2016     nil))
2017
2018 (defun spam-check-ifile ()
2019   "Check the ifile backend for the classification of this message."
2020   (let ((article-buffer-name (buffer-name))
2021         category return)
2022     (with-temp-buffer
2023       (let ((temp-buffer-name (buffer-name))
2024             (db-param (spam-get-ifile-database-parameter)))
2025         (save-excursion
2026           (set-buffer article-buffer-name)
2027           (apply 'call-process-region
2028                  (point-min) (point-max) spam-ifile-path
2029                  nil temp-buffer-name nil "-c"
2030                  (if db-param `(,db-param "-q") `("-q"))))
2031         ;; check the return now (we're back in the temp buffer)
2032         (goto-char (point-min))
2033         (if (not (eobp))
2034             (setq category (buffer-substring (point) (point-at-eol))))
2035         (when (not (zerop (length category))) ; we need a category here
2036           (if spam-ifile-all-categories
2037               (setq return category)
2038             ;; else, if spam-ifile-all-categories is not set...
2039             (when (string-equal spam-ifile-spam-category category)
2040               (setq return spam-split-group)))))) ; note return is nil otherwise
2041     return))
2042
2043 (defun spam-ifile-register-with-ifile (articles category &optional unregister)
2044   "Register an article, given as a string, with a category.
2045 Uses `gnus-newsgroup-name' if category is nil (for ham registration)."
2046   (let ((category (or category gnus-newsgroup-name))
2047         (add-or-delete-option (if unregister "-d" "-i"))
2048         (db (spam-get-ifile-database-parameter))
2049         parameters)
2050     (with-temp-buffer
2051       (dolist (article articles)
2052         (let ((article-string (spam-get-article-as-string article)))
2053           (when (stringp article-string)
2054             (insert article-string))))
2055       (apply 'call-process-region
2056              (point-min) (point-max) spam-ifile-path
2057              nil nil nil
2058              add-or-delete-option category
2059              (if db `(,db "-h") `("-h"))))))
2060
2061 (defun spam-ifile-register-spam-routine (articles &optional unregister)
2062   (spam-ifile-register-with-ifile articles spam-ifile-spam-category unregister))
2063
2064 (defun spam-ifile-unregister-spam-routine (articles)
2065   (spam-ifile-register-spam-routine articles t))
2066
2067 (defun spam-ifile-register-ham-routine (articles &optional unregister)
2068   (spam-ifile-register-with-ifile articles spam-ifile-ham-category unregister))
2069
2070 (defun spam-ifile-unregister-ham-routine (articles)
2071   (spam-ifile-register-ham-routine articles t))
2072
2073 ;;}}}
2074
2075 ;;{{{ spam-stat
2076
2077 (condition-case nil
2078     (progn
2079       (let ((spam-stat-install-hooks nil))
2080         (require 'spam-stat))
2081
2082       (defun spam-check-stat ()
2083         "Check the spam-stat backend for the classification of this message"
2084         (let ((spam-stat-split-fancy-spam-group spam-split-group) ; override
2085               (spam-stat-buffer (buffer-name)) ; stat the current buffer
2086               category return)
2087           (spam-stat-split-fancy)))
2088
2089       (defun spam-stat-register-spam-routine (articles &optional unregister)
2090         (dolist (article articles)
2091           (let ((article-string (spam-get-article-as-string article)))
2092             (with-temp-buffer
2093               (insert article-string)
2094               (if unregister
2095                   (spam-stat-buffer-change-to-non-spam)
2096               (spam-stat-buffer-is-spam))))))
2097
2098       (defun spam-stat-unregister-spam-routine (articles)
2099         (spam-stat-register-spam-routine articles t))
2100
2101       (defun spam-stat-register-ham-routine (articles &optional unregister)
2102         (dolist (article articles)
2103           (let ((article-string (spam-get-article-as-string article)))
2104             (with-temp-buffer
2105               (insert article-string)
2106               (if unregister
2107                   (spam-stat-buffer-change-to-spam)
2108               (spam-stat-buffer-is-non-spam))))))
2109
2110       (defun spam-stat-unregister-ham-routine (articles)
2111         (spam-stat-register-ham-routine articles t))
2112
2113       (defun spam-maybe-spam-stat-load ()
2114         (when spam-use-stat (spam-stat-load)))
2115
2116       (defun spam-maybe-spam-stat-save ()
2117         (when spam-use-stat (spam-stat-save))))
2118
2119   (file-error (progn
2120                 (defalias 'spam-stat-load 'ignore)
2121                 (defalias 'spam-stat-save 'ignore)
2122                 (defalias 'spam-maybe-spam-stat-load 'ignore)
2123                 (defalias 'spam-maybe-spam-stat-save 'ignore)
2124                 (defalias 'spam-stat-register-ham-routine 'ignore)
2125                 (defalias 'spam-stat-unregister-ham-routine 'ignore)
2126                 (defalias 'spam-stat-register-spam-routine 'ignore)
2127                 (defalias 'spam-stat-unregister-spam-routine 'ignore)
2128                 (defalias 'spam-stat-buffer-is-spam 'ignore)
2129                 (defalias 'spam-stat-buffer-change-to-spam 'ignore)
2130                 (defalias 'spam-stat-buffer-is-non-spam 'ignore)
2131                 (defalias 'spam-stat-buffer-change-to-non-spam 'ignore)
2132                 (defalias 'spam-stat-split-fancy 'ignore)
2133                 (defalias 'spam-check-stat 'ignore))))
2134
2135
2136
2137 ;;}}}
2138
2139 ;;{{{ Blacklists and whitelists.
2140
2141 (defvar spam-whitelist-cache nil)
2142 (defvar spam-blacklist-cache nil)
2143
2144 (defun spam-kill-whole-line ()
2145   (beginning-of-line)
2146   (let ((kill-whole-line t))
2147     (kill-line)))
2148
2149 ;;; address can be a list, too
2150 (defun spam-enter-whitelist (address &optional remove)
2151   "Enter ADDRESS (list or single) into the whitelist.
2152 With a non-nil REMOVE, remove them."
2153   (interactive "sAddress: ")
2154   (spam-enter-list address spam-whitelist remove)
2155   (setq spam-whitelist-cache nil)
2156   (spam-clear-cache 'spam-use-whitelist))
2157
2158 ;;; address can be a list, too
2159 (defun spam-enter-blacklist (address &optional remove)
2160   "Enter ADDRESS (list or single) into the blacklist.
2161 With a non-nil REMOVE, remove them."
2162   (interactive "sAddress: ")
2163   (spam-enter-list address spam-blacklist remove)
2164   (setq spam-blacklist-cache nil)
2165   (spam-clear-cache 'spam-use-whitelist))
2166
2167 (defun spam-enter-list (addresses file &optional remove)
2168   "Enter ADDRESSES into the given FILE.
2169 Either the whitelist or the blacklist files can be used.  With
2170 REMOVE not nil, remove the ADDRESSES."
2171   (if (stringp addresses)
2172       (spam-enter-list (list addresses) file remove)
2173     ;; else, we have a list of addresses here
2174     (unless (file-exists-p (file-name-directory file))
2175       (make-directory (file-name-directory file) t))
2176     (save-excursion
2177       (set-buffer
2178        (find-file-noselect file))
2179       (dolist (a addresses)
2180         (when (stringp a)
2181           (goto-char (point-min))
2182           (if (re-search-forward (regexp-quote a) nil t)
2183               ;; found the address
2184               (when remove
2185                 (spam-kill-whole-line))
2186             ;; else, the address was not found
2187             (unless remove
2188               (goto-char (point-max))
2189               (unless (bobp)
2190                 (insert "\n"))
2191               (insert a "\n")))))
2192       (save-buffer))))
2193
2194 (defun spam-filelist-build-cache (type)
2195   (let ((cache (if (eq type 'spam-use-blacklist)
2196                    spam-blacklist-cache
2197                  spam-whitelist-cache))
2198         parsed-cache)
2199     (unless (gethash type spam-caches)
2200       (while cache
2201         (let ((address (pop cache)))
2202           (unless (zerop (length address)) ; 0 for a nil address too
2203             (setq address (regexp-quote address))
2204             ;; fix regexp-quote's treatment of user-intended regexes
2205             (while (string-match "\\\\\\*" address)
2206               (setq address (replace-match ".*" t t address))))
2207           (push address parsed-cache)))
2208       (puthash type parsed-cache spam-caches))))
2209
2210 (defun spam-filelist-check-cache (type from)
2211   (when (stringp from)
2212     (spam-filelist-build-cache type)
2213     (let (found)
2214       (dolist (address (gethash type spam-caches))
2215         (when (and address (string-match address from))
2216           (setq found t)
2217           (return)))
2218       found)))
2219
2220 ;;; returns t if the sender is in the whitelist, nil or
2221 ;;; spam-split-group otherwise
2222 (defun spam-check-whitelist ()
2223   ;; FIXME!  Should it detect when file timestamps change?
2224   (unless spam-whitelist-cache
2225     (setq spam-whitelist-cache (spam-parse-list spam-whitelist)))
2226   (if (spam-from-listed-p 'spam-use-whitelist)
2227       t
2228     (if spam-use-whitelist-exclusive
2229         spam-split-group
2230       nil)))
2231
2232 (defun spam-check-blacklist ()
2233   ;; FIXME!  Should it detect when file timestamps change?
2234   (unless spam-blacklist-cache
2235     (setq spam-blacklist-cache (spam-parse-list spam-blacklist)))
2236   (and (spam-from-listed-p 'spam-use-blacklist)
2237        spam-split-group))
2238
2239 (defun spam-parse-list (file)
2240   (when (file-readable-p file)
2241     (let (contents address)
2242       (with-temp-buffer
2243         (insert-file-contents file)
2244         (while (not (eobp))
2245           (setq address (buffer-substring (point) (point-at-eol)))
2246           (forward-line 1)
2247           ;; insert the e-mail address if detected, otherwise the raw data
2248           (unless (zerop (length address))
2249             (let ((pure-address (nth 1 (gnus-extract-address-components address))))
2250               (push (or pure-address address) contents)))))
2251       (nreverse contents))))
2252
2253 (defun spam-from-listed-p (type)
2254   (let ((from (message-fetch-field "from"))
2255         found)
2256     (spam-filelist-check-cache type from)))
2257
2258 (defun spam-filelist-register-routine (articles blacklist &optional unregister)
2259   (let ((de-symbol (if blacklist 'spam-use-whitelist 'spam-use-blacklist))
2260         (declassification (if blacklist 'ham 'spam))
2261         (enter-function
2262          (if blacklist 'spam-enter-blacklist 'spam-enter-whitelist))
2263         (remove-function
2264          (if blacklist 'spam-enter-whitelist 'spam-enter-blacklist))
2265         from addresses unregister-list article-unregister-list)
2266     (dolist (article articles)
2267       (let ((from (spam-fetch-field-from-fast article))
2268             (id (spam-fetch-field-message-id-fast article))
2269             sender-ignored)
2270         (when (stringp from)
2271           (dolist (ignore-regex spam-blacklist-ignored-regexes)
2272             (when (and (not sender-ignored)
2273                        (stringp ignore-regex)
2274                        (string-match ignore-regex from))
2275               (setq sender-ignored t)))
2276           ;; remember the messages we need to unregister, unless remove is set
2277           (when (and
2278                  (null unregister)
2279                  (spam-log-unregistration-needed-p
2280                   id 'process declassification de-symbol))
2281             (push article article-unregister-list)
2282             (push from unregister-list))
2283           (unless sender-ignored
2284             (push from addresses)))))
2285
2286     (if unregister
2287         (funcall enter-function addresses t) ; unregister all these addresses
2288       ;; else, register normally and unregister what we need to
2289       (funcall remove-function unregister-list t)
2290       (dolist (article article-unregister-list)
2291         (spam-log-undo-registration
2292          (spam-fetch-field-message-id-fast article)
2293          'process
2294          declassification
2295          de-symbol))
2296       (funcall enter-function addresses nil))))
2297
2298 (defun spam-blacklist-unregister-routine (articles)
2299   (spam-blacklist-register-routine articles t))
2300
2301 (defun spam-blacklist-register-routine (articles &optional unregister)
2302   (spam-filelist-register-routine articles t unregister))
2303
2304 (defun spam-whitelist-unregister-routine (articles)
2305   (spam-whitelist-register-routine articles t))
2306
2307 (defun spam-whitelist-register-routine (articles &optional unregister)
2308   (spam-filelist-register-routine articles nil unregister))
2309
2310 ;;}}}
2311
2312 ;;{{{ Spam-report glue (gmane and resend reporting)
2313 (defun spam-report-gmane-register-routine (articles)
2314   (when articles
2315     (apply 'spam-report-gmane articles)))
2316
2317 (defun spam-report-resend-register-ham-routine (articles)
2318   (spam-report-resend-register-routine articles t))
2319
2320 (defun spam-report-resend-register-routine (articles &optional ham)
2321   (let* ((resend-to-gp 
2322           (if ham
2323               (gnus-parameter-ham-resend-to gnus-newsgroup-name)
2324             (gnus-parameter-spam-resend-to gnus-newsgroup-name)))
2325          (spam-report-resend-to (or (car-safe resend-to-gp)
2326                                     spam-report-resend-to)))
2327     (spam-report-resend articles ham)))
2328
2329 ;;}}}
2330
2331 ;;{{{ Bogofilter
2332 (defun spam-check-bogofilter-headers (&optional score)
2333   (let ((header (message-fetch-field spam-bogofilter-header)))
2334     (when header                        ; return nil when no header
2335       (if score                         ; scoring mode
2336           (if (string-match "spamicity=\\([0-9.]+\\)" header)
2337               (match-string 1 header)
2338             "0")
2339         ;; spam detection mode
2340         (when (string-match spam-bogofilter-bogosity-positive-spam-header
2341                             header)
2342           spam-split-group)))))
2343
2344 ;; return something sensible if the score can't be determined
2345 (defun spam-bogofilter-score (&optional recheck)
2346   "Get the Bogofilter spamicity score"
2347   (interactive "P")
2348   (save-window-excursion
2349     (gnus-summary-show-article t)
2350     (set-buffer gnus-article-buffer)
2351     (let ((score (or (unless recheck
2352                        (spam-check-bogofilter-headers t))
2353                      (spam-check-bogofilter t))))
2354       (gnus-summary-show-article)
2355       (message "Spamicity score %s" score)
2356       (or score "0"))))
2357
2358 (defun spam-verify-bogofilter ()
2359   "Verify the Bogofilter version is sufficient."
2360   (when (eq spam-bogofilter-valid 'never)
2361     (setq spam-bogofilter-valid
2362           (not (string-match "^bogofilter version 0\\.\\([0-9]\\|1[01]\\)\\."
2363                              (shell-command-to-string 
2364                               (format "%s -sV" spam-bogofilter-path))))))
2365   spam-bogofilter-valid)
2366   
2367 (defun spam-check-bogofilter (&optional score)
2368   "Check the Bogofilter backend for the classification of this message."
2369   (if (spam-verify-bogofilter)
2370       (let ((article-buffer-name (buffer-name))
2371             (db spam-bogofilter-database-directory)
2372             return)
2373         (with-temp-buffer
2374           (let ((temp-buffer-name (buffer-name)))
2375             (save-excursion
2376               (set-buffer article-buffer-name)
2377               (apply 'call-process-region
2378                      (point-min) (point-max)
2379                      spam-bogofilter-path
2380                      nil temp-buffer-name nil
2381                      (if db `("-d" ,db "-v") `("-v"))))
2382             (setq return (spam-check-bogofilter-headers score))))
2383         return)
2384     (gnus-error "`spam.el' doesnt support obsolete bogofilter versions")))
2385
2386 (defun spam-bogofilter-register-with-bogofilter (articles
2387                                                  spam
2388                                                  &optional unregister)
2389   "Register an article, given as a string, as spam or non-spam."
2390   (if (spam-verify-bogofilter)
2391       (dolist (article articles)
2392         (let ((article-string (spam-get-article-as-string article))
2393               (db spam-bogofilter-database-directory)
2394               (switch (if unregister
2395                           (if spam
2396                               spam-bogofilter-spam-strong-switch
2397                             spam-bogofilter-ham-strong-switch)
2398                         (if spam
2399                             spam-bogofilter-spam-switch
2400                           spam-bogofilter-ham-switch))))
2401           (when (stringp article-string)
2402             (with-temp-buffer
2403               (insert article-string)
2404               
2405               (apply 'call-process-region
2406                      (point-min) (point-max)
2407                      spam-bogofilter-path
2408                      nil nil nil switch
2409                      (if db `("-d" ,db "-v") `("-v")))))))
2410     (gnus-error "`spam.el' doesnt support obsolete bogofilter versions")))
2411
2412 (defun spam-bogofilter-register-spam-routine (articles &optional unregister)
2413   (spam-bogofilter-register-with-bogofilter articles t unregister))
2414
2415 (defun spam-bogofilter-unregister-spam-routine (articles)
2416   (spam-bogofilter-register-spam-routine articles t))
2417
2418 (defun spam-bogofilter-register-ham-routine (articles &optional unregister)
2419   (spam-bogofilter-register-with-bogofilter articles nil unregister))
2420
2421 (defun spam-bogofilter-unregister-ham-routine (articles)
2422   (spam-bogofilter-register-ham-routine articles t))
2423
2424
2425 ;;}}}
2426
2427 ;;{{{ spamoracle
2428 (defun spam-check-spamoracle ()
2429   "Run spamoracle on an article to determine whether it's spam."
2430   (let ((article-buffer-name (buffer-name)))
2431     (with-temp-buffer
2432       (let ((temp-buffer-name (buffer-name)))
2433         (save-excursion
2434           (set-buffer article-buffer-name)
2435           (let ((status
2436                  (apply 'call-process-region
2437                         (point-min) (point-max)
2438                         spam-spamoracle-binary
2439                         nil temp-buffer-name nil
2440                         (if spam-spamoracle-database
2441                             `("-f" ,spam-spamoracle-database "mark")
2442                           '("mark")))))
2443             (if (eq 0 status)
2444                 (progn
2445                   (set-buffer temp-buffer-name)
2446                   (goto-char (point-min))
2447                   (when (re-search-forward "^X-Spam: yes;" nil t)
2448                     spam-split-group))
2449               (error "Error running spamoracle: %s" status))))))))
2450
2451 (defun spam-spamoracle-learn (articles article-is-spam-p &optional unregister)
2452   "Run spamoracle in training mode."
2453   (with-temp-buffer
2454     (let ((temp-buffer-name (buffer-name)))
2455       (save-excursion
2456         (goto-char (point-min))
2457         (dolist (article articles)
2458           (insert (spam-get-article-as-string article)))
2459         (let* ((arg (if (spam-xor unregister article-is-spam-p)
2460                         "-spam"
2461                       "-good"))
2462                (status
2463                 (apply 'call-process-region
2464                        (point-min) (point-max)
2465                        spam-spamoracle-binary
2466                        nil temp-buffer-name nil
2467                        (if spam-spamoracle-database
2468                            `("-f" ,spam-spamoracle-database
2469                              "add" ,arg)
2470                          `("add" ,arg)))))
2471           (unless (eq 0 status)
2472             (error "Error running spamoracle: %s" status)))))))
2473
2474 (defun spam-spamoracle-learn-ham (articles &optional unregister)
2475   (spam-spamoracle-learn articles nil unregister))
2476
2477 (defun spam-spamoracle-unlearn-ham (articles &optional unregister)
2478   (spam-spamoracle-learn-ham articles t))
2479
2480 (defun spam-spamoracle-learn-spam (articles &optional unregister)
2481   (spam-spamoracle-learn articles t unregister))
2482
2483 (defun spam-spamoracle-unlearn-spam (articles &optional unregister)
2484   (spam-spamoracle-learn-spam articles t))
2485
2486 ;;}}}
2487
2488 ;;{{{ SpamAssassin
2489 ;;; based mostly on the bogofilter code
2490 (defun spam-check-spamassassin-headers (&optional score)
2491   "Check the SpamAssassin headers for the classification of this message."
2492   (if score                             ; scoring mode
2493       (let ((header (message-fetch-field spam-spamassassin-spam-status-header)))
2494         (when header
2495           (if (string-match "hits=\\(-?[0-9.]+\\)" header)
2496               (match-string 1 header)
2497             "0")))
2498     ;; spam detection mode
2499     (let ((header (message-fetch-field spam-spamassassin-spam-flag-header)))
2500           (when header                  ; return nil when no header
2501             (when (string-match spam-spamassassin-positive-spam-flag-header
2502                                 header)
2503               spam-split-group)))))
2504
2505 (defun spam-check-spamassassin (&optional score)
2506   "Check the SpamAssassin backend for the classification of this message."
2507   (let ((article-buffer-name (buffer-name)))
2508     (with-temp-buffer
2509       (let ((temp-buffer-name (buffer-name)))
2510         (save-excursion
2511           (set-buffer article-buffer-name)
2512           (apply 'call-process-region
2513                  (point-min) (point-max) spam-spamassassin-path
2514                  nil temp-buffer-name nil spam-spamassassin-arguments))
2515         ;; check the return now (we're back in the temp buffer)
2516         (goto-char (point-min))
2517         (spam-check-spamassassin-headers score)))))
2518
2519 ;; return something sensible if the score can't be determined
2520 (defun spam-spamassassin-score (&optional recheck)
2521   "Get the SpamAssassin score"
2522   (interactive "P")
2523   (save-window-excursion
2524     (gnus-summary-show-article t)
2525     (set-buffer gnus-article-buffer)
2526     (let ((score (or (unless recheck
2527                        (spam-check-spamassassin-headers t))
2528                      (spam-check-spamassassin t))))
2529       (gnus-summary-show-article)
2530       (message "SpamAssassin score %s" score)
2531       (or score "0"))))
2532
2533 (defun spam-spamassassin-register-with-sa-learn (articles spam
2534                                                  &optional unregister)
2535   "Register articles with spamassassin's sa-learn as spam or non-spam."
2536   (if articles
2537       (let ((action (if unregister spam-sa-learn-unregister-switch
2538                       (if spam spam-sa-learn-spam-switch
2539                         spam-sa-learn-ham-switch)))
2540             (summary-buffer-name (buffer-name)))
2541         (with-temp-buffer
2542           ;; group the articles into mbox format
2543           (dolist (article articles)
2544             (let (article-string)
2545               (save-excursion
2546                 (set-buffer summary-buffer-name)
2547                 (setq article-string (spam-get-article-as-string article)))
2548               (when (stringp article-string)
2549                 (insert "From \n") ; mbox separator (sa-learn only checks the
2550                                    ; first five chars, so we can get away with
2551                                    ; a bogus line))
2552                 (insert article-string)
2553                 (insert "\n"))))
2554           ;; call sa-learn on all messages at the same time
2555           (apply 'call-process-region
2556                  (point-min) (point-max)
2557                  spam-sa-learn-path
2558                  nil nil nil "--mbox"
2559                  (if spam-sa-learn-rebuild
2560                      (list action)
2561                    `("--no-rebuild" ,action)))))))
2562
2563 (defun spam-spamassassin-register-spam-routine (articles &optional unregister)
2564   (spam-spamassassin-register-with-sa-learn articles t unregister))
2565
2566 (defun spam-spamassassin-register-ham-routine (articles &optional unregister)
2567   (spam-spamassassin-register-with-sa-learn articles nil unregister))
2568
2569 (defun spam-spamassassin-unregister-spam-routine (articles)
2570   (spam-spamassassin-register-with-sa-learn articles t t))
2571
2572 (defun spam-spamassassin-unregister-ham-routine (articles)
2573   (spam-spamassassin-register-with-sa-learn articles nil t))
2574
2575 ;;}}}
2576
2577 ;;{{{ Bsfilter
2578 ;;; based mostly on the bogofilter code
2579 (defun spam-check-bsfilter-headers (&optional score)
2580   (if score
2581       (or (nnmail-fetch-field spam-bsfilter-probability-header)
2582           "0")
2583     (let ((header (nnmail-fetch-field spam-bsfilter-header)))
2584       (when header ; return nil when no header
2585         (when (string-match "YES" header)
2586           spam-split-group)))))
2587
2588 ;; return something sensible if the score can't be determined
2589 (defun spam-bsfilter-score (&optional recheck)
2590   "Get the Bsfilter spamicity score"
2591   (interactive "P")
2592   (save-window-excursion
2593     (gnus-summary-show-article t)
2594     (set-buffer gnus-article-buffer)
2595     (let ((score (or (unless recheck
2596                        (spam-check-bsfilter-headers t))
2597                      (spam-check-bsfilter t))))
2598       (gnus-summary-show-article)
2599       (message "Spamicity score %s" score)
2600       (or score "0"))))
2601
2602 (defun spam-check-bsfilter (&optional score)
2603   "Check the Bsfilter backend for the classification of this message"
2604   (let ((article-buffer-name (buffer-name))
2605         (dir spam-bsfilter-database-directory)
2606         return)
2607     (with-temp-buffer
2608       (let ((temp-buffer-name (buffer-name)))
2609         (save-excursion
2610           (set-buffer article-buffer-name)
2611           (apply 'call-process-region
2612                  (point-min) (point-max)
2613                  spam-bsfilter-path
2614                  nil temp-buffer-name nil
2615                  "--pipe"
2616                  "--insert-flag"
2617                  "--insert-probability"
2618                  (when dir
2619                    (list "--homedir" dir))))
2620         (setq return (spam-check-bsfilter-headers score))))
2621     return))
2622
2623 (defun spam-bsfilter-register-with-bsfilter (articles
2624                                              spam
2625                                              &optional unregister)
2626   "Register an article, given as a string, as spam or non-spam."
2627   (dolist (article articles)
2628     (let ((article-string (spam-get-article-as-string article))
2629           (switch (if unregister
2630                       (if spam
2631                           spam-bsfilter-spam-strong-switch
2632                         spam-bsfilter-ham-strong-switch)
2633                     (if spam
2634                         spam-bsfilter-spam-switch
2635                       spam-bsfilter-ham-switch))))
2636       (when (stringp article-string)
2637         (with-temp-buffer
2638           (insert article-string)
2639           (apply 'call-process-region
2640                  (point-min) (point-max)
2641                  spam-bsfilter-path
2642                  nil nil nil switch
2643                  "--update"
2644                  (when spam-bsfilter-database-directory
2645                    (list "--homedir"
2646                          spam-bsfilter-database-directory))))))))
2647
2648 (defun spam-bsfilter-register-spam-routine (articles &optional unregister)
2649   (spam-bsfilter-register-with-bsfilter articles t unregister))
2650
2651 (defun spam-bsfilter-unregister-spam-routine (articles)
2652   (spam-bsfilter-register-spam-routine articles t))
2653
2654 (defun spam-bsfilter-register-ham-routine (articles &optional unregister)
2655   (spam-bsfilter-register-with-bsfilter articles nil unregister))
2656
2657 (defun spam-bsfilter-unregister-ham-routine (articles)
2658   (spam-bsfilter-register-ham-routine articles t))
2659
2660 ;;}}}
2661
2662 ;;{{{ CRM114 Mailfilter
2663 (defun spam-check-crm114-headers (&optional score)
2664   (let ((header (message-fetch-field spam-crm114-header)))
2665     (when header                        ; return nil when no header
2666       (if score                         ; scoring mode
2667           (if (string-match "( pR: \\([0-9.-]+\\)" header)
2668               (match-string 1 header)
2669             "0")
2670         ;; spam detection mode
2671         (when (string-match spam-crm114-positive-spam-header
2672                             header)
2673           spam-split-group)))))
2674
2675 ;; return something sensible if the score can't be determined
2676 (defun spam-crm114-score ()
2677   "Get the CRM114 Mailfilter pR"
2678   (interactive)
2679   (save-window-excursion
2680     (gnus-summary-show-article t)
2681     (set-buffer gnus-article-buffer)
2682     (let ((score (or (spam-check-crm114-headers t)
2683                      (spam-check-crm114 t))))
2684       (gnus-summary-show-article)
2685       (message "pR: %s" score)
2686       (or score "0"))))
2687
2688 (defun spam-check-crm114 (&optional score)
2689   "Check the CRM114 Mailfilter backend for the classification of this message"
2690   (let ((article-buffer-name (buffer-name))
2691         (db spam-crm114-database-directory)
2692         return)
2693     (with-temp-buffer
2694       (let ((temp-buffer-name (buffer-name)))
2695         (save-excursion
2696           (set-buffer article-buffer-name)
2697           (apply 'call-process-region
2698                  (point-min) (point-max)
2699                  spam-crm114-program
2700                  nil temp-buffer-name nil
2701                  (when db (list (concat "--fileprefix=" db)))))
2702         (setq return (spam-check-crm114-headers score))))
2703     return))
2704
2705 (defun spam-crm114-register-with-crm114 (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           (db spam-crm114-database-directory)
2712           (switch (if unregister
2713                       (if spam
2714                           spam-crm114-spam-strong-switch
2715                         spam-crm114-ham-strong-switch)
2716                     (if spam
2717                         spam-crm114-spam-switch
2718                       spam-crm114-ham-switch))))
2719       (when (stringp article-string)
2720         (with-temp-buffer
2721           (insert article-string)
2722
2723           (apply 'call-process-region
2724                  (point-min) (point-max)
2725                  spam-crm114-program
2726                  nil nil nil
2727                  (when db (list switch (concat "--fileprefix=" db)))))))))
2728
2729 (defun spam-crm114-register-spam-routine (articles &optional unregister)
2730   (spam-crm114-register-with-crm114 articles t unregister))
2731
2732 (defun spam-crm114-unregister-spam-routine (articles)
2733   (spam-crm114-register-spam-routine articles t))
2734
2735 (defun spam-crm114-register-ham-routine (articles &optional unregister)
2736   (spam-crm114-register-with-crm114 articles nil unregister))
2737
2738 (defun spam-crm114-unregister-ham-routine (articles)
2739   (spam-crm114-register-ham-routine articles t))
2740
2741 ;;}}}
2742
2743 ;;}}}
2744
2745 ;;{{{ Hooks
2746
2747 ;;;###autoload
2748 (defun spam-initialize (&rest symbols)
2749   "Install the spam.el hooks and do other initialization.
2750 When SYMBOLS is given, set those variables to t.  This is so you
2751 can call spam-initialize before you set spam-use-* variables on
2752 explicitly, and matters only if you need the extra headers
2753 installed through spam-necessary-extra-headers."
2754   (interactive)
2755
2756   (dolist (var symbols)
2757     (set var t))
2758
2759   (dolist (header (spam-necessary-extra-headers))
2760     (add-to-list 'nnmail-extra-headers header)
2761     (add-to-list 'gnus-extra-headers header))
2762
2763   (setq spam-install-hooks t)
2764   ;; TODO: How do we redo this every time spam-face is customized?
2765   (push '((eq mark gnus-spam-mark) . spam-face)
2766         gnus-summary-highlight)
2767   ;; Add hooks for loading and saving the spam stats
2768   (add-hook 'gnus-save-newsrc-hook 'spam-maybe-spam-stat-save)
2769   (add-hook 'gnus-get-top-new-news-hook 'spam-maybe-spam-stat-load)
2770   (add-hook 'gnus-startup-hook 'spam-maybe-spam-stat-load)
2771   (add-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
2772   (add-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
2773   (add-hook 'gnus-get-new-news-hook 'spam-setup-widening)
2774   (add-hook 'gnus-summary-prepared-hook 'spam-find-spam))
2775
2776 (defun spam-unload-hook ()
2777   "Uninstall the spam.el hooks"
2778   (interactive)
2779   (remove-hook 'gnus-save-newsrc-hook 'spam-maybe-spam-stat-save)
2780   (remove-hook 'gnus-get-top-new-news-hook 'spam-maybe-spam-stat-load)
2781   (remove-hook 'gnus-startup-hook 'spam-maybe-spam-stat-load)
2782   (remove-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
2783   (remove-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
2784   (remove-hook 'gnus-get-new-news-hook 'spam-setup-widening)
2785   (remove-hook 'gnus-summary-prepare-hook 'spam-find-spam))
2786
2787 (when spam-install-hooks
2788   (spam-initialize))
2789 ;;}}}
2790
2791 (provide 'spam)
2792
2793 ;;; arch-tag: 07e6e0ca-ab0a-4412-b445-1f6c72a4f27f
2794 ;;; spam.el ends here