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