Revision: miles@gnu.org--gnu-2005/gnus--devo--0--patch-37
[gnus] / lisp / spam-stat.el
index 9d43ef8..fcee567 100644 (file)
@@ -1,6 +1,6 @@
 ;;; spam-stat.el --- detecting spam based on statistics
 
-;; Copyright (C) 2002, 2003 Free Software Foundation, Inc.
+;; Copyright (C) 2002, 2003, 2004 Free Software Foundation, Inc.
 
 ;; Author: Alex Schroeder <alex@gnu.org>
 ;; Keywords: network
 Use the functions to build a dictionary of words and their statistical
 distribution in spam and non-spam mails.  Then use a function to determine
 whether a buffer contains spam or not."
+  :version "22.1"
   :group 'gnus)
 
 (defcustom spam-stat-file "~/.spam-stat.el"
@@ -161,17 +162,33 @@ This variable says how many characters this will be."
   :group 'spam-stat)
 
 (defcustom spam-stat-split-fancy-spam-group "mail.spam"
-  "Name of the group where spam should be stored, if
-`spam-stat-split-fancy' is used in fancy splitting rules.  Has no
-effect when spam-stat is invoked through spam.el."
+  "Name of the group where spam should be stored.
+If `spam-stat-split-fancy' is used in fancy splitting rules.  Has
+no effect when spam-stat is invoked through spam.el."
   :type 'string
   :group 'spam-stat)
 
 (defcustom spam-stat-split-fancy-spam-threshhold 0.9
-  "Spam score threshhold in spam-stat-split-fancy."
+  "Spam score threshold in spam-stat-split-fancy."
   :type 'number
   :group 'spam-stat)
 
+(defcustom spam-stat-washing-hook nil
+  "Hook applied to each message before analysis."
+  :type 'hook
+  :group 'spam-stat)
+
+(defcustom spam-stat-process-directory-age 90
+  "Max. age of files to be processed in directory, in days.
+When using `spam-stat-process-spam-directory' or
+`spam-stat-process-non-spam-directory', only files that have
+been touched in this many days will be considered.  Without
+this filter, re-training spam-stat with several thousand messages
+will start to take a very long time.")
+
+(defvar spam-stat-last-saved-at nil
+  "Time stamp of last change of spam-stat-file on this run")
+
 (defvar spam-stat-syntax-table
   (let ((table (copy-syntax-table text-mode-syntax-table)))
     (modify-syntax-entry ?- "w" table)
@@ -194,6 +211,10 @@ This is set by hooking into Gnus.")
 (defvar spam-stat-buffer-name " *spam stat buffer*"
   "Name of the `spam-stat-buffer'.")
 
+(defvar spam-stat-coding-system
+  (if (mm-coding-system-p 'emacs-mule) 'emacs-mule 'raw-text)
+  "Coding system used for `spam-stat-file'.")
+
 ;; Hooking into Gnus
 
 (defun spam-stat-store-current-buffer ()
@@ -279,7 +300,7 @@ Use `spam-stat-ngood', `spam-stat-nbad', `spam-stat-good',
 ;; Parsing
 
 (defmacro with-spam-stat-max-buffer-size (&rest body)
-  "Narrows the buffer down to the first 4k characters, then evaluates BODY."
+  "Narrow the buffer down to the first 4k characters, then evaluate BODY."
   `(save-restriction
      (when (> (- (point-max)
                 (point-min))
@@ -289,7 +310,8 @@ Use `spam-stat-ngood', `spam-stat-nbad', `spam-stat-good',
      ,@body))
 
 (defun spam-stat-buffer-words ()
-  "Return a hash table of words and number of occurences in the buffer."
+  "Return a hash table of words and number of occurrences in the buffer."
+  (run-hooks 'spam-stat-washing-hook)
   (with-spam-stat-max-buffer-size
    (with-syntax-table spam-stat-syntax-table
      (goto-char (point-min))
@@ -338,7 +360,7 @@ Use `spam-stat-ngood', `spam-stat-nbad', `spam-stat-good',
    (lambda (word count)
      (let ((entry (gethash word spam-stat)))
        (if (not entry)
-          (error "This buffer has unknown words in it.")
+          (gnus-message 8 "This buffer has unknown words in it")
         (spam-stat-set-good entry (- (spam-stat-good entry) count))
         (spam-stat-set-bad entry (+ (spam-stat-bad entry) count))
         (spam-stat-set-score entry (spam-stat-compute-score entry))
@@ -354,7 +376,7 @@ Use `spam-stat-ngood', `spam-stat-nbad', `spam-stat-good',
    (lambda (word count)
      (let ((entry (gethash word spam-stat)))
        (if (not entry)
-          (error "This buffer has unknown words in it.")
+          (gnus-message 8 "This buffer has unknown words in it")
         (spam-stat-set-good entry (+ (spam-stat-good entry) count))
         (spam-stat-set-bad entry (- (spam-stat-bad entry) count))
         (spam-stat-set-score entry (spam-stat-compute-score entry))
@@ -365,31 +387,42 @@ Use `spam-stat-ngood', `spam-stat-nbad', `spam-stat-good',
 ;; Saving and Loading
 
 (defun spam-stat-save (&optional force)
-  "Save the `spam-stat' hash table as lisp file."
-  (interactive)
+  "Save the `spam-stat' hash table as lisp file.
+With a prefix argument save unconditionally."
+  (interactive "P")
   (when (or force spam-stat-dirty)
-    (with-temp-buffer
-      (let ((standard-output (current-buffer))
-           (font-lock-maximum-size 0))
-       (insert "(setq spam-stat-ngood "
-               (number-to-string spam-stat-ngood)
-               " spam-stat-nbad "
-               (number-to-string spam-stat-nbad)
-               " spam-stat (spam-stat-to-hash-table '(")
-       (maphash (lambda (word entry)
-                  (prin1 (list word
-                               (spam-stat-good entry)
-                               (spam-stat-bad entry))))
-                spam-stat)
-       (insert ")))")
-       (write-file spam-stat-file)))
-    (setq spam-stat-dirty nil)))
+    (let ((coding-system-for-write spam-stat-coding-system))
+      (with-temp-file spam-stat-file
+       (let ((standard-output (current-buffer))
+             (font-lock-maximum-size 0))
+         (insert (format ";-*- coding: %s; -*-\n" spam-stat-coding-system))
+         (insert (format "(setq spam-stat-ngood %d spam-stat-nbad %d
+spam-stat (spam-stat-to-hash-table '(" spam-stat-ngood spam-stat-nbad))
+         (maphash (lambda (word entry)
+                    (prin1 (list word
+                                 (spam-stat-good entry)
+                                 (spam-stat-bad entry))))
+                  spam-stat)
+         (insert ")))"))))
+    (message "Saved %s."  spam-stat-file)
+    (setq spam-stat-dirty nil
+          spam-stat-last-saved-at (nth 5 (file-attributes spam-stat-file)))))
 
 (defun spam-stat-load ()
   "Read the `spam-stat' hash table from disk."
   ;; TODO: maybe we should warn the user if spam-stat-dirty is t?
-  (load-file spam-stat-file)
-  (setq spam-stat-dirty nil))
+  (let ((coding-system-for-read spam-stat-coding-system))
+    (cond (spam-stat-dirty (message "Spam stat not loaded: spam-stat-dirty t"))
+          ((or (not (boundp 'spam-stat-last-saved-at))
+               (null spam-stat-last-saved-at)
+               (not (equal spam-stat-last-saved-at
+                           (nth 5 (file-attributes spam-stat-file)))))
+           (progn 
+             (load-file spam-stat-file)
+             (setq spam-stat-dirty nil
+                   spam-stat-last-saved-at 
+                   (nth 5 (file-attributes spam-stat-file)))))
+          (t (message "Spam stat file not loaded: no change in disk..")))))
 
 (defun spam-stat-to-hash-table (entries)
   "Turn list ENTRIES into a hash table and store as `spam-stat'.
@@ -445,7 +478,7 @@ where DIFF is the difference between SCORE and 0.5."
 (defun spam-stat-score-buffer ()
   "Return a score describing the spam-probability for this buffer."
   (setq spam-stat-score-data (spam-stat-buffer-words-with-scores))
-  (let* ((probs (mapcar (lambda (e) (cadr e)) spam-stat-score-data))
+  (let* ((probs (mapcar 'cadr spam-stat-score-data))
         (prod (apply #'* probs)))
     (/ prod (+ prod (apply #'* (mapcar #'(lambda (x) (- 1 x))
                                       probs))))))
@@ -486,10 +519,12 @@ check the variable `spam-stat-score-data'."
       (dolist (f files)
        (when (and (file-readable-p f)
                   (file-regular-p f)
-                   (> (nth 7 (file-attributes f)) 0))
+                   (> (nth 7 (file-attributes f)) 0)
+                  (< (time-to-number-of-days (time-since (nth 5 (file-attributes f))))
+                     spam-stat-process-directory-age))
          (setq count (1+ count))
          (message "Reading %s: %.2f%%" dir (/ count max))
-         (insert-file-contents f)
+         (insert-file-contents-literally f)
          (spam-stat-strip-xref)
          (funcall func)
          (erase-buffer))))))
@@ -534,7 +569,7 @@ display non-spam files; otherwise display spam files."
          (setq count (1+ count))
          (message "Reading %.2f%%, score %.2f"
                   (/ count max) (/ score count))
-         (insert-file-contents f)
+         (insert-file-contents-literally f)
          (setq buffer-score (spam-stat-score-buffer))
          (when (> buffer-score 0.9)
            (setq score (1+ score)))
@@ -563,10 +598,11 @@ COUNT defaults to 5"
                         (spam-stat-bad entry))
                      count)
               (remhash key spam-stat)))
-          spam-stat))
+          spam-stat)
+  (setq spam-stat-dirty t))
 
 (defun spam-stat-install-hooks-function ()
-  "Install the spam-stat function hooks"
+  "Install the spam-stat function hooks."
   (interactive)
   (add-hook 'nnmail-prepare-incoming-message-hook
            'spam-stat-store-current-buffer)
@@ -577,13 +613,16 @@ COUNT defaults to 5"
   (spam-stat-install-hooks-function))
 
 (defun spam-stat-unload-hook ()
-  "Uninstall the spam-stat function hooks"
+  "Uninstall the spam-stat function hooks."
   (interactive)
   (remove-hook 'nnmail-prepare-incoming-message-hook
               'spam-stat-store-current-buffer)
   (remove-hook 'gnus-select-article-hook
               'spam-stat-store-gnus-article-buffer))
 
+(add-hook 'spam-stat-unload-hook 'spam-stat-unload-hook)
+
 (provide 'spam-stat)
 
+;;; arch-tag: ff1d2200-8ddb-42fb-bb7b-1b5e20448554
 ;;; spam-stat.el ends here