merge emacs-23
[bpt/emacs.git] / lisp / mail / rmailmm.el
CommitLineData
537ab246
BG
1;;; rmailmm.el --- MIME decoding and display stuff for RMAIL
2
114f9c96 3;; Copyright (C) 2006, 2007, 2008, 2009, 2010 Free Software Foundation, Inc.
537ab246 4
6ea97e7e
GM
5;; Author: Alexander Pohoyda
6;; Alex Schroeder
537ab246
BG
7;; Maintainer: FSF
8;; Keywords: mail
9
10;; This file is part of GNU Emacs.
11
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.
16
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.
21
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/>.
24
25;;; Commentary:
26
27;; Essentially based on the design of Alexander Pohoyda's MIME
73422054
GM
28;; extensions (mime-display.el and mime.el).
29;; Call `M-x rmail-mime' when viewing an Rmail message.
537ab246 30
2e9075d3
GM
31;; Todo:
32
33;; Handle multipart/alternative.
a92cdd49 34;; Offer the option to call external/internal viewers (doc-view, xpdf, etc).
2e9075d3 35
537ab246
BG
36;;; Code:
37
38(require 'rmail)
39(require 'mail-parse)
40
f4ce6150 41;;; User options.
537ab246 42
e8652dd9
GM
43(defgroup rmail-mime nil
44 "Rmail MIME handling options."
45 :prefix "rmail-mime-"
46 :group 'rmail)
47
537ab246
BG
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)
e8652dd9 52 ("\\(image\\|audio\\|video\\|application\\)/.*" rmail-mime-bulk-handler))
f4ce6150 53 "Functions to handle various content types.
c8644de0
GM
54This is an alist with elements of the form (REGEXP FUNCTION ...).
55The first item is a regular expression matching a content-type.
56The remaining elements are handler functions to run, in order of
2e9075d3
GM
57decreasing preference. These are called until one returns non-nil.
58Note that this only applies to items with an inline Content-Disposition,
59all others are handled by `rmail-mime-bulk-handler'."
c8644de0 60 :type '(alist :key-type regexp :value-type (repeat function))
e8652dd9
GM
61 :version "23.1"
62 :group 'rmail-mime)
537ab246
BG
63
64(defcustom rmail-mime-attachment-dirs-alist
f265b4de 65 `(("text/.*" "~/Documents")
537ab246 66 ("image/.*" "~/Pictures")
f265b4de 67 (".*" "~/Desktop" "~" ,temporary-file-directory))
f4ce6150
GM
68 "Default directories to save attachments of various types into.
69This is an alist with elements of the form (REGEXP DIR ...).
70The first item is a regular expression matching a content-type.
71The remaining elements are directories, in order of decreasing preference.
72The first directory that exists is used."
73 :type '(alist :key-type regexp :value-type (repeat directory))
74 :version "23.1"
e8652dd9
GM
75 :group 'rmail-mime)
76
77(defcustom rmail-mime-show-images 'button
78 "What to do with image attachments that Emacs is capable of displaying.
79If nil, do nothing special. If `button', add an extra button
a92cdd49
GM
80that when pushed displays the image in the buffer. If a number,
81automatically show images if they are smaller than that size (in
82bytes), otherwise add a display button. Anything else means to
83automatically display the image in the buffer."
e8652dd9
GM
84 :type '(choice (const :tag "Add button to view image" button)
85 (const :tag "No special treatment" nil)
a92cdd49 86 (number :tag "Show if smaller than certain size")
e8652dd9
GM
87 (other :tag "Always show" show))
88 :version "23.2"
89 :group 'rmail-mime)
537ab246 90
f4ce6150
GM
91;;; End of user options.
92
93
537ab246
BG
94;;; Buttons
95
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))
fe6793d4
GM
100 (data (button-get button 'data))
101 (ofilename filename))
537ab246
BG
102 (setq filename (expand-file-name
103 (read-file-name (format "Save as (default: %s): " filename)
104 directory
105 (expand-file-name filename directory))
106 directory))
fe6793d4
GM
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))))
113 (with-temp-buffer
537ab246 114 (set-buffer-file-coding-system 'no-conversion)
134a027f
EZ
115 ;; Needed e.g. by jka-compr, so if the attachment is a compressed
116 ;; file, the magic signature compares equal with the unibyte
117 ;; signature string recorded in jka-compr-compression-info-list.
118 (set-buffer-multibyte nil)
fe6793d4
GM
119 (insert data)
120 (write-region nil nil filename nil nil nil t))))
537ab246 121
fe6793d4 122(define-button-type 'rmail-mime-save 'action 'rmail-mime-save)
537ab246
BG
123
124;;; Handlers
125
126(defun rmail-mime-text-handler (content-type
127 content-disposition
128 content-transfer-encoding)
129 "Handle the current buffer as a plain text MIME part."
130 (let* ((charset (cdr (assq 'charset (cdr content-type))))
131 (coding-system (when charset
132 (intern (downcase charset)))))
133 (when (coding-system-p coding-system)
134 (decode-coding-region (point-min) (point-max) coding-system))))
135
f4ce6150 136;; FIXME move to the test/ directory?
537ab246
BG
137(defun test-rmail-mime-handler ()
138 "Test of a mail using no MIME parts at all."
139 (let ((mail "To: alex@gnu.org
140Content-Type: text/plain; charset=koi8-r
141Content-Transfer-Encoding: 8bit
142MIME-Version: 1.0
143
144\372\304\322\301\327\323\324\327\325\312\324\305\41"))
145 (switch-to-buffer (get-buffer-create "*test*"))
146 (erase-buffer)
147 (set-buffer-multibyte nil)
148 (insert mail)
149 (rmail-mime-show t)
150 (set-buffer-multibyte t)))
151
e8652dd9
GM
152
153(defun rmail-mime-insert-image (type data)
154 "Insert an image of type TYPE, where DATA is the image data."
155 (end-of-line)
156 (insert ?\n)
157 (insert-image (create-image data type t)))
158
159(defun rmail-mime-image (button)
160 "Display the image associated with BUTTON."
161 (let ((inhibit-read-only t))
162 (rmail-mime-insert-image (button-get button 'image-type)
163 (button-get button 'image-data))))
164
165(define-button-type 'rmail-mime-image 'action 'rmail-mime-image)
166
167
537ab246
BG
168(defun rmail-mime-bulk-handler (content-type
169 content-disposition
e8652dd9 170 content-transfer-encoding)
2e9075d3 171 "Handle the current buffer as an attachment to download.
e8652dd9
GM
172For images that Emacs is capable of displaying, the behavior
173depends upon the value of `rmail-mime-show-images'."
174 ;; Find the default directory for this media type.
537ab246
BG
175 (let* ((directory (catch 'directory
176 (dolist (entry rmail-mime-attachment-dirs-alist)
177 (when (string-match (car entry) (car content-type))
178 (dolist (dir (cdr entry))
179 (when (file-directory-p dir)
180 (throw 'directory dir)))))))
181 (filename (or (cdr (assq 'name (cdr content-type)))
182 (cdr (assq 'filename (cdr content-disposition)))
183 "noname"))
184 (label (format "\nAttached %s file: " (car content-type)))
e8652dd9 185 (data (buffer-string))
69220882
GM
186 (udata (string-as-unibyte data))
187 (size (length udata))
a92cdd49 188 (osize size)
69220882 189 (units '(B kB MB GB))
e8652dd9 190 type)
69220882
GM
191 (while (and (> size 1024.0) ; cribbed from gnus-agent-expire-done-message
192 (cdr units))
193 (setq size (/ size 1024.0)
194 units (cdr units)))
537ab246
BG
195 (delete-region (point-min) (point-max))
196 (insert label)
197 (insert-button filename
198 :type 'rmail-mime-save
2e9075d3 199 'help-echo "mouse-2, RET: Save attachment"
537ab246 200 'filename filename
fe6793d4 201 'directory (file-name-as-directory directory)
2e9075d3 202 'data data)
69220882 203 (insert (format " (%.0f%s)" size (car units)))
e8652dd9
GM
204 (when (and rmail-mime-show-images
205 (string-match "image/\\(.*\\)" (setq type (car content-type)))
206 (setq type (concat "." (match-string 1 type))
207 type (image-type-from-file-name type))
208 (memq type image-types)
209 (image-type-available-p type))
2e9075d3 210 (insert " ")
a92cdd49
GM
211 (cond ((or (eq rmail-mime-show-images 'button)
212 (and (numberp rmail-mime-show-images)
213 (>= osize rmail-mime-show-images)))
e8652dd9
GM
214 (insert-button "Display"
215 :type 'rmail-mime-image
216 'help-echo "mouse-2, RET: Show image"
217 'image-type type
69220882 218 'image-data udata))
e8652dd9 219 (t
69220882 220 (rmail-mime-insert-image type udata))))))
537ab246
BG
221
222(defun test-rmail-mime-bulk-handler ()
223 "Test of a mail used as an example in RFC 2183."
224 (let ((mail "Content-Type: image/jpeg
225Content-Disposition: attachment; filename=genome.jpeg;
226 modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";
227Content-Description: a complete map of the human genome
228Content-Transfer-Encoding: base64
229
230iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAABGdBTUEAALGPC/xhBQAAAAZQ
231TFRF////AAAAVcLTfgAAAPZJREFUeNq9ldsOwzAIQ+3//+l1WlvA5ZLsoUiTto4TB+ISoAjy
232+ITfRBfcAmgRFFeAm+J6uhdKdFhFWUgDkFsK0oUp/9G2//Kj7Jx+5tSKOdBscgUYiKHRS/me
233WATQdRUvAK0Bnmshmtn79PpaLBbbOZkjKvRnjRZoRswOkG1wFchKew2g9wXVJVZL/m4+B+vv
2349AxQQR2Q33SgAYJzzVACdAWjAfRYzYFO9n6SLnydtQHSMxYDMAKqZ/8FS/lTK+zuq3CtK64L
235UDwbgUEAUmk2Zyg101d6PhCDySgAvTvDgKiuOrc4dLxUb7UMnhGIexyI+d6U+ABuNAP4Simx
236lgAAAABJRU5ErkJggg==
237"))
238 (switch-to-buffer (get-buffer-create "*test*"))
239 (erase-buffer)
240 (insert mail)
241 (rmail-mime-show)))
242
243(defun rmail-mime-multipart-handler (content-type
244 content-disposition
245 content-transfer-encoding)
246 "Handle the current buffer as a multipart MIME body.
247The current buffer should be narrowed to the body. CONTENT-TYPE,
248CONTENT-DISPOSITION, and CONTENT-TRANSFER-ENCODING are the values
249of the respective parsed headers. See `rmail-mime-handle' for their
250format."
251 ;; Some MUAs start boundaries with "--", while it should start
252 ;; with "CRLF--", as defined by RFC 2046:
253 ;; The boundary delimiter MUST occur at the beginning of a line,
254 ;; i.e., following a CRLF, and the initial CRLF is considered to
255 ;; be attached to the boundary delimiter line rather than part
256 ;; of the preceding part.
257 ;; We currently don't handle that.
258 (let ((boundary (cdr (assq 'boundary content-type)))
259 beg end next)
260 (unless boundary
261 (rmail-mm-get-boundary-error-message
262 "No boundary defined" content-type content-disposition
263 content-transfer-encoding))
264 (setq boundary (concat "\n--" boundary))
265 ;; Hide the body before the first bodypart
266 (goto-char (point-min))
267 (when (and (search-forward boundary nil t)
268 (looking-at "[ \t]*\n"))
269 (delete-region (point-min) (match-end 0)))
537ab246
BG
270 ;; Loop over all body parts, where beg points at the beginning of
271 ;; the part and end points at the end of the part. next points at
272 ;; the beginning of the next part.
273 (setq beg (point-min))
274 (while (search-forward boundary nil t)
275 (setq end (match-beginning 0))
276 ;; If this is the last boundary according to RFC 2046, hide the
277 ;; epilogue, else hide the boundary only. Use a marker for
278 ;; `next' because `rmail-mime-show' may change the buffer.
ffa1fed6 279 (cond ((looking-at "--[ \t]*$")
537ab246
BG
280 (setq next (point-max-marker)))
281 ((looking-at "[ \t]*\n")
ffa1fed6 282 (setq next (copy-marker (match-end 0) t)))
537ab246
BG
283 (t
284 (rmail-mm-get-boundary-error-message
285 "Malformed boundary" content-type content-disposition
286 content-transfer-encoding)))
287 (delete-region end next)
288 ;; Handle the part.
a92cdd49
GM
289 (save-restriction
290 (narrow-to-region beg end)
291 (rmail-mime-show))
292 (goto-char (setq beg next)))))
293
537ab246
BG
294
295(defun test-rmail-mime-multipart-handler ()
296 "Test of a mail used as an example in RFC 2046."
297 (let ((mail "From: Nathaniel Borenstein <nsb@bellcore.com>
298To: Ned Freed <ned@innosoft.com>
299Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST)
300Subject: Sample message
301MIME-Version: 1.0
302Content-type: multipart/mixed; boundary=\"simple boundary\"
303
304This is the preamble. It is to be ignored, though it
305is a handy place for composition agents to include an
306explanatory note to non-MIME conformant readers.
307
308--simple boundary
309
310This is implicitly typed plain US-ASCII text.
311It does NOT end with a linebreak.
312--simple boundary
313Content-type: text/plain; charset=us-ascii
314
315This is explicitly typed plain US-ASCII text.
316It DOES end with a linebreak.
317
318--simple boundary--
319
320This is the epilogue. It is also to be ignored."))
321 (switch-to-buffer (get-buffer-create "*test*"))
322 (erase-buffer)
323 (insert mail)
324 (rmail-mime-show t)))
325
326;;; Main code
327
328(defun rmail-mime-handle (content-type
329 content-disposition
330 content-transfer-encoding)
331 "Handle the current buffer as a MIME part.
332The current buffer should be narrowed to the respective body, and
333point should be at the beginning of the body.
334
335CONTENT-TYPE, CONTENT-DISPOSITION, and CONTENT-TRANSFER-ENCODING
0c9ff2c5
GM
336are the values of the respective parsed headers. The latter should
337be downcased. The parsed headers for CONTENT-TYPE and CONTENT-DISPOSITION
338have the form
537ab246
BG
339
340 \(VALUE . ALIST)
341
342In other words:
343
344 \(VALUE (ATTRIBUTE . VALUE) (ATTRIBUTE . VALUE) ...)
345
346VALUE is a string and ATTRIBUTE is a symbol.
347
348Consider the following header, for example:
349
350Content-Type: multipart/mixed;
351 boundary=\"----=_NextPart_000_0104_01C617E4.BDEC4C40\"
352
353The parsed header value:
354
355\(\"multipart/mixed\"
356 \(\"boundary\" . \"----=_NextPart_000_0104_01C617E4.BDEC4C40\"))"
357 ;; Handle the content transfer encodings we know. Unknown transfer
358 ;; encodings will be passed on to the various handlers.
359 (cond ((string= content-transfer-encoding "base64")
360 (when (ignore-errors
361 (base64-decode-region (point) (point-max)))
362 (setq content-transfer-encoding nil)))
363 ((string= content-transfer-encoding "quoted-printable")
364 (quoted-printable-decode-region (point) (point-max))
365 (setq content-transfer-encoding nil))
366 ((string= content-transfer-encoding "8bit")
367 ;; FIXME: Is this the correct way?
c893016b
SM
368 ;; No, of course not, it just means there's no decoding to do.
369 ;; (set-buffer-multibyte nil)
370 (setq content-transfer-encoding nil)
371 ))
537ab246
BG
372 ;; Inline stuff requires work. Attachments are handled by the bulk
373 ;; handler.
374 (if (string= "inline" (car content-disposition))
375 (let ((stop nil))
376 (dolist (entry rmail-mime-media-type-handlers-alist)
377 (when (and (string-match (car entry) (car content-type)) (not stop))
378 (progn
379 (setq stop (funcall (cadr entry) content-type
380 content-disposition
381 content-transfer-encoding))))))
382 ;; Everything else is an attachment.
383 (rmail-mime-bulk-handler content-type
384 content-disposition
385 content-transfer-encoding)))
386
387(defun rmail-mime-show (&optional show-headers)
388 "Handle the current buffer as a MIME message.
389If SHOW-HEADERS is non-nil, then the headers of the current part
390will shown as usual for a MIME message. The headers are also
391shown for the content type message/rfc822. This function will be
392called recursively if multiple parts are available.
393
394The current buffer must contain a single message. It will be
395modified."
396 (let ((end (point-min))
397 content-type
398 content-transfer-encoding
399 content-disposition)
400 ;; `point-min' returns the beginning and `end' points at the end
401 ;; of the headers.
402 (goto-char (point-min))
403 ;; If we're showing a part without headers, then it will start
404 ;; with a newline.
405 (if (eq (char-after) ?\n)
406 (setq end (1+ (point)))
407 (when (search-forward "\n\n" nil t)
408 (setq end (match-end 0))
409 (save-restriction
410 (narrow-to-region (point-min) end)
411 ;; FIXME: Default disposition of the multipart entities should
412 ;; be inherited.
413 (setq content-type
414 (mail-fetch-field "Content-Type")
415 content-transfer-encoding
416 (mail-fetch-field "Content-Transfer-Encoding")
417 content-disposition
418 (mail-fetch-field "Content-Disposition")))))
0c9ff2c5
GM
419 ;; Per RFC 2045, C-T-E is case insensitive (bug#5070), but the others
420 ;; are not completely so. Hopefully mail-header-parse-* DTRT.
421 (if content-transfer-encoding
422 (setq content-transfer-encoding (downcase content-transfer-encoding)))
423 (setq content-type
424 (if content-type
425 (mail-header-parse-content-type content-type)
426 ;; FIXME: Default "message/rfc822" in a "multipart/digest"
427 ;; according to RFC 2046.
428 '("text/plain")))
537ab246
BG
429 (setq content-disposition
430 (if content-disposition
431 (mail-header-parse-content-disposition content-disposition)
432 ;; If none specified, we are free to choose what we deem
433 ;; suitable according to RFC 2183. We like inline.
434 '("inline")))
435 ;; Unrecognized disposition types are to be treated like
436 ;; attachment according to RFC 2183.
437 (unless (member (car content-disposition) '("inline" "attachment"))
438 (setq content-disposition '("attachment")))
439 ;; Hide headers and handle the part.
440 (save-restriction
441 (cond ((string= (car content-type) "message/rfc822")
442 (narrow-to-region end (point-max)))
443 ((not show-headers)
444 (delete-region (point-min) end)))
445 (rmail-mime-handle content-type content-disposition
446 content-transfer-encoding))))
447
2e9075d3
GM
448(define-derived-mode rmail-mime-mode fundamental-mode "RMIME"
449 "Major mode used in `rmail-mime' buffers."
450 (setq font-lock-defaults '(rmail-font-lock-keywords t t nil nil)))
451
73422054 452;;;###autoload
537ab246 453(defun rmail-mime ()
f4ce6150
GM
454 "Process the current Rmail message as a MIME message.
455This creates a temporary \"*RMAIL*\" buffer holding a decoded
2e9075d3 456copy of the message. Inline content-types are handled according to
f4ce6150
GM
457`rmail-mime-media-type-handlers-alist'. By default, this
458displays text and multipart messages, and offers to download
459attachments as specfied by `rmail-mime-attachment-dirs-alist'."
537ab246 460 (interactive)
f4ce6150 461 (let ((data (rmail-apply-in-message rmail-current-message 'buffer-string))
537ab246
BG
462 (buf (get-buffer-create "*RMAIL*")))
463 (set-buffer buf)
f4ce6150 464 (setq buffer-undo-list t)
537ab246 465 (let ((inhibit-read-only t))
d73a0317
GM
466 ;; Decoding the message in fundamental mode for speed, only
467 ;; switching to rmail-mime-mode at the end for display. Eg
468 ;; quoted-printable-decode-region gets very slow otherwise (Bug#4993).
469 (fundamental-mode)
537ab246
BG
470 (erase-buffer)
471 (insert data)
f4ce6150 472 (rmail-mime-show t)
d73a0317 473 (rmail-mime-mode)
f4ce6150 474 (set-buffer-modified-p nil))
537ab246
BG
475 (view-buffer buf)))
476
477(defun rmail-mm-get-boundary-error-message (message type disposition encoding)
478 "Return MESSAGE with more information on the main mime components."
479 (error "%s; type: %s; disposition: %s; encoding: %s"
480 message type disposition encoding))
481
482(provide 'rmailmm)
483
35426db4
GM
484;; Local Variables:
485;; generated-autoload-file: "rmail.el"
486;; End:
487
537ab246
BG
488;; arch-tag: 3f2c5e5d-1aef-4512-bc20-fd737c9d5dd9
489;;; rmailmm.el ends here