;;; vm-bogofilter.el version gaw-3 ;; ;; This module provides an interface between ViewMail and bogofilter, or any ;; other similar ``learning'' spam filter. ;; ;; Some portions Copyright (C) 2003 by Greg A. Woods ;; Copyright under the terms of the GNU GPL Version 2. ;; ;; Home page: http://www.weird.com/~woods/projects/ ;; E-mail: "Greg A. Woods" ;; ;; Based on vm-bogofilter.el v1.1.2, Copyright (C) 2003 by Bjorn Knutsson ;; Copyright under the terms of the GNU GPL Version 2. ;; ;; Home page: http://www.cis.upenn.edu/~bjornk/ ;; E-mail: Björn Knutsson ;; ;; Bjorn Knutsson, CIS, 3330 Walnut Street, Philadelphia, PA 19104-6389, USA ;; ;; Based on vm-spamassassin.el v1.1, Copyright (C) 2002 by Markus Mohnen ;; Copyright under the terms of the GNU GPL Version 2. ;; ;; This program is free software; you can redistribute it and/or modify it ;; under the terms of the GNU General Public License as published by the Free ;; Software Foundation; either version 2 of the License, or (at your option) ;; any later version. ;; ;; This program is distributed in the hope that it will be useful, but WITHOUT ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ;; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ;; more details. ;; ;; You can find a copies of the GNU General Public License online at many sites ;; and you may receive a copy of the GNU General Public License by writing to ;; the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. ;; ;;; Version history: ;; ;; v gaw-3: 2004/09/11-??:?? ;; * save the folder at the end of vm-bogofilter-filter-arrived-messages ;; since the buffer was modified and it is almost certain that there ;; are actual changes to the filtered region, such as the addition of ;; a new "X-Bogosity:" header in each newly filtered message. ;; Previously the user was expected to save the folder manually but ;; doing this automatically is somewhat safer. ;; * make the "just-retag" function work like the others. ;; * make the "options-test" default to very verbose to show all the ;; tokens, their counts, and probabilities ;; * improve some documentation strings. ;; ;; v gaw-2: 2003/12/04-23:15 ;; * hopefully I have fixed all the virtual folder related bugs. ;; * Added a "just-retag" function. ;; * Added an "update-and-retag" function. ;; * Added "new-spam" and "new-clean" functions. ;; * added a few more checks for obvious errors and incompatabilities. ;; * Sundry cleanup and re-org, including refactoring command-line ;; option setup variables in order to support the new functions. ;; ;; v gaw-1: local hacks to 1.1.2 ;; * add support for handling marked messages to most interactive ;; functions. ;; * Add a "test" function ;; * get rid of any need for that stupid "formail" junk. ;; * get rid of the `-old' functions. ;; * at least throw an error when bogofilter fails! ;; * update documentation ;; ;; v 1.1.2: Borg assimilation version (12-Sep-2003) ;; * Great minds think alike. Olivier Cappe independently ;; created his own version of vm-bogofilter.el based on ;; vm-spamassassin.el with the same basic functions. ;; He submitted a patch to my version to harmonize them. ;; * Added comment about vm-delete-after-archiving, as suggested ;; by Olivier. ;; v 1.1.1: minor edits ;; * Chris McMahan submitted a patch that disables running ;; bogfilter on incoming mail. While at first potentially ;; confusing, this means that you can run bogofilter via ;; e.g. procmail filters, and then use vm-bogofilter.el to ;; (re-)educate bogofilter about false positives/negatives. ;; * Documentation of a folder problem added ;; v 1.1: functional update ;; * Changed re-training functions to also re-tag the the message ;; in the VM folder, thus making the tag on the message in VM ;; be consistent with bogofilter's opinion about the message. ;; Notice!! If you use the tag in the message, you should be ;; aware that a message re-classified as spam may still not ;; be tagged as spam by bogofilter, and vice versa, if the ;; bogofilter database contains too many counter-examples. ;; The old re-training functions are still present, if you ;; prefer not to muck around with your inbox. They've been ;; renamed vm-bogofilter-is-spam-old/vm-bogofilter-is-clean-old ;; and works as before. ;; v 1.0.1: update ;; * Very minor edits of texts, no functional changes. ;; v 1.0: initial release ;; * First release, based on Markus Mohnen's vm-spamassassin ;; ;; ;; To use this program, you need reasonably recent versions of VM from ;; http://www.wonderworks.com/vm) and bogofilter from ;; http://sourceforge.net/projects/bogofilter/ ;; ;; This interface was developed for and has been tested with, VM 7.18 and ;; bogofilter 0.92.6. You must use bogofilter newer than 0.15.0 as prior to ;; that it did not really understand unix mailbox format properly at all. ;; ;; This interface should be compatible with Bogofilter-like programs such as ;; bmf 0.9.4 from http://sourceforge.net/projects/bmf/, though note bmf 0.9.4 ;; is broken and does not properly understand unix mailbox format. ;; ;; ;;; Installation: ;; ;; Put this file on your Emacs-Lisp load path (i.e. in your local site-lisp ;; directory where VM is installed) and add following into your ~/.vm startup ;; file: ;; ;; (require 'vm-bogofilter) ;; ;; ;;; Usage: ;; ;; Whenever you get new mail bogofilter will be invoked on them. Mail detected ;; as spam will be tagged by bogofilter, and you can use any existing matching ;; and selection mechanisms to dispose of them as you see fit. ;; ;; For example you can create a virtual folder containing all of the messages ;; tagged as spam (or alternately one with non-spam, or both). Pre-declaring ;; virtual folders in your ~/.vm can be done like this: ;; ;; (defvar my-vm-virtual-spam-folder "zzzz Spam-I-Am zzzz" ;; "The name of the spam virtual folder. ;; Changes to this variable only take effect when your ~/.vm is reloaded.") ;; ;; (setq vm-virtual-folder-alist ;; (list ;; (list (symbol-value 'my-vm-virtual-spam-folder) ;; (list (list (file-name-nondirectory ;; (symbol-value 'vm-primary-inbox))) ;; '(header "^X-Bogosity: Yes"))) ;; ;; any number of additional virtual folders can ;; ;; similarly be pre-defined ;; ))) ;; ;; To get the spam out of your INBOX when using virtual folders like this you ;; can define one additional virtual folder using the rule: ;; ;; '(not (virtual-folder-member)) ;; ;; If you first visit your virtual spam folder, and then visit this second ;; virtual folder and use it instead of your INBOX then it will only contain ;; the non-spam messages that are also not already in any other virtual folder. ;; ;; A concrete example of doing all of the above automatically, with full ;; integration into the standard `vm-get-new-mail' function can be found in the ;; ~/.vm file in the following archive: ;; ;; ftp://ftp.weird.com/pub/local/dotfiles.tar.gz ;; ;; ;; Alternately if you normally use `vm-auto-archive-messages' then adding the ;; following form to your existing in a separate 'spam' folder: ;; ;; ("^X-Bogosity: " ("Yes," . "spam")) ;; ;; If you enable auto-archiving through `vm-auto-folder-alist' as above then ;; you can also set `vm-delete-after-archiving' to make VM automatically delete ;; the archived spams from the main folder. ;; ;; ;; If a message is tagged as spam incorrectly, you can re-train bogofilter by ;; calling the function `vm-bogofilter-is-clean' on that message. ;; ;; Similarly, calling `vm-bogofilter-is-spam' will re-train bogofilter to ;; recognize a clean-marked message as spam. ;; ;; These functions and others can be bound to keys in your ~/.vm, for example: ;; ;; (define-key vm-mode-map "\C-cs" 'vm-bogofilter-new-spam) ;; (define-key vm-mode-map "\C-cS" 'vm-bogofilter-is-spam) ;; (define-key vm-mode-map "\C-cn" 'vm-bogofilter-new-clean) ;; (define-key vm-mode-map "\C-cN" 'vm-bogofilter-is-clean) ;; (define-key vm-mode-map "\C-ct" 'vm-bogofilter-test-if-spam) ;; (define-key vm-mode-map "\C-cT" 'vm-bogofilter-just-retag) ;; (define-key vm-mode-map "\C-cu" 'vm-bogofilter-update-and-tag) ;; ;; You may use `vm-next-command-uses-marks' with these functions too. ;; ;; Note re-training may or may not change the spam-status of a message. ;; Because of the way bogofilter works, even a message explicitly declared as ;; spam may not be tagged as spam if there have been enough similar non-spam ;; messages processed through bogofilter. Remember, bogofilter is not trained ;; to recognize individual messages, but rather just the statistical occurence ;; of tokens in the message. You may have to train bogofilter on a number of ;; spam messages before it recognizes any of them as spam. See the ;; documentation for bogofilter. Notice also that even if the tag changes, ;; this will not undo actions previously taken based on the tag, e.g. moving ;; messages to another (virtual) folder with auto-folders. ;; ;; If you have a folders of pre-sorted spam and non-spam messages then you may ;; want to use them to train bogofilter before you start using it with your ;; daily incoming mail. You can also do this through VM with the ;; `vm-bogofilter-new-spam' and `vm-bogofilter-new-clean' functions. ;; ;; ;;; BUGS: ;; ;; You must use normal unix mailbox folders ('From_) for your incoming ;; folder(s), i.e. a mailbox format compatible with 'bogofilter -M'. You can ;; check the raw folder to see if you have a blank line before the "From "-line ;; separating messages. See the documentation for `vm-default-folder-type'. ;; Note that as of 0.15.7 bogofilter still does not recognize content-length. ;; ;; vm-bogofilter is not really very smart about errors, though it tries to ;; detect problems via the exit code from bogofilter and will throw an error ;; for any non-zero exit (including signals). ;; ;; Errors encountered during initial processing _should_ leave the INBOX ;; intact, though any outpu tfrom the filter program may already have been ;; inserted into the INBOX buffer before the first new message. ;; ;; Note that errors signalled during the execution of any of the ;; `vm-retrieved-spooled-mail-hook' are not handled very gracefuly at all by VM ;; (at least as of 7.18). Such errors are likely coding errors, but may be ;; possible if some of the setup variables are drastically mis-configured. In ;; any case the result is that the new messages are not "assimilated" and the ;; summary remains unchanged (and the crash box is deleted), making it look as ;; though the retrieved messages have simply disappeared. In fact though they ;; will still be there in the INBOX buffer -- just hidden from view. ;; ;; Errors encountered during reclassification _should_ leave the original ;; message in the current buffer, though as with initial processing any output ;; from the filter program may already have been inserted in the buffer before ;; the original message. ;; ;; ;;; Customization: ;; ;; M-x customize RET vm-bogofilter ;;; Code: (eval-when-compile (require 'vm)) ;;; Customisation: (defgroup vm-bogofilter nil "VM Spam Filter Options" :group 'vm) ;; XXX in theory there could be four variables specifying program names: ;; 1. the filter & tagging program (takes mbox input and produces tagged mbox) ;; 2. the test program (takes one message and tests it) ;; 3. the reclassify-as-spam program (takes one message, filters & tags it as spam) ;; 3. the reclassify-as-clean program (takes one message, filters & tags it as clean) ;; (luckily bogofilter does all of this with one program using command-line flags) ;; (defcustom vm-bogofilter-program "bogofilter" "*Name of the filter program. Note that \"sh -c\" is always used so this name will be searched in the current PATH." :group 'vm-bogofilter :type 'string) (defcustom vm-bogofilter-program-options-common "" "*Common options always passed to the filter program. If you've just upgraded from bogofilter 0.15.3 or older and have not yet re-trained using the new version then you may wish to include '-H'. Other potential options might include algorithm selection, wordlist/database controls, logging controls, etc." :group 'vm-bogofilter :type 'string) ;; XXX this variable's name assumes the current mbox-only implementation. ;; (defcustom vm-bogofilter-program-options-mbox "-M" "*Input format option for the filter program used when filtering multiple messages. This interface currently only works with 'From_ (unix mbox) formatted incoming folders and filtering of new messages is done by sending the whole block to the stdin of one filter process, thus '-M' must be used to tell \"bogofilter\" how to process its intput." :group 'vm-bogofilter :type 'string) (defcustom vm-bogofilter-program-options-update "-u" "*Update/self-learning options for the filter program. Use '-u' with bogofilter to run it in update/registration mode where it first guesses whether the message is spam or not and then registers it as such in the word list(s)." :group 'vm-bogofilter :type 'string) (defcustom vm-bogofilter-program-options-new-clean "-n" "*Options used when declaring a new message as clean (non-spam) with `vm-bogofilter-new-clean'." :group 'vm-bogofilter :type 'string) (defcustom vm-bogofilter-program-options-new-spam "-s" "*Options used when declaring a new message as spam with `vm-bogofilter-new-spam'." :group 'vm-bogofilter :type 'string) (defcustom vm-bogofilter-program-options-is-clean "-S -n" "*Options used when re-declaring a spam-tagged message as clean (non-spam) with `vm-bogofilter-is-clean'. Use just '-N' with \"bmf\". (yes, it's the opposite of bogofilter!)" :group 'vm-bogofilter :type 'string) (defcustom vm-bogofilter-program-options-is-spam "-N -s" "*Options used when re-declaring a clean-tagged message as spam with `vm-bogofilter-is-spam'. Use just '-S' with \"bmf\". (yes, it's the opposite of bogofilter!)" :group 'vm-bogofilter :type 'string) (defcustom vm-bogofilter-program-options-test "-vvv" "*Options used when testing a message with `vm-bogofilter-test-if-spam'. Use '-vvv' with \"bogofitler\" to get very verbose results. When using \"bmf\" use '-t' as well." :group 'vm-bogofilter :type 'string) (defcustom vm-bogofilter-program-options-retag "-p -e" "*Options used when just re-tagging a message with `vm-bogofilter-retag' and `vm-bogofilter-filter-arrived-messages'. Use '-p' with \"bogofilter\" or \"bmf\" to tell it to run in pass-through mode where it adds a tag header to the message. '-e' must also be used with \"bogofilter\" to tell it to only exit non-zero when there is a real error (i.e. tell it not to use the exit code to identify spam vs. non-spam)." :group 'vm-bogofilter :type 'string) (defcustom vm-bogofilter-invoke-through-vm t "*If not set 'nil prior to loading of this file then the function `vm-bogofilter-filter-arrived-messages' will be added to the hook list `vm-retrieved-spooled-mail-hook'. If you do set this to 'nil before loading this file then you can use `vm-bogofilter-update-and-tag' to filter and tag selected messages on demand." :group 'vm-bogofilter :type 'boolean) (defun vm-bogofilter-filter-arrived-messages () "The function used to do the filtering of new messages through bogofilter in update/register mode. The entire region of the current buffer corresponding to the newly appended messages is piped through bogofilter and replaced with the output from bogofilter thus the incoming folder must have the type 'From_ (i.e. be in standard unix \"mbox\" format). The folder is then saved to preserve any changes made by bogofilter to the headers of the newly filtered messages. This function is normally added to `vm-retrieved-spooled-mail-hook' automatically when this file is loaded unless `vm-bogofilter-invoke-through-vm' is pre-set to 'nil." (if (not (eq vm-folder-type 'From_)) (error "Bogofilter can only deal with \"mbox\" formatted folders!")) (if (not vm-keep-crash-boxes) (error "Using `vm-bogofilter-filter-arrived-messages' without `vm-keep-crash-boxes' set is suicide!")) (save-excursion (vm-save-restriction (let ((buffer-read-only nil) (tail-cons (vm-last vm-message-list))) (widen) (if (null tail-cons) (goto-char (point-min)) (goto-char (vm-text-end-of (car tail-cons))) (beginning-of-line) (forward-line)) (message "Filtering new messages... ") (let ((res (call-process-region (point) (point-max) (or shell-file-name "sh") nil t nil shell-command-switch (concat vm-bogofilter-program " " vm-bogofilter-program-options-common " " vm-bogofilter-program-options-mbox " " vm-bogofilter-program-options-retag " " vm-bogofilter-program-options-update)))) (if (and res (not (and (integerp res) (zerop res)))) (error "Something went wrong filtering new messages (exit %s)" res)) ;; output filtered through bogofilter has been inserted in the buffer ;; just in front of the original copy, leaving the point at the end ;; of the filtered copy, so now we can just delete the old copy (delete-region (point) (point-max))) (message "Filtering new messages... done.") (vm-save-folder))))) ;; XXX this normally just replaces the message buffer pane with the output from ;; bogofilter so it's a bit ugly but it is functional. (Ideally the output ;; should be displayed in the message buffer.) ;; (defun vm-bogofilter-test-if-spam () "Test the status of the current message, displaying the results." (interactive) (vm-follow-summary-cursor) (vm-pipe-message-to-command (concat vm-bogofilter-program " " vm-bogofilter-program-options-common " " vm-bogofilter-program-options-test) nil)) ;; Based, vaguely, on a mish-mash of vm-pipe-message-to-command, ;; vm-edit-message, and vm-edit-message-end. ;; ;; XXX we could/should add a new flag to the cached metadata to record whether ;; or not a message has been tagged, and possibly provide some protection, or ;; at least warnings about tagging an already tagged message, re-tagging an ;; un-tagged message, etc. ;; (defun vm-bogofilter-retag (mlist reclassify-options &optional update-options) "Core workhorse function for re-tagging and/or reclassifying each of messages in MLIST, one-by-one. RECLASSIFY-OPTIONS is string of command-line options, which if not 'nil will be used to tell bogofilter to re-classify the single message being fed to its STDIN. If not 'nil the message will first be re-classified with a separate invocation of `vm-bogofilter-program', otherwise this step is skipped. UPDATE-OPTIONS is an optional string of command-line options for bogofilter to cause it run in update (auto-learning) mode. Normally RECLASSIFY-OPTIONS should be given as 'nil when using UPDATE-OPTIONS. In all cases the message is filtered through `vm-bogofilter-program' using the `vm-bogofilter-program-retag-options' on the command-line and the output replaces the original message text. Returns the count of messages processed from MLIST." (let ((buffer (get-buffer-create "*VM-bogofilter*")) (retag-count 0) m) (while mlist (setq m (car mlist)) ;; XXX is this test bogus? I.e. do we care? (if (and (vm-virtual-message-p m) (null (vm-virtual-messages-of m))) (error "Can't re-classify unmirrored virtual messages.")) (save-excursion ;; find the buffer containing the (real) message (set-buffer (vm-buffer-of (vm-real-message-of m))) ;; clear out the command output buffer (save-excursion (set-buffer buffer) (erase-buffer)) ;; get ready to parse just the specified message (vm-save-restriction (vm-save-buffer-excursion (widen) (goto-char (vm-headers-of (vm-real-message-of m))) (narrow-to-region (point) (vm-text-end-of (vm-real-message-of m))) ;; XXX can all filters we care to support reclassify _and_ retag in one ;; pass? If so then this next form should go away and the right ;; command-line options should be given to the single invocation in the ;; next form. What about filters that might not retag with the ;; measured bogosity, but rather based specifically on the command-line ;; option? (if (not (eq reclassify-options nil)) (progn (let ((res (call-process-region (point-min) (point-max) (or shell-file-name "sh") nil buffer nil shell-command-switch (concat vm-bogofilter-program " " vm-bogofilter-program-options-common " " reclassify-options)))) ;; XXX maybe we could treat this error as non-fatal? (if (and res (not (and (integerp res) (zerop res)))) (error "Something went wrong re-classifying message (exit %s)" res))) ;; clear out the filtering buffer again (will contain an output ;; from the reclassifying operation). (save-excursion (set-buffer buffer) (erase-buffer)))) (let ((buffer-read-only nil)) ;; send the message through bogofilter (again) to retag the message. ;; We do this instead of trying to replace the tag header as doing it ;; this way keeps the code independent of any details about the tag ;; header. Note we don't use the DELETE flag -- this preserves the ;; original message should an error occur, possibly prepending any ;; error message to the region. (Maybe BUFFER should be "(t t)"?) (let ((res (call-process-region (point-min) (point-max) (or shell-file-name "sh") nil t nil shell-command-switch (concat vm-bogofilter-program " " vm-bogofilter-program-options-common " " vm-bogofilter-program-options-retag " " update-options)))) (if (and res (not (and (integerp res) (zerop res)))) (error "Something went wrong re-tagging message (exit %s)" res))) ;; output filtered through bogofilter has been inserted in the buffer ;; just in front of the original copy, leaving the point at the end ;; of the filtered copy, so now we can just delete the old copy of ;; the message (delete-region (point) (vm-text-end-of (vm-real-message-of m))) ;; XXX would now recompute Content-Length header if necessary!!! )))) (vm-increment retag-count) (setq mlist (cdr mlist))) ;; return the count of messages processed. retag-count)) ;; XXX all of the remaining interactive functions are almost identical ;; internally and no doubt the majority of their guts could be refactored out ;; into one common template function. (defun vm-bogofilter-just-retag (&optional count) "Retag message(s). Prefix arg N retags the current and the next N - 1 messages. Prefix arg -N retags the current and previous N - 1 messages. When invoked on marked messages (via vm-next-command-uses-marks), only the marked messages in the current folder are re-tagged." (interactive "p") ;; make sure we're in the right place in case we're not using marks (vm-follow-summary-cursor) (vm-select-folder-buffer) (vm-check-for-killed-summary) (vm-check-for-killed-presentation) ;; make sure we're allowed to do what we're doing.... ;; XXX is the intention to test the virtual folder's state, or the underlying ;; true folder's state? (vm-error-if-folder-read-only) (vm-error-if-folder-empty) ;; by default we just act on the current message (or count (setq count 1)) (let ((mlist (vm-select-marked-or-prefixed-messages count)) (retag-count 0)) (if (interactive-p) (message "Re-tagging message(s)... ")) (setq retag-count (vm-bogofilter-retag mlist nil)) (vm-discard-cached-data-internal mlist) (vm-display nil nil '(vm-bogofilter-is-spam) '(vm-bogofilter-is-spam reading-message startup)) (if (interactive-p) (message "%d message%s re-tagged." retag-count (if (= 1 retag-count) "" "s"))) ;; move to the last message processed IFF numeric prefix given and not ;; chasing marks.... (if (and (not (eq last-command 'vm-next-command-uses-marks)) (not (= count 1))) (vm-next-message count t executing-kbd-macro))) (vm-preview-current-message) (vm-update-summary-and-mode-line)) ; XXX necessary? done by vm-preview-current-message? (defun vm-bogofilter-update-and-tag (&optional count) "Filter message(s) in update (auto-learning) mode, tagging them with the result of the filtering. Prefix arg N retags the current and the next N - 1 messages. Prefix arg -N retags the current and previous N - 1 messages. When invoked on marked messages (via vm-next-command-uses-marks), only the marked messages in the current folder are re-filtered." (interactive "p") ;; make sure we're in the right place in case we're not using marks (vm-follow-summary-cursor) (vm-select-folder-buffer) (vm-check-for-killed-summary) (vm-check-for-killed-presentation) ;; make sure we're allowed to do what we're doing.... ;; XXX is the intention to test the virtual folder's state, or the underlying ;; true folder's state? (vm-error-if-folder-read-only) (vm-error-if-folder-empty) ;; by default we just act on the current message (or count (setq count 1)) (let ((mlist (vm-select-marked-or-prefixed-messages count)) (retag-count 0)) (if (interactive-p) (message "Filtering message(s)... ")) (setq retag-count (vm-bogofilter-retag mlist nil vm-bogofilter-program-options-update)) (vm-discard-cached-data-internal mlist) (vm-display nil nil '(vm-bogofilter-is-spam) '(vm-bogofilter-is-spam reading-message startup)) (if (interactive-p) (message "%d message%s filtered." retag-count (if (= 1 retag-count) "" "s"))) ;; move to the last message processed IFF numeric prefix given and not ;; chasing marks.... (if (and (not (eq last-command 'vm-next-command-uses-marks)) (not (= count 1))) (vm-next-message count t executing-kbd-macro))) (vm-preview-current-message) (vm-update-summary-and-mode-line)) (defun vm-bogofilter-is-spam (&optional count) "Re-declare clean-tagged message(s) as being spam, and re-tag them with the computed result of the re-filtering. NOTE: Should only be used on already clean-tagged messages. Prefix arg N reclassifies the current and the next N - 1 messages. Prefix arg -N reclassifies the current and previous N - 1 messages. When invoked on marked messages (via vm-next-command-uses-marks), only the marked messages in the current folder are reclassified." (interactive "p") ;; make sure we're in the right place in case we're not using marks (vm-follow-summary-cursor) (vm-select-folder-buffer) (vm-check-for-killed-summary) (vm-check-for-killed-presentation) ;; make sure we're allowed to do what we're doing.... ;; XXX is the intention to test the virtual folder's state, or the underlying ;; true folder's state? (vm-error-if-folder-read-only) (vm-error-if-folder-empty) ;; by default we just act on the current message (or count (setq count 1)) (let ((mlist (vm-select-marked-or-prefixed-messages count)) (retag-count 0)) (if (interactive-p) (message "Reclassifying message(s) as spam... ")) (setq retag-count (vm-bogofilter-retag mlist vm-bogofilter-program-options-is-spam)) (vm-discard-cached-data-internal mlist) (vm-display nil nil '(vm-bogofilter-is-spam) '(vm-bogofilter-is-spam reading-message startup)) (if (interactive-p) (message "%d message%s reclassified as spam." retag-count (if (= 1 retag-count) "" "s"))) ;; move to the last message processed IFF numeric prefix given and not ;; chasing marks.... (if (and (not (eq last-command 'vm-next-command-uses-marks)) (not (= count 1))) (vm-next-message count t executing-kbd-macro))) (vm-preview-current-message) (vm-update-summary-and-mode-line)) (defun vm-bogofilter-is-clean (&optional count) "Re-declare spam-tagged message(s) as not being clean, and re-tag them with the computed result of the re-filtering. NOTE: Should only be used on already spam-tagged messages. Prefix arg N reclassifies the current and the next N - 1 messages. Prefix arg -N reclassifies the current and previous N - 1 messages. When invoked on marked messages (via vm-next-command-uses-marks), only the marked messages in the current folder are reclassified." (interactive "p") ;; make sure we're in the right place in case we're not using marks (vm-follow-summary-cursor) (vm-select-folder-buffer) (vm-check-for-killed-summary) (vm-check-for-killed-presentation) ;; make sure we're allowed to do what we're doing.... ;; XXX is the intention to test the virtual folder's state, or the underlying ;; true folder's state? (vm-error-if-folder-read-only) (vm-error-if-folder-empty) ;; by default we just act on the current message (or count (setq count 1)) (let ((mlist (vm-select-marked-or-prefixed-messages count)) (retag-count 0)) (if (interactive-p) (message "Reclassifying message(s) as clean... ")) (setq retag-count (vm-bogofilter-retag mlist vm-bogofilter-program-options-is-clean)) (vm-discard-cached-data-internal mlist) (vm-display nil nil '(vm-bogofilter-is-clean) '(vm-bogofilter-is-clean reading-message startup)) (if (interactive-p) (message "%d message%s reclassified as clean." retag-count (if (= 1 retag-count) "" "s"))) ;; move to the last message processed IFF numeric prefix given and not ;; chasing marks.... (if (and (not (eq last-command 'vm-next-command-uses-marks)) (not (= count 1))) (vm-next-message count t executing-kbd-macro))) (vm-preview-current-message) (vm-update-summary-and-mode-line)) (defun vm-bogofilter-new-spam (&optional count) "Declare message(s) as spam, and tag them with the computed result of the filtering. Prefix arg N reclassifies the current and the next N - 1 messages. Prefix arg -N reclassifies the current and previous N - 1 messages. When invoked on marked messages (via vm-next-command-uses-marks), only the marked messages in the current folder are reclassified." (interactive "p") ;; make sure we're in the right place in case we're not using marks (vm-follow-summary-cursor) (vm-select-folder-buffer) (vm-check-for-killed-summary) (vm-check-for-killed-presentation) ;; make sure we're allowed to do what we're doing.... ;; XXX is the intention to test the virtual folder's state, or the underlying ;; true folder's state? (vm-error-if-folder-read-only) (vm-error-if-folder-empty) ;; by default we just act on the current message (or count (setq count 1)) (let ((mlist (vm-select-marked-or-prefixed-messages count)) (retag-count 0)) (if (interactive-p) (message "Classifying message(s) as spam... ")) (setq retag-count (vm-bogofilter-retag mlist vm-bogofilter-program-options-new-spam)) (vm-discard-cached-data-internal mlist) (vm-display nil nil '(vm-bogofilter-new-spam) '(vm-bogofilter-new-spam reading-message startup)) (if (interactive-p) (message "%d message%s classified as spam." retag-count (if (= 1 retag-count) "" "s"))) ;; move to the last message processed IFF numeric prefix given and not ;; chasing marks.... (if (and (not (eq last-command 'vm-next-command-uses-marks)) (not (= count 1))) (vm-next-message count t executing-kbd-macro))) (vm-preview-current-message) (vm-update-summary-and-mode-line)) (defun vm-bogofilter-new-clean (&optional count) "Declare message(s) as not being spam, and tag them with the computed result of the filtering. Prefix arg N reclassifies the current and the next N - 1 messages. Prefix arg -N reclassifies the current and previous N - 1 messages. When invoked on marked messages (via vm-next-command-uses-marks), only the marked messages in the current folder are reclassified." (interactive "p") ;; make sure we're in the right place in case we're not using marks (vm-follow-summary-cursor) (vm-select-folder-buffer) (vm-check-for-killed-summary) (vm-check-for-killed-presentation) ;; make sure we're allowed to do what we're doing.... ;; XXX is the intention to test the virtual folder's state, or the underlying ;; true folder's state? (vm-error-if-folder-read-only) (vm-error-if-folder-empty) ;; by default we just act on the current message (or count (setq count 1)) (let ((mlist (vm-select-marked-or-prefixed-messages count)) (retag-count 0)) (if (interactive-p) (message "Classifying message(s) as clean... ")) (setq retag-count (vm-bogofilter-retag mlist vm-bogofilter-program-options-new-clean)) (vm-discard-cached-data-internal mlist) (vm-display nil nil '(vm-bogofilter-new-clean) '(vm-bogofilter-new-clean reading-message startup)) (if (interactive-p) (message "%d message%s classified as clean." retag-count (if (= 1 retag-count) "" "s"))) ;; move to the last message processed IFF numeric prefix given and not ;; chasing marks.... (if (and (not (eq last-command 'vm-next-command-uses-marks)) (not (= count 1))) (vm-next-message count t executing-kbd-macro))) (vm-preview-current-message) (vm-update-summary-and-mode-line)) ;;; Hooking into VM ;; doing this when the file is first loaded is somewhat silly given there's ;; supposed to be a variable that controls whether it's enabled or not -- maybe ;; it should be done in a function set as a `vm-mode-hook' function so that the ;; test can use a value set after the file is loaded. Of course if something ;; resembling this code is incorporated into VM then adding this hook will no ;; doubt be left up to the user to choose to do. ;; (if vm-bogofilter-invoke-through-vm (add-hook 'vm-retrieved-spooled-mail-hook 'vm-bogofilter-filter-arrived-messages)) (provide 'vm-bogofilter) ;;; vm-bogofilter.el ends here