diff options
author | Aaron LI <aaronly.me@gmail.com> | 2016-05-16 09:41:22 +0800 |
---|---|---|
committer | Aaron LI <aaronly.me@gmail.com> | 2016-05-16 09:41:22 +0800 |
commit | 36750b13d1584f03d7a2c95f7f1c5332dc81723b (patch) | |
tree | 3462b4dd080d9b26cce5d6ba813590ea57f4671a | |
parent | 7375bd3290dcb2b488efac8d53b7e8283f7adcf4 (diff) | |
download | dotfiles-36750b13d1584f03d7a2c95f7f1c5332dc81723b.tar.bz2 |
_spacemacs.d/local: import mu4e git20160515
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-actions.el | 312 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-compose.el | 778 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-context.el | 157 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-contrib.el | 164 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-draft.el | 474 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-headers.el | 1694 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-lists.el | 93 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-main.el | 225 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-mark.el | 466 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-message.el | 284 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-meta.el | 11 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-proc.el | 524 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-speedbar.el | 124 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-utils.el | 1238 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-vars.el | 870 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-view.el | 1541 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e.el | 93 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/org-mu4e.el | 323 | ||||
-rw-r--r-- | _spacemacs.d/local/mu4e/org-old-mu4e.el | 289 |
19 files changed, 9660 insertions, 0 deletions
diff --git a/_spacemacs.d/local/mu4e/mu4e-actions.el b/_spacemacs.d/local/mu4e/mu4e-actions.el new file mode 100644 index 0000000..7f145a1 --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-actions.el @@ -0,0 +1,312 @@ +;;; mu4e-actions.el -- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Example actions for messages, attachments (see chapter 'Actions' in the +;; manual) + +;;; Code: +(eval-when-compile (byte-compile-disable-warning 'cl-functions)) +(require 'cl) +(require 'ido) + +(require 'mu4e-utils) +(require 'mu4e-message) +(require 'mu4e-meta) + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mu4e-action-count-lines (msg) + "Count the number of lines in the e-mail message. +Works for headers view and message-view." + (message "Number of lines: %s" + (shell-command-to-string + (concat "wc -l < " (shell-quote-argument (mu4e-message-field msg :path)))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defvar mu4e-msg2pdf (concat mu4e-builddir "/toys/msg2pdf/msg2pdf") + "Path to the msg2pdf toy.") + +(defun mu4e-action-view-as-pdf (msg) + "Convert the message to pdf, then show it. +Works for the message view." + (unless (file-executable-p mu4e-msg2pdf) + (mu4e-error "msg2pdf not found; please set `mu4e-msg2pdf'")) + (let* ((pdf + (shell-command-to-string + (concat mu4e-msg2pdf " " + (shell-quote-argument (mu4e-message-field msg :path)) + " 2> /dev/null"))) + (pdf (and pdf (> (length pdf) 5) + (substring pdf 0 -1)))) ;; chop \n + (unless (and pdf (file-exists-p pdf)) + (mu4e-warn "Failed to create PDF file")) + (find-file pdf))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defun mu4e~write-body-to-html (msg) + "Write the body (either html or text) to a temporary file; +return the filename." + (let* ((html (mu4e-message-field msg :body-html)) + (txt (mu4e-message-field msg :body-txt)) + (tmpfile (mu4e-make-temp-file "html")) + (attachments (remove-if (lambda (part) + (or (null (plist-get part :attachment)) + (null (plist-get part :cid)))) + (mu4e-message-field msg :parts)))) + (unless (or html txt) + (mu4e-error "No body part for this message")) + (with-temp-buffer + (insert "<head><meta charset=\"UTF-8\"></head>\n") + (insert (or html (concat "<pre>" txt "</pre>"))) + (write-file tmpfile) + ;; rewrite attachment urls + (mapc (lambda (attachment) + (goto-char (point-min)) + (while (re-search-forward (format "src=\"cid:%s\"" (plist-get attachment :cid)) nil t) + (if (plist-get attachment :temp) + (replace-match (format "src=\"%s\"" (plist-get attachment :temp))) + (replace-match (format "src=\"%s%s\"" temporary-file-directory (plist-get attachment :name))) + (mu4e~proc-extract 'save (mu4e-message-field :docid) (plist-get attachment :index) mu4e-decryption-policy temporary-file-directory) + (mu4e-remove-file-later (format "%s%s" temporary-file-directory (plist-get attachment :name)))))) + attachments) + (save-buffer) + tmpfile))) + +(defun mu4e-action-view-in-browser (msg) + "View the body of the message in a browser. +You can influence the browser to use with the variable +`browse-url-generic-program', and see the discussion of privacy +aspects in `(mu4e) Displaying rich-text messages'." + (browse-url (concat "file://" + (mu4e~write-body-to-html msg)))) + +(defun mu4e-action-view-with-xwidget (msg) + "View the body of the message inside xwidget-webkit. This is +only available in emacs 25+; also see the discussion of privacy +aspects in `(mu4e) Displaying rich-text messages'." + (unless (fboundp 'xwidget-webkit-browse-url) + (mu4e-error "No xwidget support available")) + (xwidget-webkit-browse-url + (concat "file://" (mu4e~write-body-to-html msg)) t)) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defconst mu4e-text2speech-command "festival --tts" + "Program that speaks out text it receives on standard-input.") + +(defun mu4e-action-message-to-speech (msg) + "Pronounce the message text using `mu4e-text2speech-command'." + (unless (mu4e-message-field msg :body-txt) + (mu4e-warn "No text body for this message")) + (with-temp-buffer + (insert (mu4e-message-field msg :body-txt)) + (shell-command-on-region (point-min) (point-max) + mu4e-text2speech-command))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + + + + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defvar mu4e-captured-message nil + "The last-captured message (the s-expression).") + +(defun mu4e-action-capture-message (msg) + "Remember MSG; we can create a an attachment based on this msg +with `mu4e-compose-attach-captured-message'." + (setq mu4e-captured-message msg) + (message "Message has been captured")) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defvar mu4e-org-contacts-file nil + "File to store contact information for org-contacts. +Needed by `mu4e-action-add-org-contact'.") + +(eval-when-compile ;; silence compiler warning about free variable + (unless (require 'org-capture nil 'noerror) + (defvar org-capture-templates nil))) + +(defun mu4e-action-add-org-contact (msg) + "Add an org-contact entry based on the From: address of the +current message (in headers or view). You need to set +`mu4e-org-contacts-file' to the full path to the file where you +store your org-contacts." + (unless (require 'org-capture nil 'noerror) + (mu4e-error "org-capture is not available.")) + (unless mu4e-org-contacts-file + (mu4e-error "`mu4e-org-contacts-file' is not defined.")) + (let* ((sender (car-safe (mu4e-message-field msg :from))) + (name (car-safe sender)) (email (cdr-safe sender)) + (blurb + (format + (concat + "* %%?%s\n" + ":PROPERTIES:\n" + ":EMAIL: %s\n" + ":NICK:\n" + ":BIRTHDAY:\n" + ":END:\n\n") + (or name email "") + (or email ""))) + (key "mu4e-add-org-contact-key") + (org-capture-templates + (append org-capture-templates + (list (list key "contacts" 'entry + (list 'file mu4e-org-contacts-file) blurb))))) + (message "%S" org-capture-templates) + (when (fboundp 'org-capture) + (org-capture nil key)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mu4e-action-git-apply-patch (msg) + "Apply the git [patch] message." + (let ((path (ido-read-directory-name "Target directory: " + (car ido-work-directory-list) + "~/" t))) + (setf ido-work-directory-list + (cons path (delete path ido-work-directory-list))) + (shell-command + (format "cd %s; git apply %s" + path + (mu4e-message-field msg :path))))) + +(defun mu4e-action-git-apply-mbox (msg) + "Apply and commit the git [patch] message." + (let ((path (ido-read-directory-name "Target directory: " + (car ido-work-directory-list) + "~/" t))) + (setf ido-work-directory-list + (cons path (delete path ido-work-directory-list))) + (shell-command + (format "cd %s; git am %s" + path + (mu4e-message-field msg :path))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defvar mu4e-action-tags-header "X-Keywords" + "Header where tags are stored. Used by `mu4e-action-retag-message'. + Make sure it is one of the headers mu recognizes for storing + tags: X-Keywords, X-Label, Keywords. Also note that changing + this setting on already tagged messages can lead to messages + with multiple tags headers.") + +(defun mu4e~contains-line-matching (regexp path) + "Determine whether the file at path contains a line matching + the given regexp." + (with-temp-buffer + (insert-file-contents path) + (save-excursion + (goto-char (point-min)) + (if (re-search-forward regexp nil t) + t + nil)))) + +(defun mu4e~replace-first-line-matching (regexp to-string path) + "Replace the first line in the file at path that matches regexp + with the string replace." + (with-temp-file path + (insert-file-contents path) + (save-excursion + (goto-char (point-min)) + (if (re-search-forward regexp nil t) + (replace-match to-string nil nil))))) + +(defun mu4e-action-retag-message (msg &optional retag-arg) + "Change tags of a message. Example: +tag \"+long tag\" -oldtag + adds 'tag' and 'long tag', and removes oldtag." + (let* ((retag (or retag-arg (read-string "Tags: "))) + (path (mu4e-message-field msg :path)) + (maildir (mu4e-message-field msg :maildir)) + (oldtags (mu4e-message-field msg :tags)) + (header mu4e-action-tags-header) + (sep (cond ((string= header "Keywords") ", ") + ((string= header "X-Label") " ") + ((string= header "X-Keywords") ", ") + (t ", "))) + (taglist (if oldtags (copy-sequence oldtags) '())) + tagstr) + (dolist (tag (split-string-and-unquote retag) taglist) + (cond + ((string-match "^\\+\\(.+\\)" tag) + (setq taglist (push (match-string 1 tag) taglist))) + ((string-match "^\\-\\(.+\\)" tag) + (setq taglist (delete (match-string 1 tag) taglist))) + (t + (setq taglist (push tag taglist))))) + + (setq taglist (sort (delete-dups taglist) 'string<)) + (setq tagstr (mapconcat 'identity taglist sep)) + + (setq tagstr (replace-regexp-in-string "[\\&]" "\\\\\\&" tagstr)) + (setq tagstr (replace-regexp-in-string "[/]" "\\&" tagstr)) + + (if (not (mu4e~contains-line-matching (concat header ":.*") path)) + ;; Add tags header just before the content + (mu4e~replace-first-line-matching + "^$" (concat header ": " tagstr "\n") path) + + ;; replaces keywords, restricted to the header + (mu4e~replace-first-line-matching + (concat header ":.*") + (concat header ": " tagstr) + path)) + + (mu4e-message (concat "tagging: " (mapconcat 'identity taglist ", "))) + (mu4e-refresh-message path maildir))) + +(defun mu4e-action-show-thread (msg) + "Show all messages that are in the same thread as the message +at point." + (let ((msgid (mu4e-message-field msg :message-id))) + (when msgid + (let ((mu4e-headers-show-threads t) + (mu4e-headers-include-related t)) + (mu4e-headers-search + (format "msgid:%s" msgid)))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(provide 'mu4e-actions) diff --git a/_spacemacs.d/local/mu4e/mu4e-compose.el b/_spacemacs.d/local/mu4e/mu4e-compose.el new file mode 100644 index 0000000..af2b34b --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-compose.el @@ -0,0 +1,778 @@ +;; -*-mode: emacs-lisp; tab-width: 8; indent-tabs-mode: t -*- +;; mu4e-compose.el -- part of mu4e, the mu mail user agent for emacs +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; In this file, various functions to compose/send messages, piggybacking on +;; gnus' message mode + + +;; Magic / Rupe Goldberg + +;; 1) When we reply/forward a message, we get it from the backend, ie: +;; we send to the backend (mu4e-compose): +;; compose type:reply docid:30935 +;; backend responds with: +;; (:compose reply :original ( .... <original message> )) + + +;; 2) When we compose a message, message and headers are separated by +;; `mail-header-separator', ie. '--text follows this line--. We use +;; before-save-hook and after-save-hook to remove/re-add this special line, so +;; it stays in the buffer, but never hits the disk. +;; see: +;; mu4e~compose-insert-mail-header-separator +;; mu4e~compose-remove-mail-header-separator +;; +;; (maybe we can get away with remove it only just before sending? what does +;; gnus do?) + +;; 3) When sending a message, we want to do a few things: +;; a) move the message from drafts to the sent folder (maybe; depends on +;; `mu4e-sent-messages-behavior') +;; b) if it's a reply, mark the replied-to message as "R", i.e. replied +;; if it's a forward, mark the forwarded message as "P", i.e. passed (forwarded) +;; c) kill all buffers looking at the sent message + +;; a) is dealt with by message-mode, but we need to tell it where to move the +;; sent message. We do this by adding an Fcc: header with the target folder, +;; see `mu4e~compose-setup-fcc-maybe'. Since message-mode does not natively +;; understand maildirs, we also need to tell it what to do, so we also set +;; `message-fcc-handler-function' there. Finally, we add the the message in +;; the sent-folder to the database. +;; +;; b) this is handled in `mu4e~compose-set-parent-flag' +;; +;; c) this is handled in our handler for the `sent'-message from the backend +;; (`mu4e-sent-handler') +;; + +;;; Code: + +(eval-when-compile (byte-compile-disable-warning 'cl-functions)) +(require 'cl) + +(require 'message) +(require 'mail-parse) +(require 'smtpmail) +(require 'rfc2368) + +(require 'mu4e-utils) +(require 'mu4e-vars) +(require 'mu4e-proc) +(require 'mu4e-actions) +(require 'mu4e-message) +(require 'mu4e-draft) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; 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 on 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)) + :safe 'symbolp + :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-format-flowed nil + "Whether to compose messages to be sent as format=flowed (or + with long lines if `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. + +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) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defun mu4e-compose-attach-captured-message () + "Insert the last captured message file as an attachment. +Messages are captured with `mu4e-action-capture-message'." + (interactive) + (unless mu4e-captured-message + (mu4e-warn "No message has been captured")) + (let ((path (plist-get mu4e-captured-message :path))) + (unless (file-exists-p path) + (mu4e-warn "Captured message file not found")) + (mml-attach-file + path + "message/rfc822" + (or (plist-get mu4e-captured-message :subject) "No subject") + "attachment"))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +;; 'fcc' refers to saving a copy of a sent message to a certain folder. that's +;; what these 'Sent mail' folders are for! +;; +;; We let message mode take care of this by adding a field + +;; Fcc: <full-path-to-message-in-target-folder> + +;; in the "message-send-hook" (ie., just before sending). message mode will +;; then take care of the saving when the message is actually sent. +;; +;; note, where and if you make this copy depends on the value of +;; `mu4e-sent-messages-behavior'. + +(defun mu4e~compose-setup-fcc-maybe () + "Maybe setup Fcc, based on `mu4e-sent-messages-behavior'. +If needed, set the Fcc header, and register the handler function." + (let* ((sent-behavior + ;; Note; we cannot simply use functionp here, since at least + ;; delete is a function, too... + (if (member mu4e-sent-messages-behavior '(delete trash sent)) + mu4e-sent-messages-behavior + (if (functionp mu4e-sent-messages-behavior) + (funcall mu4e-sent-messages-behavior) + mu4e-sent-messages-behavior))) + (mdir + (case sent-behavior + (delete nil) + (trash (mu4e-get-trash-folder mu4e-compose-parent-message)) + (sent (mu4e-get-sent-folder mu4e-compose-parent-message)) + (otherwise + (mu4e-error "unsupported value '%S' `mu4e-sent-messages-behavior'." + mu4e-sent-messages-behavior)))) + (fccfile (and mdir + (concat mu4e-maildir mdir "/cur/" + (mu4e~draft-message-filename-construct "S"))))) + ;; if there's an fcc header, add it to the file + (when fccfile + (message-add-header (concat "Fcc: " fccfile "\n")) + ;; sadly, we cannot define as 'buffer-local'... this will screw up gnus + ;; etc. if you run it after mu4e so, (hack hack) we reset it to the old + ;; handler after we've done our thing. + (setq message-fcc-handler-function + (lexical-let ((maildir mdir) (old-handler message-fcc-handler-function)) + (lambda (file) + (setq message-fcc-handler-function old-handler) ;; reset the fcc handler + (write-file file) ;; writing maildirs files is easy + (mu4e~proc-add file (or maildir "/")))))))) ;; update the database + +(defvar mu4e-compose-hidden-headers + `("^References:" "^Face:" "^X-Face:" + "^X-Draft-From:" "^User-agent:") + "Hidden headers when composing.") + +(defun mu4e~compose-hide-headers () + "Hide the headers as per `mu4e-compose-hidden-headers'." + (let ((message-hidden-headers mu4e-compose-hidden-headers)) + (message-hide-headers))) + +(defconst mu4e~compose-address-fields-regexp + "^\\(To\\|B?Cc\\|Reply-To\\|From\\):") + +(defun mu4e~compose-register-message-save-hooks () + "Just before saving, we remove the mail-header-separator; just +after saving we restore it; thus, the separator should never +appear on disk." + (add-hook 'before-save-hook + 'mu4e~draft-remove-mail-header-separator nil t) + (add-hook 'after-save-hook + (lambda () + (mu4e~compose-set-friendly-buffer-name) + (mu4e~draft-insert-mail-header-separator) + ;; hide some headers again + (mu4e~compose-hide-headers) + (set-buffer-modified-p nil) + (mu4e-message "Saved (%d lines)" (count-lines (point-min) (point-max))) + ;; update the file on disk -- ie., without the separator + (mu4e~proc-add (buffer-file-name) mu4e~draft-drafts-folder)) nil t)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; address completion; inspired by org-contacts.el and +;; https://github.com/nordlow/elisp/blob/master/mine/completion-styles-cycle.el +(defun mu4e~compose-complete-handler (str pred action) + (cond + ((eq action nil) + (try-completion str mu4e~contacts pred)) + ((eq action t) + (all-completions str mu4e~contacts pred)) + ((eq action 'metadata) + ;; our contacts are already sorted - just need to tell the + ;; completion machinery not to try to undo that... + '(metadata + (display-sort-function . mu4e~sort-contacts-for-completion) + (cycle-sort-function . mu4e~sort-contacts-for-completion))))) + +(defun mu4e~compose-complete-contact (&optional start) + "Complete the text at START with a contact. +Ie. either 'name <email>' or 'email')." + (interactive) + (let ((mail-abbrev-mode-regexp mu4e~compose-address-fields-regexp) + (eoh ;; end-of-headers + (save-excursion + (goto-char (point-min)) + (search-forward-regexp mail-header-separator nil t)))) + ;; try to complete only when we're in the headers area, + ;; looking at an address field. + (when (and eoh (> eoh (point)) (mail-abbrev-in-expansion-header-p)) + (let* ((end (point)) + (start + (or start + (save-excursion + (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*") + (goto-char (match-end 0)) + (point))))) + (list start end 'mu4e~compose-complete-handler))))) + +(defun mu4e~compose-setup-completion () + "Set up auto-completion of addresses." + (set (make-local-variable 'completion-ignore-case) t) + (set (make-local-variable 'completion-cycle-threshold) 7) + (add-to-list (make-local-variable 'completion-styles) 'substring) + (add-hook 'completion-at-point-functions + 'mu4e~compose-complete-contact nil t)) + +(defun mu4e~remove-refs-maybe () + "Remove the References: header if the In-Reply-To header is +missing. This allows the user to effectively start a new +message-thread by removing the In-Reply-To header." + (unless (message-fetch-field "in-reply-to") + (message-remove-header "References"))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defvar mu4e-compose-mode-map nil + "Keymap for \"*mu4e-compose*\" buffers.") +(unless mu4e-compose-mode-map + (setq mu4e-compose-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-S-u") 'mu4e-update-mail-and-index) + (define-key map (kbd "C-c C-u") 'mu4e-update-mail-and-index) + (define-key map (kbd "C-c C-k") 'mu4e-message-kill-buffer) + (define-key map (kbd "M-q") 'mu4e-fill-paragraph) + map))) + +(defun mu4e-fill-paragraph (&optional region) + "If `use-hard-newlines', takes a multi-line paragraph and makes +it into a single line of text. Assume paragraphs are separated +by blank lines. If `use-hard-newlines' is not enabled, this +simply executes `fill-paragraph'." + ;; Inspired by https://www.emacswiki.org/emacs/UnfillParagraph + (interactive (progn (barf-if-buffer-read-only) '(t))) + (if mu4e-compose-format-flowed + (let ((fill-column (point-max)) + (use-hard-newlines nil)); rfill "across" hard newlines + (fill-paragraph nil region)) + (fill-paragraph nil region))) + +(defun mu4e-toggle-use-hard-newlines () + (interactive) + (setq use-hard-newlines (not use-hard-newlines)) + (if use-hard-newlines + (turn-off-auto-fill) + (turn-on-auto-fill))) + + +(defvar mu4e-compose-mode-abbrev-table nil) +(define-derived-mode mu4e-compose-mode message-mode "mu4e:compose" + "Major mode for the mu4e message composition, derived from `message-mode'. +\\{message-mode-map}." + (progn + (use-local-map mu4e-compose-mode-map) + + (set (make-local-variable 'global-mode-string) '(:eval (mu4e-context-label))) + + (set (make-local-variable 'message-signature) mu4e-compose-signature) + ;; set this to allow mu4e to work when gnus-agent is unplugged in gnus + (set (make-local-variable 'message-send-mail-real-function) nil) + (make-local-variable 'message-default-charset) + ;; if the default charset is not set, use UTF-8 + (unless message-default-charset + (setq message-default-charset 'utf-8)) + ;; make sure mu4e is started in the background (ie. we don't want to error + ;; out when sending the message; better to do it now if there's a problem) + (mu4e~start) ;; start mu4e in background, if needed + (mu4e~compose-register-message-save-hooks) + ;; set the default directory to the user's home dir; this is probably more + ;; useful e.g. when finding an attachment file the directory the current + ;; mail files lives in... + (setq default-directory (expand-file-name "~/")) + + ;; offer completion for e-mail addresses + (when mu4e-compose-complete-addresses + (mu4e~compose-setup-completion)) + + (when mu4e-compose-format-flowed + (turn-off-auto-fill) + (setq truncate-lines nil + word-wrap t + use-hard-newlines t) + ;; Set the marks in the fringes before activating visual-line-mode + (set (make-local-variable 'visual-line-fringe-indicators) + '(left-curly-arrow right-curly-arrow)) + (visual-line-mode t)) + + (define-key-after + (lookup-key message-mode-map [menu-bar text]) + [mu4e-hard-newlines] + '(menu-item "Format=flowed" mu4e-toggle-use-hard-newlines + :button (:toggle . use-hard-newlines) + :help "Toggle format=flowed" + :visible (eq major-mode 'mu4e-compose-mode) + :enable mu4e-compose-format-flowed) + 'sep) + + (define-key-after + (lookup-key mml-mode-map [menu-bar Attachments]) + [mu4e-compose-attach-captured-message] + '(menu-item "Attach captured message" + mu4e-compose-attach-captured-message + :help "Attach message captured in Headers View (with 'a c')" + :visible (eq major-mode 'mu4e-compose-mode)) + (quote Attach\ External...)) + + ;; setup the fcc-stuff, if needed + (add-hook 'message-send-hook + (lambda () ;; mu4e~compose-save-before-sending + ;; when in-reply-to was removed, remove references as well. + (when (eq mu4e~compose-type 'reply) + (mu4e~remove-refs-maybe)) + (when use-hard-newlines + (mu4e-send-harden-newlines)) + ;; for safety, always save the draft before sending + (set-buffer-modified-p t) + (save-buffer) + (mu4e~compose-setup-fcc-maybe) + (widen)) nil t) + ;; when the message has been sent. + (add-hook 'message-sent-hook + (lambda () ;; mu4e~compose-mark-after-sending + (setq mu4e-sent-func 'mu4e-sent-handler) + (mu4e~proc-sent (buffer-file-name) mu4e~draft-drafts-folder)) nil t)) + ;; mark these two hooks as permanent-local, so they'll survive mode-changes + ;; (put 'mu4e~compose-save-before-sending 'permanent-local-hook t) + (put 'mu4e~compose-mark-after-sending 'permanent-local-hook t)) + +(defun mu4e-send-harden-newlines () + "Set the hard property to all newlines." + (save-excursion + (goto-char (point-min)) + (while (search-forward "\n" nil t) + (put-text-property (1- (point)) (point) 'hard t)))) + +(defconst mu4e~compose-buffer-max-name-length 30 + "Maximum length of the mu4e-send-buffer-name.") + +(defvar mu4e~compose-type nil + "Compose-type for this buffer.") + +(defun mu4e~compose-set-friendly-buffer-name (&optional compose-type) + "Set some user-friendly buffer name based on the compose type." + (let* ((subj (message-field-value "subject")) + (subj (unless (and subj (string-match "^[:blank:]*$" subj)) subj)) + (str (or subj + (case compose-type + (reply "*reply*") + (forward "*forward*") + (otherwise "*draft*"))))) + (rename-buffer (generate-new-buffer-name + (truncate-string-to-width str + mu4e~compose-buffer-max-name-length + nil nil t))))) + +(defun* mu4e~compose-handler (compose-type &optional original-msg includes) + "Create a new draft message, or open an existing one. + +COMPOSE-TYPE determines the kind of message to compose and is a +symbol, either `reply', `forward', `edit', `resend' `new'. `edit' +is for editing existing (draft) messages. When COMPOSE-TYPE is +`reply' or `forward', MSG should be a message plist. If +COMPOSE-TYPE is `new', ORIGINAL-MSG should be nil. + +Optionally (when forwarding, replying) ORIGINAL-MSG is the original +message we will forward / reply to. + +Optionally (when forwarding) INCLUDES contains a list of + (:file-name <filename> :mime-type <mime-type> :disposition <disposition>) +for the attachements to include; file-name refers to +a file which our backend has conveniently saved for us (as a +tempfile)." + + ;; Run the hooks defined for `mu4e-compose-pre-hook'. If compose-type is + ;; `reply', `forward' or `edit', `mu4e-compose-parent-message' points to the + ;; message being forwarded or replied to, otherwise it is nil. + (set (make-local-variable 'mu4e-compose-parent-message) original-msg) + (put 'mu4e-compose-parent-message 'permanent-local t) + ;; maybe switch the context + (mu4e~context-autoswitch mu4e-compose-parent-message + mu4e-compose-context-policy) + (run-hooks 'mu4e-compose-pre-hook) + + ;; this opens (or re-opens) a messages with all the basic headers set. + (condition-case nil + (mu4e-draft-open compose-type original-msg) + (quit (kill-buffer) (mu4e-message "Operation aborted") + (return-from mu4e~compose-handler))) + ;; insert mail-header-separator, which is needed by message mode to separate + ;; headers and body. will be removed before saving to disk + (mu4e~draft-insert-mail-header-separator) + ;; include files -- e.g. when forwarding a message with attachments, + ;; we take those from the original. + (save-excursion + (goto-char (point-max)) ;; put attachments at the end + (dolist (att includes) + (mml-attach-file + (plist-get att :file-name) (plist-get att :mime-type)))) + ;; buffer is not user-modified yet + (mu4e~compose-set-friendly-buffer-name compose-type) + (set-buffer-modified-p nil) + ;; now jump to some useful positions, and start writing that mail! + + (if (member compose-type '(new forward)) + (message-goto-to) + (message-goto-body)) + ;; bind to `mu4e-compose-parent-message' of compose buffer + (set (make-local-variable 'mu4e-compose-parent-message) original-msg) + (put 'mu4e-compose-parent-message 'permanent-local t) + ;; remember the compose-type + (set (make-local-variable 'mu4e~compose-type) compose-type) + (put 'mu4e~compose-type 'permanent-local t) + + ;; hide some headers + (mu4e~compose-hide-headers) + ;; switch on the mode + (mu4e-compose-mode) + (when mu4e-compose-in-new-frame + ;; make sure to close the frame when we're done with the message these are + ;; all buffer-local; + (push 'delete-frame message-exit-actions) + (push 'delete-frame message-postpone-actions))) + +(defun mu4e-sent-handler (docid path) + "Handler function, called with DOCID and PATH for the just-sent +message. For Forwarded ('Passed') and Replied messages, try to set +the appropriate flag at the message forwarded or replied-to." + (mu4e~compose-set-parent-flag path) + (when (file-exists-p path) ;; maybe the draft was not saved at all + (mu4e~proc-remove docid)) + ;; kill any remaining buffers for the draft file, or they will hang around... + ;; this seems a bit hamfisted... + (dolist (buf (buffer-list)) + (when (and (buffer-file-name buf) + (string= (buffer-file-name buf) path)) + (if message-kill-buffer-on-exit + (kill-buffer buf)))) + ;; now, try to go back to some previous buffer, in the order + ;; view->headers->main + (if (buffer-live-p mu4e~view-buffer) + (switch-to-buffer mu4e~view-buffer) + (if (buffer-live-p mu4e~headers-buffer) + (switch-to-buffer mu4e~headers-buffer) + ;; if all else fails, back to the main view + (when (fboundp 'mu4e) (mu4e)))) + (mu4e-message "Message sent")) + +(defun mu4e-message-kill-buffer () + "Wrapper around `message-kill-buffer'. +It restores mu4e window layout after killing the compose-buffer." + (interactive) + (let ((current-buffer (current-buffer))) + (message-kill-buffer) + ;; Compose buffer killed + (when (not (equal current-buffer (current-buffer))) + ;; Restore mu4e + (if mu4e-compose-in-new-frame + (delete-frame) + (if (buffer-live-p mu4e~view-buffer) + (switch-to-buffer mu4e~view-buffer) + (if (buffer-live-p mu4e~headers-buffer) + (switch-to-buffer mu4e~headers-buffer) + ;; if all else fails, back to the main view + (when (fboundp 'mu4e) (mu4e)))))))) + +(defun mu4e~compose-set-parent-flag (path) + "Set the 'replied' \"R\" flag on messages we replied to, and the +'passed' \"F\" flag on message we have forwarded. + +If a message has an 'in-reply-to' header, it is considered a reply +to the message with the corresponding message id. If it does not +have an 'in-reply-to' header, but does have a 'references' header, +it is considered to be a forward message for the message +corresponding with the /last/ message-id in the references header. + +Now, if the message has been determined to be either a forwarded +message or a reply, we instruct the server to update that message +with resp. the 'P' (passed) flag for a forwarded message, or the +'R' flag for a replied message. The original messages are also +marked as Seen. + +Function assumes that it's executed in the context of the message +buffer." + (let ((buf (find-file-noselect path))) + (when buf + (with-current-buffer buf + (message-narrow-to-headers-or-head) + (let ((in-reply-to (message-fetch-field "in-reply-to")) + (forwarded-from) + (references (message-fetch-field "references"))) + (unless in-reply-to + (when references + (with-temp-buffer ;; inspired by `message-shorten-references'. + (insert references) + (goto-char (point-min)) + (let ((refs)) + (while (re-search-forward "<[^ <]+@[^ <]+>" nil t) + (push (match-string 0) refs)) + ;; the last will be the first + (setq forwarded-from (first refs)))))) + ;; remove the <> + (when (and in-reply-to (string-match "<\\(.*\\)>" in-reply-to)) + (mu4e~proc-move (match-string 1 in-reply-to) nil "+R-N")) + (when (and forwarded-from (string-match "<\\(.*\\)>" forwarded-from)) + (mu4e~proc-move (match-string 1 forwarded-from) nil "+P-N"))))))) + +(defun mu4e-compose (compose-type) + "Start composing a message of COMPOSE-TYPE, where COMPOSE-TYPE +is a symbol, one of `reply', `forward', `edit', `resend' +`new'. All but `new' take the message at point as input. Symbol +`edit' is only allowed for draft messages." + (let ((msg (mu4e-message-at-point 'noerror))) + ;; some sanity checks + (unless (or msg (eq compose-type 'new)) + (mu4e-warn "No message at point")) + (unless (member compose-type '(reply forward edit resend new)) + (mu4e-error "Invalid compose type '%S'" compose-type)) + (when (and (eq compose-type 'edit) + (not (member 'draft (mu4e-message-field msg :flags)))) + (mu4e-warn "Editing is only allowed for draft messages")) + + ;; 'new is special, since it takes no existing message as arg; therefore, we + ;; don't need to involve the backend, and call the handler *directly* + (if (eq compose-type 'new) + (mu4e~compose-handler 'new) + ;; otherwise, we need the doc-id + (let* ((docid (mu4e-message-field msg :docid)) + ;; decrypt (or not), based on `mu4e-decryption-policy'. + (decrypt + (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)))) + ;; if there's a visible view window, select that before starting composing + ;; a new message, so that one will be replaced by the compose window. The + ;; 10-or-so line headers buffer is not a good place to write it... + (let ((viewwin (get-buffer-window mu4e~view-buffer))) + (when (window-live-p viewwin) + (select-window viewwin))) + ;; talk to the backend + (mu4e~proc-compose compose-type decrypt docid))))) + +(defun mu4e-compose-reply () + "Compose a reply for the message at point in the headers buffer." + (interactive) + (mu4e-compose 'reply)) + +(defun mu4e-compose-forward () + "Forward the message at point in the headers buffer." + (interactive) + (mu4e-compose 'forward)) + +(defun mu4e-compose-edit () + "Edit the draft message at point in the headers buffer. +This is only possible if the message at point is, in fact, a +draft message." + (interactive) + (mu4e-compose 'edit)) + +(defun mu4e-compose-resend () + "Resend the message at point in the headers buffer." + (interactive) + (mu4e-compose 'resend)) + +(defun mu4e-compose-new () + "Start writing a new message." + (interactive) + (mu4e-compose 'new)) + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; mu4e-compose-func and mu4e-send-func are wrappers so we can set ourselves +;; as default emacs mailer (define-mail-user-agent etc.) + +;;;###autoload +(defun mu4e~compose-mail (&optional to subject other-headers continue + switch-function yank-action send-actions return-action) + "This is mu4e's implementation of `compose-mail'." + + ;; 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 + (mu4e~compose-handler 'new) + + (when (message-goto-to) ;; reset to-address, if needed + (message-delete-line)) + (message-add-header (concat "To: " to "\n")) + + (when (message-goto-subject) ;; reset subject, if needed + (message-delete-line)) + (message-add-header (concat "Subject: " subject "\n")) + + ;; add any other headers specified + (when other-headers + (message-add-header other-headers)) + + ;; yank message + (if (bufferp yank-action) + (list 'insert-buffer yank-action) + yank-action) + + ;; try to put the user at some reasonable spot... + (if (not to) + (message-goto-to) + (if (not subject) + (message-goto-subject) + (message-goto-body)))) + +;; happily, we can re-use most things from message mode +;;;###autoload +(define-mail-user-agent 'mu4e-user-agent + 'mu4e~compose-mail + 'message-send-and-exit + 'message-kill-buffer + 'message-send-hook) +;; Without this `mail-user-agent' cannot be set to `mu4e-user-agent' +;; through customize, as the custom type expects a function. Not +;; sure whether this function is actually ever used; if it is then +;; returning the symbol is probably the correct thing to do, as other +;; such functions suggest. +(defun mu4e-user-agent () + 'mu4e-user-agent) + +(defun mu4e~compose-browse-url-mail (url &optional ignored) + "Adapter for `browse-url-mailto-function." + (let* ((headers (rfc2368-parse-mailto-url url)) + (to (cdr (assoc "To" headers))) + (subject (cdr (assoc "Subject" headers))) + (body (cdr (assoc "Body" headers)))) + (mu4e~compose-mail to subject) + (if body + (progn + (message-goto-body) + (insert body) + (if (not to) + (message-goto-to) + (if (not subject) + (message-goto-subject) + (message-goto-body))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defun mu4e-compose-goto-top () + "Go to the beginning of the message or buffer. +Go to the beginning of the message or, if already there, go to the +beginning of the buffer." + (interactive) + (let ((old-position (point))) + (message-goto-body) + (when (equal (point) old-position) + (goto-char (point-min))))) + +(define-key mu4e-compose-mode-map + (vector 'remap 'beginning-of-buffer) 'mu4e-compose-goto-top) + +(defun mu4e-compose-goto-bottom () + "Go to the end of the message or buffer. +Go to the end of the message (before signature) or, if already there, go to the +end of the buffer." + (interactive) + (let ((old-position (point)) + (message-position (save-excursion (message-goto-body) (point)))) + (goto-char (point-max)) + (when (re-search-backward message-signature-separator message-position t) + (forward-line -1)) + (when (equal (point) old-position) + (goto-char (point-max))))) + +(define-key mu4e-compose-mode-map + (vector 'remap 'end-of-buffer) 'mu4e-compose-goto-bottom) + +(provide 'mu4e-compose) + +;; Load mu4e completely even when this file was loaded through +;; autoload. +(require 'mu4e) diff --git a/_spacemacs.d/local/mu4e/mu4e-context.el b/_spacemacs.d/local/mu4e/mu4e-context.el new file mode 100644 index 0000000..f5f8fff --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-context.el @@ -0,0 +1,157 @@ +; mu4e-context.el -- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2015-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; A mu4e 'context' is a a set of variable-settings and functions, which can be +;; used e.g. to switch between accounts. + +(eval-when-compile (byte-compile-disable-warning 'cl-functions)) +(require 'cl) + +(require 'mu4e-utils) + +(defvar mu4e-contexts nil "The list of `mu4e-context' objects +describing mu4e's contexts.") + +(defvar mu4e~context-current nil + "The current context; for internal use. Use + `mu4e-context-switch' to change it.") + +(defun mu4e-context-current () + "Get the currently active context, or nil if there is none." + mu4e~context-current) + +(defun mu4e-context-label () + "Propertized string with the current context name, or \"\" if + there is none." + (if (mu4e-context-current) + (concat "[" (propertize (mu4e~quote-for-modeline + (mu4e-context-name (mu4e-context-current))) + 'face 'mu4e-title-face) "]") "")) + +(defstruct mu4e-context + "A mu4e context object with the following members: +- `name': the name of the context, eg. \"Work\" or \"Private\".' +- `enter-func': a parameterless function invoked when entering + this context, or nil +- `leave-func':a parameterless fuction invoked when leaving this + context, or nil +- `match-func': a function called when comnposing a new messages, + and takes a message plist +for the message replied to or forwarded, and nil +otherwise. Before composing a new message, `mu4e' switches to the +first context for which `match-func' return t." + name ;; name of the context, e.g. "work" + (enter-func nil) ;; function invoked when entering the context + (leave-func nil) ;; function invoked when leaving the context + (match-func nil) ;; function that takes a msg-proplist, and return t + ;; if it matches, nil otherwise + vars) ;; alist of variables. + +(defun mu4e~context-ask-user (prompt) + "Let user choose some context based on its name." + (when mu4e-contexts + (let* ((names (map 'list (lambda (context) + (cons (mu4e-context-name context) context)) + mu4e-contexts)) + (context (mu4e-read-option prompt names))) + (or context (mu4e-error "No such context"))))) + +(defun mu4e-context-switch (&optional force name) + "Switch context to a context with NAME which is part of +`mu4e-contexts'; if NAME is nil, query user. + +If the new context is the same and the current context, only +switch (run associated functions) when prefix argument FORCE is +non-nil." + (interactive "P") + (unless mu4e-contexts + (mu4e-error "No contexts defined")) + (let* ((names (map 'list (lambda (context) + (cons (mu4e-context-name context) context)) + mu4e-contexts)) + (context + (if name + (cdr-safe (assoc name names)) + (mu4e~context-ask-user "Switch to context: ")))) + (unless context (mu4e-error "No such context")) + ;; if new context is same as old one one switch with FORCE is set. + (when (or force (not (eq context (mu4e-context-current)))) + (when (and (mu4e-context-current) + (mu4e-context-leave-func mu4e~context-current)) + (funcall (mu4e-context-leave-func mu4e~context-current))) + ;; enter the new context + (when (mu4e-context-enter-func context) + (funcall (mu4e-context-enter-func context))) + (when (mu4e-context-vars context) + (mapc #'(lambda (cell) + (set (car cell) (cdr cell))) + (mu4e-context-vars context))) + (setq mu4e~context-current context) + (mu4e-message "Switched context to %s" (mu4e-context-name context))) + context)) + +(defun mu4e~context-autoswitch (&optional msg policy) + "When contexts are defined but there is no context yet, switch +to the first whose :match-func return non-nil. If none of them +match, return the first. For MSG and POLICY, see `mu4e-context-determine'." + (when mu4e-contexts + (let ((context (mu4e-context-determine msg policy))) + (when context (mu4e-context-switch + nil (mu4e-context-name context)))))) + +(defun mu4e-context-determine (msg &optional policy) + "Return the first context with a match-func that returns t. MSG +points to the plist for the message replied to or forwarded, or +nil if there is no such MSG; similar to what +`mu4e-compose-pre-hook' does. + +POLICY specifies how to do the determination. If POLICY is +'always-ask, we ask the user unconditionally. + +In all other cases, if any context matches (using its match +function), this context is returned. If none of the contexts +match, POLICY determines what to do: + +- pick-first: pick the first of the contexts available +- ask: ask the user +- ask-if-none: ask if there is no context yet +- otherwise, return nil. Effectively, this leaves the current context as it is." + (when mu4e-contexts + (if (eq policy 'always-ask) + (mu4e~context-ask-user "Select context: ") + (or ;; is there a matching one? + (find-if (lambda (context) + (when (mu4e-context-match-func context) + (funcall (mu4e-context-match-func context) msg))) + mu4e-contexts) + ;; no context found yet; consult policy + (case policy + (pick-first (car mu4e-contexts)) + (ask (mu4e~context-ask-user "Select context: ")) + (ask-if-none (or (mu4e-context-current) + (mu4e~context-ask-user "Select context: "))) + (otherwise nil)))))) + +(provide 'mu4e-context) + diff --git a/_spacemacs.d/local/mu4e/mu4e-contrib.el b/_spacemacs.d/local/mu4e/mu4e-contrib.el new file mode 100644 index 0000000..8952401 --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-contrib.el @@ -0,0 +1,164 @@ +;;; mu4e-contrib.el -- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2013-2016 Dirk-Jan C. Binnema + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Some user-contributed functions for mu4e + +;; Contributed by sabof + +(require 'mu4e) + +(defun mu4e-headers-mark-all-unread-read () + "Put a ! \(read) mark on all visible unread messages." + (interactive) + (mu4e-headers-mark-for-each-if + (cons 'read nil) + (lambda (msg param) + (memq 'unread (mu4e-msg-field msg :flags))))) + +(defun mu4e-headers-flag-all-read () + "Flag all visible messages as \"read\"." + (interactive) + (mu4e-headers-mark-all-unread-read) + (mu4e-mark-execute-all t)) + +;;; + +(defun mu4e-headers-mark-all () + "Mark all messages within current query results and ask user to execute which action." + (interactive) + (mu4e-headers-mark-for-each-if + (cons 'something nil) + (lambda (msg param) t)) + (mu4e-mark-execute-all)) + +;;; + +;;; Bookmark handlers +;; +;; Allow bookmarking a mu4e buffer in regular emacs bookmarks. + +;; Probably this can be moved to mu4e-view.el. +(add-hook 'mu4e-view-mode-hook + #'(lambda () + (set (make-local-variable 'bookmark-make-record-function) + 'mu4e-view-bookmark-make-record))) +;; And this can be moved to mu4e-headers.el. +(add-hook 'mu4e-headers-mode-hook + #'(lambda () + (set (make-local-variable 'bookmark-make-record-function) + 'mu4e-view-bookmark-make-record))) + +(defun mu4e-view-bookmark-make-record () + "Make a bookmark entry for a mu4e buffer." + (let* ((msg (mu4e-message-at-point)) + (maildir (plist-get msg :maildir)) + (date (format-time-string "%Y%m%d" (plist-get msg :date))) + (query (format "maildir:%s date:%s" maildir date)) + (docid (plist-get msg :docid)) + (mode (symbol-name major-mode)) + (subject (or (plist-get msg :subject) "No subject"))) + `(,subject + ,@(bookmark-make-record-default 'no-file 'no-context) + (location . (,query . ,docid)) + (mode . ,mode) + (handler . mu4e-bookmark-jump)))) + +(defun mu4e-bookmark-jump (bookmark) + "Handler function for record returned by `mu4e-view-bookmark-make-record'. +BOOKMARK is a bookmark name or a bookmark record." + (let* ((path (bookmark-prop-get bookmark 'location)) + (mode (bookmark-prop-get bookmark 'mode)) + (docid (cdr path)) + (query (car path))) + (call-interactively 'mu4e) + (mu4e-headers-search query) + (sit-for 0.5) + (mu4e~headers-goto-docid docid) + (mu4e~headers-highlight docid) + (unless (string= mode "mu4e-headers-mode") + (call-interactively 'mu4e-headers-view-message) + (run-with-timer 0.1 nil + (lambda (bmk) + (bookmark-default-handler + `("" (buffer . ,(current-buffer)) . + ,(bookmark-get-bookmark-record bmk)))) + bookmark)))) + + + +;;; handling spam with Bogofilter with possibility to define it for SpamAssassin +;;; contributed by Gour + +;; to add the actions to the menu, you can use something like: + +;; (add-to-list 'mu4e-headers-actions +;; '("sMark as spam" . mu4e-register-msg-as-spam) t) +;; (add-to-list 'mu4e-headers-actions +;; '("hMark as ham" . mu4e-register-msg-as-ham) t) + +(defvar mu4e-register-as-spam-cmd nil + "Command for invoking spam processor to register message as spam, +for example for bogofilter, use \"/usr/bin/bogofilter -Ns < %s\" ") + +(defvar mu4e-register-as-ham-cmd nil + "Command for invoking spam processor to register message as ham. +For example for bogofile, use \"/usr/bin/bogofilter -Sn < %s\"") + +(defun mu4e-register-msg-as-spam (msg) + "Mark message as spam." + (interactive) + (let* ((path (shell-quote-argument (mu4e-message-field msg :path))) + (command (format mu4e-register-as-spam-cmd path))) ;; re-register msg as spam + (shell-command command)) +(mu4e-mark-at-point 'delete nil)) + +(defun mu4e-register-msg-as-ham (msg) + "Mark message as ham." + (interactive) + (let* ((path (shell-quote-argument(mu4e-message-field msg :path))) + (command (format mu4e-register-as-ham-cmd path))) ;; re-register msg as ham + (shell-command command)) +(mu4e-mark-at-point 'something nil)) + +;; (add-to-list 'mu4e-view-actions +;; '("sMark as spam" . mu4e-view-register-msg-as-spam) t) +;; (add-to-list 'mu4e-view-actions +;; '("hMark as ham" . mu4e-view-register-msg-as-ham) t) + +(defun mu4e-view-register-msg-as-spam (msg) + "Mark message as spam (view mode)." + (interactive) + (let* ((path (shell-quote-argument (mu4e-message-field msg :path))) + (command (format mu4e-register-as-spam-cmd path))) + (shell-command command)) + (mu4e-view-mark-for-delete)) + +(defun mu4e-view-register-msg-as-ham (msg) + "Mark message as ham (view mode)." + (interactive) + (let* ((path (shell-quote-argument(mu4e-message-field msg :path))) + (command (format mu4e-register-as-ham-cmd path))) + (shell-command command)) + (mu4e-view-mark-for-something)) + +;;; end of spam-filtering functions + +(provide 'mu4e-contrib) diff --git a/_spacemacs.d/local/mu4e/mu4e-draft.el b/_spacemacs.d/local/mu4e/mu4e-draft.el new file mode 100644 index 0000000..c41e38d --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-draft.el @@ -0,0 +1,474 @@ +;; mu4e-draft.el -- part of mu4e, the mu mail user agent for emacs +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; In this file, various functions to create draft messages + +;; Code + +(eval-when-compile (byte-compile-disable-warning 'cl-functions)) +(require 'cl) + +(require 'mu4e-vars) +(require 'mu4e-utils) +(require 'mu4e-message) +(require 'message) ;; mail-header-separator +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defcustom mu4e-compose-dont-reply-to-self nil + "If non-nil, don't include self (that is, any member of +`mu4e-user-mail-address-list') in replies." + :type 'boolean + :group 'mu4e-compose) + +(defcustom mu4e-compose-cite-function + (or message-cite-function 'message-cite-original-without-signature) + "The function to use to cite message in replies and forwarded +messages. This is the mu4e-specific version of +`message-cite-function'." + :type 'function + :group 'mu4e-compose) + +(defcustom mu4e-compose-signature + (or message-signature "Sent with my mu4e") + "The message signature (i.e. the blob at the bottom of +messages). This is the mu4e-specific version of +`message-signature'." + :group 'mu4e-compose) + +(defcustom mu4e-compose-signature-auto-include t + "Whether to automatically include a message-signature in new +messages (if it is set)." + :type 'boolean + :group 'mu4e-compose) + +(defcustom mu4e-compose-auto-include-date nil + "Whether to include a date header when starting to draft a +message; if nil, only do so when sending the message." + :type 'boolean + :group 'mu4e-compose) + +(defcustom mu4e-compose-in-new-frame nil + "Whether to compose messages in a new frame instead of the +current window." + :type 'boolean + :group 'mu4e-compose) + +(defvar mu4e-user-agent-string + (format "mu4e %s; emacs %s" mu4e-mu-version emacs-version) + "The User-Agent string for mu4e.") + +(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 +its settings apply." + (with-temp-buffer + (when (fboundp 'mu4e-view-message-text) ;; keep bytecompiler happy + (let ((mu4e-view-date-format "%Y-%m-%dT%T%z")) + (insert (mu4e-view-message-text msg))) + (message-yank-original) + (goto-char (point-min)) + (push-mark (point-max)) + ;; set the the signature separator to 'loose', since in the real world, + ;; many message don't follow the standard... + (let ((message-signature-separator "^-- *$") + (message-signature-insert-empty-line t)) + (funcall mu4e-compose-cite-function)) + (pop-mark) + (goto-char (point-min)) + (mu4e~fontify-cited) + (buffer-string)))) + +(defun mu4e~draft-header (hdr val) + "Return a header line of the form \"HDR: VAL\". +If VAL is nil, return nil." + ;; note: the propertize here is currently useless, since gnus sets its own + ;; later. + (when val (format "%s: %s\n" + (propertize hdr 'face 'mu4e-header-key-face) + (propertize val 'face 'mu4e-header-val-face)))) + +(defun mu4e~draft-references-construct (msg) + "Construct the value of the References: header based on MSG as a +comma-separated string. Normally, this the concatenation of the +existing References + In-Reply-To (which may be empty, an note +that :references includes the old in-reply-to as well) and the +message-id. If the message-id is empty, returns the old +References. If both are empty, return nil." + (let* ( ;; these are the ones from the message being replied to / forwarded + (refs (mu4e-message-field msg :references)) + (msgid (mu4e-message-field msg :message-id)) + ;; now, append in + (refs (if (and msgid (not (string= msgid ""))) + (append refs (list msgid)) refs)) + ;; no doubles + (refs (delete-duplicates refs :test #'equal))) + (mapconcat (lambda (id) (format "<%s>" id)) refs " "))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; determine the recipient fields for new messages + +(defun mu4e~draft-recipients-list-to-string (lst) + "Convert a lst LST of address cells into a string with a list of +e-mail addresses. If LST is nil, returns nil." + (when lst + (mapconcat + (lambda (addrcell) + (let ((name (car addrcell)) + (email (cdr addrcell))) + (if name + (format "%s <%s>" (mu4e~rfc822-quoteit name) email) + (format "%s" email)))) + lst ", "))) + +(defun mu4e~draft-address-cell-equal (cell1 cell2) + "Return t if CELL1 and CELL2 have the same e-mail address. +The comparison is done case-insensitively. If the cells done +match return nil. CELL1 and CELL2 are cons cells of the +form (NAME . EMAIL)." + (string= + (downcase (or (cdr cell1) "")) + (downcase (or (cdr cell2) "")))) + + +(defun mu4e~draft-create-to-lst (origmsg) + "Create a list of address for the To: in a new message, based on +the original message ORIGMSG. If the Reply-To address is set, use +that, otherwise use the From address. Note, whatever was in the To: +field before, goes to the Cc:-list (if we're doing a reply-to-all). +Special case: if we were the sender of the original, we simple copy +the list form the original." + (let ((reply-to + (or (plist-get origmsg :reply-to) (plist-get origmsg :from)))) + (delete-duplicates reply-to :test #'mu4e~draft-address-cell-equal) + (if mu4e-compose-dont-reply-to-self + (delete-if + (lambda (to-cell) + (member-if + (lambda (addr) + (string= (downcase addr) (downcase (cdr to-cell)))) + mu4e-user-mail-address-list)) + reply-to) + reply-to))) + + +(defun mu4e~draft-create-cc-lst (origmsg reply-all) + "Create a list of address for the Cc: in a new message, based on +the original message ORIGMSG, and whether it's a reply-all." + (when reply-all + (let* ((cc-lst ;; get the cc-field from the original, remove dups + (delete-duplicates + (append + (plist-get origmsg :to) + (plist-get origmsg :cc)) + :test #'mu4e~draft-address-cell-equal)) + ;; now we have the basic list, but we must remove + ;; addresses also in the to list + (cc-lst + (delete-if + (lambda (cc-cell) + (find-if + (lambda (to-cell) + (mu4e~draft-address-cell-equal cc-cell to-cell)) + (mu4e~draft-create-to-lst origmsg))) + cc-lst)) + ;; finally, we need to remove ourselves from the cc-list + ;; unless mu4e-compose-keep-self-cc is non-nil + (cc-lst + (if (or mu4e-compose-keep-self-cc (null user-mail-address)) + cc-lst + (delete-if + (lambda (cc-cell) + (member-if + (lambda (addr) + (string= (downcase addr) (downcase (cdr cc-cell)))) + mu4e-user-mail-address-list)) + cc-lst)))) + cc-lst))) + +(defun mu4e~draft-recipients-construct (field origmsg &optional reply-all) + "Create value (a string) for the recipient field FIELD (a +symbol, :to or :cc), based on the original message ORIGMSG, +and (optionally) REPLY-ALL which indicates this is a reply-to-all +message. Return nil if there are no recipients for the particular field." + (mu4e~draft-recipients-list-to-string + (case field + (:to + (mu4e~draft-create-to-lst origmsg)) + (:cc + (mu4e~draft-create-cc-lst origmsg reply-all)) + (otherwise + (mu4e-error "Unsupported field"))))) + + +(defun mu4e~draft-from-construct () + "Construct a value for the From:-field of the reply to MSG, +based on `user-full-name' and `user-mail-address'; if the latter is +nil, function returns nil." + (when user-mail-address + (if user-full-name + (format "%s <%s>" (mu4e~rfc822-quoteit user-full-name) user-mail-address) + (format "%s" user-mail-address)))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mu4e~draft-insert-mail-header-separator () + "Insert `mail-header-separator' in the first empty line of the message. +`message-mode' needs this line to know where the headers end and +the body starts. Note, in `mu4e-compose-mode', we use +`before-save-hook' and `after-save-hook' to ensure that this +separator is never written to the message file. Also see +`mu4e-remove-mail-header-separator'." + ;; we set this here explicitly, since (as it has happened) a wrong + ;; value for this (such as "") breaks address completion and other things + (set (make-local-variable 'mail-header-separator) "--text follows this line--") + (put 'mail-header-separator 'permanent-local t) + (save-excursion + ;; make sure there's not one already + (mu4e~draft-remove-mail-header-separator) + (let ((sepa (propertize mail-header-separator + 'intangible t + ;; don't make this read-only, message-mode + ;; seems to require it being writable in some cases + ;;'read-only "Can't touch this" + 'rear-nonsticky t + 'font-lock-face 'mu4e-compose-separator-face))) + (widen) + ;; search for the first empty line + (goto-char (point-min)) + (if (search-forward-regexp "^$" nil t) + (replace-match sepa) + (progn ;; no empty line? then prepend one + (goto-char (point-max)) + (insert "\n" sepa)))))) + +(defun mu4e~draft-remove-mail-header-separator () + "Remove `mail-header-separator; we do this before saving a +file (and restore it afterwards), to ensure that the separator +never hits the disk. Also see `mu4e~draft-insert-mail-header-separator." + (save-excursion + (widen) + (goto-char (point-min)) + ;; remove the --text follows this line-- separator + (when (search-forward-regexp (concat "^" mail-header-separator) nil t) + (let ((inhibit-read-only t)) + (replace-match ""))))) + + +(defun mu4e~draft-reply-all-p (origmsg) + "Ask user whether she wants to reply to *all* recipients. +If there is just one recipient of ORIGMSG do nothing." + (let* ((recipnum + (+ (length (mu4e~draft-create-to-lst origmsg)) + (length (mu4e~draft-create-cc-lst origmsg t)))) + (response + (if (= recipnum 1) + 'all ;; with one recipient, we can reply to 'all'.... + (mu4e-read-option + "Reply to " + `( (,(format "all %d recipients" recipnum) . all) + ("sender only" . sender-only)))))) + (eq response 'all))) + +(defun mu4e~draft-message-filename-construct (&optional flagstr) + "Construct a randomized name for a message file with flags FLAGSTR. +It looks something like + <time>-<random>.<hostname>:2, +You can append flags." + (let* ((sysname (if (fboundp 'system-name) + (system-name) + (with-no-warnings system-name))) + (hostname (downcase + (save-match-data + (substring sysname + (string-match "^[^.]+" sysname) + (match-end 0)))))) + (format "%s.%04x%04x%04x%04x.%s:2,%s" + (format-time-string "%s" (current-time)) + (random 65535) (random 65535) (random 65535) (random 65535) + hostname (or flagstr "")))) + +(defun mu4e~draft-common-construct () + "Construct the common headers for each message." + (concat + (mu4e~draft-header "User-agent" mu4e-user-agent-string) + (when mu4e-compose-auto-include-date + (mu4e~draft-header "Date" (message-make-date))))) + +(defconst mu4e~draft-reply-prefix "Re: " + "String to prefix replies with.") + +(defun mu4e~draft-reply-construct (origmsg) + "Create a draft message as a reply to original message +ORIGMSG. Replying-to-self is a special; in that case, the To and Cc +fields will be the same as in the original." + (let* ((reply-to-self (mu4e-message-contact-field-matches-me origmsg :from)) + (recipnum + (+ (length (mu4e~draft-create-to-lst origmsg)) + (length (mu4e~draft-create-cc-lst origmsg t)))) + ;; reply-to-self implies reply-all + (reply-all (or reply-to-self (mu4e~draft-reply-all-p origmsg))) + (old-msgid (plist-get origmsg :message-id)) + (subject + (concat mu4e~draft-reply-prefix + (message-strip-subject-re (or (plist-get origmsg :subject) ""))))) + (concat + (mu4e~draft-header "From" (or (mu4e~draft-from-construct) "")) + (mu4e~draft-header "Reply-To" mu4e-compose-reply-to-address) + + (if reply-to-self + ;; When we're replying to ourselves, simply keep the same headers. + (concat + (mu4e~draft-header "To" (mu4e~draft-recipients-list-to-string + (mu4e-message-field origmsg :to))) + (mu4e~draft-header "Cc" (mu4e~draft-recipients-list-to-string + (mu4e-message-field origmsg :cc)))) + + ;; if there's no-one in To, copy the CC-list + (if (zerop (length (mu4e~draft-create-to-lst origmsg))) + (mu4e~draft-header "To" (mu4e~draft-recipients-construct :cc origmsg reply-all)) + ;; otherwise... + (concat + (mu4e~draft-header "To" (mu4e~draft-recipients-construct :to origmsg)) + (mu4e~draft-header "Cc" (mu4e~draft-recipients-construct :cc origmsg + reply-all))))) + (mu4e~draft-header "Subject" subject) + (mu4e~draft-header "References" + (mu4e~draft-references-construct origmsg)) + (mu4e~draft-common-construct) + (when old-msgid + (mu4e~draft-header "In-reply-to" (format "<%s>" old-msgid))) + "\n\n" + (mu4e~draft-cite-original origmsg)))) + +(defconst mu4e~draft-forward-prefix "Fwd: " + "String to prefix replies with.") + +(defun mu4e~draft-forward-construct (origmsg) + "Create a draft forward message for original message ORIGMSG." + (let ((subject + (or (plist-get origmsg :subject) ""))) + (concat + (mu4e~draft-header "From" (or (mu4e~draft-from-construct) "")) + (mu4e~draft-header "Reply-To" mu4e-compose-reply-to-address) + (mu4e~draft-header "To" "") + (mu4e~draft-common-construct) + (mu4e~draft-header "References" + (mu4e~draft-references-construct origmsg)) + (mu4e~draft-header "Subject" + (concat + ;; if there's no Fwd: yet, prepend it + (if (string-match "^Fwd:" subject) + "" + mu4e~draft-forward-prefix) + subject)) + "\n\n" + (mu4e~draft-cite-original origmsg)))) + +(defun mu4e~draft-newmsg-construct () + "Create a new message." + (concat + (mu4e~draft-header "From" (or (mu4e~draft-from-construct) "")) + (mu4e~draft-header "Reply-To" mu4e-compose-reply-to-address) + (mu4e~draft-header "To" "") + (mu4e~draft-header "Subject" "") + (mu4e~draft-common-construct))) + +(defvar mu4e~draft-drafts-folder nil + "The drafts-folder for this compose buffer, based on +`mu4e-drafts-folder', which is evaluated once.") + +(defun mu4e~draft-open-file (path) + "Open the the draft file at PATH." + (if mu4e-compose-in-new-frame + (find-file-other-frame path) + (find-file path))) + +(defun mu4e~draft-determine-path (draft-dir) + "Determine the path for a new draft file." + (format "%s/%s/cur/%s" + mu4e-maildir draft-dir (mu4e~draft-message-filename-construct "DS"))) + + +(defun mu4e-draft-open (compose-type &optional msg) + "Open a draft file for a new message (when COMPOSE-TYPE is `reply', `forward' or `new'), +open an existing draft (when COMPOSE-TYPE is `edit'), or re-send +an existing message (when COMPOSE-TYPE is `resend'). + +The name of the draft folder is constructed from the +concatenation of `mu4e-maildir' and `mu4e-drafts-folder' (the +latter will be evaluated). The message file name is a unique name +determined by `mu4e-send-draft-file-name'. The initial contents +will be created from either `mu4e~draft-reply-construct', or +`mu4e~draft-forward-construct' or `mu4e~draft-newmsg-construct'." + (unless mu4e-maildir (mu4e-error "mu4e-maildir not set")) + (let ((draft-dir nil)) + (case compose-type + + (edit + ;; 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))) + (mu4e~draft-open-file (mu4e-message-field msg :path))) + + (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))) + (let ((draft-path (mu4e~draft-determine-path draft-dir))) + (copy-file (mu4e-message-field msg :path) draft-path) + (mu4e~draft-open-file draft-path))) + + ((reply forward new) + ;; case-3: creating a new message; in this case, we can determing + ;; mu4e-get-drafts-folder + (setq draft-dir (mu4e-get-drafts-folder msg)) + (let ((draft-path (mu4e~draft-determine-path draft-dir)) + (initial-contents + (case compose-type + (reply (mu4e~draft-reply-construct msg)) + (forward (mu4e~draft-forward-construct msg)) + (new (mu4e~draft-newmsg-construct))))) + (mu4e~draft-open-file draft-path) + (insert initial-contents) + (newline) + ;; include the message signature (if it's set) + (if (and mu4e-compose-signature-auto-include mu4e-compose-signature) + (let ((message-signature mu4e-compose-signature)) + (save-excursion + (message-insert-signature) + (mu4e~fontify-signature)))))) + (t (mu4e-error "unsupported compose-type %S" compose-type))) + ;; evaluate mu4e~drafts-drafts-folder once, here, and use that value + ;; throughout. + (set (make-local-variable 'mu4e~draft-drafts-folder) draft-dir) + (put 'mu4e~draft-drafts-folder 'permanent-local t) + (unless mu4e~draft-drafts-folder + (mu4e-error "failed to determine drafts folder")))) + + +(provide 'mu4e-draft) diff --git a/_spacemacs.d/local/mu4e/mu4e-headers.el b/_spacemacs.d/local/mu4e/mu4e-headers.el new file mode 100644 index 0000000..f45d7b4 --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-headers.el @@ -0,0 +1,1694 @@ +;;; mu4e-headers.el -- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; In this file are function related mu4e-headers-mode, to creating the list of +;; one-line descriptions of emails, aka 'headers' (not to be confused with +;; headers like 'To:' or 'Subject:') + +;; Code: +(eval-when-compile (byte-compile-disable-warning 'cl-functions)) +(require 'cl) + +(require 'fringe) +(require 'hl-line) + +(require 'mu4e-utils) ;; utility functions +(require 'mu4e-proc) +(require 'mu4e-vars) +(require 'mu4e-mark) +(require 'mu4e-compose) +(require 'mu4e-actions) +(require 'mu4e-message) + +;; the headers view +(defgroup mu4e-headers nil + "Settings for the headers view." + :group 'mu4e) + +(defcustom mu4e-headers-fields + '( (:human-date . 12) + (:flags . 6) + (:mailing-list . 10) + (:from . 22) + (:subject . nil)) + "A list of header fields to show in the headers buffer. +Each element has the form (HEADER . WIDTH), where HEADER is one of +the available headers (see `mu4e-header-info') and WIDTH is the +respective width in characters. A width of `nil' means +'unrestricted', and this is best reserved for the rightmost (last) +field. Note that emacs may become very slow with excessively long +lines (1000s of characters), so if you regularly get such messages, +you want to avoid fields with `nil' altogether." + :type `(repeat (cons (choice ,@(mapcar (lambda (h) + (list 'const :tag + (plist-get (cdr h) :help) + (car h))) + mu4e-header-info)) + (choice (integer :tag "width") + (const :tag "unrestricted width" nil)))) + :group 'mu4e-headers) + +(defcustom mu4e-headers-date-format "%x" + "Date format to use in the headers view. +In the format of `format-time-string'." + :type 'string + :group 'mu4e-headers) + +(defcustom mu4e-headers-time-format "%X" + "Time format to use in the headers view. +In the format of `format-time-string'." + :type 'string + :group 'mu4e-headers) + +(defcustom mu4e-headers-long-date-format "%c" + "Date format to use in the headers view tooltip. +In the format of `format-time-string'." + :type 'string + :group 'mu4e-headers) + +(defcustom mu4e-headers-visible-lines 10 + "Number of lines to display in the header view when using the +horizontal split-view. This includes the header-line at the top, +and the mode-line." + :type 'integer + :group 'mu4e-headers) + +(defcustom mu4e-headers-visible-columns 30 + "Number of columns to display for the header view when using the +vertical split-view." + :type 'integer + :group 'mu4e-headers) + +(defcustom mu4e-headers-auto-update t + "Whether to automatically update the current headers buffer if an +indexing operation showed changes." + :type 'boolean + :group 'mu4e-headers) + +(defcustom mu4e-headers-results-limit 500 + "Maximum number of results to show; this affects performance +quite a bit, especially when `mu4e-headers-include-related' is +non-nil. Set to -1 for no limits, and you temporarily (for one +query) ignore the limit by pressing a C-u before invoking the +search." + :type '(choice (const :tag "Unlimited" -1) + (integer :tag "Limit")) + :group 'mu4e-headers) + +(make-obsolete-variable 'mu4e-search-results-limit + 'mu4e-headers-results-limit "0.9.9.5-dev6") + +(defcustom mu4e-headers-skip-duplicates nil + "With this option set to non-nil, show only one of duplicate +messages. This is useful when you have multiple copies of the same +message, which is a common occurence for example when using Gmail +and offlineimap." + :type 'boolean + :group 'mu4e-headers) + +(defcustom mu4e-headers-include-related nil + "With this option set to non-nil, not just return the matches for +a searches, but also messages that are related (through their +references) to these messages. This can be useful e.g. to include +sent messages into message threads." + :type 'boolean + :group 'mu4e-headers) + +(defcustom mu4e-headers-visible-flags + '(draft flagged new passed replied seen trashed attach encrypted signed unread) + "An ordered list of flags to show in the headers buffer. Each +element is a symbol in the list (DRAFT FLAGGED NEW PASSED +REPLIED SEEN TRASHED ATTACH ENCRYPTED SIGNED UNREAD)." + :type '(set + (const :tag "Draft" draft) + (const :tag "Flagged" flagged) + (const :tag "New" new) + (const :tag "Passed" passed) + (const :tag "Replied" replied) + (const :tag "Seen" seen) + (const :tag "Trashed" trashed) + (const :tag "Attach" attach) + (const :tag "Encrypted" encrypted) + (const :tag "Signed" signed) + (const :tag "Unread" unread)) + :group 'mu4e-headers) + +(defcustom mu4e-headers-found-hook nil + "Hook run just *after* all of the headers for the last search +query have been received and are displayed." + :type 'hook + :group 'mu4e-headers) + +(defcustom mu4e-headers-search-bookmark-hook nil + "Hook run just after we invoke a bookmarked search. This +function receives the query as its parameter. + +The reason to use this instead of `mu4e-headers-search-hook' +is if you only want to execute a hook when a search is entered +via a bookmark, e.g. if you'd like to treat the bookmarks as a +custom folder and change the options for the search, +e.g. `mu4e-headers-show-threads', `mu4e-headers-include-related', +`mu4e-headers-skip-duplicates` or `mu4e-headers-results-limit'." + :type 'hook + :group 'mu4e-headers) + +(defcustom mu4e-headers-search-hook nil + "Hook run just before executing a new search operation. This +function receives the query as its parameter. + +This is a more general hook facility than the +`mu4e-headers-search-bookmark-hook'. It gets called on every +executed search, not just those that are invoked via bookmarks, +but also manually invoked searches." + :type 'hook + :group 'mu4e-headers) + +(defvar mu4e-headers-sort-field :date + "Field to sort the headers by. +Field must be a symbol, one of: :date, :subject, :size, :prio, +:from, :to.") + +(defvar mu4e-headers-sort-direction 'descending + "Direction to sort by; a symbol either `descending' (sorting + Z->A) or `ascending' (sorting A->Z).") + +;; marks for headers of the form; each is a cons-cell (basic . fancy) +;; each of which is basic ascii char and something fancy, respectively +(defvar mu4e-headers-draft-mark '("D" . "âš’") "Draft.") +(defvar mu4e-headers-flagged-mark '("F" . "✚") "Flagged.") +(defvar mu4e-headers-new-mark '("N" . "✱") "New.") +(defvar mu4e-headers-passed-mark '("P" . "â¯") "Passed (fwd).") +(defvar mu4e-headers-replied-mark '("R" . "â®") "Replied.") +(defvar mu4e-headers-seen-mark '("S" . "✔") "Seen.") +(defvar mu4e-headers-trashed-mark '("T" . "âš") "Trashed.") +(defvar mu4e-headers-attach-mark '("a" . "âš“") "W/ attachments.") +(defvar mu4e-headers-encrypted-mark '("x" . "âš´") "Encrypted.") +(defvar mu4e-headers-signed-mark '("s" . "☡") "Signed.") +(defvar mu4e-headers-unread-mark '("u" . "⎕") "Unread.") + +;; thread prefix marks +(defvar mu4e-headers-has-child-prefix '("+" . "â—¼ ") "Parent.") +(defvar mu4e-headers-empty-parent-prefix '("-" . "â—½ ") "Orphan.") +(defvar mu4e-headers-first-child-prefix '("\\" . "â”—â–¶") "First child.") +(defvar mu4e-headers-duplicate-prefix '("=" . "≡ ") "Duplicate.") +(defvar mu4e-headers-default-prefix '("|" . "│ ") "Default.") + +(defvar mu4e-headers-actions + '( ("capture message" . mu4e-action-capture-message) + ("show this thread" . mu4e-action-show-thread)) + "List of actions to perform on messages in the headers list. +The actions are of the form (NAME SHORTCUT FUNC) where: +* NAME is the name of the action (e.g. \"Count lines\") +* SHORTCUT is a one-character shortcut to call this action +* FUNC is a function which receives a message plist as an argument.") + +(defvar mu4e-headers-custom-markers + '(("Older than" + (lambda (msg date) (time-less-p (mu4e-msg-field msg :date) date)) + (lambda () (mu4e-get-time-date "Match messages before: "))) + ("Newer than" + (lambda (msg date) (time-less-p date (mu4e-msg-field msg :date))) + (lambda () (mu4e-get-time-date "Match messages after: "))) + ("Bigger than" + (lambda (msg bytes) (> (mu4e-msg-field msg :size) (* 1024 bytes))) + (lambda () (read-number "Match messages bigger than (Kbytes): ")))) + "List of custom markers -- functions to mark message that match +some custom function. Each of the list members has the following format: + (NAME PREDICATE-FUNC PARAM-FUNC) +* NAME is the name of the predicate function, and the first character +is the shortcut (so keep those unique). +* PREDICATE-FUNC is a function that takes two parameters, MSG +and (optionally) PARAM, and should return non-nil when there's a +match. +* PARAM-FUNC is function that is evaluated once, and its value is then passed to +PREDICATE-FUNC as PARAM. This is useful for getting user-input.") + +(defvar mu4e-headers-show-threads t + "Whether to show threads in the headers list.") + +(defvar mu4e-headers-full-search nil + "Whether to show all results. +If this is nil show results up to `mu4e-search-results-limit')") + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +;;;; internal variables/constants ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; docid cookies +(defconst mu4e~headers-docid-pre "\376" + "Each header starts (invisibly) with the `mu4e~headers-docid-pre', +followed by the docid, followed by `mu4e~headers-docid-post'.") +(defconst mu4e~headers-docid-post "\377" + "Each header starts (invisibly) with the `mu4e~headers-docid-pre', +followed by the docid, followed by `mu4e~headers-docid-post'.") + +(defvar mu4e~headers-view-win nil + "The view window connected to this headers view.") + +(defvar mu4e~headers-sort-field-choices + '( ("date" . :date) + ("from" . :from) + ("prio" . :prio) + ("zsize" . :size) + ("subject" . :subject) + ("to" . :to)) + "List of cells describing the various sort-options. +In the format needed for `mu4e-read-option'.") +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defun mu4e~headers-clear () + "Clear the header buffer and related data structures." + (when (buffer-live-p mu4e~headers-buffer) + (let ((inhibit-read-only t)) + (with-current-buffer mu4e~headers-buffer + (setq mu4e~view-msg nil) + (mu4e~mark-clear) + (erase-buffer))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; handler functions +;; +;; next are a bunch of handler functions; those will be called from mu4e~proc in +;; response to output from the server process + +(defun mu4e~headers-view-handler (msg) + "Handler function for displaying a message." + (mu4e-view msg mu4e~headers-buffer)) + +(defun mu4e~headers-view-this-message-p (docid) + "Is DOCID currently being viewed?" + (let ((viewbuf (get-buffer mu4e~view-buffer-name))) + (when (and viewbuf (buffer-live-p viewbuf)) + (with-current-buffer viewbuf + (eq docid (plist-get mu4e~view-msg :docid)))))) + +(defun mu4e~headers-update-handler (msg is-move) + "Update handler, will be called when a message has been updated +in the database. This function will update the current list of +headers." + (when (buffer-live-p mu4e~headers-buffer) + (with-current-buffer mu4e~headers-buffer + (let* ((docid (mu4e-message-field msg :docid)) + (initial-message-at-point (mu4e~headers-docid-at-point)) + (initial-column (current-column)) + (point (mu4e~headers-docid-pos docid))) + (when point ;; is the message present in this list? + + ;; if it's marked, unmark it now + (when (mu4e-mark-docid-marked-p docid) + (mu4e-mark-set 'unmark)) + + ;; re-use the thread info from the old one; this is needed because + ;; *update* messages don't have thread info by themselves (unlike + ;; search results) + ;; since we still have the search results, re-use + ;; those + (plist-put msg :thread + (mu4e~headers-field-for-docid docid :thread)) + + ;; first, remove the old one (otherwise, we'd have two headers with + ;; the same docid... + (mu4e~headers-remove-handler docid) + + ;; if we're actually viewing this message (in mu4e-view mode), we + ;; update it; that way, the flags can be updated, as well as the path + ;; (which is useful for viewing the raw message) + (when (mu4e~headers-view-this-message-p docid) + (mu4e-view msg mu4e~headers-buffer)) + + ;; now, if this update was about *moving* a message, we don't show it + ;; anymore (of course, we cannot be sure if the message really no + ;; longer matches the query, but this seem a good heuristic. + ;; if it was only a flag-change, show the message with its updated flags. + (unless is-move + (mu4e~headers-header-handler msg point)) + + (if (and initial-message-at-point + (mu4e~headers-goto-docid initial-message-at-point)) + (progn + (move-to-column initial-column) + (mu4e~headers-highlight initial-message-at-point)) + ;; attempt to highlight the corresponding line and make it visible + (mu4e~headers-highlight docid)) + ))))) + +(defun mu4e~headers-remove-handler (docid) + "Remove handler, will be called when a message with DOCID has +been removed from the database. This function will hide the removed +message from the current list of headers. If the message is not +present, don't do anything." + (when (buffer-live-p mu4e~headers-buffer) + (with-current-buffer mu4e~headers-buffer + (mu4e~headers-remove-header docid t) + + ;; if we were viewing this message, close it now. + (when (and (mu4e~headers-view-this-message-p docid) + (buffer-live-p mu4e~view-buffer)) + (with-current-buffer mu4e~view-buffer + ;; XXX it seems this sometimes fails; investigate; + ;; for now, just ignore the error + (ignore-errors + (kill-buffer-and-window))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defsubst mu4e~headers-contact-str (contacts) + "Turn the list of contacts CONTACTS (with elements (NAME . EMAIL) +into a string." + (mapconcat + (lambda (ct) + (let ((name (car ct)) (email (cdr ct))) + (or name email "?"))) contacts ", ")) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defsubst mu4e~headers-thread-prefix (thread) + "Calculate the thread prefix based on thread info THREAD." + (when thread + (let ((get-prefix + (lambda (cell) (if mu4e-use-fancy-chars (cdr cell) (car cell))))) + (concat + (make-string (* (if (plist-get thread :empty-parent) 0 1) + (plist-get thread :level)) ?\s) + (cond + ((plist-get thread :has-child) + (funcall get-prefix mu4e-headers-has-child-prefix)) + ((plist-get thread :empty-parent) + (funcall get-prefix mu4e-headers-empty-parent-prefix)) + ((plist-get thread :first-child) + (funcall get-prefix mu4e-headers-first-child-prefix)) + ((plist-get thread :duplicate) + (funcall get-prefix mu4e-headers-duplicate-prefix)) + (t + (funcall get-prefix mu4e-headers-default-prefix))) + " ")))) + +(defsubst mu4e~headers-flags-str (flags) + "Get a display string for the flags. +Note that `mu4e-flags-to-string' is for internal use only; this +function is for display. (This difference is significant, since +internally, the Maildir spec determines what the flags look like, +while our display may be different)." + (let ((str "") + (get-prefix + (lambda (cell) (if mu4e-use-fancy-chars (cdr cell) (car cell))))) + (dolist (flag mu4e-headers-visible-flags) + (when (member flag flags) + (setq str + (concat str + (case flag + ('draft (funcall get-prefix mu4e-headers-draft-mark)) + ('flagged (funcall get-prefix mu4e-headers-flagged-mark)) + ('new (funcall get-prefix mu4e-headers-new-mark)) + ('passed (funcall get-prefix mu4e-headers-passed-mark)) + ('replied (funcall get-prefix mu4e-headers-replied-mark)) + ('seen (funcall get-prefix mu4e-headers-seen-mark)) + ('trashed (funcall get-prefix mu4e-headers-trashed-mark)) + ('attach (funcall get-prefix mu4e-headers-attach-mark)) + ('encrypted (funcall get-prefix mu4e-headers-encrypted-mark)) + ('signed (funcall get-prefix mu4e-headers-signed-mark)) + ('unread (funcall get-prefix mu4e-headers-unread-mark))))))) + str)) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defconst mu4e-headers-from-or-to-prefix '("" . "To ") + "Prefix for the :from-or-to field. +It's a cons cell with the car element being the From: prefix, the +cdr element the To: prefix.") + +(defsubst mu4e~headers-from-or-to (msg) + "When the from address for message MSG is one of the the user's addresses, +\(as per `mu4e-user-mail-address-list'), show the To address; +otherwise ; show the from address; prefixed with the appropriate +`mu4e-headers-from-or-to-prefix'." + (let ((addr (cdr-safe (car-safe (mu4e-message-field msg :from))))) + (if (mu4e-user-mail-address-p addr) + (concat (cdr mu4e-headers-from-or-to-prefix) + (mu4e~headers-contact-str (mu4e-message-field msg :to))) + (concat (car mu4e-headers-from-or-to-prefix) + (mu4e~headers-contact-str (mu4e-message-field msg :from)))))) + +(defsubst mu4e~headers-human-date (msg) + "Show a 'human' date. +If the date is today, show the time, otherwise, show the +date. The formats used for date and time are +`mu4e-headers-date-format' and `mu4e-headers-time-format'." + (let ((date (mu4e-msg-field msg :date))) + (if (equal date '(0 0 0)) + "None" + (let ((day1 (decode-time date)) + (day2 (decode-time (current-time)))) + (if (and + (eq (nth 3 day1) (nth 3 day2)) ;; day + (eq (nth 4 day1) (nth 4 day2)) ;; month + (eq (nth 5 day1) (nth 5 day2))) ;; year + (format-time-string mu4e-headers-time-format date) + (format-time-string mu4e-headers-date-format date)))))) + + +(defsubst mu4e~headers-thread-subject (msg) + "Get the subject if it is the first one in a thread; otherwise, +return the thread-prefix without the subject-text. In other words, +show the subject of a thread only once, similar to e.g. 'mutt'." + (let* ((tinfo (mu4e-message-field msg :thread)) + (subj (mu4e-msg-field msg :subject))) + (concat ;; prefix subject with a thread indicator + (mu4e~headers-thread-prefix tinfo) + (if (or (not tinfo) (zerop (plist-get tinfo :level)) + (plist-get tinfo :empty-parent)) + (truncate-string-to-width subj 600) "")))) + + +(defsubst mu4e~headers-mailing-list (list) + "Get some identifier for the mailing list." + (if list + (propertize (mu4e-get-mailing-list-shortname list) 'help-echo list) + "")) + +(defun mu4e~headers-custom-field (msg field) + "Show some custom header field, or raise an error if it is not +found." + (let* ((item (or (assoc field mu4e-header-info-custom) + (mu4e-error "field %S not found" field))) + (func (or (plist-get (cdr-safe item) :function) + (mu4e-error "no :function defined for field %S %S" field (cdr item))))) + (funcall func msg))) + +(defun mu4e~headers-field-apply-basic-properties (msg field val width) + (case field + (:subject + (concat ;; prefix subject with a thread indicator + (mu4e~headers-thread-prefix (mu4e-message-field msg :thread)) + ;; "["(plist-get (mu4e-message-field msg :thread) :path) "] " + ;; work-around: emacs' display gets really slow when lines are too long; + ;; so limit subject length to 600 + (truncate-string-to-width val 600))) + (:thread-subject (mu4e~headers-thread-subject msg)) + ((:maildir :path :message-id) val) + ((:to :from :cc :bcc) (mu4e~headers-contact-str val)) + ;; if we (ie. `user-mail-address' is the 'From', show + ;; 'To', otherwise show From + (:from-or-to (mu4e~headers-from-or-to msg)) + (:date (format-time-string mu4e-headers-date-format val)) + (:mailing-list (mu4e~headers-mailing-list val)) + (:human-date (propertize (mu4e~headers-human-date msg) + 'help-echo (format-time-string + mu4e-headers-long-date-format + (mu4e-msg-field msg :date)))) + (:flags (propertize (mu4e~headers-flags-str val) + 'help-echo (format "%S" val))) + (:tags (propertize (mapconcat 'identity val ", "))) + (:size (mu4e-display-size val)) + (t (mu4e~headers-custom-field msg field)))) + +(defun mu4e~headers-field-truncate-to-width (_msg _field val width) + "Truncate VAL to WIDTH." + (if width + (truncate-string-to-width val width 0 ?\s t) + val)) + +(defvar mu4e~headers-field-handler-functions + '(mu4e~headers-field-apply-basic-properties + mu4e~headers-field-truncate-to-width)) + +(defun mu4e~headers-field-handler (f-w msg) + "Create a description of the field of MSG described by F-W." + (let* ((field (car f-w)) + (width (cdr f-w)) + (val (mu4e-message-field msg (car f-w)))) + (dolist (func mu4e~headers-field-handler-functions) + (setq val (funcall func msg field val width))) + val)) + +(defvar mu4e~headers-line-handler-functions + '(mu4e~headers-line-apply-flag-face)) + +(defun mu4e~headers-line-apply-flag-face (msg line) + "Adjust LINE's face property based on FLAGS." + (let* ((flags (mu4e-message-field msg :flags)) + (face (cond + ((memq 'trashed flags) 'mu4e-trashed-face) + ((memq 'draft flags) 'mu4e-draft-face) + ((or (memq 'unread flags) (memq 'new flags)) + 'mu4e-unread-face) + ((memq 'flagged flags) 'mu4e-flagged-face) + ((memq 'replied flags) 'mu4e-replied-face) + ((memq 'passed flags) 'mu4e-forwarded-face) + (t 'mu4e-header-face)))) + ;; hmmm, this only works with emacs 24.4+ + (when (fboundp 'add-face-text-property) + (add-face-text-property 0 (length line) face t line)) + line)) + +(defun mu4e~headers-line-handler (msg line) + (dolist (func mu4e~headers-line-handler-functions) + (setq line (funcall func msg line))) + line) + +;; note: this function is very performance-sensitive +(defun mu4e~headers-header-handler (msg &optional point) + "Create a one line description of MSG in this buffer, at POINT, +if provided, or at the end of the buffer otherwise." + (let ((docid (mu4e-message-field msg :docid)) + (line (mapconcat (lambda (f-w) + (mu4e~headers-field-handler f-w msg)) + mu4e-headers-fields " "))) + (setq line (mu4e~headers-line-handler msg line)) + (mu4e~headers-add-header line docid point msg))) + +(defconst mu4e~no-matches "No matching messages found") +(defconst mu4e~end-of-results "End of search results") + +(defun mu4e~headers-found-handler (count) + "Create a one line description of the number of headers found +after the end of the search results." + (when (buffer-live-p mu4e~headers-buffer) + (with-current-buffer mu4e~headers-buffer + (save-excursion + (goto-char (point-max)) + (let ((inhibit-read-only t) + (str (if (zerop count) mu4e~no-matches mu4e~end-of-results))) + (insert (propertize str 'face 'mu4e-system-face 'intangible t)) + (unless (zerop count) + (mu4e-message "Found %d matching message%s" + count (if (= 1 count) "" "s"))))) + ;; if we need to jump to some specific message, do so now + (goto-char (point-min)) + (when mu4e~headers-msgid-target + (mu4e-headers-goto-message-id mu4e~headers-msgid-target)) + (when mu4e~headers-view-target + (mu4e-headers-view-message)) ;; view the message at point + (setq mu4e~headers-view-target nil + mu4e~headers-msgid-target nil)) + (when (mu4e~headers-docid-at-point) + (mu4e~headers-highlight (mu4e~headers-docid-at-point))) + ;; run-hooks + (run-hooks 'mu4e-headers-found-hook))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defmacro mu4e~headers-defun-mark-for (mark) + "Define a function mu4e~headers-mark-MARK." + (let ((funcname (intern (format "mu4e-headers-mark-for-%s" mark))) + (docstring (format "Mark header at point with %s." mark))) + `(progn + (defun ,funcname () ,docstring + (interactive) + (mu4e-headers-mark-and-next ',mark)) + (put ',funcname 'definition-name ',mark)))) + +(mu4e~headers-defun-mark-for refile) +(mu4e~headers-defun-mark-for something) +(mu4e~headers-defun-mark-for delete) +(mu4e~headers-defun-mark-for flag) +(mu4e~headers-defun-mark-for move) +(mu4e~headers-defun-mark-for read) +(mu4e~headers-defun-mark-for trash) +(mu4e~headers-defun-mark-for unflag) +(mu4e~headers-defun-mark-for untrash) +(mu4e~headers-defun-mark-for unmark) +(mu4e~headers-defun-mark-for unread) +(mu4e~headers-defun-mark-for action) + + +;;; headers-mode and mode-map ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defvar mu4e-headers-mode-map nil + "Keymap for *mu4e-headers* buffers.") +(unless mu4e-headers-mode-map + (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 "s" 'mu4e-headers-search) + (define-key map "S" 'mu4e-headers-search-edit) + + (define-key map "/" 'mu4e-headers-search-narrow) + + (define-key map "j" 'mu4e~headers-jump-to-maildir) + + (define-key map (kbd "<M-left>") 'mu4e-headers-query-prev) + (define-key map (kbd "\\") 'mu4e-headers-query-prev) + (define-key map (kbd "<M-right>") 'mu4e-headers-query-next) + + (define-key map "b" 'mu4e-headers-search-bookmark) + (define-key map "B" 'mu4e-headers-search-bookmark-edit) + + (define-key map "O" 'mu4e-headers-change-sorting) + (define-key map "P" 'mu4e-headers-toggle-threading) + (define-key map "Q" 'mu4e-headers-toggle-full-search) + (define-key map "W" 'mu4e-headers-toggle-include-related) + (define-key map "V" 'mu4e-headers-toggle-skip-duplicates) + + (define-key map "q" 'mu4e~headers-quit-buffer) + (define-key map "g" 'mu4e-headers-rerun-search) ;; for compatibility + + (define-key map "%" 'mu4e-headers-mark-pattern) + (define-key map "t" 'mu4e-headers-mark-subthread) + (define-key map "T" 'mu4e-headers-mark-thread) + + ;; navigation between messages + (define-key map "p" 'mu4e-headers-prev) + (define-key map "n" 'mu4e-headers-next) + (define-key map (kbd "<M-up>") 'mu4e-headers-prev) + (define-key map (kbd "<M-down>") 'mu4e-headers-next) + + (define-key map (kbd "[") 'mu4e-headers-prev-unread) + (define-key map (kbd "]") 'mu4e-headers-next-unread) + + ;; change the number of headers + (define-key map (kbd "C-+") 'mu4e-headers-split-view-grow) + (define-key map (kbd "C--") 'mu4e-headers-split-view-shrink) + (define-key map (kbd "<C-kp-add>") 'mu4e-headers-split-view-grow) + (define-key map (kbd "<C-kp-subtract>") 'mu4e-headers-split-view-shrink) + + (define-key map ";" 'mu4e-context-switch) + + ;; switching to view mode (if it's visible) + (define-key map "y" 'mu4e-select-other-view) + + ;; marking/unmarking ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (define-key map (kbd "<backspace>") 'mu4e-headers-mark-for-trash) + (define-key map (kbd "d") 'mu4e-headers-mark-for-trash) + (define-key map (kbd "<delete>") 'mu4e-headers-mark-for-delete) + (define-key map (kbd "<deletechar>") 'mu4e-headers-mark-for-delete) + (define-key map (kbd "D") 'mu4e-headers-mark-for-delete) + (define-key map (kbd "m") 'mu4e-headers-mark-for-move) + (define-key map (kbd "r") 'mu4e-headers-mark-for-refile) + + (define-key map (kbd "?") 'mu4e-headers-mark-for-unread) + (define-key map (kbd "!") 'mu4e-headers-mark-for-read) + (define-key map (kbd "A") 'mu4e-headers-mark-for-action) + + (define-key map (kbd "u") 'mu4e-headers-mark-for-unmark) + (define-key map (kbd "+") 'mu4e-headers-mark-for-flag) + (define-key map (kbd "-") 'mu4e-headers-mark-for-unflag) + (define-key map (kbd "=") 'mu4e-headers-mark-for-untrash) + (define-key map (kbd "&") 'mu4e-headers-mark-custom) + + (define-key map (kbd "*") 'mu4e-headers-mark-for-something) + (define-key map (kbd "<kp-multiply>") 'mu4e-headers-mark-for-something) + (define-key map (kbd "<insertchar>") 'mu4e-headers-mark-for-something) + (define-key map (kbd "<insert>") 'mu4e-headers-mark-for-something) + + + (define-key map (kbd "#") 'mu4e-mark-resolve-deferred-marks) + + (define-key map "U" 'mu4e-mark-unmark-all) + (define-key map "x" 'mu4e-mark-execute-all) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + (define-key map "a" 'mu4e-headers-action) + + ;; message composition + (define-key map "R" 'mu4e-compose-reply) + (define-key map "F" 'mu4e-compose-forward) + (define-key map "C" 'mu4e-compose-new) + (define-key map "E" 'mu4e-compose-edit) + + (define-key map (kbd "RET") 'mu4e-headers-view-message) + (define-key map [mouse-2] 'mu4e-headers-view-message) + + (define-key map "$" 'mu4e-show-log) + (define-key map "H" 'mu4e-display-manual) + + ;; menu + (define-key map [menu-bar] (make-sparse-keymap)) + (let ((menumap (make-sparse-keymap "Headers"))) + (define-key map [menu-bar headers] (cons "Headers" menumap)) + + (define-key menumap [mu4e~headers-quit-buffer] + '("Quit view" . mu4e~headers-quit-buffer)) + (define-key menumap [display-help] '("Help" . mu4e-display-manual)) + + (define-key menumap [sepa0] '("--")) + + (define-key menumap [toggle-include-related] + '(menu-item "Toggle related messages" mu4e-headers-toggle-include-related + :button (:toggle . (and (boundp 'mu4e-headers-include-related) + mu4e-headers-include-related)))) + (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)))) + + (define-key menumap [sepa1] '("--")) + + (define-key menumap [execute-marks] '("Execute marks" + . mu4e-mark-execute-all)) + (define-key menumap [unmark-all] '("Unmark all" . mu4e-mark-unmark-all)) + (define-key menumap [unmark] '("Unmark" . mu4e-headers-mark-for-unmark)) + + (define-key menumap [mark-pattern] '("Mark pattern" . + mu4e-headers-mark-pattern)) + (define-key menumap [mark-as-read] '("Mark as read" . + mu4e-headers-mark-for-read)) + (define-key menumap [mark-as-unread] + '("Mark as unread" . mu4e-headers-mark-for-unread)) + + (define-key menumap [mark-delete] + '("Mark for deletion" . mu4e-headers-mark-for-delete)) + (define-key menumap [mark-trash] + '("Mark for trash" . mu4e-headers-mark-for-trash)) + (define-key menumap [mark-move] + '("Mark for move" . mu4e-headers-mark-for-move)) + (define-key menumap [sepa2] '("--")) + + + (define-key menumap [resend] '("Resend" . mu4e-compose-resend)) + (define-key menumap [forward] '("Forward" . mu4e-compose-forward)) + (define-key menumap [reply] '("Reply" . mu4e-compose-reply)) + (define-key menumap [compose-new] '("Compose new" . mu4e-compose-new)) + + + (define-key menumap [sepa3] '("--")) + + (define-key menumap [query-next] '("Next query" . mu4e-headers-query-next)) + (define-key menumap [query-prev] '("Previous query" . + mu4e-headers-query-prev)) + (define-key menumap [narrow-search] '("Narrow search" . + mu4e-headers-search-narrow)) + (define-key menumap [bookmark] '("Search bookmark" . + mu4e-headers-search-bookmark)) + (define-key menumap [jump] '("Jump to maildir" . + mu4e~headers-jump-to-maildir)) + (define-key menumap [refresh] '("Refresh" . mu4e-headers-rerun-search)) + (define-key menumap [search] '("Search" . mu4e-headers-search)) + + + (define-key menumap [sepa4] '("--")) + + (define-key menumap [view] '("View" . mu4e-headers-view-message)) + (define-key menumap [next] '("Next" . mu4e-headers-next)) + (define-key menumap [previous] '("Previous" . mu4e-headers-prev)) + (define-key menumap [sepa5] '("--"))) + map))) +(fset 'mu4e-headers-mode-map mu4e-headers-mode-map) + + +(defun mu4e~header-line-format () + "Get the format for the header line." + (let ((uparrow (if mu4e-use-fancy-chars " â–²" " ^")) + (downarrow (if mu4e-use-fancy-chars " â–¼" " V"))) + (cons + (make-string + (+ mu4e~mark-fringe-len (floor (fringe-columns 'left t))) ?\s) + (mapcar + (lambda (item) + (let* ((field (car item)) (width (cdr item)) + (info (cdr (assoc field + (append mu4e-header-info mu4e-header-info-custom)))) + (sortable (plist-get info :sortable)) + ;; if sortable, it is either t (when field is sortable itself) + ;; or a symbol (if another field is used for sorting) + (sortfield (when sortable (if (booleanp sortable) field sortable))) + (help (plist-get info :help)) + ;; triangle to mark the sorted-by column + (arrow + (when (and sortable (eq sortfield mu4e-headers-sort-field)) + (if (eq mu4e-headers-sort-direction 'descending) downarrow uparrow))) + (name (concat (plist-get info :shortname) arrow)) + (map (make-sparse-keymap))) + (when sortable + (define-key map [header-line mouse-1] + (lambda (&optional e) + ;; getting the field, inspired by `tabulated-list-col-sort' + (interactive "e") + (let* ((obj (posn-object (event-start e))) + (field + (and obj (get-text-property 0 'field (car obj))))) + ;; "t": if we're already sorted by field, the sort-order is + ;; changed + (mu4e-headers-change-sorting field t))))) + (concat + (propertize + (if width + (truncate-string-to-width name width 0 ?\s t) + name) + 'face (when arrow 'bold) + 'help-echo help + 'mouse-face (when sortable 'highlight) + 'keymap (when sortable map) + 'field field) " "))) + mu4e-headers-fields)))) + +(defvar mu4e-headers-mode-abbrev-table nil) + +(defun mu4e~headers-do-auto-update () + "Update the current headers buffer after indexing has brought +some changes, `mu4e-headers-auto-update' is non-nil and there is no +user-interaction ongoing." + (when (and mu4e-headers-auto-update ;; must be set + (zerop (mu4e-mark-marks-num)) ;; non active marks + (not (active-minibuffer-window))) ;; no user input + (with-current-buffer mu4e~headers-buffer + (mu4e-headers-rerun-search)))) + +(define-derived-mode mu4e-headers-mode special-mode + "mu4e:headers" + "Major mode for displaying mu4e search results. +\\{mu4e-headers-mode-map}." + (use-local-map mu4e-headers-mode-map) + (make-local-variable 'mu4e~headers-proc) + (make-local-variable 'mu4e~highlighted-docid) + (make-local-variable 'global-mode-string) + (set (make-local-variable 'hl-line-face) 'mu4e-header-highlight-face) + + ;; maybe update the current headers upon indexing changes + (add-hook 'mu4e-index-updated-hook 'mu4e~headers-do-auto-update nil t) + (setq + truncate-lines t + buffer-undo-list t ;; don't record undo information + overwrite-mode nil + header-line-format (mu4e~header-line-format)) + + (mu4e~mark-initialize) ;; initialize the marking subsystem + (hl-line-mode 1)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; highlighting +(defvar mu4e~highlighted-docid nil + "The highlighted docid") + +(defun mu4e~headers-highlight (docid) + "Highlight the header with DOCID, or do nothing if it's not found. +Also, unhighlight any previously highlighted headers." + (with-current-buffer mu4e~headers-buffer + (save-excursion + ;; first, unhighlight the previously highlighted docid, if any + (when (and docid mu4e~highlighted-docid + (mu4e~headers-goto-docid mu4e~highlighted-docid)) + (hl-line-unhighlight)) + ;; now, highlight the new one + (when (mu4e~headers-goto-docid docid) + (hl-line-highlight))) + (setq mu4e~highlighted-docid docid))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mu4e~headers-select-window () + "When there is a visible window for the headers buffer, make sure +to select it. This is needed when adding new headers, otherwise +adding a lot of new headers looks really choppy." + (let ((win (get-buffer-window mu4e~headers-buffer))) + (when win (select-window win)))) + +;;;; headers in the buffer are prefixed by an invisible string with the docid +;;;; followed by an EOT ('end-of-transmission', \004, ^D) non-printable ascii +;;;; character. this string also has a text-property with the docid. the former +;;;; is used for quickly finding a certain header, the latter for retrieving the +;;;; docid at point without string matching etc. + +(defsubst mu4e~headers-docid-cookie (docid) + "Create an invisible string containing DOCID; this is to be used +at the beginning of lines to identify headers." + (propertize (format "%s%d%s" + mu4e~headers-docid-pre docid mu4e~headers-docid-post) + 'docid docid 'invisible t));; + +(defsubst mu4e~headers-docid-at-point (&optional point) + "Get the docid for the header at POINT, or at current (point) if +nil. Returns the docid, or nil if there is none." + (save-excursion + (when point + (goto-char point)) + (get-text-property (line-beginning-position) 'docid))) + +(defun mu4e~headers-goto-docid (docid &optional to-mark) + "Go to the beginning of the line with the header with docid +DOCID, or nil if it cannot be found. If the optional TO-MARK is +non-nil, go to the point directly *after* the docid-cookie instead +of the beginning of the line." + (let ((oldpoint (point)) (newpoint)) + (goto-char (point-min)) + (setq newpoint + (search-forward (mu4e~headers-docid-cookie docid) nil t)) + (unless to-mark + (if (null newpoint) + (goto-char oldpoint) ;; not found; restore old pos + (progn + (beginning-of-line) ;; found, move to beginning of line + (setq newpoint (point))))) + newpoint)) ;; return the point, or nil if not found + + +(defsubst mu4e~headers-docid-pos (docid) + "Return the pos of the beginning of the line with the header with +docid DOCID, or nil if it cannot be found." + (let ((pos)) + (save-excursion + (setq pos (mu4e~headers-goto-docid docid))) + pos)) + +(defsubst mu4e~headers-field-for-docid (docid field) + "Get FIELD (a symbol, see `mu4e-headers-names') for the message +with DOCID which must be present in the headers buffer." + (save-excursion + (when (mu4e~headers-goto-docid docid) + (mu4e-message-field (mu4e-message-at-point) field)))) + +(defun mu4e-headers-goto-message-id (msgid) + "Go to the next message with message-id MSGID. Return the +message plist, or nil if not found." + (mu4e-headers-find-if + (lambda (msg) + (let ((this-msgid (mu4e-message-field msg :message-id))) + (when (and this-msgid (string= msgid this-msgid)) + msg))))) + +;;;; markers mark headers for +(defun mu4e~headers-mark (docid mark) + "(Visually) mark the header for DOCID with character MARK." + (with-current-buffer mu4e~headers-buffer + (let ((inhibit-read-only t) (oldpoint (point))) + (unless (mu4e~headers-goto-docid docid) + (mu4e-error "Cannot find message with docid %S" docid)) + ;; now, we're at the beginning of the header, looking at + ;; <docid>\004 + ;; (which is invisible). jump past that… + (unless (re-search-forward mu4e~headers-docid-post nil t) + (mu4e-error "Cannot find the `mu4e~headers-docid-post' separator")) + + ;; clear old marks, and add the new ones. + (let ((msg (get-text-property (point) 'msg))) + (delete-char mu4e~mark-fringe-len) + (insert (propertize + (format mu4e~mark-fringe-format mark) + 'face 'mu4e-header-marks-face + 'docid docid + 'msg msg))) + (goto-char oldpoint)))) + + +(defsubst mu4e~headers-add-header (str docid point &optional msg) + "Add header STR with DOCID to the buffer at POINT if non-nil, or +at (point-max) otherwise. If MSG is not nil, add it as the +text-property `msg'." + (when (buffer-live-p mu4e~headers-buffer) + (with-current-buffer mu4e~headers-buffer + (let ((inhibit-read-only t) + (is-first-header (= (point-min) (point-max)))) + (save-excursion + (goto-char (if point point (point-max))) + (insert + (propertize + (concat + (mu4e~headers-docid-cookie docid) + mu4e~mark-fringe + str "\n") + 'docid docid 'msg msg))))))) + +(defun mu4e~headers-remove-header (docid &optional ignore-missing) + "Remove header with DOCID at point. +When IGNORE-MISSING is non-nill, don't raise an error when the +docid is not found." + (with-current-buffer mu4e~headers-buffer + (if (mu4e~headers-goto-docid docid) + (let ((inhibit-read-only t)) + (delete-region (line-beginning-position) (line-beginning-position 2))) + (unless ignore-missing + (mu4e-error "Cannot find message with docid %S" docid))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mu4e~headers-search-execute (expr ignore-history) + "Search in the mu database for EXPR, and switch to the output +buffer for the results. If IGNORE-HISTORY is true, do *not* update +the query history stack." + ;; note: we don't want to update the history if this query comes from + ;; `mu4e~headers-query-next' or `mu4e~headers-query-prev'. + (mu4e-hide-other-mu4e-buffers) + (let* ((buf (get-buffer-create mu4e~headers-buffer-name)) + (inhibit-read-only t) + (maxnum (unless mu4e-headers-full-search mu4e-headers-results-limit))) + (with-current-buffer buf + (mu4e-headers-mode) + (unless ignore-history + ;; save the old present query to the history list + (when mu4e~headers-last-query + (mu4e~headers-push-query mu4e~headers-last-query 'past))) + (setq + mu4e~headers-buffer buf + mode-name "mu4e-headers" + mu4e~headers-last-query expr + global-mode-string + '(:eval + (concat + (propertize + (mu4e~quote-for-modeline mu4e~headers-last-query) + 'face 'mu4e-modeline-face) + " " + (mu4e-context-label))))) + + (switch-to-buffer buf) + (run-hook-with-args 'mu4e-headers-search-hook expr) + (mu4e~proc-find + expr + mu4e-headers-show-threads + mu4e-headers-sort-field + mu4e-headers-sort-direction + maxnum + mu4e-headers-skip-duplicates + mu4e-headers-include-related))) + +(defun mu4e~headers-redraw-get-view-window () + "Close all windows, redraw the headers buffer based on the value +of `mu4e-split-view', and return a window for the message view." + (mu4e-hide-other-mu4e-buffers) + (unless (buffer-live-p mu4e~headers-buffer) + (mu4e-error "No headers buffer available")) + (switch-to-buffer mu4e~headers-buffer) + ;; kill the existing view win + (when (buffer-live-p mu4e~view-buffer) + (kill-buffer mu4e~view-buffer)) + ;; get a new view window + (setq mu4e~headers-view-win + (let* ((new-win-func + (cond + ((eq mu4e-split-view 'horizontal) ;; split horizontally + '(split-window-vertically mu4e-headers-visible-lines)) + ((eq mu4e-split-view 'vertical) ;; split vertically + '(split-window-horizontally mu4e-headers-visible-columns))))) + (cond ((with-demoted-errors "Unable to split window: %S" + (eval new-win-func))) + (t ;; no splitting; just use the currently selected one + (selected-window))))) + mu4e~headers-view-win) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; search-based marking + +(defun mu4e-headers-for-each (func) + "Call FUNC for each header, moving point to the header. +FUNC receives one argument, the message s-expression for the +corresponding header." + (save-excursion + (goto-char (point-min)) + (while (search-forward mu4e~headers-docid-pre nil t) + ;; not really sure why we need to jump to bol; we do need to, otherwise we + ;; miss lines sometimes... + (let ((msg (get-text-property (line-beginning-position) 'msg))) + (when msg + (funcall func msg)))))) + +(defun mu4e-headers-find-if (func &optional backward) + "Move to the next header for which FUNC returns non-`nil', +starting from the current position. FUNC receives one argument, the +message s-expression for the corresponding header. If BACKWARD is +non-`nil', search backwards. Returns the new position, or `nil' if +nothing was found. If you want to exclude matches for the current +message, you can use `mu4e-headers-find-if-next'." + (let ((pos) + (search-func (if backward 'search-backward 'search-forward))) + (save-excursion + (while (and (null pos) + (funcall search-func mu4e~headers-docid-pre nil t)) + ;; not really sure why we need to jump to bol; we do need to, otherwise we + ;; miss lines sometimes... + (let ((msg (get-text-property (line-beginning-position) 'msg))) + (when (and msg (funcall func msg)) + (setq pos (point)))))) + (when pos + (goto-char pos)))) + +(defun mu4e-headers-find-if-next (func &optional backwards) + "Like `mu4e-headers-find-if', but do not match the current header. +Move to the next or (if BACKWARDS is non-`nil') header for which FUNC +returns non-`nil', starting from the current position." + (let ((pos)) + (save-excursion + (if backwards + (beginning-of-line) + (end-of-line)) + (setq pos (mu4e-headers-find-if func backwards))) + (when pos (goto-char pos)))) + +(defvar mu4e~headers-regexp-hist nil + "History list of regexps used.") + +(defun mu4e-headers-mark-for-each-if (markpair mark-pred &optional param) + "Mark all headers for which predicate function MARK-PRED returns +non-nil with MARKPAIR. MARK-PRED is function that receives two +arguments, MSG (the message at point) and PARAM (a user-specified +parameter). MARKPAIR is a cell (MARK . TARGET); see +`mu4e-mark-at-point' for details about marks." + (mu4e-headers-for-each + (lambda (msg) + (when (funcall mark-pred msg param) + (mu4e-mark-at-point (car markpair) (cdr markpair)))))) + +(defun mu4e-headers-mark-pattern () + "Ask user for a kind of mark (move, delete etc.), a field to +match and a regular expression to match with. Then, mark all +matching messages with that mark." + (interactive) + (let ((markpair (mu4e~mark-get-markpair "Mark matched messages with: " t)) + (field (mu4e-read-option "Field to match: " + '( ("subject" . :subject) + ("from" . :from) + ("to" . :to) + ("cc" . :cc) + ("bcc" . :bcc) + ("list" . :mailing-list)))) + (pattern (read-string + (mu4e-format "Regexp:") + nil 'mu4e~headers-regexp-hist))) + (mu4e-headers-mark-for-each-if + markpair + (lambda (msg param) + (let* ((do-mark) (value (mu4e-msg-field msg field))) + (setq do-mark + (if (member field '(:to :from :cc :bcc :reply-to)) + (find-if (lambda (contact) + (let ((name (car contact)) (email (cdr contact))) + (or (and name (string-match pattern name)) + (and email (string-match pattern email))))) value) + (string-match pattern (or value ""))))))))) + +(defun mu4e-headers-mark-custom () + "Mark messages based on a user-provided predicate function." + (interactive) + (let* ((pred (mu4e-read-option "Match function: " + mu4e-headers-custom-markers)) + (param (when (cdr pred) (eval (cdr pred)))) + (markpair (mu4e~mark-get-markpair "Mark matched messages with: " t))) + (mu4e-headers-mark-for-each-if markpair (car pred) param))) + +(defun mu4e~headers-get-thread-info (msg what) + "Get WHAT (a symbol, either path or thread-id) for MSG." + (let* ((thread (or (mu4e-message-field msg :thread) + (mu4e-error "No thread info found"))) + (path (or (plist-get thread :path) + (mu4e-error "No threadpath found")))) + (case what + (path path) + (thread-id + (save-match-data + ;; the thread id is the first segment of the thread path + (when (string-match "^\\([[:xdigit:]]+\\):?" path) + (match-string 1 path)))) + (otherwise (mu4e-error "Not supported"))))) + + +(defun mu4e-headers-mark-thread-using-markpair (markpair &optional subthread) + "Mark the thread at point using the given markpair. If SUBTHREAD is +non-nil, marking is limited to the message at point and its +descendants." + (let* ((mark (car markpair)) + (allowed-marks (mapcar 'car mu4e-marks))) + (unless (memq mark allowed-marks) + (mu4e-error "The mark (%s) has to be one of: %s" + mark allowed-marks))) + ;; note: the tread id is shared by all messages in a thread + (let* ((msg (mu4e-message-at-point)) + (thread-id (mu4e~headers-get-thread-info msg 'thread-id)) + (path (mu4e~headers-get-thread-info msg 'path)) + (last-marked-point)) + (mu4e-headers-for-each + (lambda (mymsg) + (let ((my-thread-id (mu4e~headers-get-thread-info mymsg 'thread-id))) + (if subthread + ;; subthread matching; mymsg's thread path should have path as its + ;; prefix + (when (string-match (concat "^" path) + (mu4e~headers-get-thread-info mymsg 'path)) + (mu4e-mark-at-point (car markpair) (cdr markpair)) + (setq last-marked-point (point))) + ;; nope; not looking for the subthread; looking for the whole thread + (when (string= thread-id + (mu4e~headers-get-thread-info mymsg 'thread-id)) + (mu4e-mark-at-point (car markpair) (cdr markpair)) + (setq last-marked-point (point))))))) + (when last-marked-point + (goto-char last-marked-point) + (mu4e-headers-next)))) + +(defun mu4e-headers-mark-thread (&optional subthread markpair) + "Like `mu4e-headers-mark-thread-using-markpair' but prompt for the markpair." + (interactive + (let* ((subthread current-prefix-arg)) + (list current-prefix-arg + ;; FIXME: e.g., for refiling we should evaluate this + ;; for each line separately + (mu4e~mark-get-markpair + (if subthread "Mark subthread with: " "Mark whole thread with: ") t)))) + (mu4e-headers-mark-thread-using-markpair markpair subthread)) + +(defun mu4e-headers-mark-subthread (&optional markpair) + "Like `mu4e-mark-thread', but only for a sub-thread." + (interactive) + (if markpair (mu4e-headers-mark-thread t markpair) + (let ((current-prefix-arg t)) + (call-interactively 'mu4e-headers-mark-thread)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +;;; the query past / present / future ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defvar mu4e~headers-query-past nil + "Stack of queries before the present one.") +(defvar mu4e~headers-query-future nil + "Stack of queries after the present one.") +(defvar mu4e~headers-query-stack-size 20 + "Maximum size for the query stacks.") + +(defun mu4e~headers-push-query (query where) + "Push QUERY to one of the query stacks. +WHERE is a symbol telling us where to push; it's a symbol, either +'future or 'past. Functional also removes duplicats, limits the +stack size." + (let ((stack + (case where + (past mu4e~headers-query-past) + (future mu4e~headers-query-future)))) + ;; only add if not the same item + (unless (and stack (string= (car stack) query)) + (push query stack) + ;; limit the stack to `mu4e~headers-query-stack-size' elements + (when (> (length stack) mu4e~headers-query-stack-size) + (setq stack (subseq stack 0 mu4e~headers-query-stack-size))) + ;; remove all duplicates of the new element + (remove-if (lambda (elm) (string= elm (car stack))) (cdr stack)) + ;; update the stacks + (case where + (past (setq mu4e~headers-query-past stack)) + (future (setq mu4e~headers-query-future stack)))))) + +(defun mu4e~headers-pop-query (whence) + "Pop a query from the stack. +WHENCE is a symbol telling us where to get it from, either `future' +or `past'." + (case whence + (past + (unless mu4e~headers-query-past + (mu4e-warn "No more previous queries")) + (pop mu4e~headers-query-past)) + (future + (unless mu4e~headers-query-future + (mu4e-warn "No more next queries")) + (pop mu4e~headers-query-future)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +;;; interactive functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defvar mu4e~headers-search-hist nil + "History list of searches.") + +(defvar mu4e~headers-msgid-target nil + "Message-id to jump to after the search has finished.") + +(defvar mu4e~headers-view-target nil + "Whether to automatically view (open) the target message (as + per `mu4e~headers-msgid-target').") + +(defun mu4e-headers-search (&optional expr prompt edit ignore-history msgid show) + "Search in the mu database for EXPR, and switch to the output +buffer for the results. This is an interactive function which ask +user for EXPR. PROMPT, if non-nil, is the prompt used by this +function (default is \"Search for:\"). If EDIT is non-nil, +instead of executing the query for EXPR, let the user edit the +query before executing it. If IGNORE-HISTORY is true, do *not* +update the query history stack. If MSGID is non-nil, attempt to +move point to the first message with that message-id after +searching. If SHOW is non-nil, show the message with MSGID." + ;; note: we don't want to update the history if this query comes from + ;; `mu4e~headers-query-next' or `mu4e~headers-query-prev'." + (interactive) + (let* ((prompt (mu4e-format (or prompt "Search for: "))) + (expr + (if edit + (read-string prompt expr) + (or expr + (read-string prompt nil 'mu4e~headers-search-hist))))) + (mu4e-mark-handle-when-leaving) + (mu4e~headers-search-execute expr ignore-history) + (setq mu4e~headers-msgid-target msgid + mu4e~headers-view-target show))) + +(defun mu4e-headers-search-edit () + "Edit the last search expression." + (interactive) + (mu4e-headers-search mu4e~headers-last-query nil t)) + +(defun mu4e-headers-search-bookmark (&optional expr edit) + "Search using some bookmarked query EXPR. +If EDIT is non-nil, let the user edit the bookmark before starting +the search." + (interactive) + (let ((expr + (or expr + (mu4e-ask-bookmark (if edit "Select bookmark: " "Bookmark: "))))) + (run-hook-with-args 'mu4e-headers-search-bookmark-hook expr) + (mu4e-headers-search expr (when edit "Edit bookmark: ") edit))) + +(defun mu4e-headers-search-bookmark-edit () + "Edit an existing bookmark before executing it." + (interactive) + (mu4e-headers-search-bookmark nil t)) + + +(defun mu4e-headers-search-narrow (filter ) + "Narrow the last search by appending search expression FILTER to +the last search expression. Note that you can go back to previous +query (effectively, 'widen' it), with `mu4e-headers-query-prev'." + (interactive + (let ((filter + (read-string (mu4e-format "Narrow down to: ") + nil 'mu4e~headers-search-hist nil t))) + (list filter))) + (unless mu4e~headers-last-query + (mu4e-warn "There's nothing to filter")) + (mu4e-headers-search + (format "(%s) AND (%s)" mu4e~headers-last-query filter))) + + +(defun mu4e-headers-change-sorting (&optional field dir) + "Change the sorting/threading parameters. +FIELD is the field to sort by; DIR is a symbol: either 'ascending, +'descending, 't (meaning: if FIELD is the same as the current +sortfield, change the sort-order) or nil (ask the user)." + (interactive) + (let* ((field + (or field + (mu4e-read-option "Sortfield: " mu4e~headers-sort-field-choices))) + ;; note: 'sortable' is either a boolean (meaning: if non-nil, this is + ;; sortable field), _or_ another field (meaning: sort by this other field). + (sortable (plist-get (cdr (assoc field mu4e-header-info)) :sortable)) + ;; error check + (sortable + (if sortable + sortable + (mu4e-error "Not a sortable field"))) + (sortfield (if (booleanp sortable) field sortable)) + (dir + (case dir + ((ascending descending) dir) + ;; change the sort order if field = curfield + (t + (if (eq sortfield mu4e-headers-sort-field) + (if (eq mu4e-headers-sort-direction 'ascending) + 'descending 'ascending) + 'descending)) + (mu4e-read-option "Direction: " + '(("ascending" . 'ascending) ("descending" . 'descending)))))) + (setq + mu4e-headers-sort-field sortfield + mu4e-headers-sort-direction dir) + (mu4e-message "Sorting by %s (%s)" + (symbol-name sortfield) + (symbol-name mu4e-headers-sort-direction)) + (mu4e-headers-rerun-search))) + +(defun mu4e~headers-toggle (name togglevar dont-refresh) + "Toggle variable TOGGLEVAR for feature NAME. Unless DONT-REFRESH is non-nil, +re-run the last search." + (set togglevar (not (symbol-value togglevar))) + (mu4e-message "%s turned %s%s" + name + (if (symbol-value togglevar) "on" "off") + (if dont-refresh + " (press 'g' to refresh)" "")) + (unless dont-refresh + (mu4e-headers-rerun-search))) + +(defun mu4e-headers-toggle-threading (&optional dont-refresh) + "Toggle `mu4e-headers-show-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)) + +(defun mu4e-headers-toggle-full-search (&optional dont-refresh) + "Toggle `mu4e-headers-full-search'. 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)) + +(defun mu4e-headers-toggle-include-related (&optional dont-refresh) + "Toggle `mu4e-headers-include-related'. With prefix-argument, do +_not_ refresh the last search with the new setting for threading." + (interactive "P") + (mu4e~headers-toggle "Include-related" + 'mu4e-headers-include-related dont-refresh)) + +(defun mu4e-headers-toggle-skip-duplicates (&optional dont-refresh) + "Toggle `mu4e-headers-skip-duplicates'. With prefix-argument, do +_not_ refresh the last search with the new setting for threading." + (interactive "P") + (mu4e~headers-toggle "Skip-duplicates" + 'mu4e-headers-skip-duplicates dont-refresh)) + +(defvar mu4e~headers-loading-buf nil + "A buffer for loading a message view.") + +(defun mu4e~headers-get-loading-buf () + "Get a buffer to give feedback while loading a message view." + (unless (buffer-live-p mu4e~headers-loading-buf) + (setq mu4e~headers-loading-buf + (get-buffer-create " *mu4e-loading*"))) + (with-current-buffer mu4e~headers-loading-buf + (let ((inhibit-read-only t)) + (erase-buffer) + (local-set-key (kbd "q") 'kill-buffer-and-window) + (insert (propertize "Waiting for message..." + 'face 'mu4e-system-face 'intangible t)))) + mu4e~headers-loading-buf) + +(defun mu4e-headers-view-message () + "View message at point. +If there's an existing window for the view, re-use that one. If +not, create a new one, depending on the value of +`mu4e-split-view': if it's a symbol `horizontal' or `vertical', +split the window accordingly; if it is nil, replace the current +window. " + (interactive) + (unless (eq major-mode 'mu4e-headers-mode) + (mu4e-error "Must be in mu4e-headers-mode (%S)" major-mode)) + (let* ((msg (mu4e-message-at-point)) + (docid (or (mu4e-message-field msg :docid) + (mu4e-warn "No message at point"))) + ;; decrypt (or not), based on `mu4e-decryption-policy'. + (decrypt + (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))) + (viewwin (mu4e~headers-redraw-get-view-window))) + (unless (window-live-p viewwin) + (mu4e-error "Cannot get a message view")) + (select-window viewwin) + (switch-to-buffer (mu4e~headers-get-loading-buf)) + (mu4e~proc-view docid mu4e-view-show-images decrypt))) + +(defun mu4e-headers-rerun-search () + "Rerun the search for the last search expression." + (interactive) + ;; if possible, try to return to the same message + (let* ((msg (mu4e-message-at-point)) + (msgid (and msg (mu4e-message-field msg :message-id)))) + (mu4e-headers-search mu4e~headers-last-query nil nil t msgid))) + +(defun mu4e~headers-query-navigate (whence) + "Execute the previous query from the query stacks. +WHENCE determines where the query is taken from and is a symbol, +either `future' or `past'." + (let ((query (mu4e~headers-pop-query whence)) + (where (if (eq whence 'future) 'past 'future))) + (when query + (mu4e~headers-push-query mu4e~headers-last-query where) + (mu4e-headers-search query nil nil t)))) + +(defun mu4e-headers-query-next () + "Execute the previous query from the query stacks." + (interactive) + (mu4e~headers-query-navigate 'future)) + +(defun mu4e-headers-query-prev () + "Execute the previous query from the query stacks." + (interactive) + (mu4e~headers-query-navigate 'past)) + +;; forget the past so we don't repeat it :/ +(defun mu4e-headers-forget-queries () + "Forget all the complete query history." + (interactive) + (setq ;; note: don't forget the present one + mu4e~headers-query-past nil + mu4e~headers-query-future nil) + (mu4e-message "Query history cleared")) + +(defun mu4e~headers-move (lines) + "Move point LINES lines forward (if LINES is positive) or +backward (if LINES is negative). If this succeeds, return the new +docid. Otherwise, return nil." + (unless (eq major-mode 'mu4e-headers-mode) + (mu4e-error "Must be in mu4e-headers-mode (%S)" major-mode)) + (let ((succeeded (zerop (forward-line lines))) + (docid (mu4e~headers-docid-at-point))) + ;; move point, even if this function is called when this window is not + ;; visible + (when docid + ;; update all windows showing the headers buffer + (walk-windows + (lambda (win) + (when (eq (window-buffer win) mu4e~headers-buffer) + (set-window-point win (point)))) + nil t) + ;;(set-window-point (get-buffer-window mu4e~headers-buffer t) (point)) + ;; attempt to highlight the new line, display the message + (mu4e~headers-highlight docid) + ;; update message view if it was already showing + (when (and mu4e-split-view (window-live-p mu4e~headers-view-win)) + (mu4e-headers-view-message)) + docid))) + +(defun mu4e-headers-next (&optional n) + "Move point to the next message header. +If this succeeds, return the new docid. Otherwise, return nil. +Optionally, takes an integer N (prefix argument), to the Nth next +header." + (interactive "P") + (mu4e~headers-move (or n 1))) + +(defun mu4e-headers-prev (&optional n) + "Move point to the previous message header. +If this succeeds, return the new docid. Otherwise, return nil. +Optionally, takes an integer N (prefix argument), to the Nth +previous header." + (interactive "P") + (mu4e~headers-move (- (or n 1)))) + +(defun mu4e~headers-prev-or-next-unread (backwards) + "Move point to the next message that is unread (and +untrashed). If BACKWARDS is non-`nil', move backwards." + (interactive) + (or (mu4e-headers-find-if-next + (lambda (msg) + (let ((flags (mu4e-message-field msg :flags))) + (and (member 'unread flags) (not (member 'trashed flags))))) + backwards) + (mu4e-message (format "No %s unread message found" + (if backwards "previous" "next"))))) + +(defun mu4e-headers-prev-unread () + "Move point to the previous message that is unread (and +untrashed)." + (interactive) + (mu4e~headers-prev-or-next-unread t)) + +(defun mu4e-headers-next-unread () + "Move point to the next message that is unread (and +untrashed)." + (interactive) + (mu4e~headers-prev-or-next-unread nil)) + +(defun mu4e~headers-jump-to-maildir (maildir) + "Show the messages in maildir (user is prompted to ask what +maildir)." + (interactive + (let ((maildir (mu4e-ask-maildir "Jump to maildir: "))) + (list maildir))) + (when maildir + (mu4e-mark-handle-when-leaving) + (mu4e-headers-search + (format "maildir:\"%s\"" maildir)))) + +(defun mu4e-headers-split-view-grow (&optional n) + "In split-view, grow the headers window. +In horizontal split-view, increase the number of lines shown by N. +In vertical split-view, increase the number of columns shown by N. +If N is negative shrink the headers window. When not in split-view +do nothing." + (interactive "P") + (let ((n (or n 1)) + (hwin (get-buffer-window mu4e~headers-buffer))) + (when (and (buffer-live-p mu4e~view-buffer) (window-live-p hwin)) + (let ((n (or n 1))) + (case mu4e-split-view + ;; emacs has weird ideas about what horizontal, vertical means... + (horizontal + (window-resize hwin n nil) + (incf mu4e-headers-visible-lines n)) + (vertical + (window-resize hwin n t) + (incf mu4e-headers-visible-columns n))))))) + +(defun mu4e-headers-split-view-shrink (&optional n) + "In split-view, shrink the headers window. +In horizontal split-view, decrease the number of lines shown by N. +In vertical split-view, decrease the number of columns shown by N. +If N is negative grow the headers window. When not in split-view +do nothing." + (interactive "P") + (mu4e-headers-split-view-grow (- (or n 1)))) + +(defun mu4e-headers-action (&optional actionfunc) + "Ask user what to do with message-at-point, then do it. +The actions are specified in `mu4e-headers-actions'. Optionally, +pass ACTIONFUNC, which is a function that takes a msg-plist +argument." + (interactive) + (let ((msg (mu4e-message-at-point)) + (afunc (or actionfunc (mu4e-read-option "Action: " mu4e-headers-actions)))) + (funcall afunc msg))) + +(defun mu4e-headers-mark-and-next (mark) + "Set mark MARK on the message at point or on all messages in the +region if there is a region, then move to the next message." + (interactive) + (mu4e-mark-set mark) + (mu4e-headers-next)) + +(defun mu4e~headers-quit-buffer () + "Quit the mu4e-headers buffer. +This is a rather complex function, to ensure we don't disturb +other windows." + (interactive) + (unless (eq major-mode 'mu4e-headers-mode) + (mu4e-error "Must be in mu4e-headers-mode (%S)" major-mode)) + (mu4e-mark-handle-when-leaving) + (let ((curbuf (current-buffer)) (curwin (selected-window)) + (headers-visible)) + (walk-windows + (lambda (win) + (with-selected-window win + ;; if we the view window connected to this one, kill it + (when (and (not (one-window-p win)) (eq mu4e~headers-view-win win)) + (delete-window win) + (setq mu4e~headers-view-win nil))) + ;; and kill any _other_ (non-selected) window that shows the current + ;; buffer + (when (and + (eq curbuf (window-buffer win)) ;; does win show curbuf? + (not (eq curwin win)) ;; it's not the curwin? + (not (one-window-p))) ;; and not the last one? + (delete-window win)))) ;; delete it! + ;; now, all *other* windows should be gone. kill ourselves, and return + ;; to the main view + (kill-buffer) + (mu4e~main-view))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(provide 'mu4e-headers) diff --git a/_spacemacs.d/local/mu4e/mu4e-lists.el b/_spacemacs.d/local/mu4e/mu4e-lists.el new file mode 100644 index 0000000..00884da --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-lists.el @@ -0,0 +1,93 @@ +;;; mu4e-lists.el -- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; In this file, we create a table of list-id -> shortname for mailing lists. +;; The shortname (friendly) should a at most 8 characters, camel-case + + +(defvar mu4e~mailing-lists + '( ("bbdb-info.lists.sourceforge.net" . "BBDB") + ("boost-announce.lists.boost.org" . "BoostA") + ("boost-interest.lists.boost.org" . "BoostI") + ("conkeror.mozdev.org" . "Conkeror") + ("curl-library.cool.haxx.se" . "LibCurl") + ("crypto-gram-list.schneier.com " . "CryptoGr") + ("dbus.lists.freedesktop.org" . "DBus") + ("desktop-devel-list.gnome.org" . "GnomeDT") + ("emacs-devel.gnu.org" . "EmacsDev") + ("emacs-orgmode.gnu.org" . "Orgmode") + ("emms-help.gnu.org" . "Emms") + ("enlightenment-devel.lists.sourceforge.net" . "E-Dev") + ("erlang-questions.erlang.org" . "Erlang") + ("evolution-hackers.lists.ximian.com" . "EvoDev") + ("farsight-devel.lists.sourceforge.net" . "Farsight") + ("mailman.lists.freedesktop.org" . "FDeskTop") + ("gcc-help.gcc.gnu.org" . "Gcc") + ("gmime-devel-list.gnome.org" . "GMimeDev") + ("gnome-shell-list.gnome.org" . "GnomeSh") + ("gnu-emacs-sources.gnu.org" . "EmacsSrc") + ("gnupg-users.gnupg.org" . "GnupgU") + ("gpe.handhelds.org" . "GPE") + ("gstreamer-devel.lists.freedesktop.org" . "GstDev") + ("gstreamer-devel.lists.sourceforge.net" . "GstDev") + ("gstreamer-openmax.lists.sourceforge.net" . "GstOmx") + ("gtk-devel-list.gnome.org" . "GtkDev") + ("gtkmm-list.gnome.org" . "GtkmmDev") + ("guile-devel.gnu.org" . "GuileDev") + ("guile-user.gnu.org" . "GuileUsr") + ("help-gnu-emacs.gnu.org" . "EmacsUsr") + ("lggdh-algemeen.vvtp.tudelft.nl" . "LGGDH") + ("linux-bluetooth.vger.kernel.org" . "Bluez") + ("maemo-developers.maemo.org" . "MaemoDev") + ("maemo-users.maemo.org" . "MaemoUsr") + ("monit-general.nongnu.org" . "Monit") + ("mu-discuss.googlegroups.com" . "Mu") + ("nautilus-list.gnome.org" . "Nautilus") + ("notmuch.notmuchmail.org" . "Notmuch") + ("orbit-list.gnome.org" . "ORBit") + ("pulseaudio-discuss.lists.freedesktop.org" . "PulseA") + ("sqlite-announce.sqlite.org" . "SQliteAnn") + ("sqlite-dev.sqlite.org" . "SQLiteDev") + ("sup-talk.rubyforge.org" . "Sup") + ("sylpheed-claws-users.lists.sourceforge.net" . "Sylpheed") + ("tinymail-devel-list.gnome.org" . "Tinymail") + ("unicode.sarasvati.unicode.org" . "Unicode") + ("xapian-discuss.lists.xapian.org" . "Xapian") + ("xdg.lists.freedesktop.org" . "XDG") + ("wl-en.lists.airs.net" . "Wdrlust") + ("wl-en.ml.gentei.org" . "WdrLust") + ("xapian-devel.lists.xapian.org" . "Xapian") + ("zsh-users.zsh.org" . "ZshUsr")) + "AList of cells (MAILING-LIST-ID . SHORTNAME)") + +(defvar 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'.") + +(defvar 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.") + +(provide 'mu4e-lists) diff --git a/_spacemacs.d/local/mu4e/mu4e-main.el b/_spacemacs.d/local/mu4e/mu4e-main.el new file mode 100644 index 0000000..0f4dc78 --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-main.el @@ -0,0 +1,225 @@ +;;; mu4e-main.el -- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Code: +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(require 'smtpmail) ;; the queing stuff (silence elint) +(require 'mu4e-utils) ;; utility functions +(require 'mu4e-context) ;; the context + + +(defconst mu4e~main-buffer-name " *mu4e-main*" + "*internal* Name of the mu4e main view buffer.") + +(defvar mu4e-main-mode-map + (let ((map (make-sparse-keymap))) + + (define-key map "b" 'mu4e-headers-search-bookmark) + (define-key map "B" 'mu4e-headers-search-bookmark-edit) + + (define-key map "s" 'mu4e-headers-search) + (define-key map "q" 'mu4e-quit) + (define-key map "j" 'mu4e~headers-jump-to-maildir) + (define-key map "C" 'mu4e-compose-new) + + (define-key map "m" 'mu4e~main-toggle-mail-sending-mode) + (define-key map "f" 'smtpmail-send-queued-mail) + + ;; + (define-key map "U" 'mu4e-update-mail-and-index) + (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 "S" 'mu4e-interrupt-update-mail) + (define-key map (kbd "C-S-u") 'mu4e-update-mail-and-index) + (define-key map ";" 'mu4e-context-switch) + + (define-key map "$" 'mu4e-show-log) + (define-key map "A" 'mu4e-about) + (define-key map "N" 'mu4e-news) + (define-key map "H" 'mu4e-display-manual) + map) + + "Keymap for the *mu4e-main* buffer.") +(fset 'mu4e-main-mode-map mu4e-main-mode-map) + +(defvar mu4e-main-mode-abbrev-table nil) +(define-derived-mode mu4e-main-mode special-mode "mu4e:main" + "Major mode for the mu4e main screen. +\\{mu4e-main-mode-map}." + (use-local-map mu4e-main-mode-map) + (setq + truncate-lines t + overwrite-mode 'overwrite-mode-binary) + + ;; show context in mode-string + (set (make-local-variable 'global-mode-string) '(:eval (mu4e-context-label))) + (set (make-local-variable 'revert-buffer-function) #'mu4e~main-view-real)) + + +(defun mu4e~main-action-str (str &optional func-or-shortcut) + "Highlight the first occurence of [.] in STR. +If FUNC-OR-SHORTCUT is non-nil and if it is a function, call it +when STR is clicked (using RET or mouse-2); if FUNC-OR-SHORTCUT is +a string, execute the corresponding keyboard action when it is +clicked." + (let ((newstr + (replace-regexp-in-string + "\\[\\(..?\\)\\]" + (lambda(m) + (format "[%s]" + (propertize (match-string 1 m) 'face 'mu4e-highlight-face))) + str)) + (map (make-sparse-keymap)) + (func (if (functionp func-or-shortcut) + func-or-shortcut + (if (stringp func-or-shortcut) + (lexical-let ((macro func-or-shortcut)) + (lambda()(interactive) + (execute-kbd-macro macro))))))) + (define-key map [mouse-2] func) + (define-key map (kbd "RET") func) + (put-text-property 0 (length newstr) 'keymap map newstr) + (put-text-property (string-match "\\[.+$" newstr) + (- (length newstr) 1) 'mouse-face 'highlight newstr) newstr)) + +;; NEW +;; This is the old `mu4e~main-view' function but without +;; buffer switching at the end. +(defun mu4e~main-view-real (ignore-auto noconfirm) + (let ((buf (get-buffer-create mu4e~main-buffer-name)) + (inhibit-read-only t)) + (with-current-buffer buf + (erase-buffer) + (insert + "* " + (propertize "mu4e - mu for emacs version " 'face 'mu4e-title-face) + (propertize mu4e-mu-version 'face 'mu4e-header-key-face) + + ;; show some server properties; in this case; a big C when there's + ;; crypto support, a big G when there's Guile support + " " + (propertize + (concat + (when (plist-get mu4e~server-props :crypto) "C") + (when (plist-get mu4e~server-props :guile) "G")) + 'face 'mu4e-title-face) + + "\n\n" + (propertize " Basics\n\n" 'face 'mu4e-title-face) + (mu4e~main-action-str "\t* [j]ump to some maildir\n" 'mu4e-jump-to-maildir) + (mu4e~main-action-str "\t* enter a [s]earch query\n" 'mu4e-search) + (mu4e~main-action-str "\t* [C]ompose a new message\n" 'mu4e-compose-new) + "\n" + (propertize " Bookmarks\n\n" 'face 'mu4e-title-face) + ;; TODO: it's a bit uncool to hard-code the "b" shortcut... + (mapconcat + (lambda (bm) + (let* ((query (nth 0 bm)) (title (nth 1 bm)) (key (nth 2 bm))) + (mu4e~main-action-str + (concat "\t* [b" (make-string 1 key) "] " title) + (concat "b" (make-string 1 key))))) + mu4e-bookmarks "\n") + "\n\n" + (propertize " Misc\n\n" 'face 'mu4e-title-face) + + (mu4e~main-action-str "\t* [;]Switch focus\n" 'mu4e-context-switch) + + (mu4e~main-action-str "\t* [U]pdate email & database\n" + 'mu4e-update-mail-and-index) + + ;; show the queue functions if `smtpmail-queue-dir' is defined + (if (file-directory-p smtpmail-queue-dir) + (mu4e~main-view-queue) + "") + "\n" + (mu4e~main-action-str "\t* [N]ews\n" 'mu4e-news) + (mu4e~main-action-str "\t* [A]bout mu4e\n" 'mu4e-about) + (mu4e~main-action-str "\t* [H]elp\n" 'mu4e-display-manual) + (mu4e~main-action-str "\t* [q]uit\n" 'mu4e-quit)) + (mu4e-main-mode) + ))) + +(defun mu4e~main-view-queue () + "Display queue-related actions in the main view." + (concat + (mu4e~main-action-str "\t* toggle [m]ail sending mode " + 'mu4e~main-toggle-mail-sending-mode) + "(currently " + (propertize (if smtpmail-queue-mail "queued" "direct") + 'face 'mu4e-header-key-face) + ")\n" + (let ((queue-size (mu4e~main-queue-size))) + (if (zerop queue-size) + "" + (mu4e~main-action-str + (format "\t* [f]lush %s queued %s\n" + (propertize (int-to-string queue-size) + 'face 'mu4e-header-key-face) + (if (> queue-size 1) "mails" "mail")) + 'smtpmail-send-queued-mail))))) + +(defun mu4e~main-queue-size () + "Return, as an int, the number of emails in the queue." + (condition-case nil + (with-temp-buffer + (insert-file-contents (expand-file-name smtpmail-queue-index-file + smtpmail-queue-dir)) + (count-lines (point-min) (point-max))) + (error 0))) + +(defun mu4e~main-view () + "Create the mu4e main-view, and switch to it." + (mu4e~main-view-real nil nil) + (switch-to-buffer mu4e~main-buffer-name) + (goto-char (point-min)) + (setq global-mode-string '(:eval (mu4e-context-label)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Interactive functions +;; NEW +;; Toggle mail sending mode without switching +(defun mu4e~main-toggle-mail-sending-mode () + "Toggle sending mail mode, either queued or direct." + (interactive) + (let ((curpos (point))) + (unless (file-directory-p smtpmail-queue-dir) + (mu4e-error "`smtpmail-queue-dir' does not exist")) + (setq smtpmail-queue-mail (not smtpmail-queue-mail)) + (message + (concat "Outgoing mail will now be " + (if smtpmail-queue-mail "queued" "sent directly"))) + (mu4e~main-view-real nil nil) + (goto-char curpos))) + + +;; (progn +;; (define-key mu4e-compose-mode-map (kbd "C-c m") 'mu4e~main-toggle-mail-sending-mode) +;; (define-key mu4e-view-mode-map (kbd "C-c m") 'mu4e~main-toggle-mail-sending-mode) +;; (define-key mu4e-compose-mode-map (kbd "C-c m") 'mu4e~main-toggle-mail-sending-mode) +;; (define-key mu4e-headers-mode-map (kbd "C-c m") 'mu4e~main-toggle-mail-sending-mode) +;; ) + +(provide 'mu4e-main) diff --git a/_spacemacs.d/local/mu4e/mu4e-mark.el b/_spacemacs.d/local/mu4e/mu4e-mark.el new file mode 100644 index 0000000..4b17f34 --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-mark.el @@ -0,0 +1,466 @@ +;; mu4e-mark.el -- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; In this file are function related to marking messages; they assume we are +;; currently in the headers buffer. + +;; Code: +(require 'mu4e-proc) +(require 'mu4e-utils) +(require 'mu4e-message) + +(eval-when-compile (byte-compile-disable-warning 'cl-functions)) + +;; keep byte-compiler happy +(declare-function mu4e~headers-mark "mu4e-headers") +(declare-function mu4e~headers-goto-docid "mu4e-headers") +(declare-function mu4e-headers-next "mu4e-headers") + + +(defcustom mu4e-headers-leave-behavior 'ask + "What to do when user leaves the headers view. +That is when he e.g. quits, refreshes or does a new search. +Value is one of the following symbols: +- `ask' ask user whether to ignore the marks +- `apply' automatically apply the marks before doing anything else +- `ignore' automatically ignore the marks without asking" + :type '(choice (const ask :tag "ask user whether to ignore marks") + (const apply :tag "apply marks without asking") + (const ignore :tag "ignore marks without asking")) + :group 'mu4e-headers) + +(defcustom mu4e-mark-execute-pre-hook nil + "Hook run just *before* a mark is applied to a message. The hook function +is called with two arguments, the mark being executed and the message itself.") + +(defvar mu4e-headers-show-target t + "Whether to show targets (such as '-> delete', '-> /archive') +when marking message. Normally, this is useful information for the +user, however, when you often mark large numbers (thousands) of +message, showing the target makes this quite a bit slower (showing +the target uses an emacs feature called 'overlays', which aren't +particularly fast).") + +;;; insert stuff;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defvar mu4e~mark-map nil + "Map (hash) of docid->markinfo; when a message is marked, the +information is added here. +markinfo is a cons cell consisting of the following: +\(mark . target) +where + MARK is the type of mark (move, trash, delete) + TARGET (optional) is the target directory (for 'move')") + +;; the mark-map is specific for the current header buffer +;; currently, there can't be more than one, but we never know what will +;; happen in the future + +;; the fringe is the space on the left of headers, where we put marks below some +;; handy definitions; only `mu4e-mark-fringe-len' should be change (if ever), +;; the others follow from that. +(defconst mu4e~mark-fringe-len 2 + "Width of the fringe for marks on the left.") +(defconst mu4e~mark-fringe (make-string mu4e~mark-fringe-len ?\s) + "The space on the left of message headers to put marks.") +(defconst mu4e~mark-fringe-format (format "%%-%ds" mu4e~mark-fringe-len) + "Format string to set a mark and leave remaining space.") + +(defun mu4e~mark-initialize () + "Initialize the marks subsystem." + (set (make-local-variable 'mu4e~mark-map) (make-hash-table))) + +(defun mu4e~mark-clear () + "Clear the marks subsystem." + (clrhash mu4e~mark-map)) + +(defun mu4e~mark-find-headers-buffer () + "Find the headers buffer, if any." + (find-if + (lambda (b) + (with-current-buffer b + (eq major-mode 'mu4e-headers-mode))) + (buffer-list))) + +(defmacro mu4e~mark-in-context (&rest body) + "Evaluate BODY in the context of the headers buffer in case this +is either a headers or view buffer." + `(cond + ((eq major-mode 'mu4e-headers-mode) ,@body) + ((eq major-mode 'mu4e-view-mode) + (when (buffer-live-p mu4e~view-headers-buffer) + (let* ((msg (mu4e-message-at-point)) + (docid (mu4e-message-field msg :docid))) + (with-current-buffer mu4e~view-headers-buffer + (if (mu4e~headers-goto-docid docid) + ,@body + (mu4e-error "cannot find message in headers buffer.")))))) + (t + ;; even in other modes (e.g. mu4e-main-mode we try to find + ;; the headers buffer + (let ((hbuf (mu4e~mark-find-headers-buffer))) + (if (buffer-live-p hbuf) + (with-current-buffer hbuf + ,@body) + (progn (mu4e-message "%S" major-mode) ,@body)))))) + +(defvar mu4e-marks + '((refile + :char ("r" . "â–¶") + :prompt "refile" + :dyn-target (lambda (target msg) (mu4e-get-refile-folder msg)) + :action (lambda (docid msg target) (mu4e~proc-move docid + (mu4e~mark-check-target target) "-N"))) + (delete + :char ("D" . "âŒ") + :prompt "Delete" + :show-target (lambda (target) "delete") + :action (lambda (docid msg target) (mu4e~proc-remove docid))) + (flag + :char ("+" . "✚") + :prompt "+flag" + :show-target (lambda (target) "flag") + :action (lambda (docid msg target) (mu4e~proc-move docid nil "+F-u-N"))) + (move + :char ("m" . "â–·") + :prompt "move" + :ask-target mu4e~mark-get-move-target + :action (lambda (docid msg target) (mu4e~proc-move docid + (mu4e~mark-check-target target) "-N"))) + (read + :char ("!" . "â—¼") + :prompt "!read" + :show-target (lambda (target) "read") + :action (lambda (docid msg target) (mu4e~proc-move docid nil "+S-u-N"))) + (trash + :char ("d" . "â–¼") + :prompt "dtrash" + :dyn-target (lambda (target msg) (mu4e-get-trash-folder msg)) + :action (lambda (docid msg target) (mu4e~proc-move docid + (mu4e~mark-check-target target) "+T-N"))) + (unflag + :char ("-" . "âž–") + :prompt "-unflag" + :show-target (lambda (target) "unflag") + :action (lambda (docid msg target) (mu4e~proc-move docid nil "-F-N"))) + (untrash + :char ("=" . "â–²") + :prompt "=untrash" + :show-target (lambda (target) "untrash") + :action (lambda (docid msg target) (mu4e~proc-move docid nil "-T"))) + (unread + :char ("?" . "â—»") + :prompt "?unread" + :show-target (lambda (target) "unread") + :action (lambda (docid msg target) (mu4e~proc-move docid nil "-S+u-N"))) + (unmark + :char " " + :prompt "unmark" + :action (mu4e-error "No action for unmarking")) + (action + :char ( "a" . "â—¯") + :prompt "action" + :ask-target (lambda () (mu4e-read-option "Action: " mu4e-headers-actions)) + :action (lambda (docid msg actionfunc) + (save-excursion + (when (mu4e~headers-goto-docid docid) + (mu4e-headers-action actionfunc))))) + (something + :char ("*" . "✱") + :prompt "*something" + :action (mu4e-error "No action for deferred mark"))) + + "The list of all the possible marks. +This is an alist mapping mark symbols to their properties. The +properties are: + :char (string) or (basic . fancy) The character to display in + the headers view. Either a single-character string, or a + dotted-pair cons cell where the second item will be used if + `mu4e-use-fancy-chars' is `t', otherwise we'll use + the first one. It can also be a plain string for backwards + compatibility since we didn't always support + `mu4e-use-fancy-chars' here. + :prompt (string) The prompt to use when asking for marks (used for + example when marking a whole thread) + :ask-target (function returning a string) Get the target. This + function run once per bulk-operation, and thus is suitable + for user-interaction. If nil, the target is nil. + :dyn-target (function from (TARGET MSG) to string). Compute + the dynamic target. This is run once per message, which is + passed as MSG. The default is to just return the target. + :show-target (function from TARGET to string) How to display + the target. + :action (function taking (DOCID MSG TARGET)). The action to + apply on the message.") + + +(defun mu4e-mark-at-point (mark target) + "Mark (or unmark) message at point. +MARK specifies the mark-type. For `move'-marks and `trash'-marks +the TARGET argument is non-nil and specifies to which +maildir the message is to be moved/trashed. The function works in +both headers buffers and message buffers. + +The following marks are available, and the corresponding props: + + MARK TARGET description + ---------------------------------------------------------- + `refile' y mark this message for archiving + `something' n mark this message for *something* (decided later) + `delete' n remove the message + `flag' n mark this message for flagging + `move' y move the message to some folder + `read' n mark the message as read + `trash' y trash the message to some folder + `unflag' n mark this message for unflagging + `untrash' n remove the 'trashed' flag from a message + `unmark' n unmark this message + `unread' n mark the message as unread + `action' y mark the message for some action." + (interactive) + (let* ((msg (mu4e-message-at-point)) + (docid (mu4e-message-field msg :docid)) + ;; get a cell with the mark char and the 'target' 'move' already has a + ;; target (the target folder) the other ones get a pseudo "target", as + ;; info for the user. + (markdesc (cdr (or (assq mark mu4e-marks) (mu4e-error "Invalid mark %S" mark)))) + (get-markkar + (lambda (char) + (if (listp char) + (if mu4e-use-fancy-chars (cdr char) (car char)) + char))) + (markkar (funcall get-markkar (plist-get markdesc :char))) + (target (mu4e~mark-get-dyn-target mark target)) + (show-fct (plist-get markdesc :show-target)) + (shown-target (if show-fct + (funcall show-fct target) + (if target (format "%S" target))))) + (unless docid (mu4e-warn "No message on this line")) + (unless (eq major-mode 'mu4e-headers-mode) (mu4e-error "Not in headers-mode")) + (save-excursion + (when (mu4e~headers-mark docid markkar) + ;; update the hash -- remove everything current, and if add the new stuff, + ;; unless we're unmarking + (remhash docid mu4e~mark-map) + ;; remove possible overlays + (remove-overlays (line-beginning-position) (line-end-position)) + ;; now, let's set a mark (unless we were unmarking) + (unless (eql mark 'unmark) + (puthash docid (cons mark target) mu4e~mark-map) + ;; when we have a target (ie., when moving), show the target folder in + ;; an overlay + (when (and shown-target mu4e-headers-show-target) + (let* ((targetstr (propertize (concat "-> " shown-target " ") + 'face 'mu4e-system-face)) + ;; mu4e~headers-goto-docid docid t \will take us just after the + ;; docid cookie and then we skip the mu4e~mark-fringe + (start (+ (length mu4e~mark-fringe) + (mu4e~headers-goto-docid docid t))) + (overlay (make-overlay start (+ start (length targetstr))))) + (overlay-put overlay 'display targetstr) + docid))))))) + + +(defun mu4e~mark-get-move-target () + "Ask for a move target, and propose to create it if it does not exist." + (interactive) + ;; (mu4e-message-at-point) ;; raises error if there is none + (let* ((target (mu4e-ask-maildir "Move message to: ")) + (target (if (string= (substring target 0 1) "/") + target + (concat "/" target))) + (fulltarget (concat mu4e-maildir target))) + (when (or (file-directory-p fulltarget) + (and (yes-or-no-p + (format "%s does not exist. Create now?" fulltarget)) + (mu4e~proc-mkdir fulltarget))) + target))) + +(defun mu4e~mark-ask-target (mark) + "Ask the target for MARK, if the user should be asked the target." + (let ((getter (plist-get (cdr (assq mark mu4e-marks)) :ask-target))) + (and getter (funcall getter)))) + +(defun mu4e~mark-get-dyn-target (mark target) + "Get the dynamic target for MARK. The result may depend on the +message at point." + (let ((getter (plist-get (cdr (assq mark mu4e-marks)) :dyn-target))) + (if getter + (funcall getter target (mu4e-message-at-point)) + target))) + + +(defun mu4e-mark-set (mark &optional target) + "Mark the header at point, or, if region is active, mark all +headers in the region. Optionally, provide TARGET (for moves)." + (unless target + (setq target (mu4e~mark-ask-target mark))) + (if (not (use-region-p)) + ;; single message + (mu4e-mark-at-point mark target) + ;; mark all messages in the region. + (save-excursion + (let ((cant-go-further) (eor (region-end))) + (goto-char (region-beginning)) + (while (and (<= (point) eor) (not cant-go-further)) + (mu4e-mark-at-point mark target) + (setq cant-go-further (not (mu4e-headers-next)))))))) + +(defun mu4e-mark-restore (docid) + "Restore the visual mark for the message with DOCID." + (let ((markcell (gethash docid mu4e~mark-map))) + (when markcell + (save-excursion + (when (mu4e~headers-goto-docid docid) + (mu4e-mark-at-point (car markcell) (cdr markcell))))))) + +(defun mu4e~mark-get-markpair (prompt &optional allow-something) + "Ask user for a mark; return (MARK . TARGET). +If ALLOW-SOMETHING is non-nil, allow the 'something' pseudo mark +as well." + (let* ((marks (mapcar (lambda (markdescr) + (cons (plist-get (cdr markdescr) :prompt) + (car markdescr))) + mu4e-marks)) + (marks + (if allow-something + marks (remove-if (lambda (m) (eq 'something (cdr m))) marks))) + (mark (mu4e-read-option prompt marks)) + (target (mu4e~mark-ask-target mark))) + (cons mark target))) + + +(defun mu4e-mark-resolve-deferred-marks () + "Check if there are any deferred ('something') marks. +If there are such marks, replace them with a _real_ mark (ask the +user which one)." + (interactive) + (mu4e~mark-in-context + (let ((markpair)) + (maphash + (lambda (docid val) + (let ((mark (car val)) (target (cdr val))) + (when (eql mark 'something) + (unless markpair + (setq markpair + (mu4e~mark-get-markpair "Set deferred mark(s) to: " nil))) + (save-excursion + (when (mu4e~headers-goto-docid docid) + (mu4e-mark-set (car markpair) (cdr markpair))))))) + mu4e~mark-map)))) + +(defun mu4e~mark-check-target (target) + "Check if the target exists; if not, offer to create it." + (let ((fulltarget (concat mu4e-maildir target))) + (if (not (mu4e-create-maildir-maybe fulltarget)) + (mu4e-error "Target dir %s does not exist " fulltarget) + target))) + +(defun mu4e-mark-execute-all (&optional no-confirmation) + "Execute the actions for all marked messages in this buffer. +After the actions have been executed succesfully, the affected +messages are *hidden* from the current header list. Since the +headers are the result of a search, we cannot be certain that the +messages no longer match the current one - to get that +certainty, we need to rerun the search, but we don't want to do +that automatically, as it may be too slow and/or break the user's +flow. Therefore, we hide the message, which in practice seems to +work well. + +If NO-CONFIRMATION is non-nil, don't ask user for confirmation." + (interactive) + (mu4e~mark-in-context + (let ((marknum (hash-table-count mu4e~mark-map))) + (if (zerop marknum) + (message "Nothing is marked") + (mu4e-mark-resolve-deferred-marks) + (when (or no-confirmation + (y-or-n-p + (format "Are you sure you want to execute %d mark%s?" + marknum (if (> marknum 1) "s" "")))) + (maphash + (lambda (docid val) + (let* ((mark (car val)) (target (cdr val)) + (markdescr (assq mark mu4e-marks)) + (msg (save-excursion + (mu4e~headers-goto-docid docid) + (mu4e-message-at-point)))) + ;; note: whenever you do something with the message, + ;; it looses its N (new) flag + (if markdescr + (progn + (run-hook-with-args + 'mu4e-mark-execute-pre-hook mark msg) + (funcall (plist-get (cdr markdescr) :action) docid msg target)) + (mu4e-error "Unrecognized mark %S" mark)))) + mu4e~mark-map)) + (mu4e-mark-unmark-all) + (message nil))))) + +(defun mu4e-mark-unmark-all () + "Unmark all marked messages." + (interactive) + (mu4e~mark-in-context + (when (or (null mu4e~mark-map) (zerop (hash-table-count mu4e~mark-map))) + (mu4e-warn "Nothing is marked")) + (maphash + (lambda (docid val) + (save-excursion + (when (mu4e~headers-goto-docid docid) + (mu4e-mark-set 'unmark)))) + mu4e~mark-map) + ;; in any case, clear the marks map + (mu4e~mark-clear))) + +(defun mu4e-mark-docid-marked-p (docid) + "Is the given docid marked?" + (when (gethash docid mu4e~mark-map) t)) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defun mu4e-mark-marks-num () + "Return the number of marks in the current buffer." + (if mu4e~mark-map (hash-table-count mu4e~mark-map) 0)) + + +(defun mu4e-mark-handle-when-leaving () + "If there are any marks in the current buffer, handle those +according to the value of `mu4e-headers-leave-behavior'. This +function is to be called before any further action (like searching, +quitting the buffer) is taken; returning t means 'take the following +action', return nil means 'don't do anything'." + (mu4e~mark-in-context + (let ((marknum (mu4e-mark-marks-num)) + (what mu4e-headers-leave-behavior)) + (unless (zerop marknum) ;; nothing to do? + (when (eq what 'ask) + (setq what (mu4e-read-option + (format "There are %d existing mark(s); should we: " marknum) + '( ("apply marks" . apply) + ("ignore marks?" . ignore))))) + ;; we determined what to do... now do it + (when (eq what 'apply) + (mu4e-mark-execute-all t)))))) + + +(provide 'mu4e-mark) +;; End of mu4e-mark.el diff --git a/_spacemacs.d/local/mu4e/mu4e-message.el b/_spacemacs.d/local/mu4e/mu4e-message.el new file mode 100644 index 0000000..e339077 --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-message.el @@ -0,0 +1,284 @@ +;;; mu4e-message.el -- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2012-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Functions to get data from mu4e-message plist structure + +;;; Code: +(eval-when-compile (byte-compile-disable-warning 'cl-functions)) + +(require 'mu4e-vars) +(require 'mu4e-utils) + +(require 'cl) +(require 'html2text) + + +(defcustom mu4e-html2text-command + (if (fboundp 'shr-insert-document) 'mu4e-shr2text 'html2text) + + "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 the current buffer in html +as input, and transforms it into html (like the `html2text' +function). + +In both cases, the output is expected to be in UTF-8 encoding. + +Newer emacs has the shr renderer, and when its available, +conversion defaults `mu4e-shr2text'; otherwise, the default is +emacs' built-in `html2text' function." + :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 which mu4e will consider the plain text part to be 'This +messages requires html' text bodies." + :type 'integer + :group 'mu4e-view) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defsubst mu4e-message-field-raw (msg field) + "Retrieve FIELD from message plist MSG. +FIELD is one of :from, :to, :cc, :bcc, :subject, :data, +:message-id, :path, :maildir, :priority, :attachments, +:references, :in-reply-to, :body-txt, :body-html + +Returns `nil' if the field does not exist. + +A message plist looks something like: +\(:docid 32461 + :from ((\"Nikola Tesla\" . \"niko@example.com\")) + :to ((\"Thomas Edison\" . \"tom@example.com\")) + :cc ((\"Rupert The Monkey\" . \"rupert@example.com\")) + :subject \"RE: what about the 50K?\" + :date (20369 17624 0) + :size 4337 + :message-id \"6BDC23465F79238C8233AB82D81EE81AF0114E4E74@123213.mail.example.com\" + :path \"/home/tom/Maildir/INBOX/cur/133443243973_1.10027.atlas:2,S\" + :maildir \"/INBOX\" + :priority normal + :flags (seen) + :attachments + ((:index 2 :name \"photo.jpg\" :mime-type \"image/jpeg\" :size 147331) + (:index 3 :name \"book.pdf\" :mime-type \"application/pdf\" :size 192220)) + :references (\"6BDC23465F79238C8384574032D81EE81AF0114E4E74@123213.mail.example.com\" + \"6BDC23465F79238203498230942D81EE81AF0114E4E74@123213.mail.example.com\") + :in-reply-to \"6BDC23465F79238203498230942D81EE81AF0114E4E74@123213.mail.example.com\" + :body-txt \"Hi Tom, ...\" +\)). +Some notes on the format: +- The address fields are lists of pairs (NAME . EMAIL), where NAME can be nil. +- The date is in format emacs uses in `current-time' +- Attachments are a list of elements with fields :index (the number of + the MIME-part), :name (the file name, if any), :mime-type (the + MIME-type, if any) and :size (the size in bytes, if any). +- Messages in the Headers view come from the database and do not have + :attachments, :body-txt or :body-html fields. Message in the + Message view use the actual message file, and do include these fields." + ;; after all this documentation, the spectacular implementation + (if msg + (plist-get msg field) + (mu4e-error "message must be non-nil"))) + +(defsubst mu4e-message-field (msg field) + "Retrieve FIELD from message plist MSG. +Like `mu4e-message-field-nil', but will sanitize `nil' values: +- all string field except body-txt/body-html: nil -> \"\" +- numeric fields + dates : nil -> 0 +- all others : return the value +Thus, function will return nil for empty lists, non-existing body-txt or body-html." + (let ((val (mu4e-message-field-raw msg field))) + (cond + (val + val) ;; non-nil -> just return it + ((member field '(:subject :message-id :path :maildir :in-reply-to)) + "") ;; string fields except body-txt, body-html: nil -> "" + ((member field '(:body-html :body-txt)) + val) + ((member field '(:docid :size)) + 0) ;; numeric type: nil -> 0 + (t + val)))) ;; otherwise, just return nil + +(defsubst mu4e-message-has-field (msg field) + "Return t if MSG contains FIELD, nil otherwise." + (plist-member msg field)) + +(defsubst mu4e-message-at-point (&optional noerror) + "Get the message s-expression for the message at point in either +the headers buffer or the view buffer, or nil if there is no such +message. If optional NOERROR is non-nil, do not raise an error when +there is no message at point." + (let ((msg (or (get-text-property (point) 'msg) mu4e~view-msg))) + (if msg + msg + (unless noerror (mu4e-warn "No message at point"))))) + +(defsubst mu4e-message-field-at-point (field) + "Get the field FIELD from the message at point. +This is equivalent to: + (mu4e-message-field (mu4e-message-at-point) FIELD)." + (mu4e-message-field (mu4e-message-at-point) field)) + +(defun mu4e-message-body-text (msg) + "Get the body in text form for this message. +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, thiss function prefers the text part, but this can +be changed by setting `mu4e-view-prefer-html'." + (let* ((txt (mu4e-message-field msg :body-txt)) + (html (mu4e-message-field msg :body-html)) + (body + (cond + ;; does it look like some text? ie., if the text part is more than + ;; mu4e-view-html-plaintext-ratio-heuristic times shorter than the + ;; html part, it should't be used + ;; This is an heuristic to guard against 'This messages requires + ;; html' text bodies. + ((and (> (* mu4e-view-html-plaintext-ratio-heuristic + (length txt)) (length html)) + ;; use html if it's prefered, unless there is no html + (or (not mu4e-view-prefer-html) (not html))) + txt) + ;; otherwise, it there some html? + (html + (with-temp-buffer + (insert html) + (cond + ((stringp mu4e-html2text-command) + (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))) + ((functionp mu4e-html2text-command) + (funcall mu4e-html2text-command)) + (t (mu4e-error "Invalid `mu4e-html2text-command'"))) + (buffer-string)) + ) + (t ;; otherwise, an empty body + "")))) + ;; and finally, remove some crap from the remaining string; it seems + ;; esp. outlook lies about its encoding (ie., it says 'iso-8859-1' but + ;; really it's 'windows-1252'), thus giving us these funky chars. here, we + ;; either remove them, or replace with 'what-was-meant' (heuristically) + (with-temp-buffer + (insert body) + (goto-char (point-min)) + (while (re-search-forward "[
’]" nil t) + (replace-match + (cond + ((string= (match-string 0) "’") "'") + (t "")))) + (buffer-string)))) + +(defun mu4e-message-contact-field-matches (msg cfield rx) + "Checks whether any of the of the contacts in field +CFIELD (either :to, :from, :cc or :bcc, or a list of those) of +msg MSG matches (with their name or e-mail address) regular +expressions RX. If there is a match, return non-nil; otherwise +return nil. RX can also be a list of regular expressions, in +which case any of those are tried for a match." + (if (and cfield (listp cfield)) + (or (mu4e-message-contact-field-matches msg (car cfield) rx) + (mu4e-message-contact-field-matches msg (cdr cfield) rx)) + (when cfield + (if (listp rx) + ;; if rx is a list, try each one of them for a match + (find-if + (lambda (a-rx) (mu4e-message-contact-field-matches msg cfield a-rx)) + rx) + ;; not a list, check the rx + (find-if + (lambda (ct) + (let ((name (car ct)) (email (cdr ct))) + (or + (and name (string-match rx name)) + (and email (string-match rx email))))) + (mu4e-message-field msg cfield)))))) + +(defun mu4e-message-contact-field-matches-me (msg cfield) + "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 e-mail address in +`mu4e-user-mail-address-list'. Returns the contact cell that +matched, or nil." + (find-if + (lambda (cc-cell) + (member-if + (lambda (addr) + (string= (downcase addr) (downcase (cdr cc-cell)))) + mu4e-user-mail-address-list)) + (mu4e-message-field msg cfield))) + +(defsubst mu4e-message-part-field (msgpart field) + "Get some field in a message part; a part would look something like: + (:index 2 :name \"photo.jpg\" :mime-type \"image/jpeg\" :size 147331)." + (plist-get msgpart field)) + +;; backward compatibility ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defalias 'mu4e-msg-field 'mu4e-message-field) +(defalias 'mu4e-body-text 'mu4e-message-body-text) ;; backward compatibility + +(defun mu4e-field-at-point (field) + "Get FIELD (a symbol, see `mu4e-header-info') for the message at +point in eiter the headers buffer or the view buffer." + (plist-get (mu4e-message-at-point) field)) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defun mu4e-shr2text () + "Html 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." + (interactive) + (let ((dom (libxml-parse-html-region (point-min) (point-max))) + ;; When HTML emails contain references to remote images, + ;; retrieving these images leaks information. For example, + ;; the sender can see when I openend the email and from which + ;; computer (IP address). For this reason, it is preferrable + ;; to not retrieve images. + ;; See this discussion on mu-discuss: + ;; https://groups.google.com/forum/#!topic/mu-discuss/gr1cwNNZnXo + (shr-inhibit-images t)) + (erase-buffer) + (shr-insert-document dom) + (goto-char (point-min)))) + +(provide 'mu4e-message) diff --git a/_spacemacs.d/local/mu4e/mu4e-meta.el b/_spacemacs.d/local/mu4e/mu4e-meta.el new file mode 100644 index 0000000..50c87f9 --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-meta.el @@ -0,0 +1,11 @@ +;; auto-generated +(defconst mu4e-mu-version "0.9.17" + "Required mu binary version; mu4e's version must agree with this.") + +(defconst mu4e-builddir "/home/aly/build/mu" + "Top-level build directory.") + +(defconst mu4e-doc-dir "/home/aly/local/mu/share/doc/mu" + "Mu4e's data-dir.") + +(provide 'mu4e-meta) diff --git a/_spacemacs.d/local/mu4e/mu4e-proc.el b/_spacemacs.d/local/mu4e/mu4e-proc.el new file mode 100644 index 0000000..d93af5d --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-proc.el @@ -0,0 +1,524 @@ +;; mu4e-proc.el -- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Code: +(require 'mu4e-vars) +(require 'mu4e-utils) +(require 'mu4e-meta) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; internal vars + +(defvar mu4e~proc-buf nil + "Buffer (string) for data received from the backend.") +(defconst mu4e~proc-name " *mu4e-proc*" + "Name of the server process, buffer.") +(defvar mu4e~proc-process nil + "The mu-server process.") + +;; dealing with the length cookie that precedes expressions +(defconst mu4e~cookie-pre "\376" + "Each expression we get from the backend (mu server) starts with +a length cookie: + <`mu4e~cookie-pre'><length-in-hex><`mu4e~cookie-post'>.") +(defconst mu4e~cookie-post "\377" + "Each expression we get from the backend (mu server) starts with +a length cookie: + <`mu4e~cookie-pre'><length-in-hex><`mu4e~cookie-post'>.") +(defconst mu4e~cookie-matcher-rx + (concat mu4e~cookie-pre "\\([[:xdigit:]]+\\)" mu4e~cookie-post) + "Regular expression matching the length cookie. +Match 1 will be the length (in hex).") + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defsubst mu4e~proc-send-command (frm &rest args) + "Send as command to the mu server process. +Start the process if needed." + (unless (mu4e~proc-running-p) + (mu4e~proc-start)) + (let ((cmd (apply 'format frm args))) + (mu4e-log 'to-server "%s" cmd) + (process-send-string mu4e~proc-process (concat cmd "\n")))) + +(defun mu4e~proc-start () + "Start the mu server process." + (unless (file-executable-p mu4e-mu-binary) + (mu4e-error (format "`mu4e-mu-binary' (%S) not found" mu4e-mu-binary))) + (let* ((process-connection-type nil) ;; use a pipe + (args '("server")) + (args (append args (when mu4e-mu-home + (list (concat "--muhome=" mu4e-mu-home)))))) + (setq mu4e~proc-buf "") + (setq mu4e~proc-process (apply 'start-process + mu4e~proc-name mu4e~proc-name + mu4e-mu-binary args)) + ;; register a function for (:info ...) sexps + (unless mu4e~proc-process + (mu4e-error "Failed to start the mu4e backend")) + (set-process-query-on-exit-flag mu4e~proc-process nil) + (set-process-coding-system mu4e~proc-process 'binary 'utf-8-unix) + (set-process-filter mu4e~proc-process 'mu4e~proc-filter) + (set-process-sentinel mu4e~proc-process 'mu4e~proc-sentinel))) + +(defun mu4e~proc-kill () + "Kill the mu server process." + (let* ((buf (get-buffer mu4e~proc-name)) + (proc (and (buffer-live-p buf) (get-buffer-process buf)))) + (when proc + (let ((delete-exited-processes t)) + ;; the mu server signal handler will make it quit after 'quit' + (mu4e~proc-send-command "cmd:quit")) + ;; try sending SIGINT (C-c) to process, so it can exit gracefully + (ignore-errors + (signal-process proc 'SIGINT)))) + (setq + mu4e~proc-process nil + mu4e~proc-buf nil)) + +(defun mu4e~proc-running-p () + "Whether the mu process is running." + (when (and mu4e~proc-process + (memq (process-status mu4e~proc-process) + '(run open listen connect stop))) + t)) + +(defsubst mu4e~proc-eat-sexp-from-buf () + "'Eat' the next s-expression from `mu4e~proc-buf'. +Note: this is a string, not an emacs-buffer. `mu4e~proc-buf gets +its contents from the mu-servers in the following form: + <`mu4e~cookie-pre'><length-in-hex><`mu4e~cookie-post'> +Function returns this sexp, or nil if there was none. +`mu4e~proc-buf' is updated as well, with all processed sexp data +removed." + (ignore-errors ;; the server may die in the middle... + ;; mu4e~cookie-matcher-rx: + ;; (concat mu4e~cookie-pre "\\([[:xdigit:]]+\\)]" mu4e~cookie-post) + (let ((b (string-match mu4e~cookie-matcher-rx mu4e~proc-buf)) + (sexp-len) (objcons)) + (when b + (setq sexp-len (string-to-number (match-string 1 mu4e~proc-buf) 16)) + ;; does mu4e~proc-buf contain the full sexp? + (when (>= (length mu4e~proc-buf) (+ sexp-len (match-end 0))) + ;; clear-up start + (setq mu4e~proc-buf (substring mu4e~proc-buf (match-end 0))) + ;; note: we read the input in binary mode -- here, we take the part + ;; that is the sexp, and convert that to utf-8, before we interpret + ;; it. + (setq objcons (read-from-string + (decode-coding-string + (substring mu4e~proc-buf 0 sexp-len) + 'utf-8 t))) + (when objcons + (setq mu4e~proc-buf (substring mu4e~proc-buf sexp-len)) + (car objcons))))))) + + +(defun mu4e~proc-filter (proc str) + "A process-filter for the 'mu server' output. +It accumulates the strings into valid sexps by checking of the +';;eox' end-of-sexp marker, and then evaluating them. + +The server output is as follows: + + 1. an error + (:error 2 :message \"unknown command\") + ;; eox + => this will be passed to `mu4e-error-func'. + + 2a. a message sexp looks something like: + \( + :docid 1585 + :from ((\"Donald Duck\" . \"donald@example.com\")) + :to ((\"Mickey Mouse\" . \"mickey@example.com\")) + :subject \"Wicked stuff\" + :date (20023 26572 0) + :size 15165 + :references (\"200208121222.g7CCMdb80690@msg.id\") + :in-reply-to \"200208121222.g7CCMdb80690@msg.id\" + :message-id \"foobar32423847ef23@pluto.net\" + :maildir: \"/archive\" + :path \"/home/mickey/Maildir/inbox/cur/1312254065_3.32282.pluto,4cd5bd4e9:2,\" + :priority high + :flags (new unread) + :attachments ((2 \"hello.jpg\" \"image/jpeg\") (3 \"laah.mp3\" \"audio/mp3\")) + :body-txt \" <message body>\" +\) +;; eox + => this will be passed to `mu4e-header-func'. + + 2b. After the list of message sexps has been returned (see 2a.), + we'll receive a sexp that looks like + (:found <n>) with n the number of messages found. The <n> will be + passed to `mu4e-found-func'. + + 3. a view looks like: + (:view <msg-sexp>) + => the <msg-sexp> (see 2.) will be passed to `mu4e-view-func'. + + 4. a database update looks like: + (:update <msg-sexp> :move <nil-or-t>) + + => the <msg-sexp> (see 2.) will be passed to + `mu4e-update-func', :move tells us whether this is a move to + another maildir, or merely a flag change. + + 5. a remove looks like: + (:remove <docid>) + => the docid will be passed to `mu4e-remove-func' + + 6. a compose looks like: + (:compose <reply|forward|edit|new> [:original<msg-sexp>] [:include <attach>]) + `mu4e-compose-func'." + (mu4e-log 'misc "* Received %d byte(s)" (length str)) + (setq mu4e~proc-buf (concat mu4e~proc-buf str)) ;; update our buffer + (let ((sexp (mu4e~proc-eat-sexp-from-buf))) + (with-local-quit + (while sexp + (mu4e-log 'from-server "%S" sexp) + (cond + ;; a header plist can be recognized by the existence of a :date field + ((plist-get sexp :date) + (funcall mu4e-header-func sexp)) + + ;; the found sexp, we receive after getting all the headers + ((plist-get sexp :found) + (funcall mu4e-found-func (plist-get sexp :found))) + + ;; viewing a specific message + ((plist-get sexp :view) + (funcall mu4e-view-func (plist-get sexp :view))) + + ;; receive an erase message + ((plist-get sexp :erase) + (funcall mu4e-erase-func)) + + ;; receive a :sent message + ((plist-get sexp :sent) + (funcall mu4e-sent-func + (plist-get sexp :docid) + (plist-get sexp :path))) + + ;; received a pong message + ((plist-get sexp :pong) + (funcall mu4e-pong-func + (plist-get sexp :props))) + + ;; received a contacts message + ;; note: we use 'member', to match (:contacts nil) + ((plist-member sexp :contacts) + (funcall mu4e-contacts-func + (plist-get sexp :contacts))) + + ;; something got moved/flags changed + ((plist-get sexp :update) + (funcall mu4e-update-func + (plist-get sexp :update) (plist-get sexp :move))) + + ;; a message got removed + ((plist-get sexp :remove) + (funcall mu4e-remove-func (plist-get sexp :remove))) + + ;; start composing a new message + ((plist-get sexp :compose) + (funcall mu4e-compose-func + (plist-get sexp :compose) + (plist-get sexp :original) + (plist-get sexp :include))) + + ;; do something with a temporary file + ((plist-get sexp :temp) + (funcall mu4e-temp-func + (plist-get sexp :temp) ;; name of the temp file + (plist-get sexp :what) ;; what to do with it + ;; (pipe|emacs|open-with...) + (plist-get sexp :docid) ;; docid of the message + (plist-get sexp :param)));; parameter for the action + + ;; get some info + ((plist-get sexp :info) + (funcall mu4e-info-func sexp)) + + ;; receive an error + ((plist-get sexp :error) + (funcall mu4e-error-func + (plist-get sexp :error) + (plist-get sexp :message))) + + (t (mu4e-message "Unexpected data from server [%S]" sexp))) + + (setq sexp (mu4e~proc-eat-sexp-from-buf)))))) + + +;; error codes are defined in src/mu-util +;;(defconst mu4e-xapian-empty 19 "Error code: xapian is empty/non-existent") + +(defun mu4e~proc-sentinel (proc msg) + "Function that will be called when the mu-server process terminates." + (let ((status (process-status proc)) (code (process-exit-status proc))) + (setq mu4e~proc-process nil) + (setq mu4e~proc-buf "") ;; clear any half-received sexps + (cond + ((eq status 'signal) + (cond + ((eq code 9) (message nil)) + ;;(message "the mu server process has been stopped")) + (t (error (format "mu server process received signal %d" code))))) + ((eq status 'exit) + (cond + ((eq code 0) + (message nil)) ;; don't do anything + ((eq code 11) + (error "Database is locked by another process")) + ((eq code 15) + (error "Database needs upgrade; try `mu index --rebuild' from the command line")) + ((eq code 19) + (error "Database empty; try indexing some messages")) + (t (error "mu server process ended with exit code %d" code)))) + (t + (error "Something bad happened to the mu server process"))))) + +(defsubst mu4e~docid-msgid-param (docid-or-msgid) + "Construct a backend parameter based on DOCID-OR-MSGID." + (format + (if (stringp docid-or-msgid) + "msgid:\"%s\"" + "docid:%d") + docid-or-msgid)) + +(defun mu4e~proc-remove (docid) + "Remove message identified by docid. +The results are reporter through either (:update ... ) or (:error) +sexp, which are handled my `mu4e-error-func', respectively." + (mu4e~proc-send-command "cmd:remove docid:%d" docid)) + +(defun mu4e~proc-escape (str) + "Escape STRING for transport -- put it in quotes, and escape existing quotation. +In particular, backslashes and double-quotes." + (let ((esc (replace-regexp-in-string "\\\\" "\\\\\\\\" str))) + (format "\"%s\"" (replace-regexp-in-string "\"" "\\\\\"" esc)))) + +(defun mu4e~proc-find (query threads sortfield sortdir maxnum skip-dups include-related) + "Start a database query for QUERY. +If THREADS is non-nil, show results in threaded fasion, SORTFIELD +is a symbol describing the field to sort by (or nil); see +`mu4e~headers-sortfield-choices'. If SORT is `descending', sort +Z->A, if it's `ascending', sort A->Z. MAXNUM determines the maximum +number of results to return, or nil for 'unlimited'. If SKIP-DUPS +is non-nil, show only one of duplicate messages (see +`mu4e-headers-skip-duplicates'). If INCLUDE-RELATED is non-nil, +include messages related to the messages matching the search +query (see `mu4e-headers-include-related'). + +For each +result found, a function is called, depending on the kind of +result. The variables `mu4e-error-func' contain the function that +will be called for, resp., a message (header row) or an error." + (mu4e~proc-send-command + (concat + "cmd:find query:%s threads:%s sortfield:%s reverse:%s maxnum:%d " + "skip-dups:%s include-related:%s") + (mu4e~proc-escape query) + (if threads "true" "false") + ;; sortfield is e.g. ':subject'; this removes the ':' + (if (null sortfield) "nil" (substring (symbol-name sortfield) 1)) + ;; TODO: use ascending/descending in backend too (it's clearer than 'reverse' + (if (eq sortdir 'descending) "true" "false") + (if maxnum maxnum -1) + (if skip-dups "true" "false") + (if include-related "true" "false"))) + +(defun mu4e~proc-move (docid-or-msgid &optional maildir flags) + "Move message identified by DOCID-OR-MSGID. +At least one of MAILDIR and FLAGS should be specified. Note, even +if MAILDIR is nil, this is still a move, since a change in flags +still implies a change in message filename. + +MAILDIR (), optionally +setting FLAGS (keyword argument :flags). optionally setting FLAGS +in the process. If MAILDIR is nil, message will be moved within the +same maildir. + +MAILDIR must be a maildir, that is, the part _without_ cur/ or new/ +or the root-maildir-prefix. E.g. \"/archive\". This directory must +already exist. + +The FLAGS parameter can have the following forms: + 1. a list of flags such as '(passed replied seen) + 2. a string containing the one-char versions of the flags, e.g. \"PRS\" + 3. a delta-string specifying the changes with +/- and the one-char flags, + e.g. \"+S-N\" to set Seen and remove New. + +The flags are any of `deleted', `flagged', `new', `passed', `replied' `seen' or +`trashed', or the corresponding \"DFNPRST\" as defined in [1]. See +`mu4e-string-to-flags' and `mu4e-flags-to-string'. +The server reports the results for the operation through +`mu4e-update-func'. + +If the variable `mu4e-change-filenames-when-moving' is +non-nil, moving to a different maildir generates new names for +the target files; this helps certain tools (such as mbsync). + +The results are reported through either (:update ... ) +or (:error ) sexp, which are handled my `mu4e-update-func' and +`mu4e-error-func', respectively." + (unless (or maildir flags) + (mu4e-error "At least one of maildir and flags must be specified")) + (unless (or (not maildir) (file-exists-p (concat mu4e-maildir "/" maildir "/"))) + (mu4e-error "Target dir does not exist")) + (let* ((idparam (mu4e~docid-msgid-param docid-or-msgid)) + (flagstr + (when flags + (concat " flags:" + (if (stringp flags) flags (mu4e-flags-to-string flags))))) + (path + (when maildir + (format " maildir:%s" (mu4e~proc-escape maildir)))) + (rename + (if (and maildir mu4e-change-filenames-when-moving) "true" "false"))) + (mu4e~proc-send-command "cmd:move %s %s %s %s" + idparam (or flagstr "") (or path "") + (format "newname:%s" rename)))) + +(defun mu4e~proc-index (path my-addresses) + "Update the message database for filesystem PATH, which should +point to some maildir directory structure. MY-ADDRESSES is a list +of 'my' email addresses (see `mu4e-user-mail-address-list')." + (let ((path (mu4e~proc-escape path)) + (addrs (when my-addresses (mapconcat 'identity my-addresses ",")))) + (if addrs + (mu4e~proc-send-command "cmd:index path:%s my-addresses:%s" path addrs) + (mu4e~proc-send-command "cmd:index path:%s" path)))) + +(defun mu4e~proc-add (path maildir) + "Add the message at PATH to the database. +With MAILDIR set to the maildir this message resides in, +e.g. '/drafts'; if this works, we will receive (:info add :path +<path> :docid <docid>) as well as (:update <msg-sexp>)." + (mu4e~proc-send-command "cmd:add path:%s %s" + (mu4e~proc-escape path) + (if maildir + (format "maildir:%s" (mu4e~proc-escape maildir)) + ""))) + +(defun mu4e~proc-sent (path maildir) + "Add the message at PATH to the database. +With MAILDIR set to the maildir this message resides in, +e.g. '/drafts'. + + if this works, we will receive (:info add :path <path> :docid +<docid> :fcc <path>)." + (mu4e~proc-send-command "cmd:sent path:%s maildir:%s" + (mu4e~proc-escape path) (mu4e~proc-escape maildir))) + + +(defun mu4e~proc-compose (type decrypt &optional docid) + "Start composing a message of certain TYPE (a symbol, either +`forward', `reply', `edit', `resend' or `new', based on an +original message (ie, replying to, forwarding, editing, +resending) with DOCID or nil for type `new'. + +The result will be delivered to the function registered as +`mu4e-compose-func'." + (unless (member type '(forward reply edit resend new)) + (mu4e-error "Unsupported compose-type %S" type)) + (unless (eq (null docid) (eq type 'new)) + (mu4e-error "`new' implies docid not-nil, and vice-versa")) + (mu4e~proc-send-command + "cmd:compose type:%s docid:%d extract-encrypted:%s use-agent:true" + (symbol-name type) docid (if decrypt "true" "false"))) + +(defun mu4e~proc-mkdir (path) + "Create a new maildir-directory at filesystem PATH." + (mu4e~proc-send-command "cmd:mkdir path:%s" (mu4e~proc-escape path))) + +(defun mu4e~proc-extract (action docid partidx decrypt &optional path what param) + "Extract an attachment with index PARTIDX from message with DOCID +and perform ACTION on it (as symbol, either `save', `open', `temp') which +mean: + * save: save the part to PARAM1 (a path) (non-optional for save)$ + * open: open the part with the default application registered for doing so + * temp: save to a temporary file, then respond with + (:temp <path> :what <what> :param <param>)." + (let ((cmd + (concat "cmd:extract " + (case action + (save + (format "action:save docid:%d index:%d path:%s extract-encrypted:%s use-agent:true" + docid partidx (mu4e~proc-escape path) (if decrypt "true" "false"))) + (open (format "action:open docid:%d index:%d extract-encrypted:%s use-agent:true" + docid partidx (if decrypt "true" "false"))) + (temp + (format "action:temp docid:%d index:%d what:%s%s extract-encrypted:%s use-agent:true" + docid partidx what + (if param + (if (stringp param) + (format " param:%s" (mu4e~proc-escape param)) + (format " param:%S" param)) "") (if decrypt "true" "false"))) + (otherwise (mu4e-error "Unsupported action %S" action)))) + )) + (mu4e~proc-send-command "%s" cmd))) + + +(defun mu4e~proc-ping () + "Sends a ping to the mu server, expecting a (:pong ...) in response." + (mu4e~proc-send-command "cmd:ping")) + +(defun mu4e~proc-contacts (personal after) + "Sends the contacts command to the mu server. +A (:contacts (<list>)) is expected in response. If PERSONAL is +non-nil, only get personal contacts, if AFTER is non-nil, get +only contacts seen AFTER (the time_t value)." + (mu4e~proc-send-command + "cmd:contacts personal:%s after:%d" + (if personal "true" "false") + (or after 0))) + +(defun mu4e~proc-view (docid-or-msgid &optional images decrypt) + "Get one particular message based on its DOCID-OR-MSGID. +Optionally, if IMAGES is non-nil, backend will any images +attached to the message, and return them as temp files. +The result will be delivered to the function registered as +`mu4e-view-func'." + (mu4e~proc-send-command + "cmd:view %s extract-images:%s extract-encrypted:%s use-agent:true" + (mu4e~docid-msgid-param docid-or-msgid) + (if images "true" "false") + (if decrypt "true" "false"))) + +(defun mu4e~proc-view-path (path &optional images decrypt) + "View message at PATH (keyword argument). +Optionally, if IMAGES is non-nil, backend will any images +attached to the message, and return them as temp files. The +result will be delivered to the function registered as +`mu4e-view-func'." + (mu4e~proc-send-command + "cmd:view path:%s extract-images:%s extract-encrypted:%s use-agent:true" + (mu4e~proc-escape path) + (if images "true" "false") + (if decrypt "true" "false"))) + + +(provide 'mu4e-proc) +;; End of mu4e-proc.el diff --git a/_spacemacs.d/local/mu4e/mu4e-speedbar.el b/_spacemacs.d/local/mu4e/mu4e-speedbar.el new file mode 100644 index 0000000..b359511 --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-speedbar.el @@ -0,0 +1,124 @@ +;;; mu4e-speedbar --- Speedbar support for mu4e + +;; Copyright (C) 2012-2016 Antono Vasiljev, Dirk-Jan C. Binnema +;; +;; Author: Antono Vasiljev <self@antono.info> +;; Version: 0.1 +;; Keywords: file, tags, tools +;; +;; This file is not part of GNU Emacs. +;; +;; This program 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, or (at your option) +;; any later version. +;; +;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: +;; +;; Speedbar provides a frame in which files, and locations in files +;; are displayed. These functions provide mu4e specific support, +;; showing maildir list in the side-bar. +;; +;; This file requires speedbar. + +;;; Code: + +(require 'speedbar) +(require 'mu4e-vars) +(require 'mu4e-headers) + +(defvar mu4e-main-speedbar-key-map nil + "Keymap used when in mu4e display mode.") +(defvar mu4e-headers-speedbar-key-map nil + "Keymap used when in mu4e display mode.") +(defvar mu4e-view-speedbar-key-map nil + "Keymap used when in mu4e display mode.") + +(defvar mu4e-main-speedbar-menu-items nil + "Additional menu-items to add to speedbar frame.") +(defvar mu4e-headers-speedbar-menu-items nil + "Additional menu-items to add to speedbar frame.") +(defvar mu4e-view-speedbar-menu-items nil + "Additional menu-items to add to speedbar frame.") + + +(defun mu4e-speedbar-install-variables () + "Install those variables used by speedbar to enhance mu4e." + (dolist (keymap + '( mu4e-main-speedbar-key-map + mu4e-headers-speedbar-key-map + mu4e-view-speedbar-key-map)) + (unless keymap + (setq keymap (speedbar-make-specialized-keymap)) + (define-key keymap "RET" 'speedbar-edit-line) + (define-key keymap "e" 'speedbar-edit-line)))) + + +;; Make sure our special speedbar major mode is loaded +(if (featurep 'speedbar) + (mu4e-speedbar-install-variables) + (add-hook 'speedbar-load-hook 'mu4e-speedbar-install-variables)) + +(defun mu4e~speedbar-render-maildir-list () + "Insert the list of maildirs in the speedbar." + (interactive) + (mapcar (lambda (maildir-name) + (speedbar-insert-button + (concat " " maildir-name) + 'mu4e-highlight-face + 'highlight + 'mu4e~speedbar-maildir + maildir-name)) + (mu4e-get-maildirs))) + +(defun mu4e~speedbar-maildir (&optional text token ident) + "Jump to maildir TOKEN. TEXT and INDENT are not used." + (speedbar-with-attached-buffer + (mu4e-headers-search (concat "\"maildir:" token "\"") + current-prefix-arg))) + +(defun mu4e~speedbar-render-bookmark-list () + "Insert the list of bookmarks in the speedbar" + (interactive) + (mapcar (lambda (bookmark) + (speedbar-insert-button + (concat " " (nth 1 bookmark)) + 'mu4e-highlight-face + 'highlight + 'mu4e~speedbar-bookmark + (nth 0 bookmark))) + mu4e-bookmarks)) + +(defun mu4e~speedbar-bookmark (&optional text token ident) + "Run bookmarked query TOKEN. TEXT and INDENT are not used." + (speedbar-with-attached-buffer + (mu4e-headers-search token current-prefix-arg))) + +;;;###autoload +(defun mu4e-speedbar-buttons (buffer) + "Create buttons for any mu4e BUFFER." + (interactive) + (erase-buffer) + (insert (propertize "* mu4e\n\n" 'face 'mu4e-title-face)) + + (insert (propertize " Bookmarks\n" 'face 'mu4e-title-face)) + (mu4e~speedbar-render-bookmark-list) + (insert "\n") + (insert (propertize " Maildirs\n" 'face 'mu4e-title-face)) + (mu4e~speedbar-render-maildir-list)) + +(defun mu4e-main-speedbar-buttons (buffer) (mu4e-speedbar-buttons buffer)) +(defun mu4e-headers-speedbar-buttons (buffer) (mu4e-speedbar-buttons buffer)) +(defun mu4e-view-speedbar-buttons (buffer) (mu4e-speedbar-buttons buffer)) + + +(provide 'mu4e-speedbar) +;;; mu4e-speedbar.el ends here diff --git a/_spacemacs.d/local/mu4e/mu4e-utils.el b/_spacemacs.d/local/mu4e/mu4e-utils.el new file mode 100644 index 0000000..1f8c9c0 --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-utils.el @@ -0,0 +1,1238 @@ +;;; mu4e-utils.el -- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema +;; Copyright (C) 2013 Tibor Simko + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Utility functions used in the mu4e + +;;; Code: +(eval-when-compile (byte-compile-disable-warning 'cl-functions)) +(require 'cl) + +(eval-when-compile (require 'org nil 'noerror)) + +(require 'mu4e-vars) +(require 'mu4e-meta) +(require 'mu4e-lists) +(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~context-autoswitch "mu4e-context") +(declare-function mu4e-context-determine "mu4e-context") +(declare-function mu4e-context-vars "mu4e-context") +(declare-function show-all "org") + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; the following is taken from org.el; we copy it here since we don't want to +;; depend on org-mode directly (it causes byte-compilation errors) TODO: a +;; cleaner solution.... +(defconst mu4e~ts-regexp0 + (concat + "\\(\\([0-9]\\{4\\}\\)-\\([0-9]\\{2\\}\\)-\\([0-9]\\{2\\}\\)" + "\\( +[^]+0-9>\r\n -]+\\)?\\( +\\([0-9]\\{1,2\\}\\):" + "\\([0-9]\\{2\\}\\)\\)?\\)") + "Regular expression matching time strings for analysis. +This one does not require the space after the date, so it can be +used on a string that terminates immediately after the date.") + +(defun mu4e-parse-time-string (s &optional nodefault) + "Parse the standard Org-mode time string. +This should be a lot faster than the normal `parse-time-string'. +If time is not given, defaults to 0:00. However, with optional +NODEFAULT, hour and minute fields will be nil if not given." + (if (string-match mu4e~ts-regexp0 s) + (list 0 + (if (or (match-beginning 8) (not nodefault)) + (string-to-number (or (match-string 8 s) "0"))) + (if (or (match-beginning 7) (not nodefault)) + (string-to-number (or (match-string 7 s) "0"))) + (string-to-number (match-string 4 s)) + (string-to-number (match-string 3 s)) + (string-to-number (match-string 2 s)) + nil nil nil) + (mu4e-error "Not a standard mu4e time string: %s" s))) + + +(defun mu4e-user-mail-address-p (addr) + "If ADDR is one of user's e-mail addresses return t, nil otherwise. +User's addresses are set in `mu4e-user-mail-address-list')." + (when (and addr mu4e-user-mail-address-list + (find addr mu4e-user-mail-address-list :test 'string=)) + t)) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(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)))) + (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)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; 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)) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mu4e-remove-file-later (filename) + "Remove FILENAME in a few seconds." + (run-at-time "10 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)) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; 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-error "mu4e-attachment-dir evaluates to nil"))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mu4e~guess-maildir (path) + "Guess the maildir for some path, or nil if cannot find it." + (let ((idx (string-match mu4e-maildir path))) + (when (and idx (zerop idx)) + (replace-regexp-in-string + mu4e-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-format (frm &rest args) + "Create [mu4e]-prefixed string based on format FRM and ARGS." + (concat + "[" (propertize "mu4e" 'face 'mu4e-title-face) "] " + (apply 'format frm args))) + +(defun mu4e-message (frm &rest args) + "Like `message', but prefixed with mu4e. +If we're waiting for user-input or if there's some message in the +echo area, don't show anything." + (unless (or (active-minibuffer-window)) + (message "%s" (apply 'mu4e-format frm args)))) + +(defun mu4e-index-message (frm &rest args) + "Like `mu4e-message', but specifically for +index-messages. Doesn't display anything if +`mu4e-hide-index-messages' is non-nil. " + (unless mu4e-hide-index-messages + (apply 'mu4e-message frm args))) + +(defun mu4e-error (frm &rest args) + "Create [mu4e]-prefixed error based on format FRM and ARGS. +Does a local-exit and does not return, and raises a +debuggable (backtrace) error." + (mu4e-log 'error (apply 'mu4e-format frm args)) + (error "%s" (apply 'mu4e-format frm args))) + +;; the user-error function is only available in emacs-trunk +(unless (fboundp 'user-error) + (defalias 'user-error 'error)) + +(defun mu4e-warn (frm &rest args) + "Create [mu4e]-prefixed warning based on format FRM and ARGS. +Does a local-exit and does not return. In emacs versions below +24.2, the functions is the same as `mu4e-error'." + (mu4e-log 'error (apply 'mu4e-format frm args)) + (user-error "%s" (apply 'mu4e-format frm args))) + +(defun mu4e~read-char-choice (prompt choices) + "Read and return one of CHOICES, prompting for PROMPT. +Any input that is not one of CHOICES is ignored. This mu4e's +version of `read-char-choice', that becomes case-insentive after +trying an exact match." + (let ((choice) (chosen) (inhibit-quit nil)) + (while (not chosen) + (message nil);; this seems needed... + (setq choice (read-char-exclusive prompt)) + (setq chosen (or (member choice choices) + (member (downcase choice) choices) + (member (upcase choice) choices)))) + (car chosen))) + +(defun mu4e-read-option (prompt options) + "Ask user for an option from a list on the input area. +PROMPT describes a multiple-choice question to the user. +OPTIONS describe the options, and is a list of cells describing +particular options. Cells have the following structure: + + (OPTIONSTRING . RESULT) + +where OPTIONSTRING is a non-empty string describing the +option. The first character of OPTIONSTRING is used as the +shortcut, and obviously all shortcuts must be different, so you +can prefix the string with an uniquifying character. + +The options are provided as a list for the user to choose from; +user can then choose by typing CHAR. Example: + (mu4e-read-option \"Choose an animal: \" + '((\"Monkey\" . monkey) (\"Gnu\" . gnu) (\"xMoose\" . moose))) + +User now will be presented with a list: \"Choose an animal: + [M]onkey, [G]nu, [x]Moose\". + +Function will return the cdr of the list element." + (let* ((prompt (mu4e-format "%s" prompt)) + (chosen) + (optionsstr + (mapconcat + (lambda (option) + ;; try to detect old-style options, and warn + (when (characterp (car-safe (cdr-safe option))) + (mu4e-error + (concat "Please use the new format for options/actions; " + "see the manual"))) + (let* ((kar (substring (car option) 0 1)) + (val (cdr option))) + (concat + "[" (propertize kar 'face 'mu4e-highlight-face) "]" + (substring (car option) 1)))) + options ", ")) + (response + (mu4e~read-char-choice + (concat prompt optionsstr + " [" (propertize "C-g" 'face 'mu4e-highlight-face) + " to cancel]") + ;; the allowable chars + (map 'list (lambda(elm) (string-to-char (car elm))) options))) + (chosen + (find-if + (lambda (option) (eq response (string-to-char (car option)))) + options))) + (if chosen + (cdr chosen) + (mu4e-warn "Unknown shortcut '%c'" response)))) + +(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-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-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/, so +the list of maildirs will not change until you restart mu4e." + (unless mu4e-maildir (mu4e-error "`mu4e-maildir' is not defined")) + (unless (and mu4e-maildir-list mu4e-cache-maildir-list) + (setq mu4e-maildir-list + (sort + (append + (when (file-accessible-directory-p + (concat mu4e-maildir "/cur")) '("/")) + (mu4e~get-maildirs-1 mu4e-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 defined in +`mu4e-maildir-shortcuts', then return the corresponding folder +name. If the special shortcut 'o' (for _o_ther) is used, or if +`mu4e-maildir-shortcuts' is not defined, let user choose from all +maildirs under `mu4e-maildir'." + (let ((prompt (mu4e-format "%s" prompt))) + (if (not mu4e-maildir-shortcuts) + (funcall mu4e-completing-read-function prompt (mu4e-get-maildirs)) + (let* ((mlist (append mu4e-maildir-shortcuts '(("ther" . ?o)))) + (fnames + (mapconcat + (lambda (item) + (concat + "[" + (propertize (make-string 1 (cdr item)) + 'face 'mu4e-highlight-face) + "]" + (car item))) + mlist ", ")) + (kar (read-char (concat prompt fnames)))) + (if (member kar '(?/ ?o)) ;; user chose 'other'? + (funcall mu4e-completing-read-function prompt + (mu4e-get-maildirs) nil nil "/") + (or (car-safe + (find-if (lambda (item) (= kar (cdr item))) + mu4e-maildir-shortcuts)) + (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-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)) + +(defun mu4e-ask-bookmark (prompt &optional kar) + "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) + (let ((query (nth 0 bm)) (title (nth 1 bm)) (key (nth 2 bm))) + (concat + "[" (propertize (make-string 1 key) + 'face 'mu4e-highlight-face) + "]" + title))) 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 (find-if + (lambda (bm) + (= kar (nth 2 bm))) + mu4e-bookmarks) + (mu4e-warn "Unknown shortcut '%c'" kar))) + (expr (nth 0 chosen-bm)) + (query (eval expr))) + (if (stringp query) + query + (mu4e-warn "Expression must evaluate to query string ('%S')" expr)))) + + +(defun mu4e-bookmark-define (query descr key) + "Define a bookmark for QUERY with description DESCR and short +character KEY in the list of `mu4e-bookmarks'. This replaces any +existing bookmark with KEY." + (setq mu4e-bookmarks (remove-if (lambda (bm) (= (nth 2 bm) key)) mu4e-bookmarks)) + (add-to-list 'mu4e-bookmarks (list query descr key) t)) + + +;;; converting flags->string and vice-versa ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mu4e~flags-to-string-raw (flags) + "Convert a list of flags into a string as seen in Maildir +message files; flags are symbols draft, flagged, new, passed, +replied, seen, trashed and the string is the concatenation of the +uppercased first letters of these flags, as per [1]. Other flags +than the ones listed here are ignored. +Also see `mu4e-flags-to-string'. +\[1\]: http://cr.yp.to/proto/maildir.html" + (when flags + (let ((kar (case (car flags) + ('draft ?D) + ('flagged ?F) + ('new ?N) + ('passed ?P) + ('replied ?R) + ('seen ?S) + ('trashed ?T) + ('attach ?a) + ('encrypted ?x) + ('signed ?s) + ('unread ?u)))) + (concat (and kar (string kar)) + (mu4e~flags-to-string-raw (cdr flags)))))) + +(defun mu4e-flags-to-string (flags) + "Remove duplicates and sort the output of `mu4e~flags-to-string-raw'." + (concat + (sort (remove-duplicates + (append (mu4e~flags-to-string-raw flags) nil)) '>))) + +(defun mu4e~string-to-flags-1 (str) + "Convert a string with message flags as seen in Maildir +messages into a list of flags in; flags are symbols draft, +flagged, new, passed, replied, seen, trashed and the string is +the concatenation of the uppercased first letters of these flags, +as per [1]. Other letters than the ones listed here are ignored. +Also see `mu4e-flags-to-string'. +\[1\]: http://cr.yp.to/proto/maildir.html." + (when (/= 0 (length str)) + (let ((flag + (case (string-to-char str) + (?D 'draft) + (?F 'flagged) + (?P 'passed) + (?R 'replied) + (?S 'seen) + (?T 'trashed)))) + (append (when flag (list flag)) + (mu4e~string-to-flags-1 (substring str 1)))))) + +(defun mu4e-string-to-flags (str) + "Convert a string with message flags as seen in Maildir messages +into a list of flags in; flags are symbols draft, flagged, new, +passed, replied, seen, trashed and the string is the concatenation +of the uppercased first letters of these flags, as per [1]. Other +letters than the ones listed here are ignored. Also see +`mu4e-flags-to-string'. \[1\]: +http://cr.yp.to/proto/maildir.html " + ;; "Remove duplicates from the output of `mu4e~string-to-flags-1'" + (remove-duplicates (mu4e~string-to-flags-1 str))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defun mu4e-display-size (size) + "Get a string representation of SIZE (in bytes)." + (cond + ((>= size 1000000) (format "%2.1fM" (/ size 1000000.0))) + ((and (>= size 1000) (< size 1000000)) + (format "%2.1fK" (/ size 1000.0))) + ((< size 1000) (format "%d" size)) + (t (propertize "?" 'face 'mu4e-system-face)))) + + +(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 (case major-mode + ('mu4e-main-mode "(mu4e)Main view") + ('mu4e-headers-mode "(mu4e)Headers view") + ('mu4e-view-mode "(mu4e)Message view") + (t "mu4e")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mu4e-last-query () + "Get the most recent query or nil if there is none." + (when (buffer-live-p mu4e~headers-buffer) + (with-current-buffer mu4e~headers-buffer + mu4e~headers-last-query))) + +(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~view-buffer) + ((eq major-mode 'mu4e-view-mode) + mu4e~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.") + +;; 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))) + (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" + (plist-get info :processed) (plist-get info :updated)) + (progn + (mu4e-index-message + "Indexing completed; processed %d, updated %d, cleaned-up %d" + (plist-get info :processed) (plist-get info :updated) + (plist-get info :cleaned-up)) + (unless (zerop (plist-get info :updated)) + (run-hooks 'mu4e-index-updated-hook))))) + ((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 + (case errcode + (4 (user-error "No matches for this search query.")) + (t (error "Error %d: %s" errcode errmsg)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;; RFC2822 handling of phrases in mail-addresses +;;; The optional display-name contains a phrase, it sits before the angle-addr +;;; as specified in RFC2822 for email-addresses in header fields. +;;; contributed by jhelberg + +(defun mu4e~rfc822-phrase-type (ph) + "Return either atom, quoted-string, a corner-case or nil. This + checks for empty string first. Then quotes around the phrase + (returning 'rfc822-quoted-string). Then whether there is a quote + inside the phrase (returning 'rfc822-containing-quote). + The reverse of the RFC atext definition is then tested. + If it matches, nil is returned, if not, it is an 'rfc822-atom, which + is returned." + (cond + ((= (length ph) 0) 'rfc822-empty) + ((= (aref ph 0) ?\") + (if (string-match "\"\\([^\"\\\n]\\|\\\\.\\|\\\\\n\\)*\"" ph) + 'rfc822-quoted-string + 'rfc822-containing-quote)) ; starts with quote, but doesn't end with one + ((string-match-p "[\"]" ph) 'rfc822-containing-quote) + ((string-match-p "[\000-\037()\*<>@,;:\\\.]+" ph) nil) + (t 'rfc822-atom))) + +(defun mu4e~rfc822-quoteit (ph) + "Quote RFC822 phrase only if necessary. + Atoms and quoted strings don't need quotes. The rest do. In + case a phrase contains a quote, it will be escaped." + (let ((type (mu4e~rfc822-phrase-type ph))) + (cond + ((eq type 'rfc822-atom) ph) + ((eq type 'rfc822-quoted-string) ph) + ((eq type 'rfc822-containing-quote) + (format "\"%s\"" + (replace-regexp-in-string "\"" "\\\\\"" ph))) + (t (format "\"%s\"" ph))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defsubst mu4e~process-contact (contact) + "Process CONTACT, and either return nil when it should not be included, +or (rfc822-string . CONTACT) otherwise." + (when mu4e-contact-rewrite-function + (setq contact (funcall mu4e-contact-rewrite-function contact))) + (when contact + (let ((name (plist-get contact :name)) + (mail (plist-get contact :mail)) + (ignore-rx (or mu4e-compose-complete-ignore-address-regexp "$^"))) + (when (and mail (not (string-match ignore-rx mail))) + (cons + (if name (format "%s <%s>" (mu4e~rfc822-quoteit name) mail) mail) + contact))))) + + +(defun mu4e~sort-contacts (contacts) + "Destructively sort contacts (only for cycling) in order of + 'mostly likely contact'.t See the code for the detail" + (let* ((now (+ (float-time) 3600)) ;; allow for clock diffs + (recent (- (float-time) (* 15 24 3600)))) + (sort* contacts + (lambda (c1 c2) + (let* ( (c1 (cdr c1)) (c2 (cdr c2)) + (personal1 (plist-get c1 :personal)) + (personal2 (plist-get c2 :personal)) + ;; note: freq, tstamp can only be missing if the rewrite + ;; function removed them. If the rewrite function changed the + ;; contact somehow, we guess it's important. + (freq1 (or (plist-get c1 :freq) 500)) + (freq2 (or (plist-get c2 :freq) 500)) + (tstamp1 (or (plist-get c1 :tstamp) now)) + (tstamp2 (or (plist-get c2 :tstamp) now))) + ;; only one is personal? if so, that one comes first + (if (not (equal personal1 personal2)) + (if personal1 t nil) + ;; only one is recent? that one comes first + (if (not (equal (> tstamp1 recent) (> tstamp2 recent))) + (> tstamp1 tstamp2) + ;; otherwise, use the frequency + (> freq1 freq2)))))))) + +(defun mu4e~sort-contacts-for-completion (contacts) + "Takes CONTACTS, which is a list of RFC-822 addresses, and sort them based +on the ranking in `mu4e~contacts.'" + (sort* contacts + (lambda (c1 c2) + (let ((rank1 (gethash c1 mu4e~contacts)) + (rank2 (gethash c2 mu4e~contacts))) + (< rank1 rank2))))) + +;; start and stopping +(defun mu4e~fill-contacts (contact-data) + "We receive a list of contacts, which each contact of the form + (:me NAME :mail EMAIL :tstamp TIMESTAMP :freq FREQUENCY) +and fill the hash `mu4e~contacts-for-completion' with it, with +each contact mapped to an integer for their ranking. + +This is used by the completion function in mu4e-compose." + (let ((contacts) (rank 0)) + (dolist (contact contact-data) + (let ((contact-maybe (mu4e~process-contact contact))) + ;; note, this gives cells (rfc822-address . contact) + (when contact-maybe (push contact-maybe contacts)))) + (setq contacts (mu4e~sort-contacts contacts)) + ;; now, 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. + (setq mu4e~contacts (make-hash-table :test 'equal :weakness nil + :size (length contacts))) + (dolist (contact contacts) + (puthash (car contact) rank mu4e~contacts) + (incf rank)) + (mu4e-index-message "Contacts received: %d" + (hash-table-count mu4e~contacts)))) + +(defun mu4e~check-requirements () + "Check for the settings required for running mu4e." + (unless (>= emacs-major-version 23) + (mu4e-error "Emacs >= 23.x is required for mu4e")) + (when mu4e~server-props + (let ((version (plist-get mu4e~server-props :version))) + (unless (string= version mu4e-mu-version) + (mu4e-error "mu server has version %s, but we need %s" + 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.")) + (unless mu4e-maildir + (mu4e-error "Please set `mu4e-maildir' to the full path to your + Maildir directory.")) + ;; expand mu4e-maildir, mu4e-attachment-dir + (setq mu4e-maildir (expand-file-name mu4e-maildir)) + (unless (mu4e-create-maildir-maybe mu4e-maildir) + (mu4e-error "%s is not a valid maildir directory" mu4e-maildir)) + (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-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 () + "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~fill-contacts) + (mu4e~proc-contacts + mu4e-compose-complete-only-personal + (when mu4e-compose-complete-only-after + (float-time + (apply 'encode-time + (mu4e-parse-time-string mu4e-compose-complete-only-after))))))) + +(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 requirements, then start mu4e. When +successful, call FUNC (if non-nil) afterwards." + ;; if we're already running, simply go to the main view + (if (mu4e-running-p) ;; already running? + (when func (funcall func)) ;; yes! run func if defined + (progn + ;; no! try to set a context, do some checks, set up pong handler and ping + ;; the server maybe switch the context + (mu4e~context-autoswitch nil mu4e-context-policy) + (lexical-let ((func func)) + (mu4e~check-requirements) + ;; set up the 'pong' handler func + (setq mu4e-pong-func + (lambda (props) + (setq mu4e~server-props props) ;; save props from the server + (let ((version (plist-get props :version)) + (doccount (plist-get props :doccount))) + (mu4e~check-requirements) + (when func (funcall func)) + (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))))) + (mu4e-message "Started mu4e with %d message%s in store" + doccount (if (= doccount 1) "" "s")))))) + ;; wake up server + (mu4e~proc-ping) + ;; maybe request the list of contacts, automatically refresh after + ;; reindexing + (mu4e~request-contacts) + (add-hook 'mu4e-index-updated-hook 'mu4e~request-contacts)))) + +(defun mu4e-clear-caches () + "Clear any cached resources." + (setq + mu4e-maildir-list nil + mu4e~contacts nil)) + +(defun mu4e~stop () + "Stop the mu4e session." + (when mu4e~update-timer + (cancel-timer mu4e~update-timer) + (setq mu4e~update-timer nil)) + (mu4e-clear-caches) + (mu4e~proc-kill) + ;; kill all main/view/headers buffer + (mapcar + (lambda (buf) + (with-current-buffer buf + (when (member major-mode + '(mu4e-headers-mode mu4e-view-mode mu4e-main-mode)) + (kill-buffer)))) + (buffer-list))) + + + +(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)) + (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) + (unless mu4e-maildir + (mu4e-error "`mu4e-maildir' is not defined")) + (mu4e~proc-index mu4e-maildir mu4e-user-mail-address-list)) + +(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-interrupt-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)) + (let* ((status (process-status proc)) + (code (process-exit-status proc)) + (maybe-error (or (not (eq status 'exit)) (/= code 0))) + (buf (and (buffer-live-p mu4e~update-buffer) mu4e~update-buffer)) + (win (and buf (get-buffer-window buf)))) + (message nil) + (if maybe-error + (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)) + (if (window-live-p win) + (with-selected-window win (kill-buffer-and-window)) + (when (buffer-live-p buf) (kill-buffer buf))))) + +;; 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*" + 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 start + (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-interrupt-update-mail () + "Stop the update process by sending SIGINT to it." + (interactive) + (let* ((proc (and (buffer-live-p mu4e~update-buffer) + (get-buffer-process mu4e~update-buffer)))) + (when (process-live-p proc) + (interrupt-process proc t)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; logging / debugging +(defvar mu4e~log-max-lines 1200 + "*internal* Last <n> number of lines to keep around in the buffer.") +(defconst mu4e~log-buffer-name "*mu4e-log*" + "*internal* Name of the logging buffer.") + +(defun mu4e-log (type frm &rest args) + "Write a message of TYPE with format-string FRM and ARGS in +*mu4e-log* buffer, if the variable mu4e-debug is non-nil. Type is +either 'to-server, 'from-server or 'misc. This function is meant for debugging." + (when mu4e-debug + (with-current-buffer (get-buffer-create mu4e~log-buffer-name) + (view-mode) + (setq buffer-undo-list t) + (let* ((inhibit-read-only t) + (tstamp (propertize (format-time-string "%Y-%m-%d %T" + (current-time)) + 'face 'font-lock-string-face)) + (msg-face + (case type + (from-server 'font-lock-type-face) + (to-server 'font-lock-function-name-face) + (misc 'font-lock-variable-name-face) + (error 'font-lock-warning-face) + (otherwise (mu4e-error "Unsupported log type")))) + (msg (propertize (apply 'format frm args) 'face msg-face))) + (goto-char (point-max)) + (insert tstamp + (case type + (from-server " <- ") + (to-server " -> ") + (error " !! ") + (otherwise " ")) + msg "\n") + + ;; if `mu4e-log-max-lines is specified and exceeded, clearest the oldest + ;; lines + (when (numberp mu4e~log-max-lines) + (let ((lines (count-lines (point-min) (point-max)))) + (when (> lines mu4e~log-max-lines) + (goto-char (point-max)) + (forward-line (- mu4e~log-max-lines lines)) + (beginning-of-line) + (delete-region (point-min) (point))))))))) + +(defun mu4e-toggle-logging () + "Toggle between enabling/disabling debug-mode (in debug-mode, +mu4e logs some of its internal workings to a log-buffer. See +`mu4e-visit-log'." + (interactive) + (mu4e-log 'misc "logging disabled") + (setq mu4e-debug (not mu4e-debug)) + (mu4e-message "debug logging has been %s" + (if mu4e-debug "enabled" "disabled")) + (mu4e-log 'misc "logging enabled")) + +(defun mu4e-show-log () + "Visit the mu4e debug log." + (interactive) + (let ((buf (get-buffer mu4e~log-buffer-name))) + (unless (buffer-live-p buf) + (mu4e-warn "No debug log available")) + (switch-to-buffer buf))) + + +(defun mu4e-split-ranges-to-numbers (str n) + "Convert STR containing attachment numbers into a list of numbers. +STR is a string; N is the highest possible number in the list. +This includes expanding e.g. 3-5 into 3,4,5. If the letter +\"a\" ('all')) is given, that is expanded to a list with numbers [1..n]." + (let ((str-split (split-string str)) + beg end list) + (dolist (elem str-split list) + ;; special number "a" converts into all attachments 1-N. + (when (equal elem "a") + (setq elem (concat "1-" (int-to-string n)))) + (if (string-match "\\([0-9]+\\)-\\([0-9]+\\)" elem) + ;; we have found a range A-B, which needs converting + ;; into the numbers A, A+1, A+2, ... B. + (progn + (setq beg (string-to-number (match-string 1 elem)) + end (string-to-number (match-string 2 elem))) + (while (<= beg end) + (add-to-list 'list beg 'append) + (setq beg (1+ beg)))) + ;; else just a number + (add-to-list 'list (string-to-number elem) 'append))) + ;; Check that all numbers are valid. + (mapc + #'(lambda (x) + (cond + ((> x n) + (mu4e-warn "Attachment %d bigger than maximum (%d)" x n)) + ((< x 1) + (mu4e-warn "Attachment number must be greater than 0 (%d)" x)))) + list))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defvar mu4e-imagemagick-identify "identify" + "Name/path of the Imagemagick 'identify' program.") + +(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 inmagemagick 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 (shell-command-to-string + (format "%s -format '%%w' %s" + identify (shell-quote-argument imgpath))))) + (width (and props (string-to-number props))) + (img (if have-im + (if (> (or width 0) (or maxwidth 0)) + (create-image imgpath 'imagemagick nil :width maxwidth) + (create-image imgpath 'imagemagick)) + (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) + (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)) + + +(defun mu4e-get-time-date (prompt) + "Determine the emacs time value for the time/date entered by user + after PROMPT. Formats are all that are accepted by + `parse-time-string'." + (let ((timestr (read-string (mu4e-format "%s" prompt)))) + (apply 'encode-time (mu4e-parse-time-string timestr)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(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." + (interactive) + (unless (file-exists-p path) + (mu4e-error "Cannot find %s" path)) + (lexical-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"))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defun mu4e-refresh-message (path maildir) + "Re-parse message at PATH and MAILDIR; if this works, we will +receive (:info add :path <path> :docid <docid>) as well as (:update +<msg-sexp>)." + (mu4e~proc-add path maildir)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(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 1)))) + (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." + (let ((inhibit-read-only t)) + (save-excursion + ;; give the footer a different color... + (goto-char (point-min)) + (let ((p (search-forward "^-- *$" nil t))) + (when p + (add-text-properties p (point-max) '(face mu4e-footer-face))))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defun mu4e~quote-for-modeline (str) + "Quote a string to be used literally in the modeline." + (replace-regexp-in-string "%" "%%" str t t)) + + +(provide 'mu4e-utils) +;;; End of mu4e-utils.el diff --git a/_spacemacs.d/local/mu4e/mu4e-vars.el b/_spacemacs.d/local/mu4e/mu4e-vars.el new file mode 100644 index 0000000..94c3a86 --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-vars.el @@ -0,0 +1,870 @@ +;;; mu4e-vars.el -- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Code: + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Customization +(require 'mu4e-meta) +(require 'message) + +(defgroup mu4e nil + "mu4e - mu for emacs" + :group 'mail) + +(defcustom mu4e-mu-home nil + "Location of the mu homedir, or nil for the default." + :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-maildir (expand-file-name "~/Maildir") + "The file system path to your Maildir. Must not be a symbolic +link." + :type 'directory + :safe 'stringp + :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 succesfully (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 when we the retrieval process (as + per `mu4e-get-mail-command') finished with a non-zero exit code." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-index-update-error-continue t + "Whether to continue with indexing when we the retrieval + process (as per `mu4e-get-mail-command') finished with a non-zero + exit code." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-index-update-in-background t + "Whether to run the automatic mail retrieval in the +background." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-update-interval nil + "Number of seconds between automatic calls to retrieve mail and +update the database. 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.") + +(defvar mu4e-hide-index-messages nil + "If non-nil, mu4e does not show the \"Indexing...\" messages, or + any messages relating to updated contacts.") + +(defcustom mu4e-change-filenames-when-moving nil + "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 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." + :type 'directory + :group 'mu4e + :safe 'stringp) + +(defcustom mu4e-user-mail-address-list `(,user-mail-address) + "List of e-mail addresses to consider 'my email addresses'. +I.e. addresses whose presence in an email imply that it is a +personal message. Note that e-mail addresses are case-sensitive, +as per RFC531." + :type '(repeat (string :tag "Address")) + :group 'mu4e) + +;; 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") + +(defcustom mu4e-use-fancy-chars nil + "Whether to use fancy (Unicode) characters for marks and +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, in the format of + `format-time-string'." + :type 'string + :group 'mu4e) + +(defvar mu4e-debug nil + "When set to non-nil, log debug information to the *mu4e-log* buffer.") + +(defcustom mu4e-bookmarks + '( ("flag:unread AND NOT flag:trashed" "Unread messages" ?u) + ("date:today..now" "Today's messages" ?t) + ("date:7d..now" "Last 7 days" ?w) + ("mime:image/*" "Messages with images" ?p)) + "A list of pre-defined queries. +These will show up in the main screen. Each of the list elements +is a three-element list of the form (QUERY DESCRIPTION KEY), +where QUERY is a string with a mu query, DESCRIPTION is a short +description of the query (this will show up in the UI), and KEY +is a shortcut key for the query." + :type '(repeat (list (string :tag "Query") + (string :tag "Description") + character)) + :group 'mu4e) + +(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). + * 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 "Don't split" nil)) + :group 'mu4e-headers) + +(defcustom mu4e-view-show-images nil + "Whether to automatically display attached images in the message +view buffer." + :type 'boolean + :group 'mu4e-view) + +(make-obsolete-variable 'mu4e-show-images + 'mu4e-view-show-images "0.9.9.x") + +(defcustom mu4e-confirm-quit t + "Whether to confirm to quit mu4e." + :type 'boolean + :group 'mu4e) + +(defcustom mu4e-cited-regexp "^ *\\(\\(>+ ?\\)+\\)" + "Regular expression that determines whether a line is a citation." + :type 'string + :group 'mu4e) + +(defcustom mu4e-completing-read-function 'ido-completing-read + "Function to be used to receive input from the user with +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) + :safe 'symbolp + :group 'mu4e)) + + +;; crypto +(defgroup mu4e-crypto nil + "Crypto-related settings." + :group 'mu4e) + +(defcustom mu4e-auto-retrieve-keys nil + "Attempt to automatically retrieve public keys when needed." + :type 'boolean + :group 'mu4e-crypto) + +(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." + :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) + +;; completion; we put them here rather than in mu4e-compose, as mu4e-utils needs +;; the variables. + +(defgroup mu4e-compose nil + "Message-composition related settings." + :group 'mu4e) + +;; address completion +(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, +i.e. addresses from messages where user was explicitly in one of +the address fields (this excludes mailing list messages). See +`mu4e-user-mail-address-list' and the mu-index manpage for details for +details (in particular, how to define your own e-mail addresses)." + :type 'boolean + :group 'mu4e-compose) + +(defcustom mu4e-compose-complete-only-after "2010-01-01" + "Consider only contacts last seen after this date. +Date must be a string, in a format parseable by +`org-parse-time-string'. This excludes really old contacts. +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) + "This returns 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))) + +(defcustom mu4e-contact-rewrite-function nil + "Either nil or a function to be used for when processing +contacts and rewrite them or remove them altogether. + +If the function receives the contact as a list of the form + (:name NAME :mail EMAIL ... other properties ... ) +(other properties may be there as well) + +The function should return either: + - nil: remove this contact, or +- the rewritten cell, or +- the existing cell as-is + +For rewriting, it is recommended to use `plist-put' to set the +changed parameters, so the other properties stay in place. Those +are needed for sorting the contacts." + :type 'function + :group 'mu4e-compose) + + +(defcustom mu4e-compose-complete-ignore-address-regexp "no-?reply" + "Ignore any e-mail addresses for completion if they match this regexp." + :type 'string + :group 'mu4e-compose) + +(defcustom mu4e-compose-reply-to-address nil + "The Reply-To address (if this, for some reason, is not equal to +the From: address.)" + :type 'string + :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 + "Non-nil means your e-mail address is kept on the CC list when +replying to messages." + :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.") + +;; Folders +(defgroup mu4e-folders nil + "Special folders." + :group 'mu4e) + +(defcustom mu4e-drafts-folder "/drafts" + "Your folder for draft messages, relative to `mu4e-maildir'. +e.g. \"/drafts\". Instead of a string, may also be a function that +takes a message (a msg plist, see `mu4e-message-get-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 `mu4e-maildir', +e.g. \"/Archive\". Instead of a string, may also be a function that +takes a message (a msg plist, see `mu4e-message-get-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 `mu4e-maildir', +e.g. \"/Sent Items\". Instead of a string, may also be a function +that takes a message (a msg plist, see `mu4e-message-get-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 `mu4e-maildir', +e.g. \"/trash\". Instead of a string, may also be a function that +takes a message (a msg plist, see `mu4e-message-get-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). The list contains elements +of the form (maildir . shortcut), where MAILDIR is a maildir (such +as \"/archive/\"), and shortcut 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 automatically for you." + :type '(repeat (cons (string :tag "Maildir") character)) + :group 'mu4e-folders) + +;; Faces +(defgroup mu4e-faces nil + "Type faces (fonts) used in mu4e." + :group 'mu4e + :group 'faces) + +(defface mu4e-unread-face + '((t :inherit font-lock-keyword-face :bold t)) + "Face for an unread message header." + :group 'mu4e-faces) + +(defface mu4e-moved-face + '((t :inherit font-lock-comment-face :slant italic)) + "Face for a message header that has been moved to some folder. +\(It's still visible in the search results, since we cannot +be sure it no longer matches)." + :group 'mu4e-faces) + +(defface mu4e-trashed-face + '((t :inherit font-lock-comment-face :strike-through t)) + "Face for an message header in the trash folder." + :group 'mu4e-faces) + +(defface mu4e-draft-face + '((t :inherit font-lock-string-face)) + "Face for a draft message header +I.e. a message with the draft flag set." + :group 'mu4e-faces) + +(defface mu4e-flagged-face + '((t :inherit font-lock-constant-face :bold t)) + "Face for a flagged message header." + :group 'mu4e-faces) + +(defface mu4e-replied-face + '((t :inherit font-lock-builtin-face :bold nil)) + "Face for a replied message header." + :group 'mu4e-faces) + +(defface mu4e-forwarded-face + '((t :inherit font-lock-builtin-face :bold nil)) + "Face for a passed (forwarded) message header." + :group 'mu4e-faces) + +(defface mu4e-header-face + '((t :inherit default)) + "Face for a header without any special flags." + :group 'mu4e-faces) + +(defface mu4e-header-title-face + '((t :inherit font-lock-type-face)) + "Face for a header title in the headers view." + :group 'mu4e-faces) + +(defface mu4e-header-highlight-face + '((t :inherit region :weight bold :underline t)) + "Face for the header at point." + :group 'mu4e-faces) + +(defface mu4e-header-marks-face + '((t :inherit font-lock-preprocessor-face)) + "Face for the mark in the headers list." + :group 'mu4e-faces) + +(defface mu4e-header-key-face + '((t :inherit message-header-name :bold t)) + "Face for a header key (such as \"Foo\" in \"Subject:\ Foo\")." + :group 'mu4e-faces) + +(defface mu4e-header-value-face + '((t :inherit font-lock-doc-face)) + "Face for a header value (such as \"Re: Hello!\")." + :group 'mu4e-faces) + +(defface mu4e-special-header-value-face + '((t :inherit font-lock-variable-name-face)) + "Face for special header values." + :group 'mu4e-faces) + +(defface mu4e-link-face + '((t :inherit link)) + "Face for showing URLs and attachments in the message view." + :group 'mu4e-faces) + +(defface mu4e-contact-face + '((t :inherit font-lock-variable-name-face)) + "Face for showing URLs and attachments in the message view." + :group 'mu4e-faces) + +(defface mu4e-highlight-face + '((t :inherit highlight)) + "Face for highlighting things." + :group 'mu4e-faces) + +(defface mu4e-title-face + '((t :inherit font-lock-type-face :bold t)) + "Face for a header title in the headers view." + :group 'mu4e-faces) + +(defface mu4e-modeline-face + '((t :inherit font-lock-string-face :bold t)) + "Face for the query in the mode-line." + :group 'mu4e-faces) + +(defface mu4e-view-body-face + '((t :inherit default)) + "Face for the body in the message-view." + :group 'mu4e-faces) + +(defface mu4e-footer-face + '((t :inherit font-lock-comment-face)) + "Face for message footers (signatures)." + :group 'mu4e-faces) + +(defface mu4e-url-number-face + '((t :inherit font-lock-constant-face :bold t)) + "Face for the number tags for URLs." + :group 'mu4e-faces) + +(defface mu4e-attach-number-face + '((t :inherit font-lock-variable-name-face :bold t)) + "Face for the number tags for attachments." + :group 'mu4e-faces) + +(defface mu4e-cited-1-face + '((t :inherit font-lock-builtin-face :bold nil :italic t)) + "Face for cited message parts (level 1)." + :group 'mu4e-faces) + +(defface mu4e-cited-2-face + '((t :inherit font-lock-type-face :bold nil :italic t)) + "Face for cited message parts (level 2)." + :group 'mu4e-faces) + +(defface mu4e-cited-3-face + '((t :inherit font-lock-variable-name-face :bold nil :italic t)) + "Face for cited message parts (level 3)." + :group 'mu4e-faces) + +(defface mu4e-cited-4-face + '((t :inherit font-lock-keyword-face :bold nil :italic t)) + "Face for cited message parts (level 4)." + :group 'mu4e-faces) + +(defface mu4e-cited-5-face + '((t :inherit font-lock-comment-face :bold nil :italic t)) + "Face for cited message parts (level 5)." + :group 'mu4e-faces) + +(defface mu4e-cited-6-face + '((t :inherit font-lock-comment-delimiter-face :bold nil :italic t)) + "Face for cited message parts (level 6)." + :group 'mu4e-faces) + +(defface mu4e-cited-7-face + '((t :inherit font-lock-preprocessor-face :bold nil :italic t)) + "Face for cited message parts (level 7)." + :group 'mu4e-faces) + +(defface mu4e-system-face + '((t :inherit font-lock-comment-face :slant italic)) + "Face for system message (such as the footers for message headers)." + :group 'mu4e-faces) + +(defface mu4e-ok-face + '((t :inherit font-lock-comment-face :bold t :slant normal)) + "Face for things that are okay." + :group 'mu4e-faces) + +(defface mu4e-warning-face + '((t :inherit font-lock-warning-face :bold t :slant normal)) + "Face for warnings / error." + :group 'mu4e-faces) + +(defface mu4e-compose-separator-face + '((t :inherit message-separator :slant italic)) + "Face for the separator between headers / message in +mu4e-compose-mode." + :group 'mu4e-faces) + +(defface mu4e-compose-header-face + '((t :inherit message-separator :slant italic)) + "Face for the separator between headers / message in +mu4e-compose-mode." + :group 'mu4e-faces) + +(defface mu4e-region-code + '((t (:background "DarkSlateGray"))) + "Face for highlighting marked region in mu4e-view buffer." + :group 'mu4e-faces) + +;; headers info +(defconst mu4e-header-info + '( (:attachments . + ( :name "Attachments" + :shortname "Atts" + :help "Message attachments" + :sortable nil)) + (:bcc . + ( :name "Bcc" + :shortname "Bcc" + :help "Blind Carbon-Copy recipients for the message" + :sortable t)) + (:cc . + ( :name "Cc" + :shortname "Cc" + :help "Carbon-Copy recipients for the message" + :sortable t)) + (:date . + ( :name "Date" + :shortname "Date" + :help "Date/time when the message was written" + :sortable t)) + (:human-date . + ( :name "Date" + :shortname "Date" + :help "Date/time when the message was written." + :sortable :date)) + (:flags . + ( :name "Flags" + :shortname "Flgs" + :help "Flags for the message" + :sortable nil)) + (:from . + ( :name "From" + :shortname "From" + :help "The sender of the message" + :sortable t)) + (:from-or-to . + ( :name "From/To" + :shortname "From/To" + :help "Sender of the message if it's not me; otherwise the recipient" + :sortable nil)) + (:maildir . + ( :name "Maildir" + :shortname "Maildir" + :help "Maildir for this message" + :sortable t)) + (:mailing-list . + ( :name "List" + :shortname "List" + :help "Mailing list for this message" + :sortable nil)) + (:message-id . + ( :name "Message-Id" + :shortname "MsgID" + :help "Message-Id for this message" + :sortable nil)) + (:path . + ( :name "Path" + :shortname "Path" + :help "Full filesystem path to the message" + :sortable t)) + (:signature . + ( :name "Signature" + :shortname "Sgn" + :help "Check for the cryptographic signature" + :sortable nil)) + (:decryption . + ( :name "Decryption" + :shortname "Dec" + :help "Check the cryptographic decryption status" + :sortable nil)) + (:size . + ( :name "Size" + :shortname "Size" + :help "Size of the message" + :sortable t)) + (:subject . + ( :name "Subject" + :shortname "Subject" + :help "Subject of the message" + :sortable t)) + (:tags . + ( :name "Tags" + :shortname "Tags" + :help "Tags for the message" + :sortable nil)) + (:thread-subject . + ( :name "Subject" + :shortname "Subject" + :help "Subject of the thread" + :sortable :subject)) + (:to . + ( :name "To" + :shortname "To" + :help "Recipient of the message" + :sortable t))) + "An alist of all possible header fields and information about them. +This is used in the user-interface (the column headers in the header list, and +the fields the message view). + +Most fields should be self-explanatory. A special one is +`:from-or-to', which is equal to `:from' unless `:from' matches one +of the addresses in `mu4e-user-mail-address-list', in which case it +will be equal to `:to'. + +Furthermore, the property `:sortable' determines whether we can +sort by this field. This can be either a boolean (nil or t), or a +symbol for /another/ field. For example, the `:human-date' field +uses `:date' for that. + +Note, `:sortable' does not work for custom header fields.") + + +(defvar mu4e-header-info-custom + '( (:recipnum . + ( :name "Number of recipients" + :shortname "Recip#" + :help "Number of recipients for this message" + :function + (lambda (msg) + (format "%d" + (+ (length (mu4e-message-field msg :to)) + (length (mu4e-message-field msg :cc)))))))) +"A list of custom (user-defined) headers. The format is similar +to `mu4e-header-info', but adds a :function property, which +should point to a function that takes a message p-list as +argument, and returns a string. See the default value of +`mu4e-header-info-custom for an example.") + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; run-time vars used in multiple places + +;; headers +(defconst mu4e~headers-buffer-name "*mu4e-headers*" + "Name of the buffer for message headers.") +(defvar mu4e~headers-buffer nil "Buffer for message headers.") +; 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.") + +(defvar mu4e~view-buffer nil "The view buffer.") + +(defvar mu4e~view-msg nil "The message being viewed in view mode.") + +(defvar mu4e~view-headers-buffer nil + "The headers buffer connected to this view.") + +(defvar mu4e~contacts nil + "Hash of that maps contacts (ie. 'name <e-mail>') to an integer +with their sort order. We need to keep this information around to +quickly re-sort subsets of the contacts in the completions function in +mu4e-compose.") + +(defvar mu4e~server-props nil + "Properties we receive from the mu4e server process. +\(in the 'pong-handler').") + +(defvar mu4e~headers-last-query nil + "The present (most recent) query.") + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; our handlers funcs +;; these handler funcs define what happens when we receive a certain message +;; from the server +(defun mu4e~default-handler (&rest args) + "*internal* Dummy handler function." + (error "Not handled: %S" args)) + +(defvar mu4e-error-func 'mu4e~default-handler + "A function called for each error returned from the server +process; the function is passed an error plist as argument. See +`mu4e~proc-filter' for the format.") + +(defvar mu4e-update-func 'mu4e~default-handler + "A function called for each :update sexp returned from the server +process; the function is passed a msg sexp as argument. See +`mu4e~proc-filter' for the format.") + +(defvar mu4e-remove-func 'mu4e~default-handler + "A function called for each :remove sexp returned from the server +process, when some message has been deleted. The function is passed +the docid of the removed message.") + +(defvar mu4e-sent-func 'mu4e~default-handler + "A function called for each :sent sexp returned from the server +process, 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~default-handler + "A function called for each single message sexp returned from the +server process. The function is passed a message sexp as +argument. See `mu4e~proc-filter' for the format.") + +(defvar mu4e-header-func 'mu4e~default-handler + "A function called for each message returned from the server +process; the function is passed a msg plist as argument. See +`mu4e~proc-filter' for the format.") + +(defvar mu4e-found-func 'mu4e~default-handler + "A function called for when we received a :found sexp after the +headers have returns, to report on the number of matches. See +`mu4e~proc-filter' for the format.") + +(defvar mu4e-erase-func 'mu4e~default-handler + "A function called for when we received an :erase sexp after the +headers have returns, to clear the current headers buffer. See +`mu4e~proc-filter' for the format.") + +(defvar mu4e-compose-func 'mu4e~default-handler + "A function called for each message returned from the server +process that is used as basis for composing a new message (ie., +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 <msg-plist>.") + +(defvar mu4e-info-func 'mu4e~default-handler + "A function called for each (:info type ....) sexp received from +the server process.") + +(defvar mu4e-pong-func 'mu4e~default-handler + "A function called for each (:pong type ....) sexp received from +the server process.") + +(defvar mu4e-contacts-func 'mu4e~default-handler + "A function called for each (:contacts (<list-of-contacts>) sexp +received from the server process.") + +(defvar mu4e-temp-func 'mu4e~default-handler + "A function called for each (:temp <file> <cookie>) sexp received +from the server process.") +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(provide 'mu4e-vars) +;;; End of mu4e-vars.el diff --git a/_spacemacs.d/local/mu4e/mu4e-view.el b/_spacemacs.d/local/mu4e/mu4e-view.el new file mode 100644 index 0000000..75fc79f --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-view.el @@ -0,0 +1,1541 @@ +;;; mu4e-view.el -- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; In this file we define mu4e-view-mode (+ helper functions), which is used for +;; viewing e-mail messages + +;;; Code: +(require 'mu4e-utils) ;; utility functions +(require 'mu4e-vars) +(require 'mu4e-mark) +(require 'mu4e-proc) +(require 'mu4e-compose) +(require 'mu4e-actions) +(require 'mu4e-message) + +(require 'comint) +(require 'browse-url) +(require 'button) +(require 'epa) +(require 'epg) +(require 'thingatpt) +(require 'calendar) + +(eval-when-compile (byte-compile-disable-warning 'cl-functions)) +(require 'cl) + + +;; the message view +(defgroup mu4e-view nil + "Settings for the message view." + :group 'mu4e) + +(defcustom mu4e-view-fields + '(:from :to :cc :subject :flags :date :maildir :mailing-list :tags + :attachments :signature :decryption) + "Header fields to display in the message view buffer. +For the complete list of available headers, see `mu4e-header-info'." + :type (list 'symbol) + :group 'mu4e-view) + + +(defcustom mu4e-view-show-addresses nil + "Whether to initially show full e-mail addresses for contacts in +address fields, rather than only their names." + :type 'boolean + :group 'mu4e-view) + +(make-obsolete-variable 'mu4e-view-wrap-lines nil "0.9.9-dev7") +(make-obsolete-variable 'mu4e-view-hide-cited nil "0.9.9-dev7") + +(defcustom mu4e-view-date-format "%c" + "Date format to use in the message view. +In the format of `format-time-string'." + :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." + :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." + :group 'mu4e-view) + +(defcustom mu4e-view-scroll-to-next t + "If non-nil, move to the next message when calling +`mu4e-view-scroll-up-or-next' (typically bound to SPC) when at the +end of a message. Otherwise, don't move to the next message.") + +(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." + :type 'boolean + :group 'mu4e-view) + +(defvar mu4e-view-actions + '( ("capture message" . mu4e-action-capture-message) + ("view as pdf" . mu4e-action-view-as-pdf) + ("show this thread" . mu4e-action-show-thread)) + "List of actions to perform on messages in view mode. +The actions are of the form: + (NAME FUNC) +where: +* NAME is the name of the action (e.g. \"Count lines\") +* FUNC is a function which receives a message plist as an argument. + +The first letter of NAME is used as a shortcut character.") + +(defvar mu4e-view-attachment-actions + '( ("wopen-with" . mu4e-view-open-attachment-with) + ("ein-emacs" . mu4e-view-open-attachment-emacs) + ("dimport-in-diary" . mu4e-view-import-attachment-diary) + ("|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.") + +(defvar mu4e-view-fill-headers t + "If non-nil, automatically fill the headers when viewing them.") + +(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.") + +(defvar mu4e-view-clickable-urls-keymap + (let ((map (make-sparse-keymap))) + (define-key map [mouse-1] 'mu4e~view-browse-url-from-binding) + (define-key map [?\M-\r] 'mu4e~view-browse-url-from-binding) + map) + "Keymap used for the urls inside the body.") + +(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 "<S-return>") 'mu4e~view-save-attach-from-binding) + map) + "Keymap used in the \"Attachements\" header field.") + +(defcustom mu4e-view-auto-mark-as-read t + "Automatically mark messages are 'read' when you read +them. This is typically the expected behavior, but can be turned +off, for example when using a read-only file-system." + :type 'boolean + :group 'mu4e-view) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defvar mu4e~view-cited-hidden nil "Whether cited lines are hidden.") +(defvar mu4e~view-link-map nil + "A map of some number->url so we can jump to url by number.") + +(defvar mu4e~path-parent-docid-map (make-hash-table :test 'equal) + "A map of msg paths --> parent-docids. +This is to determine what is the parent docid for embedded +message extracted at some path.") + +(defvar mu4e~view-attach-map nil + "A mapping of user-visible attachment number to the actual part index.") +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defun mu4e-view-message-with-message-id (msgid) + "View message with message-id MSGID. This (re)creates a +headers-buffer with a search for MSGID, then open a view for that +message." + (mu4e-headers-search (concat "msgid:" msgid) nil nil t msgid t)) + +(define-obsolete-function-alias 'mu4e-view-message-with-msgid + 'mu4e-view-message-with-message-id "0.9.17") + +(defun mu4e~view-custom-field (msg field) + "Show some custom header field, or raise an error if it is not +found." + (let* ((item (or (assoc field mu4e-header-info-custom) + (mu4e-error "field %S not found" field))) + (func (or (plist-get (cdr-safe item) :function) + (mu4e-error "no :function defined for field %S %S" + field (cdr item))))) + (funcall func msg))) + + +(defun mu4e-view-message-text (msg) + "Return the message to display (as a string), based on the MSG plist." + (concat + (mapconcat + (lambda (field) + (let ((fieldval (mu4e-message-field msg field))) + (case field + (:subject (mu4e~view-construct-header field fieldval)) + (:path (mu4e~view-construct-header field fieldval)) + (:maildir (mu4e~view-construct-header field fieldval)) + ((:flags :tags) (mu4e~view-construct-flags-tags-header + field fieldval)) + + ;; contact fields + (:to (mu4e~view-construct-contacts-header msg field)) + (:from (mu4e~view-construct-contacts-header msg field)) + (:cc (mu4e~view-construct-contacts-header msg field)) + (:bcc (mu4e~view-construct-contacts-header msg field)) + + ;; if we (`user-mail-address' are the From, show To, otherwise, + ;; show From + (:from-or-to + (let* ((from (mu4e-message-field msg :from)) + (from (and from (cdar from)))) + (if (mu4e-user-mail-address-p from) + (mu4e~view-construct-contacts-header msg :to) + (mu4e~view-construct-contacts-header msg :from)))) + ;; date + (:date + (let ((datestr + (when fieldval (format-time-string mu4e-view-date-format + fieldval)))) + (if datestr (mu4e~view-construct-header field datestr) ""))) + ;; size + (:size + (mu4e~view-construct-header field (mu4e-display-size fieldval))) + (:mailing-list + (mu4e~view-construct-header field fieldval)) + (:message-id + (mu4e~view-construct-header field fieldval)) + ;; attachments + (:attachments (mu4e~view-construct-attachments-header msg)) + ;; pgp-signatures + (:signature (mu4e~view-construct-signature-header msg)) + ;; pgp-decryption + (:decryption (mu4e~view-construct-decryption-header msg)) + (t (mu4e~view-construct-header field + (mu4e~view-custom-field msg field)))))) + mu4e-view-fields "") + "\n" + (let ((body (mu4e-message-body-text msg))) + (when (fboundp 'add-face-text-property) + (add-face-text-property 0 (length body) 'mu4e-view-body-face t body)) + body))) + +(defun mu4e~view-embedded-winbuf () + "Get a buffer (shown in a window) for the embedded message." + (let* ((buf (get-buffer-create mu4e~view-embedded-buffer-name)) + (win (or (get-buffer-window buf) (split-window-vertically)))) + (select-window win) + (switch-to-buffer buf))) + +(defun mu4e~delete-all-overlays () + "`delete-all-overlays' with compatibility fallback." + (if (functionp 'delete-all-overlays) + (delete-all-overlays) + (remove-overlays))) + + +(defun mu4e-view (msg headersbuf) + "Display the message MSG in a new buffer, and keep in sync with HDRSBUF. +'In sync' here means that moving to the next/previous message in +the the message view affects HDRSBUF, as does marking etc. + +As a side-effect, a message that is being viewed loses its 'unread' +marking if it still had that." + (let* ((embedded ;; is it as an embedded msg (ie. message/rfc822 att)? + (when (gethash (mu4e-message-field msg :path) + mu4e~path-parent-docid-map) t)) + (buf + (if embedded + (mu4e~view-embedded-winbuf) + (get-buffer-create mu4e~view-buffer-name)))) + ;; note: mu4e~view-mark-as-read-maybe will pseudo-recursively call mu4e-view + ;; again by triggering mu4e~view again as it marks the message as read + (with-current-buffer buf + (switch-to-buffer buf) + (setq mu4e~view-msg msg) + (when (or embedded (not (mu4e~view-mark-as-read-maybe msg))) + (let ((inhibit-read-only t)) + (erase-buffer) + (mu4e~delete-all-overlays) + (insert (mu4e-view-message-text msg)) + (goto-char (point-min)) + (mu4e~fontify-cited) + (mu4e~fontify-signature) + (mu4e~view-make-urls-clickable) + (mu4e~view-show-images-maybe msg) + (setq + mu4e~view-buffer buf + mu4e~view-headers-buffer headersbuf) + (when embedded (local-set-key "q" 'kill-buffer-and-window)) + (mu4e-view-mode)))))) + +(defun mu4e~view-get-property-from-event (prop) + "Get the property PROP at point, or the location of the mouse. +The action is chosen based on the `last-command-event'. +Meant to be evoked from interactive commands." + (if (and (eventp last-command-event) + (mouse-event-p last-command-event)) + (let ((posn (event-end last-command-event))) + (when (numberp (posn-point posn)) + (get-text-property + (posn-point posn) + prop + (window-buffer (posn-window posn))) + )) + (get-text-property (point) prop))) + +(defun mu4e~view-construct-header (field val &optional dont-propertize-val) + "Return header field FIELD (as in `mu4e-header-info') with value +VAL if VAL is non-nil. If DONT-PROPERTIZE-VAL is non-nil, do not +add text-properties to VAL." + (let* ((info (cdr (assoc field + (append mu4e-header-info mu4e-header-info-custom)))) + (key (plist-get info :name)) + (help (plist-get info :help))) + (if (and val (> (length val) 0)) + (with-temp-buffer + (insert (propertize (concat key ":") + 'face 'mu4e-header-key-face + 'help-echo help) " " + (if dont-propertize-val + val + (propertize val 'face 'mu4e-header-value-face)) "\n") + (when mu4e-view-fill-headers + ;; temporarily set the fill column <margin> positions to the right, so + ;; we can indent the following lines correctly + (let* ((margin 1) + (fill-column (max (- fill-column margin) 0))) + (fill-region (point-min) (point-max)) + (goto-char (point-min)) + (while (and (zerop (forward-line 1)) (not (looking-at "^$"))) + (indent-to-column margin)))) + (buffer-string)) + ""))) + +(defun mu4e~view-compose-contact (&optional point) + "Compose a message for the address at point." + (interactive) + (unless (get-text-property (or point (point)) 'email) + (mu4e-error "No address at point")) + (mu4e~compose-mail (get-text-property (or point (point)) 'long))) + +(defun mu4e~view-copy-contact (&optional full) + "Compose a message for the address at (point)." + (interactive "P") + (let ((email (get-text-property (point) 'email)) + (long (get-text-property (point) 'long))) + (unless email (mu4e-error "No address at point")) + (kill-new (if full long email)) + (mu4e-message "Address copied."))) + +(defun mu4e~view-construct-contacts-header (msg field) + "Add a header for a contact field (ie., :to, :from, :cc, :bcc)." + (mu4e~view-construct-header field + (mapconcat + (lambda(c) + (let* ((name (when (car c) + (replace-regexp-in-string "[[:cntrl:]]" "" (car c)))) + (email (when (cdr c) + (replace-regexp-in-string "[[:cntrl:]]" "" (cdr c)))) + (short (or name email)) ;; name may be nil + (long (if name (format "%s <%s>" name email) email))) + (propertize + (if mu4e-view-show-addresses long short) + 'long long + 'short short + 'email email + 'keymap mu4e-view-contacts-header-keymap + 'face 'mu4e-contact-face + 'mouse-face 'highlight + 'help-echo (format "<%s>\n%s" email + "[mouse-2] or C to compose a mail for this recipient")))) + (mu4e-message-field msg field) ", ") t)) + + +(defun mu4e~view-construct-flags-tags-header (field val) + "Construct a Flags: header." + (mu4e~view-construct-header + field + (mapconcat + (lambda (flag) + (propertize + (if (symbolp flag) + (symbol-name flag) + flag) + 'face 'mu4e-special-header-value-face)) + val + (propertize ", " 'face 'mu4e-header-value-face)) t)) + +(defun mu4e~view-construct-signature-header (msg) + "Construct a Signature: header, if there are any signed parts." + (let* ((parts (mu4e-message-field msg :parts)) + (verdicts + (remove-if 'null + (mapcar (lambda (part) (mu4e-message-part-field part :signature)) + parts))) + (val (when verdicts + (mapconcat + (lambda (v) + (propertize (symbol-name v) + 'face (if (eq v 'verified) + 'mu4e-ok-face 'mu4e-warning-face))) + verdicts ", "))) + (btn (when val + (with-temp-buffer + (insert-text-button "Details" + 'action (lambda (b) + (mu4e-view-verify-msg-popup + (button-get b 'msg)))) + (buffer-string)))) + (val (when val (concat val " (" btn ")")))) + (mu4e~view-construct-header :signature val t))) + +(defun mu4e~view-construct-decryption-header (msg) + "Construct a Decryption: header, if there are any encrypted parts." + (let* ((parts (mu4e-message-field msg :parts)) + (verdicts + (remove-if 'null + (mapcar (lambda (part) + (mu4e-message-part-field part :decryption)) + parts))) + (succeeded (remove-if (lambda (v) (eq v 'failed)) verdicts)) + (failed (remove-if (lambda (v) (eq v 'succeeded)) verdicts)) + (succ (when succeeded + (propertize + (concat (number-to-string (length succeeded)) + " part(s) decrypted") + 'face 'mu4e-ok-face))) + (fail (when failed + (propertize + (concat (number-to-string (length failed)) + " part(s) failed") + 'face 'mu4e-warning-face))) + (val (concat succ fail))) + (mu4e~view-construct-header :decryption val t))) + +(defun mu4e~view-open-attach-from-binding () + "Open the attachement at point, or click location." + (interactive) + (let* (( msg (mu4e~view-get-property-from-event 'mu4e-msg)) + ( attnum (mu4e~view-get-property-from-event 'mu4e-attnum))) + (when (and msg attnum) + (mu4e-view-open-attachment msg attnum)))) + +(defun mu4e~view-save-attach-from-binding () + "Save the attachement at point, or click location." + (interactive) + (let* (( msg (mu4e~view-get-property-from-event 'mu4e-msg)) + ( attnum (mu4e~view-get-property-from-event 'mu4e-attnum))) + (when (and msg attnum) + (mu4e-view-save-attachment-single msg attnum)))) + +(defun mu4e~view-construct-attachments-header (msg) + "Display attachment information; the field looks like something like: + :parts ((:index 1 :name \"1.part\" :mime-type \"text/plain\" + :type (leaf) :attachment nil :size 228) + (:index 2 :name \"analysis.doc\" + :mime-type \"application/msword\" + :type (leaf attachment) :attachment nil :size 605196))" + (setq mu4e~view-attach-map ;; buffer local + (make-hash-table :size 64 :weakness nil)) + (let* ((id 0) + (attachments + ;; we only list parts that look like attachments, ie. that have a + ;; non-nil :attachment property; we record a mapping between + ;; user-visible numbers and the part indices + (remove-if-not + (lambda (part) + (let* ((mtype (or (mu4e-message-part-field part :mime-type) + "application/octet-stream")) + (attachtype (mu4e-message-part-field part :type)) + (isattach + (or ;; we consider parts marked either + ;; "attachment" or "inline" as attachment. + (member 'attachment attachtype) + ;; list inline parts as attachment (so they can be + ;; saved), unless they are text/plain, which are + ;; usually just message footers in mailing lists + (and (member 'inline attachtype) + (not (string-match "^text/plain" mtype)))))) + (or ;; remove if it's not an attach *or* if it's an + ;; image/audio/application type (but not a signature) + isattach + (string-match "^\\(image\\|audio\\)" mtype) + (string= "message/rfc822" mtype) + (string= "text/calendar" mtype) + (and (string-match "^application" mtype) + (not (string-match "signature" mtype)))))) + (mu4e-message-field msg :parts))) + (attstr + (mapconcat + (lambda (part) + (let ((index (mu4e-message-part-field part :index)) + (name (mu4e-message-part-field part :name)) + (size (mu4e-message-part-field part :size))) + (incf id) + (puthash id index mu4e~view-attach-map) + + (concat + (propertize (format "[%d]" id) + 'face 'mu4e-attach-number-face) + (propertize name 'face 'mu4e-link-face + 'keymap mu4e-view-attachments-header-keymap + 'mouse-face 'highlight + 'help-echo (concat + "[mouse-1] or [M-RET] opens the attachment\n" + "[mouse-2] or [S-RET] offers to save it") + 'mu4e-msg msg + 'mu4e-attnum id + ) + (when (and size (> size 0)) + (propertize (format "(%s)" (mu4e-display-size size)) + 'face 'mu4e-header-key-face))))) + attachments ", "))) + (when attachments + (mu4e~view-construct-header :attachments attstr t)))) + +(defun mu4e-view-for-each-part (msg func) + "Apply FUNC to each part in MSG. +FUNC should be a function taking two arguments: + 1. the message MSG, and + 2. a plist describing the attachment. The plist looks like: + (:index 1 :name \"test123.doc\" + :mime-type \"application/msword\" :attachment t :size 1234)." + (dolist (part (mu4e-msg-field msg :parts)) + (funcall func msg part))) + +(defvar mu4e-view-mode-map nil + "Keymap for \"*mu4e-view*\" buffers.") +(unless mu4e-view-mode-map + (setq mu4e-view-mode-map + (let ((map (make-sparse-keymap))) + + (define-key map (kbd "C-S-u") 'mu4e-update-mail-and-index) + (define-key map (kbd "C-c C-u") 'mu4e-update-mail-and-index) + + (define-key map "q" 'mu4e~view-quit-buffer) + + ;; note, 'z' is by-default bound to 'bury-buffer' + ;; but that's not very useful in this case + (define-key map "z" 'ignore) + + (define-key map "s" 'mu4e-headers-search) + (define-key map "S" 'mu4e-view-search-edit) + (define-key map "/" 'mu4e-view-search-narrow) + + (define-key map (kbd "<M-left>") 'mu4e-headers-query-prev) + (define-key map (kbd "<M-right>") 'mu4e-headers-query-next) + + (define-key map "b" 'mu4e-headers-search-bookmark) + (define-key map "B" 'mu4e-headers-search-bookmark-edit) + + (define-key map "%" 'mu4e-view-mark-pattern) + (define-key map "t" 'mu4e-view-mark-subthread) + (define-key map "T" 'mu4e-view-mark-thread) + + (define-key map "v" 'mu4e-view-verify-msg-popup) + + (define-key map "j" 'mu4e~headers-jump-to-maildir) + + (define-key map "g" 'mu4e-view-go-to-url) + (define-key map "k" 'mu4e-view-save-url) + (define-key map "f" 'mu4e-view-fetch-url) + + (define-key map "F" 'mu4e-compose-forward) + (define-key map "R" 'mu4e-compose-reply) + (define-key map "C" 'mu4e-compose-new) + (define-key map "E" 'mu4e-compose-edit) + + (define-key map "." 'mu4e-view-raw-message) + (define-key map "|" 'mu4e-view-pipe) + (define-key map "a" 'mu4e-view-action) + + (define-key map ";" 'mu4e-context-switch) + + ;; toggle header settings + (define-key map "O" 'mu4e-headers-change-sorting) + (define-key map "P" 'mu4e-headers-toggle-threading) + (define-key map "Q" 'mu4e-headers-toggle-full-search) + (define-key map "W" 'mu4e-headers-toggle-include-related) + + ;; change the number of headers + (define-key map (kbd "C-+") 'mu4e-headers-split-view-grow) + (define-key map (kbd "C--") 'mu4e-headers-split-view-shrink) + (define-key map (kbd "<C-kp-add>") 'mu4e-headers-split-view-grow) + (define-key map (kbd "<C-kp-subtract>") 'mu4e-headers-split-view-shrink) + + ;; intra-message navigation + (define-key map (kbd "SPC") 'mu4e-view-scroll-up-or-next) + (define-key map (kbd "<home>") 'beginning-of-buffer) + (define-key map (kbd "<end>") 'end-of-buffer) + (define-key map (kbd "RET") 'mu4e-scroll-up) + (define-key map (kbd "<backspace>") 'mu4e-scroll-down) + + ;; navigation between messages + (define-key map "p" 'mu4e-view-headers-prev) + (define-key map "n" 'mu4e-view-headers-next) + ;; the same + (define-key map (kbd "<M-down>") 'mu4e-view-headers-next) + (define-key map (kbd "<M-up>") 'mu4e-view-headers-prev) + + (define-key map (kbd "[") 'mu4e-view-headers-prev-unread) + (define-key map (kbd "]") 'mu4e-view-headers-next-unread) + + ;; switching to view mode (if it's visible) + (define-key map "y" 'mu4e-select-other-view) + + ;; attachments + (define-key map "e" 'mu4e-view-save-attachment) + (define-key map "o" 'mu4e-view-open-attachment) + (define-key map "A" 'mu4e-view-attachment-action) + + ;; marking/unmarking + (define-key map "d" 'mu4e-view-mark-for-trash) + (define-key map (kbd "<delete>") 'mu4e-view-mark-for-delete) + (define-key map (kbd "<deletechar>") 'mu4e-view-mark-for-delete) + (define-key map (kbd "D") 'mu4e-view-mark-for-delete) + (define-key map (kbd "m") 'mu4e-view-mark-for-move) + (define-key map (kbd "r") 'mu4e-view-mark-for-refile) + + (define-key map (kbd "?") 'mu4e-view-mark-for-unread) + (define-key map (kbd "!") 'mu4e-view-mark-for-read) + + (define-key map (kbd "+") 'mu4e-view-mark-for-flag) + (define-key map (kbd "-") 'mu4e-view-mark-for-unflag) + (define-key map (kbd "=") 'mu4e-view-mark-for-untrash) + (define-key map (kbd "&") 'mu4e-view-mark-custom) + + (define-key map (kbd "*") 'mu4e-view-mark-for-something) + (define-key map (kbd "<kp-multiply>") 'mu4e-view-mark-for-something) + (define-key map (kbd "<insert>") 'mu4e-view-mark-for-something) + (define-key map (kbd "<insertchar>") 'mu4e-view-mark-for-something) + + (define-key map (kbd "#") 'mu4e-mark-resolve-deferred-marks) + + ;; misc + (define-key map "w" 'visual-line-mode) + (define-key map "#" 'mu4e-view-toggle-hide-cited) + (define-key map "h" 'mu4e-view-toggle-html) + (define-key map (kbd "M-q") 'mu4e-view-fill-long-lines) + + ;; next 3 only warn user when attempt in the message view + (define-key map "u" 'mu4e-view-unmark) + (define-key map "U" 'mu4e-view-unmark-all) + (define-key map "x" 'mu4e-view-marked-execute) + + (define-key map "$" 'mu4e-show-log) + (define-key map "H" 'mu4e-display-manual) + + ;; menu + (define-key map [menu-bar] (make-sparse-keymap)) + (let ((menumap (make-sparse-keymap "View"))) + (define-key map [menu-bar headers] (cons "View" menumap)) + + (define-key menumap [quit-buffer] + '("Quit view" . mu4e~view-quit-buffer)) + (define-key menumap [display-help] '("Help" . mu4e-display-manual)) + + (define-key menumap [sepa0] '("--")) + (define-key menumap [wrap-lines] + '("Toggle wrap lines" . visual-line-mode)) + (define-key menumap [toggle-html] + '("Toggle view-html" . mu4e-view-toggle-html)) + (define-key menumap [hide-cited] + '("Toggle hide cited" . mu4e-view-toggle-hide-cited)) + (define-key menumap [raw-view] + '("View raw message" . mu4e-view-raw-message)) + (define-key menumap [pipe] + '("Pipe through shell" . mu4e-view-pipe)) + ;; (define-key menumap [inspect] + ;; '("Inspect with guile" . mu4e-inspect-message)) + + (define-key menumap [sepa8] '("--")) + (define-key menumap [open-att] + '("Open attachment" . mu4e-view-open-attachment)) + (define-key menumap [extract-att] + '("Extract attachment" . mu4e-view-save-attachment)) + + (define-key menumap [save-url] + '("Save URL to kill-ring" . mu4e-view-save-url)) + (define-key menumap [fetch-url] + '("Fetch URL" . mu4e-view-fetch-url)) + (define-key menumap [goto-url] + '("Visit URL" . mu4e-view-go-to-url)) + + (define-key menumap [sepa1] '("--")) + (define-key menumap [mark-delete] + '("Mark for deletion" . mu4e-view-mark-for-delete)) + (define-key menumap [mark-trash] + '("Mark for trash" . mu4e-view-mark-for-trash)) + (define-key menumap [mark-move] + '("Mark for move" . mu4e-view-mark-for-move)) + + (define-key menumap [sepa2] '("--")) + (define-key menumap [resend] '("Resend" . mu4e-compose-resend)) + (define-key menumap [forward] '("Forward" . mu4e-compose-forward)) + (define-key menumap [reply] '("Reply" . mu4e-compose-reply)) + (define-key menumap [compose-new] '("Compose new" . mu4e-compose-new)) + (define-key menumap [sepa3] '("--")) + + (define-key menumap [query-next] + '("Next query" . mu4e-headers-query-next)) + (define-key menumap [query-prev] + '("Previous query" . mu4e-headers-query-prev)) + (define-key menumap [narrow-search] + '("Narrow search" . mu4e-headers-search-narrow)) + (define-key menumap [bookmark] + '("Search bookmark" . mu4e-headers-search-bookmark)) + (define-key menumap [jump] + '("Jump to maildir" . mu4e~headers-jump-to-maildir)) + (define-key menumap [search] + '("Search" . mu4e-headers-search)) + + (define-key menumap [sepa4] '("--")) + (define-key menumap [next] '("Next" . mu4e-view-headers-next)) + (define-key menumap [previous] '("Previous" . mu4e-view-headers-prev))) + map))) + +(fset 'mu4e-view-mode-map mu4e-view-mode-map) + +(defcustom mu4e-view-mode-hook nil + "Hook run when entering Mu4e-View mode." + :options '(turn-on-visual-line-mode) + :type 'hook + :group 'mu4e-view) + +(defvar mu4e-view-mode-abbrev-table nil) +(define-derived-mode mu4e-view-mode special-mode "mu4e:view" + "Major mode for viewing an e-mail message in mu4e. +\\{mu4e-view-mode-map}." + (use-local-map mu4e-view-mode-map) + + (make-local-variable 'mu4e~view-headers-buffer) + (make-local-variable 'mu4e~view-msg) + (make-local-variable 'mu4e~view-link-map) + (make-local-variable 'mu4e~view-attach-map) + (make-local-variable 'mu4e~view-cited-hidden) + + ;; show context in mode-string + (set (make-local-variable 'global-mode-string) '(:eval (mu4e-context-label))) + + (setq buffer-undo-list t);; don't record undo info + + ;; autopair mode gives error when pressing RET + ;; turn it off + (when (boundp 'autopair-dont-activate) + (setq autopair-dont-activate t))) + +(defun mu4e~view-mark-as-read-maybe (msg) + "Clear the message MSG New/Unread status and set it to Seen. +If the message is not New/Unread, do nothing. Evaluates to t if it +triggers any changes, nil otherwise. If this function does any +changes, it triggers a refresh." + (when (and mu4e-view-auto-mark-as-read msg) + (let ((flags (mu4e-message-field msg :flags)) + (msgid (mu4e-message-field msg :message-id)) + (docid (mu4e-message-field msg :docid))) + ;; attached (embedded) messages don't have docids; leave them alone if it is a new message + (when (and docid (or (member 'unread flags) (member 'new flags))) + ;; mark /all/ messages with this message-id as read, so all copies of + ;; this message will be marked as read. + (mu4e~proc-move msgid nil "+S-u-N") + t)))) + +(defun mu4e~view-browse-url-func (url) + "Return a function that executes `browse-url' with URL. +What browser is called is depending on +`browse-url-browser-function' and `browse-url-mailto-function'." + (save-match-data + (if (string-match "^mailto:" url) + (lexical-let ((url url)) + (lambda () + (interactive) + (mu4e~compose-browse-url-mail url))) + (lexical-let ((url url)) + (lambda () + (interactive) + (browse-url url)))))) + +(defun mu4e~view-browse-url-from-binding (&optional url) + "View in browser the url at point, or click location. +If the optional argument URL is provided, browse that instead. +If the url is mailto link, start writing an email to that address." + (interactive) + (let* (( url (or url (mu4e~view-get-property-from-event 'mu4e-url)))) + (when url + (if (string-match-p "^mailto:" url) + (mu4e~compose-browse-url-mail url) + (browse-url url))))) + +(defun mu4e~view-show-images-maybe (msg) + "Show attached images, if `mu4e-show-images' is non-nil." + (when (and (display-images-p) mu4e-view-show-images) + (mu4e-view-for-each-part msg + (lambda (msg part) + (when (string-match "^image/" + (or (mu4e-message-part-field part :mime-type) + "application/object-stream")) + (let ((imgfile (mu4e-message-part-field part :temp))) + (when (and imgfile (file-exists-p imgfile)) + (save-excursion + (goto-char (point-max)) + (mu4e-display-image imgfile + mu4e-view-image-max-width + mu4e-view-image-max-height))))))))) + + +(defvar mu4e~view-beginning-of-url-regexp + "https?\\://\\|mailto:" + "Regexp that matches the beginning of http:/https:/mailto: +URLs; match-string 1 will contain the matched URL, if any.") + +;; this is fairly simplistic... +(defun mu4e~view-make-urls-clickable () + "Turn things that look like URLs into clickable things. +Also number them so they can be opened using `mu4e-view-go-to-url'." + (let ((num 0)) + (save-excursion + (setq mu4e~view-link-map ;; buffer local + (make-hash-table :size 32 :weakness nil)) + (goto-char (point-min)) + (while (re-search-forward mu4e~view-beginning-of-url-regexp nil t) + (let ((bounds (thing-at-point-bounds-of-url-at-point))) + (when bounds + (let* ((url (thing-at-point-url-at-point)) + (ov (make-overlay (car bounds) (cdr bounds)))) + (puthash (incf num) url mu4e~view-link-map) + (add-text-properties + (car bounds) + (cdr bounds) + `(face mu4e-link-face + mouse-face highlight + mu4e-url ,url + keymap ,mu4e-view-clickable-urls-keymap + help-echo + "[mouse-1] or [M-RET] to open the link")) + (overlay-put ov 'after-string + (propertize (format "[%d]" num) + 'face 'mu4e-url-number-face))))))))) + + +(defun mu4e~view-hide-cited () + "Toggle hiding of cited lines in the message body." + (save-excursion + (let ((inhibit-read-only t)) + (goto-char (point-min)) + (flush-lines mu4e-cited-regexp) + (setq mu4e~view-cited-hidden t)))) + +(defmacro mu4e~view-in-headers-context (&rest body) + "Evaluate BODY in the context of the headers buffer connected to +this view." + `(progn + (unless (buffer-live-p mu4e~view-headers-buffer) + (mu4e-error "no headers-buffer connected")) + (let* ((msg (mu4e-message-at-point)) + (docid (mu4e-message-field msg :docid)) + (curwin (selected-window))) + (unless docid + (mu4e-error "message without docid: action is not possible.")) + (with-current-buffer mu4e~view-headers-buffer + (when (get-buffer-window) + (select-window (get-buffer-window))) + (if (mu4e~headers-goto-docid docid) + ,@body + (mu4e-error "cannot find message in headers buffer.")))))) + +(defun mu4e-view-headers-next (&optional n) + "Move point to the next message header in the headers buffer +connected with this message view. If this succeeds, return the new +docid. Otherwise, return nil. Optionally, takes an integer +N (prefix argument), to the Nth next header." + (interactive "P") + (mu4e~view-in-headers-context + (mu4e~headers-move (or n 1)))) + +(defun mu4e-view-headers-prev (&optional n) + "Move point to the previous message header in the headers buffer +connected with this message view. If this succeeds, return the new +docid. Otherwise, return nil. Optionally, takes an integer +N (prefix argument), to the Nth previous header." + (interactive "P") + (mu4e~view-in-headers-context + (mu4e~headers-move (- (or n 1))))) + +(defun mu4e~view-prev-or-next-unread (backwards) + "Move point to the next or previous (when BACKWARDS is non-`nil') +unread message header in the headers buffer connected with this +message view. If this succeeds, return the new docid. Otherwise, +return nil." + (mu4e~view-in-headers-context + (mu4e~headers-prev-or-next-unread backwards)) + (mu4e-select-other-view) + (mu4e-headers-view-message)) + +(defun mu4e-view-headers-prev-unread () +"Move point to the previous unread message header in the headers +buffer connected with this message view. If this succeeds, return +the new docid. Otherwise, return nil." + (interactive) + (mu4e~view-prev-or-next-unread t)) + +(defun mu4e-view-headers-next-unread () + "Move point to the next unread message header in the headers +buffer connected with this message view. If this succeeds, return +the new docid. Otherwise, return nil." + (interactive) + (mu4e~view-prev-or-next-unread nil)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Interactive functions + +(defun mu4e-view-toggle-hide-cited () + "Toggle hiding of cited lines in the message body." + (interactive) + (if mu4e~view-cited-hidden + (mu4e-view-refresh) + (mu4e~view-hide-cited))) + +(defun mu4e-view-toggle-html () + "Toggle html-display of the message body (if any)." + (interactive) + (setq mu4e-view-prefer-html (not mu4e-view-prefer-html)) + (mu4e-view-refresh)) + +(defun mu4e-view-refresh () + "Redisplay the current message." + (interactive) + (mu4e-view mu4e~view-msg mu4e~view-headers-buffer) + (setq mu4e~view-cited-hidden nil)) + +(defun mu4e-view-action (&optional msg) + "Ask user for some action to apply on MSG, then do it. +If MSG is nil apply action to message returned +bymessage-at-point. The actions are specified in +`mu4e-view-actions'." + (interactive) + (let* ((msg (or msg (mu4e-message-at-point))) + (actionfunc (mu4e-read-option "Action: " mu4e-view-actions))) + (funcall actionfunc msg))) + +(defun mu4e-view-mark-pattern () + "Ask user for a kind of mark (move, delete etc.), a field to +match and a regular expression to match with. Then, mark all +matching messages with that mark." + (interactive) + (mu4e~view-in-headers-context (mu4e-headers-mark-pattern))) + +(defun mu4e-view-mark-thread (&optional markpair) + "Ask user for a kind of mark (move, delete etc.), and apply it +to all messages in the thread at point in the headers view. The +optional MARKPAIR can also be used to provide the mark +selection." + (interactive) + (mu4e~view-in-headers-context + (if markpair (mu4e-headers-mark-thread nil markpair) + (call-interactively 'mu4e-headers-mark-thread)))) + +(defun mu4e-view-mark-subthread (&optional markpair) + "Ask user for a kind of mark (move, delete etc.), and apply it +to all messages in the subthread at point in the headers view. +The optional MARKPAIR can also be used to provide the mark +selection." + (interactive) + (mu4e~view-in-headers-context + (if markpair (mu4e-headers-mark-subthread markpair) + (mu4e-headers-mark-subthread)))) + +(defun mu4e-view-search-narrow () + "Run `mu4e-headers-search-narrow' in the headers buffer." + (interactive) + (mu4e~view-in-headers-context + (call-interactively 'mu4e-headers-search-narrow))) + +(defun mu4e-view-search-edit () + "Run `mu4e-headers-search-edit' in the headers buffer." + (interactive) + (mu4e~view-in-headers-context (mu4e-headers-search-edit))) + +(defun mu4e-mark-region-code () + "Highlight region marked with `message-mark-inserted-region'. +Add this function to `mu4e-view-mode-hook' to enable this feature." + (require 'message) + (let (beg end ov-beg ov-end ov-inv) + (save-excursion + (goto-char (point-min)) + (while (re-search-forward + (concat "^" message-mark-insert-begin) nil t) + (setq ov-beg (match-beginning 0) + ov-end (match-end 0) + ov-inv (make-overlay ov-beg ov-end) + beg ov-end) + (overlay-put ov-inv 'invisible t) + (when (re-search-forward + (concat "^" message-mark-insert-end) nil t) + (setq ov-beg (match-beginning 0) + ov-end (match-end 0) + ov-inv (make-overlay ov-beg ov-end) + end ov-beg) + (overlay-put ov-inv 'invisible t)) + (when (and beg end) + (let ((ov (make-overlay beg end))) + (overlay-put ov 'face 'mu4e-region-code)) + (setq beg nil end nil)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Wash functions +(defun mu4e-view-fill-long-lines () + "Fill lines that are wider than the window width or `fill-column'." + (interactive) + (with-current-buffer mu4e~view-buffer + (save-excursion + (let ((inhibit-read-only t) + (width (window-width (get-buffer-window (current-buffer))))) + (save-restriction + (message-goto-body) + (while (not (eobp)) + (end-of-line) + (when (>= (current-column) (min fill-column width)) + (narrow-to-region (min (1+ (point)) (point-max)) + (point-at-bol)) + (let ((goback (point-marker))) + (fill-paragraph nil) + (goto-char (marker-position goback))) + (widen)) + (forward-line 1))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; attachment handling +(defun mu4e~view-get-attach-num (prompt msg &optional multi) + "Ask the user with PROMPT for an attachment number for MSG, and +ensure it is valid. The number is [1..n] for attachments +\[0..(n-1)] in the message. If MULTI is nil, return the number for +the attachment; otherwise (MULTI is non-nil), accept ranges of +attachment numbers, as per `mu4e-split-ranges-to-numbers', and +return the corresponding string." + (let* ((count (hash-table-count mu4e~view-attach-map)) (def)) + (when (zerop count) (mu4e-error "No attachments for this message")) + (if (not multi) + (if (= count 1) + (read-number (mu4e-format "%s: " prompt) 1) + (read-number (mu4e-format "%s (1-%d): " prompt count))) + (progn + (setq def (if (= count 1) "1" (format "1-%d" count))) + (read-string (mu4e-format "%s (default %s): " prompt def) + nil nil def))))) + +(defun mu4e~view-get-attach (msg attnum) + "Return the attachment plist in MSG corresponding to attachment +number ATTNUM." + (let* ((partid (gethash attnum mu4e~view-attach-map)) + (attach + (find-if + (lambda (part) + (eq (mu4e-message-part-field part :index) partid)) + (mu4e-message-field msg :parts)))) + (or attach (mu4e-error "Not a valid attachment")))) + + +(defun mu4e~view-request-attachment-path (fname path) + "Ask the user where to save FNAME (default is PATH/FNAME)." + (let ((fpath (expand-file-name + (read-file-name + (mu4e-format "Save as ") + path nil nil fname) path))) + (if (file-directory-p fpath) + (expand-file-name fname fpath) + fpath))) + +(defun mu4e~view-request-attachments-dir (path) + "Ask the user where to save multiple attachments (default is PATH)." + (let ((fpath (expand-file-name + (read-directory-name + (mu4e-format "Save in directory ") + path nil nil nil) path))) + (if (file-directory-p fpath) + fpath))) + +(defun mu4e-view-save-attachment-single (&optional msg attnum) + "Save attachment number ATTNUM from MSG. +If MSG is nil use the message returned by `message-at-point'. +If ATTNUM is nil ask for the attachment number." + (interactive) + (let* ((msg (or msg (mu4e-message-at-point))) + (attnum (or attnum + (mu4e~view-get-attach-num "Attachment to save" msg))) + (att (mu4e~view-get-attach msg attnum)) + (fname (plist-get att :name)) + (mtype (plist-get att :mime-type)) + (path (concat + (mu4e~get-attachment-dir fname mtype) "/")) + (index (plist-get att :index)) + (retry t) (fpath)) + (while retry + (setq fpath (mu4e~view-request-attachment-path fname path)) + (setq retry + (and (file-exists-p fpath) + (not (y-or-n-p (mu4e-format "Overwrite '%s'?" fpath)))))) + (mu4e~proc-extract + 'save (mu4e-message-field msg :docid) + index mu4e-decryption-policy fpath))) + + +(defun mu4e-view-save-attachment-multi (&optional msg) + "Offer to save multiple email attachments from the current message. +Default is to save all messages, [1..n], where n is the number of +attachments. You can type multiple values separated by space, e.g. + 1 3-6 8 +will save attachments 1,3,4,5,6 and 8. + +Furthermore, there is a shortcut \"a\" which so means all +attachments, but as this is the default, you may not need it." + (interactive) + (let* ((msg (or msg (mu4e-message-at-point))) + (attachstr (mu4e~view-get-attach-num + "Attachment number range (or 'a' for 'all')" msg t)) + (count (hash-table-count mu4e~view-attach-map)) + (attachnums (mu4e-split-ranges-to-numbers attachstr count))) + (if mu4e-save-multiple-attachments-without-asking + (let* ((path (concat (mu4e~get-attachment-dir) "/")) + (attachdir (mu4e~view-request-attachments-dir path))) + (dolist (num attachnums) + (let* ((att (mu4e~view-get-attach msg num)) + (fname (plist-get att :name)) + (index (plist-get att :index)) + (retry t) + fpath) + (while retry + (setq fpath (expand-file-name (concat attachdir fname) path)) + (setq retry + (and (file-exists-p fpath) + (not (y-or-n-p + (mu4e-format "Overwrite '%s'?" fpath)))))) + (mu4e~proc-extract + 'save (mu4e-message-field msg :docid) + index mu4e-decryption-policy fpath)))) + (dolist (num attachnums) + (mu4e-view-save-attachment-single msg num))))) + +(defun mu4e-view-save-attachment (&optional multi) + "Offer to save attachment(s). +If MULTI (prefix-argument) is nil, save a single one, otherwise, +offer to save a range of attachments." + (interactive "P") + (if multi + (mu4e-view-save-attachment-multi) + (mu4e-view-save-attachment-single))) + +(defun mu4e-view-open-attachment (&optional msg attnum) + "Open attachment number ATTNUM from MSG. +If MSG is nil use the message returned by `message-at-point'. +If ATTNUM is nil ask for the attachment number." + (interactive) + (let* ((msg (or msg (mu4e-message-at-point))) + (attnum (or attnum + (mu4e~view-get-attach-num "Attachment to open" msg))) + (att (or (mu4e~view-get-attach msg attnum))) + (index (plist-get att :index)) + (docid (mu4e-message-field msg :docid)) + (mimetype (plist-get att :mime-type))) + (if (and mimetype (string= mimetype "message/rfc822")) + ;; special handling for message-attachments; we open them in mu4e. we also + ;; send the docid as parameter (4th arg); we'll get this back from the + ;; server, and use it to determine the parent message (ie., the current + ;; message) when showing the embedded message/rfc822, and return to the + ;; current message when quitting that one. + (mu4e~view-temp-action docid index "mu4e" docid) + ;; otherwise, open with the default program (handled in mu-server + (mu4e~proc-extract 'open docid index mu4e-decryption-policy)))) + + +(defun mu4e~view-temp-action (docid index what &optional param) + "Open attachment INDEX for message with DOCID, and invoke ACTION." + (interactive) + (mu4e~proc-extract 'temp docid index mu4e-decryption-policy nil what param )) + +(defvar mu4e~view-open-with-hist nil "History list for the open-with argument.") + +(defun mu4e-view-open-attachment-with (msg attachnum &optional cmd) + "Open MSG's attachment ATTACHNUM with CMD. +If CMD is nil, ask user for it." + (interactive) + (let* ((att (mu4e~view-get-attach msg attachnum)) + (cmd (or cmd + (read-string + (mu4e-format "Shell command to open it with: ") + nil 'mu4e~view-open-with-hist))) + (index (plist-get att :index))) + (mu4e~view-temp-action + (mu4e-message-field msg :docid) index "open-with" cmd))) + +(defvar mu4e~view-pipe-hist nil + "History list for the pipe argument.") + +(defun mu4e-view-pipe-attachment (msg attachnum &optional pipecmd) + "Feed MSG's attachment ATTACHNUM through pipe PIPECMD. +If PIPECMD is nil, ask user for it." + (interactive) + (let* ((att (mu4e~view-get-attach msg attachnum)) + (pipecmd (or pipecmd + (read-string + (mu4e-format "Pipe: ") + nil + 'mu4e~view-pipe-hist))) + (index (plist-get att :index))) + (mu4e~view-temp-action + (mu4e-message-field msg :docid) index "pipe" pipecmd))) + + +(defun mu4e-view-open-attachment-emacs (msg attachnum) + "Open MSG's attachment ATTACHNUM in the current emacs instance." + (interactive) + (let* ((att (mu4e~view-get-attach msg attachnum)) + (index (plist-get att :index))) + (mu4e~view-temp-action (mu4e-message-field msg :docid) index "emacs"))) + +(defun mu4e-view-import-attachment-diary (msg attachnum) + "Open MSG's attachment ATTACHNUM in the current emacs instance." + (interactive) + (let* ((att (mu4e~view-get-attach msg attachnum)) + (index (plist-get att :index))) + (mu4e~view-temp-action (mu4e-message-field msg :docid) index "diary"))) + +(defun mu4e-view-attachment-action (&optional msg) + "Ask user what to do with attachments in MSG +If MSG is nil use the message returned by `message-at-point'. +The actions are specified in `mu4e-view-attachment-actions'." + (interactive) + (let* ((msg (or msg (mu4e-message-at-point))) + (actionfunc (mu4e-read-option + "Action on attachment: " + mu4e-view-attachment-actions)) + (attnum (mu4e~view-get-attach-num "Which attachment" msg))) + (when (and actionfunc attnum) + (funcall actionfunc msg attnum)))) + +;; handler-function to handle the response we get from the server when we +;; want to do something with one of the attachments. +(defun mu4e~view-temp-handler (path what docid param) + "Handler function for doing things with temp files (ie., +attachments) in response to a (mu4e~proc-extract 'temp ... )." + (cond + ((string= what "open-with") + ;; 'param' will be the program to open-with + (start-process "*mu4e-open-with-proc*" "*mu4e-open-with*" param path)) + ((string= what "pipe") + ;; 'param' will be the pipe command, path the infile for this + (mu4e-process-file-through-pipe path param)) + ;; if it's mu4e, it's some embedded message; 'param' may contain the docid + ;; of the parent message. + ((string= what "mu4e") + ;; remember the mapping path->docid, which maps the path of the embedded + ;; message to the docid of its parent + (puthash path docid mu4e~path-parent-docid-map) + (mu4e~proc-view-path path mu4e-view-show-images mu4e-decryption-policy)) + ((string= what "emacs") + (find-file path) + ;; make the buffer read-only since it usually does not make + ;; sense to edit the temp buffer; use C-x C-q if you insist... + (setq buffer-read-only t)) + ((string= what "diary") + (icalendar-import-file path diary-file)) + (t (mu4e-error "Unsupported action %S" what)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defun mu4e-view-mark-custom () + "Run some custom mark function." + (mu4e~view-in-headers-context + (mu4e-headers-mark-custom))) + +(defun mu4e~view-split-view-p () + "Return t if we're in split-view, nil otherwise." + (member mu4e-split-view '(horizontal vertical))) + +(defun mu4e-view-scroll-up-or-next () + "Scroll-up the current message. +If `mu4e-view-scroll-to-next' is non-nil, and we can't scroll-up +anymore, go the next message." + (interactive) + (condition-case nil + (scroll-up) + (error + (when mu4e-view-scroll-to-next + (mu4e-view-headers-next))))) + +(defun mu4e-scroll-up () + "Scroll text of selected window up one line." + (interactive) + (scroll-up 1)) + +(defun mu4e-scroll-down () + "Scroll text of selected window down one line." + (interactive) + (scroll-down 1)) + +(defun mu4e-view-unmark-all () + "If we're in split-view, unmark all messages. +Otherwise, warn user that unmarking only works in the header +list." + (interactive) + (if (mu4e~view-split-view-p) + (mu4e~view-in-headers-context (mu4e-mark-unmark-all)) + (mu4e-message "Unmarking needs to be done in the header list view"))) + +(defun mu4e-view-unmark () + "If we're in split-view, unmark message at point. +Otherwise, warn user that unmarking only works in the header +list." + (interactive) + (if (mu4e~view-split-view-p) + (mu4e-view-mark-for-unmark) + (mu4e-message "Unmarking needs to be done in the header list view"))) + + +(defmacro mu4e~view-defun-mark-for (mark) + "Define a function mu4e-view-mark-for-MARK." + (let ((funcname (intern (format "mu4e-view-mark-for-%s" mark))) + (docstring (format "Mark the current message for %s." mark))) + `(progn + (defun ,funcname () ,docstring + (interactive) + (mu4e~view-in-headers-context + (mu4e-headers-mark-and-next ',mark))) + (put ',funcname 'definition-name ',mark)))) + +(mu4e~view-defun-mark-for move) +(mu4e~view-defun-mark-for trash) +(mu4e~view-defun-mark-for refile) +(mu4e~view-defun-mark-for delete) +(mu4e~view-defun-mark-for flag) +(mu4e~view-defun-mark-for unflag) +(mu4e~view-defun-mark-for unmark) +(mu4e~view-defun-mark-for something) +(mu4e~view-defun-mark-for read) +(mu4e~view-defun-mark-for unread) + +(defun mu4e-view-marked-execute () + "Execute the marks." + (interactive) + (mu4e~view-in-headers-context + (mu4e-mark-execute-all))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; URL handling +(defun mu4e~view-get-urls-num (prompt &optional multi) + "Ask the user with PROMPT for an URL number for MSG, and ensure +it is valid. The number is [1..n] for URLs \[0..(n-1)] in the +message. If MULTI is nil, return the number for the URL; +otherwise (MULTI is non-nil), accept ranges of URL numbers, as +per `mu4e-split-ranges-to-numbers', and return the corresponding +string." + (let* ((count (hash-table-count mu4e~view-link-map)) (def)) + (when (zerop count) (mu4e-error "No links for this message")) + (if (not multi) + (if (= count 1) + (read-number (mu4e-format "%s: " prompt) 1) + (read-number (mu4e-format "%s (1-%d): " prompt count))) + (progn + (setq def (if (= count 1) "1" (format "1-%d" count))) + (read-string (mu4e-format "%s (default %s): " prompt def) + nil nil def))))) + +(defun mu4e-view-go-to-url (&optional multi) + "Offer to go to url(s). If MULTI (prefix-argument) is nil, go to +a single one, otherwise, offer to go to a range of urls." + (interactive "P") + (mu4e~view-handle-urls "URL to visit" + multi (lambda (url) (mu4e~view-browse-url-from-binding url)))) + +(defun mu4e-view-save-url (&optional multi) + "Offer to save urls(s) to the kill-ring. If +MULTI (prefix-argument) is nil, save a single one, otherwise, offer +to save a range of URLs." + (interactive "P") + (mu4e~view-handle-urls "URL to save" multi + (lambda (url) + (kill-new url) + (mu4e-message "Saved %s to the kill-ring" url)))) + +(defun mu4e-view-fetch-url (&optional multi) + "Offer to fetch (download) urls(s). If MULTI (prefix-argument) is nil, +download a single one, otherwise, offer to fetch a range of +URLs. The urls are fetched to `mu4e-attachment-dir'." + (interactive "P") + (mu4e~view-handle-urls "URL to fetch" multi + (lambda (url) + (let ((target (concat (mu4e~get-attachment-dir url) "/" + (file-name-nondirectory url)))) + (url-copy-file url target) + (mu4e-message "Fetched %s -> %s" url target))))) + +(defun mu4e~view-handle-urls (prompt multi urlfunc) + "If MULTI is nil, apply URLFUNC to a single uri, otherwise, apply +it to a range of uris. PROMPT is the query to present to the user." + (interactive "P") + (if multi + (mu4e~view-handle-multi-urls prompt urlfunc) + (mu4e~view-handle-single-url prompt urlfunc))) + +(defun mu4e~view-handle-single-url (prompt urlfunc &optional num) + "Apply URLFUNC to url NUM in the current message, prompting the +user with PROMPT." + (interactive) + (let* ((num (or num (mu4e~view-get-urls-num prompt))) + (url (gethash num mu4e~view-link-map))) + (unless url (mu4e-warn "Invalid number for URL")) + (funcall urlfunc url))) + +(defun mu4e~view-handle-multi-urls (prompt urlfunc) + "Apply URLFUNC to a a range of urls in the current message, +prompting the user with PROMPT. + +Default is to aplly it to all URLs, [1..n], where n is the number +of urls. You can type multiple values separated by space, e.g. 1 +3-6 8 will visit urls 1,3,4,5,6 and 8. + +Furthermore, there is a shortcut \"a\" which means all urls, but as +this is the default, you may not need it." + (interactive) + (let* ((linkstr (mu4e~view-get-urls-num + "URL number range (or 'a' for 'all')" t)) + (count (hash-table-count mu4e~view-link-map)) + (linknums (mu4e-split-ranges-to-numbers linkstr count))) + (dolist (num linknums) + (mu4e~view-handle-single-url prompt urlfunc num)))) + +(defun mu4e-view-for-each-uri (func) + "Execute FUNC (which receives a uri) for each uri in the current + message." + (maphash (lambda (num uri) (funcall func uri)) mu4e~view-link-map)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defconst mu4e~view-raw-buffer-name " *mu4e-raw-view*" + "*internal* Name for the raw message view buffer") + +(defun mu4e-view-raw-message () + "Display the raw contents of message at point in a new buffer." + (interactive) + (let ((path (mu4e-message-field-at-point :path)) + (buf (get-buffer-create mu4e~view-raw-buffer-name))) + (unless (and path (file-readable-p path)) + (mu4e-error "Not a readable file: %S" path)) + (with-current-buffer buf + (let ((inhibit-read-only t)) + (erase-buffer) + (insert-file-contents path) + (view-mode) + (goto-char (point-min)))) + (switch-to-buffer buf))) + +(defun mu4e-view-pipe (cmd) + "Pipe the message at point through shell command CMD, and display +the results." + (interactive "sShell command: ") + (let ((path (mu4e-message-field (mu4e-message-at-point) :path))) + (mu4e-process-file-through-pipe path cmd))) + +(defconst mu4e~verify-buffer-name " *mu4e-verify*") + +(defun mu4e-view-verify-msg-popup (&optional msg) + "Pop-up a little signature verification window for (optional) MSG +or message-at-point." + (interactive) + (let* ((msg (or msg (mu4e-message-at-point))) + (path (mu4e-message-field msg :path)) + (cmd (format "%s verify --verbose %s %s" + mu4e-mu-binary + (shell-quote-argument path) + (if mu4e-decryption-policy + "--decrypt --use-agent" + ""))) + (output (shell-command-to-string cmd)) + ;; create a new one + (buf (get-buffer-create mu4e~verify-buffer-name)) + (win (or (get-buffer-window buf) + (split-window-vertically (- (window-height) 6))))) + (with-selected-window win + (let ((inhibit-read-only t)) + ;; (set-window-dedicated-p win t) + (switch-to-buffer buf) + (erase-buffer) + (insert output) + (goto-char (point-min)) + (local-set-key "q" 'kill-buffer-and-window)) + (setq buffer-read-only t)) + (select-window win))) + + +(defun mu4e~view-quit-buffer () + "Quit the mu4e-view buffer. +This is a rather complex function, to ensure we don't disturb +other windows." + (interactive) + (unless (eq major-mode 'mu4e-view-mode) + (mu4e-error "Must be in mu4e-view-mode (%S)" major-mode)) + (let ((curbuf (current-buffer)) (curwin (selected-window)) + (headers-win)) + (walk-windows + (lambda (win) + ;; check whether the headers buffer window is visible + (when (eq mu4e~view-headers-buffer (window-buffer win)) + (setq headers-win win)) + ;; and kill any _other_ (non-selected) window that shows the current + ;; buffer + (when + (and + (eq curbuf (window-buffer win)) ;; does win show curbuf? + (not (eq curwin win)) ;; but it's not the curwin? + (not (one-window-p))) ;; and not the last one on the frame? + (delete-window win)))) ;; delete it! + ;; now, all *other* windows should be gone. + ;; if the headers view is also visible, kill ourselves + window; otherwise + ;; switch to the headers view + (if (window-live-p headers-win) + ;; headers are visible + (progn + (kill-buffer-and-window) ;; kill the view win + (setq mu4e~headers-view-win nil) + (select-window headers-win)) ;; and switch to the headers win... + ;; headers are not visible... + (progn + (kill-buffer) + (setq mu4e~headers-view-win nil) + (when (buffer-live-p mu4e~view-headers-buffer) + (switch-to-buffer mu4e~view-headers-buffer)))))) + +(provide 'mu4e-view) +;; end of mu4e-view diff --git a/_spacemacs.d/local/mu4e/mu4e.el b/_spacemacs.d/local/mu4e/mu4e.el new file mode 100644 index 0000000..fd01875 --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e.el @@ -0,0 +1,93 @@ +;;; mu4e.el --- part of mu4e, the mu mail user agent +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Keywords: email +;; Version: 0.0 + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Code: + +(eval-when-compile (require 'cl)) + +(require 'mu4e-meta) ;; autogenerated file with metadata (version etc.) +(require 'mu4e-headers) ;; headers view +(require 'mu4e-view) ;; message view +(require 'mu4e-main) ;; main screen +(require 'mu4e-compose) ;; message composition / sending +(require 'mu4e-proc) ;; communication with backend +(require 'mu4e-utils) ;; utility functions +(require 'mu4e-context) ;; support for contexts +(require 'mu4e-speedbar) ;; support for speedbar + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; register our handler functions; these connect server messages to functions +;; to handle them. +;; +;; +;; these are all defined in mu4e-headers +(setq mu4e-update-func 'mu4e~headers-update-handler) +(setq mu4e-header-func 'mu4e~headers-header-handler) +(setq mu4e-found-func 'mu4e~headers-found-handler) +(setq mu4e-view-func 'mu4e~headers-view-handler) +(setq mu4e-remove-func 'mu4e~headers-remove-handler) +(setq mu4e-erase-func 'mu4e~headers-clear) + +;; these ones are defined in mu4e-utils +(setq mu4e-info-func 'mu4e-info-handler) +(setq mu4e-error-func 'mu4e-error-handler) +;; note: mu4e-utils also dynamically (temporarily) +;; registers mu4e-pong func + +;; this one is defined in mu4e-compose +(setq mu4e-compose-func 'mu4e~compose-handler) +;; note: mu4e-compose.el dynamically registers mu4e-sent-func +;; we don't do that here, because it's only a local (temporary) +;; handler + +;; this one is defined in mu4e-view +(setq mu4e-temp-func 'mu4e~view-temp-handler) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;###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." + (interactive "P") + ;; start mu4e, then show the main view + (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))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(provide 'mu4e) + +;;; mu4e.el ends here diff --git a/_spacemacs.d/local/mu4e/org-mu4e.el b/_spacemacs.d/local/mu4e/org-mu4e.el new file mode 100644 index 0000000..b9e9688 --- /dev/null +++ b/_spacemacs.d/local/mu4e/org-mu4e.el @@ -0,0 +1,323 @@ +;;; org-mu4e -- Support for links to mu4e messages/queries from within +;;; org-mode, and for writing message in org-mode, sending them as +;;; rich-text +;; +;; Copyright (C) 2012-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Keywords: outlines, hypermedia, calendar, mail +;; Version: 0.0 + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of 1the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Code: + + +;; The expect version here is org 8.x + + +;; the 'noerror is just to make sure bytecompilations does not break... +;; FIXME: find a better solution +(require 'org nil 'noerror) +(require 'org-exp nil 'noerror) + +(eval-when-compile (require 'cl)) +(require 'mu4e) + +(defgroup org-mu4e nil + "Settings for the org-mode related functionality in mu4e." + :group 'mu4e + :group 'org) + +(defvar org-mu4e-link-query-in-headers-mode nil + "If non-nil, `org-store-link' in `mu4e-headers-mode' links to the +the current query; otherwise, it links to the message at point.") + +(defcustom org-mu4e-link-desc-func + (lambda (msg) (or (plist-get msg :subject) "No subject")) + "Function that takes a msg and returns a string for the +description part of an org-mode link. + +Example usage: + + (defun my-link-descr (msg) + (let ((subject (or (plist-get msg :subject) + \"No subject\")) + (date (or (format-time-string mu4e-headers-date-format + (mu4e-msg-field msg :date)) + \"No date\"))) + (concat subject \" \" date))) + + (setq org-mu4e-link-desc-func 'my-link-descr)" + :type 'function + :group 'org-mu4e) + +(defun org-mu4e-store-link () + "Store a link to a mu4e query or message." + (when (member major-mode '(mu4e-headers-mode mu4e-view-mode)) + (if (and (eq major-mode 'mu4e-headers-mode) + org-mu4e-link-query-in-headers-mode) + ;; storing links to queries + (let* ((query (mu4e-last-query)) + desc link) + (org-store-link-props :type "mu4e" :query query) + (setq + desc (concat "mu4e:query:" query) + link desc) + (org-add-link-props :link link :description desc) + link) + ;; storing links to messages + (let* ((msg (mu4e-message-at-point)) + (msgid (or (plist-get msg :message-id) "<none>")) + (from (or (plist-get msg :from) '(("none" . "none")))) + (fromname (car (car from))) + (fromaddress (cdr (car from))) + (to (or (plist-get msg :to) '(("none" . "none")))) + (toname (car (car to))) + (toaddress (cdr (car to))) + (fromto (if (mu4e-user-mail-address-p fromaddress) + (format "to %s <%s>" toname toaddress) + (format "from %s <%s>" fromname fromaddress))) + (date (plist-get msg :date)) + (date-ts (format-time-string (org-time-stamp-format t) date)) + (date-ts-ia (format-time-string (org-time-stamp-format t t) date)) + (subject (or (plist-get msg :subject) "<none>")) + link) + (org-store-link-props :type "mu4e" :link link + :message-id msgid) + (setq link (concat "mu4e:msgid:" msgid)) + (org-add-link-props :link link + :to (format "%s <%s>" toname toaddress) + :toname toname + :toaddress toaddress + :from (format "%s <%s>" fromname fromaddress) + :fromname fromname + :fromaddress fromaddress + :fromto fromto + :date date-ts-ia + :date-timestamp date-ts + :date-timestamp-inactive date-ts-ia + :subject subject + :description (funcall org-mu4e-link-desc-func msg)) + link)))) + +(org-add-link-type "mu4e" 'org-mu4e-open) +(add-hook 'org-store-link-functions 'org-mu4e-store-link) + +(defun org-mu4e-open (path) + "Open the mu4e message (for paths starting with 'msgid:') or run +the query (for paths starting with 'query:')." + (cond + ((string-match "^msgid:\\(.+\\)" path) + (mu4e-view-message-with-message-id (match-string 1 path))) + ((string-match "^query:\\(.+\\)" path) + (mu4e-headers-search (match-string 1 path) current-prefix-arg)) + (t (mu4e-error "mu4e: unrecognized link type '%s'" path)))) + +(defun org-mu4e-store-and-capture () + "Store a link to the current message or query (depending on +`org-mu4e-link-query-in-headers-mode', and capture it with +org-mode)." + (interactive) + (org-mu4e-store-link) + (org-capture)) + + + +;;; editing with org-mode ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; below, some functions for the org->html conversion +;; based on / inspired by Eric Schulte's org-mime.el +;; Homepage: http://orgmode.org/worg/org-contrib/org-mime.php +;; +;; EXPERIMENTAL +(defun org~mu4e-mime-file (ext path id) + "Create a file for an attachment." + (format (concat "<#part type=\"%s\" filename=\"%s\" " + "disposition=inline id=\"<%s>\">\n<#/part>\n") + ext path id)) + +(defun org~mu4e-mime-multipart (plain html &optional images) + "Create a multipart/alternative with text/plain and text/html alternatives. +If the html portion of the message includes images, wrap the html +and images in a multipart/related part." + (concat "<#multipart type=alternative><#part type=text/plain>" + plain + (when images "<#multipart type=related>") + "<#part type=text/html>" + html + images + (when images "<#/multipart>\n") + "<#/multipart>\n")) + +(defun org~mu4e-mime-replace-images (str current-file) + "Replace images in html files with cid links." + (let (html-images) + (cons + (replace-regexp-in-string ;; replace images in html + "src=\"\\([^\"]+\\)\"" + (lambda (text) + (format + "src=\"cid:%s\"" + (let* ((url (and (string-match "src=\"\\([^\"]+\\)\"" text) + (match-string 1 text))) + (path (expand-file-name + url (file-name-directory current-file))) + (ext (file-name-extension path)) + (id (replace-regexp-in-string "[\/\\\\]" "_" path))) + (add-to-list 'html-images + (org~mu4e-mime-file + (concat "image/" ext) path id)) + id))) + str) + html-images))) + +(defun org~mu4e-mime-convert-to-html () + "Convert the current body to html." + (unless (fboundp 'org-export-string-as) + (mu4e-error "require function 'org-export-string-as not found.")) + (unless (executable-find "dvipng") + (mu4e-error "Required program dvipng not found")) + (let* ((begin + (save-excursion + (goto-char (point-min)) + (search-forward mail-header-separator))) + (end (point-max)) + (raw-body (buffer-substring begin end)) + (tmp-file (make-temp-name (expand-file-name "mail" + temporary-file-directory))) + ;; because we probably don't want to skip part of our mail + (org-export-skip-text-before-1st-heading nil) + ;; because we probably don't want to export a huge style file + (org-export-htmlize-output-type 'inline-css) + ;; makes the replies with ">"s look nicer + (org-export-preserve-breaks t) + ;; dvipng for inline latex because MathJax doesn't work in mail + (org-export-with-LaTeX-fragments 'dvipng) + ;; to hold attachments for inline html images + (html-and-images + (org~mu4e-mime-replace-images + (org-export-string-as raw-body 'html t) + tmp-file)) + (html-images (cdr html-and-images)) + (html (car html-and-images))) + (delete-region begin end) + (save-excursion + (goto-char begin) + (newline) + (insert (org~mu4e-mime-multipart + raw-body html (mapconcat 'identity html-images "\n")))))) + +;; next some functions to make the org/mu4e-compose-mode switch as smooth as +;; possible. +(defun org~mu4e-mime-decorate-headers () + "Make the headers visually distinctive (org-mode)." + (save-excursion + (goto-char (point-min)) + (let* ((eoh (when (search-forward mail-header-separator) + (match-end 0))) + (olay (make-overlay (point-min) eoh))) + (when olay + (overlay-put olay 'face 'font-lock-comment-face))))) + +(defun org~mu4e-mime-undecorate-headers () + "Don't make the headers visually distinctive. +\(well, mu4e-compose-mode will take care of that)." + (save-excursion + (goto-char (point-min)) + (let* ((eoh (when (search-forward mail-header-separator) + (match-end 0)))) + (remove-overlays (point-min) eoh)))) + +(defvar org-mu4e-convert-to-html nil + "Whether to do automatic org-mode => html conversion when sending messages.") + +(defun org~mu4e-mime-convert-to-html-maybe () + "Convert to html if `org-mu4e-convert-to-html' is non-nil. +This function is called when sending a message (from +`message-send-hook') and, if non-nil, will send the message as +the rich-text version of the what is assumed to be an org-mode +body." + (when org-mu4e-convert-to-html + (mu4e-message "Converting to html") + (org~mu4e-mime-convert-to-html))) + +(defun org~mu4e-mime-switch-headers-or-body () + "Switch the buffer to either mu4e-compose-mode (when in headers) +or org-mode (when in the body)." + (interactive) + (let* ((sepapoint + (save-excursion + (goto-char (point-min)) + (search-forward-regexp mail-header-separator nil t)))) + ;; only do stuff when the sepapoint exist; note that after sending the + ;; message, this function maybe called on a message with the sepapoint + ;; stripped. This is why we don't use `message-point-in-header'. + (when sepapoint + (cond + ;; we're in the body, but in mu4e-compose-mode? + ;; if so, switch to org-mode + ((and (> (point) sepapoint) (eq major-mode 'mu4e-compose-mode)) + (org-mode) + (add-hook 'before-save-hook + (lambda () + (mu4e-error "Switch to mu4e-compose-mode (M-m) before saving.")) + nil t) + (org~mu4e-mime-decorate-headers) + (local-set-key (kbd "M-m") + (lambda (keyseq) + (interactive "kEnter mu4e-compose-mode key sequence: ") + (let ((func (lookup-key mu4e-compose-mode-map keyseq))) + (if func (funcall func) (insert keyseq)))))) + ;; we're in the headers, but in org-mode? + ;; if so, switch to mu4e-compose-mode + ((and (<= (point) sepapoint) (eq major-mode 'org-mode)) + (org~mu4e-mime-undecorate-headers) + (mu4e-compose-mode) + (add-hook 'message-send-hook 'org~mu4e-mime-convert-to-html-maybe nil t))) + ;; and add the hook + (add-hook 'post-command-hook 'org~mu4e-mime-switch-headers-or-body t t)))) + + +(defun org-mu4e-compose-org-mode () + "Pseudo-Minor mode for mu4e-compose-mode, to edit the message +body using org-mode." + (interactive) + (unless (member major-mode '(org-mode mu4e-compose-mode)) + (mu4e-error "Need org-mode or mu4e-compose-mode")) + ;; we can check if we're already in org-mu4e-compose-mode by checking if the + ;; post-command-hook is set; hackish...but a buffer-local variable does not + ;; seem to survive buffer switching + (if (not (member 'org~mu4e-mime-switch-headers-or-body post-command-hook)) + (progn + (org~mu4e-mime-switch-headers-or-body) + (mu4e-message + (concat + "org-mu4e-compose-org-mode enabled; " + "press M-m before issuing message-mode commands"))) + (progn ;; otherwise, remove crap + (remove-hook 'post-command-hook 'org~mu4e-mime-switch-headers-or-body t) + (org~mu4e-mime-undecorate-headers) ;; shut off org-mode stuff + (mu4e-compose-mode) + (message "org-mu4e-compose-org-mode disabled")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(provide 'org-mu4e) + +;;; org-mu4e.el ends here diff --git a/_spacemacs.d/local/mu4e/org-old-mu4e.el b/_spacemacs.d/local/mu4e/org-old-mu4e.el new file mode 100644 index 0000000..d73a780 --- /dev/null +++ b/_spacemacs.d/local/mu4e/org-old-mu4e.el @@ -0,0 +1,289 @@ +;;; org-mu4e -- Support for links to mu4e messages/queries from within org-mode, +;;; and for writing message in org-mode, sending them as rich-text +;; +;; Copyright (C) 2012-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Keywords: outlines, hypermedia, calendar, mail +;; Version: 0.0 + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Code: + +;; This should support org-mode versions 6.x. 7.x + +;; the 'noerror is just to make sure bytecompilations does not break... +;; FIXME: find a better solution +(require 'org nil 'noerror) +(require 'org-exp nil 'noerror) + +(eval-when-compile (require 'cl)) +(require 'mu4e) + +(defgroup org-mu4e nil + "Settings for the org-mode related functionality in mu4e." + :group 'mu4e + :group 'org) + +(defcustom org-mu4e-link-desc-func + (lambda (msg) (or (plist-get msg :subject) "No subject")) + "Function that takes a msg and returns a string for the +description part of an org-mode link. + +Example usage: + + (defun my-link-descr (msg) + (let ((subject (or (plist-get msg :subject) + \"No subject\")) + (date (or (format-time-string mu4e-headers-date-format + (mu4e-msg-field msg :date)) + \"No date\"))) + (concat subject \" \" date))) + + (setq org-mu4e-link-desc-func 'my-link-descr)" + :type 'function + :group 'org-mu4e) + +(defun org-mu4e-store-link () + "Store a link to a mu4e query or message." + (cond + ;; storing links to queries + ((eq major-mode 'mu4e-headers-mode) + (let* ((query (mu4e-last-query)) + desc link) + (org-store-link-props :type "mu4e" :query query) + (setq + desc (concat "mu4e:query:" query) + link desc) + (org-add-link-props :link link :description desc) + link)) + ;; storing links to messages + ((eq major-mode 'mu4e-view-mode) + (let* ((msg (mu4e-message-at-point)) + (msgid (or (plist-get msg :message-id) "<none>")) + link) + (org-store-link-props :type "mu4e" :link link + :message-id msgid) + (setq link (concat "mu4e:msgid:" msgid)) + (org-add-link-props :link link + :description (funcall org-mu4e-link-desc-func msg)) + link)))) + +(org-add-link-type "mu4e" 'org-mu4e-open) +(add-hook 'org-store-link-functions 'org-mu4e-store-link) + +(defun org-mu4e-open (path) + "Open the mu4e message (for paths starting with 'msgid:') or run +the query (for paths starting with 'query:')." + (require 'mu4e) + (cond + ((string-match "^msgid:\\(.+\\)" path) + (mu4e-view-message-with-message-id (match-string 1 path))) + ((string-match "^query:\\(.+\\)" path) + (mu4e-headers-search (match-string 1 path) current-prefix-arg)) + (t (mu4e-error "mu4e: unrecognized link type '%s'" path)))) + + + + +;;; editing with org-mode ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; below, some functions for the org->html conversion +;; based on / inspired by Eric Schulte's org-mime.el +;; Homepage: http://orgmode.org/worg/org-contrib/org-mime.php +;; +;; EXPERIMENTAL +(defun org~mu4e-mime-file (ext path id) + "Create a file for an attachment." + (format (concat "<#part type=\"%s\" filename=\"%s\" " + "disposition=inline id=\"<%s>\">\n<#/part>\n") + ext path id)) + +(defun org~mu4e-mime-multipart (plain html &optional images) + "Create a multipart/alternative with text/plain and text/html alternatives. +If the html portion of the message includes images, wrap the html +and images in a multipart/related part." + (concat "<#multipart type=alternative><#part type=text/plain>" + plain + (when images "<#multipart type=related>") + "<#part type=text/html>" + html + images + (when images "<#/multipart>\n") + "<#/multipart>\n")) + +(defun org~mu4e-mime-replace-images (str current-file) + "Replace images in html files with cid links." + (let (html-images) + (cons + (replace-regexp-in-string ;; replace images in html + "src=\"\\([^\"]+\\)\"" + (lambda (text) + (format + "src=\"cid:%s\"" + (let* ((url (and (string-match "src=\"\\([^\"]+\\)\"" text) + (match-string 1 text))) + (path (expand-file-name + url (file-name-directory current-file))) + (ext (file-name-extension path)) + (id (replace-regexp-in-string "[\/\\\\]" "_" path))) + (add-to-list 'html-images + (org~mu4e-mime-file + (concat "image/" ext) path id)) + id))) + str) + html-images))) + +(defun org~mu4e-mime-convert-to-html () + "Convert the current body to html." + (unless (fboundp 'org-export-string) + (mu4e-error "require function 'org-export-string not found.")) + (unless (executable-find "dvipng") + (mu4e-error "Required program dvipng not found")) + (let* ((begin + (save-excursion + (goto-char (point-min)) + (search-forward mail-header-separator))) + (end (point-max)) + (raw-body (buffer-substring begin end)) + (tmp-file (make-temp-name (expand-file-name "mail" + temporary-file-directory))) + (body (org-export-string raw-body 'org + (file-name-directory tmp-file))) + ;; because we probably don't want to skip part of our mail + (org-export-skip-text-before-1st-heading nil) + ;; because we probably don't want to export a huge style file + (org-export-htmlize-output-type 'inline-css) + ;; makes the replies with ">"s look nicer + (org-export-preserve-breaks t) + ;; dvipng for inline latex because MathJax doesn't work in mail + (org-export-with-LaTeX-fragments 'dvipng) + ;; to hold attachments for inline html images + (html-and-images + (org~mu4e-mime-replace-images + (org-export-string raw-body 'html + (file-name-directory tmp-file)) + tmp-file)) + (html-images (cdr html-and-images)) + (html (car html-and-images))) + (delete-region begin end) + (save-excursion + (goto-char begin) + (newline) + (insert (org~mu4e-mime-multipart + body html (mapconcat 'identity html-images "\n")))))) + +;; next some functions to make the org/mu4e-compose-mode switch as smooth as +;; possible. +(defun org~mu4e-mime-decorate-headers () + "Make the headers visually distinctive (org-mode)." + (save-excursion + (goto-char (point-min)) + (let* ((eoh (when (search-forward mail-header-separator) + (match-end 0))) + (olay (make-overlay (point-min) eoh))) + (when olay + (overlay-put olay 'face 'font-lock-comment-face))))) + +(defun org~mu4e-mime-undecorate-headers () + "Don't make the headers visually distinctive. +\(well, mu4e-compose-mode will take care of that)." + (save-excursion + (goto-char (point-min)) + (let* ((eoh (when (search-forward mail-header-separator) + (match-end 0)))) + (remove-overlays (point-min) eoh)))) + +(defvar org-mu4e-convert-to-html nil + "Whether to do automatic org-mode => html conversion when sending messages.") + +(defun org~mu4e-mime-convert-to-html-maybe () + "Convert to html if `org-mu4e-convert-to-html' is non-nil. +This function is called when sending a message (from +`message-send-hook') and, if non-nil, will send the message as +the rich-text version of the what is assumed to be an org-mode +body." + (when org-mu4e-convert-to-html + (mu4e-message "Converting to html") + (org~mu4e-mime-convert-to-html))) + +(defun org~mu4e-mime-switch-headers-or-body () + "Switch the buffer to either mu4e-compose-mode (when in headers) +or org-mode (when in the body)." + (interactive) + (let* ((sepapoint + (save-excursion + (goto-char (point-min)) + (search-forward-regexp mail-header-separator nil t)))) + ;; only do stuff when the sepapoint exist; note that after sending the + ;; message, this function maybe called on a message with the sepapoint + ;; stripped. This is why we don't use `message-point-in-header'. + (when sepapoint + (cond + ;; we're in the body, but in mu4e-compose-mode? + ;; if so, switch to org-mode + ((and (> (point) sepapoint) (eq major-mode 'mu4e-compose-mode)) + (org-mode) + (add-hook 'before-save-hook + (lambda () + (mu4e-error "Switch to mu4e-compose-mode (M-m) before saving.")) + nil t) + (org~mu4e-mime-decorate-headers) + (local-set-key (kbd "M-m") + (lambda (keyseq) + (interactive "kEnter mu4e-compose-mode key sequence: ") + (let ((func (lookup-key mu4e-compose-mode-map keyseq))) + (if func (funcall func) (insert keyseq)))))) + ;; we're in the headers, but in org-mode? + ;; if so, switch to mu4e-compose-mode + ((and (<= (point) sepapoint) (eq major-mode 'org-mode)) + (org~mu4e-mime-undecorate-headers) + (mu4e-compose-mode) + (add-hook 'message-send-hook 'org~mu4e-mime-convert-to-html-maybe nil t))) + ;; and add the hook + (add-hook 'post-command-hook 'org~mu4e-mime-switch-headers-or-body t t)))) + + +(defun org-mu4e-compose-org-mode () + "Pseudo-Minor mode for mu4e-compose-mode, to edit the message +body using org-mode." + (interactive) + (unless (member major-mode '(org-mode mu4e-compose-mode)) + (mu4e-error "Need org-mode or mu4e-compose-mode")) + ;; we can check if we're already in org-mu4e-compose-mode by checking if the + ;; post-command-hook is set; hackish...but a buffer-local variable does not + ;; seem to survive buffer switching + (if (not (member 'org~mu4e-mime-switch-headers-or-body post-command-hook)) + (progn + (org~mu4e-mime-switch-headers-or-body) + (mu4e-message + (concat + "org-mu4e-compose-org-mode enabled; " + "press M-m before issuing message-mode commands"))) + (progn ;; otherwise, remove crap + (remove-hook 'post-command-hook 'org~mu4e-mime-switch-headers-or-body t) + (org~mu4e-mime-undecorate-headers) ;; shut off org-mode stuff + (mu4e-compose-mode) + (message "org-mu4e-compose-org-mode disabled")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(provide 'org-mu4e) + +;;; org-mu4e.el ends here |