1
0
mirror of https://github.com/djcb/mu.git synced 2024-07-02 08:21:06 +02:00
mu/toys/mm/mm-common.el

414 lines
15 KiB
EmacsLisp
Raw Normal View History

2011-09-12 19:52:32 +02:00
;;; mm-common.el -- part of mm, the mu mail user agent
;;
;; Copyright (C) 2011 Dirk-Jan C. Binnema
;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
;; Keywords: email
;; Version: 0.0
;; This file is not 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:
;;; Code:
(eval-when-compile (require 'cl))
(require 'ido)
(defun mm/eval-msg-string (str)
"Get the plist describing an email message, from STR containing
a message sexp.
a message sexp looks something like:
\(
:from ((\"Donald Duck\" . \"donald@example.com\"))
:to ((\"Mickey Mouse\" . \"mickey@example.com\"))
:subject \"Wicked stuff\"
:date (20023 26572 0)
:size 15165
:references (\"200208121222.g7CCMdb80690@msg.id\")
:in-reply-to \"200208121222.g7CCMdb80690@msg.id\"
:message-id \"foobar32423847ef23@pluto.net\"
:maildir: \"/archive\"
:path \"/home/mickey/Maildir/inbox/cur/1312254065_3.32282.pluto,4cd5bd4e9:2,\"
:priority high
:flags (new unread)
:attachments ((2 \"hello.jpg\" \"image/jpeg\") (3 \"laah.mp3\" \"audio/mp3\"))
:body-txt \" <message body>\"
\)
other fields are :cc, :bcc, :body-html
When the s-expression comes from the database ('mu find'), the
fields :attachments, :body-txt, :body-html, :references, :in-reply-to
are missing (because that information is not stored in the
database -- at least not in a usable way."
(condition-case nil
(car (read-from-string str));; read-from-string returns a cons
(error "Failed to parse message")))
(defun mm/msg-field (msg field)
"Get a field from this message, or nil. The fields are the
fields of the message, which are the various items of the plist
as described in `mm/eval-msg-string'
There is also the special field :body (which is either :body-txt,
or if not available, :body-html converted to text)."
(case field
(:body
(let* ((body (mm/msg-field msg :body-txt))
(body (or body (with-temp-buffer
(mm/msg-field msg :body-html)
(html2text)
(buffer-string)))))))
(t (plist-get msg field))))
;;; converting flags->string and vice-versa ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun mm/flags-to-string (flags)
"Remove duplicates and sort the output of `mm/flags-to-string-raw'."
(concat
(sort (remove-duplicates (append (mm/flags-to-string-raw flags) nil)) '>)))
(defun mm/flags-to-string-raw (flags)
"Convert a list of flags into a string as seen in Maildir
message files; flags are symbols draft, flagged, new, passed,
replied, seen, trashed and the string is the concatenation of the
uppercased first letters of these flags, as per [1]. Other flags
than the ones listed here are ignored.
Also see `mm/flags-to-string'.
\[1\]: http://cr.yp.to/proto/maildir.html"
(when flags
(let ((kar (case (car flags)
('draft ?D)
('flagged ?F)
('new ?N)
('passed ?P)
('replied ?R)
('seen ?S)
('trashed ?T)
('encrypted ?x)
('signed ?s)
('unread ?u))))
(concat (and kar (string kar))
(mm/flags-to-string-raw (cdr flags))))))
(defun mm/string-to-flags (str)
"Remove duplicates from the output of `mm/string-to-flags-1'"
(remove-duplicates (mm/string-to-flags-1 str)))
(defun mm/string-to-flags-1 (str)
"Convert a string with message flags as seen in Maildir
messages into a list of flags in; flags are symbols draft,
flagged, new, passed, replied, seen, trashed and the string is
the concatenation of the uppercased first letters of these flags,
as per [1]. Other letters than the ones listed here are ignored.
Also see `mu/flags-to-string'.
\[1\]: http://cr.yp.to/proto/maildir.html"
(when (/= 0 (length str))
(let ((flag
(case (string-to-char str)
(?D 'draft)
(?F 'flagged)
(?P 'passed)
(?R 'replied)
(?S 'seen)
(?T 'trashed))))
(append (when flag (list flag))
(mm/string-to-flags-1 (substring str 1))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; moving message files, changing flags ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun mm/move-msg (uid &optional targetdir flags ignore-already)
"Move message identified by UID to TARGETDIR using 'mu mv', and
update the database with the new situation. TARGETDIR must be a
maildir - that is, the part _without_ cur/ or new/. 'mu mv' will
calculate the target directory and the exact file name. See
`mm/msg-map' for a discussion about UID.
After the file system move (rename) has been done, 'mu remove'
and/or 'mu add' are invoked asynchronously to update the database
with the changes.
Optionally, you can specify the FLAGS for the new file. The FLAGS
parameter can have the following forms:
1. a list of flags such as '(passed replied seen)
2. a string containing the one-char versions of the flags, e.g. \"PRS\"
3. a delta-string specifying the changes with +/- and the one-char flags,
e.g. \"+S-N\" to set Seen and remove New.
The flags are any of `deleted', `flagged', `new', `passed', `replied' `seen' or
`trashed', or the corresponding \"DFNPRST\" as defined in [1]. See
`mm/string-to-flags' and `mm/flags-to-string'.
If TARGETDIR is '/dev/null', remove SRC. After the file system
move, the database will be updated as well, using the 'mu add'
and 'mu remove' commands.
If IGNORE-ALREADY is non-nil, don't consider it an error when the target file is
the same as the source file.
Function returns t the move succeeds, in other cases, it returns
nil.
\[1\] URL `http://cr.yp.to/proto/maildir.html'."
(let* ((src (mm/msg-map-get-path uid)))
(unless src (error "Source path not registered for %S" uid))
(unless (or targetdir src) (error "Either targetdir or flags required"))
(unless (file-readable-p src) (error "Source is unreadable (%S)" src))
(let* ((flagstr (if (stringp flags) flags (mm/flags-to-string flags)))
(argl (remove-if 'not ;; build up the arg list
(list "mv" "--print-target" "--ignore-dups"
(when flagstr (concat "--flags=" flagstr))
src targetdir)))
;; execute it, and get the results
(rv (apply 'mm/mu-run argl))
(code (car rv)) (output (cdr rv)))
(unless (= 0 code) (error "Moving message failed: %S" output))
;; success!
(let ((targetpath (substring output 0 -1)))
(when (and targetpath (not (string= src targetpath)))
(mm/msg-map-update uid targetpath) ;; update the UID-map
(mm/db-remove-async src) ;; remove the src from the db
(unless (string= targetdir "/dev/null")
(mm/db-add-async targetpath))) ;; add the target to the db
(mm/db-update-execute)
t))))
;;; some functions for *asyncronously* updating the database
(defvar mm/db-update-proc nil
"*internal* Process for async db updates.")
(defvar mm/db-update-name "*mm-db-update*"
"*internal* name of the db-update process")
(defvar mm/db-add-paths nil
"*internal* List of message paths to add to the database.")
(defvar mm/db-remove-paths nil
"*internal* List of message paths to remove from the database.")
(defun mm/db-update-proc-sentinel (proc msg)
"Check the database update process upon completion."
(let ((procbuf (process-buffer proc))
(status (process-status proc))
(exit-status (process-exit-status proc)))
(when (and (buffer-live-p procbuf) (memq status '(exit signal)))
(case status
('signal (mm/log "Process killed"))
('exit
(case exit-status
(mm/log "Result: %s" (mm/error-string exit-status))))))
;; try to update again, maybe there are some new updates
(mm/db-update-execute)))
(defun mm/db-update-execute ()
"Update the database; remove paths in `mm/db-remove-paths',
and add paths in `mm/db-add-paths'. Updating is ansynchronous."
;; when it's already running, do nothing
(unless (and mm/db-update-proc (eq (process-status mm/db-update-proc) 'run))
(when mm/db-remove-paths
(let ((remove-paths (copy-list mm/db-remove-paths)))
(mm/log (concat mm/mu-binary " remove "
(mapconcat 'identity remove-paths " ")))
(setq mm/db-remove-paths nil) ;; clear the old list
(setq mm/db-update-proc
(apply 'start-process mm/db-update-name mm/db-update-name mm/mu-binary
"remove" remove-paths))
(set-process-sentinel mm/db-update-proc 'mm/db-update-proc-sentinel)))))
;; when it's already running, do nothing
(unless (and mm/db-update-proc (eq (process-status mm/db-update-proc) 'run))
(when mm/db-add-paths
(let ((add-paths (copy-list mm/db-add-paths)))
(mm/log (concat mm/mu-binary " add " (mapconcat 'identity add-paths " ")))
(setq mm/db-add-paths nil) ;; clear the old list
(setq mm/db-update-proc
(apply 'start-process mm/db-update-name mm/db-update-name mm/mu-binary
"add" add-paths))
(set-process-sentinel mm/db-update-proc 'mm/db-update-proc-sentinel))))
(defun mm/db-add-async (path-or-paths)
"Asynchronously add msg at PATH-OR-PATHS to
database. PATH-OR-PATHS is either a single path or a list of them."
(setq mm/db-add-paths
(append mm/db-add-paths
(if (listp path-or-paths) path-or-paths `(,path-or-paths)))))
;; (mm/db-update-execute))
(defun mm/db-remove-async (path-or-paths)
"Asynchronously remove msg at PATH-OR-PATHS from
database. PATH-OR-PATHS is either a single path or a list of
them."
(setq mm/db-remove-paths
(append mm/db-remove-paths
(if (listp path-or-paths) path-or-paths `(,path-or-paths)))))
;; (mm/db-update-execute))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; error codes / names ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; generated with:
;; cat mu-util.h | sed 's/\([A-Z_]\+\).*=\(.*\),/(defconst \L\1 \2)/' < "$<" \
;; | sed 's/_/-/g' > mu-errors.el
(defconst mm/err 1)
(defconst mm/err-in-parameters 2)
(defconst mm/err-internal 3)
(defconst mm/err-no-matches 4)
(defconst mm/err-xapian 11)
(defconst mm/err-xapian-query 13)
(defconst mm/err-xapian-dir-not-accessible 14)
(defconst mm/err-xapian-not-up-to-date 15)
(defconst mm/err-xapian-missing-data 16)
(defconst mm/err-xapian-corruption 17)
(defconst mm/err-xapian-cannot-get-writelock 18)
(defconst mm/err-gmime 30)
(defconst mm/err-contacts 50)
(defconst mm/err-contacts-cannot-retrieve 51)
(defconst mm/err-file 70)
(defconst mm/err-file-invalid-name 71)
(defconst mm/err-file-cannot-link 72)
(defconst mm/err-file-cannot-open 73)
(defconst mm/err-file-cannot-read 74)
(defconst mm/err-file-cannot-create 75)
(defconst mm/err-file-cannot-mkdir 76)
(defconst mm/err-file-stat-failed 77)
(defconst mm/err-file-readdir-failed 78)
(defconst mm/err-file-invalid-source 79)
(defconst mm/err-file-target-equals-source 80)
;; TODO: use 'case' instead...
(defun mm/error-string (err)
"Convert an exit code from mu into a string."
(cond
((eql err mm/err) "General error")
((eql err mm/err-in-parameters) "Error in parameters")
((eql err mm/err-internal) "Internal error")
((eql err mm/err-no-matches) "No matches")
((eql err mm/err-xapian) "Xapian error")
((eql err mm/err-xapian-query) "Error in query")
((eql err mm/err-xapian-dir-not-accessible) "Database dir not accessible")
((eql err mm/err-xapian-not-up-to-date) "Database is not up-to-date")
((eql err mm/err-xapian-missing-data) "Missing data")
((eql err mm/err-xapian-corruption) "Database seems to be corrupted")
((eql err mm/err-xapian-cannot-get-writelock)"Database is locked")
((eql err mm/err-gmime) "GMime-related error")
((eql err mm/err-contacts) "Contacts-related error")
((eql err mm/err-contacts-cannot-retrieve) "Failed to retrieve contacts")
((eql err mm/err-file) "File error")
((eql err mm/err-file-invalid-name) "Invalid file name")
((eql err mm/err-file-cannot-link) "Failed to link file")
((eql err mm/err-file-cannot-open) "Cannot open file")
((eql err mm/err-file-cannot-read) "Cannot read file")
((eql err mm/err-file-cannot-create) "Cannot create file")
((eql err mm/err-file-cannot-mkdir) "mu-mkdir failed")
((eql err mm/err-file-stat-failed) "stat(2) failed")
((eql err mm/err-file-readdir-failed) "readdir failed")
((eql err mm/err-file-invalid-source) "Invalid source file")
((eql err mm/err-file-target-equals-source) "Source is same as target")
(t (format "Unknown error (%d)" err))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; other helper function ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun mm/mu-run (&rest args)
"Run 'mu' synchronously with ARGS as command-line argument;,
where <exit-code> is the exit code of the program, or 1 if the
process was killed. <str> contains whatever the command wrote on
standard output/error, or nil if there was none or in case of
error. `mm/mu-run' is like `shell-command-to-string', but with
better possibilities for error handling. The --muhome= parameter is
added automatically if `mm/mu-home' is non-nil."
(let* ((rv)
(allargs (remove-if 'not
(append args (when mm/mu-home (concat "--muhome=" mm/mu-home)))))
(cmdstr (concat mm/mu-binary " " (mapconcat 'identity allargs " ")))
(str (with-output-to-string
(with-current-buffer standard-output ;; but we also get stderr...
(setq rv (apply 'call-process mm/mu-binary nil t nil
args))))))
(mm/log "%s %s => %S" mm/mu-binary (mapconcat 'identity args " ") rv)
(when (and (numberp rv) (/= 0 rv))
(error (mm/error-string rv)))
`(,(if (numberp rv) rv 1) . ,str)))
(defun mm/ask-maildir (prompt &optional fullpath)
"Ask user with PROMPT for a maildir name, if fullpath is
non-nill, return the fulpath (i.e., `mm/maildir' prepended to the
chosen folder)."
(unless (and mm/inbox-folder mm/drafts-folder mm/sent-folder)
(error "`mm/inbox-folder', `mm/drafts-folder' and
`mm/sent-folder' must be set"))
(unless mm/maildir (error "`mm/maildir' must be set"))
(interactive)
(let* ((showfolders
(append (list mm/inbox-folder mm/drafts-folder mm/sent-folder)
mm/working-folders))
(chosen (ido-completing-read prompt showfolders)))
(concat (if fullpath mm/maildir "") chosen)))
(defun mm/new-buffer (bufname)
"Return a new buffer BUFNAME; if such already exists, kill the
old one first."
(when (get-buffer bufname)
(progn
(message (format "Killing %s" bufname))
(kill-buffer bufname)))
(get-buffer-create bufname))
(defconst mm/log-buffer-name "*mm-log*"
"*internal* Name of the logging buffer.")
(defun mm/log (frm &rest args)
"Write something in the *mm-log* buffer - mainly useful for debugging."
(with-current-buffer (get-buffer-create mm/log-buffer-name)
(goto-char (point-max))
(insert (apply 'format (concat (format-time-string "%x %X " (current-time))
frm "\n") args))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(provide 'mm-common)