;; mu4e-proc.el -- part of mu4e, the mu mail user agent ;; ;; Copyright (C) 2011-2017 Dirk-Jan C. Binnema ;; Author: Dirk-Jan C. Binnema ;; Maintainer: Dirk-Jan C. Binnema ;; 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 . ;;; Commentary: ;;; Code: (require 'mu4e-vars) (require 'mu4e-utils) (require 'mu4e-meta) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; internal vars (defconst mu4e~proc-name "*mu4e-proc*" "Name of the server process, buffer.") (defvar mu4e~proc-process nil "The mu-server process.") ;; dealing with the length cookie that precedes expressions (defconst mu4e~cookie-pre "\376" "Each expression we get from the backend (mu server) starts with a length cookie: <`mu4e~cookie-pre'><`mu4e~cookie-post'>.") (defconst mu4e~cookie-post "\377" "Each expression we get from the backend (mu server) starts with a length cookie: <`mu4e~cookie-pre'><`mu4e~cookie-post'>.") (defconst mu4e~cookie-matcher-rx (concat mu4e~cookie-pre "\\([[:xdigit:]]+\\)" mu4e~cookie-post) "Regular expression matching the length cookie. Match 1 will be the length (in hex).") (defvar mu4e~proc-buf nil "Buffer (string) for data received from the backend.") (defconst mu4e~proc-name " *mu4e-proc*" "Name of the server process, buffer.") (defvar mu4e~proc-process nil "The mu-server process.") (defun mu4e~proc-running-p () "Whether the mu process is running." (when (and mu4e~proc-process (memq (process-status mu4e~proc-process) '(run open listen connect stop))) t)) (defsubst mu4e~proc-eat-sexp-from-buf () "'Eat' the next s-expression from `mu4e~proc-buf'. Note: this is a string, not an emacs-buffer. `mu4e~proc-buf gets its contents from the mu-servers in the following form: <`mu4e~cookie-pre'><`mu4e~cookie-post'> Function returns this sexp, or nil if there was none. `mu4e~proc-buf' is updated as well, with all processed sexp data removed." (ignore-errors ;; the server may die in the middle... ;; mu4e~cookie-matcher-rx: ;; (concat mu4e~cookie-pre "\\([[:xdigit:]]+\\)]" mu4e~cookie-post) (let ((b (string-match mu4e~cookie-matcher-rx mu4e~proc-buf)) (sexp-len) (objcons)) (when b (setq sexp-len (string-to-number (match-string 1 mu4e~proc-buf) 16)) ;; does mu4e~proc-buf contain the full sexp? (when (>= (length mu4e~proc-buf) (+ sexp-len (match-end 0))) ;; clear-up start (setq mu4e~proc-buf (substring mu4e~proc-buf (match-end 0))) ;; note: we read the input in binary mode -- here, we take the part ;; that is the sexp, and convert that to utf-8, before we interpret ;; it. (setq objcons (read-from-string (decode-coding-string (substring mu4e~proc-buf 0 sexp-len) 'utf-8 t))) (when objcons (setq mu4e~proc-buf (substring mu4e~proc-buf sexp-len)) (car objcons))))))) (defun mu4e~proc-filter (proc str) "A process-filter for the 'mu server' output. It accumulates the strings into valid sexps by checking of the ';;eox' end-of-sexp marker, and then evaluating them. The server output is as follows: 1. an error (:error 2 :message \"unknown command\") ;; eox => this will be passed to `mu4e-error-func'. 2a. a message sexp looks something like: \( :docid 1585 :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 \" \" \) ;; eox => this will be passed to `mu4e-header-func'. 2b. After the list of message sexps has been returned (see 2a.), we'll receive a sexp that looks like (:found ) with n the number of messages found. The will be passed to `mu4e-found-func'. 3. a view looks like: (:view ) => the (see 2.) will be passed to `mu4e-view-func'. 4. a database update looks like: (:update :move ) => the (see 2.) will be passed to `mu4e-update-func', :move tells us whether this is a move to another maildir, or merely a flag change. 5. a remove looks like: (:remove ) => the docid will be passed to `mu4e-remove-func' 6. a compose looks like: (:compose [:original] [:include ]) `mu4e-compose-func'." (mu4e-log 'misc "* Received %d byte(s)" (length str)) (setq mu4e~proc-buf (concat mu4e~proc-buf str)) ;; update our buffer (let ((sexp (mu4e~proc-eat-sexp-from-buf))) (with-local-quit (while sexp (mu4e-log 'from-server "%S" sexp) (cond ;; a header plist can be recognized by the existence of a :date field ((plist-get sexp :date) (funcall mu4e-header-func sexp)) ;; the found sexp, we receive after getting all the headers ((plist-get sexp :found) (funcall mu4e-found-func (plist-get sexp :found))) ;; viewing a specific message ((plist-get sexp :view) (funcall mu4e-view-func (plist-get sexp :view))) ;; receive an erase message ((plist-get sexp :erase) (funcall mu4e-erase-func)) ;; receive a :sent message ((plist-get sexp :sent) (funcall mu4e-sent-func (plist-get sexp :docid) (plist-get sexp :path))) ;; received a pong message ((plist-get sexp :pong) (funcall mu4e-pong-func (plist-get sexp :props))) ;; received a contacts message ;; note: we use 'member', to match (:contacts nil) ((plist-member sexp :contacts) (funcall mu4e-contacts-func (plist-get sexp :contacts))) ;; something got moved/flags changed ((plist-get sexp :update) (funcall mu4e-update-func (plist-get sexp :update) (plist-get sexp :move))) ;; a message got removed ((plist-get sexp :remove) (funcall mu4e-remove-func (plist-get sexp :remove))) ;; start composing a new message ((plist-get sexp :compose) (funcall mu4e-compose-func (plist-get sexp :compose) (plist-get sexp :original) (plist-get sexp :include))) ;; do something with a temporary file ((plist-get sexp :temp) (funcall mu4e-temp-func (plist-get sexp :temp) ;; name of the temp file (plist-get sexp :what) ;; what to do with it ;; (pipe|emacs|open-with...) (plist-get sexp :docid) ;; docid of the message (plist-get sexp :param)));; parameter for the action ;; get some info ((plist-get sexp :info) (funcall mu4e-info-func sexp)) ;; receive an error ((plist-get sexp :error) (funcall mu4e-error-func (plist-get sexp :error) (plist-get sexp :message))) (t (mu4e-message "Unexpected data from server [%S]" sexp))) (setq sexp (mu4e~proc-eat-sexp-from-buf)))))) (defun mu4e~escape (str) "Escape STRING for transport -- put it in quotes, and escape existing quotation. In particular, backslashes and double-quotes." (let ((esc (replace-regexp-in-string "\\\\" "\\\\\\\\" str))) (format "%s" (replace-regexp-in-string "\"" "\\\\\"" esc)))) (defun mu4e~proc-send-command (frm &rest args) "Send as command to the mu server process. Start the process if needed." (unless (mu4e~proc-running-p) (mu4e~proc-start)) (let ((cmd (apply 'format frm args))) (mu4e-log 'to-server "%S" cmd) (process-send-string mu4e~proc-process (concat cmd "\n")))) (defun mu4e~proc-start () "Start the mu server process." (unless (file-executable-p mu4e-mu-binary) (mu4e-error (format "`mu4e-mu-binary' (%S) not found" mu4e-mu-binary))) (let* ((process-connection-type nil) ;; use a pipe (args '("server")) (args (append args (when mu4e-mu-home (list (concat "--muhome=" mu4e-mu-home)))))) (setq mu4e~proc-buf "") (setq mu4e~proc-process (apply 'start-process mu4e~proc-name mu4e~proc-name mu4e-mu-binary args)) ;; register a function for (:info ...) sexps (unless mu4e~proc-process (mu4e-error "Failed to start the mu4e backend")) (set-process-query-on-exit-flag mu4e~proc-process nil) (set-process-coding-system mu4e~proc-process 'binary 'utf-8-unix) (set-process-filter mu4e~proc-process 'mu4e~proc-filter) (set-process-sentinel mu4e~proc-process 'mu4e~proc-sentinel))) (defun mu4e~proc-kill () "Kill the mu server process." (let* ((buf (get-buffer mu4e~proc-name)) (proc (and (buffer-live-p buf) (get-buffer-process buf)))) (when proc (let ((delete-exited-processes t)) ;; the mu server signal handler will make it quit after 'quit' (mu4e~proc-send-command "cmd:quit")) ;; try sending SIGINT (C-c) to process, so it can exit gracefully (ignore-errors (signal-process proc 'SIGINT)))) (setq mu4e~proc-process nil mu4e~proc-buf nil)) ;; error codes are defined in src/mu-util ;;(defconst mu4e-xapian-empty 19 "Error code: xapian is empty/non-existent") (defun mu4e~proc-sentinel (proc msg) "Function that will be called when the mu-server process terminates." (let ((status (process-status proc)) (code (process-exit-status proc))) (setq mu4e~proc-process nil) (setq mu4e~proc-buf "") ;; clear any half-received sexps (cond ((eq status 'signal) (cond ((or(eq code 9) (eq code 2)) (message nil)) ;;(message "the mu server process has been stopped")) (t (error (format "mu server process received signal %d" code))))) ((eq status 'exit) (cond ((eq code 0) (message nil)) ;; don't do anything ((eq code 11) (error "Database is locked by another process")) ((eq code 15) (error "Database needs upgrade; try `mu index --rebuild' from the command line")) ((eq code 19) (error "Database empty; try indexing some messages")) (t (error "mu server process ended with exit code %d" code)))) (t (error "Something bad happened to the mu server process"))))) (defsubst mu4e~docid-msgid-param (docid-or-msgid) "Construct a backend parameter based on DOCID-OR-MSGID." (format (if (stringp docid-or-msgid) "msgid:\"%s\"" "docid:%d") docid-or-msgid)) (defun mu4e~proc-find (query threads sortfield sortdir maxnum skip-dups include-related) "Start a database query for QUERY. If THREADS is non-nil, show results in threaded fasion, SORTFIELD is a symbol describing the field to sort by (or nil); see `mu4e~headers-sortfield-choices'. If SORT is `descending', sort Z->A, if it's `ascending', sort A->Z. MAXNUM determines the maximum number of results to return, or nil for 'unlimited'. If SKIP-DUPS is non-nil, show only one of duplicate messages (see `mu4e-headers-skip-duplicates'). If INCLUDE-RELATED is non-nil, include messages related to the messages matching the search query (see `mu4e-headers-include-related'). For each result found, a function is called, depending on the kind of result. The variables `mu4e-error-func' contain the function that will be called for, resp., a message (header row) or an error." (mu4e~proc-send-command (concat "cmd:find query:%s threads:%s sortfield:%s reverse:%s maxnum:%d " "skip-dups:%s include-related:%s") (base64-encode-string query t) (if threads "true" "false") ;; sortfield is e.g. ':subject'; this removes the ':' (if (null sortfield) "nil" (substring (symbol-name sortfield) 1)) ;; TODO: use ascending/descending in backend too (it's clearer than 'reverse' (if (eq sortdir 'descending) "true" "false") (if maxnum maxnum -1) (if skip-dups "true" "false") (if include-related "true" "false"))) (defun mu4e~proc-move (docid-or-msgid &optional maildir flags) "Move message identified by DOCID-OR-MSGID. At least one of MAILDIR and FLAGS should be specified. Note, even if MAILDIR is nil, this is still a move, since a change in flags still implies a change in message filename. MAILDIR (), optionally setting FLAGS (keyword argument :flags). optionally setting FLAGS in the process. If MAILDIR is nil, message will be moved within the same maildir. MAILDIR must be a maildir, that is, the part _without_ cur/ or new/ or the root-maildir-prefix. E.g. \"/archive\". This directory must already exist. 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 `mu4e-string-to-flags' and `mu4e-flags-to-string'. The server reports the results for the operation through `mu4e-update-func'. If the variable `mu4e-change-filenames-when-moving' is non-nil, moving to a different maildir generates new names for the target files; this helps certain tools (such as mbsync). The results are reported through either (:update ... ) or (:error ) sexp, which are handled my `mu4e-update-func' and `mu4e-error-func', respectively." (unless (or maildir flags) (mu4e-error "At least one of maildir and flags must be specified")) (unless (or (not maildir) (file-exists-p (concat mu4e-maildir "/" maildir "/"))) (mu4e-error "Target dir does not exist")) (let* ((idparam (mu4e~docid-msgid-param docid-or-msgid)) (flagstr (when flags (concat " flags:" (if (stringp flags) flags (mu4e-flags-to-string flags))))) (path (when maildir (format " maildir:%s" (mu4e~escape maildir)))) (rename (if (and maildir mu4e-change-filenames-when-moving) "true" "false"))) (mu4e~proc-send-command "cmd:move %s %s %s %s" idparam (or flagstr "") (or path "") (format "newname:%s" rename)))) (defun mu4e~proc-index (path my-addresses cleanup lazy-check) "Update the message database for filesystem PATH, which should point to some maildir directory structure. MY-ADDRESSES is a list of 'my' email addresses (see `mu4e-user-mail-address-list')." (let ((path (mu4e~escape path)) (addrs (when my-addresses (mapconcat 'identity my-addresses ",")))) (if addrs (mu4e~proc-send-command "cmd:index path:%s my-addresses:%s cleanup:%s lazy-check:%s" path addrs (if cleanup "true" : "false") (if lazy-check "true")) (mu4e~proc-send-command "cmd:index path:%s" path)))) (defun mu4e~proc-add (path maildir) "Add the message at PATH to the database. With MAILDIR set to the maildir this message resides in, e.g. '/drafts'; if this works, we will receive (:info add :path :docid ) as well as (:update )." (mu4e~proc-send-command "cmd:add path:%s %s" (mu4e~escape path) (if maildir (format "maildir:%s" (mu4e~escape maildir)) ""))) (defun mu4e~proc-sent (path maildir) "Add the message at PATH to the database. With MAILDIR set to the maildir this message resides in, e.g. '/drafts'. if this works, we will receive (:info add :path :docid :fcc )." (mu4e~proc-send-command "cmd:sent path:%s maildir:%s" (mu4e~escape path) (mu4e~escape maildir))) (defun mu4e~proc-compose (type decrypt &optional docid) "Start composing a message of certain TYPE (a symbol, either `forward', `reply', `edit', `resend' or `new', based on an original message (ie, replying to, forwarding, editing, resending) with DOCID or nil for type `new'. The result will be delivered to the function registered as `mu4e-compose-func'." (unless (member type '(forward reply edit resend new)) (mu4e-error "Unsupported compose-type %S" type)) (unless (eq (null docid) (eq type 'new)) (mu4e-error "`new' implies docid not-nil, and vice-versa")) (mu4e~proc-send-command "cmd:compose type:%s docid:%d extract-encrypted:%s use-agent:true" (symbol-name type) docid (if decrypt "true" "false"))) (defun mu4e~proc-mkdir (path) "Create a new maildir-directory at filesystem PATH." (mu4e~proc-send-command "cmd:mkdir path:%s" (mu4e~escape path))) (defun mu4e~proc-extract (action docid partidx decrypt &optional path what param) "Extract an attachment with index PARTIDX from message with DOCID and perform ACTION on it (as symbol, either `save', `open', `temp') which mean: * save: save the part to PARAM1 (a path) (non-optional for save)$ * open: open the part with the default application registered for doing so * temp: save to a temporary file, then respond with (:temp :what :param )." (let ((cmd (concat "cmd:extract " (case action (save (format "action:save docid:%d index:%d path:%s extract-encrypted:%s use-agent:true" docid partidx (mu4e~escape path) (if decrypt "true" "false"))) (open (format "action:open docid:%d index:%d extract-encrypted:%s use-agent:true" docid partidx (if decrypt "true" "false"))) (temp (format "action:temp docid:%d index:%d what:%s%s extract-encrypted:%s use-agent:true" docid partidx what (if param (if (stringp param) (format " param:%s" (mu4e~escape param)) (format " param:%S" param)) "") (if decrypt "true" "false"))) (otherwise (mu4e-error "Unsupported action %S" action)))) )) (mu4e~proc-send-command "%s" cmd))) (defun mu4e~proc-ping () "Sends a ping to the mu server, expecting a (:pong ...) in response." (mu4e~proc-send-command "cmd:ping")) (defun mu4e~proc-contacts (personal after) "Sends the contacts command to the mu server. A (:contacts ()) is expected in response. If PERSONAL is non-nil, only get personal contacts, if AFTER is non-nil, get only contacts seen AFTER (the time_t value)." (mu4e~proc-send-command "cmd:contacts personal:%s after:%d" (if personal "true" "false") (or after 0))) (defun mu4e~proc-view (docid-or-msgid &optional images decrypt) "Get one particular message based on its DOCID-OR-MSGID. Optionally, if IMAGES is non-nil, backend will any images attached to the message, and return them as temp files. The result will be delivered to the function registered as `mu4e-view-func'." (mu4e~proc-send-command "cmd:view %s extract-images:%s extract-encrypted:%s use-agent:true" (mu4e~docid-msgid-param docid-or-msgid) (if images "true" "false") (if decrypt "true" "false"))) (defun mu4e~proc-view-path (path &optional images decrypt) "View message at PATH (keyword argument). Optionally, if IMAGES is non-nil, backend will any images attached to the message, and return them as temp files. The result will be delivered to the function registered as `mu4e-view-func'." (mu4e~proc-send-command "cmd:view path:%s extract-images:%s extract-encrypted:%s use-agent:true" (mu4e~escape path) (if images "true" "false") (if decrypt "true" "false"))) (defun mu4e~proc-remove (docid) "Remove message identified by docid. The results are reporter through either (:update ... ) or (:error) sexp, which are handled my `mu4e-error-func', respectively." (mu4e~proc-send-command "cmd:remove docid:%d" docid)) (provide 'mu4e-proc) ;; End of mu4e-proc.el