2bc099ee6008dbc4b447c5f242a955bab508bf36
[elisp/gnus.git-] / lisp / spam.el
1 ;;; spam.el --- Identifying spam
2 ;; Copyright (C) 2002, 2003 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 ;;; Code:
36
37 (require 'path-util)
38
39 (require 'gnus-sum)
40
41 (require 'gnus-uu)                      ; because of key prefix issues
42 (require 'gnus) ; for the definitions of group content classification and spam processors
43 (require 'message)                      ;for the message-fetch-field functions
44
45 ;; for nnimap-split-download-body-default
46 (eval-when-compile (require 'nnimap))
47
48 ;; autoload query-dig
49 (eval-and-compile
50   (autoload 'query-dig "dig"))
51
52 ;; autoload spam-report
53 (eval-and-compile
54   (autoload 'spam-report-gmane "spam-report"))
55
56 ;; autoload query-dns
57 (eval-and-compile
58   (autoload 'query-dns "dns"))
59
60 ;;; Main parameters.
61
62 (defgroup spam nil
63   "Spam configuration.")
64
65 (defcustom spam-directory "~/News/spam/"
66   "Directory for spam whitelists and blacklists."
67   :type 'directory
68   :group 'spam)
69
70 (defcustom spam-move-spam-nonspam-groups-only t
71   "Whether spam should be moved in non-spam groups only.
72 When nil, only ham and unclassified groups will have their spam moved
73 to the spam-process-destination.  When t, spam will also be moved from
74 spam groups."
75   :type 'boolean
76   :group 'spam)
77
78 (defcustom spam-mark-only-unseen-as-spam t
79   "Whether only unseen articles should be marked as spam in spam
80 groups.  When nil, all unread articles in a spam group are marked as
81 spam.  Set this if you want to leave an article unread in a spam group
82 without losing it to the automatic spam-marking process."
83   :type 'boolean
84   :group 'spam)
85
86 (defcustom spam-mark-ham-unread-before-move-from-spam-group nil
87   "Whether ham should be marked unread before it's moved out of a spam
88 group according to ham-process-destination.  This variable is an
89 official entry in the international Longest Variable Name
90 Competition."
91   :type 'boolean
92   :group 'spam)
93
94 (defcustom spam-whitelist (expand-file-name "whitelist" spam-directory)
95   "The location of the whitelist.
96 The file format is one regular expression per line.
97 The regular expression is matched against the address."
98   :type 'file
99   :group 'spam)
100
101 (defcustom spam-blacklist (expand-file-name "blacklist" spam-directory)
102   "The location of the blacklist.
103 The file format is one regular expression per line.
104 The regular expression is matched against the address."
105   :type 'file
106   :group 'spam)
107
108 (defcustom spam-use-dig t
109   "Whether query-dig should be used instead of query-dns."
110   :type 'boolean
111   :group 'spam)
112
113 (defcustom spam-use-blacklist nil
114   "Whether the blacklist should be used by spam-split."
115   :type 'boolean
116   :group 'spam)
117
118 (defcustom spam-use-whitelist nil
119   "Whether the whitelist should be used by spam-split."
120   :type 'boolean
121   :group 'spam)
122
123 (defcustom spam-use-whitelist-exclusive nil
124   "Whether whitelist-exclusive should be used by spam-split.
125 Exclusive whitelisting means that all messages from senders not in the whitelist
126 are considered spam."
127   :type 'boolean
128   :group 'spam)
129
130 (defcustom spam-use-blackholes nil
131   "Whether blackholes should be used by spam-split."
132   :type 'boolean
133   :group 'spam)
134
135 (defcustom spam-use-hashcash nil
136   "Whether hashcash payments should be detected by spam-split."
137   :type 'boolean
138   :group 'spam)
139
140 (defcustom spam-use-regex-headers nil
141   "Whether a header regular expression match should be used by spam-split.
142 Also see the variable `spam-spam-regex-headers' and `spam-ham-regex-headers'."
143   :type 'boolean
144   :group 'spam)
145
146 (defcustom spam-use-bogofilter-headers nil
147   "Whether bogofilter headers should be used by spam-split.
148 Enable this if you pre-process messages with Bogofilter BEFORE Gnus sees them."
149   :type 'boolean
150   :group 'spam)
151
152 (defcustom spam-use-bogofilter nil
153   "Whether bogofilter should be invoked by spam-split.
154 Enable this if you want Gnus to invoke Bogofilter on new messages."
155   :type 'boolean
156   :group 'spam)
157
158 (defcustom spam-use-BBDB nil
159   "Whether BBDB should be used by spam-split."
160   :type 'boolean
161   :group 'spam)
162
163 (defcustom spam-use-BBDB-exclusive nil
164   "Whether BBDB-exclusive should be used by spam-split.
165 Exclusive BBDB means that all messages from senders not in the BBDB are 
166 considered spam."
167   :type 'boolean
168   :group 'spam)
169
170 (defcustom spam-use-ifile nil
171   "Whether ifile should be used by spam-split."
172   :type 'boolean
173   :group 'spam)
174
175 (defcustom spam-use-stat nil
176   "Whether spam-stat should be used by spam-split."
177   :type 'boolean
178   :group 'spam)
179
180 (defcustom spam-split-group "spam"
181   "Group name where incoming spam should be put by spam-split."
182   :type 'string
183   :group 'spam)
184
185 (defcustom spam-junk-mailgroups (cons spam-split-group '("mail.junk" "poste.pourriel"))
186   "Mailgroups with spam contents.
187 All unmarked article in such group receive the spam mark on group entry."
188   :type '(repeat (string :tag "Group"))
189   :group 'spam)
190
191 (defcustom spam-blackhole-servers '("bl.spamcop.net" "relays.ordb.org" 
192                                     "dev.null.dk" "relays.visi.com")
193   "List of blackhole servers."
194   :type '(repeat (string :tag "Server"))
195   :group 'spam)
196
197 (defcustom spam-blackhole-good-server-regex nil
198   "String matching IP addresses that should not be checked in the blackholes"
199   :type 'regexp
200   :group 'spam)
201
202 (defcustom spam-face 'gnus-splash-face
203   "Face for spam-marked articles"
204   :type 'face
205   :group 'spam)
206
207 (defcustom spam-regex-headers-spam '("^X-Spam-Flag: YES")
208   "Regular expression for positive header spam matches"
209   :type '(repeat (regexp :tag "Regular expression to match spam header"))
210   :group 'spam)
211
212 (defcustom spam-regex-headers-ham '("^X-Spam-Flag: NO")
213   "Regular expression for positive header ham matches"
214   :type '(repeat (regexp :tag "Regular expression to match ham header"))
215   :group 'spam)
216
217 (defgroup spam-ifile nil
218   "Spam ifile configuration."
219   :group 'spam)
220
221 (defcustom spam-ifile-path (exec-installed-p "ifile")
222   "File path of the ifile executable program."
223   :type '(choice (file :tag "Location of ifile")
224                  (const :tag "ifile is not installed"))
225   :group 'spam-ifile)
226
227 (defcustom spam-ifile-database-path nil
228   "File path of the ifile database."
229   :type '(choice (file :tag "Location of the ifile database")
230                  (const :tag "Use the default"))
231   :group 'spam-ifile)
232
233 (defcustom spam-ifile-spam-category "spam"
234   "Name of the spam ifile category."  
235   :type 'string
236   :group 'spam-ifile)
237
238 (defcustom spam-ifile-ham-category nil
239   "Name of the ham ifile category.  If nil, the current group name will
240 be used."
241   :type '(choice (string :tag "Use a fixed category")
242                 (const :tag "Use the current group name"))
243   :group 'spam-ifile)
244
245 (defcustom spam-ifile-all-categories nil
246   "Whether the ifile check will return all categories, or just spam.
247 Set this to t if you want to use the spam-split invocation of ifile as
248 your main source of newsgroup names."
249   :type 'boolean
250   :group 'spam-ifile)
251
252 (defgroup spam-bogofilter nil
253   "Spam bogofilter configuration."
254   :group 'spam)
255
256 (defcustom spam-bogofilter-path (exec-installed-p "bogofilter")
257   "File path of the Bogofilter executable program."
258   :type '(choice (file :tag "Location of bogofilter")
259                  (const :tag "Bogofilter is not installed"))
260   :group 'spam-bogofilter)
261
262 (defcustom spam-bogofilter-header "X-Bogosity"
263   "The header that Bogofilter inserts in messages."
264   :type 'string
265   :group 'spam-bogofilter)
266
267 (defcustom spam-bogofilter-spam-switch "-s"
268   "The switch that Bogofilter uses to register spam messages."
269   :type 'string
270   :group 'spam-bogofilter)
271
272 (defcustom spam-bogofilter-ham-switch "-n"
273   "The switch that Bogofilter uses to register ham messages."
274   :type 'string
275   :group 'spam-bogofilter)
276
277 (defcustom spam-bogofilter-bogosity-positive-spam-header "^\\(Yes\\|Spam\\)"
278   "The regex on `spam-bogofilter-header' for positive spam identification."
279   :type 'regexp
280   :group 'spam-bogofilter)
281
282 (defcustom spam-bogofilter-database-directory nil
283   "Directory path of the Bogofilter databases."
284   :type '(choice (directory :tag "Location of the Bogofilter database directory")
285                  (const :tag "Use the default"))
286   :group 'spam-ifile)
287
288 ;;; Key bindings for spam control.
289
290 (gnus-define-keys gnus-summary-mode-map
291   "St" spam-bogofilter-score
292   "Sx" gnus-summary-mark-as-spam
293   "Mst" spam-bogofilter-score
294   "Msx" gnus-summary-mark-as-spam
295   "\M-d" gnus-summary-mark-as-spam)
296
297 ;;; How to highlight a spam summary line.
298
299 ;; TODO: How do we redo this every time spam-face is customized?
300
301 (push '((eq mark gnus-spam-mark) . spam-face)
302       gnus-summary-highlight)
303
304 ;; convenience functions
305 (defun spam-group-ham-mark-p (group mark &optional spam)
306   (when (stringp group)
307     (let* ((marks (spam-group-ham-marks group spam))
308            (marks (if (symbolp mark) 
309                       marks 
310                     (mapcar 'symbol-value marks))))
311       (memq mark marks))))
312
313 (defun spam-group-spam-mark-p (group mark)
314   (spam-group-ham-mark-p group mark t))
315
316 (defun spam-group-ham-marks (group &optional spam)
317   (when (stringp group)
318     (let* ((marks (if spam
319                      (gnus-parameter-spam-marks group)
320                    (gnus-parameter-ham-marks group)))
321            (marks (car marks))
322            (marks (if (listp (car marks)) (car marks) marks)))
323       marks)))
324
325 (defun spam-group-spam-marks (group)
326   (spam-group-ham-marks group t))
327
328 (defun spam-group-spam-contents-p (group)
329   (if (stringp group)
330       (or (member group spam-junk-mailgroups)
331           (memq 'gnus-group-spam-classification-spam 
332                 (gnus-parameter-spam-contents group)))
333     nil))
334   
335 (defun spam-group-ham-contents-p (group)
336   (if (stringp group)
337       (memq 'gnus-group-spam-classification-ham 
338             (gnus-parameter-spam-contents group))
339     nil))
340
341 (defun spam-group-processor-p (group processor)
342   (if (and (stringp group)
343            (symbolp processor))
344       (member processor (car (gnus-parameter-spam-process group)))
345     nil))
346
347 (defun spam-group-spam-processor-report-gmane-p (group)
348   (spam-group-processor-p group 'gnus-group-spam-exit-processor-report-gmane))
349
350 (defun spam-group-spam-processor-bogofilter-p (group)
351   (spam-group-processor-p group 'gnus-group-spam-exit-processor-bogofilter))
352
353 (defun spam-group-spam-processor-blacklist-p (group)
354   (spam-group-processor-p group 'gnus-group-spam-exit-processor-blacklist))
355
356 (defun spam-group-spam-processor-ifile-p (group)
357   (spam-group-processor-p group 'gnus-group-spam-exit-processor-ifile))
358
359 (defun spam-group-ham-processor-ifile-p (group)
360   (spam-group-processor-p group 'gnus-group-ham-exit-processor-ifile))
361
362 (defun spam-group-ham-processor-bogofilter-p (group)
363   (spam-group-processor-p group 'gnus-group-ham-exit-processor-bogofilter))
364
365 (defun spam-group-spam-processor-stat-p (group)
366   (spam-group-processor-p group 'gnus-group-spam-exit-processor-stat))
367
368 (defun spam-group-ham-processor-stat-p (group)
369   (spam-group-processor-p group 'gnus-group-ham-exit-processor-stat))
370
371 (defun spam-group-ham-processor-whitelist-p (group)
372   (spam-group-processor-p group 'gnus-group-ham-exit-processor-whitelist))
373
374 (defun spam-group-ham-processor-BBDB-p (group)
375   (spam-group-processor-p group 'gnus-group-ham-exit-processor-BBDB))
376
377 (defun spam-group-ham-processor-copy-p (group)
378   (spam-group-processor-p group 'gnus-group-ham-exit-processor-copy))
379
380 ;;; Summary entry and exit processing.
381
382 (defun spam-summary-prepare ()
383   (spam-mark-junk-as-spam-routine))
384
385 (add-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
386
387 ;; The spam processors are invoked for any group, spam or ham or neither
388 (defun spam-summary-prepare-exit ()
389   (unless gnus-group-is-exiting-without-update-p
390     (gnus-message 6 "Exiting summary buffer and applying spam rules")
391     (when (and spam-bogofilter-path
392                (spam-group-spam-processor-bogofilter-p gnus-newsgroup-name))
393       (gnus-message 5 "Registering spam with bogofilter")
394       (spam-bogofilter-register-spam-routine))
395   
396     (when (and spam-ifile-path
397                (spam-group-spam-processor-ifile-p gnus-newsgroup-name))
398       (gnus-message 5 "Registering spam with ifile")
399       (spam-ifile-register-spam-routine))
400   
401     (when (spam-group-spam-processor-stat-p gnus-newsgroup-name)
402       (gnus-message 5 "Registering spam with spam-stat")
403       (spam-stat-register-spam-routine))
404
405     (when (spam-group-spam-processor-blacklist-p gnus-newsgroup-name)
406       (gnus-message 5 "Registering spam with the blacklist")
407       (spam-blacklist-register-routine))
408
409     (when (spam-group-spam-processor-report-gmane-p gnus-newsgroup-name)
410       (gnus-message 5 "Registering spam with the Gmane report")
411       (spam-report-gmane-register-routine))
412
413     (if spam-move-spam-nonspam-groups-only      
414         (when (not (spam-group-spam-contents-p gnus-newsgroup-name))
415           (spam-mark-spam-as-expired-and-move-routine
416            (gnus-parameter-spam-process-destination gnus-newsgroup-name)))
417       (gnus-message 5 "Marking spam as expired and moving it to %s" gnus-newsgroup-name)
418       (spam-mark-spam-as-expired-and-move-routine 
419        (gnus-parameter-spam-process-destination gnus-newsgroup-name)))
420
421     ;; now we redo spam-mark-spam-as-expired-and-move-routine to only
422     ;; expire spam, in case the above did not expire them
423     (gnus-message 5 "Marking spam as expired without moving it")
424     (spam-mark-spam-as-expired-and-move-routine nil)
425
426     (when (spam-group-ham-contents-p gnus-newsgroup-name)
427       (when (spam-group-ham-processor-whitelist-p gnus-newsgroup-name)
428         (gnus-message 5 "Registering ham with the whitelist")
429         (spam-whitelist-register-routine))
430       (when (spam-group-ham-processor-ifile-p gnus-newsgroup-name)
431         (gnus-message 5 "Registering ham with ifile")
432         (spam-ifile-register-ham-routine))
433       (when (spam-group-ham-processor-bogofilter-p gnus-newsgroup-name)
434         (gnus-message 5 "Registering ham with Bogofilter")
435         (spam-bogofilter-register-ham-routine))
436       (when (spam-group-ham-processor-stat-p gnus-newsgroup-name)
437         (gnus-message 5 "Registering ham with spam-stat")
438         (spam-stat-register-ham-routine))
439       (when (spam-group-ham-processor-BBDB-p gnus-newsgroup-name)
440         (gnus-message 5 "Registering ham with the BBDB")
441         (spam-BBDB-register-routine)))
442
443     (when (spam-group-ham-processor-copy-p gnus-newsgroup-name)
444       (gnus-message 5 "Copying ham")
445       (spam-ham-move-routine
446        (gnus-parameter-ham-process-destination gnus-newsgroup-name) t))
447
448     ;; now move all ham articles out of spam groups
449     (when (spam-group-spam-contents-p gnus-newsgroup-name)
450       (gnus-message 5 "Moving ham messages from spam group")
451       (spam-ham-move-routine
452        (gnus-parameter-ham-process-destination gnus-newsgroup-name)))))
453
454 (add-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
455
456 (defun spam-mark-junk-as-spam-routine ()
457   ;; check the global list of group names spam-junk-mailgroups and the
458   ;; group parameters
459   (when (spam-group-spam-contents-p gnus-newsgroup-name)
460     (gnus-message 5 "Marking %s articles as spam"
461                   (if spam-mark-only-unseen-as-spam 
462                       "unseen"
463                     "unread"))
464     (let ((articles (if spam-mark-only-unseen-as-spam 
465                         gnus-newsgroup-unseen
466                       gnus-newsgroup-unreads)))
467       (dolist (article articles)
468         (gnus-summary-mark-article article gnus-spam-mark)))))
469
470 (defun spam-mark-spam-as-expired-and-move-routine (&optional group)
471   (gnus-summary-kill-process-mark)
472   (let ((articles gnus-newsgroup-articles)
473         article tomove)
474     (dolist (article articles)
475       (when (eq (gnus-summary-article-mark article) gnus-spam-mark)
476         (gnus-summary-mark-article article gnus-expirable-mark)
477         (push article tomove)))
478
479     ;; now do the actual move
480     (when (and tomove
481                (stringp group))
482       (dolist (article tomove)
483         (gnus-summary-set-process-mark article))
484       (when tomove (gnus-summary-move-article nil group))))
485   (gnus-summary-yank-process-mark))
486  
487 (defun spam-ham-move-routine (&optional group copy)
488   (gnus-summary-kill-process-mark)
489   (let ((articles gnus-newsgroup-articles)
490         article mark tomove)
491     (when (stringp group)               ; this routine will do nothing
492                                         ; without a valid group
493       (dolist (article articles)
494         (when (spam-group-ham-mark-p gnus-newsgroup-name
495                                      (gnus-summary-article-mark article))
496           (push article tomove)))
497
498       ;; now do the actual move
499       (when tomove
500         (dolist (article tomove)
501           (when spam-mark-ham-unread-before-move-from-spam-group
502             (gnus-summary-mark-article article gnus-unread-mark))           
503           (gnus-summary-set-process-mark article))
504         (if copy
505             (gnus-summary-copy-article nil group)
506           (gnus-summary-move-article nil group)))))
507   (gnus-summary-yank-process-mark))
508  
509 (defun spam-generic-register-routine (spam-func ham-func)
510   (let ((articles gnus-newsgroup-articles)
511         article mark ham-articles spam-articles)
512
513     (while articles
514       (setq article (pop articles)
515             mark (gnus-summary-article-mark article))
516       (cond ((spam-group-spam-mark-p gnus-newsgroup-name mark) 
517              (push article spam-articles))
518             ((memq article gnus-newsgroup-saved))
519             ((spam-group-ham-mark-p gnus-newsgroup-name mark)
520              (push article ham-articles))))
521
522     (when (and ham-articles ham-func)
523       (mapc ham-func ham-articles))     ; we use mapc because unlike
524                                         ; mapcar it discards the
525                                         ; return values
526     (when (and spam-articles spam-func)
527       (mapc spam-func spam-articles)))) ; we use mapc because unlike
528                                         ; mapcar it discards the
529                                         ; return values
530
531 (eval-and-compile
532   (defalias 'spam-point-at-eol (if (fboundp 'point-at-eol)
533                                    'point-at-eol
534                                  'line-end-position)))
535
536 (defun spam-get-article-as-string (article)
537   (let ((article-buffer (spam-get-article-as-buffer article))
538                         article-string)
539     (when article-buffer
540       (save-window-excursion
541         (set-buffer article-buffer)
542         (setq article-string (buffer-string))))
543   article-string))
544
545 (defun spam-get-article-as-buffer (article)
546   (let ((article-buffer))
547     (when (numberp article)
548       (save-window-excursion
549         (gnus-summary-goto-subject article)
550         (gnus-summary-show-article t)
551         (setq article-buffer (get-buffer gnus-article-buffer))))
552     article-buffer))
553
554 ;; disabled for now
555 ;; (defun spam-get-article-as-filename (article)
556 ;;   (let ((article-filename))
557 ;;     (when (numberp article)
558 ;;       (nnml-possibly-change-directory (gnus-group-real-name gnus-newsgroup-name))
559 ;;       (setq article-filename (expand-file-name (int-to-string article) nnml-current-directory)))
560 ;;     (if (file-exists-p article-filename)
561 ;;      article-filename
562 ;;       nil)))
563
564 (defun spam-fetch-field-from-fast (article)
565   "Fetch the `from' field quickly, using the internal gnus-data-list function"
566   (if (and (numberp article)
567            (assoc article (gnus-data-list nil)))
568       (mail-header-from (gnus-data-header (assoc article (gnus-data-list nil))))
569     nil))
570
571 (defun spam-fetch-field-subject-fast (article)
572   "Fetch the `subject' field quickly, using the internal gnus-data-list function"
573   (if (and (numberp article)
574            (assoc article (gnus-data-list nil)))
575       (mail-header-subject (gnus-data-header (assoc article (gnus-data-list nil))))
576     nil))
577
578 \f
579 ;;;; Spam determination.
580
581 (defvar spam-list-of-checks
582   '((spam-use-blacklist                 .       spam-check-blacklist)
583     (spam-use-regex-headers             .       spam-check-regex-headers)
584     (spam-use-whitelist                 .       spam-check-whitelist)
585     (spam-use-BBDB                      .       spam-check-BBDB)
586     (spam-use-ifile                     .       spam-check-ifile)
587     (spam-use-stat                      .       spam-check-stat)
588     (spam-use-blackholes                .       spam-check-blackholes)
589     (spam-use-hashcash                  .       spam-check-hashcash)
590     (spam-use-bogofilter-headers        .       spam-check-bogofilter-headers)
591     (spam-use-bogofilter                .       spam-check-bogofilter))
592 "The spam-list-of-checks list contains pairs associating a parameter
593 variable with a spam checking function.  If the parameter variable is
594 true, then the checking function is called, and its value decides what
595 happens.  Each individual check may return nil, t, or a mailgroup
596 name.  The value nil means that the check does not yield a decision,
597 and so, that further checks are needed.  The value t means that the
598 message is definitely not spam, and that further spam checks should be
599 inhibited.  Otherwise, a mailgroup name is returned where the mail
600 should go, and further checks are also inhibited.  The usual mailgroup
601 name is the value of `spam-split-group', meaning that the message is
602 definitely a spam.")
603
604 (defvar spam-list-of-statistical-checks
605   '(spam-use-ifile spam-use-stat spam-use-bogofilter)
606 "The spam-list-of-statistical-checks list contains all the mail
607 splitters that need to have the full message body available.")
608
609 (defun spam-split (&rest specific-checks)
610   "Split this message into the `spam' group if it is spam.
611 This function can be used as an entry in `nnmail-split-fancy', for
612 example like this: (: spam-split).  It can take checks as parameters.
613
614 See the Info node `(gnus)Fancy Mail Splitting' for more details."
615   (interactive)
616   (save-excursion
617     (save-restriction
618       (dolist (check spam-list-of-statistical-checks)
619         (when (symbol-value check)
620           (widen)
621           (gnus-message 8 "spam-split: widening the buffer (%s requires it)"
622                         (symbol-name check))
623           (return)))
624       ;;   (progn (widen) (debug (buffer-string)))
625       (let ((list-of-checks spam-list-of-checks)
626             decision)
627         (while (and list-of-checks (not decision))
628           (let ((pair (pop list-of-checks)))
629             (when (and (symbol-value (car pair))
630                        (or (null specific-checks)
631                            (memq (car pair) specific-checks)))
632               (gnus-message 5 "spam-split: calling the %s function" (symbol-name (cdr pair)))
633               (setq decision (funcall (cdr pair))))))
634         (if (eq decision t)
635             nil
636           decision)))))
637   
638 (defun spam-setup-widening ()
639   (dolist (check spam-list-of-statistical-checks)
640     (when (symbol-value check)
641       (setq nnimap-split-download-body-default t))))
642
643 (add-hook 'gnus-get-new-news-hook 'spam-setup-widening)
644
645 \f
646 ;;;; Regex headers
647
648 (defun spam-check-regex-headers ()
649   (let (ret found)
650     (dolist (h-regex spam-regex-headers-ham)
651       (unless found
652         (goto-char (point-min))
653         (when (re-search-forward h-regex nil t)
654           (message "Ham regex header search positive.")
655           (setq found t))))
656     (dolist (s-regex spam-regex-headers-spam)
657       (unless found
658         (goto-char (point-min))
659         (when (re-search-forward s-regex nil t)
660           (message "Spam regex header search positive." (match-string 1))
661           (setq found t)
662           (setq ret spam-split-group))))
663     ret))
664
665 \f
666 ;;;; Blackholes.
667
668 (defun spam-check-blackholes ()
669   "Check the Received headers for blackholed relays."
670   (let ((headers (message-fetch-field "received"))
671         ips matches)
672     (when headers
673       (with-temp-buffer
674         (insert headers)
675         (goto-char (point-min))
676         (gnus-message 5 "Checking headers for relay addresses")
677         (while (re-search-forward
678                 "\\[\\([0-9]+.[0-9]+.[0-9]+.[0-9]+\\)\\]" nil t)
679           (gnus-message 9 "Blackhole search found host IP %s." (match-string 1))
680           (push (mapconcat 'identity
681                            (nreverse (split-string (match-string 1) "\\."))
682                            ".")
683                 ips)))
684       (dolist (server spam-blackhole-servers)
685         (dolist (ip ips)
686           (unless (and spam-blackhole-good-server-regex
687                        (string-match spam-blackhole-good-server-regex ip))
688             (let ((query-string (concat ip "." server)))
689               (if spam-use-dig
690                   (let ((query-result (query-dig query-string)))
691                     (when query-result
692                       (gnus-message 5 "(DIG): positive blackhole check '%s'" 
693                                     query-result)
694                       (push (list ip server query-result)
695                             matches)))
696                 ;; else, if not using dig.el
697                 (when (query-dns query-string)
698                   (gnus-message 5 "positive blackhole check")
699                   (push (list ip server (query-dns query-string 'TXT))
700                         matches))))))))
701     (when matches
702       spam-split-group)))
703 \f
704 ;;;; Hashcash.
705
706 (condition-case nil
707     (progn
708       (require 'hashcash)
709       
710       (defun spam-check-hashcash ()
711         "Check the headers for hashcash payments."
712         (mail-check-payment)))          ;mail-check-payment returns a boolean
713
714   (file-error (progn
715                 (defalias 'mail-check-payment 'ignore)
716                 (defalias 'spam-check-hashcash 'ignore))))
717 \f
718 ;;;; BBDB 
719
720 ;;; original idea for spam-check-BBDB from Alexander Kotelnikov
721 ;;; <sacha@giotto.sj.ru>
722
723 ;; all this is done inside a condition-case to trap errors
724
725 (condition-case nil
726     (progn
727       (require 'bbdb)
728       (require 'bbdb-com)
729       
730   (defun spam-enter-ham-BBDB (from)
731     "Enter an address into the BBDB; implies ham (non-spam) sender"
732     (when (stringp from)
733       (let* ((parsed-address (gnus-extract-address-components from))
734              (name (or (car parsed-address) "Ham Sender"))
735              (net-address (car (cdr parsed-address))))
736         (gnus-message 5 "Adding address %s to BBDB" from)
737         (when (and net-address
738                    (not (bbdb-search-simple nil net-address)))
739           (bbdb-create-internal name nil net-address nil nil 
740                                 "ham sender added by spam.el")))))
741
742   (defun spam-BBDB-register-routine ()
743     (spam-generic-register-routine 
744      ;; spam function
745      nil
746      ;; ham function
747      (lambda (article)
748        (spam-enter-ham-BBDB (spam-fetch-field-from-fast article)))))
749
750   (defun spam-check-BBDB ()
751     "Mail from people in the BBDB is classified as ham or non-spam"
752     (let ((who (message-fetch-field "from")))
753       (when who
754         (setq who (cadr (gnus-extract-address-components who)))
755         (if (bbdb-search-simple nil who)
756             t 
757           (if spam-use-BBDB-exclusive
758               spam-split-group
759             nil))))))
760
761   (file-error (progn
762                 (defalias 'bbdb-search-simple 'ignore)
763                 (defalias 'spam-check-BBDB 'ignore)
764                 (defalias 'spam-BBDB-register-routine 'ignore)
765                 (defalias 'spam-enter-ham-BBDB 'ignore)
766                 (defalias 'bbdb-create-internal 'ignore)
767                 (defalias 'bbdb-records 'ignore))))
768
769 \f
770 ;;;; ifile
771
772 ;;; check the ifile backend; return nil if the mail was NOT classified
773 ;;; as spam
774
775 (defun spam-get-ifile-database-parameter ()
776   "Get the command-line parameter for ifile's database from spam-ifile-database-path."
777   (if spam-ifile-database-path
778       (format "--db-file=%s" spam-ifile-database-path)
779     nil))
780     
781 (defun spam-check-ifile ()
782   "Check the ifile backend for the classification of this message"
783   (let ((article-buffer-name (buffer-name)) 
784         category return)
785     (with-temp-buffer
786       (let ((temp-buffer-name (buffer-name))
787             (db-param (spam-get-ifile-database-parameter)))
788         (save-excursion
789           (set-buffer article-buffer-name)
790           (if db-param
791               (call-process-region (point-min) (point-max) spam-ifile-path
792                                    nil temp-buffer-name nil "-q" "-c" db-param)
793             (call-process-region (point-min) (point-max) spam-ifile-path
794                                  nil temp-buffer-name nil "-q" "-c")))
795         (goto-char (point-min))
796         (if (not (eobp))
797             (setq category (buffer-substring (point) (spam-point-at-eol))))
798         (when (not (zerop (length category))) ; we need a category here
799           (if spam-ifile-all-categories
800               (setq return category)
801             ;; else, if spam-ifile-all-categories is not set...
802             (when (string-equal spam-ifile-spam-category category)
803               (setq return spam-split-group))))))
804     return))
805
806 (defun spam-ifile-register-with-ifile (article-string category)
807   "Register an article, given as a string, with a category.
808 Uses `gnus-newsgroup-name' if category is nil (for ham registration)."
809   (when (stringp article-string)
810     (let ((category (or category gnus-newsgroup-name))
811           (db-param (spam-get-ifile-database-parameter)))
812       (with-temp-buffer
813         (insert article-string)
814         (if db-param
815             (call-process-region (point-min) (point-max) spam-ifile-path 
816                                  nil nil nil 
817                                  "-h" "-i" category db-param)
818           (call-process-region (point-min) (point-max) spam-ifile-path 
819                                nil nil nil 
820                                "-h" "-i" category))))))
821
822 (defun spam-ifile-register-spam-routine ()
823   (spam-generic-register-routine 
824    (lambda (article)
825      (spam-ifile-register-with-ifile 
826       (spam-get-article-as-string article) spam-ifile-spam-category))
827    nil))
828
829 (defun spam-ifile-register-ham-routine ()
830   (spam-generic-register-routine 
831    nil
832    (lambda (article)
833      (spam-ifile-register-with-ifile 
834       (spam-get-article-as-string article) spam-ifile-ham-category))))
835
836 \f
837 ;;;; spam-stat
838
839 (condition-case nil
840     (progn
841       (let ((spam-stat-install-hooks nil))
842         (require 'spam-stat))
843       
844       (defun spam-check-stat ()
845         "Check the spam-stat backend for the classification of this message"
846         (let ((spam-stat-split-fancy-spam-group spam-split-group) ; override
847               (spam-stat-buffer (buffer-name)) ; stat the current buffer
848               category return)
849           (spam-stat-split-fancy)))
850
851       (defun spam-stat-register-spam-routine ()
852         (spam-generic-register-routine 
853          (lambda (article)
854            (let ((article-string (spam-get-article-as-string article)))
855              (with-temp-buffer
856                (insert article-string)
857                (spam-stat-buffer-is-spam))))
858          nil))
859
860       (defun spam-stat-register-ham-routine ()
861         (spam-generic-register-routine 
862          nil
863          (lambda (article)
864            (let ((article-string (spam-get-article-as-string article)))
865              (with-temp-buffer
866                (insert article-string)
867                (spam-stat-buffer-is-non-spam))))))
868
869       (defun spam-maybe-spam-stat-load ()
870         (when spam-use-stat (spam-stat-load)))
871       
872       (defun spam-maybe-spam-stat-save ()
873         (when spam-use-stat (spam-stat-save)))
874
875       ;; Add hooks for loading and saving the spam stats
876       (add-hook 'gnus-save-newsrc-hook 'spam-maybe-spam-stat-save)
877       (add-hook 'gnus-get-top-new-news-hook 'spam-maybe-spam-stat-load)
878       (add-hook 'gnus-startup-hook 'spam-maybe-spam-stat-load))
879
880   (file-error (progn
881                 (defalias 'spam-stat-register-ham-routine 'ignore)
882                 (defalias 'spam-stat-register-spam-routine 'ignore)
883                 (defalias 'spam-stat-buffer-is-spam 'ignore)
884                 (defalias 'spam-stat-buffer-is-non-spam 'ignore)
885                 (defalias 'spam-stat-split-fancy 'ignore)
886                 (defalias 'spam-stat-load 'ignore)
887                 (defalias 'spam-stat-save 'ignore)
888                 (defalias 'spam-check-stat 'ignore))))
889
890 \f
891
892 ;;;; Blacklists and whitelists.
893
894 (defvar spam-whitelist-cache nil)
895 (defvar spam-blacklist-cache nil)
896
897 (defun spam-enter-whitelist (address)
898   "Enter ADDRESS into the whitelist."
899   (interactive "sAddress: ")
900   (spam-enter-list address spam-whitelist)
901   (setq spam-whitelist-cache nil))
902
903 (defun spam-enter-blacklist (address)
904   "Enter ADDRESS into the blacklist."
905   (interactive "sAddress: ")
906   (spam-enter-list address spam-blacklist)
907   (setq spam-blacklist-cache nil))
908
909 (defun spam-enter-list (address file)
910   "Enter ADDRESS into the given FILE, either the whitelist or the blacklist."
911   (unless (file-exists-p (file-name-directory file))
912     (make-directory (file-name-directory file) t))
913   (save-excursion
914     (set-buffer
915      (find-file-noselect file))
916     (goto-char (point-max))
917     (unless (bobp)
918       (insert "\n"))
919     (insert address "\n")
920     (save-buffer)))
921
922 ;;; returns t if the sender is in the whitelist, nil or spam-split-group otherwise
923 (defun spam-check-whitelist ()
924   ;; FIXME!  Should it detect when file timestamps change?
925   (unless spam-whitelist-cache
926     (setq spam-whitelist-cache (spam-parse-list spam-whitelist)))
927   (if (spam-from-listed-p spam-whitelist-cache) 
928       t
929     (if spam-use-whitelist-exclusive
930         spam-split-group
931       nil)))
932
933 (defun spam-check-blacklist ()
934   ;; FIXME!  Should it detect when file timestamps change?
935   (unless spam-blacklist-cache
936     (setq spam-blacklist-cache (spam-parse-list spam-blacklist)))
937   (and (spam-from-listed-p spam-blacklist-cache) spam-split-group))
938
939 (defun spam-parse-list (file)
940   (when (file-readable-p file)
941     (let (contents address)
942       (with-temp-buffer
943         (insert-file-contents file)
944         (while (not (eobp))
945           (setq address (buffer-substring (point) (spam-point-at-eol)))
946           (forward-line 1)
947           (unless (zerop (length address))
948             (setq address (regexp-quote address))
949             (while (string-match "\\\\\\*" address)
950               (setq address (replace-match ".*" t t address)))
951             (push address contents))))
952       (nreverse contents))))
953
954 (defun spam-from-listed-p (cache)
955   (let ((from (message-fetch-field "from"))
956         found)
957     (while cache
958       (when (string-match (pop cache) from)
959         (setq found t
960               cache nil)))
961     found))
962
963 (defun spam-blacklist-register-routine ()
964   (spam-generic-register-routine 
965    ;; the spam function
966    (lambda (article)
967      (let ((from (spam-fetch-field-from-fast article)))
968        (when (stringp from)
969            (spam-enter-blacklist from))))
970    ;; the ham function
971    nil))
972
973 (defun spam-whitelist-register-routine ()
974   (spam-generic-register-routine 
975    ;; the spam function
976    nil 
977    ;; the ham function
978    (lambda (article)
979      (let ((from (spam-fetch-field-from-fast article)))
980        (when (stringp from)
981            (spam-enter-whitelist from))))))
982
983 \f
984 ;;;; Spam-report glue
985 (defun spam-report-gmane-register-routine ()
986   (spam-generic-register-routine
987    'spam-report-gmane
988    nil))
989
990 \f
991 ;;;; Bogofilter
992 (defun spam-check-bogofilter-headers (&optional score)
993   (let ((header (message-fetch-field spam-bogofilter-header)))
994       (when (and header
995                  (string-match spam-bogofilter-bogosity-positive-spam-header
996                                header))
997           (if score
998               (when (string-match "spamicity=\\([0-9.]+\\)" header)
999                 (match-string 1 header))
1000             spam-split-group))))
1001
1002 ;; return something sensible if the score can't be determined
1003 (defun spam-bogofilter-score ()
1004   "Get the Bogofilter spamicity score"
1005   (interactive)
1006   (save-window-excursion
1007     (gnus-summary-show-article t)
1008     (set-buffer gnus-article-buffer)
1009     (let ((score (or (spam-check-bogofilter-headers t)
1010                      (spam-check-bogofilter t))))
1011       (message "Spamicity score %s" score)
1012       (or score "0"))))
1013
1014 (defun spam-check-bogofilter (&optional score)
1015   "Check the Bogofilter backend for the classification of this message"
1016   (let ((article-buffer-name (buffer-name)) 
1017         return)
1018     (with-temp-buffer
1019       (let ((temp-buffer-name (buffer-name)))
1020         (save-excursion
1021           (set-buffer article-buffer-name)
1022           (if spam-bogofilter-database-directory
1023               (call-process-region (point-min) (point-max) 
1024                                    spam-bogofilter-path
1025                                    nil temp-buffer-name nil "-v"
1026                                    "-d" spam-bogofilter-database-directory)
1027             (call-process-region (point-min) (point-max) spam-bogofilter-path
1028                                  nil temp-buffer-name nil "-v")))
1029         (setq return (spam-check-bogofilter-headers score))))
1030     return))
1031
1032 (defun spam-bogofilter-register-with-bogofilter (article-string spam)
1033   "Register an article, given as a string, as spam or non-spam."
1034   (when (stringp article-string)
1035     (let ((switch (if spam spam-bogofilter-spam-switch 
1036                     spam-bogofilter-ham-switch)))
1037       (with-temp-buffer
1038         (insert article-string)
1039         (if spam-bogofilter-database-directory
1040             (call-process-region (point-min) (point-max) 
1041                                  spam-bogofilter-path
1042                                  nil nil nil "-v" switch
1043                                  "-d" spam-bogofilter-database-directory)
1044           (call-process-region (point-min) (point-max) spam-bogofilter-path
1045                                nil nil nil "-v" switch))))))
1046
1047 (defun spam-bogofilter-register-spam-routine ()
1048   (spam-generic-register-routine 
1049    (lambda (article)
1050      (spam-bogofilter-register-with-bogofilter
1051       (spam-get-article-as-string article) t))
1052    nil))
1053
1054 (defun spam-bogofilter-register-ham-routine ()
1055   (spam-generic-register-routine 
1056    nil
1057    (lambda (article)
1058      (spam-bogofilter-register-with-bogofilter
1059       (spam-get-article-as-string article) nil))))
1060
1061 (provide 'spam)
1062
1063 ;;; spam.el ends here.