Mercurial > emacs
changeset 105250:ceeb0491fbb4
* net/tramp-imap.el: New package.
author | Michael Albinus <michael.albinus@gmx.de> |
---|---|
date | Mon, 28 Sep 2009 11:59:22 +0000 |
parents | 99d98ed749b9 |
children | 2ce28d0d9fe1 |
files | lisp/net/tramp-imap.el |
diffstat | 1 files changed, 801 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lisp/net/tramp-imap.el Mon Sep 28 11:59:22 2009 +0000 @@ -0,0 +1,801 @@ +;;; tramp-imap.el --- Tramp interface to IMAP through imap.el + +;; Copyright (C) 2009 Free Software Foundation, Inc. + +;; Author: Teodor Zlatanov <tzz@lifelogs.com> +;; Keywords: mail, comm + +;; 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 3 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Package to provide Tramp over IMAP + +;;; Setup: + +;; just load and open files, e.g. +;; /imaps:user@yourhosthere.com:/INBOX.test/1 +;; or +;; /imap:user@yourhosthere.com:/INBOX.test/1 + +;; where `imap' goes over IMAP, while `imaps' goes over IMAP+SSL + +;; This module will use imap-hash.el to access the IMAP mailbox. + +;; This module will use auth-source.el to authenticate against the +;; IMAP server, PLUS it will use auth-source.el to get your passphrase +;; for the symmetrically encrypted messages. For the former, use the +;; usual IMAP ports. For the latter, use the port "tramp-imap". + +;; example .authinfo / .netrc file: + +;; machine yourhosthere.com port tramp-imap login USER password SYMMETRIC-PASSPHRASE + +;; note above is the symmetric encryption passphrase for GPG +;; below is the regular password for IMAP itself and other things on that host + +;; machine yourhosthere.com login USER password NORMAL-PASSWORD + + +;;; Code: + +(require 'assoc) +(require 'tramp) +(require 'tramp-compat) +(require 'message) +(require 'imap-hash) +(require 'epa) +(autoload 'auth-source-user-or-password "auth-source") + +;; Define Tramp IMAP method ... +(defconst tramp-imap-method "imap" + "*Method to connect via IMAP protocol.") + +(add-to-list 'tramp-methods (list tramp-imap-method '(tramp-default-port 143))) + +;; Add a default for `tramp-default-user-alist'. Default is the local user. +(add-to-list 'tramp-default-user-alist + `(,tramp-imap-method nil ,(user-login-name))) + +;; Define Tramp IMAPS method ... +(defconst tramp-imaps-method "imaps" + "*Method to connect via secure IMAP protocol.") + +;; ... and add it to the method list. +(add-to-list 'tramp-methods (list tramp-imaps-method '(tramp-default-port 993))) + +;; Add a default for `tramp-default-user-alist'. Default is the local user. +(add-to-list 'tramp-default-user-alist + `(,tramp-imaps-method nil ,(user-login-name))) + +;; Add completion function for IMAP method. +;; (tramp-set-completion-function +;; tramp-imap-method tramp-completion-function-alist-ssh) ; TODO: test this +;; tramp-imaps-method tramp-completion-function-alist-ssh) ; TODO: test this + +;; New handlers should be added here. +(defconst tramp-imap-file-name-handler-alist + '( + ;; `access-file' performed by default handler + (add-name-to-file . ignore) + ;; `byte-compiler-base-file-name' performed by default handler + (copy-file . tramp-imap-handle-copy-file) + (delete-directory . ignore) ;; tramp-imap-handle-delete-directory) + (delete-file . tramp-imap-handle-delete-file) + ;; `diff-latest-backup-file' performed by default handler + (directory-file-name . tramp-handle-directory-file-name) + (directory-files . tramp-handle-directory-files) + (directory-files-and-attributes + . tramp-imap-handle-directory-files-and-attributes) + ;; `dired-call-process' performed by default handler + ;; `dired-compress-file' performed by default handler + ;; `dired-uncache' performed by default handler + (expand-file-name . tramp-imap-handle-expand-file-name) + ;; `file-accessible-directory-p' performed by default handler + (file-attributes . tramp-imap-handle-file-attributes) + (file-directory-p . tramp-imap-handle-file-directory-p) + (file-executable-p . tramp-imap-handle-file-executable-p) + (file-exists-p . tramp-imap-handle-file-exists-p) + (file-local-copy . tramp-imap-handle-file-local-copy) + (file-remote-p . tramp-handle-file-remote-p) + (file-modes . tramp-handle-file-modes) + (file-name-all-completions . tramp-imap-handle-file-name-all-completions) + (file-name-as-directory . tramp-handle-file-name-as-directory) + (file-name-completion . tramp-handle-file-name-completion) + (file-name-directory . tramp-handle-file-name-directory) + (file-name-nondirectory . tramp-handle-file-name-nondirectory) + ;; `file-name-sans-versions' performed by default handler + (file-newer-than-file-p . tramp-imap-handle-file-newer-than-file-p) + (file-ownership-preserved-p . ignore) + (file-readable-p . tramp-imap-handle-file-readable-p) + (file-regular-p . tramp-handle-file-regular-p) + (file-symlink-p . tramp-handle-file-symlink-p) + ;; `file-truename' performed by default handler + (file-writable-p . tramp-imap-handle-file-writable-p) + (find-backup-file-name . tramp-handle-find-backup-file-name) + ;; `find-file-noselect' performed by default handler + ;; `get-file-buffer' performed by default handler + (insert-directory . tramp-imap-handle-insert-directory) + (insert-file-contents . tramp-imap-handle-insert-file-contents) + (load . tramp-handle-load) + (make-directory . ignore) ;; tramp-imap-handle-make-directory) + (make-directory-internal . ignore) ;; tramp-imap-handle-make-directory-internal) + (make-symbolic-link . ignore) + (rename-file . tramp-imap-handle-rename-file) + (set-file-modes . ignore) + (set-file-times . ignore) ;; tramp-imap-handle-set-file-times) + (set-visited-file-modtime . ignore) + (shell-command . ignore) + (substitute-in-file-name . tramp-handle-substitute-in-file-name) + (unhandled-file-name-directory . tramp-handle-unhandled-file-name-directory) + (vc-registered . ignore) + (verify-visited-file-modtime . ignore) + (write-region . tramp-imap-handle-write-region) + (executable-find . ignore) + (start-file-process . ignore) + (process-file . ignore) +) + "Alist of handler functions for Tramp IMAP method. +Operations not mentioned here will be handled by the default Emacs primitives.") + +(defgroup tramp-imap nil + "Tramp over IMAP configuration." + :version "23.2" + :group 'applications) + +(defcustom tramp-imap-subject-marker "tramp-imap-subject-marker" + "The subject marker that Tramp-IMAP will use." + :type 'string + :version "23.2" + :group 'tramp-imap) + +;; TODO: these will be defcustoms later. +(defvar tramp-imap-passphrase-cache nil) ;; can be t or 'never +(defvar tramp-imap-passphrase nil) + +(defun tramp-imap-file-name-p (filename) + "Check if it's a filename for IMAP protocol." + (let ((v (tramp-dissect-file-name filename))) + (or + (string= (tramp-file-name-method v) tramp-imap-method) + (string= (tramp-file-name-method v) tramp-imaps-method)))) + +(defun tramp-imap-file-name-handler (operation &rest args) + "Invoke the IMAP related OPERATION. +First arg specifies the OPERATION, second arg is a list of arguments to +pass to the OPERATION." + (let ((fn (assoc operation tramp-imap-file-name-handler-alist))) + (if fn + (save-match-data (apply (cdr fn) args)) + (tramp-run-real-handler operation args)))) + +(add-to-list 'tramp-foreign-file-name-handler-alist + (cons 'tramp-imap-file-name-p 'tramp-imap-file-name-handler)) + +(defun tramp-imap-handle-copy-file + (filename newname &optional ok-if-already-exists keep-date preserve-uid-gid) + "Like `copy-file' for Tramp files." + (tramp-imap-do-copy-or-rename-file + 'copy filename newname ok-if-already-exists keep-date preserve-uid-gid)) + +(defun tramp-imap-handle-rename-file + (filename newname &optional ok-if-already-exists) + "Like `rename-file' for Tramp files." + (tramp-imap-do-copy-or-rename-file + 'rename filename newname ok-if-already-exists t t)) + +(defun tramp-imap-do-copy-or-rename-file + (op filename newname &optional ok-if-already-exists keep-date preserve-uid-gid) + "Copy or rename a remote file. +OP must be `copy' or `rename' and indicates the operation to perform. +FILENAME specifies the file to copy or rename, NEWNAME is the name of +the new file (for copy) or the new name of the file (for rename). +OK-IF-ALREADY-EXISTS means don't barf if NEWNAME exists already. +KEEP-DATE means to make sure that NEWNAME has the same timestamp +as FILENAME. PRESERVE-UID-GID, when non-nil, instructs to keep +the uid and gid if both files are on the same host. + +This function is invoked by `tramp-imap-handle-copy-file' and +`tramp-imap-handle-rename-file'. It is an error if OP is neither +of `copy' and `rename'." + (unless (memq op '(copy rename)) + (error "Unknown operation `%s', must be `copy' or `rename'" op)) + (setq filename (expand-file-name filename)) + (setq newname (expand-file-name newname)) + (when (file-directory-p newname) + (setq newname (expand-file-name (file-name-nondirectory filename) newname))) + + (let ((t1 (and (tramp-tramp-file-p filename) + (tramp-imap-file-name-p filename))) + (t2 (and (tramp-tramp-file-p newname) + (tramp-imap-file-name-p newname)))) + + (when (and (not ok-if-already-exists) (file-exists-p newname)) + (with-parsed-tramp-file-name (if t1 filename newname) nil + (tramp-error + v 'file-already-exists "File %s already exists" newname))) + + (with-parsed-tramp-file-name (if t1 filename newname) nil + (tramp-message v 0 "Transferring %s to %s..." filename newname)) + + ;; We just make a local copy of FILENAME, and write it then to + ;; NEWNAME. This must be optimized, when both files are located + ;; on the same IMAP server. + (with-temp-buffer + (if (and t1 t2) + ;; We don't encrypt. + (with-parsed-tramp-file-name newname nil + (insert (tramp-imap-get-file filename nil)) + (tramp-imap-put-file + v (current-buffer) + (tramp-imap-file-name-name v) + (tramp-imap-get-file-inode newname) + nil)) + ;; One of them is not located on a IMAP mailbox. + (insert-file-contents filename) + (write-region (point-min) (point-max) newname))) + + (with-parsed-tramp-file-name (if t1 filename newname) nil + (tramp-message v 0 "Transferring %s to %s...done" filename newname)) + + (when (eq op 'rename) + (delete-file filename)))) + +;; TODO: revise this much +(defun tramp-imap-handle-expand-file-name (name &optional dir) + "Like `expand-file-name' for Tramp files." + ;; If DIR is not given, use DEFAULT-DIRECTORY or "/". + (setq dir (or dir default-directory "/")) + ;; Unless NAME is absolute, concat DIR and NAME. + (unless (file-name-absolute-p name) + (setq name (concat (file-name-as-directory dir) name))) + ;; If NAME is not a Tramp file, run the real handler. + (if (or (tramp-completion-mode-p) (not (tramp-tramp-file-p name))) + (tramp-drop-volume-letter + (tramp-run-real-handler 'expand-file-name (list name nil))) + ;; Dissect NAME. + (with-parsed-tramp-file-name name nil + (unless (tramp-run-real-handler 'file-name-absolute-p (list localname)) + (setq localname (concat "/" localname))) + ;; There might be a double slash, for example when "~/" + ;; expands to "/". Remove this. + (while (string-match "//" localname) + (setq localname (replace-match "/" t t localname))) + ;; Do normal `expand-file-name' (this does "/./" and "/../"). + ;; We bind `directory-sep-char' here for XEmacs on Windows, + ;; which would otherwise use backslash. `default-directory' is + ;; bound, because on Windows there would be problems with UNC + ;; shares or Cygwin mounts. + (let ((default-directory (tramp-compat-temporary-file-directory))) + (tramp-make-tramp-file-name + method user host + (tramp-drop-volume-letter + (tramp-run-real-handler + 'expand-file-name (list localname)))))))) + +;; This function should return "foo/" for directories and "bar" for +;; files. +(defun tramp-imap-handle-file-name-all-completions (filename directory) + "Like `file-name-all-completions' for Tramp files." + (all-completions + filename + (with-parsed-tramp-file-name (expand-file-name directory) nil + (save-match-data + (let ((entries + (tramp-imap-get-file-entries v localname))) + (mapcar + (lambda (x) + (list + (if (string-match "d" (nth 9 x)) + (file-name-as-directory (nth 0 x)) + (nth 0 x)))) + entries)))))) + +(defun tramp-imap-get-file-entries (vec localname &optional exact) + "Read entries returned by IMAP server. EXACT limits to exact matches. +Result is a list of (LOCALNAME LINK COUNT UID GID ATIME MTIME CTIME +SIZE MODE WEIRD INODE DEVICE)." + (tramp-message vec 5 "working on %s" localname) + (let* ((name (tramp-imap-file-name-name vec)) + (search-name (or name "")) + (search-name (if exact (concat search-name "$") search-name)) + (iht (tramp-imap-make-iht vec search-name))) +;; TODO: catch errors + ;; (tramp-error vec 'none "bad name %s or mailbox %s" name mbox)) + (imap-hash-map (lambda (uid headers body) + (let ((subject (substring + (aget headers 'Subject "") + (length tramp-imap-subject-marker)))) + (list + subject + nil + -1 + 1 + 1 + '(0 0) + '(0 0) + '(0 0) + 1 + "-rw-rw-rw-" + nil + uid + (tramp-get-device vec)))) + iht t))) + +(defun tramp-imap-handle-write-region (start end filename &optional append visit lockname confirm) + "Like `write-region' for Tramp files." + (setq filename (expand-file-name filename)) + (with-parsed-tramp-file-name filename nil + ;; XEmacs takes a coding system as the seventh argument, not `confirm'. + (when (and (not (featurep 'xemacs)) + confirm (file-exists-p filename)) + (unless (y-or-n-p (format "File %s exists; overwrite anyway? " + filename)) + (tramp-error v 'file-error "File not overwritten"))) + (tramp-flush-file-property v localname) + (let* ((old-buffer (current-buffer)) + (inode (tramp-imap-get-file-inode filename)) + (min 1) + (max (point-max)) + ;; Make sure we have good start and end values. + (start (or start min)) + (end (or end max)) + temp-buffer) + (with-temp-buffer + (setq temp-buffer (if (and (eq start min) (eq end max)) + old-buffer + ;; If this is a region write, insert the substring. + (insert + (with-current-buffer old-buffer + (buffer-substring-no-properties start end))) + (current-buffer))) + (tramp-imap-put-file v + temp-buffer + (tramp-imap-file-name-name v) + inode + t))) + (when (eq visit t) + (set-visited-file-modtime)))) + +(defun tramp-imap-handle-insert-directory + (filename switches &optional wildcard full-directory-p) + "Like `insert-directory' for Tramp files." + (setq filename (expand-file-name filename)) + (when full-directory-p + ;; Called from `dired-add-entry'. + (setq filename (file-name-as-directory filename))) + (with-parsed-tramp-file-name filename nil + (save-match-data + (let ((base (file-name-nondirectory localname)) + (entries (copy-sequence + (tramp-imap-get-file-entries + v (file-name-directory localname))))) + + (when wildcard + (when (string-match "\\." base) + (setq base (replace-match "\\\\." nil nil base))) + (when (string-match "\\*" base) + (setq base (replace-match ".*" nil nil base))) + (when (string-match "\\?" base) + (setq base (replace-match ".?" nil nil base)))) + + ;; Filter entries. + (setq entries + (delq + nil + (if (or wildcard (zerop (length base))) + ;; Check for matching entries. + (mapcar + (lambda (x) + (when (string-match + (format "^%s" base) (nth 0 x)) + x)) + entries) + ;; We just need the only and only entry FILENAME. + (list (assoc base entries))))) + + ;; Sort entries. + (setq entries + (sort + entries + (lambda (x y) + (if (string-match "t" switches) + ;; Sort by date. + (tramp-time-less-p (nth 6 y) (nth 6 x)) + ;; Sort by name. + (string-lessp (nth 0 x) (nth 0 y)))))) + + ;; Handle "-F" switch. + (when (string-match "F" switches) + (mapc + (lambda (x) + (when (not (zerop (length (car x)))) + (cond + ((char-equal ?d (string-to-char (nth 9 x))) + (setcar x (concat (car x) "/"))) + ((char-equal ?x (string-to-char (nth 9 x))) + (setcar x (concat (car x) "*")))))) + entries)) + + ;; Print entries. + (mapcar + (lambda (x) + (when (not (zerop (length (nth 0 x)))) + (insert + (format + "%10s %3d %-8s %-8s %8s %s " + (nth 9 x) ; mode + (nth 11 x) ; inode + "nobody" "nogroup" + (nth 8 x) ; size + (format-time-string + (if (tramp-time-less-p + (tramp-time-subtract (current-time) (nth 6 x)) + tramp-half-a-year) + "%b %e %R" + "%b %e %Y") + (nth 6 x)))) ; date + ;; For the file name, we set the `dired-filename' + ;; property. This allows to handle file names with + ;; leading or trailing spaces as well. + (let ((pos (point))) + (insert (format "%s" (nth 0 x))) ; file name + (put-text-property pos (point) 'dired-filename t)) + (insert "\n") + (forward-line) + (beginning-of-line))) + entries))))) + +(defun tramp-imap-handle-insert-file-contents + (filename &optional visit beg end replace) + "Like `insert-file-contents' for Tramp files." + (barf-if-buffer-read-only) + (when visit + (setq buffer-file-name (expand-file-name filename)) + (set-visited-file-modtime) + (set-buffer-modified-p nil)) + (with-parsed-tramp-file-name filename nil + (if (not (file-exists-p filename)) + (tramp-error + v 'file-error "File `%s' not found on remote host" filename) + (let ((point (point)) + size data) + (tramp-message v 4 "Fetching file %s..." filename) + (insert (tramp-imap-get-file filename t)) + (setq size (- (point) point)) +;;; TODO: handle ranges. +;;; (let ((beg (or beg (point-min))) +;;; (end (min (or end (point-max)) (point-max)))) +;;; (setq size (- end beg)) +;;; (buffer-substring beg end)) + (goto-char point) + (tramp-message v 4 "Fetching file %s...done" filename) + (list (expand-file-name filename) size))))) + +(defun tramp-imap-handle-file-exists-p (filename) + "Like `file-exists-p' for Tramp files." + (and (file-attributes filename) t)) + +(defun tramp-imap-handle-file-directory-p (filename) + "Like `file-directory-p' for Tramp-IMAP files." + ;; We allow only mailboxes to be a directory. + (with-parsed-tramp-file-name (expand-file-name filename default-directory) nil + (and (string-match "^/[^/]*$" (directory-file-name localname)) t))) + +(defun tramp-imap-handle-file-attributes (filename &optional id-format) + "Like `file-attributes' for Tramp-IMAP FILENAME." + (with-parsed-tramp-file-name (expand-file-name filename) nil + (cdr-safe (nth 0 (tramp-imap-get-file-entries v localname))))) + +(defun tramp-imap-get-file-inode (filename &optional id-format) + "Get inode equivalent \(actually the UID) for Tramp-IMAP FILENAME." + (nth 10 (tramp-compat-file-attributes filename id-format))) + +(defun tramp-imap-handle-file-executable-p (filename) + "Like `file-executable-p' for Tramp files. False for IMAP." + nil) + +(defun tramp-imap-handle-file-readable-p (filename) + "Like `file-readable-p' for Tramp files. True for IMAP." + (file-exists-p filename)) + +(defun tramp-imap-handle-file-writable-p (filename) + "Like `file-writable-p' for Tramp files. True for IMAP." + ;; `file-exists-p' does not work yet for directories. + ;; (file-exists-p (file-name-directory filename))) + (file-directory-p (file-name-directory filename))) + +(defun tramp-imap-handle-delete-file (filename) + "Like `delete-file' for Tramp files." + (cond + ((not (file-exists-p filename)) nil) + (t (with-parsed-tramp-file-name (expand-file-name filename) nil + (let ((iht (tramp-imap-make-iht v))) + (imap-hash-rem (tramp-imap-get-file-inode filename) iht)))))) + +(defun tramp-imap-handle-directory-files-and-attributes + (directory &optional full match nosort id-format) + "Like `directory-files-and-attributes' for Tramp files." + (mapcar + (lambda (x) + (cons x (tramp-compat-file-attributes + (if full x (expand-file-name x directory)) id-format))) + (directory-files directory full match nosort))) + +;; TODO: fix this in tramp-imap-get-file-entries. +(defun tramp-imap-handle-file-newer-than-file-p (file1 file2) + "Like `file-newer-than-file-p' for Tramp files." + (cond + ((not (file-exists-p file1)) nil) + ((not (file-exists-p file2)) t) + (t (tramp-time-less-p (nth 5 (file-attributes file2)) + (nth 5 (file-attributes file1)))))) + +(defun tramp-imap-handle-file-local-copy (filename) + "Like `file-local-copy' for Tramp files." + (with-parsed-tramp-file-name (expand-file-name filename) nil + (unless (file-exists-p filename) + (tramp-error + v 'file-error + "Cannot make local copy of non-existing file `%s'" filename)) + (let ((tmpfile (tramp-compat-make-temp-file filename))) + (tramp-message v 4 "Fetching %s to tmp file %s..." filename tmpfile) + (with-temp-buffer + (insert-file-contents filename) + (write-region (point-min) (point-max) tmpfile) + (tramp-message v 4 "Fetching %s to tmp file %s...done" filename tmpfile) + tmpfile)))) + +(defun tramp-imap-put-file (vec filename-or-buffer &optional subject inode encode) + "Write contents of FILENAME-OR-BUFFER to Tramp-IMAP file VEC with name SUBJECT. +When INODE is given, delete that old remote file after writing the new one +\(normally this is the old file with the same name)." + ;; `tramp-current-host' is used in `tramp-imap-passphrase-callback-function'. + (let ((tramp-current-host (tramp-file-name-real-host vec)) + (iht (tramp-imap-make-iht vec))) + (imap-hash-put (list + (list (cons + 'Subject + (format + "%s%s" + tramp-imap-subject-marker + (or subject "no subject")))) + (cond ((bufferp filename-or-buffer) + (with-current-buffer filename-or-buffer + (if encode + (tramp-imap-encode-buffer) + (buffer-string)))) + ;; TODO: allow file names. + (t "No body available"))) + iht + inode))) + +(defun tramp-imap-get-file (filename &optional decode) + ;; (debug (tramp-imap-get-file-inode filename)) + (with-parsed-tramp-file-name (expand-file-name filename) nil + (condition-case () + ;; `tramp-current-host' is used in + ;; `tramp-imap-passphrase-callback-function'. + (let* ((tramp-current-host (tramp-file-name-real-host v)) + (iht (tramp-imap-make-iht v)) + (inode (tramp-imap-get-file-inode filename)) + (data (imap-hash-get inode iht t))) + (if decode + (with-temp-buffer + (insert (nth 1 data)) + ;;(debug inode (buffer-string)) + (tramp-imap-decode-buffer)) + (nth 1 data))) + (error (tramp-error + v 'file-error "File `%s' could not be read" filename))))) + +(defun tramp-imap-passphrase-callback-function (context key-id handback) + "Called by EPG to get a passphrase for Tramp-IMAP. +CONTEXT is the encryption/decryption EPG context. +HANDBACK is just carried through. +KEY-ID can be 'SYM or 'PIN among others." + (let* ((server tramp-current-host) + (port "tramp-imap") ; this is NOT the server password! + (auth-passwd + (auth-source-user-or-password "password" server port))) + (or + (copy-sequence auth-passwd) + ;; If we cache the passphrase and we have one. + (if (and (eq tramp-imap-passphrase-cache t) + tramp-imap-passphrase) + ;; Do we reuse it? + (if (y-or-n-p "Reuse the passphrase? ") + (copy-sequence tramp-imap-passphrase) + ;; Don't reuse: revert caching behavior to nil, erase passphrase, + ;; call ourselves again. + (setq tramp-imap-passphrase-cache nil) + (setq tramp-imap-passphrase nil) + (tramp-imap-passphrase-callback-function context key-id handback)) + (let ((p (if (eq key-id 'SYM) + (read-passwd + "Tramp-IMAP passphrase for symmetric encryption: " + (eq (epg-context-operation context) 'encrypt) + tramp-imap-passphrase) + (read-passwd + (if (eq key-id 'PIN) + "Tramp-IMAP passphrase for PIN: " + (let ((entry (assoc key-id epg-user-id-alist))) + (if entry + (format "Tramp-IMAP passphrase for %s %s: " + key-id (cdr entry)) + (format "Tramp-IMAP passphrase for %s: " key-id)))) + nil + tramp-imap-passphrase)))) + + ;; If we have an answer, the passphrase has changed, + ;; the user hasn't declined keeping the passphrase, + ;; and they answer yes to keep it now... + (when (and + p + (not (equal tramp-imap-passphrase p)) + (not (eq tramp-imap-passphrase-cache 'never)) + (y-or-n-p "Keep the passphrase? ")) + (setq tramp-imap-passphrase (copy-sequence p)) + (setq tramp-imap-passphrase-cache t)) + + ;; If we still don't have a passphrase, the user didn't want + ;; to keep it. + (when (and + p + (not tramp-imap-passphrase)) + (setq tramp-imap-passphrase-cache 'never)) + + p))))) + +(defun tramp-imap-encode-buffer () + (let ((context (epg-make-context 'OpenPGP)) + cipher) + (epg-context-set-armor context t) + (epg-context-set-passphrase-callback context + #'tramp-imap-passphrase-callback-function) + (epg-context-set-progress-callback context + (cons #'epa-progress-callback-function + "Encrypting...")) + (message "Encrypting...") + (setq cipher (epg-encrypt-string + context + (encode-coding-string (buffer-string) 'utf-8) + nil)) + (message "Encrypting...done") + cipher)) + +(defun tramp-imap-decode-buffer () + (let ((context (epg-make-context 'OpenPGP)) + plain) + (epg-context-set-passphrase-callback context + #'tramp-imap-passphrase-callback-function) + (epg-context-set-progress-callback context + (cons #'epa-progress-callback-function + "Decrypting...")) + (message "Decrypting...") + (setq plain (decode-coding-string + (epg-decrypt-string context (buffer-string)) + 'utf-8)) + (message "Decrypting...done") + plain)) + +(defun tramp-imap-file-name-mailbox (vec) + (nth 0 (tramp-imap-file-name-parse vec))) + +(defun tramp-imap-file-name-name (vec) + (nth 1 (tramp-imap-file-name-parse vec))) + +(defun tramp-imap-file-name-localname (vec) + (nth 1 (tramp-imap-file-name-parse vec))) + +(defun tramp-imap-file-name-parse (vec) + (let ((name (substring-no-properties (tramp-file-name-localname vec)))) + (if (string-match "^/\\([^/]+\\)/?\\(.*\\)$" name) + (list (match-string 1 name) + (match-string 2 name)) + nil))) + +(defun tramp-imap-make-iht (vec &optional needed-subject) + "Translate the Tramp vector VEC to the imap-hash structure. +With NEEDED-SUBJECT, alters the imap-hash test accordingly." + (let* ((mbox (tramp-imap-file-name-mailbox vec)) + (server (tramp-file-name-real-host vec)) + (method (tramp-file-name-method vec)) + (user (tramp-file-name-user vec)) + (ssl (string-equal method tramp-imaps-method)) + (port (or (tramp-file-name-port vec) + (tramp-get-method-parameter method 'tramp-default-port))) + (result (imap-hash-make server port mbox))) + ;; Return the IHT with a test override to look for the subject + ;; marker. Set also user and ssl tags. + (setq result (plist-put result :user user) + result (plist-put result :ssl ssl) + result (plist-put + result + :test (format "^%s%s" + tramp-imap-subject-marker + (if needed-subject needed-subject "")))))) + +;;; TODO: + +;; * Implement `tramp-imap-handle-delete-directory', +;; `tramp-imap-handle-make-directory', +;; `tramp-imap-handle-make-directory-internal', +;; `tramp-imap-handle-set-file-times'. + +;; * Encode the subject. If the filename has trailing spaces (like +;; "test "), those characters get lost, for example in dired listings. + +;; * When opening a dired buffer, like "/imap::INBOX.test", there are +;; several error messages: +;; "Buffer has a running process; kill it? (yes or no) " +;; "error in process filter: Internal error, tag 6 status BAD code nil text No mailbox selected." +;; Afterwards, everything seems to be fine. + +;; * imaps works for local IMAP servers. Accessing +;; "/imaps:imap.gmail.com:/INBOX.test/" results in error +;; "error in process filter: Internal error, tag 5 status BAD code nil text UNSELECT not allowed now. + +(provide 'tramp-imap) +;;; tramp-imap.el ends here + +;; Ignore, for testing only. + +;;; (setq tramp-imap-subject-marker "T") +;;; (tramp-imap-get-file-entries (tramp-dissect-file-name "/imap:yourhosthere.com:/INBOX.test/4") t) +;;; (tramp-imap-get-file-entries (tramp-dissect-file-name "/imap:yourhosthere.com:/INBOX.test/") t) +;;; (tramp-imap-get-file-entries (tramp-dissect-file-name "/imap:yourhosthere.com:/test/4") t) +;;; (tramp-imap-get-file-entries (tramp-dissect-file-name "/imap:yourhosthere.com:/test/") t) +;;; (tramp-imap-get-file-entries (tramp-dissect-file-name "/imap:yourhosthere.com:/test/welcommen") t) +;;; (tramp-imap-get-file-entries (tramp-dissect-file-name "/imap:yourhosthere.com:/test/welcommen") t t) +;;;(tramp-imap-get-file-inode "/imap:yourhosthere.com:/test/welcome") +;;; (dired-copy-file "/etc/fstab" "/imap:yourhosthere.com:/test/welcome" t) +;;; (write-region 1 100 "/imap:yourhosthere.com:/test/welcome") +;;; (tramp-imap-get-file "/imap:yourhosthere.com:/test/welcome" t) +;;(with-temp-buffer (insert "hello") (write-file "/imap:yourhosthere.com:/test/welcome")) +;;(with-temp-buffer (insert "hello") (write-file "/imap:yourhosthere.com:/test/welcome2")) +;;(file-writable-p "/imap:yourhosthere.com:/test/welcome2") +;;(file-name-directory "/imap:yourhosthere.com:/test/welcome2") +;;(with-temp-buffer (insert "hello") (delete-file "/tmp/hellotest") (write-file "/tmp/hellotest") (write-file "/imap:yourhosthere.com:/test/welcome2")) +;;;(file-exists-p "/imap:yourhosthere.com:/INBOX.test/4") +;;;(file-attributes "/imap:yourhosthere.com:/INBOX.test/4") +;;;(setq vec (tramp-dissect-file-name "/imap:yourhosthere.com:/INBOX.test/4")) +;;;(tramp-imap-handle-file-attributes "/imap:yourhosthere.com:/INBOX.test/4") +;;; (tramp-imap-handle-insert-file-contents "/imap:user@yourhosthere.com:/INBOX.test/4" nil nil nil nil) +;;;(insert-file-contents "/imap:yourhosthere.com:/INBOX.test/4") +;;;(file-attributes "/imap:yourhosthere.com:/test/welcommen") +;;;(insert-file-contents "/imap:yourhosthere.com:/test/welcome") +;;;(file-exists-p "/imap:yourhosthere.com:/test/welcome2") +;;;(tramp-imap-handle-file-attributes "/imap:yourhosthere.com:/test/welcome") +;;;(tramp-imap-get-file-inode "/imap:yourhosthere.com:/test/welcommen") +;;;(tramp-imap-get-file-inode "/imap:yourhosthere.com:/test/welcome") +;;;(file-writable-p "/imap:yourhosthere.com:/test/welcome2") +;;; (delete-file "/imap:yourhosthere.com:/test/welcome") +;;; (tramp-imap-get-file "/imap:yourhosthere.com:/test/welcommen" t) +;;; (tramp-imap-get-file "/imap:yourhosthere.com:/test/welcome" t) +;;;(tramp-imap-file-name-mailbox (tramp-dissect-file-name "/imap:yourhosthere.com:/INBOX.test")) +;;;(tramp-imap-file-name-mailbox (tramp-dissect-file-name "/imap:yourhosthere.com:/INBOX.test/new/old")) +;;;(tramp-imap-file-name-mailbox (tramp-dissect-file-name "/imap:yourhosthere.com:/INBOX.test/new")) +;;;(tramp-imap-file-name-parse (tramp-dissect-file-name "/imap:yourhosthere.com:/INBOX.test/new/two")) +;;;(tramp-imap-file-name-parse (tramp-dissect-file-name "/imap:yourhosthere.com:/INBOX.test/new/one")) +;;;(tramp-imap-file-name-parse (tramp-dissect-file-name "/imap:yourhosthere.com:/INBOX.test")) +;;; (tramp-imap-file-name-parse (tramp-dissect-file-name "/imap:yourhosthere.com:/test/4")) +;;; (tramp-imap-file-name-parse (tramp-dissect-file-name "/imap:yourhosthere.com:/test/")) +;;; (tramp-imap-file-name-parse (tramp-dissect-file-name "/imap:yourhosthere.com:/test/welcommen")) +;;; (tramp-imap-file-name-parse (tramp-dissect-file-name "/imap:yourhosthere.com:/test/welcommen")) +;;; (tramp-imap-make-iht (tramp-dissect-file-name "/imap:yourhosthere.com:/test/welcommen")) +;;; (tramp-imap-make-iht (tramp-dissect-file-name "/imap:yourhosthere.com:/INBOX.test/4")) +;;; (tramp-imap-make-iht (tramp-dissect-file-name "/imap:yourhosthere.com:/INBOX.test/4") "extra")