(spam-set-difference): new function to replace
[gnus] / lisp / spam.el
index 8c985ae..0c0a509 100644 (file)
@@ -73,7 +73,7 @@
 (defgroup spam nil
   "Spam configuration.")
 
-(defcustom spam-directory "~/News/spam/"
+(defcustom spam-directory (nnheader-concat gnus-directory "spam/")
   "Directory for spam whitelists and blacklists."
   :type 'directory
   :group 'spam)
@@ -162,6 +162,11 @@ The regular expression is matched against the address."
   :type 'boolean
   :group 'spam)
 
+(defcustom spam-use-gmane-xref nil
+  "Whether the Gmane spam xref should be used by `spam-split'."
+  :type 'boolean
+  :group 'spam)
+
 (defcustom spam-use-blacklist nil
   "Whether the blacklist should be used by `spam-split'."
   :type 'boolean
@@ -245,8 +250,22 @@ considered spam."
   :type 'boolean
   :group 'spam)
 
+(defcustom spam-use-spamassassin nil
+  "Whether spamassassin should be invoked by `spam-split'.
+Enable this if you want Gnus to invoke SpamAssassin on new messages."
+  :type 'boolean
+  :group 'spam)
+
+(defcustom spam-use-spamassassin-headers nil
+  "Whether spamassassin headers should be checked by `spam-split'.
+Enable this if you pre-process messages with SpamAssassin BEFORE Gnus sees
+them."
+  :type 'boolean
+  :group 'spam)
+
 (defcustom spam-install-hooks (or
                               spam-use-dig
+                              spam-use-gmane-xref
                               spam-use-blacklist
                               spam-use-whitelist
                               spam-use-whitelist-exclusive
@@ -254,8 +273,10 @@ considered spam."
                               spam-use-hashcash
                               spam-use-regex-headers
                               spam-use-regex-body
-                              spam-use-bogofilter-headers
                               spam-use-bogofilter
+                              spam-use-bogofilter-headers
+                              spam-use-spamassassin
+                              spam-use-spamassassin-headers
                               spam-use-BBDB
                               spam-use-BBDB-exclusive
                               spam-use-ifile
@@ -281,14 +302,23 @@ All unmarked article in such group receive the spam mark on group entry."
   :type '(repeat (string :tag "Group"))
   :group 'spam)
 
+
+(defcustom spam-gmane-xref-spam-group "gmane.spam.detected"
+  "The group where spam xrefs can be found on Gmane.
+Only meaningful if you enable `spam-use-gmane-xref'."
+  :type 'string
+  :group 'spam)
+
 (defcustom spam-blackhole-servers '("bl.spamcop.net" "relays.ordb.org"
                                    "dev.null.dk" "relays.visi.com")
-  "List of blackhole servers."
+  "List of blackhole servers.
+Only meaningful if you enable `spam-use-blackholes'."
   :type '(repeat (string :tag "Server"))
   :group 'spam)
 
 (defcustom spam-blackhole-good-server-regex nil
-  "String matching IP addresses that should not be checked in the blackholes."
+  "String matching IP addresses that should not be checked in the blackholes.
+Only meaningful if you enable `spam-use-blackholes'."
   :type '(radio (const nil)
                (regexp :format "%t: %v\n" :size 0))
   :group 'spam)
@@ -299,22 +329,26 @@ All unmarked article in such group receive the spam mark on group entry."
   :group 'spam)
 
 (defcustom spam-regex-headers-spam '("^X-Spam-Flag: YES")
-  "Regular expression for positive header spam matches."
+  "Regular expression for positive header spam matches.
+Only meaningful if you enable `spam-use-regex-headers'."
   :type '(repeat (regexp :tag "Regular expression to match spam header"))
   :group 'spam)
 
 (defcustom spam-regex-headers-ham '("^X-Spam-Flag: NO")
-  "Regular expression for positive header ham matches."
+  "Regular expression for positive header ham matches.
+Only meaningful if you enable `spam-use-regex-headers'."
   :type '(repeat (regexp :tag "Regular expression to match ham header"))
   :group 'spam)
 
 (defcustom spam-regex-body-spam '()
-  "Regular expression for positive body spam matches."
+  "Regular expression for positive body spam matches.
+Only meaningful if you enable `spam-use-regex-body'."
   :type '(repeat (regexp :tag "Regular expression to match spam body"))
   :group 'spam)
 
 (defcustom spam-regex-body-ham '()
-  "Regular expression for positive body ham matches."
+  "Regular expression for positive body ham matches.
+Only meaningful if you enable `spam-use-regex-body'."
   :type '(repeat (regexp :tag "Regular expression to match ham body"))
   :group 'spam)
 
@@ -417,12 +451,76 @@ When nil, use the default spamoracle database."
                 (const :tag "Use the default"))
   :group 'spam-spamoracle)
 
+(defgroup spam-spamassassin nil
+  "Spam SpamAssassin configuration."
+  :group 'spam)
+
+(defcustom spam-spamassassin-path (executable-find "spamassassin")
+  "File path of the spamassassin executable program.
+Hint: set this to \"spamc\" if you have spamd running.  See the spamc and
+spamd man pages for more information on these programs."
+  :type '(choice (file :tag "Location of spamc")
+                (const :tag "spamassassin is not installed"))
+  :group 'spam-spamassassin)
+
+(defcustom spam-spamassassin-arguments ()
+  "Arguments to pass to the spamassassin executable.
+This must be a list.  For example, `(\"-C\" \"configfile\")'."
+  :type '(restricted-sexp :match-alternatives (listp))
+  :group 'spam-spamassassin)
+
+(defcustom spam-spamassassin-spam-flag-header "X-Spam-Flag"
+  "The header inserted by SpamAssassin to flag spam."
+  :type 'string
+  :group 'spam-spamassassin)
+
+(defcustom spam-spamassassin-positive-spam-flag-header "YES"
+  "The regex on `spam-spamassassin-spam-flag-header' for positive spam
+identification"
+  :type 'string
+  :group 'spam-spamassassin)
+
+(defcustom spam-spamassassin-spam-status-header "X-Spam-Status"
+  "The header inserted by SpamAssassin, giving extended scoring information"
+  :type 'string
+  :group 'spam-spamassassin)
+
+(defcustom spam-sa-learn-path (executable-find "sa-learn")
+  "File path of the sa-learn executable program."
+  :type '(choice (file :tag "Location of spamassassin")
+                (const :tag "spamassassin is not installed"))
+  :group 'spam-spamassassin)
+
+(defcustom spam-sa-learn-rebuild t
+  "Whether sa-learn should rebuild the database every time it is called
+Enable this if you want sa-learn to rebuild the database automatically.  Doing
+this will slightly increase the running time of the spam registration process.
+If you choose not to do this, you will have to run \"sa-learn --rebuild\" in
+order for SpamAssassin to recognize the new registered spam."
+  :type 'boolean
+  :group 'spam-spamassassin)
+
+(defcustom spam-sa-learn-spam-switch "--spam"
+  "The switch that sa-learn uses to register spam messages"
+  :type 'string
+  :group 'spam-spamassassin)
+
+(defcustom spam-sa-learn-ham-switch "--ham"
+  "The switch that sa-learn uses to register ham messages"
+  :type 'string
+  :group 'spam-spamassassin)
+
+(defcustom spam-sa-learn-unregister-switch "--forget"
+  "The switch that sa-learn uses to unregister messages messages"
+  :type 'string
+  :group 'spam-spamassassin)
+
 ;;; Key bindings for spam control.
 
 (gnus-define-keys gnus-summary-mode-map
-  "St" spam-bogofilter-score
+  "St" spam-generic-score
   "Sx" gnus-summary-mark-as-spam
-  "Mst" spam-bogofilter-score
+  "Mst" spam-generic-score
   "Msx" gnus-summary-mark-as-spam
   "\M-d" gnus-summary-mark-as-spam)
 
@@ -506,12 +604,14 @@ finds ham or spam.")
     (gnus-group-spam-exit-processor-ifile        spam spam-use-ifile)
     (gnus-group-spam-exit-processor-stat         spam spam-use-stat)
     (gnus-group-spam-exit-processor-spamoracle   spam spam-use-spamoracle)
+    (gnus-group-spam-exit-processor-spamassassin spam spam-use-spamassassin)
     (gnus-group-ham-exit-processor-ifile         ham spam-use-ifile)
     (gnus-group-ham-exit-processor-bogofilter    ham spam-use-bogofilter)
     (gnus-group-ham-exit-processor-stat          ham spam-use-stat)
     (gnus-group-ham-exit-processor-whitelist     ham spam-use-whitelist)
     (gnus-group-ham-exit-processor-BBDB          ham spam-use-BBDB)
     (gnus-group-ham-exit-processor-copy          ham spam-use-ham-copy)
+    (gnus-group-ham-exit-processor-spamassassin  ham spam-use-spamassassin)
     (gnus-group-ham-exit-processor-spamoracle    ham spam-use-spamoracle))
   "The `spam-list-of-processors' list.
 This list contains pairs associating a ham/spam exit processor
@@ -586,6 +686,15 @@ Respects the process/prefix convention."
     (gnus-summary-remove-process-mark article)
     (spam-report-gmane article)))
 
+(defun spam-generic-score ()
+  (interactive)
+  "Invoke whatever scoring method we can."
+  (if (or
+       spam-use-spamassassin
+       spam-use-spamassassin-headers)
+      (spam-spamassassin-score)
+    (spam-bogofilter-score)))
+
 ;;; Summary entry and exit processing.
 
 (defun spam-summary-prepare ()
@@ -609,7 +718,7 @@ Respects the process/prefix convention."
             (new-articles (spam-list-articles
                            gnus-newsgroup-articles
                            classification))
-            (changed-articles (gnus-set-difference old-articles new-articles)))
+            (changed-articles (spam-set-difference new-articles old-articles)))
        ;; now that we have the changed articles, we go through the processors
        (dolist (processor-param spam-list-of-processors)
          (let ((processor (nth 0 processor-param))
@@ -676,6 +785,20 @@ Respects the process/prefix convention."
   (setq spam-old-ham-articles nil)
   (setq spam-old-spam-articles nil))
 
+(defun spam-set-difference (list1 list2)
+  "Return a set difference of LIST1 and LIST2.  
+When either list is nil, the other is returned."
+  (if (and list1 list2)
+      ;; we have two non-nil lists
+      (progn
+       (dolist (item (append list1 list2))
+         (when (and (memq item list1) (memq item list2))
+           (setq list1 (delq item list1))
+           (setq list2 (delq item list2))))
+       (append list1 list2))
+    ;; if either of the lists was nil, return the other one
+    (if list1 list1 list2)))
+
 (defun spam-mark-junk-as-spam-routine ()
   ;; check the global list of group names spam-junk-mailgroups and the
   ;; group parameters
@@ -786,22 +909,12 @@ Respects the process/prefix convention."
     (spam-ham-copy-or-move-routine nil groups)))
 
 (defun spam-get-article-as-string (article)
-  (let ((article-buffer (spam-get-article-as-buffer article))
-       article-string)
-    (when article-buffer
-      (save-window-excursion
-       (set-buffer article-buffer)
-       (setq article-string (buffer-string))))
-    article-string))
-
-(defun spam-get-article-as-buffer (article)
-  (let ((article-buffer))
-    (when (numberp article)
-      (save-window-excursion
-       (gnus-summary-goto-subject article)
-       (gnus-summary-show-article t)
-       (setq article-buffer (get-buffer gnus-article-buffer))))
-    article-buffer))
+  (when (numberp article)
+    (with-temp-buffer
+      (gnus-request-article-this-buffer
+       article
+       gnus-newsgroup-name)
+      (buffer-string))))
 
 ;; disabled for now
 ;; (defun spam-get-article-as-filename (article)
@@ -815,64 +928,85 @@ Respects the process/prefix convention."
 ;;     article-filename
 ;;       nil)))
 
-(defun spam-fetch-field-fast (article field)
+(defun spam-fetch-field-fast (article field &optional prepared-data-header)
   "Fetch a field quickly, using the internal gnus-data-list function"
   (when (numberp article)
-    (let* ((header (assoc article (gnus-data-list nil)))
-          (data-header (if header (gnus-data-header header) nil)))
-      (cond
-       ((equal field 'from)
-       (mail-header-from data-header))
-       ((equal field 'message-id)
-       (mail-header-message-id data-header))
-       ((equal field 'subject)
-       (mail-header-subject data-header))
-       ((equal field 'references)
-       (mail-header-references data-header))
-       ((equal field 'date)
-       (mail-header-date data-header))
-       ((equal field 'xref)
-       (mail-header-xref data-header))
-       ((equal field 'extra)
-       (mail-header-extra data-header))
-       (t
-       nil)))))
-
-(defun spam-fetch-field-from-fast (article)
-  (spam-fetch-field-fast article 'from))
-
-(defun spam-fetch-field-subject-fast (article)
-  (spam-fetch-field-fast article 'subject))
-
-(defun spam-fetch-field-message-id-fast (article)
-  (spam-fetch-field-fast article 'message-id))
-
-(defun spam-insert-fake-headers (article)
-  (insert (format "From: %s\n" (spam-fetch-field-fast article 'from)))
-  (insert (format "Subject: %s\n" (spam-fetch-field-fast article 'subject)))
-  (insert (format "Message-ID: %s\n" (spam-fetch-field-fast article 'message-id)))
-  (insert (format "Date: %s\n" (spam-fetch-field-fast article 'date)))
-  (insert (format "References: %s\n" (spam-fetch-field-fast article 'references)))
-  (insert (format "Xref: %s\n" (spam-fetch-field-fast article 'xref)))
-  (when (spam-fetch-field-fast article 'extra)
-    (insert (format "%s\n" (spam-fetch-field-fast article 'extra)))))
+    (let* ((data-header (or prepared-data-header
+                           (spam-fetch-article-header article))))
+      (if (arrayp data-header)
+       (cond
+        ((equal field 'from)
+         (mail-header-from data-header))
+        ((equal field 'message-id)
+         (mail-header-message-id data-header))
+        ((equal field 'subject)
+         (mail-header-subject data-header))
+        ((equal field 'references)
+         (mail-header-references data-header))
+        ((equal field 'date)
+         (mail-header-date data-header))
+        ((equal field 'xref)
+         (mail-header-xref data-header))
+        ((equal field 'extra)
+         (mail-header-extra data-header))
+        (t
+         nil))
+       (gnus-error 5 "Article %d has a nil data header" article)))))
+
+(defun spam-fetch-field-from-fast (article &optional prepared-data-header)
+  (spam-fetch-field-fast article 'from prepared-data-header))
+
+(defun spam-fetch-field-subject-fast (article &optional prepared-data-header)
+  (spam-fetch-field-fast article 'subject prepared-data-header))
+
+(defun spam-fetch-field-message-id-fast (article &optional prepared-data-header)
+  (spam-fetch-field-fast article 'message-id prepared-data-header))
+
+(defun spam-generate-fake-headers (article)
+  (let ((dh (spam-fetch-article-header article)))
+    (if dh
+       (concat
+        (format 
+         (concat "From: %s\nSubject: %s\nMessage-ID: %s\n"
+                 "Date: %s\nReferences: %s\nXref: %s\n")
+         (spam-fetch-field-fast article 'from dh)
+         (spam-fetch-field-fast article 'subject dh)
+         (spam-fetch-field-fast article 'message-id dh)
+         (spam-fetch-field-fast article 'date dh)
+         (spam-fetch-field-fast article 'references dh)
+         (spam-fetch-field-fast article 'xref dh))
+        (when (spam-fetch-field-fast article 'extra dh)
+          (format "%s\n" (spam-fetch-field-fast article 'extra dh))))
+      (gnus-error
+       5
+       "spam-generate-fake-headers: article %d didn't have a valid header"
+       article))))
+
+(defun spam-fetch-article-header (article)
+  (save-excursion
+    (set-buffer gnus-summary-buffer)
+    (nth 3 (assq article gnus-newsgroup-data))))
 
 \f
 ;;;; Spam determination.
 
 (defvar spam-list-of-checks
-  '((spam-use-blacklist         . spam-check-blacklist)
-    (spam-use-regex-headers     . spam-check-regex-headers)
-    (spam-use-regex-body        . spam-check-regex-body)
-    (spam-use-whitelist         . spam-check-whitelist)
-    (spam-use-BBDB              . spam-check-BBDB)
-    (spam-use-ifile             . spam-check-ifile)
-    (spam-use-spamoracle         . spam-check-spamoracle)
-    (spam-use-stat              . spam-check-stat)
-    (spam-use-blackholes        . spam-check-blackholes)
-    (spam-use-hashcash          . spam-check-hashcash)
-    (spam-use-bogofilter-headers . spam-check-bogofilter-headers)
-    (spam-use-bogofilter        . spam-check-bogofilter))
+  '((spam-use-blacklist                .       spam-check-blacklist)
+    (spam-use-regex-headers            .       spam-check-regex-headers)
+    (spam-use-gmane-xref               .       spam-check-gmane-xref)
+    (spam-use-regex-body               .       spam-check-regex-body)
+    (spam-use-whitelist                .       spam-check-whitelist)
+    (spam-use-BBDB                     .       spam-check-BBDB)
+    (spam-use-BBDB-exclusive           .       spam-check-BBDB)
+    (spam-use-ifile                    .       spam-check-ifile)
+    (spam-use-spamoracle               .       spam-check-spamoracle)
+    (spam-use-stat                     .       spam-check-stat)
+    (spam-use-blackholes               .       spam-check-blackholes)
+    (spam-use-hashcash                 .       spam-check-hashcash)
+    (spam-use-spamassassin-headers     .       spam-check-spamassassin-headers)
+    (spam-use-spamassassin             .       spam-check-spamassassin)
+    (spam-use-bogofilter-headers       .       spam-check-bogofilter-headers)
+    (spam-use-bogofilter               .       spam-check-bogofilter))
   "The spam-list-of-checks list contains pairs associating a
 parameter variable with a spam checking function.  If the
 parameter variable is true, then the checking function is called,
@@ -893,6 +1027,7 @@ definitely a spam.")
     spam-use-stat
     spam-use-bogofilter
     spam-use-blackholes
+    spam-use-spamassassin
     spam-use-spamoracle)
   "The spam-list-of-statistical-checks list contains all the mail
 splitters that need to have the full message body available.
@@ -999,6 +1134,7 @@ See the Info node `(gnus)Fancy Mail Splitting' for more details."
 
         (let* ((spam-split-symbolic-return t)
                (spam-split-symbolic-return-positive t)
+               (fake-headers (spam-generate-fake-headers article))
                (split-return
                 (or registry-lookup
                     (with-temp-buffer
@@ -1006,7 +1142,8 @@ See the Info node `(gnus)Fancy Mail Splitting' for more details."
                           (gnus-request-article-this-buffer
                            article
                            group)
-                        (spam-insert-fake-headers article))
+                        ;; else, we fake the article
+                        (when fake-headers (insert fake-headers)))
                       (if (or (null first-method)
                               (equal first-method 'default))
                           (spam-split)
@@ -1039,6 +1176,10 @@ See the Info node `(gnus)Fancy Mail Splitting' for more details."
                         nil
                         spam-whitelist-unregister-routine
                         nil)
+    (spam-use-ham-copy   nil
+                        nil
+                        nil
+                        nil)
     (spam-use-BBDB      spam-BBDB-register-routine
                         nil
                         spam-BBDB-unregister-routine
@@ -1061,6 +1202,10 @@ See the Info node `(gnus)Fancy Mail Splitting' for more details."
                         ;; does Gmane support unregistration?
                         nil
                         nil)
+    (spam-use-spamassassin spam-spamassassin-register-ham-routine
+                          spam-spamassassin-register-spam-routine
+                          spam-spamassassin-unregister-ham-routine
+                          spam-spamassassin-unregister-spam-routine)
     (spam-use-bogofilter spam-bogofilter-register-ham-routine
                         spam-bogofilter-register-spam-routine
                         spam-bogofilter-unregister-ham-routine
@@ -1141,7 +1286,7 @@ functions")
                            gnus-newsgroup-articles
                            classification)))
        ;; process them
-       (gnus-message 5 "%s %d %s articles with classification %s, check %s"
+       (gnus-message 5 "%s %d %s articles as %s using backend %s"
                      (if unregister "Unregistering" "Registering")
                      (length articles)
                      (if specific-articles "specific" "")
@@ -1173,8 +1318,10 @@ functions")
           type
           cell-list))
 
-      (gnus-error 5 (format "%s called with bad ID, type, classification, check, or group"
-                           "spam-log-processing-to-registry")))))
+      (gnus-error 
+       5 
+       (format "%s call with bad ID, type, classification, spam-check, or group"
+              "spam-log-processing-to-registry")))))
 
 ;;; check if a ham- or spam-processor registration has been done
 (defun spam-log-registered-p (id type)
@@ -1183,8 +1330,10 @@ functions")
             (spam-process-type-valid-p type))
        (cdr-safe (gnus-registry-fetch-extra id type))
       (progn
-       (gnus-error 5 (format "%s called with bad ID, type, classification, or check"
-                             "spam-log-registered-p"))
+       (gnus-error 
+        5 
+        (format "%s called with bad ID, type, classification, or spam-check"
+                "spam-log-registered-p"))
        nil))))
 
 ;;; check what a ham- or spam-processor registration says
@@ -1203,6 +1352,7 @@ functions")
        nil
       decision)))
 
+
 ;;; check if a ham- or spam-processor registration needs to be undone
 (defun spam-log-unregistration-needed-p (id type classification check)
   (when spam-log-to-registry
@@ -1219,8 +1369,10 @@ functions")
                (setq found t))))
          found)
       (progn
-       (gnus-error 5 (format "%s called with bad ID, type, classification, or check"
-                             "spam-log-unregistration-needed-p"))
+       (gnus-error 
+        5 
+        (format "%s called with bad ID, type, classification, or spam-check"
+                "spam-log-unregistration-needed-p"))
        nil))))
 
 
@@ -1243,7 +1395,7 @@ functions")
           type
           new-cell-list))
       (progn
-       (gnus-error 5 (format "%s called with bad ID, type, check, or group"
+       (gnus-error 5 (format "%s call with bad ID, type, spam-check, or group"
                              "spam-log-undo-registration"))
        nil))))
 
@@ -1253,6 +1405,20 @@ functions")
     (when (symbol-value check)
       (setq nnimap-split-download-body-default t))))
 
+\f
+;;;; Gmane xrefs
+(defun spam-check-gmane-xref ()
+  (let ((header (or
+                (message-fetch-field "Xref")
+                (message-fetch-field "Newsgroups")))
+       (spam-split-group (if spam-split-symbolic-return
+                             'spam
+                           spam-split-group)))
+    (when header                       ; return nil when no header
+      (when (string-match spam-gmane-xref-spam-group
+                         header)
+         spam-split-group))))
+
 \f
 ;;;; Regex body
 
@@ -1786,9 +1952,9 @@ REMOVE not nil, remove the ADDRESSES."
     (set-buffer gnus-article-buffer)
     (let ((score (or (spam-check-bogofilter-headers t)
                     (spam-check-bogofilter t))))
+      (gnus-summary-show-article)
       (message "Spamicity score %s" score)
-      (or score "0"))
-    (gnus-summary-show-article)))
+      (or score "0"))))
 
 (defun spam-check-bogofilter (&optional score)
   "Check the Bogofilter backend for the classification of this message"
@@ -1870,7 +2036,7 @@ REMOVE not nil, remove the ADDRESSES."
                  (goto-char (point-min))
                  (when (re-search-forward "^X-Spam: yes;" nil t)
                    spam-split-group))
-             (error "Error running spamoracle" status))))))))
+             (error "Error running spamoracle: %s" status))))))))
 
 (defun spam-spamoracle-learn (articles article-is-spam-p &optional unregister)
   "Run spamoracle in training mode."
@@ -1892,8 +2058,8 @@ REMOVE not nil, remove the ADDRESSES."
                           `("-f" ,spam-spamoracle-database
                             "add" ,arg)
                         `("add" ,arg)))))
-         (when (not (eq 0 status))
-           (error "Error running spamoracle" status)))))))
+         (unless (eq 0 status)
+           (error "Error running spamoracle: %s" status)))))))
 
 (defun spam-spamoracle-learn-ham (articles &optional unregister)
   (spam-spamoracle-learn articles nil unregister))
@@ -1908,6 +2074,95 @@ REMOVE not nil, remove the ADDRESSES."
   (spam-spamoracle-learn-spam articles t))
 
 \f
+;;;; SpamAssassin
+;;; based mostly on the bogofilter code
+(defun spam-check-spamassassin-headers (&optional score)
+  "Check the SpamAssassin headers for the classification of this message."
+  (if score                            ; scoring mode
+      (let ((header (message-fetch-field spam-spamassassin-spam-status-header)))
+       (when header
+         (if (string-match "hits=\\(-?[0-9.]+\\)" header)
+             (match-string 1 header)
+           "0")))
+    ;; spam detection mode
+    (let ((header (message-fetch-field spam-spamassassin-spam-flag-header))
+         (spam-split-group (if spam-split-symbolic-return
+                                'spam
+                              spam-split-group)))
+         (when header                  ; return nil when no header
+           (when (string-match spam-spamassassin-positive-spam-flag-header
+                               header)
+             spam-split-group)))))
+
+(defun spam-check-spamassassin (&optional score)
+  "Check the SpamAssassin backend for the classification of this message."
+  (let ((article-buffer-name (buffer-name)))
+    (with-temp-buffer
+      (let ((temp-buffer-name (buffer-name)))
+       (save-excursion
+         (set-buffer article-buffer-name)
+         (apply 'call-process-region
+                (point-min) (point-max) spam-spamassassin-path
+                nil temp-buffer-name nil spam-spamassassin-arguments))
+       ;; check the return now (we're back in the temp buffer)
+       (goto-char (point-min))
+       (spam-check-spamassassin-headers score)))))
+
+;; return something sensible if the score can't be determined
+(defun spam-spamassassin-score ()
+  "Get the SpamAssassin score"
+  (interactive)
+  (save-window-excursion
+    (gnus-summary-show-article t)
+    (set-buffer gnus-article-buffer)
+    (let ((score (or (spam-check-spamassassin-headers t)
+                    (spam-check-spamassassin t))))
+      (gnus-summary-show-article)
+      (message "SpamAssassin score %s" score)
+      (or score "0"))))
+
+(defun spam-spamassassin-register-with-sa-learn (articles spam
+                                                &optional unregister)
+  "Register articles with spamassassin's sa-learn as spam or non-spam."
+  (if articles
+      (let ((action (if unregister spam-sa-learn-unregister-switch
+                     (if spam spam-sa-learn-spam-switch
+                       spam-sa-learn-ham-switch)))
+           (summary-buffer-name (buffer-name)))
+       (with-temp-buffer
+         ;; group the articles into mbox format
+         (dolist (article articles)
+           (let (article-string)
+             (save-excursion
+               (set-buffer summary-buffer-name)
+               (setq article-string (spam-get-article-as-string article)))
+             (when (stringp article-string)
+               (insert "From \n") ; mbox separator (sa-learn only checks the
+                                  ; first five chars, so we can get away with
+                                  ; a bogus line))
+               (insert article-string)
+               (insert "\n"))))
+         ;; call sa-learn on all messages at the same time
+         (apply 'call-process-region
+                (point-min) (point-max)
+                spam-sa-learn-path
+                nil nil nil "--mbox"
+                (if spam-sa-learn-rebuild
+                    (list action)
+                  `("--no-rebuild" ,action)))))))
+
+(defun spam-spamassassin-register-spam-routine (articles &optional unregister)
+  (spam-spamassassin-register-with-sa-learn articles t unregister))
+
+(defun spam-spamassassin-register-ham-routine (articles &optional unregister)
+  (spam-spamassassin-register-with-sa-learn articles nil unregister))
+
+(defun spam-spamassassin-unregister-spam-routine (articles)
+  (spam-spamassassin-register-with-sa-learn articles t t))
+
+(defun spam-spamassassin-unregister-ham-routine (articles)
+  (spam-spamassassin-register-with-sa-learn articles nil t))
+\f
 ;;;; Hooks
 
 ;;;###autoload
@@ -1925,7 +2180,7 @@ REMOVE not nil, remove the ADDRESSES."
   (add-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit)
   (add-hook 'gnus-summary-prepare-hook 'spam-summary-prepare)
   (add-hook 'gnus-get-new-news-hook 'spam-setup-widening)
-  (add-hook 'gnus-summary-prepare-hook 'spam-find-spam))
+  (add-hook 'gnus-summary-prepared-hook 'spam-find-spam))
 
 (defun spam-unload-hook ()
   "Uninstall the spam.el hooks"