From: yamaoka Date: Fri, 2 May 2003 02:42:20 +0000 (+0000) Subject: Feedback from the t-gnus-6_15 branch. X-Git-Tag: before-feeding-back-t-gnus-6_16-~1 X-Git-Url: http://git.chise.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e6b31519e256eaa52280b45df80d5b436c1539b1;p=elisp%2Fgnus.git- Feedback from the t-gnus-6_15 branch. --- diff --git a/lisp/canlock-om.el b/lisp/canlock-om.el new file mode 100644 index 0000000..831b7f5 --- /dev/null +++ b/lisp/canlock-om.el @@ -0,0 +1,215 @@ +;;; canlock-om.el --- Mule 2 specific functions for canlock +;; Copyright (C) 2001 Katsumi Yamaoka + +;; Author: Katsumi Yamaoka +;; Keywords: mule, cancel-lock + +;; 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, 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 should have received a copy of the GNU General Public License +;; along with this program; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; This program is used to make canlock.el work with Mule 2.3 based on +;; Emacs 19.34. See README.ja in the canlock distribution for details. + +;;; Code: + +(eval-and-compile + (cond ((and (boundp 'emacs-major-version) + (> emacs-major-version 19)) + (error "\ +Error: You should never use canlock-om.el(c) for this environment")) + ((and (boundp 'MULE) + (boundp 'emacs-major-version) + (= emacs-major-version 19) + (>= emacs-minor-version 29))) + (t + (error "Error: Canlock does not support this version of Emacs")))) + +(eval-when-compile + (require 'cl)) + +(require 'custom) +(eval-and-compile + (unless (fboundp 'custom-declare-variable) + (error "Error: Canlock requires new custom"))) + +(eval-when-compile + (unless (fboundp 'byte-compile-file-form-custom-declare-variable) + (defun byte-compile-file-form-custom-declare-variable (form) + ;; Bind defcustom'ed variables. + (if (memq 'free-vars byte-compile-warnings) + (setq byte-compile-bound-variables + (cons (nth 1 (nth 1 form)) byte-compile-bound-variables))) + (if (memq ':version (nthcdr 4 form)) + ;; Make the variable uncustomizable. + `(defvar ,(nth 1 (nth 1 form)) ,(nth 1 (nth 2 form)) + ,(substring (nth 3 form) + (if (string-match "^[\t *]+" (nth 3 form)) + (match-end 0) + 0))) + ;; Ignore unsupported keyword(s). + (if (memq ':set-after (nthcdr 4 form)) + (let ((newform (list (car form) (nth 1 form) + (nth 2 form) (nth 3 form))) + (args (nthcdr 4 form))) + (while args + (or (eq (car args) ':set-after) + (setq newform (nconc newform (list (car args) + (car (cdr args)))))) + (setq args (cdr (cdr args)))) + newform) + form))) + (put 'custom-declare-variable 'byte-hunk-handler + 'byte-compile-file-form-custom-declare-variable)) + + (define-compiler-macro with-temp-buffer (&whole form &rest forms) + (let ((def (if (fboundp 'with-temp-buffer) + (symbol-function 'with-temp-buffer)))) + (if (and def + (consp def) + (or (eq (car def) 'macro) + (and (eq (car def) 'autoload) + (memq (nth 4 def) '(macro t))))) + form + ;; The function definition is imported from APEL. + `(let ((obuffer (current-buffer)) + (buffer (generate-new-buffer " *temp*"))) + (unwind-protect + (progn + (set-buffer buffer) + ,@forms) + (if (buffer-name buffer) + (kill-buffer buffer)) + (if (buffer-live-p obuffer) + (set-buffer obuffer)))))))) + +(autoload 'base64-encode "base64") + +(defcustom canlock-base64-encode-function 'base64-encode-string + "Function to call to base64 encode a string." + :type '(radio (function-item base64-encode-string) + (function-item base64-encode) + (function-item canlock-base64-encode-string-with-mmencode) + (function :tag "Other")) + :group 'canlock) + +(defcustom canlock-mmencode-program "mmencode" + "Name of mmencode program." + :type 'string + :group 'canlock) + +(defcustom canlock-mmencode-args-for-encoding nil + "Arguments passed to mmencode program for encoding." + :type 'sexp + :group 'canlock) + +(defun canlock-base64-encode-string-with-mmencode (string) + "Base64 encode a string using mmencode." + (with-temp-buffer + (setq mc-flag nil) + (insert string) + (let ((default-process-coding-system (cons *iso-2022-jp*dos *noconv*)) + program-coding-system-alist selective-display) + (apply 'call-process-region (point-min) (point-max) + canlock-mmencode-program t t nil + canlock-mmencode-args-for-encoding)) + (goto-char (point-max)) + (skip-chars-backward "\n") + (buffer-substring (point-min) (point)))) + +;; The following macros will only be used to byte-compile canlock.el. +(eval-when-compile + (define-compiler-macro base64-encode-string + (&whole form string &optional no-line-break) + (if (and (string-equal (buffer-name) " *Compiler Input*") + (string-equal ";;; canlock.el" + (buffer-substring (point-min) + (min (+ (point-min) 14) + (point-max))))) + (if no-line-break + `(let ((string ,string)) + (if ,no-line-break + (with-temp-buffer + (insert (funcall canlock-base64-encode-function string)) + (goto-char (point-min)) + (while (search-forward "\n" nil t) + (delete-char -1)) + (buffer-string)) + (funcall canlock-base64-encode-function string))) + `(funcall canlock-base64-encode-function ,string)) + form)) + + (define-compiler-macro split-string (&whole form string &optional pattern) + (if (and (string-equal (buffer-name) " *Compiler Input*") + (string-equal ";;; canlock.el" + (buffer-substring (point-min) + (min (+ (point-min) 14) + (point-max))))) + ;; The function definition is imported from APEL. + (if pattern + `(let ((string ,string) + (pattern ,pattern) + (start 0) + parts) + (while (string-match pattern string start) + (setq parts (cons (substring string + start (match-beginning 0)) + parts) + start (match-end 0))) + (nreverse (cons (substring string start) parts))) + `(let ((string ,string) + (start 0) + parts) + (while (string-match "[ \f\t\n\r\v]+" string start) + (setq parts (cons (substring string + start (match-beginning 0)) + parts) + start (match-end 0))) + (nreverse (cons (substring string start) parts)))) + form))) + +;; The following variables might not be bound if the old version of +;; canlock.el(c) exists. +(eval-when-compile + (defvar canlock-openssl-args) + (defvar canlock-openssl-program)) + +(defun canlock-om-sha1-with-openssl (message) + "Make a SHA-1 digest of MESSAGE using OpenSSL." + (with-temp-buffer + (setq mc-flag nil) + (insert message) + (let ((default-process-coding-system (cons *iso-2022-jp*dos *noconv*)) + program-coding-system-alist selective-display) + (apply 'call-process-region (point-min) (point-max) + canlock-openssl-program t t nil canlock-openssl-args)) + (goto-char (point-min)) + (insert "\"") + (while (re-search-forward "\\([0-9A-Fa-f][0-9A-Fa-f]\\)" nil t) + (replace-match "\\\\x\\1")) + (insert "\"") + (goto-char (point-min)) + (read (current-buffer)))) + +;; Override the original function. +(eval-after-load "canlock" + '(defalias 'canlock-sha1-with-openssl 'canlock-om-sha1-with-openssl)) + +(provide 'canlock-om) + +(require 'canlock) + +;;; canlock-om.el ends here diff --git a/lisp/canlock.el b/lisp/canlock.el new file mode 100644 index 0000000..ee97fd3 --- /dev/null +++ b/lisp/canlock.el @@ -0,0 +1,311 @@ +;;; canlock.el --- functions for Cancel-Lock feature + +;; Copyright (C) 1998, 1999, 2001, 2002, 2003 Free Software Foundation, Inc. + +;; Author: Katsumi Yamaoka +;; Keywords: news, cancel-lock, hmac, sha1, rfc2104 + +;; 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, 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 should have received a copy of the GNU General Public License +;; along with this program; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; Canlock is a library for generating and verifying Cancel-Lock and/or +;; Cancel-Key header in news articles. This is used to protect articles +;; from rogue cancel, supersede or replace attacks. The method is based +;; on draft-ietf-usefor-cancel-lock-01.txt which was released on November +;; 3rd 1998. For instance, you can add Cancel-Lock (and possibly Cancel- +;; Key) header in a news article by using a hook which will be evaluated +;; just before sending an article as follows: +;; +;; (add-hook '*e**a*e-header-hook 'canlock-insert-header t) +;; +;; Verifying Cancel-Lock is mainly a function of news servers, however, +;; you can verify your own article using the command `canlock-verify' in +;; the (raw) article buffer. You will be prompted for the password for +;; each time if the option `canlock-password' or `canlock-password-for- +;; verify' is nil. Note that setting these options is a bit unsafe. + +;;; Code: + +(eval-when-compile + (require 'cl)) + +(autoload 'sha1-binary "sha1-el") +(autoload 'base64-encode-string "base64") +(autoload 'mail-fetch-field "mail-utils") +(defvar mail-header-separator) + +(defgroup canlock nil + "The Cancel-Lock feature." + :group 'applications) + +(defcustom canlock-sha1-function 'sha1-binary + "Function to call to make a SHA-1 message digest." + :type '(radio (function-item sha1-binary) + (function-item canlock-sha1-with-openssl) + (function :tag "Other")) + :group 'canlock) + +(defcustom canlock-sha1-function-for-verify canlock-sha1-function + "Function to call to make a SHA-1 message digest for verifying." + :type '(radio (function-item sha1-binary) + (function-item canlock-sha1-with-openssl) + (function :tag "Other")) + :group 'canlock) + +(defcustom canlock-openssl-program "openssl" + "Name of OpenSSL program." + :type 'string + :group 'canlock) + +(defcustom canlock-openssl-args '("sha1") + "Arguments passed to the OpenSSL program." + :type 'sexp + :group 'canlock) + +(defcustom canlock-ignore-errors nil + "If non-nil, ignore any error signals." + :type 'boolean + :group 'canlock) + +(defcustom canlock-password nil + "Password to use when signing a Cancel-Lock or a Cancel-Key header." + :type 'string + :group 'canlock) + +(defcustom canlock-password-for-verify canlock-password + "Password to use when verifying a Cancel-Lock or a Cancel-Key header." + :type 'string + :group 'canlock) + +(defcustom canlock-force-insert-header nil + "If non-nil, insert a Cancel-Lock or a Cancel-Key header even if the +buffer does not look like a news message." + :type 'boolean + :group 'canlock) + +(defun canlock-sha1-with-openssl (message) + "Make a SHA-1 digest of MESSAGE using OpenSSL." + (let (default-enable-multibyte-characters) + (with-temp-buffer + (let ((coding-system-for-read 'binary) + (coding-system-for-write 'binary) + selective-display + (case-fold-search t)) + (insert message) + (apply 'call-process-region (point-min) (point-max) + canlock-openssl-program t t nil canlock-openssl-args) + (goto-char (point-min)) + (insert "\"") + (while (re-search-forward "\\([0-9a-f][0-9a-f]\\)" nil t) + (replace-match "\\\\x\\1")) + (insert "\"") + (goto-char (point-min)) + (read (current-buffer)))))) + +(eval-when-compile + (defmacro canlock-string-as-unibyte (string) + "Return a unibyte string with the same individual bytes as STRING." + (if (fboundp 'string-as-unibyte) + (list 'string-as-unibyte string) + string))) + +(defun canlock-sha1 (message) + "Make a SHA-1 digest of MESSAGE as a unibyte string of length 20 bytes." + (canlock-string-as-unibyte (funcall canlock-sha1-function message))) + +(defun canlock-make-cancel-key (message-id password) + "Make a Cancel-Key header." + (when (> (length password) 20) + (setq password (canlock-sha1 password))) + (setq password (concat password (make-string (- 64 (length password)) 0))) + (let ((ipad (mapconcat (lambda (byte) + (char-to-string (logxor 54 byte))) + password "")) + (opad (mapconcat (lambda (byte) + (char-to-string (logxor 92 byte))) + password ""))) + (base64-encode-string + (canlock-sha1 + (concat opad + (canlock-sha1 + (concat ipad (canlock-string-as-unibyte message-id)))))))) + +(defun canlock-narrow-to-header () + "Narrow the buffer to the head of the message." + (let (case-fold-search) + (narrow-to-region + (goto-char (point-min)) + (goto-char (if (re-search-forward + (format "^$\\|^%s$" + (regexp-quote mail-header-separator)) + nil t) + (match-beginning 0) + (point-max)))))) + +(defun canlock-delete-headers () + "Delete Cancel-Key or Cancel-Lock headers in the narrowed buffer." + (let ((case-fold-search t)) + (goto-char (point-min)) + (while (re-search-forward "^Cancel-\\(Key\\|Lock\\):" nil t) + (delete-region (match-beginning 0) + (if (re-search-forward "^[^\t ]" nil t) + (goto-char (match-beginning 0)) + (point-max)))))) + +(defun canlock-fetch-fields (&optional key) + "Return a list of the values of Cancel-Lock header. +If KEY is non-nil, look for a Cancel-Key header instead. The buffer +is expected to be narrowed to just the headers of the message." + (let ((field (mail-fetch-field (if key "Cancel-Key" "Cancel-Lock"))) + fields rest + (case-fold-search t)) + (when field + (setq fields (split-string field "[\t\n\r ,]+")) + (while fields + (when (string-match "^sha1:" (setq field (pop fields))) + (push (substring field 5) rest))) + (nreverse rest)))) + +(defun canlock-fetch-id-for-key () + "Return a Message-ID in Cancel, Supersedes or Replaces header. +The buffer is expected to be narrowed to just the headers of the +message." + (or (let ((cancel (mail-fetch-field "Control"))) + (and cancel + (string-match "^cancel[\t ]+\\(<[^\t\n @<>]+@[^\t\n @<>]+>\\)" + cancel) + (match-string 1 cancel))) + (mail-fetch-field "Supersedes") + (mail-fetch-field "Replaces"))) + +;;;###autoload +(defun canlock-insert-header (&optional id-for-key id-for-lock password) + "Insert a Cancel-Key and/or a Cancel-Lock header if possible." + (let (news control key-for-key key-for-lock) + (save-excursion + (save-restriction + (canlock-narrow-to-header) + (when (setq news (or canlock-force-insert-header + (mail-fetch-field "Newsgroups"))) + (unless id-for-key + (setq id-for-key (canlock-fetch-id-for-key))) + (if (and (setq control (mail-fetch-field "Control")) + (string-match + "^cancel[\t ]+\\(<[^\t\n @<>]+@[^\t\n @<>]+>\\)" + control)) + (setq id-for-lock nil) + (unless id-for-lock + (setq id-for-lock (mail-fetch-field "Message-ID")))) + (canlock-delete-headers) + (goto-char (point-max)))) + (when news + (if (not (or id-for-key id-for-lock)) + (message "There are no Message-ID(s)") + (unless password + (setq password (or canlock-password + (read-passwd + "Password for Canlock: ")))) + (if (or (not (stringp password)) (zerop (length password))) + (message "Password for Canlock is bad") + (setq key-for-key (when id-for-key + (canlock-make-cancel-key + id-for-key password)) + key-for-lock (when id-for-lock + (canlock-make-cancel-key + id-for-lock password))) + (if (not (or key-for-key key-for-lock)) + (message "Couldn't insert Canlock header") + (when key-for-key + (insert "Cancel-Key: sha1:" key-for-key "\n")) + (when key-for-lock + (insert "Cancel-Lock: sha1:" + (base64-encode-string (canlock-sha1 key-for-lock)) + "\n"))))))))) + +;;;###autoload +(defun canlock-verify (&optional buffer) + "Verify Cancel-Lock or Cancel-Key in BUFFER. +If BUFFER is nil, the current buffer is assumed. Signal an error if +it fails. You can modify the behavior of this function to return non- +nil instead of to signal an error by setting the option +`canlock-ignore-errors' to non-nil." + (interactive) + (let ((canlock-sha1-function (or canlock-sha1-function-for-verify + canlock-sha1-function)) + keys locks errmsg id-for-key id-for-lock password + key-for-key key-for-lock match) + (save-excursion + (when buffer + (set-buffer buffer)) + (save-restriction + (widen) + (canlock-narrow-to-header) + (setq keys (canlock-fetch-fields 'key) + locks (canlock-fetch-fields)) + (if (not (or keys locks)) + (setq errmsg + "There are neither Cancel-Lock nor Cancel-Key headers") + (setq id-for-key (canlock-fetch-id-for-key) + id-for-lock (mail-fetch-field "Message-ID")) + (or id-for-key id-for-lock + (setq errmsg "There are no Message-ID(s)"))))) + + (if errmsg + (if canlock-ignore-errors + errmsg + (error "%s" errmsg)) + + (setq password (or canlock-password-for-verify + (read-passwd "Password for Canlock: "))) + (if (or (not (stringp password)) (zerop (length password))) + (progn + (setq errmsg "Password for Canlock is bad") + (if canlock-ignore-errors + errmsg + (error "%s" errmsg))) + + (when keys + (when id-for-key + (setq key-for-key (canlock-make-cancel-key id-for-key password)) + (while (and keys (not match)) + (setq match (string-equal key-for-key (pop keys))))) + (setq keys (if match "good" "bad"))) + (setq match nil) + + (when locks + (when id-for-lock + (setq key-for-lock + (base64-encode-string + (canlock-sha1 (canlock-make-cancel-key id-for-lock + password)))) + (when (and locks (not match)) + (setq match (string-equal key-for-lock (pop locks))))) + (setq locks (if match "good" "bad"))) + + (prog1 + (when (member "bad" (list keys locks)) + "bad") + (cond ((and keys locks) + (message "Cancel-Key is %s, Cancel-Lock is %s" keys locks)) + (locks + (message "Cancel-Lock is %s" locks)) + (keys + (message "Cancel-Key is %s" keys)))))))) + +(provide 'canlock) + +;;; canlock.el ends here diff --git a/lisp/compface.el b/lisp/compface.el new file mode 100644 index 0000000..185f949 --- /dev/null +++ b/lisp/compface.el @@ -0,0 +1,57 @@ +;;; compface.el --- functions for converting X-Face headers +;; Copyright (C) 2002 Free Software Foundation, Inc. + +;; Author: Lars Magne Ingebrigtsen +;; Keywords: news + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;;; Code: + +;;;### +(defun uncompface (face) + "Convert FACE to pbm. +Requires the external programs `uncompface', and `icontopbm'. On a +GNU/Linux system these might be in packages with names like `compface' +or `faces-xface' and `netpbm' or `libgr-progs', for instance." + (with-temp-buffer + (insert face) + (and (eq 0 (apply 'call-process-region (point-min) (point-max) + "uncompface" + 'delete '(t nil) nil)) + (progn + (goto-char (point-min)) + (insert "/* Width=48, Height=48 */\n") + ;; I just can't get "icontopbm" to work correctly on its + ;; own in XEmacs. And Emacs doesn't understand un-raw pbm + ;; files. + (if (not (featurep 'xemacs)) + (eq 0 (call-process-region (point-min) (point-max) + "icontopbm" + 'delete '(t nil))) + (shell-command-on-region (point-min) (point-max) + "icontopbm | pnmnoraw" + (current-buffer) t) + t)) + (buffer-string)))) + +(provide 'compface) + +;;; compface.el ends here diff --git a/lisp/deuglify.el b/lisp/deuglify.el new file mode 100644 index 0000000..89be369 --- /dev/null +++ b/lisp/deuglify.el @@ -0,0 +1,470 @@ +;;; deuglify.el --- deuglify broken Outlook (Express) articles + +;; Copyright (C) 2002, 2003 Free Software Foundation, Inc. +;; Copyright (C) 2001, 2002 Raymond Scholz + +;; Author: Raymond Scholz +;; Thomas Steffen (unwrapping algorithm, +;; based on an idea of Stefan Monnier) +;; Keywords: mail, news + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; This file enables Gnus to repair broken citations produced by +;; common user agents like MS Outlook (Express). It may repair +;; articles of other user agents too. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; +;; Outlook sometimes wraps cited lines before sending a message as +;; seen in this example: +;; +;; Example #1 +;; ---------- +;; +;; John Doe wrote: +;; +;; > This sentence no verb. This sentence no verb. This sentence +;; no +;; > verb. This sentence no verb. This sentence no verb. This +;; > sentence no verb. +;; +;; The function `gnus-article-outlook-unwrap-lines' tries to recognize those +;; erroneously wrapped lines and will unwrap them. I.e. putting the +;; wrapped parts ("no" in this example) back where they belong (at the +;; end of the cited line above). +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Note that some people not only use broken user agents but also +;; practice a bad citation style by omitting blank lines between the +;; cited text and their own text. +;: +;; Example #2 +;; ---------- +;; +;; John Doe wrote: +;; +;; > This sentence no verb. This sentence no verb. This sentence no +;; You forgot in all your sentences. +;; > verb. This sentence no verb. This sentence no verb. This +;; > sentence no verb. +;; +;; Unwrapping "You forgot in all your sentences." would be illegal as +;; this part wasn't intended to be cited text. +;; `gnus-article-outlook-unwrap-lines' will only unwrap lines if the resulting +;; citation line will be of a certain maximum length. You can control +;; this by adjusting `gnus-outlook-deuglify-unwrap-max'. Also +;; unwrapping will only be done if the line above the (possibly) +;; wrapped line has a minimum length of `gnus-outlook-deuglify-unwrap-min'. +;; +;; Furthermore no unwrapping will be undertaken if the last character +;; is one of the chars specified in +;; `gnus-outlook-deuglify-unwrap-stop-chars'. Setting this to ".?!" +;; inhibits unwrapping if the cited line ends with a full stop, +;; question mark or exclamation mark. Note that this variable +;; defaults to `nil', triggering a few false positives but generally +;; giving you better results. +;; +;; Unwrapping works on every level of citation. Thus you will be able +;; repair broken citations of broken user agents citing broken +;; citations of broken user agents citing broken citations... +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Citations are commonly introduced with an attribution line +;; indicating who wrote the cited text. Outlook adds superfluous +;; information that can be found in the header of the message to this +;; line and often wraps it. +;; +;; If that weren't enough, lots of people write their own text above +;; the cited text and cite the complete original article below. +;; +;; Example #3 +;; ---------- +;; +;; Hey, John. There's no in all your sentences! +;; +;; John Doe wrote in message +;; news:a87usw8$dklsssa$2@some.news.server... +;; > This sentence no verb. This sentence no verb. This sentence +;; no +;; > verb. This sentence no verb. This sentence no verb. This +;; > sentence no verb. +;; > +;; > Bye, John +;; +;; Repairing the attribution line will be done by function +;; `gnus-article-outlook-repair-attribution which calls other function that +;; try to recognize and repair broken attribution lines. See variable +;; `gnus-outlook-deuglify-attrib-cut-regexp' for stuff that should be +;; cut off from the beginning of an attribution line and variable +;; `gnus-outlook-deuglify-attrib-verb-regexp' for the verbs that are +;; required to be found in an attribution line. These function return +;; the point where the repaired attribution line starts. +;; +;; Rearranging the article so that the cited text appears above the +;; new text will be done by function +;; `gnus-article-outlook-rearrange-citation'. This function calls +;; `gnus-article-outlook-repair-attribution to find and repair an attribution +;; line. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Well, and that's what the message will look like after applying +;; deuglification: +;; +;; Example #3 (deuglified) +;; ----------------------- +;; +;; John Doe wrote: +;; +;; > This sentence no verb. This sentence no verb. This sentence no +;; > verb. This sentence no verb. This sentence no verb. This +;; > sentence no verb. +;; > +;; > Bye, John +;; +;; Hey, John. There's no in all your sentences! +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Usage +;; ----- +;; +;; Press `W k' in the Summary Buffer. +;; +;; Non recommended usage :-) +;; --------------------- +;; +;; To automatically invoke deuglification on every article you read, +;; put something like that in your .gnus: +;; +;; (add-hook 'gnus-article-decode-hook 'gnus-article-outlook-unwrap-lines) +;; +;; or _one_ of the following lines: +;; +;; ;; repair broken attribution lines +;; (add-hook 'gnus-article-decode-hook 'gnus-article-outlook-repair-attribution) +;; +;; ;; repair broken attribution lines and citations +;; (add-hook 'gnus-article-decode-hook 'gnus-article-outlook-rearrange-citation) +;; +;; Note that there always may be some false positives, so I suggest +;; using the manual invocation. After deuglification you may want to +;; refill the whole article using `W w'. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Limitations +;; ----------- +;; +;; As I said before there may (or will) be a few false positives on +;; unwrapping cited lines with `gnus-article-outlook-unwrap-lines'. +;; +;; `gnus-article-outlook-repair-attribution will only fix the first +;; attribution line found in the article. Furthermore it fixed to +;; certain kinds of attributions. And there may be horribly many +;; false positives, vanishing lines and so on -- so don't trust your +;; eyes. Again I recommend manual invocation. +;; +;; `gnus-article-outlook-rearrange-citation' carries all the limitations of +;; `gnus-article-outlook-repair-attribution. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; See ChangeLog for other changes. +;; +;; Revision 1.5 2002/01/27 14:39:17 rscholz +;; * New variable `gnus-outlook-deuglify-no-wrap-chars' to inhibit +;; unwrapping if one these chars is first in the possibly wrapped line. +;; * Improved rearranging of the article. +;; * New function `gnus-outlook-repair-attribution-block' for repairing +;; those big "Original Message (following some headers)" attributions. +;; +;; Revision 1.4 2002/01/03 14:05:00 rscholz +;; Renamed `gnus-outlook-deuglify-article' to +;; `gnus-article-outlook-deuglify-article'. +;; Made it easier to deuglify the article while being in Gnus' Article +;; Edit Mode. (suggested by Phil Nitschke) +;; +;; +;; Revision 1.3 2002/01/02 23:35:54 rscholz +;; Fix a bug that caused succeeding long attribution lines to be +;; unwrapped. Minor doc fixes and regular expression tuning. +;; +;; Revision 1.2 2001/12/30 20:14:34 rscholz +;; Clean up source. +;; +;; Revision 1.1 2001/12/30 20:13:32 rscholz +;; Initial revision +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;; Code: + +(require 'gnus-art) +(require 'gnus-sum) + +(defconst gnus-outlook-deuglify-version "1.5 Gnus version" + "Version of gnus-outlook-deuglify.") + +;;; User Customizable Variables: + +(defgroup gnus-outlook-deuglify nil + "Deuglify articles generated by broken user agents like MS Outlook (Express).") + +;;;###autoload +(defcustom gnus-outlook-deuglify-unwrap-min 45 + "Minimum length of the cited line above the (possibly) wrapped line." + :type 'number + :group 'gnus-outlook-deuglify) + +;;;###autoload +(defcustom gnus-outlook-deuglify-unwrap-max 95 + "Maximum length of the cited line after unwrapping." + :type 'number + :group 'gnus-outlook-deuglify) + +(defcustom gnus-outlook-deuglify-cite-marks ">|#%" + "Characters that indicate cited lines." + :type 'string + :group 'gnus-outlook-deuglify) + +(defcustom gnus-outlook-deuglify-unwrap-stop-chars nil ;; ".?!" or nil + "Characters that inhibit unwrapping if they are the last one on the cited line above the possible wrapped line." + :type 'string + :group 'gnus-outlook-deuglify) + +(defcustom gnus-outlook-deuglify-no-wrap-chars "`" + "Characters that inhibit unwrapping if they are the first one in the possibly wrapped line." + :type 'string + :group 'gnus-outlook-deuglify) + +(defcustom gnus-outlook-deuglify-attrib-cut-regexp + "\\(On \\|Am \\)?\\(Mon\\|Tue\\|Wed\\|Thu\\|Fri\\|Sat\\|Sun\\),[^,]+, " + "Regular expression matching the beginning of an attribution line that should be cut off." + :type 'string + :group 'gnus-outlook-deuglify) + +(defcustom gnus-outlook-deuglify-attrib-verb-regexp + "wrote\\|writes\\|says\\|schrieb\\|schreibt\\|meinte\\|skrev\\|a écrit\\|schreef\\|escribió" + "Regular expression matching the verb used in an attribution line." + :type 'string + :group 'gnus-outlook-deuglify) + +(defcustom gnus-outlook-deuglify-attrib-end-regexp + ": *\\|\\.\\.\\." + "Regular expression matching the end of an attribution line." + :type 'string + :group 'gnus-outlook-deuglify) + +;;;###autoload +(defcustom gnus-outlook-display-hook nil + "A hook called after an deuglified article has been prepared. +It is run after `gnus-article-prepare-hook'." + :type 'hook + :group 'gnus-outlook-deuglify) + +;; Functions + +(defun gnus-outlook-display-article-buffer () + "Redisplay current buffer or article buffer." + (with-current-buffer (or gnus-article-buffer (current-buffer)) + ;; "Emulate" `gnus-article-prepare-display' without calling + ;; it. Calling `gnus-article-prepare-display' on an already + ;; prepared article removes all MIME parts. I'm unsure whether + ;; this is a bug or not. + (gnus-article-highlight t) + (gnus-treat-article nil) + (gnus-run-hooks 'gnus-article-prepare-hook + 'gnus-outlook-display-hook))) + +;;;###autoload +(defun gnus-article-outlook-unwrap-lines (&optional nodisplay) + "Unwrap lines that appear to be wrapped citation lines. +You can control what lines will be unwrapped by frobbing +`gnus-outlook-deuglify-unwrap-min' and `gnus-outlook-deuglify-unwrap-max', +indicating the miminum and maximum length of an unwrapped citation line. If +NODISPLAY is non-nil, don't redisplay the article buffer." + (interactive "P") + (save-excursion + (let ((case-fold-search nil) + (inhibit-read-only t) + (cite-marks gnus-outlook-deuglify-cite-marks) + (no-wrap gnus-outlook-deuglify-no-wrap-chars) + (stop-chars gnus-outlook-deuglify-unwrap-stop-chars)) + (gnus-with-article-buffer + (article-goto-body) + (while (re-search-forward + (concat + "^\\([ \t" cite-marks "]*\\)" + "\\([" cite-marks "].*[^\n " stop-chars "]\\)[ \t]?\n" + "\\1\\([^\n " cite-marks no-wrap "]+.*\\)$") + nil t) + (let ((len12 (- (match-end 2) (match-beginning 1))) + (len3 (- (match-end 3) (match-beginning 3)))) + (if (and (> len12 gnus-outlook-deuglify-unwrap-min) + (< (+ len12 len3) gnus-outlook-deuglify-unwrap-max)) + (progn + (replace-match "\\1\\2 \\3") + (goto-char (match-beginning 0))))))))) + (unless nodisplay (gnus-outlook-display-article-buffer))) + +(defun gnus-outlook-rearrange-article (attr-start) + "Put the text from `attr-start' to the end of buffer at the top of the article buffer." + (save-excursion + (let ((inhibit-read-only t) + (cite-marks gnus-outlook-deuglify-cite-marks)) + (gnus-with-article-buffer + (article-goto-body) + ;; article does not start with attribution + (unless (= (point) attr-start) + (gnus-kill-all-overlays) + (let ((cur (point)) + ;; before signature or end of buffer + (to (if (gnus-article-search-signature) + (point) + (point-max)))) + ;; handle the case where the full quote is below the + ;; signature + (if (< to attr-start) + (setq to (point-max))) + (transpose-regions cur attr-start attr-start to))))))) + +;; John Doe wrote in message +;; news:a87usw8$dklsssa$2@some.news.server... + +(defun gnus-outlook-repair-attribution-outlook () + "Repair a broken attribution line (Outlook)." + (save-excursion + (let ((case-fold-search nil) + (inhibit-read-only t) + (cite-marks gnus-outlook-deuglify-cite-marks)) + (gnus-with-article-buffer + (article-goto-body) + (if (re-search-forward + (concat "^\\([^" cite-marks "].+\\)" + "\\(" gnus-outlook-deuglify-attrib-verb-regexp "\\)" + "\\(.*\n?[^\n" cite-marks "].*\\)?" + "\\(" gnus-outlook-deuglify-attrib-end-regexp "\\)$") + nil t) + (progn + (gnus-kill-all-overlays) + (replace-match "\\1\\2\\4") + (match-beginning 0))))))) + + +;; ----- Original Message ----- +;; From: "John Doe" +;; To: "Doe Foundation" +;; Sent: Monday, November 19, 2001 12:13 PM +;; Subject: More Doenuts + +(defun gnus-outlook-repair-attribution-block () + "Repair a big broken attribution block." + (save-excursion + (let ((case-fold-search nil) + (inhibit-read-only t) + (cite-marks gnus-outlook-deuglify-cite-marks)) + (gnus-with-article-buffer + (article-goto-body) + (if (re-search-forward + (concat "^[" cite-marks " \t]*--* ?[^-]+ [^-]+ ?--*\\s *\n" + "[^\n:]+:[ \t]*\\([^\n]+\\)\n" + "\\([^\n:]+:[ \t]*[^\n]+\n\\)+") + nil t) + (progn + (gnus-kill-all-overlays) + (replace-match "\\1 wrote:\n") + (match-beginning 0))))))) + +;; On Wed, 16 Jan 2002 23:23:30 +0100, John Doe wrote: + +(defun gnus-outlook-repair-attribution-other () + "Repair a broken attribution line (other user agents than Outlook)." + (save-excursion + (let ((case-fold-search nil) + (inhibit-read-only t) + (cite-marks gnus-outlook-deuglify-cite-marks)) + (gnus-with-article-buffer + (article-goto-body) + (if (re-search-forward + (concat "^\\("gnus-outlook-deuglify-attrib-cut-regexp"\\)?" + "\\([^" cite-marks "].+\\)\n\\([^\n" cite-marks "].*\\)?" + "\\(" gnus-outlook-deuglify-attrib-verb-regexp "\\).*" + "\\(" gnus-outlook-deuglify-attrib-end-regexp "\\)$") + nil t) + (progn + (gnus-kill-all-overlays) + (replace-match "\\4 \\5\\6\\7") + (match-beginning 0))))))) + +;;;###autoload +(defun gnus-article-outlook-repair-attribution (&optional nodisplay) + "Repair a broken attribution line. +If NODISPLAY is non-nil, don't redisplay the article buffer." + (interactive "P") + (let ((attrib-start + (or + (gnus-outlook-repair-attribution-other) + (gnus-outlook-repair-attribution-block) + (gnus-outlook-repair-attribution-outlook)))) + (unless nodisplay (gnus-outlook-display-article-buffer)) + attrib-start)) + +(defun gnus-article-outlook-rearrange-citation (&optional nodisplay) + "Repair broken citations. +If NODISPLAY is non-nil, don't redisplay the article buffer." + (interactive "P") + (let ((attrib-start (gnus-article-outlook-repair-attribution 'nodisplay))) + ;; rearrange citations if an attribution line has been recognized + (if attrib-start + (gnus-outlook-rearrange-article attrib-start))) + (unless nodisplay (gnus-outlook-display-article-buffer))) + +;;;###autoload +(defun gnus-outlook-deuglify-article (&optional nodisplay) + "Full deuglify of broken Outlook (Express) articles. +Treat dumbquotes, unwrap lines, repair attribution and rearrange citation. If +NODISPLAY is non-nil, don't redisplay the article buffer." + (interactive "P") + ;; apply treatment of dumb quotes + (gnus-article-treat-dumbquotes) + ;; repair wrapped cited lines + (gnus-article-outlook-unwrap-lines 'nodisplay) + ;; repair attribution line and rearrange citation. + (gnus-article-outlook-rearrange-citation 'nodisplay) + (unless nodisplay (gnus-outlook-display-article-buffer))) + +;;;###autoload +(defun gnus-article-outlook-deuglify-article () + "Deuglify broken Outlook (Express) articles and redisplay." + (interactive) + (gnus-outlook-deuglify-article nil)) + +(provide 'deuglify) + +;; Local Variables: +;; coding: iso-8859-1 +;; End: + +;;; deuglify.el ends here diff --git a/lisp/dig.el b/lisp/dig.el new file mode 100644 index 0000000..e71d6db --- /dev/null +++ b/lisp/dig.el @@ -0,0 +1,188 @@ +;;; dig.el --- Domain Name System dig interface +;; Copyright (c) 2000, 2001, 2003 Free Software Foundation, Inc. + +;; Author: Simon Josefsson +;; Keywords: DNS BIND dig + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your +;; option) any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; This provide an interface for "dig". +;; +;; For interactive use, try M-x dig and type a hostname. Use `q' to quit +;; dig buffer. +;; +;; For use in elisp programs, call `dig-invoke' and use +;; `dig-extract-rr' to extract resource records. + +;;; Release history: + +;; 2000-10-28 posted on gnu.emacs.sources + +;;; Code: + +(eval-when-compile (require 'cl)) + +(defgroup dig nil + "Dig configuration.") + +(defcustom dig-program "dig" + "Name of dig (domain information groper) binary." + :type 'file + :group 'dig) + +(defcustom dig-dns-server nil + "DNS server to query. +If nil, use system defaults." + :type '(choice (const :tag "System defaults") + string) + :group 'dig) + +(defcustom dig-font-lock-keywords + '(("^;; [A-Z]+ SECTION:" 0 font-lock-keyword-face) + ("^;;.*" 0 font-lock-comment-face) + ("^; <<>>.*" 0 font-lock-type-face) + ("^;.*" 0 font-lock-function-name-face)) + "Default expressions to highlight in dig mode." + :type 'sexp + :group 'dig) + +(defun dig-invoke (domain &optional + query-type query-class query-option + dig-option server) + "Call dig with given arguments and return buffer containing output. +DOMAIN is a string with a DNS domain. QUERY-TYPE is an optional string +with a DNS type. QUERY-CLASS is an optional string with a DNS class. +QUERY-OPTION is an optional string with dig \"query options\". +DIG-OPTIONS is an optional string with parameters for the dig program. +SERVER is an optional string with a domain name server to query. + +Dig is an external program found in the BIND name server distribution, +and is a commonly available debugging tool." + (let (buf cmdline) + (setq buf (generate-new-buffer "*dig output*")) + (if dig-option (push dig-option cmdline)) + (if query-option (push query-option cmdline)) + (if query-class (push query-class cmdline)) + (if query-type (push query-type cmdline)) + (push domain cmdline) + (if server (push (concat "@" server) cmdline) + (if dig-dns-server (push (concat "@" dig-dns-server) cmdline))) + (apply 'call-process dig-program nil buf nil cmdline) + buf)) + +(defun dig-extract-rr (domain &optional type class) + "Extract resource records for DOMAIN, TYPE and CLASS from buffer. +Buffer should contain output generated by `dig-invoke'." + (save-excursion + (goto-char (point-min)) + (if (re-search-forward + (concat domain "\\.?[\t ]+[0-9wWdDhHmMsS]+[\t ]+" + (upcase (or class "IN")) "[\t ]+" (upcase (or type "A"))) + nil t) + (let (b e) + (end-of-line) + (setq e (point)) + (beginning-of-line) + (setq b (point)) + (when (search-forward " (" e t) + (search-forward " )")) + (end-of-line) + (setq e (point)) + (buffer-substring b e)) + (and (re-search-forward (concat domain "\\.?[\t ]+[0-9wWdDhHmMsS]+[\t ]+" + (upcase (or class "IN")) + "[\t ]+CNAME[\t ]+\\(.*\\)$") nil t) + (dig-extract-rr (match-string 1) type class))))) + +(defun dig-rr-get-pkix-cert (rr) + (let (b e str) + (string-match "[^\t ]+[\t ]+[0-9wWdDhHmMsS]+[\t ]+IN[\t ]+CERT[\t ]+\\(1\\|PKIX\\)[\t ]+[0-9]+[\t ]+[0-9]+[\t ]+(?" rr) + (setq b (match-end 0)) + (string-match ")" rr) + (setq e (match-beginning 0)) + (setq str (substring rr b e)) + (while (string-match "[\t \n\r]" str) + (setq str (replace-match "" nil nil str))) + str)) + +;; XEmacs does it like this. For Emacs, we have to set the +;; `font-lock-defaults' buffer-local variable. +(put 'dig-mode 'font-lock-defaults '(dig-font-lock-keywords t)) + +(put 'dig-mode 'mode-class 'special) + +(defvar dig-mode-map nil) +(unless dig-mode-map + (setq dig-mode-map (make-sparse-keymap)) + (suppress-keymap dig-mode-map) + + (define-key dig-mode-map "q" 'dig-exit)) + +(defun dig-mode () + "Major mode for displaying dig output." + (interactive) + (kill-all-local-variables) + (setq mode-name "dig") + (setq major-mode 'dig-mode) + (use-local-map dig-mode-map) + (buffer-disable-undo) + (unless (featurep 'xemacs) + (set (make-local-variable 'font-lock-defaults) + '(dig-font-lock-keywords t))) + (when (featurep 'font-lock) + (font-lock-set-defaults))) + +(defun dig-exit () + "Quit dig output buffer." + (interactive) + (kill-buffer (current-buffer))) + +(defun dig (domain &optional + query-type query-class query-option dig-option server) + "Query addresses of a DOMAIN using dig, by calling `dig-invoke'. +Optional arguments are passed to `dig-invoke'." + (interactive "sHost: ") + (switch-to-buffer + (dig-invoke domain query-type query-class query-option dig-option server)) + (goto-char (point-min)) + (and (search-forward ";; ANSWER SECTION:" nil t) + (forward-line)) + (dig-mode) + (setq buffer-read-only t) + (set-buffer-modified-p nil)) + +;; named for consistency with query-dns in dns.el +(defun query-dig (domain &optional + query-type query-class query-option dig-option server) + "Query addresses of a DOMAIN using dig. +It works by calling `dig-invoke' and `dig-extract-rr'. Optional +arguments are passed to `dig-invoke' and `dig-extract-rr'. Returns +nil for domain/class/type queries that results in no data." +(let ((buffer (dig-invoke domain query-type query-class + query-option dig-option server))) + (when buffer + (switch-to-buffer buffer) + (let ((digger (dig-extract-rr domain query-type query-class))) + (kill-buffer buffer) + digger)))) + +(provide 'dig) + +;;; dig.el ends here diff --git a/lisp/dns.el b/lisp/dns.el new file mode 100644 index 0000000..44a002a --- /dev/null +++ b/lisp/dns.el @@ -0,0 +1,358 @@ +;;; dns.el --- Domain Name Service lookups +;; Copyright (C) 2002, 2003 Free Software Foundation, Inc. + +;; Author: Lars Magne Ingebrigtsen +;; Keywords: network + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;;; Code: + +(eval-when-compile (require 'cl)) + +(require 'mm-util) + +(defvar dns-timeout 5 + "How many seconds to wait when doing DNS queries.") + +(defvar dns-servers nil + "Which DNS servers to query. +If nil, /etc/resolv.conf will be consulted.") + +;;; Internal code: + +(defvar dns-query-types + '((A 1) + (NS 2) + (MD 3) + (MF 4) + (CNAME 5) + (SOA 6) + (MB 7) + (MG 8) + (MR 9) + (NULL 10) + (WKS 11) + (PRT 12) + (HINFO 13) + (MINFO 14) + (MX 15) + (TXT 16) + (AXFR 252) + (MAILB 253) + (MAILA 254) + (* 255)) + "Names of query types and their values.") + +(defvar dns-classes + '((IN 1) + (CS 2) + (CH 3) + (HS 4)) + "Classes of queries.") + +(defun dns-write-bytes (value &optional length) + (let (bytes) + (dotimes (i (or length 1)) + (push (% value 256) bytes) + (setq value (/ value 256))) + (dolist (byte bytes) + (insert byte)))) + +(defun dns-read-bytes (length) + (let ((value 0)) + (dotimes (i length) + (setq value (logior (* value 256) (following-char))) + (forward-char 1)) + value)) + +(defun dns-get (type spec) + (cadr (assq type spec))) + +(defun dns-inverse-get (value spec) + (let ((found nil)) + (while (and (not found) + spec) + (if (eq value (cadr (car spec))) + (setq found (caar spec)) + (pop spec))) + found)) + +(defun dns-write-name (name) + (dolist (part (split-string name "\\.")) + (dns-write-bytes (length part)) + (insert part)) + (dns-write-bytes 0)) + +(defun dns-read-string-name (string buffer) + (mm-with-unibyte-buffer + (insert string) + (goto-char (point-min)) + (dns-read-name buffer))) + +(defun dns-read-name (&optional buffer) + (let ((ended nil) + (name nil) + length) + (while (not ended) + (setq length (dns-read-bytes 1)) + (if (= 192 (logand length (lsh 3 6))) + (let ((offset (+ (* (logand 63 length) 256) + (dns-read-bytes 1)))) + (save-excursion + (when buffer + (set-buffer buffer)) + (goto-char (1+ offset)) + (setq ended (dns-read-name buffer)))) + (if (zerop length) + (setq ended t) + (push (buffer-substring (point) + (progn (forward-char length) (point))) + name)))) + (if (stringp ended) + (if (null name) + ended + (concat (mapconcat 'identity (nreverse name) ".") "." ended)) + (mapconcat 'identity (nreverse name) ".")))) + +(defun dns-write (spec &optional tcp-p) + "Write a DNS packet according to SPEC. +If TCP-P, the first two bytes of the package with be the length field." + (with-temp-buffer + (dns-write-bytes (dns-get 'id spec) 2) + (dns-write-bytes + (logior + (lsh (if (dns-get 'response-p spec) 1 0) -7) + (lsh + (cond + ((eq (dns-get 'opcode spec) 'query) 0) + ((eq (dns-get 'opcode spec) 'inverse-query) 1) + ((eq (dns-get 'opcode spec) 'status) 2) + (t (error "No such opcode: %s" (dns-get 'opcode spec)))) + -3) + (lsh (if (dns-get 'authoritative-p spec) 1 0) -2) + (lsh (if (dns-get 'truncated-p spec) 1 0) -1) + (lsh (if (dns-get 'recursion-desired-p spec) 1 0) 0))) + (dns-write-bytes + (cond + ((eq (dns-get 'response-code spec) 'no-error) 0) + ((eq (dns-get 'response-code spec) 'format-error) 1) + ((eq (dns-get 'response-code spec) 'server-failure) 2) + ((eq (dns-get 'response-code spec) 'name-error) 3) + ((eq (dns-get 'response-code spec) 'not-implemented) 4) + ((eq (dns-get 'response-code spec) 'refused) 5) + (t 0))) + (dns-write-bytes (length (dns-get 'queries spec)) 2) + (dns-write-bytes (length (dns-get 'answers spec)) 2) + (dns-write-bytes (length (dns-get 'authorities spec)) 2) + (dns-write-bytes (length (dns-get 'additionals spec)) 2) + (dolist (query (dns-get 'queries spec)) + (dns-write-name (car query)) + (dns-write-bytes (cadr (assq (or (dns-get 'type query) 'A) + dns-query-types)) 2) + (dns-write-bytes (cadr (assq (or (dns-get 'class query) 'IN) + dns-classes)) 2)) + (dolist (slot '(answers authorities additionals)) + (dolist (resource (dns-get slot spec)) + (dns-write-name (car resource)) + (dns-write-bytes (cadr (assq (dns-get 'type resource) dns-query-types)) + 2) + (dns-write-bytes (cadr (assq (dns-get 'class resource) dns-classes)) + 2) + (dns-write-bytes (dns-get 'ttl resource) 4) + (dns-write-bytes (length (dns-get 'data resource)) 2) + (insert (dns-get 'data resource)))) + (when tcp-p + (goto-char (point-min)) + (dns-write-bytes (buffer-size) 2)) + (buffer-string))) + +(defun dns-read (packet) + (mm-with-unibyte-buffer + (let ((spec nil) + queries answers authorities additionals) + (insert packet) + (goto-char (point-min)) + (push (list 'id (dns-read-bytes 2)) spec) + (let ((byte (dns-read-bytes 1))) + (push (list 'response-p (if (zerop (logand byte (lsh 1 7))) nil t)) + spec) + (let ((opcode (logand byte (lsh 7 3)))) + (push (list 'opcode + (cond ((eq opcode 0) 'query) + ((eq opcode 1) 'inverse-query) + ((eq opcode 2) 'status))) + spec)) + (push (list 'authoritative-p (if (zerop (logand byte (lsh 1 2))) + nil t)) spec) + (push (list 'truncated-p (if (zerop (logand byte (lsh 1 2))) nil t)) + spec) + (push (list 'recursion-desired-p + (if (zerop (logand byte (lsh 1 0))) nil t)) spec)) + (let ((rc (logand (dns-read-bytes 1) 15))) + (push (list 'response-code + (cond + ((eq rc 0) 'no-error) + ((eq rc 1) 'format-error) + ((eq rc 2) 'server-failure) + ((eq rc 3) 'name-error) + ((eq rc 4) 'not-implemented) + ((eq rc 5) 'refused))) + spec)) + (setq queries (dns-read-bytes 2)) + (setq answers (dns-read-bytes 2)) + (setq authorities (dns-read-bytes 2)) + (setq additionals (dns-read-bytes 2)) + (let ((qs nil)) + (dotimes (i queries) + (push (list (dns-read-name) + (list 'type (dns-inverse-get (dns-read-bytes 2) + dns-query-types)) + (list 'class (dns-inverse-get (dns-read-bytes 2) + dns-classes))) + qs)) + (push (list 'queries qs) spec)) + (dolist (slot '(answers authorities additionals)) + (let ((qs nil) + type) + (dotimes (i (symbol-value slot)) + (push (list (dns-read-name) + (list 'type + (setq type (dns-inverse-get (dns-read-bytes 2) + dns-query-types))) + (list 'class (dns-inverse-get (dns-read-bytes 2) + dns-classes)) + (list 'ttl (dns-read-bytes 4)) + (let ((length (dns-read-bytes 2))) + (list 'data + (dns-read-type + (buffer-substring + (point) + (progn (forward-char length) (point))) + type)))) + qs)) + (push (list slot qs) spec))) + (nreverse spec)))) + +(defun dns-read-type (string type) + (let ((buffer (current-buffer)) + (point (point))) + (prog1 + (mm-with-unibyte-buffer + (insert string) + (goto-char (point-min)) + (cond + ((eq type 'A) + (let ((bytes nil)) + (dotimes (i 4) + (push (dns-read-bytes 1) bytes)) + (mapconcat 'number-to-string (nreverse bytes) "."))) + ((eq type 'NS) + (dns-read-string-name string buffer)) + ((eq type 'CNAME) + (dns-read-string-name string buffer)) + (t string))) + (goto-char point)))) + +(defun dns-parse-resolv-conf () + (when (file-exists-p "/etc/resolv.conf") + (with-temp-buffer + (insert-file-contents "/etc/resolv.conf") + (goto-char (point-min)) + (while (re-search-forward "^nameserver[\t ]+\\([^ \t\n]+\\)" nil t) + (push (match-string 1) dns-servers)) + (setq dns-servers (nreverse dns-servers))))) + +;;; Interface functions. + +(autoload 'gnus-xmacs-open-network-stream "gnus-xmas" nil nil 'macro) + +(defmacro dns-make-network-process (server) + (if (featurep 'xemacs) + `(let ((coding-system-for-read 'binary) + (coding-system-for-write 'binary)) + (gnus-xmas-open-network-stream "dns" (current-buffer) + ,server "domain" 'udp)) + `(let ((server ,server) + (coding-system-for-read 'binary) + (coding-system-for-write 'binary) + (default-process-coding-system '(binary . binary)) + program-coding-system-alist) + (if (fboundp 'make-network-process) + (make-network-process + :name "dns" + :coding 'binary + :buffer (current-buffer) + :host server + :service "domain" + :type 'datagram) + ;; Older versions of Emacs doesn't have + ;; `make-network-process', so we fall back on opening a TCP + ;; connection to the DNS server. + (open-network-stream "dns" (current-buffer) server "domain"))))) + +(defun query-dns (name &optional type fullp) + "Query a DNS server for NAME of TYPE. +If FULLP, return the entire record returned." + (setq type (or type 'A)) + (unless dns-servers + (dns-parse-resolv-conf) + (unless dns-servers + (error "No DNS server configuration found"))) + (mm-with-unibyte-buffer + (let ((process (condition-case () + (dns-make-network-process (car dns-servers)) + (error + (message "dns: Got an error while trying to talk to %s" + (car dns-servers)) + nil))) + (tcp-p (and (not (fboundp 'make-network-process)) + (not (featurep 'xemacs)))) + (step 100) + (times (* dns-timeout 1000)) + (id (random 65000))) + (when process + (process-send-string + process + (dns-write `((id ,id) + (opcode query) + (queries ((,name (type ,type)))) + (recursion-desired-p t)) + tcp-p)) + (while (and (zerop (buffer-size)) + (> times 0)) + (accept-process-output process 0 step) + (decf times step)) + (ignore-errors + (delete-process process)) + (when tcp-p + (goto-char (point-min)) + (delete-region (point) (+ (point) 2))) + (unless (zerop (buffer-size)) + (let ((result (dns-read (buffer-string)))) + (if fullp + result + (let ((answer (car (dns-get 'answers result)))) + (when (eq type (dns-get 'type answer)) + (dns-get 'data answer)))))))))) + +(provide 'dns) + +;;; dns.el ends here diff --git a/lisp/gnus-delay.el b/lisp/gnus-delay.el new file mode 100644 index 0000000..cc212fa --- /dev/null +++ b/lisp/gnus-delay.el @@ -0,0 +1,195 @@ +;;; gnus-delay.el --- Delayed posting of articles + +;; Copyright (C) 2001, 2002, 2003 Free Software Foundation, Inc. + +;; Author: Kai Großjohann +;; Keywords: mail, news, extensions + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your +;; option) any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; Provide delayed posting of articles. + +;;; Todo: + +;; * `gnus-delay-send-queue' barfs when group does not exist. +;; * Integrate gnus-delay.el into the rest of Gnus automatically. How +;; should this be done? Basically, we need to do what +;; `gnus-delay-initialize' does. But in which files? + +;;; Code: + +(require 'nndraft) +(require 'gnus-draft) + +;;;###autoload +(defgroup gnus-delay nil + "Arrange for sending postings later." + :group 'gnus) + +(defcustom gnus-delay-group "delayed" + "Group name for storing delayed articles." + :type 'string + :group 'gnus-delay) + +(defcustom gnus-delay-header "X-Gnus-Delayed" + "Header name for storing info about delayed articles." + :type 'string + :group 'gnus-delay) + +(defcustom gnus-delay-default-delay "3d" + "*Default length of delay." + :type 'string + :group 'gnus-delay) + +(defcustom gnus-delay-default-hour 8 + "*If deadline is given as date, then assume this time of day." + :type 'integer + :group 'gnus-delay) + +;;;###autoload +(defun gnus-delay-article (delay) + "Delay this article by some time. +DELAY is a string, giving the length of the time. Possible values are: + +* for in minutes (`m'), hours (`h'), days (`d'), + weeks (`w'), months (`M'), or years (`Y'); + +* YYYY-MM-DD for a specific date. The time of day is given by the + variable `gnus-delay-default-hour', minute and second are zero. + +* hh:mm for a specific time. Use 24h format. If it is later than this + time, then the deadline is tomorrow, else today." + (interactive + (list (read-string + "Target date (YYYY-MM-DD) or length of delay (units in [mhdwMY]): " + gnus-delay-default-delay))) + (let (num unit days year month day hour minute deadline) + (cond ((string-match + "\\([0-9][0-9][0-9]?[0-9]?\\)-\\([0-9]+\\)-\\([0-9]+\\)" + delay) + (setq year (string-to-number (match-string 1 delay)) + month (string-to-number (match-string 2 delay)) + day (string-to-number (match-string 3 delay))) + (setq deadline + (message-make-date + (encode-time 0 0 ; second and minute + gnus-delay-default-hour + day month year)))) + ((string-match "\\([0-9]+\\):\\([0-9]+\\)" delay) + (setq hour (string-to-number (match-string 1 delay)) + minute (string-to-number (match-string 2 delay))) + ;; Use current time, except... + (setq deadline (apply 'vector (decode-time (current-time)))) + ;; ... for minute and hour. + (aset deadline 1 minute) + (aset deadline 2 hour) + ;; Convert to seconds. + (setq deadline (time-to-seconds (apply 'encode-time + (append deadline nil)))) + ;; If this time has passed already, add a day. + (when (< deadline (time-to-seconds (current-time))) + (setq deadline (+ 3600 deadline))) ;3600 secs/day + ;; Convert seconds to date header. + (setq deadline (message-make-date + (seconds-to-time deadline)))) + ((string-match "\\([0-9]+\\)\\s-*\\([mhdwMY]\\)" delay) + (setq num (match-string 1 delay)) + (setq unit (match-string 2 delay)) + ;; Start from seconds, then multiply into needed units. + (setq num (string-to-number num)) + (cond ((string= unit "Y") + (setq delay (* num 60 60 24 365))) + ((string= unit "M") + (setq delay (* num 60 60 24 30))) + ((string= unit "w") + (setq delay (* num 60 60 24 7))) + ((string= unit "d") + (setq delay (* num 60 60 24))) + ((string= unit "h") + (setq delay (* num 60 60))) + (t + (setq delay (* num 60)))) + (setq deadline (message-make-date + (seconds-to-time (+ (time-to-seconds (current-time)) + delay))))) + (t (error "Malformed delay `%s'" delay))) + (message-add-header (format "%s: %s" gnus-delay-header deadline))) + (set-buffer-modified-p t) + ;; If group does not exist, create it. + (let ((group (format "nndraft:%s" gnus-delay-group))) + (gnus-agent-queue-setup gnus-delay-group)) + (message-disassociate-draft) + (nndraft-request-associate-buffer gnus-delay-group) + (save-buffer 0) + (kill-buffer (current-buffer)) + (message-do-actions message-postpone-actions)) + +;;;###autoload +(defun gnus-delay-send-queue () + "Send all the delayed messages that are due now." + (interactive) + (save-excursion + (let* ((group (format "nndraft:%s" gnus-delay-group)) + (message-send-hook (copy-sequence message-send-hook)) + articles + article deadline) + (when (gnus-gethash group gnus-newsrc-hashtb) + (gnus-activate-group group) + (add-hook 'message-send-hook + '(lambda () + (message-remove-header gnus-delay-header))) + (setq articles (nndraft-articles)) + (while (setq article (pop articles)) + (gnus-request-head article group) + (set-buffer nntp-server-buffer) + (goto-char (point-min)) + (if (re-search-forward + (concat "^" (regexp-quote gnus-delay-header) ":\\s-+") + nil t) + (progn + (setq deadline (nnheader-header-value)) + (setq deadline (apply 'encode-time + (parse-time-string deadline))) + (setq deadline (time-since deadline)) + (when (and (>= (nth 0 deadline) 0) + (>= (nth 1 deadline) 0)) + (message "Sending delayed article %d" article) + (gnus-draft-send article group) + (message "Sending delayed article %d...done" article))) + (message "Delay header missing for article %d" article))))))) + +;;;###autoload +(defun gnus-delay-initialize (&optional no-keymap no-check) + "Initialize the gnus-delay package. +This sets up a key binding in `message-mode' to delay a message. +This tells Gnus to look for delayed messages after getting new news. + +The optional arg NO-KEYMAP is ignored. +Checking delayed messages is skipped if optional arg NO-CHECK is non-nil." + (unless no-check + (add-hook 'gnus-get-new-news-hook 'gnus-delay-send-queue))) + +(provide 'gnus-delay) + +;; Local Variables: +;; coding: iso-8859-1 +;; End: + +;;; gnus-delay.el ends here diff --git a/lisp/gnus-diary.el b/lisp/gnus-diary.el new file mode 100644 index 0000000..dafb8c3 --- /dev/null +++ b/lisp/gnus-diary.el @@ -0,0 +1,469 @@ +;;; gnus-diary.el --- Wrapper around the NNDiary Gnus backend + +;; Copyright (c) 2001, 2002, 2003 Free Software Foundation, Inc. +;; Copyright (C) 1999, 2000, 2001 Didier Verna. + +;; Author: Didier Verna +;; Maintainer: Didier Verna +;; Created: Tue Jul 20 10:42:55 1999 +;; Keywords: calendar mail news + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software +;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + +;;; Commentary: + +;; Contents management by FCM version 0.1. + +;; Description: +;; =========== + +;; Gnus-Diary is a wrapper around the NNDiary Gnus backend. It is here to +;; make your nndiary-user life easier in different ways. So, you don't have +;; to use it if you don't want to. But, really, you should. + +;; Gnus-Diary offers the following features on top of the NNDiary backend: + +;; - A nice summary line format: +;; Displaying diary messages in standard summary line format (usually +;; something like ": ") is pretty useless. Most of the +;; time, you're the one who wrote the message, and you mostly want to see +;; the event's date. Gnus-Diary offers you a nice summary line format +;; which will do this. By default, a summary line will appear like this: +;; +;; : +;; +;; for example, here's how Joe's birthday is displayed in my +;; "nndiary:birhdays" summary buffer (the message is expirable, but will +;; never be deleted, as it specifies a regular event): +;; +;; E Sat, Sep 22 01, 12:00: Joe's birthday (in 6 months, 1 week) + +;; - More article sorting functions: +;; Gnus-Diary adds a new sorting function called +;; `gnus-summary-sort-by-schedule'. This function lets you organize your +;; diary summary buffers from the closest event to the farthest one. + +;; - Automatic generation of diary group parameters: +;; When you create a new diary group, or visit one, Gnus-Diary checks your +;; group parameters, and if needed, sets the summary line format to the +;; diary-specific value, adds the diary-specific sorting functions, and +;; also adds the different `X-Diary-*' headers to the group's +;; posting-style. It is then easier to send a diary message, because if +;; you use `C-u a' or `C-u m' on a diary group to prepare a message, these +;; headers will be inserted automatically (but not filled with proper +;; values yet). + +;; - An interactive mail-to-diary convertion function: +;; The function `gnus-diary-check-message' ensures that the current message +;; contains all the required diary headers, and prompts you for values / +;; correction if needed. This function is hooked in the nndiary backend so +;; that moving an article to an nndiary group will trigger it +;; automatically. It is also bound to `C-c D c' in message-mode and +;; article-edit-mode in order to ease the process of converting a usual +;; mail to a diary one. This function takes a prefix argument which will +;; force prompting of all diary headers, regardless of their +;; presence/validity. That way, you can very easily reschedule a diary +;; message for instance. + + +;; Usage: +;; ===== + +;; 0/ Don't use any `gnus-user-format-function-[d|D]'. Gnus-Diary provides +;; both of these (sorry if you used them before). +;; 1/ Add '(require 'gnus-diary) to your gnusrc file. +;; 2/ Customize your gnus-diary options to suit your needs. + + + +;; Bugs / Todo: +;; =========== + + +;;; Code: + +(require 'nndiary) +(require 'message) +(require 'gnus-art) + +(defgroup gnus-diary nil + "Utilities on top of the nndiary backend for Gnus.") + +(defcustom gnus-diary-summary-line-format "%U%R%z %uD: %(%s%) (%ud)\n" + "*Summary line format for nndiary groups." + :type 'string + :group 'gnus-diary + :group 'gnus-summary-format) + +(defcustom gnus-diary-time-format "%a, %b %e %y, %H:%M" + "*Time format to display appointements in nndiary summary buffers. +Please refer to `format-time-string' for information on possible values." + :type 'string + :group 'gnus-diary) + +(defcustom gnus-diary-delay-format-function 'gnus-diary-delay-format-english + "*Function called to format a diary delay string. +It is passed two arguments. The first one is non nil if the delay is in +the past. The second one is of the form ((NUM . UNIT) ...) where NUM is +an integer and UNIT is one of 'year 'month 'week 'day 'hour or 'minute. +It should return strings like \"In 2 months, 3 weeks\", \"3 hours, +1 minute ago\" and so on. + +There are currently two built-in format functions: +`gnus-diary-delay-format-english' (the default) +`gnus-diary-delay-format-french'" + :type '(choice (const :tag "english" gnus-diary-delay-format-english) + (const :tag "french" gnus-diary-delay-format-french) + (symbol :tag "other")) + :group 'gnus-diary) + +(defconst gnus-diary-version nndiary-version + "Current Diary backend version.") + + +;; Compatibility functions ================================================== + +(eval-and-compile + (if (fboundp 'kill-entire-line) + (defalias 'gnus-diary-kill-entire-line 'kill-entire-line) + (defun gnus-diary-kill-entire-line () + (beginning-of-line) + (let ((kill-whole-line t)) + (kill-line))))) + + +;; Summary line format ====================================================== + +(defun gnus-diary-delay-format-french (past delay) + (if (null delay) + "maintenant!" + ;; Keep only a precision of two degrees + (and (> (length delay) 1) (setcdr (cdr delay) nil)) + (concat (if past "il y a " "dans ") + (let ((str "") + del) + (while (setq del (pop delay)) + (setq str (concat str + (int-to-string (car del)) " " + (cond ((eq (cdr del) 'year) + "an") + ((eq (cdr del) 'month) + "mois") + ((eq (cdr del) 'week) + "semaine") + ((eq (cdr del) 'day) + "jour") + ((eq (cdr del) 'hour) + "heure") + ((eq (cdr del) 'minute) + "minute")) + (unless (or (eq (cdr del) 'month) + (= (car del) 1)) + "s") + (if delay ", ")))) + str)))) + + +(defun gnus-diary-delay-format-english (past delay) + (if (null delay) + "now!" + ;; Keep only a precision of two degrees + (and (> (length delay) 1) (setcdr (cdr delay) nil)) + (concat (unless past "in ") + (let ((str "") + del) + (while (setq del (pop delay)) + (setq str (concat str + (int-to-string (car del)) " " + (symbol-name (cdr del)) + (and (> (car del) 1) "s") + (if delay ", ")))) + str) + (and past " ago")))) + + +(defun gnus-diary-header-schedule (headers) + ;; Same as `nndiary-schedule', but given a set of headers HEADERS + (mapcar + (lambda (elt) + (let ((head (cdr (assoc (intern (format "X-Diary-%s" (car elt))) + headers)))) + (when head + (nndiary-parse-schedule-value head (cadr elt) (caddr elt))))) + nndiary-headers)) + +;; #### NOTE: Gnus sometimes gives me a HEADER not corresponding to any +;; message, with all fields set to nil here. I don't know what it is for, and +;; I just ignore it. +(defun gnus-user-format-function-d (header) + ;; Returns an aproximative delay string for the next occurence of this + ;; message. The delay is given only in the first non zero unit. + ;; Code partly stolen from article-make-date-line + (let* ((extras (mail-header-extra header)) + (sched (gnus-diary-header-schedule extras)) + (occur (nndiary-next-occurence sched (current-time))) + (now (current-time)) + (real-time (subtract-time occur now))) + (if (null real-time) + "?????" + (let* ((sec (+ (* (float (car real-time)) 65536) (cadr real-time))) + (past (< sec 0)) + delay) + (and past (setq sec (- sec))) + (unless (zerop sec) + ;; This is a bit convoluted, but basically we go through the time + ;; units for years, weeks, etc, and divide things to see whether + ;; that results in positive answers. + (let ((units `((year . ,(* 365.25 24 3600)) + (month . ,(* 31 24 3600)) + (week . ,(* 7 24 3600)) + (day . ,(* 24 3600)) + (hour . 3600) + (minute . 60))) + unit num) + (while (setq unit (pop units)) + (unless (zerop (setq num (ffloor (/ sec (cdr unit))))) + (setq delay (append delay `((,(floor num) . ,(car unit)))))) + (setq sec (- sec (* num (cdr unit))))))) + (funcall gnus-diary-delay-format-function past delay))) + )) + +;; #### NOTE: Gnus sometimes gives me a HEADER not corresponding to any +;; message, with all fields set to nil here. I don't know what it is for, and +;; I just ignore it. +(defun gnus-user-format-function-D (header) + ;; Returns a formatted time string for the next occurence of this message. + (let* ((extras (mail-header-extra header)) + (sched (gnus-diary-header-schedule extras)) + (occur (nndiary-next-occurence sched (current-time)))) + (format-time-string gnus-diary-time-format occur))) + + +;; Article sorting functions ================================================ + +(defun gnus-article-sort-by-schedule (h1 h2) + (let* ((now (current-time)) + (e1 (mail-header-extra h1)) + (e2 (mail-header-extra h2)) + (s1 (gnus-diary-header-schedule e1)) + (s2 (gnus-diary-header-schedule e2)) + (o1 (nndiary-next-occurence s1 now)) + (o2 (nndiary-next-occurence s2 now))) + (if (and (= (car o1) (car o2)) (= (cadr o1) (cadr o2))) + (< (mail-header-number h1) (mail-header-number h2)) + (time-less-p o1 o2)))) + + +(defun gnus-thread-sort-by-schedule (h1 h2) + (gnus-article-sort-by-schedule (gnus-thread-header h1) + (gnus-thread-header h2))) + +(defun gnus-summary-sort-by-schedule (&optional reverse) + "Sort nndiary summary buffers by schedule of appointements. +Optional prefix (or REVERSE argument) means sort in reverse order." + (interactive "P") + (gnus-summary-sort 'schedule reverse)) + +(defvar gnus-summary-misc-menu) ;; Avoid byte compiler warning. +;; The function `easy-menu-add-item' is not available under Emacs +;; versions prior to 20.3. Could anyone try to emulate it? +(if (eval-when-compile + (require 'easymenu) + (or (fboundp 'easy-menu-add-item) + (progn + (defalias 'easy-menu-add-item 'ignore) + nil))) +(add-hook 'gnus-summary-menu-hook + (lambda () + (easy-menu-add-item gnus-summary-misc-menu + '("Sort") + ["Sort by schedule" + gnus-summary-sort-by-schedule + (eq (car (gnus-find-method-for-group + gnus-newsgroup-name)) + 'nndiary)] + "Sort by number"))) + ) + + + +;; Group parameters autosetting ============================================= + +(defun gnus-diary-update-group-parameters (group) + ;; Ensure that nndiary groups have convenient group parameters: + ;; - a posting style containing X-Diary headers + ;; - a nice summary line format + ;; - NNDiary specific sorting by schedule functions + ;; In general, try not to mess with what the user might have modified. + (let ((posting-style (gnus-group-get-parameter group 'posting-style t))) + ;; Posting style: + (mapcar (lambda (elt) + (let ((header (format "X-Diary-%s" (car elt)))) + (unless (assoc header posting-style) + (setq posting-style (append posting-style + `((,header "*"))))) + )) + nndiary-headers) + (gnus-group-set-parameter group 'posting-style posting-style) + ;; Summary line format: + (unless (gnus-group-get-parameter group 'gnus-summary-line-format t) + (gnus-group-set-parameter group 'gnus-summary-line-format + `(,gnus-diary-summary-line-format))) + ;; Sorting by schedule: + (unless (gnus-group-get-parameter group 'gnus-article-sort-functions) + (gnus-group-set-parameter group 'gnus-article-sort-functions + '((append gnus-article-sort-functions + (list + 'gnus-article-sort-by-schedule))))) + (unless (gnus-group-get-parameter group 'gnus-thread-sort-functions) + (gnus-group-set-parameter group 'gnus-thread-sort-functions + '((append gnus-thread-sort-functions + (list + 'gnus-thread-sort-by-schedule))))) + )) + +;; Called when a group is subscribed. This is needed because groups created +;; because of mail splitting are *not* created with the backend function. +;; Thus, `nndiary-request-create-group-hooks' is inoperative. +(defun gnus-diary-maybe-update-group-parameters (group) + (when (eq (car (gnus-find-method-for-group group)) 'nndiary) + (gnus-diary-update-group-parameters group))) + +(add-hook 'nndiary-request-create-group-hooks + 'gnus-diary-update-group-parameters) +;; Now that we have `gnus-subscribe-newsgroup-hooks', this is not needed +;; anymore. Maybe I should remove this completely. +(add-hook 'nndiary-request-update-info-hooks + 'gnus-diary-update-group-parameters) +(add-hook 'gnus-subscribe-newsgroup-hooks + 'gnus-diary-maybe-update-group-parameters) + + +;; Diary Message Checking =================================================== + +(defvar gnus-diary-header-value-history nil + ;; History variable for header value prompting + ) + +(defun gnus-diary-narrow-to-headers () + "Narrow the current buffer to the header part. +Point is left at the beginning of the region. +The buffer is assumed to contain a message, but the format is unknown." + (cond ((eq major-mode 'message-mode) + (message-narrow-to-headers)) + (t + (goto-char (point-min)) + (when (search-forward "\n\n" nil t) + (narrow-to-region (point-min) (- (point) 1)) + (goto-char (point-min)))) + )) + +(defun gnus-diary-add-header (str) + "Add a header to the current buffer. +The buffer is assumed to contain a message, but the format is unknown." + (cond ((eq major-mode 'message-mode) + (message-add-header str)) + (t + (save-restriction + (gnus-diary-narrow-to-headers) + (goto-char (point-max)) + (if (string-match "\n$" str) + (insert str) + (insert str ?\n)))) + )) + +(defun gnus-diary-check-message (arg) + "Ensure that the current message is a valid for NNDiary. +This function checks that all NNDiary required headers are present and +valid, and prompts for values / correction otherwise. + +If ARG (or prefix) is non-nil, force prompting for all fields." + (interactive "P") + (save-excursion + (mapcar + (lambda (head) + (let ((header (concat "X-Diary-" (car head))) + (ask arg) + value invalid) + ;; First, try to find the header, and checks for validity: + (save-restriction + (gnus-diary-narrow-to-headers) + (when (re-search-forward (concat "^" header ":") nil t) + (unless (eq (char-after) ? ) + (insert " ")) + (setq value (buffer-substring (point) (gnus-point-at-eol))) + (and (string-match "[ \t]*\\([^ \t]+\\)[ \t]*" value) + (setq value (match-string 1 value))) + (condition-case () + (nndiary-parse-schedule-value value + (nth 1 head) (nth 2 head)) + (t + (setq invalid t))) + ;; #### NOTE: this (along with the `gnus-diary-add-header' + ;; function) could be rewritten in a better way, in particular + ;; not to blindly remove an already present header and reinsert + ;; it somewhere else afterwards. + (when (or ask invalid) + (gnus-diary-kill-entire-line)) + )) + ;; Now, loop until a valid value is provided: + (while (or ask (not value) invalid) + (let ((prompt (concat (and invalid + (prog1 "(current value invalid) " + (beep))) + header ": "))) + (setq value + (if (listp (nth 1 head)) + (completing-read prompt (cons '("*" nil) (nth 1 head)) + nil t value + gnus-diary-header-value-history) + (read-string prompt value + gnus-diary-header-value-history)))) + (setq ask nil) + (setq invalid nil) + (condition-case () + (nndiary-parse-schedule-value value + (nth 1 head) (nth 2 head)) + (t + (setq invalid t)))) + (gnus-diary-add-header (concat header ": " value)) + )) + nndiary-headers) + )) + +(add-hook 'nndiary-request-accept-article-hooks + (lambda () (gnus-diary-check-message nil))) + +(define-key message-mode-map "\C-cDc" 'gnus-diary-check-message) +(define-key gnus-article-edit-mode-map "\C-cDc" 'gnus-diary-check-message) + + +;; The end ================================================================== + +(defun gnus-diary-version () + "Current Diary backend version." + (interactive) + (message "NNDiary version %s" nndiary-version)) + +(define-key message-mode-map "\C-cDv" 'gnus-diary-version) +(define-key gnus-article-edit-mode-map "\C-cDv" 'gnus-diary-version) + + +(provide 'gnus-diary) + +;;; gnus-diary.el ends here diff --git a/lisp/gnus-dired.el b/lisp/gnus-dired.el new file mode 100644 index 0000000..cf54427 --- /dev/null +++ b/lisp/gnus-dired.el @@ -0,0 +1,206 @@ +;;; gnus-dired.el --- utility functions where gnus and dired meet + +;; Copyright (C) 1996, 1997, 1998, 1999, 2001, 2002, 2003 +;; Free Software Foundation, Inc. + +;; Authors: Benjamin Rutt , +;; Shenghuo Zhu +;; Keywords: mail, news, extensions + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; This package provides utility functions for intersections of gnus +;; and dired. To enable the gnus-dired-mode minor mode which will +;; have the effect of installing keybindings in dired-mode, place the +;; following in your ~/.gnus: + +;; (require 'gnus-dired) ;, isn't needed due to autoload cookies +;; (add-hook 'dired-mode-hook 'turn-on-gnus-dired-mode) + +;; Note that if you visit dired buffers before your ~/.gnus file has +;; been read, those dired buffers won't have the keybindings in +;; effect. To get around that problem, you may want to add the above +;; statements to your ~/.emacs instead. + +;;; Code: + +(require 'dired) +(require 'gnus-ems) +(require 'gnus-msg) +(require 'gnus-util) +(require 'message) +(require 'mm-encode) +(require 'mml) + +(defvar gnus-dired-mode nil + "Minor mode for intersections of gnus and dired.") + +(defvar gnus-dired-mode-map nil) + +(unless gnus-dired-mode-map + (setq gnus-dired-mode-map (make-sparse-keymap)) + + (gnus-define-keys gnus-dired-mode-map + "\C-c\C-a" gnus-dired-attach + "\C-c\C-l" gnus-dired-find-file-mailcap + "\C-cP" gnus-dired-print)) + +(defun gnus-dired-mode (&optional arg) + "Minor mode for intersections of gnus and dired. + +\\{gnus-dired-mode-map}" + (interactive "P") + (when (eq major-mode 'dired-mode) + (set (make-local-variable 'gnus-dired-mode) + (if (null arg) (not gnus-dired-mode) + (> (prefix-numeric-value arg) 0))) + (when gnus-dired-mode + (gnus-add-minor-mode 'gnus-dired-mode "" gnus-dired-mode-map) + (gnus-run-hooks 'gnus-dired-mode-hook)))) + +;;;###autoload +(defun turn-on-gnus-dired-mode () + "Convenience method to turn on gnus-dired-mode." + (gnus-dired-mode 1)) + +;; Method to attach files to a gnus composition. +(defun gnus-dired-attach (files-to-attach) + "Attach dired's marked files to a gnus message composition. +If called non-interactively, FILES-TO-ATTACH should be a list of +filenames." + (interactive + (list + (delq nil + (mapcar + ;; don't attach directories + (lambda (f) (if (file-directory-p f) nil f)) + (nreverse (dired-map-over-marks (dired-get-filename) nil)))))) + (let ((destination nil) + (files-str nil) + (bufs nil)) + ;; warn if user tries to attach without any files marked + (if (null files-to-attach) + (error "No files to attach") + (setq files-str + (mapconcat + (lambda (f) (file-name-nondirectory f)) + files-to-attach ", ")) + (setq bufs (message-buffers)) + + ;; set up destination message buffer + (if (and bufs + (y-or-n-p "Attach files to existing message buffer? ")) + (setq destination + (if (= (length bufs) 1) + (get-buffer (car bufs)) + (completing-read "Attach to which message buffer: " + (mapcar + (lambda (b) + (cons b (get-buffer b))) + bufs) + nil t))) + ;; setup a new gnus message buffer + (gnus-setup-message 'message (message-mail)) + (setq destination (current-buffer))) + + ;; set buffer to destination buffer, and attach files + (set-buffer destination) + (goto-char (point-max)) ;attach at end of buffer + (while files-to-attach + (mml-attach-file (car files-to-attach) + (or (mm-default-file-encoding (car files-to-attach)) + "application/octet-stream") nil) + (setq files-to-attach (cdr files-to-attach))) + (message "Attached file(s) %s" files-str)))) + +(autoload 'mailcap-parse-mailcaps "mailcap" "" t) + +(defun gnus-dired-find-file-mailcap (&optional file-name arg) + "In dired, visit FILE-NAME according to the mailcap file. +If ARG is non-nil, open it in a new buffer." + (interactive (list + (file-name-sans-versions (dired-get-filename) t) + current-prefix-arg)) + (mailcap-parse-mailcaps) + (if (file-exists-p file-name) + (let (mime-type method) + (if (and (not arg) + (not (file-directory-p file-name)) + (string-match "\\.[^\\.]+$" file-name) + (setq mime-type + (mailcap-extension-to-mime + (match-string 0 file-name))) + (stringp + (setq method + (cdr (assoc 'viewer + (car (mailcap-mime-info mime-type + 'all))))))) + (let ((view-command (mm-mailcap-command method file-name nil))) + (message "viewing via %s" view-command) + (start-process "*display*" + nil + shell-file-name + shell-command-switch + view-command)) + (find-file file-name))) + (if (file-symlink-p file-name) + (error "File is a symlink to a nonexistent target") + (error "File no longer exists; type `g' to update Dired buffer")))) + +(defun gnus-dired-print (&optional file-name print-to) + "In dired, print FILE-NAME according to the mailcap file. + +If there is no print command, print in a PostScript image. If the +optional argument PRINT-TO is nil, send the image to the printer. If +PRINT-TO is a string, save the PostScript image in a file with that +name. If PRINT-TO is a number, prompt the user for the name of the +file to save in." + (interactive (list + (file-name-sans-versions (dired-get-filename) t) + (ps-print-preprint current-prefix-arg))) + (mailcap-parse-mailcaps) + (cond + ((file-directory-p file-name) + (error "Can't print a directory")) + ((file-exists-p file-name) + (let (mime-type method) + (if (and (string-match "\\.[^\\.]+$" file-name) + (setq mime-type + (mailcap-extension-to-mime + (match-string 0 file-name))) + (stringp + (setq method (mailcap-mime-info mime-type "print")))) + (call-process shell-file-name nil + (generate-new-buffer " *mm*") + nil + shell-command-switch + (mm-mailcap-command method file-name mime-type)) + (with-temp-buffer + (insert-file-contents file-name) + (gnus-print-buffer)) + (ps-despool print-to)))) + ((file-symlink-p file-name) + (error "File is a symlink to a nonexistent target")) + (t + (error "File no longer exists; type `g' to update Dired buffer")))) + +(provide 'gnus-dired) + +;;; gnus-dired.el ends here diff --git a/lisp/gnus-fun.el b/lisp/gnus-fun.el new file mode 100644 index 0000000..8791640 --- /dev/null +++ b/lisp/gnus-fun.el @@ -0,0 +1,251 @@ +;;; gnus-fun.el --- various frivolous extension functions to Gnus +;; Copyright (C) 2002, 2003 Free Software Foundation, Inc. + +;; Author: Lars Magne Ingebrigtsen +;; Keywords: news + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;;; Code: + +(eval-when-compile + (require 'cl) + (require 'mm-util)) + +(defcustom gnus-x-face-directory (expand-file-name "x-faces" gnus-directory) + "*Directory where X-Face PBM files are stored." + :group 'gnus-fun + :type 'directory) + +(defcustom gnus-convert-pbm-to-x-face-command "pbmtoxbm %s | compface" + "Command for converting a PBM to an X-Face." + :group 'gnus-fun + :type 'string) + +(defcustom gnus-convert-image-to-x-face-command "giftopnm %s | ppmnorm | pnmscale -width 48 -height 48 | ppmtopgm | pgmtopbm | pbmtoxbm | compface" + "Command for converting an image to an X-Face. +By default it takes a GIF filename and output the X-Face header data +on stdout." + :group 'gnus-fun + :type 'string) + +(defcustom gnus-convert-image-to-face-command "djpeg %s | ppmnorm | pnmscale -width 48 -height 48 | ppmquant %d | pnmtopng" + "Command for converting an image to an Face. +By default it takes a JPEG filename and output the Face header data +on stdout." + :group 'gnus-fun + :type 'string) + +(defun gnus-shell-command-to-string (command) + "Like `shell-command-to-string' except not mingling ERROR." + (with-output-to-string + (call-process shell-file-name nil (list standard-output nil) + nil shell-command-switch command))) + +(defun gnus-shell-command-on-region (start end command) + "A simplified `shell-command-on-region'. +Output to the current buffer, replace text, and don't mingle error." + (call-process-region start end shell-file-name t + (list (current-buffer) nil) + nil shell-command-switch command)) + +;;;###autoload +(defun gnus-random-x-face () + "Return X-Face header data chosen randomly from `gnus-x-face-directory'." + (interactive) + (when (file-exists-p gnus-x-face-directory) + (let* ((files (directory-files gnus-x-face-directory t "\\.pbm$")) + (file (nth (random (length files)) files))) + (when file + (gnus-shell-command-to-string + (format gnus-convert-pbm-to-x-face-command + (shell-quote-argument file))))))) + +;;;###autoload +(defun gnus-insert-random-x-face-header () + "Insert a random X-Face header from `gnus-x-face-directory'." + (interactive) + (let ((data (gnus-random-x-face))) + (save-excursion + (message-goto-eoh) + (if data + (insert "X-Face: " data) + (message + "No face returned by `gnus-random-x-face'. Does %s/*.pbm exist?" + gnus-x-face-directory))))) + +;;;###autoload +(defun gnus-x-face-from-file (file) + "Insert an X-Face header based on an image file." + (interactive "fImage file name (by default GIF): ") + (when (file-exists-p file) + (gnus-shell-command-to-string + (format gnus-convert-image-to-x-face-command + (shell-quote-argument (expand-file-name file)))))) + +;;;###autoload +(defun gnus-face-from-file (file) + "Return an Face header based on an image file." + (interactive "fImage file name (by default JPEG): ") + (when (file-exists-p file) + (let ((done nil) + (attempt "") + (quant 16)) + (while (and (not done) + (> quant 1)) + (setq attempt + (let ((coding-system-for-read 'binary)) + (gnus-shell-command-to-string + (format gnus-convert-image-to-face-command + (shell-quote-argument (expand-file-name file)) + quant)))) + (if (> (length attempt) 726) + (progn + (setq quant (- quant 2)) + (message "Length %d; trying quant %d" + (length attempt) quant)) + (setq done t))) + (if done + (mm-with-unibyte-buffer + (insert attempt) + (gnus-face-encode)) + nil)))) + +(defun gnus-face-encode () + (let ((step 72)) + (base64-encode-region (point-min) (point-max)) + (goto-char (point-min)) + (while (search-forward "\n" nil t) + (replace-match "")) + (goto-char (point-min)) + (while (> (- (point-max) (point)) + step) + (forward-char step) + (insert "\n ") + (setq step 76)) + (buffer-string))) + +;;;###autoload +(defun gnus-convert-face-to-png (face) + "Convert FACE (which is base64-encoded) to a PNG. +The PNG is returned as a string." + (mm-with-unibyte-buffer + (insert face) + (ignore-errors + (base64-decode-region (point-min) (point-max))) + (buffer-string))) + +;;;###autoload +(defun gnus-convert-png-to-face (file) + "Convert FILE to a Face. +FILE should be a PNG file that's 48x48 and smaller than or equal to +726 bytes." + (mm-with-unibyte-buffer + (insert-file-contents file) + (when (> (buffer-size) 726) + (error "The file is %d bytes long, which is too long" + (buffer-size))) + (gnus-face-encode))) + +(defface gnus-x-face '((t (:foreground "black" :background "white"))) + "Face to show X-Face. +The colors from this face are used as the foreground and background +colors of the displayed X-Faces." + :group 'gnus-article-headers) + +(defun gnus-display-x-face-in-from (data) + "Display the X-Face DATA in the From header." + (let ((default-enable-multibyte-characters nil) + pbm) + (when (or (gnus-image-type-available-p 'xface) + (and (gnus-image-type-available-p 'pbm) + (setq pbm (uncompface data)))) + (save-excursion + (save-restriction + (article-narrow-to-head) + (gnus-article-goto-header "from") + (when (bobp) + (insert "From: [no `from' set]\n") + (forward-char -17)) + (gnus-add-image + 'xface + (gnus-put-image + (if (gnus-image-type-available-p 'xface) + (gnus-create-image + (concat "X-Face: " data) + 'xface t :ascent 'center :face 'gnus-x-face) + (gnus-create-image + pbm 'pbm t :ascent 'center :face 'gnus-x-face)))) + (gnus-add-wash-type 'xface)))))) + +(defun gnus-grab-cam-x-face () + "Grab a picture off the camera and make it into an X-Face." + (interactive) + (shell-command "xawtv-remote snap ppm") + (let ((file nil)) + (while (null (setq file (directory-files "/tftpboot/sparky/tmp" + t "snap.*ppm"))) + (sleep-for 1)) + (setq file (car file)) + (with-temp-buffer + (shell-command + (format "pnmcut -left 110 -top 30 -width 144 -height 144 '%s' | ppmnorm 2>/dev/null | pnmscale -width 48 | ppmtopgm | pgmtopbm -threshold -value 0.92 | pbmtoxbm | compface" + file) + (current-buffer)) + ;;(sleep-for 3) + (delete-file file) + (buffer-string)))) + +(defun gnus-grab-cam-face () + "Grab a picture off the camera and make it into an X-Face." + (interactive) + (shell-command "xawtv-remote snap ppm") + (let ((file nil) + result) + (while (null (setq file (directory-files "/tftpboot/sparky/tmp" + t "snap.*ppm"))) + (sleep-for 1)) + (setq file (car file)) + (shell-command + (format "pnmcut -left 110 -top 30 -width 144 -height 144 '%s' | pnmscale -width 48 -height 48 | ppmtopgm > /tmp/gnus.face.ppm" + file)) + (let ((gnus-convert-image-to-face-command + (format "cat '%%s' | ppmquant %%d | ppmchange %s | pnmtopng" + (gnus-fun-ppm-change-string)))) + (setq result (gnus-face-from-file "/tmp/gnus.face.ppm"))) + (delete-file file) + ;;(delete-file "/tmp/gnus.face.ppm") + result)) + +(defun gnus-fun-ppm-change-string () + (let* ((possibilites '("%02x0000" "00%02x00" "0000%02x" + "%02x%02x00" "00%02x%02x" "%02x00%02x")) + (format (concat "'#%02x%02x%02x' '#" + (nth (random 6) possibilites) + "'")) + (values nil)) + (dotimes (i 255) + (push (format format i i i i i i) + values)) + (mapconcat 'identity values " "))) + +(provide 'gnus-fun) + +;;; gnus-fun.el ends here diff --git a/lisp/gnus-namazu.el b/lisp/gnus-namazu.el new file mode 100644 index 0000000..5ef95eb --- /dev/null +++ b/lisp/gnus-namazu.el @@ -0,0 +1,907 @@ +;;; gnus-namazu.el --- Search mail with Namazu -*- coding: iso-2022-7bit; -*- + +;; Copyright (C) 2000,2001,2002 TSUCHIYA Masatoshi + +;; Author: TSUCHIYA Masatoshi +;; Keywords: mail searching namazu + +;; This file is a part of Semi-Gnus. + +;; 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, 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 should have received a copy of the GNU General Public License +;; along with this program; if not, you can either send email to this +;; program's maintainer or write to: The Free Software Foundation, +;; Inc.; 59 Temple Place, Suite 330; Boston, MA 02111-1307, USA. + + +;;; Commentary: + +;; This file defines the command to search mails and persistent +;; articles with Namazu and to browse its results with Gnus. +;; +;; Namazu is a full-text search engine intended for easy use. For +;; more detail about Namazu, visit the following page: +;; +;; http://namazu.org/ + + +;;; Quick Start: + +;; If this module has already been installed, only 3 steps are +;; required to search articles with this module. +;; +;; (1) Install Namazu. +;; (2) Start Gnus and type M-x gnus-namazu-create-index RET to make +;; index of articles. +;; (3) In group buffer or in summary buffer, type C-c C-n query RET. + + +;;; Install: + +;; Before installing this module, you must install Namazu. +;; +;; This file is a part of T-gnus but is not *YET* a part of Gnus. +;; When you would like to use this module in Gnus (not T-gnus), put +;; this file into the lisp/ directory in the Gnus source tree and run +;; `make install'. And then, put the following expression into your +;; ~/.gnus. +;; +;; (require 'gnus-namazu) +;; (gnus-namazu-insinuate) +;; +;; In order to make index of articles with Namazu before using this +;; module, type M-x gnus-namazu-create-index RET. Otherwise, you can +;; create index by yourself with the following commands: +;; +;; % mkdir ~/News/namazu +;; % mknmz -a -h -O ~/News/namazu ~/Mail ~/News/cache +;; +;; The first command makes the directory for index files, and the +;; second command generates index files of mails and persistent +;; articles. +;; +;; In order to update indices for incoming articles, this module +;; automatically runs mknmz, the indexer of Namazu, at an interval of +;; 3 days; this period is set to `gnus-namazu-index-update-interval'. +;; +;; Indices will be updated when `gnus-namazu-search' is called. If +;; you want to update indices everywhen Gnus is started, you can put +;; the following expression to your ~/.gnus. +;; +;; (add-hook 'gnus-startup-hook 'gnus-namazu-update-all-indices) +;; +;; In order to control mknmz closely, disable the automatic updating +;; feature and run mknmz by yourself. In this case, set nil to the +;; above option. +;; +;; (setq gnus-namazu-index-update-interval nil) +;; +;; When your index is put into the directory other than the default +;; one (~/News/namazu), it is necessary to set its place to +;; `gnus-namazu-index-directories' as follows: +;; +;; (setq gnus-namazu-index-directories +;; (list (expand-file-name "~/namazu"))) + + +;;; Code: + +(eval-when-compile (require 'cl)) +(require 'nnoo) +(require 'nnheader) +(require 'nnmail) +(require 'gnus-sum) + +;; It is required for Mule 2.3. See the file Mule23@1934.en. +(eval-and-compile + (autoload 'regexp-opt "regexp-opt")) + +;; To suppress byte-compile warning. +(eval-when-compile + (defvar nnml-directory) + (defvar nnmh-directory)) + + +(defgroup gnus-namazu nil + "Search nnmh and nnml groups in Gnus with Namazu." + :group 'namazu + :group 'gnus + :prefix "gnus-namazu-") + +(defconst gnus-namazu-default-index-directory + (expand-file-name "namazu" gnus-directory) + "Default place of Namazu index files.") + +(defcustom gnus-namazu-index-directories + (list + (or (and (boundp 'gnus-namazu-index-directory) + (symbol-value 'gnus-namazu-index-directory)) + (and (boundp 'nnir-namazu-index-directory) + (symbol-value 'nnir-namazu-index-directory)) + gnus-namazu-default-index-directory)) + "*Places of Namazu index files." + :type '(repeat directory) + :group 'gnus-namazu) + +(defcustom gnus-namazu-command + (or (and (boundp 'namazu-command) + (symbol-value 'namazu-command)) + (and (boundp 'nnir-namazu-program) + (symbol-value 'nnir-namazu-program)) + "namazu") + "*Name of the executable file of Namazu." + :type 'string + :group 'gnus-namazu) + +(defcustom gnus-namazu-additional-arguments nil + "*Additional arguments of Namazu. +The options `-q', `-a', and `-l' are always used, very few other +options make any sense in this context." + :type '(repeat string) + :group 'gnus-namazu) + +(defcustom gnus-namazu-index-update-interval + 259200 ; 3 days == 259200 seconds. + "*Number of seconds between running the indexer of Namazu." + :type '(choice (const :tag "Never run the indexer" nil) + (integer :tag "Number of seconds")) + :group 'gnus-namazu) + +(defcustom gnus-namazu-make-index-command "mknmz" + "*Name of the executable file of the indexer of Namazu." + :type 'string + :group 'gnus-namazu) + +(defcustom gnus-namazu-make-index-arguments + (nconc + (list "--all" "--mailnews" "--deny=^.*[^0-9].*$") + (when (or (and (boundp 'current-language-environment) + (string= "Japanese" + (symbol-value 'current-language-environment))) + (boundp 'MULE)) + (list "--indexing-lang=ja"))) + "*Arguments of the indexer of Namazu." + :type '(repeat string) + :group 'gnus-namazu) + +(defcustom gnus-namazu-field-keywords + '("date" "from" "newsgroups" "size" "subject" "summary" "to" "uri") + "*List of keywords to do field-search." + :type '(repeat string) + :group 'gnus-namazu) + +(defcustom gnus-namazu-coding-system + (if (memq system-type '(windows-nt OS/2 emx)) + (if (boundp 'MULE) '*sjis* 'shift_jis) + (if (boundp 'MULE) '*euc-japan* 'euc-japan)) + "*Coding system for Namazu process." + :type 'coding-system + :group 'gnus-namazu) + +(defcustom gnus-namazu-need-path-normalization + (and (memq system-type '(windows-nt OS/2 emx)) t) + "*Non-nil means that outputs of namazu may contain drive letters." + :type 'boolean + :group 'gnus-namazu) + +(defcustom gnus-namazu-case-sensitive-filesystem + (not (eq system-type 'windows-nt)) + "*Non-nil means that the using file system distinguishes cases of characters." + :type 'boolean + :group 'gnus-namazu) + +(defcustom gnus-namazu-query-highlight t + "Non-nil means that queried words is highlighted." + :type 'boolean + :group 'gnus-namazu) + +(defface gnus-namazu-query-highlight-face + '((((type tty pc) (class color)) + (:background "magenta4" :foreground "cyan1")) + (((class color) (background light)) + (:background "magenta4" :foreground "lightskyblue1")) + (((class color) (background dark)) + (:background "palevioletred2" :foreground "brown4")) + (t (:inverse-video t))) + "Face used for namazu query matching words." + :group 'gnus-namazu) + +;;; Internal Variable: +(defconst gnus-namazu/group-name-regexp "\\`nnvirtual:namazu-search\\?") + +;; Multibyte group name: +(and + (fboundp 'gnus-group-decoded-name) + (let ((gnus-group-name-charset-group-alist + (list (cons gnus-namazu/group-name-regexp gnus-namazu-coding-system))) + (query (decode-coding-string + (string 27 36 66 52 65 59 122 27 40 66) + (if (boundp 'MULE) '*iso-2022-jp* 'iso-2022-7bit)))) + (not (string-match query + (gnus-summary-buffer-name + (encode-coding-string + (concat "nnvirtual:namazu-search?query=" query) + gnus-namazu-coding-system))))) + (let (current-load-list) + (defadvice gnus-summary-buffer-name + (before gnus-namazu-summary-buffer-name activate compile) + "Advised by `gnus-namazu' to handle encoded group names." + (ad-set-arg 0 (gnus-group-decoded-name (ad-get-arg 0)))))) + +(defmacro gnus-namazu/make-article (group number) + `(cons ,group ,number)) +(defmacro gnus-namazu/article-group (x) `(car ,x)) +(defmacro gnus-namazu/article-number (x) `(cdr ,x)) + +(defsubst gnus-namazu/indexed-servers () + "Choice appropriate servers from opened ones, and return thier list." + (append + (gnus-servers-using-backend 'nnml) + (gnus-servers-using-backend 'nnmh))) + +(defsubst gnus-namazu/default-index-directory () + (if (member gnus-namazu-default-index-directory + gnus-namazu-index-directories) + gnus-namazu-default-index-directory + (car gnus-namazu-index-directories))) + +(defun gnus-namazu/setup () + (and (boundp 'gnus-group-name-charset-group-alist) + (not (member (cons gnus-namazu/group-name-regexp + gnus-namazu-coding-system) + gnus-group-name-charset-group-alist)) + (let ((pair (assoc gnus-namazu/group-name-regexp + gnus-group-name-charset-group-alist))) + (if pair + (setcdr pair gnus-namazu-coding-system) + (push (cons gnus-namazu/group-name-regexp + gnus-namazu-coding-system) + gnus-group-name-charset-group-alist)))) + (gnus-namazu-update-all-indices)) + +(defun gnus-namazu/server-directory (server) + "Return the top directory of the server SERVER." + (and (memq (car server) '(nnml nnmh)) + (nnoo-change-server (car server) (nth 1 server) (nthcdr 2 server)) + (file-name-as-directory + (expand-file-name (if (eq 'nnml (car server)) + nnml-directory + nnmh-directory))))) + +;;; Functions to call Namazu. +(defsubst gnus-namazu/normalize-results () + "Normalize file names returned by Namazu in this current buffer." + (goto-char (point-min)) + (while (not (eobp)) + (when (looking-at "file://") + (delete-region (point) (match-end 0))) + (when (if gnus-namazu-need-path-normalization + (or (not (looking-at "/\\(.\\)|/")) + (replace-match "\\1:/")) + (eq ?~ (char-after (point)))) + (insert (expand-file-name + (buffer-substring (gnus-point-at-bol) (gnus-point-at-eol)))) + (delete-region (point) (gnus-point-at-eol))) + (forward-line 1))) + +(defsubst gnus-namazu/call-namazu (query) + (let ((coding-system-for-read gnus-namazu-coding-system) + (coding-system-for-write gnus-namazu-coding-system) + (input-coding-system gnus-namazu-coding-system) + (output-coding-system gnus-namazu-coding-system) + (default-process-coding-system + (cons gnus-namazu-coding-system gnus-namazu-coding-system)) + program-coding-system-alist + (file-name-coding-system gnus-namazu-coding-system) + (pathname-coding-system gnus-namazu-coding-system)) + (apply 'call-process + `(,gnus-namazu-command + nil ; input from /dev/null + t ; output + nil ; don't redisplay + "-q" ; don't be verbose + "-a" ; show all matches + "-l" ; use list format + ,@gnus-namazu-additional-arguments + ,query + ,@gnus-namazu-index-directories)))) + +(defsubst gnus-namazu/group-prefixed-name (group method) + "Return the whole name from GROUP and METHOD." + (if gnus-namazu-case-sensitive-filesystem + (gnus-group-prefixed-name group method) + (let* ((orig (gnus-group-prefixed-name group method)) + (name (downcase orig))) + (catch 'found-group + (mapatoms (lambda (sym) + (when (string= name (downcase (symbol-name sym))) + (throw 'found-group (symbol-name sym)))) + gnus-newsrc-hashtb) + orig)))) + +(defun gnus-namazu/real-group-name (cond str) + "Generate the real group name from the partial path, STR." + (if cond + str + (catch 'found-group + (dolist (group (gnus-namazu/possible-real-groups + (nnheader-replace-chars-in-string str ?/ ?.))) + (when (gnus-gethash group gnus-newsrc-hashtb) + (throw 'found-group group)))))) + +(defun gnus-namazu/possible-real-groups (str) + "Regard the string STR as the partial path of the cached article and +generate possible group names from it." + (if (string-match "_\\(_\\(_\\)?\\)?" str) + (let ((prefix (substring str 0 (match-beginning 0))) + (suffix (substring str (match-end 0)))) + (cond + ((match-beginning 2) ;; The number of discoverd underscores = 3 + (nconc + (gnus-namazu/possible-real-groups (concat prefix "/__" suffix)) + (gnus-namazu/possible-real-groups (concat prefix ".._" suffix)))) + ((match-beginning 1) ;; The number of discoverd underscores = 2 + (nconc + (gnus-namazu/possible-real-groups (concat prefix "//" suffix)) + (gnus-namazu/possible-real-groups (concat prefix ".." suffix)))) + (t ;; The number of discoverd underscores = 1 + (gnus-namazu/possible-real-groups (concat prefix "/" suffix))))) + (if (string-match "\\." str) + ;; Handle the first occurence of period. + (list (concat (substring str 0 (match-beginning 0)) + ":" + (substring str (match-end 0))) + str) + (list str)))) + +(defun gnus-namazu/search (groups query) + (with-temp-buffer + (let ((exit-status (gnus-namazu/call-namazu query))) + (unless (zerop exit-status) + (error "Namazu finished abnormally: %d" exit-status)) + (let* ((articles) + (server-alist + (delq nil + (let (dir) + (mapcar + (lambda (s) + (when (setq dir (gnus-namazu/server-directory s)) + (cons (file-name-as-directory dir) s))) + (gnus-namazu/indexed-servers))))) + (topdir-regexp (regexp-opt (mapcar 'car server-alist))) + (cache-regexp (concat + (regexp-quote + (file-name-as-directory + (expand-file-name gnus-cache-directory))) + "\\(.*\\)/\\([0-9]+\\)$")) + (agent-regexp (concat + (regexp-quote + (file-name-as-directory + (expand-file-name gnus-agent-directory))) + "\\(.*\\)/\\([0-9]+\\)$"))) + (gnus-namazu/normalize-results) + (goto-char (point-min)) + (while (not (eobp)) + (let (server group file) + (and (or + ;; Check the discoverd file is the persistent article. + (and (looking-at cache-regexp) + (setq file (match-string-no-properties 2) + group (gnus-namazu/real-group-name + (gnus-use-long-file-name 'not-cache) + (match-string-no-properties 1)))) + ;; Check the discoverd file is covered by the agent. + (and (looking-at agent-regexp) + (setq file (match-string-no-properties 2) + group (gnus-namazu/real-group-name + nnmail-use-long-file-names + (match-string-no-properties 1)))) + ;; Check the discovered file is managed by Gnus servers. + (and (looking-at topdir-regexp) + (setq file (buffer-substring-no-properties + (match-end 0) (gnus-point-at-eol)) + server (cdr (assoc (match-string-no-properties 0) + server-alist))) + ;; Check validity of the file name. + (string-match "/\\([0-9]+\\)\\'" file) + (progn + (setq group (substring file 0 (match-beginning 0)) + file (match-string 1 file)) + (setq group + (gnus-namazu/group-prefixed-name + (if nnmail-use-long-file-names + group + (nnheader-replace-chars-in-string group + ?/ ?.)) + server))))) + (or (not groups) + (member group groups)) + (push (gnus-namazu/make-article group (string-to-number file)) + articles))) + (forward-line 1)) + (nreverse articles))))) + + +;;; User Interface: +(defun gnus-namazu/get-target-groups () + (cond + ((eq major-mode 'gnus-group-mode) + ;; In Group buffer. + (cond + (current-prefix-arg + (gnus-group-process-prefix current-prefix-arg)) + (gnus-group-marked + (prog1 gnus-group-marked (gnus-group-unmark-all-groups))))) + ((eq major-mode 'gnus-summary-mode) + ;; In Summary buffer. + (if current-prefix-arg + (list (gnus-read-group "Group: ")) + (if (and + (gnus-ephemeral-group-p gnus-newsgroup-name) + (string-match gnus-namazu/group-name-regexp gnus-newsgroup-name)) + (cadr (assq 'gnus-namazu-target-groups + (gnus-info-method (gnus-get-info gnus-newsgroup-name)))) + (list gnus-newsgroup-name)))))) + +(defun gnus-namazu/get-current-query () + (and (eq major-mode 'gnus-summary-mode) + (gnus-ephemeral-group-p gnus-newsgroup-name) + (string-match gnus-namazu/group-name-regexp gnus-newsgroup-name) + (cadr (assq 'gnus-namazu-current-query + (gnus-info-method (gnus-get-info gnus-newsgroup-name)))))) + +(defvar gnus-namazu/read-query-original-buffer nil) +(defvar gnus-namazu/read-query-prompt nil) +(defvar gnus-namazu/read-query-history nil) + +(defun gnus-namazu/get-current-subject () + (and gnus-namazu/read-query-original-buffer + (bufferp gnus-namazu/read-query-original-buffer) + (with-current-buffer gnus-namazu/read-query-original-buffer + (when (eq major-mode 'gnus-summary-mode) + (let ((s (gnus-summary-article-subject))) + ;; Remove typically prefixes of mailing lists. + (when (string-match + "^\\(\\[[^]]*[0-9]+\\]\\|([^)]*[0-9]+)\\)\\s-*" s) + (setq s (substring s (match-end 0)))) + (when (string-match + "^\\(Re\\(\\^?\\([0-9]+\\|\\[[0-9]+\\]\\)\\)?:\\s-*\\)+" s) + (setq s (substring s (match-end 0)))) + (when (string-match "\\s-*(\\(re\\|was\\)\\b" s) + (setq s (substring s 0 (match-beginning 0)))) + s))))) + +(defun gnus-namazu/get-current-from () + (and gnus-namazu/read-query-original-buffer + (bufferp gnus-namazu/read-query-original-buffer) + (with-current-buffer gnus-namazu/read-query-original-buffer + (when (eq major-mode 'gnus-summary-mode) + (cadr (mail-extract-address-components + (mail-header-from + (gnus-summary-article-header)))))))) + +(defun gnus-namazu/get-current-to () + (and gnus-namazu/read-query-original-buffer + (bufferp gnus-namazu/read-query-original-buffer) + (with-current-buffer gnus-namazu/read-query-original-buffer + (when (eq major-mode 'gnus-summary-mode) + (cadr (mail-extract-address-components + (cdr (assq 'To (mail-header-extra + (gnus-summary-article-header)))))))))) + +(defmacro gnus-namazu/minibuffer-prompt-end () + (if (fboundp 'minibuffer-prompt-end) + '(minibuffer-prompt-end) + '(point-min))) + +(defun gnus-namazu/message (string &rest arguments) + (let* ((s1 (concat + gnus-namazu/read-query-prompt + (buffer-substring (gnus-namazu/minibuffer-prompt-end) + (point-max)))) + (s2 (apply (function format) string arguments)) + (w (- (window-width) + (string-width s1) + (string-width s2) + 1))) + (message (if (>= w 0) + (concat s1 (make-string w ?\ ) s2) + s2)) + (if (sit-for 0.3) (message s1)) + s2)) + +(defun gnus-namazu/complete-query () + (interactive) + (let ((pos (point))) + (cond + ((and (re-search-backward "\\+\\([-a-z]*\\)" nil t) + (= pos (match-end 0))) + (let* ((partial (match-string 1)) + (completions + (all-completions + partial + (mapcar 'list gnus-namazu-field-keywords)))) + (cond + ((null completions) + (gnus-namazu/message "No completions of %s" partial)) + ((= 1 (length completions)) + (goto-char (match-beginning 1)) + (delete-region (match-beginning 1) (match-end 1)) + (insert (car completions) ":") + (setq pos (point)) + (gnus-namazu/message "Completed")) + (t + (let ((x (try-completion partial (mapcar 'list completions)))) + (if (string= x partial) + (if (and (eq last-command + 'gnus-namazu/field-keyword-completion) + completion-auto-help) + (with-output-to-temp-buffer "*Completions*" + (display-completion-list completions)) + (gnus-namazu/message "Sole completion")) + (goto-char (match-beginning 1)) + (delete-region (match-beginning 1) (match-end 1)) + (insert x) + (setq pos (point)))))))) + ((and (looking-at "\\+subject:") + (= pos (match-end 0))) + (let ((s (gnus-namazu/get-current-subject))) + (when s + (goto-char pos) + (insert "\"" s "\"") + (setq pos (point))))) + ((and (looking-at "\\+from:") + (= pos (match-end 0))) + (let ((f (gnus-namazu/get-current-from))) + (when f + (goto-char pos) + (insert "\"" f "\"") + (setq pos (point))))) + ((and (looking-at "\\+to:") + (= pos (match-end 0))) + (let ((to (gnus-namazu/get-current-to))) + (when to + (goto-char pos) + (insert "\"" to "\"") + (setq pos (point)))))) + (goto-char pos))) + +(defvar gnus-namazu/read-query-map + (let ((keymap (copy-keymap minibuffer-local-map))) + (define-key keymap "\t" 'gnus-namazu/complete-query) + keymap)) + +(defun gnus-namazu/read-query (prompt &optional initial) + (let ((gnus-namazu/read-query-original-buffer (current-buffer)) + (gnus-namazu/read-query-prompt prompt)) + (unless initial + (when (setq initial (gnus-namazu/get-current-query)) + (setq initial (cons initial 0)))) + (read-from-minibuffer prompt initial gnus-namazu/read-query-map nil + 'gnus-namazu/read-query-history))) + +(defun gnus-namazu/highlight-words (query) + (with-temp-buffer + (insert " " query) + ;; Remove tokens for NOT search + (goto-char (point-min)) + (while (re-search-forward "[$B!!(B \t\r\f\n]+not[$B!!(B \t\r\f\n]+\ +\\([^$B!!(B \t\r\f\n\"{(/]+\\|\"[^\"]+\"\\|{[^}]+}\\|([^)]+)\\|/[^/]+/\\)+" nil t) + (delete-region (match-beginning 0) (match-end 0))) + ;; Remove tokens for Field search + (goto-char (point-min)) + (while (re-search-forward "[$B!!(B \t\r\f\n]+\\+[^$B!!(B \t\r\f\n:]+:\ +\\([^$B!!(B \t\r\f\n\"{(/]+\\|\"[^\"]+\"\\|{[^}]+}\\|([^)]+)\\|/[^/]+/\\)+" nil t) + (delete-region (match-beginning 0) (match-end 0))) + ;; Remove tokens for Regexp search + (goto-char (point-min)) + (while (re-search-forward "/[^/]+/" nil t) + (delete-region (match-beginning 0) (match-end 0))) + ;; Remove brackets, double quote, asterisk and operators + (goto-char (point-min)) + (while (re-search-forward "\\([(){}\"*]\\|\\b\\(and\\|or\\)\\b\\)" nil t) + (delete-region (match-beginning 0) (match-end 0))) + ;; Collect all keywords + (setq query nil) + (goto-char (point-min)) + (while (re-search-forward "[^$B!!(B \t\r\f\n]+" nil t) + (push (match-string 0) query)) + (when query + (let (en ja) + (dolist (q query) + (if (string-match "\\cj" q) + (push q ja) + (push q en))) + (append + (when en + (list (list (concat "\\b\\(" (regexp-opt en) "\\)\\b") + 0 0 'gnus-namazu-query-highlight-face))) + (when ja + (list (list (regexp-opt ja) + 0 0 'gnus-namazu-query-highlight-face)))))))) + +(defun gnus-namazu/truncate-article-list (articles) + (let ((hit (length articles))) + (when (and gnus-large-newsgroup + (> hit gnus-large-newsgroup)) + (let* ((cursor-in-echo-area nil) + (input (read-from-minibuffer + (format "\ +Too many articles were retrieved. How many articles (max %d): " + hit) + (cons (number-to-string gnus-large-newsgroup) 0)))) + (unless (string-match "\\`[ \t]*\\'" input) + (setcdr (nthcdr (min (1- (string-to-number input)) hit) articles) + nil))))) + articles) + +;;;###autoload +(defun gnus-namazu-search (groups query) + "Search QUERY through GROUPS with Namazu, +and make a virtual group contains its results." + (interactive + (list + (gnus-namazu/get-target-groups) + (gnus-namazu/read-query "Enter query: "))) + (gnus-namazu/setup) + (let ((articles (gnus-namazu/search groups query))) + (if articles + (let ((real-groups groups) + (vgroup + (apply (function format) + "nnvirtual:namazu-search?query=%s&groups=%s&id=%d%d%d" + query + (if groups (mapconcat 'identity groups ",") "ALL") + (current-time)))) + (gnus-namazu/truncate-article-list articles) + (unless real-groups + (dolist (a articles) + (add-to-list 'real-groups (gnus-namazu/article-group a)))) + ;; Generate virtual group which includes all results. + (when (fboundp 'gnus-group-decoded-name) + (setq vgroup + (encode-coding-string vgroup gnus-namazu-coding-system))) + (setq vgroup + (gnus-group-read-ephemeral-group + vgroup + `(nnvirtual ,vgroup + (nnvirtual-component-groups ,real-groups) + (gnus-namazu-target-groups ,groups) + (gnus-namazu-current-query ,query)) + t (cons (current-buffer) (current-window-configuration)) t)) + (when gnus-namazu-query-highlight + (gnus-group-set-parameter vgroup 'highlight-words + (gnus-namazu/highlight-words query))) + ;; Generate new summary buffer which contains search results. + (gnus-group-read-group + t t vgroup + (sort (delq nil ;; Ad-hoc fix, to avoid wrong-type-argument error. + (mapcar + (lambda (a) + (nnvirtual-reverse-map-article + (gnus-namazu/article-group a) + (gnus-namazu/article-number a))) + articles)) + '<))) + (message "No entry.")))) + +(defmacro gnus-namazu/lock-file-name (&optional directory) + `(expand-file-name "NMZ.lock2" ,directory)) + +(defmacro gnus-namazu/status-file-name (&optional directory) + `(expand-file-name "NMZ.status" ,directory)) + +(defmacro gnus-namazu/index-file-name (&optional directory) + `(expand-file-name "NMZ.i" ,directory)) + +(defun gnus-namazu/mknmz-cleanup (directory) + (let ((lockfile (gnus-namazu/lock-file-name directory))) + (when (file-exists-p lockfile) + (delete-file lockfile) + (dolist (tmpfile (directory-files directory t "\\`NMZ\\..*\\.tmp\\'" t)) + (delete-file tmpfile))))) + +;;;###autoload +(defun gnus-namazu-create-index (directory &optional target-directories force) + "Create index under DIRECTORY." + (interactive + (list + (if (and current-prefix-arg (> (length gnus-namazu-index-directories) 1)) + (completing-read "Directory: " + (mapcar 'list gnus-namazu-index-directories) nil t) + (gnus-namazu/default-index-directory)) + nil t)) + (setq directory (file-name-as-directory (expand-file-name directory))) + (unless target-directories + (setq target-directories + (delq nil + (mapcar (lambda (dir) + (when (file-directory-p dir) dir)) + (append + (mapcar 'gnus-namazu/server-directory + (gnus-namazu/indexed-servers)) + (list + (expand-file-name gnus-cache-directory) + (expand-file-name gnus-agent-directory))))))) + (if (file-exists-p (gnus-namazu/lock-file-name directory)) + (when force + (error "Found lock file: %s" (gnus-namazu/lock-file-name directory))) + (with-current-buffer + (get-buffer-create (concat " *mknmz*" directory)) + (erase-buffer) + (unless (file-directory-p directory) + (make-directory directory t)) + (setq default-directory directory) + (let ((args (append gnus-namazu-make-index-arguments + target-directories))) + (insert "% " gnus-namazu-make-index-command " " + (mapconcat 'identity args " ") "\n") + (goto-char (point-max)) + (when force + (pop-to-buffer (current-buffer))) + (message "Make index at %s..." directory) + (unwind-protect + (apply 'call-process gnus-namazu-make-index-command nil t t args) + (gnus-namazu/mknmz-cleanup directory)) + (message "Make index at %s...done" directory) + (unless force + (kill-buffer (current-buffer))))))) + +(defun gnus-namazu/lapse-seconds (start end) + "Return lapse seconds from START to END. +START and END are lists which represent time in Emacs-style." + (+ (* (- (car end) (car start)) 65536) + (cadr end) + (- (cadr start)))) + +(defun gnus-namazu/index-old-p (directory) + "Return non-nil value when the index under the DIRECTORY is older +than the period that is set to `gnus-namazu-index-update-interval'" + (let ((file (gnus-namazu/index-file-name directory))) + (or (not (file-exists-p file)) + (and (integerp gnus-namazu-index-update-interval) + (>= (gnus-namazu/lapse-seconds + (nth 5 (file-attributes file)) + (current-time)) + gnus-namazu-index-update-interval))))) + +(defvar gnus-namazu/update-directories nil) +(defvar gnus-namazu/update-process nil) + +(defun gnus-namazu/update-p (directory &optional force) + "Return the DIRECTORY when the index undef the DIRECTORY should be updated." + (setq directory (file-name-as-directory (expand-file-name directory))) + (labels ((error-message (format &rest args) + (apply (if force 'error 'message) format args) + nil)) + (if gnus-namazu/update-process + (error-message "%s" "Can not run two update processes simultaneously") + (and (or force + (gnus-namazu/index-old-p directory)) + (let ((status-file (gnus-namazu/status-file-name directory))) + (or (file-exists-p status-file) + (error-message "Can not find status file: %s" status-file))) + (let ((lock-file (gnus-namazu/lock-file-name directory))) + (or (not (file-exists-p lock-file)) + (error-message "Found lock file: %s" lock-file))) + directory)))) + +;;;###autoload +(defun gnus-namazu-update-index (directory &optional force) + "Update the index under the DIRECTORY." + (interactive + (list + (if (and current-prefix-arg (> (length gnus-namazu-index-directories) 1)) + (completing-read "Directory: " + (mapcar 'list gnus-namazu-index-directories) nil t) + (gnus-namazu/default-index-directory)) + t)) + (when (setq directory (gnus-namazu/update-p directory force)) + (with-current-buffer (get-buffer-create (concat " *mknmz*" directory)) + (buffer-disable-undo) + (erase-buffer) + (unless (file-directory-p directory) + (make-directory directory t)) + (setq default-directory directory) + (let ((proc (start-process gnus-namazu-make-index-command + (current-buffer) + gnus-namazu-make-index-command + (format "--update=%s" directory)))) + (if (processp proc) + (prog1 (setq gnus-namazu/update-process proc) + (process-kill-without-query proc) + (set-process-sentinel proc 'gnus-namazu/update-sentinel) + (add-hook 'kill-emacs-hook 'gnus-namazu-stop-update) + (message "Update index at %s..." directory)) + (goto-char (point-min)) + (if (re-search-forward "^ERROR:.*$" nil t) + (progn + (pop-to-buffer (current-buffer)) + (funcall (if force 'error 'message) + "Update index at %s...%s" directory (match-string 0))) + (kill-buffer (current-buffer)) + (funcall (if force 'error 'message) + "Can not start %s" gnus-namazu-make-index-command)) + nil))))) + +;;;###autoload +(defun gnus-namazu-update-all-indices (&optional directories force) + "Update all indices which is set to `gnus-namazu-index-directories'." + (interactive (list nil t)) + (when (setq directories + (delq nil (mapcar + (lambda (d) (gnus-namazu/update-p d force)) + (or directories gnus-namazu-index-directories)))) + (setq gnus-namazu/update-directories (cdr directories)) + (gnus-namazu-update-index (car directories)))) + +(defun gnus-namazu/update-sentinel (process event) + (let ((buffer (process-buffer process))) + (when (buffer-name buffer) + (with-current-buffer buffer + (gnus-namazu/mknmz-cleanup default-directory) + (goto-char (point-min)) + (cond + ((re-search-forward "^ERROR:.*$" nil t) + (pop-to-buffer (current-buffer)) + (message "Update index at %s...%s" + default-directory (match-string 0)) + (setq gnus-namazu/update-directories nil)) + ((and (eq 'exit (process-status process)) + (zerop (process-exit-status process))) + (message "Update index at %s...done" default-directory) + (unless (or debug-on-error debug-on-quit) + (kill-buffer buffer))))))) + (setq gnus-namazu/update-process nil) + (when gnus-namazu/update-directories + (gnus-namazu-update-all-indices gnus-namazu/update-directories))) + +;;;###autoload +(defun gnus-namazu-stop-update () + "Stop the running indexer of Namazu." + (interactive) + (setq gnus-namazu/update-directories nil) + (and gnus-namazu/update-process + (processp gnus-namazu/update-process) + (kill-process gnus-namazu/update-process))) + +(let (current-load-list) + (defadvice gnus-offer-save-summaries + (before gnus-namazu-kill-summary-buffers activate compile) + "Advised by `gnus-namazu'. +In order to avoid annoying questions, kill summary buffers which +generated by `gnus-namazu' itself before `gnus-offer-save-summaries' +is called." + (let ((buffers (buffer-list))) + (while buffers + (when (with-current-buffer (car buffers) + (and (eq major-mode 'gnus-summary-mode) + (gnus-ephemeral-group-p gnus-newsgroup-name) + (string-match gnus-namazu/group-name-regexp + gnus-newsgroup-name))) + (kill-buffer (car buffers))) + (setq buffers (cdr buffers)))))) + +;;;###autoload +(defun gnus-namazu-insinuate () + (add-hook + 'gnus-group-mode-hook + (lambda () + (define-key gnus-group-mode-map "\C-c\C-n" 'gnus-namazu-search))) + (add-hook + 'gnus-summary-mode-hook + (lambda () + (define-key gnus-summary-mode-map "\C-c\C-n" 'gnus-namazu-search)))) + +(provide 'gnus-namazu) + +;; gnus-namazu.el ends here. diff --git a/lisp/gnus-registry.el b/lisp/gnus-registry.el new file mode 100644 index 0000000..324155d --- /dev/null +++ b/lisp/gnus-registry.el @@ -0,0 +1,245 @@ +;;; gnus-registry.el --- article registry for Gnus +;; Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003 +;; Free Software Foundation, Inc. + +;; Author: Ted Zlatanov +;; Keywords: news + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;;; Code: + +(eval-when-compile (require 'cl)) + +(require 'gnus) +(require 'gnus-int) +(require 'gnus-sum) +(require 'nnmail) + +(defgroup gnus-registry nil + "The Gnus registry." + :group 'gnus) + +(defvar gnus-registry-hashtb nil + "*The article registry by Message ID.") + +(defvar gnus-registry-headers-hashtb nil + "*The article header registry by Message ID. Unused for now.") + +(defcustom gnus-registry-unfollowed-groups '("delayed" "drafts" "queue") + "List of groups that gnus-registry-split-fancy-with-parent won't follow. +The group names are matched, they don't have to be fully qualified." + :group 'gnus-registry + :type '(repeat string)) + +(defcustom gnus-registry-unregistered-group-regex "^nntp" + "Group name regex that gnus-registry-register-message-ids won't process." + :group 'gnus-registry + :type 'regexp) + +;; Function(s) missing in Emacs 20 +(when (memq nil (mapcar 'fboundp '(puthash))) + (require 'cl) + (unless (fboundp 'puthash) + ;; alias puthash is missing from Emacs 20 cl-extra.el + (defalias 'puthash 'cl-puthash))) + +(defun gnus-registry-translate-to-alist () + (setq gnus-registry-alist (hashtable-to-alist gnus-registry-hashtb))) + +(defun gnus-registry-translate-from-alist () + (setq gnus-registry-hashtb (alist-to-hashtable gnus-registry-alist))) + +(defun alist-to-hashtable (alist) + "Build a hashtable from the values in ALIST." + (let ((ht (make-hash-table + :size 4096 + :test 'equal))) + (mapc + (lambda (kv-pair) + (puthash (car kv-pair) (cdr kv-pair) ht)) + alist) + ht)) + +(defun hashtable-to-alist (hash) + "Build an alist from the values in HASH." + (let ((list nil)) + (maphash + (lambda (key value) + (setq list (cons (cons key value) list))) + hash) + list)) + +(defun gnus-register-action (action data-header from &optional to method) + (let* ((id (mail-header-id data-header)) + (from (gnus-group-guess-full-name from)) + (to (if to (gnus-group-guess-full-name to) nil)) + (to-name (if to to "the Bit Bucket")) + (old-entry (gethash id gnus-registry-hashtb))) + (gnus-message 5 "Registry: article %s %s from %s to %s" + id + (if method "respooling" "going") + from + to) + + ;; All except copy will need a delete + (gnus-registry-delete-group id from) + + (when (equal 'copy action) + (gnus-registry-add-group id from)) ; undo the delete + + (gnus-registry-add-group id to))) + +(defun gnus-register-spool-action (id group) + ;; do not process the draft IDs +; (unless (string-match "totally-fudged-out-message-id" id) +; (let ((group (gnus-group-guess-full-name group))) + (when (string-match "\r$" id) + (setq id (substring id 0 -1))) + (gnus-message 5 "Registry: article %s spooled to %s" + id + group) + (gnus-registry-add-group id group)) +;) + +;; Function for nn{mail|imap}-split-fancy: look up all references in +;; the cache and if a match is found, return that group. +(defun gnus-registry-split-fancy-with-parent () + "Split this message into the same group as its parent. The parent +is obtained from the registry. This function can be used as an entry +in `nnmail-split-fancy' or `nnimap-split-fancy', for example like +this: (: gnus-registry-split-fancy-with-parent) + +For a message to be split, it looks for the parent message in the +References or In-Reply-To header and then looks in the registry to +see which group that message was put in. This group is returned. + +See the Info node `(gnus)Fancy Mail Splitting' for more details." + (let ((refstr (or (message-fetch-field "references") + (message-fetch-field "in-reply-to"))) + (references nil) + (res nil)) + (when refstr + (setq references (nreverse (gnus-split-references refstr))) + (mapcar (lambda (x) + (setq res (or (gnus-registry-fetch-group x) res)) + (when (or (gnus-registry-grep-in-list + res + gnus-registry-unfollowed-groups) + (gnus-registry-grep-in-list + res + nnmail-split-fancy-with-parent-ignore-groups)) + (setq res nil))) + references) + (gnus-message + 5 + "gnus-registry-split-fancy-with-parent traced %s to group %s" + refstr (if res res "nil")) + res))) + +(defun gnus-registry-register-message-ids () + "Register the Message-ID of every article in the group" + (unless (and gnus-registry-unregistered-group-regex + (string-match gnus-registry-unregistered-group-regex gnus-newsgroup-name)) + (dolist (article gnus-newsgroup-articles) + (let ((id (gnus-registry-fetch-message-id-fast article))) + (unless (gnus-registry-fetch-group id) + (gnus-message 9 "Registry: Registering article %d with group %s" + article gnus-newsgroup-name) + (gnus-registry-add-group (gnus-registry-fetch-message-id-fast article) + gnus-newsgroup-name)))))) + +(defun gnus-registry-fetch-message-id-fast (article) + "Fetch the Message-ID quickly, using the internal gnus-data-list function" + (if (and (numberp article) + (assoc article (gnus-data-list nil))) + (mail-header-id (gnus-data-header (assoc article (gnus-data-list nil)))) + nil)) + +(defun gnus-registry-grep-in-list (word list) + (when word + (memq nil + (mapcar 'not + (mapcar + (lambda (x) + (string-match x word)) + list))))) + +(defun gnus-registry-fetch-group (id) + "Get the group of a message, based on the message ID. +Returns the first place where the trail finds a spool action." + (when id + (let ((trail (gethash id gnus-registry-hashtb))) + (if trail + (car trail) + nil)))) + +(defun gnus-registry-delete-group (id group) + "Get the group of a message, based on the message ID. +Returns the first place where the trail finds a spool action." + (when group + (when id + (let ((trail (gethash id gnus-registry-hashtb)) + (group (gnus-group-short-name group))) + (puthash id (if trail + (delete group trail) + nil) + gnus-registry-hashtb)) + ;; now, clear the entry if it's empty + (unless (gethash id gnus-registry-hashtb) + (remhash id gnus-registry-hashtb))))) + +(defun gnus-registry-add-group (id group) + "Get the group of a message, based on the message ID. +Returns the first place where the trail finds a spool action." + ;; make sure there are no duplicate entries + (when group + (when id + (let ((group (gnus-group-short-name group))) + (gnus-registry-delete-group id group) + (let ((trail (gethash id gnus-registry-hashtb))) + (puthash id (if trail + (cons group trail) + (list group)) + gnus-registry-hashtb)))))) + +(defun gnus-registry-clear () + "Clear the Gnus registry." + (interactive) + (setq gnus-registry-alist nil + gnus-registry-headers-alist nil) + (gnus-registry-translate-from-alist)) + +; also does copy, respool, and crosspost +(add-hook 'gnus-summary-article-move-hook 'gnus-register-action) +(add-hook 'gnus-summary-article-delete-hook 'gnus-register-action) +(add-hook 'gnus-summary-article-expire-hook 'gnus-register-action) +(add-hook 'nnmail-spool-hook 'gnus-register-spool-action) + +(add-hook 'gnus-save-newsrc-hook 'gnus-registry-translate-to-alist) +(add-hook 'gnus-read-newsrc-el-hook 'gnus-registry-translate-from-alist) + +(add-hook 'gnus-summary-prepare-hook 'gnus-registry-register-message-ids) + +;; TODO: a lot of things + +(provide 'gnus-registry) + +;;; gnus-registry.el ends here diff --git a/lisp/gnus-sieve.el b/lisp/gnus-sieve.el new file mode 100644 index 0000000..b11ade5 --- /dev/null +++ b/lisp/gnus-sieve.el @@ -0,0 +1,239 @@ +;;; gnus-sieve.el --- Utilities to manage sieve scripts for Gnus +;; Copyright (C) 2001, 2003 Free Software Foundation, Inc. + +;; Author: NAGY Andras , +;; Simon Josefsson + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; Gnus glue to generate complete Sieve scripts from Gnus Group +;; Parameters with "if" test predicates. + +;;; Code: + +(require 'gnus) +(require 'gnus-sum) +(require 'format-spec) +(autoload 'sieve-mode "sieve-mode") +(eval-when-compile + (require 'sieve)) + +;; Variables + +(defgroup gnus-sieve nil + "Manage sieve scripts in Gnus." + :group 'gnus) + +(defcustom gnus-sieve-file "~/.sieve" + "Path to your Sieve script." + :type 'file + :group 'gnus-sieve) + +(defcustom gnus-sieve-region-start "\n## Begin Gnus Sieve Script\n" + "Line indicating the start of the autogenerated region in +your Sieve script." + :type 'string + :group 'gnus-sieve) + +(defcustom gnus-sieve-region-end "\n## End Gnus Sieve Script\n" + "Line indicating the end of the autogenerated region in +your Sieve script." + :type 'string + :group 'gnus-sieve) + +(defcustom gnus-sieve-select-method nil + "Which select method we generate the Sieve script for. + +For example: \"nnimap:mailbox\"" + :group 'gnus-sieve) + +(defcustom gnus-sieve-crosspost t + "Whether the generated Sieve script should do crossposting." + :type 'bool + :group 'gnus-sieve) + +(defcustom gnus-sieve-update-shell-command "echo put %f | sieveshell %s" + "Shell command to execute after updating your Sieve script. The following +formatting characters are recognized: + +%f Script's file name (gnus-sieve-file) +%s Server name (from gnus-sieve-select-method)" + :type 'string + :group 'gnus-sieve) + +;;;###autoload +(defun gnus-sieve-update () + "Update the Sieve script in gnus-sieve-file, by replacing the region +between gnus-sieve-region-start and gnus-sieve-region-end with +\(gnus-sieve-script gnus-sieve-select-method gnus-sieve-crosspost\), then +execute gnus-sieve-update-shell-command. +See the documentation for these variables and functions for details." + (interactive) + (gnus-sieve-generate) + (save-buffer) + (shell-command + (format-spec gnus-sieve-update-shell-command + (format-spec-make ?f gnus-sieve-file + ?s (or (cadr (gnus-server-get-method + nil gnus-sieve-select-method)) + ""))))) + +;;;###autoload +(defun gnus-sieve-generate () + "Generate the Sieve script in gnus-sieve-file, by replacing the region +between gnus-sieve-region-start and gnus-sieve-region-end with +\(gnus-sieve-script gnus-sieve-select-method gnus-sieve-crosspost\). +See the documentation for these variables and functions for details." + (interactive) + (require 'sieve) + (find-file gnus-sieve-file) + (goto-char (point-min)) + (if (re-search-forward (regexp-quote gnus-sieve-region-start) nil t) + (delete-region (match-end 0) + (or (re-search-forward (regexp-quote + gnus-sieve-region-end) nil t) + (point))) + (insert sieve-template)) + (insert gnus-sieve-region-start + (gnus-sieve-script gnus-sieve-select-method gnus-sieve-crosspost) + gnus-sieve-region-end)) + +(defun gnus-sieve-guess-rule-for-article () + "Guess a sieve rule based on RFC822 article in buffer. +Return nil if no rule could be guessed." + (when (message-fetch-field "sender") + `(sieve address "sender" ,(message-fetch-field "sender")))) + +;;;###autoload +(defun gnus-sieve-article-add-rule () + (interactive) + (gnus-summary-select-article nil 'force) + (with-current-buffer gnus-original-article-buffer + (let ((rule (gnus-sieve-guess-rule-for-article)) + (info (gnus-get-info gnus-newsgroup-name))) + (if (null rule) + (error "Could not guess rule for article.") + (gnus-info-set-params info (cons rule (gnus-info-params info))) + (message "Added rule in group %s for article: %s" gnus-newsgroup-name + rule))))) + +;; Internals + +;; FIXME: do proper quoting of " etc +(defun gnus-sieve-string-list (list) + "Convert an elisp string list to a Sieve string list. + +For example: +\(gnus-sieve-string-list '(\"to\" \"cc\")) + => \"[\\\"to\\\", \\\"cc\\\"]\" +" + (concat "[\"" (mapconcat 'identity list "\", \"") "\"]")) + +(defun gnus-sieve-test-list (list) + "Convert an elisp test list to a Sieve test list. + +For example: +\(gnus-sieve-test-list '((address \"sender\" \"boss@company.com\") (size :over 4K))) + => \"(address \\\"sender\\\" \\\"boss@company.com\\\", size :over 4K)\"" + (concat "(" (mapconcat 'gnus-sieve-test list ", ") ")")) + +;; FIXME: do proper quoting +(defun gnus-sieve-test-token (token) + "Convert an elisp test token to a Sieve test token. + +For example: +\(gnus-sieve-test-token 'address) + => \"address\" + +\(gnus-sieve-test-token \"sender\") + => \"\\\"sender\\\"\" + +\(gnus-sieve-test-token '(\"to\" \"cc\")) + => \"[\\\"to\\\", \\\"cc\\\"]\"" + (cond + ((symbolp token) ;; Keyword + (symbol-name token)) + + ((stringp token) ;; String + (concat "\"" token "\"")) + + ((and (listp token) ;; String list + (stringp (car token))) + (gnus-sieve-string-list token)) + + ((and (listp token) ;; Test list + (listp (car token))) + (gnus-sieve-test-list token)))) + +(defun gnus-sieve-test (test) + "Convert an elisp test to a Sieve test. + +For example: +\(gnus-sieve-test '(address \"sender\" \"sieve-admin@extundo.com\")) + => \"address \\\"sender\\\" \\\"sieve-admin@extundo.com\\\"\" + +\(gnus-sieve-test '(anyof ((header :contains (\"to\" \"cc\") \"my@address.com\") + (size :over 100K)))) + => \"anyof (header :contains [\\\"to\\\", \\\"cc\\\"] \\\"my@address.com\\\", + size :over 100K)\"" + (mapconcat 'gnus-sieve-test-token test " ")) + +(defun gnus-sieve-script (&optional method crosspost) + "Generate a Sieve script based on groups with select method METHOD +\(or all groups if nil\). Only groups having a `sieve' parameter are +considered. This parameter should contain an elisp test +\(see the documentation of gnus-sieve-test for details\). For each +such group, a Sieve IF control structure is generated, having the +test as the condition and { fileinto \"group.name\"; } as the body. + +If CROSSPOST is nil, each conditional body contains a \"stop\" command +which stops execution after a match is found. + +For example: If the INBOX.list.sieve group has the + + (sieve address \"sender\" \"sieve-admin@extundo.com\") + +group parameter, (gnus-sieve-script) results in: + + if address \"sender\" \"sieve-admin@extundo.com\" { + fileinto \"INBOX.list.sieve\"; + } + +This is returned as a string." + (let* ((newsrc (cdr gnus-newsrc-alist)) + script) + (dolist (info newsrc) + (when (or (not method) + (gnus-server-equal method (gnus-info-method info))) + (let* ((group (gnus-info-group info)) + (spec (gnus-group-find-parameter group 'sieve t))) + (when spec + (push (concat "if " (gnus-sieve-test spec) " {\n" + "\tfileinto \"" (gnus-group-real-name group) "\";\n" + (if crosspost + "" + "\tstop;\n") + "}") + script))))) + (mapconcat 'identity script "\n"))) + +(provide 'gnus-sieve) + +;;; gnus-sieve.el ends here diff --git a/lisp/hex-util.el b/lisp/hex-util.el new file mode 100644 index 0000000..6936bf3 --- /dev/null +++ b/lisp/hex-util.el @@ -0,0 +1,73 @@ +;;; hex-util.el --- Functions to encode/decode hexadecimal string. + +;; Copyright (C) 1999, 2001 Free Software Foundation, Inc. + +;; Author: Shuhei KOBAYASHI +;; Keywords: data + +;; This file is part of FLIM (Faithful Library about Internet Message). + +;; 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, 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 should have received a copy of the GNU General Public License +;; along with this program; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;;; Code: + +(eval-when-compile + (defmacro hex-char-to-num (chr) + (` (let ((chr (, chr))) + (cond + ((and (<= ?a chr)(<= chr ?f)) (+ (- chr ?a) 10)) + ((and (<= ?A chr)(<= chr ?F)) (+ (- chr ?A) 10)) + ((and (<= ?0 chr)(<= chr ?9)) (- chr ?0)) + (t (error "Invalid hexadecimal digit `%c'" chr)))))) + (defmacro num-to-hex-char (num) + (` (aref "0123456789abcdef" (, num))))) + +(defun decode-hex-string (string) + "Decode hexadecimal STRING to octet string." + (let* ((len (length string)) + (dst (make-string (/ len 2) 0)) + (idx 0)(pos 0)) + (while (< pos len) +;;; logior and lsh are not byte-coded. +;;; (aset dst idx (logior (lsh (hex-char-to-num (aref string pos)) 4) +;;; (hex-char-to-num (aref string (1+ pos))))) + (aset dst idx (+ (* (hex-char-to-num (aref string pos)) 16) + (hex-char-to-num (aref string (1+ pos))))) + (setq idx (1+ idx) + pos (+ 2 pos))) + dst)) + +(defun encode-hex-string (string) + "Encode octet STRING to hexadecimal string." + (let* ((len (length string)) + (dst (make-string (* len 2) 0)) + (idx 0)(pos 0)) + (while (< pos len) +;;; logand and lsh are not byte-coded. +;;; (aset dst idx (num-to-hex-char (logand (lsh (aref string pos) -4) 15))) + (aset dst idx (num-to-hex-char (/ (aref string pos) 16))) + (setq idx (1+ idx)) +;;; (aset dst idx (num-to-hex-char (logand (aref string pos) 15))) + (aset dst idx (num-to-hex-char (% (aref string pos) 16))) + (setq idx (1+ idx) + pos (1+ pos))) + dst)) + +(provide 'hex-util) + +;;; hex-util.el ends here diff --git a/lisp/html2text.el b/lisp/html2text.el new file mode 100644 index 0000000..4b89f8f --- /dev/null +++ b/lisp/html2text.el @@ -0,0 +1,551 @@ +;;; html2text.el --- a simple html to plain text converter +;; Copyright (C) 2002, 2003 Free Software Foundation, Inc. + +;; Author: Joakim Hove + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; These functions provide a simple way to wash/clean html infected +;; mails. Definitely do not work in all cases, but some improvement +;; in readability is generally obtained. Formatting is only done in +;; the buffer, so the next time you enter the article it will be +;; "re-htmlized". +;; +;; The main function is "html2text" + +;;; Code: + +;; +;; +;; + +(eval-when-compile + (require 'cl)) + +(defvar html2text-format-single-element-list '(("hr" . html2text-clean-hr))) + +(defvar html2text-replace-list + '((" " . " ") (">" . ">") ("<" . "<") (""" . "\"")) + "The map of entity to text. + +This is an alist were each element is a dotted pair consisting of an +old string, and a replacement string. This replacement is done by the +function \"html2text-substitute\" which basically performs a +replace-string operation for every element in the list. This is +completely verbatim - without any use of REGEXP.") + +(defvar html2text-remove-tag-list + '("html" "body" "p" "img" "dir" "head" "div" "br" "font" "title" "meta") + "A list of removable tags. + +This is a list of tags which should be removed, without any +formatting. Observe that if you the tags in the list are presented +*without* any \"<\" or \">\". All occurences of a tag appearing in +this list are removed, irrespective of whether it is a closing or +opening tag, or if the tag has additional attributes. The actual +deletion is done by the function \"html2text-remove-tags\". + +For instance the text: + +\"Here comes something big .\" + +will be reduced to: + +\"Here comes something big.\" + +If this list contains the element \"font\".") + +(defvar html2text-format-tag-list + '(("b" . html2text-clean-bold) + ("u" . html2text-clean-underline) + ("i" . html2text-clean-italic) + ("blockquote" . html2text-clean-blockquote) + ("a" . html2text-clean-anchor) + ("ul" . html2text-clean-ul) + ("ol" . html2text-clean-ol) + ("dl" . html2text-clean-dl) + ("center" . html2text-clean-center)) + "An alist of tags and processing functions. + +This is an alist where each dotted pair consists of a tag, and then +the name of a function to be called when this tag is found. The +function is called with the arguments p1, p2, p3 and p4. These are +demontrated below: + +\" This is bold text \" + ^ ^ ^ ^ + | | | | +p1 p2 p3 p4 + +Then the called function will typically format the text somewhat and +remove the tags.") + +(defvar html2text-remove-tag-list2 '("li" "dt" "dd" "meta") + "Another list of removable tags. + +This is a list of tags which are removed similarly to the list +`html2text-remove-tag-list' - but these tags are retained for the +formatting, and then moved afterward.") + +;; +;; +;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; +;; +;; + +(defun html2text-buffer-head () + (if (string= mode-name "Article") + (beginning-of-buffer) + (beginning-of-buffer) + ) + ) + +(defun html2text-replace-string (from-string to-string p1 p2) + (goto-char p1) + (let ((delta (- (string-width to-string) (string-width from-string))) + (change 0)) + (while (search-forward from-string p2 t) + (replace-match to-string) + (setq change (+ change delta)) + ) + change + ) + ) + +;; +;; +;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; +;; i.e. +;; + +(defun html2text-attr-value (attr-list attr) + (nth 1 (assoc attr attr-list)) + ) + +(defun html2text-get-attr (p1 p2 tag) + (goto-char p1) + (re-search-forward " +[^ ]" p2 t) + (let* ((attr-string (buffer-substring-no-properties (1- (point)) (1- p2))) + (tmp-list (split-string attr-string)) + (attr-list) + (counter 0) + (prev (car tmp-list)) + (this (nth 1 tmp-list)) + (next (nth 2 tmp-list)) + (index 1)) + + (cond + ;; size=3 + ((string-match "[^ ]=[^ ]" prev) + (let ((attr (nth 0 (split-string prev "="))) + (value (nth 1 (split-string prev "=")))) + (setq attr-list (cons (list attr value) attr-list)) + ) + ) + ;; size= 3 + ((string-match "[^ ]=\\'" prev) + (setq attr-list (cons (list (substring prev 0 -1) this) attr-list)) + ) + ) + + (while (< index (length tmp-list)) + (cond + ;; size=3 + ((string-match "[^ ]=[^ ]" this) + (let ((attr (nth 0 (split-string this "="))) + (value (nth 1 (split-string this "=")))) + (setq attr-list (cons (list attr value) attr-list)) + ) + ) + ;; size =3 + ((string-match "\\`=[^ ]" this) + (setq attr-list (cons (list prev (substring this 1)) attr-list))) + + ;; size= 3 + ((string-match "[^ ]=\\'" this) + (setq attr-list (cons (list (substring this 0 -1) next) attr-list)) + ) + + ;; size = 3 + ((string= "=" this) + (setq attr-list (cons (list prev next) attr-list)) + ) + ) + (setq index (1+ index)) + (setq prev this) + (setq this next) + (setq next (nth (1+ index) tmp-list)) + ) + + ;; + ;; Tags with no accompanying "=" i.e. value=nil + ;; + (setq prev (car tmp-list)) + (setq this (nth 1 tmp-list)) + (setq next (nth 2 tmp-list)) + (setq index 1) + + (if (not (string-match "=" prev)) + (progn + (if (not (string= (substring this 0 1) "=")) + (setq attr-list (cons (list prev nil) attr-list)) + ) + ) + ) + + (while (< index (1- (length tmp-list))) + (if (not (string-match "=" this)) + (if (not (or (string= (substring next 0 1) "=") + (string= (substring prev -1) "="))) + (setq attr-list (cons (list this nil) attr-list)) + ) + ) + (setq index (1+ index)) + (setq prev this) + (setq this next) + (setq next (nth (1+ index) tmp-list)) + ) + + (if this + (progn + (if (not (string-match "=" this)) + (progn + (if (not (string= (substring prev -1) "=")) + (setq attr-list (cons (list this nil) attr-list)) + ) + ) + ) + ) + ) + attr-list ;; return - value + ) + ) + +;; +;; +;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; +;; +;; +(defun html2text-clean-list-items (p1 p2 list-type) + (goto-char p1) + (let ((item-nr 0) + (items 0)) + (while (re-search-forward "
  • " p2 t) + (setq items (1+ items))) + (goto-char p1) + (while (< item-nr items) + (setq item-nr (1+ item-nr)) + (re-search-forward "
  • " (point-max) t) + (cond + ((string= list-type "ul") (insert " o ")) + ((string= list-type "ol") (insert (format " %s: " item-nr))) + (t (insert " x "))) + ) + ) + ) + +(defun html2text-clean-dtdd (p1 p2) + (goto-char p1) + (let ((items 0) + (item-nr 0)) + (while (re-search-forward "
    " p2 t) + (setq items (1+ items))) + (goto-char p1) + (while (< item-nr items) + (setq item-nr (1+ item-nr)) + (re-search-forward "
    \\([ ]*\\)" (point-max) t) + (when (match-string 1) + (delete-region (point) (- (point) (string-width (match-string 1))))) + (let ((def-p1 (point)) + (def-p2 0)) + (re-search-forward "\\([ ]*\\)\\(
    \\|
    \\)" (point-max) t) + (if (match-string 1) + (progn + (let* ((mw1 (string-width (match-string 1))) + (mw2 (string-width (match-string 2))) + (mw (+ mw1 mw2))) + (goto-char (- (point) mw)) + (delete-region (point) (+ (point) mw1)) + (setq def-p2 (point)))) + (setq def-p2 (- (point) (string-width (match-string 2))))) + (put-text-property def-p1 def-p2 'face 'bold))))) + +(defun html2text-delete-tags (p1 p2 p3 p4) + (delete-region p1 p2) + (delete-region (- p3 (- p2 p1)) (- p4 (- p2 p1)))) + +(defun html2text-delete-single-tag (p1 p2) + (delete-region p1 p2)) + +(defun html2text-clean-hr (p1 p2) + (html2text-delete-single-tag p1 p2) + (goto-char p1) + (newline 1) + (insert (make-string fill-column ?-)) + ) + +(defun html2text-clean-ul (p1 p2 p3 p4) + (html2text-delete-tags p1 p2 p3 p4) + (html2text-clean-list-items p1 (- p3 (- p1 p2)) "ul") + ) + +(defun html2text-clean-ol (p1 p2 p3 p4) + (html2text-delete-tags p1 p2 p3 p4) + (html2text-clean-list-items p1 (- p3 (- p1 p2)) "ol") + ) + +(defun html2text-clean-dl (p1 p2 p3 p4) + (html2text-delete-tags p1 p2 p3 p4) + (html2text-clean-dtdd p1 (- p3 (- p1 p2))) + ) + +(defun html2text-clean-center (p1 p2 p3 p4) + (html2text-delete-tags p1 p2 p3 p4) + (center-region p1 (- p3 (- p2 p1))) + ) + +(defun html2text-clean-bold (p1 p2 p3 p4) + (put-text-property p2 p3 'face 'bold) + (html2text-delete-tags p1 p2 p3 p4) + ) + +(defun html2text-clean-title (p1 p2 p3 p4) + (put-text-property p2 p3 'face 'bold) + (html2text-delete-tags p1 p2 p3 p4) + ) + +(defun html2text-clean-underline (p1 p2 p3 p4) + (put-text-property p2 p3 'face 'underline) + (html2text-delete-tags p1 p2 p3 p4) + ) + +(defun html2text-clean-italic (p1 p2 p3 p4) + (put-text-property p2 p3 'face 'italic) + (html2text-delete-tags p1 p2 p3 p4) + ) + +(defun html2text-clean-font (p1 p2 p3 p4) + (html2text-delete-tags p1 p2 p3 p4) + ) + +(defun html2text-clean-blockquote (p1 p2 p3 p4) + (html2text-delete-tags p1 p2 p3 p4) + ) + +(defun html2text-clean-anchor (p1 p2 p3 p4) + ;; If someone can explain how to make the URL clickable I will + ;; surely improve upon this. + (let* ((attr-list (html2text-get-attr p1 p2 "a")) + (href (html2text-attr-value attr-list "href"))) + (delete-region p1 p4) + (when href + (goto-char p1) + (insert (substring href 1 -1 )) + (put-text-property p1 (point) 'face 'bold)))) + +;; +;; +;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; +;; +;; + +(defun html2text-fix-paragraph (p1 p2) + (goto-char p1) + (let ((has-br-line) + (refill-start) + (refill-stop)) + (if (re-search-forward "
    $" p2 t) + (setq has-br-line t) + ) + (if has-br-line + (progn + (goto-char p1) + (if (re-search-forward ".+[^<][^b][^r][^>]$" p2 t) + (progn + (beginning-of-line) + (setq refill-start (point)) + (goto-char p2) + (re-search-backward ".+[^<][^b][^r][^>]$" refill-start t) + (next-line 1) + (end-of-line) + ;; refill-stop should ideally be adjusted to + ;; accomodate the "
    " strings which are removed + ;; between refill-start and refill-stop. Can simply + ;; be returned from my-replace-string + (setq refill-stop (+ (point) + (html2text-replace-string + "
    " "" + refill-start (point)))) + ;; (message "Point = %s refill-stop = %s" (point) refill-stop) + ;; (sleep-for 4) + (fill-region refill-start refill-stop) + ) + ) + ) + ) + ) + (html2text-replace-string "
    " "" p1 p2) + ) + +;; +;; This one is interactive ... +;; +(defun html2text-fix-paragraphs () + "This _tries_ to fix up the paragraphs - this is done in quite a ad-hook +fashion, quite close to pure guess-work. It does work in some cases though." + (interactive) + (html2text-buffer-head) + (replace-regexp "^
    $" "") + ;; Removing lonely
    on a single line, if they are left intact we + ;; dont have any paragraphs at all. + (html2text-buffer-head) + (while (not (eobp)) + (let ((p1 (point))) + (forward-paragraph 1) + ;;(message "Kaller fix med p1=%s p2=%s " p1 (1- (point))) (sleep-for 5) + (html2text-fix-paragraph p1 (1- (point))) + (goto-char p1) + (when (not (eobp)) + (forward-paragraph 1))))) + +;; +;;
    +;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; +;; +;; + +(defun html2text-remove-tags (tag-list) + "Removes the tags listed in the list \"html2text-remove-tag-list\". +See the documentation for that variable." + (interactive) + (dolist (tag tag-list) + (html2text-buffer-head) + (while (re-search-forward (format "\\(]*>\\)" tag) (point-max) t) + (let ((p1 (point))) + (search-backward "<") + (delete-region (point) p1))))) + +(defun html2text-format-tags () + "See the variable \"html2text-format-tag-list\" for documentation" + (interactive) + (dolist (tag-and-function html2text-format-tag-list) + (let ((tag (car tag-and-function)) + (function (cdr tag-and-function))) + (html2text-buffer-head) + (while (re-search-forward (format "\\(<%s\\( [^>]*\\)?>\\)" tag) + (point-max) t) + (let ((p1) + (p2 (point)) + (p3) (p4) + (attr (match-string 1))) + (search-backward "<" (point-min) t) + (setq p1 (point)) + (re-search-forward (format "" tag) (point-max) t) + (setq p4 (point)) + (search-backward "]*\\)?>\\)" tag) + (point-max) t) + (let ((p1) + (p2 (point))) + (search-backward "<" (point-min) t) + (setq p1 (point)) + (funcall function p1 p2) + ) + ) + ) + ) + ) + +;; +;; Main function +;; + +;;;###autoload +(defun html2text () + "Convert HTML to plain text in the current buffer." + (interactive) + (save-excursion + (let ((case-fold-search t) + (buffer-read-only)) + (html2text-remove-tags html2text-remove-tag-list) + (html2text-format-tags) + (html2text-remove-tags html2text-remove-tag-list2) + (html2text-substitute) + (html2text-format-single-elements) + (html2text-fix-paragraphs)))) + +;; +;; +;; + +;;; html2text.el ends here diff --git a/lisp/mailheader.el b/lisp/mailheader.el new file mode 100644 index 0000000..796ae75 --- /dev/null +++ b/lisp/mailheader.el @@ -0,0 +1,182 @@ +;;; mail-header.el --- Mail header parsing, merging, formatting + +;; Copyright (C) 1996 by Free Software Foundation, Inc. + +;; Author: Erik Naggum +;; Keywords: tools, mail, news + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; This package provides an abstraction to RFC822-style messages, used in +;; mail news, and some other systems. The simple syntactic rules for such +;; headers, such as quoting and line folding, are routinely reimplemented +;; in many individual packages. This package removes the need for this +;; redundancy by representing message headers as association lists, +;; offering functions to extract the set of headers from a message, to +;; parse individual headers, to merge sets of headers, and to format a set +;; of headers. + +;; The car of each element in the message-header alist is a symbol whose +;; print name is the name of the header, in all lower-case. The cdr of an +;; element depends on the operation. After extracting headers from a +;; message, it is a string, the value of the header. An extracted set of +;; headers may be parsed further, which may turn it into a list, whose car +;; is the original value and whose subsequent elements depend on the +;; header. For formatting, it is evaluated to obtain the strings to be +;; inserted. For merging, one set of headers consists of strings, while +;; the other set will be evaluated with the symbols in the first set of +;; headers bound to their respective values. + +;;; Code: + +(eval-when-compile (require 'cl)) + +;; Make the byte-compiler shut up. +(defvar headers) + +(defun mail-header-extract () + "Extract headers from current buffer after point. +Returns a header alist, where each element is a cons cell (name . value), +where NAME is a symbol, and VALUE is the string value of the header having +that name." + (let ((message-headers ()) (top (point)) + start end) + (while (and (setq start (point)) + (> (skip-chars-forward "^\0- :") 0) + (eq (char-after) ?:) + (setq end (point)) + (progn (forward-char) + (> (skip-chars-forward " \t") 0))) + (let ((header (intern (downcase (buffer-substring start end)))) + (value (list (buffer-substring + (point) (progn (end-of-line) (point)))))) + (while (progn (forward-char) (> (skip-chars-forward " \t") 0)) + (push (buffer-substring (point) (progn (end-of-line) (point))) + value)) + (push (if (cdr value) + (cons header (mapconcat #'identity (nreverse value) " ")) + (cons header (car value))) + message-headers))) + (goto-char top) + (nreverse message-headers))) + +(defun mail-header-extract-no-properties () + "Extract headers from current buffer after point, without properties. +Returns a header alist, where each element is a cons cell (name . value), +where NAME is a symbol, and VALUE is the string value of the header having +that name." + (mapcar + (lambda (elt) + (set-text-properties 0 (length (cdr elt)) nil (cdr elt)) + elt) + (mail-header-extract))) + +(defun mail-header-parse (parsing-rules headers) + "Apply PARSING-RULES to HEADERS. +PARSING-RULES is an alist whose keys are header names (symbols) and whose +value is a parsing function. The function takes one argument, a string, +and return a list of values, which will destructively replace the value +associated with the key in HEADERS, after being prepended with the original +value." + (dolist (rule parsing-rules) + (let ((header (assq (car rule) headers))) + (when header + (if (consp (cdr header)) + (setf (cddr header) (funcall (cdr rule) (cadr header))) + (setf (cdr header) + (cons (cdr header) (funcall (cdr rule) (cdr header)))))))) + headers) + +(defsubst mail-header (header &optional header-alist) + "Return the value associated with header HEADER in HEADER-ALIST. +If the value is a string, it is the original value of the header. If the +value is a list, its first element is the original value of the header, +with any subsequent elements being the result of parsing the value. +If HEADER-ALIST is nil, the dynamically bound variable `headers' is used." + (cdr (assq header (or header-alist headers)))) + +(defun mail-header-set (header value &optional header-alist) + "Set the value associated with header HEADER to VALUE in HEADER-ALIST. +HEADER-ALIST defaults to the dynamically bound variable `headers' if nil. +See `mail-header' for the semantics of VALUE." + (let* ((alist (or header-alist headers)) + (entry (assq header alist))) + (if entry + (setf (cdr entry) value) + (nconc alist (list (cons header value))))) + value) + +(defsetf mail-header (header &optional header-alist) (value) + `(mail-header-set ,header ,value ,header-alist)) + +(defun mail-header-merge (merge-rules headers) + "Return a new header alist with MERGE-RULES applied to HEADERS. +MERGE-RULES is an alist whose keys are header names (symbols) and whose +values are forms to evaluate, the results of which are the new headers. It +should be a string or a list of string. The first element may be nil to +denote that the formatting functions must use the remaining elements, or +skip the header altogether if there are no other elements. + The macro `mail-header' can be used to access headers in HEADERS." + (mapcar + (lambda (rule) + (cons (car rule) (eval (cdr rule)))) + merge-rules)) + +(defvar mail-header-format-function + (lambda (header value) + "Function to format headers without a specified formatting function." + (insert (capitalize (symbol-name header)) + ": " + (if (consp value) (car value) value) + "\n"))) + +(defun mail-header-format (format-rules headers) + "Use FORMAT-RULES to format HEADERS and insert into current buffer. +FORMAT-RULES is an alist whose keys are header names (symbols), and whose +values are functions that format the header, the results of which are +inserted, unless it is nil. The function takes two arguments, the header +symbol, and the value of that header. If the function itself is nil, the +default action is to insert the value of the header, unless it is nil. +The headers are inserted in the order of the FORMAT-RULES. +A key of t represents any otherwise unmentioned headers. +A key of nil has as its value a list of defaulted headers to ignore." + (let ((ignore (append (cdr (assq nil format-rules)) + (mapcar #'car format-rules)))) + (dolist (rule format-rules) + (let* ((header (car rule)) + (value (mail-header header))) + (cond ((null header) 'ignore) + ((eq header t) + (dolist (defaulted headers) + (unless (memq (car defaulted) ignore) + (let* ((header (car defaulted)) + (value (cdr defaulted))) + (if (cdr rule) + (funcall (cdr rule) header value) + (funcall mail-header-format-function header value)))))) + (value + (if (cdr rule) + (funcall (cdr rule) header value) + (funcall mail-header-format-function header value)))))) + (insert "\n"))) + +(provide 'mailheader) + +;;; mail-header.el ends here diff --git a/lisp/mm-extern.el b/lisp/mm-extern.el new file mode 100644 index 0000000..2627397 --- /dev/null +++ b/lisp/mm-extern.el @@ -0,0 +1,168 @@ +;;; mm-extern.el --- showing message/external-body +;; Copyright (C) 2000, 2001, 2003 Free Software Foundation, Inc. + +;; Author: Shenghuo Zhu +;; Keywords: message external-body + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your +;; option) any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;;; Code: + +(eval-when-compile (require 'cl)) + +(require 'mm-util) +(require 'mm-decode) +(require 'mm-url) + +(defvar mm-extern-function-alist + '((local-file . mm-extern-local-file) + (url . mm-extern-url) + (anon-ftp . mm-extern-anon-ftp) + (ftp . mm-extern-ftp) +;;; (tftp . mm-extern-tftp) + (mail-server . mm-extern-mail-server) +;;; (afs . mm-extern-afs)) + )) + +(defvar mm-extern-anonymous "anonymous") + +(defun mm-extern-local-file (handle) + (erase-buffer) + (let ((name (cdr (assq 'name (cdr (mm-handle-type handle))))) + (coding-system-for-read mm-binary-coding-system)) + (unless name + (error "The filename is not specified")) + (mm-disable-multibyte-mule4) + (if (file-exists-p name) + (mm-insert-file-contents name nil nil nil nil t) + (error (format "File %s is gone" name))))) + +(defun mm-extern-url (handle) + (erase-buffer) + (let ((url (cdr (assq 'url (cdr (mm-handle-type handle))))) + (name buffer-file-name) + (coding-system-for-read mm-binary-coding-system)) + (unless url + (error "URL is not specified")) + (mm-with-unibyte-current-buffer-mule4 + (mm-url-insert-file-contents url)) + (mm-disable-multibyte-mule4) + (setq buffer-file-name name))) + +(defun mm-extern-anon-ftp (handle) + (erase-buffer) + (let* ((params (cdr (mm-handle-type handle))) + (name (cdr (assq 'name params))) + (site (cdr (assq 'site params))) + (directory (cdr (assq 'directory params))) + (mode (cdr (assq 'mode params))) + (path (concat "/" (or mm-extern-anonymous + (read-string (format "ID for %s: " site))) + "@" site ":" directory "/" name)) + (coding-system-for-read mm-binary-coding-system)) + (unless name + (error "The filename is not specified")) + (mm-disable-multibyte-mule4) + (mm-insert-file-contents path nil nil nil nil t))) + +(defun mm-extern-ftp (handle) + (let (mm-extern-anonymous) + (mm-extern-anon-ftp handle))) + +(defun mm-extern-mail-server (handle) + (require 'message) + (let* ((params (cdr (mm-handle-type handle))) + (server (cdr (assq 'server params))) + (subject (or (cdr (assq 'subject params)) "none")) + (buf (current-buffer)) + info) + (if (y-or-n-p (format "Send a request message to %s?" server)) + (save-window-excursion + (message-mail server subject) + (message-goto-body) + (delete-region (point) (point-max)) + (insert-buffer-substring buf) + (message "Requesting external body...") + (message-send-and-exit) + (setq info "Request is sent.") + (message info)) + (setq info "Request is not sent.")) + (goto-char (point-min)) + (insert "[" info "]\n\n"))) + +;;;###autoload +(defun mm-inline-external-body (handle &optional no-display) + "Show the external-body part of HANDLE. +This function replaces the buffer of HANDLE with a buffer contains +the entire message. +If NO-DISPLAY is nil, display it. Otherwise, do nothing after replacing." + (let* ((access-type (cdr (assq 'access-type + (cdr (mm-handle-type handle))))) + (func (cdr (assq (intern + (downcase + (or access-type + (error "Couldn't find access type")))) + mm-extern-function-alist))) + gnus-displaying-mime buf + handles) + (unless (mm-handle-cache handle) + (unless func + (error (format "Access type (%s) is not supported" access-type))) + (with-temp-buffer + (mm-insert-part handle) + (goto-char (point-max)) + (insert "\n\n") + (setq handles (mm-dissect-buffer t))) + (unless (bufferp (car handles)) + (mm-destroy-parts handles) + (error "Multipart external body is not supported")) + (save-excursion ;; single part + (set-buffer (setq buf (mm-handle-buffer handles))) + (let (good) + (unwind-protect + (progn + (funcall func handle) + (setq good t)) + (unless good + (mm-destroy-parts handles)))) + (mm-handle-set-cache handle handles)) + (setq gnus-article-mime-handles + (mm-merge-handles gnus-article-mime-handles handles))) + (unless no-display + (save-excursion + (save-restriction + (narrow-to-region (point) (point)) + (gnus-display-mime (mm-handle-cache handle)) + (mm-handle-set-undisplayer + handle + `(lambda () + (let (buffer-read-only) + (condition-case nil + ;; This is only valid on XEmacs. + (mapcar (lambda (prop) + (remove-specifier + (face-property 'default prop) (current-buffer))) + '(background background-pixmap foreground)) + (error nil)) + (delete-region ,(point-min-marker) ,(point-max-marker)))))))))) + +(provide 'mm-extern) + +;;; mm-extern.el ends here diff --git a/lisp/mm-url.el b/lisp/mm-url.el new file mode 100644 index 0000000..42d3fce --- /dev/null +++ b/lisp/mm-url.el @@ -0,0 +1,445 @@ +;;; mm-url.el --- a wrapper of url functions/commands for Gnus +;; Copyright (C) 2001, 2002, 2003 Free Software Foundation, Inc. + +;; Author: Shenghuo Zhu + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your +;; option) any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; Some codes are stolen from w3 and url packages. Some are moved from +;; nnweb. + +;; TODO: Support POST, cookie. + +;;; Code: + +(eval-when-compile (require 'cl)) + +(require 'mm-util) +(require 'gnus) + +(eval-and-compile + (autoload 'exec-installed-p "path-util")) + +(eval-when-compile + (require 'timer)) + +(defgroup mm-url nil + "A wrapper of url package and external url command for Gnus." + :group 'gnus) + +(defcustom mm-url-use-external (not + (condition-case nil + (require 'url) + (error nil))) + "*If non-nil, use external grab program `mm-url-program'." + :type 'boolean + :group 'mm-url) + +(defvar mm-url-predefined-programs + '((wget "wget" "-q" "-O" "-") + (w3m "w3m" "-dump_source") + (lynx "lynx" "-source") + (curl "curl"))) + +(defcustom mm-url-program + (cond + ((exec-installed-p "wget") 'wget) + ((exec-installed-p "w3m") 'w3m) + ((exec-installed-p "lynx") 'lynx) + ((exec-installed-p "curl") 'curl) + (t "GET")) + "The url grab program. +Likely values are `wget', `w3m', `lynx' and `curl'." + :type '(choice + (symbol :tag "wget" wget) + (symbol :tag "w3m" w3m) + (symbol :tag "lynx" lynx) + (symbol :tag "curl" curl) + (string :tag "other")) + :group 'mm-url) + +(defcustom mm-url-arguments nil + "The arguments for `mm-url-program'." + :type '(repeat string) + :group 'mm-url) + + +;;; Internal variables + +(defvar mm-url-package-name + (gnus-replace-in-string + (gnus-replace-in-string gnus-version " v.*$" "") + " " "-")) + +(defvar mm-url-package-version gnus-version-number) + +;; Stolen from w3. +(defvar mm-url-html-entities + '( + ;;(excl . 33) + (quot . 34) + ;;(num . 35) + ;;(dollar . 36) + ;;(percent . 37) + (amp . 38) + (rsquo . 39) ; should be U+8217 + ;;(apos . 39) + ;;(lpar . 40) + ;;(rpar . 41) + ;;(ast . 42) + ;;(plus . 43) + ;;(comma . 44) + ;;(period . 46) + ;;(colon . 58) + ;;(semi . 59) + (lt . 60) + ;;(equals . 61) + (gt . 62) + ;;(quest . 63) + ;;(commat . 64) + ;;(lsqb . 91) + ;;(rsqb . 93) + (uarr . 94) ; should be U+8593 + ;;(lowbar . 95) + (lsquo . 96) ; should be U+8216 + (lcub . 123) + ;;(verbar . 124) + (rcub . 125) + (tilde . 126) + (nbsp . 160) + (iexcl . 161) + (cent . 162) + (pound . 163) + (curren . 164) + (yen . 165) + (brvbar . 166) + (sect . 167) + (uml . 168) + (copy . 169) + (ordf . 170) + (laquo . 171) + (not . 172) + (shy . 173) + (reg . 174) + (macr . 175) + (deg . 176) + (plusmn . 177) + (sup2 . 178) + (sup3 . 179) + (acute . 180) + (micro . 181) + (para . 182) + (middot . 183) + (cedil . 184) + (sup1 . 185) + (ordm . 186) + (raquo . 187) + (frac14 . 188) + (frac12 . 189) + (frac34 . 190) + (iquest . 191) + (Agrave . 192) + (Aacute . 193) + (Acirc . 194) + (Atilde . 195) + (Auml . 196) + (Aring . 197) + (AElig . 198) + (Ccedil . 199) + (Egrave . 200) + (Eacute . 201) + (Ecirc . 202) + (Euml . 203) + (Igrave . 204) + (Iacute . 205) + (Icirc . 206) + (Iuml . 207) + (ETH . 208) + (Ntilde . 209) + (Ograve . 210) + (Oacute . 211) + (Ocirc . 212) + (Otilde . 213) + (Ouml . 214) + (times . 215) + (Oslash . 216) + (Ugrave . 217) + (Uacute . 218) + (Ucirc . 219) + (Uuml . 220) + (Yacute . 221) + (THORN . 222) + (szlig . 223) + (agrave . 224) + (aacute . 225) + (acirc . 226) + (atilde . 227) + (auml . 228) + (aring . 229) + (aelig . 230) + (ccedil . 231) + (egrave . 232) + (eacute . 233) + (ecirc . 234) + (euml . 235) + (igrave . 236) + (iacute . 237) + (icirc . 238) + (iuml . 239) + (eth . 240) + (ntilde . 241) + (ograve . 242) + (oacute . 243) + (ocirc . 244) + (otilde . 245) + (ouml . 246) + (divide . 247) + (oslash . 248) + (ugrave . 249) + (uacute . 250) + (ucirc . 251) + (uuml . 252) + (yacute . 253) + (thorn . 254) + (yuml . 255) + + ;; Special handling of these + (frac56 . "5/6") + (frac16 . "1/6") + (frac45 . "4/5") + (frac35 . "3/5") + (frac25 . "2/5") + (frac15 . "1/5") + (frac23 . "2/3") + (frac13 . "1/3") + (frac78 . "7/8") + (frac58 . "5/8") + (frac38 . "3/8") + (frac18 . "1/8") + + ;; The following 5 entities are not mentioned in the HTML 2.0 + ;; standard, nor in any other HTML proposed standard of which I + ;; am aware. I am not even sure they are ISO entity names. *** + ;; Hence, some arrangement should be made to give a bad HTML + ;; message when they are seen. + (ndash . 45) + (mdash . 45) + (emsp . 32) + (ensp . 32) + (sim . 126) + (le . "<=") + (agr . "alpha") + (rdquo . "''") + (ldquo . "``") + (trade . "(TM)") + ;; To be done + ;; (shy . ????) ; soft hyphen + ) + "*An assoc list of entity names and how to actually display them.") + +(defconst mm-url-unreserved-chars + '( + ?a ?b ?c ?d ?e ?f ?g ?h ?i ?j ?k ?l ?m ?n ?o ?p ?q ?r ?s ?t ?u ?v ?w ?x ?y ?z + ?A ?B ?C ?D ?E ?F ?G ?H ?I ?J ?K ?L ?M ?N ?O ?P ?Q ?R ?S ?T ?U ?V ?W ?X ?Y ?Z + ?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9 + ?- ?_ ?. ?! ?~ ?* ?' ?\( ?\)) + "A list of characters that are _NOT_ reserved in the URL spec. +This is taken from RFC 2396.") + +(defun mm-url-load-url () + "Load `url-insert-file-contents'." + (unless (condition-case () + (require 'url-handlers) + (error nil)) + ;; w3-4.0pre0.46 or earlier version. + (require 'w3-vars) + (require 'url))) + +(defun mm-url-insert-file-contents (url) + (if mm-url-use-external + (progn + (if (string-match "^file:/+" url) + (insert-file-contents (substring url (1- (match-end 0)))) + (mm-url-insert-file-contents-external url)) + (goto-char (point-min)) + (if (fboundp 'url-generic-parse-url) + (setq url-current-object + (url-generic-parse-url url))) + (list url (buffer-size))) + (mm-url-load-url) + (let ((name buffer-file-name) + (url-request-extra-headers (list (cons "Connection" "Close"))) + (url-package-name (or mm-url-package-name + url-package-name)) + (url-package-version (or mm-url-package-version + url-package-version)) + result) + (setq result (url-insert-file-contents url)) + (save-excursion + (goto-char (point-min)) + (while (re-search-forward "\r 1000\r ?" nil t) + (replace-match ""))) + (setq buffer-file-name name) + (if (and (fboundp 'url-generic-parse-url) + (listp result)) + (setq url-current-object (url-generic-parse-url + (car result)))) + result))) + +(defun mm-url-insert-file-contents-external (url) + (let (program args) + (if (symbolp mm-url-program) + (let ((item (cdr (assq mm-url-program mm-url-predefined-programs)))) + (setq program (car item) + args (append (cdr item) (list url)))) + (setq program mm-url-program + args (append mm-url-arguments (list url)))) + (apply 'call-process program nil t nil args))) + +(defvar mm-url-timeout 30 + "The number of seconds before timing out an URL fetch.") + +(defvar mm-url-retries 10 + "The number of retries after timing out when fetching an URL.") + +(defun mm-url-insert (url &optional follow-refresh) + "Insert the contents from an URL in the current buffer. +If FOLLOW-REFRESH is non-nil, redirect refresh url in META." + (let ((times mm-url-retries) + (done nil) + (first t) + result) + (while (and (not (zerop (decf times))) + (not done)) + (with-timeout (mm-url-timeout) + (unless first + (message "Trying again (%s)..." (- mm-url-retries times))) + (setq first nil) + (if follow-refresh + (save-restriction + (narrow-to-region (point) (point)) + (mm-url-insert-file-contents url) + (goto-char (point-min)) + (when (re-search-forward + "]*URL=\\([^\"]+\\)\"" nil t) + (let ((url (match-string 1))) + (delete-region (point-min) (point-max)) + (setq result (mm-url-insert url t))))) + (setq result (mm-url-insert-file-contents url))) + (setq done t))) + result)) + +(defun mm-url-decode-entities () + "Decode all HTML entities." + (goto-char (point-min)) + (while (re-search-forward "&\\(#[0-9]+\\|[a-z]+\\);" nil t) + (let ((elem (if (eq (aref (match-string 1) 0) ?\#) + (let ((c + (string-to-number (substring + (match-string 1) 1)))) + (if (mm-char-or-char-int-p c) c 32)) + (or (cdr (assq (intern (match-string 1)) + mm-url-html-entities)) + ?#)))) + (unless (stringp elem) + (setq elem (char-to-string elem))) + (replace-match elem t t)))) + +(defun mm-url-decode-entities-nbsp () + "Decode all HTML entities and   to a space." + (let ((mm-url-html-entities (cons '(nbsp . 32) mm-url-html-entities))) + (mm-url-decode-entities))) + +(defun mm-url-decode-entities-string (string) + (with-temp-buffer + (insert string) + (mm-url-decode-entities) + (buffer-string))) + +(defun mm-url-form-encode-xwfu (chunk) + "Escape characters in a string for application/x-www-form-urlencoded. +Blasphemous crap because someone didn't think %20 was good enough for encoding +spaces. Die Die Die." + ;; This will get rid of the 'attributes' specified by the file type, + ;; which are useless for an application/x-www-form-urlencoded form. + (if (consp chunk) + (setq chunk (cdr chunk))) + + (mapconcat + (lambda (char) + (cond + ((= char ? ) "+") + ((memq char mm-url-unreserved-chars) (char-to-string char)) + (t (upcase (format "%%%02x" char))))) + ;; Fixme: Should this actually be accepting multibyte? Is there a + ;; better way in XEmacs? + (if (featurep 'mule) + (encode-coding-string chunk + (if (fboundp 'find-coding-systems-string) + (car (find-coding-systems-string chunk)) + (static-if (boundp 'MULE) + file-coding-system + buffer-file-coding-system))) + chunk) + "")) + +(defun mm-url-encode-www-form-urlencoded (pairs) + "Return PAIRS encoded for forms." + (mapconcat + (lambda (data) + (concat (mm-url-form-encode-xwfu (car data)) "=" + (mm-url-form-encode-xwfu (cdr data)))) + pairs "&")) + +(defun mm-url-fetch-form (url pairs) + "Fetch a form from URL with PAIRS as the data using the POST method." + (mm-url-load-url) + (let ((url-request-data (mm-url-encode-www-form-urlencoded pairs)) + (url-request-method "POST") + (url-request-extra-headers + '(("Content-type" . "application/x-www-form-urlencoded")))) + (url-insert-file-contents url) + (setq buffer-file-name nil)) + t) + +(defun mm-url-fetch-simple (url content) + (mm-url-load-url) + (let ((url-request-data content) + (url-request-method "POST") + (url-request-extra-headers + '(("Content-type" . "application/x-www-form-urlencoded")))) + (url-insert-file-contents url) + (setq buffer-file-name nil)) + t) + +(defun mm-url-remove-markup () + "Remove all HTML markup, leaving just plain text." + (goto-char (point-min)) + (while (search-forward "" nil t) + (point-max)))) + (goto-char (point-min)) + (while (re-search-forward "<[^>]+>" nil t) + (replace-match "" t t))) + +(provide 'mm-url) + +;;; mm-url.el ends here diff --git a/lisp/mml-sec.el b/lisp/mml-sec.el new file mode 100644 index 0000000..c18cf2f --- /dev/null +++ b/lisp/mml-sec.el @@ -0,0 +1,275 @@ +;;; mml-sec.el --- A package with security functions for MML documents +;; Copyright (C) 2000, 2001, 2002, 2003 Free Software Foundation, Inc. + +;; Author: Simon Josefsson + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;;; Code: + +(require 'mml-smime) +(eval-when-compile (require 'cl)) +(autoload 'mml2015-sign "mml2015") +(autoload 'mml2015-encrypt "mml2015") +(autoload 'mml1991-sign "mml1991") +(autoload 'mml1991-encrypt "mml1991") +(autoload 'message-goto-body "message") +(autoload 'mml-insert-tag "mml") + +(defvar mml-sign-alist + '(("smime" mml-smime-sign-buffer mml-smime-sign-query) + ("pgp" mml-pgp-sign-buffer list) + ("pgpauto" mml-pgpauto-sign-buffer list) + ("pgpmime" mml-pgpmime-sign-buffer list)) + "Alist of MIME signer functions.") + +(defvar mml-default-sign-method (caar mml-sign-alist) + "Default sign method.") + +(defvar mml-encrypt-alist + '(("smime" mml-smime-encrypt-buffer mml-smime-encrypt-query) + ("pgp" mml-pgp-encrypt-buffer list) + ("pgpauto" mml-pgpauto-sign-buffer list) + ("pgpmime" mml-pgpmime-encrypt-buffer list)) + "Alist of MIME encryption functions.") + +(defvar mml-default-encrypt-method (caar mml-encrypt-alist) + "Default encryption method.") + +(defcustom mml-signencrypt-style-alist + '(("smime" separate) + ("pgp" separate) + ("pgpauto" separate) + ("pgpmime" separate)) + "Alist specifying if `signencrypt' results in two separate operations or not. +The first entry indicates the MML security type, valid entries include +the strings \"smime\", \"pgp\", and \"pgpmime\". The second entry is +a symbol `separate' or `combined' where `separate' means that MML signs +and encrypt messages in a two step process, and `combined' means that MML +signs and encrypt the message in one step. +Note that the `combined' mode is NOT supported by all OpenPGP implementations, +in particular PGP version 2 does not support it!" + :type '(repeat (list (choice (const :tag "S/MIME" "smime") + (const :tag "PGP" "pgp") + (const :tag "PGP/MIME" "pgpmime") + (string :tag "User defined")) + (choice (const :tag "Separate" separate) + (const :tag "Combined" combined))))) + +;;; Configuration/helper functions + +(defun mml-signencrypt-style (method &optional style) + "Function for setting/getting the signencrypt-style used. Takes two +arguments, the method (e.g. \"pgp\") and optionally the mode +\(e.g. combined). If the mode is omitted, the current value is returned. + +For example, if you prefer to use combined sign & encrypt with +smime, putting the following in your Gnus startup file will +enable that behavior: + +\(mml-set-signencrypt-style \"smime\" combined) + +You can also customize or set `mml-signencrypt-style-alist' instead." + (let ((style-item (assoc method mml-signencrypt-style-alist))) + (if style-item + (if (or (eq style 'separate) + (eq style 'combined)) + ;; valid style setting? + (setf (second style-item) style) + ;; otherwise, just return the current value + (second style-item)) + (gnus-message 3 "Warning, attempt to set invalid signencrypt-style")))) + +;;; Security functions + +(defun mml-smime-sign-buffer (cont) + (or (mml-smime-sign cont) + (error "Signing failed... inspect message logs for errors"))) + +(defun mml-smime-encrypt-buffer (cont &optional sign) + (when sign + (message "Combined sign and encrypt S/MIME not support yet") + (sit-for 1)) + (or (mml-smime-encrypt cont) + (error "Encryption failed... inspect message logs for errors"))) + +(defun mml-pgp-sign-buffer (cont) + (or (mml1991-sign cont) + (error "Signing failed... inspect message logs for errors"))) + +(defun mml-pgp-encrypt-buffer (cont &optional sign) + (or (mml1991-encrypt cont sign) + (error "Encryption failed... inspect message logs for errors"))) + +(defun mml-pgpmime-sign-buffer (cont) + (or (mml2015-sign cont) + (error "Signing failed... inspect message logs for errors"))) + +(defun mml-pgpmime-encrypt-buffer (cont &optional sign) + (or (mml2015-encrypt cont sign) + (error "Encryption failed... inspect message logs for errors"))) + +(defun mml-pgpauto-sign-buffer (cont) + (message-goto-body) + (or (if (re-search-backward "Content-Type: *multipart/.*" nil t) ; there must be a better way... + (mml2015-sign cont) + (mml1991-sign cont)) + (error "Encryption failed... inspect message logs for errors"))) + +(defun mml-pgpauto-encrypt-buffer (cont &optional sign) + (message-goto-body) + (or (if (re-search-backward "Content-Type: *multipart/.*" nil t) ; there must be a better way... + (mml2015-encrypt cont sign) + (mml1991-encrypt cont sign)) + (error "Encryption failed... inspect message logs for errors"))) + +(defun mml-secure-part (method &optional sign) + (save-excursion + (let ((tags (funcall (nth 2 (assoc method (if sign mml-sign-alist + mml-encrypt-alist)))))) + (cond ((re-search-backward + "<#\\(multipart\\|part\\|external\\|mml\\)" nil t) + (goto-char (match-end 0)) + (insert (if sign " sign=" " encrypt=") method) + (while tags + (let ((key (pop tags)) + (value (pop tags))) + (when value + ;; Quote VALUE if it contains suspicious characters. + (when (string-match "[\"'\\~/*;() \t\n]" value) + (setq value (prin1-to-string value))) + (insert (format " %s=%s" key value)))))) + ((or (re-search-backward + (concat "^" (regexp-quote mail-header-separator) "\n") nil t) + (re-search-forward + (concat "^" (regexp-quote mail-header-separator) "\n") nil t)) + (goto-char (match-end 0)) + (apply 'mml-insert-tag 'part (cons (if sign 'sign 'encrypt) + (cons method tags)))) + (t (error "The message is corrupted. No mail header separator")))))) + +(defun mml-secure-sign-pgp () + "Add MML tags to PGP sign this MML part." + (interactive) + (mml-secure-part "pgp" 'sign)) + +(defun mml-secure-sign-pgpauto () + "Add MML tags to PGP-auto sign this MML part." + (interactive) + (mml-secure-part "pgpauto" 'sign)) + +(defun mml-secure-sign-pgpmime () + "Add MML tags to PGP/MIME sign this MML part." + (interactive) + (mml-secure-part "pgpmime" 'sign)) + +(defun mml-secure-sign-smime () + "Add MML tags to S/MIME sign this MML part." + (interactive) + (mml-secure-part "smime" 'sign)) + +(defun mml-secure-encrypt-pgp () + "Add MML tags to PGP encrypt this MML part." + (interactive) + (mml-secure-part "pgp")) + +(defun mml-secure-encrypt-pgpmime () + "Add MML tags to PGP/MIME encrypt this MML part." + (interactive) + (mml-secure-part "pgpmime")) + +(defun mml-secure-encrypt-smime () + "Add MML tags to S/MIME encrypt this MML part." + (interactive) + (mml-secure-part "smime")) + +;; defuns that add the proper <#secure ...> tag to the top of the message body +(defun mml-secure-message (method &optional modesym) + (let ((mode (prin1-to-string modesym)) + insert-loc) + (mml-unsecure-message) + (save-excursion + (goto-char (point-min)) + (cond ((re-search-forward + (concat "^" (regexp-quote mail-header-separator) "\n") nil t) + (goto-char (setq insert-loc (match-end 0))) + (unless (looking-at "<#secure") + (mml-insert-tag + 'secure 'method method 'mode mode))) + (t (error + "The message is corrupted. No mail header separator")))) + (when (eql insert-loc (point)) + (forward-line 1)))) + +(defun mml-unsecure-message () + "Remove security related MML tags from message." + (interactive) + (save-excursion + (goto-char (point-max)) + (when (re-search-backward "^<#secure.*>\n" nil t) + (delete-region (match-beginning 0) (match-end 0))))) + +(defun mml-secure-message-sign-smime () + "Add MML tag to encrypt/sign the entire message." + (interactive) + (mml-secure-message "smime" 'sign)) + +(defun mml-secure-message-sign-pgp () + "Add MML tag to encrypt/sign the entire message." + (interactive) + (mml-secure-message "pgp" 'sign)) + +(defun mml-secure-message-sign-pgpmime () + "Add MML tag to encrypt/sign the entire message." + (interactive) + (mml-secure-message "pgpmime" 'sign)) + +(defun mml-secure-message-sign-pgpauto () + "Add MML tag to encrypt/sign the entire message." + (interactive) + (mml-secure-message "pgpauto" 'sign)) + +(defun mml-secure-message-encrypt-smime (&optional dontsign) + "Add MML tag to encrypt and sign the entire message. +If called with a prefix argument, only encrypt (do NOT sign)." + (interactive "P") + (mml-secure-message "smime" (if dontsign 'encrypt 'signencrypt))) + +(defun mml-secure-message-encrypt-pgp (&optional dontsign) + "Add MML tag to encrypt and sign the entire message. +If called with a prefix argument, only encrypt (do NOT sign)." + (interactive "P") + (mml-secure-message "pgp" (if dontsign 'encrypt 'signencrypt))) + +(defun mml-secure-message-encrypt-pgpmime (&optional dontsign) + "Add MML tag to encrypt and sign the entire message. +If called with a prefix argument, only encrypt (do NOT sign)." + (interactive "P") + (mml-secure-message "pgpmime" (if dontsign 'encrypt 'signencrypt))) + +(defun mml-secure-message-encrypt-pgpauto (&optional dontsign) + "Add MML tag to encrypt and sign the entire message. +If called with a prefix argument, only encrypt (do NOT sign)." + (interactive "P") + (mml-secure-message "pgpauto" (if dontsign 'encrypt 'signencrypt))) + +(provide 'mml-sec) + +;;; mml-sec.el ends here diff --git a/lisp/mml-smime.el b/lisp/mml-smime.el new file mode 100644 index 0000000..bccc8a1 --- /dev/null +++ b/lisp/mml-smime.el @@ -0,0 +1,196 @@ +;;; mml-smime.el --- S/MIME support for MML +;; Copyright (c) 2000, 2001, 2003 Free Software Foundation, Inc. + +;; Author: Simon Josefsson +;; Keywords: Gnus, MIME, S/MIME, MML + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your +;; option) any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;;; Code: + +(require 'smime) +(require 'mm-decode) + +(defun mml-smime-sign (cont) + (when (null smime-keys) + (customize-variable 'smime-keys) + (error "No S/MIME keys configured, use customize to add your key")) + (smime-sign-buffer (cdr (assq 'keyfile cont))) + (goto-char (point-max))) + +(defun mml-smime-encrypt (cont) + (let (certnames certfiles tmp file tmpfiles) + ;; xxx tmp files are always an security issue + (while (setq tmp (pop cont)) + (if (and (consp tmp) (eq (car tmp) 'certfile)) + (push (cdr tmp) certnames))) + (while (setq tmp (pop certnames)) + (if (not (and (not (file-exists-p tmp)) + (get-buffer tmp))) + (push tmp certfiles) + (setq file (mm-make-temp-file (expand-file-name "mml." + mm-tmp-directory))) + (with-current-buffer tmp + (write-region (point-min) (point-max) file)) + (push file certfiles) + (push file tmpfiles))) + (if (smime-encrypt-buffer certfiles) + (progn + (while (setq tmp (pop tmpfiles)) + (delete-file tmp)) + t) + (while (setq tmp (pop tmpfiles)) + (delete-file tmp)) + nil)) + (goto-char (point-max))) + +(defun mml-smime-sign-query () + ;; query information (what certificate) from user when MML tag is + ;; added, for use later by the signing process + (when (null smime-keys) + (customize-variable 'smime-keys) + (error "No S/MIME keys configured, use customize to add your key")) + (list 'keyfile + (if (= (length smime-keys) 1) + (cadar smime-keys) + (or (let ((from (cadr (funcall gnus-extract-address-components + (or (save-excursion + (save-restriction + (message-narrow-to-headers) + (message-fetch-field "from"))) + ""))))) + (and from (smime-get-key-by-email from))) + (smime-get-key-by-email + (completing-read "Sign this part with what signature? " + smime-keys nil nil + (and (listp (car-safe smime-keys)) + (caar smime-keys)))))))) + +(defun mml-smime-get-file-cert () + (ignore-errors + (list 'certfile (read-file-name + "File with recipient's S/MIME certificate: " + smime-certificate-directory nil t "")))) + +(defun mml-smime-get-dns-cert () + ;; todo: deal with comma separated multiple recipients + (let (result who bad cert) + (condition-case () + (while (not result) + (setq who (read-from-minibuffer + (format "%sLookup certificate for: " (or bad "")) + (cadr (funcall gnus-extract-address-components + (or (save-excursion + (save-restriction + (message-narrow-to-headers) + (message-fetch-field "to"))) + ""))))) + (if (setq cert (smime-cert-by-dns who)) + (setq result (list 'certfile (buffer-name cert))) + (setq bad (format "`%s' not found. " who)))) + (quit)) + result)) + +(defun mml-smime-encrypt-query () + ;; todo: add ldap support (xemacs ldap api?) + ;; todo: try dns/ldap automatically first, before prompting user + (let (certs done) + (while (not done) + (ecase (read (gnus-completing-read-with-default + "dns" "Fetch certificate from" + '(("dns") ("file")) nil t)) + (dns (setq certs (append certs + (mml-smime-get-dns-cert)))) + (file (setq certs (append certs + (mml-smime-get-file-cert))))) + (setq done (not (y-or-n-p "Add more recipients? ")))) + certs)) + +(defun mml-smime-verify (handle ctl) + (with-temp-buffer + (insert-buffer-substring (mm-handle-multipart-original-buffer ctl)) + (goto-char (point-min)) + (insert (format "Content-Type: %s; " (mm-handle-media-type ctl))) + (insert (format "protocol=\"%s\"; " + (mm-handle-multipart-ctl-parameter ctl 'protocol))) + (insert (format "micalg=\"%s\"; " + (mm-handle-multipart-ctl-parameter ctl 'micalg))) + (insert (format "boundary=\"%s\"\n\n" + (mm-handle-multipart-ctl-parameter ctl 'boundary))) + (when (get-buffer smime-details-buffer) + (kill-buffer smime-details-buffer)) + (let ((buf (current-buffer)) + (good-signature (smime-noverify-buffer)) + (good-certificate (and (or smime-CA-file smime-CA-directory) + (smime-verify-buffer))) + addresses openssl-output) + (setq openssl-output (with-current-buffer smime-details-buffer + (buffer-string))) + (if (not good-signature) + (progn + ;; we couldn't verify message, fail with openssl output as message + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Failed") + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details + (concat "OpenSSL failed to verify message integrity:\n" + "-------------------------------------------\n" + openssl-output))) + ;; verify mail addresses in mail against those in certificate + (when (and (smime-pkcs7-region (point-min) (point-max)) + (smime-pkcs7-certificates-region (point-min) (point-max))) + (with-temp-buffer + (insert-buffer-substring buf) + (goto-char (point-min)) + (while (re-search-forward "-----END CERTIFICATE-----" nil t) + (when (smime-pkcs7-email-region (point-min) (point)) + (setq addresses (append (smime-buffer-as-string-region + (point-min) (point)) addresses))) + (delete-region (point-min) (point))) + (setq addresses (mapcar 'downcase addresses)))) + (if (not (member (downcase (or (mm-handle-multipart-from ctl) "")) addresses)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Sender address forged") + (if good-certificate + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Ok (sender authenticated)") + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Ok (sender not trusted)"))) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details + (concat "Sender claimed to be: " (mm-handle-multipart-from ctl) "\n" + (if addresses + (concat "Addresses in certificate: " + (mapconcat 'identity addresses ", ")) + "No addresses found in certificate. (Requires OpenSSL 0.9.6 or later.)") + "\n" "\n" + "OpenSSL output:\n" + "---------------\n" openssl-output "\n" + "Certificate(s) inside S/MIME signature:\n" + "---------------------------------------\n" + (buffer-string) "\n"))))) + handle) + +(defun mml-smime-verify-test (handle ctl) + smime-openssl-program) + +(provide 'mml-smime) + +;;; mml-smime.el ends here diff --git a/lisp/mml1991.el b/lisp/mml1991.el new file mode 100644 index 0000000..741cfe2 --- /dev/null +++ b/lisp/mml1991.el @@ -0,0 +1,298 @@ +;;; mml1991.el --- Old PGP message format (RFC 1991) support for MML +;; Copyright (C) 1998, 1999, 2000, 2001, 2003 Free Software Foundation, Inc. + +;; Author: Sascha Ldecke , +;; Simon Josefsson (Mailcrypt interface, Gnus glue) +;; Keywords PGP + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;;; Code: + +(eval-when-compile + (require 'cl) + (require 'mm-util)) + +(autoload 'quoted-printable-decode-region "qp") +(autoload 'quoted-printable-encode-region "qp") + +(defvar mml1991-use mml2015-use + "The package used for PGP.") + +(defvar mml1991-function-alist + '((mailcrypt mml1991-mailcrypt-sign + mml1991-mailcrypt-encrypt) + (gpg mml1991-gpg-sign + mml1991-gpg-encrypt) + (pgg mml1991-pgg-sign + mml1991-pgg-encrypt)) + "Alist of PGP functions.") + +;;; mailcrypt wrapper + +(eval-and-compile + (autoload 'mc-sign-generic "mc-toplev")) + +(defvar mml1991-decrypt-function 'mailcrypt-decrypt) +(defvar mml1991-verify-function 'mailcrypt-verify) + +(defun mml1991-mailcrypt-sign (cont) + (let ((text (current-buffer)) + headers signature + (result-buffer (get-buffer-create "*GPG Result*"))) + ;; Save MIME Content[^ ]+: headers from signing + (goto-char (point-min)) + (while (looking-at "^Content[^ ]+:") (forward-line)) + (unless (bobp) + (setq headers (buffer-string)) + (delete-region (point-min) (point))) + (goto-char (point-max)) + (unless (bolp) + (insert "\n")) + (quoted-printable-decode-region (point-min) (point-max)) + (with-temp-buffer + (setq signature (current-buffer)) + (insert-buffer-substring text) + (unless (mc-sign-generic (message-options-get 'message-sender) + nil nil nil nil) + (unless (> (point-max) (point-min)) + (pop-to-buffer result-buffer) + (error "Sign error"))) + (goto-char (point-min)) + (while (re-search-forward "\r+$" nil t) + (replace-match "" t t)) + (quoted-printable-encode-region (point-min) (point-max)) + (set-buffer text) + (delete-region (point-min) (point-max)) + (if headers (insert headers)) + (insert "\n") + (insert-buffer-substring signature) + (goto-char (point-max))))) + +(defun mml1991-mailcrypt-encrypt (cont &optional sign) + (let ((text (current-buffer)) + (mc-pgp-always-sign + (or mc-pgp-always-sign + sign + (eq t (or (message-options-get 'message-sign-encrypt) + (message-options-set + 'message-sign-encrypt + (or (y-or-n-p "Sign the message? ") + 'not)))) + 'never)) + cipher + (result-buffer (get-buffer-create "*GPG Result*"))) + ;; Strip MIME Content[^ ]: headers since it will be ASCII ARMOURED + (goto-char (point-min)) + (while (looking-at "^Content[^ ]+:") (forward-line)) + (unless (bobp) + (delete-region (point-min) (point))) + (mm-with-unibyte-current-buffer-mule4 + (with-temp-buffer + (setq cipher (current-buffer)) + (insert-buffer-substring text) + (unless (mc-encrypt-generic + (or + (message-options-get 'message-recipients) + (message-options-set 'message-recipients + (read-string "Recipients: "))) + nil + (point-min) (point-max) + (message-options-get 'message-sender) + 'sign) + (unless (> (point-max) (point-min)) + (pop-to-buffer result-buffer) + (error "Encrypt error"))) + (goto-char (point-min)) + (while (re-search-forward "\r+$" nil t) + (replace-match "" t t)) + (set-buffer text) + (delete-region (point-min) (point-max)) + ;;(insert "Content-Type: application/pgp-encrypted\n\n") + ;;(insert "Version: 1\n\n") + (insert "\n") + (insert-buffer-substring cipher) + (goto-char (point-max)))))) + +;;; gpg wrapper + +(eval-and-compile + (autoload 'gpg-sign-cleartext "gpg")) + +(defun mml1991-gpg-sign (cont) + (let ((text (current-buffer)) + headers signature + (result-buffer (get-buffer-create "*GPG Result*"))) + ;; Save MIME Content[^ ]+: headers from signing + (goto-char (point-min)) + (while (looking-at "^Content[^ ]+:") (forward-line)) + (unless (bobp) + (setq headers (buffer-string)) + (delete-region (point-min) (point))) + (goto-char (point-max)) + (unless (bolp) + (insert "\n")) + (quoted-printable-decode-region (point-min) (point-max)) + (with-temp-buffer + (unless (gpg-sign-cleartext text (setq signature (current-buffer)) + result-buffer + nil + (message-options-get 'message-sender)) + (unless (> (point-max) (point-min)) + (pop-to-buffer result-buffer) + (error "Sign error"))) + (goto-char (point-min)) + (while (re-search-forward "\r+$" nil t) + (replace-match "" t t)) + (quoted-printable-encode-region (point-min) (point-max)) + (set-buffer text) + (delete-region (point-min) (point-max)) + (if headers (insert headers)) + (insert "\n") + (insert-buffer-substring signature) + (goto-char (point-max))))) + +(defun mml1991-gpg-encrypt (cont &optional sign) + (let ((text (current-buffer)) + cipher + (result-buffer (get-buffer-create "*GPG Result*"))) + ;; Strip MIME Content[^ ]: headers since it will be ASCII ARMOURED + (goto-char (point-min)) + (while (looking-at "^Content[^ ]+:") (forward-line)) + (unless (bobp) + (delete-region (point-min) (point))) + (mm-with-unibyte-current-buffer-mule4 + (with-temp-buffer + (flet ((gpg-encrypt-func + (sign plaintext ciphertext result recipients &optional + passphrase sign-with-key armor textmode) + (if sign + (gpg-sign-encrypt + plaintext ciphertext result recipients passphrase + sign-with-key armor textmode) + (gpg-encrypt + plaintext ciphertext result recipients passphrase + armor textmode)))) + (unless (gpg-encrypt-func + sign + text (setq cipher (current-buffer)) + result-buffer + (split-string + (or + (message-options-get 'message-recipients) + (message-options-set 'message-recipients + (read-string "Recipients: "))) + "[ \f\t\n\r\v,]+") + nil + (message-options-get 'message-sender) + t t) ; armor & textmode + (unless (> (point-max) (point-min)) + (pop-to-buffer result-buffer) + (error "Encrypt error")))) + (goto-char (point-min)) + (while (re-search-forward "\r+$" nil t) + (replace-match "" t t)) + (set-buffer text) + (delete-region (point-min) (point-max)) + ;;(insert "Content-Type: application/pgp-encrypted\n\n") + ;;(insert "Version: 1\n\n") + (insert "\n") + (insert-buffer-substring cipher) + (goto-char (point-max)))))) + +;; pgg wrapper + +(defvar pgg-output-buffer) +(defvar pgg-errors-buffer) + +(defun mml1991-pgg-sign (cont) + (let (headers) + ;; Don't sign headers. + (goto-char (point-min)) + (while (not (looking-at "^$")) + (forward-line)) + (unless (eobp) ;; no headers? + (setq headers (buffer-substring (point-min) (point))) + (forward-line) ;; skip header/body separator + (delete-region (point-min) (point))) + (quoted-printable-decode-region (point-min) (point-max)) + (unless (let ((pgg-default-user-id + (or (message-options-get 'message-sender) + pgg-default-user-id))) + (pgg-sign-region (point-min) (point-max) t)) + (pop-to-buffer pgg-errors-buffer) + (error "Encrypt error")) + (delete-region (point-min) (point-max)) + (insert-buffer-substring pgg-output-buffer) + (goto-char (point-min)) + (while (re-search-forward "\r+$" nil t) + (replace-match "" t t)) + (quoted-printable-encode-region (point-min) (point-max)) + (goto-char (point-min)) + (if headers (insert headers)) + (insert "\n") + t)) + +(defun mml1991-pgg-encrypt (cont &optional sign) + (let (headers) + ;; Strip MIME Content[^ ]: headers since it will be ASCII ARMOURED + (goto-char (point-min)) + (while (looking-at "^Content[^ ]+:") (forward-line)) + (unless (bobp) + (delete-region (point-min) (point))) + (unless (pgg-encrypt-region + (point-min) (point-max) + (split-string + (or + (message-options-get 'message-recipients) + (message-options-set 'message-recipients + (read-string "Recipients: "))) + "[ \f\t\n\r\v,]+") + sign) + (pop-to-buffer pgg-errors-buffer) + (error "Encrypt error")) + (delete-region (point-min) (point-max)) + ;;(insert "Content-Type: application/pgp-encrypted\n\n") + ;;(insert "Version: 1\n\n") + (insert "\n") + (insert-buffer-substring pgg-output-buffer) + t)) + +;;;###autoload +(defun mml1991-encrypt (cont &optional sign) + (let ((func (nth 2 (assq mml1991-use mml1991-function-alist)))) + (if func + (funcall func cont sign) + (error "Cannot find encrypt function")))) + +;;;###autoload +(defun mml1991-sign (cont) + (let ((func (nth 1 (assq mml1991-use mml1991-function-alist)))) + (if func + (funcall func cont) + (error "Cannot find sign function")))) + +(provide 'mml1991) + +;; Local Variables: +;; coding: iso-8859-1 +;; End: + +;;; mml1991.el ends here diff --git a/lisp/mml2015.el b/lisp/mml2015.el new file mode 100644 index 0000000..ef2fe9f --- /dev/null +++ b/lisp/mml2015.el @@ -0,0 +1,921 @@ +;;; mml2015.el --- MIME Security with Pretty Good Privacy (PGP) +;; Copyright (C) 2000, 2001, 2002, 2003 Free Software Foundation, Inc. + +;; Author: Shenghuo Zhu +;; Keywords: PGP MIME MML + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your +;; option) any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; RFC 2015 is updated by RFC 3156, this file should be compatible +;; with both. + +;;; Code: + +(eval-when-compile (require 'cl)) +(eval-when-compile (require 'gnus-clfns)) +(require 'mm-decode) +(require 'mm-util) + +(defvar mml2015-use (or + (progn + (ignore-errors + (require 'pgg)) + (and (fboundp 'pgg-sign-region) + 'pgg)) + (progn + (ignore-errors + (require 'gpg)) + (and (fboundp 'gpg-sign-detached) + 'gpg)) + (progn (ignore-errors + (load "mc-toplev")) + (and (fboundp 'mc-encrypt-generic) + (fboundp 'mc-sign-generic) + (fboundp 'mc-cleanup-recipient-headers) + 'mailcrypt))) + "The package used for PGP/MIME.") + +;; Something is not RFC2015. +(defvar mml2015-function-alist + '((mailcrypt mml2015-mailcrypt-sign + mml2015-mailcrypt-encrypt + mml2015-mailcrypt-verify + mml2015-mailcrypt-decrypt + mml2015-mailcrypt-clear-verify + mml2015-mailcrypt-clear-decrypt) + (gpg mml2015-gpg-sign + mml2015-gpg-encrypt + mml2015-gpg-verify + mml2015-gpg-decrypt + mml2015-gpg-clear-verify + mml2015-gpg-clear-decrypt) + (pgg mml2015-pgg-sign + mml2015-pgg-encrypt + mml2015-pgg-verify + mml2015-pgg-decrypt + mml2015-pgg-clear-verify + mml2015-pgg-clear-decrypt)) + "Alist of PGP/MIME functions.") + +(defvar mml2015-result-buffer nil) + +(defcustom mml2015-unabbrev-trust-alist + '(("TRUST_UNDEFINED" . nil) + ("TRUST_NEVER" . nil) + ("TRUST_MARGINAL" . t) + ("TRUST_FULLY" . t) + ("TRUST_ULTIMATE" . t)) + "Map GnuPG trust output values to a boolean saying if you trust the key." + :type '(repeat (cons (regexp :tag "GnuPG output regexp") + (boolean :tag "Trust key")))) + +;;; mailcrypt wrapper + +(eval-and-compile + (autoload 'mailcrypt-decrypt "mailcrypt") + (autoload 'mailcrypt-verify "mailcrypt") + (autoload 'mc-pgp-always-sign "mailcrypt") + (autoload 'mc-encrypt-generic "mc-toplev") + (autoload 'mc-cleanup-recipient-headers "mc-toplev") + (autoload 'mc-sign-generic "mc-toplev")) + +(eval-when-compile + (defvar mc-default-scheme) + (defvar mc-schemes)) + +(defvar mml2015-decrypt-function 'mailcrypt-decrypt) +(defvar mml2015-verify-function 'mailcrypt-verify) + +(defun mml2015-format-error (err) + (if (stringp (cadr err)) + (cadr err) + (format "%S" (cdr err)))) + +(defun mml2015-mailcrypt-decrypt (handle ctl) + (catch 'error + (let (child handles result) + (unless (setq child (mm-find-part-by-type + (cdr handle) + "application/octet-stream" nil t)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Corrupted") + (throw 'error handle)) + (with-temp-buffer + (mm-insert-part child) + (setq result + (condition-case err + (funcall mml2015-decrypt-function) + (error + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details (mml2015-format-error err)) + nil) + (quit + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details "Quit.") + nil))) + (unless (car result) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Failed") + (throw 'error handle)) + (setq handles (mm-dissect-buffer t))) + (mm-destroy-parts handle) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info + (concat "OK" + (let ((sig (with-current-buffer mml2015-result-buffer + (mml2015-gpg-extract-signature-details)))) + (concat ", Signer: " sig)))) + (if (listp (car handles)) + handles + (list handles))))) + +(defun mml2015-mailcrypt-clear-decrypt () + (let (result) + (setq result + (condition-case err + (funcall mml2015-decrypt-function) + (error + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details (mml2015-format-error err)) + nil) + (quit + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details "Quit.") + nil))) + (if (car result) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "OK") + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Failed")))) + +(defun mml2015-fix-micalg (alg) + (and alg + ;; Mutt/1.2.5i has seen sending micalg=php-sha1 + (upcase (if (string-match "^p[gh]p-" alg) + (substring alg (match-end 0)) + alg)))) + +(defun mml2015-mailcrypt-verify (handle ctl) + (catch 'error + (let (part) + (unless (setq part (mm-find-raw-part-by-type + ctl (or (mm-handle-multipart-ctl-parameter + ctl 'protocol) + "application/pgp-signature") + t)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Corrupted") + (throw 'error handle)) + (with-temp-buffer + (insert "-----BEGIN PGP SIGNED MESSAGE-----\n") + (insert (format "Hash: %s\n\n" + (or (mml2015-fix-micalg + (mm-handle-multipart-ctl-parameter + ctl 'micalg)) + "SHA1"))) + (save-restriction + (narrow-to-region (point) (point)) + (insert part "\n") + (goto-char (point-min)) + (while (not (eobp)) + (if (looking-at "^-") + (insert "- ")) + (forward-line))) + (unless (setq part (mm-find-part-by-type + (cdr handle) "application/pgp-signature" nil t)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Corrupted") + (throw 'error handle)) + (save-restriction + (narrow-to-region (point) (point)) + (mm-insert-part part) + (goto-char (point-min)) + (if (re-search-forward "^-----BEGIN PGP [^-]+-----\r?$" nil t) + (replace-match "-----BEGIN PGP SIGNATURE-----" t t)) + (if (re-search-forward "^-----END PGP [^-]+-----\r?$" nil t) + (replace-match "-----END PGP SIGNATURE-----" t t))) + (let ((mc-gpg-debug-buffer (get-buffer-create " *gnus gpg debug*"))) + (unless (condition-case err + (prog1 + (funcall mml2015-verify-function) + (if (get-buffer " *mailcrypt stderr temp") + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details + (with-current-buffer " *mailcrypt stderr temp" + (buffer-string)))) + (if (get-buffer " *mailcrypt stdout temp") + (kill-buffer " *mailcrypt stdout temp")) + (if (get-buffer " *mailcrypt stderr temp") + (kill-buffer " *mailcrypt stderr temp")) + (if (get-buffer " *mailcrypt status temp") + (kill-buffer " *mailcrypt status temp")) + (if (get-buffer mc-gpg-debug-buffer) + (kill-buffer mc-gpg-debug-buffer))) + (error + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details (mml2015-format-error err)) + nil) + (quit + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details "Quit.") + nil)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Failed") + (throw 'error handle)))) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "OK") + handle))) + +(defun mml2015-mailcrypt-clear-verify () + (let ((mc-gpg-debug-buffer (get-buffer-create " *gnus gpg debug*"))) + (if (condition-case err + (prog1 + (funcall mml2015-verify-function) + (if (get-buffer " *mailcrypt stderr temp") + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details + (with-current-buffer " *mailcrypt stderr temp" + (buffer-string)))) + (if (get-buffer " *mailcrypt stdout temp") + (kill-buffer " *mailcrypt stdout temp")) + (if (get-buffer " *mailcrypt stderr temp") + (kill-buffer " *mailcrypt stderr temp")) + (if (get-buffer " *mailcrypt status temp") + (kill-buffer " *mailcrypt status temp")) + (if (get-buffer mc-gpg-debug-buffer) + (kill-buffer mc-gpg-debug-buffer))) + (error + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details (mml2015-format-error err)) + nil) + (quit + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details "Quit.") + nil)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "OK") + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Failed")))) + +(defun mml2015-mailcrypt-sign (cont) + (mc-sign-generic (message-options-get 'message-sender) + nil nil nil nil) + (let ((boundary + (funcall mml-boundary-function (incf mml-multipart-number))) + hash point) + (goto-char (point-min)) + (unless (re-search-forward "^-----BEGIN PGP SIGNED MESSAGE-----\r?$" nil t) + (error "Cannot find signed begin line")) + (goto-char (match-beginning 0)) + (forward-line 1) + (unless (looking-at "Hash:[ \t]*\\([a-zA-Z0-9]+\\)") + (error "Cannot not find PGP hash")) + (setq hash (match-string 1)) + (unless (re-search-forward "^$" nil t) + (error "Cannot not find PGP message")) + (forward-line 1) + (delete-region (point-min) (point)) + (insert (format "Content-Type: multipart/signed; boundary=\"%s\";\n" + boundary)) + (insert (format "\tmicalg=pgp-%s; protocol=\"application/pgp-signature\"\n" + (downcase hash))) + (insert (format "\n--%s\n" boundary)) + (setq point (point)) + (goto-char (point-max)) + (unless (re-search-backward "^-----END PGP SIGNATURE-----\r?$" nil t) + (error "Cannot find signature part")) + (replace-match "-----END PGP MESSAGE-----" t t) + (goto-char (match-beginning 0)) + (unless (re-search-backward "^-----BEGIN PGP SIGNATURE-----\r?$" + nil t) + (error "Cannot find signature part")) + (replace-match "-----BEGIN PGP MESSAGE-----" t t) + (goto-char (match-beginning 0)) + (save-restriction + (narrow-to-region point (point)) + (goto-char point) + (while (re-search-forward "^- -" nil t) + (replace-match "-" t t)) + (goto-char (point-max))) + (insert (format "--%s\n" boundary)) + (insert "Content-Type: application/pgp-signature\n\n") + (goto-char (point-max)) + (insert (format "--%s--\n" boundary)) + (goto-char (point-max)))) + +(defun mml2015-mailcrypt-encrypt (cont &optional sign) + (let ((mc-pgp-always-sign + (or mc-pgp-always-sign + sign + (eq t (or (message-options-get 'message-sign-encrypt) + (message-options-set + 'message-sign-encrypt + (or (y-or-n-p "Sign the message? ") + 'not)))) + 'never))) + (mm-with-unibyte-current-buffer-mule4 + (mc-encrypt-generic + (or (message-options-get 'message-recipients) + (message-options-set 'message-recipients + (mc-cleanup-recipient-headers + (read-string "Recipients: ")))) + nil nil nil + (message-options-get 'message-sender)))) + (goto-char (point-min)) + (unless (looking-at "-----BEGIN PGP MESSAGE-----") + (error "Fail to encrypt the message")) + (let ((boundary + (funcall mml-boundary-function (incf mml-multipart-number)))) + (insert (format "Content-Type: multipart/encrypted; boundary=\"%s\";\n" + boundary)) + (insert "\tprotocol=\"application/pgp-encrypted\"\n\n") + (insert (format "--%s\n" boundary)) + (insert "Content-Type: application/pgp-encrypted\n\n") + (insert "Version: 1\n\n") + (insert (format "--%s\n" boundary)) + (insert "Content-Type: application/octet-stream\n\n") + (goto-char (point-max)) + (insert (format "--%s--\n" boundary)) + (goto-char (point-max)))) + +;;; gpg wrapper + +(eval-and-compile + (autoload 'gpg-decrypt "gpg") + (autoload 'gpg-verify "gpg") + (autoload 'gpg-verify-cleartext "gpg") + (autoload 'gpg-sign-detached "gpg") + (autoload 'gpg-sign-encrypt "gpg") + (autoload 'gpg-encrypt "gpg") + (autoload 'gpg-passphrase-read "gpg")) + +(defun mml2015-gpg-passphrase () + (or (message-options-get 'gpg-passphrase) + (message-options-set 'gpg-passphrase (gpg-passphrase-read)))) + +(defun mml2015-gpg-decrypt-1 () + (let ((cipher (current-buffer)) plain result) + (if (with-temp-buffer + (prog1 + (gpg-decrypt cipher (setq plain (current-buffer)) + mml2015-result-buffer nil) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details + (with-current-buffer mml2015-result-buffer + (buffer-string))) + (set-buffer cipher) + (erase-buffer) + (insert-buffer-substring plain) + (goto-char (point-min)) + (while (search-forward "\r\n" nil t) + (replace-match "\n" t t)))) + '(t) + ;; Some wrong with the return value, check plain text buffer. + (if (> (point-max) (point-min)) + '(t) + nil)))) + +(defun mml2015-gpg-decrypt (handle ctl) + (let ((mml2015-decrypt-function 'mml2015-gpg-decrypt-1)) + (mml2015-mailcrypt-decrypt handle ctl))) + +(defun mml2015-gpg-clear-decrypt () + (let (result) + (setq result (mml2015-gpg-decrypt-1)) + (if (car result) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "OK") + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Failed")))) + +(defun mml2015-gpg-pretty-print-fpr (fingerprint) + (let* ((result "") + (fpr-length (string-width fingerprint)) + (n-slice 0) + slice) + (setq fingerprint (string-to-list fingerprint)) + (while fingerprint + (setq fpr-length (- fpr-length 4)) + (setq slice (butlast fingerprint fpr-length)) + (setq fingerprint (nthcdr 4 fingerprint)) + (setq n-slice (1+ n-slice)) + (setq result + (concat + result + (case n-slice + (1 slice) + (otherwise (concat " " slice)))))) + result)) + +(defun mml2015-gpg-extract-signature-details () + (goto-char (point-min)) + (let* ((expired (re-search-forward + "^\\[GNUPG:\\] SIGEXPIRED$" + nil t)) + (signer (and (re-search-forward + "^\\[GNUPG:\\] GOODSIG \\([0-9A-Za-z]*\\) \\(.*\\)$" + nil t) + (cons (match-string 1) (match-string 2)))) + (fprint (and (re-search-forward + "^\\[GNUPG:\\] VALIDSIG \\([0-9a-zA-Z]*\\) " + nil t) + (match-string 1))) + (trust (and (re-search-forward + "^\\[GNUPG:\\] \\(TRUST_.*\\)$" + nil t) + (match-string 1))) + (trust-good-enough-p + (cdr (assoc trust mml2015-unabbrev-trust-alist)))) + (cond ((and signer fprint) + (concat (cdr signer) + (unless trust-good-enough-p + (concat "\nUntrusted, Fingerprint: " + (mml2015-gpg-pretty-print-fpr fprint))) + (when expired + (format "\nWARNING: Signature from expired key (%s)" + (car signer))))) + ((re-search-forward + "^\\(gpg: \\)?Good signature from \"\\(.*\\)\"$" nil t) + (match-string 2)) + (t + "From unknown user")))) + +(defun mml2015-gpg-verify (handle ctl) + (catch 'error + (let (part message signature info-is-set-p) + (unless (setq part (mm-find-raw-part-by-type + ctl (or (mm-handle-multipart-ctl-parameter + ctl 'protocol) + "application/pgp-signature") + t)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Corrupted") + (throw 'error handle)) + (with-temp-buffer + (setq message (current-buffer)) + (insert part) + ;; Convert to in verify mode. Sign and + ;; clearsign use --textmode. The conversion is not necessary. + ;; In clearverify, the conversion is not necessary either. + (goto-char (point-min)) + (end-of-line) + (while (not (eobp)) + (unless (eq (char-before) ?\r) + (insert "\r")) + (forward-line) + (end-of-line)) + (with-temp-buffer + (setq signature (current-buffer)) + (unless (setq part (mm-find-part-by-type + (cdr handle) "application/pgp-signature" nil t)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Corrupted") + (throw 'error handle)) + (mm-insert-part part) + (unless (condition-case err + (prog1 + (gpg-verify message signature mml2015-result-buffer) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details + (with-current-buffer mml2015-result-buffer + (buffer-string)))) + (error + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details (mml2015-format-error err)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Error.") + (setq info-is-set-p t) + nil) + (quit + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details "Quit.") + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Quit.") + (setq info-is-set-p t) + nil)) + (unless info-is-set-p + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Failed")) + (throw 'error handle))) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info + (with-current-buffer mml2015-result-buffer + (mml2015-gpg-extract-signature-details)))) + handle))) + +(defun mml2015-gpg-clear-verify () + (if (condition-case err + (prog1 + (gpg-verify-cleartext (current-buffer) mml2015-result-buffer) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details + (with-current-buffer mml2015-result-buffer + (buffer-string)))) + (error + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details (mml2015-format-error err)) + nil) + (quit + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details "Quit.") + nil)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info + (with-current-buffer mml2015-result-buffer + (mml2015-gpg-extract-signature-details))) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Failed"))) + +(defun mml2015-gpg-sign (cont) + (let ((boundary + (funcall mml-boundary-function (incf mml-multipart-number))) + (text (current-buffer)) signature) + (goto-char (point-max)) + (unless (bolp) + (insert "\n")) + (with-temp-buffer + (unless (gpg-sign-detached text (setq signature (current-buffer)) + mml2015-result-buffer + nil + (message-options-get 'message-sender) + t t) ; armor & textmode + (unless (> (point-max) (point-min)) + (pop-to-buffer mml2015-result-buffer) + (error "Sign error"))) + (goto-char (point-min)) + (while (re-search-forward "\r+$" nil t) + (replace-match "" t t)) + (set-buffer text) + (goto-char (point-min)) + (insert (format "Content-Type: multipart/signed; boundary=\"%s\";\n" + boundary)) + ;;; FIXME: what is the micalg? + (insert "\tmicalg=pgp-sha1; protocol=\"application/pgp-signature\"\n") + (insert (format "\n--%s\n" boundary)) + (goto-char (point-max)) + (insert (format "\n--%s\n" boundary)) + (insert "Content-Type: application/pgp-signature\n\n") + (insert-buffer-substring signature) + (goto-char (point-max)) + (insert (format "--%s--\n" boundary)) + (goto-char (point-max))))) + +(defun mml2015-gpg-encrypt (cont &optional sign) + (let ((boundary + (funcall mml-boundary-function (incf mml-multipart-number))) + (text (current-buffer)) + cipher) + (mm-with-unibyte-current-buffer-mule4 + (with-temp-buffer + ;; set up a function to call the correct gpg encrypt routine + ;; with the right arguments. (FIXME: this should be done + ;; differently.) + (flet ((gpg-encrypt-func + (sign plaintext ciphertext result recipients &optional + passphrase sign-with-key armor textmode) + (if sign + (gpg-sign-encrypt + plaintext ciphertext result recipients passphrase + sign-with-key armor textmode) + (gpg-encrypt + plaintext ciphertext result recipients passphrase + armor textmode)))) + (unless (gpg-encrypt-func + sign ; passed in when using signencrypt + text (setq cipher (current-buffer)) + mml2015-result-buffer + (split-string + (or + (message-options-get 'message-recipients) + (message-options-set 'message-recipients + (read-string "Recipients: "))) + "[ \f\t\n\r\v,]+") + nil + (message-options-get 'message-sender) + t t) ; armor & textmode + (unless (> (point-max) (point-min)) + (pop-to-buffer mml2015-result-buffer) + (error "Encrypt error")))) + (goto-char (point-min)) + (while (re-search-forward "\r+$" nil t) + (replace-match "" t t)) + (set-buffer text) + (delete-region (point-min) (point-max)) + (insert (format "Content-Type: multipart/encrypted; boundary=\"%s\";\n" + boundary)) + (insert "\tprotocol=\"application/pgp-encrypted\"\n\n") + (insert (format "--%s\n" boundary)) + (insert "Content-Type: application/pgp-encrypted\n\n") + (insert "Version: 1\n\n") + (insert (format "--%s\n" boundary)) + (insert "Content-Type: application/octet-stream\n\n") + (insert-buffer-substring cipher) + (goto-char (point-max)) + (insert (format "--%s--\n" boundary)) + (goto-char (point-max)))))) + +;;; pgg wrapper + +(eval-when-compile + (defvar pgg-errors-buffer) + (defvar pgg-output-buffer)) + +(eval-and-compile + (autoload 'pgg-decrypt-region "pgg") + (autoload 'pgg-verify-region "pgg") + (autoload 'pgg-sign-region "pgg") + (autoload 'pgg-encrypt-region "pgg")) + +(defun mml2015-pgg-decrypt (handle ctl) + (catch 'error + (let ((pgg-errors-buffer mml2015-result-buffer) + child handles result decrypt-status) + (unless (setq child (mm-find-part-by-type + (cdr handle) + "application/octet-stream" nil t)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Corrupted") + (throw 'error handle)) + (with-temp-buffer + (mm-insert-part child) + (if (condition-case err + (prog1 + (pgg-decrypt-region (point-min) (point-max)) + (setq decrypt-status + (with-current-buffer mml2015-result-buffer + (buffer-string))) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details + decrypt-status)) + (error + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details (mml2015-format-error err)) + nil) + (quit + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details "Quit.") + nil)) + (with-current-buffer pgg-output-buffer + (goto-char (point-min)) + (while (search-forward "\r\n" nil t) + (replace-match "\n" t t)) + (setq handles (mm-dissect-buffer t)) + (mm-destroy-parts handle) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "OK") + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details + (concat decrypt-status + (when (stringp (car handles)) + "\n" (mm-handle-multipart-ctl-parameter + handles 'gnus-details)))) + (if (listp (car handles)) + handles + (list handles))) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Failed") + (throw 'error handle)))))) + +(defun mml2015-pgg-clear-decrypt () + (let ((pgg-errors-buffer mml2015-result-buffer)) + (if (prog1 + (pgg-decrypt-region (point-min) (point-max)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details + (with-current-buffer mml2015-result-buffer + (buffer-string)))) + (progn + (erase-buffer) + (insert-buffer-substring pgg-output-buffer) + (goto-char (point-min)) + (while (search-forward "\r\n" nil t) + (replace-match "\n" t t)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "OK")) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Failed")))) + +(defun mml2015-pgg-verify (handle ctl) + (let ((pgg-errors-buffer mml2015-result-buffer) + signature-file part signature) + (if (or (null (setq part (mm-find-raw-part-by-type + ctl (or (mm-handle-multipart-ctl-parameter + ctl 'protocol) + "application/pgp-signature") + t))) + (null (setq signature (mm-find-part-by-type + (cdr handle) "application/pgp-signature" nil t)))) + (progn + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Corrupted") + handle) + (with-temp-buffer + (insert part) + ;; Convert to in verify mode. Sign and + ;; clearsign use --textmode. The conversion is not necessary. + ;; In clearverify, the conversion is not necessary either. + (goto-char (point-min)) + (end-of-line) + (while (not (eobp)) + (unless (eq (char-before) ?\r) + (insert "\r")) + (forward-line) + (end-of-line)) + (with-temp-file (setq signature-file (mm-make-temp-file "pgg")) + (mm-insert-part signature)) + (if (condition-case err + (prog1 + (pgg-verify-region (point-min) (point-max) + signature-file t) + (goto-char (point-min)) + (while (search-forward "\r\n" nil t) + (replace-match "\n" t t)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details + (concat (with-current-buffer pgg-output-buffer + (buffer-string)) + (with-current-buffer pgg-errors-buffer + (buffer-string))))) + (error + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details (mml2015-format-error err)) + nil) + (quit + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details "Quit.") + nil)) + (progn + (delete-file signature-file) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info + (with-current-buffer pgg-errors-buffer + (mml2015-gpg-extract-signature-details)))) + (delete-file signature-file) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Failed"))))) + handle) + +(defun mml2015-pgg-clear-verify () + (let ((pgg-errors-buffer mml2015-result-buffer) + (text (buffer-string)) + (coding-system buffer-file-coding-system)) + (if (condition-case err + (prog1 + (mm-with-unibyte-buffer + (insert (encode-coding-string text coding-system)) + (pgg-verify-region (point-min) (point-max) nil t)) + (goto-char (point-min)) + (while (search-forward "\r\n" nil t) + (replace-match "\n" t t)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details + (concat (with-current-buffer pgg-output-buffer + (buffer-string)) + (with-current-buffer pgg-errors-buffer + (buffer-string))))) + (error + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details (mml2015-format-error err)) + nil) + (quit + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-details "Quit.") + nil)) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info + (with-current-buffer pgg-errors-buffer + (mml2015-gpg-extract-signature-details))) + (mm-set-handle-multipart-parameter + mm-security-handle 'gnus-info "Failed")))) + +(defun mml2015-pgg-sign (cont) + (let ((pgg-errors-buffer mml2015-result-buffer) + (boundary (funcall mml-boundary-function (incf mml-multipart-number))) + (pgg-default-user-id (or (message-options-get 'mml-sender) + pgg-default-user-id))) + (unless (pgg-sign-region (point-min) (point-max)) + (pop-to-buffer mml2015-result-buffer) + (error "Sign error")) + (goto-char (point-min)) + (insert (format "Content-Type: multipart/signed; boundary=\"%s\";\n" + boundary)) + ;;; FIXME: what is the micalg? + (insert "\tmicalg=pgp-sha1; protocol=\"application/pgp-signature\"\n") + (insert (format "\n--%s\n" boundary)) + (goto-char (point-max)) + (insert (format "\n--%s\n" boundary)) + (insert "Content-Type: application/pgp-signature\n\n") + (insert-buffer-substring pgg-output-buffer) + (goto-char (point-max)) + (insert (format "--%s--\n" boundary)) + (goto-char (point-max)))) + +(defun mml2015-pgg-encrypt (cont &optional sign) + (let ((pgg-errors-buffer mml2015-result-buffer) + (boundary (funcall mml-boundary-function (incf mml-multipart-number)))) + (unless (pgg-encrypt-region (point-min) (point-max) + (split-string + (or + (message-options-get 'message-recipients) + (message-options-set 'message-recipients + (read-string "Recipients: "))) + "[ \f\t\n\r\v,]+") + sign) + (pop-to-buffer mml2015-result-buffer) + (error "Encrypt error")) + (delete-region (point-min) (point-max)) + (goto-char (point-min)) + (insert (format "Content-Type: multipart/encrypted; boundary=\"%s\";\n" + boundary)) + (insert "\tprotocol=\"application/pgp-encrypted\"\n\n") + (insert (format "--%s\n" boundary)) + (insert "Content-Type: application/pgp-encrypted\n\n") + (insert "Version: 1\n\n") + (insert (format "--%s\n" boundary)) + (insert "Content-Type: application/octet-stream\n\n") + (insert-buffer-substring pgg-output-buffer) + (goto-char (point-max)) + (insert (format "--%s--\n" boundary)) + (goto-char (point-max)))) + +;;; General wrapper + +(defun mml2015-clean-buffer () + (if (gnus-buffer-live-p mml2015-result-buffer) + (with-current-buffer mml2015-result-buffer + (erase-buffer) + t) + (setq mml2015-result-buffer + (gnus-get-buffer-create "*MML2015 Result*")) + nil)) + +(defsubst mml2015-clear-decrypt-function () + (nth 6 (assq mml2015-use mml2015-function-alist))) + +(defsubst mml2015-clear-verify-function () + (nth 5 (assq mml2015-use mml2015-function-alist))) + +;;;###autoload +(defun mml2015-decrypt (handle ctl) + (mml2015-clean-buffer) + (let ((func (nth 4 (assq mml2015-use mml2015-function-alist)))) + (if func + (funcall func handle ctl) + handle))) + +;;;###autoload +(defun mml2015-decrypt-test (handle ctl) + mml2015-use) + +;;;###autoload +(defun mml2015-verify (handle ctl) + (mml2015-clean-buffer) + (let ((func (nth 3 (assq mml2015-use mml2015-function-alist)))) + (if func + (funcall func handle ctl) + handle))) + +;;;###autoload +(defun mml2015-verify-test (handle ctl) + mml2015-use) + +;;;###autoload +(defun mml2015-encrypt (cont &optional sign) + (mml2015-clean-buffer) + (let ((func (nth 2 (assq mml2015-use mml2015-function-alist)))) + (if func + (funcall func cont sign) + (error "Cannot find encrypt function")))) + +;;;###autoload +(defun mml2015-sign (cont) + (mml2015-clean-buffer) + (let ((func (nth 1 (assq mml2015-use mml2015-function-alist)))) + (if func + (funcall func cont) + (error "Cannot find sign function")))) + +;;;###autoload +(defun mml2015-self-encrypt () + (mml2015-encrypt nil)) + +(provide 'mml2015) + +;;; mml2015.el ends here diff --git a/lisp/netrc.el b/lisp/netrc.el new file mode 100644 index 0000000..3bfc76d --- /dev/null +++ b/lisp/netrc.el @@ -0,0 +1,128 @@ +;;; netrc.el --- .netrc parsing functionality +;; Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2002 +;; Free Software Foundation, Inc. + +;; Author: Lars Magne Ingebrigtsen +;; Modularizer: Ted Zlatanov +;; Keywords: news + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; Just the .netrc parsing functionality, abstracted so other packages +;; besides Gnus can use it. + +;;; Code: + +;;; +;;; .netrc and .authinforc parsing +;;; + +(eval-and-compile + (defalias 'netrc-point-at-eol + (if (fboundp 'point-at-eol) + 'point-at-eol + 'line-end-position))) + +(defun netrc-parse (file) + "Parse FILE and return an list of all entries in the file." + (when (file-exists-p file) + (with-temp-buffer + (let ((tokens '("machine" "default" "login" + "password" "account" "macdef" "force" + "port")) + alist elem result pair) + (insert-file-contents file) + (goto-char (point-min)) + ;; Go through the file, line by line. + (while (not (eobp)) + (narrow-to-region (point) (netrc-point-at-eol)) + ;; For each line, get the tokens and values. + (while (not (eobp)) + (skip-chars-forward "\t ") + ;; Skip lines that begin with a "#". + (if (eq (char-after) ?#) + (goto-char (point-max)) + (unless (eobp) + (setq elem + (if (= (following-char) ?\") + (read (current-buffer)) + (buffer-substring + (point) (progn (skip-chars-forward "^\t ") + (point))))) + (cond + ((equal elem "macdef") + ;; We skip past the macro definition. + (widen) + (while (and (zerop (forward-line 1)) + (looking-at "$"))) + (narrow-to-region (point) (point))) + ((member elem tokens) + ;; Tokens that don't have a following value are ignored, + ;; except "default". + (when (and pair (or (cdr pair) + (equal (car pair) "default"))) + (push pair alist)) + (setq pair (list elem))) + (t + ;; Values that haven't got a preceding token are ignored. + (when pair + (setcdr pair elem) + (push pair alist) + (setq pair nil))))))) + (when alist + (push (nreverse alist) result)) + (setq alist nil + pair nil) + (widen) + (forward-line 1)) + (nreverse result))))) + +(defun netrc-machine (list machine &optional port defaultport) + "Return the netrc values from LIST for MACHINE or for the default entry. +If PORT specified, only return entries with matching port tokens. +Entries without port tokens default to DEFAULTPORT." + (let ((rest list) + result) + (while list + (when (equal (cdr (assoc "machine" (car list))) machine) + (push (car list) result)) + (pop list)) + (unless result + ;; No machine name matches, so we look for default entries. + (while rest + (when (assoc "default" (car rest)) + (push (car rest) result)) + (pop rest))) + (when result + (setq result (nreverse result)) + (while (and result + (not (equal (or port defaultport "nntp") + (or (netrc-get (car result) "port") + defaultport "nntp")))) + (pop result)) + (car result)))) + +(defun netrc-get (alist type) + "Return the value of token TYPE from ALIST." + (cdr (assoc type alist))) + +(provide 'netrc) + +;;; netrc.el ends here diff --git a/lisp/nndiary.el b/lisp/nndiary.el new file mode 100644 index 0000000..42cb838 --- /dev/null +++ b/lisp/nndiary.el @@ -0,0 +1,1709 @@ +;;; nndiary.el --- A diary backend for Gnus + +;; Copyright (C) 1999, 2000, 2001, 2003 +;; Free Software Foundation, Inc. + +;; Author: Didier Verna +;; Maintainer: Didier Verna +;; Created: Fri Jul 16 18:55:42 1999 +;; Keywords: calendar mail news + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software +;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + +;;; Commentary: + +;; Contents management by FCM version 0.1. + +;; Description: +;; =========== + +;; This package implements NNDiary, a diary backend for Gnus. NNDiary is a +;; mail backend, pretty similar to nnml in its functionnning (it has all the +;; features of nnml, actually), but in which messages are treated as event +;; reminders. + +;; Here is a typical scenario: +;; - You've got a date with Andy Mc Dowell or Bruce Willis (select according +;; to your sexual preference) in one month. You don't want to forget it. +;; - Send a (special) diary message to yourself (see below). +;; - Forget all about it and keep on getting and reading new mail, as usual. +;; - From time to time, as you type `g' in the group buffer and as the date +;; is getting closer, the message will pop up again, just like if it were +;; new and unread. +;; - Read your "new" messages, this one included, and start dreaming of the +;; night you're gonna have. +;; - Once the date is over (you actually fell asleep just after dinner), the +;; message will be automatically deleted if it is marked as expirable. + +;; Some more notes on the diary backend: +;; - NNDiary is a *real* mail backend. You *really* send real diary +;; messsages. This means for instance that you can give appointements to +;; anybody (provided they use Gnus and NNDiary) by sending the diary message +;; to them as well. +;; - However, since NNDiary also has a 'request-post method, you can also +;; `C-u a' instead of `C-u m' on a diary group and the message won't actually +;; be sent; just stored in the group. +;; - The events you want to remember need not be punctual. You can set up +;; reminders for regular dates (like once each week, each monday at 13:30 +;; and so on). Diary messages of this kind will never be deleted (unless +;; you do it explicitely). But that, you guessed. + + +;; Usage: +;; ===== + +;; 1/ NNDiary has two modes of operation: traditional (the default) and +;; autonomous. +;; a/ In traditional mode, NNDiary does not get new mail by itself. You +;; have to move mails from your primary mail backend to nndiary +;; groups. +;; b/ In autonomous mode, NNDiary retrieves its own mail and handles it +;; independantly of your primary mail backend. To use NNDiary in +;; autonomous mode, you have several things to do: +;; i/ Put (setq nndiary-get-new-mail t) in your gnusrc file. +;; ii/ Diary messages contain several `X-Diary-*' special headers. +;; You *must* arrange that these messages be split in a private +;; folder *before* Gnus treat them. You need this because Gnus +;; is not able yet to manage multiple backends for mail +;; retrieval. Getting them from a separate source will +;; compensate this misfeature to some extent, as we will see. +;; As an example, here's my procmailrc entry to store diary files +;; in ~/.nndiary (the default nndiary mail source file): +;; +;; :0 HD : +;; * ^X-Diary +;; .nndiary +;; iii/ Customize the variables `nndiary-mail-sources' and +;; `nndiary-split-methods'. These are replacements for the usual +;; mail sources and split methods which, and will be used in +;; autonomous mode. `nndiary-mail-sources' defaults to +;; '(file :path "~/.nndiary"). +;; 2/ Install nndiary somewhere Emacs / Gnus can find it. Normally, you +;; *don't* have to '(require 'nndiary) anywhere. Gnus will do so when +;; appropriate as long as nndiary is somewhere in the load path. +;; 3/ Now, customize the rest of nndiary. In particular, you should +;; customize `nndiary-reminders', the list of times when you want to be +;; reminded of your appointements (e.g. 3 weeks before, then 2 days +;; before, then 1 hour before and that's it). +;; 4/ You *must* use the group timestamp feature of Gnus. This adds a +;; timestamp to each groups' parameters (please refer to the Gnus +;; documentation ("Group Timestamp" info node) to see how it's done. +;; 5/ Once you have done this, you may add a permanent nndiary virtual server +;; (something like '(nndiary "")) to your `gnus-secondary-select-methods'. +;; Yes, this server will be able to retrieve mails and split them when you +;; type `g' in the group buffer, just as if it were your only mail backend. +;; This is the benefit of using a private folder. +;; 6/ Hopefully, almost everything (see the TODO section below) will work as +;; expected when you restart Gnus: in the group buffer, `g' and `M-g' will +;; also get your new diary mails, `F' will find your new diary groups etc. + + +;; How to send diary messages: +;; ========================== + +;; There are 7 special headers in diary messages. These headers are of the +;; form `X-Diary-', the being one of `Minute', `Hour', +;; `Dom', `Month', `Year', `Time-Zone' and `Dow'. `Dom' means "Day of Month", +;; and `dow' means "Day of Week". These headers actually behave like crontab +;; specifications and define the event date(s). + +;; For all headers but the `Time-Zone' one, a header value is either a +;; star (meaning all possible values), or a list of fields (separated by a +;; comma). A field is either an integer, or a range. A range is two integers +;; separated by a dash. Possible integer values are 0-59 for `Minute', 0-23 +;; for `Hour', 1-31 for `Dom', `1-12' for Month, above 1971 for `Year' and 0-6 +;; for `Dow' (0 = sunday). As a special case, a star in either `Dom' or `Dow' +;; doesn't mean "all possible values", but "use only the other field". Note +;; that if both are star'ed, the use of either one gives the same result :-), + +;; The `Time-Zone' header is special in that it can have only one value (you +;; bet ;-). +;; A star doesn't mean "all possible values" (because it has no sense), but +;; "the current local time zone". + +;; As an example, here's how you would say "Each Monday and each 1st of month, +;; at 12:00, 20:00, 21:00, 22:00, 23:00 and 24:00, from 1999 to 2010" (I let +;; you find what to do then): +;; +;; X-Diary-Minute: 0 +;; X-Diary-Hour: 12, 20-24 +;; X-Diary-Dom: 1 +;; X-Diary-Month: * +;; X-Diary-Year: 1999-2010 +;; X-Diary-Dow: 1 +;; X-Diary-Time-Zone: * +;; +;; +;; Sending a diary message is not different from sending any other kind of +;; mail, except that such messages are identified by the presence of these +;; special headers. + + + +;; Bugs / Todo: +;; =========== + +;; * Respooling doesn't work because contrary to the request-scan function, +;; Gnus won't allow me to override the split methods when calling the +;; respooling backend functions. +;; * There's a bug in the time zone mechanism with variable TZ locations. +;; * We could allow a keyword like `ask' in X-Diary-* headers, that would mean +;; "ask for value upon reception of the message". +;; * We could add an optional header X-Diary-Reminders to specify a special +;; reminders value for this message. Suggested by Jody Klymak. +;; * We should check messages validity in other circumstances than just +;; moving an article from sonwhere else (request-accept). For instance, when +;; editing / saving and so on. + + +;; Remarks: +;; ======= + +;; * nnoo. +;; NNDiary is very similar to nnml. This makes the idea of using nnoo (to +;; derive nndiary from nnml) natural. However, my experience with nnoo is +;; that for reasonably complex backends like this one, noo is a burden +;; rather than an help. It's tricky to use, not everything can be +;; inherited, what can be inherited and when is not very clear, and you've +;; got to be very careful because a little mistake can fuck up your your +;; other backends, especially because their variables will be use instead of +;; your real ones. Finally, I found it easier to just clone the needed +;; parts of nnml, and tracking nnml updates is not a big deal. + +;; IMHO, nnoo is actually badly designed. A much simpler, and yet more +;; powerful one would be to make *real* functions and variables for a new +;; backend based on another. Lisp is a reflexive language so that's a very +;; easy thing to do: inspect the function's form, replace occurences of +;; (even in strings) with , and you're done. + +;; * nndiary-get-new-mail, nndiary-mail-source and nndiary-split-methods: +;; NNDiary has some experimental parts, in the sense Gnus normally uses only +;; one mail backends for mail retreival and splitting. This backend is also +;; an attempt to make it behave differently. For Gnus developpers: as you +;; can see if you snarf into the code, that was not a very difficult thing +;; to do. Something should be done about the respooling breakage though. + + +;;; Code: + +(require 'nnoo) +(require 'nnheader) +(require 'nnmail) +(eval-when-compile (require 'cl)) + +(require 'gnus-start) +(require 'gnus-sum) + +;; Compatibility Functions ================================================= + +(eval-and-compile + (if (fboundp 'signal-error) + (defun nndiary-error (&rest args) + (apply #'signal-error 'nndiary args)) + (defun nndiary-error (&rest args) + (apply #'error args)))) + + +;; Backend behavior customization =========================================== + +(defgroup nndiary nil + "The Gnus Diary backend." + :group 'gnus-diary) + +(defcustom nndiary-mail-sources + `((file :path ,(expand-file-name "~/.nndiary"))) + "*NNDiary specific mail sources. +This variable is used by nndiary in place of the standard `mail-sources' +variable when `nndiary-get-new-mail' is set to non-nil. These sources +must contain diary messages ONLY." + :group 'nndiary + :group 'mail-source + :type 'sexp) + +(defcustom nndiary-split-methods '(("diary" "")) + "*NNDiary specific split methods. +This variable is used by nndiary in place of the standard +`nnmail-split-methods' variable when `nndiary-get-new-mail' is set to +non-nil." + :group 'nndiary + :group 'nnmail-split + :type '(choice (repeat :tag "Alist" (group (string :tag "Name") regexp)) + (function-item nnmail-split-fancy) + (function :tag "Other"))) + + +(defcustom nndiary-reminders '((0 . day)) + "*Different times when you want to be reminded of your appointements. +Diary articles will appear again, as if they'd been just received. + +Entries look like (3 . day) which means something like \"Please +Hortense, would you be so kind as to remind me of my appointments 3 days +before the date, thank you very much. Anda, hmmm... by the way, are you +doing anything special tonight ?\". + +The units of measure are 'minute 'hour 'day 'week 'month and 'year (no, +not 'century, sorry). + +NOTE: the units of measure actually express dates, not durations: if you +use 'week, messages will pop up on Sundays at 00:00 (or Mondays if +`nndiary-week-starts-on-monday' is non nil) and *not* 7 days before the +appointement, if you use 'month, messages will pop up on the first day of +each months, at 00:00 and so on. + +If you really want to specify a duration (like 24 hours exactly), you can +use the equivalent in minutes (the smallest unit). A fuzz of 60 seconds +maximum in the reminder is not that painful, I think. Although this +scheme might appear somewhat weird at a first glance, it is very powerful. +In order to make this clear, here are some examples: + +- '(0 . day): this is the default value of `nndiary-reminders'. It means + pop up the appointements of the day each morning at 00:00. + +- '(1 . day): this means pop up the appointements the day before, at 00:00. + +- '(6 . hour): for an appointement at 18:30, this would pop up the + appointement message at 12:00. + +- '(360 . minute): for an appointement at 18:30 and 15 seconds, this would + pop up the appointement message at 12:30." + :group 'nndiary + :type '(repeat (cons :format "%v\n" + (integer :format "%v") + (choice :format "%[%v(s)%] before...\n" + :value day + (const :format "%v" minute) + (const :format "%v" hour) + (const :format "%v" day) + (const :format "%v" week) + (const :format "%v" month) + (const :format "%v" year))))) + +(defcustom nndiary-week-starts-on-monday nil + "*Whether a week starts on monday (otherwise, sunday)." + :type 'boolean + :group 'nndiary) + + +(defcustom nndiary-request-create-group-hooks nil + "*Hooks to run after `nndiary-request-create-group' is executed. +The hooks will be called with the full group name as argument." + :group 'nndiary + :type 'hook) + +(defcustom nndiary-request-update-info-hooks nil + "*Hooks to run after `nndiary-request-update-info-group' is executed. +The hooks will be called with the full group name as argument." + :group 'nndiary + :type 'hook) + +(defcustom nndiary-request-accept-article-hooks nil + "*Hooks to run before accepting an article. +Executed near the beginning of `nndiary-request-accept-article'. +The hooks will be called with the article in the current buffer." + :group 'nndiary + :type 'hook) + +(defcustom nndiary-check-directory-twice t + "*If t, check directories twice to avoid NFS failures." + :group 'nndiary + :type 'boolean) + + +;; Backend declaration ====================================================== + +;; Well, most of this is nnml clonage. + +(nnoo-declare nndiary) + +(defvoo nndiary-directory (nnheader-concat gnus-directory "diary/") + "Spool directory for the nndiary backend.") + +(defvoo nndiary-active-file + (expand-file-name "active" nndiary-directory) + "Active file for the nndiary backend.") + +(defvoo nndiary-newsgroups-file + (expand-file-name "newsgroups" nndiary-directory) + "Newsgroups description file for the nndiary backend.") + +(defvoo nndiary-get-new-mail nil + "Whether nndiary gets new mail and split it. +Contrary to traditional mail backends, this variable can be set to t +even if your primary mail backend also retreives mail. In such a case, +NDiary uses its own mail-sources and split-methods.") + +(defvoo nndiary-nov-is-evil nil + "If non-nil, Gnus will never use nov databases for nndiary groups. +Using nov databases will speed up header fetching considerably. +This variable shouldn't be flipped much. If you have, for some reason, +set this to t, and want to set it to nil again, you should always run +the `nndiary-generate-nov-databases' command. The function will go +through all nnml directories and generate nov databases for them +all. This may very well take some time.") + +(defvoo nndiary-prepare-save-mail-hook nil + "*Hook run narrowed to an article before saving.") + +(defvoo nndiary-inhibit-expiry nil + "If non-nil, inhibit expiry.") + + + +(defconst nndiary-version "0.2-b14" + "Current Diary backend version.") + +(defun nndiary-version () + "Current Diary backend version." + (interactive) + (message "NNDiary version %s" nndiary-version)) + +(defvoo nndiary-nov-file-name ".overview") + +(defvoo nndiary-current-directory nil) +(defvoo nndiary-current-group nil) +(defvoo nndiary-status-string "" ) +(defvoo nndiary-nov-buffer-alist nil) +(defvoo nndiary-group-alist nil) +(defvoo nndiary-active-timestamp nil) +(defvoo nndiary-article-file-alist nil) + +(defvoo nndiary-generate-active-function 'nndiary-generate-active-info) +(defvoo nndiary-nov-buffer-file-name nil) +(defvoo nndiary-file-coding-system nnmail-file-coding-system) + +(defconst nndiary-headers + '(("Minute" 0 59) + ("Hour" 0 23) + ("Dom" 1 31) + ("Month" 1 12) + ("Year" 1971) + ("Dow" 0 6) + ("Time-Zone" (("Y" -43200) + + ("X" -39600) + + ("W" -36000) + + ("V" -32400) + + ("U" -28800) + ("PST" -28800) + + ("T" -25200) + ("MST" -25200) + ("PDT" -25200) + + ("S" -21600) + ("CST" -21600) + ("MDT" -21600) + + ("R" -18000) + ("EST" -18000) + ("CDT" -18000) + + ("Q" -14400) + ("AST" -14400) + ("EDT" -14400) + + ("P" -10800) + ("ADT" -10800) + + ("O" -7200) + + ("N" -3600) + + ("Z" 0) + ("GMT" 0) + ("UT" 0) + ("UTC" 0) + ("WET" 0) + + ("A" 3600) + ("CET" 3600) + ("MET" 3600) + ("MEZ" 3600) + ("BST" 3600) + ("WEST" 3600) + + ("B" 7200) + ("EET" 7200) + ("CEST" 7200) + ("MEST" 7200) + ("MESZ" 7200) + + ("C" 10800) + + ("D" 14400) + + ("E" 18000) + + ("F" 21600) + + ("G" 25200) + + ("H" 28800) + + ("I" 32400) + ("JST" 32400) + + ("K" 36000) + ("GST" 36000) + + ("L" 39600) + + ("M" 43200) + ("NZST" 43200) + + ("NZDT" 46800)))) + ;; List of NNDiary headers that specify the time spec. Each header name is + ;; followed by either two integers (specifying a range of possible values + ;; for this header) or one list (specifying all the possible values for this + ;; header). In the latter case, the list does NOT include the unspecifyed + ;; spec (*). + ;; For time zone values, we have symbolic time zone names associated with + ;; the (relative) number of seconds ahead GMT. + ) + +(defsubst nndiary-schedule () + (let (head) + (condition-case arg + (mapcar + (lambda (elt) + (setq head (nth 0 elt)) + (nndiary-parse-schedule (nth 0 elt) (nth 1 elt) (nth 2 elt))) + nndiary-headers) + (t + (nnheader-report 'nndiary "X-Diary-%s header parse error: %s." + head (cdr arg)) + nil)) + )) + +;;; Interface functions ===================================================== + +(nnoo-define-basics nndiary) + +(deffoo nndiary-retrieve-headers (sequence &optional group server fetch-old) + (when (nndiary-possibly-change-directory group server) + (save-excursion + (set-buffer nntp-server-buffer) + (erase-buffer) + (let* ((file nil) + (number (length sequence)) + (count 0) + (file-name-coding-system nnmail-pathname-coding-system) + beg article + (nndiary-check-directory-twice + (and nndiary-check-directory-twice + ;; To speed up, disable it in some case. + (or (not (numberp nnmail-large-newsgroup)) + (<= number nnmail-large-newsgroup))))) + (if (stringp (car sequence)) + 'headers + (if (nndiary-retrieve-headers-with-nov sequence fetch-old) + 'nov + (while sequence + (setq article (car sequence)) + (setq file (nndiary-article-to-file article)) + (when (and file + (file-exists-p file) + (not (file-directory-p file))) + (insert (format "221 %d Article retrieved.\n" article)) + (setq beg (point)) + (nnheader-insert-head file) + (goto-char beg) + (if (search-forward "\n\n" nil t) + (forward-char -1) + (goto-char (point-max)) + (insert "\n\n")) + (insert ".\n") + (delete-region (point) (point-max))) + (setq sequence (cdr sequence)) + (setq count (1+ count)) + (and (numberp nnmail-large-newsgroup) + (> number nnmail-large-newsgroup) + (zerop (% count 20)) + (nnheader-message 6 "nndiary: Receiving headers... %d%%" + (/ (* count 100) number)))) + + (and (numberp nnmail-large-newsgroup) + (> number nnmail-large-newsgroup) + (nnheader-message 6 "nndiary: Receiving headers...done")) + + (nnheader-fold-continuation-lines) + 'headers)))))) + +(deffoo nndiary-open-server (server &optional defs) + (nnoo-change-server 'nndiary server defs) + (when (not (file-exists-p nndiary-directory)) + (ignore-errors (make-directory nndiary-directory t))) + (cond + ((not (file-exists-p nndiary-directory)) + (nndiary-close-server) + (nnheader-report 'nndiary "Couldn't create directory: %s" + nndiary-directory)) + ((not (file-directory-p (file-truename nndiary-directory))) + (nndiary-close-server) + (nnheader-report 'nndiary "Not a directory: %s" nndiary-directory)) + (t + (nnheader-report 'nndiary "Opened server %s using directory %s" + server nndiary-directory) + t))) + +(deffoo nndiary-request-regenerate (server) + (nndiary-possibly-change-directory nil server) + (nndiary-generate-nov-databases server) + t) + +(deffoo nndiary-request-article (id &optional group server buffer) + (nndiary-possibly-change-directory group server) + (let* ((nntp-server-buffer (or buffer nntp-server-buffer)) + (file-name-coding-system nnmail-pathname-coding-system) + path gpath group-num) + (if (stringp id) + (when (and (setq group-num (nndiary-find-group-number id)) + (cdr + (assq (cdr group-num) + (nnheader-article-to-file-alist + (setq gpath + (nnmail-group-pathname + (car group-num) + nndiary-directory)))))) + (setq path (concat gpath (int-to-string (cdr group-num))))) + (setq path (nndiary-article-to-file id))) + (cond + ((not path) + (nnheader-report 'nndiary "No such article: %s" id)) + ((not (file-exists-p path)) + (nnheader-report 'nndiary "No such file: %s" path)) + ((file-directory-p path) + (nnheader-report 'nndiary "File is a directory: %s" path)) + ((not (save-excursion (let ((nnmail-file-coding-system + nndiary-file-coding-system)) + (nnmail-find-file path)))) + (nnheader-report 'nndiary "Couldn't read file: %s" path)) + (t + (nnheader-report 'nndiary "Article %s retrieved" id) + ;; We return the article number. + (cons (if group-num (car group-num) group) + (string-to-int (file-name-nondirectory path))))))) + +(deffoo nndiary-request-group (group &optional server dont-check) + (let ((file-name-coding-system nnmail-pathname-coding-system)) + (cond + ((not (nndiary-possibly-change-directory group server)) + (nnheader-report 'nndiary "Invalid group (no such directory)")) + ((not (file-exists-p nndiary-current-directory)) + (nnheader-report 'nndiary "Directory %s does not exist" + nndiary-current-directory)) + ((not (file-directory-p nndiary-current-directory)) + (nnheader-report 'nndiary "%s is not a directory" + nndiary-current-directory)) + (dont-check + (nnheader-report 'nndiary "Group %s selected" group) + t) + (t + (nnheader-re-read-dir nndiary-current-directory) + (nnmail-activate 'nndiary) + (let ((active (nth 1 (assoc group nndiary-group-alist)))) + (if (not active) + (nnheader-report 'nndiary "No such group: %s" group) + (nnheader-report 'nndiary "Selected group %s" group) + (nnheader-insert "211 %d %d %d %s\n" + (max (1+ (- (cdr active) (car active))) 0) + (car active) (cdr active) group))))))) + +(deffoo nndiary-request-scan (&optional group server) + ;; Use our own mail sources and split methods while Gnus doesn't let us have + ;; multiple backends for retrieving mail. + (let ((mail-sources nndiary-mail-sources) + (nnmail-split-methods nndiary-split-methods)) + (setq nndiary-article-file-alist nil) + (nndiary-possibly-change-directory group server) + (nnmail-get-new-mail 'nndiary 'nndiary-save-nov nndiary-directory group))) + +(deffoo nndiary-close-group (group &optional server) + (setq nndiary-article-file-alist nil) + t) + +(deffoo nndiary-request-create-group (group &optional server args) + (nndiary-possibly-change-directory nil server) + (nnmail-activate 'nndiary) + (cond + ((assoc group nndiary-group-alist) + t) + ((and (file-exists-p (nnmail-group-pathname group nndiary-directory)) + (not (file-directory-p (nnmail-group-pathname + group nndiary-directory)))) + (nnheader-report 'nndiary "%s is a file" + (nnmail-group-pathname group nndiary-directory))) + (t + (let (active) + (push (list group (setq active (cons 1 0))) + nndiary-group-alist) + (nndiary-possibly-create-directory group) + (nndiary-possibly-change-directory group server) + (let ((articles (nnheader-directory-articles nndiary-current-directory))) + (when articles + (setcar active (apply 'min articles)) + (setcdr active (apply 'max articles)))) + (nnmail-save-active nndiary-group-alist nndiary-active-file) + (run-hook-with-args 'nndiary-request-create-group-hooks + (gnus-group-prefixed-name group + (list "nndiary" server))) + t)) + )) + +(deffoo nndiary-request-list (&optional server) + (save-excursion + (let ((nnmail-file-coding-system nnmail-active-file-coding-system) + (file-name-coding-system nnmail-pathname-coding-system)) + (nnmail-find-file nndiary-active-file)) + (setq nndiary-group-alist (nnmail-get-active)) + t)) + +(deffoo nndiary-request-newgroups (date &optional server) + (nndiary-request-list server)) + +(deffoo nndiary-request-list-newsgroups (&optional server) + (save-excursion + (nnmail-find-file nndiary-newsgroups-file))) + +(deffoo nndiary-request-expire-articles (articles group &optional server force) + (nndiary-possibly-change-directory group server) + (let ((active-articles + (nnheader-directory-articles nndiary-current-directory)) + article rest number) + (nnmail-activate 'nndiary) + ;; Articles not listed in active-articles are already gone, + ;; so don't try to expire them. + (setq articles (gnus-intersection articles active-articles)) + (while articles + (setq article (nndiary-article-to-file (setq number (pop articles)))) + (if (and (nndiary-deletable-article-p group number) + ;; Don't use nnmail-expired-article-p. Our notion of expiration + ;; is a bit peculiar ... + (or force (nndiary-expired-article-p article))) + (progn + ;; Allow a special target group. + (unless (eq nnmail-expiry-target 'delete) + (with-temp-buffer + (nndiary-request-article number group server (current-buffer)) + (let ((nndiary-current-directory nil)) + (nnmail-expiry-target-group nnmail-expiry-target group))) + (nndiary-possibly-change-directory group server)) + (nnheader-message 5 "Deleting article %s in %s" number group) + (condition-case () + (funcall nnmail-delete-file-function article) + (file-error (push number rest))) + (setq active-articles (delq number active-articles)) + (nndiary-nov-delete-article group number)) + (push number rest))) + (let ((active (nth 1 (assoc group nndiary-group-alist)))) + (when active + (setcar active (or (and active-articles + (apply 'min active-articles)) + (1+ (cdr active))))) + (nnmail-save-active nndiary-group-alist nndiary-active-file)) + (nndiary-save-nov) + (nconc rest articles))) + +(deffoo nndiary-request-move-article + (article group server accept-form &optional last) + (let ((buf (get-buffer-create " *nndiary move*")) + result) + (nndiary-possibly-change-directory group server) + (nndiary-update-file-alist) + (and + (nndiary-deletable-article-p group article) + (nndiary-request-article article group server) + (let (nndiary-current-directory + nndiary-current-group + nndiary-article-file-alist) + (save-excursion + (set-buffer buf) + (insert-buffer-substring nntp-server-buffer) + (setq result (eval accept-form)) + (kill-buffer (current-buffer)) + result)) + (progn + (nndiary-possibly-change-directory group server) + (condition-case () + (funcall nnmail-delete-file-function + (nndiary-article-to-file article)) + (file-error nil)) + (nndiary-nov-delete-article group article) + (when last + (nndiary-save-nov) + (nnmail-save-active nndiary-group-alist nndiary-active-file)))) + result)) + +(deffoo nndiary-request-accept-article (group &optional server last) + (nndiary-possibly-change-directory group server) + (nnmail-check-syntax) + (run-hooks 'nndiary-request-accept-article-hooks) + (when (nndiary-schedule) + (let (result) + (when nnmail-cache-accepted-message-ids + (nnmail-cache-insert (nnmail-fetch-field "message-id") group)) + (if (stringp group) + (and + (nnmail-activate 'nndiary) + (setq result + (car (nndiary-save-mail + (list (cons group (nndiary-active-number group)))))) + (progn + (nnmail-save-active nndiary-group-alist nndiary-active-file) + (and last (nndiary-save-nov)))) + (and + (nnmail-activate 'nndiary) + (if (and (not (setq result + (nnmail-article-group 'nndiary-active-number))) + (yes-or-no-p "Moved to `junk' group; delete article? ")) + (setq result 'junk) + (setq result (car (nndiary-save-mail result)))) + (when last + (nnmail-save-active nndiary-group-alist nndiary-active-file) + (when nnmail-cache-accepted-message-ids + (nnmail-cache-close)) + (nndiary-save-nov)))) + result)) + ) + +(deffoo nndiary-request-post (&optional server) + (nnmail-do-request-post 'nndiary-request-accept-article server)) + +(deffoo nndiary-request-replace-article (article group buffer) + (nndiary-possibly-change-directory group) + (save-excursion + (set-buffer buffer) + (nndiary-possibly-create-directory group) + (let ((chars (nnmail-insert-lines)) + (art (concat (int-to-string article) "\t")) + headers) + (when (ignore-errors + (nnmail-write-region + (point-min) (point-max) + (or (nndiary-article-to-file article) + (expand-file-name (int-to-string article) + nndiary-current-directory)) + nil (if (nnheader-be-verbose 5) nil 'nomesg)) + t) + (setq headers (nndiary-parse-head chars article)) + ;; Replace the NOV line in the NOV file. + (save-excursion + (set-buffer (nndiary-open-nov group)) + (goto-char (point-min)) + (if (or (looking-at art) + (search-forward (concat "\n" art) nil t)) + ;; Delete the old NOV line. + (delete-region (progn (beginning-of-line) (point)) + (progn (forward-line 1) (point))) + ;; The line isn't here, so we have to find out where + ;; we should insert it. (This situation should never + ;; occur, but one likes to make sure...) + (while (and (looking-at "[0-9]+\t") + (< (string-to-int + (buffer-substring + (match-beginning 0) (match-end 0))) + article) + (zerop (forward-line 1))))) + (beginning-of-line) + (nnheader-insert-nov headers) + (nndiary-save-nov) + t))))) + +(deffoo nndiary-request-delete-group (group &optional force server) + (nndiary-possibly-change-directory group server) + (when force + ;; Delete all articles in GROUP. + (let ((articles + (directory-files + nndiary-current-directory t + (concat nnheader-numerical-short-files + "\\|" (regexp-quote nndiary-nov-file-name) "$"))) + article) + (while articles + (setq article (pop articles)) + (when (file-writable-p article) + (nnheader-message 5 "Deleting article %s in %s..." article group) + (funcall nnmail-delete-file-function article)))) + ;; Try to delete the directory itself. + (ignore-errors (delete-directory nndiary-current-directory))) + ;; Remove the group from all structures. + (setq nndiary-group-alist + (delq (assoc group nndiary-group-alist) nndiary-group-alist) + nndiary-current-group nil + nndiary-current-directory nil) + ;; Save the active file. + (nnmail-save-active nndiary-group-alist nndiary-active-file) + t) + +(deffoo nndiary-request-rename-group (group new-name &optional server) + (nndiary-possibly-change-directory group server) + (let ((new-dir (nnmail-group-pathname new-name nndiary-directory)) + (old-dir (nnmail-group-pathname group nndiary-directory))) + (when (ignore-errors + (make-directory new-dir t) + t) + ;; We move the articles file by file instead of renaming + ;; the directory -- there may be subgroups in this group. + ;; One might be more clever, I guess. + (let ((files (nnheader-article-to-file-alist old-dir))) + (while files + (rename-file + (concat old-dir (cdar files)) + (concat new-dir (cdar files))) + (pop files))) + ;; Move .overview file. + (let ((overview (concat old-dir nndiary-nov-file-name))) + (when (file-exists-p overview) + (rename-file overview (concat new-dir nndiary-nov-file-name)))) + (when (<= (length (directory-files old-dir)) 2) + (ignore-errors (delete-directory old-dir))) + ;; That went ok, so we change the internal structures. + (let ((entry (assoc group nndiary-group-alist))) + (when entry + (setcar entry new-name)) + (setq nndiary-current-directory nil + nndiary-current-group nil) + ;; Save the new group alist. + (nnmail-save-active nndiary-group-alist nndiary-active-file) + t)))) + +(deffoo nndiary-set-status (article name value &optional group server) + (nndiary-possibly-change-directory group server) + (let ((file (nndiary-article-to-file article))) + (cond + ((not (file-exists-p file)) + (nnheader-report 'nndiary "File %s does not exist" file)) + (t + (with-temp-file file + (nnheader-insert-file-contents file) + (nnmail-replace-status name value)) + t)))) + + +;;; Interface optional functions ============================================ + +(deffoo nndiary-request-update-info (group info &optional server) + (nndiary-possibly-change-directory group) + (let ((timestamp (gnus-group-parameter-value (gnus-info-params info) + 'timestamp t))) + (if (not timestamp) + (nnheader-report 'nndiary "Group %s doesn't have a timestamp" group) + ;; else + ;; Figure out which articles should be re-new'ed + (let ((articles (nndiary-flatten (gnus-info-read info) 0)) + article file unread buf) + (save-excursion + (setq buf (nnheader-set-temp-buffer " *nndiary update*")) + (while (setq article (pop articles)) + (setq file (concat nndiary-current-directory + (int-to-string article))) + (and (file-exists-p file) + (nndiary-renew-article-p file timestamp) + (push article unread))) + ;;(message "unread: %s" unread) + (sit-for 1) + (kill-buffer buf)) + (setq unread (sort unread '<)) + (and unread + (gnus-info-set-read info (gnus-update-read-articles + (gnus-info-group info) unread t))) + )) + (run-hook-with-args 'nndiary-request-update-info-hooks + (gnus-info-group info)) + t)) + + + +;;; Internal functions ====================================================== + +(defun nndiary-article-to-file (article) + (nndiary-update-file-alist) + (let (file) + (if (setq file (cdr (assq article nndiary-article-file-alist))) + (expand-file-name file nndiary-current-directory) + ;; Just to make sure nothing went wrong when reading over NFS -- + ;; check once more. + (if nndiary-check-directory-twice + (when (file-exists-p + (setq file (expand-file-name (number-to-string article) + nndiary-current-directory))) + (nndiary-update-file-alist t) + file))))) + +(defun nndiary-deletable-article-p (group article) + "Say whether ARTICLE in GROUP can be deleted." + (let (path) + (when (setq path (nndiary-article-to-file article)) + (when (file-writable-p path) + (or (not nnmail-keep-last-article) + (not (eq (cdr (nth 1 (assoc group nndiary-group-alist))) + article))))))) + +;; Find an article number in the current group given the Message-ID. +(defun nndiary-find-group-number (id) + (save-excursion + (set-buffer (get-buffer-create " *nndiary id*")) + (let ((alist nndiary-group-alist) + number) + ;; We want to look through all .overview files, but we want to + ;; start with the one in the current directory. It seems most + ;; likely that the article we are looking for is in that group. + (if (setq number (nndiary-find-id nndiary-current-group id)) + (cons nndiary-current-group number) + ;; It wasn't there, so we look through the other groups as well. + (while (and (not number) + alist) + (or (string= (caar alist) nndiary-current-group) + (setq number (nndiary-find-id (caar alist) id))) + (or number + (setq alist (cdr alist)))) + (and number + (cons (caar alist) number)))))) + +(defun nndiary-find-id (group id) + (erase-buffer) + (let ((nov (expand-file-name nndiary-nov-file-name + (nnmail-group-pathname group + nndiary-directory))) + number found) + (when (file-exists-p nov) + (nnheader-insert-file-contents nov) + (while (and (not found) + (search-forward id nil t)) ; We find the ID. + ;; And the id is in the fourth field. + (if (not (and (search-backward "\t" nil t 4) + (not (search-backward"\t" (gnus-point-at-bol) t)))) + (forward-line 1) + (beginning-of-line) + (setq found t) + ;; We return the article number. + (setq number + (ignore-errors (read (current-buffer)))))) + number))) + +(defun nndiary-retrieve-headers-with-nov (articles &optional fetch-old) + (if (or gnus-nov-is-evil nndiary-nov-is-evil) + nil + (let ((nov (expand-file-name nndiary-nov-file-name + nndiary-current-directory))) + (when (file-exists-p nov) + (save-excursion + (set-buffer nntp-server-buffer) + (erase-buffer) + (nnheader-insert-file-contents nov) + (if (and fetch-old + (not (numberp fetch-old))) + t ; Don't remove anything. + (nnheader-nov-delete-outside-range + (if fetch-old (max 1 (- (car articles) fetch-old)) + (car articles)) + (car (last articles))) + t)))))) + +(defun nndiary-possibly-change-directory (group &optional server) + (when (and server + (not (nndiary-server-opened server))) + (nndiary-open-server server)) + (if (not group) + t + (let ((pathname (nnmail-group-pathname group nndiary-directory)) + (file-name-coding-system nnmail-pathname-coding-system)) + (when (not (equal pathname nndiary-current-directory)) + (setq nndiary-current-directory pathname + nndiary-current-group group + nndiary-article-file-alist nil)) + (file-exists-p nndiary-current-directory)))) + +(defun nndiary-possibly-create-directory (group) + (let ((dir (nnmail-group-pathname group nndiary-directory))) + (unless (file-exists-p dir) + (make-directory (directory-file-name dir) t) + (nnheader-message 5 "Creating mail directory %s" dir)))) + +(defun nndiary-save-mail (group-art) + "Called narrowed to an article." + (let (chars headers) + (setq chars (nnmail-insert-lines)) + (nnmail-insert-xref group-art) + (run-hooks 'nnmail-prepare-save-mail-hook) + (run-hooks 'nndiary-prepare-save-mail-hook) + (goto-char (point-min)) + (while (looking-at "From ") + (replace-match "X-From-Line: ") + (forward-line 1)) + ;; We save the article in all the groups it belongs in. + (let ((ga group-art) + first) + (while ga + (nndiary-possibly-create-directory (caar ga)) + (let ((file (concat (nnmail-group-pathname + (caar ga) nndiary-directory) + (int-to-string (cdar ga))))) + (if first + ;; It was already saved, so we just make a hard link. + (funcall nnmail-crosspost-link-function first file t) + ;; Save the article. + (nnmail-write-region (point-min) (point-max) file nil + (if (nnheader-be-verbose 5) nil 'nomesg)) + (setq first file))) + (setq ga (cdr ga)))) + ;; Generate a nov line for this article. We generate the nov + ;; line after saving, because nov generation destroys the + ;; header. + (setq headers (nndiary-parse-head chars)) + ;; Output the nov line to all nov databases that should have it. + (let ((ga group-art)) + (while ga + (nndiary-add-nov (caar ga) (cdar ga) headers) + (setq ga (cdr ga)))) + group-art)) + +(defun nndiary-active-number (group) + "Compute the next article number in GROUP." + (let ((active (cadr (assoc group nndiary-group-alist)))) + ;; The group wasn't known to nndiary, so we just create an active + ;; entry for it. + (unless active + ;; Perhaps the active file was corrupt? See whether + ;; there are any articles in this group. + (nndiary-possibly-create-directory group) + (nndiary-possibly-change-directory group) + (unless nndiary-article-file-alist + (setq nndiary-article-file-alist + (sort + (nnheader-article-to-file-alist nndiary-current-directory) + 'car-less-than-car))) + (setq active + (if nndiary-article-file-alist + (cons (caar nndiary-article-file-alist) + (caar (last nndiary-article-file-alist))) + (cons 1 0))) + (push (list group active) nndiary-group-alist)) + (setcdr active (1+ (cdr active))) + (while (file-exists-p + (expand-file-name (int-to-string (cdr active)) + (nnmail-group-pathname group nndiary-directory))) + (setcdr active (1+ (cdr active)))) + (cdr active))) + +(defun nndiary-add-nov (group article headers) + "Add a nov line for the GROUP base." + (save-excursion + (set-buffer (nndiary-open-nov group)) + (goto-char (point-max)) + (mail-header-set-number headers article) + (nnheader-insert-nov headers))) + +(defsubst nndiary-header-value () + (buffer-substring (match-end 0) (progn (end-of-line) (point)))) + +(defun nndiary-parse-head (chars &optional number) + "Parse the head of the current buffer." + (save-excursion + (save-restriction + (unless (zerop (buffer-size)) + (narrow-to-region + (goto-char (point-min)) + (if (search-forward "\n\n" nil t) (1- (point)) (point-max)))) + (let ((headers (nnheader-parse-naked-head))) + (mail-header-set-chars headers chars) + (mail-header-set-number headers number) + headers)))) + +(defun nndiary-open-nov (group) + (or (cdr (assoc group nndiary-nov-buffer-alist)) + (let ((buffer (get-buffer-create (format " *nndiary overview %s*" + group)))) + (save-excursion + (set-buffer buffer) + (set (make-local-variable 'nndiary-nov-buffer-file-name) + (expand-file-name + nndiary-nov-file-name + (nnmail-group-pathname group nndiary-directory))) + (erase-buffer) + (when (file-exists-p nndiary-nov-buffer-file-name) + (nnheader-insert-file-contents nndiary-nov-buffer-file-name))) + (push (cons group buffer) nndiary-nov-buffer-alist) + buffer))) + +(defun nndiary-save-nov () + (save-excursion + (while nndiary-nov-buffer-alist + (when (buffer-name (cdar nndiary-nov-buffer-alist)) + (set-buffer (cdar nndiary-nov-buffer-alist)) + (when (buffer-modified-p) + (nnmail-write-region 1 (point-max) nndiary-nov-buffer-file-name + nil 'nomesg)) + (set-buffer-modified-p nil) + (kill-buffer (current-buffer))) + (setq nndiary-nov-buffer-alist (cdr nndiary-nov-buffer-alist))))) + +;;;###autoload +(defun nndiary-generate-nov-databases (&optional server) + "Generate NOV databases in all nndiary directories." + (interactive (list (or (nnoo-current-server 'nndiary) ""))) + ;; Read the active file to make sure we don't re-use articles + ;; numbers in empty groups. + (nnmail-activate 'nndiary) + (unless (nndiary-server-opened server) + (nndiary-open-server server)) + (setq nndiary-directory (expand-file-name nndiary-directory)) + ;; Recurse down the directories. + (nndiary-generate-nov-databases-1 nndiary-directory nil t) + ;; Save the active file. + (nnmail-save-active nndiary-group-alist nndiary-active-file)) + +(defun nndiary-generate-nov-databases-1 (dir &optional seen no-active) + "Regenerate the NOV database in DIR." + (interactive "DRegenerate NOV in: ") + (setq dir (file-name-as-directory dir)) + ;; Only scan this sub-tree if we haven't been here yet. + (unless (member (file-truename dir) seen) + (push (file-truename dir) seen) + ;; We descend recursively + (let ((dirs (directory-files dir t nil t)) + dir) + (while (setq dir (pop dirs)) + (when (and (not (string-match "^\\." (file-name-nondirectory dir))) + (file-directory-p dir)) + (nndiary-generate-nov-databases-1 dir seen)))) + ;; Do this directory. + (let ((files (sort (nnheader-article-to-file-alist dir) + 'car-less-than-car))) + (if (not files) + (let* ((group (nnheader-file-to-group + (directory-file-name dir) nndiary-directory)) + (info (cadr (assoc group nndiary-group-alist)))) + (when info + (setcar info (1+ (cdr info))))) + (funcall nndiary-generate-active-function dir) + ;; Generate the nov file. + (nndiary-generate-nov-file dir files) + (unless no-active + (nnmail-save-active nndiary-group-alist nndiary-active-file)))))) + +(eval-when-compile (defvar files)) +(defun nndiary-generate-active-info (dir) + ;; Update the active info for this group. + (let* ((group (nnheader-file-to-group + (directory-file-name dir) nndiary-directory)) + (entry (assoc group nndiary-group-alist)) + (last (or (caadr entry) 0))) + (setq nndiary-group-alist (delq entry nndiary-group-alist)) + (push (list group + (cons (or (caar files) (1+ last)) + (max last + (or (let ((f files)) + (while (cdr f) (setq f (cdr f))) + (caar f)) + 0)))) + nndiary-group-alist))) + +(defun nndiary-generate-nov-file (dir files) + (let* ((dir (file-name-as-directory dir)) + (nov (concat dir nndiary-nov-file-name)) + (nov-buffer (get-buffer-create " *nov*")) + chars file headers) + (save-excursion + ;; Init the nov buffer. + (set-buffer nov-buffer) + (buffer-disable-undo) + (erase-buffer) + (set-buffer nntp-server-buffer) + ;; Delete the old NOV file. + (when (file-exists-p nov) + (funcall nnmail-delete-file-function nov)) + (while files + (unless (file-directory-p (setq file (concat dir (cdar files)))) + (erase-buffer) + (nnheader-insert-file-contents file) + (narrow-to-region + (goto-char (point-min)) + (progn + (search-forward "\n\n" nil t) + (setq chars (- (point-max) (point))) + (max 1 (1- (point))))) + (unless (zerop (buffer-size)) + (goto-char (point-min)) + (setq headers (nndiary-parse-head chars (caar files))) + (save-excursion + (set-buffer nov-buffer) + (goto-char (point-max)) + (nnheader-insert-nov headers))) + (widen)) + (setq files (cdr files))) + (save-excursion + (set-buffer nov-buffer) + (nnmail-write-region 1 (point-max) nov nil 'nomesg) + (kill-buffer (current-buffer)))))) + +(defun nndiary-nov-delete-article (group article) + (save-excursion + (set-buffer (nndiary-open-nov group)) + (when (nnheader-find-nov-line article) + (delete-region (point) (progn (forward-line 1) (point))) + (when (bobp) + (let ((active (cadr (assoc group nndiary-group-alist))) + num) + (when active + (if (eobp) + (setf (car active) (1+ (cdr active))) + (when (and (setq num (ignore-errors (read (current-buffer)))) + (numberp num)) + (setf (car active) num))))))) + t)) + +(defun nndiary-update-file-alist (&optional force) + (when (or (not nndiary-article-file-alist) + force) + (setq nndiary-article-file-alist + (nnheader-article-to-file-alist nndiary-current-directory)))) + + +(defun nndiary-string-to-int (str min &optional max) + ;; Like `string-to-int' but barf if STR is not exactly an integer, and not + ;; within the specified bounds. + ;; Signals are caught by `nndiary-schedule'. + (if (not (string-match "^[ \t]*[0-9]+[ \t]*$" str)) + (nndiary-error "not an integer value") + ;; else + (let ((val (string-to-int str))) + (and (or (< val min) + (and max (> val max))) + (nndiary-error "value out of range")) + val))) + +(defun nndiary-parse-schedule-value (str min-or-values max) + ;; Parse the schedule string STR, or signal an error. + ;; Signals are caught by `nndary-schedule'. + (if (string-match "[ \t]*\\*[ \t]*" str) + ;; unspecifyed + nil + ;; specifyed + (if (listp min-or-values) + ;; min-or-values is values + ;; #### NOTE: this is actually only a hack for time zones. + (let ((val (and (string-match "[ \t]*\\([^ \t]+\\)[ \t]*" str) + (match-string 1 str)))) + (if (and val (setq val (assoc val min-or-values))) + (list (cadr val)) + (nndiary-error "invalid syntax"))) + ;; min-or-values is min + (mapcar + (lambda (val) + (let ((res (split-string val "-"))) + (cond + ((= (length res) 1) + (nndiary-string-to-int (car res) min-or-values max)) + ((= (length res) 2) + ;; don't know if crontab accepts this, but ensure + ;; that BEG is <= END + (let ((beg (nndiary-string-to-int (car res) min-or-values max)) + (end (nndiary-string-to-int (cadr res) min-or-values max))) + (cond ((< beg end) + (cons beg end)) + ((= beg end) + beg) + (t + (cons end beg))))) + (t + (nndiary-error "invalid syntax"))) + )) + (split-string str ","))) + )) + +;; ### FIXME: remove this function if it's used only once. +(defun nndiary-parse-schedule (head min-or-values max) + ;; Parse the cron-like value of header X-Diary-HEAD in current buffer. + ;; - Returns nil if `*' + ;; - Otherwise returns a list of integers and/or ranges (BEG . END) + ;; The exception is the Timze-Zone value which is always of the form (STR). + ;; Signals are caught by `nndary-schedule'. + (let ((header (format "^X-Diary-%s: \\(.*\\)$" head))) + (goto-char (point-min)) + (if (not (re-search-forward header nil t)) + (nndiary-error "header missing") + ;; else + (nndiary-parse-schedule-value (match-string 1) min-or-values max)) + )) + +(defun nndiary-max (spec) + ;; Returns the max of specification SPEC, or nil for permanent schedules. + (unless (null spec) + (let ((elts spec) + (max 0) + elt) + (while (setq elt (pop elts)) + (if (integerp elt) + (and (> elt max) (setq max elt)) + (and (> (cdr elt) max) (setq max (cdr elt))))) + max))) + +(defun nndiary-flatten (spec min &optional max) + ;; flatten the spec by expanding ranges to all possible values. + (let (flat n) + (cond ((null spec) + ;; this happens when I flatten something else than one of my + ;; schedules (a list of read articles for instance). + (unless (null max) + (setq n min) + (while (<= n max) + (push n flat) + (setq n (1+ n))))) + (t + (let ((elts spec) + elt) + (while (setq elt (pop elts)) + (if (integerp elt) + (push elt flat) + ;; else + (setq n (car elt)) + (while (<= n (cdr elt)) + (push n flat) + (setq n (1+ n)))))))) + flat)) + +(defun nndiary-unflatten (spec) + ;; opposite of flatten: build ranges if possible + (setq spec (sort spec '<)) + (let (min max res) + (while (setq min (pop spec)) + (setq max min) + (while (and (car spec) (= (car spec) (1+ max))) + (setq max (1+ max)) + (pop spec)) + (if (= max min) + (setq res (append res (list min))) + (setq res (append res (list (cons min max)))))) + res)) + +(defun nndiary-compute-reminders (date) + ;; Returns a list of times corresponding to the reminders of date DATE. + ;; See the comment in `nndiary-reminders' about rounding. + (let* ((reminders nndiary-reminders) + (date-elts (decode-time date)) + ;; ### NOTE: out-of-range values are accepted by encode-time. This + ;; makes our life easier. + (monday (- (nth 3 date-elts) + (if nndiary-week-starts-on-monday + (if (zerop (nth 6 date-elts)) + 6 + (- (nth 6 date-elts) 1)) + (nth 6 date-elts)))) + reminder res) + ;; remove the DOW and DST entries + (setcdr (nthcdr 5 date-elts) (nthcdr 8 date-elts)) + (while (setq reminder (pop reminders)) + (push + (cond ((eq (cdr reminder) 'minute) + (subtract-time + (apply 'encode-time 0 (nthcdr 1 date-elts)) + (seconds-to-time (* (car reminder) 60.0)))) + ((eq (cdr reminder) 'hour) + (subtract-time + (apply 'encode-time 0 0 (nthcdr 2 date-elts)) + (seconds-to-time (* (car reminder) 3600.0)))) + ((eq (cdr reminder) 'day) + (subtract-time + (apply 'encode-time 0 0 0 (nthcdr 3 date-elts)) + (seconds-to-time (* (car reminder) 86400.0)))) + ((eq (cdr reminder) 'week) + (subtract-time + (apply 'encode-time 0 0 0 monday (nthcdr 4 date-elts)) + (seconds-to-time (* (car reminder) 604800.0)))) + ((eq (cdr reminder) 'month) + (subtract-time + (apply 'encode-time 0 0 0 1 (nthcdr 4 date-elts)) + (seconds-to-time (* (car reminder) 18748800.0)))) + ((eq (cdr reminder) 'year) + (subtract-time + (apply 'encode-time 0 0 0 1 1 (nthcdr 5 date-elts)) + (seconds-to-time (* (car reminder) 400861056.0))))) + res)) + (sort res 'time-less-p))) + +(defun nndiary-last-occurence (sched) + ;; Returns the last occurence of schedule SCHED as an Emacs time struct, or + ;; nil for permanent schedule or errors. + (let ((minute (nndiary-max (nth 0 sched))) + (hour (nndiary-max (nth 1 sched))) + (year (nndiary-max (nth 4 sched))) + (time-zone (or (and (nth 6 sched) (car (nth 6 sched))) + (current-time-zone)))) + (when year + (or minute (setq minute 59)) + (or hour (setq hour 23)) + ;; I'll just compute all possible values and test them by decreasing + ;; order until one succeeds. This is probably quide rude, but I got + ;; bored in finding a good algorithm for doing that ;-) + ;; ### FIXME: remove identical entries. + (let ((dom-list (nth 2 sched)) + (month-list (sort (nndiary-flatten (nth 3 sched) 1 12) '>)) + (year-list (sort (nndiary-flatten (nth 4 sched) 1971) '>)) + (dow-list (nth 5 sched))) + ;; Special case: an asterisk in one of the days specifications means + ;; that only the other should be taken into account. If both are + ;; unspecified, you would get all possible days in both. + (cond ((null dow-list) + ;; this gets all days if dom-list is nil + (setq dom-list (nndiary-flatten dom-list 1 31))) + ((null dom-list) + ;; this also gets all days if dow-list is nil + (setq dow-list (nndiary-flatten dow-list 0 6))) + (t + (setq dom-list (nndiary-flatten dom-list 1 31)) + (setq dow-list (nndiary-flatten dow-list 0 6)))) + (or + (catch 'found + (while (setq year (pop year-list)) + (let ((months month-list) + month) + (while (setq month (pop months)) + ;; Now we must merge the Dows with the Doms. To do that, we + ;; have to know which day is the 1st one for this month. + ;; Maybe there's simpler, but decode-time(encode-time) will + ;; give us the answer. + (let ((first (nth 6 (decode-time + (encode-time 0 0 0 1 month year + time-zone)))) + (max (cond ((= month 2) + (if (date-leap-year-p year) 29 28)) + ((<= month 7) + (if (zerop (% month 2)) 30 31)) + (t + (if (zerop (% month 2)) 31 30)))) + (doms dom-list) + (dows dow-list) + day days) + ;; first, review the doms to see if they are valid. + (while (setq day (pop doms)) + (and (<= day max) + (push day days))) + ;; second add all possible dows + (while (setq day (pop dows)) + ;; days start at 1. + (setq day (1+ (- day first))) + (and (< day 0) (setq day (+ 7 day))) + (while (<= day max) + (push day days) + (setq day (+ 7 day)))) + ;; Finally, if we have some days, they are valid + (when days + (sort days '>) + (throw 'found + (encode-time 0 minute hour + (car days) month year time-zone))) + ))))) + ;; There's an upper limit, but we didn't find any last occurence. + ;; This means that the schedule is undecidable. This can happen if + ;; you happen to say something like "each Feb 31 until 2038". + (progn + (nnheader-report 'nndiary "Undecidable schedule") + nil)) + )))) + +(defun nndiary-next-occurence (sched now) + ;; Returns the next occurence of schedule SCHED, starting from time NOW. + ;; If there's no next occurence, returns the last one (if any) which is then + ;; in the past. + (let* ((today (decode-time now)) + (this-minute (nth 1 today)) + (this-hour (nth 2 today)) + (this-day (nth 3 today)) + (this-month (nth 4 today)) + (this-year (nth 5 today)) + (minute-list (sort (nndiary-flatten (nth 0 sched) 0 59) '<)) + (hour-list (sort (nndiary-flatten (nth 1 sched) 0 23) '<)) + (dom-list (nth 2 sched)) + (month-list (sort (nndiary-flatten (nth 3 sched) 1 12) '<)) + (years (if (nth 4 sched) + (sort (nndiary-flatten (nth 4 sched) 1971) '<) + t)) + (dow-list (nth 5 sched)) + (year (1- this-year)) + (time-zone (or (and (nth 6 sched) (car (nth 6 sched))) + (current-time-zone)))) + ;; Special case: an asterisk in one of the days specifications means that + ;; only the other should be taken into account. If both are unspecified, + ;; you would get all possible days in both. + (cond ((null dow-list) + ;; this gets all days if dom-list is nil + (setq dom-list (nndiary-flatten dom-list 1 31))) + ((null dom-list) + ;; this also gets all days if dow-list is nil + (setq dow-list (nndiary-flatten dow-list 0 6))) + (t + (setq dom-list (nndiary-flatten dom-list 1 31)) + (setq dow-list (nndiary-flatten dow-list 0 6)))) + ;; Remove past years. + (unless (eq years t) + (while (and (car years) (< (car years) this-year)) + (pop years))) + (if years + ;; Because we might not be limited in years, we must guard against + ;; infinite loops. Appart from cases like Feb 31, there are probably + ;; other ones, (no monday XXX 2nd etc). I don't know any algorithm to + ;; decide this, so I assume that if we reach 10 years later, the + ;; schedule is undecidable. + (or + (catch 'found + (while (if (eq years t) + (and (setq year (1+ year)) + (<= year (+ 10 this-year))) + (setq year (pop years))) + (let ((months month-list) + month) + ;; Remove past months for this year. + (and (= year this-year) + (while (and (car months) (< (car months) this-month)) + (pop months))) + (while (setq month (pop months)) + ;; Now we must merge the Dows with the Doms. To do that, we + ;; have to know which day is the 1st one for this month. + ;; Maybe there's simpler, but decode-time(encode-time) will + ;; give us the answer. + (let ((first (nth 6 (decode-time + (encode-time 0 0 0 1 month year + time-zone)))) + (max (cond ((= month 2) + (if (date-leap-year-p year) 29 28)) + ((<= month 7) + (if (zerop (% month 2)) 30 31)) + (t + (if (zerop (% month 2)) 31 30)))) + (doms dom-list) + (dows dow-list) + day days) + ;; first, review the doms to see if they are valid. + (while (setq day (pop doms)) + (and (<= day max) + (push day days))) + ;; second add all possible dows + (while (setq day (pop dows)) + ;; days start at 1. + (setq day (1+ (- day first))) + (and (< day 0) (setq day (+ 7 day))) + (while (<= day max) + (push day days) + (setq day (+ 7 day)))) + ;; Aaaaaaall right. Now we have a valid list of DAYS for + ;; this month and this year. + (when days + (setq days (sort days '<)) + ;; Remove past days for this year and this month. + (and (= year this-year) + (= month this-month) + (while (and (car days) (< (car days) this-day)) + (pop days))) + (while (setq day (pop days)) + (let ((hours hour-list) + hour) + ;; Remove past hours for this year, this month and + ;; this day. + (and (= year this-year) + (= month this-month) + (= day this-day) + (while (and (car hours) + (< (car hours) this-hour)) + (pop hours))) + (while (setq hour (pop hours)) + (let ((minutes minute-list) + minute) + ;; Remove past hours for this year, this month, + ;; this day and this hour. + (and (= year this-year) + (= month this-month) + (= day this-day) + (= hour this-hour) + (while (and (car minutes) + (< (car minutes) this-minute)) + (pop minutes))) + (while (setq minute (pop minutes)) + ;; Ouch! Here, we've got a complete valid + ;; schedule. It's a good one if it's in the + ;; future. + (let ((time (encode-time 0 minute hour day + month year + time-zone))) + (and (time-less-p now time) + (throw 'found time))) + )))) + )) + ))) + )) + (nndiary-last-occurence sched)) + ;; else + (nndiary-last-occurence sched)) + )) + +(defun nndiary-expired-article-p (file) + (with-temp-buffer + (if (nnheader-insert-head file) + (let ((sched (nndiary-schedule))) + ;; An article has expired if its last schedule (if any) is in the + ;; past. A permanent schedule never expires. + (and sched + (setq sched (nndiary-last-occurence sched)) + (time-less-p sched (current-time)))) + ;; else + (nnheader-report 'nndiary "Could not read file %s" file) + nil) + )) + +(defun nndiary-renew-article-p (file timestamp) + (erase-buffer) + (if (nnheader-insert-head file) + (let ((now (current-time)) + (sched (nndiary-schedule))) + ;; The article should be re-considered as unread if there's a reminder + ;; between the group timestamp and the current time. + (when (and sched (setq sched (nndiary-next-occurence sched now))) + (let ((reminders ;; add the next occurence itself at the end. + (append (nndiary-compute-reminders sched) (list sched)))) + (while (and reminders (time-less-p (car reminders) timestamp)) + (pop reminders)) + ;; The reminders might be empty if the last date is in the past, + ;; or we've got at least the next occurence itself left. All past + ;; dates are renewed. + (or (not reminders) + (time-less-p (car reminders) now))) + )) + ;; else + (nnheader-report 'nndiary "Could not read file %s" file) + nil)) + +;; The end... =============================================================== + +(mapcar + (lambda (elt) + (let ((header (intern (format "X-Diary-%s" (car elt))))) + ;; Required for building NOV databases and some other stuff + (add-to-list 'gnus-extra-headers header) + (add-to-list 'nnmail-extra-headers header))) + nndiary-headers) + +(unless (assoc "nndiary" gnus-valid-select-methods) + (gnus-declare-backend "nndiary" 'post-mail 'respool 'address)) + +(provide 'nndiary) + + +;;; nndiary.el ends here diff --git a/lisp/nnir.el b/lisp/nnir.el new file mode 100644 index 0000000..846db89 --- /dev/null +++ b/lisp/nnir.el @@ -0,0 +1,1375 @@ +;;; nnir.el --- search mail with various search engines +;; Copyright (C) 1998 Kai Großjohann + +;; Author: Kai Großjohann +;; Keywords: news, mail, searching, ir, glimpse, wais + +;; This file is not part of GNU Emacs. + +;; This 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; The most recent version of this can always be fetched from the +;; following FTP site: +;; ls6-ftp.cs.uni-dortmund.de:/pub/src/emacs + +;; This code is still in the development stage but I'd like other +;; people to have a look at it. Please do not hesitate to contact me +;; with your ideas. + +;; What does it do? Well, it allows you to index your mail using some +;; search engine (freeWAIS-sf and Glimpse are currently supported), +;; then type `G G' in the Group buffer and issue a query to the search +;; engine. You will then get a buffer which shows all articles +;; matching the query, sorted by Retrieval Status Value (score). + +;; When looking at the retrieval result (in the Summary buffer) you +;; can type `G T' (aka M-x gnus-summary-nnir-goto-thread RET) on an +;; article. You will be teleported into the group this article came +;; from, showing the thread this article is part of. (See below for +;; restrictions.) + +;; The Lisp installation is simple: just put this file on your +;; load-path, byte-compile it, and load it from ~/.gnus or something. +;; This will install a new command `G G' in your Group buffer for +;; searching your mail. Note that you also need to configure a number +;; of variables, as described below. + +;; Restrictions: +;; +;; * Currently, this expects that you use nnml or another +;; one-file-per-message backend. +;; * It can only search one mail backend. +;; * There are restrictions to the Glimpse setup. +;; * There are restrictions to the Wais setup. +;; * gnus-summary-nnir-goto-thread: Fetches whole group first, before +;; limiting to the right articles. This is much too slow, of +;; course. May issue a query for number of articles to fetch; you +;; must accept the default of all articles at this point or things +;; may break. + +;; The Lisp setup involves setting a few variables and setting up the +;; search engine. The first variable to set is `nnir-mail-backend'. +;; For me, `gnus-secondary-select-methods' contains just one select +;; method, and this is also what I put in `nnir-mail-backend'. Type +;; `C-h v nnir-mail-backend RET' for more information -- the variable +;; documentation includes more details and a few examples. The second +;; variable to set is `nnir-search-engine'. Choose one of the engines +;; listed in `nnir-engines'. (Actually `nnir-engines' is an alist, +;; type `C-h v nnir-engines RET' for more information; this includes +;; examples for setting `nnir-search-engine', too.) + +;; You must also set up a search engine. I'll tell you about the two +;; search engines currently supported: + +;; 1. freeWAIS-sf +;; +;; As always with freeWAIS-sf, you need a so-called `format file'. I +;; use the following file: +;; +;; ,----- +;; | # Kai's format file for freeWAIS-sf for indexing mails. +;; | # Each mail is in a file, much like the MH format. +;; | +;; | # Document separator should never match -- each file is a document. +;; | record-sep: /^@this regex should never match@$/ +;; | +;; | # Searchable fields specification. +;; | +;; | region: /^[sS]ubject:/ /^[sS]ubject: */ +;; | subject "Subject header" stemming TEXT BOTH +;; | end: /^[^ \t]/ +;; | +;; | region: /^([tT][oO]|[cC][cC]):/ /^([tT][oO]|[cC][cC]): */ +;; | to "To and Cc headers" SOUNDEX BOTH +;; | end: /^[^ \t]/ +;; | +;; | region: /^[fF][rR][oO][mM]:/ /^[fF][rR][oO][mM]: */ +;; | from "From header" SOUNDEX BOTH +;; | end: /^[^ \t]/ +;; | +;; | region: /^$/ +;; | stemming TEXT GLOBAL +;; | end: /^@this regex should never match@$/ +;; `----- +;; +;; 1998-07-22: waisindex would dump core on me for large articles with +;; the above settings. I used /^$/ as the end regex for the global +;; field. That seemed to work okay. + +;; There is a Perl module called `WAIS.pm' which is available from +;; CPAN as well as ls6-ftp.cs.uni-dortmund.de:/pub/wais/Perl. This +;; module comes with a nifty tool called `makedb', which I use for +;; indexing. Here's my `makedb.conf': +;; +;; ,----- +;; | # Config file for makedb +;; | +;; | # Global options +;; | waisindex = /usr/local/bin/waisindex +;; | wais_opt = -stem -t fields +;; | # `-stem' option necessary when `stemming' is specified for the +;; | # global field in the *.fmt file +;; | +;; | # Own variables +;; | homedir = /home/kai +;; | +;; | # The mail database. +;; | database = mail +;; | files = `find $homedir/Mail -name \*[0-9] -print` +;; | dbdir = $homedir/.wais +;; | limit = 100 +;; `----- +;; +;; The Lisp setup involves the `nnir-wais-*' variables. The most +;; difficult to understand variable is probably +;; `nnir-wais-remove-prefix'. Here's what it does: the output of +;; `waissearch' basically contains the file name and the (full) +;; directory name. As Gnus works with group names rather than +;; directory names, the directory name is transformed into a group +;; name as follows: first, a prefix is removed from the (full) +;; directory name, then all `/' are replaced with `.'. The variable +;; `nnir-wais-remove-prefix' should contain a regex matching exactly +;; this prefix. It defaults to `$HOME/Mail/' (note the trailing +;; slash). + +;; 2. Glimpse +;; +;; The code expects you to have one Glimpse index which contains all +;; your mail files. The Lisp setup involves setting the +;; `nnir-glimpse-*' variables. The most difficult to understand +;; variable is probably `nnir-glimpse-remove-prefix', it corresponds +;; to `nnir-wais-remove-prefix', see above. The `nnir-glimpse-home' +;; variable should be set to the value of the `-H' option which allows +;; one to search this Glimpse index. I have indexed my whole home +;; directory with Glimpse, so I assume a default of `$HOME'. + +;; 3. Namazu +;; +;; The Namazu backend requires you to have one directory containing all +;; index files, this is controlled by the `nnir-namazu-index-directory' +;; variable. To function the `nnir-namazu-remove-prefix' variable must +;; also be correct, see the documentation for `nnir-wais-remove-prefix' +;; above. +;; +;; It is particularly important not to pass any any switches to namazu +;; that will change the output format. Good switches to use include +;; `--sort', `--ascending', `--early' and `--late'. Refer to the Namazu +;; documentation for further information on valid switches. +;; +;; To index my mail with the `mknmz' program I use the following +;; configuration file: +;; +;; ,---- +;; | package conf; # Don't remove this line! +;; | +;; | # Paths which will not be indexed. Don't use `^' or `$' anchors. +;; | $EXCLUDE_PATH = "spam|sent"; +;; | +;; | # Header fields which should be searchable. case-insensitive +;; | $REMAIN_HEADER = "from|date|message-id|subject"; +;; | +;; | # Searchable fields. case-insensitive +;; | $SEARCH_FIELD = "from|date|message-id|subject"; +;; | +;; | # The max length of a word. +;; | $WORD_LENG_MAX = 128; +;; | +;; | # The max length of a field. +;; | $MAX_FIELD_LENGTH = 256; +;; `---- +;; +;; My mail is stored in the directories ~/Mail/mail/, ~/Mail/lists/ and +;; ~/Mail/archive/, so to index them I go to the directory set in +;; `nnir-namazu-index-directory' and issue the following command. +;; +;; mknmz --mailnews ~/Mail/archive/ ~/Mail/mail/ ~/Mail/lists/ +;; +;; For maximum searching efficiency I have a cron job set to run this +;; command every four hours. + +;; Developer information: + +;; I have tried to make the code expandable. Basically, it is divided +;; into two layers. The upper layer is somewhat like the `nnvirtual' +;; or `nnkiboze' backends: given a specification of what articles to +;; show from another backend, it creates a group containing exactly +;; those articles. The lower layer issues a query to a search engine +;; and produces such a specification of what articles to show from the +;; other backend. + +;; The interface between the two layers consists of the single +;; function `nnir-run-query', which just selects the appropriate +;; function for the search engine one is using. The input to +;; `nnir-run-query' is a string, representing the query as input by +;; the user. The output of `nnir-run-query' is supposed to be a +;; vector, each element of which should in turn be a three-element +;; vector. The first element should be group name of the article, the +;; second element should be the article number, and the third element +;; should be the Retrieval Status Value (RSV) as returned from the +;; search engine. An RSV is the score assigned to the document by the +;; search engine. For Boolean search engines like Glimpse, the RSV is +;; always 1000 (or 1 or 100, or whatever you like). + +;; The sorting order of the articles in the summary buffer created by +;; nnir is based on the order of the articles in the above mentioned +;; vector, so that's where you can do the sorting you'd like. Maybe +;; it would be nice to have a way of displaying the search result +;; sorted differently? + +;; So what do you need to do when you want to add another search +;; engine? You write a function that executes the query. Temporary +;; data from the search engine can be put in `nnir-tmp-buffer'. This +;; function should return the list of articles as a vector, as +;; described above. Then, you need to register this backend in +;; `nnir-engines'. Then, users can choose the backend by setting +;; `nnir-search-engine'. + +;; Todo, or future ideas: + +;; * Make it so that Glimpse can also be called without `-F'. +;; +;; * It should be possible to restrict search to certain groups. +;; +;; * There is currently no error checking. +;; +;; * The summary buffer display is currently really ugly, with all the +;; added information in the subjects. How could I make this +;; prettier? +;; +;; * A function which can be called from an nnir summary buffer which +;; teleports you into the group the current article came from and +;; shows you the whole thread this article is part of. +;; Implementation suggestions? +;; (1998-07-24: There is now a preliminary implementation, but +;; it is much too slow and quite fragile.) +;; +;; * Support other mail backends. In particular, probably quite a few +;; people use nnfolder. How would one go about searching nnfolders +;; and producing the right data needed? The group name and the RSV +;; are simple, but what about the article number? +;; +;; * Support compressed mail files. Probably, just stripping off the +;; `.gz' or `.Z' file name extension is sufficient. +;; +;; * Support a find/grep combination. +;; +;; * At least for imap, the query is performed twice. +;; +;; * Support multiple mail backends. The information that is needed +;; by nnir could be put in the server parameters. (Use sensible +;; default values, though: include the name of the backend in the +;; default value such that people do not have to mess with the +;; server parameters if they don't want to.) It is not clear how to +;; do the user interface, though. Hm. Maybe offer the user a +;; completable list of backends to search? Or use the +;; process-marked groups to find out which backends to search? Or +;; always search all backends? +;; + +;; Have you got other ideas? + +;;; Setup Code: + +(defconst nnir-version "1.72" + "Version of NNIR.") + +(require 'cl) +(require 'nnoo) +(require 'gnus-group) +(require 'gnus-sum) +(eval-and-compile + (require 'gnus-util)) + +(nnoo-declare nnir) +(nnoo-define-basics nnir) + +(gnus-declare-backend "nnir" 'mail) + +;;; Developer Extension Variable: + +(defvar nnir-engines + '((glimpse nnir-run-glimpse + ((group . "Group spec: "))) + (wais nnir-run-waissearch + ()) + (excite nnir-run-excite-search + ()) + (imap nnir-run-imap + ()) + (swish++ nnir-run-swish++ + ((group . "Group spec: "))) + (swish-e nnir-run-swish-e + ((group . "Group spec: "))) + (namazu nnir-run-namazu + ())) +"Alist of supported search engines. +Each element in the alist is a three-element list (ENGINE FUNCTION ARGS). +ENGINE is a symbol designating the searching engine. FUNCTION is also +a symbol, giving the function that does the search. The third element +ARGS is a list of cons pairs (PARAM . PROMPT). When issuing a query, +the FUNCTION will issue a query for each of the PARAMs, using PROMPT. + +The value of `nnir-search-engine' must be one of the ENGINE symbols. +For example, use the following line for searching using freeWAIS-sf: + (setq nnir-search-engine 'wais) +Use the following line if you read your mail via IMAP and your IMAP +server supports searching: + (setq nnir-search-engine 'imap) +Note that you have to set additional variables for most backends. For +example, the `wais' backend needs the variables `nnir-wais-program', +`nnir-wais-database' and `nnir-wais-remove-prefix'. + +Add an entry here when adding a new search engine.") + +;;; User Customizable Variables: + +(defgroup nnir nil + "Search nnmh and nnml groups in Gnus with Glimpse, freeWAIS-sf, or EWS.") + +;; Mail backend. + +;; TODO: +;; If `nil', use server parameters to find out which server to search. CCC +;; +(defcustom nnir-mail-backend '(nnml "") + "*Specifies which backend should be searched. +More precisely, this is used to determine from which backend to fetch the +messages found. + +This must be equal to an existing server, so maybe it is best to use +something like the following: + (setq nnir-mail-backend (nth 0 gnus-secondary-select-methods)) +The above line works fine if the mail backend you want to search is +the first element of gnus-secondary-select-methods (`nth' starts counting +at zero)." + :type '(sexp) + :group 'nnir) + +;; Search engine to use. + +(defcustom nnir-search-engine 'wais + "*The search engine to use. Must be a symbol. +See `nnir-engines' for a list of supported engines, and for example +settings of `nnir-search-engine'." + :type '(sexp) + :group 'nnir) + +;; Glimpse engine. + +(defcustom nnir-glimpse-program "glimpse" + "*Name of Glimpse executable." + :type '(string) + :group 'nnir) + +(defcustom nnir-glimpse-home (getenv "HOME") + "*Value of `-H' glimpse option. +`~' and environment variables must be expanded, see the functions +`expand-file-name' and `substitute-in-file-name'." + :type '(directory) + :group 'nnir) + +(defcustom nnir-glimpse-remove-prefix (concat (getenv "HOME") "/Mail/") + "*The prefix to remove from each file name returned by Glimpse +in order to get a group name (albeit with / instead of .). This is a +regular expression. + +For example, suppose that Glimpse returns file names such as +\"/home/john/Mail/mail/misc/42\". For this example, use the following +setting: (setq nnir-glimpse-remove-prefix \"/home/john/Mail/\") +Note the trailing slash. Removing this prefix gives \"mail/misc/42\". +`nnir' knows to remove the \"/42\" and to replace \"/\" with \".\" to +arrive at the correct group name, \"mail.misc\"." + :type '(regexp) + :group 'nnir) + +(defcustom nnir-glimpse-additional-switches '("-i") + "*A list of strings, to be given as additional arguments to glimpse. +The switches `-H', `-W', `-l' and `-y' are always used -- calling +glimpse without them does not make sense in our situation. +Suggested elements to put here are `-i' and `-w'. + +Note that this should be a list. Ie, do NOT use the following: + (setq nnir-glimpse-additional-switches \"-i -w\") ; wrong! +Instead, use this: + (setq nnir-glimpse-additional-switches '(\"-i\" \"-w\"))" + :type '(repeat (string)) + :group 'nnir) + +;; freeWAIS-sf. + +(defcustom nnir-wais-program "waissearch" + "*Name of waissearch executable." + :type '(string) + :group 'nnir) + +(defcustom nnir-wais-database (expand-file-name "~/.wais/mail") + "*Name of Wais database containing the mail. + +Note that this should be a file name without extension. For example, +if you have a file /home/john/.wais/mail.fmt, use this: + (setq nnir-wais-database \"/home/john/.wais/mail\") +The string given here is passed to `waissearch -d' as-is." + :type '(file) + :group 'nnir) + +(defcustom nnir-wais-remove-prefix (concat (getenv "HOME") "/Mail/") + "*The prefix to remove from each directory name returned by waissearch +in order to get a group name (albeit with / instead of .). This is a +regular expression. + +This variable is similar to `nnir-glimpse-remove-prefix', only for Wais, +not Glimpse." + :type '(regexp) + :group 'nnir) + +;; EWS (Excite for Web Servers) engine. + +(defcustom nnir-excite-aquery-program "aquery.pl" + "*Name of the EWS query program. Should be `aquery.pl' or a path to same." + :type '(string) + :group 'nnir) + +(defcustom nnir-excite-collection "Mail" + "*Name of the EWS collection to search." + :type '(string) + :group 'nnir) + +(defcustom nnir-excite-remove-prefix (concat (getenv "HOME") "/Mail/") + "*The prefix to remove from each file name returned by EWS +in order to get a group name (albeit with / instead of .). This is a +regular expression. + +This variable is very similar to `nnir-glimpse-remove-prefix', except +that it is for EWS, not Glimpse." + :type '(regexp) + :group 'nnir) + +(defcustom nnir-imap-default-charset nil + "*Name of the charset of the strings that appear in the search criteria." + :type '(choice (const nil) symbol) + :group 'nnir) + +;; Swish++. Next three variables Copyright (C) 2000, 2001 Christoph +;; Conrad . +;; Swish++ home page: http://homepage.mac.com/pauljlucas/software/swish/ + +(defcustom nnir-swish++-configuration-file + (expand-file-name "~/Mail/swish++.conf") + "*Configuration file for swish++." + :type '(file) + :group 'nnir) + +(defcustom nnir-swish++-program "search" + "*Name of swish++ search executable." + :type '(string) + :group 'nnir) + +(defcustom nnir-swish++-additional-switches '() + "*A list of strings, to be given as additional arguments to swish++. + +Note that this should be a list. Ie, do NOT use the following: + (setq nnir-swish++-additional-switches \"-i -w\") ; wrong +Instead, use this: + (setq nnir-swish++-additional-switches '(\"-i\" \"-w\"))" + :type '(repeat (string)) + :group 'nnir) + +(defcustom nnir-swish++-remove-prefix (concat (getenv "HOME") "/Mail/") + "*The prefix to remove from each file name returned by swish++ +in order to get a group name (albeit with / instead of .). This is a +regular expression. + +This variable is very similar to `nnir-glimpse-remove-prefix', except +that it is for swish++, not Glimpse." + :type '(regexp) + :group 'nnir) + +;; Swish-E. Next three variables Copyright (C) 2000 Christoph Conrad +;; . +;; URL: http://sunsite.berkeley.edu/SWISH-E/ +;; New version: http://www.boe.es/swish-e + +(defcustom nnir-swish-e-index-file + (expand-file-name "~/Mail/index.swish-e") + "*Index file for swish-e." + :type '(file) + :group 'nnir) + +(defcustom nnir-swish-e-program "swish-e" + "*Name of swish-e search executable." + :type '(string) + :group 'nnir) + +(defcustom nnir-swish-e-additional-switches '() + "*A list of strings, to be given as additional arguments to swish-e. + +Note that this should be a list. Ie, do NOT use the following: + (setq nnir-swish-e-additional-switches \"-i -w\") ; wrong +Instead, use this: + (setq nnir-swish-e-additional-switches '(\"-i\" \"-w\"))" + :type '(repeat (string)) + :group 'nnir) + +(defcustom nnir-swish-e-remove-prefix (concat (getenv "HOME") "/Mail/") + "*The prefix to remove from each file name returned by swish-e +in order to get a group name (albeit with / instead of .). This is a +regular expression. + +This variable is very similar to `nnir-glimpse-remove-prefix', except +that it is for swish-e, not Glimpse." + :type '(regexp) + :group 'nnir) + +;; Namazu engine, see + +(defcustom nnir-namazu-program "namazu" + "*Name of Namazu search executable." + :type '(string) + :group 'nnir) + +(defcustom nnir-namazu-index-directory (expand-file-name "~/Mail/namazu/") + "*Index directory for Namazu." + :type '(directory) + :group 'nnir) + +(defcustom nnir-namazu-additional-switches '() + "*A list of strings, to be given as additional arguments to namazu. +The switches `-q', `-a', and `-s' are always used, very few other switches +make any sense in this context. + +Note that this should be a list. Ie, do NOT use the following: + (setq nnir-namazu-additional-switches \"-i -w\") ; wrong +Instead, use this: + (setq nnir-namazu-additional-switches '(\"-i\" \"-w\"))" + :type '(repeat (string)) + :group 'nnir) + +(defcustom nnir-namazu-remove-prefix (concat (getenv "HOME") "/Mail/") + "*The prefix to remove from each file name returned by Namazu +in order to get a group name (albeit with / instead of .). + +This variable is very similar to `nnir-glimpse-remove-prefix', except +that it is for Namazu, not Glimpse." + :type '(directory) + :group 'nnir) + +;;; Internal Variables: + +(defvar nnir-current-query nil + "Internal: stores current query (= group name).") + +(defvar nnir-current-server nil + "Internal: stores current server (does it ever change?).") + +(defvar nnir-current-group-marked nil + "Internal: stores current list of process-marked groups.") + +(defvar nnir-artlist nil + "Internal: stores search result.") + +(defvar nnir-tmp-buffer " *nnir*" + "Internal: temporary buffer.") + +;;; Code: + +;; Gnus glue. + +(defun gnus-group-make-nnir-group (extra-parms query) + "Create an nnir group. Asks for query." + (interactive "P\nsQuery: ") + (let ((parms nil)) + (if extra-parms + (setq parms (nnir-read-parms query)) + (setq parms (list (cons 'query query)))) + (gnus-group-read-ephemeral-group + (concat "nnir:" (prin1-to-string parms)) '(nnir "") t + (cons (current-buffer) + gnus-current-window-configuration) + nil))) + +;; Emacs 19 compatibility? +(or (fboundp 'kbd) (defalias 'kbd 'read-kbd-macro)) + +(defun nnir-group-mode-hook () + (define-key gnus-group-mode-map + (if (fboundp 'read-kbd-macro) + (kbd "G G") + "GG") ; XEmacs 19 compat + 'gnus-group-make-nnir-group)) +(add-hook 'gnus-group-mode-hook + (lambda () + (unless (string-match "T-gnus" gnus-version) + (nnir-group-mode-hook)))) + + + +;; Summary mode commands. + +(defun gnus-summary-nnir-goto-thread () + "Only applies to nnir groups. Go to group this article came from +and show thread that contains this article." + (interactive) + (unless (eq 'nnir (car (gnus-find-method-for-group gnus-newsgroup-name))) + (error "Can't execute this command unless in nnir group.")) + (let* ((cur (gnus-summary-article-number)) + (backend-group (nnir-artlist-artitem-group nnir-artlist cur)) + (backend-number (nnir-artlist-artitem-number nnir-artlist cur))) + (gnus-group-read-ephemeral-group + backend-group + nnir-mail-backend + t ; activate + (cons (current-buffer) + 'summary) ; window config + nil + (list backend-number)) + (gnus-summary-limit (list backend-number)) + (gnus-summary-refer-thread))) + +(if (fboundp 'eval-after-load) + (eval-after-load "gnus-sum" + '(define-key gnus-summary-goto-map + "T" 'gnus-summary-nnir-goto-thread)) + (add-hook 'gnus-summary-mode-hook + (function (lambda () + (define-key gnus-summary-goto-map + "T" 'gnus-summary-nnir-goto-thread))))) + + + +;; Gnus backend interface functions. + +(deffoo nnir-open-server (server &optional definitions) + ;; Just set the server variables appropriately. + (nnoo-change-server 'nnir server definitions)) + +(deffoo nnir-request-group (group &optional server fast) + "GROUP is the query string." + (nnir-possibly-change-server server) + ;; Check for cache and return that if appropriate. + (if (and (equal group nnir-current-query) + (equal gnus-group-marked nnir-current-group-marked) + (or (null server) + (equal server nnir-current-server))) + nnir-artlist + ;; Cache miss. + (setq nnir-artlist (nnir-run-query group)) + (save-excursion + (set-buffer nntp-server-buffer) + (if (zerop (length nnir-artlist)) + (progn + (setq nnir-current-query nil + nnir-current-server nil + nnir-current-group-marked nil + nnir-artlist nil) + (nnheader-report 'nnir "Search produced empty results.")) + ;; Remember data for cache. + (setq nnir-current-query group) + (when server (setq nnir-current-server server)) + (setq nnir-current-group-marked gnus-group-marked) + (nnheader-insert "211 %d %d %d %s\n" + (nnir-artlist-length nnir-artlist) ; total # + 1 ; first # + (nnir-artlist-length nnir-artlist) ; last # + group))))) ; group name + +(deffoo nnir-retrieve-headers (articles &optional group server fetch-old) + (save-excursion + (let ((artlist (copy-sequence articles)) + (idx 1) + (art nil) + (artitem nil) + (artgroup nil) (artno nil) + (artrsv nil) + (artfullgroup nil) + (novitem nil) + (novdata nil) + (foo nil)) + (while (not (null artlist)) + (setq art (car artlist)) + (or (numberp art) + (nnheader-report + 'nnir + "nnir-retrieve-headers doesn't grok message ids: %s" + art)) + (setq artitem (nnir-artlist-article nnir-artlist art)) + (setq artrsv (nnir-artitem-rsv artitem)) + (setq artgroup (nnir-artitem-group artitem)) + (setq artno (nnir-artitem-number artitem)) + (setq artfullgroup (nnir-group-full-name artgroup)) + ;; retrieve NOV or HEAD data for this article, transform into + ;; NOV data and prepend to `novdata' + (set-buffer nntp-server-buffer) + (case (setq foo (gnus-retrieve-headers (list artno) artfullgroup nil)) + (nov + (goto-char (point-min)) + (setq novitem (nnheader-parse-nov)) + (unless novitem + (pop-to-buffer nntp-server-buffer) + (error + "nnheader-parse-nov returned nil for article %s in group %s" + artno artfullgroup))) + (headers + (goto-char (point-min)) + (setq novitem (nnheader-parse-head)) + (unless novitem + (pop-to-buffer nntp-server-buffer) + (error + "nnheader-parse-head returned nil for article %s in group %s" + artno artfullgroup))) + (t (nnheader-report 'nnir "Don't support header type %s." foo))) + ;; replace article number in original group with article number + ;; in nnir group + (mail-header-set-number novitem idx) + (mail-header-set-from novitem + (mail-header-from novitem)) + (mail-header-set-subject + novitem + (format "[%d: %s/%d] %s" + artrsv artgroup artno + (mail-header-subject novitem))) + ;;-(mail-header-set-extra novitem nil) + (push novitem novdata) + (setq artlist (cdr artlist)) + (setq idx (1+ idx))) + (setq novdata (nreverse novdata)) + (set-buffer nntp-server-buffer) (erase-buffer) + (mapcar 'nnheader-insert-nov novdata) + 'nov))) + +(deffoo nnir-request-article (article + &optional group server to-buffer) + (save-excursion + (let* ((artitem (nnir-artlist-article nnir-artlist + article)) + (artgroup (nnir-artitem-group artitem)) + (artno (nnir-artitem-number artitem)) + ;; Bug? + ;; Why must we bind nntp-server-buffer here? It won't + ;; work if `buf' is used, say. (Of course, the set-buffer + ;; line below must then be updated, too.) + (nntp-server-buffer (or to-buffer nntp-server-buffer))) + (set-buffer nntp-server-buffer) + (erase-buffer) + (message "Requesting article %d from group %s" + artno + (nnir-group-full-name artgroup)) + (gnus-request-article artno (nnir-group-full-name artgroup) + nntp-server-buffer) + (cons artgroup artno)))) + + +(nnoo-define-skeleton nnir) + +;;; Search Engine Interfaces: + +;; Glimpse interface. +(defun nnir-run-glimpse (query &optional group) + "Run given query against glimpse. Returns a vector of (group name, file name) +pairs (also vectors, actually)." + (save-excursion + (let ((artlist nil) + (groupspec (cdr (assq 'group query))) + (qstring (cdr (assq 'query query)))) + (when (and group groupspec) + (error (concat "It does not make sense to use a group spec" + " with process-marked groups."))) + (when group + (setq groupspec (gnus-group-real-name group))) + (set-buffer (get-buffer-create nnir-tmp-buffer)) + (erase-buffer) + (if groupspec + (message "Doing glimpse query %s on %s..." query groupspec) + (message "Doing glimpse query %s..." query)) + (let* ((cp-list + `( ,nnir-glimpse-program + nil ; input from /dev/null + t ; output + nil ; don't redisplay + "-H" ,nnir-glimpse-home ; search home dir + "-W" ; match pattern in file + "-l" "-y" ; misc options + ,@nnir-glimpse-additional-switches + "-F" ,nnir-glimpse-remove-prefix ; restrict output to mail + ,qstring ; the query, in glimpse format + )) + (exitstatus + (progn + (message "%s args: %s" nnir-glimpse-program + (mapconcat 'identity (cddddr cp-list) " ")) + (apply 'call-process cp-list)))) + (unless (or (null exitstatus) + (zerop exitstatus)) + (nnheader-report 'nnir "Couldn't run glimpse: %s" exitstatus) + ;; Glimpse failure reason is in this buffer, show it if + ;; the user wants it. + (when (> gnus-verbose 6) + (display-buffer nnir-tmp-buffer)))) + (when groupspec + (keep-lines groupspec)) + (if groupspec + (message "Doing glimpse query %s on %s...done" query groupspec) + (message "Doing glimpse query %s...done" query)) + (sit-for 0) + ;; CCC: The following work of extracting group name and article + ;; number from the Glimpse output can probably better be done by + ;; just going through the buffer once, and plucking out the + ;; right information from each line. + ;; remove superfluous stuff from glimpse output + (goto-char (point-min)) + (delete-non-matching-lines "/[0-9]+$") + ;;(delete-matching-lines "\\.overview~?$") + (goto-char (point-min)) + (while (re-search-forward (concat "^" nnir-glimpse-remove-prefix) nil t) + (replace-match "")) + ;; separate group name from article number with \t + ;; XEmacs compatible version + (goto-char (point-max)) + (while (re-search-backward "/[0-9]+$" nil t) + (delete-char 1 nil) + (insert-char ?\t 1)) +; Emacs compatible version +; (goto-char (point-min)) +; (while (re-search-forward "\\(/\\)[0-9]+$" nil t) +; (replace-match "\t" t t nil 1)) + ;; replace / with . in group names + (subst-char-in-region (point-min) (point-max) ?/ ?. t) + ;; massage buffer to contain some Lisp; + ;; this depends on the artlist encoding internals + ;; maybe this dependency should be removed? + (goto-char (point-min)) + (while (not (eobp)) + (insert "[\"") + (skip-chars-forward "^\t") + (insert "\" ") + (end-of-line) + (insert " 1000 ]") ; 1000 = score + (forward-line 1)) + (insert "])\n") + (goto-char (point-min)) + (insert "(setq artlist [\n") + (eval-buffer) + (sort* artlist + (function (lambda (x y) + (if (string-lessp (nnir-artitem-group x) + (nnir-artitem-group y)) + t + (< (nnir-artitem-number x) + (nnir-artitem-number y)))))) + ))) + +;; freeWAIS-sf interface. +(defun nnir-run-waissearch (query &optional group) + "Run given query agains waissearch. Returns vector of (group name, file name) +pairs (also vectors, actually)." + (when group + (error "The freeWAIS-sf backend cannot search specific groups.")) + (save-excursion + (let ((qstring (cdr (assq 'query query))) + (artlist nil) + (score nil) (artno nil) (dirnam nil) (group nil)) + (set-buffer (get-buffer-create nnir-tmp-buffer)) + (erase-buffer) + (message "Doing WAIS query %s..." query) + (call-process nnir-wais-program + nil ; input from /dev/null + t ; output to current buffer + nil ; don't redisplay + "-d" nnir-wais-database ; database to search + qstring) + (message "Massaging waissearch output...") + ;; remove superfluous lines + (keep-lines "Score:") + ;; extract data from result lines + (goto-char (point-min)) + (while (re-search-forward + "Score: +\\([0-9]+\\).*'\\([0-9]+\\) +\\([^']+\\)/'" nil t) + (setq score (match-string 1) + artno (match-string 2) + dirnam (match-string 3)) + (unless (string-match nnir-wais-remove-prefix dirnam) + (nnheader-report 'nnir "Dir name %s doesn't contain prefix %s" + dirnam nnir-wais-remove-prefix)) + (setq group (substitute ?. ?/ (replace-match "" t t dirnam))) + (push (vector group + (string-to-int artno) + (string-to-int score)) + artlist)) + (message "Massaging waissearch output...done") + (apply 'vector + (sort* artlist + (function (lambda (x y) + (> (nnir-artitem-rsv x) + (nnir-artitem-rsv y))))))))) + +;; EWS (Excite for Web Servers) interface +(defun nnir-run-excite-search (query &optional group) + "Run a given query against EWS. Returns vector of (group name, file name) +pairs (also vectors, actually)." + (when group + (error "Searching specific groups not implemented for EWS.")) + (save-excursion + (let ((qstring (cdr (assq 'query query))) + artlist group article-num article) + (setq nnir-current-query query) + (set-buffer (get-buffer-create nnir-tmp-buffer)) + (erase-buffer) + (message "Doing EWS query %s..." qstring) + (call-process nnir-excite-aquery-program + nil ; input from /dev/null + t ; output to current buffer + nil ; don't redisplay + nnir-excite-collection + (if (string= (substring qstring 0 1) "(") + qstring + (format "(concept %s)" qstring))) + (message "Gathering query output...") + + (goto-char (point-min)) + (while (re-search-forward + "^[0-9]+\\s-[0-9]+\\s-[0-9]+\\s-\\(\\S-*\\)" nil t) + (setq article (match-string 1)) + (unless (string-match + (concat "^" (regexp-quote nnir-excite-remove-prefix) + "\\(.*\\)/\\([0-9]+\\)") article) + (nnheader-report 'nnir "Dir name %s doesn't contain prefix %s" + article nnir-excite-remove-prefix)) + (setq group (substitute ?. ?/ (match-string 1 article))) + (setq article-num (match-string 2 article)) + (setq artlist (vconcat artlist (vector (vector group + (string-to-int article-num) + 1000))))) + (message "Gathering query output...done") + artlist))) + +;; IMAP interface. The following function is Copyright (C) 1998 Simon +;; Josefsson . +;; todo: +;; nnir invokes this two (2) times???! +;; we should not use nnimap at all but open our own server connection +;; we should not LIST * but use nnimap-list-pattern from defs +;; send queries as literals +;; handle errors + +(eval-when-compile + (defvar nnimap-server-buffer)) + +(defun nnir-run-imap (query &optional group) + (require 'imap) + (require 'nnimap) + (require 'mm-util) + (unless group + (error "Must specify groups for IMAP searching.")) + (save-excursion + (let ((qstring (cdr (assq 'query query))) + (server (cadr nnir-mail-backend)) + (defs (caddr nnir-mail-backend)) + artlist buf) + (message "Opening server %s" server) + (condition-case () + (when (nnimap-open-server server defs) ;; xxx + (setq buf nnimap-server-buffer) ;; xxx + (message "Searching %s..." group) + (let ((arts 0) + (mbx (gnus-group-real-name group)) + (multibyte-p (mm-multibyte-p)) + charset coding-system) + (when (imap-mailbox-select mbx nil buf) + (with-temp-buffer + (if multibyte-p + (mm-enable-multibyte)) + (insert qstring) + (setq charset (car (mm-find-mime-charset-region + (point-min)(point-max))))) + (unless charset + (setq charset nnir-imap-default-charset)) + (mapcar + (lambda (artnum) + (push (vector mbx artnum 1) artlist) + (setq arts (1+ arts))) + (if (and (not (eq charset 'us-ascii)) + (setq coding-system (mm-charset-to-coding-system + charset))) + (imap-search + (concat "CHARSET " (symbol-name charset) " TEXT \"" + (mm-encode-coding-string qstring coding-system) + "\"") buf) + (imap-search (concat "TEXT \"" qstring "\"") buf))) + (message "Searching %s... %d matches" mbx arts))) + (message "Searching %s...done" group)) + (quit nil)) + (reverse artlist)))) + +;; Swish++ interface. The following function is Copyright (C) 2000, +;; 2001 Christoph Conrad . +;; -cc- Todo +;; Search by +;; - group +;; Sort by +;; - rank (default) +;; - article number +;; - file size +;; - group +(defun nnir-run-swish++ (query &optional group) + "Run given query against swish++. +Returns a vector of (group name, file name) pairs (also vectors, +actually). + +Tested with swish++ 4.7 on GNU/Linux and with with swish++ 5.0b2 on +Windows NT 4.0." + + (when group + (error "The swish++ backend cannot search specific groups.")) + + (save-excursion + (let ( (qstring (cdr (assq 'query query))) + (groupspec (cdr (assq 'group query))) + (artlist nil) + (score nil) (artno nil) (dirnam nil) (group nil) ) + + (when (equal "" qstring) + (error "swish++: You didn't enter anything.")) + + (set-buffer (get-buffer-create nnir-tmp-buffer)) + (erase-buffer) + + (if groupspec + (message "Doing swish++ query %s on %s..." qstring groupspec) + (message "Doing swish++ query %s..." qstring)) + + (let* ((cp-list `( ,nnir-swish++-program + nil ; input from /dev/null + t ; output + nil ; don't redisplay + "--config-file" ,nnir-swish++-configuration-file + ,@nnir-swish++-additional-switches + ,qstring ; the query, in swish++ format + )) + (exitstatus + (progn + (message "%s args: %s" nnir-swish++-program + (mapconcat 'identity (cddddr cp-list) " "));; ??? + (apply 'call-process cp-list)))) + (unless (or (null exitstatus) + (zerop exitstatus)) + (nnheader-report 'nnir "Couldn't run swish++: %s" exitstatus) + ;; swish++ failure reason is in this buffer, show it if + ;; the user wants it. + (when (> gnus-verbose 6) + (display-buffer nnir-tmp-buffer)))) + + ;; The results are output in the format of: + ;; V 4.7 Linux + ;; rank relative-path-name file-size file-title + ;; V 5.0b2: + ;; rank relative-path-name file-size topic?? + ;; where rank is an integer from 1 to 100. + (goto-char (point-min)) + (while (re-search-forward + "\\(^[0-9]+\\) \\([^ ]+\\) [0-9]+ \\(.*\\)$" nil t) + (setq score (match-string 1) + artno (file-name-nondirectory (match-string 2)) + dirnam (file-name-directory (match-string 2))) + + ;; don't match directories + (when (string-match "^[0-9]+$" artno) + (when (not (null dirnam)) + + ; maybe limit results to matching groups. + (when (or (not groupspec) + (string-match groupspec dirnam)) + + ;; remove nnir-swish++-remove-prefix from beginning of dirname + (when (string-match (concat "^" nnir-swish++-remove-prefix) + dirnam) + (setq dirnam (replace-match "" t t dirnam))) + + (setq dirnam (substring dirnam 0 -1)) + ;; eliminate all ".", "/", "\" from beginning. Always matches. + (string-match "^[./\\]*\\(.*\\)$" dirnam) + ;; "/" -> "." + (setq group (substitute ?. ?/ (match-string 1 dirnam))) + ;; "\\" -> "." + (setq group (substitute ?. ?\\ group)) + + (push (vector group + (string-to-int artno) + (string-to-int score)) + artlist))))) + + (message "Massaging swish++ output...done") + + ;; Sort by score + (apply 'vector + (sort* artlist + (function (lambda (x y) + (> (nnir-artitem-rsv x) + (nnir-artitem-rsv y))))))))) + +;; Swish-E interface. The following function is Copyright (C) 2000, +;; 2001 by Christoph Conrad . +(defun nnir-run-swish-e (query &optional group) + "Run given query against swish-e. +Returns a vector of (group name, file name) pairs (also vectors, +actually). + +Tested with swish-e-2.0.1 on Windows NT 4.0." + + ;; swish-e crashes with empty parameter to "-w" on commandline... + (when group + (error "The swish-e backend cannot search specific groups.")) + + (save-excursion + (let ( (qstring (cdr (assq 'query query))) + (artlist nil) + (score nil) (artno nil) (dirnam nil) (group nil) ) + + (when (equal "" qstring) + (error "swish-e: You didn't enter anything.")) + + (set-buffer (get-buffer-create nnir-tmp-buffer)) + (erase-buffer) + + (message "Doing swish-e query %s..." query) + (let* ((cp-list `( ,nnir-swish-e-program + nil ; input from /dev/null + t ; output + nil ; don't redisplay + "-f" ,nnir-swish-e-index-file + ,@nnir-swish-e-additional-switches + "-w" + ,qstring ; the query, in swish-e format + )) + (exitstatus + (progn + (message "%s args: %s" nnir-swish-e-program + (mapconcat 'identity (cddddr cp-list) " ")) + (apply 'call-process cp-list)))) + (unless (or (null exitstatus) + (zerop exitstatus)) + (nnheader-report 'nnir "Couldn't run swish-e: %s" exitstatus) + ;; swish-e failure reason is in this buffer, show it if + ;; the user wants it. + (when (> gnus-verbose 6) + (display-buffer nnir-tmp-buffer)))) + + ;; The results are output in the format of: + ;; rank path-name file-title file-size + (goto-char (point-min)) + (while (re-search-forward + "\\(^[0-9]+\\) \\([^ ]+\\) \"\\([^\"]+\\)\" [0-9]+$" nil t) + (setq score (match-string 1) + artno (match-string 3) + dirnam (file-name-directory (match-string 2))) + + ;; don't match directories + (when (string-match "^[0-9]+$" artno) + (when (not (null dirnam)) + + ;; remove nnir-swish-e-remove-prefix from beginning of dirname + (when (string-match (concat "^" nnir-swish-e-remove-prefix) + dirnam) + (setq dirnam (replace-match "" t t dirnam))) + + (setq dirnam (substring dirnam 0 -1)) + ;; eliminate all ".", "/", "\" from beginning. Always matches. + (string-match "^[./\\]*\\(.*\\)$" dirnam) + ;; "/" -> "." + (setq group (substitute ?. ?/ (match-string 1 dirnam))) + ;; Windows "\\" -> "." + (setq group (substitute ?. ?\\ group)) + + (push (vector group + (string-to-int artno) + (string-to-int score)) + artlist)))) + + (message "Massaging swish-e output...done") + + ;; Sort by score + (apply 'vector + (sort* artlist + (function (lambda (x y) + (> (nnir-artitem-rsv x) + (nnir-artitem-rsv y))))))))) + +;; Namazu interface +(defun nnir-run-namazu (query &optional group) + "Run given query against Namazu. Returns a vector of (group name, file name) +pairs (also vectors, actually). + +Tested with Namazu 2.0.6 on a GNU/Linux system." + (when group + (error "The Namazu backend cannot search specific groups")) + (save-excursion + (let ( + (artlist nil) + (qstring (cdr (assq 'query query))) + (score nil) + (group nil) + (article nil) + ) + (set-buffer (get-buffer-create nnir-tmp-buffer)) + (erase-buffer) + (let* ((cp-list + `( ,nnir-namazu-program + nil ; input from /dev/null + t ; output + nil ; don't redisplay + "-q" ; don't be verbose + "-a" ; show all matches + "-s" ; use short format + ,@nnir-namazu-additional-switches + ,qstring ; the query, in namazu format + ,nnir-namazu-index-directory ; index directory + )) + (exitstatus + (let ((process-environment (copy-sequence process-environment))) + ;; Disable locale. + (setenv "LC_ALL" "C") + (message "%s args: %s" nnir-namazu-program + (mapconcat 'identity (cddddr cp-list) " ")) + (apply 'call-process cp-list)))) + (unless (or (null exitstatus) + (zerop exitstatus)) + (nnheader-report 'nnir "Couldn't run namazu: %s" exitstatus) + ;; Namazu failure reason is in this buffer, show it if + ;; the user wants it. + (when (> gnus-verbose 6) + (display-buffer nnir-tmp-buffer)))) + + ;; Namazu output looks something like this: + ;; 2. Re: Gnus agent expire broken (score: 55) + ;; /home/henrik/Mail/mail/sent/1310 (4,138 bytes) + + (goto-char (point-min)) + (while (re-search-forward + "^\\([0-9]+\\.\\).*\\((score: \\([0-9]+\\)\\))\n\\([^ ]+\\)" + nil t) + (setq score (match-string 3) + group (file-name-directory (match-string 4)) + article (file-name-nondirectory (match-string 4))) + + ;; make sure article and group is sane + (when (and (string-match "^[0-9]+$" article) + (not (null group))) + (when (string-match (concat "^" nnir-namazu-remove-prefix) group) + (setq group (replace-match "" t t group))) + + ;; remove trailing slash from groupname + (setq group (substring group 0 -1)) + + ;; stuff results into artlist vector + (push (vector (substitute ?. ?/ group) + (string-to-int article) + (string-to-int score)) artlist))) + + ;; sort artlist by score + (apply 'vector + (sort* artlist + (function (lambda (x y) + (> (nnir-artitem-rsv x) + (nnir-artitem-rsv y))))))))) + +;;; Util Code: + +(defun nnir-read-parms (query) + "Reads additional search parameters according to `nnir-engines'." + (let ((parmspec (caddr (assoc nnir-search-engine nnir-engines)))) + (cons (cons 'query query) + (mapcar 'nnir-read-parm parmspec)))) + +(defun nnir-read-parm (parmspec) + "Reads a single search parameter. +`parmspec' is a cons cell, the car is a symbol, the cdr is a prompt." + (let ((sym (car parmspec)) + (prompt (cdr parmspec))) + (cons sym (read-string prompt)))) + +(defun nnir-run-query (query) + "Invoke appropriate search engine function (see `nnir-engines'). +If some groups were process-marked, run the query for each of the groups +and concat the results." + (let ((search-func (cadr (assoc nnir-search-engine nnir-engines))) + (q (car (read-from-string query)))) + (if gnus-group-marked + (apply 'append + (mapcar (lambda (x) + (funcall search-func q x)) + gnus-group-marked)) + (funcall search-func q nil)))) + +(defun nnir-group-full-name (shortname) + "For the given group name, return a full Gnus group name. +The Gnus backend/server information is added." + (gnus-group-prefixed-name shortname nnir-mail-backend)) + +(defun nnir-possibly-change-server (server) + (unless (and server (nnir-server-opened server)) + (nnir-open-server server))) + + +;; Data type article list. + +(defun nnir-artlist-length (artlist) + "Returns number of articles in artlist." + (length artlist)) + +(defun nnir-artlist-article (artlist n) + "Returns from ARTLIST the Nth artitem (counting starting at 1)." + (elt artlist (1- n))) + +(defun nnir-artitem-group (artitem) + "Returns the group from the ARTITEM." + (elt artitem 0)) + +(defun nnir-artlist-artitem-group (artlist n) + "Returns from ARTLIST the group of the Nth artitem (counting from 1)." + (nnir-artitem-group (nnir-artlist-article artlist n))) + +(defun nnir-artitem-number (artitem) + "Returns the number from the ARTITEM." + (elt artitem 1)) + +(defun nnir-artlist-artitem-number (artlist n) + "Returns from ARTLIST the number of the Nth artitem (counting from 1)." + (nnir-artitem-number (nnir-artlist-article artlist n))) + +(defun nnir-artitem-rsv (artitem) + "Returns the Retrieval Status Value (RSV, score) from the ARTITEM." + (elt artitem 2)) + +(defun nnir-artlist-artitem-rsv (artlist n) + "Returns from ARTLIST the Retrieval Status Value of the Nth artitem +(counting from 1)." + (nnir-artitem-rsv (nnir-artlist-article artlist n))) + +;; unused? +(defun nnir-artlist-groups (artlist) + "Returns a list of all groups in the given ARTLIST." + (let ((res nil) + (with-dups nil)) + ;; from each artitem, extract group component + (setq with-dups (mapcar 'nnir-artitem-group artlist)) + ;; remove duplicates from above + (mapcar (function (lambda (x) (add-to-list 'res x))) + with-dups) + res)) + + +;; The end. +(provide 'nnir) diff --git a/lisp/nnmaildir.el b/lisp/nnmaildir.el new file mode 100644 index 0000000..65bb5bd --- /dev/null +++ b/lisp/nnmaildir.el @@ -0,0 +1,1636 @@ +;;; nnmaildir.el --- maildir backend for Gnus +;; Public domain. + +;; Author: Paul Jarc + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; Maildir format is documented at +;; and in the maildir(5) man page from qmail (available at +;; ). nnmaildir also stores +;; extra information in the .nnmaildir/ directory within a maildir. +;; +;; Some goals of nnmaildir: +;; * Everything Just Works, and correctly. E.g., NOV data is automatically +;; regenerated when stale; no need for manually running +;; *-generate-nov-databases. +;; * Perfect reliability: [C-g] will never corrupt its data in memory, and +;; SIGKILL will never corrupt its data in the filesystem. +;; * Allow concurrent operation as much as possible. If files change out +;; from under us, adapt to the changes or degrade gracefully. +;; * We use the filesystem as a database, so that, e.g., it's easy to +;; manipulate marks from outside Gnus. +;; * All information about a group is stored in the maildir, for easy backup, +;; copying, restoring, etc. +;; +;; Todo: +;; * Add a hook for when moving messages from new/ to cur/, to support +;; nnmail's duplicate detection. +;; * Improve generated Xrefs, so crossposts are detectable. +;; * Improve code readability. + +;;; Code: + +;; eval this before editing +[(progn + (put 'nnmaildir--with-nntp-buffer 'lisp-indent-function 0) + (put 'nnmaildir--with-work-buffer 'lisp-indent-function 0) + (put 'nnmaildir--with-nov-buffer 'lisp-indent-function 0) + (put 'nnmaildir--with-move-buffer 'lisp-indent-function 0) + ) +] + +(eval-and-compile + (require 'nnheader) + (require 'gnus) + (require 'gnus-util) + (require 'gnus-range) + (require 'gnus-start) + (require 'gnus-int) + (require 'message)) +(eval-when-compile + (require 'cl) + (require 'nnmail)) + +(defconst nnmaildir-version "Gnus") + +(defvar nnmaildir-article-file-name nil + "*The filename of the most recently requested article. This variable is set +by nnmaildir-request-article.") + +;; The filename of the article being moved/copied: +(defvar nnmaildir--file nil) + +;; Variables to generate filenames of messages being delivered: +(defvar nnmaildir--delivery-time "") +(defconst nnmaildir--delivery-pid (concat "P" (number-to-string (emacs-pid)))) +(defvar nnmaildir--delivery-count nil) + +;; An obarry containing symbols whose names are server names and whose values +;; are servers: +(defvar nnmaildir--servers (make-vector 3 0)) +;; The current server: +(defvar nnmaildir--cur-server nil) + +;; A copy of nnmail-extra-headers +(defvar nnmaildir--extra nil) + +;; A NOV structure looks like this (must be prin1-able, so no defstruct): +["subject\tfrom\tdate" + "references\tchars\lines" + "To: you\tIn-Reply-To: " + (12345 67890) ;; modtime of the corresponding article file + (to in-reply-to)] ;; contemporary value of nnmail-extra-headers +(defconst nnmaildir--novlen 5) +(defmacro nnmaildir--nov-new (beg mid end mtime extra) + `(vector ,beg ,mid ,end ,mtime ,extra)) +(defmacro nnmaildir--nov-get-beg (nov) `(aref ,nov 0)) +(defmacro nnmaildir--nov-get-mid (nov) `(aref ,nov 1)) +(defmacro nnmaildir--nov-get-end (nov) `(aref ,nov 2)) +(defmacro nnmaildir--nov-get-mtime (nov) `(aref ,nov 3)) +(defmacro nnmaildir--nov-get-extra (nov) `(aref ,nov 4)) +(defmacro nnmaildir--nov-set-beg (nov value) `(aset ,nov 0 ,value)) +(defmacro nnmaildir--nov-set-mid (nov value) `(aset ,nov 1 ,value)) +(defmacro nnmaildir--nov-set-end (nov value) `(aset ,nov 2 ,value)) +(defmacro nnmaildir--nov-set-mtime (nov value) `(aset ,nov 3 ,value)) +(defmacro nnmaildir--nov-set-extra (nov value) `(aset ,nov 4 ,value)) + +(defstruct nnmaildir--art + (prefix nil :type string) ;; "time.pid.host" + (suffix nil :type string) ;; ":2,flags" + (num nil :type natnum) ;; article number + (msgid nil :type string) ;; "" + (nov nil :type vector)) ;; cached nov structure, or nil + +(defstruct nnmaildir--grp + (name nil :type string) ;; "group.name" + (new nil :type list) ;; new/ modtime + (cur nil :type list) ;; cur/ modtime + (min 1 :type natnum) ;; minimum article number + (count 0 :type natnum) ;; count of articles + (nlist nil :type list) ;; list of articles, ordered descending by number + (flist nil :type vector) ;; obarray mapping filename prefix->article + (mlist nil :type vector) ;; obarray mapping message-id->article + (cache nil :type vector) ;; nov cache + (index nil :type natnum) ;; index of next cache entry to replace + (mmth nil :type vector)) ;; obarray mapping mark name->dir modtime + ; ("Mark Mod Time Hash") + +(defstruct nnmaildir--srv + (address nil :type string) ;; server address string + (method nil :type list) ;; (nnmaildir "address" ...) + (prefix nil :type string) ;; "nnmaildir+address:" + (dir nil :type string) ;; "/expanded/path/to/server/dir/" + (ls nil :type function) ;; directory-files function + (groups nil :type vector) ;; obarray mapping group name->group + (curgrp nil :type nnmaildir--grp) ;; current group, or nil + (error nil :type string) ;; last error message, or nil + (mtime nil :type list) ;; modtime of dir + (gnm nil) ;; flag: split from mail-sources? + (target-prefix nil :type string)) ;; symlink target prefix + +(defun nnmaildir--expired-article (group article) + (setf (nnmaildir--art-nov article) nil) + (let ((flist (nnmaildir--grp-flist group)) + (mlist (nnmaildir--grp-mlist group)) + (min (nnmaildir--grp-min group)) + (count (1- (nnmaildir--grp-count group))) + (prefix (nnmaildir--art-prefix article)) + (msgid (nnmaildir--art-msgid article)) + (new-nlist nil) + (nlist-pre '(nil . nil)) + nlist-post num) + (unless (zerop count) + (setq nlist-post (nnmaildir--grp-nlist group) + num (nnmaildir--art-num article)) + (if (eq num (caar nlist-post)) + (setq new-nlist (cdr nlist-post)) + (setq new-nlist nlist-post + nlist-pre nlist-post + nlist-post (cdr nlist-post)) + (while (/= num (caar nlist-post)) + (setq nlist-pre nlist-post + nlist-post (cdr nlist-post))) + (setq nlist-post (cdr nlist-post)) + (if (eq num min) + (setq min (caar nlist-pre))))) + (let ((inhibit-quit t)) + (setf (nnmaildir--grp-min group) min) + (setf (nnmaildir--grp-count group) count) + (setf (nnmaildir--grp-nlist group) new-nlist) + (setcdr nlist-pre nlist-post) + (unintern prefix flist) + (unintern msgid mlist)))) + +(defun nnmaildir--nlist-art (group num) + (let ((entry (assq num (nnmaildir--grp-nlist group)))) + (if entry + (cdr entry)))) +(defmacro nnmaildir--flist-art (list file) + `(symbol-value (intern-soft ,file ,list))) +(defmacro nnmaildir--mlist-art (list msgid) + `(symbol-value (intern-soft ,msgid ,list))) + +(defun nnmaildir--pgname (server gname) + (let ((prefix (nnmaildir--srv-prefix server))) + (if prefix (concat prefix gname) + (setq gname (gnus-group-prefixed-name gname + (nnmaildir--srv-method server))) + (setf (nnmaildir--srv-prefix server) (gnus-group-real-prefix gname)) + gname))) + +(defun nnmaildir--param (pgname param) + (setq param (gnus-group-find-parameter pgname param 'allow-list)) + (if (vectorp param) (setq param (aref param 0))) + (eval param)) + +(defmacro nnmaildir--with-nntp-buffer (&rest body) + `(save-excursion + (set-buffer nntp-server-buffer) + ,@body)) +(defmacro nnmaildir--with-work-buffer (&rest body) + `(save-excursion + (set-buffer (get-buffer-create " *nnmaildir work*")) + ,@body)) +(defmacro nnmaildir--with-nov-buffer (&rest body) + `(save-excursion + (set-buffer (get-buffer-create " *nnmaildir nov*")) + ,@body)) +(defmacro nnmaildir--with-move-buffer (&rest body) + `(save-excursion + (set-buffer (get-buffer-create " *nnmaildir move*")) + ,@body)) + +(defmacro nnmaildir--subdir (dir subdir) + `(file-name-as-directory (concat ,dir ,subdir))) +(defmacro nnmaildir--srvgrp-dir (srv-dir gname) + `(nnmaildir--subdir ,srv-dir ,gname)) +(defmacro nnmaildir--tmp (dir) `(nnmaildir--subdir ,dir "tmp")) +(defmacro nnmaildir--new (dir) `(nnmaildir--subdir ,dir "new")) +(defmacro nnmaildir--cur (dir) `(nnmaildir--subdir ,dir "cur")) +(defmacro nnmaildir--nndir (dir) `(nnmaildir--subdir ,dir ".nnmaildir")) +(defmacro nnmaildir--nov-dir (dir) `(nnmaildir--subdir ,dir "nov")) +(defmacro nnmaildir--marks-dir (dir) `(nnmaildir--subdir ,dir "marks")) +(defmacro nnmaildir--num-dir (dir) `(nnmaildir--subdir ,dir "num")) +(defmacro nnmaildir--num-file (dir) `(concat ,dir ":")) + +(defmacro nnmaildir--unlink (file-arg) + `(let ((file ,file-arg)) + (if (file-attributes file) (delete-file file)))) +(defun nnmaildir--mkdir (dir) + (or (file-exists-p (file-name-as-directory dir)) + (make-directory-internal (directory-file-name dir)))) +(defun nnmaildir--delete-dir-files (dir ls) + (when (file-attributes dir) + (mapcar 'delete-file (funcall ls dir 'full "\\`[^.]" 'nosort)) + (delete-directory dir))) + +(defun nnmaildir--group-maxnum (server group) + (if (zerop (nnmaildir--grp-count group)) 0 + (let ((x (nnmaildir--srvgrp-dir (nnmaildir--srv-dir server) + (nnmaildir--grp-name group)))) + (setq x (nnmaildir--nndir x) + x (nnmaildir--num-dir x) + x (nnmaildir--num-file x) + x (file-attributes x)) + (if x (1- (nth 1 x)) 0)))) + +;; Make the given server, if non-nil, be the current server. Then make the +;; given group, if non-nil, be the current group of the current server. Then +;; return the group object for the current group. +(defun nnmaildir--prepare (server group) + (let (x groups) + (catch 'return + (if (null server) + (unless (setq server nnmaildir--cur-server) + (throw 'return nil)) + (unless (setq server (intern-soft server nnmaildir--servers)) + (throw 'return nil)) + (setq server (symbol-value server) + nnmaildir--cur-server server)) + (unless (setq groups (nnmaildir--srv-groups server)) + (throw 'return nil)) + (unless (nnmaildir--srv-method server) + (setq x (concat "nnmaildir:" (nnmaildir--srv-address server)) + x (gnus-server-to-method x)) + (unless x (throw 'return nil)) + (setf (nnmaildir--srv-method server) x)) + (if (null group) + (unless (setq group (nnmaildir--srv-curgrp server)) + (throw 'return nil)) + (unless (setq group (intern-soft group groups)) + (throw 'return nil)) + (setq group (symbol-value group))) + group))) + +(defun nnmaildir--tab-to-space (string) + (let ((pos 0)) + (while (string-match "\t" string pos) + (aset string (match-beginning 0) ? ) + (setq pos (match-end 0)))) + string) + +(defun nnmaildir--update-nov (server group article) + (let ((nnheader-file-coding-system 'binary) + (srv-dir (nnmaildir--srv-dir server)) + (storage-version 1) ;; [version article-number msgid [...nov...]] + dir gname pgname msgdir prefix suffix file attr mtime novdir novfile + nov msgid nov-beg nov-mid nov-end field val old-extra num numdir + deactivate-mark) + (catch 'return + (setq gname (nnmaildir--grp-name group) + pgname (nnmaildir--pgname server gname) + dir (nnmaildir--srvgrp-dir srv-dir gname) + msgdir (if (nnmaildir--param pgname 'read-only) + (nnmaildir--new dir) (nnmaildir--cur dir)) + prefix (nnmaildir--art-prefix article) + suffix (nnmaildir--art-suffix article) + file (concat msgdir prefix suffix) + attr (file-attributes file)) + (unless attr + (nnmaildir--expired-article group article) + (throw 'return nil)) + (setq mtime (nth 5 attr) + attr (nth 7 attr) + nov (nnmaildir--art-nov article) + dir (nnmaildir--nndir dir) + novdir (nnmaildir--nov-dir dir) + novfile (concat novdir prefix)) + (unless (equal nnmaildir--extra nnmail-extra-headers) + (setq nnmaildir--extra (copy-sequence nnmail-extra-headers))) + (nnmaildir--with-nov-buffer + ;; First we'll check for already-parsed NOV data. + (cond ((not (file-exists-p novfile)) + ;; The NOV file doesn't exist; we have to parse the message. + (setq nov nil)) + ((not nov) + ;; The file exists, but the data isn't in memory; read the file. + (erase-buffer) + (nnheader-insert-file-contents novfile) + (setq nov (read (current-buffer))) + (if (not (and (vectorp nov) + (/= 0 (length nov)) + (equal storage-version (aref nov 0)))) + ;; This NOV data seems to be in the wrong format. + (setq nov nil) + (unless (nnmaildir--art-num article) + (setf (nnmaildir--art-num article) (aref nov 1))) + (unless (nnmaildir--art-msgid article) + (setf (nnmaildir--art-msgid article) (aref nov 2))) + (setq nov (aref nov 3))))) + ;; Now check whether the already-parsed data (if we have any) is + ;; usable: if the message has been edited or if nnmail-extra-headers + ;; has been augmented since this data was parsed from the message, + ;; then we have to reparse. Otherwise it's up-to-date. + (when (and nov (equal mtime (nnmaildir--nov-get-mtime nov))) + ;; The timestamp matches. Now check nnmail-extra-headers. + (setq old-extra (nnmaildir--nov-get-extra nov)) + (when (equal nnmaildir--extra old-extra) ;; common case + ;; Save memory; use a single copy of the list value. + (nnmaildir--nov-set-extra nov nnmaildir--extra) + (throw 'return nov)) + ;; They're not equal, but maybe the new is a subset of the old. + (if (null nnmaildir--extra) + ;; The empty set is a subset of every set. + (throw 'return nov)) + (if (not (memq nil (mapcar (lambda (e) (memq e old-extra)) + nnmaildir--extra))) + (throw 'return nov))) + ;; Parse the NOV data out of the message. + (erase-buffer) + (nnheader-insert-file-contents file) + (insert "\n") + (goto-char (point-min)) + (save-restriction + (if (search-forward "\n\n" nil 'noerror) + (progn + (setq nov-mid (count-lines (point) (point-max))) + (narrow-to-region (point-min) (1- (point)))) + (setq nov-mid 0)) + (goto-char (point-min)) + (delete-char 1) + (setq nov (nnheader-parse-naked-head) + field (or (mail-header-lines nov) 0))) + (unless (or (zerop field) (nnmaildir--param pgname 'distrust-Lines:)) + (setq nov-mid field)) + (setq nov-mid (number-to-string nov-mid) + nov-mid (concat (number-to-string attr) "\t" nov-mid)) + (save-match-data + (setq field (or (mail-header-references nov) "")) + (nnmaildir--tab-to-space field) + (setq nov-mid (concat field "\t" nov-mid) + nov-beg (mapconcat + (lambda (f) (nnmaildir--tab-to-space (or f ""))) + (list (mail-header-subject nov) + (mail-header-from nov) + (mail-header-date nov)) "\t") + nov-end (mapconcat + (lambda (extra) + (setq field (symbol-name (car extra)) + val (cdr extra)) + (nnmaildir--tab-to-space field) + (nnmaildir--tab-to-space val) + (concat field ": " val)) + (mail-header-extra nov) "\t"))) + (setq msgid (mail-header-id nov)) + (if (or (null msgid) (nnheader-fake-message-id-p msgid)) + (setq msgid (concat "<" prefix "@nnmaildir>"))) + (nnmaildir--tab-to-space msgid) + ;; The data is parsed; create an nnmaildir NOV structure. + (setq nov (nnmaildir--nov-new nov-beg nov-mid nov-end mtime + nnmaildir--extra) + num (nnmaildir--art-num article)) + (unless num + ;; Allocate a new article number. + (erase-buffer) + (setq numdir (nnmaildir--num-dir dir) + file (nnmaildir--num-file numdir) + num -1) + (nnmaildir--mkdir numdir) + (write-region "" nil file nil 'no-message) + (while file + ;; Get the number of links to file. + (setq attr (nth 1 (file-attributes file))) + (if (= attr num) + ;; We've already tried this number, in the previous loop + ;; iteration, and failed. + (signal 'error `("Corrupt internal nnmaildir data" ,numdir))) + ;; If attr is 123, try to link file to "123". This atomically + ;; increases the link count and creates the "123" link, failing + ;; if that link was already created by another Gnus, just after + ;; we stat()ed file. + (condition-case nil + (progn + (add-name-to-file file (concat numdir (format "%x" attr))) + (setq file nil)) ;; Stop looping. + (file-already-exists nil)) + (setq num attr)) + (setf (nnmaildir--art-num article) num)) + ;; Store this new NOV data in a file + (erase-buffer) + (prin1 (vector storage-version num msgid nov) (current-buffer)) + (setq file (concat novfile ":")) + (nnmaildir--unlink file) + (write-region (point-min) (point-max) file nil 'no-message nil 'excl)) + (rename-file file novfile 'replace) + (setf (nnmaildir--art-msgid article) msgid) + nov))) + +(defun nnmaildir--cache-nov (group article nov) + (let ((cache (nnmaildir--grp-cache group)) + (index (nnmaildir--grp-index group)) + goner) + (unless (nnmaildir--art-nov article) + (setq goner (aref cache index)) + (if goner (setf (nnmaildir--art-nov goner) nil)) + (aset cache index article) + (setf (nnmaildir--grp-index group) (% (1+ index) (length cache)))) + (setf (nnmaildir--art-nov article) nov))) + +(defun nnmaildir--grp-add-art (server group article) + (let ((nov (nnmaildir--update-nov server group article)) + count num min nlist nlist-cdr insert-nlist) + (when nov + (setq count (1+ (nnmaildir--grp-count group)) + num (nnmaildir--art-num article) + min (if (= count 1) num + (min num (nnmaildir--grp-min group))) + nlist (nnmaildir--grp-nlist group)) + (if (or (null nlist) (> num (caar nlist))) + (setq nlist (cons (cons num article) nlist)) + (setq insert-nlist t + nlist-cdr (cdr nlist)) + (while (and nlist-cdr (< num (caar nlist-cdr))) + (setq nlist nlist-cdr + nlist-cdr (cdr nlist)))) + (let ((inhibit-quit t)) + (setf (nnmaildir--grp-count group) count) + (setf (nnmaildir--grp-min group) min) + (if insert-nlist + (setcdr nlist (cons (cons num article) nlist-cdr)) + (setf (nnmaildir--grp-nlist group) nlist)) + (set (intern (nnmaildir--art-prefix article) + (nnmaildir--grp-flist group)) + article) + (set (intern (nnmaildir--art-msgid article) + (nnmaildir--grp-mlist group)) + article) + (set (intern (nnmaildir--grp-name group) + (nnmaildir--srv-groups server)) + group)) + (nnmaildir--cache-nov group article nov) + t))) + +(defun nnmaildir--group-ls (server pgname) + (or (nnmaildir--param pgname 'directory-files) + (nnmaildir--srv-ls server))) + +(defun nnmaildir-article-number-to-file-name + (number group-name server-address-string) + (let ((group (nnmaildir--prepare server-address-string group-name)) + article dir pgname) + (catch 'return + (unless group + ;; The given group or server does not exist. + (throw 'return nil)) + (setq article (nnmaildir--nlist-art group number)) + (unless article + ;; The given article number does not exist in this group. + (throw 'return nil)) + (setq pgname (nnmaildir--pgname nnmaildir--cur-server group-name) + dir (nnmaildir--srv-dir nnmaildir--cur-server) + dir (nnmaildir--srvgrp-dir dir group-name) + dir (if (nnmaildir--param pgname 'read-only) + (nnmaildir--new dir) (nnmaildir--cur dir))) + (concat dir (nnmaildir--art-prefix article) + (nnmaildir--art-suffix article))))) + +(defun nnmaildir-article-number-to-base-name + (number group-name server-address-string) + (let ((x (nnmaildir--prepare server-address-string group-name))) + (when x + (setq x (nnmaildir--nlist-art x number)) + (and x (cons (nnmaildir--art-prefix x) + (nnmaildir--art-suffix x)))))) + +(defun nnmaildir-base-name-to-article-number + (base-name group-name server-address-string) + (let ((x (nnmaildir--prepare server-address-string group-name))) + (when x + (setq x (nnmaildir--grp-flist x) + x (nnmaildir--flist-art x base-name)) + (and x (nnmaildir--art-num x))))) + +(defun nnmaildir--nlist-iterate (nlist ranges func) + (let (entry high low nlist2) + (if (eq ranges 'all) + (setq ranges `((1 . ,(caar nlist))))) + (while ranges + (setq entry (car ranges) ranges (cdr ranges)) + (while (and ranges (eq entry (car ranges))) + (setq ranges (cdr ranges))) ;; skip duplicates + (if (numberp entry) + (setq low entry + high entry) + (setq low (car entry) + high (cdr entry))) + (setq nlist2 nlist) ;; Don't assume any sorting of ranges + (catch 'iterate-loop + (while nlist2 + (if (<= (caar nlist2) high) (throw 'iterate-loop nil)) + (setq nlist2 (cdr nlist2)))) + (catch 'iterate-loop + (while nlist2 + (setq entry (car nlist2) nlist2 (cdr nlist2)) + (if (< (car entry) low) (throw 'iterate-loop nil)) + (funcall func (cdr entry))))))) + +(defun nnmaildir--up2-1 (n) + (if (zerop n) 1 (1- (lsh 1 (1+ (logb n)))))) + +(defun nnmaildir--system-name () + (gnus-replace-in-string + (gnus-replace-in-string + (gnus-replace-in-string + (system-name) + "\\\\" "\\134" 'literal) + "/" "\\057" 'literal) + ":" "\\072" 'literal)) + +(defun nnmaildir-request-type (group &optional article) + 'mail) + +(defun nnmaildir-status-message (&optional server) + (nnmaildir--prepare server nil) + (nnmaildir--srv-error nnmaildir--cur-server)) + +(defun nnmaildir-server-opened (&optional server) + (and nnmaildir--cur-server + (if server + (string-equal server (nnmaildir--srv-address nnmaildir--cur-server)) + t) + (nnmaildir--srv-groups nnmaildir--cur-server) + t)) + +(defun nnmaildir-open-server (server &optional defs) + (let ((x server) + dir size) + (catch 'return + (setq server (intern-soft x nnmaildir--servers)) + (if server + (and (setq server (symbol-value server)) + (nnmaildir--srv-groups server) + (setq nnmaildir--cur-server server) + (throw 'return t)) + (setq server (make-nnmaildir--srv :address x)) + (let ((inhibit-quit t)) + (set (intern x nnmaildir--servers) server))) + (setq dir (assq 'directory defs)) + (unless dir + (setf (nnmaildir--srv-error server) + "You must set \"directory\" in the select method") + (throw 'return nil)) + (setq dir (cadr dir) + dir (eval dir) + dir (expand-file-name dir) + dir (file-name-as-directory dir)) + (unless (file-exists-p dir) + (setf (nnmaildir--srv-error server) (concat "No such directory: " dir)) + (throw 'return nil)) + (setf (nnmaildir--srv-dir server) dir) + (setq x (assq 'directory-files defs)) + (if (null x) + (setq x (if nnheader-directory-files-is-safe 'directory-files + 'nnheader-directory-files-safe)) + (setq x (cadr x)) + (unless (functionp x) + (setf (nnmaildir--srv-error server) + (concat "Not a function: " (prin1-to-string x))) + (throw 'return nil))) + (setf (nnmaildir--srv-ls server) x) + (setq size (length (funcall x dir nil "\\`[^.]" 'nosort)) + size (nnmaildir--up2-1 size)) + (and (setq x (assq 'get-new-mail defs)) + (setq x (cdr x)) + (car x) + (setf (nnmaildir--srv-gnm server) t) + (require 'nnmail)) + (setq x (assq 'target-prefix defs)) + (if x + (progn + (setq x (cadr x) + x (eval x)) + (setf (nnmaildir--srv-target-prefix server) x)) + (setq x (assq 'create-directory defs)) + (if x + (progn + (setq x (cadr x) + x (eval x) + x (file-name-as-directory x)) + (setf (nnmaildir--srv-target-prefix server) x)) + (setf (nnmaildir--srv-target-prefix server) ""))) + (setf (nnmaildir--srv-groups server) (make-vector size 0)) + (setq nnmaildir--cur-server server) + t))) + +(defun nnmaildir--parse-filename (file) + (let ((prefix (car file)) + timestamp len) + (if (string-match "\\`\\([0-9]+\\)\\(\\..*\\)\\'" prefix) + (progn + (setq timestamp (concat "0000" (match-string 1 prefix)) + len (- (length timestamp) 4)) + (vector (string-to-number (substring timestamp 0 len)) + (string-to-number (substring timestamp len)) + (match-string 2 prefix) + file)) + file))) + +(defun nnmaildir--sort-files (a b) + (catch 'return + (if (consp a) + (throw 'return (and (consp b) (string-lessp (car a) (car b))))) + (if (consp b) (throw 'return t)) + (if (< (aref a 0) (aref b 0)) (throw 'return t)) + (if (> (aref a 0) (aref b 0)) (throw 'return nil)) + (if (< (aref a 1) (aref b 1)) (throw 'return t)) + (if (> (aref a 1) (aref b 1)) (throw 'return nil)) + (string-lessp (aref a 2) (aref b 2)))) + +(defun nnmaildir--scan (gname scan-msgs groups method srv-dir srv-ls) + (catch 'return + (let ((36h-ago (- (car (current-time)) 2)) + absdir nndir tdir ndir cdir nattr cattr isnew pgname read-only ls + files num dir flist group x) + (setq absdir (nnmaildir--srvgrp-dir srv-dir gname) + nndir (nnmaildir--nndir absdir)) + (unless (file-exists-p absdir) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "No such directory: " absdir)) + (throw 'return nil)) + (setq tdir (nnmaildir--tmp absdir) + ndir (nnmaildir--new absdir) + cdir (nnmaildir--cur absdir) + nattr (file-attributes ndir) + cattr (file-attributes cdir)) + (unless (and (file-exists-p tdir) nattr cattr) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "Not a maildir: " absdir)) + (throw 'return nil)) + (setq group (nnmaildir--prepare nil gname) + pgname (nnmaildir--pgname nnmaildir--cur-server gname)) + (if group + (setq isnew nil) + (setq isnew t + group (make-nnmaildir--grp :name gname :index 0)) + (nnmaildir--mkdir nndir) + (nnmaildir--mkdir (nnmaildir--nov-dir nndir)) + (nnmaildir--mkdir (nnmaildir--marks-dir nndir)) + (write-region "" nil (concat nndir "markfile") nil 'no-message)) + (setq read-only (nnmaildir--param pgname 'read-only) + ls (or (nnmaildir--param pgname 'directory-files) srv-ls)) + (unless read-only + (setq x (nth 11 (file-attributes tdir))) + (unless (and (= x (nth 11 nattr)) (= x (nth 11 cattr))) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "Maildir spans filesystems: " absdir)) + (throw 'return nil)) + (mapcar + (lambda (file) + (setq x (file-attributes file)) + (if (or (> (cadr x) 1) (< (car (nth 4 x)) 36h-ago)) + (delete-file file))) + (funcall ls tdir 'full "\\`[^.]" 'nosort))) + (or scan-msgs + isnew + (throw 'return t)) + (setq nattr (nth 5 nattr)) + (if (equal nattr (nnmaildir--grp-new group)) + (setq nattr nil)) + (if read-only (setq dir (and (or isnew nattr) ndir)) + (when (or isnew nattr) + (mapcar + (lambda (file) + (let ((path (concat ndir file))) + (and (time-less-p (nth 5 (file-attributes path)) (current-time)) + (rename-file path (concat cdir file ":2,"))))) + (funcall ls ndir nil "\\`[^.]" 'nosort)) + (setf (nnmaildir--grp-new group) nattr)) + (setq cattr (nth 5 (file-attributes cdir))) + (if (equal cattr (nnmaildir--grp-cur group)) + (setq cattr nil)) + (setq dir (and (or isnew cattr) cdir))) + (unless dir (throw 'return t)) + (setq files (funcall ls dir nil "\\`[^.]" 'nosort) + files (save-match-data + (mapcar + (lambda (f) + (string-match "\\`\\([^:]*\\)\\(\\(:.*\\)?\\)\\'" f) + (cons (match-string 1 f) (match-string 2 f))) + files))) + (when isnew + (setq num (nnmaildir--up2-1 (length files))) + (setf (nnmaildir--grp-flist group) (make-vector num 0)) + (setf (nnmaildir--grp-mlist group) (make-vector num 0)) + (setf (nnmaildir--grp-mmth group) (make-vector 1 0)) + (setq num (nnmaildir--param pgname 'nov-cache-size)) + (if (numberp num) (if (< num 1) (setq num 1)) + (setq num 16 + cdir (nnmaildir--marks-dir nndir) + ndir (nnmaildir--subdir cdir "tick") + cdir (nnmaildir--subdir cdir "read")) + (mapcar + (lambda (file) + (setq file (car file)) + (if (or (not (file-exists-p (concat cdir file))) + (file-exists-p (concat ndir file))) + (setq num (1+ num)))) + files)) + (setf (nnmaildir--grp-cache group) (make-vector num nil)) + (let ((inhibit-quit t)) + (set (intern gname groups) group)) + (or scan-msgs (throw 'return t))) + (setq flist (nnmaildir--grp-flist group) + files (mapcar + (lambda (file) + (and (null (nnmaildir--flist-art flist (car file))) + file)) + files) + files (delq nil files) + files (mapcar 'nnmaildir--parse-filename files) + files (sort files 'nnmaildir--sort-files)) + (mapcar + (lambda (file) + (setq file (if (consp file) file (aref file 3)) + x (make-nnmaildir--art :prefix (car file) :suffix (cdr file))) + (nnmaildir--grp-add-art nnmaildir--cur-server group x)) + files) + (if read-only (setf (nnmaildir--grp-new group) nattr) + (setf (nnmaildir--grp-cur group) cattr))) + t)) + +(defun nnmaildir-request-scan (&optional scan-group server) + (let ((coding-system-for-write nnheader-file-coding-system) + (output-coding-system nnheader-file-coding-system) + (buffer-file-coding-system nil) + (file-coding-system nil) + (file-coding-system-alist nil) + (nnmaildir-get-new-mail t) + (nnmaildir-group-alist nil) + (nnmaildir-active-file nil) + x srv-ls srv-dir method groups target-prefix group dirs grp-dir seen + deactivate-mark) + (nnmaildir--prepare server nil) + (setq srv-ls (nnmaildir--srv-ls nnmaildir--cur-server) + srv-dir (nnmaildir--srv-dir nnmaildir--cur-server) + method (nnmaildir--srv-method nnmaildir--cur-server) + groups (nnmaildir--srv-groups nnmaildir--cur-server) + target-prefix (nnmaildir--srv-target-prefix nnmaildir--cur-server)) + (nnmaildir--with-work-buffer + (save-match-data + (if (stringp scan-group) + (if (nnmaildir--scan scan-group t groups method srv-dir srv-ls) + (if (nnmaildir--srv-gnm nnmaildir--cur-server) + (nnmail-get-new-mail 'nnmaildir nil nil scan-group)) + (unintern scan-group groups)) + (setq x (nth 5 (file-attributes srv-dir)) + scan-group (null scan-group)) + (if (equal x (nnmaildir--srv-mtime nnmaildir--cur-server)) + (if scan-group + (mapatoms (lambda (sym) + (nnmaildir--scan (symbol-name sym) t groups + method srv-dir srv-ls)) + groups)) + (setq dirs (funcall srv-ls srv-dir nil "\\`[^.]" 'nosort) + dirs (if (zerop (length target-prefix)) + dirs + (gnus-remove-if + (lambda (dir) + (and (>= (length dir) (length target-prefix)) + (string= (substring dir 0 + (length target-prefix)) + target-prefix))) + dirs)) + seen (nnmaildir--up2-1 (length dirs)) + seen (make-vector seen 0)) + (mapcar + (lambda (grp-dir) + (if (nnmaildir--scan grp-dir scan-group groups method srv-dir + srv-ls) + (intern grp-dir seen))) + dirs) + (setq x nil) + (mapatoms (lambda (group) + (setq group (symbol-name group)) + (unless (intern-soft group seen) + (setq x (cons group x)))) + groups) + (mapcar (lambda (grp) (unintern grp groups)) x) + (setf (nnmaildir--srv-mtime nnmaildir--cur-server) + (nth 5 (file-attributes srv-dir)))) + (and scan-group + (nnmaildir--srv-gnm nnmaildir--cur-server) + (nnmail-get-new-mail 'nnmaildir nil nil)))))) + t) + +(defun nnmaildir-request-list (&optional server) + (nnmaildir-request-scan 'find-new-groups server) + (let (pgname ro deactivate-mark) + (nnmaildir--prepare server nil) + (nnmaildir--with-nntp-buffer + (erase-buffer) + (mapatoms (lambda (group) + (setq pgname (symbol-name group) + pgname (nnmaildir--pgname nnmaildir--cur-server pgname) + group (symbol-value group) + ro (nnmaildir--param pgname 'read-only)) + (insert (nnmaildir--grp-name group) " ") + (princ (nnmaildir--group-maxnum nnmaildir--cur-server group) + nntp-server-buffer) + (insert " ") + (princ (nnmaildir--grp-min group) nntp-server-buffer) + (insert " " (if ro "n" "y") "\n")) + (nnmaildir--srv-groups nnmaildir--cur-server)))) + t) + +(defun nnmaildir-request-newgroups (date &optional server) + (nnmaildir-request-list server)) + +(defun nnmaildir-retrieve-groups (groups &optional server) + (let (group deactivate-mark) + (nnmaildir--prepare server nil) + (nnmaildir--with-nntp-buffer + (erase-buffer) + (mapcar + (lambda (gname) + (setq group (nnmaildir--prepare nil gname)) + (if (null group) (insert "411 no such news group\n") + (insert "211 ") + (princ (nnmaildir--grp-count group) nntp-server-buffer) + (insert " ") + (princ (nnmaildir--grp-min group) nntp-server-buffer) + (insert " ") + (princ (nnmaildir--group-maxnum nnmaildir--cur-server group) + nntp-server-buffer) + (insert " " gname "\n"))) + groups))) + 'group) + +(defun nnmaildir-request-update-info (gname info &optional server) + (let ((group (nnmaildir--prepare server gname)) + pgname flist always-marks never-marks old-marks dotfile num dir + markdirs marks mark ranges markdir article read end new-marks ls + old-mmth new-mmth mtime mark-sym existing missing deactivate-mark) + (catch 'return + (unless group + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "No such group: " gname)) + (throw 'return nil)) + (setq gname (nnmaildir--grp-name group) + pgname (nnmaildir--pgname nnmaildir--cur-server gname) + flist (nnmaildir--grp-flist group)) + (when (zerop (nnmaildir--grp-count group)) + (gnus-info-set-read info nil) + (gnus-info-set-marks info nil 'extend) + (throw 'return info)) + (setq old-marks (cons 'read (gnus-info-read info)) + old-marks (cons old-marks (gnus-info-marks info)) + always-marks (nnmaildir--param pgname 'always-marks) + never-marks (nnmaildir--param pgname 'never-marks) + existing (nnmaildir--grp-nlist group) + existing (mapcar 'car existing) + existing (nreverse existing) + existing (gnus-compress-sequence existing 'always-list) + missing (list (cons 1 (nnmaildir--group-maxnum + nnmaildir--cur-server group))) + missing (gnus-range-difference missing existing) + dir (nnmaildir--srv-dir nnmaildir--cur-server) + dir (nnmaildir--srvgrp-dir dir gname) + dir (nnmaildir--nndir dir) + dir (nnmaildir--marks-dir dir) + ls (nnmaildir--group-ls nnmaildir--cur-server pgname) + markdirs (funcall ls dir nil "\\`[^.]" 'nosort) + new-mmth (nnmaildir--up2-1 (length markdirs)) + new-mmth (make-vector new-mmth 0) + old-mmth (nnmaildir--grp-mmth group)) + (mapcar + (lambda (mark) + (setq markdir (nnmaildir--subdir dir mark) + mark-sym (intern mark) + ranges nil) + (catch 'got-ranges + (if (memq mark-sym never-marks) (throw 'got-ranges nil)) + (when (memq mark-sym always-marks) + (setq ranges existing) + (throw 'got-ranges nil)) + (setq mtime (nth 5 (file-attributes markdir))) + (set (intern mark new-mmth) mtime) + (when (equal mtime (symbol-value (intern-soft mark old-mmth))) + (setq ranges (assq mark-sym old-marks)) + (if ranges (setq ranges (cdr ranges))) + (throw 'got-ranges nil)) + (mapcar + (lambda (prefix) + (setq article (nnmaildir--flist-art flist prefix)) + (if article + (setq ranges + (gnus-add-to-range ranges + `(,(nnmaildir--art-num article)))))) + (funcall ls markdir nil "\\`[^.]" 'nosort))) + (if (eq mark-sym 'read) (setq read ranges) + (if ranges (setq marks (cons (cons mark-sym ranges) marks))))) + markdirs) + (gnus-info-set-read info (gnus-range-add read missing)) + (gnus-info-set-marks info marks 'extend) + (setf (nnmaildir--grp-mmth group) new-mmth) + info))) + +(defun nnmaildir-request-group (gname &optional server fast) + (let ((group (nnmaildir--prepare server gname)) + deactivate-mark) + (catch 'return + (unless group + ;; (insert "411 no such news group\n") + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "No such group: " gname)) + (throw 'return nil)) + (setf (nnmaildir--srv-curgrp nnmaildir--cur-server) group) + (if fast (throw 'return t)) + (nnmaildir--with-nntp-buffer + (erase-buffer) + (insert "211 ") + (princ (nnmaildir--grp-count group) nntp-server-buffer) + (insert " ") + (princ (nnmaildir--grp-min group) nntp-server-buffer) + (insert " ") + (princ (nnmaildir--group-maxnum nnmaildir--cur-server group) + nntp-server-buffer) + (insert " " gname "\n") + t)))) + +(defun nnmaildir-request-create-group (gname &optional server args) + (nnmaildir--prepare server nil) + (catch 'return + (let ((target-prefix (nnmaildir--srv-target-prefix nnmaildir--cur-server)) + srv-dir dir groups) + (when (zerop (length gname)) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + "Invalid (empty) group name") + (throw 'return nil)) + (when (eq (aref "." 0) (aref gname 0)) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + "Group names may not start with \".\"") + (throw 'return nil)) + (when (save-match-data (string-match "[\0/\t]" gname)) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "Illegal characters (null, tab, or /) in group name: " + gname)) + (throw 'return nil)) + (setq groups (nnmaildir--srv-groups nnmaildir--cur-server)) + (when (intern-soft gname groups) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "Group already exists: " gname)) + (throw 'return nil)) + (setq srv-dir (nnmaildir--srv-dir nnmaildir--cur-server)) + (if (file-name-absolute-p target-prefix) + (setq dir (expand-file-name target-prefix)) + (setq dir srv-dir + dir (file-truename dir) + dir (concat dir target-prefix))) + (setq dir (nnmaildir--subdir dir gname)) + (nnmaildir--mkdir dir) + (nnmaildir--mkdir (nnmaildir--tmp dir)) + (nnmaildir--mkdir (nnmaildir--new dir)) + (nnmaildir--mkdir (nnmaildir--cur dir)) + (unless (string= target-prefix "") + (make-symbolic-link (concat target-prefix gname) + (concat srv-dir gname))) + (nnmaildir-request-scan 'find-new-groups)))) + +(defun nnmaildir-request-rename-group (gname new-name &optional server) + (let ((group (nnmaildir--prepare server gname)) + (coding-system-for-write nnheader-file-coding-system) + (output-coding-system nnheader-file-coding-system) + (buffer-file-coding-system nil) + (file-coding-system nil) + (file-coding-system-alist nil) + srv-dir x groups) + (catch 'return + (unless group + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "No such group: " gname)) + (throw 'return nil)) + (when (zerop (length new-name)) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + "Invalid (empty) group name") + (throw 'return nil)) + (when (eq (aref "." 0) (aref new-name 0)) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + "Group names may not start with \".\"") + (throw 'return nil)) + (when (save-match-data (string-match "[\0/\t]" new-name)) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "Illegal characters (null, tab, or /) in group name: " + new-name)) + (throw 'return nil)) + (if (string-equal gname new-name) (throw 'return t)) + (when (intern-soft new-name + (nnmaildir--srv-groups nnmaildir--cur-server)) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "Group already exists: " new-name)) + (throw 'return nil)) + (setq srv-dir (nnmaildir--srv-dir nnmaildir--cur-server)) + (condition-case err + (rename-file (concat srv-dir gname) + (concat srv-dir new-name)) + (error + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "Error renaming link: " (prin1-to-string err))) + (throw 'return nil))) + (setq x (nnmaildir--srv-groups nnmaildir--cur-server) + groups (make-vector (length x) 0)) + (mapatoms (lambda (sym) + (unless (eq (symbol-value sym) group) + (set (intern (symbol-name sym) groups) + (symbol-value sym)))) + x) + (setq group (copy-sequence group)) + (setf (nnmaildir--grp-name group) new-name) + (set (intern new-name groups) group) + (setf (nnmaildir--srv-groups nnmaildir--cur-server) groups) + t))) + +(defun nnmaildir-request-delete-group (gname force &optional server) + (let ((group (nnmaildir--prepare server gname)) + pgname grp-dir target dir ls deactivate-mark) + (catch 'return + (unless group + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "No such group: " gname)) + (throw 'return nil)) + (setq gname (nnmaildir--grp-name group) + pgname (nnmaildir--pgname nnmaildir--cur-server gname) + grp-dir (nnmaildir--srv-dir nnmaildir--cur-server) + target (car (file-attributes (concat grp-dir gname))) + grp-dir (nnmaildir--srvgrp-dir grp-dir gname)) + (unless (or force (stringp target)) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "Not a symlink: " gname)) + (throw 'return nil)) + (if (eq group (nnmaildir--srv-curgrp nnmaildir--cur-server)) + (setf (nnmaildir--srv-curgrp nnmaildir--cur-server) nil)) + (unintern gname (nnmaildir--srv-groups nnmaildir--cur-server)) + (if (not force) + (progn + (setq grp-dir (directory-file-name grp-dir)) + (nnmaildir--unlink grp-dir)) + (setq ls (nnmaildir--group-ls nnmaildir--cur-server pgname)) + (if (nnmaildir--param pgname 'read-only) + (progn (delete-directory (nnmaildir--tmp grp-dir)) + (nnmaildir--unlink (nnmaildir--new grp-dir)) + (delete-directory (nnmaildir--cur grp-dir))) + (nnmaildir--delete-dir-files (nnmaildir--tmp grp-dir) ls) + (nnmaildir--delete-dir-files (nnmaildir--new grp-dir) ls) + (nnmaildir--delete-dir-files (nnmaildir--cur grp-dir) ls)) + (setq dir (nnmaildir--nndir grp-dir)) + (mapcar (lambda (subdir) (nnmaildir--delete-dir-files subdir ls)) + `(,(nnmaildir--nov-dir dir) ,(nnmaildir--num-dir dir) + ,@(funcall ls (nnmaildir--marks-dir dir) 'full "\\`[^.]" + 'nosort))) + (setq dir (nnmaildir--nndir grp-dir)) + (nnmaildir--unlink (concat dir "markfile")) + (nnmaildir--unlink (concat dir "markfile{new}")) + (delete-directory (nnmaildir--marks-dir dir)) + (delete-directory dir) + (if (not (stringp target)) + (delete-directory grp-dir) + (setq grp-dir (directory-file-name grp-dir) + dir target) + (unless (eq (aref "/" 0) (aref dir 0)) + (setq dir (concat (file-truename + (nnmaildir--srv-dir nnmaildir--cur-server)) + dir))) + (delete-directory dir) + (nnmaildir--unlink grp-dir))) + t))) + +(defun nnmaildir-retrieve-headers (articles &optional gname server fetch-old) + (let ((group (nnmaildir--prepare server gname)) + srv-dir dir nlist mlist article num start stop nov nlist2 insert-nov + deactivate-mark) + (setq insert-nov + (lambda (article) + (setq nov (nnmaildir--update-nov nnmaildir--cur-server group + article)) + (when nov + (nnmaildir--cache-nov group article nov) + (setq num (nnmaildir--art-num article)) + (princ num nntp-server-buffer) + (insert "\t" (nnmaildir--nov-get-beg nov) "\t" + (nnmaildir--art-msgid article) "\t" + (nnmaildir--nov-get-mid nov) "\tXref: nnmaildir " + gname ":") + (princ num nntp-server-buffer) + (insert "\t" (nnmaildir--nov-get-end nov) "\n")))) + (catch 'return + (unless group + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (if gname (concat "No such group: " gname) "No current group")) + (throw 'return nil)) + (nnmaildir--with-nntp-buffer + (erase-buffer) + (setq mlist (nnmaildir--grp-mlist group) + nlist (nnmaildir--grp-nlist group) + gname (nnmaildir--grp-name group) + srv-dir (nnmaildir--srv-dir nnmaildir--cur-server) + dir (nnmaildir--srvgrp-dir srv-dir gname)) + (cond + ((null nlist)) + ((and fetch-old (not (numberp fetch-old))) + (nnmaildir--nlist-iterate nlist 'all insert-nov)) + ((null articles)) + ((stringp (car articles)) + (mapcar + (lambda (msgid) + (setq article (nnmaildir--mlist-art mlist msgid)) + (if article (funcall insert-nov article))) + articles)) + (t + (if fetch-old + ;; Assume the article range list is sorted ascending + (setq stop (car articles) + start (car (last articles)) + stop (if (numberp stop) stop (car stop)) + start (if (numberp start) start (cdr start)) + stop (- stop fetch-old) + stop (if (< stop 1) 1 stop) + articles (list (cons stop start)))) + (nnmaildir--nlist-iterate nlist articles insert-nov))) + (sort-numeric-fields 1 (point-min) (point-max)) + 'nov)))) + +(defun nnmaildir-request-article (num-msgid &optional gname server to-buffer) + (let ((group (nnmaildir--prepare server gname)) + (case-fold-search t) + list article dir pgname deactivate-mark) + (catch 'return + (unless group + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (if gname (concat "No such group: " gname) "No current group")) + (throw 'return nil)) + (if (numberp num-msgid) + (setq article (nnmaildir--nlist-art group num-msgid)) + (setq list (nnmaildir--grp-mlist group) + article (nnmaildir--mlist-art list num-msgid)) + (if article (setq num-msgid (nnmaildir--art-num article)) + (catch 'found + (mapatoms + (lambda (group-sym) + (setq group (symbol-value group-sym) + list (nnmaildir--grp-mlist group) + article (nnmaildir--mlist-art list num-msgid)) + (when article + (setq num-msgid (nnmaildir--art-num article)) + (throw 'found nil))) + (nnmaildir--srv-groups nnmaildir--cur-server)))) + (unless article + (setf (nnmaildir--srv-error nnmaildir--cur-server) "No such article") + (throw 'return nil))) + (setq gname (nnmaildir--grp-name group) + pgname (nnmaildir--pgname nnmaildir--cur-server gname) + dir (nnmaildir--srv-dir nnmaildir--cur-server) + dir (nnmaildir--srvgrp-dir dir gname) + dir (if (nnmaildir--param pgname 'read-only) + (nnmaildir--new dir) (nnmaildir--cur dir)) + nnmaildir-article-file-name + (concat dir + (nnmaildir--art-prefix article) + (nnmaildir--art-suffix article))) + (unless (file-exists-p nnmaildir-article-file-name) + (nnmaildir--expired-article group article) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + "Article has expired") + (throw 'return nil)) + (save-excursion + (set-buffer (or to-buffer nntp-server-buffer)) + (erase-buffer) + (nnheader-insert-file-contents nnmaildir-article-file-name)) + (cons gname num-msgid)))) + +(defun nnmaildir-request-post (&optional server) + (let (message-required-mail-headers) + (funcall message-send-mail-function))) + +(defun nnmaildir-request-replace-article (number gname buffer) + (let ((group (nnmaildir--prepare nil gname)) + (coding-system-for-write nnheader-file-coding-system) + (output-coding-system nnheader-file-coding-system) + (buffer-file-coding-system nil) + (file-coding-system nil) + (file-coding-system-alist nil) + dir file article suffix tmpfile deactivate-mark) + (catch 'return + (unless group + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "No such group: " gname)) + (throw 'return nil)) + (when (nnmaildir--param (nnmaildir--pgname nnmaildir--cur-server gname) + 'read-only) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "Read-only group: " group)) + (throw 'return nil)) + (setq dir (nnmaildir--srv-dir nnmaildir--cur-server) + dir (nnmaildir--srvgrp-dir dir gname) + article (nnmaildir--nlist-art group number)) + (unless article + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "No such article: " (number-to-string number))) + (throw 'return nil)) + (setq suffix (nnmaildir--art-suffix article) + file (nnmaildir--art-prefix article) + tmpfile (concat (nnmaildir--tmp dir) file)) + (when (file-exists-p tmpfile) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "File exists: " tmpfile)) + (throw 'return nil)) + (save-excursion + (set-buffer buffer) + (write-region (point-min) (point-max) tmpfile nil 'no-message nil + 'excl)) + (unix-sync) ;; no fsync :( + (rename-file tmpfile (concat (nnmaildir--cur dir) file suffix) 'replace) + t))) + +(defun nnmaildir-request-move-article (article gname server accept-form + &optional last) + (let ((group (nnmaildir--prepare server gname)) + pgname suffix result nnmaildir--file deactivate-mark) + (catch 'return + (unless group + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "No such group: " gname)) + (throw 'return nil)) + (setq gname (nnmaildir--grp-name group) + pgname (nnmaildir--pgname nnmaildir--cur-server gname) + article (nnmaildir--nlist-art group article)) + (unless article + (setf (nnmaildir--srv-error nnmaildir--cur-server) "No such article") + (throw 'return nil)) + (setq suffix (nnmaildir--art-suffix article) + nnmaildir--file (nnmaildir--srv-dir nnmaildir--cur-server) + nnmaildir--file (nnmaildir--srvgrp-dir nnmaildir--file gname) + nnmaildir--file (if (nnmaildir--param pgname 'read-only) + (nnmaildir--new nnmaildir--file) + (nnmaildir--cur nnmaildir--file)) + nnmaildir--file (concat nnmaildir--file + (nnmaildir--art-prefix article) + suffix)) + (unless (file-exists-p nnmaildir--file) + (nnmaildir--expired-article group article) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + "Article has expired") + (throw 'return nil)) + (nnmaildir--with-move-buffer + (erase-buffer) + (nnheader-insert-file-contents nnmaildir--file) + (setq result (eval accept-form))) + (unless (or (null result) (nnmaildir--param pgname 'read-only)) + (nnmaildir--unlink nnmaildir--file) + (nnmaildir--expired-article group article)) + result))) + +(defun nnmaildir-request-accept-article (gname &optional server last) + (let ((group (nnmaildir--prepare server gname)) + (coding-system-for-write nnheader-file-coding-system) + (output-coding-system nnheader-file-coding-system) + (buffer-file-coding-system nil) + (file-coding-system nil) + (file-coding-system-alist nil) + srv-dir dir file time tmpfile curfile 24h article) + (catch 'return + (unless group + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "No such group: " gname)) + (throw 'return nil)) + (setq gname (nnmaildir--grp-name group)) + (when (nnmaildir--param (nnmaildir--pgname nnmaildir--cur-server gname) + 'read-only) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "Read-only group: " gname)) + (throw 'return nil)) + (setq srv-dir (nnmaildir--srv-dir nnmaildir--cur-server) + dir (nnmaildir--srvgrp-dir srv-dir gname) + time (current-time) + file (format-time-string "%s." time)) + (unless (string-equal nnmaildir--delivery-time file) + (setq nnmaildir--delivery-time file + nnmaildir--delivery-count 0)) + (when (and (consp (cdr time)) + (consp (cddr time))) + (setq file (concat file "M" (number-to-string (caddr time))))) + (setq file (concat file nnmaildir--delivery-pid) + file (concat file "Q" (number-to-string nnmaildir--delivery-count)) + file (concat file "." (nnmaildir--system-name)) + tmpfile (concat (nnmaildir--tmp dir) file) + curfile (concat (nnmaildir--cur dir) file ":2,")) + (when (file-exists-p tmpfile) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "File exists: " tmpfile)) + (throw 'return nil)) + (when (file-exists-p curfile) + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "File exists: " curfile)) + (throw 'return nil)) + (setq nnmaildir--delivery-count (1+ nnmaildir--delivery-count) + 24h (run-with-timer 86400 nil + (lambda () + (nnmaildir--unlink tmpfile) + (setf (nnmaildir--srv-error + nnmaildir--cur-server) + "24-hour timer expired") + (throw 'return nil)))) + (condition-case nil + (add-name-to-file nnmaildir--file tmpfile) + (error + (write-region (point-min) (point-max) tmpfile nil 'no-message nil + 'excl) + (unix-sync))) ;; no fsync :( + (cancel-timer 24h) + (condition-case err + (add-name-to-file tmpfile curfile) + (error + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "Error linking: " (prin1-to-string err))) + (nnmaildir--unlink tmpfile) + (throw 'return nil))) + (nnmaildir--unlink tmpfile) + (setq article (make-nnmaildir--art :prefix file :suffix ":2,")) + (if (nnmaildir--grp-add-art nnmaildir--cur-server group article) + (cons gname (nnmaildir--art-num article)))))) + +(defun nnmaildir-save-mail (group-art) + (catch 'return + (unless group-art + (throw 'return nil)) + (let (ga gname x groups nnmaildir--file deactivate-mark) + (save-excursion + (goto-char (point-min)) + (save-match-data + (while (looking-at "From ") + (replace-match "X-From-Line: ") + (forward-line 1)))) + (setq groups (nnmaildir--srv-groups nnmaildir--cur-server) + ga (car group-art) group-art (cdr group-art) + gname (car ga)) + (or (intern-soft gname groups) + (nnmaildir-request-create-group gname) + (throw 'return nil)) ;; not that nnmail bothers to check :( + (unless (nnmaildir-request-accept-article gname) + (throw 'return nil)) + (setq nnmaildir--file (nnmaildir--srv-dir nnmaildir--cur-server) + nnmaildir--file (nnmaildir--srvgrp-dir nnmaildir--file gname) + x (nnmaildir--prepare nil gname) + x (nnmaildir--grp-nlist x) + x (cdar x) + nnmaildir--file (concat nnmaildir--file + (nnmaildir--art-prefix x) + (nnmaildir--art-suffix x))) + (delq nil + (mapcar + (lambda (ga) + (setq gname (car ga)) + (and (or (intern-soft gname groups) + (nnmaildir-request-create-group gname)) + (nnmaildir-request-accept-article gname) + ga)) + group-art))))) + +(defun nnmaildir-active-number (gname) + 0) + +(defun nnmaildir-request-expire-articles (ranges &optional gname server force) + (let ((no-force (not force)) + (group (nnmaildir--prepare server gname)) + pgname time boundary bound-iter high low target dir nlist nlist2 + stop article didnt nnmaildir--file nnmaildir-article-file-name + deactivate-mark) + (catch 'return + (unless group + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (if gname (concat "No such group: " gname) "No current group")) + (throw 'return (gnus-uncompress-range ranges))) + (setq gname (nnmaildir--grp-name group) + pgname (nnmaildir--pgname nnmaildir--cur-server gname)) + (if (nnmaildir--param pgname 'read-only) + (throw 'return (gnus-uncompress-range ranges))) + (setq time (nnmaildir--param pgname 'expire-age)) + (unless time + (setq time (or (and nnmail-expiry-wait-function + (funcall nnmail-expiry-wait-function gname)) + nnmail-expiry-wait)) + (if (eq time 'immediate) + (setq time 0) + (if (numberp time) + (setq time (* time 86400))))) + (when no-force + (unless (integerp time) ;; handle 'never + (throw 'return (gnus-uncompress-range ranges))) + (setq boundary (current-time) + high (- (car boundary) (/ time 65536)) + low (- (cadr boundary) (% time 65536))) + (if (< low 0) + (setq low (+ low 65536) + high (1- high))) + (setcar (cdr boundary) low) + (setcar boundary high)) + (setq dir (nnmaildir--srv-dir nnmaildir--cur-server) + dir (nnmaildir--srvgrp-dir dir gname) + dir (nnmaildir--cur dir) + nlist (nnmaildir--grp-nlist group) + ranges (reverse ranges)) + (nnmaildir--with-move-buffer + (nnmaildir--nlist-iterate + nlist ranges + (lambda (article) + (setq nnmaildir--file (nnmaildir--art-prefix article) + nnmaildir--file (concat dir nnmaildir--file + (nnmaildir--art-suffix article)) + time (file-attributes nnmaildir--file)) + (cond + ((null time) + (nnmaildir--expired-article group article)) + ((and no-force + (progn + (setq time (nth 5 time) + bound-iter boundary) + (while (and bound-iter time + (= (car bound-iter) (car time))) + (setq bound-iter (cdr bound-iter) + time (cdr time))) + (and bound-iter time + (car-less-than-car bound-iter time)))) + (setq didnt (cons (nnmaildir--art-num article) didnt))) + (t + (setq nnmaildir-article-file-name nnmaildir--file + target (if force nil + (save-excursion + (save-restriction + (nnmaildir--param pgname 'expire-group))))) + (when (and (stringp target) + (not (string-equal target pgname))) ;; Move it. + (erase-buffer) + (nnheader-insert-file-contents nnmaildir--file) + (gnus-request-accept-article target nil nil 'no-encode)) + (if (equal target pgname) + ;; Leave it here. + (setq didnt (cons (nnmaildir--art-num article) didnt)) + (nnmaildir--unlink nnmaildir--file) + (nnmaildir--expired-article group article)))))) + (erase-buffer)) + didnt))) + +(defun nnmaildir-request-set-mark (gname actions &optional server) + (let ((group (nnmaildir--prepare server gname)) + (coding-system-for-write nnheader-file-coding-system) + (output-coding-system nnheader-file-coding-system) + (buffer-file-coding-system nil) + (file-coding-system nil) + (file-coding-system-alist nil) + del-mark del-action add-action set-action marksdir markfile nlist + ranges begin end article all-marks todo-marks did-marks mdir mfile + pgname ls permarkfile deactivate-mark) + (setq del-mark + (lambda (mark) + (setq mfile (nnmaildir--subdir marksdir (symbol-name mark)) + mfile (concat mfile (nnmaildir--art-prefix article))) + (nnmaildir--unlink mfile)) + del-action (lambda (article) (mapcar del-mark todo-marks)) + add-action + (lambda (article) + (mapcar + (lambda (mark) + (setq mdir (nnmaildir--subdir marksdir (symbol-name mark)) + permarkfile (concat mdir ":") + mfile (concat mdir (nnmaildir--art-prefix article))) + (unless (memq mark did-marks) + (setq did-marks (cons mark did-marks)) + (nnmaildir--mkdir mdir) + (unless (file-attributes permarkfile) + (condition-case nil + (add-name-to-file markfile permarkfile) + (file-error + ;; AFS can't make hard links in separate directories + (write-region "" nil permarkfile nil 'no-message))))) + (unless (file-exists-p mfile) + (add-name-to-file permarkfile mfile))) + todo-marks)) + set-action (lambda (article) + (funcall add-action) + (mapcar (lambda (mark) + (unless (memq mark todo-marks) + (funcall del-mark mark))) + all-marks))) + (catch 'return + (unless group + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "No such group: " gname)) + (mapcar (lambda (action) + (setq ranges (gnus-range-add ranges (car action)))) + actions) + (throw 'return ranges)) + (setq nlist (nnmaildir--grp-nlist group) + marksdir (nnmaildir--srv-dir nnmaildir--cur-server) + marksdir (nnmaildir--srvgrp-dir marksdir gname) + marksdir (nnmaildir--nndir marksdir) + markfile (concat marksdir "markfile") + marksdir (nnmaildir--marks-dir marksdir) + gname (nnmaildir--grp-name group) + pgname (nnmaildir--pgname nnmaildir--cur-server gname) + ls (nnmaildir--group-ls nnmaildir--cur-server pgname) + all-marks (funcall ls marksdir nil "\\`[^.]" 'nosort) + all-marks (mapcar 'intern all-marks)) + (mapcar + (lambda (action) + (setq ranges (car action) + todo-marks (caddr action)) + (mapcar (lambda (mark) (add-to-list 'all-marks mark)) todo-marks) + (if (numberp (cdr ranges)) (setq ranges (list ranges))) + (nnmaildir--nlist-iterate nlist ranges + (cond ((eq 'del (cadr action)) del-action) + ((eq 'add (cadr action)) add-action) + (t set-action)))) + actions) + nil))) + +(defun nnmaildir-close-group (gname &optional server) + (let ((group (nnmaildir--prepare server gname)) + pgname ls dir msgdir files flist dirs) + (if (null group) + (progn + (setf (nnmaildir--srv-error nnmaildir--cur-server) + (concat "No such group: " gname)) + nil) + (setq pgname (nnmaildir--pgname nnmaildir--cur-server gname) + ls (nnmaildir--group-ls nnmaildir--cur-server pgname) + dir (nnmaildir--srv-dir nnmaildir--cur-server) + dir (nnmaildir--srvgrp-dir dir gname) + msgdir (if (nnmaildir--param pgname 'read-only) + (nnmaildir--new dir) (nnmaildir--cur dir)) + dir (nnmaildir--nndir dir) + dirs (cons (nnmaildir--nov-dir dir) + (funcall ls (nnmaildir--marks-dir dir) 'full "\\`[^.]" + 'nosort)) + dirs (mapcar + (lambda (dir) + (cons dir (funcall ls dir nil "\\`[^.]" 'nosort))) + dirs) + files (funcall ls msgdir nil "\\`[^.]" 'nosort) + flist (nnmaildir--up2-1 (length files)) + flist (make-vector flist 0)) + (save-match-data + (mapcar + (lambda (file) + (string-match "\\`\\([^:]*\\)\\(:.*\\)?\\'" file) + (intern (match-string 1 file) flist)) + files)) + (mapcar + (lambda (dir) + (setq files (cdr dir) + dir (file-name-as-directory (car dir))) + (mapcar + (lambda (file) + (unless (or (intern-soft file flist) (string= file ":")) + (setq file (concat dir file)) + (delete-file file))) + files)) + dirs) + t))) + +(defun nnmaildir-close-server (&optional server) + (let (flist ls dirs dir files file x) + (nnmaildir--prepare server nil) + (when nnmaildir--cur-server + (setq server nnmaildir--cur-server + nnmaildir--cur-server nil) + (unintern (nnmaildir--srv-address server) nnmaildir--servers))) + t) + +(defun nnmaildir-request-close () + (let (servers buffer) + (mapatoms (lambda (server) + (setq servers (cons (symbol-name server) servers))) + nnmaildir--servers) + (mapcar 'nnmaildir-close-server servers) + (setq buffer (get-buffer " *nnmaildir work*")) + (if buffer (kill-buffer buffer)) + (setq buffer (get-buffer " *nnmaildir nov*")) + (if buffer (kill-buffer buffer)) + (setq buffer (get-buffer " *nnmaildir move*")) + (if buffer (kill-buffer buffer))) + t) + +(provide 'nnmaildir) + +;; Local Variables: +;; indent-tabs-mode: t +;; fill-column: 77 +;; End: + +;;; nnmaildir.el ends here diff --git a/lisp/nnnil.el b/lisp/nnnil.el new file mode 100644 index 0000000..08a097d --- /dev/null +++ b/lisp/nnnil.el @@ -0,0 +1,81 @@ +;;; nnnil.el --- empty backend for Gnus +;; Public domain. + +;; Author: Paul Jarc + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; nnnil is a Gnus backend that provides no groups or articles. It's useful +;; as a primary select method when you want all your real select methods to +;; be secondary or foreign. + +;;; Code: + +(eval-and-compile + (require 'nnheader)) + +(defvar nnnil-status-string "") + +(defun nnnil-retrieve-headers (articles &optional group server fetch-old) + (save-excursion + (set-buffer nntp-server-buffer) + (erase-buffer)) + 'nov) + +(defun nnnil-open-server (server &optional definitions) + t) + +(defun nnnil-close-server (&optional server) + t) + +(defun nnnil-request-close () + t) + +(defun nnnil-server-opened (&optional server) + t) + +(defun nnnil-status-message (&optional server) + nnnil-status-string) + +(defun nnnil-request-article (article &optional group server to-buffer) + (setq nnnil-status-string "No such group") + nil) + +(defun nnnil-request-group (group &optional server fast) + (let (deactivate-mark) + (save-excursion + (set-buffer nntp-server-buffer) + (erase-buffer) + (insert "411 no such news group\n"))) + (setq nnnil-status-string "No such group") + nil) + +(defun nnnil-close-group (group &optional server) + t) + +(defun nnnil-request-list (&optional server) + (save-excursion + (set-buffer nntp-server-buffer) + (erase-buffer)) + t) + +(defun nnnil-request-post (&optional server) + (setq nnnil-status-string "Read-only server") + nil) + +(provide 'nnnil) diff --git a/lisp/nnrss.el b/lisp/nnrss.el new file mode 100644 index 0000000..6e94f09 --- /dev/null +++ b/lisp/nnrss.el @@ -0,0 +1,771 @@ +;;; nnrss.el --- interfacing with RSS +;; Copyright (C) 2001, 2002, 2003 Free Software Foundation, Inc. + +;; Author: Shenghuo Zhu +;; Keywords: RSS + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your +;; option) any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;;; Code: + +(eval-when-compile (require 'cl)) + +(require 'gnus) +(require 'nnoo) +(require 'nnmail) +(require 'message) +(require 'mm-util) +(require 'gnus-util) +(require 'time-date) +(require 'rfc2231) +(require 'mm-url) +(eval-when-compile + (ignore-errors + (require 'xml))) +(eval '(require 'xml)) + +(nnoo-declare nnrss) + +(defvoo nnrss-directory (nnheader-concat gnus-directory "rss/") + "Where nnrss will save its files.") + +;; (group max rss-url) +(defvoo nnrss-server-data nil) + +;; (num timestamp url subject author date extra) +(defvoo nnrss-group-data nil) +(defvoo nnrss-group-max 0) +(defvoo nnrss-group-min 1) +(defvoo nnrss-group nil) +(defvoo nnrss-group-hashtb nil) +(defvoo nnrss-status-string "") + +(defconst nnrss-version "nnrss 1.0") + +(defvar nnrss-group-alist '() + "List of RSS addresses.") + +(defvar nnrss-use-local nil) + +(defvar nnrss-description-field 'X-Gnus-Description + "Field name used for DESCRIPTION. +To use the description in headers, put this name into `nnmail-extra-headers'.") + +(defvar nnrss-url-field 'X-Gnus-Url + "Field name used for URL. +To use the description in headers, put this name into `nnmail-extra-headers'.") + +(defvar nnrss-content-function nil + "A function which is called in `nnrss-request-article'. +The arguments are (ENTRY GROUP ARTICLE). +ENTRY is the record of the current headline. GROUP is the group name. +ARTICLE is the article number of the current headline.") + +(nnoo-define-basics nnrss) + +;;; Interface functions + +(deffoo nnrss-retrieve-headers (articles &optional group server fetch-old) + (nnrss-possibly-change-group group server) + (let (e) + (save-excursion + (set-buffer nntp-server-buffer) + (erase-buffer) + (dolist (article articles) + (if (setq e (assq article nnrss-group-data)) + (insert (number-to-string (car e)) "\t" ;; number + (if (nth 3 e) + (nnrss-format-string (nth 3 e)) "") + "\t" ;; subject + (if (nth 4 e) + (nnrss-format-string (nth 4 e)) + "(nobody)") + "\t" ;;from + (or (nth 5 e) "") + "\t" ;; date + (format "<%d@%s.nnrss>" (car e) group) + "\t" ;; id + "\t" ;; refs + "-1" "\t" ;; chars + "-1" "\t" ;; lines + "" "\t" ;; Xref + (if (and (nth 6 e) + (memq nnrss-description-field + nnmail-extra-headers)) + (concat (symbol-name nnrss-description-field) + ": " + (nnrss-format-string (nth 6 e)) + "\t") + "") + (if (and (nth 2 e) + (memq nnrss-url-field + nnmail-extra-headers)) + (concat (symbol-name nnrss-url-field) + ": " + (nnrss-format-string (nth 2 e)) + "\t") + "") + "\n"))))) + 'nov) + +(deffoo nnrss-request-group (group &optional server dont-check) + (nnrss-possibly-change-group group server) + (if dont-check + t + (nnrss-check-group group server) + (nnheader-report 'nnrss "Opened group %s" group) + (nnheader-insert + "211 %d %d %d %s\n" nnrss-group-max nnrss-group-min nnrss-group-max + (prin1-to-string group) + t))) + +(deffoo nnrss-close-group (group &optional server) + t) + +(deffoo nnrss-request-article (article &optional group server buffer) + (nnrss-possibly-change-group group server) + (let ((e (assq article nnrss-group-data)) + (boundary "=-=-=-=-=-=-=-=-=-") + (nntp-server-buffer (or buffer nntp-server-buffer)) + post err) + (when e + (catch 'error + (with-current-buffer nntp-server-buffer + (erase-buffer) + (goto-char (point-min)) + (insert "Mime-Version: 1.0\nContent-Type: multipart/alternative; boundary=\"" boundary "\"\n") + (if group + (insert "Newsgroups: " group "\n")) + (if (nth 3 e) + (insert "Subject: " (nnrss-format-string (nth 3 e)) "\n")) + (if (nth 4 e) + (insert "From: " (nnrss-format-string (nth 4 e)) "\n")) + (if (nth 5 e) + (insert "Date: " (nnrss-format-string (nth 5 e)) "\n")) + (insert "Message-ID: " (format "<%d@%s.nnrss>" (car e) group) "\n") + (insert "\n") + (let ((text (if (nth 6 e) + (nnrss-string-as-multibyte (nth 6 e)))) + (link (if (nth 2 e) + (nth 2 e)))) + (insert "\n\n--" boundary "\nContent-Type: text/plain\n\n") + (let ((point (point))) + (if text + (progn (insert text) + (goto-char point) + (while (re-search-forward "\n" nil t) + (replace-match " ")) + (goto-char (point-max)) + (insert "\n\n"))) + (if link + (insert link))) + (insert "\n\n--" boundary "\nContent-Type: text/html\n\n") + (let ((point (point))) + (if text + (progn (insert "\n" text "\n") + (goto-char point) + (while (re-search-forward "\n" nil t) + (replace-match " ")) + (goto-char (point-max)) + (insert "\n\n"))) + (if link + (insert "

    link

    \n")))) + (if nnrss-content-function + (funcall nnrss-content-function e group article))))) + (cond + (err + (nnheader-report 'nnrss err)) + ((not e) + (nnheader-report 'nnrss "no such id: %d" article)) + (t + (nnheader-report 'nnrss "article %s retrieved" (car e)) + ;; we return the article number. + (cons nnrss-group (car e)))))) + +(deffoo nnrss-request-list (&optional server) + (nnrss-possibly-change-group nil server) + (nnrss-generate-active) + t) + +(deffoo nnrss-open-server (server &optional defs connectionless) + (nnrss-read-server-data server) + (nnoo-change-server 'nnrss server defs) + t) + +(deffoo nnrss-request-expire-articles + (articles group &optional server force) + (nnrss-possibly-change-group group server) + (let (e days not-expirable changed) + (dolist (art articles) + (if (and (setq e (assq art nnrss-group-data)) + (nnmail-expired-article-p + group + (if (listp (setq days (nth 1 e))) days + (days-to-time (- days (time-to-days '(0 0))))) + force)) + (setq nnrss-group-data (delq e nnrss-group-data) + changed t) + (push art not-expirable))) + (if changed + (nnrss-save-group-data group server)) + not-expirable)) + +(deffoo nnrss-request-delete-group (group &optional force server) + (nnrss-possibly-change-group group server) + (setq nnrss-server-data + (delq (assoc group nnrss-server-data) nnrss-server-data)) + (nnrss-save-server-data server) + (let ((file (expand-file-name + (nnrss-translate-file-chars + (concat group (and server + (not (equal server "")) + "-") + server ".el")) nnrss-directory))) + (ignore-errors + (delete-file file))) + t) + +(deffoo nnrss-request-list-newsgroups (&optional server) + (nnrss-possibly-change-group nil server) + (save-excursion + (set-buffer nntp-server-buffer) + (erase-buffer) + (dolist (elem nnrss-group-alist) + (if (third elem) + (insert (car elem) "\t" (third elem) "\n")))) + t) + +(nnoo-define-skeleton nnrss) + +;;; Internal functions +(eval-when-compile (defun xml-rpc-method-call (&rest args))) +(defun nnrss-fetch (url &optional local) + "Fetch the url and put it in a the expected lisp structure." + (with-temp-buffer + ;some CVS versions of url.el need this to close the connection quickly + (let* (xmlform htmlform) + ;; bit o' work necessary for w3 pre-cvs and post-cvs + (if local + (let ((coding-system-for-read 'binary)) + (insert-file-contents url)) + (mm-url-insert url)) + +;; Because xml-parse-region can't deal with anything that isn't +;; xml and w3-parse-buffer can't deal with some xml, we have to +;; parse with xml-parse-region first and, if that fails, parse +;; with w3-parse-buffer. Yuck. Eventually, someone should find out +;; why w3-parse-buffer fails to parse some well-formed xml and +;; fix it. + + (condition-case err + (setq xmlform (xml-parse-region (point-min) (point-max))) + (error (if (fboundp 'w3-parse-buffer) + (setq htmlform (caddar (w3-parse-buffer + (current-buffer)))) + (message "nnrss: Not valid XML and w3 parse not available (%s)" + url)))) + (if htmlform + htmlform + xmlform)))) + +(defun nnrss-possibly-change-group (&optional group server) + (when (and server + (not (nnrss-server-opened server))) + (nnrss-open-server server)) + (when (and group (not (equal group nnrss-group))) + (nnrss-read-group-data group server) + (setq nnrss-group group))) + +(defvar nnrss-extra-categories '(nnrss-snarf-moreover-categories)) + +(defun nnrss-generate-active () + (if (y-or-n-p "fetch extra categories? ") + (dolist (func nnrss-extra-categories) + (funcall func))) + (save-excursion + (set-buffer nntp-server-buffer) + (erase-buffer) + (dolist (elem nnrss-group-alist) + (insert (prin1-to-string (car elem)) " 0 1 y\n")) + (dolist (elem nnrss-server-data) + (unless (assoc (car elem) nnrss-group-alist) + (insert (prin1-to-string (car elem)) " 0 1 y\n"))))) + +;;; data functions + +(defun nnrss-read-server-data (server) + (setq nnrss-server-data nil) + (let ((file (expand-file-name + (nnrss-translate-file-chars + (concat "nnrss" (and server + (not (equal server "")) + "-") + server + ".el")) + nnrss-directory))) + (when (file-exists-p file) + (with-temp-buffer + (let ((coding-system-for-read 'binary) + (input-coding-system 'binary) + emacs-lisp-mode-hook) + (insert-file-contents file) + (emacs-lisp-mode) + (goto-char (point-min)) + (eval-buffer)))))) + +(defun nnrss-save-server-data (server) + (gnus-make-directory nnrss-directory) + (let ((file (expand-file-name + (nnrss-translate-file-chars + (concat "nnrss" (and server + (not (equal server "")) + "-") + server ".el")) + nnrss-directory))) + (let ((coding-system-for-write 'binary) + (output-coding-system 'binary) + print-level print-length) + (with-temp-file file + (insert "(setq nnrss-group-alist '" + (prin1-to-string nnrss-group-alist) + ")\n") + (insert "(setq nnrss-server-data '" + (prin1-to-string nnrss-server-data) + ")\n"))))) + +(defun nnrss-read-group-data (group server) + (setq nnrss-group-data nil) + (setq nnrss-group-hashtb (gnus-make-hashtable)) + (let ((pair (assoc group nnrss-server-data))) + (setq nnrss-group-max (or (cadr pair) 0)) + (setq nnrss-group-min (+ nnrss-group-max 1))) + (let ((file (expand-file-name + (nnrss-translate-file-chars + (concat group (and server + (not (equal server "")) + "-") + server ".el")) + nnrss-directory))) + (when (file-exists-p file) + (with-temp-buffer + (let ((coding-system-for-read 'binary) + (input-coding-system 'binary) + emacs-lisp-mode-hook) + (insert-file-contents file) + (emacs-lisp-mode) + (goto-char (point-min)) + (eval-buffer))) + (dolist (e nnrss-group-data) + (gnus-sethash (nth 2 e) e nnrss-group-hashtb) + (if (and (car e) (> nnrss-group-min (car e))) + (setq nnrss-group-min (car e))) + (if (and (car e) (< nnrss-group-max (car e))) + (setq nnrss-group-max (car e))))))) + +(defun nnrss-save-group-data (group server) + (gnus-make-directory nnrss-directory) + (let ((file (expand-file-name + (nnrss-translate-file-chars + (concat group (and server + (not (equal server "")) + "-") + server ".el")) + nnrss-directory))) + (let ((coding-system-for-write 'binary) + (output-coding-system 'binary) + print-level print-length) + (with-temp-file file + (insert "(setq nnrss-group-data '" + (prin1-to-string nnrss-group-data) + ")\n"))))) + +;;; URL interface + +(defun nnrss-no-cache (url) + "") + +(defun nnrss-insert-w3 (url) + (mm-with-unibyte-current-buffer + (mm-url-insert url))) + +(defun nnrss-decode-entities-unibyte-string (string) + (if string + (mm-with-unibyte-buffer + (insert string) + (mm-url-decode-entities-nbsp) + (buffer-string)))) + +(defalias 'nnrss-insert 'nnrss-insert-w3) + +(if (featurep 'xemacs) + (defalias 'nnrss-string-as-multibyte 'identity) + (defalias 'nnrss-string-as-multibyte 'string-as-multibyte)) + +;;; Snarf functions + +(defun nnrss-check-group (group server) + (let (file xml subject url extra changed author + date rss-ns rdf-ns content-ns dc-ns) + (if (and nnrss-use-local + (file-exists-p (setq file (expand-file-name + (nnrss-translate-file-chars + (concat group ".xml")) + nnrss-directory)))) + (nnrss-fetch file t) + (setq url (or (nth 2 (assoc group nnrss-server-data)) + (second (assoc group nnrss-group-alist)))) + (unless url + (setq url + (cdr + (assoc 'href + (nnrss-discover-feed + (read-string + (format "URL to search for %s: " group) "http://"))))) + (let ((pair (assoc group nnrss-server-data))) + (if pair + (setcdr (cdr pair) (list url)) + (push (list group nnrss-group-max url) nnrss-server-data))) + (setq changed t)) + (setq xml (nnrss-fetch url))) + ;; See + ;; http://feeds.archive.org/validator/docs/howto/declare_namespaces.html + ;; for more RSS namespaces. + (setq dc-ns (nnrss-get-namespace-prefix xml "http://purl.org/dc/elements/1.1/") + rdf-ns (nnrss-get-namespace-prefix xml "http://www.w3.org/1999/02/22-rdf-syntax-ns#") + rss-ns (nnrss-get-namespace-prefix xml "http://purl.org/rss/1.0/") + content-ns (nnrss-get-namespace-prefix xml "http://purl.org/rss/1.0/modules/content/")) + (dolist (item (nreverse (nnrss-find-el (intern (concat rss-ns "item")) xml))) + (when (and (listp item) + (eq (intern (concat rss-ns "item")) (car item)) + (setq url (nnrss-decode-entities-unibyte-string + (nnrss-node-text rss-ns 'link (cddr item)))) + (not (gnus-gethash url nnrss-group-hashtb))) + (setq subject (nnrss-node-text rss-ns 'title item)) + (setq extra (or (nnrss-node-text content-ns 'encoded item) + (nnrss-node-text rss-ns 'description item))) + (setq author (or (nnrss-node-text rss-ns 'author item) + (nnrss-node-text dc-ns 'creator item))) + (setq date (or (nnrss-node-text dc-ns 'date item) + (nnrss-node-text rss-ns 'pubDate item) + (message-make-date))) + (push + (list + (incf nnrss-group-max) + (current-time) + url + (and subject (nnrss-decode-entities-unibyte-string subject)) + (and author (nnrss-decode-entities-unibyte-string author)) + date + (and extra (nnrss-decode-entities-unibyte-string extra))) + nnrss-group-data) + (gnus-sethash url (car nnrss-group-data) nnrss-group-hashtb) + (setq changed t))) + (when changed + (nnrss-save-group-data group server) + (let ((pair (assoc group nnrss-server-data))) + (if pair + (setcar (cdr pair) nnrss-group-max) + (push (list group nnrss-group-max) nnrss-server-data))) + (nnrss-save-server-data server)))) + +(defun nnrss-generate-download-script () + "Generate a download script in the current buffer. +It is useful when `(setq nnrss-use-local t)'." + (interactive) + (insert "#!/bin/sh\n") + (insert "WGET=wget\n") + (insert "RSSDIR='" (expand-file-name nnrss-directory) "'\n") + (dolist (elem nnrss-server-data) + (let ((url (or (nth 2 elem) + (second (assoc (car elem) nnrss-group-alist))))) + (insert "$WGET -q -O \"$RSSDIR\"/'" + (nnrss-translate-file-chars (concat (car elem) ".xml")) + "' '" url "'\n")))) + +(defun nnrss-translate-file-chars (name) + (let ((nnheader-file-name-translation-alist + (append nnheader-file-name-translation-alist '((?' . ?_))))) + (nnheader-translate-file-chars name))) + +(defvar nnrss-moreover-url + "http://w.moreover.com/categories/category_list_rss.html" + "The url of moreover.com categories.") + +(defun nnrss-snarf-moreover-categories () + "Snarf RSS links from moreover.com." + (interactive) + (let (category name url changed) + (with-temp-buffer + (nnrss-insert nnrss-moreover-url) + (goto-char (point-min)) + (while (re-search-forward + "\\| elements that are links to RSS from the parsed data." + (delq nil (mapcar + (lambda (el) + (if (nnrss-rsslink-p el) el)) + (nnrss-find-el 'link data)))) + +(defun nnrss-extract-hrefs (data) + "Recursively extract hrefs from a page's source. DATA should be +the output of xml-parse-region or w3-parse-buffer." + (mapcar (lambda (ahref) + (cdr (assoc 'href (cadr ahref)))) + (nnrss-find-el 'a data))) + +(defmacro nnrss-match-macro (base-uri item + onsite-list offsite-list) + `(cond ((or (string-match (concat "^" ,base-uri) ,item) + (not (string-match "://" ,item))) + (setq ,onsite-list (append ,onsite-list (list ,item)))) + (t (setq ,offsite-list (append ,offsite-list (list ,item)))))) + +(defun nnrss-order-hrefs (base-uri hrefs) + "Given a list of hrefs, sort them using the following priorities: + 1. links ending in .rss + 2. links ending in .rdf + 3. links ending in .xml + 4. links containing the above + 5. offsite links + +BASE-URI is used to determine the location of the links and +whether they are `offsite' or `onsite'." + (let (rss-onsite-end rdf-onsite-end xml-onsite-end + rss-onsite-in rdf-onsite-in xml-onsite-in + rss-offsite-end rdf-offsite-end xml-offsite-end + rss-offsite-in rdf-offsite-in xml-offsite-in) + (mapcar (lambda (href) + (if (not (null href)) + (cond ((string-match "\\.rss$" href) + (nnrss-match-macro + base-uri href rss-onsite-end rss-offsite-end)) + ((string-match "\\.rdf$" href) + (nnrss-match-macro + base-uri href rdf-onsite-end rdf-offsite-end)) + ((string-match "\\.xml$" href) + (nnrss-match-macro + base-uri href xml-onsite-end xml-offsite-end)) + ((string-match "rss" href) + (nnrss-match-macro + base-uri href rss-onsite-in rss-offsite-in)) + ((string-match "rdf" href) + (nnrss-match-macro + base-uri href rdf-onsite-in rdf-offsite-in)) + ((string-match "xml" href) + (nnrss-match-macro + base-uri href xml-onsite-in xml-offsite-in))))) + hrefs) + (append + rss-onsite-end rdf-onsite-end xml-onsite-end + rss-onsite-in rdf-onsite-in xml-onsite-in + rss-offsite-end rdf-offsite-end xml-offsite-end + rss-offsite-in rdf-offsite-in xml-offsite-in))) + +(defun nnrss-discover-feed (url) + "Given a page, find an RSS feed using Mark Pilgrim's +`ultra-liberal rss locator' (http://diveintomark.org/2002/08/15.html)." + + (let ((parsed-page (nnrss-fetch url))) + +;; 1. if this url is the rss, use it. + (if (nnrss-rss-p parsed-page) + (let ((rss-ns (nnrss-get-namespace-prefix parsed-page "http://purl.org/rss/1.0/"))) + (nnrss-rss-title-description rss-ns parsed-page url)) + +;; 2. look for the (length urllist) 1) + (let ((completion-ignore-case t) + (selection + (mapcar (lambda (listinfo) + (cons (cdr (assoc "sitename" listinfo)) + (string-to-int + (cdr (assoc "feedid" listinfo))))) + feedinfo))) + (cdr (assoc + (completing-read + "Multiple feeds found. Select one: " + selection nil t) urllist))) + (cdar urllist)))))) + (error (message "XML-RPC is not available... not checking Syndic8.")))) + +(defun nnrss-rss-p (data) + "Test if data is an RSS feed. Simply ensures that the first +element is rss or rdf." + (or (eq (caar data) 'rss) + (eq (caar data) 'rdf:RDF))) + +(defun nnrss-rss-title-description (rss-namespace data url) + "Return the title of an RSS feed." + (if (nnrss-rss-p data) + (let ((description (intern (concat rss-namespace "description"))) + (title (intern (concat rss-namespace "title"))) + (channel (nnrss-find-el (intern (concat rss-namespace "channel")) + data))) + (list + (cons 'description (caddr (nth 0 (nnrss-find-el description channel)))) + (cons 'title (caddr (nth 0 (nnrss-find-el title channel)))) + (cons 'href url))))) + +(defun nnrss-get-namespace-prefix (el uri) + "Given EL (containing a parsed element) and URI (containing a string +that gives the URI for which you want to retrieve the namespace +prefix), return the prefix." + (let* ((prefix (car (rassoc uri (cadar el)))) + (nslist (if prefix + (split-string (symbol-name prefix) ":"))) + (ns (cond ((eq (length nslist) 1) ; no prefix given + "") + ((eq (length nslist) 2) ; extract prefix + (cadr nslist))))) + (if (and ns (not (eq ns ""))) + (concat ns ":") + ns))) + +(provide 'nnrss) + + +;;; nnrss.el ends here + diff --git a/lisp/sha1-el.el b/lisp/sha1-el.el new file mode 100644 index 0000000..6bd25c6 --- /dev/null +++ b/lisp/sha1-el.el @@ -0,0 +1,432 @@ +;;; sha1-el.el --- SHA1 Secure Hash Algorithm in Emacs-Lisp. + +;; Copyright (C) 1999, 2001 Free Software Foundation, Inc. + +;; Author: Shuhei KOBAYASHI +;; Keywords: SHA1, FIPS 180-1 + +;; This file is part of FLIM (Faithful Library about Internet Message). + +;; 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, 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 should have received a copy of the GNU General Public License +;; along with this program; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; This program is implemented from the definition of SHA-1 in FIPS PUB +;; 180-1 (Federal Information Processing Standards Publication 180-1), +;; "Announcing the Standard for SECURE HASH STANDARD". +;; +;; (EXCEPTION; two optimizations taken from GnuPG/cipher/sha1.c) +;; +;; Test cases from FIPS PUB 180-1. +;; +;; (sha1 "abc") +;; => a9993e364706816aba3e25717850c26c9cd0d89d +;; +;; (sha1 "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") +;; => 84983e441c3bd26ebaae4aa1f95129e5e54670f1 +;; +;; (sha1 (make-string 1000000 ?a)) +;; => 34aa973cd4c4daa4f61eeb2bdbad27316534016f +;; +;; BUGS: +;; * It is assumed that length of input string is less than 2^29 bytes. +;; * It is caller's responsibility to make string (or region) unibyte. +;; +;; TODO: +;; * Rewrite from scratch! +;; This version is much faster than Keiichi Suzuki's another sha1.el, +;; but it is too dirty. + +;;; Code: + +(require 'hex-util) + +(autoload 'executable-find "executable") + +;;; +;;; external SHA1 function. +;;; + +(defvar sha1-maximum-internal-length 500 + "*Maximum length of message to use lisp version of SHA1 function. +If message is longer than this, `sha1-program' is used instead. + +If this variable is set to 0, use extarnal program only. +If this variable is set to nil, use internal function only.") + +(defvar sha1-program '("openssl" "sha1") + "*Name of program to compute SHA1. +It must be a string \(program name\) or list of strings \(name and its args\).") + +(defvar sha1-use-external + (exec-installed-p (car sha1-program)) + "*Use external sha1 program. +If this variable is set to nil, use internal function only.") + +(defun sha1-string-external (string) + ;; `with-temp-buffer' is new in v20, so we do not use it. + (save-excursion + (let (buffer) + (unwind-protect + (let (prog args) + (if (consp sha1-program) + (setq prog (car sha1-program) + args (cdr sha1-program)) + (setq prog sha1-program + args nil)) + (setq buffer (set-buffer + (generate-new-buffer " *sha1 external*"))) + (insert string) + (apply (function call-process-region) + (point-min)(point-max) + prog t t nil args) + ;; SHA1 is 40 bytes long in hexadecimal form. + (buffer-substring (point-min)(+ (point-min) 40))) + (and buffer + (buffer-name buffer) + (kill-buffer buffer)))))) + +(defun sha1-region-external (beg end) + (sha1-string-external (buffer-substring-no-properties beg end))) + +;;; +;;; internal SHA1 function. +;;; + +(eval-when-compile + ;; optional second arg of string-to-number is new in v20. + (defconst sha1-K0-high 23170) ; (string-to-number "5A82" 16) + (defconst sha1-K0-low 31129) ; (string-to-number "7999" 16) + (defconst sha1-K1-high 28377) ; (string-to-number "6ED9" 16) + (defconst sha1-K1-low 60321) ; (string-to-number "EBA1" 16) + (defconst sha1-K2-high 36635) ; (string-to-number "8F1B" 16) + (defconst sha1-K2-low 48348) ; (string-to-number "BCDC" 16) + (defconst sha1-K3-high 51810) ; (string-to-number "CA62" 16) + (defconst sha1-K3-low 49622) ; (string-to-number "C1D6" 16) + +;;; original definition of sha1-F0. +;;; (defmacro sha1-F0 (B C D) +;;; (` (logior (logand (, B) (, C)) +;;; (logand (lognot (, B)) (, D))))) +;;; a little optimization from GnuPG/cipher/sha1.c. + (defmacro sha1-F0 (B C D) + (` (logxor (, D) (logand (, B) (logxor (, C) (, D)))))) + (defmacro sha1-F1 (B C D) + (` (logxor (, B) (, C) (, D)))) +;;; original definition of sha1-F2. +;;; (defmacro sha1-F2 (B C D) +;;; (` (logior (logand (, B) (, C)) +;;; (logand (, B) (, D)) +;;; (logand (, C) (, D))))) +;;; a little optimization from GnuPG/cipher/sha1.c. + (defmacro sha1-F2 (B C D) + (` (logior (logand (, B) (, C)) + (logand (, D) (logior (, B) (, C)))))) + (defmacro sha1-F3 (B C D) + (` (logxor (, B) (, C) (, D)))) + + (defmacro sha1-S1 (W-high W-low) + (` (let ((W-high (, W-high)) + (W-low (, W-low))) + (setq S1W-high (+ (% (* W-high 2) 65536) + (/ W-low (, (/ 65536 2))))) + (setq S1W-low (+ (/ W-high (, (/ 65536 2))) + (% (* W-low 2) 65536)))))) + (defmacro sha1-S5 (A-high A-low) + (` (progn + (setq S5A-high (+ (% (* (, A-high) 32) 65536) + (/ (, A-low) (, (/ 65536 32))))) + (setq S5A-low (+ (/ (, A-high) (, (/ 65536 32))) + (% (* (, A-low) 32) 65536)))))) + (defmacro sha1-S30 (B-high B-low) + (` (progn + (setq S30B-high (+ (/ (, B-high) 4) + (* (% (, B-low) 4) (, (/ 65536 4))))) + (setq S30B-low (+ (/ (, B-low) 4) + (* (% (, B-high) 4) (, (/ 65536 4)))))))) + + (defmacro sha1-OP (round) + (` (progn + (sha1-S5 sha1-A-high sha1-A-low) + (sha1-S30 sha1-B-high sha1-B-low) + (setq sha1-A-low (+ ((, (intern (format "sha1-F%d" round))) + sha1-B-low sha1-C-low sha1-D-low) + sha1-E-low + (, (symbol-value + (intern (format "sha1-K%d-low" round)))) + (aref block-low idx) + (progn + (setq sha1-E-low sha1-D-low) + (setq sha1-D-low sha1-C-low) + (setq sha1-C-low S30B-low) + (setq sha1-B-low sha1-A-low) + S5A-low))) + (setq carry (/ sha1-A-low 65536)) + (setq sha1-A-low (% sha1-A-low 65536)) + (setq sha1-A-high (% (+ ((, (intern (format "sha1-F%d" round))) + sha1-B-high sha1-C-high sha1-D-high) + sha1-E-high + (, (symbol-value + (intern (format "sha1-K%d-high" round)))) + (aref block-high idx) + (progn + (setq sha1-E-high sha1-D-high) + (setq sha1-D-high sha1-C-high) + (setq sha1-C-high S30B-high) + (setq sha1-B-high sha1-A-high) + S5A-high) + carry) + 65536))))) + + (defmacro sha1-add-to-H (H X) + (` (progn + (setq (, (intern (format "sha1-%s-low" H))) + (+ (, (intern (format "sha1-%s-low" H))) + (, (intern (format "sha1-%s-low" X))))) + (setq carry (/ (, (intern (format "sha1-%s-low" H))) 65536)) + (setq (, (intern (format "sha1-%s-low" H))) + (% (, (intern (format "sha1-%s-low" H))) 65536)) + (setq (, (intern (format "sha1-%s-high" H))) + (% (+ (, (intern (format "sha1-%s-high" H))) + (, (intern (format "sha1-%s-high" X))) + carry) + 65536))))) + ) + +;;; buffers (H0 H1 H2 H3 H4). +(defvar sha1-H0-high) +(defvar sha1-H0-low) +(defvar sha1-H1-high) +(defvar sha1-H1-low) +(defvar sha1-H2-high) +(defvar sha1-H2-low) +(defvar sha1-H3-high) +(defvar sha1-H3-low) +(defvar sha1-H4-high) +(defvar sha1-H4-low) + +(defun sha1-block (block-high block-low) + (let (;; step (c) --- initialize buffers (A B C D E). + (sha1-A-high sha1-H0-high) (sha1-A-low sha1-H0-low) + (sha1-B-high sha1-H1-high) (sha1-B-low sha1-H1-low) + (sha1-C-high sha1-H2-high) (sha1-C-low sha1-H2-low) + (sha1-D-high sha1-H3-high) (sha1-D-low sha1-H3-low) + (sha1-E-high sha1-H4-high) (sha1-E-low sha1-H4-low) + (idx 16)) + ;; step (b). + (let (;; temporary variables used in sha1-S1 macro. + S1W-high S1W-low) + (while (< idx 80) + (sha1-S1 (logxor (aref block-high (- idx 3)) + (aref block-high (- idx 8)) + (aref block-high (- idx 14)) + (aref block-high (- idx 16))) + (logxor (aref block-low (- idx 3)) + (aref block-low (- idx 8)) + (aref block-low (- idx 14)) + (aref block-low (- idx 16)))) + (aset block-high idx S1W-high) + (aset block-low idx S1W-low) + (setq idx (1+ idx)))) + ;; step (d). + (setq idx 0) + (let (;; temporary variables used in sha1-OP macro. + S5A-high S5A-low S30B-high S30B-low carry) + (while (< idx 20) (sha1-OP 0) (setq idx (1+ idx))) + (while (< idx 40) (sha1-OP 1) (setq idx (1+ idx))) + (while (< idx 60) (sha1-OP 2) (setq idx (1+ idx))) + (while (< idx 80) (sha1-OP 3) (setq idx (1+ idx)))) + ;; step (e). + (let (;; temporary variables used in sha1-add-to-H macro. + carry) + (sha1-add-to-H H0 A) + (sha1-add-to-H H1 B) + (sha1-add-to-H H2 C) + (sha1-add-to-H H3 D) + (sha1-add-to-H H4 E)))) + +(defun sha1-binary (string) + "Return the SHA1 of STRING in binary form." + (let (;; prepare buffers for a block. byte-length of block is 64. + ;; input block is split into two vectors. + ;; + ;; input block: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ... + ;; block-high: +-0-+ +-1-+ +-2-+ +-3-+ + ;; block-low: +-0-+ +-1-+ +-2-+ +-3-+ + ;; + ;; length of each vector is 80, and elements of each vector are + ;; 16bit integers. elements 0x10-0x4F of each vector are + ;; assigned later in `sha1-block'. + (block-high (eval-when-compile (make-vector 80 nil))) + (block-low (eval-when-compile (make-vector 80 nil)))) + (unwind-protect + (let* (;; byte-length of input string. + (len (length string)) + (lim (* (/ len 64) 64)) + (rem (% len 4)) + (idx 0)(pos 0)) + ;; initialize buffers (H0 H1 H2 H3 H4). + (setq sha1-H0-high 26437 ; (string-to-number "6745" 16) + sha1-H0-low 8961 ; (string-to-number "2301" 16) + sha1-H1-high 61389 ; (string-to-number "EFCD" 16) + sha1-H1-low 43913 ; (string-to-number "AB89" 16) + sha1-H2-high 39098 ; (string-to-number "98BA" 16) + sha1-H2-low 56574 ; (string-to-number "DCFE" 16) + sha1-H3-high 4146 ; (string-to-number "1032" 16) + sha1-H3-low 21622 ; (string-to-number "5476" 16) + sha1-H4-high 50130 ; (string-to-number "C3D2" 16) + sha1-H4-low 57840) ; (string-to-number "E1F0" 16) + ;; loop for each 64 bytes block. + (while (< pos lim) + ;; step (a). + (setq idx 0) + (while (< idx 16) + (aset block-high idx (+ (* (aref string pos) 256) + (aref string (1+ pos)))) + (setq pos (+ pos 2)) + (aset block-low idx (+ (* (aref string pos) 256) + (aref string (1+ pos)))) + (setq pos (+ pos 2)) + (setq idx (1+ idx))) + (sha1-block block-high block-low)) + ;; last block. + (if (prog1 + (< (- len lim) 56) + (setq lim (- len rem)) + (setq idx 0) + (while (< pos lim) + (aset block-high idx (+ (* (aref string pos) 256) + (aref string (1+ pos)))) + (setq pos (+ pos 2)) + (aset block-low idx (+ (* (aref string pos) 256) + (aref string (1+ pos)))) + (setq pos (+ pos 2)) + (setq idx (1+ idx))) + ;; this is the last (at most) 32bit word. + (cond + ((= rem 3) + (aset block-high idx (+ (* (aref string pos) 256) + (aref string (1+ pos)))) + (setq pos (+ pos 2)) + (aset block-low idx (+ (* (aref string pos) 256) + 128))) + ((= rem 2) + (aset block-high idx (+ (* (aref string pos) 256) + (aref string (1+ pos)))) + (aset block-low idx 32768)) + ((= rem 1) + (aset block-high idx (+ (* (aref string pos) 256) + 128)) + (aset block-low idx 0)) + (t ;; (= rem 0) + (aset block-high idx 32768) + (aset block-low idx 0))) + (setq idx (1+ idx)) + (while (< idx 16) + (aset block-high idx 0) + (aset block-low idx 0) + (setq idx (1+ idx)))) + ;; last block has enough room to write the length of string. + (progn + ;; write bit length of string to last 4 bytes of the block. + (aset block-low 15 (* (% len 8192) 8)) + (setq len (/ len 8192)) + (aset block-high 15 (% len 65536)) + ;; XXX: It is not practical to compute SHA1 of + ;; such a huge message on emacs. + ;; (setq len (/ len 65536)) ; for 64bit emacs. + ;; (aset block-low 14 (% len 65536)) + ;; (aset block-high 14 (/ len 65536)) + (sha1-block block-high block-low)) + ;; need one more block. + (sha1-block block-high block-low) + (fillarray block-high 0) + (fillarray block-low 0) + ;; write bit length of string to last 4 bytes of the block. + (aset block-low 15 (* (% len 8192) 8)) + (setq len (/ len 8192)) + (aset block-high 15 (% len 65536)) + ;; XXX: It is not practical to compute SHA1 of + ;; such a huge message on emacs. + ;; (setq len (/ len 65536)) ; for 64bit emacs. + ;; (aset block-low 14 (% len 65536)) + ;; (aset block-high 14 (/ len 65536)) + (sha1-block block-high block-low)) + ;; make output string (in binary form). + (let ((result (make-string 20 0))) + (aset result 0 (/ sha1-H0-high 256)) + (aset result 1 (% sha1-H0-high 256)) + (aset result 2 (/ sha1-H0-low 256)) + (aset result 3 (% sha1-H0-low 256)) + (aset result 4 (/ sha1-H1-high 256)) + (aset result 5 (% sha1-H1-high 256)) + (aset result 6 (/ sha1-H1-low 256)) + (aset result 7 (% sha1-H1-low 256)) + (aset result 8 (/ sha1-H2-high 256)) + (aset result 9 (% sha1-H2-high 256)) + (aset result 10 (/ sha1-H2-low 256)) + (aset result 11 (% sha1-H2-low 256)) + (aset result 12 (/ sha1-H3-high 256)) + (aset result 13 (% sha1-H3-high 256)) + (aset result 14 (/ sha1-H3-low 256)) + (aset result 15 (% sha1-H3-low 256)) + (aset result 16 (/ sha1-H4-high 256)) + (aset result 17 (% sha1-H4-high 256)) + (aset result 18 (/ sha1-H4-low 256)) + (aset result 19 (% sha1-H4-low 256)) + result)) + ;; do not leave a copy of input string. + (fillarray block-high nil) + (fillarray block-low nil)))) + +(defun sha1-string-internal (string) + (encode-hex-string (sha1-binary string))) + +(defun sha1-region-internal (beg end) + (sha1-string-internal (buffer-substring-no-properties beg end))) + +;;; +;;; application interface. +;;; + +(defun sha1-region (beg end) + (if (and sha1-use-external + sha1-maximum-internal-length + (> (abs (- end beg)) sha1-maximum-internal-length)) + (sha1-region-external beg end) + (sha1-region-internal beg end))) + +(defun sha1-string (string) + (if (and sha1-use-external + sha1-maximum-internal-length + (> (length string) sha1-maximum-internal-length)) + (sha1-string-external string) + (sha1-string-internal string))) + +(defun sha1 (object &optional beg end) + "Return the SHA1 (Secure Hash Algorithm) of an object. +OBJECT is either a string or a buffer. +Optional arguments BEG and END denote buffer positions for computing the +hash of a portion of OBJECT." + (if (stringp object) + (sha1-string object) + (save-excursion + (set-buffer object) + (sha1-region (or beg (point-min)) (or end (point-max)))))) + +(provide 'sha1-el) + +;;; sha1-el.el ends here diff --git a/lisp/sieve-manage.el b/lisp/sieve-manage.el new file mode 100644 index 0000000..8897dd1 --- /dev/null +++ b/lisp/sieve-manage.el @@ -0,0 +1,614 @@ +;;; sieve-manage.el --- Implementation of the managesive protocol in elisp +;; Copyright (C) 2001, 2003 Free Software Foundation, Inc. + +;; Author: Simon Josefsson + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; This library provides an elisp API for the managesieve network +;; protocol. +;; +;; Currently only the CRAM-MD5 authentication mechanism is supported. +;; +;; The API should be fairly obvious for anyone familiar with the +;; managesieve protocol, interface functions include: +;; +;; `sieve-manage-open' +;; open connection to managesieve server, returning a buffer to be +;; used by all other API functions. +;; +;; `sieve-manage-opened' +;; check if a server is open or not +;; +;; `sieve-manage-close' +;; close a server connection. +;; +;; `sieve-manage-authenticate' +;; `sieve-manage-listscripts' +;; `sieve-manage-deletescript' +;; `sieve-manage-getscript' +;; performs managesieve protocol actions +;; +;; and that's it. Example of a managesieve session in *scratch*: +;; +;; (setq my-buf (sieve-manage-open "my.server.com")) +;; " *sieve* my.server.com:2000*" +;; +;; (sieve-manage-authenticate "myusername" "mypassword" my-buf) +;; 'auth +;; +;; (sieve-manage-listscripts my-buf) +;; ("vacation" "testscript" ("splitmail") "badscript") +;; +;; References: +;; +;; draft-martin-managesieve-02.txt, +;; "A Protocol for Remotely Managing Sieve Scripts", +;; by Tim Martin. +;; +;; Release history: +;; +;; 2001-10-31 Committed to Oort Gnus. +;; 2002-07-27 Added DELETESCRIPT. Suggested by Ned Ludd. + +;;; Code: + +(require 'rfc2104) +(or (fboundp 'md5) + (require 'md5)) +(eval-and-compile + (autoload 'starttls-open-stream "starttls")) + +;; User customizable variables: + +(defgroup sieve-manage nil + "Low-level Managesieve protocol issues." + :group 'mail + :prefix "sieve-") + +(defcustom sieve-manage-log "*sieve-manage-log*" + "Name of buffer for managesieve session trace." + :type 'string) + +(defcustom sieve-manage-default-user (user-login-name) + "Default username to use." + :type 'string) + +(defcustom sieve-manage-server-eol "\r\n" + "The EOL string sent from the server." + :type 'string) + +(defcustom sieve-manage-client-eol "\r\n" + "The EOL string we send to the server." + :type 'string) + +(defcustom sieve-manage-streams '(network starttls shell) + "Priority of streams to consider when opening connection to server.") + +(defcustom sieve-manage-stream-alist + '((network sieve-manage-network-p sieve-manage-network-open) + (shell sieve-manage-shell-p sieve-manage-shell-open) + (starttls sieve-manage-starttls-p sieve-manage-starttls-open)) + "Definition of network streams. + +\(NAME CHECK OPEN) + +NAME names the stream, CHECK is a function returning non-nil if the +server support the stream and OPEN is a function for opening the +stream.") + +(defcustom sieve-manage-authenticators '(cram-md5 plain) + "Priority of authenticators to consider when authenticating to server.") + +(defcustom sieve-manage-authenticator-alist + '((cram-md5 sieve-manage-cram-md5-p sieve-manage-cram-md5-auth) + (plain sieve-manage-plain-p sieve-manage-plain-auth)) + "Definition of authenticators. + +\(NAME CHECK AUTHENTICATE) + +NAME names the authenticator. CHECK is a function returning non-nil if +the server support the authenticator and AUTHENTICATE is a function +for doing the actual authentication.") + +(defcustom sieve-manage-default-port 2000 + "Default port number for managesieve protocol." + :type 'integer) + +;; Internal variables: + +(defconst sieve-manage-local-variables '(sieve-manage-server + sieve-manage-port + sieve-manage-auth + sieve-manage-stream + sieve-manage-username + sieve-manage-password + sieve-manage-process + sieve-manage-client-eol + sieve-manage-server-eol + sieve-manage-capability)) +(defconst sieve-manage-default-stream 'network) +(defconst sieve-manage-coding-system-for-read 'binary) +(defconst sieve-manage-coding-system-for-write 'binary) +(defvar sieve-manage-stream nil) +(defvar sieve-manage-auth nil) +(defvar sieve-manage-server nil) +(defvar sieve-manage-port nil) +(defvar sieve-manage-username nil) +(defvar sieve-manage-password nil) +(defvar sieve-manage-state 'closed + "Managesieve state. +Valid states are `closed', `initial', `nonauth', and `auth'.") +(defvar sieve-manage-process nil) +(defvar sieve-manage-capability nil) + +;; Internal utility functions + +(defsubst sieve-manage-disable-multibyte () + "Enable multibyte in the current buffer." + (when (fboundp 'set-buffer-multibyte) + (set-buffer-multibyte nil))) + +;; Uses the dynamically bound `reason' variable. +(defvar reason) +(defun sieve-manage-interactive-login (buffer loginfunc) + "Login to server in BUFFER. +LOGINFUNC is passed a username and a password, it should return t if +it where sucessful authenticating itself to the server, nil otherwise. +Returns t if login was successful, nil otherwise." + (with-current-buffer buffer + (make-variable-buffer-local 'sieve-manage-username) + (make-variable-buffer-local 'sieve-manage-password) + (let (user passwd ret reason) + ;; (condition-case () + (while (or (not user) (not passwd)) + (setq user (or sieve-manage-username + (read-from-minibuffer + (concat "Managesieve username for " + sieve-manage-server ": ") + (or user sieve-manage-default-user)))) + (setq passwd (or sieve-manage-password + (read-passwd + (concat "Managesieve password for " user "@" + sieve-manage-server ": ")))) + (when (and user passwd) + (if (funcall loginfunc user passwd) + (progn + (setq ret t + sieve-manage-username user) + (if (and (not sieve-manage-password) + (y-or-n-p "Store password for this session? ")) + (setq sieve-manage-password passwd))) + (if reason + (message "Login failed (reason given: %s)..." reason) + (message "Login failed...")) + (setq reason nil) + (setq passwd nil) + (sit-for 1)))) + ;; (quit (with-current-buffer buffer + ;; (setq user nil + ;; passwd nil))) + ;; (error (with-current-buffer buffer + ;; (setq user nil + ;; passwd nil)))) + ret))) + +(defun sieve-manage-erase (&optional p buffer) + (let ((buffer (or buffer (current-buffer)))) + (and sieve-manage-log + (with-current-buffer (get-buffer-create sieve-manage-log) + (sieve-manage-disable-multibyte) + (buffer-disable-undo) + (goto-char (point-max)) + (insert-buffer-substring buffer (with-current-buffer buffer + (point-min)) + (or p (with-current-buffer buffer + (point-max))))))) + (delete-region (point-min) (or p (point-max)))) + +(defun sieve-manage-open-1 (buffer) + (with-current-buffer buffer + (sieve-manage-erase) + (setq sieve-manage-state 'initial + sieve-manage-process + (condition-case () + (funcall (nth 2 (assq sieve-manage-stream + sieve-manage-stream-alist)) + "sieve" buffer sieve-manage-server sieve-manage-port) + ((error quit) nil))) + (when sieve-manage-process + (while (and (eq sieve-manage-state 'initial) + (memq (process-status sieve-manage-process) '(open run))) + (message "Waiting for response from %s..." sieve-manage-server) + (accept-process-output sieve-manage-process 1)) + (message "Waiting for response from %s...done" sieve-manage-server) + (and (memq (process-status sieve-manage-process) '(open run)) + sieve-manage-process)))) + +;; Streams + +(defun sieve-manage-network-p (buffer) + t) + +(defun sieve-manage-network-open (name buffer server port) + (let* ((port (or port sieve-manage-default-port)) + (coding-system-for-read sieve-manage-coding-system-for-read) + (coding-system-for-write sieve-manage-coding-system-for-write) + (process (open-network-stream name buffer server port))) + (when process + (while (and (memq (process-status process) '(open run)) + (set-buffer buffer) ;; XXX "blue moon" nntp.el bug + (goto-char (point-min)) + (not (sieve-manage-parse-greeting-1))) + (accept-process-output process 1) + (sit-for 1)) + (sieve-manage-erase nil buffer) + (when (memq (process-status process) '(open run)) + process)))) + +(defun imap-starttls-p (buffer) + ;; (and (imap-capability 'STARTTLS buffer) + (condition-case () + (progn + (require 'starttls) + (call-process "starttls")) + (error nil))) + +(defun imap-starttls-open (name buffer server port) + (let* ((port (or port sieve-manage-default-port)) + (coding-system-for-read sieve-manage-coding-system-for-read) + (coding-system-for-write sieve-manage-coding-system-for-write) + (process (starttls-open-stream name buffer server port)) + done) + (when process + (while (and (memq (process-status process) '(open run)) + (set-buffer buffer) ;; XXX "blue moon" nntp.el bug + (goto-char (point-min)) + (not (sieve-manage-parse-greeting-1))) + (accept-process-output process 1) + (sit-for 1)) + (sieve-manage-erase nil buffer) + (sieve-manage-send "STARTTLS") + (starttls-negotiate process)) + (when (memq (process-status process) '(open run)) + process))) + +;; Authenticators + +(defun sieve-manage-plain-p (buffer) + (sieve-manage-capability "SASL" "PLAIN" buffer)) + +(defun sieve-manage-plain-auth (buffer) + "Login to managesieve server using the PLAIN SASL method." + (let* ((done (sieve-manage-interactive-login + buffer + (lambda (user passwd) + (sieve-manage-send (concat "AUTHENTICATE \"PLAIN\" \"" + (base64-encode-string + (concat (char-to-string 0) + user + (char-to-string 0) + passwd)) + "\"")) + (let ((rsp (sieve-manage-parse-okno))) + (if (sieve-manage-ok-p rsp) + t + (setq reason (cdr-safe rsp)) + nil)))))) + (if done + (message "sieve: Authenticating using PLAIN...done") + (message "sieve: Authenticating using PLAIN...failed")))) + +(defun sieve-manage-cram-md5-p (buffer) + (sieve-manage-capability "SASL" "CRAM-MD5" buffer)) + +(defun sieve-manage-cram-md5-auth (buffer) + "Login to managesieve server using the CRAM-MD5 SASL method." + (message "sieve: Authenticating using CRAM-MD5...") + (let* ((done (sieve-manage-interactive-login + buffer + (lambda (user passwd) + (sieve-manage-send "AUTHENTICATE \"CRAM-MD5\"") + (sieve-manage-send + (concat + "\"" + (base64-encode-string + (concat + user " " + (rfc2104-hash 'md5 64 16 passwd + (base64-decode-string + (prog1 + (sieve-manage-parse-string) + (sieve-manage-erase)))))) + "\"")) + (let ((rsp (sieve-manage-parse-okno))) + (if (sieve-manage-ok-p rsp) + t + (setq reason (cdr-safe rsp)) + nil)))))) + (if done + (message "sieve: Authenticating using CRAM-MD5...done") + (message "sieve: Authenticating using CRAM-MD5...failed")))) + +;; Managesieve API + +(defun sieve-manage-open (server &optional port stream auth buffer) + "Open a network connection to a managesieve SERVER (string). +Optional variable PORT is port number (integer) on remote server. +Optional variable STREAM is any of `sieve-manage-streams' (a symbol). +Optional variable AUTH indicates authenticator to use, see +`sieve-manage-authenticators' for available authenticators. If nil, chooses +the best stream the server is capable of. +Optional variable BUFFER is buffer (buffer, or string naming buffer) +to work in." + (setq buffer (or buffer (format " *sieve* %s:%d" server (or port 2000)))) + (with-current-buffer (get-buffer-create buffer) + (mapcar 'make-variable-buffer-local sieve-manage-local-variables) + (sieve-manage-disable-multibyte) + (buffer-disable-undo) + (setq sieve-manage-server (or server sieve-manage-server)) + (setq sieve-manage-port (or port sieve-manage-port)) + (setq sieve-manage-stream (or stream sieve-manage-stream)) + (message "sieve: Connecting to %s..." sieve-manage-server) + (if (let ((sieve-manage-stream + (or sieve-manage-stream sieve-manage-default-stream))) + (sieve-manage-open-1 buffer)) + ;; Choose stream. + (let (stream-changed) + (message "sieve: Connecting to %s...done" sieve-manage-server) + (when (null sieve-manage-stream) + (let ((streams sieve-manage-streams)) + (while (setq stream (pop streams)) + (if (funcall (nth 1 (assq stream + sieve-manage-stream-alist)) buffer) + (setq stream-changed + (not (eq (or sieve-manage-stream + sieve-manage-default-stream) + stream)) + sieve-manage-stream stream + streams nil))) + (unless sieve-manage-stream + (error "Couldn't figure out a stream for server")))) + (when stream-changed + (message "sieve: Reconnecting with stream `%s'..." + sieve-manage-stream) + (sieve-manage-close buffer) + (if (sieve-manage-open-1 buffer) + (message "sieve: Reconnecting with stream `%s'...done" + sieve-manage-stream) + (message "sieve: Reconnecting with stream `%s'...failed" + sieve-manage-stream)) + (setq sieve-manage-capability nil)) + (if (sieve-manage-opened buffer) + ;; Choose authenticator + (when (and (null sieve-manage-auth) + (not (eq sieve-manage-state 'auth))) + (let ((auths sieve-manage-authenticators)) + (while (setq auth (pop auths)) + (if (funcall (nth 1 (assq + auth + sieve-manage-authenticator-alist)) + buffer) + (setq sieve-manage-auth auth + auths nil))) + (unless sieve-manage-auth + (error "Couldn't figure out authenticator for server")))))) + (message "sieve: Connecting to %s...failed" sieve-manage-server)) + (when (sieve-manage-opened buffer) + (sieve-manage-erase) + buffer))) + +(defun sieve-manage-opened (&optional buffer) + "Return non-nil if connection to managesieve server in BUFFER is open. +If BUFFER is nil then the current buffer is used." + (and (setq buffer (get-buffer (or buffer (current-buffer)))) + (buffer-live-p buffer) + (with-current-buffer buffer + (and sieve-manage-process + (memq (process-status sieve-manage-process) '(open run)))))) + +(defun sieve-manage-close (&optional buffer) + "Close connection to managesieve server in BUFFER. +If BUFFER is nil, the current buffer is used." + (with-current-buffer (or buffer (current-buffer)) + (when (sieve-manage-opened) + (sieve-manage-send "LOGOUT") + (sit-for 1)) + (when (and sieve-manage-process + (memq (process-status sieve-manage-process) '(open run))) + (delete-process sieve-manage-process)) + (setq sieve-manage-process nil) + (sieve-manage-erase) + t)) + +(defun sieve-manage-authenticate (&optional user passwd buffer) + "Authenticate to server in BUFFER, using current buffer if nil. +It uses the authenticator specified when opening the server. If the +authenticator requires username/passwords, they are queried from the +user and optionally stored in the buffer. If USER and/or PASSWD is +specified, the user will not be questioned and the username and/or +password is remembered in the buffer." + (with-current-buffer (or buffer (current-buffer)) + (if (not (eq sieve-manage-state 'nonauth)) + (eq sieve-manage-state 'auth) + (make-variable-buffer-local 'sieve-manage-username) + (make-variable-buffer-local 'sieve-manage-password) + (if user (setq sieve-manage-username user)) + (if passwd (setq sieve-manage-password passwd)) + (if (funcall (nth 2 (assq sieve-manage-auth + sieve-manage-authenticator-alist)) buffer) + (setq sieve-manage-state 'auth))))) + +(defun sieve-manage-capability (&optional name value buffer) + (with-current-buffer (or buffer (current-buffer)) + (if (null name) + sieve-manage-capability + (if (null value) + (nth 1 (assoc name sieve-manage-capability)) + (when (string-match value (nth 1 (assoc name sieve-manage-capability))) + (nth 1 (assoc name sieve-manage-capability))))))) + +(defun sieve-manage-listscripts (&optional buffer) + (with-current-buffer (or buffer (current-buffer)) + (sieve-manage-send "LISTSCRIPTS") + (sieve-manage-parse-listscripts))) + +(defun sieve-manage-havespace (name size &optional buffer) + (with-current-buffer (or buffer (current-buffer)) + (sieve-manage-send (format "HAVESPACE \"%s\" %s" name size)) + (sieve-manage-parse-okno))) + +(eval-and-compile + (if (fboundp 'string-bytes) + (defalias 'sieve-string-bytes 'string-bytes) + (defalias 'sieve-string-bytes 'length))) + +(defun sieve-manage-putscript (name content &optional buffer) + (with-current-buffer (or buffer (current-buffer)) + (sieve-manage-send (format "PUTSCRIPT \"%s\" {%d+}%s%s" name + (sieve-string-bytes content) + sieve-manage-client-eol content)) + (sieve-manage-parse-okno))) + +(defun sieve-manage-deletescript (name &optional buffer) + (with-current-buffer (or buffer (current-buffer)) + (sieve-manage-send (format "DELETESCRIPT \"%s\"" name)) + (sieve-manage-parse-okno))) + +(defun sieve-manage-getscript (name output-buffer &optional buffer) + (with-current-buffer (or buffer (current-buffer)) + (sieve-manage-send (format "GETSCRIPT \"%s\"" name)) + (let ((script (sieve-manage-parse-string))) + (sieve-manage-parse-crlf) + (with-current-buffer output-buffer + (insert script)) + (sieve-manage-parse-okno)))) + +(defun sieve-manage-setactive (name &optional buffer) + (with-current-buffer (or buffer (current-buffer)) + (sieve-manage-send (format "SETACTIVE \"%s\"" name)) + (sieve-manage-parse-okno))) + +;; Protocol parsing routines + +(defun sieve-manage-ok-p (rsp) + (string= (downcase (or (car-safe rsp) "")) "ok")) + +(defsubst sieve-manage-forward () + (or (eobp) (forward-char))) + +(defun sieve-manage-is-okno () + (when (looking-at (concat + "^\\(OK\\|NO\\)\\( (\\([^)]+\\))\\)?\\( \\(.*\\)\\)?" + sieve-manage-server-eol)) + (let ((status (match-string 1)) + (resp-code (match-string 3)) + (response (match-string 5))) + (when response + (goto-char (match-beginning 5)) + (setq response (sieve-manage-is-string))) + (list status resp-code response)))) + +(defun sieve-manage-parse-okno () + (let (rsp) + (while (null rsp) + (accept-process-output (get-buffer-process (current-buffer)) 1) + (goto-char (point-min)) + (setq rsp (sieve-manage-is-okno))) + (sieve-manage-erase) + rsp)) + +(defun sieve-manage-parse-capability-1 () + "Accept a managesieve greeting." + (let (str) + (while (setq str (sieve-manage-is-string)) + (if (eq (char-after) ? ) + (progn + (sieve-manage-forward) + (push (list str (sieve-manage-is-string)) + sieve-manage-capability)) + (push (list str) sieve-manage-capability)) + (forward-line))) + (when (re-search-forward (concat "^OK" sieve-manage-server-eol) nil t) + (setq sieve-manage-state 'nonauth))) + +(defalias 'sieve-manage-parse-greeting-1 'sieve-manage-parse-capability-1) + +(defun sieve-manage-is-string () + (cond ((looking-at "\"\\([^\"]+\\)\"") + (prog1 + (match-string 1) + (goto-char (match-end 0)))) + ((looking-at (concat "{\\([0-9]+\\)}" sieve-manage-server-eol)) + (let ((pos (match-end 0)) + (len (string-to-number (match-string 1)))) + (if (< (point-max) (+ pos len)) + nil + (goto-char (+ pos len)) + (buffer-substring pos (+ pos len))))))) + +(defun sieve-manage-parse-string () + (let (rsp) + (while (null rsp) + (accept-process-output (get-buffer-process (current-buffer)) 1) + (goto-char (point-min)) + (setq rsp (sieve-manage-is-string))) + (sieve-manage-erase (point)) + rsp)) + +(defun sieve-manage-parse-crlf () + (when (looking-at sieve-manage-server-eol) + (sieve-manage-erase (match-end 0)))) + +(defun sieve-manage-parse-listscripts () + (let (tmp rsp data) + (while (null rsp) + (while (null (or (setq rsp (sieve-manage-is-okno)) + (setq tmp (sieve-manage-is-string)))) + (accept-process-output (get-buffer-process (current-buffer)) 1) + (goto-char (point-min))) + (when tmp + (while (not (looking-at (concat "\\( ACTIVE\\)?" + sieve-manage-server-eol))) + (accept-process-output (get-buffer-process (current-buffer)) 1) + (goto-char (point-min))) + (if (match-string 1) + (push (cons 'active tmp) data) + (push tmp data)) + (goto-char (match-end 0)) + (setq tmp nil))) + (sieve-manage-erase) + (if (sieve-manage-ok-p rsp) + data + rsp))) + +(defun sieve-manage-send (cmdstr) + (setq cmdstr (concat cmdstr sieve-manage-client-eol)) + (and sieve-manage-log + (with-current-buffer (get-buffer-create sieve-manage-log) + (sieve-manage-disable-multibyte) + (buffer-disable-undo) + (goto-char (point-max)) + (insert cmdstr))) + (process-send-string sieve-manage-process cmdstr)) + +(provide 'sieve-manage) + +;; sieve-manage.el ends here diff --git a/lisp/sieve-mode.el b/lisp/sieve-mode.el new file mode 100644 index 0000000..e4945c9 --- /dev/null +++ b/lisp/sieve-mode.el @@ -0,0 +1,204 @@ +;;; sieve-mode.el --- Sieve code editing commands for Emacs +;; Copyright (C) 2001, 2003 Free Software Foundation, Inc. + +;; Author: Simon Josefsson + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; This file contain editing mode functions and font-lock support for +;; editing Sieve scripts. It sets up C-mode with support for +;; sieve-style #-comments and a lightly hacked syntax table. It was +;; strongly influenced by awk-mode.el. +;; +;; Put something similar to the following in your .emacs to use this file: +;; +;; (load "~/lisp/sieve") +;; (setq auto-mode-alist (cons '("\\.siv\\'" . sieve-mode) auto-mode-alist)) +;; +;; References: +;; +;; RFC 3028, +;; "Sieve: A Mail Filtering Language", +;; by Tim Showalter. +;; +;; Release history: +;; +;; 2001-03-02 version 1.0 posted to gnu.emacs.sources +;; version 1.1 change file extension into ".siv" (official one) +;; added keymap and menubar to hook into sieve-manage +;; 2001-10-31 version 1.2 committed to Oort Gnus + +;;; Code: + +(autoload 'sieve-manage "sieve") +(autoload 'sieve-upload "sieve") +(autoload 'c-mode "cc-mode") +(require 'easymenu) +(eval-when-compile + (require 'font-lock)) + +(defgroup sieve nil + "Sieve." + :group 'languages) + +(defcustom sieve-mode-hook nil + "Hook run in sieve mode buffers." + :group 'sieve + :type 'hook) + +;; Font-lock + +(defvar sieve-control-commands-face 'sieve-control-commands-face + "Face name used for Sieve Control Commands.") + +(defface sieve-control-commands-face + '((((type tty) (class color)) (:foreground "blue" :weight light)) + (((class grayscale) (background light)) (:foreground "LightGray" :bold t)) + (((class grayscale) (background dark)) (:foreground "DimGray" :bold t)) + (((class color) (background light)) (:foreground "Orchid")) + (((class color) (background dark)) (:foreground "LightSteelBlue")) + (t (:bold t))) + "Face used for Sieve Control Commands.") + +(defvar sieve-action-commands-face 'sieve-action-commands-face + "Face name used for Sieve Action Commands.") + +(defface sieve-action-commands-face + '((((type tty) (class color)) (:foreground "blue" :weight bold)) + (((class color) (background light)) (:foreground "Blue")) + (((class color) (background dark)) (:foreground "LightSkyBlue")) + (t (:inverse-video t :bold t))) + "Face used for Sieve Action Commands.") + +(defvar sieve-test-commands-face 'sieve-test-commands-face + "Face name used for Sieve Test Commands.") + +(defface sieve-test-commands-face + '((((type tty) (class color)) (:foreground "magenta")) + (((class grayscale) (background light)) + (:foreground "LightGray" :bold t :underline t)) + (((class grayscale) (background dark)) + (:foreground "Gray50" :bold t :underline t)) + (((class color) (background light)) (:foreground "CadetBlue")) + (((class color) (background dark)) (:foreground "Aquamarine")) + (t (:bold t :underline t))) + "Face used for Sieve Test Commands.") + +(defvar sieve-tagged-arguments-face 'sieve-tagged-arguments-face + "Face name used for Sieve Tagged Arguments.") + +(defface sieve-tagged-arguments-face + '((((type tty) (class color)) (:foreground "cyan" :weight bold)) + (((class grayscale) (background light)) (:foreground "LightGray" :bold t)) + (((class grayscale) (background dark)) (:foreground "DimGray" :bold t)) + (((class color) (background light)) (:foreground "Purple")) + (((class color) (background dark)) (:foreground "Cyan")) + (t (:bold t))) + "Face used for Sieve Tagged Arguments.") + + +(defconst sieve-font-lock-keywords + (eval-when-compile + (list + ;; control commands + (cons (regexp-opt '("require" "if" "else" "elsif" "stop")) + 'sieve-control-commands-face) + ;; action commands + (cons (regexp-opt '("fileinto" "redirect" "reject" "keep" "discard")) + 'sieve-action-commands-face) + ;; test commands + (cons (regexp-opt '("address" "allof" "anyof" "exists" "false" + "true" "header" "not" "size" "envelope")) + 'sieve-test-commands-face) + (cons "\\Sw+:\\sw+" + 'sieve-tagged-arguments-face)))) + +;; Syntax table + +(defvar sieve-mode-syntax-table nil + "Syntax table in use in sieve-mode buffers.") + +(if sieve-mode-syntax-table + () + (setq sieve-mode-syntax-table (make-syntax-table)) + (modify-syntax-entry ?\\ "\\" sieve-mode-syntax-table) + (modify-syntax-entry ?\n "> " sieve-mode-syntax-table) + (modify-syntax-entry ?\f "> " sieve-mode-syntax-table) + (modify-syntax-entry ?\# "< " sieve-mode-syntax-table) + (modify-syntax-entry ?/ "." sieve-mode-syntax-table) + (modify-syntax-entry ?* "." sieve-mode-syntax-table) + (modify-syntax-entry ?+ "." sieve-mode-syntax-table) + (modify-syntax-entry ?- "." sieve-mode-syntax-table) + (modify-syntax-entry ?= "." sieve-mode-syntax-table) + (modify-syntax-entry ?% "." sieve-mode-syntax-table) + (modify-syntax-entry ?< "." sieve-mode-syntax-table) + (modify-syntax-entry ?> "." sieve-mode-syntax-table) + (modify-syntax-entry ?& "." sieve-mode-syntax-table) + (modify-syntax-entry ?| "." sieve-mode-syntax-table) + (modify-syntax-entry ?_ "_" sieve-mode-syntax-table) + (modify-syntax-entry ?\' "\"" sieve-mode-syntax-table)) + +;; Key map definition + +(defvar sieve-mode-map + (let ((map (make-sparse-keymap))) + (define-key map "\C-c\C-l" 'sieve-upload) + (define-key map "\C-c\C-c" 'sieve-upload-and-bury) + (define-key map "\C-c\C-m" 'sieve-manage) + map) + "Key map used in sieve mode.") + +;; Menu definition + +(defvar sieve-mode-menu nil + "Menubar used in sieve mode.") + +;; Code for Sieve editing mode. + +;;;###autoload +(define-derived-mode sieve-mode c-mode "Sieve" + "Major mode for editing Sieve code. +This is much like C mode except for the syntax of comments. Its keymap +inherits from C mode's and it has the same variables for customizing +indentation. It has its own abbrev table and its own syntax table. + +Turning on Sieve mode runs `sieve-mode-hook'." + (set (make-local-variable 'paragraph-start) (concat "$\\|" page-delimiter)) + (set (make-local-variable 'paragraph-separate) paragraph-start) + (set (make-local-variable 'comment-start) "#") + (set (make-local-variable 'comment-end) "") + ;;(set (make-local-variable 'comment-start-skip) "\\(^\\|\\s-\\);?#+ *") + (set (make-local-variable 'comment-start-skip) "#+ *") + (unless (featurep 'xemacs) + (set (make-local-variable 'font-lock-defaults) + '(sieve-font-lock-keywords nil nil ((?_ . "w"))))) + (easy-menu-add-item nil nil sieve-mode-menu)) + +;; Menu + +(easy-menu-define sieve-mode-menu sieve-mode-map + "Sieve Menu." + '("Sieve" + ["Upload script" sieve-upload t] + ["Manage scripts on server" sieve-manage t])) + +(provide 'sieve-mode) + +;; sieve-mode.el ends here diff --git a/lisp/sieve.el b/lisp/sieve.el new file mode 100644 index 0000000..8297f57 --- /dev/null +++ b/lisp/sieve.el @@ -0,0 +1,383 @@ +;;; sieve.el --- Utilities to manage sieve scripts +;; Copyright (C) 2001, 2003 Free Software Foundation, Inc. + +;; Author: Simon Josefsson + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; This file contain utilities to facilate upload, download and +;; general management of sieve scripts. Currently only the +;; Managesieve protocol is supported (using sieve-manage.el), but when +;; (useful) alternatives become available, they might be supported as +;; well. +;; +;; The cursor navigation was inspired by biff-mode by Franklin Lee. +;; +;; Release history: +;; +;; 2001-10-31 Committed to Oort Gnus. +;; 2002-07-27 Fix down-mouse-2 and down-mouse-3 in manage-mode. Fix menubar +;; in manage-mode. Change some messages. Added sieve-deactivate*, +;; sieve-remove. Fixed help text in manage-mode. Suggested by +;; Ned Ludd. +;; +;; Todo: +;; +;; * Namespace? This file contains `sieve-manage' and +;; `sieve-manage-mode', but there is a sieve-manage.el file as well. +;; Can't think of a good solution though, this file need a *-mode, +;; and naming it `sieve-mode' would collide with sieve-mode.el. One +;; solution would be to come up with some better name that this file +;; can use that doesn't have the managesieve specific "manage" in +;; it. sieve-dired? i dunno. we could copy all off sieve.el into +;; sieve-manage.el too, but I'd like to separate the interface from +;; the protocol implementation since the backends are likely to +;; change (well). +;; +;; * Define servers? We could have a customize buffer to create a server, +;; with authentication/stream/etc parameters, much like Gnus, and then +;; only use names of defined servers when interacting with M-x sieve-*. +;; Right now you can't use STARTTLS, which sieve-manage.el provides + +;;; Code: + +(require 'sieve-manage) +(require 'sieve-mode) + +;; User customizable variables: + +(defgroup sieve nil + "Manage sieve scripts." + :group 'tools) + +(defcustom sieve-new-script "" + "Name of name script indicator." + :type 'string + :group 'sieve) + +(defcustom sieve-buffer "*sieve*" + "Name of sieve management buffer." + :type 'string + :group 'sieve) + +(defcustom sieve-template "\ +require \"fileinto\"; + +# Example script (remove comment character '#' to make it effective!): +# +# if header :contains \"from\" \"coyote\" { +# discard; +# } elsif header :contains [\"subject\"] [\"$$$\"] { +# discard; +# } else { +# fileinto \"INBOX\"; +# } +" + "Template sieve script." + :type 'string + :group 'sieve) + +;; Internal variables: + +(defvar sieve-manage-buffer nil) +(defvar sieve-buffer-header-end nil) + +;; Sieve-manage mode: + +(defvar sieve-manage-mode-map nil + "Keymap for `sieve-manage-mode'.") + +(if sieve-manage-mode-map + () + (setq sieve-manage-mode-map (make-sparse-keymap)) + (suppress-keymap sieve-manage-mode-map) + ;; various + (define-key sieve-manage-mode-map "?" 'sieve-help) + (define-key sieve-manage-mode-map "h" 'sieve-help) + (define-key sieve-manage-mode-map "q" 'sieve-bury-buffer) + ;; activating + (define-key sieve-manage-mode-map "m" 'sieve-activate) + (define-key sieve-manage-mode-map "u" 'sieve-deactivate) + (define-key sieve-manage-mode-map "\M-\C-?" 'sieve-deactivate-all) + ;; navigation keys + (define-key sieve-manage-mode-map "\C-p" 'sieve-prev-line) + (define-key sieve-manage-mode-map [up] 'sieve-prev-line) + (define-key sieve-manage-mode-map "\C-n" 'sieve-next-line) + (define-key sieve-manage-mode-map [down] 'sieve-next-line) + (define-key sieve-manage-mode-map " " 'sieve-next-line) + (define-key sieve-manage-mode-map "n" 'sieve-next-line) + (define-key sieve-manage-mode-map "p" 'sieve-prev-line) + (define-key sieve-manage-mode-map "\C-m" 'sieve-edit-script) + (define-key sieve-manage-mode-map "f" 'sieve-edit-script) + (define-key sieve-manage-mode-map "o" 'sieve-edit-script-other-window) + (define-key sieve-manage-mode-map "r" 'sieve-remove) + (define-key sieve-manage-mode-map [(down-mouse-2)] 'sieve-edit-script) + (define-key sieve-manage-mode-map [(down-mouse-3)] 'sieve-manage-mode-menu)) + +(define-derived-mode sieve-manage-mode fundamental-mode "SIEVE" + "Mode used for sieve script management." + (setq mode-name "SIEVE") + (buffer-disable-undo (current-buffer)) + (setq truncate-lines t) + (easy-menu-add-item nil nil sieve-manage-mode-menu)) + +(put 'sieve-manage-mode 'mode-class 'special) + +(easy-menu-define sieve-manage-mode-menu sieve-manage-mode-map + "Sieve Menu." + '("Manage Sieve" + ["Edit script" sieve-edit-script t] + ["Activate script" sieve-activate t] + ["Deactivate script" sieve-deactivate t])) + +;; This is necessary to allow correct handling of \\[cvs-mode-diff-map] +;; in substitute-command-keys. +;(fset 'sieve-manage-mode-map sieve-manage-mode-map) + +;; Commands used in sieve-manage mode: + +(defun sieve-activate (&optional pos) + (interactive "d") + (let ((name (sieve-script-at-point)) err) + (when (or (null name) (string-equal name sieve-new-script)) + (error "No sieve script at point")) + (message "Activating script %s..." name) + (setq err (sieve-manage-setactive name sieve-manage-buffer)) + (sieve-refresh-scriptlist) + (if (sieve-manage-ok-p err) + (message "Activating script %s...done" name) + (message "Activating script %s...failed: %s" name (nth 2 err))))) + +(defun sieve-deactivate-all (&optional pos) + (interactive "d") + (let ((name (sieve-script-at-point)) err) + (message "Deactivating scripts...") + (setq err (sieve-manage-setactive "" sieve-manage-buffer)) + (sieve-refresh-scriptlist) + (if (sieve-manage-ok-p err) + (message "Deactivating scripts...done") + (message "Deactivating scripts...failed" (nth 2 err))))) + +(defalias 'sieve-deactivate 'sieve-deactivate-all) + +(defun sieve-remove (&optional pos) + (interactive "d") + (let ((name (sieve-script-at-point)) err) + (when (or (null name) (string-equal name sieve-new-script)) + (error "No sieve script at point")) + (message "Removing sieve script %s..." name) + (setq err (sieve-manage-deletescript name sieve-manage-buffer)) + (unless (sieve-manage-ok-p err) + (error "Removing sieve script %s...failed: " err)) + (sieve-refresh-scriptlist) + (message "Removing sieve script %s...done" name))) + +(defun sieve-edit-script (&optional pos) + (interactive "d") + (let ((name (sieve-script-at-point))) + (unless name + (error "No sieve script at point")) + (if (not (string-equal name sieve-new-script)) + (let ((newbuf (generate-new-buffer name)) + err) + (setq err (sieve-manage-getscript name newbuf sieve-manage-buffer)) + (switch-to-buffer newbuf) + (unless (sieve-manage-ok-p err) + (error "Sieve download failed: %s" err))) + (switch-to-buffer (get-buffer-create "template.siv")) + (insert sieve-template)) + (sieve-mode) + (message "Press C-c C-l to upload script to server."))) + +(defmacro sieve-change-region (&rest body) + "Turns off sieve-region before executing BODY, then re-enables it after. +Used to bracket operations which move point in the sieve-buffer." + `(progn + (sieve-highlight nil) + ,@body + (sieve-highlight t))) +(put 'sieve-change-region 'lisp-indent-function 0) + +(defun sieve-next-line (&optional arg) + (interactive) + (unless arg + (setq arg 1)) + (if (save-excursion + (forward-line arg) + (sieve-script-at-point)) + (sieve-change-region + (forward-line arg)) + (message "End of list"))) + +(defun sieve-prev-line (&optional arg) + (interactive) + (unless arg + (setq arg -1)) + (if (save-excursion + (forward-line arg) + (sieve-script-at-point)) + (sieve-change-region + (forward-line arg)) + (message "Beginning of list"))) + +(defun sieve-help () + "Display help for various sieve commands." + (interactive) + (if (eq last-command 'sieve-help) + ;; would need minor-mode for log-edit-mode + (describe-function 'sieve-mode) + (message (substitute-command-keys + "`\\[sieve-edit-script]':edit `\\[sieve-activate]':activate `\\[sieve-deactivate]':deactivate `\\[sieve-remove]':remove")))) + +(defun sieve-bury-buffer (buf &optional mainbuf) + "Hide the buffer BUF that was temporarily popped up. +BUF is assumed to be a temporary buffer used from the buffer MAINBUF." + (interactive (list (current-buffer))) + (save-current-buffer + (let ((win (if (eq buf (window-buffer (selected-window))) (selected-window) + (get-buffer-window buf t)))) + (when win + (if (window-dedicated-p win) + (condition-case () + (delete-window win) + (error (iconify-frame (window-frame win)))) + (if (and mainbuf (get-buffer-window mainbuf)) + (delete-window win))))) + (with-current-buffer buf + (bury-buffer (unless (and (eq buf (window-buffer (selected-window))) + (not (window-dedicated-p (selected-window)))) + buf))) + (when mainbuf + (let ((mainwin (or (get-buffer-window mainbuf) + (get-buffer-window mainbuf 'visible)))) + (when mainwin (select-window mainwin)))))) + +;; Create buffer: + +(defun sieve-setup-buffer (server port) + (setq buffer-read-only nil) + (erase-buffer) + (buffer-disable-undo) + (insert "\ +Server : " server ":" (or port "2000") " + +") + (set (make-local-variable 'sieve-buffer-header-end) + (point-max))) + +(defun sieve-script-at-point (&optional pos) + "Return name of sieve script at point POS, or nil." + (interactive "d") + (get-char-property (or pos (point)) 'script-name)) + +(eval-and-compile + (defalias 'sieve-make-overlay (if (fboundp 'make-overlay) + 'make-overlay + 'make-extent)) + (defalias 'sieve-overlay-put (if (fboundp 'overlay-put) + 'overlay-put + 'set-extent-property)) + (defalias 'sieve-overlays-at (if (fboundp 'overlays-at) + 'overlays-at + 'extents-at))) + +(defun sieve-highlight (on) + "Turn ON or off highlighting on the current language overlay." + (sieve-overlay-put (car (sieve-overlays-at (point))) + 'face (if on 'highlight 'default))) + +(defun sieve-insert-scripts (scripts) + "Format and insert LANGUAGE-LIST strings into current buffer at point." + (while scripts + (let ((p (point)) + (ext nil) + (script (pop scripts))) + (if (consp script) + (insert (format " ACTIVE %s" (cdr script))) + (insert (format " %s" script))) + (setq ext (sieve-make-overlay p (point))) + (sieve-overlay-put ext 'mouse-face 'highlight) + (sieve-overlay-put ext 'script-name (if (consp script) + (cdr script) + script)) + (insert "\n")))) + +(defun sieve-open-server (server &optional port) + ;; open server + (set (make-local-variable 'sieve-manage-buffer) + (sieve-manage-open server)) + ;; authenticate + (sieve-manage-authenticate nil nil sieve-manage-buffer)) + +(defun sieve-refresh-scriptlist () + (interactive) + (with-current-buffer sieve-buffer + (setq buffer-read-only nil) + (delete-region (or sieve-buffer-header-end (point-max)) (point-max)) + (goto-char (point-max)) + ;; get list of script names and print them + (let ((scripts (sieve-manage-listscripts sieve-manage-buffer))) + (if (null scripts) + (insert (format (concat "No scripts on server, press RET on %s to " + "create a new script.\n") sieve-new-script)) + (insert (format (concat "%d script%s on server, press RET on a script " + "name edits it, or\npress RET on %s to create " + "a new script.\n") (length scripts) + (if (eq (length scripts) 1) "" "s") + sieve-new-script))) + (save-excursion + (sieve-insert-scripts (list sieve-new-script)) + (sieve-insert-scripts scripts))) + (sieve-highlight t) + (setq buffer-read-only t))) + +;;;###autoload +(defun sieve-manage (server &optional port) + (interactive "sServer: ") + (switch-to-buffer (get-buffer-create sieve-buffer)) + (sieve-manage-mode) + (sieve-setup-buffer server port) + (if (sieve-open-server server port) + (sieve-refresh-scriptlist) + (message "Could not open server %s" server))) + +;;;###autoload +(defun sieve-upload (&optional name) + (interactive) + (unless name + (setq name (buffer-name))) + (when (or (get-buffer sieve-buffer) (call-interactively 'sieve-manage)) + (let ((script (buffer-string)) err) + (with-current-buffer (get-buffer sieve-buffer) + (setq err (sieve-manage-putscript name script sieve-manage-buffer)) + (if (sieve-manage-ok-p err) + (message (concat + "Sieve upload done. Use `C-c RET' to manage scripts.")) + (message "Sieve upload failed: %s" (nth 2 err))))))) + +;;;###autoload +(defun sieve-upload-and-bury (&optional name) + (interactive) + (sieve-upload name) + (bury-buffer)) + +(provide 'sieve) + +;; sieve.el ends here diff --git a/lisp/smime.el b/lisp/smime.el new file mode 100644 index 0000000..5701ec8 --- /dev/null +++ b/lisp/smime.el @@ -0,0 +1,619 @@ +;;; smime.el --- S/MIME support library +;; Copyright (c) 2000, 2001, 2003 Free Software Foundation, Inc. + +;; Author: Simon Josefsson +;; Keywords: SMIME X.509 PEM OpenSSL + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your +;; option) any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; This library perform S/MIME operations from within Emacs. +;; +;; Functions for fetching certificates from public repositories are +;; provided, currently only from DNS. LDAP support (via EUDC) is planned. +;; +;; It uses OpenSSL (tested with version 0.9.5a and 0.9.6) for signing, +;; encryption and decryption. +;; +;; Some general knowledge of S/MIME, X.509, PKCS#12, PEM etc is +;; probably required to use this library in any useful way. +;; Especially, don't expect this library to buy security for you. If +;; you don't understand what you are doing, you're as likely to lose +;; security than gain any by using this library. +;; +;; This library is not intended to provide a "raw" API for S/MIME, +;; PKCSx or similar, it's intended to perform common operations +;; done on messages encoded in these formats. The terminology chosen +;; reflect this. +;; +;; The home of this file is in Gnus CVS, but also available from +;; http://josefsson.org/smime.html. + +;;; Quick introduction: + +;; Get your S/MIME certificate from VeriSign or someplace. I used +;; Netscape to generate the key and certificate request and stuff, and +;; Netscape can export the key into PKCS#12 format. +;; +;; Enter OpenSSL. To be able to use this library, it need to have the +;; SMIME key readable in PEM format. OpenSSL is used to convert the +;; key: +;; +;; $ openssl pkcs12 -in mykey.p12 -clcerts -nodes > mykey.pem +;; ... +;; +;; Now, use M-x customize-variable smime-keys and add mykey.pem as +;; a key. +;; +;; Now you should be able to sign messages! Create a buffer and write +;; something and run M-x smime-sign-buffer RET RET and you should see +;; your message MIME armoured and a signature. Encryption, M-x +;; smime-encrypt-buffer, should also work. +;; +;; To be able to verify messages you need to build up trust with +;; someone. Perhaps you trust the CA that issued your certificate, at +;; least I did, so I export it's certificates from my PKCS#12 +;; certificate with: +;; +;; $ openssl pkcs12 -in mykey.p12 -cacerts -nodes > cacert.pem +;; ... +;; +;; Now, use M-x customize-variable smime-CAs and add cacert.pem as a +;; CA certificate. +;; +;; You should now be able to sign messages, and even verify messages +;; sent by others that use the same CA as you. + +;; Bugs: +;; +;; Don't complain that this package doesn't do encrypted PEM files, +;; submit a patch instead. I store my keys in a safe place, so I +;; didn't need the encryption. Also, programming was made easier by +;; that decision. One might think that this even influenced were I +;; store my keys, and one would probably be right. :-) +;; +;; Update: Mathias Herberts sent the patch. However, it uses +;; environment variables to pass the password to OpenSSL, which is +;; slightly insecure. Hence a new todo: use a better -passin method. +;; +;; Cache password for e.g. 1h +;; +;; Suggestions and comments are appreciated, mail me at simon@josefsson.org. + +;; begin rant +;; +;; I would include pointers to introductory text on concepts used in +;; this library here, but the material I've read are so horrible I +;; don't want to recomend them. +;; +;; Why can't someone write a simple introduction to all this stuff? +;; Until then, much of this resemble security by obscurity. +;; +;; Also, I'm not going to mention anything about the wonders of +;; cryptopolitics. Oops, I just did. +;; +;; end rant + +;;; Revision history: + +;; 2000-06-05 initial version, committed to Gnus CVS contrib/ +;; 2000-10-28 retrieve certificates via DNS CERT RRs +;; 2001-10-14 posted to gnu.emacs.sources + +;;; Code: + +(require 'dig) +(require 'comint) +(eval-when-compile (require 'cl)) + +(defgroup smime nil + "S/MIME configuration.") + +(defcustom smime-keys nil + "*Map mail addresses to a file containing Certificate (and private key). +The file is assumed to be in PEM format. You can also associate additional +certificates to be sent with every message to each address." + :type '(repeat (list (string :tag "Mail address") + (file :tag "File name") + (repeat :tag "Additional certificate files" + (file :tag "File name")))) + :group 'smime) + +(defcustom smime-CA-directory nil + "*Directory containing certificates for CAs you trust. +Directory should contain files (in PEM format) named to the X.509 +hash of the certificate. This can be done using OpenSSL such as: + +$ ln -s ca.pem `openssl x509 -noout -hash -in ca.pem`.0 + +where `ca.pem' is the file containing a PEM encoded X.509 CA +certificate." + :type '(choice (const :tag "none" nil) + directory) + :group 'smime) + +(defcustom smime-CA-file nil + "*Files containing certificates for CAs you trust. +File should contain certificates in PEM format." + :type '(choice (const :tag "none" nil) + file) + :group 'smime) + +(defcustom smime-certificate-directory "~/Mail/certs/" + "*Directory containing other people's certificates. +It should contain files named to the X.509 hash of the certificate, +and the files themself should be in PEM format." +;The S/MIME library provide simple functionality for fetching +;certificates into this directory, so there is no need to populate it +;manually. + :type 'directory + :group 'smime) + +(defcustom smime-openssl-program + (and (condition-case () + (eq 0 (call-process "openssl" nil nil nil "version")) + (error nil)) + "openssl") + "*Name of OpenSSL binary." + :type 'string + :group 'smime) + +;; OpenSSL option to select the encryption cipher + +(defcustom smime-encrypt-cipher "-des3" + "*Cipher algorithm used for encryption." + :type '(choice (const :tag "Triple DES" "-des3") + (const :tag "DES" "-des") + (const :tag "RC2 40 bits" "-rc2-40") + (const :tag "RC2 64 bits" "-rc2-64") + (const :tag "RC2 128 bits" "-rc2-128")) + :group 'smime) + +(defcustom smime-dns-server nil + "*DNS server to query certificates from. +If nil, use system defaults." + :type '(choice (const :tag "System defaults") + string) + :group 'smime) + +(defvar smime-details-buffer "*OpenSSL output*") + +(eval-and-compile + (defalias 'smime-make-temp-file + (if (fboundp 'make-temp-file) + 'make-temp-file + (lambda (prefix &optional dir-flag) ;; Simple implementation + (expand-file-name + (make-temp-name prefix) + (if (fboundp 'temp-directory) + (temp-directory) + temporary-file-directory)))))) + +;; Password dialog function + +(defun smime-ask-passphrase () + "Asks the passphrase to unlock the secret key." + (let ((passphrase + (comint-read-noecho + "Passphrase for secret key (RET for no passphrase): " t))) + (if (string= passphrase "") + nil + passphrase))) + +;; OpenSSL wrappers. + +(defun smime-call-openssl-region (b e buf &rest args) + (case (apply 'call-process-region b e smime-openssl-program nil buf nil args) + (0 t) + (1 (message "OpenSSL: An error occurred parsing the command options.") nil) + (2 (message "OpenSSL: One of the input files could not be read.") nil) + (3 (message "OpenSSL: An error occurred creating the PKCS#7 file or when reading the MIME message.") nil) + (4 (message "OpenSSL: An error occurred decrypting or verifying the message.") nil) + (t (error "Unknown OpenSSL exitcode") nil))) + +(defun smime-make-certfiles (certfiles) + (if certfiles + (append (list "-certfile" (expand-file-name (car certfiles))) + (smime-make-certfiles (cdr certfiles))))) + +;; Sign+encrypt region + +(defun smime-sign-region (b e keyfile) + "Sign region with certified key in KEYFILE. +If signing fails, the buffer is not modified. Region is assumed to +have proper MIME tags. KEYFILE is expected to contain a PEM encoded +private key and certificate as its car, and a list of additional +certificates to include in its caar. If no additional certificates is +included, KEYFILE may be the file containing the PEM encoded private +key and certificate itself." + (smime-new-details-buffer) + (let ((keyfile (or (car-safe keyfile) keyfile)) + (certfiles (and (cdr-safe keyfile) (cadr keyfile))) + (buffer (generate-new-buffer (generate-new-buffer-name " *smime*"))) + (passphrase (smime-ask-passphrase)) + (tmpfile (smime-make-temp-file "smime"))) + (if passphrase + (setenv "GNUS_SMIME_PASSPHRASE" passphrase)) + (prog1 + (when (prog1 + (apply 'smime-call-openssl-region b e (list buffer tmpfile) + "smime" "-sign" "-signer" (expand-file-name keyfile) + (append + (smime-make-certfiles certfiles) + (if passphrase + (list "-passin" "env:GNUS_SMIME_PASSPHRASE")))) + (if passphrase + (setenv "GNUS_SMIME_PASSPHRASE" "" t)) + (with-current-buffer smime-details-buffer + (insert-file-contents tmpfile) + (delete-file tmpfile))) + (delete-region b e) + (insert-buffer-substring buffer) + (goto-char b) + (when (looking-at "^MIME-Version: 1.0$") + (delete-region (point) (progn (forward-line 1) (point)))) + t) + (with-current-buffer smime-details-buffer + (goto-char (point-max)) + (insert-buffer-substring buffer)) + (kill-buffer buffer)))) + +(defun smime-encrypt-region (b e certfiles) + "Encrypt region for recipients specified in CERTFILES. +If encryption fails, the buffer is not modified. Region is assumed to +have proper MIME tags. CERTFILES is a list of filenames, each file +is expected to contain of a PEM encoded certificate." + (smime-new-details-buffer) + (let ((buffer (generate-new-buffer (generate-new-buffer-name " *smime*"))) + (tmpfile (smime-make-temp-file "smime"))) + (prog1 + (when (prog1 + (apply 'smime-call-openssl-region b e (list buffer tmpfile) + "smime" "-encrypt" smime-encrypt-cipher + (mapcar 'expand-file-name certfiles)) + (with-current-buffer smime-details-buffer + (insert-file-contents tmpfile) + (delete-file tmpfile))) + (delete-region b e) + (insert-buffer-substring buffer) + (goto-char b) + (when (looking-at "^MIME-Version: 1.0$") + (delete-region (point) (progn (forward-line 1) (point)))) + t) + (with-current-buffer smime-details-buffer + (goto-char (point-max)) + (insert-buffer-substring buffer)) + (kill-buffer buffer)))) + +;; Sign+encrypt buffer + +(defun smime-sign-buffer (&optional keyfile buffer) + "S/MIME sign BUFFER with key in KEYFILE. +KEYFILE should contain a PEM encoded key and certificate." + (interactive) + (with-current-buffer (or buffer (current-buffer)) + (smime-sign-region + (point-min) (point-max) + (if keyfile + keyfile + (smime-get-key-with-certs-by-email + (completing-read + (concat "Sign using which key? " + (if smime-keys (concat "(default " (caar smime-keys) ") ") + "")) + smime-keys nil nil (car-safe (car-safe smime-keys)))))))) + +(defun smime-encrypt-buffer (&optional certfiles buffer) + "S/MIME encrypt BUFFER for recipients specified in CERTFILES. +CERTFILES is a list of filenames, each file is expected to consist of +a PEM encoded key and certificate. Uses current buffer if BUFFER is +nil." + (interactive) + (with-current-buffer (or buffer (current-buffer)) + (smime-encrypt-region + (point-min) (point-max) + (or certfiles + (list (read-file-name "Recipient's S/MIME certificate: " + smime-certificate-directory nil)))))) + +;; Verify+decrypt region + +(defun smime-verify-region (b e) + "Verify S/MIME message in region between B and E. +Returns non-nil on success. +Any details (stdout and stderr) are left in the buffer specified by +`smime-details-buffer'." + (smime-new-details-buffer) + (let ((CAs (append (if smime-CA-file + (list "-CAfile" + (expand-file-name smime-CA-file))) + (if smime-CA-directory + (list "-CApath" + (expand-file-name smime-CA-directory)))))) + (unless CAs + (error "No CA configured")) + (if (apply 'smime-call-openssl-region b e (list smime-details-buffer t) + "smime" "-verify" "-out" "/dev/null" CAs) + t + (insert-buffer-substring smime-details-buffer) + nil))) + +(defun smime-noverify-region (b e) + "Verify integrity of S/MIME message in region between B and E. +Returns non-nil on success. +Any details (stdout and stderr) are left in the buffer specified by +`smime-details-buffer'." + (smime-new-details-buffer) + (if (apply 'smime-call-openssl-region b e (list smime-details-buffer t) + "smime" "-verify" "-noverify" "-out" '("/dev/null")) + t + (insert-buffer-substring smime-details-buffer) + nil)) + +(eval-when-compile + (defvar from)) + +(defun smime-decrypt-region (b e keyfile) + "Decrypt S/MIME message in region between B and E with key in KEYFILE. +On success, replaces region with decrypted data and return non-nil. +Any details (stderr on success, stdout and stderr on error) are left +in the buffer specified by `smime-details-buffer'." + (smime-new-details-buffer) + (let ((buffer (generate-new-buffer (generate-new-buffer-name " *smime*"))) + CAs (passphrase (smime-ask-passphrase)) + (tmpfile (smime-make-temp-file "smime"))) + (if passphrase + (setenv "GNUS_SMIME_PASSPHRASE" passphrase)) + (if (prog1 + (apply 'smime-call-openssl-region b e + (list buffer tmpfile) + "smime" "-decrypt" "-recip" (expand-file-name keyfile) + (if passphrase + (list "-passin" "env:GNUS_SMIME_PASSPHRASE"))) + (if passphrase + (setenv "GNUS_SMIME_PASSPHRASE" "" t)) + (with-current-buffer smime-details-buffer + (insert-file-contents tmpfile) + (delete-file tmpfile))) + (progn + (delete-region b e) + (when (boundp 'from) + ;; `from' is dynamically bound in mm-dissect. + (insert "From: " from "\n")) + (insert-buffer-substring buffer) + (kill-buffer buffer) + t) + (with-current-buffer smime-details-buffer + (insert-buffer-substring buffer)) + (kill-buffer buffer) + (delete-region b e) + (insert-buffer-substring smime-details-buffer) + nil))) + +;; Verify+Decrypt buffer + +(defun smime-verify-buffer (&optional buffer) + "Verify integrity of S/MIME message in BUFFER. +Uses current buffer if BUFFER is nil. Returns non-nil on success. +Any details (stdout and stderr) are left in the buffer specified by +`smime-details-buffer'." + (interactive) + (with-current-buffer (or buffer (current-buffer)) + (smime-verify-region (point-min) (point-max)))) + +(defun smime-noverify-buffer (&optional buffer) + "Verify integrity of S/MIME message in BUFFER. +Does NOT verify validity of certificate (only message integrity). +Uses current buffer if BUFFER is nil. Returns non-nil on success. +Any details (stdout and stderr) are left in the buffer specified by +`smime-details-buffer'." + (interactive) + (with-current-buffer (or buffer (current-buffer)) + (smime-noverify-region (point-min) (point-max)))) + +(defun smime-decrypt-buffer (&optional buffer keyfile) + "Decrypt S/MIME message in BUFFER using KEYFILE. +Uses current buffer if BUFFER is nil, and query user of KEYFILE if it's nil. +On success, replaces data in buffer and return non-nil. +Any details (stderr on success, stdout and stderr on error) are left +in the buffer specified by `smime-details-buffer'." + (interactive) + (with-current-buffer (or buffer (current-buffer)) + (smime-decrypt-region + (point-min) (point-max) + (expand-file-name + (or keyfile + (smime-get-key-by-email + (completing-read + (concat "Decipher using which key? " + (if smime-keys (concat "(default " (caar smime-keys) ") ") + "")) + smime-keys nil nil (car-safe (car-safe smime-keys))))))))) + +;; Various operations + +(defun smime-new-details-buffer () + (with-current-buffer (get-buffer-create smime-details-buffer) + (erase-buffer))) + +(defun smime-pkcs7-region (b e) + "Convert S/MIME message between points B and E into a PKCS7 message." + (smime-new-details-buffer) + (when (smime-call-openssl-region b e smime-details-buffer "smime" "-pk7out") + (delete-region b e) + (insert-buffer-substring smime-details-buffer) + t)) + +(defun smime-pkcs7-certificates-region (b e) + "Extract any certificates enclosed in PKCS7 message between points B and E." + (smime-new-details-buffer) + (when (smime-call-openssl-region + b e smime-details-buffer "pkcs7" "-print_certs" "-text") + (delete-region b e) + (insert-buffer-substring smime-details-buffer) + t)) + +(defun smime-pkcs7-email-region (b e) + "Get email addresses contained in certificate between points B and E. +A string or a list of strings is returned." + (smime-new-details-buffer) + (when (smime-call-openssl-region + b e smime-details-buffer "x509" "-email" "-noout") + (delete-region b e) + (insert-buffer-substring smime-details-buffer) + t)) + +;; Utility functions + +(defun smime-get-certfiles (keyfile keys) + (if keys + (let ((curkey (car keys)) + (otherkeys (cdr keys))) + (if (string= keyfile (cadr curkey)) + (caddr curkey) + (smime-get-certfiles keyfile otherkeys))))) + +(eval-and-compile + (defalias 'smime-point-at-eol + (if (fboundp 'point-at-eol) + 'point-at-eol + 'line-end-position))) + +(defun smime-buffer-as-string-region (b e) + "Return each line in region between B and E as a list of strings." + (save-excursion + (goto-char b) + (let (res) + (while (< (point) e) + (let ((str (buffer-substring (point) (smime-point-at-eol)))) + (unless (string= "" str) + (push str res))) + (forward-line)) + res))) + +;; Find certificates + +(defun smime-mail-to-domain (mailaddr) + (if (string-match "@" mailaddr) + (replace-match "." 'fixedcase 'literal mailaddr) + mailaddr)) + +(defun smime-cert-by-dns (mail) + (let* ((dig-dns-server smime-dns-server) + (digbuf (dig-invoke (smime-mail-to-domain mail) "cert" nil nil "+vc")) + (retbuf (generate-new-buffer (format "*certificate for %s*" mail))) + (certrr (with-current-buffer digbuf + (dig-extract-rr (smime-mail-to-domain mail) "cert"))) + (cert (and certrr (dig-rr-get-pkix-cert certrr)))) + (if cert + (with-current-buffer retbuf + (insert "-----BEGIN CERTIFICATE-----\n") + (let ((i 0) (len (length cert))) + (while (> (- len 64) i) + (insert (substring cert i (+ i 64)) "\n") + (setq i (+ i 64))) + (insert (substring cert i len) "\n")) + (insert "-----END CERTIFICATE-----\n")) + (kill-buffer retbuf) + (setq retbuf nil)) + (kill-buffer digbuf) + retbuf)) + +;; User interface. + +(defvar smime-buffer "*SMIME*") + +(defvar smime-mode-map nil) +(put 'smime-mode 'mode-class 'special) + +(unless smime-mode-map + (setq smime-mode-map (make-sparse-keymap)) + (suppress-keymap smime-mode-map) + + (define-key smime-mode-map "q" 'smime-exit) + (define-key smime-mode-map "f" 'smime-certificate-info)) + +(defun smime-mode () + "Major mode for browsing, viewing and fetching certificates. + +All normal editing commands are switched off. +\\ + +The following commands are available: + +\\{smime-mode-map}" + (interactive) + (kill-all-local-variables) + (setq major-mode 'smime-mode) + (setq mode-name "SMIME") + (setq mode-line-process nil) + (use-local-map smime-mode-map) + (buffer-disable-undo) + (setq truncate-lines t) + (setq buffer-read-only t)) + +(defun smime-certificate-info (certfile) + (interactive "fCertificate file: ") + (let ((buffer (get-buffer-create (format "*certificate %s*" certfile)))) + (switch-to-buffer buffer) + (erase-buffer) + (call-process smime-openssl-program nil buffer 'display + "x509" "-in" (expand-file-name certfile) "-text") + (fundamental-mode) + (set-buffer-modified-p nil) + (toggle-read-only t) + (goto-char (point-min)))) + +(defun smime-draw-buffer () + (with-current-buffer smime-buffer + (let (buffer-read-only) + (erase-buffer) + (insert "\nYour keys:\n") + (dolist (key smime-keys) + (insert + (format "\t\t%s: %s\n" (car key) (cadr key)))) + (insert "\nTrusted Certificate Authoritys:\n") + (insert "\nKnown Certificates:\n")))) + +(defun smime () + "Go to the SMIME buffer." + (interactive) + (unless (get-buffer smime-buffer) + (save-excursion + (set-buffer (get-buffer-create smime-buffer)) + (smime-mode))) + (smime-draw-buffer) + (switch-to-buffer smime-buffer)) + +(defun smime-exit () + "Quit the S/MIME buffer." + (interactive) + (kill-buffer (current-buffer))) + +;; Other functions + +(defun smime-get-key-by-email (email) + (cadr (assoc email smime-keys))) + +(defun smime-get-key-with-certs-by-email (email) + (cdr (assoc email smime-keys))) + +(provide 'smime) + +;;; smime.el ends here diff --git a/lisp/spam-report.el b/lisp/spam-report.el new file mode 100644 index 0000000..46884c4 --- /dev/null +++ b/lisp/spam-report.el @@ -0,0 +1,97 @@ +;;; spam-report.el --- Reporting spam +;; Copyright (C) 2002, 2003 Free Software Foundation, Inc. + +;; Author: Teodor Zlatanov +;; Keywords: network + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;;; This module addresses a few aspects of spam reporting under Gnus. Page +;;; breaks are used for grouping declarations and documentation relating to +;;; each particular aspect. + +;;; Code: +(require 'gnus) +(require 'gnus-sum) + +(defgroup spam-report nil + "Spam reporting configuration.") + +(defcustom spam-report-gmane-regex nil + "String matching Gmane newsgroups if wanted, e.g. \"^nntp+.*:gmane.\" +This is probably handled better with group/topic parameters." + :type 'regexp + :group 'spam-report) + +(defcustom spam-report-gmane-spam-header + "^X-Report-Spam: http://\\([^/]+\\)\\(.*\\)$" + "String matching Gmane spam-reporting header. Two match groups are needed." + :type 'regexp + :group 'spam-report) + +(defcustom spam-report-gmane-use-article-number t + "Whether the article number (faster!) or the header should be used." + :type 'boolean + :group 'spam-report) + +(defun spam-report-gmane (article) + "Report an article as spam through Gmane" + (interactive "nEnter the article number: ") + (when (and gnus-newsgroup-name + (or (null spam-report-gmane-regex) + (string-match spam-report-gmane-regex gnus-newsgroup-name))) + (gnus-message 6 "Reporting spam article %d to spam.gmane.org..." article) + (if spam-report-gmane-use-article-number + (spam-report-url-ping "spam.gmane.org" + (format "/%s:%d" + (gnus-group-real-name gnus-newsgroup-name) + article)) + (with-current-buffer nntp-server-buffer + (gnus-request-head article gnus-newsgroup-name) + (goto-char (point-min)) + (if (re-search-forward spam-report-gmane-spam-header nil t) + (let* ((host (match-string 1)) + (report (match-string 2)) + (url (format "http://%s%s" host report))) + (gnus-message 10 "Reporting spam through URL %s..." url) + (spam-report-url-ping host report)) + (gnus-message 10 "Could not find X-Report-Spam in article %d..." + article)))))) + + +(defun spam-report-url-ping (host report) + "Ping a host through HTTP, addressing a specific GET resource" + (let ((tcp-connection)) + (with-temp-buffer + (or (setq tcp-connection + (open-network-stream + "URL ping" + (buffer-name) + host + 80)) + (error "Could not open connection to %s" host)) + (set-marker (process-mark tcp-connection) (point-min)) + (process-send-string tcp-connection + (format "GET %s HTTP/1.1\nHost: %s\n\n" + report host))))) + +(provide 'spam-report) + +;;; spam-report.el ends here. diff --git a/lisp/spam-stat.el b/lisp/spam-stat.el new file mode 100644 index 0000000..96df016 --- /dev/null +++ b/lisp/spam-stat.el @@ -0,0 +1,572 @@ +;;; spam-stat.el --- detecting spam based on statistics + +;; Copyright (C) 2002, 2003 Free Software Foundation, Inc. + +;; Author: Alex Schroeder +;; Keywords: network +;; URL: http://www.emacswiki.org/cgi-bin/wiki.pl?SpamStat + +;; This file is part of GNU Emacs. + +;; This 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, or (at your option) +;; any later version. + +;; This 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; This implements spam analysis according to Paul Graham in "A Plan +;; for Spam". The basis for all this is a statistical distribution of +;; words for your spam and non-spam mails. We need this information +;; in a hash-table so that the analysis can use the information when +;; looking at your mails. Therefore, before you begin, you need tons +;; of mails (Graham uses 4000 non-spam and 4000 spam mails for his +;; experiments). +;; +;; The main interface to using spam-stat, are the following functions: +;; +;; `spam-stat-buffer-is-spam' -- called in a buffer, that buffer is +;; considered to be a new spam mail; use this for new mail that has +;; not been processed before +;; +;; `spam-stat-buffer-is-non-spam' -- called in a buffer, that buffer +;; is considered to be a new non-spam mail; use this for new mail that +;; has not been processed before +;; +;; `spam-stat-buffer-change-to-spam' -- called in a buffer, that +;; buffer is no longer considered to be normal mail but spam; use this +;; to change the status of a mail that has already been processed as +;; non-spam +;; +;; `spam-stat-buffer-change-to-non-spam' -- called in a buffer, that +;; buffer is no longer considered to be spam but normal mail; use this +;; to change the status of a mail that has already been processed as +;; spam +;; +;; `spam-stat-save' -- save the hash table to the file; the filename +;; used is stored in the variable `spam-stat-file' +;; +;; `spam-stat-load' -- load the hash table from a file; the filename +;; used is stored in the variable `spam-stat-file' +;; +;; `spam-stat-score-word' -- return the spam score for a word +;; +;; `spam-stat-score-buffer' -- return the spam score for a buffer +;; +;; `spam-stat-split-fancy' -- for fancy mail splitting; add +;; the rule (: spam-stat-split-fancy) to `nnmail-split-fancy' +;; +;; This requires the following in your ~/.gnus file: +;; +;; (require 'spam-stat) +;; (spam-stat-load) + +;;; Testing: + +;; Typical test will involve calls to the following functions: +;; +;; Reset: (spam-stat-reset) +;; Learn spam: (spam-stat-process-spam-directory "~/Mail/mail/spam") +;; Learn non-spam: (spam-stat-process-non-spam-directory "~/Mail/mail/misc") +;; Save table: (spam-stat-save) +;; File size: (nth 7 (file-attributes spam-stat-file)) +;; Number of words: (hash-table-count spam-stat) +;; Test spam: (spam-stat-test-directory "~/Mail/mail/spam") +;; Test non-spam: (spam-stat-test-directory "~/Mail/mail/misc") +;; Reduce table size: (spam-stat-reduce-size) +;; Save table: (spam-stat-save) +;; File size: (nth 7 (file-attributes spam-stat-file)) +;; Number of words: (hash-table-count spam-stat) +;; Test spam: (spam-stat-test-directory "~/Mail/mail/spam") +;; Test non-spam: (spam-stat-test-directory "~/Mail/mail/misc") + +;;; Dictionary Creation: + +;; Typically, you will filter away mailing lists etc. using specific +;; rules in `nnmail-split-fancy'. Somewhere among these rules, you +;; will filter spam. Here is how you would create your dictionary: + +;; Reset: (spam-stat-reset) +;; Learn spam: (spam-stat-process-spam-directory "~/Mail/mail/spam") +;; Learn non-spam: (spam-stat-process-non-spam-directory "~/Mail/mail/misc") +;; Repeat for any other non-spam group you need... +;; Reduce table size: (spam-stat-reduce-size) +;; Save table: (spam-stat-save) + +;;; Todo: + +;; Speed it up. Integrate with Gnus such that it uses spam and expiry +;; marks to call the appropriate functions when leaving the summary +;; buffer and saves the hash table when leaving Gnus. More testing: +;; More mails, disabling SpamAssassin, double checking algorithm, find +;; improved algorithm. + +;;; Thanks: + +;; Ted Zlatanov +;; Jesper Harder +;; Dan Schmidt +;; Lasse Rasinen +;; Milan Zamazal + + + +;;; Code: + +(defgroup spam-stat nil + "Statistical spam detection for Emacs. +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." + :group 'gnus) + +(defcustom spam-stat-file "~/.spam-stat.el" + "File used to save and load the dictionary. +See `spam-stat-to-hash-table' for the format of the file." + :type 'file + :group 'spam-stat) + +(defcustom spam-stat-install-hooks t + "Whether spam-stat should install its hooks in Gnus. +This is set to nil if you use spam-stat through spam.el." + :type 'boolean + :group 'spam-stat) + +(defcustom spam-stat-unknown-word-score 0.2 + "The score to use for unknown words. +Also used for words that don't appear often enough." + :type 'number + :group 'spam-stat) + +(defcustom spam-stat-max-word-length 15 + "Only words shorter than this will be considered." + :type 'integer + :group 'spam-stat) + +(defcustom spam-stat-max-buffer-length 10240 + "Only the beginning of buffers will be analyzed. +This variable says how many characters this will be." + :type 'integer + :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." + :type 'string + :group 'spam-stat) + +(defcustom spam-stat-split-fancy-spam-threshhold 0.9 + "Spam score threshhold in spam-stat-split-fancy." + :type 'number + :group 'spam-stat) + +(defvar spam-stat-syntax-table + (let ((table (copy-syntax-table text-mode-syntax-table))) + (modify-syntax-entry ?- "w" table) + (modify-syntax-entry ?_ "w" table) + (modify-syntax-entry ?. "w" table) + (modify-syntax-entry ?! "w" table) + (modify-syntax-entry ?? "w" table) + (modify-syntax-entry ?+ "w" table) + table) + "Syntax table used when processing mails for statistical analysis. +The important part is which characters are word constituents.") + +(defvar spam-stat-buffer nil + "Buffer to use for scoring while splitting. +This is set by hooking into Gnus.") + +(defvar spam-stat-buffer-name " *spam stat buffer*" + "Name of the `spam-stat-buffer'.") + +;; Functions missing in Emacs 20 + +(when (memq nil (mapcar 'fboundp + '(gethash hash-table-count make-hash-table + mapc puthash))) + (require 'cl) + (unless (fboundp 'puthash) + ;; alias puthash is missing from Emacs 20 cl-extra.el + (defalias 'puthash 'cl-puthash))) + +(eval-when-compile + (unless (fboundp 'with-syntax-table) + ;; Imported from Emacs 21.2 + (defmacro with-syntax-table (table &rest body) "\ +Evaluate BODY with syntax table of current buffer set to a copy of TABLE. +The syntax table of the current buffer is saved, BODY is evaluated, and the +saved table is restored, even in case of an abnormal exit. +Value is what BODY returns." + (let ((old-table (make-symbol "table")) + (old-buffer (make-symbol "buffer"))) + `(let ((,old-table (syntax-table)) + (,old-buffer (current-buffer))) + (unwind-protect + (progn + (set-syntax-table (copy-syntax-table ,table)) + ,@body) + (save-current-buffer + (set-buffer ,old-buffer) + (set-syntax-table ,old-table)))))))) + +;; Hooking into Gnus + +(defun spam-stat-store-current-buffer () + "Store a copy of the current buffer in `spam-stat-buffer'." + (save-excursion + (let ((str (buffer-string))) + (set-buffer (get-buffer-create spam-stat-buffer-name)) + (erase-buffer) + (insert str) + (setq spam-stat-buffer (current-buffer))))) + +(defun spam-stat-store-gnus-article-buffer () + "Store a copy of the current article in `spam-stat-buffer'. +This uses `gnus-article-buffer'." + (save-excursion + (set-buffer gnus-original-article-buffer) + (spam-stat-store-current-buffer))) + +(when spam-stat-install-hooks + (add-hook 'nnmail-prepare-incoming-message-hook + 'spam-stat-store-current-buffer) + (add-hook 'gnus-select-article-hook + 'spam-stat-store-gnus-article-buffer)) + +;; Data -- not using defstruct in order to save space and time + +(defvar spam-stat (make-hash-table :test 'equal) + "Hash table used to store the statistics. +Use `spam-stat-load' to load the file. +Every word is used as a key in this table. The value is a vector. +Use `spam-stat-ngood', `spam-stat-nbad', `spam-stat-good', +`spam-stat-bad', and `spam-stat-score' to access this vector.") + +(defvar spam-stat-ngood 0 + "The number of good mails in the dictionary.") + +(defvar spam-stat-nbad 0 + "The number of bad mails in the dictionary.") + +(defsubst spam-stat-good (entry) + "Return the number of times this word belongs to good mails." + (aref entry 0)) + +(defsubst spam-stat-bad (entry) + "Return the number of times this word belongs to bad mails." + (aref entry 1)) + +(defsubst spam-stat-score (entry) + "Set the score of this word." + (if entry + (aref entry 2) + spam-stat-unknown-word-score)) + +(defsubst spam-stat-set-good (entry value) + "Set the number of times this word belongs to good mails." + (aset entry 0 value)) + +(defsubst spam-stat-set-bad (entry value) + "Set the number of times this word belongs to bad mails." + (aset entry 1 value)) + +(defsubst spam-stat-set-score (entry value) + "Set the score of this word." + (aset entry 2 value)) + +(defsubst spam-stat-make-entry (good bad) + "Return a vector with the given properties." + (let ((entry (vector good bad nil))) + (spam-stat-set-score entry (spam-stat-compute-score entry)) + entry)) + +;; Computing + +(defun spam-stat-compute-score (entry) + "Compute the score of this word. 1.0 means spam." + ;; promote all numbers to floats for the divisions + (let* ((g (* 2.0 (spam-stat-good entry))) + (b (float (spam-stat-bad entry)))) + (cond ((< (+ g b) 5) + .2) + ((= 0 spam-stat-ngood) + .99) + ((= 0 spam-stat-nbad) + .01) + (t + (max .01 + (min .99 (/ (/ b spam-stat-nbad) + (+ (/ g spam-stat-ngood) + (/ b spam-stat-nbad))))))))) + +;; Parsing + +(defmacro with-spam-stat-max-buffer-size (&rest body) + "Narrows the buffer down to the first 4k characters, then evaluates BODY." + `(save-restriction + (when (> (- (point-max) + (point-min)) + spam-stat-max-buffer-length) + (narrow-to-region (point-min) + (+ (point-min) spam-stat-max-buffer-length))) + ,@body)) + +(defun spam-stat-buffer-words () + "Return a hash table of words and number of occurences in the buffer." + (with-spam-stat-max-buffer-size + (with-syntax-table spam-stat-syntax-table + (goto-char (point-min)) + (let ((result (make-hash-table :test 'equal)) + word count) + (while (re-search-forward "\\w+" nil t) + (setq word (match-string-no-properties 0) + count (1+ (gethash word result 0))) + (when (< (length word) spam-stat-max-word-length) + (puthash word count result))) + result)))) + +(defun spam-stat-buffer-is-spam () + "Consider current buffer to be a new spam mail." + (setq spam-stat-nbad (1+ spam-stat-nbad)) + (maphash + (lambda (word count) + (let ((entry (gethash word spam-stat))) + (if entry + (spam-stat-set-bad entry (+ count (spam-stat-bad entry))) + (setq entry (spam-stat-make-entry 0 count))) + (spam-stat-set-score entry (spam-stat-compute-score entry)) + (puthash word entry spam-stat))) + (spam-stat-buffer-words))) + +(defun spam-stat-buffer-is-non-spam () + "Consider current buffer to be a new non-spam mail." + (setq spam-stat-ngood (1+ spam-stat-ngood)) + (maphash + (lambda (word count) + (let ((entry (gethash word spam-stat))) + (if entry + (spam-stat-set-good entry (+ count (spam-stat-good entry))) + (setq entry (spam-stat-make-entry count 0))) + (spam-stat-set-score entry (spam-stat-compute-score entry)) + (puthash word entry spam-stat))) + (spam-stat-buffer-words))) + +(defun spam-stat-buffer-change-to-spam () + "Consider current buffer no longer normal mail but spam." + (setq spam-stat-nbad (1+ spam-stat-nbad) + spam-stat-ngood (1- spam-stat-ngood)) + (maphash + (lambda (word count) + (let ((entry (gethash word spam-stat))) + (if (not entry) + (error "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)) + (puthash word entry spam-stat)))) + (spam-stat-buffer-words))) + +(defun spam-stat-buffer-change-to-non-spam () + "Consider current buffer no longer spam but normal mail." + (setq spam-stat-nbad (1- spam-stat-nbad) + spam-stat-ngood (1+ spam-stat-ngood)) + (maphash + (lambda (word count) + (let ((entry (gethash word spam-stat))) + (if (not entry) + (error "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)) + (puthash word entry spam-stat)))) + (spam-stat-buffer-words))) + +;; Saving and Loading + +(defun spam-stat-save () + "Save the `spam-stat' hash table as lisp file." + (interactive) + (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)))) + +(defun spam-stat-load () + "Read the `spam-stat' hash table from disk." + (load-file spam-stat-file)) + +(defun spam-stat-to-hash-table (entries) + "Turn list ENTRIES into a hash table and store as `spam-stat'. +Every element in ENTRIES has the form \(WORD GOOD BAD) where WORD is +the word string, NGOOD is the number of good mails it has appeared in, +NBAD is the number of bad mails it has appeared in, GOOD is the number +of times it appeared in good mails, and BAD is the number of times it +has appeared in bad mails." + (let ((table (make-hash-table :test 'equal))) + (mapc (lambda (l) + (puthash (car l) + (spam-stat-make-entry (nth 1 l) (nth 2 l)) + table)) + entries) + table)) + +(defun spam-stat-reset () + "Reset `spam-stat' to an empty hash-table. +This deletes all the statistics." + (interactive) + (setq spam-stat (make-hash-table :test 'equal) + spam-stat-ngood 0 + spam-stat-nbad 0)) + +;; Scoring buffers + +(defvar spam-stat-score-data nil + "Raw data used in the last run of `spam-stat-score-buffer'.") + +(defsubst spam-stat-score-word (word) + "Return score for WORD. +The default score for unknown words is stored in +`spam-stat-unknown-word-score'." + (spam-stat-score (gethash word spam-stat))) + +(defun spam-stat-buffer-words-with-scores () + "Process current buffer, return the 15 most conspicuous words. +These are the words whose spam-stat differs the most from 0.5. +The list returned contains elements of the form \(WORD SCORE DIFF), +where DIFF is the difference between SCORE and 0.5." + (with-spam-stat-max-buffer-size + (with-syntax-table spam-stat-syntax-table + (let (result word score) + (maphash (lambda (word ignore) + (setq score (spam-stat-score-word word) + result (cons (list word score (abs (- score 0.5))) + result))) + (spam-stat-buffer-words)) + (setq result (sort result (lambda (a b) (< (nth 2 b) (nth 2 a))))) + (setcdr (nthcdr 14 result) nil) + result)))) + +(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)) + (prod (apply #'* probs))) + (/ prod (+ prod (apply #'* (mapcar #'(lambda (x) (- 1 x)) + probs)))))) + +(defun spam-stat-split-fancy () + "Return the name of the spam group if the current mail is spam. +Use this function on `nnmail-split-fancy'. If you are interested in +the raw data used for the last run of `spam-stat-score-buffer', +check the variable `spam-stat-score-data'." + (condition-case var + (progn + (set-buffer spam-stat-buffer) + (goto-char (point-min)) + (when (> (spam-stat-score-buffer) spam-stat-split-fancy-spam-threshhold) + (when (boundp 'nnmail-split-trace) + (mapc (lambda (entry) + (push entry nnmail-split-trace)) + spam-stat-score-data)) + spam-stat-split-fancy-spam-group)) + (error (message "Error in spam-stat-split-fancy: %S" var) + nil))) + +;; Testing + +(defun spam-stat-process-directory (dir func) + "Process all the regular files in directory DIR using function FUNC." + (let* ((files (directory-files dir t "^[^.]")) + (max (/ (length files) 100.0)) + (count 0)) + (with-temp-buffer + (dolist (f files) + (when (and (file-readable-p f) + (file-regular-p f) + (> (nth 7 (file-attributes f)) 0)) + (setq count (1+ count)) + (message "Reading %s: %.2f%%" dir (/ count max)) + (insert-file-contents f) + (funcall func) + (erase-buffer)))))) + +(defun spam-stat-process-spam-directory (dir) + "Process all the regular files in directory DIR as spam." + (interactive "D") + (spam-stat-process-directory dir 'spam-stat-buffer-is-spam)) + +(defun spam-stat-process-non-spam-directory (dir) + "Process all the regular files in directory DIR as non-spam." + (interactive "D") + (spam-stat-process-directory dir 'spam-stat-buffer-is-non-spam)) + +(defun spam-stat-count () + "Return size of `spam-stat'." + (interactive) + (hash-table-count spam-stat)) + +(defun spam-stat-test-directory (dir) + "Test all the regular files in directory DIR for spam. +If the result is 1.0, then all files are considered spam. +If the result is 0.0, non of the files is considered spam. +You can use this to determine error rates." + (interactive "D") + (let* ((files (directory-files dir t "^[^.]")) + (total (length files)) + (score 0.0); float + (max (/ total 100.0)); float + (count 0)) + (with-temp-buffer + (dolist (f files) + (when (and (file-readable-p f) + (file-regular-p f) + (> (nth 7 (file-attributes f)) 0)) + (setq count (1+ count)) + (message "Reading %.2f%%, score %.2f%%" + (/ count max) (/ score count)) + (insert-file-contents f) + (when (> (spam-stat-score-buffer) 0.9) + (setq score (1+ score))) + (erase-buffer)))) + (message "Final score: %d / %d = %f" score total (/ score total)))) + +;; Shrinking the dictionary + +(defun spam-stat-reduce-size (&optional count) + "Reduce the size of `spam-stat'. +This removes all words that occur less than COUNT from the dictionary. +COUNT defaults to 5" + (interactive) + (setq count (or count 5)) + (maphash (lambda (key entry) + (when (< (+ (spam-stat-good entry) + (spam-stat-bad entry)) + count) + (remhash key spam-stat))) + spam-stat)) + +(provide 'spam-stat) + +;;; spam-stat.el ends here diff --git a/lisp/spam.el b/lisp/spam.el new file mode 100644 index 0000000..4f84c11 --- /dev/null +++ b/lisp/spam.el @@ -0,0 +1,1065 @@ +;;; spam.el --- Identifying spam +;; Copyright (C) 2002, 2003 Free Software Foundation, Inc. + +;; Author: Lars Magne Ingebrigtsen +;; Keywords: network + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;;; This module addresses a few aspects of spam control under Gnus. Page +;;; breaks are used for grouping declarations and documentation relating to +;;; each particular aspect. + +;;; The integration with Gnus is not yet complete. See various `FIXME' +;;; comments, below, for supplementary explanations or discussions. + +;;; Several TODO items are marked as such + +;;; Code: + +(require 'path-util) + +(eval-when-compile (require 'cl)) + +(require 'gnus-sum) + +(require 'gnus-uu) ; because of key prefix issues +(require 'gnus) ; for the definitions of group content classification and spam processors +(require 'message) ;for the message-fetch-field functions + +;; for nnimap-split-download-body-default +(eval-when-compile (require 'nnimap)) + +;; autoload query-dig +(eval-and-compile + (autoload 'query-dig "dig")) + +;; autoload spam-report +(eval-and-compile + (autoload 'spam-report-gmane "spam-report")) + +;; autoload query-dns +(eval-and-compile + (autoload 'query-dns "dns")) + +;;; Main parameters. + +(defgroup spam nil + "Spam configuration.") + +(defcustom spam-directory "~/News/spam/" + "Directory for spam whitelists and blacklists." + :type 'directory + :group 'spam) + +(defcustom spam-move-spam-nonspam-groups-only t + "Whether spam should be moved in non-spam groups only. +When nil, only ham and unclassified groups will have their spam moved +to the spam-process-destination. When t, spam will also be moved from +spam groups." + :type 'boolean + :group 'spam) + +(defcustom spam-mark-only-unseen-as-spam t + "Whether only unseen articles should be marked as spam in spam +groups. When nil, all unread articles in a spam group are marked as +spam. Set this if you want to leave an article unread in a spam group +without losing it to the automatic spam-marking process." + :type 'boolean + :group 'spam) + +(defcustom spam-mark-ham-unread-before-move-from-spam-group nil + "Whether ham should be marked unread before it's moved out of a spam +group according to ham-process-destination. This variable is an +official entry in the international Longest Variable Name +Competition." + :type 'boolean + :group 'spam) + +(defcustom spam-whitelist (expand-file-name "whitelist" spam-directory) + "The location of the whitelist. +The file format is one regular expression per line. +The regular expression is matched against the address." + :type 'file + :group 'spam) + +(defcustom spam-blacklist (expand-file-name "blacklist" spam-directory) + "The location of the blacklist. +The file format is one regular expression per line. +The regular expression is matched against the address." + :type 'file + :group 'spam) + +(defcustom spam-use-dig t + "Whether query-dig should be used instead of query-dns." + :type 'boolean + :group 'spam) + +(defcustom spam-use-blacklist nil + "Whether the blacklist should be used by spam-split." + :type 'boolean + :group 'spam) + +(defcustom spam-use-whitelist nil + "Whether the whitelist should be used by spam-split." + :type 'boolean + :group 'spam) + +(defcustom spam-use-whitelist-exclusive nil + "Whether whitelist-exclusive should be used by spam-split. +Exclusive whitelisting means that all messages from senders not in the whitelist +are considered spam." + :type 'boolean + :group 'spam) + +(defcustom spam-use-blackholes nil + "Whether blackholes should be used by spam-split." + :type 'boolean + :group 'spam) + +(defcustom spam-use-hashcash nil + "Whether hashcash payments should be detected by spam-split." + :type 'boolean + :group 'spam) + +(defcustom spam-use-regex-headers nil + "Whether a header regular expression match should be used by spam-split. +Also see the variable `spam-spam-regex-headers' and `spam-ham-regex-headers'." + :type 'boolean + :group 'spam) + +(defcustom spam-use-bogofilter-headers nil + "Whether bogofilter headers should be used by spam-split. +Enable this if you pre-process messages with Bogofilter BEFORE Gnus sees them." + :type 'boolean + :group 'spam) + +(defcustom spam-use-bogofilter nil + "Whether bogofilter should be invoked by spam-split. +Enable this if you want Gnus to invoke Bogofilter on new messages." + :type 'boolean + :group 'spam) + +(defcustom spam-use-BBDB nil + "Whether BBDB should be used by spam-split." + :type 'boolean + :group 'spam) + +(defcustom spam-use-BBDB-exclusive nil + "Whether BBDB-exclusive should be used by spam-split. +Exclusive BBDB means that all messages from senders not in the BBDB are +considered spam." + :type 'boolean + :group 'spam) + +(defcustom spam-use-ifile nil + "Whether ifile should be used by spam-split." + :type 'boolean + :group 'spam) + +(defcustom spam-use-stat nil + "Whether spam-stat should be used by spam-split." + :type 'boolean + :group 'spam) + +(defcustom spam-split-group "spam" + "Group name where incoming spam should be put by spam-split." + :type 'string + :group 'spam) + +(defcustom spam-junk-mailgroups (cons spam-split-group '("mail.junk" "poste.pourriel")) + "Mailgroups with spam contents. +All unmarked article in such group receive the spam mark on group entry." + :type '(repeat (string :tag "Group")) + :group 'spam) + +(defcustom spam-blackhole-servers '("bl.spamcop.net" "relays.ordb.org" + "dev.null.dk" "relays.visi.com") + "List of blackhole servers." + :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" + :type 'regexp + :group 'spam) + +(defcustom spam-face 'gnus-splash-face + "Face for spam-marked articles" + :type 'face + :group 'spam) + +(defcustom spam-regex-headers-spam '("^X-Spam-Flag: YES") + "Regular expression for positive header spam matches" + :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" + :type '(repeat (regexp :tag "Regular expression to match ham header")) + :group 'spam) + +(defgroup spam-ifile nil + "Spam ifile configuration." + :group 'spam) + +(defcustom spam-ifile-path (exec-installed-p "ifile") + "File path of the ifile executable program." + :type '(choice (file :tag "Location of ifile") + (const :tag "ifile is not installed")) + :group 'spam-ifile) + +(defcustom spam-ifile-database-path nil + "File path of the ifile database." + :type '(choice (file :tag "Location of the ifile database") + (const :tag "Use the default")) + :group 'spam-ifile) + +(defcustom spam-ifile-spam-category "spam" + "Name of the spam ifile category." + :type 'string + :group 'spam-ifile) + +(defcustom spam-ifile-ham-category nil + "Name of the ham ifile category. If nil, the current group name will +be used." + :type '(choice (string :tag "Use a fixed category") + (const :tag "Use the current group name")) + :group 'spam-ifile) + +(defcustom spam-ifile-all-categories nil + "Whether the ifile check will return all categories, or just spam. +Set this to t if you want to use the spam-split invocation of ifile as +your main source of newsgroup names." + :type 'boolean + :group 'spam-ifile) + +(defgroup spam-bogofilter nil + "Spam bogofilter configuration." + :group 'spam) + +(defcustom spam-bogofilter-path (exec-installed-p "bogofilter") + "File path of the Bogofilter executable program." + :type '(choice (file :tag "Location of bogofilter") + (const :tag "Bogofilter is not installed")) + :group 'spam-bogofilter) + +(defcustom spam-bogofilter-header "X-Bogosity" + "The header that Bogofilter inserts in messages." + :type 'string + :group 'spam-bogofilter) + +(defcustom spam-bogofilter-spam-switch "-s" + "The switch that Bogofilter uses to register spam messages." + :type 'string + :group 'spam-bogofilter) + +(defcustom spam-bogofilter-ham-switch "-n" + "The switch that Bogofilter uses to register ham messages." + :type 'string + :group 'spam-bogofilter) + +(defcustom spam-bogofilter-bogosity-positive-spam-header "^\\(Yes\\|Spam\\)" + "The regex on `spam-bogofilter-header' for positive spam identification." + :type 'regexp + :group 'spam-bogofilter) + +(defcustom spam-bogofilter-database-directory nil + "Directory path of the Bogofilter databases." + :type '(choice (directory :tag "Location of the Bogofilter database directory") + (const :tag "Use the default")) + :group 'spam-ifile) + +;;; Key bindings for spam control. + +(gnus-define-keys gnus-summary-mode-map + "St" spam-bogofilter-score + "Sx" gnus-summary-mark-as-spam + "Mst" spam-bogofilter-score + "Msx" gnus-summary-mark-as-spam + "\M-d" gnus-summary-mark-as-spam) + +;;; How to highlight a spam summary line. + +;; TODO: How do we redo this every time spam-face is customized? + +(push '((eq mark gnus-spam-mark) . spam-face) + gnus-summary-highlight) + +;; convenience functions +(defun spam-group-ham-mark-p (group mark &optional spam) + (when (stringp group) + (let* ((marks (spam-group-ham-marks group spam)) + (marks (if (symbolp mark) + marks + (mapcar 'symbol-value marks)))) + (memq mark marks)))) + +(defun spam-group-spam-mark-p (group mark) + (spam-group-ham-mark-p group mark t)) + +(defun spam-group-ham-marks (group &optional spam) + (when (stringp group) + (let* ((marks (if spam + (gnus-parameter-spam-marks group) + (gnus-parameter-ham-marks group))) + (marks (car marks)) + (marks (if (listp (car marks)) (car marks) marks))) + marks))) + +(defun spam-group-spam-marks (group) + (spam-group-ham-marks group t)) + +(defun spam-group-spam-contents-p (group) + (if (stringp group) + (or (member group spam-junk-mailgroups) + (memq 'gnus-group-spam-classification-spam + (gnus-parameter-spam-contents group))) + nil)) + +(defun spam-group-ham-contents-p (group) + (if (stringp group) + (memq 'gnus-group-spam-classification-ham + (gnus-parameter-spam-contents group)) + nil)) + +(defun spam-group-processor-p (group processor) + (if (and (stringp group) + (symbolp processor)) + (member processor (car (gnus-parameter-spam-process group))) + nil)) + +(defun spam-group-spam-processor-report-gmane-p (group) + (spam-group-processor-p group 'gnus-group-spam-exit-processor-report-gmane)) + +(defun spam-group-spam-processor-bogofilter-p (group) + (spam-group-processor-p group 'gnus-group-spam-exit-processor-bogofilter)) + +(defun spam-group-spam-processor-blacklist-p (group) + (spam-group-processor-p group 'gnus-group-spam-exit-processor-blacklist)) + +(defun spam-group-spam-processor-ifile-p (group) + (spam-group-processor-p group 'gnus-group-spam-exit-processor-ifile)) + +(defun spam-group-ham-processor-ifile-p (group) + (spam-group-processor-p group 'gnus-group-ham-exit-processor-ifile)) + +(defun spam-group-ham-processor-bogofilter-p (group) + (spam-group-processor-p group 'gnus-group-ham-exit-processor-bogofilter)) + +(defun spam-group-spam-processor-stat-p (group) + (spam-group-processor-p group 'gnus-group-spam-exit-processor-stat)) + +(defun spam-group-ham-processor-stat-p (group) + (spam-group-processor-p group 'gnus-group-ham-exit-processor-stat)) + +(defun spam-group-ham-processor-whitelist-p (group) + (spam-group-processor-p group 'gnus-group-ham-exit-processor-whitelist)) + +(defun spam-group-ham-processor-BBDB-p (group) + (spam-group-processor-p group 'gnus-group-ham-exit-processor-BBDB)) + +(defun spam-group-ham-processor-copy-p (group) + (spam-group-processor-p group 'gnus-group-ham-exit-processor-copy)) + +;;; Summary entry and exit processing. + +(defun spam-summary-prepare () + (spam-mark-junk-as-spam-routine)) + +(add-hook 'gnus-summary-prepare-hook 'spam-summary-prepare) + +;; The spam processors are invoked for any group, spam or ham or neither +(defun spam-summary-prepare-exit () + (unless gnus-group-is-exiting-without-update-p + (gnus-message 6 "Exiting summary buffer and applying spam rules") + (when (and spam-bogofilter-path + (spam-group-spam-processor-bogofilter-p gnus-newsgroup-name)) + (gnus-message 5 "Registering spam with bogofilter") + (spam-bogofilter-register-spam-routine)) + + (when (and spam-ifile-path + (spam-group-spam-processor-ifile-p gnus-newsgroup-name)) + (gnus-message 5 "Registering spam with ifile") + (spam-ifile-register-spam-routine)) + + (when (spam-group-spam-processor-stat-p gnus-newsgroup-name) + (gnus-message 5 "Registering spam with spam-stat") + (spam-stat-register-spam-routine)) + + (when (spam-group-spam-processor-blacklist-p gnus-newsgroup-name) + (gnus-message 5 "Registering spam with the blacklist") + (spam-blacklist-register-routine)) + + (when (spam-group-spam-processor-report-gmane-p gnus-newsgroup-name) + (gnus-message 5 "Registering spam with the Gmane report") + (spam-report-gmane-register-routine)) + + (if spam-move-spam-nonspam-groups-only + (when (not (spam-group-spam-contents-p gnus-newsgroup-name)) + (spam-mark-spam-as-expired-and-move-routine + (gnus-parameter-spam-process-destination gnus-newsgroup-name))) + (gnus-message 5 "Marking spam as expired and moving it to %s" gnus-newsgroup-name) + (spam-mark-spam-as-expired-and-move-routine + (gnus-parameter-spam-process-destination gnus-newsgroup-name))) + + ;; now we redo spam-mark-spam-as-expired-and-move-routine to only + ;; expire spam, in case the above did not expire them + (gnus-message 5 "Marking spam as expired without moving it") + (spam-mark-spam-as-expired-and-move-routine nil) + + (when (spam-group-ham-contents-p gnus-newsgroup-name) + (when (spam-group-ham-processor-whitelist-p gnus-newsgroup-name) + (gnus-message 5 "Registering ham with the whitelist") + (spam-whitelist-register-routine)) + (when (spam-group-ham-processor-ifile-p gnus-newsgroup-name) + (gnus-message 5 "Registering ham with ifile") + (spam-ifile-register-ham-routine)) + (when (spam-group-ham-processor-bogofilter-p gnus-newsgroup-name) + (gnus-message 5 "Registering ham with Bogofilter") + (spam-bogofilter-register-ham-routine)) + (when (spam-group-ham-processor-stat-p gnus-newsgroup-name) + (gnus-message 5 "Registering ham with spam-stat") + (spam-stat-register-ham-routine)) + (when (spam-group-ham-processor-BBDB-p gnus-newsgroup-name) + (gnus-message 5 "Registering ham with the BBDB") + (spam-BBDB-register-routine))) + + (when (spam-group-ham-processor-copy-p gnus-newsgroup-name) + (gnus-message 5 "Copying ham") + (spam-ham-move-routine + (gnus-parameter-ham-process-destination gnus-newsgroup-name) t)) + + ;; now move all ham articles out of spam groups + (when (spam-group-spam-contents-p gnus-newsgroup-name) + (gnus-message 5 "Moving ham messages from spam group") + (spam-ham-move-routine + (gnus-parameter-ham-process-destination gnus-newsgroup-name))))) + +(add-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit) + +(defun spam-mark-junk-as-spam-routine () + ;; check the global list of group names spam-junk-mailgroups and the + ;; group parameters + (when (spam-group-spam-contents-p gnus-newsgroup-name) + (gnus-message 5 "Marking %s articles as spam" + (if spam-mark-only-unseen-as-spam + "unseen" + "unread")) + (let ((articles (if spam-mark-only-unseen-as-spam + gnus-newsgroup-unseen + gnus-newsgroup-unreads))) + (dolist (article articles) + (gnus-summary-mark-article article gnus-spam-mark))))) + +(defun spam-mark-spam-as-expired-and-move-routine (&optional group) + (gnus-summary-kill-process-mark) + (let ((articles gnus-newsgroup-articles) + article tomove) + (dolist (article articles) + (when (eq (gnus-summary-article-mark article) gnus-spam-mark) + (gnus-summary-mark-article article gnus-expirable-mark) + (push article tomove))) + + ;; now do the actual move + (when (and tomove + (stringp group)) + (dolist (article tomove) + (gnus-summary-set-process-mark article)) + (when tomove (gnus-summary-move-article nil group)))) + (gnus-summary-yank-process-mark)) + +(defun spam-ham-move-routine (&optional group copy) + (gnus-summary-kill-process-mark) + (let ((articles gnus-newsgroup-articles) + article mark tomove) + (when (stringp group) ; this routine will do nothing + ; without a valid group + (dolist (article articles) + (when (spam-group-ham-mark-p gnus-newsgroup-name + (gnus-summary-article-mark article)) + (push article tomove))) + + ;; now do the actual move + (when tomove + (dolist (article tomove) + (when spam-mark-ham-unread-before-move-from-spam-group + (gnus-summary-mark-article article gnus-unread-mark)) + (gnus-summary-set-process-mark article)) + (if copy + (gnus-summary-copy-article nil group) + (gnus-summary-move-article nil group))))) + (gnus-summary-yank-process-mark)) + +(defun spam-generic-register-routine (spam-func ham-func) + (let ((articles gnus-newsgroup-articles) + article mark ham-articles spam-articles) + + (while articles + (setq article (pop articles) + mark (gnus-summary-article-mark article)) + (cond ((spam-group-spam-mark-p gnus-newsgroup-name mark) + (push article spam-articles)) + ((memq article gnus-newsgroup-saved)) + ((spam-group-ham-mark-p gnus-newsgroup-name mark) + (push article ham-articles)))) + + (when (and ham-articles ham-func) + (mapc ham-func ham-articles)) ; we use mapc because unlike + ; mapcar it discards the + ; return values + (when (and spam-articles spam-func) + (mapc spam-func spam-articles)))) ; we use mapc because unlike + ; mapcar it discards the + ; return values + +(eval-and-compile + (defalias 'spam-point-at-eol (if (fboundp 'point-at-eol) + 'point-at-eol + 'line-end-position))) + +(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)) + +;; disabled for now +;; (defun spam-get-article-as-filename (article) +;; (let ((article-filename)) +;; (when (numberp article) +;; (nnml-possibly-change-directory (gnus-group-real-name gnus-newsgroup-name)) +;; (setq article-filename (expand-file-name (int-to-string article) nnml-current-directory))) +;; (if (file-exists-p article-filename) +;; article-filename +;; nil))) + +(defun spam-fetch-field-from-fast (article) + "Fetch the `from' field quickly, using the internal gnus-data-list function" + (if (and (numberp article) + (assoc article (gnus-data-list nil))) + (mail-header-from (gnus-data-header (assoc article (gnus-data-list nil)))) + nil)) + +(defun spam-fetch-field-subject-fast (article) + "Fetch the `subject' field quickly, using the internal gnus-data-list function" + (if (and (numberp article) + (assoc article (gnus-data-list nil))) + (mail-header-subject (gnus-data-header (assoc article (gnus-data-list nil)))) + nil)) + + +;;;; Spam determination. + +(defvar spam-list-of-checks + '((spam-use-blacklist . spam-check-blacklist) + (spam-use-regex-headers . spam-check-regex-headers) + (spam-use-whitelist . spam-check-whitelist) + (spam-use-BBDB . spam-check-BBDB) + (spam-use-ifile . spam-check-ifile) + (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)) +"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, and its value decides what +happens. Each individual check may return nil, t, or a mailgroup +name. The value nil means that the check does not yield a decision, +and so, that further checks are needed. The value t means that the +message is definitely not spam, and that further spam checks should be +inhibited. Otherwise, a mailgroup name is returned where the mail +should go, and further checks are also inhibited. The usual mailgroup +name is the value of `spam-split-group', meaning that the message is +definitely a spam.") + +(defvar spam-list-of-statistical-checks + '(spam-use-ifile spam-use-stat spam-use-bogofilter) +"The spam-list-of-statistical-checks list contains all the mail +splitters that need to have the full message body available.") + +(defun spam-split (&rest specific-checks) + "Split this message into the `spam' group if it is spam. +This function can be used as an entry in `nnmail-split-fancy', for +example like this: (: spam-split). It can take checks as parameters. + +See the Info node `(gnus)Fancy Mail Splitting' for more details." + (interactive) + (save-excursion + (save-restriction + (dolist (check spam-list-of-statistical-checks) + (when (symbol-value check) + (widen) + (gnus-message 8 "spam-split: widening the buffer (%s requires it)" + (symbol-name check)) + (return))) + ;; (progn (widen) (debug (buffer-string))) + (let ((list-of-checks spam-list-of-checks) + decision) + (while (and list-of-checks (not decision)) + (let ((pair (pop list-of-checks))) + (when (and (symbol-value (car pair)) + (or (null specific-checks) + (memq (car pair) specific-checks))) + (gnus-message 5 "spam-split: calling the %s function" (symbol-name (cdr pair))) + (setq decision (funcall (cdr pair)))))) + (if (eq decision t) + nil + decision))))) + +(defun spam-setup-widening () + (dolist (check spam-list-of-statistical-checks) + (when (symbol-value check) + (setq nnimap-split-download-body-default t)))) + +(add-hook 'gnus-get-new-news-hook 'spam-setup-widening) + + +;;;; Regex headers + +(defun spam-check-regex-headers () + (let (ret found) + (dolist (h-regex spam-regex-headers-ham) + (unless found + (goto-char (point-min)) + (when (re-search-forward h-regex nil t) + (message "Ham regex header search positive.") + (setq found t)))) + (dolist (s-regex spam-regex-headers-spam) + (unless found + (goto-char (point-min)) + (when (re-search-forward s-regex nil t) + (message "Spam regex header search positive." (match-string 1)) + (setq found t) + (setq ret spam-split-group)))) + ret)) + + +;;;; Blackholes. + +(defun spam-check-blackholes () + "Check the Received headers for blackholed relays." + (let ((headers (message-fetch-field "received")) + ips matches) + (when headers + (with-temp-buffer + (insert headers) + (goto-char (point-min)) + (gnus-message 5 "Checking headers for relay addresses") + (while (re-search-forward + "\\[\\([0-9]+.[0-9]+.[0-9]+.[0-9]+\\)\\]" nil t) + (gnus-message 9 "Blackhole search found host IP %s." (match-string 1)) + (push (mapconcat 'identity + (nreverse (split-string (match-string 1) "\\.")) + ".") + ips))) + (dolist (server spam-blackhole-servers) + (dolist (ip ips) + (unless (and spam-blackhole-good-server-regex + (string-match spam-blackhole-good-server-regex ip)) + (let ((query-string (concat ip "." server))) + (if spam-use-dig + (let ((query-result (query-dig query-string))) + (when query-result + (gnus-message 5 "(DIG): positive blackhole check '%s'" + query-result) + (push (list ip server query-result) + matches))) + ;; else, if not using dig.el + (when (query-dns query-string) + (gnus-message 5 "positive blackhole check") + (push (list ip server (query-dns query-string 'TXT)) + matches)))))))) + (when matches + spam-split-group))) + +;;;; Hashcash. + +(condition-case nil + (progn + (require 'hashcash) + + (defun spam-check-hashcash () + "Check the headers for hashcash payments." + (mail-check-payment))) ;mail-check-payment returns a boolean + + (file-error (progn + (defalias 'mail-check-payment 'ignore) + (defalias 'spam-check-hashcash 'ignore)))) + +;;;; BBDB + +;;; original idea for spam-check-BBDB from Alexander Kotelnikov +;;; + +;; all this is done inside a condition-case to trap errors + +(condition-case nil + (progn + (require 'bbdb) + (require 'bbdb-com) + + (defun spam-enter-ham-BBDB (from) + "Enter an address into the BBDB; implies ham (non-spam) sender" + (when (stringp from) + (let* ((parsed-address (gnus-extract-address-components from)) + (name (or (car parsed-address) "Ham Sender")) + (net-address (car (cdr parsed-address)))) + (gnus-message 5 "Adding address %s to BBDB" from) + (when (and net-address + (not (bbdb-search-simple nil net-address))) + (bbdb-create-internal name nil net-address nil nil + "ham sender added by spam.el"))))) + + (defun spam-BBDB-register-routine () + (spam-generic-register-routine + ;; spam function + nil + ;; ham function + (lambda (article) + (spam-enter-ham-BBDB (spam-fetch-field-from-fast article))))) + + (defun spam-check-BBDB () + "Mail from people in the BBDB is classified as ham or non-spam" + (let ((who (message-fetch-field "from"))) + (when who + (setq who (cadr (gnus-extract-address-components who))) + (if (bbdb-search-simple nil who) + t + (if spam-use-BBDB-exclusive + spam-split-group + nil)))))) + + (file-error (progn + (defalias 'bbdb-search-simple 'ignore) + (defalias 'spam-check-BBDB 'ignore) + (defalias 'spam-BBDB-register-routine 'ignore) + (defalias 'spam-enter-ham-BBDB 'ignore) + (defalias 'bbdb-create-internal 'ignore) + (defalias 'bbdb-records 'ignore)))) + + +;;;; ifile + +;;; check the ifile backend; return nil if the mail was NOT classified +;;; as spam + +(defun spam-get-ifile-database-parameter () + "Get the command-line parameter for ifile's database from spam-ifile-database-path." + (if spam-ifile-database-path + (format "--db-file=%s" spam-ifile-database-path) + nil)) + +(defun spam-check-ifile () + "Check the ifile backend for the classification of this message" + (let ((article-buffer-name (buffer-name)) + category return) + (with-temp-buffer + (let ((temp-buffer-name (buffer-name)) + (db-param (spam-get-ifile-database-parameter))) + (save-excursion + (set-buffer article-buffer-name) + (if db-param + (call-process-region (point-min) (point-max) spam-ifile-path + nil temp-buffer-name nil "-q" "-c" db-param) + (call-process-region (point-min) (point-max) spam-ifile-path + nil temp-buffer-name nil "-q" "-c"))) + (goto-char (point-min)) + (if (not (eobp)) + (setq category (buffer-substring (point) (spam-point-at-eol)))) + (when (not (zerop (length category))) ; we need a category here + (if spam-ifile-all-categories + (setq return category) + ;; else, if spam-ifile-all-categories is not set... + (when (string-equal spam-ifile-spam-category category) + (setq return spam-split-group)))))) + return)) + +(defun spam-ifile-register-with-ifile (article-string category) + "Register an article, given as a string, with a category. +Uses `gnus-newsgroup-name' if category is nil (for ham registration)." + (when (stringp article-string) + (let ((category (or category gnus-newsgroup-name)) + (db-param (spam-get-ifile-database-parameter))) + (with-temp-buffer + (insert article-string) + (if db-param + (call-process-region (point-min) (point-max) spam-ifile-path + nil nil nil + "-h" "-i" category db-param) + (call-process-region (point-min) (point-max) spam-ifile-path + nil nil nil + "-h" "-i" category)))))) + +(defun spam-ifile-register-spam-routine () + (spam-generic-register-routine + (lambda (article) + (spam-ifile-register-with-ifile + (spam-get-article-as-string article) spam-ifile-spam-category)) + nil)) + +(defun spam-ifile-register-ham-routine () + (spam-generic-register-routine + nil + (lambda (article) + (spam-ifile-register-with-ifile + (spam-get-article-as-string article) spam-ifile-ham-category)))) + + +;;;; spam-stat + +(condition-case nil + (progn + (let ((spam-stat-install-hooks nil)) + (require 'spam-stat)) + + (defun spam-check-stat () + "Check the spam-stat backend for the classification of this message" + (let ((spam-stat-split-fancy-spam-group spam-split-group) ; override + (spam-stat-buffer (buffer-name)) ; stat the current buffer + category return) + (spam-stat-split-fancy))) + + (defun spam-stat-register-spam-routine () + (spam-generic-register-routine + (lambda (article) + (let ((article-string (spam-get-article-as-string article))) + (with-temp-buffer + (insert article-string) + (spam-stat-buffer-is-spam)))) + nil)) + + (defun spam-stat-register-ham-routine () + (spam-generic-register-routine + nil + (lambda (article) + (let ((article-string (spam-get-article-as-string article))) + (with-temp-buffer + (insert article-string) + (spam-stat-buffer-is-non-spam)))))) + + (defun spam-maybe-spam-stat-load () + (when spam-use-stat (spam-stat-load))) + + (defun spam-maybe-spam-stat-save () + (when spam-use-stat (spam-stat-save))) + + ;; Add hooks for loading and saving the spam stats + (add-hook 'gnus-save-newsrc-hook 'spam-maybe-spam-stat-save) + (add-hook 'gnus-get-top-new-news-hook 'spam-maybe-spam-stat-load) + (add-hook 'gnus-startup-hook 'spam-maybe-spam-stat-load)) + + (file-error (progn + (defalias 'spam-stat-register-ham-routine 'ignore) + (defalias 'spam-stat-register-spam-routine 'ignore) + (defalias 'spam-stat-buffer-is-spam 'ignore) + (defalias 'spam-stat-buffer-is-non-spam 'ignore) + (defalias 'spam-stat-split-fancy 'ignore) + (defalias 'spam-stat-load 'ignore) + (defalias 'spam-stat-save 'ignore) + (defalias 'spam-check-stat 'ignore)))) + + + +;;;; Blacklists and whitelists. + +(defvar spam-whitelist-cache nil) +(defvar spam-blacklist-cache nil) + +(defun spam-enter-whitelist (address) + "Enter ADDRESS into the whitelist." + (interactive "sAddress: ") + (spam-enter-list address spam-whitelist) + (setq spam-whitelist-cache nil)) + +(defun spam-enter-blacklist (address) + "Enter ADDRESS into the blacklist." + (interactive "sAddress: ") + (spam-enter-list address spam-blacklist) + (setq spam-blacklist-cache nil)) + +(defun spam-enter-list (address file) + "Enter ADDRESS into the given FILE, either the whitelist or the blacklist." + (unless (file-exists-p (file-name-directory file)) + (make-directory (file-name-directory file) t)) + (save-excursion + (set-buffer + (find-file-noselect file)) + (goto-char (point-max)) + (unless (bobp) + (insert "\n")) + (insert address "\n") + (save-buffer))) + +;;; returns t if the sender is in the whitelist, nil or spam-split-group otherwise +(defun spam-check-whitelist () + ;; FIXME! Should it detect when file timestamps change? + (unless spam-whitelist-cache + (setq spam-whitelist-cache (spam-parse-list spam-whitelist))) + (if (spam-from-listed-p spam-whitelist-cache) + t + (if spam-use-whitelist-exclusive + spam-split-group + nil))) + +(defun spam-check-blacklist () + ;; FIXME! Should it detect when file timestamps change? + (unless spam-blacklist-cache + (setq spam-blacklist-cache (spam-parse-list spam-blacklist))) + (and (spam-from-listed-p spam-blacklist-cache) spam-split-group)) + +(defun spam-parse-list (file) + (when (file-readable-p file) + (let (contents address) + (with-temp-buffer + (insert-file-contents file) + (while (not (eobp)) + (setq address (buffer-substring (point) (spam-point-at-eol))) + (forward-line 1) + (unless (zerop (length address)) + (setq address (regexp-quote address)) + (while (string-match "\\\\\\*" address) + (setq address (replace-match ".*" t t address))) + (push address contents)))) + (nreverse contents)))) + +(defun spam-from-listed-p (cache) + (let ((from (message-fetch-field "from")) + found) + (while cache + (when (string-match (pop cache) from) + (setq found t + cache nil))) + found)) + +(defun spam-blacklist-register-routine () + (spam-generic-register-routine + ;; the spam function + (lambda (article) + (let ((from (spam-fetch-field-from-fast article))) + (when (stringp from) + (spam-enter-blacklist from)))) + ;; the ham function + nil)) + +(defun spam-whitelist-register-routine () + (spam-generic-register-routine + ;; the spam function + nil + ;; the ham function + (lambda (article) + (let ((from (spam-fetch-field-from-fast article))) + (when (stringp from) + (spam-enter-whitelist from)))))) + + +;;;; Spam-report glue +(defun spam-report-gmane-register-routine () + (spam-generic-register-routine + 'spam-report-gmane + nil)) + + +;;;; Bogofilter +(defun spam-check-bogofilter-headers (&optional score) + (let ((header (message-fetch-field spam-bogofilter-header))) + (when (and header + (string-match spam-bogofilter-bogosity-positive-spam-header + header)) + (if score + (when (string-match "spamicity=\\([0-9.]+\\)" header) + (match-string 1 header)) + spam-split-group)))) + +;; return something sensible if the score can't be determined +(defun spam-bogofilter-score () + "Get the Bogofilter spamicity score" + (interactive) + (save-window-excursion + (gnus-summary-show-article t) + (set-buffer gnus-article-buffer) + (let ((score (or (spam-check-bogofilter-headers t) + (spam-check-bogofilter t)))) + (message "Spamicity score %s" score) + (or score "0")))) + +(defun spam-check-bogofilter (&optional score) + "Check the Bogofilter backend for the classification of this message" + (let ((article-buffer-name (buffer-name)) + return) + (with-temp-buffer + (let ((temp-buffer-name (buffer-name))) + (save-excursion + (set-buffer article-buffer-name) + (if spam-bogofilter-database-directory + (call-process-region (point-min) (point-max) + spam-bogofilter-path + nil temp-buffer-name nil "-v" + "-d" spam-bogofilter-database-directory) + (call-process-region (point-min) (point-max) spam-bogofilter-path + nil temp-buffer-name nil "-v"))) + (setq return (spam-check-bogofilter-headers score)))) + return)) + +(defun spam-bogofilter-register-with-bogofilter (article-string spam) + "Register an article, given as a string, as spam or non-spam." + (when (stringp article-string) + (let ((switch (if spam spam-bogofilter-spam-switch + spam-bogofilter-ham-switch))) + (with-temp-buffer + (insert article-string) + (if spam-bogofilter-database-directory + (call-process-region (point-min) (point-max) + spam-bogofilter-path + nil nil nil "-v" switch + "-d" spam-bogofilter-database-directory) + (call-process-region (point-min) (point-max) spam-bogofilter-path + nil nil nil "-v" switch)))))) + +(defun spam-bogofilter-register-spam-routine () + (spam-generic-register-routine + (lambda (article) + (spam-bogofilter-register-with-bogofilter + (spam-get-article-as-string article) t)) + nil)) + +(defun spam-bogofilter-register-ham-routine () + (spam-generic-register-routine + nil + (lambda (article) + (spam-bogofilter-register-with-bogofilter + (spam-get-article-as-string article) nil)))) + +(provide 'spam) + +;;; spam.el ends here. diff --git a/lisp/tls.el b/lisp/tls.el new file mode 100644 index 0000000..a2cced9 --- /dev/null +++ b/lisp/tls.el @@ -0,0 +1,130 @@ +;;; tls.el --- TLS/SSL support via wrapper around GnuTLS + +;; Copyright (C) 2003 Free Software Foundation, Inc. + +;; Author: Simon Josefsson +;; Keywords: comm, tls, gnutls, ssl + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; This package implements a simple wrapper around "gnutls-cli" to +;; make Emacs support TLS/SSL. +;; +;; Usage is the same as `open-network-stream', i.e.: +;; +;; (setq tmp (open-tls-stream "test" (current-buffer) "news.mozilla.org" 563)) +;; ... +;; # +;; (process-send-string tmp "mode reader\n") +;; 200 secnews.netscape.com Netscape-Collabra/3.52 03615 NNRP ready ... +;; nil +;; (process-send-string tmp "quit\n") +;; 205 +;; nil + +;; To use this package as a replacement for ssl.el by William M. Perry +;; , you need to evaluate the following: +;; +;; (defalias 'open-ssl-stream 'open-tls-stream) + +;;; Code: + +(require 'pces) + +(eval-and-compile + (autoload 'format-spec "format-spec") + (autoload 'format-spec-make "format-spec")) + +(defgroup tls nil + "Transport Layer Security (TLS) parameters." + :group 'comm) + +(defcustom tls-program '("gnutls-cli -p %p %h" + "gnutls-cli -p %p %h --protocols ssl3") + "List of strings containing commands to start TLS stream to a host. +Each entry in the list is tried until a connection is successful. +%s is replaced with server hostname, %p with port to connect to. +The program should read input on stdin and write output to +stdout. Also see `tls-success' for what the program should output +after successful negotiation." + :type '(repeat string) + :group 'tls) + +(defcustom tls-process-connection-type nil + "*Value for `process-connection-type' to use when starting process." + :type 'boolean + :group 'tls) + +(defcustom tls-success "- Handshake was completed" + "*Regular expression indicating completed TLS handshakes. +The default is what GNUTLS's \"gnutls-cli\" outputs." + :type 'regexp + :group 'tls) + +(defun open-tls-stream (name buffer host service) + "Open a TLS connection for a service to a host. +Returns a subprocess-object to represent the connection. +Input and output work as for subprocesses; `delete-process' closes it. +Args are NAME BUFFER HOST SERVICE. +NAME is name for process. It is modified if necessary to make it unique. +BUFFER is the buffer (or buffer-name) to associate with the process. + Process output goes at end of that buffer, unless you specify + an output stream or filter function to handle the output. + BUFFER may be also nil, meaning that this process is not associated + with any buffer +Third arg is name of the host to connect to, or its IP address. +Fourth arg SERVICE is name of the service desired, or an integer +specifying a port number to connect to." + (let ((cmds tls-program) cmd done) + (message "Opening TLS connection to `%s'..." host) + (while (and (not done) (setq cmd (pop cmds))) + (message "Opening TLS connection with `%s'..." cmd) + (let* ((process-connection-type tls-process-connection-type) + (process (as-binary-process + (start-process + name buffer shell-file-name shell-command-switch + (format-spec + cmd + (format-spec-make + ?h host + ?p (if (integerp service) + (int-to-string service) + service)))))) + response) + (while (and process + (memq (process-status process) '(open run)) + (save-excursion + (set-buffer buffer) ;; XXX "blue moon" nntp.el bug + (goto-char (point-min)) + (not (setq done (re-search-forward tls-success nil t))))) + (accept-process-output process 1) + (sit-for 1)) + (message "Opening TLS connection with `%s'...%s" cmd + (if done "done" "failed")) + (if done + (setq done process) + (delete-process process)))) + (message "Opening TLS connection to `%s'...%s" + host (if done "done" "failed")) + done)) + +(provide 'tls) + +;;; tls.el ends here diff --git a/lisp/yenc.el b/lisp/yenc.el new file mode 100644 index 0000000..3fea50f --- /dev/null +++ b/lisp/yenc.el @@ -0,0 +1,120 @@ +;;; yenc.el --- elisp native yenc decoder +;; Copyright (c) 2002 Free Software Foundation, Inc. + +;; Author: Jesper Harder +;; Keywords: yenc news + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; Functions for decoding yenc encoded messages. +;; +;; Limitations: +;; +;; * Does not handle multipart messages. +;; * No support for external decoders. +;; * Doesn't check the crc32 checksum (if present). + +;;; Code: + +(eval-when-compile (require 'cl)) + +(defconst yenc-begin-line + "^=ybegin.*$") + +(defconst yenc-decoding-vector + [214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 + 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 + 248 249 250 251 252 253 254 255 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 + 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 + 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 + 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 + 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 + 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 + 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 + 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 + 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 + 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 + 208 209 210 211 212 213]) + +;;;###autoload +(defun yenc-decode-region (start end) + "Yenc decode region between START and END using an internal decoder." + (interactive "r") + (let (work-buffer) + (unwind-protect + (save-excursion + (goto-char start) + (when (re-search-forward yenc-begin-line end t) + (let ((first (match-end 0)) + (header-alist (yenc-parse-line (match-string 0))) + bytes last footer-alist char) + (when (re-search-forward "^=ypart.*$" end t) + (setq first (match-end 0))) + (when (re-search-forward "^=yend.*$" end t) + (setq last (match-beginning 0)) + (setq footer-alist (yenc-parse-line (match-string 0))) + (let (default-enable-multibyte-characters) + (setq work-buffer (generate-new-buffer " *yenc-work*"))) + (while (< first last) + (setq char (char-after first)) + (cond ((or (eq char ?\r) + (eq char ?\n))) + ((eq char ?=) + (setq char (char-after (incf first))) + (with-current-buffer work-buffer + (insert-char (mod (- char 106) 256) 1))) + (t + (with-current-buffer work-buffer + ;;(insert-char (mod (- char 42) 256) 1) + (insert-char (aref yenc-decoding-vector char) 1)))) + (incf first)) + (setq bytes (buffer-size work-buffer)) + (unless (and (= (cdr (assq 'size header-alist)) bytes) + (= (cdr (assq 'size footer-alist)) bytes)) + (message "Warning: Size mismatch while decoding.")) + (goto-char start) + (delete-region start end) + (insert-buffer-substring work-buffer)))) + (and work-buffer (kill-buffer work-buffer)))))) + +;;;###autoload +(defun yenc-extract-filename () + "Extract file name from an yenc header." + (save-excursion + (when (re-search-forward yenc-begin-line nil t) + (cdr (assoc 'name (yenc-parse-line (match-string 0))))))) + +(defun yenc-parse-line (str) + "Extract file name and size from STR." + (let (result name) + (when (string-match "^=y.*size=\\([0-9]+\\)" str) + (push (cons 'size (string-to-number (match-string 1 str))) result)) + (when (string-match "^=y.*name=\\(.*\\)$" str) + (setq name (match-string 1 str)) + ;; Remove trailing white space + (when (string-match " +$" name) + (setq name (substring name 0 (match-beginning 0)))) + (push (cons 'name name) result)) + result)) + +(provide 'yenc) + +;;; yenc.el ends here