;;; mailcap.el --- MIME media types configuration -*- lexical-binding: t -*- ;; Copyright (C) 1998-2024 Free Software Foundation, Inc. ;; Author: William M. Perry ;; Lars Magne Ingebrigtsen ;; Keywords: news, mail, multimedia ;; This file is part of GNU Emacs. ;; 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 3 of the License, 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. If not, see . ;;; Commentary: ;; Provides configuration of MIME media types from directly from Lisp ;; and via the usual mailcap mechanism (RFC 1524). Deals with ;; mime.types similarly. ;;; Code: (require 'cl-lib) (autoload 'mail-header-parse-content-type "mail-parse") (defgroup mailcap nil "Definition of viewers for MIME types." :version "21.1" :group 'mime) (defcustom mailcap-prefer-mailcap-viewers t "If non-nil, prefer viewers specified in ~/.mailcap. If nil, the most specific viewer will be chosen, even if there is a general override in ~/.mailcap. For instance, if /etc/mailcap has an entry for \"image/gif\", that one will be chosen even if you have an entry for \"image/*\" in your ~/.mailcap file." :type 'boolean) (defvar mailcap-parse-args-syntax-table (let ((table (copy-syntax-table emacs-lisp-mode-syntax-table))) (modify-syntax-entry ?' "\"" table) (modify-syntax-entry ?` "\"" table) (modify-syntax-entry ?{ "(" table) (modify-syntax-entry ?} ")" table) table) "A syntax table for parsing SGML attributes.") (defvar mailcap-print-command (mapconcat #'identity (cons (if (boundp 'lpr-command) lpr-command "lpr") (when (boundp 'lpr-switches) (if (stringp lpr-switches) (list lpr-switches) lpr-switches))) " ") "Shell command (including switches) used to print PostScript files.") (defun mailcap--get-user-mime-data (sym) (let ((val (default-value sym)) res) (dolist (entry val) (push (list (cdr (assq 'viewer entry)) (cdr (assq 'type entry)) (cdr (assq 'test entry))) res)) (nreverse res))) (defun mailcap--set-user-mime-data (sym val) (let (res) (pcase-dolist (`(,viewer ,type ,test) val) (push `((viewer . ,viewer) (type . ,type) ,@(when test `((test . ,test)))) res)) (set-default sym (nreverse res)))) (defcustom mailcap-user-mime-data nil "A list of viewers preferred for different MIME types. The elements of the list are lists of the following structure (VIEWER MIME-TYPE TEST) where VIEWER is either a Lisp command, e.g., a major mode, or a string containing a shell command for viewing files of the defined MIME-TYPE. In case of a shell command, %s will be replaced with the file. MIME-TYPE is a regular expression being matched against the actual MIME type. It is implicitly surrounded with ^ and $. TEST is a Lisp form which is evaluated in order to test if the entry should be chosen. The `test' entry is optional. When selecting a viewer for a given MIME type, the first viewer in this list with a matching MIME-TYPE and successful TEST is selected. Only if none matches, the standard `mailcap-mime-data' is consulted." :version "26.1" :type '(repeat (list (choice (function :tag "Function or mode") (string :tag "Shell command")) (regexp :tag "MIME Type") (sexp :tag "Test (optional)"))) :get #'mailcap--get-user-mime-data :set #'mailcap--set-user-mime-data) ;; Postpone using defcustom for this as it's so big and we essentially ;; have to have two copies of the data around then. Perhaps just ;; customize the Lisp viewers and rely on the normal configuration ;; files for the rest? -- fx (defvar mailcap-mime-data `(("application" ("vnd\\.ms-excel" (viewer . "gnumeric %s") (test . (getenv "DISPLAY")) (type . "application/vnd\\.ms-excel")) ("octet-stream" (viewer . mailcap-save-binary-file) (non-viewer . t) (type . "application/octet-stream")) ("dvi" (viewer . "xdvi -safer %s") (test . (eq window-system 'x)) ("needsx11") (type . "application/dvi") ("print" . "dvips -qRP %s")) ("dvi" (viewer . "dvitty %s") (test . (not (getenv "DISPLAY"))) (type . "application/dvi") ("print" . "dvips -qRP %s")) ("emacs-lisp" (viewer . mailcap-maybe-eval) (type . "application/emacs-lisp")) ("x-emacs-lisp" (viewer . mailcap-maybe-eval) (type . "application/x-emacs-lisp")) ("x-tar" (viewer . mailcap-save-binary-file) (non-viewer . t) (type . "application/x-tar")) ("x-latex" (viewer . tex-mode) (type . "application/x-latex")) ("x-tex" (viewer . tex-mode) (type . "application/x-tex")) ("latex" (viewer . tex-mode) (type . "application/latex")) ("tex" (viewer . tex-mode) (type . "application/tex")) ("texinfo" (viewer . texinfo-mode) (type . "application/tex")) ("zip" (viewer . mailcap-save-binary-file) (non-viewer . t) (type . "application/zip") ("copiousoutput")) ("pdf" (viewer . doc-view-mode) (type . "application/pdf") (test . window-system)) ("pdf" (viewer . pdf-view-mode) (type . "application/pdf") (test . window-system)) ("pdf" (viewer . "gv -safer %s") (type . "application/pdf") (test . window-system) ("print" . ,(concat "pdf2ps %s - | " mailcap-print-command))) ("pdf" (viewer . "gpdf %s") (type . "application/pdf") ("print" . ,(concat "pdftops %s - | " mailcap-print-command)) (test . (eq window-system 'x))) ("pdf" (viewer . "xpdf %s") (type . "application/pdf") ("print" . ,(concat "pdftops %s - | " mailcap-print-command)) (test . (eq window-system 'x))) ("pdf" (viewer . ,(concat "pdftotext %s -")) (type . "application/pdf") ("print" . ,(concat "pdftops %s - | " mailcap-print-command)) ("copiousoutput")) ("postscript" (viewer . "gv -safer %s") (type . "application/postscript") (test . window-system) ("print" . ,(concat mailcap-print-command " %s")) ("needsx11")) ("postscript" (viewer . "ghostview -dSAFER %s") (type . "application/postscript") (test . (eq window-system 'x)) ("print" . ,(concat mailcap-print-command " %s")) ("needsx11")) ("postscript" (viewer . "ps2ascii %s") (type . "application/postscript") (test . (not (getenv "DISPLAY"))) ("print" . ,(concat mailcap-print-command " %s")) ("copiousoutput")) ("sieve" (viewer . sieve-mode) (type . "application/sieve")) ("pgp-keys" (viewer . "gpg --import --interactive --verbose") (type . "application/pgp-keys") ("needsterminal"))) ("audio" ("x-mpeg" (viewer . "maplay %s") (type . "audio/x-mpeg")) (".*" (viewer . "showaudio") (type . "audio/*"))) ("message" ("rfc-*822" (viewer . mm-view-message) (test . (and (featurep 'gnus) (gnus-alive-p))) (type . "message/rfc822")) ("rfc-*822" (viewer . vm-mode) (type . "message/rfc822")) ("rfc-*822" (viewer . view-mode) (type . "message/rfc822"))) ("image" ("x-xwd" (viewer . "xwud -in %s") (type . "image/x-xwd") ("compose" . "xwd -frame > %s") (test . (eq window-system 'x)) ("needsx11")) ("x11-dump" (viewer . "xwud -in %s") (type . "image/x-xwd") ("compose" . "xwd -frame > %s") (test . (eq window-system 'x)) ("needsx11")) ("windowdump" (viewer . "xwud -in %s") (type . "image/x-xwd") ("compose" . "xwd -frame > %s") (test . (eq window-system 'x)) ("needsx11")) (".*" (viewer . "display %s") (type . "image/*") (test . (eq window-system 'x)) ("needsx11"))) ("text" ("plain" (viewer . view-mode) (type . "text/plain")) ("plain" (viewer . fundamental-mode) (type . "text/plain")) ("enriched" (viewer . enriched-decode) (type . "text/enriched")) ("dns" (viewer . dns-mode) (type . "text/dns"))) ("video" ("mpeg" (viewer . "mpeg_play %s") (type . "video/mpeg") (test . (eq window-system '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")))) "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\" . )) (\"text\" (\"plain\" . ))) Where is another assoc list of the various information related to the mailcap RFC 1524. This is keyed on the lowercase attribute name (viewer, test, etc). This looks like: ((viewer . VIEWERINFO) (test . TESTINFO) (xxxx . \"STRING\") FLAG) 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 must name a function of zero arguments which is called in a buffer holding the MIME part's content. TESTINFO is a test for the viewer's applicability, or nil. If nil, it means the viewer is always valid. If it is a Lisp function, it is called with a list of items from any extra fields from the Content-Type header as argument to return a boolean value for the validity. Otherwise, if it is a non-function Lisp symbol or list whose car is a symbol, it is `eval'uated to yield the validity. If it is a string or list of strings, it represents a shell command to run to return a true or false shell value for the validity. The last matching entry in this structure takes precedence over preceding entries.") (put 'mailcap-mime-data 'risky-local-variable t) (defvar mailcap--computed-mime-data nil "Computed version of the mailcap data incorporating all sources. Same format as `mailcap-mime-data'.") (defcustom mailcap-download-directory nil "Directory to which `mailcap-save-binary-file' downloads files by default. nil means your home directory." :type '(choice (const :tag "Home directory" nil) directory)) (defvar mailcap-poor-system-types '(ms-dos windows-nt) "Systems that don't have a Unix-like directory hierarchy.") ;;; ;;; Utility functions ;;; (defun mailcap-save-binary-file () (goto-char (point-min)) (unwind-protect (let ((file (read-file-name "Filename to save as: " (or mailcap-download-directory "~/"))) (require-final-newline nil)) (write-region (point-min) (point-max) file)) (kill-buffer (current-buffer)))) (defvar mailcap-maybe-eval-warning "*** WARNING *** This MIME part contains untrusted and possibly harmful content. If you evaluate the Emacs Lisp code contained in it, a lot of nasty things can happen. Please examine the code very carefully before you instruct Emacs to evaluate it. You can browse the buffer containing the code using \\[scroll-other-window]. If you are unsure what to do, please answer \"no\"." "Text of warning message displayed by `mailcap-maybe-eval'. Make sure that this text consists only of few text lines. Otherwise, Gnus might fail to display all of it.") (defun mailcap-maybe-eval () "Maybe evaluate a buffer of Emacs Lisp code." (let ((lisp-buffer (current-buffer))) (goto-char (point-min)) (when (save-window-excursion (delete-other-windows) (let ((buffer (generate-new-buffer "*Warning*"))) (unwind-protect (with-current-buffer buffer (insert (substitute-command-keys mailcap-maybe-eval-warning)) (goto-char (point-min)) (display-buffer buffer) (yes-or-no-p "This is potentially dangerous emacs-lisp code, evaluate it? ")) (kill-buffer buffer)))) (eval-buffer (current-buffer))) (when (buffer-live-p lisp-buffer) (with-current-buffer lisp-buffer (emacs-lisp-mode))))) ;;; ;;; The mailcap parser ;;; (defun mailcap-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))) (defvar mailcap-parsed-p nil) (defun mailcap-parse-mailcaps (&optional path force) "Parse out all the mailcaps specified in a path string PATH. Components of PATH are separated by the `path-separator' character appropriate for this system. If FORCE, re-parse even if already parsed. If PATH is omitted, use the value of environment variable MAILCAPS if set; otherwise (on Unix) use the path from RFC 1524, plus /usr/local/etc/mailcap." (interactive (list nil t)) (when (or (not mailcap-parsed-p) force) (cond (path nil) ((getenv "MAILCAPS") (setq path (getenv "MAILCAPS"))) ((memq system-type mailcap-poor-system-types) (setq path '(("~/.mailcap" user) ("~/mail.cap" user) ("~/etc/mail.cap" user)))) (t (setq path ;; This is per RFC 1524, specifically with /usr before ;; /usr/local. '(("~/.mailcap" user) ("/etc/mailcap" system) ("/usr/etc/mailcap" system) ("/usr/local/etc/mailcap" system))))) (when (stringp path) (setq path (mapcar #'list (split-string path path-separator t)))) (when (or (null mailcap--computed-mime-data) (seq-some (lambda (f) (file-has-changed-p (car f) 'mail-parse-mailcaps)) path)) ;; Clear out all old data. (setq mailcap--computed-mime-data nil) ;; Add the Emacs-distributed defaults (which will be used as ;; fallbacks). Do it this way instead of just copying the list, ;; since entries are destructively modified. (cl-loop for (major . minors) in mailcap-mime-data do (cl-loop for (minor . entry) in minors do (mailcap-add-mailcap-entry major minor entry))) ;; The ~/.mailcap entries will end up first in the resulting data. (dolist (spec (reverse path)) (let ((source (cadr spec)) (file-name (car spec))) (when (and (file-readable-p file-name) (file-regular-p file-name)) (mailcap-parse-mailcap file-name source))))) (setq mailcap-parsed-p t))) (defun mailcap-parse-mailcap (fname &optional source) "Parse out the mailcap file specified by FNAME. If SOURCE, mark the entry with this as the source." (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 ) (with-temp-buffer (insert-file-contents fname) (set-syntax-table mailcap-parse-args-syntax-table) (mailcap-replace-regexp "#.*" "") ; Remove all comments (mailcap-replace-regexp "\\\\[ \t]*\n" " ") ; And collapse spaces (mailcap-replace-regexp "\n+" "\n") ; And blank lines (goto-char (point-max)) (skip-chars-backward " \t\n") (delete-region (point) (point-max)) (while (not (bobp)) (skip-chars-backward " \t\n") (beginning-of-line) (setq save-pos (point) info nil) (skip-chars-forward "^/; \t\n") (downcase-region save-pos (point)) (setq major (buffer-substring save-pos (point))) (skip-chars-forward " \t") (setq minor "") (when (eq (char-after) ?/) (forward-char) (skip-chars-forward " \t") (setq save-pos (point)) (skip-chars-forward "^; \t\n") (downcase-region save-pos (point)) (setq minor (cond ((eq ?* (or (char-after save-pos) 0)) ".*") ((= (point) save-pos) ".*") (t (regexp-quote (buffer-substring save-pos (point))))))) (skip-chars-forward " \t") ;;; Got the major/minor chunks, now for the viewers/etc ;;; The first item _must_ be a viewer, according to the ;;; RFC for mailcap files (#1524) (setq viewer "") (when (eq (char-after) ?\;) (forward-char) (skip-chars-forward " \t") (setq save-pos (point)) (skip-chars-forward "^;\n") ;; skip \; (while (eq (char-before) ?\\) (delete-char -1) (forward-char) (skip-chars-forward "^;\n")) (if (eq (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) (unless (equal viewer "") (setq info (nconc (list (cons 'viewer viewer) (cons 'type (concat major "/" (if (string= minor ".*") "*" minor)))) (mailcap-parse-mailcap-extras save-pos (point)))) (mailcap-mailcap-entry-passes-test info) ;; Record where the data came from. (when source (setq info (nconc info (list (cons 'source source))))) (mailcap-add-mailcap-entry major minor info)) (beginning-of-line))))) (defun mailcap-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) (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 (not (eq (char-after (point)) ?=)) ; There is no value (setq value t) (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 (eq (char-after (1- (point))) ?\\ ) (progn (subst-char-in-region (1- (point)) (point) ?\\ ? ) (skip-chars-forward ";")) (setq done t)))) (setq value (buffer-substring val-pos (point)))) ;; `test' as symbol, others like "copiousoutput" and "needsx11" as ;; strings (push (cons (if (string-equal name "test") 'test name) value) results) (skip-chars-forward " \";\n\t")) results))) (defun mailcap-mailcap-entry-passes-test (info) "Replace the test clause of INFO itself with a boolean for some cases. This function supports only `test -n $DISPLAY' and `test -z $DISPLAY', replaces them with t or nil. As for others or if INFO has an interactive spec (needsterm, needsterminal, or needsx11) but DISPLAY is not set, the test clause will be unchanged." (let ((test (assq 'test info)) ; The test clause status) (setq status (and test (split-string (cdr test) " "))) (if (and (or (assoc "needsterm" info) (assoc "needsterminal" info) (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)))) ;;; ;;; The action routines. ;;; (defun mailcap-possible-viewers (major minor) "Return a list of possible viewers from MAJOR for minor type MINOR." (let ((result nil)) (pcase-dolist (`(,type . ,attrs) major) (when (or (equal type minor) (and minor (string-match (concat "^" type "$") minor))) (push attrs result))) (nreverse result))) (defun mailcap-unescape-mime-test (test type-info) (let (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 " ")))) (with-temp-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))) ;; Escapes: ;; %s: name of a file for the body data ;; %t: content-type ;; %{