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