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