(looking-back): Fix doc-string typo.
[bpt/emacs.git] / lisp / mail / pmailmm.el
CommitLineData
e131541f
PR
1;;; pmailmm.el --- MIME decoding and display stuff for PMAIL
2
3;; Copyright (C) 2006, 2007, 2008 Free Software Foundation, Inc.
4
5;; Maintainer: FSF
6;; Keywords: mail
7
8;; This file is part of GNU Emacs.
9
8c1ded96 10;; GNU Emacs is free software: you can redistribute it and/or modify
e131541f 11;; it under the terms of the GNU General Public License as published by
8c1ded96
GM
12;; the Free Software Foundation, either version 3 of the License, or
13;; (at your option) any later version.
e131541f
PR
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
8c1ded96 21;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
e131541f
PR
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 'pmail-mime "pmailmm"
32;; "Show MIME message." t)
8c1ded96 33;; (add-hook 'pmail-mode-hook
e131541f
PR
34;; (lambda ()
35;; (define-key pmail-mode-map (kbd "v")
36;; 'pmail-mime)))
37
38;;; Code:
39
acdc2006
PR
40;; For ...
41(require 'pmail)
42
e131541f
PR
43;;; Variables
44
45(defcustom pmail-mime-media-type-handlers-alist
46 '(("multipart/.*" pmail-mime-multipart-handler)
47 ("text/.*" pmail-mime-text-handler)
48 ("text/\\(x-\\)?patch" pmail-mime-bulk-handler)
49 ("application/pgp-signature" pmail-mime-application/pgp-signature-handler)
50 ("\\(image\\|audio\\|video\\|application\\)/.*" pmail-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 pmail-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 pmail-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 'pmail-mime-total-number-of-bulk-attachments 'permanent-local t)
74
75;;; Buttons
76
77(defun pmail-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 'pmail-mime-save
102 'action 'pmail-mime-save)
103
104;;; Handlers
105
106(defun pmail-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-pmail-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 (pmail-mime-show t)
129 (set-buffer-multibyte t)))
130
131(defun pmail-mime-bulk-handler (content-type
132 content-disposition
133 content-transfer-encoding)
134 "Handle the current buffer as an attachment to download."
135 (setq pmail-mime-total-number-of-bulk-attachments
136 (1+ pmail-mime-total-number-of-bulk-attachments))
137 ;; Find the default directory for this media type
138 (let* ((directory (catch 'directory
139 (dolist (entry pmail-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 'pmail-mime-save
153 'filename filename
154 'directory directory
155 'data data)))
156
157(defun test-pmail-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 (pmail-mime-show)))
177
178(defun pmail-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 `pmail-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)))
acdc2006 194 beg end next)
e131541f 195 (unless boundary
acdc2006
PR
196 (pmail-mm-get-boundary-error-message
197 "No boundary defined" content-type content-disposition
198 content-transfer-encoding))
e131541f
PR
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 pmail-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 `pmail-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
acdc2006
PR
221 (pmail-mm-get-boundary-error-message
222 "Malformed boundary" content-type content-disposition
223 content-transfer-encoding)))
e131541f
PR
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 (pmail-mime-show))))
231 (setq beg next)
232 (goto-char beg))))
233
234(defun test-pmail-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 (pmail-mime-show t)))
264
265;;; Main code
266
267(defun pmail-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 pmail-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 (pmail-mime-bulk-handler content-type
319 content-disposition
320 content-transfer-encoding)))
321
322(defun pmail-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. We're not using `pmail-header-get-header'
337 ;; because we must be able to handle the case of no headers
338 ;; existing in a part. In this case end is at point-min.
339 (goto-char (point-min))
340 ;; If we're showing a part without headers, then it will start
341 ;; with a newline.
342 (if (eq (char-after) ?\n)
343 (setq end (1+ (point)))
344 (when (search-forward "\n\n" nil t)
345 (setq end (match-end 0))
346 (save-restriction
347 (narrow-to-region (point-min) end)
348 ;; FIXME: Default disposition of the multipart entities should
349 ;; be inherited.
350 (setq content-type
351 (mail-fetch-field "Content-Type")
352 content-transfer-encoding
353 (mail-fetch-field "Content-Transfer-Encoding")
354 content-disposition
355 (mail-fetch-field "Content-Disposition")))))
356 (if content-type
357 (setq content-type (mail-header-parse-content-type
358 content-type))
359 ;; FIXME: Default "message/rfc822" in a "multipart/digest"
360 ;; according to RFC 2046.
361 (setq content-type '("text/plain")))
362 (setq content-disposition
363 (if content-disposition
364 (mail-header-parse-content-disposition content-disposition)
365 ;; If none specified, we are free to choose what we deem
366 ;; suitable according to RFC 2183. We like inline.
367 '("inline")))
368 ;; Unrecognized disposition types are to be treated like
369 ;; attachment according to RFC 2183.
370 (unless (member (car content-disposition) '("inline" "attachment"))
371 (setq content-disposition '("attachment")))
372 ;; Hide headers and handle the part.
373 (save-restriction
374 (cond ((string= (car content-type) "message/rfc822")
375 (pmail-header-hide-headers)
376 (narrow-to-region end (point-max)))
377 (show-headers
378 (pmail-header-hide-headers))
379 (t
380 (delete-region (point-min) end)))
381 (pmail-mime-handle content-type content-disposition
382 content-transfer-encoding))))
383
384(defun pmail-mime ()
385 "Copy buffer contents to a temporary buffer and handle MIME.
386This calls `pmail-mime-show' to do the real job."
387 (interactive)
388 (let ((data (with-current-buffer pmail-buffer
389 (save-restriction
390 (widen)
391 (buffer-substring
392 (pmail-desc-get-start pmail-current-message)
393 (pmail-desc-get-end pmail-current-message)))))
394 (buf (get-buffer-create "*PMAIL*")))
395 (set-buffer buf)
396 (let ((inhibit-read-only t))
397 (erase-buffer)
398 (insert data)
399 (pmail-mime-show t))
400 (view-buffer buf)))
401
acdc2006
PR
402(defun pmail-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))
406
e131541f
PR
407(provide 'pmailmm)
408
51a5d095
GM
409;; Local Variables:
410;; change-log-default-name: "ChangeLog.pmail"
411;; End:
412
61520927 413;; arch-tag: 3f2c5e5d-1aef-4512-bc20-fd737c9d5dd9
8c1ded96 414;;; pmailmm.el ends here