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