Synch to Gnus 200401040320.
[elisp/gnus.git-] / lisp / spam.el
1 ;;; spam.el --- Identifying spam
2 ;; Copyright (C) 2002, 2003, 2004 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
5 ;; Keywords: network
6
7 ;; This file is part of GNU Emacs.
8
9 ;; GNU Emacs is free software; you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation; either version 2, or (at your option)
12 ;; any later version.
13
14 ;; GNU Emacs is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 ;; GNU General Public License for more details.
18
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
21 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
22 ;; Boston, MA 02111-1307, USA.
23
24 ;;; Commentary:
25
26 ;;; This module addresses a few aspects of spam control under Gnus.  Page
27 ;;; breaks are used for grouping declarations and documentation relating to
28 ;;; each particular aspect.
29
30 ;;; The integration with Gnus is not yet complete.  See various `FIXME'
31 ;;; comments, below, for supplementary explanations or discussions.
32
33 ;;; Several TODO items are marked as such
34
35 ;; TODO: spam scores, detection of spam in newsgroups, cross-server splitting,
36 ;; remote processing, training through files
37
38 ;;; Code:
39
40 (require 'path-util)
41
42 (eval-when-compile (require 'cl))
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
62 ;; autoload gnus-registry
63 (eval-and-compile
64   (autoload 'gnus-registry-group-count "gnus-registry")
65   (autoload 'gnus-registry-add-group "gnus-registry")
66   (autoload 'gnus-registry-store-extra-entry "gnus-registry")
67   (autoload 'gnus-registry-fetch-extra "gnus-registry"))
68
69 ;; autoload query-dns
70 (eval-and-compile
71   (autoload 'query-dns "dns"))
72
73 ;;; Main parameters.
74
75 (defgroup spam nil
76   "Spam configuration.")
77
78 (defcustom spam-directory "~/News/spam/"
79   "Directory for spam whitelists and blacklists."
80   :type 'directory
81   :group 'spam)
82
83 (defcustom spam-move-spam-nonspam-groups-only t
84   "Whether spam should be moved in non-spam groups only.
85 When t, only ham and unclassified groups will have their spam moved
86 to the spam-process-destination.  When nil, spam will also be moved from
87 spam groups."
88   :type 'boolean
89   :group 'spam)
90
91 (defcustom spam-process-ham-in-nonham-groups nil
92   "Whether ham should be processed in non-ham groups."
93   :type 'boolean
94   :group 'spam)
95
96 (defcustom spam-log-to-registry nil
97   "Whether spam/ham processing should be logged in the registry."
98   :type 'boolean
99   :group 'spam)
100
101 (defcustom spam-split-symbolic-return nil
102   "Whether `spam-split' should work with symbols or group names."
103   :type 'boolean
104   :group 'spam)
105
106 (defcustom spam-split-symbolic-return-positive nil
107   "Whether `spam-split' should ALWAYS work with symbols or group names.
108 Do not set this if you use `spam-split' in a fancy split
109   method."
110   :type 'boolean
111   :group 'spam)
112
113 (defcustom spam-process-ham-in-spam-groups nil
114   "Whether ham should be processed in spam groups."
115   :type 'boolean
116   :group 'spam)
117
118 (defcustom spam-mark-only-unseen-as-spam t
119   "Whether only unseen articles should be marked as spam in spam groups.
120 When nil, all unread articles in a spam group are marked as
121 spam.  Set this if you want to leave an article unread in a spam group
122 without losing it to the automatic spam-marking process."
123   :type 'boolean
124   :group 'spam)
125
126 (defcustom spam-mark-ham-unread-before-move-from-spam-group nil
127   "Whether ham should be marked unread before it's moved.
128 The article is moved out of a spam group according to ham-process-destination.
129 This variable is an official entry in the international Longest Variable Name
130 Competition."
131   :type 'boolean
132   :group 'spam)
133
134 (defcustom spam-disable-spam-split-during-ham-respool nil
135   "Whether `spam-split' should be ignored while resplitting ham in a process
136 destination.  This is useful to prevent ham from ending up in the same spam
137 group after the resplit.  Don't set this to t if you have spam-split as the
138 last rule in your split configuration."
139   :type 'boolean
140   :group 'spam)
141
142 (defcustom spam-autodetect-recheck-messages nil
143   "Should spam.el recheck all meessages when autodetecting?
144 Normally this is nil, so only unseen messages will be checked."
145   :type 'boolean
146   :group 'spam)
147
148 (defcustom spam-whitelist (expand-file-name "whitelist" spam-directory)
149   "The location of the whitelist.
150 The file format is one regular expression per line.
151 The regular expression is matched against the address."
152   :type 'file
153   :group 'spam)
154
155 (defcustom spam-blacklist (expand-file-name "blacklist" spam-directory)
156   "The location of the blacklist.
157 The file format is one regular expression per line.
158 The regular expression is matched against the address."
159   :type 'file
160   :group 'spam)
161
162 (defcustom spam-use-dig t
163   "Whether `query-dig' should be used instead of `query-dns'."
164   :type 'boolean
165   :group 'spam)
166
167 (defcustom spam-use-blacklist nil
168   "Whether the blacklist should be used by `spam-split'."
169   :type 'boolean
170   :group 'spam)
171
172 (defcustom spam-blacklist-ignored-regexes nil
173   "Regular expressions that the blacklist should ignore."
174   :type '(repeat (regexp :tag "Regular expression to ignore when blacklisting"))
175   :group 'spam)
176
177 (defcustom spam-use-whitelist nil
178   "Whether the whitelist should be used by `spam-split'."
179   :type 'boolean
180   :group 'spam)
181
182 (defcustom spam-use-whitelist-exclusive nil
183   "Whether whitelist-exclusive should be used by `spam-split'.
184 Exclusive whitelisting means that all messages from senders not in the whitelist
185 are considered spam."
186   :type 'boolean
187   :group 'spam)
188
189 (defcustom spam-use-blackholes nil
190   "Whether blackholes should be used by `spam-split'."
191   :type 'boolean
192   :group 'spam)
193
194 (defcustom spam-use-hashcash nil
195   "Whether hashcash payments should be detected by `spam-split'."
196   :type 'boolean
197   :group 'spam)
198
199 (defcustom spam-use-regex-headers nil
200   "Whether a header regular expression match should be used by `spam-split'.
201 Also see the variables `spam-regex-headers-spam' and `spam-regex-headers-ham'."
202   :type 'boolean
203   :group 'spam)
204
205 (defcustom spam-use-regex-body nil
206   "Whether a body regular expression match should be used by `spam-split'.
207 Also see the variables `spam-regex-body-spam' and `spam-regex-body-ham'."
208   :type 'boolean
209   :group 'spam)
210
211 (defcustom spam-use-bogofilter-headers nil
212   "Whether bogofilter headers should be used by `spam-split'.
213 Enable this if you pre-process messages with Bogofilter BEFORE Gnus sees them."
214   :type 'boolean
215   :group 'spam)
216
217 (defcustom spam-use-bogofilter nil
218   "Whether bogofilter should be invoked by `spam-split'.
219 Enable this if you want Gnus to invoke Bogofilter on new messages."
220   :type 'boolean
221   :group 'spam)
222
223 (defcustom spam-use-BBDB nil
224   "Whether BBDB should be used by `spam-split'."
225   :type 'boolean
226   :group 'spam)
227
228 (defcustom spam-use-BBDB-exclusive nil
229   "Whether BBDB-exclusive should be used by `spam-split'.
230 Exclusive BBDB means that all messages from senders not in the BBDB are
231 considered spam."
232   :type 'boolean
233   :group 'spam)
234
235 (defcustom spam-use-ifile nil
236   "Whether ifile should be used by `spam-split'."
237   :type 'boolean
238   :group 'spam)
239
240 (defcustom spam-use-stat nil
241   "Whether `spam-stat' should be used by `spam-split'."
242   :type 'boolean
243   :group 'spam)
244
245 (defcustom spam-use-spamoracle nil
246   "Whether spamoracle should be used by `spam-split'."
247   :type 'boolean
248   :group 'spam)
249
250 (defcustom spam-install-hooks (or
251                                spam-use-dig
252                                spam-use-blacklist
253                                spam-use-whitelist
254                                spam-use-whitelist-exclusive
255                                spam-use-blackholes
256                                spam-use-hashcash
257                                spam-use-regex-headers
258                                spam-use-regex-body
259                                spam-use-bogofilter-headers
260                                spam-use-bogofilter
261                                spam-use-BBDB
262                                spam-use-BBDB-exclusive
263                                spam-use-ifile
264                                spam-use-stat
265                                spam-use-spamoracle)
266   "Whether the spam hooks should be installed.
267 Default to t if one of the spam-use-* variables is set."
268   :group 'spam
269   :type 'boolean)
270
271 (defcustom spam-split-group "spam"
272   "Group name where incoming spam should be put by `spam-split'."
273   :type 'string
274   :group 'spam)
275
276 ;;; TODO: deprecate this variable, it's confusing since it's a list of strings,
277 ;;; not regular expressions
278 (defcustom spam-junk-mailgroups (cons
279                                  spam-split-group
280                                  '("mail.junk" "poste.pourriel"))
281   "Mailgroups with spam contents.
282 All unmarked article in such group receive the spam mark on group entry."
283   :type '(repeat (string :tag "Group"))
284   :group 'spam)
285
286 (defcustom spam-blackhole-servers '("bl.spamcop.net" "relays.ordb.org"
287                                     "dev.null.dk" "relays.visi.com")
288   "List of blackhole servers."
289   :type '(repeat (string :tag "Server"))
290   :group 'spam)
291
292 (defcustom spam-blackhole-good-server-regex nil
293   "String matching IP addresses that should not be checked in the blackholes."
294   :type '(radio (const nil)
295                 (regexp :format "%t: %v\n" :size 0))
296   :group 'spam)
297
298 (defcustom spam-face 'gnus-splash-face
299   "Face for spam-marked articles."
300   :type 'face
301   :group 'spam)
302
303 (defcustom spam-regex-headers-spam '("^X-Spam-Flag: YES")
304   "Regular expression for positive header spam matches."
305   :type '(repeat (regexp :tag "Regular expression to match spam header"))
306   :group 'spam)
307
308 (defcustom spam-regex-headers-ham '("^X-Spam-Flag: NO")
309   "Regular expression for positive header ham matches."
310   :type '(repeat (regexp :tag "Regular expression to match ham header"))
311   :group 'spam)
312
313 (defcustom spam-regex-body-spam '()
314   "Regular expression for positive body spam matches."
315   :type '(repeat (regexp :tag "Regular expression to match spam body"))
316   :group 'spam)
317
318 (defcustom spam-regex-body-ham '()
319   "Regular expression for positive body ham matches."
320   :type '(repeat (regexp :tag "Regular expression to match ham body"))
321   :group 'spam)
322
323 (defgroup spam-ifile nil
324   "Spam ifile configuration."
325   :group 'spam)
326
327 (defcustom spam-ifile-path (exec-installed-p "ifile")
328   "File path of the ifile executable program."
329   :type '(choice (file :tag "Location of ifile")
330                  (const :tag "ifile is not installed"))
331   :group 'spam-ifile)
332
333 (defcustom spam-ifile-database-path nil
334   "File path of the ifile database."
335   :type '(choice (file :tag "Location of the ifile database")
336                  (const :tag "Use the default"))
337   :group 'spam-ifile)
338
339 (defcustom spam-ifile-spam-category "spam"
340   "Name of the spam ifile category."
341   :type 'string
342   :group 'spam-ifile)
343
344 (defcustom spam-ifile-ham-category nil
345   "Name of the ham ifile category.
346 If nil, the current group name will be used."
347   :type '(choice (string :tag "Use a fixed category")
348                  (const :tag "Use the current group name"))
349   :group 'spam-ifile)
350
351 (defcustom spam-ifile-all-categories nil
352   "Whether the ifile check will return all categories, or just spam.
353 Set this to t if you want to use the `spam-split' invocation of ifile as
354 your main source of newsgroup names."
355   :type 'boolean
356   :group 'spam-ifile)
357
358 (defgroup spam-bogofilter nil
359   "Spam bogofilter configuration."
360   :group 'spam)
361
362 (defcustom spam-bogofilter-path (exec-installed-p "bogofilter")
363   "File path of the Bogofilter executable program."
364   :type '(choice (file :tag "Location of bogofilter")
365                  (const :tag "Bogofilter is not installed"))
366   :group 'spam-bogofilter)
367
368 (defcustom spam-bogofilter-header "X-Bogosity"
369   "The header that Bogofilter inserts in messages."
370   :type 'string
371   :group 'spam-bogofilter)
372
373 (defcustom spam-bogofilter-spam-switch "-s"
374   "The switch that Bogofilter uses to register spam messages."
375   :type 'string
376   :group 'spam-bogofilter)
377
378 (defcustom spam-bogofilter-ham-switch "-n"
379   "The switch that Bogofilter uses to register ham messages."
380   :type 'string
381   :group 'spam-bogofilter)
382
383 (defcustom spam-bogofilter-spam-strong-switch "-S"
384   "The switch that Bogofilter uses to unregister ham messages."
385   :type 'string
386   :group 'spam-bogofilter)
387
388 (defcustom spam-bogofilter-ham-strong-switch "-N"
389   "The switch that Bogofilter uses to unregister spam messages."
390   :type 'string
391   :group 'spam-bogofilter)
392
393 (defcustom spam-bogofilter-bogosity-positive-spam-header "^\\(Yes\\|Spam\\)"
394   "The regex on `spam-bogofilter-header' for positive spam identification."
395   :type 'regexp
396   :group 'spam-bogofilter)
397
398 (defcustom spam-bogofilter-database-directory nil
399   "Directory path of the Bogofilter databases."
400   :type '(choice (directory
401                   :tag "Location of the Bogofilter database directory")
402                  (const :tag "Use the default"))
403   :group 'spam-bogofilter)
404
405 (defgroup spam-spamoracle nil
406   "Spam spamoracle configuration."
407   :group 'spam)
408
409 (defcustom spam-spamoracle-database nil
410   "Location of spamoracle database file. When nil, use the default
411 spamoracle database."
412   :type '(choice (directory :tag "Location of spamoracle database file.")
413                  (const :tag "Use the default"))
414   :group 'spam-spamoracle)
415
416 (defcustom spam-spamoracle-binary (executable-find "spamoracle")
417   "Location of the spamoracle binary."
418   :type '(choice (directory :tag "Location of the spamoracle binary")
419                  (const :tag "Use the default"))
420   :group 'spam-spamoracle)
421
422 ;;; Key bindings for spam control.
423
424 (gnus-define-keys gnus-summary-mode-map
425   "St" spam-bogofilter-score
426   "Sx" gnus-summary-mark-as-spam
427   "Mst" spam-bogofilter-score
428   "Msx" gnus-summary-mark-as-spam
429   "\M-d" gnus-summary-mark-as-spam)
430
431 (defvar spam-old-ham-articles nil
432   "List of old ham articles, generated when a group is entered.")
433
434 (defvar spam-old-spam-articles nil
435   "List of old spam articles, generated when a group is entered.")
436
437 (defvar spam-split-disabled nil
438   "If non-nil, `spam-split' is disabled, and always returns nil.")
439
440 (defvar spam-split-last-successful-check nil
441   "`spam-split' will set this to nil or a spam-use-XYZ check if it
442   finds ham or spam.")
443
444 ;; convenience functions
445 (defun spam-xor (a b)
446   "Logical exclusive `or'."
447   (and (or a b) (not (and a b))))
448
449 (defun spam-group-ham-mark-p (group mark &optional spam)
450   (when (stringp group)
451     (let* ((marks (spam-group-ham-marks group spam))
452            (marks (if (symbolp mark)
453                       marks
454                     (mapcar 'symbol-value marks))))
455       (memq mark marks))))
456
457 (defun spam-group-spam-mark-p (group mark)
458   (spam-group-ham-mark-p group mark t))
459
460 (defun spam-group-ham-marks (group &optional spam)
461   (when (stringp group)
462     (let* ((marks (if spam
463                       (gnus-parameter-spam-marks group)
464                     (gnus-parameter-ham-marks group)))
465            (marks (car marks))
466            (marks (if (listp (car marks)) (car marks) marks)))
467       marks)))
468
469 (defun spam-group-spam-marks (group)
470   (spam-group-ham-marks group t))
471
472 (defun spam-group-spam-contents-p (group)
473   (if (stringp group)
474       (or (member group spam-junk-mailgroups)
475           (memq 'gnus-group-spam-classification-spam
476                 (gnus-parameter-spam-contents group)))
477     nil))
478
479 (defun spam-group-ham-contents-p (group)
480   (if (stringp group)
481       (memq 'gnus-group-spam-classification-ham
482             (gnus-parameter-spam-contents group))
483     nil))
484
485 (defvar spam-list-of-processors
486   '((gnus-group-spam-exit-processor-report-gmane spam spam-use-gmane)
487     (gnus-group-spam-exit-processor-bogofilter   spam spam-use-bogofilter)
488     (gnus-group-spam-exit-processor-blacklist    spam spam-use-blacklist)
489     (gnus-group-spam-exit-processor-ifile        spam spam-use-ifile)
490     (gnus-group-spam-exit-processor-stat         spam spam-use-stat)
491     (gnus-group-spam-exit-processor-spamoracle   spam spam-use-spamoracle)
492     (gnus-group-ham-exit-processor-ifile         ham spam-use-ifile)
493     (gnus-group-ham-exit-processor-bogofilter    ham spam-use-bogofilter)
494     (gnus-group-ham-exit-processor-stat          ham spam-use-stat)
495     (gnus-group-ham-exit-processor-whitelist     ham spam-use-whitelist)
496     (gnus-group-ham-exit-processor-BBDB          ham spam-use-BBDB)
497     (gnus-group-ham-exit-processor-copy          ham spam-use-ham-copy)
498     (gnus-group-ham-exit-processor-spamoracle    ham spam-use-spamoracle))
499   "The spam-list-of-processors list contains pairs associating a
500 ham/spam exit processor variable with a classification and a
501 spam-use-* variable.")
502
503 (defun spam-group-processor-p (group processor)
504   (if (and (stringp group)
505            (symbolp processor))
506       (or (member processor (nth 0 (gnus-parameter-spam-process group)))
507           (spam-group-processor-multiple-p
508            group
509            (cdr-safe (assoc processor spam-list-of-processors))))
510     nil))
511
512 (defun spam-group-processor-multiple-p (group processor-info)
513   (let* ((classification (nth 0 processor-info))
514          (check (nth 1 processor-info))
515          (parameters (nth 0 (gnus-parameter-spam-process group)))
516          found)
517     (dolist (parameter parameters)
518       (when (and (null found)
519                  (listp parameter)
520                  (eq classification (nth 0 parameter))
521                  (eq check (nth 1 parameter)))
522         (setq found t)))
523     found))
524
525 (defun spam-group-spam-processor-report-gmane-p (group)
526   (spam-group-processor-p group 'gnus-group-spam-exit-processor-report-gmane))
527
528 (defun spam-group-spam-processor-bogofilter-p (group)
529   (spam-group-processor-p group 'gnus-group-spam-exit-processor-bogofilter))
530
531 (defun spam-group-spam-processor-blacklist-p (group)
532   (spam-group-processor-p group 'gnus-group-spam-exit-processor-blacklist))
533
534 (defun spam-group-spam-processor-ifile-p (group)
535   (spam-group-processor-p group 'gnus-group-spam-exit-processor-ifile))
536
537 (defun spam-group-ham-processor-ifile-p (group)
538   (spam-group-processor-p group 'gnus-group-ham-exit-processor-ifile))
539
540 (defun spam-group-spam-processor-spamoracle-p (group)
541   (spam-group-processor-p group 'gnus-group-spam-exit-processor-spamoracle))
542
543 (defun spam-group-ham-processor-bogofilter-p (group)
544   (spam-group-processor-p group 'gnus-group-ham-exit-processor-bogofilter))
545
546 (defun spam-group-spam-processor-stat-p (group)
547   (spam-group-processor-p group 'gnus-group-spam-exit-processor-stat))
548
549 (defun spam-group-ham-processor-stat-p (group)
550   (spam-group-processor-p group 'gnus-group-ham-exit-processor-stat))
551
552 (defun spam-group-ham-processor-whitelist-p (group)
553   (spam-group-processor-p group 'gnus-group-ham-exit-processor-whitelist))
554
555 (defun spam-group-ham-processor-BBDB-p (group)
556   (spam-group-processor-p group 'gnus-group-ham-exit-processor-BBDB))
557
558 (defun spam-group-ham-processor-copy-p (group)
559   (spam-group-processor-p group 'gnus-group-ham-exit-processor-copy))
560
561 (defun spam-group-ham-processor-spamoracle-p (group)
562   (spam-group-processor-p group 'gnus-group-ham-exit-processor-spamoracle))
563
564 ;;; Summary entry and exit processing.
565
566 (defun spam-summary-prepare ()
567   (setq spam-old-ham-articles
568         (spam-list-articles gnus-newsgroup-articles 'ham))
569   (setq spam-old-spam-articles
570         (spam-list-articles gnus-newsgroup-articles 'spam))
571   (spam-mark-junk-as-spam-routine))
572
573 ;; The spam processors are invoked for any group, spam or ham or neither
574 (defun spam-summary-prepare-exit ()
575   (unless gnus-group-is-exiting-without-update-p
576     (gnus-message 6 "Exiting summary buffer and applying spam rules")
577
578     ;; first of all, unregister any articles that are no longer ham or spam
579     ;; we have to iterate over the processors, or else we'll be too slow
580     (dolist (classification '(spam ham))
581       (let* ((old-articles (if (eq classification 'spam)
582                                spam-old-spam-articles
583                              spam-old-ham-articles))
584              (new-articles (spam-list-articles
585                             gnus-newsgroup-articles
586                             classification))
587              (changed-articles (gnus-set-difference old-articles new-articles)))
588         ;; now that we have the changed articles, we go through the processors
589         (dolist (processor-param spam-list-of-processors)
590           (let ((processor (nth 0 processor-param))
591                 (processor-classification (nth 1 processor-param))
592                 (check (nth 2 processor-param))
593                 unregister-list)
594             (dolist (article changed-articles)
595               (let ((id (spam-fetch-field-message-id-fast article)))
596                 (when (spam-log-unregistration-needed-p
597                        id 'process classification check)
598                   (push article unregister-list))))
599             ;; call spam-register-routine with specific articles to unregister,
600             ;; when there are articles to unregister and the check is enabled
601             (when (and unregister-list (symbol-value check))
602               (spam-register-routine classification check t unregister-list))))))
603
604     ;; find all the spam processors applicable to this group
605     (dolist (processor-param spam-list-of-processors)
606       (let ((processor (nth 0 processor-param))
607             (classification (nth 1 processor-param))
608             (check (nth 2 processor-param)))
609         (when (and (eq 'spam classification)
610                    (spam-group-processor-p gnus-newsgroup-name processor))
611           (spam-register-routine classification check))))
612
613     (if spam-move-spam-nonspam-groups-only
614         (when (not (spam-group-spam-contents-p gnus-newsgroup-name))
615           (spam-mark-spam-as-expired-and-move-routine
616            (gnus-parameter-spam-process-destination gnus-newsgroup-name)))
617       (gnus-message 5 "Marking spam as expired and moving it to %s"
618                     gnus-newsgroup-name)
619       (spam-mark-spam-as-expired-and-move-routine
620        (gnus-parameter-spam-process-destination gnus-newsgroup-name)))
621
622     ;; now we redo spam-mark-spam-as-expired-and-move-routine to only
623     ;; expire spam, in case the above did not expire them
624     (gnus-message 5 "Marking spam as expired without moving it")
625     (spam-mark-spam-as-expired-and-move-routine nil)
626
627     (when (or (spam-group-ham-contents-p gnus-newsgroup-name)
628               (and (spam-group-spam-contents-p gnus-newsgroup-name)
629                    spam-process-ham-in-spam-groups)
630               spam-process-ham-in-nonham-groups)
631       ;; find all the ham processors applicable to this group
632       (dolist (processor-param spam-list-of-processors)
633         (let ((processor (nth 0 processor-param))
634               (classification (nth 1 processor-param))
635               (check (nth 2 processor-param)))
636           (when (and (eq 'ham classification)
637                      (spam-group-processor-p gnus-newsgroup-name processor))
638             (spam-register-routine classification check)))))
639
640     (when (spam-group-ham-processor-copy-p gnus-newsgroup-name)
641       (gnus-message 5 "Copying ham")
642       (spam-ham-copy-routine
643        (gnus-parameter-ham-process-destination gnus-newsgroup-name)))
644
645     ;; now move all ham articles out of spam groups
646     (when (spam-group-spam-contents-p gnus-newsgroup-name)
647       (gnus-message 5 "Moving ham messages from spam group")
648       (spam-ham-move-routine
649        (gnus-parameter-ham-process-destination gnus-newsgroup-name))))
650
651   (setq spam-old-ham-articles nil)
652   (setq spam-old-spam-articles nil))
653
654 (defun spam-mark-junk-as-spam-routine ()
655   ;; check the global list of group names spam-junk-mailgroups and the
656   ;; group parameters
657   (when (spam-group-spam-contents-p gnus-newsgroup-name)
658     (gnus-message 5 "Marking %s articles as spam"
659                   (if spam-mark-only-unseen-as-spam
660                       "unseen"
661                     "unread"))
662     (let ((articles (if spam-mark-only-unseen-as-spam
663                         gnus-newsgroup-unseen
664                       gnus-newsgroup-unreads)))
665       (dolist (article articles)
666         (gnus-summary-mark-article article gnus-spam-mark)))))
667
668 (defun spam-mark-spam-as-expired-and-move-routine (&rest groups)
669   (if (and (car-safe groups) (listp (car-safe groups)))
670       (apply 'spam-mark-spam-as-expired-and-move-routine (car groups))
671     (gnus-summary-kill-process-mark)
672     (let ((articles gnus-newsgroup-articles)
673           (backend-supports-deletions
674            (gnus-check-backend-function
675             'request-move-article gnus-newsgroup-name))
676           article tomove deletep)
677       (dolist (article articles)
678         (when (eq (gnus-summary-article-mark article) gnus-spam-mark)
679           (gnus-summary-mark-article article gnus-expirable-mark)
680           (push article tomove)))
681
682       ;; now do the actual copies
683       (dolist (group groups)
684         (when (and tomove
685                    (stringp group))
686           (dolist (article tomove)
687             (gnus-summary-set-process-mark article))
688           (when tomove
689             (if (or (not backend-supports-deletions)
690                     (> (length groups) 1))
691                 (progn
692                   (gnus-summary-copy-article nil group)
693                   (setq deletep t))
694               (gnus-summary-move-article nil group)))))
695
696       ;; now delete the articles, if there was a copy done, and the
697       ;; backend allows it
698       (when (and deletep backend-supports-deletions)
699         (dolist (article tomove)
700           (gnus-summary-set-process-mark article))
701         (when tomove
702           (let ((gnus-novice-user nil)) ; don't ask me if I'm sure
703             (gnus-summary-delete-article nil))))
704
705       (gnus-summary-yank-process-mark))))
706
707 (defun spam-ham-copy-or-move-routine (copy groups)
708   (gnus-summary-kill-process-mark)
709   (let ((todo (spam-list-articles gnus-newsgroup-articles 'ham))
710         (backend-supports-deletions
711          (gnus-check-backend-function
712           'request-move-article gnus-newsgroup-name))
713         (respool-method (gnus-find-method-for-group gnus-newsgroup-name))
714         article mark todo deletep respool)
715
716     (when (member 'respool groups)
717       (setq respool t)                  ; boolean for later
718       (setq groups '("fake"))) ; when respooling, groups are dynamic so fake it
719
720     ;; now do the actual move
721     (dolist (group groups)
722       (when (and todo (stringp group))
723         (dolist (article todo)
724           (when spam-mark-ham-unread-before-move-from-spam-group
725             (gnus-summary-mark-article article gnus-unread-mark))
726           (gnus-summary-set-process-mark article))
727
728         (if respool                        ; respooling is with a "fake" group
729             (let ((spam-split-disabled
730                    (or spam-split-disabled
731                        spam-disable-spam-split-during-ham-respool)))
732               (gnus-summary-respool-article nil respool-method))
733           (if (or (not backend-supports-deletions) ; else, we are not respooling
734                   (> (length groups) 1))
735               (progn                ; if copying, copy and set deletep
736                 (gnus-summary-copy-article nil group)
737                 (setq deletep t))
738             (gnus-summary-move-article nil group))))) ; else move articles
739
740     ;; now delete the articles, unless a) copy is t, and there was a copy done
741     ;;                                 b) a move was done to a single group
742     ;;                                 c) backend-supports-deletions is nil
743     (unless copy
744       (when (and deletep backend-supports-deletions)
745         (dolist (article todo)
746           (gnus-summary-set-process-mark article))
747         (when todo
748           (let ((gnus-novice-user nil)) ; don't ask me if I'm sure
749             (gnus-summary-delete-article nil))))))
750
751   (gnus-summary-yank-process-mark))
752
753 (defun spam-ham-copy-routine (&rest groups)
754   (if (and (car-safe groups) (listp (car-safe groups)))
755       (apply 'spam-ham-copy-routine (car groups))
756     (spam-ham-copy-or-move-routine t groups)))
757
758 (defun spam-ham-move-routine (&rest groups)
759   (if (and (car-safe groups) (listp (car-safe groups)))
760       (apply 'spam-ham-move-routine (car groups))
761     (spam-ham-copy-or-move-routine nil groups)))
762
763 (eval-and-compile
764   (defalias 'spam-point-at-eol (if (fboundp 'point-at-eol)
765                                    'point-at-eol
766                                  'line-end-position)))
767
768 (defun spam-get-article-as-string (article)
769   (let ((article-buffer (spam-get-article-as-buffer article))
770         article-string)
771     (when article-buffer
772       (save-window-excursion
773         (set-buffer article-buffer)
774         (setq article-string (buffer-string))))
775     article-string))
776
777 (defun spam-get-article-as-buffer (article)
778   (let ((article-buffer))
779     (when (numberp article)
780       (save-window-excursion
781         (gnus-summary-goto-subject article)
782         (gnus-summary-show-article t)
783         (setq article-buffer (get-buffer gnus-article-buffer))))
784     article-buffer))
785
786 ;; disabled for now
787 ;; (defun spam-get-article-as-filename (article)
788 ;;   (let ((article-filename))
789 ;;     (when (numberp article)
790 ;;       (nnml-possibly-change-directory
791 ;;        (gnus-group-real-name gnus-newsgroup-name))
792 ;;       (setq article-filename (expand-file-name
793 ;;                              (int-to-string article) nnml-current-directory)))
794 ;;     (if (file-exists-p article-filename)
795 ;;      article-filename
796 ;;       nil)))
797
798 (defun spam-fetch-field-from-fast (article)
799   "Fetch the `from' field quickly, using the internal gnus-data-list function"
800   (if (and (numberp article)
801            (assoc article (gnus-data-list nil)))
802       (mail-header-from
803        (gnus-data-header (assoc article (gnus-data-list nil))))
804     nil))
805
806 (defun spam-fetch-field-subject-fast (article)
807   "Fetch the `subject' field quickly, using the internal
808   gnus-data-list function"
809   (if (and (numberp article)
810            (assoc article (gnus-data-list nil)))
811       (mail-header-subject
812        (gnus-data-header (assoc article (gnus-data-list nil))))
813     nil))
814
815 (defun spam-fetch-field-message-id-fast (article)
816   "Fetch the `Message-ID' field quickly, using the internal
817   gnus-data-list function"
818   (if (and (numberp article)
819            (assoc article (gnus-data-list nil)))
820       (mail-header-message-id
821        (gnus-data-header (assoc article (gnus-data-list nil))))
822     nil))
823
824 \f
825 ;;;; Spam determination.
826
827 (defvar spam-list-of-checks
828   '((spam-use-blacklist          . spam-check-blacklist)
829     (spam-use-regex-headers      . spam-check-regex-headers)
830     (spam-use-regex-body         . spam-check-regex-body)
831     (spam-use-whitelist          . spam-check-whitelist)
832     (spam-use-BBDB               . spam-check-BBDB)
833     (spam-use-ifile              . spam-check-ifile)
834     (spam-use-spamoracle         . spam-check-spamoracle)
835     (spam-use-stat               . spam-check-stat)
836     (spam-use-blackholes         . spam-check-blackholes)
837     (spam-use-hashcash           . spam-check-hashcash)
838     (spam-use-bogofilter-headers . spam-check-bogofilter-headers)
839     (spam-use-bogofilter         . spam-check-bogofilter))
840   "The spam-list-of-checks list contains pairs associating a
841 parameter variable with a spam checking function.  If the
842 parameter variable is true, then the checking function is called,
843 and its value decides what happens.  Each individual check may
844 return nil, t, or a mailgroup name.  The value nil means that the
845 check does not yield a decision, and so, that further checks are
846 needed.  The value t means that the message is definitely not
847 spam, and that further spam checks should be inhibited.
848 Otherwise, a mailgroup name or the symbol 'spam (depending on
849 spam-split-symbolic-return) is returned where the mail should go,
850 and further checks are also inhibited.  The usual mailgroup name
851 is the value of `spam-split-group', meaning that the message is
852 definitely a spam.")
853
854 (defvar spam-list-of-statistical-checks
855   '(spam-use-ifile
856     spam-use-regex-body
857     spam-use-stat
858     spam-use-bogofilter
859     spam-use-spamoracle)
860   "The spam-list-of-statistical-checks list contains all the mail
861 splitters that need to have the full message body available.")
862
863 ;;;TODO: modify to invoke self with each check if invoked without specifics
864 (defun spam-split (&rest specific-checks)
865   "Split this message into the `spam' group if it is spam.
866 This function can be used as an entry in the variable `nnmail-split-fancy',
867 for example like this: (: spam-split).  It can take checks as
868 parameters.  A string as a parameter will set the
869 spam-split-group to that string.
870
871 See the Info node `(gnus)Fancy Mail Splitting' for more details."
872   (interactive)
873   (setq spam-split-last-successful-check nil)
874   (unless spam-split-disabled
875     (let ((spam-split-group-choice spam-split-group))
876       (dolist (check specific-checks)
877         (when (stringp check)
878           (setq spam-split-group-choice check)
879           (setq specific-checks (delq check specific-checks))))
880
881       (let ((spam-split-group spam-split-group-choice))
882         (save-excursion
883           (save-restriction
884             (dolist (check spam-list-of-statistical-checks)
885               (when (and (symbolp check) (symbol-value check))
886                 (widen)
887                 (gnus-message 8 "spam-split: widening the buffer (%s requires it)"
888                               (symbol-name check))
889                 (return)))
890             ;;   (progn (widen) (debug (buffer-string)))
891             (let ((list-of-checks spam-list-of-checks)
892                   decision)
893               (while (and list-of-checks (not decision))
894                 (let ((pair (pop list-of-checks)))
895                   (when (and (symbol-value (car pair))
896                              (or (null specific-checks)
897                                  (memq (car pair) specific-checks)))
898                     (gnus-message 5 "spam-split: calling the %s function"
899                                   (symbol-name (cdr pair)))
900                     (setq decision (funcall (cdr pair)))
901                     ;; if we got a decision at all, save the current check
902                     (when decision
903                       (setq spam-split-last-successful-check (car pair)))
904
905                     (when (eq decision 'spam)
906                       (if spam-split-symbolic-return
907                           (setq decision spam-split-group)
908                         (gnus-error
909                          5
910                          (format "spam-split got %s but %s is nil"
911                                  (symbol-name decision)
912                                  (symbol-name spam-split-symbolic-return))))))))
913               (if (eq decision t)
914                   (if spam-split-symbolic-return-positive 'ham nil)
915                 decision))))))))
916
917 (defun spam-find-spam ()
918   "This function will detect spam in the current newsgroup using spam-split."
919   (interactive)
920
921   (let* ((group gnus-newsgroup-name)
922          (autodetect (gnus-parameter-spam-autodetect group))
923          (methods (gnus-parameter-spam-autodetect-methods group))
924          (first-method (nth 0 methods)))
925   (when (and autodetect
926              (not (equal first-method 'none)))
927     (mapcar
928      (lambda (article)
929        (let ((id (spam-fetch-field-message-id-fast article))
930              (subject (spam-fetch-field-subject-fast article))
931              (sender (spam-fetch-field-from-fast article)))
932          (unless (and spam-log-to-registry
933                       (spam-log-registered-p id 'incoming))
934            (let* ((spam-split-symbolic-return t)
935                   (spam-split-symbolic-return-positive t)
936                   (split-return
937                    (with-temp-buffer
938                      (gnus-request-article-this-buffer
939                       article
940                       group)
941                      (if (or (null first-method)
942                              (equal first-method 'default))
943                          (spam-split)
944                        (apply 'spam-split methods)))))
945              (if (equal split-return 'spam)
946                  (gnus-summary-mark-article article gnus-spam-mark))
947
948              (when (and split-return spam-log-to-registry)
949                (when (zerop (gnus-registry-group-count id))
950                  (gnus-registry-add-group
951                   id group subject sender))
952
953                (spam-log-processing-to-registry
954                 id
955                 'incoming
956                 split-return
957                 spam-split-last-successful-check
958                 group))))))
959      (if spam-autodetect-recheck-messages
960          gnus-newsgroup-articles
961        gnus-newsgroup-unseen)))))
962
963 (defvar spam-registration-functions
964   ;; first the ham register, second the spam register function
965   ;; third the ham unregister, fourth the spam unregister function
966   '((spam-use-blacklist  nil
967                          spam-blacklist-register-routine
968                          nil
969                          spam-blacklist-unregister-routine)
970     (spam-use-whitelist  spam-whitelist-register-routine
971                          nil
972                          spam-whitelist-unregister-routine
973                          nil)
974     (spam-use-BBDB       spam-BBDB-register-routine
975                          nil
976                          spam-BBDB-unregister-routine
977                          nil)
978     (spam-use-ifile      spam-ifile-register-ham-routine
979                          spam-ifile-register-spam-routine
980                          spam-ifile-unregister-ham-routine
981                          spam-ifile-unregister-spam-routine)
982     (spam-use-spamoracle spam-spamoracle-learn-ham
983                          spam-spamoracle-learn-spam
984                          spam-spamoracle-unlearn-ham
985                          spam-spamoracle-unlearn-spam)
986     (spam-use-stat       spam-stat-register-ham-routine
987                          spam-stat-register-spam-routine
988                          spam-stat-unregister-ham-routine
989                          spam-stat-unregister-spam-routine)
990     ;; note that spam-use-gmane is not a legitimate check
991     (spam-use-gmane      nil
992                          spam-report-gmane-register-routine
993                          ;; does Gmane support unregistration?
994                          nil
995                          nil)
996     (spam-use-bogofilter spam-bogofilter-register-ham-routine
997                          spam-bogofilter-register-spam-routine
998                          spam-bogofilter-unregister-ham-routine
999                          spam-bogofilter-unregister-spam-routine))
1000   "The spam-registration-functions list contains pairs
1001 associating a parameter variable with the ham and spam
1002 registration functions, and the ham and spam unregistration
1003 functions")
1004
1005 (defun spam-classification-valid-p (classification)
1006   (or  (eq classification 'spam)
1007        (eq classification 'ham)))
1008
1009 (defun spam-process-type-valid-p (process-type)
1010   (or  (eq process-type 'incoming)
1011        (eq process-type 'process)))
1012
1013 (defun spam-registration-check-valid-p (check)
1014   (assoc check spam-registration-functions))
1015
1016 (defun spam-unregistration-check-valid-p (check)
1017   (assoc check spam-registration-functions))
1018
1019 (defun spam-registration-function (classification check)
1020   (let ((flist (cdr-safe (assoc check spam-registration-functions))))
1021     (if (eq classification 'spam)
1022         (nth 1 flist)
1023       (nth 0 flist))))
1024
1025 (defun spam-unregistration-function (classification check)
1026   (let ((flist (cdr-safe (assoc check spam-registration-functions))))
1027     (if (eq classification 'spam)
1028         (nth 3 flist)
1029       (nth 2 flist))))
1030
1031 (defun spam-list-articles (articles classification)
1032   (let ((mark-check (if (eq classification 'spam)
1033                         'spam-group-spam-mark-p
1034                       'spam-group-ham-mark-p))
1035         list mark-cache-yes mark-cache-no)
1036     (dolist (article articles)
1037       (let ((mark (gnus-summary-article-mark article)))
1038         (unless (memq mark mark-cache-no)
1039           (if (memq mark mark-cache-yes)
1040               (push article list)
1041             ;; else, we have to actually check the mark
1042             (if (funcall mark-check
1043                          gnus-newsgroup-name
1044                          mark)
1045                 (progn
1046                   (push article list)
1047                   (push mark mark-cache-yes))
1048               (push mark mark-cache-no))))))
1049     list))
1050
1051 (defun spam-register-routine (classification
1052                               check
1053                               &optional unregister
1054                               specific-articles)
1055   (when (and (spam-classification-valid-p classification)
1056              (spam-registration-check-valid-p check))
1057     (let* ((register-function
1058             (spam-registration-function classification check))
1059            (unregister-function
1060             (spam-unregistration-function classification check))
1061            (run-function (if unregister
1062                              unregister-function
1063                            register-function))
1064            (log-function (if unregister
1065                              'spam-log-undo-registration
1066                            'spam-log-processing-to-registry))
1067            article articles)
1068
1069       (when run-function
1070         ;; make list of articles, using specific-articles if given
1071         (setq articles (or specific-articles
1072                            (spam-list-articles
1073                             gnus-newsgroup-articles
1074                             classification)))
1075         ;; process them
1076         (gnus-message 5 "%s %d %s articles with classification %s, check %s"
1077                       (if unregister "Unregistering" "Registering")
1078                       (length articles)
1079                       (if specific-articles "specific" "")
1080                       (symbol-name classification)
1081                       (symbol-name check))
1082         (funcall run-function articles)
1083         ;; now log all the registrations (or undo them, depending on unregister)
1084         (dolist (article articles)
1085           (funcall log-function
1086                    (spam-fetch-field-message-id-fast article)
1087                    'process
1088                    classification
1089                    check
1090                    gnus-newsgroup-name))))))
1091
1092 ;;; log a ham- or spam-processor invocation to the registry
1093 (defun spam-log-processing-to-registry (id type classification check group)
1094   (when spam-log-to-registry
1095     (if (and (stringp id)
1096              (stringp group)
1097              (spam-process-type-valid-p type)
1098              (spam-classification-valid-p classification)
1099              (spam-registration-check-valid-p check))
1100         (let ((cell-list (cdr-safe (gnus-registry-fetch-extra id type)))
1101               (cell (list classification check group)))
1102           (push cell cell-list)
1103           (gnus-registry-store-extra-entry
1104            id
1105            type
1106            cell-list))
1107
1108       (gnus-message 5 (format "%s called with bad ID, type, classification, check, or group"
1109                               "spam-log-processing-to-registry")))))
1110
1111 ;;; check if a ham- or spam-processor registration has been done
1112 (defun spam-log-registered-p (id type)
1113   (when spam-log-to-registry
1114     (if (and (stringp id)
1115              (spam-process-type-valid-p type))
1116         (cdr-safe (gnus-registry-fetch-extra id type))
1117       (progn
1118         (gnus-message 5 (format "%s called with bad ID, type, classification, or check"
1119                                 "spam-log-registered-p"))
1120         nil))))
1121
1122 ;;; check if a ham- or spam-processor registration needs to be undone
1123 (defun spam-log-unregistration-needed-p (id type classification check)
1124   (when spam-log-to-registry
1125     (if (and (stringp id)
1126              (spam-process-type-valid-p type)
1127              (spam-classification-valid-p classification)
1128              (spam-registration-check-valid-p check))
1129         (let ((cell-list (cdr-safe (gnus-registry-fetch-extra id type)))
1130               found)
1131           (dolist (cell cell-list)
1132             (unless found
1133               (when (and (eq classification (nth 0 cell))
1134                          (eq check (nth 1 cell)))
1135                 (setq found t))))
1136           found)
1137       (progn
1138         (gnus-message 5 (format "%s called with bad ID, type, classification, or check"
1139                                 "spam-log-unregistration-needed-p"))
1140         nil))))
1141
1142
1143 ;;; undo a ham- or spam-processor registration (the group is not used)
1144 (defun spam-log-undo-registration (id type classification check &optional group)
1145   (when (and spam-log-to-registry
1146              (spam-log-unregistration-needed-p id type classification check))
1147     (if (and (stringp id)
1148              (spam-process-type-valid-p type)
1149              (spam-classification-valid-p classification)
1150              (spam-registration-check-valid-p check))
1151         (let ((cell-list (cdr-safe (gnus-registry-fetch-extra id type)))
1152               new-cell-list found)
1153           (dolist (cell cell-list)
1154             (unless (and (eq classification (nth 0 cell))
1155                          (eq check (nth 1 cell)))
1156               (push cell new-cell-list)))
1157           (gnus-registry-store-extra-entry
1158            id
1159            type
1160            new-cell-list))
1161       (progn
1162         (gnus-message 5 (format "%s called with bad ID, type, check, or group"
1163                                 "spam-log-undo-registration"))
1164         nil))))
1165
1166 ;;; set up IMAP widening if it's necessary
1167 (defun spam-setup-widening ()
1168   (dolist (check spam-list-of-statistical-checks)
1169     (when (symbol-value check)
1170       (setq nnimap-split-download-body-default t))))
1171
1172 \f
1173 ;;;; Regex body
1174
1175 (defun spam-check-regex-body ()
1176   (let ((spam-regex-headers-ham spam-regex-body-ham)
1177         (spam-regex-headers-spam spam-regex-body-spam))
1178     (spam-check-regex-headers t)))
1179
1180 \f
1181 ;;;; Regex headers
1182
1183 (defun spam-check-regex-headers (&optional body)
1184   (let ((type (if body "body" "header"))
1185         (spam-split-group (if spam-split-symbolic-return
1186                               'spam
1187                             spam-split-group))
1188         ret found)
1189     (dolist (h-regex spam-regex-headers-ham)
1190       (unless found
1191         (goto-char (point-min))
1192         (when (re-search-forward h-regex nil t)
1193           (message "Ham regex %s search positive." type)
1194           (setq found t))))
1195     (dolist (s-regex spam-regex-headers-spam)
1196       (unless found
1197         (goto-char (point-min))
1198         (when (re-search-forward s-regex nil t)
1199           (message "Spam regex %s search positive." type)
1200           (setq found t)
1201           (setq ret spam-split-group))))
1202     ret))
1203
1204 \f
1205 ;;;; Blackholes.
1206
1207 (defun spam-reverse-ip-string (ip)
1208   (when (stringp ip)
1209     (mapconcat 'identity
1210                (nreverse (split-string ip "\\."))
1211                ".")))
1212
1213 (defun spam-check-blackholes ()
1214   "Check the Received headers for blackholed relays."
1215   (let ((headers (nnmail-fetch-field "received"))
1216         (spam-split-group (if spam-split-symbolic-return
1217                               'spam
1218                             spam-split-group))
1219         ips matches)
1220     (when headers
1221       (with-temp-buffer
1222         (insert headers)
1223         (goto-char (point-min))
1224         (gnus-message 5 "Checking headers for relay addresses")
1225         (while (re-search-forward
1226                 "\\([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+\\)" nil t)
1227           (gnus-message 9 "Blackhole search found host IP %s." (match-string 1))
1228           (push (spam-reverse-ip-string (match-string 1))
1229                 ips)))
1230       (dolist (server spam-blackhole-servers)
1231         (dolist (ip ips)
1232           (unless (and spam-blackhole-good-server-regex
1233                        ;; match the good-server-regex against the reversed (again) IP string
1234                        (string-match
1235                         spam-blackhole-good-server-regex
1236                         (spam-reverse-ip-string ip)))
1237             (unless matches
1238               (let ((query-string (concat ip "." server)))
1239                 (if spam-use-dig
1240                     (let ((query-result (query-dig query-string)))
1241                       (when query-result
1242                         (gnus-message 5 "(DIG): positive blackhole check '%s'"
1243                                       query-result)
1244                         (push (list ip server query-result)
1245                               matches)))
1246                   ;; else, if not using dig.el
1247                   (when (query-dns query-string)
1248                     (gnus-message 5 "positive blackhole check")
1249                     (push (list ip server (query-dns query-string 'TXT))
1250                           matches)))))))))
1251     (when matches
1252       spam-split-group)))
1253 \f
1254 ;;;; Hashcash.
1255
1256 (condition-case nil
1257     (progn
1258       (require 'hashcash)
1259
1260       (defun spam-check-hashcash ()
1261         "Check the headers for hashcash payments."
1262         (mail-check-payment)))   ;mail-check-payment returns a boolean
1263
1264   (file-error (progn
1265                 (defalias 'mail-check-payment 'ignore)
1266                 (defalias 'spam-check-hashcash 'ignore))))
1267 \f
1268 ;;;; BBDB
1269
1270 ;;; original idea for spam-check-BBDB from Alexander Kotelnikov
1271 ;;; <sacha@giotto.sj.ru>
1272
1273 ;; all this is done inside a condition-case to trap errors
1274
1275 (condition-case nil
1276     (progn
1277       (require 'bbdb)
1278       (require 'bbdb-com)
1279
1280       (defun spam-enter-ham-BBDB (addresses &optional remove)
1281         "Enter an address into the BBDB; implies ham (non-spam) sender"
1282         (dolist (from addresses)
1283           (when (stringp from)
1284             (let* ((parsed-address (gnus-extract-address-components from))
1285                    (name (or (nth 0 parsed-address) "Ham Sender"))
1286                    (remove-function (if remove
1287                                         'bbdb-delete-record-internal
1288                                       'ignore))
1289                    (net-address (nth 1 parsed-address))
1290                    (record (and net-address
1291                                 (bbdb-search-simple nil net-address))))
1292               (when net-address
1293                 (gnus-message 5 "%s address %s %s BBDB"
1294                               (if remove "Deleting" "Adding")
1295                               from
1296                               (if remove "from" "to"))
1297                 (if record
1298                     (funcall remove-function record)
1299                   (bbdb-create-internal name nil net-address nil nil
1300                                         "ham sender added by spam.el")))))))
1301
1302       (defun spam-BBDB-register-routine (articles &optional unregister)
1303         (let (addresses)
1304           (dolist (article articles)
1305             (when (stringp (spam-fetch-field-from-fast article))
1306               (push (spam-fetch-field-from-fast article) addresses)))
1307           ;; now do the register/unregister action
1308           (spam-enter-ham-BBDB addresses unregister)))
1309
1310       (defun spam-BBDB-unregister-routine (articles)
1311         (spam-BBDB-register-routine articles t))
1312
1313       (defun spam-check-BBDB ()
1314         "Mail from people in the BBDB is classified as ham or non-spam"
1315         (let ((who (nnmail-fetch-field "from"))
1316               (spam-split-group (if spam-split-symbolic-return
1317                                     'spam
1318                                   spam-split-group)))
1319           (when who
1320             (setq who (nth 1 (gnus-extract-address-components who)))
1321             (if (bbdb-search-simple nil who)
1322                 t
1323               (if spam-use-BBDB-exclusive
1324                   spam-split-group
1325                 nil))))))
1326
1327   (file-error (progn
1328                 (defalias 'bbdb-search-simple 'ignore)
1329                 (defalias 'spam-check-BBDB 'ignore)
1330                 (defalias 'spam-BBDB-register-routine 'ignore)
1331                 (defalias 'spam-enter-ham-BBDB 'ignore)
1332                 (defalias 'bbdb-create-internal 'ignore)
1333                 (defalias 'bbdb-delete-record-internal 'ignore)
1334                 (defalias 'bbdb-records 'ignore))))
1335
1336 \f
1337 ;;;; ifile
1338
1339 ;;; check the ifile backend; return nil if the mail was NOT classified
1340 ;;; as spam
1341
1342 (defun spam-get-ifile-database-parameter ()
1343   "Get the command-line parameter for ifile's database from
1344   spam-ifile-database-path."
1345   (if spam-ifile-database-path
1346       (format "--db-file=%s" spam-ifile-database-path)
1347     nil))
1348
1349 (defun spam-check-ifile ()
1350   "Check the ifile backend for the classification of this message."
1351   (let ((article-buffer-name (buffer-name))
1352         (spam-split-group (if spam-split-symbolic-return
1353                               'spam
1354                             spam-split-group))
1355         category return)
1356     (with-temp-buffer
1357       (let ((temp-buffer-name (buffer-name))
1358             (db-param (spam-get-ifile-database-parameter)))
1359         (save-excursion
1360           (set-buffer article-buffer-name)
1361           (apply 'call-process-region
1362                  (point-min) (point-max) spam-ifile-path
1363                  nil temp-buffer-name nil "-c"
1364                  (if db-param `(,db-param "-q") `("-q"))))
1365         ;; check the return now (we're back in the temp buffer)
1366         (goto-char (point-min))
1367         (if (not (eobp))
1368             (setq category (buffer-substring (point) (spam-point-at-eol))))
1369         (when (not (zerop (length category))) ; we need a category here
1370           (if spam-ifile-all-categories
1371               (setq return category)
1372             ;; else, if spam-ifile-all-categories is not set...
1373             (when (string-equal spam-ifile-spam-category category)
1374               (setq return spam-split-group)))))) ; note return is nil otherwise
1375     return))
1376
1377 (defun spam-ifile-register-with-ifile (articles category &optional unregister)
1378   "Register an article, given as a string, with a category.
1379 Uses `gnus-newsgroup-name' if category is nil (for ham registration)."
1380   (let ((category (or category gnus-newsgroup-name))
1381         (add-or-delete-option (if unregister "-d" "-i"))
1382         (db (spam-get-ifile-database-parameter))
1383         parameters)
1384     (with-temp-buffer
1385       (dolist (article articles)
1386         (let ((article-string (spam-get-article-as-string article)))
1387           (when (stringp article-string)
1388             (insert article-string))))
1389       (apply 'call-process-region
1390              (point-min) (point-max) spam-ifile-path
1391              nil nil nil
1392              add-or-delete-option category
1393              (if db `(,db "-h") `("-h"))))))
1394
1395 (defun spam-ifile-register-spam-routine (articles &optional unregister)
1396   (spam-ifile-register-with-ifile articles spam-ifile-spam-category unregister))
1397
1398 (defun spam-ifile-unregister-spam-routine (articles)
1399   (spam-ifile-register-spam-routine articles t))
1400
1401 (defun spam-ifile-register-ham-routine (articles &optional unregister)
1402   (spam-ifile-register-with-ifile articles spam-ifile-ham-category unregister))
1403
1404 (defun spam-ifile-unregister-ham-routine (articles)
1405   (spam-ifile-register-ham-routine articles t))
1406
1407 \f
1408 ;;;; spam-stat
1409
1410 (condition-case nil
1411     (progn
1412       (let ((spam-stat-install-hooks nil))
1413         (require 'spam-stat))
1414
1415       (defun spam-check-stat ()
1416         "Check the spam-stat backend for the classification of this message"
1417         (let ((spam-split-group (if spam-split-symbolic-return
1418                                     'spam
1419                                   spam-split-group))
1420               (spam-stat-split-fancy-spam-group spam-split-group) ; override
1421               (spam-stat-buffer (buffer-name)) ; stat the current buffer
1422               category return)
1423           (spam-stat-split-fancy)))
1424
1425       (defun spam-stat-register-spam-routine (articles &optional unregister)
1426         (dolist (article articles)
1427           (let ((article-string (spam-get-article-as-string article)))
1428             (with-temp-buffer
1429               (insert article-string)
1430               (if unregister
1431                   (spam-stat-buffer-change-to-non-spam)
1432               (spam-stat-buffer-is-spam))))))
1433
1434       (defun spam-stat-unregister-spam-routine (articles)
1435         (spam-stat-register-spam-routine articles t))
1436
1437       (defun spam-stat-register-ham-routine (articles &optional unregister)
1438         (dolist (article articles)
1439           (let ((article-string (spam-get-article-as-string article)))
1440             (with-temp-buffer
1441               (insert article-string)
1442               (if unregister
1443                   (spam-stat-buffer-change-to-spam)
1444               (spam-stat-buffer-is-non-spam))))))
1445
1446       (defun spam-stat-unregister-ham-routine (articles)
1447         (spam-stat-register-ham-routine articles t))
1448
1449       (defun spam-maybe-spam-stat-load ()
1450         (when spam-use-stat (spam-stat-load)))
1451
1452       (defun spam-maybe-spam-stat-save ()
1453         (when spam-use-stat (spam-stat-save))))
1454
1455   (file-error (progn
1456                 (defalias 'spam-stat-load 'ignore)
1457                 (defalias 'spam-stat-save 'ignore)
1458                 (defalias 'spam-maybe-spam-stat-load 'ignore)
1459                 (defalias 'spam-maybe-spam-stat-save 'ignore)
1460                 (defalias 'spam-stat-register-ham-routine 'ignore)
1461                 (defalias 'spam-stat-unregister-ham-routine 'ignore)
1462                 (defalias 'spam-stat-register-spam-routine 'ignore)
1463                 (defalias 'spam-stat-unregister-spam-routine 'ignore)
1464                 (defalias 'spam-stat-buffer-is-spam 'ignore)
1465                 (defalias 'spam-stat-buffer-change-to-spam 'ignore)
1466                 (defalias 'spam-stat-buffer-is-non-spam 'ignore)
1467                 (defalias 'spam-stat-buffer-change-to-non-spam 'ignore)
1468                 (defalias 'spam-stat-split-fancy 'ignore)
1469                 (defalias 'spam-check-stat 'ignore))))
1470
1471 \f
1472
1473 ;;;; Blacklists and whitelists.
1474
1475 (defvar spam-whitelist-cache nil)
1476 (defvar spam-blacklist-cache nil)
1477
1478 (defun spam-kill-whole-line ()
1479   (beginning-of-line)
1480   (let ((kill-whole-line t))
1481     (kill-line)))
1482
1483 ;;; address can be a list, too
1484 (defun spam-enter-whitelist (address &optional remove)
1485   "Enter ADDRESS (list or single) into the whitelist.
1486 With a non-nil REMOVE, remove them."
1487   (interactive "sAddress: ")
1488   (spam-enter-list address spam-whitelist remove)
1489   (setq spam-whitelist-cache nil))
1490
1491 ;;; address can be a list, too
1492 (defun spam-enter-blacklist (address &optional remove)
1493   "Enter ADDRESS (list or single) into the blacklist.
1494 With a non-nil REMOVE, remove them."
1495   (interactive "sAddress: ")
1496   (spam-enter-list address spam-blacklist remove)
1497   (setq spam-blacklist-cache nil))
1498
1499 (defun spam-enter-list (addresses file &optional remove)
1500   "Enter ADDRESSES into the given FILE.
1501 Either the whitelist or the blacklist files can be used.  With
1502 REMOVE not nil, remove the ADDRESSES."
1503   (if (stringp addresses)
1504       (spam-enter-list (list addresses) file remove)
1505     ;; else, we have a list of addresses here
1506     (unless (file-exists-p (file-name-directory file))
1507       (make-directory (file-name-directory file) t))
1508     (save-excursion
1509       (set-buffer
1510        (find-file-noselect file))
1511       (dolist (a addresses)
1512         (when (stringp a)
1513           (goto-char (point-min))
1514           (if (re-search-forward (regexp-quote a) nil t)
1515               ;; found the address
1516               (when remove
1517                 (spam-kill-whole-line))
1518             ;; else, the address was not found
1519             (unless remove
1520               (goto-char (point-max))
1521               (unless (bobp)
1522                 (insert "\n"))
1523               (insert a "\n")))))
1524       (save-buffer))))
1525
1526 ;;; returns t if the sender is in the whitelist, nil or
1527 ;;; spam-split-group otherwise
1528 (defun spam-check-whitelist ()
1529   ;; FIXME!  Should it detect when file timestamps change?
1530   (let ((spam-split-group (if spam-split-symbolic-return
1531                               'spam
1532                             spam-split-group)))
1533     (unless spam-whitelist-cache
1534       (setq spam-whitelist-cache (spam-parse-list spam-whitelist)))
1535     (if (spam-from-listed-p spam-whitelist-cache)
1536         t
1537       (if spam-use-whitelist-exclusive
1538           spam-split-group
1539         nil))))
1540
1541 (defun spam-check-blacklist ()
1542   ;; FIXME!  Should it detect when file timestamps change?
1543   (let ((spam-split-group (if spam-split-symbolic-return
1544                               'spam
1545                             spam-split-group)))
1546     (unless spam-blacklist-cache
1547       (setq spam-blacklist-cache (spam-parse-list spam-blacklist)))
1548     (and (spam-from-listed-p spam-blacklist-cache) spam-split-group)))
1549
1550 (defun spam-parse-list (file)
1551   (when (file-readable-p file)
1552     (let (contents address)
1553       (with-temp-buffer
1554         (insert-file-contents file)
1555         (while (not (eobp))
1556           (setq address (buffer-substring (point) (spam-point-at-eol)))
1557           (forward-line 1)
1558           ;; insert the e-mail address if detected, otherwise the raw data
1559           (unless (zerop (length address))
1560             (let ((pure-address (nth 1 (gnus-extract-address-components address))))
1561               (push (or pure-address address) contents)))))
1562       (nreverse contents))))
1563
1564 (defun spam-from-listed-p (cache)
1565   (let ((from (nnmail-fetch-field "from"))
1566         found)
1567     (while cache
1568       (let ((address (pop cache)))
1569         (unless (zerop (length address)) ; 0 for a nil address too
1570           (setq address (regexp-quote address))
1571           ;; fix regexp-quote's treatment of user-intended regexes
1572           (while (string-match "\\\\\\*" address)
1573             (setq address (replace-match ".*" t t address))))
1574         (when (and address (string-match address from))
1575           (setq found t
1576                 cache nil))))
1577     found))
1578
1579 (defun spam-filelist-register-routine (articles blacklist &optional unregister)
1580   (let ((de-symbol (if blacklist 'spam-use-whitelist 'spam-use-blacklist))
1581         (declassification (if blacklist 'ham 'spam))
1582         (enter-function
1583          (if blacklist 'spam-enter-blacklist 'spam-enter-whitelist))
1584         (remove-function
1585          (if blacklist 'spam-enter-whitelist 'spam-enter-blacklist))
1586         from addresses unregister-list)
1587     (dolist (article articles)
1588       (let ((from (spam-fetch-field-from-fast article))
1589             (id (spam-fetch-field-message-id-fast article))
1590             sender-ignored)
1591         (when (stringp from)
1592           (dolist (ignore-regex spam-blacklist-ignored-regexes)
1593             (when (and (not sender-ignored)
1594                        (stringp ignore-regex)
1595                        (string-match ignore-regex from))
1596               (setq sender-ignored t)))
1597           ;; remember the messages we need to unregister, unless remove is set
1598           (when (and
1599                  (null unregister)
1600                  (spam-log-unregistration-needed-p
1601                   id 'process declassification de-symbol))
1602             (push from unregister-list))
1603           (unless sender-ignored
1604             (push from addresses)))))
1605
1606     (if unregister
1607         (funcall enter-function addresses t) ; unregister all these addresses
1608       ;; else, register normally and unregister what we need to
1609       (funcall remove-function unregister-list t)
1610       (dolist (article unregister-list)
1611         (spam-log-undo-registration
1612          (spam-fetch-field-message-id-fast article)
1613          'process
1614          declassification
1615          de-symbol))
1616       (funcall enter-function addresses nil))))
1617
1618 (defun spam-blacklist-unregister-routine (articles)
1619   (spam-blacklist-register-routine articles t))
1620
1621 (defun spam-blacklist-register-routine (articles &optional unregister)
1622   (spam-filelist-register-routine articles t unregister))
1623
1624 (defun spam-whitelist-unregister-routine (articles)
1625   (spam-whitelist-register-routine articles t))
1626
1627 (defun spam-whitelist-register-routine (articles &optional unregister)
1628   (spam-filelist-register-routine articles nil unregister))
1629
1630 \f
1631 ;;;; Spam-report glue
1632 (defun spam-report-gmane-register-routine (articles)
1633   (when articles
1634     (apply 'spam-report-gmane articles)))
1635
1636 \f
1637 ;;;; Bogofilter
1638 (defun spam-check-bogofilter-headers (&optional score)
1639   (let ((header (nnmail-fetch-field spam-bogofilter-header))
1640         (spam-split-group (if spam-split-symbolic-return
1641                               'spam
1642                             spam-split-group)))
1643     (when header                        ; return nil when no header
1644       (if score                         ; scoring mode
1645           (if (string-match "spamicity=\\([0-9.]+\\)" header)
1646               (match-string 1 header)
1647             "0")
1648         ;; spam detection mode
1649         (when (string-match spam-bogofilter-bogosity-positive-spam-header
1650                             header)
1651           spam-split-group)))))
1652
1653 ;; return something sensible if the score can't be determined
1654 (defun spam-bogofilter-score ()
1655   "Get the Bogofilter spamicity score"
1656   (interactive)
1657   (save-window-excursion
1658     (gnus-summary-show-article t)
1659     (set-buffer gnus-article-buffer)
1660     (let ((score (or (spam-check-bogofilter-headers t)
1661                      (spam-check-bogofilter t))))
1662       (message "Spamicity score %s" score)
1663       (or score "0"))
1664     (gnus-summary-show-article)))
1665
1666 (defun spam-check-bogofilter (&optional score)
1667   "Check the Bogofilter backend for the classification of this message"
1668   (let ((article-buffer-name (buffer-name))
1669         (db spam-bogofilter-database-directory)
1670         return)
1671     (with-temp-buffer
1672       (let ((temp-buffer-name (buffer-name)))
1673         (save-excursion
1674           (set-buffer article-buffer-name)
1675           (apply 'call-process-region
1676                  (point-min) (point-max)
1677                  spam-bogofilter-path
1678                  nil temp-buffer-name nil
1679                  (if db `("-d" ,db "-v") `("-v"))))
1680         (setq return (spam-check-bogofilter-headers score))))
1681     return))
1682
1683 (defun spam-bogofilter-register-with-bogofilter (articles
1684                                                  spam
1685                                                  &optional unregister)
1686   "Register an article, given as a string, as spam or non-spam."
1687   (dolist (article articles)
1688     (let ((article-string (spam-get-article-as-string article))
1689           (db spam-bogofilter-database-directory)
1690           (switch (if unregister
1691                       (if spam
1692                           spam-bogofilter-spam-strong-switch
1693                         spam-bogofilter-ham-strong-switch)
1694                     (if spam
1695                         spam-bogofilter-spam-switch
1696                       spam-bogofilter-ham-switch))))
1697       (when (stringp article-string)
1698         (with-temp-buffer
1699           (insert article-string)
1700
1701           (apply 'call-process-region
1702                  (point-min) (point-max)
1703                  spam-bogofilter-path
1704                  nil nil nil switch
1705                  (if db `("-d" ,db "-v") `("-v"))))))))
1706
1707 (defun spam-bogofilter-register-spam-routine (articles &optional unregister)
1708   (spam-bogofilter-register-with-bogofilter articles t unregister))
1709
1710 (defun spam-bogofilter-unregister-spam-routine (articles)
1711   (spam-bogofilter-register-spam-routine articles t))
1712
1713 (defun spam-bogofilter-register-ham-routine (articles &optional unregister)
1714   (spam-bogofilter-register-with-bogofilter articles nil unregister))
1715
1716 (defun spam-bogofilter-unregister-ham-routine (articles)
1717   (spam-bogofilter-register-ham-routine articles t))
1718
1719
1720 \f
1721 ;;;; spamoracle
1722 (defun spam-check-spamoracle ()
1723   "Run spamoracle on an article to determine whether it's spam."
1724   (let ((article-buffer-name (buffer-name))
1725         (spam-split-group (if spam-split-symbolic-return
1726                               'spam
1727                             spam-split-group)))
1728     (with-temp-buffer
1729       (let ((temp-buffer-name (buffer-name)))
1730         (save-excursion
1731           (set-buffer article-buffer-name)
1732           (let ((status
1733                  (apply 'call-process-region
1734                         (point-min) (point-max)
1735                         spam-spamoracle-binary
1736                         nil temp-buffer-name nil
1737                         (if spam-spamoracle-database
1738                             `("-f" ,spam-spamoracle-database "mark")
1739                           '("mark")))))
1740             (if (eq 0 status)
1741                 (progn
1742                   (set-buffer temp-buffer-name)
1743                   (goto-char (point-min))
1744                   (when (re-search-forward "^X-Spam: yes;" nil t)
1745                     spam-split-group))
1746               (error "Error running spamoracle" status))))))))
1747
1748 (defun spam-spamoracle-learn (articles article-is-spam-p &optional unregister)
1749   "Run spamoracle in training mode."
1750   (with-temp-buffer
1751     (let ((temp-buffer-name (buffer-name)))
1752       (save-excursion
1753         (goto-char (point-min))
1754         (dolist (article articles)
1755           (insert (spam-get-article-as-string article)))
1756         (let* ((arg (if (spam-xor unregister article-is-spam-p)
1757                         "-spam"
1758                       "-good"))
1759                (status
1760                 (apply 'call-process-region
1761                        (point-min) (point-max)
1762                        spam-spamoracle-binary
1763                        nil temp-buffer-name nil
1764                        (if spam-spamoracle-database
1765                            `("-f" ,spam-spamoracle-database
1766                              "add" ,arg)
1767                          `("add" ,arg)))))
1768           (when (not (eq 0 status))
1769             (error "Error running spamoracle" status)))))))
1770
1771 (defun spam-spamoracle-learn-ham (articles &optional unregister)
1772   (spam-spamoracle-learn articles nil unregister))
1773
1774 (defun spam-spamoracle-unlearn-ham (articles &optional unregister)
1775   (spam-spamoracle-learn-ham articles t))
1776
1777 (defun spam-spamoracle-learn-spam (articles &optional unregister)
1778   (spam-spamoracle-learn articles t unregister))
1779
1780 (defun spam-spamoracle-unlearn-spam (articles &optional unregister)
1781   (spam-spamoracle-learn-spam articles t))
1782
1783 \f
1784 ;;;; Hooks
1785
1786 ;;;###autoload
1787 (defun spam-initialize ()
1788   "Install the spam.el hooks and do other initialization"
1789   (interactive)
1790   (setq spam-install-hooks t)
1791   ;; TODO: How do we redo this every time spam-face is customized?
1792   (push '((eq mark gnus-spam-mark) . spam-face)
1793         gnus-summary-highlight)
1794   ;; Add hooks for loading and saving the spam stats
1795   (add-hook 'gnus-save-newsrc-hook 'spam-maybe-spam-stat-save)
1796   (add-hook 'gnus-get-top-new-news-hook 'spam-maybe-spam-stat-load)
1797   (add-hook 'gnus-startup-hook 'spam-maybe-spam-stat-load)
1798   (add-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
1799   (add-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
1800   (add-hook 'gnus-get-new-news-hook 'spam-setup-widening)
1801   (add-hook 'gnus-summary-prepare-hook 'spam-find-spam))
1802
1803 (defun spam-unload-hook ()
1804   "Uninstall the spam.el hooks"
1805   (interactive)
1806   (remove-hook 'gnus-save-newsrc-hook 'spam-maybe-spam-stat-save)
1807   (remove-hook 'gnus-get-top-new-news-hook 'spam-maybe-spam-stat-load)
1808   (remove-hook 'gnus-startup-hook 'spam-maybe-spam-stat-load)
1809   (remove-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
1810   (remove-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
1811   (remove-hook 'gnus-get-new-news-hook 'spam-setup-widening)
1812   (remove-hook 'gnus-summary-prepare-hook 'spam-find-spam))
1813
1814 (when spam-install-hooks
1815   (spam-initialize))
1816
1817 (provide 'spam)
1818
1819 ;;; spam.el ends here.
1820
1821 (provide 'spam)
1822
1823 ;;; spam.el ends here