Mercurial > hg > xemacs-beta
diff lisp/url/mm.el @ 0:376386a54a3c r19-14
Import from CVS: tag r19-14
author | cvs |
---|---|
date | Mon, 13 Aug 2007 08:45:50 +0200 |
parents | |
children | ac2d302a0011 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lisp/url/mm.el Mon Aug 13 08:45:50 2007 +0200 @@ -0,0 +1,1241 @@ +;;; mm.el,v --- Mailcap parsing routines, and MIME handling +;; Author: wmperry +;; Created: 1996/05/28 02:46:51 +;; Version: 1.96 +;; Keywords: mail, news, hypermedia + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; +;;; Copyright (c) 1994, 1995 by William M. Perry (wmperry@spry.com) +;;; +;;; This file is not part of GNU Emacs, but the same permissions apply. +;;; +;;; GNU Emacs is free software; you can redistribute it and/or modify +;;; it under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 2, or (at your option) +;;; any later version. +;;; +;;; GNU Emacs is distributed in the hope that it will be useful, +;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Emacs; see the file COPYING. If not, write to +;;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Generalized mailcap parsing and access routines +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; +;;; Data structures +;;; --------------- +;;; The mailcap structure is an assoc list of assoc lists. +;;; 1st assoc list is keyed on the major content-type +;;; 2nd assoc list is keyed on the minor content-type (which can be a regexp) +;;; +;;; Which looks like: +;;; ----------------- +;;; ( +;;; ("application" +;;; ("postscript" . <info>) +;;; ) +;;; ("text" +;;; ("plain" . <info>) +;;; ) +;;; ) +;;; +;;; Where <info> is another assoc list of the various information +;;; related to the mailcap RFC. This is keyed on the lowercase +;;; attribute name (viewer, test, etc). This looks like: +;;; (("viewer" . viewerinfo) +;;; ("test" . testinfo) +;;; ("xxxx" . "string") +;;; ) +;;; +;;; Where viewerinfo specifies how the content-type is viewed. Can be +;;; a string, in which case it is run through a shell, with +;;; appropriate parameters, or a symbol, in which case the symbol is +;;; funcall'd, with the buffer as an argument. +;;; +;;; testinfo is a list of strings, or nil. If nil, it means the +;;; viewer specified is always valid. If it is a list of strings, +;;; these are used to determine whether a viewer passes the 'test' or +;;; not. +;;; +;;; The main interface to this code is: +;;; +;;; To set everything up: +;;; +;;; (mm-parse-mailcaps [path]) +;;; +;;; Where PATH is a unix-style path specification (: separated list +;;; of strings). If PATH is nil, the environment variable MAILCAPS +;;; will be consulted. If there is no environment variable, then a +;;; default list of paths is used. +;;; +;;; To retrieve the information: +;;; (mm-mime-info st [nd] [request]) +;;; +;;; Where st and nd are positions in a buffer that contain the +;;; content-type header information of a mail/news/whatever message. +;;; st can optionally be a string that contains the content-type +;;; information. +;;; +;;; Third argument REQUEST specifies what information to return. If +;;; it is nil or the empty string, the viewer (second field of the +;;; mailcap entry) will be returned. If it is a string, then the +;;; mailcap field corresponding to that string will be returned +;;; (print, description, whatever). If a number, then all the +;;; information for this specific viewer is returned. +;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Variables, etc +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defconst mm-version (let ((x "1.96")) + (if (string-match "Revision: \\([^ \t\n]+\\)" x) + (substring x (match-beginning 1) (match-end 1)) + x)) + "Version # of MM package") + +(defvar mm-parse-args-syntax-table + (copy-syntax-table emacs-lisp-mode-syntax-table) + "A syntax table for parsing sgml attributes.") + +(modify-syntax-entry ?' "\"" mm-parse-args-syntax-table) +(modify-syntax-entry ?` "\"" mm-parse-args-syntax-table) +(modify-syntax-entry ?{ "(" mm-parse-args-syntax-table) +(modify-syntax-entry ?} ")" mm-parse-args-syntax-table) + +;;; This is so we can use a consistent method of checking for mule support +;;; Emacs-based mule uses (boundp 'MULE), but XEmacs-based mule uses +;;; (featurep 'mule) - I choose to use the latter. + +(if (boundp 'MULE) + (provide 'mule)) + +(defvar mm-mime-data + '( + ("multipart" . ( + ("alternative". (("viewer" . mm-multipart-viewer) + ("type" . "multipart/alternative"))) + ("mixed" . (("viewer" . mm-multipart-viewer) + ("type" . "multipart/mixed"))) + (".*" . (("viewer" . mm-save-binary-file) + ("type" . "multipart/*"))) + ) + ) + ("application" . ( + ("octet-stream" . (("viewer" . mm-save-binary-file) + ("type" ."application/octet-stream"))) + ("dvi" . (("viewer" . "open %s") + ("type" . "application/dvi") + ("test" . (eq (device-type) 'ns)))) + ("dvi" . (("viewer" . "xdvi %s") + ("test" . (eq (device-type) 'x)) + ("needsx11") + ("type" . "application/dvi"))) + ("dvi" . (("viewer" . "dvitty %s") + ("test" . (not (getenv "DISPLAY"))) + ("type" . "application/dvi"))) + ("emacs-lisp" . (("viewer" . mm-maybe-eval) + ("type" . "application/emacs-lisp"))) +; ("x-tar" . (("viewer" . tar-mode) +; ("test" . (fboundp 'tar-mode)) +; ("type" . "application/x-tar"))) + ("x-tar" . (("viewer" . mm-save-binary-file) + ("type" . "application/x-tar"))) + ("x-latex" . (("viewer" . tex-mode) + ("test" . (fboundp 'tex-mode)) + ("type" . "application/x-latex"))) + ("x-tex" . (("viewer" . tex-mode) + ("test" . (fboundp 'tex-mode)) + ("type" . "application/x-tex"))) + ("latex" . (("viewer" . tex-mode) + ("test" . (fboundp 'tex-mode)) + ("type" . "application/latex"))) + ("tex" . (("viewer" . tex-mode) + ("test" . (fboundp 'tex-mode)) + ("type" . "application/tex"))) + ("texinfo" . (("viewer" . texinfo-mode) + ("test" . (fboundp 'texinfo-mode)) + ("type" . "application/tex"))) + ("zip" . (("viewer" . mm-save-binary-file) + ("type" . "application/zip") + ("copiousoutput"))) + ("pdf" . (("viewer" . "acroread %s") + ("type" . "application/pdf"))) + ("postscript" . (("viewer" . "open %s") + ("type" . "application/postscript") + ("test" . (eq (device-type) 'ns)))) + ("postscript" . (("viewer" . "ghostview %s") + ("type" . "application/postscript") + ("test" . (eq (device-type) 'x)) + ("needsx11"))) + ("postscript" . (("viewer" . "ps2ascii %s") + ("type" . "application/postscript") + ("test" . (not (getenv "DISPLAY"))) + ("copiousoutput"))) + ("x-www-pem-reply" . + (("viewer" . (w3-decode-pgp/pem "pem")) + ("test" . (fboundp 'w3-decode-pgp/pem)) + ("type" . "application/x-www-pem-reply") + )) + ("x-www-pgp-reply" . + (("viewer" . (w3-decode-pgp/pem "pgp")) + ("test" . (fboundp 'w3-decode-pgp/pem)) + ("type" . "application/x-www-pgp-reply"))) + )) + ("audio" . ( + ("x-mpeg" . (("viewer" . "maplay %s") + ("type" . "audio/x-mpeg"))) + (".*" . (("viewer" . mm-play-sound-file) + ("test" . (or (featurep 'nas-sound) + (featurep 'native-sound))) + ("type" . "audio/*"))) + (".*" . (("viewer" . "showaudio") + ("type" . "audio/*"))) + )) + ("message" . ( + ("rfc-*822" . (("viewer" . vm-mode) + ("test" . (fboundp 'vm-mode)) + ("type" . "message/rfc-822"))) + ("rfc-*822" . (("viewer" . w3-mode) + ("test" . (fboundp 'w3-mode)) + ("type" . "message/rfc-822"))) + ("rfc-*822" . (("viewer" . view-mode) + ("test" . (fboundp 'view-mode)) + ("type" . "message/rfc-822"))) + ("rfc-*822" . (("viewer" . fundamental-mode) + ("type" . "message/rfc-822"))) + )) + ("image" . ( + ("x-xwd" . (("viewer" . "xwud -in %s") + ("type" . "image/x-xwd") + ("compose" . "xwd -frame > %s") + ("test" . (eq (device-type) 'x)) + ("needsx11"))) + ("x11-dump" . (("viewer" . "xwud -in %s") + ("type" . "image/x-xwd") + ("compose" . "xwd -frame > %s") + ("test" . (eq (device-type) 'x)) + ("needsx11"))) + ("windowdump" . (("viewer" . "xwud -in %s") + ("type" . "image/x-xwd") + ("compose" . "xwd -frame > %s") + ("test" . (eq (device-type) 'x)) + ("needsx11"))) + (".*" . (("viewer" . "open %s") + ("type" . "image/*") + ("test" . (eq (device-type) 'ns)))) + (".*" . (("viewer" . "xv -perfect %s") + ("type" . "image/*") + ("test" . (eq (device-type) 'x)) + ("needsx11"))) + )) + ("text" . ( + ("plain" . (("viewer" . w3-mode) + ("test" . (fboundp 'w3-mode)) + ("type" . "text/plain"))) + ("plain" . (("viewer" . view-mode) + ("test" . (fboundp 'view-mode)) + ("type" . "text/plain"))) + ("plain" . (("viewer" . fundamental-mode) + ("type" . "text/plain"))) + ("enriched" . (("viewer" . enriched-decode-region) + ("test" . (fboundp + 'enriched-decode-region)) + ("type" . "text/enriched"))) + ("html" . (("viewer" . w3-prepare-buffer) + ("test" . (fboundp 'w3-prepare-buffer)) + ("type" . "text/html"))) + )) + ("video" . ( + ("mpeg" . (("viewer" . "mpeg_play %s") + ("type" . "video/mpeg") + ("test" . (eq (device-type) 'x)) + ("needsx11"))) + )) + ("x-world" . ( + ("x-vrml" . (("viewer" . "webspace -remote %s -URL %u") + ("type" . "x-world/x-vrml") + ("description" + "VRML document"))))) + ("archive" . ( + ("tar" . (("viewer" . tar-mode) + ("type" . "archive/tar") + ("test" . (fboundp 'tar-mode)))) + )) + ) + "*The mailcap structure is an assoc list of assoc lists. +1st assoc list is keyed on the major content-type +2nd assoc list is keyed on the minor content-type (which can be a regexp) + +Which looks like: +----------------- +( + (\"application\" + (\"postscript\" . <info>) + ) + (\"text\" + (\"plain\" . <info>) + ) +) + +Where <info> is another assoc list of the various information +related to the mailcap RFC. This is keyed on the lowercase +attribute name (viewer, test, etc). This looks like: +((\"viewer\" . viewerinfo) + (\"test\" . testinfo) + (\"xxxx\" . \"string\") +) + +Where viewerinfo specifies how the content-type is viewed. Can be +a string, in which case it is run through a shell, with +appropriate parameters, or a symbol, in which case the symbol is +funcall'd, with the buffer as an argument. + +testinfo is a list of strings, or nil. If nil, it means the +viewer specified is always valid. If it is a list of strings, +these are used to determine whether a viewer passes the 'test' or +not.") + +(defvar mm-content-transfer-encodings + '(("base64" . base64-decode) + ("7bit" . ignore) + ("8bit" . ignore) + ("binary" . ignore) + ("x-compress" . ("uncompress" "-c")) + ("x-gzip" . ("gzip" "-dc")) + ("compress" . ("uncompress" "-c")) + ("gzip" . ("gzip" "-dc")) + ("x-hqx" . ("mcvert" "-P" "-s" "-S")) + ("quoted-printable" . mm-decode-quoted-printable) + ) + "*An assoc list of content-transfer-encodings and how to decode them.") + +(defvar mm-download-directory nil + "*Where downloaded files should go by default.") + +(defvar mm-temporary-directory "/tmp" + "*Where temporary files go.") + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; A few things from w3 and url, just in case this is used without them +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defun mm-generate-unique-filename (&optional fmt) + "Generate a unique filename in mm-temporary-directory" + (if (not fmt) + (let ((base (format "mm-tmp.%d" (user-real-uid))) + (fname "") + (x 0)) + (setq fname (format "%s%d" base x)) + (while (file-exists-p + (expand-file-name fname mm-temporary-directory)) + (setq x (1+ x) + fname (concat base (int-to-string x)))) + (expand-file-name fname mm-temporary-directory)) + (let ((base (concat "mm" (int-to-string (user-real-uid)))) + (fname "") + (x 0)) + (setq fname (format fmt (concat base (int-to-string x)))) + (while (file-exists-p + (expand-file-name fname mm-temporary-directory)) + (setq x (1+ x) + fname (format fmt (concat base (int-to-string x))))) + (expand-file-name fname mm-temporary-directory)))) + +(if (not (fboundp 'copy-tree)) + (defun copy-tree (tree) + (if (consp tree) + (cons (copy-tree (car tree)) + (copy-tree (cdr tree))) + (if (vectorp tree) + (let* ((new (copy-sequence tree)) + (i (1- (length new)))) + (while (>= i 0) + (aset new i (copy-tree (aref new i))) + (setq i (1- i))) + new) + tree)))) + +(if (not (fboundp 'w3-save-binary-file)) + (defun mm-save-binary-file () + (let ((x (read-file-name "Filename to save as: " + (or mm-download-directory "~/"))) + (require-final-newline nil)) + (save-excursion + (if (featurep 'mule) + (let ((mc-flag t)) + (write-region (point-min) (point-max) x nil nil *noconv*)) + (write-region (point-min) (point-max) x)) + (kill-buffer (current-buffer))))) + (fset 'mm-save-binary-file 'w3-save-binary-file)) + +(if (not (fboundp 'w3-maybe-eval)) + (defun mm-maybe-eval () + "Maybe evaluate a buffer of emacs lisp code" + (if (yes-or-no-p "This is emacs-lisp code, evaluate it? ") + (eval-buffer (current-buffer)) + (emacs-lisp-mode))) + (fset 'mm-maybe-eval 'w3-maybe-eval)) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; The mailcap parser +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mm-in-assoc (elt list) + ;; Check to see if ELT matches any of the regexps in the car elements of LIST + (let (rslt) + (while (and list (not rslt)) + (and (car (car list)) + (string-match (car (car list)) elt) + (setq rslt (car list))) + (setq list (cdr list))) + rslt)) + +(defun mm-replace-regexp (regexp to-string) + ;; Quiet replace-regexp. + (goto-char (point-min)) + (while (re-search-forward regexp nil t) + (replace-match to-string t nil))) + +(defun mm-parse-mailcaps (&optional path) + ;; Parse out all the mailcaps specified in a unix-style path string PATH + (cond + (path nil) + ((getenv "MAILCAPS") (setq path (getenv "MAILCAPS"))) + ((memq system-type '(ms-dos ms-windows windows-nt)) + (setq path (mapconcat 'expand-file-name '("~/mail.cap" "~/etc/mail.cap") + ";"))) + (t (setq path (concat "/etc/mailcap:/usr/etc/mailcap:" + "/usr/local/etc/mailcap:" + (expand-file-name "~/.mailcap"))))) + (let ((fnames (mm-string-to-tokens path + (if (memq system-type + '(ms-dos ms-windows windows-nt)) + ?; + ?:))) fname) + (while fnames + (setq fname (car fnames)) + (if (and (file-exists-p fname) (file-readable-p fname)) + (mm-parse-mailcap (car fnames))) + (setq fnames (cdr fnames))))) + +(defun mm-parse-mailcap (fname) + ;; Parse out the mailcap file specified by FNAME + (let (major ; The major mime type (image/audio/etc) + minor ; The minor mime type (gif, basic, etc) + save-pos ; Misc saved positions used in parsing + viewer ; How to view this mime type + info ; Misc info about this mime type + ) + (save-excursion + (set-buffer (get-buffer-create " *mailcap*")) + (erase-buffer) + (insert-file-contents fname) + (set-syntax-table mm-parse-args-syntax-table) + (mm-replace-regexp "#.*" "") ; Remove all comments + (mm-replace-regexp "\n+" "\n") ; And blank lines + (mm-replace-regexp "\\\\[ \t\n]+" " ") ; And collapse spaces + (mm-replace-regexp (concat (regexp-quote "\\") "[ \t]*\n") "") + (goto-char (point-max)) + (skip-chars-backward " \t\n") + (delete-region (point) (point-max)) + (goto-char (point-min)) + (while (not (eobp)) + (skip-chars-forward " \t\n") + (setq save-pos (point) + info nil) + (skip-chars-forward "^/;") + (downcase-region save-pos (point)) + (setq major (buffer-substring save-pos (point))) + (skip-chars-forward "/ \t\n") + (setq save-pos (point)) + (skip-chars-forward "^;") + (downcase-region save-pos (point)) + (setq minor + (cond + ((= ?* (or (char-after save-pos) 0)) ".*") + ((= (point) save-pos) ".*") + (t (buffer-substring save-pos (point))))) + (skip-chars-forward "; \t\n") + ;;; Got the major/minor chunks, now for the viewers/etc + ;;; The first item _must_ be a viewer, according to the + ;;; RFC for mailcap files (#1343) + (skip-chars-forward "; \t\n") + (setq save-pos (point)) + (skip-chars-forward "^;\n") + (if (= (or (char-after save-pos) 0) ?') + (setq viewer (progn + (narrow-to-region (1+ save-pos) (point)) + (goto-char (point-min)) + (prog1 + (read (current-buffer)) + (goto-char (point-max)) + (widen)))) + (setq viewer (buffer-substring save-pos (point)))) + (setq save-pos (point)) + (end-of-line) + (setq info (nconc (list (cons "viewer" viewer) + (cons "type" (concat major "/" + (if (string= minor ".*") + "*" minor)))) + (mm-parse-mailcap-extras save-pos (point)))) + (mm-mailcap-entry-passes-test info) + (mm-add-mailcap-entry major minor info))))) + +(defun mm-parse-mailcap-extras (st nd) + ;; Grab all the extra stuff from a mailcap entry + (let ( + name ; From name= + value ; its value + results ; Assoc list of results + name-pos ; Start of XXXX= position + val-pos ; Start of value position + done ; Found end of \'d ;s? + ) + (save-restriction + (narrow-to-region st nd) + (goto-char (point-min)) + (skip-chars-forward " \n\t;") + (while (not (eobp)) + (setq done nil) + (skip-chars-forward " \";\n\t") + (setq name-pos (point)) + (skip-chars-forward "^ \n\t=") + (downcase-region name-pos (point)) + (setq name (buffer-substring name-pos (point))) + (skip-chars-forward " \t\n") + (if (/= (or (char-after (point)) 0) ?=) ; There is no value + (setq value nil) + (skip-chars-forward " \t\n=") + (setq val-pos (point)) + (if (memq (char-after val-pos) '(?\" ?')) + (progn + (setq val-pos (1+ val-pos)) + (condition-case nil + (progn + (forward-sexp 1) + (backward-char 1)) + (error (goto-char (point-max))))) + (while (not done) + (skip-chars-forward "^;") + (if (= (or (char-after (1- (point))) 0) ?\\ ) + (progn + (subst-char-in-region (1- (point)) (point) ?\\ ? ) + (skip-chars-forward ";")) + (setq done t)))) + (setq value (buffer-substring val-pos (point)))) + (setq results (cons (cons name value) results))) + results))) + +(defun mm-string-to-tokens (str &optional delim) + "Return a list of words from the string STR" + (setq delim (or delim ? )) + (let (results y) + (mapcar + (function + (lambda (x) + (cond + ((and (= x delim) y) (setq results (cons y results) y nil)) + ((/= x delim) (setq y (concat y (char-to-string x)))) + (t nil)))) str) + (nreverse (cons y results)))) + +(defun mm-mailcap-entry-passes-test (info) + ;; Return t iff a mailcap entry passes its test clause or no test + ;; clause is present. + (let (status ; Call-process-regions return value + (test (assoc "test" info)); The test clause + ) + (setq status (and test (mm-string-to-tokens (cdr test)))) + (if (and (assoc "needsx11" info) (not (getenv "DISPLAY"))) + (setq status nil) + (cond + ((and (equal (nth 0 status) "test") + (equal (nth 1 status) "-n") + (or (equal (nth 2 status) "$DISPLAY") + (equal (nth 2 status) "\"$DISPLAY\""))) + (setq status (if (getenv "DISPLAY") t nil))) + ((and (equal (nth 0 status) "test") + (equal (nth 1 status) "-z") + (or (equal (nth 2 status) "$DISPLAY") + (equal (nth 2 status) "\"$DISPLAY\""))) + (setq status (if (getenv "DISPLAY") nil t))) + (test nil) + (t nil))) + (and test (listp test) (setcdr test status)))) + +(defun mm-parse-args (st &optional nd nodowncase) + ;; Return an assoc list of attribute/value pairs from an RFC822-type string + (let ( + name ; From name= + value ; its value + results ; Assoc list of results + name-pos ; Start of XXXX= position + val-pos ; Start of value position + ) + (save-excursion + (if (stringp st) + (progn + (set-buffer (get-buffer-create " *mm-temp*")) + (set-syntax-table mm-parse-args-syntax-table) + (erase-buffer) + (insert st) + (setq st (point-min) + nd (point-max))) + (set-syntax-table mm-parse-args-syntax-table)) + (save-restriction + (narrow-to-region st nd) + (goto-char (point-min)) + (while (not (eobp)) + (skip-chars-forward "; \n\t") + (setq name-pos (point)) + (skip-chars-forward "^ \n\t=;") + (if (not nodowncase) + (downcase-region name-pos (point))) + (setq name (buffer-substring name-pos (point))) + (skip-chars-forward " \t\n") + (if (/= (or (char-after (point)) 0) ?=) ; There is no value + (setq value nil) + (skip-chars-forward " \t\n=") + (setq val-pos (point) + value + (cond + ((or (= (or (char-after val-pos) 0) ?\") + (= (or (char-after val-pos) 0) ?')) + (buffer-substring (1+ val-pos) + (condition-case () + (prog2 + (forward-sexp 1) + (1- (point)) + (skip-chars-forward "\"")) + (error + (skip-chars-forward "^ \t\n") + (point))))) + (t + (buffer-substring val-pos + (progn + (skip-chars-forward "^;") + (skip-chars-backward " \t") + (point))))))) + (setq results (cons (cons name value) results)) + (skip-chars-forward "; \n\t")) + results)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; The action routines. +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mm-possible-viewers (major minor) + ;; Return a list of possible viewers from MAJOR for minor type MINOR + (let ((exact '()) + (wildcard '())) + (while major + (cond + ((equal (car (car major)) minor) + (setq exact (cons (cdr (car major)) exact))) + ((string-match (car (car major)) minor) + (setq wildcard (cons (cdr (car major)) wildcard)))) + (setq major (cdr major))) + (nconc (nreverse exact) (nreverse wildcard)))) + +(defun mm-unescape-mime-test (test type-info) + (let ((buff (get-buffer-create " *unescape*")) + save-pos save-chr subst) + (cond + ((symbolp test) test) + ((and (listp test) (symbolp (car test))) test) + ((or (stringp test) + (and (listp test) (stringp (car test)) + (setq test (mapconcat 'identity test " ")))) + (save-excursion + (set-buffer buff) + (erase-buffer) + (insert test) + (goto-char (point-min)) + (while (not (eobp)) + (skip-chars-forward "^%") + (if (/= (- (point) + (progn (skip-chars-backward "\\\\") + (point))) + 0) ; It is an escaped % + (progn + (delete-char 1) + (skip-chars-forward "%.")) + (setq save-pos (point)) + (skip-chars-forward "%") + (setq save-chr (char-after (point))) + (cond + ((null save-chr) nil) + ((= save-chr ?t) + (delete-region save-pos (progn (forward-char 1) (point))) + (insert (or (cdr (assoc "type" type-info)) "\"\""))) + ((= save-chr ?M) + (delete-region save-pos (progn (forward-char 1) (point))) + (insert "\"\"")) + ((= save-chr ?n) + (delete-region save-pos (progn (forward-char 1) (point))) + (insert "\"\"")) + ((= save-chr ?F) + (delete-region save-pos (progn (forward-char 1) (point))) + (insert "\"\"")) + ((= save-chr ?{) + (forward-char 1) + (skip-chars-forward "^}") + (downcase-region (+ 2 save-pos) (point)) + (setq subst (buffer-substring (+ 2 save-pos) (point))) + (delete-region save-pos (1+ (point))) + (insert (or (cdr (assoc subst type-info)) "\"\""))) + (t nil)))) + (buffer-string))) + (t (error "Bad value to mm-unescape-mime-test. %s" test))))) + +(defun mm-viewer-passes-test (viewer-info type-info) + ;; Return non-nil iff the viewer specified by VIEWER-INFO passes its + ;; test clause (if any). + (let* ((test-info (assoc "test" viewer-info)) + (test (cdr test-info)) + (viewer (cdr (assoc "viewer" viewer-info))) + status + parsed-test + ) + (cond + ((not test-info) t) ; No test clause + ((not test) nil) ; Already failed test + ((eq test t) t) ; Already passed test + ((and (symbolp test) ; Lisp function as test + (fboundp test)) + (funcall test type-info)) + ((and (symbolp test) ; Lisp variable as test + (boundp test)) + (symbol-value test)) + ((and (listp test) ; List to be eval'd + (symbolp (car test))) + (eval test)) + (t + (setq test (mm-unescape-mime-test test type-info) + test (list "/bin/sh" nil nil nil "-c" test) + status (apply 'call-process test)) + (= 0 status))))) + +(defun mm-add-mailcap-entry (major minor info) + (let ((old-major (assoc major mm-mime-data))) + (if (null old-major) ; New major area + (setq mm-mime-data + (cons (cons major (list (cons minor info))) + mm-mime-data)) + (let ((cur-minor (assoc minor old-major))) + (cond + ((or (null cur-minor) ; New minor area, or + (assoc "test" info)) ; Has a test, insert at beginning + (setcdr old-major (cons (cons minor info) (cdr old-major)))) + ((and (not (assoc "test" info)); No test info, replace completely + (not (assoc "test" cur-minor))) + (setcdr cur-minor info)) + (t + (setcdr old-major (cons (cons minor info) (cdr old-major))))))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; The main whabbo +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mm-viewer-lessp (x y) + ;; Return t iff viewer X is more desirable than viewer Y + (let ((x-wild (string-match "[*?]" (or (cdr-safe (assoc "type" x)) ""))) + (y-wild (string-match "[*?]" (or (cdr-safe (assoc "type" y)) ""))) + (x-lisp (not (stringp (or (cdr-safe (assoc "viewer" x)) "")))) + (y-lisp (not (stringp (or (cdr-safe (assoc "viewer" y)) ""))))) + (cond + ((and x-lisp (not y-lisp)) + t) + ((and (not y-lisp) x-wild (not y-wild)) + t) + ((and (not x-wild) y-wild) + t) + (t nil)))) + +(defun mm-mime-info (st &optional nd request) + "Get the mime viewer command for HEADERLINE, return nil if none found. +Expects a complete content-type header line as its argument. This can +be simple like text/html, or complex like text/plain; charset=blah; foo=bar + +Third argument REQUEST specifies what information to return. If it is +nil or the empty string, the viewer (second field of the mailcap +entry) will be returned. If it is a string, then the mailcap field +corresponding to that string will be returned (print, description, +whatever). If a number, then all the information for this specific +viewer is returned." + (let ( + major ; Major encoding (text, etc) + minor ; Minor encoding (html, etc) + info ; Other info + save-pos ; Misc. position during parse + major-info ; (assoc major mm-mime-data) + minor-info ; (assoc minor major-info) + test ; current test proc. + viewers ; Possible viewers + passed ; Viewers that passed the test + viewer ; The one and only viewer + ) + (save-excursion + (cond + ((null st) + (set-buffer (get-buffer-create " *mimeparse*")) + (erase-buffer) + (insert "text/plain") + (setq st (point-min))) + ((stringp st) + (set-buffer (get-buffer-create " *mimeparse*")) + (erase-buffer) + (insert st) + (setq st (point-min))) + ((null nd) + (narrow-to-region st (progn (goto-char st) (end-of-line) (point)))) + (t (narrow-to-region st nd))) + (goto-char st) + (skip-chars-forward ": \t\n") + (buffer-enable-undo) + (setq viewer + (catch 'mm-exit + (setq save-pos (point)) + (skip-chars-forward "^/") + (downcase-region save-pos (point)) + (setq major (buffer-substring save-pos (point))) + (if (not (setq major-info (cdr (assoc major mm-mime-data)))) + (throw 'mm-exit nil)) + (skip-chars-forward "/ \t\n") + (setq save-pos (point)) + (skip-chars-forward "^ \t\n;") + (downcase-region save-pos (point)) + (setq minor (buffer-substring save-pos (point))) + (if (not + (setq viewers (mm-possible-viewers major-info minor))) + (throw 'mm-exit nil)) + (skip-chars-forward "; \t") + (if (eolp) + nil ; No qualifiers + (setq save-pos (point)) + (end-of-line) + (setq info (mm-parse-args save-pos (point))) + ) + (while viewers + (if (mm-viewer-passes-test (car viewers) info) + (setq passed (cons (car viewers) passed))) + (setq viewers (cdr viewers))) + (setq passed (sort (nreverse passed) 'mm-viewer-lessp)) + (car passed))) + (if (and (stringp (cdr (assoc "viewer" viewer))) + passed) + (setq viewer (car passed))) + (widen) + (cond + ((and (null viewer) (not (equal major "default"))) + (mm-mime-info "default" nil request)) + ((or (null request) (equal request "")) + (mm-unescape-mime-test (cdr (assoc "viewer" viewer)) info)) + ((stringp request) + (if (or (string= request "test") (string= request "viewer")) + (mm-unescape-mime-test (cdr-safe (assoc request viewer)) info))) + (t + ;; MUST make a copy *sigh*, else we modify mm-mime-data + (setq viewer (copy-tree viewer)) + (let ((view (assoc "viewer" viewer)) + (test (assoc "test" viewer))) + (if view (setcdr view (mm-unescape-mime-test (cdr view) info))) + (if test (setcdr test (mm-unescape-mime-test (cdr test) info)))) + viewer))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Experimental MIME-types parsing +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defvar mm-mime-extensions + '( + ("" . "text/plain") + (".abs" . "audio/x-mpeg") + (".aif" . "audio/aiff") + (".aifc" . "audio/aiff") + (".aiff" . "audio/aiff") + (".ano" . "application/x-annotator") + (".au" . "audio/ulaw") + (".avi" . "video/x-msvideo") + (".bcpio" . "application/x-bcpio") + (".bin" . "application/octet-stream") + (".cdf" . "application/x-netcdr") + (".cpio" . "application/x-cpio") + (".csh" . "application/x-csh") + (".dvi" . "application/x-dvi") + (".el" . "application/emacs-lisp") + (".eps" . "application/postscript") + (".etx" . "text/x-setext") + (".exe" . "application/octet-stream") + (".fax" . "image/x-fax") + (".gif" . "image/gif") + (".hdf" . "application/x-hdf") + (".hqx" . "application/mac-binhex40") + (".htm" . "text/html") + (".html" . "text/html") + (".icon" . "image/x-icon") + (".ief" . "image/ief") + (".jpg" . "image/jpeg") + (".macp" . "image/x-macpaint") + (".man" . "application/x-troff-man") + (".me" . "application/x-troff-me") + (".mif" . "application/mif") + (".mov" . "video/quicktime") + (".movie" . "video/x-sgi-movie") + (".mp2" . "audio/x-mpeg") + (".mp2a" . "audio/x-mpeg2") + (".mpa" . "audio/x-mpeg") + (".mpa2" . "audio/x-mpeg2") + (".mpe" . "video/mpeg") + (".mpeg" . "video/mpeg") + (".mpega" . "audio/x-mpeg") + (".mpegv" . "video/mpeg") + (".mpg" . "video/mpeg") + (".mpv" . "video/mpeg") + (".ms" . "application/x-troff-ms") + (".nc" . "application/x-netcdf") + (".nc" . "application/x-netcdf") + (".oda" . "application/oda") + (".pbm" . "image/x-portable-bitmap") + (".pdf" . "application/pdf") + (".pgm" . "image/portable-graymap") + (".pict" . "image/pict") + (".pnm" . "image/x-portable-anymap") + (".ppm" . "image/portable-pixmap") + (".ps" . "application/postscript") + (".qt" . "video/quicktime") + (".ras" . "image/x-raster") + (".rgb" . "image/x-rgb") + (".rtf" . "application/rtf") + (".rtx" . "text/richtext") + (".sh" . "application/x-sh") + (".sit" . "application/x-stuffit") + (".snd" . "audio/basic") + (".src" . "application/x-wais-source") + (".tar" . "archive/tar") + (".tcl" . "application/x-tcl") + (".tcl" . "application/x-tcl") + (".tex" . "application/x-tex") + (".texi" . "application/texinfo") + (".tga" . "image/x-targa") + (".tif" . "image/tiff") + (".tiff" . "image/tiff") + (".tr" . "application/x-troff") + (".troff" . "application/x-troff") + (".tsv" . "text/tab-separated-values") + (".txt" . "text/plain") + (".vbs" . "video/mpeg") + (".vox" . "audio/basic") + (".vrml" . "x-world/x-vrml") + (".wav" . "audio/x-wav") + (".wrl" . "x-world/x-vrml") + (".xbm" . "image/xbm") + (".xpm" . "image/x-pixmap") + (".xwd" . "image/windowdump") + (".zip" . "application/zip") + (".ai" . "application/postscript") + (".jpe" . "image/jpeg") + (".jpeg" . "image/jpeg") + ) + "*An assoc list of file extensions and the MIME content-types they +correspond to.") + +(defun mm-parse-mimetypes (&optional path) + ;; Parse out all the mimetypes specified in a unix-style path string PATH + (cond + (path nil) + ((getenv "MIMETYPES") (setq path (getenv "MIMETYPES"))) + ((memq system-type '(ms-dos ms-windows windows-nt)) + (setq path (mapconcat 'expand-file-name + '("~/mime.typ" "~/etc/mime.typ") ";"))) + (t (setq path (concat (expand-file-name "~/.mime-types") ":" + "/etc/mime-types:/usr/etc/mime-types:" + "/usr/local/etc/mime-types:" + "/usr/local/www/conf/mime-types")))) + (let ((fnames (mm-string-to-tokens path + (if (memq system-type + '(ms-dos ms-windows windows-nt)) + ?; + ?:))) fname) + (while fnames + (setq fname (car fnames)) + (if (and (file-exists-p fname) (file-readable-p fname)) + (mm-parse-mimetype-file (car fnames))) + (setq fnames (cdr fnames))))) + +(defun mm-parse-mimetype-file (fname) + ;; Parse out a mime-types file + (let (type ; The MIME type for this line + extns ; The extensions for this line + save-pos ; Misc. saved buffer positions + ) + (save-excursion + (set-buffer (get-buffer-create " *mime-types*")) + (erase-buffer) + (insert-file-contents fname) + (mm-replace-regexp "#.*" "") + (mm-replace-regexp "\n+" "\n") + (mm-replace-regexp "[ \t]+$" "") + (goto-char (point-max)) + (skip-chars-backward " \t\n") + (delete-region (point) (point-max)) + (goto-char (point-min)) + (while (not (eobp)) + (skip-chars-forward " \t\n") + (setq save-pos (point)) + (skip-chars-forward "^ \t") + (downcase-region save-pos (point)) + (setq type (buffer-substring save-pos (point))) + (while (not (eolp)) + (skip-chars-forward " \t") + (setq save-pos (point)) + (skip-chars-forward "^ \t\n") + (setq extns (cons (buffer-substring save-pos (point)) extns))) + (while extns + (setq mm-mime-extensions + (cons + (cons (if (= (string-to-char (car extns)) ?.) + (car extns) + (concat "." (car extns))) type) mm-mime-extensions) + extns (cdr extns))))))) + +(defun mm-extension-to-mime (extn) + "Return the MIME content type of the file extensions EXTN" + (if (and (stringp extn) + (not (= (string-to-char extn) ?.))) + (setq extn (concat "." extn))) + (cdr (assoc (downcase extn) mm-mime-extensions))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Editing/Composition of body parts +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mm-compose-type (type) + ;; Compose a body section of MIME-type TYPE. + (let* ((info (mm-mime-info type nil 5)) + (fnam (mm-generate-unique-filename)) + (comp (or (cdr (assoc "compose" info)))) + (ctyp (cdr (assoc "composetyped" info))) + (buff (get-buffer-create " *mimecompose*")) + (typeit (not ctyp)) + (retval "") + (usef nil)) + (setq comp (mm-unescape-mime-test (or comp ctyp) info)) + (while (string-match "\\([^\\\\]\\)%s" comp) + (setq comp (concat (substring comp 0 (match-end 1)) fnam + (substring comp (match-end 0) nil)) + usef t)) + (call-process (or shell-file-name + (getenv "ESHELL") (getenv "SHELL") "/bin/sh") + nil (if usef nil buff) nil "-c" comp) + (setq retval + (concat + (if typeit (concat "Content-type: " type "\r\n\r\n") "") + (if usef + (save-excursion + (set-buffer buff) + (erase-buffer) + (insert-file-contents fnam) + (buffer-string)) + (save-excursion + (set-buffer buff) + (buffer-string))) + "\r\n")) + retval)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Misc. +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mm-type-to-file (type) + "Return the file extension for content-type TYPE" + (rassoc type mm-mime-extensions)) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Miscellaneous MIME viewers written in elisp +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mm-play-sound-file (&optional buff) + "Play a sound file in buffer BUFF (defaults to current buffer)" + (setq buff (or buff (current-buffer))) + (let ((fname (mm-generate-unique-filename "%s.au")) + (synchronous-sounds t)) ; Play synchronously + (if (featurep 'mule) + (write-region (point-min) (point-max) fname nil nil *noconv*) + (write-region (point-min) (point-max) fname)) + (kill-buffer (current-buffer)) + (play-sound-file fname) + (condition-case () + (delete-file fname) + (error nil)))) + +(defun mm-parse-mime-headers (&optional no-delete) + "Return a list of the MIME headers at the top of this buffer. If +optional argument NO-DELETE is non-nil, don't delete the headers." + (let* ((st (point-min)) + (nd (progn + (goto-char (point-min)) + (skip-chars-forward " \t\n") + (if (re-search-forward "^\r*$" nil t) + (1+ (point)) + (point-max)))) + save-pos + status + hname + hvalu + result + ) + (narrow-to-region st nd) + (goto-char (point-min)) + (while (not (eobp)) + (skip-chars-forward " \t\n\r") + (setq save-pos (point)) + (skip-chars-forward "^:\n\r") + (downcase-region save-pos (point)) + (setq hname (buffer-substring save-pos (point))) + (skip-chars-forward ": \t ") + (setq save-pos (point)) + (skip-chars-forward "^\n\r") + (setq hvalu (buffer-substring save-pos (point)) + result (cons (cons hname hvalu) result))) + (or no-delete (delete-region st nd)) + result)) + +(defun mm-find-available-multiparts (separator &optional buf) + "Return a list of mime-headers for the various body parts of a +multipart message in buffer BUF with separator SEPARATOR. +The different multipart specs are put in `mm-temporary-directory'." + (let ((sep (concat "^--" separator "\r*$")) + headers + fname + results) + (save-excursion + (and buf (set-buffer buf)) + (goto-char (point-min)) + (while (re-search-forward sep nil t) + (let ((st (set-marker (make-marker) + (progn + (forward-line 1) + (beginning-of-line) + (point)))) + (nd (set-marker (make-marker) + (if (re-search-forward sep nil t) + (1- (match-beginning 0)) + (point-max))))) + (narrow-to-region st nd) + (goto-char st) + (if (looking-at "^\r*$") + (insert "Content-type: text/plain\n" + "Content-length: " (int-to-string (- nd st)) "\n")) + (setq headers (mm-parse-mime-headers) + fname (mm-generate-unique-filename)) + (let ((x (or (cdr (assoc "content-type" headers)) "text/plain"))) + (if (string-match "name=\"*\\([^ \"]+\\)\"*" x) + (setq fname (expand-file-name + (substring x (match-beginning 1) + (match-end 1)) + mm-temporary-directory)))) + (widen) + (if (assoc "content-transfer-encoding" headers) + (let ((coding (cdr + (assoc "content-transfer-encoding" headers))) + (cmd nil)) + (setq coding (and coding (downcase coding)) + cmd (or (cdr (assoc coding + mm-content-transfer-encodings)) + (read-string + (concat "How shall I decode " coding "? ") + "cat"))) + (if (string= cmd "") (setq cmd "cat")) + (if (stringp cmd) + (shell-command-on-region st nd cmd t) + (funcall cmd st nd)) + (set-marker nd (point)))) + (write-region st nd fname nil 5) + (delete-region st nd) + (setq results (cons + (cons + (cons "mm-filename" fname) headers) results))))) + results)) + +(defun mm-format-multipart-as-html (&optional buf type) + (if buf (set-buffer buf)) + (let* ((boundary (if (string-match + "boundary[ \t]*=[ \t\"]*\\([^ \"\t\n]+\\)" + type) + (regexp-quote + (substring type (match-beginning 1) (match-end 1))))) + (parts (mm-find-available-multiparts boundary))) + (erase-buffer) + (insert "<html>\n" + " <head>\n" + " <title>Multipart Message</title>\n" + " </head>\n" + " <body>\n" + " <h1> Multipart message encountered </h1>\n" + " <p> I have encountered a multipart MIME message.\n" + " The following parts have been detected. Please\n" + " select which one you want to view.\n" + " </p>\n" + " <ul>\n" + (mapconcat + (function (lambda (x) + (concat " <li> <a href=\"file:" + (cdr (assoc "mm-filename" x)) + "\">" + (or (cdr (assoc "content-description" x)) "") + "--" + (or (cdr (assoc "content-type" x)) + "unknown type") + "</a> </li>"))) + parts "\n") + " </ul>\n" + " </body>\n" + "</html>\n" + "<!-- Automatically generated by MM v" mm-version "-->\n"))) + +(defun mm-multipart-viewer () + (mm-format-multipart-as-html + (current-buffer) + (cdr (assoc "content-type" url-current-mime-headers))) + (let ((w3-working-buffer (current-buffer))) + (w3-prepare-buffer))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Transfer encodings we can decrypt automatically +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defun mm-decode-quoted-printable (&optional st nd) + (interactive) + (setq st (or st (point-min)) + nd (or nd (point-max))) + (save-restriction + (narrow-to-region st nd) + (save-excursion + (let ((buffer-read-only nil)) + (goto-char (point-min)) + (while (re-search-forward "=[0-9A-F][0-9A-F]" nil t) + (replace-match + (char-to-string + (+ + (* 16 (mm-hex-char-to-integer + (char-after (1+ (match-beginning 0))))) + (mm-hex-char-to-integer + (char-after (1- (match-end 0)))))))))))) + +;; Taken from hexl.el. +(defun mm-hex-char-to-integer (character) + "Take a char and return its value as if it was a hex digit." + (if (and (>= character ?0) (<= character ?9)) + (- character ?0) + (let ((ch (logior character 32))) + (if (and (>= ch ?a) (<= ch ?f)) + (- ch (- ?a 10)) + (error (format "Invalid hex digit `%c'." ch)))))) + + +(require 'base64) +(provide 'mm)