Remove references to deleted rmailhdr that was never in "trunk".
[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
5;; Maintainer: FSF
6;; Keywords: mail
7
8;; This file is part of GNU Emacs.
9
10;; GNU Emacs is free software: you can redistribute it and/or modify
11;; it under the terms of the GNU General Public License as published by
12;; the Free Software Foundation, either version 3 of the License, or
13;; (at your option) any later version.
14
15;; GNU Emacs is distributed in the hope that it will be useful,
16;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18;; GNU General Public License for more details.
19
20;; You should have received a copy of the GNU General Public License
21;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
22
23;;; Commentary:
24
25;; Essentially based on the design of Alexander Pohoyda's MIME
26;; extensions (mime-display.el and mime.el). To use, copy a complete
27;; message into a new buffer and call (mime-show t).
28
29;; To use:
30
31;; (autoload 'rmail-mime "rmailmm"
32;; "Show MIME message." t)
33;; (add-hook 'rmail-mode-hook
34;; (lambda ()
35;; (define-key rmail-mode-map (kbd "v")
36;; 'rmail-mime)))
37
38;;; Code:
39
40(require 'rmail)
41(require 'mail-parse)
42
43;;; Variables
44
45(defcustom rmail-mime-media-type-handlers-alist
46 '(("multipart/.*" rmail-mime-multipart-handler)
47 ("text/.*" rmail-mime-text-handler)
48 ("text/\\(x-\\)?patch" rmail-mime-bulk-handler)
49 ("application/pgp-signature" rmail-mime-application/pgp-signature-handler)
50 ("\\(image\\|audio\\|video\\|application\\)/.*" rmail-mime-bulk-handler))
51 "Alist of media type handlers, also known as agents.
52Every handler is a list of type (string symbol) where STRING is a
53regular expression to match the media type with and SYMBOL is a
54function to run. Handlers should return a non-nil value if the
55job is done."
56 :type 'list
57 :group 'mime)
58
59(defcustom rmail-mime-attachment-dirs-alist
60 '(("text/.*" "~/Documents")
61 ("image/.*" "~/Pictures")
62 (".*" "~/Desktop" "~" "/tmp"))
63 "Default directories to save attachments into.
64Each media type may have it's own list of directories in order of
65preference. The first existing directory in the list will be
66used."
67 :type 'list
68 :group 'mime)
69
70(defvar rmail-mime-total-number-of-bulk-attachments 0
71 "A total number of attached bulk bodyparts in the message. If more than 3,
72offer a way to save all attachments at once.")
73(put 'rmail-mime-total-number-of-bulk-attachments 'permanent-local t)
74
75;;; Buttons
76
77(defun rmail-mime-save (button)
78 "Save the attachment using info in the BUTTON."
79 (let* ((filename (button-get button 'filename))
80 (directory (button-get button 'directory))
81 (data (button-get button 'data)))
82 (while (file-exists-p (expand-file-name filename directory))
83 (let* ((f (file-name-sans-extension filename))
84 (i 1))
85 (when (string-match "-\\([0-9]+\\)$" f)
86 (setq i (1+ (string-to-number (match-string 1 f)))
87 f (substring f 0 (match-beginning 0))))
88 (setq filename (concat f "-" (number-to-string i) "."
89 (file-name-extension filename)))))
90 (setq filename (expand-file-name
91 (read-file-name (format "Save as (default: %s): " filename)
92 directory
93 (expand-file-name filename directory))
94 directory))
95 (when (file-regular-p filename)
96 (error (message "File `%s' already exists" filename)))
97 (with-temp-file filename
98 (set-buffer-file-coding-system 'no-conversion)
99 (insert data))))
100
101(define-button-type 'rmail-mime-save
102 'action 'rmail-mime-save)
103
104;;; Handlers
105
106(defun rmail-mime-text-handler (content-type
107 content-disposition
108 content-transfer-encoding)
109 "Handle the current buffer as a plain text MIME part."
110 (let* ((charset (cdr (assq 'charset (cdr content-type))))
111 (coding-system (when charset
112 (intern (downcase charset)))))
113 (when (coding-system-p coding-system)
114 (decode-coding-region (point-min) (point-max) coding-system))))
115
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
154 'directory directory
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.
216 (cond ((looking-at "--[ \t]*\n")
217 (setq next (point-max-marker)))
218 ((looking-at "[ \t]*\n")
219 (setq next (copy-marker (match-end 0))))
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
379(defun rmail-mime ()
380 "Copy buffer contents to a temporary buffer and handle MIME.
381This calls `rmail-mime-show' to do the real job."
382 (interactive)
383 (rmail-swap-buffers-maybe)
384 (let ((data (with-current-buffer rmail-buffer
385 (save-restriction
386 (widen)
387 (buffer-substring
388 (rmail-msgbeg rmail-current-message)
389 (rmail-msgend rmail-current-message)))))
390 (buf (get-buffer-create "*RMAIL*")))
391 (set-buffer buf)
392 (let ((inhibit-read-only t))
393 (erase-buffer)
394 (insert data)
395 (rmail-mime-show t))
396 (view-buffer buf)))
397
398(defun rmail-mm-get-boundary-error-message (message type disposition encoding)
399 "Return MESSAGE with more information on the main mime components."
400 (error "%s; type: %s; disposition: %s; encoding: %s"
401 message type disposition encoding))
402
403(provide 'rmailmm)
404
537ab246
BG
405;; arch-tag: 3f2c5e5d-1aef-4512-bc20-fd737c9d5dd9
406;;; rmailmm.el ends here