mu4e: show inline text/plain as attachment

Show inline text parts as attachments too, so we can save them; however,
filter outer really small ones (ie. footers)
This commit is contained in:
djcb 2017-02-12 11:11:16 +02:00
parent 235dc75a1a
commit 1c4dbe580c
1 changed files with 115 additions and 112 deletions

View File

@ -1,6 +1,6 @@
;;; mu4e-view.el -- part of mu4e, the mu mail user agent ;;; mu4e-view.el -- part of mu4e, the mu mail user agent
;; ;;
;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema ;; Copyright (C) 2011-2017 Dirk-Jan C. Binnema
;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
@ -53,7 +53,7 @@
(defcustom mu4e-view-fields (defcustom mu4e-view-fields
'(:from :to :cc :subject :flags :date :maildir :mailing-list :tags '(:from :to :cc :subject :flags :date :maildir :mailing-list :tags
:attachments :signature :decryption) :attachments :signature :decryption)
"Header fields to display in the message view buffer. "Header fields to display in the message view buffer.
For the complete list of available headers, see `mu4e-header-info'." For the complete list of available headers, see `mu4e-header-info'."
:type (list 'symbol) :type (list 'symbol)
@ -273,7 +273,7 @@ found."
(body (mu4e-message-body-text msg prefer-html))) (body (mu4e-message-body-text msg prefer-html)))
(setq mu4e~view-html-text nil) (setq mu4e~view-html-text nil)
(when (fboundp 'add-face-text-property) (when (fboundp 'add-face-text-property)
(add-face-text-property 0 (length body) 'mu4e-view-body-face t body)) (add-face-text-property 0 (length body) 'mu4e-view-body-face t body))
body))) body)))
(defun mu4e~view-embedded-winbuf () (defun mu4e~view-embedded-winbuf ()
@ -332,12 +332,12 @@ Meant to be evoked from interactive commands."
(if (and (eventp last-command-event) (if (and (eventp last-command-event)
(mouse-event-p last-command-event)) (mouse-event-p last-command-event))
(let ((posn (event-end last-command-event))) (let ((posn (event-end last-command-event)))
(when (numberp (posn-point posn)) (when (numberp (posn-point posn))
(get-text-property (get-text-property
(posn-point posn) (posn-point posn)
prop prop
(window-buffer (posn-window posn))) (window-buffer (posn-window posn)))
)) ))
(get-text-property (point) prop))) (get-text-property (point) prop)))
(defun mu4e~view-construct-header (field val &optional dont-propertize-val) (defun mu4e~view-construct-header (field val &optional dont-propertize-val)
@ -348,7 +348,7 @@ add text-properties to VAL."
(append mu4e-header-info mu4e-header-info-custom)))) (append mu4e-header-info mu4e-header-info-custom))))
(key (plist-get info :name)) (key (plist-get info :name))
(val (if val (propertize val 'field 'mu4e-header-field-value (val (if val (propertize val 'field 'mu4e-header-field-value
'front-sticky '(field)))) 'front-sticky '(field))))
(help (plist-get info :help))) (help (plist-get info :help)))
(if (and val (> (length val) 0)) (if (and val (> (length val) 0))
(with-temp-buffer (with-temp-buffer
@ -377,24 +377,24 @@ add text-properties to VAL."
"Fold/unfold headers' value if there are more than one line." "Fold/unfold headers' value if there are more than one line."
(interactive) (interactive)
(let ((name-pos (field-beginning)) (let ((name-pos (field-beginning))
(value-pos (1+ (field-end)))) (value-pos (1+ (field-end))))
(if (and name-pos value-pos (if (and name-pos value-pos
(eq (get-text-property name-pos 'field) 'mu4e-header-field-key)) (eq (get-text-property name-pos 'field) 'mu4e-header-field-key))
(save-excursion (save-excursion
(let* ((folded)) (let* ((folded))
(mapc (lambda (o) (mapc (lambda (o)
(when (overlay-get o 'mu4e~view-header-field-folded) (when (overlay-get o 'mu4e~view-header-field-folded)
(delete-overlay o) (delete-overlay o)
(setq folded t))) (setq folded t)))
(overlays-at value-pos)) (overlays-at value-pos))
(unless folded (unless folded
(let* ((o (make-overlay value-pos (field-end value-pos))) (let* ((o (make-overlay value-pos (field-end value-pos)))
(vals (split-string (field-string value-pos) "\n" t)) (vals (split-string (field-string value-pos) "\n" t))
(val (if (= (length vals) 1) (val (if (= (length vals) 1)
(car vals) (car vals)
(concat (substring (car vals) 0 -3) "...")))) (concat (substring (car vals) 0 -3) "..."))))
(overlay-put o 'mu4e~view-header-field-folded t) (overlay-put o 'mu4e~view-header-field-folded t)
(overlay-put o 'display val)))))))) (overlay-put o 'display val))))))))
(defun mu4e~view-compose-contact (&optional point) (defun mu4e~view-compose-contact (&optional point)
"Compose a message for the address at point." "Compose a message for the address at point."
@ -501,7 +501,7 @@ add text-properties to VAL."
"Open the attachement at point, or click location." "Open the attachement at point, or click location."
(interactive) (interactive)
(let* (( msg (mu4e~view-get-property-from-event 'mu4e-msg)) (let* (( msg (mu4e~view-get-property-from-event 'mu4e-msg))
( attnum (mu4e~view-get-property-from-event 'mu4e-attnum))) ( attnum (mu4e~view-get-property-from-event 'mu4e-attnum)))
(when (and msg attnum) (when (and msg attnum)
(mu4e-view-open-attachment msg attnum)))) (mu4e-view-open-attachment msg attnum))))
@ -509,17 +509,17 @@ add text-properties to VAL."
"Save the attachement at point, or click location." "Save the attachement at point, or click location."
(interactive) (interactive)
(let* (( msg (mu4e~view-get-property-from-event 'mu4e-msg)) (let* (( msg (mu4e~view-get-property-from-event 'mu4e-msg))
( attnum (mu4e~view-get-property-from-event 'mu4e-attnum))) ( attnum (mu4e~view-get-property-from-event 'mu4e-attnum)))
(when (and msg attnum) (when (and msg attnum)
(mu4e-view-save-attachment-single msg attnum)))) (mu4e-view-save-attachment-single msg attnum))))
(defun mu4e~view-construct-attachments-header (msg) (defun mu4e~view-construct-attachments-header (msg)
"Display attachment information; the field looks like something like: "Display attachment information; the field looks like something like:
:parts ((:index 1 :name \"1.part\" :mime-type \"text/plain\" :parts ((:index 1 :name \"1.part\" :mime-type \"text/plain\"
:type (leaf) :attachment nil :size 228) :type (leaf) :attachment nil :size 228)
(:index 2 :name \"analysis.doc\" (:index 2 :name \"analysis.doc\"
:mime-type \"application/msword\" :mime-type \"application/msword\"
:type (leaf attachment) :attachment nil :size 605196))" :type (leaf attachment) :attachment nil :size 605196))"
(setq mu4e~view-attach-map ;; buffer local (setq mu4e~view-attach-map ;; buffer local
(make-hash-table :size 64 :weakness nil)) (make-hash-table :size 64 :weakness nil))
(let* ((id 0) (let* ((id 0)
@ -531,6 +531,7 @@ add text-properties to VAL."
(lambda (part) (lambda (part)
(let* ((mtype (or (mu4e-message-part-field part :mime-type) (let* ((mtype (or (mu4e-message-part-field part :mime-type)
"application/octet-stream")) "application/octet-stream"))
(partsize (or (mu4e-message-part-field part :size) 0))
(attachtype (mu4e-message-part-field part :type)) (attachtype (mu4e-message-part-field part :type))
(isattach (isattach
(or ;; we consider parts marked either (or ;; we consider parts marked either
@ -540,7 +541,9 @@ add text-properties to VAL."
;; saved), unless they are text/plain, which are ;; saved), unless they are text/plain, which are
;; usually just message footers in mailing lists ;; usually just message footers in mailing lists
(and (member 'inline attachtype) (and (member 'inline attachtype)
(not (string-match "^text/plain" mtype)))))) (or
(> partsize 256) ;; filter out footers
(not (string-match "^text/plain" mtype)))))))
(or ;; remove if it's not an attach *or* if it's an (or ;; remove if it's not an attach *or* if it's an
;; image/audio/application type (but not a signature) ;; image/audio/application type (but not a signature)
isattach isattach
@ -573,7 +576,7 @@ add text-properties to VAL."
) )
(when (and size (> size 0)) (when (and size (> size 0))
(propertize (format "(%s)" (mu4e-display-size size)) (propertize (format "(%s)" (mu4e-display-size size))
'face 'mu4e-header-key-face))))) 'face 'mu4e-header-key-face)))))
attachments ", "))) attachments ", ")))
(when attachments (when attachments
(mu4e~view-construct-header :attachments attstr t)))) (mu4e~view-construct-header :attachments attstr t))))
@ -584,7 +587,7 @@ FUNC should be a function taking two arguments:
1. the message MSG, and 1. the message MSG, and
2. a plist describing the attachment. The plist looks like: 2. a plist describing the attachment. The plist looks like:
(:index 1 :name \"test123.doc\" (:index 1 :name \"test123.doc\"
:mime-type \"application/msword\" :attachment t :size 1234)." :mime-type \"application/msword\" :attachment t :size 1234)."
(dolist (part (mu4e-msg-field msg :parts)) (dolist (part (mu4e-msg-field msg :parts))
(funcall func msg part))) (funcall func msg part)))
@ -635,7 +638,7 @@ FUNC should be a function taking two arguments:
(define-key map "a" 'mu4e-view-action) (define-key map "a" 'mu4e-view-action)
(define-key map ";" 'mu4e-context-switch) (define-key map ";" 'mu4e-context-switch)
;; toggle header settings ;; toggle header settings
(define-key map "O" 'mu4e-headers-change-sorting) (define-key map "O" 'mu4e-headers-change-sorting)
(define-key map "P" 'mu4e-headers-toggle-threading) (define-key map "P" 'mu4e-headers-toggle-threading)
@ -664,7 +667,7 @@ FUNC should be a function taking two arguments:
(define-key map (kbd "[") 'mu4e-view-headers-prev-unread) (define-key map (kbd "[") 'mu4e-view-headers-prev-unread)
(define-key map (kbd "]") 'mu4e-view-headers-next-unread) (define-key map (kbd "]") 'mu4e-view-headers-next-unread)
;; switching to view mode (if it's visible) ;; switching to view mode (if it's visible)
(define-key map "y" 'mu4e-select-other-view) (define-key map "y" 'mu4e-select-other-view)
@ -688,7 +691,7 @@ FUNC should be a function taking two arguments:
(define-key map (kbd "-") 'mu4e-view-mark-for-unflag) (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-for-untrash)
(define-key map (kbd "&") 'mu4e-view-mark-custom) (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 "<kp-multiply>") '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 "<insert>") 'mu4e-view-mark-for-something)
@ -806,9 +809,9 @@ FUNC should be a function taking two arguments:
;; show context in mode-string ;; show context in mode-string
(make-local-variable 'global-mode-string) (make-local-variable 'global-mode-string)
(add-to-list 'global-mode-string '(:eval (mu4e-context-label))) (add-to-list 'global-mode-string '(:eval (mu4e-context-label)))
(setq buffer-undo-list t);; don't record undo info (setq buffer-undo-list t);; don't record undo info
;; autopair mode gives error when pressing RET ;; autopair mode gives error when pressing RET
;; turn it off ;; turn it off
(when (boundp 'autopair-dont-activate) (when (boundp 'autopair-dont-activate)
@ -956,7 +959,7 @@ N (prefix argument), to the Nth previous header."
unread message header in the headers buffer connected with this unread message header in the headers buffer connected with this
message view. If this succeeds, return the new docid. Otherwise, message view. If this succeeds, return the new docid. Otherwise,
return nil." return nil."
(mu4e~view-in-headers-context (mu4e~view-in-headers-context
(mu4e~headers-prev-or-next-unread backwards)) (mu4e~headers-prev-or-next-unread backwards))
(mu4e-select-other-view) (mu4e-select-other-view)
(mu4e-headers-view-message)) (mu4e-headers-view-message))
@ -1059,23 +1062,23 @@ Add this function to `mu4e-view-mode-hook' to enable this feature."
(save-excursion (save-excursion
(goto-char (point-min)) (goto-char (point-min))
(while (re-search-forward (while (re-search-forward
(concat "^" message-mark-insert-begin) nil t) (concat "^" message-mark-insert-begin) nil t)
(setq ov-beg (match-beginning 0) (setq ov-beg (match-beginning 0)
ov-end (match-end 0) ov-end (match-end 0)
ov-inv (make-overlay ov-beg ov-end) ov-inv (make-overlay ov-beg ov-end)
beg ov-end) beg ov-end)
(overlay-put ov-inv 'invisible t) (overlay-put ov-inv 'invisible t)
(when (re-search-forward (when (re-search-forward
(concat "^" message-mark-insert-end) nil t) (concat "^" message-mark-insert-end) nil t)
(setq ov-beg (match-beginning 0) (setq ov-beg (match-beginning 0)
ov-end (match-end 0) ov-end (match-end 0)
ov-inv (make-overlay ov-beg ov-end) ov-inv (make-overlay ov-beg ov-end)
end ov-beg) end ov-beg)
(overlay-put ov-inv 'invisible t)) (overlay-put ov-inv 'invisible t))
(when (and beg end) (when (and beg end)
(let ((ov (make-overlay beg end))) (let ((ov (make-overlay beg end)))
(overlay-put ov 'face 'mu4e-region-code)) (overlay-put ov 'face 'mu4e-region-code))
(setq beg nil end nil)))))) (setq beg nil end nil))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Wash functions ;; Wash functions
@ -1085,19 +1088,19 @@ Add this function to `mu4e-view-mode-hook' to enable this feature."
(with-current-buffer mu4e~view-buffer (with-current-buffer mu4e~view-buffer
(save-excursion (save-excursion
(let ((inhibit-read-only t) (let ((inhibit-read-only t)
(width (window-width (get-buffer-window (current-buffer))))) (width (window-width (get-buffer-window (current-buffer)))))
(save-restriction (save-restriction
(message-goto-body) (message-goto-body)
(while (not (eobp)) (while (not (eobp))
(end-of-line) (end-of-line)
(when (>= (current-column) (min fill-column width)) (when (>= (current-column) (min fill-column width))
(narrow-to-region (min (1+ (point)) (point-max)) (narrow-to-region (min (1+ (point)) (point-max))
(point-at-bol)) (point-at-bol))
(let ((goback (point-marker))) (let ((goback (point-marker)))
(fill-paragraph nil) (fill-paragraph nil)
(goto-char (marker-position goback))) (goto-char (marker-position goback)))
(widen)) (widen))
(forward-line 1))))))) (forward-line 1)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; attachment handling ;; attachment handling
@ -1134,9 +1137,9 @@ number ATTNUM."
(defun mu4e~view-request-attachment-path (fname path) (defun mu4e~view-request-attachment-path (fname path)
"Ask the user where to save FNAME (default is PATH/FNAME)." "Ask the user where to save FNAME (default is PATH/FNAME)."
(let ((fpath (expand-file-name (let ((fpath (expand-file-name
(read-file-name (read-file-name
(mu4e-format "Save as ") (mu4e-format "Save as ")
path nil nil fname) path))) path nil nil fname) path)))
(if (file-directory-p fpath) (if (file-directory-p fpath)
(expand-file-name fname fpath) (expand-file-name fname fpath)
fpath))) fpath)))
@ -1144,11 +1147,11 @@ number ATTNUM."
(defun mu4e~view-request-attachments-dir (path) (defun mu4e~view-request-attachments-dir (path)
"Ask the user where to save multiple attachments (default is PATH)." "Ask the user where to save multiple attachments (default is PATH)."
(let ((fpath (expand-file-name (let ((fpath (expand-file-name
(read-directory-name (read-directory-name
(mu4e-format "Save in directory ") (mu4e-format "Save in directory ")
path nil nil nil) path))) path nil nil nil) path)))
(if (file-directory-p fpath) (if (file-directory-p fpath)
fpath))) fpath)))
(defun mu4e-view-save-attachment-single (&optional msg attnum) (defun mu4e-view-save-attachment-single (&optional msg attnum)
"Save attachment number ATTNUM from MSG. "Save attachment number ATTNUM from MSG.
@ -1186,30 +1189,30 @@ Furthermore, there is a shortcut \"a\" which so means all
attachments, but as this is the default, you may not need it." attachments, but as this is the default, you may not need it."
(interactive) (interactive)
(let* ((msg (or msg (mu4e-message-at-point))) (let* ((msg (or msg (mu4e-message-at-point)))
(attachstr (mu4e~view-get-attach-num (attachstr (mu4e~view-get-attach-num
"Attachment number range (or 'a' for 'all')" msg t)) "Attachment number range (or 'a' for 'all')" msg t))
(count (hash-table-count mu4e~view-attach-map)) (count (hash-table-count mu4e~view-attach-map))
(attachnums (mu4e-split-ranges-to-numbers attachstr count))) (attachnums (mu4e-split-ranges-to-numbers attachstr count)))
(if mu4e-save-multiple-attachments-without-asking (if mu4e-save-multiple-attachments-without-asking
(let* ((path (concat (mu4e~get-attachment-dir) "/")) (let* ((path (concat (mu4e~get-attachment-dir) "/"))
(attachdir (mu4e~view-request-attachments-dir path))) (attachdir (mu4e~view-request-attachments-dir path)))
(dolist (num attachnums) (dolist (num attachnums)
(let* ((att (mu4e~view-get-attach msg num)) (let* ((att (mu4e~view-get-attach msg num))
(fname (plist-get att :name)) (fname (plist-get att :name))
(index (plist-get att :index)) (index (plist-get att :index))
(retry t) (retry t)
fpath) fpath)
(while retry (while retry
(setq fpath (expand-file-name (concat attachdir fname) path)) (setq fpath (expand-file-name (concat attachdir fname) path))
(setq retry (setq retry
(and (file-exists-p fpath) (and (file-exists-p fpath)
(not (y-or-n-p (not (y-or-n-p
(mu4e-format "Overwrite '%s'?" fpath)))))) (mu4e-format "Overwrite '%s'?" fpath))))))
(mu4e~proc-extract (mu4e~proc-extract
'save (mu4e-message-field msg :docid) 'save (mu4e-message-field msg :docid)
index mu4e-decryption-policy fpath)))) index mu4e-decryption-policy fpath))))
(dolist (num attachnums) (dolist (num attachnums)
(mu4e-view-save-attachment-single msg num))))) (mu4e-view-save-attachment-single msg num)))))
(defun mu4e-view-save-attachment (&optional multi) (defun mu4e-view-save-attachment (&optional multi)
"Offer to save attachment(s). "Offer to save attachment(s).
@ -1254,13 +1257,13 @@ ATTNUM is nil ask for the attachment number."
"Open MSG's attachment ATTACHNUM with CMD. "Open MSG's attachment ATTACHNUM with CMD.
If CMD is nil, ask user for it." If CMD is nil, ask user for it."
(let* ((att (mu4e~view-get-attach msg attachnum)) (let* ((att (mu4e~view-get-attach msg attachnum))
(ext (file-name-extension (plist-get att :name))) (ext (file-name-extension (plist-get att :name)))
(cmd (or cmd (cmd (or cmd
(read-string (read-string
(mu4e-format "Shell command to open it with: ") (mu4e-format "Shell command to open it with: ")
(assoc-default ext mu4e-view-attachment-assoc) (assoc-default ext mu4e-view-attachment-assoc)
'mu4e~view-open-with-hist))) 'mu4e~view-open-with-hist)))
(index (plist-get att :index))) (index (plist-get att :index)))
(mu4e~view-temp-action (mu4e~view-temp-action
(mu4e-message-field msg :docid) index "open-with" cmd))) (mu4e-message-field msg :docid) index "open-with" cmd)))
@ -1300,16 +1303,16 @@ If MSG is nil use the message returned by `message-at-point'.
The actions are specified in `mu4e-view-attachment-actions'." The actions are specified in `mu4e-view-attachment-actions'."
(interactive) (interactive)
(let* ((msg (or msg (mu4e-message-at-point))) (let* ((msg (or msg (mu4e-message-at-point)))
(actionfunc (mu4e-read-option (actionfunc (mu4e-read-option
"Action on attachment: " "Action on attachment: "
mu4e-view-attachment-actions)) mu4e-view-attachment-actions))
(multi (eq actionfunc 'mu4e-view-save-attachment-multi)) (multi (eq actionfunc 'mu4e-view-save-attachment-multi))
(attnum (unless multi (attnum (unless multi
(mu4e~view-get-attach-num "Which attachment" msg multi)))) (mu4e~view-get-attach-num "Which attachment" msg multi))))
(cond ((and actionfunc attnum) (cond ((and actionfunc attnum)
(funcall actionfunc msg attnum)) (funcall actionfunc msg attnum))
((and actionfunc multi) ((and actionfunc multi)
(funcall actionfunc msg))))) (funcall actionfunc msg)))))
;; handler-function to handle the response we get from the server when we ;; handler-function to handle the response we get from the server when we
;; want to do something with one of the attachments. ;; want to do something with one of the attachments.
@ -1480,7 +1483,7 @@ it to a range of uris. PROMPT is the query to present to the user."
user with PROMPT." user with PROMPT."
(interactive) (interactive)
(let* ((num (or num (mu4e~view-get-urls-num prompt))) (let* ((num (or num (mu4e~view-get-urls-num prompt)))
(url (gethash num mu4e~view-link-map))) (url (gethash num mu4e~view-link-map)))
(unless url (mu4e-warn "Invalid number for URL")) (unless url (mu4e-warn "Invalid number for URL"))
(funcall urlfunc url))) (funcall urlfunc url)))
@ -1594,12 +1597,12 @@ other windows."
;; headers are visible ;; headers are visible
(progn (progn
(kill-buffer-and-window) ;; kill the view win (kill-buffer-and-window) ;; kill the view win
(setq mu4e~headers-view-win nil) (setq mu4e~headers-view-win nil)
(select-window headers-win)) ;; and switch to the headers win... (select-window headers-win)) ;; and switch to the headers win...
;; headers are not visible... ;; headers are not visible...
(progn (progn
(kill-buffer) (kill-buffer)
(setq mu4e~headers-view-win nil) (setq mu4e~headers-view-win nil)
(when (buffer-live-p mu4e~view-headers-buffer) (when (buffer-live-p mu4e~view-headers-buffer)
(switch-to-buffer mu4e~view-headers-buffer)))))) (switch-to-buffer mu4e~view-headers-buffer))))))