;;; imap-hash.el --- Hashtable-like interface to an IMAP mailbox ;; Copyright (C) 2009 Free Software Foundation, Inc. ;; Author: Teodor Zlatanov ;; Keywords: mail ;; 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 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; This module provides hashtable-like functions on top of imap.el ;; functionality. All the authentication is handled by auth-source so ;; there are no authentication options here, only the server and ;; mailbox names are needed. ;; Create a IHT (imap-hash table) object with `imap-hash-make'. Then ;; use it with `imap-hash-map' to map a function across all the ;; messages. Use `imap-hash-get' and `imap-hash-rem' to operate on ;; individual messages. See the tramp-imap.el library in Tramp if you ;; need to see practical examples. ;; This only works with IMAP4r1. Sorry to everyone without it, but ;; the compatibility code is too annoying and it's 2009. ;; TODO: Use SEARCH instead of FETCH when a test is specified. List ;; available mailboxes. Don't select an invalid mailbox. ;;; Code: (require 'assoc) (require 'imap) (require 'sendmail) ; for mail-header-separator (require 'message) (autoload 'auth-source-user-or-password "auth-source") ;; retrieve these headers (defvar imap-hash-headers (append '(Subject From Date Message-Id References In-Reply-To Xref))) ;; from nnheader.el (defsubst imap-hash-remove-cr-followed-by-lf () (goto-char (point-max)) (while (search-backward "\r\n" nil t) (delete-char 1))) ;; from nnheader.el (defun imap-hash-ms-strip-cr (&optional string) "Strip ^M from the end of all lines in current buffer or STRING." (if string (with-temp-buffer (insert string) (imap-hash-remove-cr-followed-by-lf) (buffer-string)) (save-excursion (imap-hash-remove-cr-followed-by-lf)))) (defun imap-hash-make (server port mailbox &optional user password ssl) "Makes a new imap-hash object using SERVER, PORT, and MAILBOX. SSL, USER, PASSWORD are optional. The test is set to t, meaning all messages are considered." (when (and server port mailbox) (list :server server :port port :mailbox mailbox :ssl ssl :user user :password password :test t))) (defun imap-hash-p (iht) "Checks whether IHT is a valid imap-hash." (and (imap-hash-server iht) (imap-hash-port iht) (imap-hash-mailbox iht) (imap-hash-test iht))) (defmacro imap-hash-gather (uid) `(imap-message-get ,uid 'BODYDETAIL)) (defmacro imap-hash-data-body (details) `(nth 2 (nth 1 ,details))) (defmacro imap-hash-data-headers (details) `(nth 2 (nth 0 ,details))) (defun imap-hash-get (key iht &optional refetch) "Get the value for KEY in the imap-hash IHT. Requires either `imap-hash-fetch' to be called beforehand (e.g. by `imap-hash-map'), or REFETCH to be t. Returns a list of the headers (an alist, see `imap-hash-map') and the body of the message as a string. Also see `imap-hash-test'." (with-current-buffer (imap-hash-get-buffer iht) (when refetch (imap-hash-fetch iht nil key)) (let ((details (imap-hash-gather key))) (list (imap-hash-get-headers (imap-hash-data-headers details)) (imap-hash-get-body (imap-hash-data-body details)))))) (defun imap-hash-put (value iht &optional key) "Put VALUE in the imap-hash IHT. Returns the new key. If KEY is given, removes it. VALUE can be a list of the headers (an alist, see `imap-hash-map') and the body of the message as a string. It can also be a uid, in which case `imap-hash-get' will be called to get the value. Also see `imap-hash-test'." (let ((server-buffer (imap-hash-get-buffer iht)) (value (if (listp value) value (imap-hash-get value iht))) newuid) (when value (with-temp-buffer (funcall 'imap-hash-make-message (nth 0 value) (nth 1 value) nil) (setq newuid (nth 1 (imap-message-append (imap-hash-mailbox iht) (current-buffer) nil nil server-buffer))) (when key (imap-hash-rem key iht)))) newuid)) (defun imap-hash-make-message (headers body &optional overrides) "Make a message with HEADERS and BODY suitable for `imap-append', using `message-setup'.. Look in the alist OVERRIDES for header overrides as per `imap-hash-headers'." ;; don't insert a signature no matter what (let (message-signature) (message-setup (append overrides headers)) (message-generate-headers message-required-mail-headers) (message-remove-header "X-Draft-From") (message-goto-body) (insert (or (aget overrides 'body) body "")) (goto-char (point-min)) ;; TODO: make this search better (if (search-forward mail-header-separator nil t) (delete-region (line-beginning-position) (line-end-position)) (error "Could not find the body separator in the encoded message!")))) (defun imap-hash-rem (key iht) "Remove KEY in the imap-hash IHT. Also see `imap-hash-test'. Requires `imap-hash-fetch' to have been called and the imap-hash server buffer to be current, so it's best to use it inside `imap-hash-map'. The key will not be found on the next `imap-hash-map' call." (with-current-buffer (imap-hash-get-buffer iht) (imap-message-flags-add (imap-range-to-message-set (list key)) "\\Deleted" 'silent) (imap-mailbox-expunge t))) (defun imap-hash-clear (iht) "Remove all keys in the imap-hash IHT. Also see `imap-hash-test'." (imap-hash-map (lambda (uid b c) (imap-hash-rem uid iht)) iht)) (defun imap-hash-get-headers (text-headers) (with-temp-buffer (insert (or text-headers "")) (imap-hash-remove-cr-followed-by-lf) (mapcar (lambda (header) (cons header (message-fetch-field (format "%s" header)))) imap-hash-headers))) (defun imap-hash-get-body (text) (with-temp-buffer (insert (or text "")) (imap-hash-remove-cr-followed-by-lf) (buffer-string))) (defun imap-hash-map (function iht &optional headers-only &rest messages) "Call FUNCTION for all entries in IHT and pass it the message uid, the headers (an alist, see `imap-hash-headers'), and the body contents as a string. If HEADERS-ONLY is not nil, the body will be nil. Returns results of evaluating, as would `mapcar'. If MESSAGES are given, iterate only over those UIDs. Also see `imap-hash-test'." (imap-hash-fetch iht headers-only) (let ((test (imap-hash-test iht))) (with-current-buffer (imap-hash-get-buffer iht) (delq nil (imap-message-map (lambda (message ignored-parameter) (let* ((details (imap-hash-gather message)) (headers (imap-hash-data-headers details)) (hlist (imap-hash-get-headers headers)) (runit (cond ((stringp test) (string-match test (format "%s" (aget hlist 'Subject)))) ((functionp test) (funcall test hlist)) ;; otherwise, return test itself (t test)))) ;;(debug message headers) (when runit (funcall function message (imap-hash-get-headers headers) (imap-hash-get-body (imap-hash-data-body details)))))) "UID"))))) (defun imap-hash-count (iht) "Counts the number of messages in the imap-hash IHT. Also see `imap-hash-test'. It uses `imap-hash-map' so just use that function if you want to do more than count the elements." (length (imap-hash-map (lambda (a b c)) iht t))) (defalias 'imap-hash-size 'imap-hash-count) (defun imap-hash-test (iht) "Returns the test used by `imap-hash-map' for IHT. When the test is t, any key will be a candidate. When the test is a string, messages will be filtered on that string as a regexp against the subject. When the test is a function, messages will be filtered with it. The function is passed the message headers (see `imap-hash-get-headers')." (plist-get iht :test)) (defun imap-hash-server (iht) "Returns the server used by the imap-hash IHT." (plist-get iht :server)) (defun imap-hash-port (iht) "Returns the port used by the imap-hash IHT." (plist-get iht :port)) (defun imap-hash-ssl (iht) "Returns the SSL need for the imap-hash IHT." (plist-get iht :ssl)) (defun imap-hash-mailbox (iht) "Returns the mailbox used by the imap-hash IHT." (plist-get iht :mailbox)) (defun imap-hash-user (iht) "Returns the username used by the imap-hash IHT." (plist-get iht :user)) (defun imap-hash-password (iht) "Returns the password used by the imap-hash IHT." (plist-get iht :password)) (defun imap-hash-open-connection (iht) "Open the connection used for IMAP interactions with the imap-hash IHT." (let* ((server (imap-hash-server iht)) (port (imap-hash-port iht)) (ssl-need (imap-hash-ssl iht)) (auth-need (not (and (imap-hash-user iht) (imap-hash-password iht)))) ;; this will not be needed if auth-need is t (auth-info (when auth-need (auth-source-user-or-password '("login" "password") server port))) (auth-user (or (imap-hash-user iht) (nth 0 auth-info))) (auth-passwd (or (imap-hash-password iht) (nth 1 auth-info))) (imap-logout-timeout nil)) ;; (debug "opening server: opened+state" (imap-opened) imap-state) ;; this is the only place where IMAP vs IMAPS matters (if (imap-open server port (if ssl-need 'ssl nil) nil (current-buffer)) (progn ;; (debug "after opening server: opened+state" (imap-opened (current-buffer)) imap-state) ;; (debug "authenticating" auth-user auth-passwd) (if (not (imap-capability 'IMAP4rev1)) (error "IMAP server does not support IMAP4r1, it won't work, sorry.") (imap-authenticate auth-user auth-passwd) (imap-id) ;; (debug "after authenticating: opened+state" (imap-opened (current-buffer)) imap-state) (imap-opened (current-buffer)))) (error "Could not open the IMAP buffer")))) (defun imap-hash-get-buffer (iht) "Get or create the connection buffer to be used for the imap-hash IHT." (let* ((name (imap-hash-buffer-name iht)) (buffer (get-buffer name))) (if (and buffer (imap-opened buffer)) buffer (when buffer (kill-buffer buffer)) (with-current-buffer (get-buffer-create name) (setq buffer-undo-list t) (when (imap-hash-open-connection iht) (current-buffer)))))) (defun imap-hash-buffer-name (iht) "Get the connection buffer to be used for the imap-hash IHT." (when (imap-hash-p iht) (let ((server (imap-hash-server iht)) (port (imap-hash-port iht)) (ssl-text (if (imap-hash-ssl iht) "SSL" "NoSSL"))) (format "*imap-hash/%s:%s:%s*" server port ssl-text)))) (defun imap-hash-fetch (iht &optional headers-only &rest messages) "Fetch all the messages for imap-hash IHT. Get only the headers if HEADERS-ONLY is not nil." (with-current-buffer (imap-hash-get-buffer iht) (let ((range (if messages (list (imap-range-to-message-set messages) (imap-range-to-message-set messages)) '("1:*" . "1,*:*")))) ;; (with-current-buffer "*imap-debug*" ;; (erase-buffer)) (imap-mailbox-unselect) (imap-mailbox-select (imap-hash-mailbox iht)) ;; (debug "after selecting mailbox: opened+state" (imap-opened) imap-state) ;; (setq imap-message-data (make-vector imap-message-prime 0) (imap-fetch-safe range (concat (format "(UID RFC822.SIZE BODY %s " (if headers-only "" "BODY.PEEK[TEXT]")) (format "BODY.PEEK[HEADER.FIELDS %s])" imap-hash-headers)))))) (provide 'imap-hash) ;;; imap-hash.el ends here ;; ignore, for testing only ;;; (setq iht (imap-hash-make "yourhosthere.com" "imap" "INBOX.test")) ;;; (setq iht (imap-hash-make "yourhosthere.com" "imap" "test")) ;;; (imap-hash-make "server1" "INBOX.mailbox2") ;;; (imap-hash-p iht) ;;; (imap-hash-get 35 iht) ;;; (imap-hash-get 38 iht) ;;; (imap-hash-get 37 iht t) ;;; (mapc (lambda (buffer) (with-current-buffer buffer (erase-buffer))) '("*imap-debug*" "*imap-log*")) ;;; (imap-hash-put (imap-hash-get 5 iht) iht) ;;; (with-current-buffer (imap-hash-get-buffer iht) (let ((uid (imap-hash-put (imap-hash-get 5 iht) iht))) (imap-hash-put uid iht uid))) ;;; (imap-hash-put (imap-hash-get 35 iht) iht) ;;; (imap-hash-make-message '((Subject . "normal")) "normal body") ;;; (imap-hash-make-message '((Subject . "old")) "old body" '((Subject . "new"))) ;;; (imap-hash-make-message '((Subject . "old")) "old body" '((body . "new body")) (lambda (subject) (concat "overwrite-" subject))) ;;; (imap-hash-make-message '((Subject . "old")) "old body" '((Subject . "change this")) (lambda (subject) (concat "overwrite-" subject))) ;;; (imap-hash-make-message '((Subject . "Twelcome")) "body here" nil) ;; (with-current-buffer (imap-hash-get-buffer iht) (imap-hash-rem (imap-hash-put (imap-hash-get 5 iht) iht) iht)) ;;; (kill-buffer (imap-hash-buffer-name iht)) ;;; (imap-hash-map 'debug iht) ;;; (imap-hash-map 'debug iht t) ;;;(tramp-imap-handle-file-inode "/imap:yourhosthere.com:/test/welcome") ;;;(imap-hash-count iht) ;;; (mapc (lambda (buffer) (with-current-buffer buffer (erase-buffer))) '("*imap-debug*" "*imap-log*")) ;;; (kill-buffer (imap-hash-buffer-name iht)) ;;; this should always return t if the server is up, automatically reopening if needed ;;; (imap-opened (imap-hash-get-buffer iht)) ;;; (imap-hash-buffer-name iht) ;;; (with-current-buffer (imap-hash-get-buffer iht) (debug "mailbox data, auth and state" imap-mailbox-data imap-auth imap-state)) ;;;(tramp-imap-handle-file-inode "/imap:yourhosthere.com:/test/welcome") ;;; (imap-hash-fetch iht nil) ;;; (imap-hash-fetch iht t) ;;; (imap-hash-fetch iht nil 1 2 3) ;;; (imap-hash-fetch iht t 1 2 3)