1 ;;; rmailmm.el --- MIME decoding and display stuff for RMAIL
3 ;; Copyright (C) 2006, 2007, 2008, 2009 Free Software Foundation, Inc.
5 ;; Author: Alexander Pohoyda
10 ;; This file is part of GNU Emacs.
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.
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.
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/>.
27 ;; Essentially based on the design of Alexander Pohoyda's MIME
28 ;; extensions (mime-display.el and mime.el).
29 ;; Call `M-x rmail-mime' when viewing an Rmail message.
38 ;; FIXME should these be in an rmail group?
39 ;; FIXME we ought to be able to display images in Emacs.
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
)
44 ;; FIXME this handler not defined anywhere?
45 ;;; ("application/pgp-signature" rmail-mime-application/pgp-signature-handler)
46 ("\\(image\\|audio\\|video\\|application\\)/.*" rmail-mime-bulk-handler
))
47 "Functions to handle various content types.
48 This is an alist with elements of the form (REGEXP FUNCTION ...).
49 The first item is a regular expression matching a content-type.
50 The remaining elements are handler functions to run, in order of
51 decreasing preference. These are called until one returns non-nil."
52 :type
'(alist :key-type regexp
:value-type
(repeat function
))
56 (defcustom rmail-mime-attachment-dirs-alist
57 `(("text/.*" "~/Documents")
58 ("image/.*" "~/Pictures")
59 (".*" "~/Desktop" "~" ,temporary-file-directory
))
60 "Default directories to save attachments of various types into.
61 This is an alist with elements of the form (REGEXP DIR ...).
62 The first item is a regular expression matching a content-type.
63 The remaining elements are directories, in order of decreasing preference.
64 The first directory that exists is used."
65 :type
'(alist :key-type regexp
:value-type
(repeat directory
))
69 ;;; End of user options.
72 (defvar rmail-mime-total-number-of-bulk-attachments
0
73 "The total number of bulk attachments in the message.
74 If more than 3, offer a way to save all attachments at once.")
75 (put 'rmail-mime-total-number-of-bulk-attachments
'permanent-local t
)
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
))
83 (data (button-get button
'data
)))
84 (while (file-exists-p (expand-file-name filename directory
))
85 (let* ((f (file-name-sans-extension filename
))
87 (when (string-match "-\\([0-9]+\\)$" f
)
88 (setq i
(1+ (string-to-number (match-string 1 f
)))
89 f
(substring f
0 (match-beginning 0))))
90 (setq filename
(concat f
"-" (number-to-string i
) "."
91 (file-name-extension filename
)))))
92 (setq filename
(expand-file-name
93 (read-file-name (format "Save as (default: %s): " filename
)
95 (expand-file-name filename directory
))
97 (when (file-regular-p filename
)
98 (error (message "File `%s' already exists" filename
)))
99 (with-temp-file filename
100 (set-buffer-file-coding-system 'no-conversion
)
103 (define-button-type 'rmail-mime-save
104 'action
'rmail-mime-save
)
108 (defun rmail-mime-text-handler (content-type
110 content-transfer-encoding
)
111 "Handle the current buffer as a plain text MIME part."
112 (let* ((charset (cdr (assq 'charset
(cdr content-type
))))
113 (coding-system (when charset
114 (intern (downcase charset
)))))
115 (when (coding-system-p coding-system
)
116 (decode-coding-region (point-min) (point-max) coding-system
))))
118 ;; FIXME move to the test/ directory?
119 (defun test-rmail-mime-handler ()
120 "Test of a mail using no MIME parts at all."
121 (let ((mail "To: alex@gnu.org
122 Content-Type: text/plain; charset=koi8-r
123 Content-Transfer-Encoding: 8bit
126 \372\304\322\301\327\323\324\327\325\312\324\305\41"))
127 (switch-to-buffer (get-buffer-create "*test*"))
129 (set-buffer-multibyte nil
)
132 (set-buffer-multibyte t
)))
134 (defun rmail-mime-bulk-handler (content-type
136 content-transfer-encoding
)
137 "Handle the current buffer as an attachment to download."
138 (setq rmail-mime-total-number-of-bulk-attachments
139 (1+ rmail-mime-total-number-of-bulk-attachments
))
140 ;; Find the default directory for this media type
141 (let* ((directory (catch 'directory
142 (dolist (entry rmail-mime-attachment-dirs-alist
)
143 (when (string-match (car entry
) (car content-type
))
144 (dolist (dir (cdr entry
))
145 (when (file-directory-p dir
)
146 (throw 'directory dir
)))))))
147 (filename (or (cdr (assq 'name
(cdr content-type
)))
148 (cdr (assq 'filename
(cdr content-disposition
)))
150 (label (format "\nAttached %s file: " (car content-type
)))
151 (data (buffer-string)))
152 (delete-region (point-min) (point-max))
154 (insert-button filename
155 :type
'rmail-mime-save
160 (defun test-rmail-mime-bulk-handler ()
161 "Test of a mail used as an example in RFC 2183."
162 (let ((mail "Content-Type: image/jpeg
163 Content-Disposition: attachment; filename=genome.jpeg;
164 modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";
165 Content-Description: a complete map of the human genome
166 Content-Transfer-Encoding: base64
168 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAABGdBTUEAALGPC/xhBQAAAAZQ
169 TFRF////AAAAVcLTfgAAAPZJREFUeNq9ldsOwzAIQ+3//+l1WlvA5ZLsoUiTto4TB+ISoAjy
170 +ITfRBfcAmgRFFeAm+J6uhdKdFhFWUgDkFsK0oUp/9G2//Kj7Jx+5tSKOdBscgUYiKHRS/me
171 WATQdRUvAK0Bnmshmtn79PpaLBbbOZkjKvRnjRZoRswOkG1wFchKew2g9wXVJVZL/m4+B+vv
172 9AxQQR2Q33SgAYJzzVACdAWjAfRYzYFO9n6SLnydtQHSMxYDMAKqZ/8FS/lTK+zuq3CtK64L
173 UDwbgUEAUmk2Zyg101d6PhCDySgAvTvDgKiuOrc4dLxUb7UMnhGIexyI+d6U+ABuNAP4Simx
176 (switch-to-buffer (get-buffer-create "*test*"))
181 (defun rmail-mime-multipart-handler (content-type
183 content-transfer-encoding
)
184 "Handle the current buffer as a multipart MIME body.
185 The current buffer should be narrowed to the body. CONTENT-TYPE,
186 CONTENT-DISPOSITION, and CONTENT-TRANSFER-ENCODING are the values
187 of the respective parsed headers. See `rmail-mime-handle' for their
189 ;; Some MUAs start boundaries with "--", while it should start
190 ;; with "CRLF--", as defined by RFC 2046:
191 ;; The boundary delimiter MUST occur at the beginning of a line,
192 ;; i.e., following a CRLF, and the initial CRLF is considered to
193 ;; be attached to the boundary delimiter line rather than part
194 ;; of the preceding part.
195 ;; We currently don't handle that.
196 (let ((boundary (cdr (assq 'boundary content-type
)))
199 (rmail-mm-get-boundary-error-message
200 "No boundary defined" content-type content-disposition
201 content-transfer-encoding
))
202 (setq boundary
(concat "\n--" boundary
))
203 ;; Hide the body before the first bodypart
204 (goto-char (point-min))
205 (when (and (search-forward boundary nil t
)
206 (looking-at "[ \t]*\n"))
207 (delete-region (point-min) (match-end 0)))
209 (setq rmail-mime-total-number-of-bulk-attachments
0)
210 ;; Loop over all body parts, where beg points at the beginning of
211 ;; the part and end points at the end of the part. next points at
212 ;; the beginning of the next part.
213 (setq beg
(point-min))
214 (while (search-forward boundary nil t
)
215 (setq end
(match-beginning 0))
216 ;; If this is the last boundary according to RFC 2046, hide the
217 ;; epilogue, else hide the boundary only. Use a marker for
218 ;; `next' because `rmail-mime-show' may change the buffer.
219 (cond ((looking-at "--[ \t]*\n")
220 (setq next
(point-max-marker)))
221 ((looking-at "[ \t]*\n")
222 (setq next
(copy-marker (match-end 0))))
224 (rmail-mm-get-boundary-error-message
225 "Malformed boundary" content-type content-disposition
226 content-transfer-encoding
)))
227 (delete-region end next
)
232 (narrow-to-region beg end
)
237 (defun test-rmail-mime-multipart-handler ()
238 "Test of a mail used as an example in RFC 2046."
239 (let ((mail "From: Nathaniel Borenstein <nsb@bellcore.com>
240 To: Ned Freed <ned@innosoft.com>
241 Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST)
242 Subject: Sample message
244 Content-type: multipart/mixed; boundary=\"simple boundary\"
246 This is the preamble. It is to be ignored, though it
247 is a handy place for composition agents to include an
248 explanatory note to non-MIME conformant readers.
252 This is implicitly typed plain US-ASCII text.
253 It does NOT end with a linebreak.
255 Content-type: text/plain; charset=us-ascii
257 This is explicitly typed plain US-ASCII text.
258 It DOES end with a linebreak.
262 This is the epilogue. It is also to be ignored."))
263 (switch-to-buffer (get-buffer-create "*test*"))
266 (rmail-mime-show t
)))
270 (defun rmail-mime-handle (content-type
272 content-transfer-encoding
)
273 "Handle the current buffer as a MIME part.
274 The current buffer should be narrowed to the respective body, and
275 point should be at the beginning of the body.
277 CONTENT-TYPE, CONTENT-DISPOSITION, and CONTENT-TRANSFER-ENCODING
278 are the values of the respective parsed headers. The parsed
279 headers for CONTENT-TYPE and CONTENT-DISPOSITION have the form
285 \(VALUE (ATTRIBUTE . VALUE) (ATTRIBUTE . VALUE) ...)
287 VALUE is a string and ATTRIBUTE is a symbol.
289 Consider the following header, for example:
291 Content-Type: multipart/mixed;
292 boundary=\"----=_NextPart_000_0104_01C617E4.BDEC4C40\"
294 The parsed header value:
296 \(\"multipart/mixed\"
297 \(\"boundary\" . \"----=_NextPart_000_0104_01C617E4.BDEC4C40\"))"
298 ;; Handle the content transfer encodings we know. Unknown transfer
299 ;; encodings will be passed on to the various handlers.
300 (cond ((string= content-transfer-encoding
"base64")
302 (base64-decode-region (point) (point-max)))
303 (setq content-transfer-encoding nil
)))
304 ((string= content-transfer-encoding
"quoted-printable")
305 (quoted-printable-decode-region (point) (point-max))
306 (setq content-transfer-encoding nil
))
307 ((string= content-transfer-encoding
"8bit")
308 ;; FIXME: Is this the correct way?
309 (set-buffer-multibyte nil
)))
310 ;; Inline stuff requires work. Attachments are handled by the bulk
312 (if (string= "inline" (car content-disposition
))
314 (dolist (entry rmail-mime-media-type-handlers-alist
)
315 (when (and (string-match (car entry
) (car content-type
)) (not stop
))
317 (setq stop
(funcall (cadr entry
) content-type
319 content-transfer-encoding
))))))
320 ;; Everything else is an attachment.
321 (rmail-mime-bulk-handler content-type
323 content-transfer-encoding
)))
325 (defun rmail-mime-show (&optional show-headers
)
326 "Handle the current buffer as a MIME message.
327 If SHOW-HEADERS is non-nil, then the headers of the current part
328 will shown as usual for a MIME message. The headers are also
329 shown for the content type message/rfc822. This function will be
330 called recursively if multiple parts are available.
332 The current buffer must contain a single message. It will be
334 (let ((end (point-min))
336 content-transfer-encoding
338 ;; `point-min' returns the beginning and `end' points at the end
340 (goto-char (point-min))
341 ;; If we're showing a part without headers, then it will start
343 (if (eq (char-after) ?
\n)
344 (setq end
(1+ (point)))
345 (when (search-forward "\n\n" nil t
)
346 (setq end
(match-end 0))
348 (narrow-to-region (point-min) end
)
349 ;; FIXME: Default disposition of the multipart entities should
352 (mail-fetch-field "Content-Type")
353 content-transfer-encoding
354 (mail-fetch-field "Content-Transfer-Encoding")
356 (mail-fetch-field "Content-Disposition")))))
358 (setq content-type
(mail-header-parse-content-type
360 ;; FIXME: Default "message/rfc822" in a "multipart/digest"
361 ;; according to RFC 2046.
362 (setq content-type
'("text/plain")))
363 (setq content-disposition
364 (if content-disposition
365 (mail-header-parse-content-disposition content-disposition
)
366 ;; If none specified, we are free to choose what we deem
367 ;; suitable according to RFC 2183. We like inline.
369 ;; Unrecognized disposition types are to be treated like
370 ;; attachment according to RFC 2183.
371 (unless (member (car content-disposition
) '("inline" "attachment"))
372 (setq content-disposition
'("attachment")))
373 ;; Hide headers and handle the part.
375 (cond ((string= (car content-type
) "message/rfc822")
376 (narrow-to-region end
(point-max)))
378 (delete-region (point-min) end
)))
379 (rmail-mime-handle content-type content-disposition
380 content-transfer-encoding
))))
384 "Process the current Rmail message as a MIME message.
385 This creates a temporary \"*RMAIL*\" buffer holding a decoded
386 copy of the message. Content-types are handled according to
387 `rmail-mime-media-type-handlers-alist'. By default, this
388 displays text and multipart messages, and offers to download
389 attachments as specfied by `rmail-mime-attachment-dirs-alist'."
391 (let ((data (rmail-apply-in-message rmail-current-message
'buffer-string
))
392 (buf (get-buffer-create "*RMAIL*")))
394 (setq buffer-undo-list t
)
395 (let ((inhibit-read-only t
))
399 (set-buffer-modified-p nil
))
402 (defun rmail-mm-get-boundary-error-message (message type disposition encoding
)
403 "Return MESSAGE with more information on the main mime components."
404 (error "%s; type: %s; disposition: %s; encoding: %s"
405 message type disposition encoding
))
409 ;; arch-tag: 3f2c5e5d-1aef-4512-bc20-fd737c9d5dd9
410 ;;; rmailmm.el ends here