1
0
mirror of https://github.com/djcb/mu.git synced 2024-06-22 07:06:48 +02:00
mu/mu4e/mu4e-view-gnus.el

635 lines
26 KiB
EmacsLisp
Raw Normal View History

;;; 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 <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-view-common)
(require 'calendar)
(require 'gnus-art)
;;; Variables
(defvar gnus-icalendar-additional-identities)
(defvar helm-comp-read-use-marked)
(defvar-local mu4e~view-rendering nil)
(defcustom mu4e-view-blocked-images "."
"Regexp matching image URLs to be blocked, or nil meaning not
to block. Beware that loading external images may lower your privacy.
This can also be a function that takes a message as argument and
returns a regexp. For example, if you want to see the all images
in Github notifications (which contain no code to personally
identify you, Github will only be able to tell someone from your
IP has accessed the image), you can set this variable to the
following function:
(defun my/mu4e-view-blocked-images (msg)
(if (mu4e-message-contact-field-matches
msg :from \"notifications@github.com\")
nil \".\"))
Note that cid images that are embedded in a message wont be blocked."
:group 'mu4e-view
:type '(choice (const :tag "Allow all" nil)
(regexp :tag "Regular expression")
(function :tag "Use a function")))
(defvar mu4e-view-inhibit-images gnus-inhibit-images
"Non-nil means inhibit displaying of images inline in the article body.")
;;; 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."
(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)))
(setq mu4e~view-buffer-name gnus-article-buffer)
(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 (mu4e-view-blocked-images-fn msg))
(gnus-inhibit-images mu4e-view-inhibit-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~view-rendering t); Needed if e.g. an ics file is buttonized
(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-blocked-images-fn (msg)
(if (functionp mu4e-view-blocked-images)
(funcall mu4e-view-blocked-images msg)
mu4e-view-blocked-images))
(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)
2021-03-22 22:59:28 +01:00
"Same as `gnus-display-mime' but include 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-mode-p ()
(or (eq major-mode 'mu4e-view-mode)
(derived-mode-p '(mu4e-view-mode))))
(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 (mu4e~view-mode-p)
(apply func args)))
(defun mu4e~view-button-reply (func &rest args)
"Advice to make `gnus-button-reply' links work in mu4e."
(if (mu4e~view-mode-p)
(mu4e-compose-reply)
(apply func args)))
(defun mu4e~view-msg-mail (func &rest args)
"Advice to make `gnus-msg-mail' links compose with mu4e."
(if (mu4e~view-mode-p)
(apply 'mu4e~compose-mail args)
(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)
2021-03-22 22:59:28 +01:00
(define-key map "s" #'mu4e-headers-search)
(define-key map "S" #'mu4e-view-search-edit)
(define-key map "/" #'mu4e-view-search-narrow)
2021-03-22 22:59:28 +01:00
(define-key map (kbd "<M-left>") #'mu4e-headers-query-prev)
(define-key map (kbd "<M-right>") #'mu4e-headers-query-next)
2021-03-22 22:59:28 +01:00
(define-key map "b" #'mu4e-headers-search-bookmark)
(define-key map "B" #'mu4e-headers-search-bookmark-edit)
2021-03-22 22:59:28 +01:00
(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)
2021-03-22 22:59:28 +01:00
(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)
2021-03-22 22:59:28 +01:00
(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)
2021-03-22 22:59:28 +01:00
(define-key map "." #'mu4e-view-raw-message)
(define-key map "|" #'mu4e-view-pipe)
(define-key map "a" #'mu4e-view-action)
(define-key map "A" #'mu4e-view-mime-part-action)
(define-key map "e" #'mu4e-view-save-attachments)
2021-03-22 22:59:28 +01:00
(define-key map ";" #'mu4e-context-switch)
;; toggle header settings
2021-03-22 22:59:28 +01:00
(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
2021-03-22 22:59:28 +01:00
(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
2021-03-22 22:59:28 +01:00
(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)
2021-03-22 22:59:28 +01:00
(define-key map (kbd "RET") #'mu4e-scroll-up)
(define-key map (kbd "<backspace>") #'mu4e-scroll-down)
;; navigation between messages
2021-03-22 22:59:28 +01:00
(define-key map "p" #'mu4e-view-headers-prev)
(define-key map "n" #'mu4e-view-headers-next)
;; the same
2021-03-22 22:59:28 +01:00
(define-key map (kbd "<M-down>") #'mu4e-view-headers-next)
(define-key map (kbd "<M-up>") #'mu4e-view-headers-prev)
2021-03-22 22:59:28 +01:00
(define-key map (kbd "[") #'mu4e-view-headers-prev-unread)
(define-key map (kbd "]") #'mu4e-view-headers-next-unread)
2021-03-22 22:59:28 +01:00
;; switching from view <-> headers (when visible)
(define-key map "y" #'mu4e-select-other-view)
;; marking/unmarking
2021-03-22 22:59:28 +01:00
(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)
2021-03-22 22:59:28 +01:00
(define-key map (kbd "?") #'mu4e-view-mark-for-unread)
(define-key map (kbd "!") #'mu4e-view-mark-for-read)
2021-03-22 22:59:28 +01:00
(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)
2021-03-22 22:59:28 +01:00
(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)
2021-03-22 22:59:28 +01:00
(define-key map (kbd "#") #'mu4e-mark-resolve-deferred-marks)
;; misc
2021-03-22 22:59:28 +01:00
(define-key map "M" #'mu4e-view-massage)
(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
2021-03-22 22:59:28 +01:00
(define-key map "u" #'mu4e-view-unmark)
(define-key map "U" #'mu4e-view-unmark-all)
(define-key map "x" #'mu4e-view-marked-execute)
2021-03-22 22:59:28 +01:00
(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)
(suppress-keymap 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)
(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)
;; ;; 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-button-reply)
(advice-add 'gnus-msg-mail :around #'mu4e~view-msg-mail)
(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))))
;;; Massaging the message view
(defcustom mu4e-view-massage-options
'( ("ctoggle citations" . gnus-article-hide-citation)
("htoggle headers" . gnus-article-hide-headers)
("ytoggle crypto" . gnus-article-hide-pem))
"Various options for 'massaging' the message view. See `(gnus)
Article Treatment' for more options."
:group 'mu4e-view
:type '(alist :key-type string :value-type function))
(defun mu4e-view-massage()
"Massage current message view as per `mu4e-view-massage-options'."
(interactive)
(funcall (mu4e-read-option "Massage: " mu4e-view-massage-options)))
;;; MIME-parts
(defun mu4e~view-gather-mime-parts ()
"Gather all MIME parts as an alist that uniquely maps the number
to the gnus-part."
(let ((parts '()))
(save-excursion
(goto-char (point-min))
(while (not (eobp))
(let ((part (get-text-property (point) 'gnus-data))
(index (get-text-property (point) 'gnus-part)))
(when (and part (numberp index) (not (assoc index parts))
(push `(,index . ,part) parts)))
(goto-char (or (next-single-property-change (point) 'gnus-part)
(point-max))))))
parts))
(defun mu4e-view-save-attachments (&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 \",\".
Note, currently this does not work well with file names
containing commas."
(interactive "P")
(cl-assert (and (eq major-mode 'mu4e-view-mode)
(derived-mode-p 'gnus-article-mode)))
(let* ((parts (mu4e~view-gather-mime-parts))
(handles '())
(files '())
(helm-comp-read-use-marked t)
(compfn (if (or (and (boundp 'helm-mode) helm-mode) (and (boundp 'ivy-mode) ivy-mode))
#'completing-read
;; Fallback to `completing-read-multiple' with poor
;; completion systems.
#'completing-read-multiple))
dir)
(dolist (part parts)
(let ((fname (cdr (assoc 'filename (assoc "attachment" (cdr part))))))
(when fname
(push `(,fname . ,(cdr part)) handles)
(push fname files))))
(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))))
(mu4e-message "No attached files found"))))
(defvar mu4e-view-mime-part-actions
'(
;;
;; some basic ones
;;
;; save mime-part to a file
(:name "save" :handler gnus-article-save-part :receives index)
;; pipe mime part to some arbitrary shell command
(:name "|pipe" :handler gnus-article-pipe-part :receives index)
;; open with the default handler, if any
(:name "open" :handler mu4e~view-open-file :receives temp)
;; open with some custom file.
(:name "wopen-with" :handler (lambda (file)(mu4e~view-open-file file t))
:receives temp)
;;
;; some more examples
;;
;; import GPG key
(:name "gpg" :handler epa-import-keys :receives temp)
;; count the number of lines in a MIME-part
(:name "line-count" :handler "wc -l" :receives pipe)
;; open in this emacs instance; tries to use the attachment name,
;; so emacs can use specific modes etc.
(:name "emacs" :handler find-file :receives temp)
;; open in this emacs instance, "raw"
(:name "raw" :handler (lambda (str)
(let ((tmpbuf (get-buffer-create " *mu4e-raw-mime*")))
(with-current-buffer tmpbuf
(insert str)
(view-mode)
(goto-char (point-min)))
(switch-to-buffer tmpbuf))) :receives pipe))
"Actions for MIME-parts. Each is a plist with keys
`(:name <name> ;; name of the action; shortcut is first letter of name
:handler ;; one of:
;; - a function receiving the index/temp/pipe
;; - a string, which is taken as a shell command
:receives ;; a symbol specifying what the handler receives
;; - index: the index number of the mime part (default)
;; - temp: the full path to the mime part in a
;; temporary file, which is deleted immediately
;; after invoking handler
;; - pipe: the attachment is piped to some shell command
;; or as a string parameter to a function
).")
(defun mu4e~view-mime-part-to-temp-file (handle)
"Write mime-part N to a temporary file and return the file name.
The filename is deduced from the MIME-part's filename, or
otherwise random; the result is placed in temporary directory
with a unique name. Returns the full path for the file
created. The directory and file are self-destructed."
(let* ((tmpdir (make-temp-file "mu4e-temp-" t))
(fname (cdr-safe (assoc 'filename (assoc "attachment" (cdr handle)))))
(fname (if fname
(concat tmpdir "/" (replace-regexp-in-string "/" "-" fname))
(let ((temporary-file-directory tmpdir))
(make-temp-file "mimepart")))))
(mm-save-part-to-file handle fname)
(run-at-time "30 sec" nil (lambda () (ignore-errors (delete-directory tmpdir t))))
fname))
(defun mu4e~view-open-file (file &optional force-ask)
"Open FILE with default handler, if any. Otherwise, or if FORCE_ASK is set,
ask user for the program to open with."
(let* ((opener
(pcase system-type
(`darwin "open")
((or 'gnu 'gnu/linux 'gnu/kfreebsd) "xdg-open")))
(prog (if (or force-ask (not opener))
(read-shell-command "Open MIME-part with: ")
opener)))
(call-process prog nil 0 nil file)))
(defun mu4e-view-mime-part-action (&optional n)
"Apply some action on mime-part N in the current messsage. If N
is not specified, ask for it."
(interactive "NNumber of MIME-part: ")
(let* ((parts (mu4e~view-gather-mime-parts))
(options (mapcar (lambda (action) `(,(plist-get action :name) . ,action))
mu4e-view-mime-part-actions))
(handle (or (cdr-safe (cl-find-if (lambda (part) (eq (car part) n)) parts))
(mu4e-error "MIME-part %s not found" n)))
(action (or (and options (mu4e-read-option "Action on mime-part: " options))
(mu4e-error "No such action")))
(handler (or (plist-get action :handler)
(mu4e-error "No :handler found for action %S" action)))
(receives (or (plist-get action :receives)
(mu4e-error "No :receives found for action %S" action))))
(save-excursion
(cond
((functionp handler)
(cond
((eq receives 'index) (funcall handler n))
((eq receives 'pipe) (funcall handler (mm-with-unibyte-buffer
(mm-insert-part handle)
(buffer-string))))
((eq receives 'temp)
(funcall handler (mu4e~view-mime-part-to-temp-file handle)))
(t (mu4e-error "Invalid :receive for %S" action))))
((stringp handler)
(cond
((eq receives 'index) (shell-command (concat handler " " (shell-quote-argument n))))
((eq receives 'pipe) (mm-pipe-part handle handler))
((eq receives 'temp)
(shell-command (shell-command (concat handler " "
(shell-quote-argument
(mu4e~view-mime-part-to-temp-file handle))))))
(t (mu4e-error "Invalid action %S" action))))))))
;;;
(provide 'mu4e-view-gnus)
;;; mu4e-view.el ends here