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