mu/mu4e/mu4e-server.el

578 lines
21 KiB
EmacsLisp
Raw Normal View History

;;; mu4e-server.el -- part of mu4e -*- lexical-binding: t -*-
;; Copyright (C) 2011-2021 Dirk-Jan C. Binnema
2011-09-12 19:52:32 +02:00
;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
;; This file is not part of GNU Emacs.
;; mu4e is free software: you can redistribute it and/or modify
2011-09-12 19:52:32 +02:00
;; 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.
;; mu4e is distributed in the hope that it will be useful,
2011-09-12 19:52:32 +02:00
;; 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
2011-09-12 19:52:32 +02:00
;;; Commentary:
;;; Code:
(require 'mu4e-helpers)
;;; Configuration
(defcustom mu4e-mu-home nil
"Location of an alternate mu home dir.
If not set, use the defaults, based on the XDG Base Directory
Specification.
2021-11-10 20:30:54 +01:00
Changes to this value only take effect after (re)starting the mu
session."
:group 'mu4e
:type '(choice (const :tag "Default location" nil)
(directory :tag "Specify location"))
:safe 'stringp)
(defcustom mu4e-mu-binary (executable-find "mu")
2021-11-10 20:30:54 +01:00
"Path to the mu-binary to use.
Changes to this value only take effect after (re)starting the mu
session."
:type 'file
:group 'mu4e
:safe 'stringp)
(defcustom mu4e-mu-debug nil
"Whether to run the mu binary in debug-mode.
2021-11-10 20:30:54 +01:00
Setting this to t increases the amount of information in the log.
Changes to this value only take effect after (re)starting the mu
session."
:type 'boolean
:group 'mu4e)
(make-obsolete-variable
'mu4e-maildir
"determined by server; see `mu4e-root-maildir'." "1.3.8")
(defcustom mu4e-change-filenames-when-moving nil
"Change message file names when moving them.
2021-11-10 20:30:54 +01:00
When moving messages to different folders, normally mu/mu4e keep
the base filename the same (the flags-part of the filename may
change still). With this option set to non-nil, mu4e instead
2021-11-10 20:30:54 +01:00
changes the filename.
This latter behavior works better with some
IMAP-synchronization programs such as mbsync; the default works
better with e.g. offlineimap."
:type 'boolean
:group 'mu4e
:safe 'booleanp)
;; Handlers are not strictly internal, but are not meant
;; for overriding outside mu4e. The are mainly for breaking
;; dependency cycles.
(defvar mu4e-error-func nil
"Function called for each error received.
The function is passed an error plist as argument. See
`mu4e--server-filter' for the format.")
(defvar mu4e-update-func nil
"Function called for each :update sexp returned.
The function is passed a msg sexp as argument.
See `mu4e--server-filter' for the format.")
(defvar mu4e-remove-func nil
"Function called for each :remove sexp returned.
This happens when some message has been deleted. The function is
passed the docid of the removed message.")
(defvar mu4e-sent-func nil
"Function called for each :sent sexp received.
This happens when some message has been sent. The function is
passed the docid and the draft-path of the sent message.")
(defvar mu4e-view-func nil
"Function called for each single-message sexp.
The function is passed a message sexp as argument. See
`mu4e--server-filter' for the format.")
(make-obsolete-variable 'mu4e-header-func "mu4e-headers-append-func" "1.7.4")
(defvar mu4e-headers-append-func nil
"Function called with a list of headers to append.
The function is passed a list of message plists as argument. See
See `mu4e--server-filter' for the details.")
(defvar mu4e-found-func nil
"Function called for when we received a :found sexp.
This happens after the headers have been returned, to report on
the number of matches. See `mu4e--server-filter' for the format.")
(defvar mu4e-erase-func nil
"Function called we receive an :erase sexp.
This before new headers are displayed, to clear the current
headers buffer. See `mu4e--server-filter' for the format.")
(defvar mu4e-compose-func nil
"Function called for each compose message received.
I.e., the original message that is used as basis for composing a
new message (i.e., either a reply or a forward); the function is
passed msg and a symbol (either reply or forward). See
`mu4e--server-filter' for the format of <msg-plist>.")
(defvar mu4e-info-func nil
"Function called for each (:info type ....) sexp received.
from the server process.")
(defvar mu4e-pong-func nil
"Function called for each (:pong type ....) sexp received.")
(defvar mu4e-contacts-func nil
"A function called for each (:contacts (<list-of-contacts>)
sexp received from the server process.")
(make-obsolete-variable 'mu4e-temp-func "No longer used" "1.7.0")
;;; Internal vars
(defvar mu4e--server-buf nil
"Buffer (string) for data received from the backend.")
(defconst mu4e--server-name "*mu4e-server*"
"Name of the server process, buffer.")
(defvar mu4e--server-process nil
"The mu-server process.")
2011-09-18 22:57:46 +02:00
;; dealing with the length cookie that precedes expressions
(defconst mu4e--server-cookie-pre "\376"
"Each expression starts with a length cookie:
<`mu4e--server-cookie-pre'><length-in-hex><`mu4e--server-cookie-post'>.")
(defconst mu4e--server-cookie-post "\377"
2020-02-11 12:00:46 +01:00
"Each expression starts with a length cookie:
<`mu4e--server-cookie-pre'><length-in-hex><`mu4e--server-cookie-post'>.")
(defconst mu4e--server-cookie-matcher-rx
(concat mu4e--server-cookie-pre "\\([[:xdigit:]]+\\)"
mu4e--server-cookie-post)
"Regular expression matching the length cookie.
Match 1 will be the length (in hex).")
(defun mu4e-running-p ()
"Whether mu4e is running.
Checks whether the server process is live."
(and mu4e--server-process
(memq (process-status mu4e--server-process)
2020-02-12 17:15:00 +01:00
'(run open listen connect stop))
t))
(defsubst mu4e--server-eat-sexp-from-buf ()
"'Eat' the next s-expression from `mu4e--server-buf'.
Note: this is a string, not an emacs-buffer. `mu4e--server-buf gets
its contents from the mu-servers in the following form:
<`mu4e--server-cookie-pre'><length-in-hex><`mu4e--server-cookie-post'>
Function returns this sexp, or nil if there was none.
`mu4e--server-buf' is updated as well, with all processed sexp data
removed."
(ignore-errors ;; the server may die in the middle...
(let ((b (string-match mu4e--server-cookie-matcher-rx mu4e--server-buf))
2020-02-11 12:00:46 +01:00
(sexp-len) (objcons))
(when b
(setq sexp-len (string-to-number (match-string 1 mu4e--server-buf) 16))
;; does mu4e--server-buf contain the full sexp?
(when (>= (length mu4e--server-buf) (+ sexp-len (match-end 0)))
2020-02-11 12:00:46 +01:00
;; clear-up start
(setq mu4e--server-buf (substring mu4e--server-buf (match-end 0)))
2020-02-11 12:00:46 +01:00
;; 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--server-buf 0 sexp-len)
2020-02-11 12:00:46 +01:00
'utf-8 t)))
(when objcons
(setq mu4e--server-buf (substring mu4e--server-buf sexp-len))
2020-02-11 12:00:46 +01:00
(car objcons)))))))
(defun mu4e--server-filter (_proc str)
"Filter string STR from PROC.
This processes the \"mu server\" output. It accumulates the
strings into valid sexpsv and evaluating those.
2011-09-12 19:52:32 +02:00
The server output is as follows:
2011-09-12 19:52:32 +02:00
1. an error
(:error 2 :message \"unknown command\")
;; eox
=> passed to `mu4e-error-func'.
2011-09-12 19:52:32 +02:00
2a. a header exp looks something like:
(:headers
( ;; message 1
: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/1312_3.32282.pluto,4cd5bd4e9:2,\"
:priority high
:flags (new unread)
:meta <meta-data>
)
( .... more messages )
)
2011-09-12 19:52:32 +02:00
;; eox
=> this will be passed to `mu4e-headers-append-func'.
2b. After the list of headers has been returned (see 2a.),
we'll receive a sexp that looks like
(:found <n>) with n the number of messages found. The <n> will be
2012-04-11 21:31:52 +02:00
passed to `mu4e-found-func'.
3. a view looks like:
(:view <msg-sexp>)
2012-04-11 21:31:52 +02:00
=> the <msg-sexp> (see 2.) will be passed to `mu4e-view-func'.
the <msg-sexp> also contains :body-txt and/or :body-html
4. a database update looks like:
(:update <msg-sexp> :move <nil-or-t>)
like :header
=> the <msg-sexp> (see 2.) will be passed to
2012-04-11 21:31:52 +02:00
`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 <docid>)
2012-04-11 21:31:52 +02:00
=> the docid will be passed to `mu4e-remove-func'
6. a compose looks like:
(:compose <reply|forward|edit|new> [:original<msg-sexp>] [:include <attach>])
`mu4e-compose-func'. :original looks like :view."
(mu4e-log 'misc "* Received %d byte(s)" (length str))
(setq mu4e--server-buf (concat mu4e--server-buf str)) ;; update our buffer
(let ((sexp (mu4e--server-eat-sexp-from-buf)))
(with-local-quit
(while sexp
(mu4e-log 'from-server "%s" sexp)
2020-02-11 12:00:46 +01:00
(cond
;; a header plist can be recognized by the existence of a :date field
((plist-get sexp :headers)
(funcall mu4e-headers-append-func (plist-get sexp :headers)))
2020-02-11 12:00:46 +01:00
;; 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)))
2020-02-11 12:00:46 +01:00
;; 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)
(setq mu4e--server-props (plist-get sexp :props))
2020-02-11 12:00:46 +01:00
(funcall mu4e-pong-func sexp))
;; received a contacts message
;; note: we use 'member', to match (:contacts nil)
((plist-member sexp :contacts)
(funcall mu4e-contacts-func
(plist-get sexp :contacts)
(plist-get sexp :tstamp)))
;; something got moved/flags changed
((plist-get sexp :update)
(funcall mu4e-update-func
(plist-get sexp :update)
2020-02-11 12:00:46 +01:00
(plist-get sexp :move)
(plist-get sexp :maybe-view)))
;; 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)
2020-02-11 12:00:46 +01:00
(plist-get sexp :include)))
;; 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--server-eat-sexp-from-buf))))))
(defun mu4e--server-start ()
2017-10-24 22:05:40 +02:00
"Start the mu server process."
(let ((default-directory temporary-file-directory)) ;;ensure it's local.
;; sanity-check 1
(unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary))
(mu4e-error
"Cannot find mu, please set `mu4e-mu-binary' to the mu executable path"))
;; sanity-check 2
(let ((version (let ((s (shell-command-to-string
(concat mu4e-mu-binary " --version"))))
(and (string-match "version \\([.0-9]+\\)" s)
(match-string 1 s)))))
(unless (string= version mu4e-mu-version)
(mu4e-error
(concat
"Found mu version %s, but mu4e needs version %s"
"; please set `mu4e-mu-binary' "
"accordingly") version mu4e-mu-version)))
2017-10-24 22:05:40 +02:00
(let* ((process-connection-type nil) ;; use a pipe
2020-02-11 12:00:46 +01:00
(args (when mu4e-mu-home `(,(format"--muhome=%s" mu4e-mu-home))))
(args (if mu4e-mu-debug (cons "--debug" args) args))
2020-02-11 12:00:46 +01:00
(args (cons "server" args)))
(setq mu4e--server-buf "")
(setq mu4e--server-process (apply 'start-process
mu4e--server-name mu4e--server-name
mu4e-mu-binary args))
2017-10-24 22:05:40 +02:00
;; register a function for (:info ...) sexps
(unless mu4e--server-process
2017-10-24 22:05:40 +02:00
(mu4e-error "Failed to start the mu4e backend"))
(set-process-query-on-exit-flag mu4e--server-process nil)
(set-process-coding-system mu4e--server-process 'binary 'utf-8-unix)
(set-process-filter mu4e--server-process 'mu4e--server-filter)
(set-process-sentinel mu4e--server-process 'mu4e--server-sentinel))))
2017-10-24 22:05:40 +02:00
(defun mu4e--server-kill ()
2017-10-24 22:05:40 +02:00
"Kill the mu server process."
(let* ((buf (get-buffer mu4e--server-name))
2020-02-11 12:00:46 +01:00
(proc (and (buffer-live-p buf) (get-buffer-process buf))))
2017-10-24 22:05:40 +02:00
(when proc
(message "shutting down mu4e")
(set-process-filter mu4e--server-process nil)
(set-process-sentinel mu4e--server-process nil)
2017-10-24 22:05:40 +02:00
(let ((delete-exited-processes t))
(mu4e--server-call-mu '(quit)))
;; try sending SIGINT (C-c) to process, so it can exit gracefully
2017-10-24 22:05:40 +02:00
(ignore-errors
(signal-process proc 'SIGINT))))
2017-10-24 22:05:40 +02:00
(setq
mu4e--server-process nil
mu4e--server-buf nil))
2017-10-24 22:05:40 +02:00
;; error codes are defined in src/mu-util
;;(defconst mu4e-xapian-empty 19 "Error code: xapian is empty/non-existent")
(defun mu4e--server-sentinel (proc _msg)
"Function called when the server process PROC terminates with MSG."
2017-10-24 22:05:40 +02:00
(let ((status (process-status proc)) (code (process-exit-status proc)))
(setq mu4e--server-process nil)
(setq mu4e--server-buf "") ;; clear any half-received sexps
2017-10-24 22:05:40 +02:00
(cond
2020-02-11 12:00:46 +01:00
((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 19)
(error "Database is locked by another process"))
(t (error "Mu server process ended with exit code %d" code))))
(t
(error "Something bad happened to the mu server process")))))
2017-10-24 22:05:40 +02:00
(defun mu4e--server-call-mu (form)
"Call the mu server with some command FORM."
(unless (mu4e-running-p) (mu4e--server-start))
(let* ((print-length nil) (print-level nil)
(cmd (format "%S" form)))
(mu4e-log 'to-server "%s" cmd)
(process-send-string mu4e--server-process (concat cmd "\n"))))
(defun mu4e--server-add (path)
"Add the message at PATH to the database.
On success, we receive `'(:info add :path <path> :docid <docid>)'
as well as `'(:update <msg-sexp>)`'; otherwise, we receive an error."
(mu4e--server-call-mu `(add :path ,path)))
(defun mu4e--server-compose (type decrypt &optional docid)
"Compose a message of TYPE, DECRYPT it and use DOCID.
TYPE is 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 is delivered to the function registered as
`mu4e-compose-func'."
(mu4e--server-call-mu
`(compose
:type ,type
:decrypt ,(and decrypt t)
:docid ,docid)))
(defun mu4e--server-contacts (personal after maxnum tstamp)
"Ask for contacts with PERSONAL AFTER MAXNUM TSTAMP.
S-expression (:contacts (<list>) :tstamp \"<tstamp>\")
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). If MAX is non-nil,
get at most MAX contacts."
(mu4e--server-call-mu
`(contacts
:personal ,(and personal t)
:after ,(or after nil)
:tstamp ,(or tstamp nil)
:maxnum ,(or maxnum nil))))
(defun mu4e--server-find (query threads sortfield sortdir maxnum skip-dups
include-related)
"Run QUERY with THREADS SORTFIELD SORTDIR MAXNUM SKIP-DUPS INCLUDE-RELATED.
If THREADS is non-nil, show results in threaded fashion,
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,
2017-10-24 22:05:40 +02:00
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 to be be called for, resp., a message (header)
2017-10-24 22:05:40 +02:00
or an error."
(mu4e--server-call-mu
`(find
:query ,query
:threads ,threads
:sortfield ,sortfield
:descending ,(if (eq sortdir 'descending) t nil)
:maxnum ,maxnum
:skip-dups ,skip-dups
:include-related ,include-related)))
(defun mu4e--server-index (&optional cleanup lazy-check)
"Index messages with possible CLEANUP and LAZY-CHECK."
(mu4e--server-call-mu `(index :cleanup ,cleanup :lazy-check ,lazy-check)))
(defun mu4e--server-mkdir (path)
"Create a new maildir-directory at filesystem PATH."
(mu4e--server-call-mu `(mkdir :path ,path)))
(defun mu4e--server-move (docid-or-msgid &optional maildir flags no-view)
"Move message identified by DOCID-OR-MSGID.
Optionally to MAILDIR and optionally setting FLAGS. If MAILDIR is
nil, message will be moved within the same maildir.
2017-11-04 14:06:45 +01:00
At least one of MAILDIR and FLAGS must be specified. Note that
even when MAILDIR is nil, this is still a filesystem move, since
a change in flags implies a change in message filename.
2017-10-24 22:05:40 +02:00
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)'
2017-10-24 22:05:40 +02:00
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 forq
2017-10-24 22:05:40 +02:00
the target files; this helps certain tools (such as mbsync).
If NO-VIEW is non-nil, do not update the view.
Returns either (:update ... ) or (:error ) sexp, which are handled my
`mu4e-update-func' and `mu4e-error-func', respectively."
2017-10-24 22:05:40 +02:00
(unless (or maildir flags)
(mu4e-error "At least one of maildir and flags must be specified"))
2017-11-02 06:51:00 +01:00
(unless (or (not maildir)
2020-02-11 12:00:46 +01:00
(file-exists-p (concat (mu4e-root-maildir) "/" maildir "/")))
2017-10-24 22:05:40 +02:00
(mu4e-error "Target dir does not exist"))
(mu4e--server-call-mu
`(move
:docid ,(if (stringp docid-or-msgid) nil docid-or-msgid)
:msgid ,(if (stringp docid-or-msgid) docid-or-msgid nil)
:flags ,(or flags nil)
:maildir ,(or maildir nil)
:rename ,(and maildir mu4e-change-filenames-when-moving t)
:no-view ,(and no-view t))))
(defun mu4e--server-ping (&optional queries)
"Sends a ping to the mu server, expecting a (:pong ...) in response.
QUERIES is a list of queries for the number of results with
read/unread status are returned in the pong-response."
(mu4e--server-call-mu `(ping :queries ,queries)))
2017-10-24 22:05:40 +02:00
(defun mu4e--server-remove (docid)
"Remove message with DOCID.
The results are reporter through either (:update ... )
or (:error) sexp, which are handled my `mu4e-error-func',
respectively."
(mu4e--server-call-mu `(remove :docid ,docid)))
(defun mu4e--server-sent (path)
"Tell the mu server we sent a message at PATH.
If this works, we will receive (:info add :path <path> :docid
<docid> :fcc <path>)."
(mu4e--server-call-mu `(sent :path ,path)))
2017-10-24 22:05:40 +02:00
(defun mu4e--server-view (docid-or-msgid &optional mark-as-read)
"Get a message DOCID-OR-MSGID.
Optionally, if MARK-AS-READ is non-nil, the backend marks the
message as read before returning, if it was not already unread.
The result will be delivered to the function registered as
`mu4e-view-func'."
(mu4e--server-call-mu
`(view
:docid ,(if (stringp docid-or-msgid) nil docid-or-msgid)
:msgid ,(if (stringp docid-or-msgid) docid-or-msgid nil)
:mark-as-read ,mark-as-read
;; when moving (due to mark-as-read), change filenames
;; if so configured.
:rename ,mu4e-change-filenames-when-moving)))
(provide 'mu4e-server)
;;; mu4e-server.el ends here