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