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