mirror of
https://github.com/djcb/mu.git
synced 2024-06-30 08:01:07 +02:00
Instead of a multi-step process to display an unread message (ie. get the original, notice it's unread, then update it, replace the message with update one etc.), we now handle that in the (view /./..) command on the server side. Simplifies things, and is faster (which could be noticeable, esp. if e.g. signature verification is part of the process)
1793 lines
73 KiB
EmacsLisp
1793 lines
73 KiB
EmacsLisp
;;; mu4e-view.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
|
||
|
||
;; Copyright (C) 2011-2020 Dirk-Jan C. Binnema
|
||
|
||
;; Author: Dirk-Jan C. Binnema <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 'cl-lib)
|
||
(require 'mu4e-utils) ;; utility functions
|
||
(require 'mu4e-vars)
|
||
(require 'mu4e-mark)
|
||
(require 'mu4e-proc)
|
||
(require 'mu4e-compose)
|
||
(require 'mu4e-actions)
|
||
(require 'mu4e-message)
|
||
|
||
(eval-when-compile (require 'gnus-art))
|
||
|
||
(require 'comint)
|
||
(require 'browse-url)
|
||
(require 'button)
|
||
(require 'epa)
|
||
(require 'epg)
|
||
(require 'thingatpt)
|
||
(require 'calendar)
|
||
|
||
(declare-function mu4e-view-mode "mu4e-view")
|
||
(defvar gnus-icalendar-additional-identities)
|
||
(defvar mu4e~headers-view-win)
|
||
|
||
;;; Options
|
||
|
||
(defgroup mu4e-view nil
|
||
"Settings for the message view."
|
||
:group 'mu4e)
|
||
|
||
(defcustom mu4e-view-use-gnus nil
|
||
"Whether to (experimentally) use Gnus' article view.
|
||
\(instead of mu4e's internal viewer)."
|
||
:type 'boolean
|
||
:group 'mu4e-view)
|
||
|
||
(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.
|
||
Otherwise, just show 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."
|
||
:type 'integer
|
||
:group 'mu4e-view)
|
||
|
||
(defcustom mu4e-view-image-max-height 600
|
||
"The maximum height for images to display.
|
||
This is only effective if you're using an Emacs with Imagemagick
|
||
support, and `mu4e-view-show-images' is non-nil."
|
||
:type 'integer
|
||
:group 'mu4e-view)
|
||
|
||
(defcustom mu4e-view-scroll-to-next t
|
||
"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."
|
||
:type 'boolean
|
||
:group 'mu4e-view)
|
||
|
||
|
||
(defcustom mu4e-save-multiple-attachments-without-asking nil
|
||
"If non-nil, saving multiple attachments asks once for a
|
||
directory and saves all attachments in the chosen directory."
|
||
:type 'boolean
|
||
:group 'mu4e-view)
|
||
|
||
(defcustom 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 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 a message plist as an argument.
|
||
|
||
The first letter of NAME is used as a shortcut character."
|
||
:group 'mu4e-view
|
||
:type '(alist :key-type string :value-type function))
|
||
|
||
(defcustom mu4e-view-attachment-assoc nil
|
||
"Alist of (EXTENSION . PROGRAM).
|
||
Specify which PROGRAM to use to open attachment with EXTENSION.
|
||
Args EXTENSION and PROGRAM should be specified as strings."
|
||
:group 'mu4e-view
|
||
:type '(alist :key-type string :value-type string))
|
||
|
||
(defcustom mu4e-view-attachment-actions
|
||
'( ("ssave" . mu4e-view-save-attachment-single)
|
||
("Ssave multi" . mu4e-view-save-attachment-multi)
|
||
("wopen-with" . mu4e-view-open-attachment-with)
|
||
("ein-emacs" . mu4e-view-open-attachment-emacs)
|
||
("dimport-in-diary" . mu4e-view-import-attachment-diary)
|
||
("kimport-public-key" . mu4e-view-import-public-key)
|
||
("|pipe" . mu4e-view-pipe-attachment))
|
||
"List of actions to perform on message attachments.
|
||
The actions are cons-cells of the form:
|
||
(NAME . FUNC)
|
||
where:
|
||
* NAME is the name of the action (e.g. \"Count lines\")
|
||
* FUNC is a function which receives two arguments: the message
|
||
plist and the attachment number.
|
||
The first letter of NAME is used as a shortcut character."
|
||
:group 'mu4e-view
|
||
:type '(alist :key-type string :value-type function))
|
||
|
||
;;; Keymaps
|
||
|
||
(defvar mu4e-view-header-field-keymap
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map [mouse-1] 'mu4e~view-header-field-fold)
|
||
(define-key map (kbd "TAB") 'mu4e~view-header-field-fold)
|
||
map)
|
||
"Keymap used for header fields.")
|
||
|
||
(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 \"Attachments\" header field.")
|
||
|
||
;;; Variables
|
||
|
||
;; It's useful to have the current view message available to
|
||
;; `mu4e-view-mode-hooks' functions, and we set up this variable
|
||
;; before calling `mu4e-view-mode'. However, changing the major mode
|
||
;; clobbers any local variables. Work around that by declaring the
|
||
;; variable permanent-local.
|
||
(defvar-local mu4e~view-message nil
|
||
"The message being viewed in view mode.")
|
||
(put 'mu4e~view-message 'permanent-local t)
|
||
|
||
(defvar mu4e-view-fill-headers t
|
||
"If non-nil, automatically fill the headers when viewing them.")
|
||
|
||
(defvar mu4e~view-cited-hidden nil "Whether cited lines are hidden.")
|
||
(put 'mu4e~view-cited-hidden 'permanent-local t)
|
||
|
||
(defvar mu4e~view-link-map nil
|
||
"A map of some number->url so we can jump to url by number.")
|
||
(put 'mu4e~view-link-map 'permanent-local t)
|
||
|
||
(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.")
|
||
(put 'mu4e~path-parent-docid-map 'permanent-local t)
|
||
|
||
(defvar mu4e~view-attach-map nil
|
||
"A mapping of user-visible attachment number to the actual part index.")
|
||
(put 'mu4e~view-attach-map 'permanent-local t)
|
||
|
||
(defvar mu4e~view-rendering nil)
|
||
|
||
(defvar mu4e~view-html-text nil
|
||
"Should we prefer html or text just this once? A symbol `text'
|
||
or `html' or nil.")
|
||
|
||
;;; Main
|
||
|
||
(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)))
|
||
(cl-case field
|
||
(:subject (mu4e~view-construct-header field fieldval))
|
||
(:path (mu4e~view-construct-header field fieldval))
|
||
(:maildir (mu4e~view-construct-header field fieldval))
|
||
(:user-agent (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* ((prefer-html
|
||
(cond
|
||
((eq mu4e~view-html-text 'html) t)
|
||
((eq mu4e~view-html-text 'text) nil)
|
||
(t mu4e-view-prefer-html)))
|
||
(body (mu4e-message-body-text msg prefer-html)))
|
||
(setq mu4e~view-html-text nil)
|
||
(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)
|
||
"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.
|
||
|
||
Depending on the value of `mu4e-view-use-gnus', either use mu4e's
|
||
internal display mode, or a display mode based on Gnus'
|
||
article-mode."
|
||
(mu4e~headers-update-handler msg nil nil);; update headers, if necessary.
|
||
|
||
(mu4e~view-define-mode)
|
||
|
||
;; XXX(djcb): only called for the side-effect of setting up
|
||
;; `mu4e~view-attach-map'. Instead, we should split that function
|
||
;; into setting up the map, and actually producing the header.
|
||
(mu4e~view-construct-attachments-header msg)
|
||
|
||
;; When MSG is unread, mu4e~view-mark-as-read-maybe will trigger
|
||
;; another call to mu4e-view (via mu4e~headers-update-handler as
|
||
;; the reply handler to mu4e~proc-move)
|
||
(if mu4e-view-use-gnus
|
||
(mu4e~view-gnus msg)
|
||
(mu4e~view-internal msg)))
|
||
|
||
(defun mu4e~view-internal (msg)
|
||
"Display MSG using mu4e's internal view mode."
|
||
(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))))
|
||
(with-current-buffer buf
|
||
(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)
|
||
(when (not embedded) (setq mu4e~view-message msg))
|
||
(mu4e-view-mode)
|
||
(when embedded (local-set-key "q" 'kill-buffer-and-window)))
|
||
(switch-to-buffer buf))))
|
||
|
||
(defun mu4e~view-gnus (msg)
|
||
"View MSG using Gnus' article mode. Experimental."
|
||
(require 'gnus-art)
|
||
(let ((path (mu4e-message-field msg :path))
|
||
(inhibit-read-only t)
|
||
;; support signature verification
|
||
(mm-verify-option 'known)
|
||
(mm-decrypt-option 'known)
|
||
(gnus-article-emulate-mime t)
|
||
(gnus-buttonized-mime-types (append (list "multipart/signed"
|
||
"multipart/encrypted")
|
||
gnus-buttonized-mime-types)))
|
||
(switch-to-buffer (get-buffer-create mu4e~view-buffer-name))
|
||
(erase-buffer)
|
||
(insert-file-contents-literally path)
|
||
(unless (message-fetch-field "Content-Type" t)
|
||
;; For example, for messages in `mu4e-drafts-folder'
|
||
(let ((coding (or (default-value 'buffer-file-coding-system)
|
||
'prefer-utf-8)))
|
||
(recode-region (point-min) (point-max) coding 'no-conversion)))
|
||
(setq
|
||
gnus-summary-buffer (get-buffer-create " *appease-gnus*")
|
||
gnus-original-article-buffer (current-buffer))
|
||
(run-hooks 'gnus-article-decode-hook)
|
||
(let ((mu4e~view-rendering t) ; customize gnus in mu4e
|
||
(max-specpdl-size mu4e-view-max-specpdl-size)
|
||
(gnus-blocked-images ".") ;; don't load external images.
|
||
;; Possibly add headers (before "Attachments")
|
||
(gnus-display-mime-function (mu4e~view-gnus-display-mime msg))
|
||
(gnus-icalendar-additional-identities (mu4e-personal-addresses)))
|
||
(gnus-article-prepare-display))
|
||
(setq mu4e~view-message msg)
|
||
(mu4e-view-mode)
|
||
(setq gnus-article-decoded-p gnus-article-decode-hook)
|
||
(set-buffer-modified-p nil)
|
||
(read-only-mode)))
|
||
|
||
(defun mu4e~view-gnus-display-mime (msg)
|
||
"Same as `gnus-display-mime' but add a mu4e headers to MSG."
|
||
(lambda (&optional ihandles)
|
||
(gnus-display-mime ihandles)
|
||
(unless ihandles
|
||
(save-restriction
|
||
(article-goto-body)
|
||
(forward-line -1)
|
||
(narrow-to-region (point) (point))
|
||
(dolist (field mu4e-view-fields)
|
||
(let ((fieldval (mu4e-message-field msg field)))
|
||
(cl-case field
|
||
((:path :maildir :user-agent :mailing-list :message-id)
|
||
(mu4e~view-gnus-insert-header field fieldval))
|
||
((:flags :tags)
|
||
(let ((flags (mapconcat (lambda (flag)
|
||
(if (symbolp flag)
|
||
(symbol-name flag)
|
||
flag)) fieldval ", ")))
|
||
(mu4e~view-gnus-insert-header field flags)))
|
||
(:size (mu4e~view-gnus-insert-header
|
||
field (mu4e-display-size fieldval)))
|
||
((:subject :to :from :cc :bcc :from-or-to :date :attachments
|
||
:signature :decryption)) ; handled by Gnus
|
||
(t
|
||
(mu4e~view-gnus-insert-header-custom msg field))
|
||
)))
|
||
(let ((gnus-treatment-function-alist
|
||
'((gnus-treat-highlight-headers
|
||
gnus-article-highlight-headers))))
|
||
(gnus-treat-article 'head))))))
|
||
|
||
(defun mu4e~view-gnus-insert-header (field val)
|
||
"Insert a header FIELD with value VAL in Gnus article view."
|
||
(let* ((info (cdr (assoc field mu4e-header-info)))
|
||
(key (plist-get info :name))
|
||
(help (plist-get info :help)))
|
||
(if (and val (> (length val) 0))
|
||
(insert (propertize (concat key ":") 'help-echo help)
|
||
" " val "\n"))))
|
||
|
||
(defun mu4e~view-gnus-insert-header-custom (msg field)
|
||
"Insert the custom FIELD in Gnus article view."
|
||
(let* ((info (cdr-safe (or (assoc field mu4e-header-info-custom)
|
||
(mu4e-error "custom field %S not found" field))))
|
||
(key (plist-get info :name))
|
||
(func (or (plist-get info :function)
|
||
(mu4e-error "no :function defined for custom field %S %S"
|
||
field info)))
|
||
(val (funcall func msg))
|
||
(help (plist-get info :help)))
|
||
(when (and val (> (length val) 0))
|
||
(insert (propertize (concat key ":") 'help-echo help) " " val "\n"))))
|
||
|
||
(define-advice gnus-icalendar-event-from-handle
|
||
(:filter-args (handle-attendee) mu4e~view-fix-missing-charset)
|
||
"Do not trigger an error when displaying an ical attachment
|
||
with no charset."
|
||
(if (and (boundp 'mu4e~view-rendering) mu4e~view-rendering)
|
||
(let* ((handle (car handle-attendee))
|
||
(attendee (cadr handle-attendee))
|
||
(buf (mm-handle-buffer handle))
|
||
(ty (mm-handle-type handle))
|
||
(rest (cddr handle)))
|
||
;; Put the fallback at the end:
|
||
(setq ty (append ty '((charset . "utf-8"))))
|
||
(setq handle (cons buf (cons ty rest)))
|
||
(list handle attendee))
|
||
handle-attendee))
|
||
|
||
|
||
(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))
|
||
(val (if val (propertize val 'field 'mu4e-header-field-value
|
||
'front-sticky '(field))))
|
||
(help (plist-get info :help)))
|
||
(if (and val (> (length val) 0))
|
||
(with-temp-buffer
|
||
(insert (propertize (concat key ":")
|
||
'field 'mu4e-header-field-key
|
||
'front-sticky '(field)
|
||
'keymap mu4e-view-header-field-keymap
|
||
'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-header-field-fold ()
|
||
"Fold/unfold headers' value if there is more than one line."
|
||
(interactive)
|
||
(let ((name-pos (field-beginning))
|
||
(value-pos (1+ (field-end))))
|
||
(if (and name-pos value-pos
|
||
(eq (get-text-property name-pos 'field) 'mu4e-header-field-key))
|
||
(save-excursion
|
||
(let* ((folded))
|
||
(mapc (lambda (o)
|
||
(when (overlay-get o 'mu4e~view-header-field-folded)
|
||
(delete-overlay o)
|
||
(setq folded t)))
|
||
(overlays-at value-pos))
|
||
(unless folded
|
||
(let* ((o (make-overlay value-pos (field-end value-pos)))
|
||
(vals (split-string (field-string value-pos) "\n" t))
|
||
(val (if (= (length vals) 1)
|
||
(car vals)
|
||
(truncate-string-to-width (car vals)
|
||
(- (length (car vals)) 1) 0 nil t)))) |