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