From 9157d9102d96d06d7cc39a8dee73f4d928c46bfe Mon Sep 17 00:00:00 2001 From: "Dirk-Jan C. Binnema" Date: Sun, 29 Aug 2021 17:30:10 +0300 Subject: [PATCH] mu4e-utils: refactor into mu4e-helpers, separate files Usurp more of the utils code than can be re-used without further dependencies in helpers. Split off specific parts in their own file. After the helper/utils changes, update the rest of mu4e to take the changes into account. --- mu4e/mu4e-actions.el | 26 +- mu4e/mu4e-bookmarks.el | 150 ++++++ mu4e/mu4e-compose.el | 196 +------- mu4e/mu4e-contacts.el | 231 +++++++++ mu4e/mu4e-context.el | 36 +- mu4e/mu4e-contrib.el | 13 +- mu4e/mu4e-draft.el | 251 +++++++++- mu4e/mu4e-folders.el | 354 ++++++++++++++ mu4e/mu4e-headers.el | 91 ++-- mu4e/mu4e-helpers.el | 170 ++++++- mu4e/mu4e-icalendar.el | 25 +- mu4e/mu4e-lists.el | 51 +- mu4e/mu4e-main.el | 62 ++- mu4e/mu4e-mark.el | 1 + mu4e/mu4e-message.el | 195 +------- mu4e/mu4e-org.el | 2 +- mu4e/mu4e-proc.el | 133 ++++- mu4e/mu4e-speedbar.el | 9 +- mu4e/mu4e-update.el | 320 ++++++++++++ mu4e/mu4e-utils.el | 1060 ---------------------------------------- mu4e/mu4e-vars.el | 702 +------------------------- mu4e/mu4e-view.el | 175 +++---- mu4e/mu4e.el | 201 +++++++- 23 files changed, 2119 insertions(+), 2335 deletions(-) create mode 100644 mu4e/mu4e-bookmarks.el create mode 100644 mu4e/mu4e-contacts.el create mode 100644 mu4e/mu4e-folders.el create mode 100644 mu4e/mu4e-update.el delete mode 100644 mu4e/mu4e-utils.el diff --git a/mu4e/mu4e-actions.el b/mu4e/mu4e-actions.el index 2c8af24e..41e6d0b3 100644 --- a/mu4e/mu4e-actions.el +++ b/mu4e/mu4e-actions.el @@ -30,18 +30,11 @@ (require 'cl-lib) (require 'ido) -(require 'mu4e-utils) +(require 'mu4e-helpers) (require 'mu4e-message) +(require 'mu4e-search) (require 'mu4e-meta) -(declare-function mu4e~proc-extract "mu4e-proc") -(declare-function mu4e-headers-search "mu4e-headers") - -(defvar mu4e-headers-include-related) -(defvar mu4e-headers-show-threads) -(defvar mu4e-view-show-addresses) -(defvar mu4e-view-date-format) - ;;; Count lines @@ -177,6 +170,14 @@ Otherwise return nil." (if (re-search-forward regexp nil t) (replace-match to-string nil nil))))) +(declare-function mu4e~proc-add "mu4e-proc") +(defun mu4e--refresh-message (path) + "Re-parse message at PATH. +if this works, we will +receive (:info add :path :docid ) as well as (:update +)." + (mu4e~proc-add path)) + (defun mu4e-action-retag-message (msg &optional retag-arg) "Change tags of MSG with RETAG-ARG. @@ -231,7 +232,7 @@ would add 'tag' and 'long tag', and remove 'oldtag'." path)) (mu4e-message (concat "tagging: " (mapconcat 'identity taglist ", "))) - (mu4e-refresh-message path))) + (mu4e--refresh-message path))) (defun mu4e-action-show-thread (msg) "Show thread for message at point with point remaining on MSG. @@ -240,14 +241,13 @@ action was invoked. If invoked in view mode, continue to display the message." (let ((msgid (mu4e-message-field msg :message-id))) (when msgid - (let ((mu4e-headers-show-threads t) + (let ((mu4e-search-threads t) (mu4e-headers-include-related t)) - (mu4e-headers-search + (mu4e-search (format "msgid:%s" msgid) nil nil nil msgid (and (eq major-mode 'mu4e-view-mode) (not (eq mu4e-split-view 'single-window)))))))) - ;;; _ (provide 'mu4e-actions) ;;; mu4e-actions.el ends here diff --git a/mu4e/mu4e-bookmarks.el b/mu4e/mu4e-bookmarks.el new file mode 100644 index 00000000..ce6a2531 --- /dev/null +++ b/mu4e/mu4e-bookmarks.el @@ -0,0 +1,150 @@ +;;; mu4e-bookmarks.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*- + +;; Copyright (C) 2011-2021 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema +;; Maintainer: Dirk-Jan C. Binnema + +;; This file is not part of GNU Emacs. + +;; mu4e 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. + +;; mu4e 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 mu4e. If not, see . + +;;; Commentary: + +;;; Code: +(require 'cl-lib) +(require 'mu4e-helpers) + + +;;; Configuration + +(defgroup mu4e-bookmarks nil + "Settings for bookmarks." + :group 'mu4e) + +;; for backward compatibility, when a bookmark was defined with defstruct. +(cl-defun make-mu4e-bookmark (&key name query key) + "Create a mu4e plist. +It has has with the following elements: +- NAME: the user-visible name of the bookmark +- KEY: a single key to search for this bookmark +- QUERY: the query for this bookmark. Either a literal string or a function + that evaluates to a string." + `(:name ,name :query ,query :key ,key)) +(make-obsolete 'make-mu4e-bookmark "`unneeded; `mu4e-bookmarks' +are plists" "1.3.7") + +(defcustom mu4e-bookmarks + '(( :name "Unread messages" + :query "flag:unread AND NOT flag:trashed" + :key ?u) + ( :name "Today's messages" + :query "date:today..now" + :key ?t) + ( :name "Last 7 days" + :query "date:7d..now" + :hide-unread t + :key ?w) + ( :name "Messages with images" + :query "mime:image/*" + :key ?p)) + "List of pre-defined queries that are shown on the main screen. + +Each of the list elements is a plist with at least: +`:name' - the name of the query +`:query' - the query expression or function +`:key' - the shortcut key. + +Note that the :query parameter can be a function/lambda. + +Optionally, you can add the following: +`:hide' - if t, the bookmark is hidden from the main-view and + speedbar. +`:hide-unread' - do not show the counts of unread/total number + of matches for the query in the main-view. This can be useful +if a bookmark uses a very slow query. :hide-unread +is implied from :hide. Furthermore, it is implied if +`:query' is a function. + +Queries used to determine the unread/all counts do _not_ apply +`mu4e-query-rewrite-function'; nor do they discard duplicate or +unreadable messages (for efficiency). Thus, the numbers shown may +differ from the number you get from a 'real' query." + :type '(repeat (plist)) + :version "1.3.9" + :group 'mu4e-bookmarks) + + + (defun mu4e-ask-bookmark (prompt) + "Ask the user for a bookmark (using PROMPT) as defined in +`mu4e-bookmarks', then return the corresponding query." + (unless (mu4e-bookmarks) (mu4e-error "No bookmarks defined")) + (let* ((prompt (mu4e-format "%s" prompt)) + (bmarks + (mapconcat + (lambda (bm) + (concat + "[" (propertize (make-string 1 (plist-get bm :key)) + 'face 'mu4e-highlight-face) + "]" + (plist-get bm :name))) (mu4e-bookmarks) ", ")) + (kar (read-char (concat prompt bmarks)))) + (mu4e-get-bookmark-query kar))) + +(defun mu4e-get-bookmark-query (kar) + "Get the corresponding bookmarked query for shortcut KAR. +Raise an error if none is found." + (let* ((chosen-bm + (or (cl-find-if + (lambda (bm) + (= kar (plist-get bm :key))) + (mu4e-bookmarks)) + (mu4e-warn "Unknown shortcut '%c'" kar))) + (expr (plist-get chosen-bm :query)) + (expr (if (not (functionp expr)) expr + (funcall expr))) + (query (eval expr))) + (if (stringp query) + query + (mu4e-warn "Expression must evaluate to query string ('%S')" expr)))) + + +(defun mu4e-bookmark-define (query name key) + "Define a bookmark for QUERY with NAME and shortcut KEY. +Append it to `mu4e-bookmarks'. Replaces any existing bookmark +with KEY." + (setq mu4e-bookmarks + (cl-remove-if + (lambda (bm) + (= (plist-get bm :key) key)) + (mu4e-bookmarks))) + (cl-pushnew `(:name ,name + :query ,query + :key ,key) + mu4e-bookmarks :test 'equal)) + +(defun mu4e-bookmarks () + "Get `mu4e-bookmarks' in the (new) format. +Convert from the old format if needed." + (cl-map 'list + (lambda (item) + (if (and (listp item) (= (length item) 3)) + `(:name ,(nth 1 item) + :query ,(nth 0 item) + :key ,(nth 2 item)) + item)) + mu4e-bookmarks)) + +(provide 'mu4e-bookmarks) +;;; mu4e-bookmarks.el ends here diff --git a/mu4e/mu4e-compose.el b/mu4e/mu4e-compose.el index 26f367a7..ca469fec 100644 --- a/mu4e/mu4e-compose.el +++ b/mu4e/mu4e-compose.el @@ -72,192 +72,17 @@ (require 'smtpmail) (require 'rfc2368) -(require 'mu4e-utils) -(require 'mu4e-vars) (require 'mu4e-proc) (require 'mu4e-actions) (require 'mu4e-message) (require 'mu4e-draft) (require 'mu4e-context) -;;; Composing / Sending messages - -(defgroup mu4e-compose nil - "Customizations for composing/sending messages." - :group 'mu4e) - -(defcustom mu4e-sent-messages-behavior 'sent - "Determines what mu4e does with sent messages. - -This is one of the symbols: -* `sent' move the sent message to the Sent-folder (`mu4e-sent-folder') -* `trash' move the sent message to the Trash-folder (`mu4e-trash-folder') -* `delete' delete the sent message. - -Note, when using GMail/IMAP, you should set this to either -`trash' or `delete', since GMail already takes care of keeping -copies in the sent folder. - -Alternatively, `mu4e-sent-messages-behavior' can be a function -which takes no arguments, and which should return one of the mentioned -symbols, for example: - - (setq mu4e-sent-messages-behavior (lambda () - (if (string= (message-sendmail-envelope-from) \"foo@example.com\") - 'delete 'sent))) - -The various `message-' functions from `message-mode' are available -for querying the message information." - :type '(choice (const :tag "move message to mu4e-sent-folder" sent) - (const :tag "move message to mu4e-trash-folder" trash) - (const :tag "delete message" delete)) - :group 'mu4e-compose) - -(defcustom mu4e-compose-context-policy 'ask - "Policy for determining the context when composing a new message. - -If the value is `always-ask', ask the user unconditionally. - -In all other cases, if any context matches (using its match -function), this context is used. Otherwise, if none of the -contexts match, we have the following choices: - -- `pick-first': pick the first of the contexts available (ie. the default) -- `ask': ask the user -- `ask-if-none': ask if there is no context yet, otherwise leave it as it is -- nil: return nil; leaves the current context as is. - -Also see `mu4e-context-policy'." - :type '(choice - (const :tag "Always ask what context to use" always-ask) - (const :tag "Ask if none of the contexts match" ask) - (const :tag "Ask when there's no context yet" ask-if-none) - (const :tag "Pick the first context if none match" pick-first) - (const :tag "Don't change the context when none match" nil)) - :safe 'symbolp - :group 'mu4e-compose) - -(defcustom mu4e-compose-crypto-policy - '(encrypt-encrypted-replies sign-encrypted-replies) - "Policy to control when messages will be signed/encrypted. - -The value is a list, whose members determine the behaviour of -`mu4e~compose-crypto-message'. Specifically, it might contain: - -- `sign-all-messages': Always add a signature. -- `sign-new-messages': Add a signature to new message, ie. - messages that aren't responses to another message. -- `sign-forwarded-messages': Add a signature when forwarding - a message -- `sign-edited-messages': Add a signature to drafts -- `sign-all-replies': Add a signature when responding to - another message. -- `sign-plain-replies': Add a signature when responding to - non-encrypted messages. -- `sign-encrypted-replies': Add a signature when responding - to encrypted messages. - -It should be noted that certain symbols have priorities over one -another. So `sign-all-messages' implies `sign-all-replies', which -in turn implies `sign-plain-replies'. Adding both to the set, is -not a contradiction, but a redundant configuration. - -All `sign-*' options have a `encrypt-*' analogue." - :type '(set :greedy t - (const :tag "Sign all messages" sign-all-messages) - (const :tag "Encrypt all messages" encrypt-all-messages) - (const :tag "Sign new messages" sign-new-messages) - (const :tag "Encrypt new messages" encrypt-new-messages) - (const :tag "Sign forwarded messages" sign-forwarded-messages) - (const :tag "Encrypt forwarded messages" encrypt-forwarded-messages) - (const :tag "Sign edited messages" sign-edited-messages) - (const :tag "Encrypt edited messages" edited-forwarded-messages) - (const :tag "Sign all replies" sign-all-replies) - (const :tag "Encrypt all replies" encrypt-all-replies) - (const :tag "Sign replies to plain messages" sign-plain-replies) - (const :tag "Encrypt replies to plain messages" encrypt-plain-replies) - (const :tag "Sign replies to encrypted messages" sign-encrypted-replies) - (const :tag "Encrypt replies to encrypted messages" encrypt-encrypted-replies)) - :group 'mu4e-compose) - -(defcustom mu4e-compose-crypto-reply-encrypted-policy nil - "Policy for signing/encrypting replies to encrypted messages. -We have the following choices: - -- `sign': sign the reply -- `sign-and-encrypt': sign and encrypt the reply -- `encrypt': encrypt the reply, but don't sign it. -- anything else: do nothing." - :type '(choice - (const :tag "Sign the reply" sign) - (const :tag "Sign and encrypt the reply" sign-and-encrypt) - (const :tag "Encrypt the reply" encrypt) - (const :tag "Don't do anything" nil)) - :safe 'symbolp - :group 'mu4e-compose) - -(make-obsolete-variable 'mu4e-compose-crypto-reply-encrypted-policy "The use of the - 'mu4e-compose-crypto-reply-encrypted-policy' variable is deprecated. - 'mu4e-compose-crypto-policy' should be used instead" - "2020-03-06") - -(defcustom mu4e-compose-crypto-reply-plain-policy nil - "Policy for signing/encrypting replies to messages received unencrypted. -We have the following choices: - -- `sign': sign the reply -- `sign-and-encrypt': sign and encrypt the reply -- `encrypt': encrypt the reply, but don't sign it. -- anything else: do nothing." - :type '(choice - (const :tag "Sign the reply" sign) - (const :tag "Sign and encrypt the reply" sign-and-encrypt) - (const :tag "Encrypt the reply" encrypt) - (const :tag "Don't do anything" nil)) - :safe 'symbolp - :group 'mu4e-compose) - -(make-obsolete-variable 'mu4e-compose-crypto-reply-plain-policy "The use of the - 'mu4e-compose-crypto-reply-plain-policy' variable is deprecated. - 'mu4e-compose-crypto-policy' should be used instead" - "2020-03-06") - -(make-obsolete-variable 'mu4e-compose-crypto-reply-policy "The use of the - 'mu4e-compose-crypto-reply-policy' variable is deprecated. - 'mu4e-compose-crypto-reply-plain-policy' and - 'mu4e-compose-crypto-reply-encrypted-policy' should be used instead" - "2017-09-02") - -(defcustom mu4e-compose-format-flowed nil - "Whether to compose messages to be sent as format=flowed. -\(Or with long lines if variable `use-hard-newlines' is set to -nil). The variable `fill-flowed-encode-column' lets you customize -the width beyond which format=flowed lines are wrapped." - :type 'boolean - :safe 'booleanp - :group 'mu4e-compose) - -(defcustom mu4e-compose-pre-hook nil - "Hook run just *before* message composition starts. -If the compose-type is either 'reply' or 'forward', the variable -`mu4e-compose-parent-message' points to the message replied to / -being forwarded / edited, and `mu4e-compose-type' contains the -type of message to be composed. - -Note that there is no draft message yet when this hook runs, it -is meant for influencing the how mu4e constructs the draft -message. If you want to do something with the draft messages after -it has been constructed, `mu4e-compose-mode-hook' would be the -place to do that." - :type 'hook - :group 'mu4e-compose) - -(defvar mu4e-compose-type nil - "The compose-type for this buffer. -This is a symbol, `new', `forward', `reply' or `edit'.") - + +;;; Configuration +;; see mu4e-drafts.el + ;;; Attachments - (defun mu4e-compose-attach-message (msg) "Insert message MSG as an attachment." (let ((path (plist-get msg :path))) @@ -393,9 +218,9 @@ Message-ID." "Complete address STR with predication PRED for ACTION." (cond ((eq action nil) - (try-completion str mu4e~contacts-hash pred)) + (try-completion str mu4e--contacts-hash pred)) ((eq action t) - (all-completions str mu4e~contacts-hash pred)) + (all-completions str mu4e--contacts-hash pred)) ((eq action 'metadata) ;; our contacts are already sorted - just need to tell the ;; completion machinery not to try to undo that... @@ -529,8 +354,9 @@ buffers; lets remap its faces so it uses the ones for mu4e." (mu4e~compose-register-message-save-hooks) ;; offer completion for e-mail addresses (when mu4e-compose-complete-addresses - (unless mu4e~contacts-hash ;; work-around for https://github.com/djcb/mu/issues/1016 - (mu4e~request-contacts-maybe)) + (unless mu4e--contacts-hash + ;; work-around for https://github.com/djcb/mu/issues/1016 + (mu4e--request-contacts-maybe)) (mu4e~compose-setup-completion)) (if mu4e-compose-format-flowed (progn @@ -986,6 +812,8 @@ draft message." ;; mu4e-compose-func and mu4e-send-func are wrappers so we can set ourselves ;; as default emacs mailer (define-mail-user-agent etc.) +(declare-function mu4e "mu4e") + ;;;###autoload (defun mu4e~compose-mail (&optional to subject other-headers _continue switch-function yank-action _send-actions _return-action) @@ -1022,7 +850,7 @@ caller. It has the form (FUNCTION . ARGS). The function is called after the mail has been sent or put aside, and the mail buffer buried." (unless (mu4e-running-p) - (mu4e~start)) + (mu4e)) ;; create a new draft message 'resetting' (as below) is not actually needed in this case, but ;; let's prepare for the re-edit case as well diff --git a/mu4e/mu4e-contacts.el b/mu4e/mu4e-contacts.el new file mode 100644 index 00000000..dcb434a2 --- /dev/null +++ b/mu4e/mu4e-contacts.el @@ -0,0 +1,231 @@ +;;; mu4e-contacts.el -- part of mu4e -*- lexical-binding: t -*- + +;; Copyright (C) 2021 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema +;; Maintainer: Dirk-Jan C. Binnema + +;; This file is not part of GNU Emacs. + +;; mu4e 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. + +;; mu4e 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 mu4e. If not, see . + +;;; Commentary: + +;; Utility functions used in the mu4e + +;;; Code: +(require 'cl-lib) +(require 'mu4e-helpers) + + +;;; Configuration +(defcustom mu4e-compose-complete-addresses t + "Whether to do auto-completion of e-mail addresses." + :type 'boolean + :group 'mu4e-compose) + +(defcustom mu4e-compose-complete-only-personal nil + "Whether to consider only 'personal' e-mail addresses for completion. +That is, addresses from messages where user was explicitly in one +of the address fields (this excludes mailing list messages). +These addresses are the ones specified with `mu init'." + :type 'boolean + :group 'mu4e-compose) + +(defcustom mu4e-compose-complete-only-after "2014-01-01" + "Consider only contacts last seen after this date. + +Date must be a string of the form YYY-MM-DD. + +This is useful for limiting a potentially enormous set of +contacts for auto-completion to just those that are present in +the e-mail corpus in recent timses. Set to nil to not have any +time-based restriction." + :type 'string + :group 'mu4e-compose) + +;; names and mail-addresses can be mapped onto their canonical +;; counterpart. use the customizeable function +;; mu4e-canonical-contact-function to do that. below the identity +;; function for mapping a contact onto the canonical one. +(defun mu4e-contact-identity (contact) + "Return the name and the mail-address of a CONTACT. +It is used as the identity function for converting contacts to +their canonical counterpart; useful as an example." + (let ((name (plist-get contact :name)) + (mail (plist-get contact :mail))) + (list :name name :mail mail))) + +(make-obsolete-variable 'mu4e-contact-rewrite-function + "mu4e-contact-process-function (see docstring)" + "mu4e 1.3.2") +(make-obsolete-variable 'mu4e-compose-complete-ignore-address-regexp + "mu4e-contact-process-function (see docstring)" + "mu4e 1.3.2") + +(defcustom mu4e-contact-process-function + (lambda(addr) ;; filter-out no-reply addresses + (unless (string-match-p "no[t]?[-\\.]?repl\\(y\\|ies\\)" addr) + addr)) + "Function for processing contact information for use in auto-completion. + +The function receives the contact as a string, e.g + \"Foo Bar \" + \"cuux@example.com\" + +The function should return either: +- nil: do not use this contact for completion +- the (possibly rewritten) address, which must be +an RFC-2822-compatible e-mail address." + :type 'function + :group 'mu4e-compose) + +(defcustom mu4e-compose-reply-ignore-address + '("no-?reply") + "Addresses to prune when doing wide replies. + +This can be a regexp matching the address, a list of regexps or a +predicate function. A value of nil keeps all the addresses." + :type '(choice + (const nil) + function + string + (repeat string)) + :group 'mu4e-compose) + + +;;; Internal variables +(defvar mu4e--contacts-tstamp "0" + "Timestamp for the most recent contacts update." ) + +(defvar mu4e--contacts-hash nil + "Hash that maps contacts (ie. 'name ') to an integer for sorting. +We need to keep this information around to quickly re-sort +subsets of the contacts in the completions function in +mu4e-compose.") + +;;; user mail address +(defun mu4e-personal-addresses(&optional no-regexp) + "Get the list user's personal addresses, as passed to mu init. +The address are either plain e-mail address or /regular + expressions/. When NO-REGEXP is non-nil, do not include regexp + address patterns (if any)." + (seq-remove + (lambda(addr) (and no-regexp (string-match-p "^/.*/" addr))) + (when (mu4e-server-properties) + (plist-get (mu4e-server-properties) :personal-addresses)))) + +(defun mu4e-personal-address-p (addr) + "Is ADDR a personal address? +Evaluate to nil if ADDR matches any of the personal addresses. +Uses (mu4e-personal-addresses) for the addresses with both the plain +addresses and /regular expressions/." + (when addr + (seq-find + (lambda (m) + (if (string-match "/\\(.*\\)/" m) + (let ((rx (match-string 1 m)) + (case-fold-search t)) + (if (string-match rx addr) t nil)) + (eq t (compare-strings addr nil nil m nil nil 'case-insensitive)))) + (mu4e-personal-addresses)))) + +(define-obsolete-function-alias 'mu4e-user-mail-address-p + 'mu4e-personal-address-p "1.5.5") + + +;; don't use the older vars anymore +(make-obsolete-variable 'mu4e-user-mail-address-regexp + 'mu4e-user-mail-address-list "0.9.9.x") +(make-obsolete-variable 'mu4e-my-email-addresses + 'mu4e-user-mail-address-list "0.9.9.x") +(make-obsolete-variable 'mu4e-user-mail-address-list + "determined by server; see `mu4e-personal-addresses'." + "1.3.8") + + +(defun mu4e--update-contacts (contacts &optional tstamp) + "Receive a sorted list of CONTACTS newer than TSTAMP. +Each of the contacts has the form + (FULL_EMAIL_ADDRESS . RANK) and fill `mu4e--contacts-hash' with +it, with each contact mapped to an integer for their ranking. + +This is used by the completion function in mu4e-compose." + ;; We have our nicely sorted list, map them to a list + ;; of increasing integers. We use that map in the composer + ;; to sort them there. It would have been so much easier if emacs + ;; allowed us to use the sorted-list as-is, but no such luck. + (let ((n 0)) + (unless mu4e--contacts-hash + (setq mu4e--contacts-hash (make-hash-table :test 'equal :weakness nil + :size (length contacts)))) + (dolist (contact contacts) + (cl-incf n) + (let* ((address (plist-get contact :address)) + (address + (if (functionp mu4e-contact-process-function) + (funcall mu4e-contact-process-function address) + address))) + (when address ;; note the explicit deccode; the strings we get are + ;; utf-8, but emacs doesn't know yet. + (puthash (decode-coding-string address 'utf-8) + (plist-get contact :rank) mu4e--contacts-hash)))) + + (setq mu4e--contacts-tstamp (or tstamp "0")) + + (unless (zerop n) + (mu4e-index-message "Contacts updated: %d; total %d" + n (hash-table-count mu4e--contacts-hash))))) + +(defun mu4e-contacts-info () + "Display information about the contacts-cache. +For testing/debugging." + (interactive) + (with-current-buffer (get-buffer-create "*mu4e-contacts-info*") + (erase-buffer) + (insert (format "complete addresses: %s\n" + (if mu4e-compose-complete-addresses "yes" "no"))) + (insert (format "only personal addresses: %s\n" + (if mu4e-compose-complete-only-personal "yes" "no"))) + (insert (format "only addresses seen after: %s\n" + (or mu4e-compose-complete-only-after "no restrictions"))) + + (when mu4e--contacts-hash + (insert (format "number of contacts cached: %d\n\n" + (hash-table-count mu4e--contacts-hash))) + (let ((contacts)) + (maphash (lambda (addr rank) + (setq contacts (cons (cons rank addr) contacts))) + mu4e--contacts-hash) + (setq contacts (sort contacts + (lambda(cell1 cell2) (< (car cell1) (car cell2))))) + (dolist (contact contacts) + (insert (format "%s\n" (cdr contact)))))) + + (pop-to-buffer "*mu4e-contacts-info*"))) + +(declare-function mu4e~proc-contacts "mu4e-proc") + +(defun mu4e--request-contacts-maybe () + "If `mu4e-compose-complete-addresses' is non-nil, get/update +the list of contacts we use for autocompletion; otherwise, do +nothing." + (when mu4e-compose-complete-addresses + (mu4e~proc-contacts + mu4e-compose-complete-only-personal + mu4e-compose-complete-only-after + mu4e--contacts-tstamp))) + +(provide 'mu4e-contacts) +;;; mu4e-contacts.el ends here diff --git a/mu4e/mu4e-context.el b/mu4e/mu4e-context.el index 300c53eb..90eb0d56 100644 --- a/mu4e/mu4e-context.el +++ b/mu4e/mu4e-context.el @@ -31,6 +31,30 @@ ;;; Configuration +(defcustom mu4e-context-policy 'ask-if-none + "The policy to determine the context when entering the mu4e main view. + +If the value is `always-ask', ask the user unconditionally. + +In all other cases, if any context matches (using its match +function), this context is used. Otherwise, if none of the +contexts match, we have the following choices: + +- `pick-first': pick the first of the contexts available (ie. the default) +- `ask': ask the user +- `ask-if-none': ask if there is no context yet, otherwise leave it as it is +- nil: return nil; leaves the current context as is. + +Also see `mu4e-compose-context-policy'." + :type '(choice + (const :tag "Always ask what context to use, even if one matches" + always-ask) + (const :tag "Ask if none of the contexts match" ask) + (const :tag "Ask when there's no context yet" ask-if-none) + (const :tag "Pick the first context if none match" pick-first) + (const :tag "Don't change the context when none match" nil)) + :group 'mu4e) + (defvar mu4e-contexts nil "The list of `mu4e-context' objects describing mu4e's contexts.") @@ -42,12 +66,10 @@ '((t :inherit mu4e-title-face :weight bold)) "Face for displaying the context in the modeline." :group 'mu4e-faces) - (defvar mu4e--context-current nil "The current context. Internal; use `mu4e-context-switch' to change it.") - (defun mu4e-context-current (&optional output) "Get the currently active context, or nil if there is none. @@ -188,6 +210,16 @@ global-mode-line." (make-local-variable 'global-mode-string) '(:eval (mu4e-context-label)))) +(defmacro with-mu4e-context-vars (context &rest body) + "Evaluate BODY, with variables let-bound for CONTEXT (if any). +`funcall'." + (declare (indent 2)) + `(let* ((vars (and ,context (mu4e-context-vars ,context)))) + (cl-progv ;; XXX: perhaps use eval's lexical environment instead of progv? + (mapcar (lambda(cell) (car cell)) vars) + (mapcar (lambda(cell) (cdr cell)) vars) + (eval ,@body)))) + (define-minor-mode mu4e-context-minor-mode "Mode for switching the mu4e context." :global nil diff --git a/mu4e/mu4e-contrib.el b/mu4e/mu4e-contrib.el index c784373e..767c2c6c 100644 --- a/mu4e/mu4e-contrib.el +++ b/mu4e/mu4e-contrib.el @@ -96,7 +96,7 @@ BOOKMARK is a bookmark name or a bookmark record." (docid (cdr path)) (query (car path))) (call-interactively 'mu4e) - (mu4e-headers-search query) + (mu4e-search query) (sit-for 0.5) (mu4e~headers-goto-docid docid) (mu4e~headers-highlight docid) @@ -172,6 +172,17 @@ For example for bogofile, use \"/usr/bin/bogofilter -Sn < %s\"") ;; allowing files to be attached to an email via mu4e using the ;; eshell. Does not depend on gnus. + +(defun mu4e~active-composition-buffers () + "Return all active mu4e composition buffers" + (let (buffers) + (save-excursion + (dolist (buffer (buffer-list t)) + (set-buffer buffer) + (when (eq major-mode 'mu4e-compose-mode) + (push (buffer-name buffer) buffers)))) + (nreverse buffers))) + (defun eshell/mu4e-attach (&rest args) "Attach files to a mu4e message using eshell. If no mu4e buffers found, compose a new message and then attach the file." diff --git a/mu4e/mu4e-draft.el b/mu4e/mu4e-draft.el index d68d3f15..839b0461 100644 --- a/mu4e/mu4e-draft.el +++ b/mu4e/mu4e-draft.el @@ -27,12 +27,232 @@ ;;; Code: (require 'cl-lib) -(require 'mu4e-vars) -(require 'mu4e-utils) (require 'mu4e-message) +(require 'mu4e-contacts) +(require 'mu4e-folders) (require 'message) ;; mail-header-separator -;;; Options + +;;; Configuration +(defgroup mu4e-compose nil + "Customizations for composing/sending messages." + :group 'mu4e) + +(defcustom mu4e-compose-reply-recipients 'ask + "Which recipients to use when replying to a message. +May be 'ask, 'all, 'sender. Note that that only applies to +non-mailing-list message; for those, mu4e always asks." + :type '(choice ask + all + sender) + :group 'mu4e-compose) + +(defcustom mu4e-compose-reply-to-address nil + "The Reply-To address. +Useful when this is not equal to the From: address." + :type 'string + :group 'mu4e-compose) + +(defcustom mu4e-compose-forward-as-attachment nil + "Whether to forward messages as attachments instead of inline." + :type 'boolean + :group 'mu4e-compose) + +;; backward compatibility +(make-obsolete-variable 'mu4e-reply-to-address + 'mu4e-compose-reply-to-address + "v0.9.9") + +(defcustom mu4e-compose-keep-self-cc nil + "When non-nil. keep your e-mail address in Cc: when replying." + :type 'boolean + :group 'mu4e-compose) + +(defvar mu4e-compose-parent-message nil + "The parent message plist. +This is the message being replied to, forwarded or edited; used +in `mu4e-compose-pre-hook'. For new messages, it is nil.") + +(make-obsolete-variable 'mu4e-auto-retrieve-keys "no longer used." "1.3.1") + +(defcustom mu4e-decryption-policy t + "Policy for dealing with replying/forwarding encrypted parts. +The setting is a symbol: + * t: try to decrypt automatically + * `ask': ask before decrypting anything + * nil: don't try to decrypt anything." + :type '(choice (const :tag "Try to decrypt automatically" t) + (const :tag "Ask before decrypting anything" ask) + (const :tag "Don't try to decrypt anything" nil)) + :group 'mu4e-compose) + +;;; Composing / Sending messages + +(defcustom mu4e-sent-messages-behavior 'sent + "Determines what mu4e does with sent messages. + +This is one of the symbols: +* `sent' move the sent message to the Sent-folder (`mu4e-sent-folder') +* `trash' move the sent message to the Trash-folder (`mu4e-trash-folder') +* `delete' delete the sent message. + +Note, when using GMail/IMAP, you should set this to either +`trash' or `delete', since GMail already takes care of keeping +copies in the sent folder. + +Alternatively, `mu4e-sent-messages-behavior' can be a function +which takes no arguments, and which should return one of the mentioned +symbols, for example: + + (setq mu4e-sent-messages-behavior (lambda () + (if (string= (message-sendmail-envelope-from) \"foo@example.com\") + 'delete 'sent))) + +The various `message-' functions from `message-mode' are available +for querying the message information." + :type '(choice (const :tag "move message to mu4e-sent-folder" sent) + (const :tag "move message to mu4e-trash-folder" trash) + (const :tag "delete message" delete)) + :group 'mu4e-compose) + +(defcustom mu4e-compose-context-policy 'ask + "Policy for determining the context when composing a new message. + +If the value is `always-ask', ask the user unconditionally. + +In all other cases, if any context matches (using its match +function), this context is used. Otherwise, if none of the +contexts match, we have the following choices: + +- `pick-first': pick the first of the contexts available (ie. the default) +- `ask': ask the user +- `ask-if-none': ask if there is no context yet, otherwise leave it as it is +- nil: return nil; leaves the current context as is. + +Also see `mu4e-context-policy'." + :type '(choice + (const :tag "Always ask what context to use" always-ask) + (const :tag "Ask if none of the contexts match" ask) + (const :tag "Ask when there's no context yet" ask-if-none) + (const :tag "Pick the first context if none match" pick-first) + (const :tag "Don't change the context when none match" nil)) + :safe 'symbolp + :group 'mu4e-compose) + +(defcustom mu4e-compose-crypto-policy + '(encrypt-encrypted-replies sign-encrypted-replies) + "Policy to control when messages will be signed/encrypted. + +The value is a list, whose members determine the behaviour of +`mu4e~compose-crypto-message'. Specifically, it might contain: + +- `sign-all-messages': Always add a signature. +- `sign-new-messages': Add a signature to new message, ie. + messages that aren't responses to another message. +- `sign-forwarded-messages': Add a signature when forwarding + a message +- `sign-edited-messages': Add a signature to drafts +- `sign-all-replies': Add a signature when responding to + another message. +- `sign-plain-replies': Add a signature when responding to + non-encrypted messages. +- `sign-encrypted-replies': Add a signature when responding + to encrypted messages. + +It should be noted that certain symbols have priorities over one +another. So `sign-all-messages' implies `sign-all-replies', which +in turn implies `sign-plain-replies'. Adding both to the set, is +not a contradiction, but a redundant configuration. + +All `sign-*' options have a `encrypt-*' analogue." + :type '(set :greedy t + (const :tag "Sign all messages" sign-all-messages) + (const :tag "Encrypt all messages" encrypt-all-messages) + (const :tag "Sign new messages" sign-new-messages) + (const :tag "Encrypt new messages" encrypt-new-messages) + (const :tag "Sign forwarded messages" sign-forwarded-messages) + (const :tag "Encrypt forwarded messages" encrypt-forwarded-messages) + (const :tag "Sign edited messages" sign-edited-messages) + (const :tag "Encrypt edited messages" edited-forwarded-messages) + (const :tag "Sign all replies" sign-all-replies) + (const :tag "Encrypt all replies" encrypt-all-replies) + (const :tag "Sign replies to plain messages" sign-plain-replies) + (const :tag "Encrypt replies to plain messages" encrypt-plain-replies) + (const :tag "Sign replies to encrypted messages" sign-encrypted-replies) + (const :tag "Encrypt replies to encrypted messages" encrypt-encrypted-replies)) + :group 'mu4e-compose) + +(defcustom mu4e-compose-crypto-reply-encrypted-policy nil + "Policy for signing/encrypting replies to encrypted messages. +We have the following choices: + +- `sign': sign the reply +- `sign-and-encrypt': sign and encrypt the reply +- `encrypt': encrypt the reply, but don't sign it. +- anything else: do nothing." + :type '(choice + (const :tag "Sign the reply" sign) + (const :tag "Sign and encrypt the reply" sign-and-encrypt) + (const :tag "Encrypt the reply" encrypt) + (const :tag "Don't do anything" nil)) + :safe 'symbolp + :group 'mu4e-compose) + +(make-obsolete-variable 'mu4e-compose-crypto-reply-encrypted-policy "The use of the + 'mu4e-compose-crypto-reply-encrypted-policy' variable is deprecated. + 'mu4e-compose-crypto-policy' should be used instead" + "2020-03-06") + +(defcustom mu4e-compose-crypto-reply-plain-policy nil + "Policy for signing/encrypting replies to messages received unencrypted. +We have the following choices: + +- `sign': sign the reply +- `sign-and-encrypt': sign and encrypt the reply +- `encrypt': encrypt the reply, but don't sign it. +- anything else: do nothing." + :type '(choice + (const :tag "Sign the reply" sign) + (const :tag "Sign and encrypt the reply" sign-and-encrypt) + (const :tag "Encrypt the reply" encrypt) + (const :tag "Don't do anything" nil)) + :safe 'symbolp + :group 'mu4e-compose) + +(make-obsolete-variable 'mu4e-compose-crypto-reply-plain-policy "The use of the + 'mu4e-compose-crypto-reply-plain-policy' variable is deprecated. + 'mu4e-compose-crypto-policy' should be used instead" + "2020-03-06") + +(make-obsolete-variable 'mu4e-compose-crypto-reply-policy "The use of the + 'mu4e-compose-crypto-reply-policy' variable is deprecated. + 'mu4e-compose-crypto-reply-plain-policy' and + 'mu4e-compose-crypto-reply-encrypted-policy' should be used instead" + "2017-09-02") + +(defcustom mu4e-compose-format-flowed nil + "Whether to compose messages to be sent as format=flowed. +\(Or with long lines if variable `use-hard-newlines' is set to +nil). The variable `fill-flowed-encode-column' lets you customize +the width beyond which format=flowed lines are wrapped." + :type 'boolean + :safe 'booleanp + :group 'mu4e-compose) + +(defcustom mu4e-compose-pre-hook nil + "Hook run just *before* message composition starts. +If the compose-type is either 'reply' or 'forward', the variable +`mu4e-compose-parent-message' points to the message replied to / +being forwarded / edited, and `mu4e-compose-type' contains the +type of message to be composed. + +Note that there is no draft message yet when this hook runs, it +is meant for influencing the how mu4e constructs the draft +message. If you want to do something with the draft messages after +it has been constructed, `mu4e-compose-mode-hook' would be the +place to do that." + :type 'hook + :group 'mu4e-compose) (defcustom mu4e-compose-dont-reply-to-self nil "If non-nil, don't include self. @@ -79,6 +299,11 @@ mu4e-specific version of `message-signature'." (defvar mu4e-view-date-format) +(defvar mu4e-compose-type nil + "The compose-type for this buffer. +This is a symbol, `new', `forward', `reply' or `edit'.") + + (defun mu4e~draft-cite-original (msg) "Return a cited version of the original message MSG as a plist. This function uses `mu4e-compose-cite-function', and as such all @@ -119,6 +344,22 @@ one. Code borrowed from `message-shorten-1'." (setcdr (nthcdr (- cut 2) list) (nthcdr (+ (- cut 2) surplus 1) list))) + + +(defun mu4e~fontify-signature () + "Give the message signatures a distinctive color. This is used in +the view and compose modes and will color each signature in digest messages adhering to RFC 1153." + (let ((inhibit-read-only t)) + (save-excursion + ;; give the footer a different color... + (goto-char (point-min)) + (while (re-search-forward "^-- *$" nil t) + (let ((p (point)) + (end (or + (re-search-forward "\\(^-\\{30\\}.*$\\)" nil t) ;; 30 by RFC1153 + (point-max)))) + (add-text-properties p end '(face mu4e-footer-face))))))) + (defun mu4e~draft-references-construct (msg) "Construct the value of the References: header based on MSG. This assumes a comma-separated string. Normally, this the concatenation of the @@ -558,13 +799,13 @@ will be created from either `mu4e~draft-reply-construct', or ;; case-1: re-editing a draft messages. in this case, we do know the ;; full path, but we cannot really know 'drafts folder'... we make a ;; guess - (setq draft-dir (mu4e~guess-maildir (mu4e-message-field msg :path))) + (setq draft-dir (mu4e--guess-maildir (mu4e-message-field msg :path))) (mu4e~draft-open-file (mu4e-message-field msg :path) switch-function)) (resend ;; case-2: copy some exisisting message to a draft message, then edit ;; that. - (setq draft-dir (mu4e~guess-maildir (mu4e-message-field msg :path))) + (setq draft-dir (mu4e--guess-maildir (mu4e-message-field msg :path))) (let ((draft-path (mu4e~draft-determine-path draft-dir))) (copy-file (mu4e-message-field msg :path) draft-path) (mu4e~draft-open-file draft-path switch-function))) diff --git a/mu4e/mu4e-folders.el b/mu4e/mu4e-folders.el new file mode 100644 index 00000000..0e7e5c18 --- /dev/null +++ b/mu4e/mu4e-folders.el @@ -0,0 +1,354 @@ +;;; mu4e-folders.el -- part of mu4e -*- lexical-binding: t -*- + +;; Copyright (C) 2021 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema +;; Maintainer: Dirk-Jan C. Binnema + +;; This file is not part of GNU Emacs. + +;; mu4e 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. + +;; mu4e 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 mu4e. If not, see . + +;;; Commentary: + +;; Dealing with maildirs & folders + +;;; Code: +(require 'cl-lib) +(require 'mu4e-helpers) +(require 'mu4e-context) +(require 'mu4e-proc) + +;;; Customization +(defgroup mu4e-folders nil + "Special folders." + :group 'mu4e) + +(defcustom mu4e-drafts-folder "/drafts" + "Folder for draft messages, relative to the root maildir. +For instance, \"/drafts\". Instead of a string, may also be a +function that takes a message (a msg plist, see +`mu4e-message-field'), and returns a folder. Note, the message +parameter refers to the original message being replied to / being +forwarded / re-edited and is nil otherwise. `mu4e-drafts-folder' +is only evaluated once." + :type '(choice + (string :tag "Folder name") + (function :tag "Function return folder name")) + :group 'mu4e-folders) + +(defcustom mu4e-refile-folder "/archive" + "Folder for refiling messages, relative to the root maildir. +For instance \"/Archive\". Instead of a string, may also be a +function that takes a message (a msg plist, see +`mu4e-message-field'), and returns a folder. Note that the +message parameter refers to the message-at-point." + :type '(choice + (string :tag "Folder name") + (function :tag "Function return folder name")) + :group 'mu4e-folders) + +(defcustom mu4e-sent-folder "/sent" + "Folder for sent messages, relative to the root maildir. +For instance, \"/Sent Items\". Instead of a string, may also be a +function that takes a message (a msg plist, see +`mu4e-message-field'), and returns a folder. Note that the +message parameter refers to the original message being replied to +/ being forwarded / re-edited, and is nil otherwise." + :type '(choice + (string :tag "Folder name") + (function :tag "Function return folder name")) + :group 'mu4e-folders) + +(defcustom mu4e-trash-folder "/trash" + "Folder for trashed messages, relative to the root maildir. +For instance, \"/trash\". Instead of a string, may also be a +function that takes a message (a msg plist, see +`mu4e-message-field'), and returns a folder. When using +`mu4e-trash-folder' in the headers view (when marking messages +for trash). Note that the message parameter refers to the +message-at-point. When using it when composing a message (see +`mu4e-sent-messages-behavior'), this refers to the original +message being replied to / being forwarded / re-edited, and is +nil otherwise." + :type '(choice + (string :tag "Folder name") + (function :tag "Function return folder name")) + :group 'mu4e-folders) + +(defcustom mu4e-maildir-shortcuts nil + "A list of maildir shortcuts. +This makes it possible to quickly go to a particular +maildir (folder), or quickly moving messages to them (e.g., for +archiving or refiling). + +Each of the list elements is a plist with at least: +`:maildir' - the maildir for the shortcut (e.g. \"/archive\") +`:key' - the shortcut key. + +Optionally, you can add the following: +`:hide' - if t, the shortcut is hidden from the main-view and +speedbar. +`:hide-unread' - do not show the counts of unread/total number + of matches for the maildir in the main-view, and is implied +from `:hide'. + +For backward compatibility, an older form is recognized as well: + + (maildir . key), where MAILDIR is a maildir (such as +\"/archive/\"), and key is a single character. + +You can use these shortcuts in the headers and view buffers, for +example with `mu4e-mark-for-move-quick' (or 'm', by default) or +`mu4e-jump-to-maildir' (or 'j', by default), followed by the +designated shortcut character for the maildir. + +Unlike in search queries, folder names with spaces in them must +NOT be quoted, since mu4e does this for you." + :type '(repeat (cons (string :tag "Maildir") character)) + :version "1.3.9" + :group 'mu4e-folders) + +(defcustom mu4e-maildir-info-delimiter + (if (member system-type '(ms-dos windows-nt cygwin)) + ";" ":") + "Separator character between message identifier and flags. +It defaults to ':' on most platforms, except on Windows, where it +is not allowed and we use ';' for compatibility with mbsync, +offlineimap and other programs." + :type 'string + :group 'mu4e-folders) + +(defcustom mu4e-attachment-dir (expand-file-name "~/") + "Default directory for attaching and saving attachments. + +This can be either a string (a file system path), or a function +that takes a filename and the mime-type as arguments, and returns +the attachment dir. See Info node `(mu4e) Attachments' for +details. + +When this called for composing a message, both filename and +mime-type are nill." + :type 'directory + :group 'mu4e-folders + :safe 'stringp) + + + +(defun mu4e-maildir-shortcuts () + "Get `mu4e-maildir-shortcuts' in the (new) format. +Converts from the old format if needed." + (cl-map 'list + (lambda (item) ;; convert from old format? + (if (and (consp item) (not (consp (cdr item)))) + `(:maildir ,(car item) :key ,(cdr item)) + item)) + mu4e-maildir-shortcuts)) + +(defun mu4e--maildirs-with-query () + "Llike `mu4e-maildir-shortcuts', but with :query populated. + +This is meant to be the exact same data structure as +`mu4e-bookmarks'." + (cl-mapcar + (lambda (m) + (append + ;; we want to change the :maildir key to :name, and add a :query key + (list :name (plist-get m :maildir) + :query (format "maildir:\"%s\"" (plist-get m :maildir))) + ;; next we want to append any other keys to our previous list (e.g. :hide, + ;; :key, etc) but skipping :maildir (since it's renamed to :name) + (cl-loop for (key value) on m by 'cddr + when (not (equal key :maildir)) + append (list key value)))) + (mu4e-maildir-shortcuts))) + + +;; the standard folders can be functions too +(defun mu4e--get-folder (foldervar msg) + "Within the mu-context of MSG, get message folder FOLDERVAR. +If FOLDER is a string, return it, if it is a function, evaluate +this function with MSG as parameter which may be nil, and return +the result." + (unless (member foldervar + '(mu4e-sent-folder mu4e-drafts-folder + mu4e-trash-folder mu4e-refile-folder)) + (mu4e-error "Folder must be one of mu4e-(sent|drafts|trash|refile)-folder")) + ;; get the value with the vars for the relevants context let-bound + (with-mu4e-context-vars (mu4e-context-determine msg nil) + (let* ((folder (symbol-value foldervar)) + (val + (cond + ((stringp folder) folder) + ((functionp folder) (funcall folder msg)) + (t (mu4e-error "Unsupported type for %S" folder))))) + (or val (mu4e-error "%S evaluates to nil" foldervar))))) + +(defun mu4e-get-drafts-folder (&optional msg) + "Get the sent folder, optionallly based on MSG. +See `mu4e-drafts-folder'." (mu4e--get-folder 'mu4e-drafts-folder msg)) + +(defun mu4e-get-refile-folder (&optional msg) + "Get the folder for refiling, optionallly based on MSG. +See `mu4e-refile-folder'." (mu4e--get-folder 'mu4e-refile-folder msg)) + +(defun mu4e-get-sent-folder (&optional msg) + "Get the sent folder, optionallly based on MSG. +See `mu4e-sent-folder'." (mu4e--get-folder 'mu4e-sent-folder msg)) + +(defun mu4e-get-trash-folder (&optional msg) + "Get the sent folder, optionallly based on MSG. +See `mu4e-trash-folder'." (mu4e--get-folder 'mu4e-trash-folder msg)) + + +;;; Maildirs +(defun mu4e--guess-maildir (path) + "Guess the maildir for PATH, or nil if cannot find it." + (let ((idx (string-match (mu4e-root-maildir) path))) + (when (and idx (zerop idx)) + (replace-regexp-in-string + (mu4e-root-maildir) + "" + (expand-file-name + (concat path "/../..")))))) + +(defun mu4e-create-maildir-maybe (dir) + "Offer to create maildir DIR if it does not exist yet. +Return t if the dir already existed, or an attempt has been made to +create it -- we cannot be sure creation succeeded here, since this +is done asynchronously. Otherwise, return nil. NOte, DIR has to be +an absolute path." + (if (and (file-exists-p dir) (not (file-directory-p dir))) + (mu4e-error "File %s exists, but is not a directory" dir)) + (cond + ((file-directory-p dir) t) + ((yes-or-no-p (mu4e-format "%s does not exist yet. Create now?" dir)) + (mu4e~proc-mkdir dir) t) + (t nil))) + +(defun mu4e~get-maildirs-1 (path mdir) + "Get maildirs for MDIR under PATH. +Do so recursively and produce a list of relative paths." + (let ((dirs) + (dentries + (ignore-errors + (directory-files-and-attributes + (concat path mdir) nil + "^[^.]\\|\\.[^.][^.]" t)))) + (dolist (dentry dentries) + (when (and (booleanp (cadr dentry)) (cadr dentry)) + (if (file-accessible-directory-p + (concat (mu4e-root-maildir) "/" mdir "/" (car dentry) "/cur")) + (setq dirs (cons (concat mdir (car dentry)) dirs))) + (unless (member (car dentry) '("cur" "new" "tmp")) + (setq dirs + (append dirs + (mu4e~get-maildirs-1 path + (concat mdir + (car dentry) "/"))))))) + dirs)) + +(defvar mu4e-cache-maildir-list nil + "Whether to cache the list of maildirs. +Set it to t if you find +that generating the list on the fly is too slow. If you do, you +can set `(mu4e-root-maildir)-list' to nil to force regenerating the +cache the next time `mu4e-get-maildirs' gets called.") + +(defvar mu4e-maildir-list nil + "Cached list of maildirs.") + +(defun mu4e-get-maildirs () + "Get maildirs under `mu4e-maildir'. +Do so recursively, and produce a list of relative paths (ie., +/archive, /sent etc.). Most of the work is done in +`mu4e~get-maildirs-1'. Note, these results are /cached/ if +`mu4e-cache-maildir-list' is customized to non-nil. In that case, +the list of maildirs will not change until you restart mu4e." + (unless (and mu4e-maildir-list mu4e-cache-maildir-list) + (setq mu4e-maildir-list + (sort + (append + (when (file-accessible-directory-p + (concat (mu4e-root-maildir) "/cur")) '("/")) + (mu4e~get-maildirs-1 (mu4e-root-maildir) "/")) + (lambda (s1 s2) (string< (downcase s1) (downcase s2)))))) + mu4e-maildir-list) + +(defun mu4e-ask-maildir (prompt) + "Ask the user for a shortcut (using PROMPT). +As per (mu4e-maildir-shortcuts), then return the corresponding folder +name. If the special shortcut 'o' (for _o_ther) is used, or if +`(mu4e-maildir-shortcuts)' evaluates to nil, let user choose from +all maildirs under `mu4e-maildir'." + (let ((prompt (mu4e-format "%s" prompt))) + (if (not (mu4e-maildir-shortcuts)) + (substring-no-properties + (funcall mu4e-completing-read-function prompt (mu4e-get-maildirs))) + (let* ((mlist (append (mu4e-maildir-shortcuts) + '((:maildir "ther" :key ?o)))) + (fnames + (mapconcat + (lambda (item) + (concat + "[" + (propertize (make-string 1 (plist-get item :key)) + 'face 'mu4e-highlight-face) + "]" + (plist-get item :maildir))) + mlist ", ")) + (kar (read-char (concat prompt fnames)))) + (if (member kar '(?/ ?o)) ;; user chose 'other'? + (substring-no-properties + (funcall mu4e-completing-read-function prompt + (mu4e-get-maildirs) nil nil "/")) + (or (plist-get + (cl-find-if (lambda (item) (= kar (plist-get item :key))) + (mu4e-maildir-shortcuts)) :maildir) + (mu4e-warn "Unknown shortcut '%c'" kar))))))) + +(defun mu4e-ask-maildir-check-exists (prompt) + "Like `mu4e-ask-maildir', PROMPT for existence of the maildir. +Offer to create it if it does not exist yet." + (let* ((mdir (mu4e-ask-maildir prompt)) + (fullpath (concat (mu4e-root-maildir) mdir))) + (unless (file-directory-p fullpath) + (and (yes-or-no-p + (mu4e-format "%s does not exist. Create now?" fullpath)) + (mu4e~proc-mkdir fullpath))) + mdir)) + +;; mu4e-attachment-dir is either a string or a function that takes a +;; filename and the mime-type as argument, either (or both) which can +;; be nil + +(defun mu4e~get-attachment-dir (&optional fname mimetype) + "Get the directory for saving attachments from +`mu4e-attachment-dir' (which can be either a string or a function, +see its docstring)." + (let + ((dir + (cond + ((stringp mu4e-attachment-dir) + mu4e-attachment-dir) + ((functionp mu4e-attachment-dir) + (funcall mu4e-attachment-dir fname mimetype)) + (t + (mu4e-error "unsupported type for mu4e-attachment-dir" ))))) + (if dir + (expand-file-name dir) + (mu4e-error "mu4e-attachment-dir evaluates to nil")))) + +(provide 'mu4e-folders) +;;; mu4e-folders.el ends here diff --git a/mu4e/mu4e-headers.el b/mu4e/mu4e-headers.el index 503c6133..74e68c64 100644 --- a/mu4e/mu4e-headers.el +++ b/mu4e/mu4e-headers.el @@ -34,6 +34,8 @@ (require 'mailcap) (require 'mule-util) ;; seems _some_ people need this for truncate-string-ellipsis +(require 'mu4e-update) + (require 'mu4e-utils) ;; utility functions (require 'mu4e-proc) (require 'mu4e-vars) @@ -43,11 +45,14 @@ (require 'mu4e-compose) (require 'mu4e-actions) (require 'mu4e-message) +(require 'mu4e-folders) (declare-function mu4e-view "mu4e-view") (declare-function mu4e~main-view "mu4e-main") -;;; Options + + +;;; Configuration (defgroup mu4e-headers nil "Settings for the headers view." @@ -175,7 +180,7 @@ one of: `:date', `:subject', `:size', `:prio', `:from', `:to.', `:list'. Note that when threading is enabled (through -`mu4e-headers-show-threads'), the headers are exclusively sorted +`mu4e-search-threads'), the headers are exclusively sorted chronologically (`:date') by the newest message in the thread.") (defvar mu4e-headers-sort-direction 'descending @@ -284,6 +289,7 @@ followed by the docid, followed by `mu4e~headers-docid-post'.") "List of cells describing the various sort-options. In the format needed for `mu4e-read-option'.") + ;;; Clear (defvar mu4e~headers-render-start nil) @@ -293,6 +299,11 @@ In the format needed for `mu4e-read-option'.") "If non-nil, report on the time it took to render the messages. This is mostly useful for profiling.") + + + + + (defun mu4e~headers-clear (&optional msg) "Clear the header buffer and related data structures." (when (buffer-live-p (mu4e-get-headers-buffer)) @@ -744,14 +755,19 @@ if provided, or at the end of the buffer otherwise." ;;; Performing queries (internal) +(defconst mu4e~search-message "Searching...") +(defconst mu4e~no-matches "No matching messages found") +(defconst mu4e~end-of-results "End of search results") + + (defun mu4e--search-execute (expr ignore-history) "Search for query EXPR. Switch to the output buffer for the results. If IGNORE-HISTORY is true, do *not* update the query history stack." - (let* ((buf (get-buffer-create mu4e~headers-buffer-name)) + (let* ((buf (get-buffer-create mu4e-headers-buffer-name)) (inhibit-read-only t) - (rewritten-expr (funcall mu4e-query-rewrite-function expr)) + (rewritten-expr (funcall mu4e-search-query-rewrite-function expr)) (maxnum (unless mu4e-search-full mu4e-search-results-limit))) (with-current-buffer buf (mu4e-headers-mode) @@ -770,17 +786,13 @@ true, do *not* update the query history stack." (mu4e~headers-clear mu4e~search-message) (mu4e~proc-find rewritten-expr - mu4e-headers-show-threads + mu4e-search-threads mu4e-headers-sort-field mu4e-headers-sort-direction maxnum mu4e-headers-skip-duplicates mu4e-headers-include-related))) -(defconst mu4e~search-message "Searching...") -(defconst mu4e~no-matches "No matching messages found") -(defconst mu4e~end-of-results "End of search results") - (defvar mu4e~headers-view-target nil "Whether to automatically view (open) the target message (as per `mu4e~headers-msgid-target').") @@ -866,10 +878,6 @@ after the end of the search results." (setq mu4e-headers-mode-map (let ((map (make-sparse-keymap))) - (define-key map (kbd "C-S-u") 'mu4e-update-mail-and-index) - ;; for terminal users - (define-key map (kbd "C-c C-u") 'mu4e-update-mail-and-index) - (define-key map "j" 'mu4e~headers-jump-to-maildir) (define-key map "O" 'mu4e-headers-change-sorting) @@ -968,8 +976,8 @@ after the end of the search results." (define-key menumap [toggle-threading] '(menu-item "Toggle threading" mu4e-headers-toggle-threading :button (:toggle . - (and (boundp 'mu4e-headers-show-threads) - mu4e-headers-show-threads)))) + (and (boundp 'mu4e-search-threads) + mu4e-search-threads)))) (define-key menumap "|" '("Pipe through shell" . mu4e-view-pipe)) (define-key menumap [sepa1] '("--")) @@ -1036,7 +1044,7 @@ after the end of the search results." (mapcar (lambda (item) (let* ( ;; with threading enabled, we're necessarily sorting by date. - (sort-field (if mu4e-headers-show-threads :date mu4e-headers-sort-field)) + (sort-field (if mu4e-search-threads :date mu4e-headers-sort-field)) (field (car item)) (width (cdr item)) (info (cdr (assoc field (append mu4e-header-info mu4e-header-info-custom)))) @@ -1115,6 +1123,7 @@ no user-interaction ongoing." (mu4e~mark-initialize) ;; initialize the marking subsystem (mu4e-context-minor-mode) + (mu4e-update-minor-mode) (mu4e-search-minor-mode) (hl-line-mode 1)) @@ -1220,9 +1229,9 @@ docid is not found." (if mu4e-use-fancy-chars (cddr flag-cell) (cadr flag-cell) ) "")) - `((,mu4e-headers-full-search . ,mu4e-headers-full-label) + `((,mu4e-search-full . ,mu4e-headers-full-label) (,mu4e-headers-include-related . ,mu4e-headers-related-label) - (,mu4e-headers-show-threads . ,mu4e-headers-threaded-label)) + (,mu4e-search-threads . ,mu4e-headers-threaded-label)) "")) (name "mu4e-headers")) @@ -1491,17 +1500,17 @@ re-run the last search." (mu4e-search-rerun))) (defun mu4e-headers-toggle-threading (&optional dont-refresh) - "Toggle `mu4e-headers-show-threads'. With prefix-argument, do + "Toggle `mu4e-search-threads'. With prefix-argument, do _not_ refresh the last search with the new setting for threading." (interactive "P") - (mu4e~headers-toggle "Threading" 'mu4e-headers-show-threads dont-refresh)) + (mu4e~headers-toggle "Threading" 'mu4e-search-threads dont-refresh)) (defun mu4e-headers-toggle-full-search (&optional dont-refresh) - "Toggle `mu4e-headers-full-search'. With prefix-argument, do + "Toggle `mu4e-search-full'. With prefix-argument, do _not_ refresh the last search with the new setting for threading." (interactive "P") (mu4e~headers-toggle "Full-search" - 'mu4e-headers-full-search dont-refresh)) + 'mu4e-search-full dont-refresh)) (defun mu4e-headers-toggle-include-related (&optional dont-refresh) "Toggle `mu4e-headers-include-related'. With prefix-argument, do @@ -1520,14 +1529,6 @@ _not_ refresh the last search with the new setting for threading." (defvar mu4e~headers-loading-buf nil "A buffer for loading a message view.") -(defun mu4e~decrypt-p (msg) - "Should we decrypt this message?" - (when mu4e-view-use-old ;; we don't decrypt in the gnus-view case - (and (member 'encrypted (mu4e-message-field msg :flags)) - (if (eq mu4e-decryption-policy 'ask) - (yes-or-no-p (mu4e-format "Decrypt message?")) - mu4e-decryption-policy)))) - (defun mu4e-headers-view-message () "View message at point . If there's an existing window for the view, re-use that one . If @@ -1545,8 +1546,8 @@ window . " (if (functionp mu4e-view-auto-mark-as-read) (funcall mu4e-view-auto-mark-as-read msg) mu4e-view-auto-mark-as-read)) - (decrypt (mu4e~decrypt-p msg)) - (verify mu4e-view-use-old) + (decrypt nil) ;; XXX remove + (verify nil) ;; XXX remove (viewwin (mu4e~headers-redraw-get-view-window))) (unless (window-live-p viewwin) (mu4e-error "Cannot get a message view")) @@ -1741,6 +1742,32 @@ other windows." (kill-buffer) (mu4e~main-view 'refresh)))) + +;;; Loading messages +;; +(defvar mu4e-loading-mode-map nil "Keymap for *mu4e-loading* buffers.") +(unless mu4e-loading-mode-map + (setq mu4e-loading-mode-map + (let ((map (make-sparse-keymap))) + (define-key map "n" 'ignore) + (define-key map "p" 'ignore) + (define-key map "q" + (lambda()(interactive) + (if (eq mu4e-split-view 'single-window) + 'kill-buffer + 'kill-buffer-and-window))) + map))) +(fset 'mu4e-loading-mode-map mu4e-loading-mode-map) + +(define-derived-mode mu4e-loading-mode special-mode + "mu4e:loading" + (use-local-map mu4e-loading-mode-map) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert (propertize "Loading message..." + 'face 'mu4e-system-face 'intangible t)))) + + ;;; _ (provide 'mu4e-headers) ;;; mu4e-headers.el ends here diff --git a/mu4e/mu4e-helpers.el b/mu4e/mu4e-helpers.el index ae1d7b60..7b533f89 100644 --- a/mu4e/mu4e-helpers.el +++ b/mu4e/mu4e-helpers.el @@ -28,6 +28,7 @@ ;;; Code: (require 'seq) +(require 'ido) (require 'cl-lib) @@ -44,6 +45,126 @@ If the string exceeds this limit, it will be truncated to fit." :type 'integer :group 'mu4e) +(defcustom mu4e-completing-read-function 'ido-completing-read + "Function to be used to receive user-input during completion. +Suggested possible values are: + * `completing-read': built-in completion method + * `ido-completing-read': dynamic completion within the minibuffer." + :type 'function + :options '(completing-read ido-completing-read) + :group 'mu4e) + +(defcustom mu4e-use-fancy-chars nil + "When set, allow fancy (Unicode) characters for marks/threads. +You can customize the exact fancy characters used with +`mu4e-marks' and various `mu4e-headers-..-mark' and +`mu4e-headers..-prefix' variables." + :type 'boolean + :group 'mu4e) + +(defcustom mu4e-display-update-status-in-modeline nil + "Non-nil value will display the update status in the modeline." + :group 'mu4e + :type 'boolean) + +;; maybe move the next ones... but they're convenient +;; here because they're needed in multiple buffers. + +(defcustom mu4e-view-auto-mark-as-read t + "Automatically mark messages are 'read' when you read them. +This is the default behavior, but can be turned off, for example +when using a read-only file-system. + +This can also be set to a function; if so, receives a message +plist which should evaluate to nil if the message should *not* be +marked as read-only, or non-nil otherwise." + :type '(choice + boolean + function) + :group 'mu4e-view) + + +(defcustom mu4e-split-view 'horizontal + "How to show messages / headers. +A symbol which is either: + * `horizontal': split horizontally (headers on top) + * `vertical': split vertically (headers on the left). + * `single-window': view and headers in one window (mu4e will try not to + touch your window layout), main view in minibuffer + * anything else: don't split (show either headers or messages, + not both) +Also see `mu4e-headers-visible-lines' +and `mu4e-headers-visible-columns'." + :type '(choice (const :tag "Split horizontally" horizontal) + (const :tag "Split vertically" vertical) + (const :tag "Single window" single-window) + (const :tag "Don't split" nil)) + :group 'mu4e-headers) + +;;; Buffers + +(defconst mu4e-main-buffer-name " *mu4e-main*" + "Name of the mu4e main buffer. +The default name starts with SPC and therefore is not visible in +buffer list.") +(defconst mu4e-headers-buffer-name "*mu4e-headers*" + "Name of the buffer for message headers.") +(defconst mu4e-embedded-buffer-name " *mu4e-embedded*" + "Name for the embedded message view buffer.") + +(defun mu4e-get-headers-buffer() + "Get the name of the headers buffer." + (get-buffer mu4e-headers-buffer-name)) + +(defun mu4e-get-view-buffer() + "Get the name of the view buffer." + ;; avoid a 'require. + (when (boundp 'gnus-article-buffer) gnus-article-buffer)) + +(defun mu4e-select-other-view () + "Switch between headers view and message view." + (interactive) + (let* ((other-buf + (cond + ((eq major-mode 'mu4e-headers-mode) + (mu4e-get-view-buffer)) + ((eq major-mode 'mu4e-view-mode) + (mu4e-get-headers-buffer)))) + (other-win (and other-buf (get-buffer-window other-buf)))) + (if (window-live-p other-win) + (select-window other-win) + (mu4e-message "No window to switch to")))) + + +;;; Windows +(defun mu4e-hide-other-mu4e-buffers () + "Bury mu4e buffers. +Hide (main, headers, view) (and delete all windows displaying +it). Do _not_ bury the current buffer, though." + (interactive) + (unless (eq mu4e-split-view 'single-window) + (let ((curbuf (current-buffer))) + ;; note: 'walk-windows' does not seem to work correctly when modifying + ;; windows; therefore, the doloops here + (dolist (frame (frame-list)) + (dolist (win (window-list frame nil)) + (with-current-buffer (window-buffer win) + (unless (eq curbuf (current-buffer)) + (when (member major-mode '(mu4e-headers-mode mu4e-view-mode)) + (when (eq t (window-deletable-p win)) + (delete-window win))))))) t))) + +;;; Modeline + +(defun mu4e-quote-for-modeline (str) + "Quote STR to be used literally in the modeline. +The string will be shortened to fit if its length exceeds +`mu4e-modeline-max-width'." + (replace-regexp-in-string + "%" "%%" + (truncate-string-to-width str mu4e-modeline-max-width 0 nil t))) + + ;;; Messages, warnings and errors (defun mu4e-format (frm &rest args) @@ -175,16 +296,6 @@ Function will return the cdr of the list element." (plist-get mu4e--server-props :version)) (mu4e-error "Version unknown; did you start mu4e?"))) -(defun mu4e-personal-addresses(&optional no-regexp) - "Get the list user's personal addresses, as passed to mu init. -The address are either plain e-mail address or /regular - expressions/. When NO-REGEXP is non-nil, do not include regexp - address patterns (if any)." - (seq-remove - (lambda(addr) (and no-regexp (string-match-p "^/.*/" addr))) - (when mu4e--server-props - (plist-get mu4e--server-props :personal-addresses)))) - (defun mu4e-last-query-results () "Get the results (counts) of the last cached queries. @@ -279,17 +390,9 @@ log-buffer. See `mu4e-show-log'." (mu4e-warn "No debug log available")) (switch-to-buffer buf))) + -;;; Misc - -(defun mu4e-quote-for-modeline (str) - "Quote STR to be used literally in the modeline. -The string will be shortened to fit if its length exceeds -`mu4e-modeline-max-width'." - (replace-regexp-in-string - "%" "%%" - (truncate-string-to-width str mu4e-modeline-max-width 0 nil t))) - +;;; Flags ;; Converting flags->string and vice-versa (defun mu4e-flags-to-string (flags) @@ -337,6 +440,8 @@ http://cr.yp.to/proto/maildir.html." (_ nil)))) str)))) + +;;; Misc (defun mu4e-display-size (size) "Get a human-friendly string representation of SIZE (in bytes)." (cond @@ -383,6 +488,31 @@ This includes expanding e.g. 3-5 into 3,4,5. If the letter (mu4e-warn "Attachment number must be greater than 0 (%d)" x)))) list))) +(defun mu4e-make-temp-file (ext) + "Create a self-destructing temporary file with extension EXT. +The file will self-destruct in a short while, enough to open it +in an external program." + (let ((tmpfile (make-temp-file "mu4e-" nil (concat "." ext)))) + (run-at-time "30 sec" nil + (lambda () (ignore-errors (delete-file tmpfile)))) + tmpfile)) + +(defun mu4e-display-manual () + "Display the mu4e manual page for the current mode. +Or go to the top level if there is none." + (interactive) + (info (cl-case major-mode + ('mu4e-main-mode "(mu4e)Main view") + ('mu4e-headers-mode "(mu4e)Headers view") + ('mu4e-view-mode "(mu4e)Message view") + (t "mu4e")))) + + ;;; Macros + +(defmacro mu4e-setq-if-nil (var val) + "Set VAR to VAL if VAR is nil." + `(unless ,var (setq ,var ,val))) + (provide 'mu4e-helpers) ;;; mu4e-helpers.el ends here diff --git a/mu4e/mu4e-icalendar.el b/mu4e/mu4e-icalendar.el index 7feceaf2..3b5bef8f 100644 --- a/mu4e/mu4e-icalendar.el +++ b/mu4e/mu4e-icalendar.el @@ -53,14 +53,31 @@ (require 'cl-lib) (require 'mu4e-mark) -(require 'mu4e-utils) +(require 'mu4e-helpers) +(require 'mu4e-contacts) (require 'mu4e-headers) (require 'mu4e-view) -(require 'mu4e-vars) -(when mu4e-view-use-old - (mu4e-error "iCalender support is not available with the old viewer")) + +;;; Configuration +;;;; Calendar +(defgroup mu4e-icalendar nil + "Icalendar related settings." + :group 'mu4e) + +(defcustom mu4e-icalendar-trash-after-reply nil + "If non-nil, trash the icalendar invitation after replying." + :type 'boolean + :group 'mu4e-icalendar) + +(defcustom mu4e-icalendar-diary-file nil + "If non-nil, the file in which to add events upon reply." + :type '(choice (const :tag "Do not insert a diary entry" nil) + (string :tag "Insert a diary entry in this file")) + :group 'mu4e-icalendar) + + ;;;###autoload (defun mu4e-icalendar-setup () "Perform the necessary initialization to use mu4e-icalendar." diff --git a/mu4e/mu4e-lists.el b/mu4e/mu4e-lists.el index 654ada33..fa7333f4 100644 --- a/mu4e/mu4e-lists.el +++ b/mu4e/mu4e-lists.el @@ -1,6 +1,6 @@ -;;; mu4e-lists.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*- +;;; mu4e-lists.el -- part of mu4e -*- lexical-binding: t -*- -;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema +;; Copyright (C) 2011-2021 Dirk-Jan C. Binnema ;; Author: Dirk-Jan C. Binnema ;; Maintainer: Dirk-Jan C. Binnema @@ -27,7 +27,10 @@ ;;; Code: -(defvar mu4e~mailing-lists +(require 'cl-lib) + +;;; Configuration +(defvar mu4e-mailing-lists '( ("bbdb-info.lists.sourceforge.net" . "BBDB") ("boost-announce.lists.boost.org" . "BoostA") ("boost-interest.lists.boost.org" . "BoostI") @@ -81,22 +84,50 @@ ("wl-en.ml.gentei.org" . "WdrLust") ("xapian-devel.lists.xapian.org" . "Xapian") ("zsh-users.zsh.org" . "ZshUsr")) - "AList of cells (MAILING-LIST-ID . SHORTNAME)") + "AList of cells (MAILING-LIST-ID . SHORTNAME).") (defcustom mu4e-user-mailing-lists nil - "An alist with cells (MAILING-LIST-ID . SHORTNAME); these are -used in addition to the built-in list `mu4e~mailing-lists'." + "An alist with cells (MAILING-LIST-ID . SHORTNAME). +These are used in addition to the built-in list `mu4e~mailing-lists'." :group 'mu4e-headers :type '(repeat (cons string string))) - (defcustom mu4e-mailing-list-patterns nil - "A list of regex patterns to capture a shortname out of a list -ID. For the first regex that matches, its first matchgroup will -be used as the shortname." + "A list of regexps to capture a shortname out of a list-id. +For the first regex that matches, its first matchgroup will be +used as the shortname." :group 'mu4e-headers :type '(repeat (regexp))) + +(defvar mu4e--lists-hash nil + "Hashtable of mailing-list-id => shortname. +Based on `mu4e-mailing-lists' and `mu4e-user-mailing-lists'.") + + +(defun mu4e-get-mailing-list-shortname (list-id) + "Get the shortname for a mailing-list with list-id LIST-ID. +Based on `mu4e-mailing-lists', `mu4e-user-mailing-lists', and +`mu4e-mailing-list-patterns'." + (unless mu4e--lists-hash + (setq mu4e--lists-hash (make-hash-table :test 'equal)) + (dolist (cell mu4e-mailing-lists) + (puthash (car cell) (cdr cell) mu4e--lists-hash)) + (dolist (cell mu4e-user-mailing-lists) + (puthash (car cell) (cdr cell) mu4e--lists-hash))) + (or + (gethash list-id mu4e--lists-hash) + (and (boundp 'mu4e-mailing-list-patterns) + (cl-member-if + (lambda (pattern) + (string-match pattern list-id)) + mu4e-mailing-list-patterns) + (match-string 1 list-id)) + ;; if it's not in the db, take the part until the first dot if there is one; + ;; otherwise just return the whole thing + (if (string-match "\\([^.]*\\)\\." list-id) + (match-string 1 list-id) + list-id))) ;;; _ (provide 'mu4e-lists) ;;; mu4e-lists.el ends here diff --git a/mu4e/mu4e-main.el b/mu4e/mu4e-main.el index 5a1f630b..58d417d3 100644 --- a/mu4e/mu4e-main.el +++ b/mu4e/mu4e-main.el @@ -25,14 +25,20 @@ ;;; Code: (require 'smtpmail) ;; the queueing stuff (silence elint) -(require 'mu4e-utils) ;; utility functions +(require 'mu4e-helpers) ;; utility functions (require 'mu4e-context) ;; the context +(require 'mu4e-bookmarks) +(require 'mu4e-folders) +(require 'mu4e-update) +(require 'mu4e-contacts) (require 'mu4e-search) (require 'mu4e-vars) ;; mu-wide variables (require 'cl-lib) -;;; Mode + + +;; Configuration (define-obsolete-variable-alias 'mu4e-main-buffer-hide-personal-addresses @@ -49,6 +55,37 @@ part of the personal addresses.") "When set to t, do not hide bookmarks or maildirs that have no unread messages.") + +;;; Mode +(define-derived-mode mu4e-org-mode org-mode "mu4e:org" + "Major mode for mu4e documents, derived from + `org-mode'.") + +(defun mu4e-info (path) + "Show a buffer with the information (an org-file) at PATH." + (unless (file-exists-p path) + (mu4e-error "Cannot find %s" path)) + (let ((curbuf (current-buffer))) + (find-file path) + (mu4e-org-mode) + (setq buffer-read-only t) + (define-key mu4e-org-mode-map (kbd "q") + `(lambda () + (interactive) + (bury-buffer) + (switch-to-buffer ,curbuf))))) + +(defun mu4e-about () + "Show the mu4e 'about' page." + (interactive) + (mu4e-info (concat mu4e-doc-dir "/mu4e-about.org"))) + +(defun mu4e-news () + "Show the mu4e 'about' page." + (interactive) + (mu4e-info (concat mu4e-doc-dir "/NEWS.org"))) + + (defvar mu4e-main-mode-map (let ((map (make-sparse-keymap))) @@ -85,6 +122,7 @@ no unread messages.") overwrite-mode 'overwrite-mode-binary) (mu4e-context-minor-mode) (mu4e-search-minor-mode) + (mu4e-update-minor-mode) (set (make-local-variable 'revert-buffer-function) #'mu4e~main-view-real)) @@ -121,15 +159,23 @@ clicked." 'mouse-face 'highlight newstr) newstr)) + + +(defun mu4e--longest-of-maildirs-and-bookmarks () + "Return the length of longest name of bookmarks and maildirs." + (cl-loop for b in (append (mu4e-bookmarks) + (mu4e--maildirs-with-query)) + maximize (string-width (plist-get b :name)))) + (defun mu4e~main-bookmarks () ;; TODO: it's a bit uncool to hard-code the "b" shortcut... (cl-loop with bmks = (mu4e-bookmarks) - with longest = (mu4e~longest-of-maildirs-and-bookmarks) + with longest = (mu4e--longest-of-maildirs-and-bookmarks) with queries = (mu4e-last-query-results) for bm in bmks for key = (string (plist-get bm :key)) for name = (plist-get bm :name) - for query = (funcall (or mu4e-query-rewrite-function #'identity) + for query = (funcall (or mu4e-search-query-rewrite-function #'identity) (plist-get bm :query)) for qcounts = (and (stringp query) (cl-loop for q in queries @@ -162,8 +208,8 @@ clicked." (defun mu4e~main-maildirs () "Return a string of maildirs with their counts." - (cl-loop with mds = (mu4e~maildirs-with-query) - with longest = (mu4e~longest-of-maildirs-and-bookmarks) + (cl-loop with mds = (mu4e--maildirs-with-query) + with longest = (mu4e--longest-of-maildirs-and-bookmarks) with queries = (plist-get mu4e--server-props :queries) for m in mds for key = (string (plist-get m :key)) @@ -217,13 +263,15 @@ clicked." "The revert buffer function for `mu4e-main-mode'." (mu4e~main-view-real-1 'refresh)) +(declare-function mu4e--start "mu4e") + (defun mu4e~main-view-real-1 (&optional refresh) "Create `mu4e-main-buffer-name' and set it up. When REFRESH is non nil refresh infos from server." (let ((inhibit-read-only t)) ;; Maybe refresh infos from server. (if refresh - (mu4e~start 'mu4e~main-redraw-buffer) + (mu4e--start 'mu4e~main-redraw-buffer) (mu4e~main-redraw-buffer)))) (defun mu4e~main-redraw-buffer () diff --git a/mu4e/mu4e-mark.el b/mu4e/mu4e-mark.el index 7e2b6c1d..dca3e7b5 100644 --- a/mu4e/mu4e-mark.el +++ b/mu4e/mu4e-mark.el @@ -31,6 +31,7 @@ (require 'mu4e-proc) (require 'mu4e-utils) (require 'mu4e-message) +(require 'mu4e-folders) ;; keep byte-compiler happy (declare-function mu4e~headers-mark "mu4e-headers") diff --git a/mu4e/mu4e-message.el b/mu4e/mu4e-message.el index 84014801..492719cc 100644 --- a/mu4e/mu4e-message.el +++ b/mu4e/mu4e-message.el @@ -38,52 +38,14 @@ (defvar mu4e~view-message) (defvar shr-inhibit-images) - -(defcustom mu4e-html2text-command 'mu4e-shr2text - "Either a shell command or a function that converts from html to plain text. - -If it is a shell command, the command reads html from standard -input and outputs plain text on standard output. If you use the -htmltext program, it's recommended you use \"html2text -utf8 --width 72\". Alternatives are the python-based html2markdown, w3m -and on MacOS you may want to use textutil. - -It can also be a function, which takes a messsage-plist as -argument and is expected to return the textified html as output. - -For backward compatibility, it can also be a parameterless -function which is run in the context of a buffer with the html -and expected to transform this (like the `html2text' function). - -In all cases, the output is expected to be in UTF-8 encoding. - -The default is to use the shr renderer." - :type '(choice string function) - :group 'mu4e-view) - -(defcustom mu4e-view-prefer-html nil - "Whether to base the body display on the html-version. -If the e-mail message has no html-version the plain-text version -is always used." - :type 'boolean - :group 'mu4e-view) - -(defcustom mu4e-view-html-plaintext-ratio-heuristic 5 - "Ratio between the length of the html and the plain text part. -Below this ratio mu4e will consider the plain text part to be -'This messages requires html' text bodies. You can neutralize -it (always show the text version) by using -`most-positive-fixnum'." - :type 'integer - :group 'mu4e-view) - -(defvar mu4e-message-body-rewrite-functions '(mu4e-message-outlook-cleanup) - "List of functions to transform the message body text. -The functions take two parameters, MSG and TXT, which are the -message-plist and the text, which is the plain-text version, -ossibly converted from html and/or transformed by earlier rewrite -functions.") - + +(make-obsolete-variable 'mu4e-html2text-command "No longer in use" "1.7.0") +(make-obsolete-variable 'mu4e-view-prefer-html "No longer in use" "1.7.0") +(make-obsolete-variable 'mu4e-view-html-plaintext-ratio-heuristic + "No longer in use" "1.7.0") +(make-obsolete-variable 'mu4e-message-body-rewrite-functions + "No longer in use" "1.7.0") + ;;; Message fields (defsubst mu4e-message-field-raw (msg field) @@ -169,89 +131,11 @@ This is equivalent to: (mu4e-message-field (mu4e-message-at-point) FIELD)." (mu4e-message-field (mu4e-message-at-point) field)) -(defvar mu4e~message-body-html nil - "Whether the body text uses HTML.") - -(defun mu4e~message-use-html-p (msg prefer-html) - "Do we want to PREFER-HTML for MSG? -Determine whether we want -to use html or text. The decision is based on PREFER-HTML and -whether the message supports the given representation." - (let* ((txt (mu4e-message-field msg :body-txt)) - (html (mu4e-message-field msg :body-html)) - (txt-len (length txt)) - (html-len (length html)) - (txt-limit (* mu4e-view-html-plaintext-ratio-heuristic txt-len)) - (txt-limit (if (>= txt-limit 0) txt-limit most-positive-fixnum))) - (cond - ; user prefers html --> use html if there is - (prefer-html (> html-len 0)) - ;; otherwise (user prefers text) still use html if there is not enough - ;; text - ((< txt-limit html-len) t) - ;; otherwise, use text - (t nil)))) - -(defun mu4e~message-body-has-content-type-param (msg param) - "Does the MSG have a content-type parameter PARAM?" - (cdr - (assoc param (mu4e-message-field msg :body-txt-params)))) - -(defun mu4e~safe-iequal (a b) - "Is string A equal to a downcased B?" - (and b (equal (downcase b) a))) - -(defun mu4e-message-body-text (msg &optional prefer-html) - "Get the body in text form for message MSG. -This is either :body-txt, or if not available, :body-html -converted to text, using `mu4e-html2text-command' is non-nil, it -will use that. Normally, this function prefers the text part, -unless PREFER-HTML is non-nil." - (setq mu4e~message-body-html (mu4e~message-use-html-p msg prefer-html)) - (let ((body - (if mu4e~message-body-html - ;; use an htmml body - (cond - ((stringp mu4e-html2text-command) - (mu4e~html2text-shell msg mu4e-html2text-command)) - ((functionp mu4e-html2text-command) - (if (help-function-arglist mu4e-html2text-command) - (funcall mu4e-html2text-command msg) - ;; oldskool parameterless mu4e-html2text-command - (mu4e~html2text-wrapper mu4e-html2text-command msg))) - (t (mu4e-error "Invalid `mu4e-html2text-command'"))) - ;; use a text body - (or (with-temp-buffer - (insert (or (mu4e-message-field msg :body-txt) "")) - (if (mu4e~safe-iequal "flowed" - (mu4e~message-body-has-content-type-param - msg "format")) - (fill-flowed nil - (mu4e~safe-iequal - "yes" - (mu4e~message-body-has-content-type-param - msg "delsp")))) - (buffer-string)) "")))) - (dolist (func mu4e-message-body-rewrite-functions) - (setq body (funcall func msg body))) - body)) - -(defun mu4e-message-outlook-cleanup (_msg body) - "Clean-up MSG's BODY. -Esp. MS-Outlook-originating message may not advertise the correct -encoding (e.g. 'iso-8859-1' instead of 'windows-1252'), thus -giving us these funky chars. here, we either remove them, or -replace with." - (with-temp-buffer - (insert body) - (goto-char (point-min)) - (while (re-search-forward "\015 ’]" nil t) - (replace-match - (cond - ((string= (match-string 0) "’") "'") - ((string= (match-string 0) " ") " ") - (t "")))) - (buffer-string))) +(defun mu4e-message-body-text (_msg &optional _prefer-html) + "Get the body in text form for message MSG." + "" ;; not implemented for Gnus mode. +) + (defun mu4e-message-contact-field-matches (msg cfield rx) "Does MSG's contact-field CFIELD match rx? @@ -284,7 +168,8 @@ expressions, in which case any of those are tried for a match." (mu4e-message-field msg cfield)))))) (defun mu4e-message-contact-field-matches-me (msg cfield) - "Does contact-field CFIELD in MSG match me? Checks whether any + "Does contact-field CFIELD in MSG match me? +Checks whether any of the of the contacts in field CFIELD (either :to, :from, :cc or :bcc) of msg MSG matches *me*, that is, any of the addresses for which `mu4e-personal-address-p' return t. Returns the contact @@ -293,19 +178,20 @@ cell that matched, or nil." (mu4e-message-field msg cfield))) (defun mu4e-message-sent-by-me (msg) - "Is this message (to be) sent by me? + "Is this MSG (to be) sent by me? Checks if the from field matches user's personal addresses." (mu4e-message-contact-field-matches-me msg :from)) (defun mu4e-message-personal-p (msg) - "Does message have user's personal address in any of the -contact fields?" + "Does MSG have user's personal address? +In any of the contact + fields?" (cl-some (lambda (field) (mu4e-message-contact-field-matches-me msg field)) '(:from :to :cc :bcc))) -(defsubst mu4e-message-part-field (msgpart field) +(defsubst mu4e-message-part-field (msgpart field) "Get some FIELD from MSGPART. A part would look something like: (:index 2 :name \"photo.jpg\" :mime-type \"image/jpeg\" :size 147331)." @@ -322,41 +208,14 @@ symbol, see `mu4e-header-info'." (plist-get (mu4e-message-at-point) field)) ;;; Html2Text +(make-obsolete 'mu4e-shr2text "No longer in use" "1.7.0") -(defun mu4e~html2text-wrapper (func msg) - "Apply FUNC on a temporary buffer with html from MSG. -Return the buffer contents." - (with-temp-buffer - (insert (or (mu4e-message-field msg :body-html) "")) - (funcall func) - (or (buffer-string) ""))) - -(defun mu4e-shr2text (msg) - "Convert html in MSG to text using the shr engine. -This can be used in `mu4e-html2text-command' in a new enough -Emacs. Based on code by Titus von der Malsburg." - (mu4e~html2text-wrapper - (lambda () - (let ( - ;; When HTML emails contain references to remote images, - ;; retrieving these images leaks information. For example, - ;; the sender can see when I opened the email and from which - ;; computer (IP address). For this reason, it is preferable - ;; to not retrieve images. - ;; See this discussion on mu-discuss: - ;; https://groups.google.com/forum/#!topic/mu-discuss/gr1cwNNZnXo - (shr-inhibit-images t)) - (shr-render-region (point-min) (point-max)))) msg)) - -(defun mu4e~html2text-shell (msg _cmd) - "Convert html2 text in MSG using a shell function CMD." - (mu4e~html2text-wrapper - (lambda () - (let* ((tmp-file (mu4e-make-temp-file "html"))) - (write-region (point-min) (point-max) tmp-file) - (erase-buffer) - (call-process-shell-command mu4e-html2text-command tmp-file t t) - (delete-file tmp-file))) msg)) +(defun mu4e-copy-message-path () + "Copy the message-path of message at point to the kill ring." + (interactive) + (let ((path (mu4e-message-field-at-point :path))) + (kill-new path) + (mu4e-message "Saved '%s' to kill-ring" path))) ;;; _ (provide 'mu4e-message) diff --git a/mu4e/mu4e-org.el b/mu4e/mu4e-org.el index 7f20573b..7fbf7459 100644 --- a/mu4e/mu4e-org.el +++ b/mu4e/mu4e-org.el @@ -122,7 +122,7 @@ the query (for links starting with 'query:')." ((string-match "^msgid:\\(.+\\)" link) (mu4e-view-message-with-message-id (match-string 1 link))) ((string-match "^query:\\(.+\\)" link) - (mu4e-headers-search (match-string 1 link) current-prefix-arg)) + (mu4e-search (match-string 1 link) current-prefix-arg)) (t (mu4e-error "Unrecognized link type '%s'" link)))) (make-obsolete 'org-mu4e-open 'mu4e-org-open "1.3.6") diff --git a/mu4e/mu4e-proc.el b/mu4e/mu4e-proc.el index 97acb5a6..0dcd556c 100644 --- a/mu4e/mu4e-proc.el +++ b/mu4e/mu4e-proc.el @@ -25,9 +25,114 @@ ;;; Code: (require 'mu4e-helpers) -(require 'mu4e-utils) (require 'mu4e-meta) + +;;; 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." + :group 'mu4e + :type '(choice (const :tag "Default location" nil) + (directory :tag "Specify location")) + :safe 'stringp) + +(defcustom mu4e-mu-binary (executable-find "mu") + "Name of the mu-binary to use. +If it cannot be found in your PATH, you can specify the full +path." + :type 'file + :group 'mu4e + :safe 'stringp) + +(defcustom mu4e-mu-debug nil + "Whether to run the mu binary in debug-mode. +Setting this to t increases the amount of information in the log." + :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. +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 +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~proc-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~proc-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~proc-filter' for the format.") + +(defvar mu4e-header-func nil + "Function called for each message-header received. +The function is passed a msg plist as argument. See +`mu4e~proc-filter' for the format.") + +(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~proc-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~proc-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~proc-filter' for the format of .") + +(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 () +sexp received from the server process.") + +(make-obsolete-variable 'mu4e-temp-func "No longer used" "1.7.0") + ;;; Internal vars (defvar mu4e~proc-buf nil @@ -48,11 +153,10 @@ (concat mu4e~cookie-pre "\\([[:xdigit:]]+\\)" mu4e~cookie-post) "Regular expression matching the length cookie. Match 1 will be the length (in hex).") - -;;; Functions - -(defun mu4e~proc-running-p () - "Whether the mu process is running." + +(defun mu4e-running-p () + "Whether mu4e is running. +Checks whether the server process is live." (and mu4e~proc-process (memq (process-status mu4e~proc-process) '(run open listen connect stop)) @@ -205,15 +309,6 @@ The server output is as follows: (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)) @@ -310,7 +405,7 @@ backslashes and double-quotes." (defun mu4e~call-mu (form) "Call 'mu' with some command." - (unless (mu4e~proc-running-p) (mu4e~proc-start)) + (unless (mu4e-running-p) (mu4e~proc-start)) (let* ((print-length nil) (print-level nil) (cmd (format "%S" form))) (mu4e-log 'to-server "%s" cmd) @@ -482,9 +577,9 @@ the function registered as `mu4e-view-func'." :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 - :extract-images ,(if mu4e-view-show-images t nil) - :decrypt ,(and decrypt t) - :verify ,(and verify t)))) + :extract-images nil ;; XXX remove + :decrypt ,(and decrypt t) ;; XXX remove + :verify ,(and verify t)))) ;; XXX remove (defun mu4e~proc-view-path (path &optional images decrypt verify) "View message at PATH.. diff --git a/mu4e/mu4e-speedbar.el b/mu4e/mu4e-speedbar.el index 69248c3a..c2c414e9 100644 --- a/mu4e/mu4e-speedbar.el +++ b/mu4e/mu4e-speedbar.el @@ -1,6 +1,6 @@ ;;; mu4e-speedbar --- Speedbar support for mu4e -*- lexical-binding: t -*- -;; Copyright (C) 2012-2020 Antono Vasiljev, Dirk-Jan C. Binnema +;; Copyright (C) 2012-2021 Antono Vasiljev, Dirk-Jan C. Binnema ;; Author: Antono Vasiljev ;; Version: 0.1 @@ -35,7 +35,7 @@ (require 'mu4e-vars) (require 'mu4e-headers) (require 'mu4e-context) -(require 'mu4e-utils) +(require 'mu4e-bookmarks) (defvar mu4e-main-speedbar-key-map nil "Keymap used when in mu4e display mode.") @@ -91,8 +91,7 @@ (defun mu4e~speedbar-maildir (&optional _text token _ident) "Jump to maildir TOKEN. TEXT and INDENT are not used." (dframe-with-attached-buffer - (mu4e-headers-search (concat "\"maildir:" token "\"") - current-prefix-arg))) + (mu4e-search (concat "\"maildir:" token "\"") current-prefix-arg))) (defun mu4e~speedbar-render-bookmark-list () "Insert the list of bookmarks in the speedbar" @@ -110,7 +109,7 @@ (defun mu4e~speedbar-bookmark (&optional _text token _ident) "Run bookmarked query TOKEN. TEXT and INDENT are not used." (dframe-with-attached-buffer - (mu4e-headers-search token current-prefix-arg))) + (mu4e-search token current-prefix-arg))) ;;;###autoload (defun mu4e-speedbar-buttons (&optional _buffer) diff --git a/mu4e/mu4e-update.el b/mu4e/mu4e-update.el new file mode 100644 index 00000000..967f46cc --- /dev/null +++ b/mu4e/mu4e-update.el @@ -0,0 +1,320 @@ +;;; mu4e-update.el -- part of mu4e, -*- lexical-binding: t -*- + +;; Copyright (C) 2011-2020 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema +;; Maintainer: Dirk-Jan C. Binnema + +;; This file is not part of GNU Emacs. + +;; mu4e 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. + +;; mu4e 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 mu4e. If not, see . + +;;; Commentary: + +;; Updating the mu4e message: calling a mail retrieval program +;; and re-running the index. + +;;; Code: + +(require 'mu4e-helpers) +(require 'mu4e-proc) + +;;; Customization + +(defcustom mu4e-get-mail-command "true" + "Shell command to run to retrieve new mail. +Common values are \"offlineimap\", \"fetchmail\" or \"mbsync\", but +arbitrary shell-commands can be used. + +When set to the literal string \"true\" (the default), the +command simply finishes successfully (running the 'true' command) +without retrieving any mail. This can be useful when mail is +already retrieved in another way." + :type 'string + :group 'mu4e + :safe 'stringp) + +(defcustom mu4e-index-update-error-warning t + "Whether to display warnings during the retrieval process. +This depends on the `mu4e-get-mail-command' exit code." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-index-update-error-continue t + "Whether to continue with indexing after an error during retrieval." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-index-update-in-background t + "Whether to retrieve mail in the background." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-index-cleanup t + "Whether to run a cleanup phase after indexing. + +That is, validate that each message in the message store has a +corresponding message file in the filesystem. + +Having this option as t ensures that no non-existing messages are +shown but can slow with large message stores on slow file-systems." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-index-lazy-check nil + "Whether to only use a 'lazy check' during reindexing. +This influences how we decide whether a message +needs (re)indexing or not. + +When this is set to non-nil, mu only uses the directory +timestamps to decide whether it needs to check the messages +beneath it. This makes indexing much faster, but might miss some +changes. For this, you might want to occasionally call +`mu4e-update-index-nonlazy'." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-update-interval nil + "Number of seconds between mail retrieval/indexing. +If nil, don't update automatically. Note, changes in +`mu4e-update-interval' only take effect after restarting mu4e." + :type '(choice (const :tag "No automatic update" nil) + (integer :tag "Seconds")) + :group 'mu4e + :safe 'integerp) + +(defvar mu4e-update-pre-hook nil + "Hook run just *before* the mail-retrieval / database updating process starts. +You can use this hook for example to `mu4e-get-mail-command' with +some specific setting.") + +(defcustom mu4e-hide-index-messages nil + "Whether to hide the \"Indexing...\" and contacts messages." + :type 'boolean + :group 'mu4e) + + +(defvar mu4e-index-updated-hook nil + "Hook run when the indexing process had one or more updated messages. +This can be used as a simple way to invoke some action when new +messages appear, but note that an update in the index does not +necessarily mean a new message.") + + +(defvar mu4e-message-changed-hook nil + "Hook run when there is a message changed in db. +For new messages, it depends on `mu4e-index-updated-hook'. This +can be used as a simple way to invoke some action when a message +changed.") + +(make-obsolete-variable 'mu4e-msg-changed-hook + 'mu4e-message-changed-hook "0.9.19") + + + +;;; Internal variables +(defvar mu4e--progress-reporter nil + "Internal, the progress reporter object.") +(defvar mu4e--update-timer nil + "The mu4e update timer.") +(defconst mu4e--update-name " *mu4e-update*" + "Name of the process and buffer to update mail.") +(defconst mu4e--update-buffer-height 8 + "Height of the mu4e message retrieval/update buffer.") +(defvar mu4e--get-mail-ask-password "mu4e get-mail: Enter password: " + "Query string for `mu4e-get-mail-command' password.") +(defvar mu4e--get-mail-password-regexp "^Remote: Enter password: $" + "Regexp for a `mu4e-get-mail-command' password query.") + + +(defun mu4e--get-mail-process-filter (proc msg) + "Filter the MSG output of the `mu4e-get-mail-command' PROC. + +Currently the filter only checks if the command asks for a +password by matching the output against +`mu4e~get-mail-password-regexp'. The messages are inserted into +the process buffer. + +Also scrolls to the final line, and update the progress +throbber." + (when mu4e--progress-reporter + (progress-reporter-update mu4e--progress-reporter)) + + (when (string-match mu4e--get-mail-password-regexp msg) + (if (process-get proc 'x-interactive) + (process-send-string proc + (concat (read-passwd mu4e--get-mail-ask-password) + "\n")) + ;; TODO kill process? + (mu4e-error "Unrecognized password request"))) + (when (process-buffer proc) + (let ((inhibit-read-only t) + (procwin (get-buffer-window (process-buffer proc)))) + ;; Insert at end of buffer. Leave point alone. + (with-current-buffer (process-buffer proc) + (goto-char (point-max)) + (if (string-match ".*\r\\(.*\\)" msg) + (progn + ;; kill even with \r + (end-of-line) + (let ((end (point))) + (beginning-of-line) + (delete-region (point) end)) + (insert (match-string 1 msg))) + (insert msg))) + ;; Auto-scroll unless user is interacting with the window. + (when (and (window-live-p procwin) + (not (eq (selected-window) procwin))) + (with-selected-window procwin + (goto-char (point-max))))))) + + +(defun mu4e-index-message (frm &rest args) + "Display FRM with ARGS like `mu4e-message' for index messages. +However, if `mu4e-hide-index-messages' is non-nil, do not display anything." + (unless mu4e-hide-index-messages + (apply 'mu4e-message frm args))) + +(defun mu4e-update-index () + "Update the mu4e index." + (interactive) + (mu4e~proc-index mu4e-index-cleanup mu4e-index-lazy-check)) + +(defun mu4e-update-index-nonlazy () + "Update the mu4e index non-lazily. +This is just a convenience wrapper for indexing the non-lazy way +if you otherwise want to use `mu4e-index-lazy-check'." + (interactive) + (let ((mu4e-index-cleanup t) (mu4e-index-lazy-check nil)) + (mu4e-update-index))) + +(defvar mu4e--update-buffer nil + "The buffer of the update process when updating.") + +(define-derived-mode mu4e--update-mail-mode +special-mode "mu4e:update" + "Major mode used for retrieving new e-mail messages in `mu4e'.") + +(define-key mu4e--update-mail-mode-map (kbd "q") 'mu4e-kill-update-mail) + +(defun mu4e--temp-window (buf height) + "Create a temporary window with HEIGHT at the bottom BUF." + (let ((win + (split-window + (frame-root-window) + (- (window-height (frame-root-window)) height)))) + (set-window-buffer win buf) + (set-window-dedicated-p win t) + win)) + +(defun mu4e--update-sentinel-func (proc _msg) + "Sentinel function for the update process PROC." + (when mu4e--progress-reporter + (progress-reporter-done mu4e--progress-reporter) + (setq mu4e--progress-reporter nil)) + (unless mu4e-hide-index-messages + (message nil)) + (if (or (not (eq (process-status proc) 'exit)) + (/= (process-exit-status proc) 0)) + (progn + (when mu4e-index-update-error-warning + (mu4e-message "Update process returned with non-zero exit code") + (sit-for 5)) + (when mu4e-index-update-error-continue + (mu4e-update-index))) + (mu4e-update-index)) + (when (buffer-live-p mu4e--update-buffer) + (unless (eq mu4e-split-view 'single-window) + (mapc #'delete-window (get-buffer-window-list mu4e--update-buffer))) + (kill-buffer mu4e--update-buffer))) + +;; complicated function, as it: +;; - needs to check for errors +;; - (optionally) pop-up a window +;; - (optionally) check password requests +(defun mu4e--update-mail-and-index-real (run-in-background) + "Get a new mail by running `mu4e-get-mail-command'. +If +RUN-IN-BACKGROUND is non-nil (or called with prefix-argument), +run in the background; otherwise, pop up a window." + (let* ((process-connection-type t) + (proc (start-process-shell-command + "mu4e-update" mu4e--update-name + mu4e-get-mail-command)) + (buf (process-buffer proc)) + (win (or run-in-background + (mu4e--temp-window buf mu4e--update-buffer-height)))) + (setq mu4e--update-buffer buf) + (when (window-live-p win) + (with-selected-window win + ;; ;;(switch-to-buffer buf) + ;; (set-window-dedicated-p win t) + (erase-buffer) + (insert "\n") ;; FIXME -- needed so output starts + (mu4e--update-mail-mode))) + (setq mu4e--progress-reporter + (unless mu4e-hide-index-messages + (make-progress-reporter + (mu4e-format "Retrieving mail...")))) + (set-process-sentinel proc 'mu4e--update-sentinel-func) + ;; if we're running in the foreground, handle password requests + (unless run-in-background + (process-put proc 'x-interactive (not run-in-background)) + (set-process-filter proc 'mu4e--get-mail-process-filter)))) + +(defun mu4e-update-mail-and-index (run-in-background) + "Get a new mail by running `mu4e-get-mail-command'. +If RUN-IN-BACKGROUND is non-nil (or called with prefix-argument), +run in the background; otherwise, pop up a window." + (interactive "P") + (unless mu4e-get-mail-command + (mu4e-error "`mu4e-get-mail-command' is not defined")) + (if (and (buffer-live-p mu4e--update-buffer) + (process-live-p (get-buffer-process mu4e--update-buffer))) + (mu4e-message "Update process is already running") + (progn + (run-hooks 'mu4e-update-pre-hook) + (mu4e--update-mail-and-index-real run-in-background)))) + +(defun mu4e-kill-update-mail () + "Stop the update process by killing it." + (interactive) + (let* ((proc (and (buffer-live-p mu4e--update-buffer) + (get-buffer-process mu4e--update-buffer)))) + (when (process-live-p proc) + (kill-process proc t)))) + +(define-obsolete-function-alias 'mu4e-interrupt-update-mail + 'mu4e-kill-update-mail "1.0-alpha0") + +(define-minor-mode mu4e-update-minor-mode + "Mode for triggering mu4e updates." + :global nil + :init-value nil ;; disabled by default + :group 'mu4e + :lighter "" + :keymap + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-S-u") #'mu4e-update-mail-and-index) + ;; for terminal users + (define-key map (kbd "C-c C-u") #'mu4e-update-mail-and-index) + map)) + +(provide 'mu4e-update) +;;; mu4e-update.el ends here diff --git a/mu4e/mu4e-utils.el b/mu4e/mu4e-utils.el deleted file mode 100644 index 0e6a83fc..00000000 --- a/mu4e/mu4e-utils.el +++ /dev/null @@ -1,1060 +0,0 @@ -;;; mu4e-utils.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*- - -;; Copyright (C) 2011-2020 Dirk-Jan C. Binnema - -;; Author: Dirk-Jan C. Binnema -;; Maintainer: Dirk-Jan C. Binnema - -;; This file is not part of GNU Emacs. - -;; mu4e 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. - -;; mu4e 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 mu4e. If not, see . - -;;; Commentary: - -;; Utility functions used in the mu4e - -;;; Code: - -(require 'org) -(require 'cl-lib) -(require 'cl-seq nil 'noerror) -(require 'mu4e-vars) -(require 'mu4e-message) -(require 'mu4e-meta) -(require 'mu4e-helpers) -(require 'mu4e-lists) -(require 'mu4e-context) -(require 'doc-view) - -;; keep the byte-compiler happy -(declare-function mu4e~proc-mkdir "mu4e-proc") -(declare-function mu4e~proc-ping "mu4e-proc") -(declare-function mu4e~proc-contacts "mu4e-proc") -(declare-function mu4e~proc-kill "mu4e-proc") -(declare-function mu4e~proc-index "mu4e-proc") -(declare-function mu4e~proc-add "mu4e-proc") -(declare-function mu4e~proc-mkdir "mu4e-proc") -(declare-function mu4e~proc-running-p "mu4e-proc") - -(declare-function mu4e-message-field-at-point "mu4e-proc") -(declare-function mu4e~proc-running-p "mu4e-proc") - -(declare-function mu4e~main-view "mu4e-main") -(declare-function show-all "org") - - -(defun mu4e-index-message (frm &rest args) - "Display FRM with ARGS like `mu4e-message' for index messages. -index-messages. Doesn't display anything if -`mu4e-hide-index-messages' is non-nil. " - (unless mu4e-hide-index-messages - (apply 'mu4e-message frm args))) - - -;;; Various - -(defun mu4e-copy-message-path () - "Copy the message-path of message at point to the kill-ring." - (interactive) - (let ((path (mu4e-message-field-at-point :path))) - (kill-new path) - (mu4e-message "Saved '%s' to kill-ring" path))) - -(defun mu4e-personal-address-p (addr) - "Is ADDR a personal address? -Evaluate to nil if ADDR matches any of the personal addresses. -Uses (mu4e-personal-addresses) for the addresses with both the plain -addresses and /regular expressions/." - (when addr - (seq-find - (lambda (m) - (if (string-match "/\\(.*\\)/" m) - (let ((rx (match-string 1 m)) - (case-fold-search t)) - (if (string-match rx addr) t nil)) - (eq t (compare-strings addr nil nil m nil nil 'case-insensitive)))) - (mu4e-personal-addresses)))) - -(define-obsolete-function-alias 'mu4e-user-mail-address-p - 'mu4e-personal-address-p "1.5.5") - -(defmacro with~mu4e-context-vars (context &rest body) - "Evaluate BODY, with variables let-bound for CONTEXT (if any). -`funcall'." - (declare (indent 2)) - `(let* ((vars (and ,context (mu4e-context-vars ,context)))) - (cl-progv ;; XXX: perhaps use eval's lexical environment instead of progv? - (mapcar (lambda(cell) (car cell)) vars) - (mapcar (lambda(cell) (cdr cell)) vars) - (eval ,@body)))) - -;;; Folders (1/2) - -;; the standard folders can be functions too -(defun mu4e~get-folder (foldervar msg) - "Within the mu-context of MSG, get message folder FOLDERVAR. -If FOLDER is a string, return it, if it is a function, evaluate -this function with MSG as parameter (which may be `nil'), and -return the result." - (unless (member foldervar - '(mu4e-sent-folder mu4e-drafts-folder - mu4e-trash-folder mu4e-refile-folder)) - (mu4e-error "Folder must be one of mu4e-(sent|drafts|trash|refile)-folder")) - ;; get the value with the vars for the relevants context let-bound - (with~mu4e-context-vars (mu4e-context-determine msg nil) - (let* ((folder (symbol-value foldervar)) - (val - (cond - ((stringp folder) folder) - ((functionp folder) (funcall folder msg)) - (t (mu4e-error "unsupported type for %S" folder))))) - (or val (mu4e-error "%S evaluates to nil" foldervar))))) - -(defun mu4e-get-drafts-folder (&optional msg) - "Get the sent folder. See `mu4e-drafts-folder'." - (mu4e~get-folder 'mu4e-drafts-folder msg)) - -(defun mu4e-get-refile-folder (&optional msg) - "Get the folder for refiling. See `mu4e-refile-folder'." - (mu4e~get-folder 'mu4e-refile-folder msg)) - -(defun mu4e-get-sent-folder (&optional msg) - "Get the sent folder. See `mu4e-sent-folder'." - (mu4e~get-folder 'mu4e-sent-folder msg)) - -(defun mu4e-get-trash-folder (&optional msg) - "Get the sent folder. See `mu4e-trash-folder'." - (mu4e~get-folder 'mu4e-trash-folder msg)) - -;;; Self-destructing files - -(defun mu4e-remove-file-later (filename) - "Remove FILENAME in a few seconds." - (run-at-time "30 sec" nil - (lambda () (ignore-errors (delete-file filename))))) - -(defun mu4e-make-temp-file (ext) - "Create a temporary file with extension EXT. The file will -self-destruct in a few seconds, enough to open it in another -program." - (let ((tmpfile (make-temp-file "mu4e-" nil (concat "." ext)))) - (mu4e-remove-file-later tmpfile) - tmpfile)) - -;;; Folders (2/2) -;; -;; mu4e-attachment-dir is either a string or a function that takes a -;; filename and the mime-type as argument, either (or both) which can -;; be nil - -(defun mu4e~get-attachment-dir (&optional fname mimetype) - "Get the directory for saving attachments from -`mu4e-attachment-dir' (which can be either a string or a function, -see its docstring)." - (let - ((dir - (cond - ((stringp mu4e-attachment-dir) - mu4e-attachment-dir) - ((functionp mu4e-attachment-dir) - (funcall mu4e-attachment-dir fname mimetype)) - (t - (mu4e-error "unsupported type for mu4e-attachment-dir" ))))) - (if dir - (expand-file-name dir) - (mu4e-error "mu4e-attachment-dir evaluates to nil")))) - - - -;;; Maildirs -(defun mu4e~guess-maildir (path) - "Guess the maildir for some path, or nil if cannot find it." - (let ((idx (string-match (mu4e-root-maildir) path))) - (when (and idx (zerop idx)) - (replace-regexp-in-string - (mu4e-root-maildir) - "" - (expand-file-name - (concat path "/../..")))))) - -(defun mu4e-create-maildir-maybe (dir) - "Offer to create maildir DIR if it does not exist yet. -Return t if the dir already existed, or an attempt has been made to -create it -- we cannot be sure creation succeeded here, since this -is done asynchronously. Otherwise, return nil. NOte, DIR has to be -an absolute path." - (if (and (file-exists-p dir) (not (file-directory-p dir))) - (mu4e-error "%s exists, but is not a directory." dir)) - (cond - ((file-directory-p dir) t) - ((yes-or-no-p (mu4e-format "%s does not exist yet. Create now?" dir)) - (mu4e~proc-mkdir dir) t) - (t nil))) - -(defun mu4e~get-maildirs-1 (path mdir) - "Get maildirs under path, recursively, as a list of relative paths." - (let ((dirs) - (dentries - (ignore-errors - (directory-files-and-attributes - (concat path mdir) nil - "^[^.]\\|\\.[^.][^.]" t)))) - (dolist (dentry dentries) - (when (and (booleanp (cadr dentry)) (cadr dentry)) - (if (file-accessible-directory-p - (concat (mu4e-root-maildir) "/" mdir "/" (car dentry) "/cur")) - (setq dirs (cons (concat mdir (car dentry)) dirs))) - (unless (member (car dentry) '("cur" "new" "tmp")) - (setq dirs (append dirs (mu4e~get-maildirs-1 path - (concat mdir (car dentry) "/"))))))) - dirs)) - -(defvar mu4e-cache-maildir-list nil - "Whether to cache the list of maildirs; set it to t if you find -that generating the list on the fly is too slow. If you do, you -can set `(mu4e-root-maildir)-list' to nil to force regenerating the -cache the next time `mu4e-get-maildirs' gets called.") - -(defvar mu4e-maildir-list nil - "Cached list of maildirs.") - -(defun mu4e-get-maildirs () - "Get maildirs under `mu4e-maildir', recursively, as a list of -relative paths (ie., /archive, /sent etc.). Most of the work is -done in `mu4e~get-maildirs-1'. Note, these results are /cached/ -if `mu4e-cache-maildir-list' is customized to non-nil. In that case, -the list of maildirs will not change until you restart mu4e." - (unless (and mu4e-maildir-list mu4e-cache-maildir-list) - (setq mu4e-maildir-list - (sort - (append - (when (file-accessible-directory-p - (concat (mu4e-root-maildir) "/cur")) '("/")) - (mu4e~get-maildirs-1 (mu4e-root-maildir) "/")) - (lambda (s1 s2) (string< (downcase s1) (downcase s2)))))) - mu4e-maildir-list) - -(defun mu4e-ask-maildir (prompt) - "Ask the user for a shortcut (using PROMPT) as per -(mu4e-maildir-shortcuts), then return the corresponding folder -name. If the special shortcut 'o' (for _o_ther) is used, or if -`(mu4e-maildir-shortcuts)' evaluates to nil, let user choose from -all maildirs under `mu4e-maildir'." - (let ((prompt (mu4e-format "%s" prompt))) - (if (not (mu4e-maildir-shortcuts)) - (substring-no-properties - (funcall mu4e-completing-read-function prompt (mu4e-get-maildirs))) - (let* ((mlist (append (mu4e-maildir-shortcuts) - '((:maildir "ther" :key ?o)))) - (fnames - (mapconcat - (lambda (item) - (concat - "[" - (propertize (make-string 1 (plist-get item :key)) - 'face 'mu4e-highlight-face) - "]" - (plist-get item :maildir))) - mlist ", ")) - (kar (read-char (concat prompt fnames)))) - (if (member kar '(?/ ?o)) ;; user chose 'other'? - (substring-no-properties - (funcall mu4e-completing-read-function prompt - (mu4e-get-maildirs) nil nil "/")) - (or (plist-get - (cl-find-if (lambda (item) (= kar (plist-get item :key))) - (mu4e-maildir-shortcuts)) :maildir) - (mu4e-warn "Unknown shortcut '%c'" kar))))))) - -(defun mu4e-ask-maildir-check-exists (prompt) - "Like `mu4e-ask-maildir', but check for existence of the maildir, -and offer to create it if it does not exist yet." - (let* ((mdir (mu4e-ask-maildir prompt)) - (fullpath (concat (mu4e-root-maildir) mdir))) - (unless (file-directory-p fullpath) - (and (yes-or-no-p - (mu4e-format "%s does not exist. Create now?" fullpath)) - (mu4e~proc-mkdir fullpath))) - mdir)) - - -;;; Bookmarks - (defun mu4e-ask-bookmark (prompt) - "Ask the user for a bookmark (using PROMPT) as defined in -`mu4e-bookmarks', then return the corresponding query." - (unless (mu4e-bookmarks) (mu4e-error "No bookmarks defined")) - (let* ((prompt (mu4e-format "%s" prompt)) - (bmarks - (mapconcat - (lambda (bm) - (concat - "[" (propertize (make-string 1 (plist-get bm :key)) - 'face 'mu4e-highlight-face) - "]" - (plist-get bm :name))) (mu4e-bookmarks) ", ")) - (kar (read-char (concat prompt bmarks)))) - (mu4e-get-bookmark-query kar))) - -(defun mu4e-get-bookmark-query (kar) - "Get the corresponding bookmarked query for shortcut character -KAR, or raise an error if none is found." - (let* ((chosen-bm - (or (cl-find-if - (lambda (bm) - (= kar (plist-get bm :key))) - (mu4e-bookmarks)) - (mu4e-warn "Unknown shortcut '%c'" kar))) - (expr (plist-get chosen-bm :query)) - (expr (if (not (functionp expr)) expr - (funcall expr))) - (query (eval expr))) - (if (stringp query) - query - (mu4e-warn "Expression must evaluate to query string ('%S')" expr)))) - - -(defun mu4e-bookmark-define (query name key) - "Define a bookmark for QUERY with name NAME and -shortcut-character KEY in the list of `mu4e-bookmarks'. This -replaces any existing bookmark with KEY." - (setq mu4e-bookmarks - (cl-remove-if - (lambda (bm) - (= (plist-get bm :key) key)) - (mu4e-bookmarks))) - (cl-pushnew `(:name ,name - :query ,query - :key ,key) - mu4e-bookmarks :test 'equal)) - -(defun mu4e-display-manual () - "Display the mu4e manual page for the current mode. -Or go to the top level if there is none." - (interactive) - (info (cl-case major-mode - ('mu4e-main-mode "(mu4e)Main view") - ('mu4e-headers-mode "(mu4e)Headers view") - ('mu4e-view-mode "(mu4e)Message view") - (t "mu4e")))) - -;;; Misc - -(defun mu4e-last-query () - "Get the most recent query or nil if there is none." - (when (buffer-live-p (mu4e-get-headers-buffer)) - (with-current-buffer (mu4e-get-headers-buffer) - mu4e~headers-last-query))) - -(defvar gnus-article-buffer) ;; Fix byte-compiler warning. -(defun mu4e-get-view-buffer () - "Get the view buffer, if any." - (get-buffer - (if mu4e-view-use-old - mu4e~view-buffer-name - gnus-article-buffer))) - -(defun mu4e-get-headers-buffer () - (get-buffer mu4e~headers-buffer-name)) - -(defun mu4e-select-other-view () - "When the headers view is selected, select the message view (if -that has a live window), and vice versa." - (interactive) - (let* ((other-buf - (cond - ((eq major-mode 'mu4e-headers-mode) - (mu4e-get-view-buffer)) - ((eq major-mode 'mu4e-view-mode) - (mu4e-get-headers-buffer)))) - (other-win (and other-buf (get-buffer-window other-buf)))) - (if (window-live-p other-win) - (select-window other-win) - (mu4e-message "No window to switch to")))) - - -(defconst mu4e-output-buffer-name "*mu4e-output*" - "*internal* Name of the mu4e output buffer.") - -(defun mu4e-process-file-through-pipe (path pipecmd) - "Process file at PATH through a pipe with PIPECMD." - (let ((buf (get-buffer-create mu4e-output-buffer-name))) - (with-current-buffer buf - (let ((inhibit-read-only t)) - (erase-buffer) - (call-process-shell-command pipecmd path t t) - (view-mode))) - (switch-to-buffer buf))) - -(defvar mu4e~lists-hash nil - "Hashtable of mailing-list-id => shortname, based on - `mu4e~mailing-lists' and `mu4e-user-mailing-lists'.") - -(defun mu4e-get-mailing-list-shortname (list-id) - "Get the shortname for a mailing-list with list-id LIST-ID. based -on `mu4e~mailing-lists', `mu4e-user-mailing-lists', and -`mu4e-mailing-list-patterns'." - (unless mu4e~lists-hash - (setq mu4e~lists-hash (make-hash-table :test 'equal)) - (dolist (cell mu4e~mailing-lists) - (puthash (car cell) (cdr cell) mu4e~lists-hash)) - (dolist (cell mu4e-user-mailing-lists) - (puthash (car cell) (cdr cell) mu4e~lists-hash))) - (or - (gethash list-id mu4e~lists-hash) - (and (boundp 'mu4e-mailing-list-patterns) - (cl-member-if - (lambda (pattern) - (string-match pattern list-id)) - mu4e-mailing-list-patterns) - (match-string 1 list-id)) - ;; if it's not in the db, take the part until the first dot if there is one; - ;; otherwise just return the whole thing - (if (string-match "\\([^.]*\\)\\." list-id) - (match-string 1 list-id) - list-id))) - -(defvar mu4e-index-updated-hook nil - "Hook run when the indexing process had one or more updated messages. -This can be used as a simple way to invoke some action when new -messages appear, but note that an update in the index does not -necessarily mean a new message.") - -(defvar mu4e-message-changed-hook nil - "Hook run when there is a message changed in db. For new -messages, it depends on `mu4e-index-updated-hook'. This can be -used as a simple way to invoke some action when a message -changed.") - -(make-obsolete-variable 'mu4e-msg-changed-hook - 'mu4e-message-changed-hook "0.9.19") - -(defvar mu4e~contacts-tstamp "0" - "Timestamp for the most recent contacts update." ) - -;;; Some handler functions for server messages - -(defun mu4e-info-handler (info) - "Handler function for (:info ...) sexps received from the server -process." - (let* ((type (plist-get info :info)) - (processed (plist-get info :processed)) - (updated (plist-get info :updated)) - (cleaned-up (plist-get info :cleaned-up)) - (mainbuf (get-buffer mu4e-main-buffer-name))) - (cond - ((eq type 'add) t) ;; do nothing - ((eq type 'index) - (if (eq (plist-get info :status) 'running) - (mu4e-index-message - "Indexing... processed %d, updated %d" processed updated) - (progn - (mu4e-index-message - "%s completed; processed %d, updated %d, cleaned-up %d" - (if mu4e-index-lazy-check "Lazy indexing" "Indexing") - processed updated cleaned-up) - ;; call the updated hook if anything changed. - (unless (zerop (+ updated cleaned-up)) - (run-hooks 'mu4e-index-updated-hook)) - (unless (and (not (string= mu4e~contacts-tstamp "0")) - (zerop (plist-get info :updated))) - (mu4e~request-contacts-maybe)) - (when (and (buffer-live-p mainbuf) (get-buffer-window mainbuf)) - (save-window-excursion - (select-window (get-buffer-window mainbuf)) - (mu4e~main-view 'refresh)))))) - ((plist-get info :message) - (mu4e-index-message "%s" (plist-get info :message)))))) - -(defun mu4e-error-handler (errcode errmsg) - "Handler function for showing an error." - ;; don't use mu4e-error here; it's running in the process filter context - (cl-case errcode - (4 (user-error "No matches for this search query.")) - (110 (display-warning 'mu4e errmsg :error)) ;; schema version. - (t (error "Error %d: %s" errcode errmsg)))) - - -;;; Contacts - -(defun mu4e~update-contacts (contacts &optional tstamp) - "Receive a sorted list of CONTACTS. -Each of the contacts has the form - (FULL_EMAIL_ADDRESS . RANK) and fill `mu4e~contacts-hash' with -it, with each contact mapped to an integer for their ranking. - -This is used by the completion function in mu4e-compose." - ;; We have our nicely sorted list, map them to a list - ;; of increasing integers. We use that map in the composer - ;; to sort them there. It would have been so much easier if emacs - ;; allowed us to use the sorted-list as-is, but no such luck. - (let ((n 0)) - (unless mu4e~contacts-hash - (setq mu4e~contacts-hash (make-hash-table :test 'equal :weakness nil - :size (length contacts)))) - (dolist (contact contacts) - (cl-incf n) - (let* ((address (plist-get contact :address)) - (address - (if (functionp mu4e-contact-process-function) - (funcall mu4e-contact-process-function address) - address))) - (when address ;; note the explicit deccode; the strings we get are utf-8, - ;; but emacs doesn't know yet. - (puthash (decode-coding-string address 'utf-8) - (plist-get contact :rank) mu4e~contacts-hash)))) - - (setq mu4e~contacts-tstamp (or tstamp "0")) - - (unless (zerop n) - (mu4e-index-message "Contacts updated: %d; total %d" - n (hash-table-count mu4e~contacts-hash))))) - -(defun mu4e-contacts-info () - "Display information about the cache used for contacts -completion; for testing/debugging." - (interactive) - (with-current-buffer (get-buffer-create "*mu4e-contacts-info*") - (erase-buffer) - (insert (format "complete addresses: %s\n" - (if mu4e-compose-complete-addresses "yes" "no"))) - (insert (format "only personal addresses: %s\n" - (if mu4e-compose-complete-only-personal "yes" "no"))) - (insert (format "only addresses seen after: %s\n" - (or mu4e-compose-complete-only-after "no restrictions"))) - - (when mu4e~contacts-hash - (insert (format "number of contacts cached: %d\n\n" - (hash-table-count mu4e~contacts-hash))) - (let ((contacts)) - (maphash (lambda (addr rank) - (setq contacts (cons (cons rank addr) contacts))) - mu4e~contacts-hash) - (setq contacts (sort contacts - (lambda(cell1 cell2) (< (car cell1) (car cell2))))) - (dolist (contact contacts) - (insert (format "%s\n" (cdr contact)))))) - - (pop-to-buffer "*mu4e-contacts-info*"))) - -(defun mu4e~check-requirements () - "Check for the settings required for running mu4e." - (unless (>= emacs-major-version 25) - (mu4e-error "Emacs >= 25.x is required for mu4e")) - (when mu4e--server-props - (unless (string= (mu4e-server-version) mu4e-mu-version) - (mu4e-error "mu server has version %s, but we need %s" - (mu4e-server-version) mu4e-mu-version))) - (unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary)) - (mu4e-error "Please set `mu4e-mu-binary' to the full path to the mu - binary.")) - (dolist (var '(mu4e-sent-folder mu4e-drafts-folder - mu4e-trash-folder)) - (unless (and (boundp var) (symbol-value var)) - (mu4e-error "Please set %S" var)) - (unless (functionp (symbol-value var)) ;; functions are okay, too - (let* ((dir (symbol-value var)) - (path (concat (mu4e-root-maildir) dir))) - (unless (string= (substring dir 0 1) "/") - (mu4e-error "%S must start with a '/'" dir)) - (unless (mu4e-create-maildir-maybe path) - (mu4e-error "%s (%S) does not exist" path var)))))) - -(defun mu4e-running-p () - "Whether mu4e is running. -Checks whether the server process is live." - (mu4e~proc-running-p)) - -;;; Starting / getting mail / updating the index - -(defvar mu4e~update-timer nil - "The mu4e update timer.") -(defconst mu4e~update-name " *mu4e-update*" - "Name of the process and buffer to update mail.") -(defconst mu4e~update-buffer-height 8 - "Height of the mu4e message retrieval/update buffer.") - -(defvar mu4e~get-mail-ask-password "mu4e get-mail: Enter password: " - "Query string for `mu4e-get-mail-command' password.") -(defvar mu4e~get-mail-password-regexp "^Remote: Enter password: $" - "Regexp to match a password query in the `mu4e-get-mail-command' output.") - -(defun mu4e~request-contacts-maybe () - "If `mu4e-compose-complete-addresses' is non-nil, get/update -the list of contacts we use for autocompletion; otherwise, do -nothing." - (when mu4e-compose-complete-addresses - (setq mu4e-contacts-func 'mu4e~update-contacts) - (mu4e~proc-contacts - mu4e-compose-complete-only-personal - mu4e-compose-complete-only-after - mu4e~contacts-tstamp))) - -(defun mu4e~pong-handler (data func) - "Handle 'pong' responses from the mu server." - (let ((doccount (plist-get mu4e--server-props :doccount))) - (mu4e~check-requirements) - (when func (funcall func)) - (when (zerop doccount) - (mu4e-message "Store is empty; (re)indexing. This may take a while.") ; - (mu4e-update-index)) - (when (and mu4e-update-interval (null mu4e~update-timer)) - (setq mu4e~update-timer - (run-at-time 0 mu4e-update-interval - (lambda () (mu4e-update-mail-and-index - mu4e-index-update-in-background))))))) - -(defun mu4e~start (&optional func) - "If `mu4e-contexts' have been defined, but we don't have a -context yet, switch to the matching one, or none matches, the -first. If mu4e is already running, execute function FUNC (if -non-nil). Otherwise, check various requireme`'nts, then start mu4e. -When successful, call FUNC (if non-nil) afterwards." - (unless (mu4e-context-current) - (mu4e--context-autoswitch nil mu4e-context-policy)) - (setq mu4e-pong-func (lambda (info) (mu4e~pong-handler info func))) - (mu4e~proc-ping - (mapcar ;; send it a list of queries we'd like to see read/unread info for - (lambda (bm) - (funcall (or mu4e-query-rewrite-function #'identity) - (plist-get bm :query))) - ;; exclude bookmarks that are not strings, and with certain flags - (seq-filter (lambda (bm) - (and (stringp (plist-get bm :query)) - (not (or (plist-get bm :hide) (plist-get bm :hide-unread))))) - (append (mu4e-bookmarks) - (mu4e~maildirs-with-query))))) - ;; maybe request the list of contacts, automatically refreshed after - ;; reindexing - (unless mu4e~contacts-hash (mu4e~request-contacts-maybe))) - -(defun mu4e-clear-caches () - "Clear any cached resources." - (setq - mu4e-maildir-list nil - mu4e~contacts-hash nil - mu4e~contacts-tstamp "0")) - -(defun mu4e~stop () - "Stop the mu4e se ssion." - (when mu4e~update-timer - (cancel-timer mu4e~update-timer) - (setq mu4e~update-timer nil)) - (mu4e-clear-caches) - (mu4e~proc-kill) - ;; kill all mu4e buffers - (mapc - (lambda (buf) - ;; When using the Gnus-based viewer, the view buffer has the - ;; kill-buffer-hook function mu4e~view-kill-buffer-hook-fn which kills the - ;; mm-* buffers created by Gnus' article mode. Those have been returned by - ;; `buffer-list' but might already be deleted in case the view buffer has - ;; been killed first. So we need a `buffer-live-p' check here. - (when (buffer-live-p buf) - (with-current-buffer buf - (when (member major-mode - '(mu4e-headers-mode mu4e-view-mode mu4e-main-mode)) - (kill-buffer))))) - (buffer-list))) - -(defun mu4e~maildirs-with-query () - "Return a copy of `mu4e-maildirs-shortcuts' with :query populated. - -This is meant to be the exact same data structure as -`mu4e-bookmarks'." - (cl-mapcar - (lambda (m) - (append - ;; we want to change the :maildir key to :name, and add a :query key - (list :name (plist-get m :maildir) - :query (format "maildir:\"%s\"" (plist-get m :maildir))) - ;; next we want to append any other keys to our previous list (e.g. :hide, - ;; :key, etc) but skipping :maildir (since it's renamed to :name) - (cl-loop for (key value) on m by 'cddr - when (not (equal key :maildir)) - append (list key value)))) - (mu4e-maildir-shortcuts))) - -(defun mu4e~longest-of-maildirs-and-bookmarks () - "Return the length of longest name of bookmarks and maildirs." - (cl-loop for b in (append (mu4e-bookmarks) - (mu4e~maildirs-with-query)) - maximize (string-width (plist-get b :name)))) - - - - -;;; Indexing & Updating - -(defvar mu4e~progress-reporter nil - "Internal, the progress reporter object.") - -(defun mu4e~get-mail-process-filter (proc msg) - "Filter the output of `mu4e-get-mail-command'. -Currently the filter only checks if the command asks for a password -by matching the output against `mu4e~get-mail-password-regexp'. -The messages are inserted into the process buffer. - -Also scrolls to the final line, and update the progress throbber." - (when mu4e~progress-reporter - (progress-reporter-update mu4e~progress-reporter)) - - (when (string-match mu4e~get-mail-password-regexp msg) - (if (process-get proc 'x-interactive) - (process-send-string proc - (concat (read-passwd mu4e~get-mail-ask-password) - "\n")) - ;; TODO kill process? - (mu4e-error "Unrecognized password request"))) - (when (process-buffer proc) - (let ((inhibit-read-only t) - (procwin (get-buffer-window (process-buffer proc)))) - ;; Insert at end of buffer. Leave point alone. - (with-current-buffer (process-buffer proc) - (goto-char (point-max)) - (if (string-match ".*\r\\(.*\\)" msg) - (progn - ;; kill even with \r - (end-of-line) - (let ((end (point))) - (beginning-of-line) - (delete-region (point) end)) - (insert (match-string 1 msg))) - (insert msg))) - ;; Auto-scroll unless user is interacting with the window. - (when (and (window-live-p procwin) - (not (eq (selected-window) procwin))) - (with-selected-window procwin - (goto-char (point-max))))))) - -(defun mu4e-update-index () - "Update the mu4e index." - (interactive) - (mu4e~proc-index mu4e-index-cleanup mu4e-index-lazy-check)) - -(defun mu4e-update-index-nonlazy () - "Update the mu4e index non-lazily. -This is just a convenience wrapper for indexing the non-lazy way -if you otherwise want to use `mu4e-index-lazy-check'." - (interactive) - (let ((mu4e-index-cleanup t) (mu4e-index-lazy-check nil)) - (mu4e-update-index))) - -(defvar mu4e~update-buffer nil - "Internal, store the buffer of the update process when - updating.") - -(define-derived-mode mu4e~update-mail-mode special-mode "mu4e:update" - "Major mode used for retrieving new e-mail messages in `mu4e'.") - -(define-key mu4e~update-mail-mode-map (kbd "q") 'mu4e-kill-update-mail) - -(defun mu4e~temp-window (buf height) - "Create a temporary window with HEIGHT at the bottom of the -frame to display buffer BUF." - (let ((win - (split-window - (frame-root-window) - (- (window-height (frame-root-window)) height)))) - (set-window-buffer win buf) - (set-window-dedicated-p win t) - win)) - -(defun mu4e~update-sentinel-func (proc _msg) - "Sentinel function for the update process." - (when mu4e~progress-reporter - (progress-reporter-done mu4e~progress-reporter) - (setq mu4e~progress-reporter nil)) - (unless mu4e-hide-index-messages - (message nil)) - (if (or (not (eq (process-status proc) 'exit)) - (/= (process-exit-status proc) 0)) - (progn - (when mu4e-index-update-error-warning - (mu4e-message "Update process returned with non-zero exit code") - (sit-for 5)) - (when mu4e-index-update-error-continue - (mu4e-update-index))) - (mu4e-update-index)) - (when (buffer-live-p mu4e~update-buffer) - (unless (eq mu4e-split-view 'single-window) - (mapc #'delete-window (get-buffer-window-list mu4e~update-buffer))) - (kill-buffer mu4e~update-buffer))) - -;; complicated function, as it: -;; - needs to check for errors -;; - (optionally) pop-up a window -;; - (optionally) check password requests -(defun mu4e~update-mail-and-index-real (run-in-background) - "Get a new mail by running `mu4e-get-mail-command'. If -RUN-IN-BACKGROUND is non-nil (or called with prefix-argument), -run in the background; otherwise, pop up a window." - (let* ((process-connection-type t) - (proc (start-process-shell-command - "mu4e-update" mu4e~update-name - mu4e-get-mail-command)) - (buf (process-buffer proc)) - (win (or run-in-background - (mu4e~temp-window buf mu4e~update-buffer-height)))) - (setq mu4e~update-buffer buf) - (when (window-live-p win) - (with-selected-window win - ;; ;;(switch-to-buffer buf) - ;; (set-window-dedicated-p win t) - (erase-buffer) - (insert "\n") ;; FIXME -- needed so output starts - (mu4e~update-mail-mode))) - (setq mu4e~progress-reporter - (unless mu4e-hide-index-messages - (make-progress-reporter - (mu4e-format "Retrieving mail...")))) - (set-process-sentinel proc 'mu4e~update-sentinel-func) - ;; if we're running in the foreground, handle password requests - (unless run-in-background - (process-put proc 'x-interactive (not run-in-background)) - (set-process-filter proc 'mu4e~get-mail-process-filter)))) - -(defun mu4e-update-mail-and-index (run-in-background) - "Get a new mail by running `mu4e-get-mail-command'. If -run-in-background is non-nil (or called with prefix-argument), run -in the background; otherwise, pop up a window." - (interactive "P") - (unless mu4e-get-mail-command - (mu4e-error "`mu4e-get-mail-command' is not defined")) - (if (and (buffer-live-p mu4e~update-buffer) - (process-live-p (get-buffer-process mu4e~update-buffer))) - (mu4e-message "Update process is already running") - (progn - (run-hooks 'mu4e-update-pre-hook) - (mu4e~update-mail-and-index-real run-in-background)))) - -(defun mu4e-kill-update-mail () - "Stop the update process by killing it." - (interactive) - (let* ((proc (and (buffer-live-p mu4e~update-buffer) - (get-buffer-process mu4e~update-buffer)))) - (when (process-live-p proc) - (kill-process proc t)))) - -(define-obsolete-function-alias 'mu4e-interrupt-update-mail - 'mu4e-kill-update-mail "1.0-alpha0") - - -;;; Misc 2 - -(defvar mu4e-imagemagick-identify "identify" - "Name/path of the Imagemagick 'identify' program.") - -(defun mu4e~image-width-scale (width height max_width max_height) - "Returns a width to use for proportional image scaling -to satisfy both MAX_WIDTH and MAX_HEIGHT restrictions." - (floor - (if (<= width max_width) - (if (<= height max_height) - width ; both width and height ok, just return width - (* (/ max_height (float height)) width)) ; height is too large, scale width by hmax/h - (if (<= height max_height) - max_width ; width is too large, return max_width as scaling - (let ((width_heightscale (* (/ max_height (float height)) width))) - (min max_width width_heightscale)))))) ; both too large, return smallest width - -(defun mu4e-display-image (imgpath &optional maxwidth maxheight) - "Display image IMG at point; optionally specify MAXWIDTH and -MAXHEIGHT. Function tries to use imagemagick if available (ie., -emacs was compiled with imagemagick support); otherwise MAXWIDTH -and MAXHEIGHT are ignored." - (let* ((have-im (and (fboundp 'imagemagick-types) - (imagemagick-types))) ;; hmm, should check for specific type - (identify (and have-im maxwidth - (executable-find mu4e-imagemagick-identify))) - (props (and identify (mapcar 'string-to-number - (split-string (shell-command-to-string - (format "%s -format '%%w %%h' %s" - identify (shell-quote-argument imgpath))))))) - (width (and props (car props))) - (height (and props (car (cdr props)))) - - (img (if have-im - (create-image imgpath 'imagemagick nil - :width (mu4e~image-width-scale width height maxwidth maxheight)) - (create-image imgpath)))) - (when img - (save-excursion - (insert "\n") - (let ((size (image-size img))) ;; inspired by gnus.. - (insert-char ?\n - (max 0 (round (- (window-height) (or maxheight (cdr size)) 1) 2))) - (insert-char ?\. - (max 0 (round (- (window-width) (or maxwidth (car size))) 2))) - (insert-image img)))))) - - -(defun mu4e-hide-other-mu4e-buffers () - "Bury mu4e-buffers (main, headers, view) (and delete all windows -displaying it). Do _not_ bury the current buffer, though." - (interactive) - (unless (eq mu4e-split-view 'single-window) - (let ((curbuf (current-buffer))) - ;; note: 'walk-windows' does not seem to work correctly when modifying - ;; windows; therefore, the doloops here - (dolist (frame (frame-list)) - (dolist (win (window-list frame nil)) - (with-current-buffer (window-buffer win) - (unless (eq curbuf (current-buffer)) - (when (member major-mode '(mu4e-headers-mode mu4e-view-mode)) - (when (eq t (window-deletable-p win)) - (delete-window win))))))) t))) - - - -;;; Mu4e-org-mode - -(define-derived-mode mu4e-org-mode org-mode "mu4e:org" - "Major mode for mu4e documents, derived from - `org-mode'.") - -(defun mu4e-info (path) - "Show a buffer with the information (an org-file) at PATH." - (unless (file-exists-p path) - (mu4e-error "Cannot find %s" path)) - (let ((curbuf (current-buffer))) - (find-file path) - (mu4e-org-mode) - (setq buffer-read-only t) - (define-key mu4e-org-mode-map (kbd "q") - `(lambda () - (interactive) - (bury-buffer) - (switch-to-buffer ,curbuf))))) - -(defun mu4e-about () - "Show the mu4e 'about' page." - (interactive) - (mu4e-info (concat mu4e-doc-dir "/mu4e-about.org"))) - -(defun mu4e-news () - "Show the mu4e 'about' page." - (interactive) - (mu4e-info (concat mu4e-doc-dir "/NEWS.org"))) - -;;; Misc 3 - -(defun mu4e-refresh-message (path) - "Re-parse message at PATH; if this works, we will -receive (:info add :path :docid ) as well as (:update -)." - (mu4e~proc-add path)) - - -(defun mu4e~fontify-cited () - "Colorize message content based on the citation level. This is -used in the view and compose modes." - (save-excursion - (goto-char (point-min)) - (when (search-forward-regexp "^\n" nil t) ;; search the first empty line - (while (re-search-forward mu4e-cited-regexp nil t) - (let* ((level (string-width (replace-regexp-in-string - "[^>]" "" (match-string 0)))) - (face (unless (zerop level) - (intern-soft (format "mu4e-cited-%d-face" level))))) - (when face - (add-text-properties (line-beginning-position 1) - (line-end-position 1) `(face ,face)))))))) - -(defun mu4e~fontify-signature () - "Give the message signatures a distinctive color. This is used in -the view and compose modes and will color each signature in digest messages adhering to RFC 1153." - (let ((inhibit-read-only t)) - (save-excursion - ;; give the footer a different color... - (goto-char (point-min)) - (while (re-search-forward "^-- *$" nil t) - (let ((p (point)) - (end (or - (re-search-forward "\\(^-\\{30\\}.*$\\)" nil t) ;; 30 by RFC1153 - (point-max)))) - (add-text-properties p end '(face mu4e-footer-face))))))) - -;;; Misc 4 - -(defun mu4e~active-composition-buffers () - "Return all active mu4e composition buffers" - (let (buffers) - (save-excursion - (dolist (buffer (buffer-list t)) - (set-buffer buffer) - (when (eq major-mode 'mu4e-compose-mode) - (push (buffer-name buffer) buffers)))) - (nreverse buffers))) - -;; -;; Loading messages -;; - -(defvar mu4e-loading-mode-map nil "Keymap for *mu4e-loading* buffers.") -(unless mu4e-loading-mode-map - (setq mu4e-loading-mode-map - (let ((map (make-sparse-keymap))) - (define-key map "n" 'ignore) - (define-key map "p" 'ignore) - (define-key map "q" - (lambda()(interactive) - (if (eq mu4e-split-view 'single-window) - 'kill-buffer - 'kill-buffer-and-window))) - map))) -(fset 'mu4e-loading-mode-map mu4e-loading-mode-map) - -(define-derived-mode mu4e-loading-mode special-mode - "mu4e:loading" - (use-local-map mu4e-loading-mode-map) - (let ((inhibit-read-only t)) - (erase-buffer) - (insert (propertize "Loading message..." - 'face 'mu4e-system-face 'intangible t)))) - -;; -;; Bug Reference mode support -;; - -;; This is Emacs 28 stuff but there is no need to guard it with some (f)boundp -;; checks (which would return nil if bug-reference.el is not loaded before -;; mu4e) since the function definition doesn't hurt and `add-hook' works fine -;; for not yet defined variables (by creating them). -(declare-function bug-reference-maybe-setup-from-mail "ext:bug-reference") -(defun mu4e-view--try-setup-bug-reference-mode () - "Try to guess bug-reference setup from the current mu4e mail. -Looks at the maildir and the mail headers List, List-Id, Maildir, -To, From, Cc, and Subject and tries to guess suitable values for -`bug-reference-bug-regexp' and `bug-reference-url-format' by -matching the maildir name against GROUP-REGEXP and each header -value against HEADER-REGEXP in -`bug-reference-setup-from-mail-alist'." - (when (derived-mode-p 'mu4e-view-mode) - (let (header-values) - (save-excursion - (goto-char (point-min)) - (dolist (field '("list" "list-id" "to" "from" "cc" "subject")) - (let ((val (mail-fetch-field field))) - (when val - (push val header-values))))) - (bug-reference-maybe-setup-from-mail - (mail-fetch-field "maildir") - header-values)))) - -(add-hook 'bug-reference-auto-setup-functions - #'mu4e-view--try-setup-bug-reference-mode) - -;;; -(provide 'mu4e-utils) -;;; mu4e-utils.el ends here diff --git a/mu4e/mu4e-vars.el b/mu4e/mu4e-vars.el index f7869fa0..6a3df14d 100644 --- a/mu4e/mu4e-vars.el +++ b/mu4e/mu4e-vars.el @@ -26,114 +26,13 @@ (require 'mu4e-meta) (require 'message) - -(declare-function mu4e-error "mu4e-utils") - -;;; Customization - +(require 'mu4e-helpers) + +;;; Configuration (defgroup mu4e nil - "mu4e - mu for emacs" + "Mu4e - an email-client for Emacs." :group 'mail) -(defcustom mu4e-org-support t - "Support org-mode links." - :type 'boolean - :group 'mu4e) - -(defgroup mu4e-view nil - "Settings for the message view." - :group 'mu4e) - -(defcustom mu4e-view-use-old nil - "If non-nil, use the old viewer. -Otherwise, use the new, Gnus-based viewer." - :type 'boolean - :group 'mu4e-view) - -(make-obsolete-variable 'mu4e-view-use-gnus 'mu4e-view-use-old "1.5.10") - -(defcustom mu4e-speedbar-support nil - "Support having a speedbar to navigate folders/bookmarks." - :type 'boolean - :group 'mu4e) - -(defcustom mu4e-get-mail-command "true" - "Shell command to run to retrieve new mail. -Common values are \"offlineimap\", \"fetchmail\" or \"mbsync\", but -arbitrary shell-commands can be used. - -When set to the literal string \"true\" (the default), the -command simply finishes successfully (running the 'true' command) -without retrieving any mail. This can be useful when mail is -already retrieved in another way." - :type 'string - :group 'mu4e - :safe 'stringp) - -(defcustom mu4e-index-update-error-warning t - "Whether to display warnings during the retrieval process. -This depends on the `mu4e-get-mail-command' exit code." - :type 'boolean - :group 'mu4e - :safe 'booleanp) - -(defcustom mu4e-index-update-error-continue t - "Whether to continue with indexing after an error during retrieval." - :type 'boolean - :group 'mu4e - :safe 'booleanp) - -(defcustom mu4e-index-update-in-background t - "Whether to retrieve mail in the background." - :type 'boolean - :group 'mu4e - :safe 'booleanp) - -(defcustom mu4e-index-cleanup t - "Whether to run a cleanup phase after indexing. - -That is, validate that each message in the message store has a -corresponding message file in the filesystem. - -Having this option as t ensures that no non-existing messages are -shown but can slow with large message stores on slow file-systems." - :type 'boolean - :group 'mu4e - :safe 'booleanp) - -(defcustom mu4e-index-lazy-check nil - "Whether to only use a 'lazy check' during reindexing. -This influences how we decide whether a message -needs (re)indexing or not. - -When this is set to non-nil, mu only uses the directory -timestamps to decide whether it needs to check the messages -beneath it. This makes indexing much faster, but might miss some -changes. For this, you might want to occasionally call -`mu4e-update-index-nonlazy'." - :type 'boolean - :group 'mu4e - :safe 'booleanp) - -(defcustom mu4e-update-interval nil - "Number of seconds between mail retrieval/indexing. -If nil, don't update automatically. Note, changes in -`mu4e-update-interval' only take effect after restarting mu4e." - :type '(choice (const :tag "No automatic update" nil) - (integer :tag "Seconds")) - :group 'mu4e - :safe 'integerp) - -(defvar mu4e-update-pre-hook nil - "Hook run just *before* the mail-retrieval / database updating process starts. -You can use this hook for example to `mu4e-get-mail-command' with -some specific setting.") - -(defcustom mu4e-hide-index-messages nil - "Whether to hide the \"Indexing...\" and contacts messages." - :type 'boolean - :group 'mu4e) - (defcustom mu4e-headers-include-related t "With this option set to non-nil, not just return the matches for a searches, but also messages that are related (through their @@ -150,501 +49,13 @@ and offlineimap." :type 'boolean :group 'mu4e-headers) -(defcustom mu4e-change-filenames-when-moving nil - "Change message file names when moving them. -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 -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) - -(defcustom mu4e-attachment-dir (expand-file-name "~/") - "Default directory for attaching and saving attachments. - -This can be either a string (a file system path), or a function -that takes a filename and the mime-type as arguments, and returns -the attachment dir. See Info node `(mu4e) Attachments' for -details. - -When this called for composing a message, both filename and -mime-type are nill." - :type 'directory - :group 'mu4e - :safe 'stringp) - -;; don't use the older vars anymore -(make-obsolete-variable 'mu4e-user-mail-address-regexp - 'mu4e-user-mail-address-list "0.9.9.x") -(make-obsolete-variable 'mu4e-my-email-addresses - 'mu4e-user-mail-address-list "0.9.9.x") -(make-obsolete-variable 'mu4e-user-mail-address-list - "determined by server; see `mu4e-personal-addresses'." "1.3.8") - -(defcustom mu4e-use-fancy-chars nil - "When set, allow fancy (Unicode) characters for marks/threads. -You can customize the exact fancy characters used with -`mu4e-marks' and various `mu4e-headers-..-mark' and -`mu4e-headers..-prefix' variables." - :type 'boolean - :group 'mu4e) - (defcustom mu4e-date-format-long "%c" "Date format to use in the message view. Follows the format of `format-time-string'." :type 'string :group 'mu4e) -;; for backward compatibility, when a bookmark was defined with defstruct. -(cl-defun make-mu4e-bookmark (&key name query key) - "Create a mu4e proplist with the following elements: -- `name': the user-visible name of the bookmark -- `key': a single key to search for this bookmark -- `query': the query for this bookmark. Either a literal string or a function - that evaluates to a string." - `(:name ,name :query ,query :key ,key)) -(make-obsolete 'make-mu4e-bookmark "`unneeded; `mu4e-bookmarks' -are plists" "1.3.7") - -(defcustom mu4e-bookmarks - '(( :name "Unread messages" - :query "flag:unread AND NOT flag:trashed" - :key ?u) - ( :name "Today's messages" - :query "date:today..now" - :key ?t) - ( :name "Last 7 days" - :query "date:7d..now" - :hide-unread t - :key ?w) - ( :name "Messages with images" - :query "mime:image/*" - :key ?p)) - "List of pre-defined queries that are shown on the main screen. - -Each of the list elements is a plist with at least: -`:name' - the name of the query -`:query' - the query expression or function -`:key' - the shortcut key. - -Note that the :query parameter can be a function/lambda. - -Optionally, you can add the following: -`:hide' - if t, the bookmark is hidden from the main-view and - speedbar. -`:hide-unread' - do not show the counts of unread/total number - of matches for the query in the main-view. This can be useful -if a bookmark uses a very slow query. :hide-unread -is implied from :hide. Furthermore, it is implied if -`:query' is a function. - -Queries used to determine the unread/all counts do _not_ apply -`mu4e-query-rewrite-function'; nor do they discard duplicate or -unreadable messages (for efficiency). Thus, the numbers shown may -differ from the number you get from a 'real' query." - :type '(repeat (plist)) - :version "1.3.9" - :group 'mu4e) - -(defcustom mu4e-query-rewrite-function 'identity - "Function that takes a search expression string, and returns a - possibly changed search expression string. - -This function is applied on the search expression just before -searching, and allows users to modify the query. - -For instance, we could change and of workmail into -\"maildir:/long-path-to-work-related-emails\", by setting the function - -(setq mu4e-query-rewrite-function - (lambda(expr) - (replace-regexp-in-string \"workmail\" - \"maildir:/long-path-to-work-related-emails\" expr))) - -It is good to remember that the replacement does not understand -anything about the query, it just does text replacement." - :type 'function - :group 'mu4e) - -(defun mu4e-bookmarks () - "Get `mu4e-bookmarks' in the (new) format, converting from the -old format if needed." - (cl-map 'list - (lambda (item) - (if (and (listp item) (= (length item) 3)) - `(:name ,(nth 1 item) - :query ,(nth 0 item) - :key ,(nth 2 item)) - item)) - mu4e-bookmarks)) - - -(defcustom mu4e-split-view 'horizontal - "How to show messages / headers. -A symbol which is either: - * `horizontal': split horizontally (headers on top) - * `vertical': split vertically (headers on the left). - * `single-window': view and headers in one window (mu4e will try not to - touch your window layout), main view in minibuffer - * anything else: don't split (show either headers or messages, - not both) -Also see `mu4e-headers-visible-lines' -and `mu4e-headers-visible-columns'." - :type '(choice (const :tag "Split horizontally" horizontal) - (const :tag "Split vertically" vertical) - (const :tag "Single window" single-window) - (const :tag "Don't split" nil)) - :group 'mu4e-headers) - -(defcustom mu4e-view-max-specpdl-size 4096 - "The value of `max-specpdl-size' for displaying messages with Gnus." - :type 'integer - :group 'mu4e-view) - -(defcustom mu4e-view-show-images nil - "If non-nil, automatically display images in the view -buffer. Applies only to the _old_ message view." - :type 'boolean - :group 'mu4e-view) - -(defcustom mu4e-view-auto-mark-as-read t - "Automatically mark messages are 'read' when you read them. -This is the default behavior, but can be turned off, for example -when using a read-only file-system. - -This can also be set to a function; if so, receives a message -plist which should evaluate to nil if the message should *not* be -marked as read-only, or non-nil otherwise." - :type '(choice - boolean - function) - :group 'mu4e-view) - - -(defcustom mu4e-confirm-quit t - "Whether to confirm to quit mu4e." - :type 'boolean - :group 'mu4e) - -(defcustom mu4e-cited-regexp - "^\\(\\([[:alpha:]]+\\)\\|\\( *\\)\\)\\(\\(>+ ?\\)+\\)" - "Regex that determines whether a line is a citation. -This recognizes lines starting with numbers of '>' -and spaces as well as citations of the type \"John> ... \"." - :type 'string - :group 'mu4e) - -(defcustom mu4e-completing-read-function 'ido-completing-read - "Function to be used to receive user-input during completion. -This is used to receive the name of the maildir to switch to via -`mu4e~headers-jump-to-maildir'. - -Suggested possible values are: - * `completing-read': built-in completion method - * `ido-completing-read': dynamic completion within the minibuffer." - :type 'function - :options '(completing-read ido-completing-read) - :group 'mu4e) - -(defcustom mu4e-context-policy 'ask-if-none - "The policy to determine the context when entering the mu4e main view. - -If the value is `always-ask', ask the user unconditionally. - -In all other cases, if any context matches (using its match -function), this context is used. Otherwise, if none of the -contexts match, we have the following choices: - -- `pick-first': pick the first of the contexts available (ie. the default) -- `ask': ask the user -- `ask-if-none': ask if there is no context yet, otherwise leave it as it is -- nil: return nil; leaves the current context as is. - -Also see `mu4e-compose-context-policy'." - :type '(choice - (const :tag "Always ask what context to use, even if one matches" - always-ask) - (const :tag "Ask if none of the contexts match" ask) - (const :tag "Ask when there's no context yet" ask-if-none) - (const :tag "Pick the first context if none match" pick-first) - (const :tag "Don't change the context when none match" nil)) - :group 'mu4e) - -;;;; Crypto - -(defgroup mu4e-crypto nil - "Crypto-related settings." - :group 'mu4e) - -(make-obsolete-variable 'mu4e-auto-retrieve-keys "no longer used." "1.3.1") - -(defcustom mu4e-decryption-policy t - "Policy for dealing with encrypted parts. -The setting is a symbol: - * t: try to decrypt automatically - * `ask': ask before decrypting anything - * nil: don't try to decrypt anything. - -Note that this is not used unless `mu4e-view-use-old' is enabled." - :type '(choice (const :tag "Try to decrypt automatically" t) - (const :tag "Ask before decrypting anything" ask) - (const :tag "Don't try to decrypt anything" nil)) - :group 'mu4e-crypto) - -;;;; Address completion -;; -;; We put these options here rather than in mu4e-compose, because -;; mu4e-utils needs them. - -(defgroup mu4e-compose nil - "Message-composition related settings." - :group 'mu4e) - -(defcustom mu4e-compose-complete-addresses t - "Whether to do auto-completion of e-mail addresses." - :type 'boolean - :group 'mu4e-compose) - -(defcustom mu4e-compose-complete-only-personal nil - "Whether to consider only 'personal' e-mail addresses for completion. -That is, addresses from messages where user was explicitly in one -of the address fields (this excludes mailing list messages). -These addresses are the ones specified with `mu init'." - :type 'boolean - :group 'mu4e-compose) - -(defcustom mu4e-compose-complete-only-after "2014-01-01" - "Consider only contacts last seen after this date. - -Date must be a string of the form YYY-MM-DD. - -This is useful for limiting a potentially enormous set of -contacts for auto-completion to just those that are present in -the e-mail corpus in recent timses. Set to nil to not have any -time-based restriction." - :type 'string - :group 'mu4e-compose) - -;; names and mail-addresses can be mapped onto their canonical -;; counterpart. use the customizeable function -;; mu4e-canonical-contact-function to do that. below the identity -;; function for mapping a contact onto the canonical one. -(defun mu4e-contact-identity (contact) - "Return the name and the mail-address of a CONTACT. -It is used as the identity function for converting contacts to -their canonical counterpart; useful as an example." - (let ((name (plist-get contact :name)) - (mail (plist-get contact :mail))) - (list :name name :mail mail))) - -(make-obsolete-variable 'mu4e-contact-rewrite-function - "mu4e-contact-process-function (see docstring)" "mu4e 1.3.2") -(make-obsolete-variable 'mu4e-compose-complete-ignore-address-regexp - "mu4e-contact-process-function (see docstring)" "mu4e 1.3.2") - -(defcustom mu4e-contact-process-function - (lambda(addr) ;; filter-out no-reply addresses - (unless (string-match-p "no[t]?[-\\.]?repl\\(y\\|ies\\)" addr) - addr)) - "Function for processing contact information for use in auto-completion. - -The function receives the contact as a string, e.g - \"Foo Bar \" - \"cuux@example.com\" - -The function should return either: -- nil: do not use this contact for completion -- the (possibly rewritten) address, which must be -an RFC-2822-compatible e-mail address." - :type 'function - :group 'mu4e-compose) - -(defcustom mu4e-compose-reply-ignore-address - '("no-?reply") - "Addresses to prune when doing wide replies. - -This can be a regexp matching the address, a list of regexps or a -predicate function. A value of nil keeps all the addresses." - :type '(choice - (const nil) - function - string - (repeat string)) - :group 'mu4e-compose) - -(defcustom mu4e-compose-reply-recipients 'ask - "Which recipients to use when replying to a message. -May be 'ask, 'all, 'sender. Note that that only applies to -non-mailing-list message; for those, mu4e always asks." - :type '(choice ask - all - sender) - :group 'mu4e-compose) - -(defcustom mu4e-compose-reply-to-address nil - "The Reply-To address. -Useful when this is not equal to the From: address." - :type 'string - :group 'mu4e-compose) - -(defcustom mu4e-compose-forward-as-attachment nil - "Whether to forward messages as attachments instead of inline." - :type 'boolean - :group 'mu4e-compose) - -;; backward compatibility -(make-obsolete-variable 'mu4e-reply-to-address - 'mu4e-compose-reply-to-address - "v0.9.9") - -(defcustom mu4e-compose-keep-self-cc nil - "When non-nil. keep your e-mail address in Cc: when replying." - :type 'boolean - :group 'mu4e-compose) - -(defvar mu4e-compose-parent-message nil - "The parent message plist. -This is the message being replied to, forwarded or edited; used -in `mu4e-compose-pre-hook'. For new messages, it is nil.") - -;;;; Calendar - -(defgroup mu4e-icalendar nil - "Icalendar related settings." - :group 'mu4e) - -(defcustom mu4e-icalendar-trash-after-reply nil - "If non-nil, trash the icalendar invitation after replying." - :type 'boolean - :group 'mu4e-icalendar) - -(defcustom mu4e-icalendar-diary-file nil - "If non-nil, the file in which to add events upon reply." - :type '(choice (const :tag "Do not insert a diary entry" nil) - (string :tag "Insert a diary entry in this file")) - :group 'mu4e-icalendar) - - -;;;; Folders - -(defgroup mu4e-folders nil - "Special folders." - :group 'mu4e) - -(defcustom mu4e-drafts-folder "/drafts" - "Your folder for draft messages, relative to the root maildir. -For instance, \"/drafts\". Instead of a string, may also be a -function that takes a message (a msg plist, see -`mu4e-message-field'), and returns a folder. Note, the message -parameter refers to the original message being replied to / being -forwarded / re-edited and is nil otherwise. `mu4e-drafts-folder' -is only evaluated once." - :type '(choice - (string :tag "Folder name") - (function :tag "Function return folder name")) - :group 'mu4e-folders) - -(defcustom mu4e-refile-folder "/archive" - "Your folder for refiling messages, relative to the root maildir. -For instance \"/Archive\". Instead of a string, may also be a -function that takes a message (a msg plist, see -`mu4e-message-field'), and returns a folder. Note that the -message parameter refers to the message-at-point." - :type '(choice - (string :tag "Folder name") - (function :tag "Function return folder name")) - :group 'mu4e-folders) - -(defcustom mu4e-sent-folder "/sent" - "Your folder for sent messages, relative to the root maildir. -For instance, \"/Sent Items\". Instead of a string, may also be a -function that takes a message (a msg plist, see -`mu4e-message-field'), and returns a folder. Note that the -message parameter refers to the original message being replied to -/ being forwarded / re-edited, and is nil otherwise." - :type '(choice - (string :tag "Folder name") - (function :tag "Function return folder name")) - :group 'mu4e-folders) - -(defcustom mu4e-trash-folder "/trash" - "Your folder for trashed messages, relative to the root maildir. -For instance, \"/trash\". Instead of a string, may also be a -function that takes a message (a msg plist, see -`mu4e-message-field'), and returns a folder. When using -`mu4e-trash-folder' in the headers view (when marking messages -for trash). Note that the message parameter refers to the -message-at-point. When using it when composing a message (see -`mu4e-sent-messages-behavior'), this refers to the original -message being replied to / being forwarded / re-edited, and is -nil otherwise." - :type '(choice - (string :tag "Folder name") - (function :tag "Function return folder name")) - :group 'mu4e-folders) - -(defcustom mu4e-maildir-shortcuts nil - "A list of maildir shortcuts. -This makes it possible to quickly go to a particular -maildir (folder), or quickly moving messages to them (e.g., for -archiving or refiling). - -Each of the list elements is a plist with at least: -`:maildir' - the maildir for the shortcut (e.g. \"/archive\") -`:key' - the shortcut key. - -Optionally, you can add the following: -`:hide' - if t, the shortcut is hidden from the main-view and -speedbar. -`:hide-unread' - do not show the counts of unread/total number - of matches for the maildir in the main-view, and is implied -from `:hide'. - -For backward compatibility, an older form is recognized as well: - - (maildir . key), where MAILDIR is a maildir (such as -\"/archive/\"), and key is a single character. - -You can use these shortcuts in the headers and view buffers, for -example with `mu4e-mark-for-move-quick' (or 'm', by default) or -`mu4e-jump-to-maildir' (or 'j', by default), followed by the -designated shortcut character for the maildir. - -Unlike in search queries, folder names with spaces in them must -NOT be quoted, since mu4e does this for you." - :type '(repeat (cons (string :tag "Maildir") character)) - :version "1.3.9" - :group 'mu4e-folders) - -(defcustom mu4e-maildir-info-delimiter - (if (member system-type '(ms-dos windows-nt cygwin)) - ";" ":") - "Separator character between message identifier and flags. -It defaults to ':' on most platforms, except on Windows, -where it is not allowed and we use ';' for compatibility -with mbsync, offlineimap and other programs." - :type 'string - :group 'mu4e-folders) - - -(defun mu4e-maildir-shortcuts () - "Get `mu4e-maildir-shortcuts' in the (new) format, converting -from the old format if needed." - (cl-map 'list - (lambda (item) ;; convert from old format? - (if (and (consp item) (not (consp (cdr item)))) - `(:maildir ,(car item) :key ,(cdr item)) - item)) - mu4e-maildir-shortcuts)) - -(defcustom mu4e-display-update-status-in-modeline nil - "Non-nil value will display the update status in the modeline." - :group 'mu4e - :type 'boolean) - + ;;; Faces (defgroup mu4e-faces nil @@ -1009,111 +420,8 @@ header-view, not including, for instance, the message body.") ;;;; Main -(defvar mu4e-main-buffer-name " *mu4e-main*" - "Name of the mu4e main view buffer. The default name starts -with SPC and therefore is not visible in buffer list.") - - -;;;; Headers - -(defconst mu4e~headers-buffer-name "*mu4e-headers*" - "Name of the buffer for message headers.") - -(defvar mu4e~headers-last-query nil - "The present (most recent) query.") - -;;;; View - -(defconst mu4e~view-buffer-name "*mu4e-view*" - "Name for the message view buffer.") - -(defconst mu4e~view-embedded-buffer-name " *mu4e-embedded-view*" - "Name for the embedded message view buffer.") - ;;;; Other -(defvar mu4e~contacts-hash nil - "Hash that maps contacts (ie. 'name ') to an integer for sorting. -We need to keep this information around to quickly re-sort -subsets of the contacts in the completions function in -mu4e-compose.") - - -;;; Handler functions -;; -;; The handler functions define what happens when we receive a certain -;; message from the server. Here we register our handler functions; -;; these connect server messages to functions to handle them. -;; -;; These bindings form mu4e's central nervous system so it's not -;; really recommended to override them (they reference various -;; internal bits, which could change). - -(defun mu4e~default-handler (&rest args) - "Dummy handler function with arbitrary ARGS." - (error "Not handled: %S" args)) - -(defvar mu4e-error-func 'mu4e-error-handler - "Function called for each error received. -The function is passed an error plist as argument. See -`mu4e~proc-filter' for the format.") - -(defvar mu4e-update-func 'mu4e~headers-update-handler - "Function called for each :update sexp returned. -The function is passed a msg sexp as argument. -See `mu4e~proc-filter' for the format.") - -(defvar mu4e-remove-func 'mu4e~headers-remove-handler - "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 'mu4e~default-handler - "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 'mu4e~headers-view-handler - "Function called for each single-message sexp. -The function is passed a message sexp as argument. See -`mu4e~proc-filter' for the format.") - -(defvar mu4e-header-func 'mu4e~headers-header-handler - "Function called for each message-header received. -The function is passed a msg plist as argument. See -`mu4e~proc-filter' for the format.") - -(defvar mu4e-found-func 'mu4e~headers-found-handler - "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~proc-filter' for the format.") - -(defvar mu4e-erase-func 'mu4e~headers-clear - "Function called we receive an :erase sexp. -This before new headers are displayed, to clear the current -headers buffer. See `mu4e~proc-filter' for the format.") - -(defvar mu4e-compose-func 'mu4e~compose-handler - "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~proc-filter' for the format of .") - -(defvar mu4e-info-func 'mu4e-info-handler - "Function called for each (:info type ....) sexp received. -from the server process.") - -(defvar mu4e-pong-func 'mu4e~default-handler - "Function called for each (:pong type ....) sexp received.") - -(defvar mu4e-contacts-func 'mu4e-contacts-func - "A function called for each (:contacts () -sexp received from the server process.") - -(defvar mu4e-temp-func 'mu4e~view-temp-handler - "A function called for each (:temp ) sexp.") - ;;; Internals (defvar mu4e~headers-view-win nil diff --git a/mu4e/mu4e-view.el b/mu4e/mu4e-view.el index e31fad42..38aca930 100644 --- a/mu4e/mu4e-view.el +++ b/mu4e/mu4e-view.el @@ -46,6 +46,7 @@ (require 'mu4e-proc) (require 'mu4e-search) (require 'mu4e-utils) ;; utility functions +(require 'mu4e-contacts) (require 'mu4e-vars) ;;; Options @@ -87,108 +88,49 @@ The first letter of NAME is used as a shortcut character." :group 'mu4e-view :type '(alist :key-type string :value-type function)) +(defcustom mu4e-view-max-specpdl-size 4096 + "The value of `max-specpdl-size' for displaying messages with Gnus." + :type 'integer + :group 'mu4e-view) + ;;; Old options -;; These don't do anything useful when in "gnus" mode, except for avoid errors -;; for people that have these in their config. - -(defcustom mu4e-view-show-addresses nil - "Whether to initially show full e-mail addresses for contacts. -Otherwise, just show their names. Ignored when using the gnus-based view." - :type 'boolean - :group 'mu4e-view) - +;; Options from the old message view. +(make-obsolete-variable 'mu4e-view-show-addresses + "Unused with the new message view" "1.7.0") (make-obsolete-variable 'mu4e-view-wrap-lines nil "0.9.9-dev7") (make-obsolete-variable 'mu4e-view-hide-cited nil "0.9.9-dev7") +(make-obsolete-variable 'mu4e-view-date-format + "Unused with the new message view" "1.7.0") +(make-obsolete-variable 'mu4e-view-image-max-width + "Unused with the new message view" "1.7.0") +(make-obsolete-variable 'mu4e-view-image-max-height + "Unused with the new message view" "1.7.0") +(make-obsolete-variable 'mu4e-save-multiple-attachments-without-asking + "Unused with the new message view" "1.7.0") +(make-obsolete-variable 'mu4e-view-attachment-assoc + "Unused with the new message view" "1.7.0") +(make-obsolete-variable 'mu4e-view-attachment-actions + "See mu4e-view-mime-part-actions" "1.7.0") +(make-obsolete-variable 'mu4e-view-header-field-keymap + "Unused with the new message view" "1.7.0") +(make-obsolete-variable 'mu4e-view-header-field-keymap + "Unused with the new message view" "1.7.0") +(make-obsolete-variable 'mu4e-view-contacts-header-keymap + "Unused with the new message view" "1.7.0") +(make-obsolete-variable 'mu4e-view-attachments-header-keymap + "Unused with the new message view" "1.7.0") +(make-obsolete-variable 'mu4e-imagemagick-identify nil "1.7.0") +(make-obsolete-variable 'mu4e-view-show-images + "No longer used" "1.7.0") +(make-obsolete-variable 'mu4e-view-gnus "Old view is gone" "1.7.0") +(make-obsolete-variable 'mu4e-view-use-gnus "Gnus view is the default" "1.5.10") -(defcustom mu4e-view-date-format "%c" - "Date format to use in the message view. -In the format of `format-time-string'. Ignored when using the gnus-based view." - :type 'string - :group 'mu4e-view) - -(defcustom mu4e-view-image-max-width 800 - "The maximum width for images to display. -This is only effective if you're using an Emacs with Imagemagick -support, and `mu4e-view-show-images' is non-nil. Ignored when -using the gnus-based view." - :type 'integer - :group 'mu4e-view) - -(defcustom mu4e-view-image-max-height 600 - "The maximum height for images to display. -This is only effective if you're using an Emacs with Imagemagick -support, and `mu4e-view-show-images' is non-nil. Ignored when -using the gnus-based view." - :type 'integer - :group 'mu4e-view) - - -(defcustom mu4e-save-multiple-attachments-without-asking nil - "If non-nil, saving multiple attachments asks once for a -directory and saves all attachments in the chosen directory. -Ignored when using the gnus-based view." - :type 'boolean - :group 'mu4e-view) - -(defcustom mu4e-view-attachment-assoc nil - "Alist of (EXTENSION . PROGRAM). -Specify which PROGRAM to use to open attachment with EXTENSION. -Args EXTENSION and PROGRAM should be specified as strings. -Ignored when using the gnus-based view." - :group 'mu4e-view - :type '(alist :key-type string :value-type string)) - -(defcustom mu4e-view-attachment-actions - '( ("ssave" . mu4e-view-save-attachment-single) - ("Ssave multi" . mu4e-view-save-attachment-multi) - ("wopen-with" . mu4e-view-open-attachment-with) - ("ein-emacs" . mu4e-view-open-attachment-emacs) - ("dimport-in-diary" . mu4e-view-import-attachment-diary) - ("kimport-public-key" . mu4e-view-import-public-key) - ("|pipe" . mu4e-view-pipe-attachment)) - "List of actions to perform on message attachments. -The actions are cons-cells of the form: - (NAME . FUNC) -where: -* NAME is the name of the action (e.g. \"Count lines\") -* FUNC is a function which receives two arguments: the message - plist and the attachment number. -The first letter of NAME is used as a shortcut character. -Ignored when using the gnus-based view." - :group 'mu4e-view - :type '(alist :key-type string :value-type function)) - -;;; Keymaps - -(defvar mu4e-view-header-field-keymap - (let ((map (make-sparse-keymap))) - (define-key map [mouse-1] 'mu4e~view-header-field-fold) - (define-key map (kbd "TAB") 'mu4e~view-header-field-fold) - map) - "Keymap used for header fields. Ignored when using the -gnus-based view.") - -(defvar mu4e-view-contacts-header-keymap - (let ((map (make-sparse-keymap))) - (define-key map [mouse-2] 'mu4e~view-compose-contact) - (define-key map "C" 'mu4e~view-compose-contact) - (define-key map "c" 'mu4e~view-copy-contact) - map) - "Keymap used for the contacts in the header fields. -Ignored when using the gnus-based view.") - -(defvar mu4e-view-attachments-header-keymap - (let ((map (make-sparse-keymap))) - (define-key map [mouse-1] 'mu4e~view-open-attach-from-binding) - (define-key map [?\M-\r] 'mu4e~view-open-attach-from-binding) - (define-key map [mouse-2] 'mu4e~view-save-attach-from-binding) - (define-key map (kbd "") 'mu4e~view-save-attach-from-binding) - map) - "Keymap used in the \"Attachments\" header field. Ignored when -using the gnus-based view.") +(make-obsolete-variable 'mu4e-cited-regexp "No longer used" "1.7.0") + + ;; Helpers (defun mu4e~view-quit-buffer () @@ -1262,5 +1204,48 @@ the third MIME-part." (gnus-article-inline-part (car html-part)) (mu4e-warn "No html part in this message"))) + +(defun mu4e-process-file-through-pipe (path pipecmd) + "Process file at PATH through a pipe with PIPECMD." + (let ((buf (get-buffer-create "*mu4e-output"))) + (with-current-buffer buf + (let ((inhibit-read-only t)) + (erase-buffer) + (call-process-shell-command pipecmd path t t) + (view-mode))) + (switch-to-buffer buf))) + + +;;; Bug Reference mode support + +;; This is Emacs 28 stuff but there is no need to guard it with some (f)boundp +;; checks (which would return nil if bug-reference.el is not loaded before +;; mu4e) since the function definition doesn't hurt and `add-hook' works fine +;; for not yet defined variables (by creating them). +(declare-function bug-reference-maybe-setup-from-mail "ext:bug-reference") +(defun mu4e--view-try-setup-bug-reference-mode () + "Try to guess bug-reference setup from the current mu4e mail. +Looks at the maildir and the mail headers List, List-Id, Maildir, +To, From, Cc, and Subject and tries to guess suitable values for +`bug-reference-bug-regexp' and `bug-reference-url-format' by +matching the maildir name against GROUP-REGEXP and each header +value against HEADER-REGEXP in +`bug-reference-setup-from-mail-alist'." + (when (derived-mode-p 'mu4e-view-mode) + (let (header-values) + (save-excursion + (goto-char (point-min)) + (dolist (field '("list" "list-id" "to" "from" "cc" "subject")) + (let ((val (mail-fetch-field field))) + (when val + (push val header-values))))) + (bug-reference-maybe-setup-from-mail + (mail-fetch-field "maildir") + header-values)))) + +(add-hook 'bug-reference-auto-setup-functions + #'mu4e--view-try-setup-bug-reference-mode) + + (provide 'mu4e-view) ;;; mu4e-view.el ends here diff --git a/mu4e/mu4e.el b/mu4e/mu4e.el index a28aeab3..f7f60cd9 100644 --- a/mu4e/mu4e.el +++ b/mu4e/mu4e.el @@ -1,6 +1,6 @@ ;;; mu4e.el --- part of mu4e, the mu mail user agent -*- lexical-binding: t -*- -;; Copyright (C) 2011-2019 Dirk-Jan C. Binnema +;; Copyright (C) 2011-2021 Dirk-Jan C. Binnema ;; Author: Dirk-Jan C. Binnema ;; Maintainer: Dirk-Jan C. Binnema @@ -27,13 +27,33 @@ ;;; Code: (require 'mu4e-vars) -(require 'mu4e-headers) ;; headers view -(require 'mu4e-view) ;; message view -(require 'mu4e-main) ;; main screen -(require 'mu4e-compose) ;; message composition / sending +(require 'mu4e-helpers) +(require 'mu4e-folders) +(require 'mu4e-context) +(require 'mu4e-contacts) +(require 'mu4e-headers) +(require 'mu4e-compose) +(require 'mu4e-bookmarks) +(require 'mu4e-update) +(require 'mu4e-main) (require 'mu4e-proc) ;; communication with backend -(require 'mu4e-utils) ;; utility functions -(require 'mu4e-context) ;; support for contexts + + + +(defcustom mu4e-confirm-quit t + "Whether to confirm to quit mu4e." + :type 'boolean + :group 'mu4e) + +(defcustom mu4e-org-support t + "Support Org-mode links." + :type 'boolean + :group 'mu4e) + +(defcustom mu4e-speedbar-support nil + "Support having a speedbar to navigate folders/bookmarks." + :type 'boolean + :group 'mu4e) (when mu4e-speedbar-support (require 'mu4e-speedbar)) ;; support for speedbar @@ -48,20 +68,177 @@ ;;;###autoload (defun mu4e (&optional background) - "If mu4e is not running yet, start it. Then, show the main -window, unless BACKGROUND (prefix-argument) is non-nil." + "If mu4e is not running yet, start it. +Then, show the main window, unless BACKGROUND (prefix-argument) +is non-nil." (interactive "P") ;; start mu4e, then show the main view - (mu4e~start (unless background 'mu4e~main-view))) + (mu4e--init-handlers) + (mu4e--start (unless background 'mu4e~main-view))) (defun mu4e-quit() "Quit the mu4e session." (interactive) (if mu4e-confirm-quit (when (y-or-n-p (mu4e-format "Are you sure you want to quit?")) - (mu4e~stop)) - (mu4e~stop))) + (mu4e--stop)) + (mu4e--stop))) + +;;; Internals +(defun mu4e--check-requirements () + "Check for the settings required for running mu4e." + (unless (>= emacs-major-version 25) + (mu4e-error "Emacs >= 25.x is required for mu4e")) + (when (mu4e-server-properties) + (unless (string= (mu4e-server-version) mu4e-mu-version) + (mu4e-error "The mu server has version %s, but we need %s" + (mu4e-server-version) mu4e-mu-version))) + (unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary)) + (mu4e-error "Please set `mu4e-mu-binary' to the full path to the mu + binary")) + (dolist (var '(mu4e-sent-folder mu4e-drafts-folder + mu4e-trash-folder)) + (unless (and (boundp var) (symbol-value var)) + (mu4e-error "Please set %S" var)) + (unless (functionp (symbol-value var)) ;; functions are okay, too + (let* ((dir (symbol-value var)) + (path (concat (mu4e-root-maildir) dir))) + (unless (string= (substring dir 0 1) "/") + (mu4e-error "%S must start with a '/'" dir)) + (unless (mu4e-create-maildir-maybe path) + (mu4e-error "%s (%S) does not exist" path var)))))) + +;;; Starting / getting mail / updating the index + +(defun mu4e--pong-handler (_data func) + "Handle 'pong' responses from the mu server. +Invoke FUNC if non-nil." + (let ((doccount (plist-get (mu4e-server-properties) :doccount))) + (mu4e--check-requirements) + (when func (funcall func)) + (when (zerop doccount) + (mu4e-message "Store is empty; (re)indexing. This may take a while.") ; + (mu4e-update-index)) + (when (and mu4e-update-interval (null mu4e--update-timer)) + (setq mu4e--update-timer + (run-at-time 0 mu4e-update-interval + (lambda () (mu4e-update-mail-and-index + mu4e-index-update-in-background))))))) + +(defun mu4e--start (&optional func) + "Start mu4e. +If `mu4e-contexts' have been defined, but we don't have a context +yet, switch to the matching one, or none matches, the first. If +mu4e is already running, execute function FUNC (if non-nil). +Otherwise, check various requireme`'nts, then start mu4e. When +successful, call FUNC (if non-nil) afterwards." + (unless (mu4e-context-current) + (mu4e--context-autoswitch nil mu4e-context-policy)) + (setq mu4e-pong-func (lambda (info) (mu4e--pong-handler info func))) + (mu4e~proc-ping + (mapcar ;; send it a list of queries we'd like to see read/unread info for + (lambda (bm) + (funcall (or mu4e-search-query-rewrite-function #'identity) + (plist-get bm :query))) + ;; exclude bookmarks that are not strings, and with certain flags + (seq-filter (lambda (bm) + (and (stringp (plist-get bm :query)) + (not (or (plist-get bm :hide) + (plist-get bm :hide-unread))))) + (append (mu4e-bookmarks) + (mu4e--maildirs-with-query))))) + ;; maybe request the list of contacts, automatically refreshed after + ;; reindexing + (unless mu4e--contacts-hash (mu4e--request-contacts-maybe))) + +(defun mu4e--stop () + "Stop mu4e." + (when mu4e--update-timer + (cancel-timer mu4e--update-timer) + (setq mu4e--update-timer nil)) + (mu4e-clear-caches) + (mu4e~proc-kill) + ;; kill all mu4e buffers + (mapc + (lambda (buf) + ;; When using the Gnus-based viewer, the view buffer has the + ;; kill-buffer-hook function mu4e~view-kill-buffer-hook-fn which kills the + ;; mm-* buffers created by Gnus' article mode. Those have been returned by + ;; `buffer-list' but might already be deleted in case the view buffer has + ;; been killed first. So we need a `buffer-live-p' check here. + (when (buffer-live-p buf) + (with-current-buffer buf + (when (member major-mode + '(mu4e-headers-mode mu4e-view-mode mu4e-main-mode)) + (kill-buffer))))) + (buffer-list))) + +;;; Handlers +(defun mu4e--error-handler (errcode errmsg) + "Handler function for showing an error with ERRCODE and ERRMSG." + ;; don't use mu4e-error here; it's running in the process filter context + (cl-case errcode + (4 (mu4e-warn "No matches for this search query.")) + (110 (display-warning 'mu4e errmsg :error)) ;; schema version. + (t (error "Error %d: %s" errcode errmsg)))) + + +(defun mu4e--info-handler (info) + "Handler function for (:INFO ...) sexps received from server." + (let* ((type (plist-get info :info)) + (processed (plist-get info :processed)) + (updated (plist-get info :updated)) + (cleaned-up (plist-get info :cleaned-up)) + (mainbuf (get-buffer mu4e-main-buffer-name))) + (cond + ((eq type 'add) t) ;; do nothing + ((eq type 'index) + (if (eq (plist-get info :status) 'running) + (mu4e-index-message + "Indexing... processed %d, updated %d" processed updated) + (progn + (mu4e-index-message + "%s completed; processed %d, updated %d, cleaned-up %d" + (if mu4e-index-lazy-check "Lazy indexing" "Indexing") + processed updated cleaned-up) + ;; call the updated hook if anything changed. + (unless (zerop (+ updated cleaned-up)) + (run-hooks 'mu4e-index-updated-hook)) + (unless (and (not (string= mu4e--contacts-tstamp "0")) + (zerop (plist-get info :updated))) + (mu4e--request-contacts-maybe)) + (when (and (buffer-live-p mainbuf) (get-buffer-window mainbuf)) + (save-window-excursion + (select-window (get-buffer-window mainbuf)) + (mu4e~main-view 'refresh)))))) + ((plist-get info :message) + (mu4e-index-message "%s" (plist-get info :message)))))) + +(defun mu4e--init-handlers() + "Initialize the server message handlers. +Only set set them if they were nil before, so overriding has a +chance." + (mu4e-setq-if-nil mu4e-error-func #'mu4e--error-handler) + (mu4e-setq-if-nil mu4e-update-func #'mu4e~headers-update-handler) + (mu4e-setq-if-nil mu4e-remove-func #'mu4e~headers-remove-handler) + (mu4e-setq-if-nil mu4e-view-func #'mu4e~headers-view-handler) + (mu4e-setq-if-nil mu4e-header-func #'mu4e~headers-header-handler) + (mu4e-setq-if-nil mu4e-found-func #'mu4e~headers-found-handler) + (mu4e-setq-if-nil mu4e-erase-func #'mu4e~headers-clear) + + (mu4e-setq-if-nil mu4e-sent-func #'mu4e--default-handler) + (mu4e-setq-if-nil mu4e-compose-func #'mu4e~compose-handler) + (mu4e-setq-if-nil mu4e-contacts-func #'mu4e--update-contacts) + (mu4e-setq-if-nil mu4e-info-func #'mu4e--info-handler) + (mu4e-setq-if-nil mu4e-pong-func #'mu4e--default-handler)) + +(defun mu4e-clear-caches () + "Clear any cached resources." + (setq + mu4e-maildir-list nil + mu4e--contacts-hash nil + mu4e--contacts-tstamp "0")) ;;; _ (provide 'mu4e) ;;; mu4e.el ends here