1 ;;; rmailmm.el --- MIME decoding and display stuff for RMAIL
3 ;; Copyright (C) 2006, 2007, 2008, 2009, 2010 Free Software Foundation, Inc.
5 ;; Author: Alexander Pohoyda
10 ;; This file is part of GNU Emacs.
12 ;; GNU Emacs is free software: you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation, either version 3 of the License, or
15 ;; (at your option) any later version.
17 ;; GNU Emacs is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
27 ;; Essentially based on the design of Alexander Pohoyda's MIME
28 ;; extensions (mime-display.el and mime.el).
29 ;; Call `M-x rmail-mime' when viewing an Rmail message.
33 ;; Handle multipart/alternative.
34 ;; Offer the option to call external/internal viewers (doc-view, xpdf, etc).
43 (defgroup rmail-mime nil
44 "Rmail MIME handling options."
48 (defcustom rmail-mime-media-type-handlers-alist
49 '(("multipart/.*" rmail-mime-multipart-handler
)
50 ("text/.*" rmail-mime-text-handler
)
51 ("text/\\(x-\\)?patch" rmail-mime-bulk-handler
)
52 ("\\(image\\|audio\\|video\\|application\\)/.*" rmail-mime-bulk-handler
))
53 "Functions to handle various content types.
54 This is an alist with elements of the form (REGEXP FUNCTION ...).
55 The first item is a regular expression matching a content-type.
56 The remaining elements are handler functions to run, in order of
57 decreasing preference. These are called until one returns non-nil.
58 Note that this only applies to items with an inline Content-Disposition,
59 all others are handled by `rmail-mime-bulk-handler'."
60 :type
'(alist :key-type regexp
:value-type
(repeat function
))
64 (defcustom rmail-mime-attachment-dirs-alist
65 `(("text/.*" "~/Documents")
66 ("image/.*" "~/Pictures")
67 (".*" "~/Desktop" "~" ,temporary-file-directory
))
68 "Default directories to save attachments of various types into.
69 This is an alist with elements of the form (REGEXP DIR ...).
70 The first item is a regular expression matching a content-type.
71 The remaining elements are directories, in order of decreasing preference.
72 The first directory that exists is used."
73 :type
'(alist :key-type regexp
:value-type
(repeat directory
))
77 (defcustom rmail-mime-show-images
'button
78 "What to do with image attachments that Emacs is capable of displaying.
79 If nil, do nothing special. If `button', add an extra button
80 that when pushed displays the image in the buffer. If a number,
81 automatically show images if they are smaller than that size (in
82 bytes), otherwise add a display button. Anything else means to
83 automatically display the image in the buffer."
84 :type
'(choice (const :tag
"Add button to view image" button
)
85 (const :tag
"No special treatment" nil
)
86 (number :tag
"Show if smaller than certain size")
87 (other :tag
"Always show" show
))
91 ;;; End of user options.
96 (defun rmail-mime-save (button)
97 "Save the attachment using info in the BUTTON."
98 (let* ((filename (button-get button
'filename
))
99 (directory (button-get button
'directory
))
100 (data (button-get button
'data
))
101 (ofilename filename
))
102 (setq filename
(expand-file-name
103 (read-file-name (format "Save as (default: %s): " filename
)
105 (expand-file-name filename directory
))
107 ;; If arg is just a directory, use the default file name, but in
108 ;; that directory (copied from write-file).
109 (if (file-directory-p filename
)
110 (setq filename
(expand-file-name
111 (file-name-nondirectory ofilename
)
112 (file-name-as-directory filename
))))
114 (set-buffer-file-coding-system 'no-conversion
)
116 (write-region nil nil filename nil nil nil t
))))
118 (define-button-type 'rmail-mime-save
'action
'rmail-mime-save
)
122 (defun rmail-mime-text-handler (content-type
124 content-transfer-encoding
)
125 "Handle the current buffer as a plain text MIME part."
126 (let* ((charset (cdr (assq 'charset
(cdr content-type
))))
127 (coding-system (when charset
128 (intern (downcase charset
)))))
129 (when (coding-system-p coding-system
)
130 (decode-coding-region (point-min) (point-max) coding-system
))))
132 ;; FIXME move to the test/ directory?
133 (defun test-rmail-mime-handler ()
134 "Test of a mail using no MIME parts at all."
135 (let ((mail "To: alex@gnu.org
136 Content-Type: text/plain; charset=koi8-r
137 Content-Transfer-Encoding: 8bit
140 \372\304\322\301\327\323\324\327\325\312\324\305\41"))
141 (switch-to-buffer (get-buffer-create "*test*"))
143 (set-buffer-multibyte nil
)
146 (set-buffer-multibyte t
)))
149 (defun rmail-mime-insert-image (type data
)
150 "Insert an image of type TYPE, where DATA is the image data."
153 (insert-image (create-image data type t
)))
155 (defun rmail-mime-image (button)
156 "Display the image associated with BUTTON."
157 (let ((inhibit-read-only t
))
158 (rmail-mime-insert-image (button-get button
'image-type
)
159 (button-get button
'image-data
))))
161 (define-button-type 'rmail-mime-image
'action
'rmail-mime-image
)
164 (defun rmail-mime-bulk-handler (content-type
166 content-transfer-encoding
)
167 "Handle the current buffer as an attachment to download.
168 For images that Emacs is capable of displaying, the behavior
169 depends upon the value of `rmail-mime-show-images'."
170 ;; Find the default directory for this media type.
171 (let* ((directory (catch 'directory
172 (dolist (entry rmail-mime-attachment-dirs-alist
)
173 (when (string-match (car entry
) (car content-type
))
174 (dolist (dir (cdr entry
))
175 (when (file-directory-p dir
)
176 (throw 'directory dir
)))))))
177 (filename (or (cdr (assq 'name
(cdr content-type
)))
178 (cdr (assq 'filename
(cdr content-disposition
)))
180 (label (format "\nAttached %s file: " (car content-type
)))
181 (data (buffer-string))
182 (udata (string-as-unibyte data
))
183 (size (length udata
))
185 (units '(B kB MB GB
))
187 (while (and (> size
1024.0) ; cribbed from gnus-agent-expire-done-message
189 (setq size
(/ size
1024.0)
191 (delete-region (point-min) (point-max))
193 (insert-button filename
194 :type
'rmail-mime-save
195 'help-echo
"mouse-2, RET: Save attachment"
197 'directory
(file-name-as-directory directory
)
199 (insert (format " (%.0f%s)" size
(car units
)))
200 (when (and rmail-mime-show-images
201 (string-match "image/\\(.*\\)" (setq type
(car content-type
)))
202 (setq type
(concat "." (match-string 1 type
))
203 type
(image-type-from-file-name type
))
204 (memq type image-types
)
205 (image-type-available-p type
))
207 (cond ((or (eq rmail-mime-show-images
'button
)
208 (and (numberp rmail-mime-show-images
)
209 (>= osize rmail-mime-show-images
)))
210 (insert-button "Display"
211 :type
'rmail-mime-image
212 'help-echo
"mouse-2, RET: Show image"
216 (rmail-mime-insert-image type udata
))))))
218 (defun test-rmail-mime-bulk-handler ()
219 "Test of a mail used as an example in RFC 2183."
220 (let ((mail "Content-Type: image/jpeg
221 Content-Disposition: attachment; filename=genome.jpeg;
222 modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";
223 Content-Description: a complete map of the human genome
224 Content-Transfer-Encoding: base64
226 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAABGdBTUEAALGPC/xhBQAAAAZQ
227 TFRF////AAAAVcLTfgAAAPZJREFUeNq9ldsOwzAIQ+3//+l1WlvA5ZLsoUiTto4TB+ISoAjy
228 +ITfRBfcAmgRFFeAm+J6uhdKdFhFWUgDkFsK0oUp/9G2//Kj7Jx+5tSKOdBscgUYiKHRS/me
229 WATQdRUvAK0Bnmshmtn79PpaLBbbOZkjKvRnjRZoRswOkG1wFchKew2g9wXVJVZL/m4+B+vv
230 9AxQQR2Q33SgAYJzzVACdAWjAfRYzYFO9n6SLnydtQHSMxYDMAKqZ/8FS/lTK+zuq3CtK64L
231 UDwbgUEAUmk2Zyg101d6PhCDySgAvTvDgKiuOrc4dLxUb7UMnhGIexyI+d6U+ABuNAP4Simx
234 (switch-to-buffer (get-buffer-create "*test*"))
239 (defun rmail-mime-multipart-handler (content-type
241 content-transfer-encoding
)
242 "Handle the current buffer as a multipart MIME body.
243 The current buffer should be narrowed to the body. CONTENT-TYPE,
244 CONTENT-DISPOSITION, and CONTENT-TRANSFER-ENCODING are the values
245 of the respective parsed headers. See `rmail-mime-handle' for their
247 ;; Some MUAs start boundaries with "--", while it should start
248 ;; with "CRLF--", as defined by RFC 2046:
249 ;; The boundary delimiter MUST occur at the beginning of a line,
250 ;; i.e., following a CRLF, and the initial CRLF is considered to
251 ;; be attached to the boundary delimiter line rather than part
252 ;; of the preceding part.
253 ;; We currently don't handle that.
254 (let ((boundary (cdr (assq 'boundary content-type
)))
257 (rmail-mm-get-boundary-error-message
258 "No boundary defined" content-type content-disposition
259 content-transfer-encoding
))
260 (setq boundary
(concat "\n--" boundary
))
261 ;; Hide the body before the first bodypart
262 (goto-char (point-min))
263 (when (and (search-forward boundary nil t
)
264 (looking-at "[ \t]*\n"))
265 (delete-region (point-min) (match-end 0)))
266 ;; Loop over all body parts, where beg points at the beginning of
267 ;; the part and end points at the end of the part. next points at
268 ;; the beginning of the next part.
269 (setq beg
(point-min))
270 (while (search-forward boundary nil t
)
271 (setq end
(match-beginning 0))
272 ;; If this is the last boundary according to RFC 2046, hide the
273 ;; epilogue, else hide the boundary only. Use a marker for
274 ;; `next' because `rmail-mime-show' may change the buffer.
275 (cond ((looking-at "--[ \t]*$")
276 (setq next
(point-max-marker)))
277 ((looking-at "[ \t]*\n")
278 (setq next
(copy-marker (match-end 0) t
)))
280 (rmail-mm-get-boundary-error-message
281 "Malformed boundary" content-type content-disposition
282 content-transfer-encoding
)))
283 (delete-region end next
)
286 (narrow-to-region beg end
)
288 (goto-char (setq beg next
)))))
291 (defun test-rmail-mime-multipart-handler ()
292 "Test of a mail used as an example in RFC 2046."
293 (let ((mail "From: Nathaniel Borenstein <nsb@bellcore.com>
294 To: Ned Freed <ned@innosoft.com>
295 Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST)
296 Subject: Sample message
298 Content-type: multipart/mixed; boundary=\"simple boundary\"
300 This is the preamble. It is to be ignored, though it
301 is a handy place for composition agents to include an
302 explanatory note to non-MIME conformant readers.
306 This is implicitly typed plain US-ASCII text.
307 It does NOT end with a linebreak.
309 Content-type: text/plain; charset=us-ascii
311 This is explicitly typed plain US-ASCII text.
312 It DOES end with a linebreak.
316 This is the epilogue. It is also to be ignored."))
317 (switch-to-buffer (get-buffer-create "*test*"))
320 (rmail-mime-show t
)))
324 (defun rmail-mime-handle (content-type
326 content-transfer-encoding
)
327 "Handle the current buffer as a MIME part.
328 The current buffer should be narrowed to the respective body, and
329 point should be at the beginning of the body.
331 CONTENT-TYPE, CONTENT-DISPOSITION, and CONTENT-TRANSFER-ENCODING
332 are the values of the respective parsed headers. The latter should
333 be downcased. The parsed headers for CONTENT-TYPE and CONTENT-DISPOSITION
340 \(VALUE (ATTRIBUTE . VALUE) (ATTRIBUTE . VALUE) ...)
342 VALUE is a string and ATTRIBUTE is a symbol.
344 Consider the following header, for example:
346 Content-Type: multipart/mixed;
347 boundary=\"----=_NextPart_000_0104_01C617E4.BDEC4C40\"
349 The parsed header value:
351 \(\"multipart/mixed\"
352 \(\"boundary\" . \"----=_NextPart_000_0104_01C617E4.BDEC4C40\"))"
353 ;; Handle the content transfer encodings we know. Unknown transfer
354 ;; encodings will be passed on to the various handlers.
355 (cond ((string= content-transfer-encoding
"base64")
357 (base64-decode-region (point) (point-max)))
358 (setq content-transfer-encoding nil
)))
359 ((string= content-transfer-encoding
"quoted-printable")
360 (quoted-printable-decode-region (point) (point-max))
361 (setq content-transfer-encoding nil
))
362 ((string= content-transfer-encoding
"8bit")
363 ;; FIXME: Is this the correct way?
364 ;; No, of course not, it just means there's no decoding to do.
365 ;; (set-buffer-multibyte nil)
366 (setq content-transfer-encoding nil
)
368 ;; Inline stuff requires work. Attachments are handled by the bulk
370 (if (string= "inline" (car content-disposition
))
372 (dolist (entry rmail-mime-media-type-handlers-alist
)
373 (when (and (string-match (car entry
) (car content-type
)) (not stop
))
375 (setq stop
(funcall (cadr entry
) content-type
377 content-transfer-encoding
))))))
378 ;; Everything else is an attachment.
379 (rmail-mime-bulk-handler content-type
381 content-transfer-encoding
)))
383 (defun rmail-mime-show (&optional show-headers
)
384 "Handle the current buffer as a MIME message.
385 If SHOW-HEADERS is non-nil, then the headers of the current part
386 will shown as usual for a MIME message. The headers are also
387 shown for the content type message/rfc822. This function will be
388 called recursively if multiple parts are available.
390 The current buffer must contain a single message. It will be
392 (let ((end (point-min))
394 content-transfer-encoding
396 ;; `point-min' returns the beginning and `end' points at the end
398 (goto-char (point-min))
399 ;; If we're showing a part without headers, then it will start
401 (if (eq (char-after) ?
\n)
402 (setq end
(1+ (point)))
403 (when (search-forward "\n\n" nil t
)
404 (setq end
(match-end 0))
406 (narrow-to-region (point-min) end
)
407 ;; FIXME: Default disposition of the multipart entities should
410 (mail-fetch-field "Content-Type")
411 content-transfer-encoding
412 (mail-fetch-field "Content-Transfer-Encoding")
414 (mail-fetch-field "Content-Disposition")))))
415 ;; Per RFC 2045, C-T-E is case insensitive (bug#5070), but the others
416 ;; are not completely so. Hopefully mail-header-parse-* DTRT.
417 (if content-transfer-encoding
418 (setq content-transfer-encoding
(downcase content-transfer-encoding
)))
421 (mail-header-parse-content-type content-type
)
422 ;; FIXME: Default "message/rfc822" in a "multipart/digest"
423 ;; according to RFC 2046.
425 (setq content-disposition
426 (if content-disposition
427 (mail-header-parse-content-disposition content-disposition
)
428 ;; If none specified, we are free to choose what we deem
429 ;; suitable according to RFC 2183. We like inline.
431 ;; Unrecognized disposition types are to be treated like
432 ;; attachment according to RFC 2183.
433 (unless (member (car content-disposition
) '("inline" "attachment"))
434 (setq content-disposition
'("attachment")))
435 ;; Hide headers and handle the part.
437 (cond ((string= (car content-type
) "message/rfc822")
438 (narrow-to-region end
(point-max)))
440 (delete-region (point-min) end
)))
441 (rmail-mime-handle content-type content-disposition
442 content-transfer-encoding
))))
444 (define-derived-mode rmail-mime-mode fundamental-mode
"RMIME"
445 "Major mode used in `rmail-mime' buffers."
446 (setq font-lock-defaults
'(rmail-font-lock-keywords t t nil nil
)))
450 "Process the current Rmail message as a MIME message.
451 This creates a temporary \"*RMAIL*\" buffer holding a decoded
452 copy of the message. Inline content-types are handled according to
453 `rmail-mime-media-type-handlers-alist'. By default, this
454 displays text and multipart messages, and offers to download
455 attachments as specfied by `rmail-mime-attachment-dirs-alist'."
457 (let ((data (rmail-apply-in-message rmail-current-message
'buffer-string
))
458 (buf (get-buffer-create "*RMAIL*")))
460 (setq buffer-undo-list t
)
461 (let ((inhibit-read-only t
))
462 ;; Decoding the message in fundamental mode for speed, only
463 ;; switching to rmail-mime-mode at the end for display. Eg
464 ;; quoted-printable-decode-region gets very slow otherwise (Bug#4993).
470 (set-buffer-modified-p nil
))
473 (defun rmail-mm-get-boundary-error-message (message type disposition encoding
)
474 "Return MESSAGE with more information on the main mime components."
475 (error "%s; type: %s; disposition: %s; encoding: %s"
476 message type disposition encoding
))
481 ;; generated-autoload-file: "rmail.el"
484 ;; arch-tag: 3f2c5e5d-1aef-4512-bc20-fd737c9d5dd9
485 ;;; rmailmm.el ends here