*** empty log message ***
[gnus] / lisp / mm-decode.el
1 ;;; mm-decode.el --- Functions for decoding MIME things
2 ;; Copyright (C) 1998 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
5 ;;      MORIOKA Tomohiko <morioka@jaist.ac.jp>
6 ;; This file is part of GNU Emacs.
7
8 ;; GNU Emacs is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation; either version 2, or (at your option)
11 ;; any later version.
12
13 ;; GNU Emacs is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 ;; GNU General Public License for more details.
17
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
20 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
21 ;; Boston, MA 02111-1307, USA.
22
23 ;;; Commentary:
24
25 ;;; Code:
26
27 (require 'mail-parse)
28 (require 'mailcap)
29 (require 'mm-bodies)
30
31 (defvar mm-inline-media-tests
32   '(("image/jpeg" mm-inline-image (featurep 'jpeg))
33     ("image/png" mm-inline-image (featurep 'png))
34     ("image/gif" mm-inline-image (featurep 'gif))
35     ("image/tiff" mm-inline-image (featurep 'tiff))
36     ("image/xbm" mm-inline-image (eq (device-type) 'x))
37     ("image/xpm" mm-inline-image (featurep 'xpm))
38     ("image/bmp" mm-inline-image (featurep 'bmp))
39     ("text/plain" mm-inline-text t)
40     ("text/enriched" mm-inline-text t)
41     ("text/richtext" mm-inline-text t)
42     ("text/html" mm-inline-text (featurep 'w3))
43     ("audio/wav" mm-inline-audio
44      (and (or (featurep 'nas-sound) (featurep 'native-sound))
45           (device-sound-enabled-p)))
46     ("audio/au" mm-inline-audio
47      (and (or (featurep 'nas-sound) (featurep 'native-sound))
48           (device-sound-enabled-p))))
49   "Alist of media types/test that say whether the media types can be displayed inline.")
50
51 (defvar mm-user-display-methods
52   '(("image/.*" . inline)
53     ("text/.*" . inline)))
54
55 (defvar mm-user-automatic-display
56   '("text/plain" "text/enriched" "text/richtext" "text/html" "image/gif"))
57
58 (defvar mm-alternative-precedence
59   '("text/plain" "text/enriched" "text/richtext" "text/html")
60   "List that describes the precedence of alternative parts.")
61
62 (defvar mm-tmp-directory "/tmp/"
63   "Where mm will store its temporary files.")
64
65 ;;; Internal variables.
66
67 (defvar mm-dissection-list nil)
68 (defvar mm-last-shell-command "")
69 (defvar mm-content-id-alist nil)
70
71 ;;; Convenience macros.
72
73 (defmacro mm-handle-buffer (handle)
74   `(nth 0 ,handle))
75 (defmacro mm-handle-type (handle)
76   `(nth 1 ,handle))
77 (defmacro mm-handle-encoding (handle)
78   `(nth 2 ,handle))
79 (defmacro mm-handle-undisplayer (handle)
80   `(nth 3 ,handle))
81 (defmacro mm-handle-set-undisplayer (handle function)
82   `(setcar (nthcdr 3 ,handle) ,function))
83 (defmacro mm-handle-disposition (handle)
84   `(nth 4 ,handle))
85 (defmacro mm-handle-description (handle)
86   `(nth 5 ,handle))
87
88 ;;; The functions.
89
90 (defun mm-dissect-buffer (&optional no-strict-mime)
91   "Dissect the current buffer and return a list of MIME handles."
92   (save-excursion
93     (let (ct ctl type subtype cte cd description id result)
94       (save-restriction
95         (mail-narrow-to-head)
96         (when (and (or no-strict-mime
97                        (mail-fetch-field "mime-version"))
98                    (setq ct (mail-fetch-field "content-type")))
99           (setq ctl (condition-case () (mail-header-parse-content-type ct)
100                       (error nil))
101                 cte (mail-fetch-field "content-transfer-encoding")
102                 cd (mail-fetch-field "content-disposition")
103                 description (mail-fetch-field "content-description")
104                 id (mail-fetch-field "content-id"))))
105       (if (not ctl)
106           (mm-dissect-singlepart '("text/plain") nil no-strict-mime nil nil)
107         (setq type (split-string (car ctl) "/"))
108         (setq subtype (cadr type)
109               type (pop type))
110         (setq
111          result
112          (cond
113           ((equal type "multipart")
114            (mm-dissect-multipart ctl))
115           (t
116            (mm-dissect-singlepart
117             ctl
118             (and cte (intern (downcase (mail-header-remove-whitespace
119                                         (mail-header-remove-comments
120                                          cte)))))
121             no-strict-mime
122             (and cd (condition-case ()
123                         (mail-header-parse-content-disposition cd)
124                       (error nil)))))))
125         (when id
126           (push (cons id result) mm-content-id-alist))
127         result))))
128
129 (defun mm-dissect-singlepart (ctl cte &optional force cdl description)
130   (when (or force
131             (not (equal "text/plain" (car ctl))))
132     (let ((res (list (list (mm-copy-to-buffer) ctl cte nil cdl description))))
133       (push (car res) mm-dissection-list)
134       res)))
135
136 (defun mm-remove-all-parts ()
137   "Remove all MIME handles."
138   (interactive)
139   (mapcar 'mm-remove-part mm-dissection-list)
140   (setq mm-dissection-list nil))
141
142 (defun mm-dissect-multipart (ctl)
143   (goto-char (point-min))
144   (let* ((boundary (concat "\n--" (mail-content-type-get ctl 'boundary)))
145         (close-delimiter (concat boundary "--[ \t]*$"))
146         start parts 
147         (end (save-excursion    
148                (goto-char (point-max))
149                (if (re-search-backward close-delimiter nil t)
150                    (match-beginning 0)
151                  (point-max)))))
152     (while (search-forward boundary end t)
153       (goto-char (match-beginning 0))
154       (when start
155         (save-excursion
156           (save-restriction
157             (narrow-to-region start (point))
158             (setq parts (nconc (mm-dissect-buffer t) parts)))))
159       (forward-line 2)
160       (setq start (point)))
161     (when start
162       (save-excursion
163         (save-restriction
164           (narrow-to-region start end)
165           (setq parts (nconc (mm-dissect-buffer t) parts)))))
166     (nreverse parts)))
167
168 (defun mm-copy-to-buffer ()
169   "Copy the contents of the current buffer to a fresh buffer."
170   (save-excursion
171     (let ((obuf (current-buffer))
172           beg)
173       (goto-char (point-min))
174       (search-forward "\n\n" nil t)
175       (setq beg (point))
176       (set-buffer (generate-new-buffer " *mm*"))
177       (insert-buffer-substring obuf beg)
178       (current-buffer))))
179
180 (defun mm-display-part (handle &optional no-default)
181   "Display the MIME part represented by HANDLE."
182   (save-excursion
183     (mailcap-parse-mailcaps)
184     (if (mm-handle-undisplayer handle)
185         (mm-remove-part handle)
186       (let* ((type (car (mm-handle-type handle)))
187              (method (mailcap-mime-info type))
188              (user-method (mm-user-method type)))
189         (if (eq user-method 'inline)
190             (progn
191               (forward-line 1)
192               (mm-display-inline handle))
193           (when (or user-method
194                     method
195                     (not no-default))
196             (mm-display-external
197              handle (or user-method method 'mailcap-save-binary-file))))))))
198
199 (defun mm-display-external (handle method)
200   "Display HANDLE using METHOD."
201   (mm-with-unibyte-buffer
202     (insert-buffer-substring (mm-handle-buffer handle))
203     (mm-decode-content-transfer-encoding (mm-handle-encoding handle))
204     (if (functionp method)
205         (let ((cur (current-buffer)))
206           (switch-to-buffer (generate-new-buffer "*mm*"))
207           (buffer-disable-undo)
208           (mm-set-buffer-file-coding-system 'no-conversion)
209           (insert-buffer-substring cur)
210           (funcall method)
211           (mm-handle-set-undisplayer handle (current-buffer)))
212       (let* ((dir (make-temp-name (expand-file-name "emm." mm-tmp-directory)))
213              (filename (mail-content-type-get
214                         (mm-handle-disposition handle) 'filename))
215              (needsterm (assoc "needsterm"
216                                (mailcap-mime-info
217                                 (car (mm-handle-type handle)) t)))
218              process file)
219         ;; We create a private sub-directory where we store our files.
220         (make-directory dir)
221         (set-file-modes dir 448)
222         (if filename
223             (setq file (expand-file-name (or filename "mm.") dir))
224           (setq file (make-temp-name (expand-file-name "mm." dir))))
225         (write-region (point-min) (point-max)
226                       file nil 'nomesg nil 'no-conversion)
227         (setq process
228               (if needsterm
229                   (start-process "*display*" nil
230                                  "xterm"
231                                  "-e" (format method file))
232                 (start-process "*display*" (generate-new-buffer "*mm*")
233                                shell-file-name
234                                "-c" (format method file))))
235         (mm-handle-set-undisplayer handle (cons file process))
236         (message "Displaying %s..." (format method file))))))
237
238 (defun mm-remove-part (handle)
239   "Remove the displayed MIME part represented by HANDLE."
240   (let ((object (mm-handle-undisplayer handle)))
241     (condition-case ()
242         (cond
243          ;; Internally displayed part.
244          ((mm-annotationp object)
245           (delete-annotation object))
246          ((or (functionp object)
247               (and (listp object)
248                    (eq (car object) 'lambda)))
249           (funcall object))
250          ;; Externally displayed part.
251          ((consp object)
252           (condition-case ()
253               (delete-file (car object))
254             (error nil))
255           (condition-case ()
256               (delete-directory (file-name-directory (car object)))
257             (error nil))
258           (condition-case ()
259               (kill-process (cdr object))
260             (error nil)))
261          ((bufferp object)
262           (when (buffer-live-p object)
263             (kill-buffer object))))
264       (error nil))
265     (mm-handle-set-undisplayer handle nil)))
266
267 (defun mm-display-inline (handle)
268   (let* ((type (car (mm-handle-type handle)))
269          (function (cadr (assoc type mm-inline-media-tests))))
270     (funcall function handle)))
271
272 (defun mm-inlinable-p (type)
273   "Say whether TYPE can be displayed inline."
274   (let ((alist mm-inline-media-tests)
275         test)
276     (while alist
277       (when (equal type (caar alist))
278         (setq test (caddar alist)
279               alist nil)
280         (setq test (eval test)))
281       (pop alist))
282     test))
283
284 (defun mm-user-method (type)
285   "Return the user-defined method for TYPE."
286   (let ((methods mm-user-display-methods)
287         method result)
288     (while (setq method (pop methods))
289       (when (string-match (car method) type)
290         (when (or (not (eq (cdr method) 'inline))
291                   (mm-inlinable-p type))
292           (setq result (cdr method)
293                 methods nil))))
294     result))
295
296 (defun mm-automatic-display-p (type)
297   "Return the user-defined method for TYPE."
298   (let ((methods mm-user-automatic-display)
299         method result)
300     (while (setq method (pop methods))
301       (when (string-match method type)
302         (setq result t
303               methods nil)))
304     result))
305
306 (defun add-mime-display-method (type method)
307   "Make parts of TYPE be displayed with METHOD.
308 This overrides entries in the mailcap file."
309   (push (cons type method) mm-user-display-methods))
310
311 (defun mm-destroy-part (handle)
312   "Destroy the data structures connected to HANDLE."
313   (mm-remove-part handle)
314   (when (buffer-live-p (mm-handle-buffer handle))
315     (kill-buffer (mm-handle-buffer handle))))
316
317 (defun mm-quote-arg (arg)
318   "Return a version of ARG that is safe to evaluate in a shell."
319   (let ((pos 0) new-pos accum)
320     ;; *** bug: we don't handle newline characters properly
321     (while (setq new-pos (string-match "[!`\"$\\& \t{}]" arg pos))
322       (push (substring arg pos new-pos) accum)
323       (push "\\" accum)
324       (push (list (aref arg new-pos)) accum)
325       (setq pos (1+ new-pos)))
326     (if (= pos 0)
327         arg
328       (apply 'concat (nconc (nreverse accum) (list (substring arg pos)))))))
329
330 ;;;
331 ;;; Functions for outputting parts
332 ;;;
333
334 (defun mm-get-part (handle)
335   "Return the contents of HANDLE as a string."
336   (mm-with-unibyte-buffer
337     (insert-buffer-substring (mm-handle-buffer handle))
338     (mm-decode-content-transfer-encoding (mm-handle-encoding handle))
339     (buffer-string)))
340
341 (defvar mm-default-directory nil)
342
343 (defun mm-save-part (handle)
344   "Write HANDLE to a file."
345   (let* ((name (mail-content-type-get (mm-handle-type handle) 'name))
346          (filename (mail-content-type-get
347                     (mm-handle-disposition handle) 'filename))
348          file)
349     (when filename
350       (setq filename (file-name-nondirectory filename)))
351     (setq file
352           (read-file-name "Save MIME part to: "
353                           (expand-file-name
354                            (or filename name "")
355                            (or mm-default-directory default-directory))))
356     (setq mm-default-directory (file-name-directory file))
357     (mm-with-unibyte-buffer
358       (insert-buffer-substring (mm-handle-buffer handle))
359       (mm-decode-content-transfer-encoding (mm-handle-encoding handle))
360       (when (or (not (file-exists-p file))
361                 (yes-or-no-p (format "File %s already exists; overwrite? "
362                                      file)))
363         (write-region (point-min) (point-max) file)))))
364
365 (defun mm-pipe-part (handle)
366   "Pipe HANDLE to a process."
367   (let* ((name (mail-content-type-get (mm-handle-type handle) 'name))
368          (command
369           (read-string "Shell command on MIME part: " mm-last-shell-command)))
370     (mm-with-unibyte-buffer
371       (insert-buffer-substring (mm-handle-buffer handle))
372       (mm-decode-content-transfer-encoding (mm-handle-encoding handle))
373       (shell-command-on-region (point-min) (point-max) command nil))))
374
375 (defun mm-interactively-view-part (handle)
376   "Display HANDLE using METHOD."
377   (let* ((type (car (mm-handle-type handle)))
378          (methods
379           (mapcar (lambda (i) (list (cdr (assoc 'viewer i))))
380                   (mailcap-mime-info type 'all)))
381          (method (completing-read "Viewer: " methods)))
382     (mm-display-external (copy-sequence handle) method)))
383
384 (defun mm-preferred-alternative (handles &optional preferred)
385   "Say which of HANDLES are preferred."
386   (let ((prec (if preferred (list preferred) mm-alternative-precedence))
387         p h result type)
388     (while (setq p (pop prec))
389       (setq h handles)
390       (while h
391         (setq type (car (mm-handle-type (car h))))
392         (when (and (equal p type)
393                    (mm-automatic-display-p type)
394                    (or (not (mm-handle-disposition (car h)))
395                        (equal (car (mm-handle-disposition (car h)))
396                               "inline")))
397           (setq result (car h)
398                 h nil
399                 prec nil))
400         (pop h)))
401     result))
402
403 (defun mm-get-content-id (id)
404   "Return the handle(s) referred to by ID."
405   (cdr (assoc id mm-content-id-alist)))
406
407 (provide 'mm-decode)
408
409 ;; mm-decode.el ends here