;;; mu4e-view-gnus.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*- ;; Copyright (C) 2021 Dirk-Jan C. Binnema ;; Author: Dirk-Jan C. Binnema ;; Maintainer: Dirk-Jan C. Binnema ;; This file is not part of GNU Emacs. ;; GNU Emacs is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; GNU Emacs is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs. If not, see . ;;; Commentary: ;; In this file we define mu4e-view-mode (+ helper functions), which is used for ;; viewing e-mail messages ;;; Code: (require 'mu4e-view-common) (require 'calendar) (require 'gnus-art) ;;; Variables (defvar gnus-icalendar-additional-identities) (defvar helm-comp-read-use-marked) (defvar mu4e~view-rendering nil) ;;; Main ;; remember the mime-handles, so we can clean them up when ;; we quit this buffer. (defvar-local mu4e~gnus-article-mime-handles nil) (put 'mu4e~gnus-article-mime-handles 'permanent-local t) (defun mu4e~view-gnus (msg) "View MSG using Gnus' article mode. Experimental." (let ((path (mu4e-message-field msg :path)) (inhibit-read-only t) (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)) (buffer-disable-undo) (insert-file-contents-literally path nil nil nil t) (mm-enable-multibyte) (setq gnus-summary-buffer (get-buffer-create " *appease-gnus*") gnus-original-article-buffer (current-buffer) mu4e~view-message msg) (let* ((ct (mail-fetch-field "Content-Type")) (ct (and ct (mail-header-parse-content-type ct))) (charset (mail-content-type-get ct 'charset)) (charset (and charset (intern charset))) (gnus-newsgroup-charset (if (and charset (coding-system-p charset)) charset (detect-coding-region (point-min) (point-max) t)))) (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 'no-regexp))) (mu4e-view-mode) (gnus-article-prepare-display)) (setq mu4e~gnus-article-mime-handles gnus-article-mime-handles) (mu4e~view-activate-urls) ;; `mu4e-view-mode' derives from `gnus-article-mode'. (setq gnus-article-decoded-p gnus-article-decode-hook) (set-buffer-modified-p nil) (add-hook 'kill-buffer-hook #'mu4e~view-kill-buffer-hook-fn))) (defun mu4e~view-kill-buffer-hook-fn () ;; cleanup the mm-* buffers that the view spawns (when mu4e~gnus-article-mime-handles (mm-destroy-parts mu4e~gnus-article-mime-handles) (setq mu4e~gnus-article-mime-handles nil))) (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-nop (func &rest args) "Do nothing when in mu4e-view-mode. This is useful for advising some Gnus-functionality that does not work in mu4e." (unless (or (eq major-mode 'mu4e-view-mode) (derived-mode-p '(mu4e-view-mode))) (apply func args))) (defvar 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 "") 'mu4e-headers-query-prev) (define-key map (kbd "") '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 "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 "") 'mu4e-headers-split-view-grow) (define-key map (kbd "") 'mu4e-headers-split-view-shrink) ;; intra-message navigation (define-key map (kbd "SPC") 'mu4e-view-scroll-up-or-next) (define-key map (kbd "") 'beginning-of-buffer) (define-key map (kbd "") 'end-of-buffer) (define-key map (kbd "RET") 'mu4e-scroll-up) (define-key map (kbd "") '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 "") 'mu4e-view-headers-next) (define-key map (kbd "") '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) ;; marking/unmarking (define-key map "d" 'mu4e-view-mark-for-trash) (define-key map (kbd "") 'mu4e-view-mark-for-delete) (define-key map (kbd "") '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 "") 'mu4e-view-mark-for-something) (define-key map (kbd "") 'mu4e-view-mark-for-something) (define-key map (kbd "") '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 (kbd "M-q") 'article-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))) (define-key map [menu-bar headers] (cons "Mu4e" 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 [raw-view] '("View raw message" . mu4e-view-raw-message)) (define-key menumap [pipe] '("Pipe through shell" . mu4e-view-pipe)) (define-key menumap [sepa1] '("--")) (define-key menumap [mark-delete] '("Mark for deletion" . mu4e-view-mark-for-delete)) (define-key menumap [mark-untrash] '("Mark for untrash" . mu4e-view-mark-for-untrash)) (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))) (set-keymap-parent map special-mode-map) map) "Keymap for mu4e-view mode") (set-keymap-parent mu4e-view-mode-map button-buffer-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) (defun mu4e~view-mode-body () "Body of the mode-function." (use-local-map mu4e-view-mode-map) (mu4e-context-in-modeline) (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))) ;; "Define the major-mode for the mu4e-view." (define-derived-mode mu4e-view-mode gnus-article-mode "mu4e:view" "Major mode for viewing an e-mail message in mu4e, based on Gnus' article-mode." ;; Restore C-h b default behavior (define-key mu4e-view-mode-map (kbd "C-h b") 'describe-bindings) (setq mu4e~view-buffer-name gnus-article-buffer) ;; ;; turn off gnus modeline changes and menu items (advice-add 'gnus-set-mode-line :around #'mu4e~view-nop) (advice-add 'gnus-button-reply :around #'mu4e~view-nop) (mu4e~view-mode-body)) (defun mu4e-view-message-text (msg) "Return the message as a string, for replying/forwarding etc.." (with-temp-buffer (let ((path (mu4e-message-field msg :path)) (inhibit-read-only t) (gnus-article-emulate-mime t)) (buffer-disable-undo) (insert-file-contents-literally path nil nil nil t) (mm-enable-multibyte) (let* ((ct (mail-fetch-field "Content-Type")) (ct (and ct (mail-header-parse-content-type ct))) (charset (mail-content-type-get ct 'charset)) (charset (and charset (intern charset))) (gnus-newsgroup-charset (if (and charset (coding-system-p charset)) charset (detect-coding-region (point-min) (point-max) t)))) (run-hooks 'gnus-article-decode-hook)) (gnus-article-prepare-display) (buffer-string)))) (defun mu4e-view-save-attachment (&optional arg) "Save mime parts from current mu4e gnus view buffer. When helm-mode is enabled provide completion on attachments and possibility to mark candidates to save, otherwise completion on attachments is done with `completing-read-multiple', in this case use \",\" to separate candidate, completion is provided after each \",\"." (interactive "P") (cl-assert (and (eq major-mode 'mu4e-view-mode) (derived-mode-p 'gnus-article-mode))) (let ((handles '()) (files '()) (helm-comp-read-use-marked t) (compfn (if (and (boundp 'helm-mode) helm-mode) #'completing-read ;; Fallback to `completing-read-multiple' with poor ;; completion systems. #'completing-read-multiple)) dir) (save-excursion (goto-char (point-min)) (while (not (eobp)) (let ((handle (get-text-property (point) 'gnus-data))) (when (consp handle) (let ((fname (cdr (assoc 'filename (assoc "attachment" (cdr handle)))))) (when fname (push `(,fname . ,handle) handles) (push fname files))))) (forward-line 1))) (if files (progn (setq files (funcall compfn "Save part(s): " files) dir (if arg (read-directory-name "Save to directory: ") mu4e-attachment-dir)) (cl-loop for (f . h) in handles when (member f files) do (mm-save-part-to-file h (expand-file-name f dir)))) (message "No attached files found")))) ;;; (provide 'mu4e-view-gnus) ;;; mu4e-view.el ends here