889893cb443273a6031f90a428f13d266fe8590b
[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 ;;; Convenience macros.
32
33 (defmacro mm-handle-buffer (handle)
34   `(nth 0 ,handle))
35 (defmacro mm-handle-type (handle)
36   `(nth 1 ,handle))
37 (defmacro mm-handle-encoding (handle)
38   `(nth 2 ,handle))
39 (defmacro mm-handle-undisplayer (handle)
40   `(nth 3 ,handle))
41 (defmacro mm-handle-set-undisplayer (handle function)
42   `(setcar (nthcdr 3 ,handle) ,function))
43 (defmacro mm-handle-disposition (handle)
44   `(nth 4 ,handle))
45 (defmacro mm-handle-description (handle)
46   `(nth 5 ,handle))
47
48 (defvar mm-inline-media-tests
49   '(("image/jpeg" mm-inline-image
50      (and (featurep 'jpeg) (mm-image-fit-p handle)))
51     ("image/png" mm-inline-image
52      (and (featurep 'png) (mm-image-fit-p handle)))
53     ("image/gif" mm-inline-image
54      (and (featurep 'gif) (mm-image-fit-p handle)))
55     ("image/tiff" mm-inline-image
56      (and (featurep 'tiff) (mm-image-fit-p handle)))
57     ("image/xbm" mm-inline-image (and (fboundp 'device-type)
58                                       (eq (device-type) 'x)))
59     ("image/xpm" mm-inline-image (featurep 'xpm))
60     ("image/bmp" mm-inline-image (featurep 'bmp))
61     ("text/plain" mm-inline-text t)
62     ("text/enriched" mm-inline-text t)
63     ("text/richtext" mm-inline-text t)
64     ("text/html" mm-inline-text (locate-library "w3"))
65     ("message/delivery-status" mm-inline-text t)
66     ("audio/wav" mm-inline-audio
67      (and (or (featurep 'nas-sound) (featurep 'native-sound))
68           (device-sound-enabled-p)))
69     ("audio/au" mm-inline-audio
70      (and (or (featurep 'nas-sound) (featurep 'native-sound))
71           (device-sound-enabled-p))))
72   "Alist of media types/test that say whether the media types can be displayed inline.")
73
74 (defvar mm-user-display-methods
75   '(("image/.*" . inline)
76     ("text/.*" . inline)
77     ("message/delivery-status" . inline)))
78
79 (defvar mm-user-automatic-display
80   '("text/plain" "text/enriched" "text/richtext" "text/html" 
81     "image/.*" "message/delivery-status" "multipart/.*"))
82
83 (defvar mm-alternative-precedence
84   '("text/html" "text/enriched" "text/richtext" "text/plain")
85   "List that describes the precedence of alternative parts.")
86
87 (defvar mm-tmp-directory "/tmp/"
88   "Where mm will store its temporary files.")
89
90 ;;; Internal variables.
91
92 (defvar mm-dissection-list nil)
93 (defvar mm-last-shell-command "")
94 (defvar mm-content-id-alist nil)
95
96 ;;; The functions.
97
98 (defun mm-dissect-buffer (&optional no-strict-mime)
99   "Dissect the current buffer and return a list of MIME handles."
100   (save-excursion
101     (let (ct ctl type subtype cte cd description id result)
102       (save-restriction
103         (mail-narrow-to-head)
104         (when (and (or no-strict-mime
105                        (mail-fetch-field "mime-version"))
106                    (setq ct (mail-fetch-field "content-type")))
107           (setq ctl (condition-case () (mail-header-parse-content-type ct)
108                       (error nil))
109                 cte (mail-fetch-field "content-transfer-encoding")
110                 cd (mail-fetch-field "content-disposition")
111                 description (mail-fetch-field "content-description")
112                 id (mail-fetch-field "content-id"))))
113       (if (not ctl)
114           (mm-dissect-singlepart
115            '("text/plain") nil no-strict-mime nil description)
116         (setq type (split-string (car ctl) "/"))
117         (setq subtype (cadr type)
118               type (pop type))
119         (setq
120          result
121          (cond
122           ((equal type "multipart")
123            (cons (car ctl) (mm-dissect-multipart ctl)))
124           (t
125            (mm-dissect-singlepart
126             ctl
127             (and cte (intern (downcase (mail-header-remove-whitespace
128                                         (mail-header-remove-comments
129                                          cte)))))
130             no-strict-mime
131             (and cd (condition-case ()
132                         (mail-header-parse-content-disposition cd)
133                       (error nil)))
134             description))))
135         (when id
136           (push (cons id result) mm-content-id-alist))
137         result))))
138
139 (defun mm-dissect-singlepart (ctl cte &optional force cdl description)
140   (when (or force
141             (not (equal "text/plain" (car ctl))))
142     (let ((res (list (mm-copy-to-buffer) ctl cte nil cdl description)))
143       (push (car res) mm-dissection-list)
144       res)))
145
146 (defun mm-remove-all-parts ()
147   "Remove all MIME handles."
148   (interactive)
149   (mapcar 'mm-remove-part mm-dissection-list)
150   (setq mm-dissection-list nil))
151
152 (defun mm-dissect-multipart (ctl)
153   (goto-char (point-min))
154   (let* ((boundary (concat "\n--" (mail-content-type-get ctl 'boundary)))
155         (close-delimiter (concat (regexp-quote boundary) "--[ \t]*$"))
156         start parts 
157         (end (save-excursion    
158                (goto-char (point-max))
159                (if (re-search-backward close-delimiter nil t)
160                    (match-beginning 0)
161                  (point-max)))))
162     (while (search-forward boundary end t)
163       (goto-char (match-beginning 0))
164       (when start
165         (save-excursion
166           (save-restriction
167             (narrow-to-region start (point))
168             (setq parts (nconc (list (mm-dissect-buffer t)) parts)))))
169       (forward-line 2)
170       (setq start (point)))
171     (when start
172       (save-excursion
173         (save-restriction
174           (narrow-to-region start end)
175           (setq parts (nconc (list (mm-dissect-buffer t)) parts)))))
176     (nreverse parts)))
177
178 (defun mm-copy-to-buffer ()
179   "Copy the contents of the current buffer to a fresh buffer."
180   (save-excursion
181     (let ((obuf (current-buffer))
182           beg)
183       (goto-char (point-min))
184       (search-forward-regexp "^\n" nil t)
185       (setq beg (point))
186       (set-buffer (generate-new-buffer " *mm*"))
187       (insert-buffer-substring obuf beg)
188       (current-buffer))))
189
190 (defun mm-inlinable-part-p (type)
191   "Say whether TYPE can be displayed inline."
192   (eq (mm-user-method type) 'inline))
193
194 (defun mm-display-part (handle &optional no-default)
195   "Display the MIME part represented by HANDLE.
196 Returns nil if the part is removed; inline if displayed inline;
197 external if displayed external."
198   (save-excursion
199     (mailcap-parse-mailcaps)
200     (if (mm-handle-displayed-p handle)
201         (mm-remove-part handle)
202       (let* ((type (car (mm-handle-type handle)))
203              (method (mailcap-mime-info type))
204              (user-method (mm-user-method type)))
205         (if (eq user-method 'inline)
206             (progn
207               (forward-line 1)
208               (mm-display-inline handle))
209           (when (or user-method
210                     method
211                     (not no-default))
212             (if (and (not user-method)
213                      (not method)
214                      (equal "text" (car (split-string type))))
215                 (progn
216                   (mm-insert-inline handle (mm-get-part handle))
217                   'inline)
218               (mm-display-external
219                handle (or user-method method
220                           'mailcap-save-binary-file))
221               'external)))))))
222
223 (defun mm-display-external (handle method)
224   "Display HANDLE using METHOD."
225   (mm-with-unibyte-buffer
226     (insert-buffer-substring (mm-handle-buffer handle))
227     (mm-decode-content-transfer-encoding
228      (mm-handle-encoding handle) (car (mm-handle-type handle)))
229     (if (functionp method)
230         (let ((cur (current-buffer)))
231           (if (eq method 'mailcap-save-binary-file)
232               (progn
233                 (set-buffer (generate-new-buffer "*mm*"))
234                 (setq method nil))
235             (let ((win (get-buffer-window cur t)))
236               (when win
237                 (select-window win)))
238             (switch-to-buffer (generate-new-buffer "*mm*")))
239           (buffer-disable-undo)
240           (mm-set-buffer-file-coding-system 'no-conversion)
241           (insert-buffer-substring cur)
242           (message "Viewing with %s" method)
243           (let ((mm (current-buffer)))
244             (unwind-protect
245                 (if method
246                     (funcall method)
247                   (mm-save-part handle))
248               (mm-handle-set-undisplayer handle mm))))
249       (let* ((dir (make-temp-name (expand-file-name "emm." mm-tmp-directory)))
250              (filename (mail-content-type-get
251                         (mm-handle-disposition handle) 'filename))
252              (needsterm (assoc "needsterm"
253                                (mailcap-mime-info
254                                 (car (mm-handle-type handle)) t)))
255              process file)
256         ;; We create a private sub-directory where we store our files.
257         (make-directory dir)
258         (set-file-modes dir 448)
259         (if filename
260             (setq file (expand-file-name (file-name-nondirectory filename)
261                                          dir))
262           (setq file (make-temp-name (expand-file-name "mm." dir))))
263         (write-region (point-min) (point-max)
264                       file nil 'nomesg nil 'no-conversion)
265         (message "Viewing with %s" method)
266         (unwind-protect
267             (setq process
268                   (if needsterm
269                       (start-process "*display*" nil
270                                      "xterm"
271                                      "-e" shell-file-name "-c"
272                                      (format method
273                                              (mm-quote-arg file)))
274                     (start-process "*display*" (generate-new-buffer "*mm*")
275                                    shell-file-name
276                                    "-c" (format method
277                                                 (mm-quote-arg file)))))
278           (mm-handle-set-undisplayer handle (cons file process)))
279         (message "Displaying %s..." (format method file))))))
280
281 (defun mm-remove-parts (handles)
282   "Remove the displayed MIME parts represented by HANDLE."
283   (if (and (listp handles)
284            (bufferp (car handles)))
285       (mm-remove-part handles)
286     (let (handle)
287       (while (setq handle (pop handles))
288         (cond
289          ((stringp handle)
290           )
291          ((and (listp handle)
292                (stringp (car handle)))
293           (mm-remove-parts (cdr handle)))
294          (t
295           (mm-remove-part handle)))))))
296
297 (defun mm-destroy-parts (handles)
298   "Remove the displayed MIME parts represented by HANDLE."
299   (if (and (listp handles)
300            (bufferp (car handles)))
301       (mm-destroy-part handles)
302     (let (handle)
303       (while (setq handle (pop handles))
304         (cond
305          ((stringp handle)
306           )
307          ((and (listp handle)
308                (stringp (car handle)))
309           (mm-destroy-parts (cdr handle)))
310          (t
311           (mm-destroy-part handle)))))))
312
313 (defun mm-remove-part (handle)
314   "Remove the displayed MIME part represented by HANDLE."
315   (when (listp handle)
316     (let ((object (mm-handle-undisplayer handle)))
317       (condition-case ()
318           (cond
319            ;; Internally displayed part.
320            ((mm-annotationp object)
321             (delete-annotation object))
322            ((or (functionp object)
323                 (and (listp object)
324                      (eq (car object) 'lambda)))
325             (funcall object))
326            ;; Externally displayed part.
327            ((consp object)
328             (condition-case ()
329                 (delete-file (car object))
330               (error nil))
331             (condition-case ()
332                 (delete-directory (file-name-directory (car object)))
333               (error nil))
334             (condition-case ()
335                 (kill-process (cdr object))
336               (error nil)))
337            ((bufferp object)
338             (when (buffer-live-p object)
339               (kill-buffer object))))
340         (error nil))
341       (mm-handle-set-undisplayer handle nil))))
342
343 (defun mm-display-inline (handle)
344   (let* ((type (car (mm-handle-type handle)))
345          (function (cadr (assoc type mm-inline-media-tests))))
346     (funcall function handle)
347     (goto-char (point-min))))
348
349 (defun mm-inlinable-p (type)
350   "Say whether TYPE can be displayed inline."
351   (let ((alist mm-inline-media-tests)
352         test)
353     (while alist
354       (when (equal type (caar alist))
355         (setq test (caddar alist)
356               alist nil)
357         (setq test (eval test)))
358       (pop alist))
359     test))
360
361 (defun mm-user-method (type)
362   "Return the user-defined method for TYPE."
363   (let ((methods mm-user-display-methods)
364         method result)
365     (while (setq method (pop methods))
366       (when (string-match (car method) type)
367         (when (or (not (eq (cdr method) 'inline))
368                   (mm-inlinable-p type))
369           (setq result (cdr method)
370                 methods nil))))
371     result))
372
373 (defun mm-automatic-display-p (type)
374   "Return the user-defined method for TYPE."
375   (let ((methods mm-user-automatic-display)
376         method result)
377     (while (setq method (pop methods))
378       (when (and (string-match method type)
379                  (mm-inlinable-p type))
380         (setq result t
381               methods nil)))
382     result))
383
384 (defun add-mime-display-method (type method)
385   "Make parts of TYPE be displayed with METHOD.
386 This overrides entries in the mailcap file."
387   (push (cons type method) mm-user-display-methods))
388
389 (defun mm-destroy-part (handle)
390   "Destroy the data structures connected to HANDLE."
391   (when (listp handle)
392     (mm-remove-part handle)
393     (when (buffer-live-p (mm-handle-buffer handle))
394       (kill-buffer (mm-handle-buffer handle)))))
395
396 (defun mm-handle-displayed-p (handle)
397   "Say whether HANDLE is displayed or not."
398   (mm-handle-undisplayer handle))
399   
400 (defun mm-quote-arg (arg)
401   "Return a version of ARG that is safe to evaluate in a shell."
402   (let ((pos 0) new-pos accum)
403     ;; *** bug: we don't handle newline characters properly
404     (while (setq new-pos (string-match "[;!`\"$\\& \t{} ]" arg pos))
405       (push (substring arg pos new-pos) accum)
406       (push "\\" accum)
407       (push (list (aref arg new-pos)) accum)
408       (setq pos (1+ new-pos)))
409     (if (= pos 0)
410         arg
411       (apply 'concat (nconc (nreverse accum) (list (substring arg pos)))))))
412
413 ;;;
414 ;;; Functions for outputting parts
415 ;;;
416
417 (defun mm-get-part (handle)
418   "Return the contents of HANDLE as a string."
419   (mm-with-unibyte-buffer
420     (insert-buffer-substring (mm-handle-buffer handle))
421     (mm-decode-content-transfer-encoding
422      (mm-handle-encoding handle)
423      (car (mm-handle-type handle)))
424     (buffer-string)))
425
426 (defvar mm-default-directory nil)
427
428 (defun mm-save-part (handle)
429   "Write HANDLE to a file."
430   (let* ((name (mail-content-type-get (mm-handle-type handle) 'name))
431          (filename (mail-content-type-get
432                     (mm-handle-disposition handle) 'filename))
433          file)
434     (when filename
435       (setq filename (file-name-nondirectory filename)))
436     (setq file
437           (read-file-name "Save MIME part to: "
438                           (expand-file-name
439                            (or filename name "")
440                            (or mm-default-directory default-directory))))
441     (setq mm-default-directory (file-name-directory file))
442     (mm-with-unibyte-buffer
443       (insert-buffer-substring (mm-handle-buffer handle))
444       (mm-decode-content-transfer-encoding
445        (mm-handle-encoding handle)
446        (car (mm-handle-type handle)))
447       (when (or (not (file-exists-p file))
448                 (yes-or-no-p (format "File %s already exists; overwrite? "
449                                      file)))
450         (write-region (point-min) (point-max) file)))))
451
452 (defun mm-pipe-part (handle)
453   "Pipe HANDLE to a process."
454   (let* ((name (mail-content-type-get (mm-handle-type handle) 'name))
455          (command
456           (read-string "Shell command on MIME part: " mm-last-shell-command)))
457     (mm-with-unibyte-buffer
458       (insert-buffer-substring (mm-handle-buffer handle))
459       (mm-decode-content-transfer-encoding
460        (mm-handle-encoding handle)
461        (car (mm-handle-type handle)))
462       (shell-command-on-region (point-min) (point-max) command nil))))
463
464 (defun mm-interactively-view-part (handle)
465   "Display HANDLE using METHOD."
466   (let* ((type (car (mm-handle-type handle)))
467          (methods
468           (mapcar (lambda (i) (list (cdr (assoc 'viewer i))))
469                   (mailcap-mime-info type 'all)))
470          (method (completing-read "Viewer: " methods)))
471     (mm-display-external (copy-sequence handle) method)))
472
473 (defun mm-preferred-alternative (handles &optional preferred)
474   "Say which of HANDLES are preferred."
475   (let ((prec (if preferred (list preferred) mm-alternative-precedence))
476         p h result type)
477     (while (setq p (pop prec))
478       (setq h handles)
479       (while h
480         (setq type
481               (if (stringp (caar h))
482                   (caar h)
483                 (car (mm-handle-type (car h)))))
484         (when (and (equal p type)
485                    (mm-automatic-display-p type)
486                    (or (stringp (caar h))
487                        (not (mm-handle-disposition (car h)))
488                        (equal (car (mm-handle-disposition (car h)))
489                               "inline")))
490           (setq result (car h)
491                 h nil
492                 prec nil))
493         (pop h)))
494     result))
495
496 (defun mm-get-content-id (id)
497   "Return the handle(s) referred to by ID."
498   (cdr (assoc id mm-content-id-alist)))
499
500 (defun mm-get-image (handle)
501   "Return an image instance based on HANDLE."
502   (let ((type (cadr (split-string (car (mm-handle-type handle)) "/"))))
503     (mm-with-unibyte-buffer
504       (insert-buffer-substring (mm-handle-buffer handle))
505       (mm-decode-content-transfer-encoding
506        (mm-handle-encoding handle)
507        (car (mm-handle-type handle)))
508       (make-image-specifier
509        (vector (intern type) :data (buffer-string))))))
510
511 (defun mm-image-fit-p (handle)
512   "Say whether the image in HANDLE will fit the current window."
513   (or t
514       (let ((image (make-image-instance (mm-get-image handle))))
515         (and (< (image-instance-width image)
516                 (window-pixel-width))
517              (< (image-instance-height image)
518                 (window-pixel-height))))))
519
520 (provide 'mm-decode)
521
522 ;; mm-decode.el ends here