X-Git-Url: http://git.chise.org/gitweb/?a=blobdiff_plain;f=lisp%2Fmml.el;h=b2f194ced646347895e3803e847b181b2c91788f;hb=ebdecdf203f300217a9a7f533dcf43fec5d427b4;hp=c31e7fdcc9e89e922b6099ab6a77602aed529c11;hpb=58450d89a9bb6a0bcd1c278b29433d1a5f86f763;p=elisp%2Fgnus.git- diff --git a/lisp/mml.el b/lisp/mml.el index c31e7fd..b2f194c 100644 --- a/lisp/mml.el +++ b/lisp/mml.el @@ -1,5 +1,5 @@ ;;; mml.el --- A package for parsing and validating MML documents -;; Copyright (C) 1998 Free Software Foundation, Inc. +;; Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003 Free Software Foundation, Inc. ;; Author: Lars Magne Ingebrigtsen ;; This file is part of GNU Emacs. @@ -23,6 +23,68 @@ ;;; Code: +(require 'mm-util) +(require 'mm-bodies) +(require 'mm-encode) +(require 'mm-decode) +(require 'mml-sec) +(eval-when-compile (require 'cl)) + +(eval-and-compile + (autoload 'message-make-message-id "message") + (autoload 'gnus-setup-posting-charset "gnus-msg") + (autoload 'gnus-add-minor-mode "gnus-ems") + (autoload 'message-fetch-field "message") + (autoload 'fill-flowed-encode "flow-fill") + (autoload 'message-posting-charset "message")) + +(defcustom mml-content-type-parameters + '(name access-type expiration size permission format) + "*A list of acceptable parameters in MML tag. +These parameters are generated in Content-Type header if exists." + :type '(repeat (symbol :tag "Parameter")) + :group 'message) + +(defcustom mml-content-disposition-parameters + '(filename creation-date modification-date read-date) + "*A list of acceptable parameters in MML tag. +These parameters are generated in Content-Disposition header if exists." + :type '(repeat (symbol :tag "Parameter")) + :group 'message) + +(defvar mml-tweak-type-alist nil + "A list of (TYPE . FUNCTION) for tweaking MML parts. +TYPE is a string containing a regexp to match the MIME type. FUNCTION +is a Lisp function which is called with the MML handle to tweak the +part. This variable is used only when no TWEAK parameter exists in +the MML handle.") + +(defvar mml-tweak-function-alist nil + "A list of (NAME . FUNCTION) for tweaking MML parts. +NAME is a string containing the name of the TWEAK parameter in the MML +handle. FUNCTION is a Lisp function which is called with the MML +handle to tweak the part.") + +(defvar mml-tweak-sexp-alist + '((mml-externalize-attachments . mml-tweak-externalize-attachments)) + "A list of (SEXP . FUNCTION) for tweaking MML parts. +SEXP is a s-expression. If the evaluation of SEXP is non-nil, FUNCTION +is called. FUNCTION is a Lisp function which is called with the MML +handle to tweak the part.") + +(defvar mml-externalize-attachments nil + "*If non-nil, local-file attachments are generated as external parts.") + +(defvar mml-generate-multipart-alist nil + "*Alist of multipart generation functions. +Each entry has the form (NAME . FUNCTION), where +NAME is a string containing the name of the part (without the +leading \"/multipart/\"), +FUNCTION is a Lisp function which is called to generate the part. + +The Lisp function has to supply the appropriate MIME headers and the +contents of this part.") + (defvar mml-syntax-table (let ((table (copy-syntax-table emacs-lisp-mode-syntax-table))) (modify-syntax-entry ?\\ "/" table) @@ -36,112 +98,1021 @@ (modify-syntax-entry ?\' " " table) table)) +(defvar mml-boundary-function 'mml-make-boundary + "A function called to suggest a boundary. +The function may be called several times, and should try to make a new +suggestion each time. The function is called with one parameter, +which is a number that says how many times the function has been +called for this message.") + +(defvar mml-confirmation-set nil + "A list of symbols, each of which disables some warning. +`unknown-encoding': always send messages contain characters with +unknown encoding; `use-ascii': always use ASCII for those characters +with unknown encoding; `multipart': always send messages with more than +one charsets.") + +(defvar mml-generate-default-type "text/plain") + +(defvar mml-buffer-list nil) + +(defun mml-generate-new-buffer (name) + (let ((buf (generate-new-buffer name))) + (push buf mml-buffer-list) + buf)) + +(defun mml-destroy-buffers () + (let (kill-buffer-hook) + (mapcar 'kill-buffer mml-buffer-list) + (setq mml-buffer-list nil))) + (defun mml-parse () "Parse the current buffer as an MML document." - (goto-char (point-min)) - (let ((table (syntax-table))) - (unwind-protect - (progn - (set-syntax-table mml-syntax-table) - (mml-parse-1)) - (set-syntax-table table)))) - + (save-excursion + (goto-char (point-min)) + (let ((table (syntax-table))) + (unwind-protect + (progn + (set-syntax-table mml-syntax-table) + (mml-parse-1)) + (set-syntax-table table))))) + (defun mml-parse-1 () "Parse the current buffer as an MML document." - (let (struct) + (let (struct tag point contents charsets warn use-ascii no-markup-p raw) (while (and (not (eobp)) - (not (looking-at "\n")) + (delete-region (match-beginning 0) (match-end 0)) + (cond ((string= mode "sign") + (setq tags (list "sign" method))) + ((string= mode "encrypt") + (setq tags (list "encrypt" method))) + ((string= mode "signencrypt") + (setq tags (list "sign" method "encrypt" method)))) + (eval `(mml-insert-tag ,secure-mode + ,@tags + ,(if recipients "recipients") + ,recipients + ,(if sender "sender") + ,sender)) + ;; restart the parse + (goto-char location))) + ((looking-at "<#multipart") (push (nconc (mml-read-tag) (mml-parse-1)) struct)) - ((looking-at "")) - (setq elem (buffer-substring (point) (progn (forward-sexp 1) (point)))) + (while (not (looking-at ">[ \t]*\n?")) + (setq elem (buffer-substring-no-properties + (point) (progn (forward-sexp 1) (point)))) (skip-chars-forward "= \t\n") - (setq val (buffer-substring (point) (progn (forward-sexp 1) (point)))) + (setq val (buffer-substring-no-properties + (point) (progn (forward-sexp 1) (point)))) (when (string-match "^\"\\(.*\\)\"$" val) (setq val (match-string 1 val))) (push (cons (intern elem) val) contents) (skip-chars-forward " \t\n")) - (forward-char 1) + (goto-char (match-end 0)) + ;; Don't skip the leading space. + ;;(skip-chars-forward " \t\n") + ;; Put the tag location into the returned contents + (setq contents (append (list (cons 'tag-location orig-point)) contents)) (cons (intern name) (nreverse contents)))) -(defun mml-read-part () - "Return the buffer up till the next part, multipart or closing part or multipart." - (let ((beg (point))) - (if (re-search-forward "") - (goto-char (match-beginning 0)))) - (buffer-substring beg (goto-char (point-max)))))) +(defun mml-buffer-substring-no-properties-except-hard-newlines (start end) + (let ((str (buffer-substring-no-properties start end)) + (bufstart start) tmp) + (while (setq tmp (text-property-any start end 'hard 't)) + (set-text-properties (- tmp bufstart) (- tmp bufstart -1) + '(hard t) str) + (setq start (1+ tmp))) + str)) + +(defun mml-read-part (&optional mml) + "Return the buffer up till the next part, multipart or closing part or multipart. +If MML is non-nil, return the buffer up till the correspondent mml tag." + (let ((beg (point)) (count 1)) + ;; If the tag ended at the end of the line, we go to the next line. + (when (looking-at "[ \t]*\n") + (forward-line 1)) + (if mml + (progn + (while (and (> count 0) (not (eobp))) + (if (re-search-forward "<#\\(/\\)?mml." nil t) + (setq count (+ count (if (match-beginning 1) -1 1))) + (goto-char (point-max)))) + (mml-buffer-substring-no-properties-except-hard-newlines + beg (if (> count 0) + (point) + (match-beginning 0)))) + (if (re-search-forward + "<#\\(/\\)?\\(multipart\\|part\\|external\\|mml\\)." nil t) + (prog1 + (mml-buffer-substring-no-properties-except-hard-newlines + beg (match-beginning 0)) + (if (or (not (match-beginning 1)) + (equal (match-string 2) "multipart")) + (goto-char (match-beginning 0)) + (when (looking-at "[ \t]*\n") + (forward-line 1)))) + (mml-buffer-substring-no-properties-except-hard-newlines + beg (goto-char (point-max))))))) (defvar mml-boundary nil) +(defvar mml-base-boundary "-=-=") +(defvar mml-multipart-number 0) (defun mml-generate-mime () "Generate a MIME message based on the current MML document." - (setq mml-boundary "=-=-=") - (let ((cont (mml-parse))) - (with-temp-buffer - (if (and (consp (car cont)) - (= (length cont) 1)) - (mml-generate-mime-1 (car cont)) - (mml-generate-mime-1 (nconc (list 'multipart '(type . "mixed")) - cont))) - (buffer-string)))) + (let ((cont (mml-parse)) + (mml-multipart-number mml-multipart-number)) + (if (not cont) + nil + (with-temp-buffer + (if (and (consp (car cont)) + (= (length cont) 1)) + (mml-generate-mime-1 (car cont)) + (mml-generate-mime-1 (nconc (list 'multipart '(type . "mixed")) + cont))) + (buffer-string))))) (defun mml-generate-mime-1 (cont) - (cond - ((eq (car cont) 'part) - (let (coded encoding charset filename type) - (setq type (or (cdr (assq 'type cont)) "text/plain")) + (let ((mm-use-ultra-safe-encoding + (or mm-use-ultra-safe-encoding (assq 'sign cont)))) + (save-restriction + (narrow-to-region (point) (point)) + (mml-tweak-part cont) + (cond + ((or (eq (car cont) 'part) (eq (car cont) 'mml)) + (let ((raw (cdr (assq 'raw cont))) + coded encoding charset filename type flowed) + (setq type (or (cdr (assq 'type cont)) "text/plain")) + (if (and (not raw) + (member (car (split-string type "/")) '("text" "message"))) + (progn + (with-temp-buffer + (setq charset (mm-charset-to-coding-system + (cdr (assq 'charset cont)))) + (when (eq charset 'ascii) + (setq charset nil)) + (cond + ((cdr (assq 'buffer cont)) + (insert-buffer-substring (cdr (assq 'buffer cont)))) + ((and (setq filename (cdr (assq 'filename cont))) + (not (equal (cdr (assq 'nofile cont)) "yes"))) + (let ((coding-system-for-read charset)) + (mm-insert-file-contents filename))) + ((eq 'mml (car cont)) + (insert (cdr (assq 'contents cont)))) + (t + (save-restriction + (narrow-to-region (point) (point)) + (insert (cdr (assq 'contents cont))) + ;; Remove quotes from quoted tags. + (goto-char (point-min)) + (while (re-search-forward + "<#!+/?\\(part\\|multipart\\|external\\|mml\\)" + nil t) + (delete-region (+ (match-beginning 0) 2) + (+ (match-beginning 0) 3)))))) + (cond + ((eq (car cont) 'mml) + (let ((mml-boundary (funcall mml-boundary-function + (incf mml-multipart-number))) + (mml-generate-default-type "text/plain")) + (mml-to-mime)) + (let ((mm-7bit-chars (concat mm-7bit-chars "\x1b"))) + ;; ignore 0x1b, it is part of iso-2022-jp + (setq encoding (mm-body-7-or-8)))) + ((string= (car (split-string type "/")) "message") + (let ((mm-7bit-chars (concat mm-7bit-chars "\x1b"))) + ;; ignore 0x1b, it is part of iso-2022-jp + (setq encoding (mm-body-7-or-8)))) + (t + ;; Only perform format=flowed filling on text/plain + ;; parts where there either isn't a format parameter + ;; in the mml tag or it says "flowed" and there + ;; actually are hard newlines in the text. + (let (use-hard-newlines) + (when (and (string= type "text/plain") + (or (null (assq 'format cont)) + (string= (cdr (assq 'format cont)) + "flowed")) + (setq use-hard-newlines + (text-property-any + (point-min) (point-max) 'hard 't))) + (fill-flowed-encode) + ;; Indicate that `mml-insert-mime-headers' should + ;; insert a "; format=flowed" string unless the + ;; user has already specified it. + (setq flowed (null (assq 'format cont))))) + (setq charset (mm-encode-body charset)) + (setq encoding (mm-body-encoding + charset (cdr (assq 'encoding cont)))))) + (setq coded (buffer-string))) + (mml-insert-mime-headers cont type charset encoding flowed) + (insert "\n") + (insert coded)) + (mm-with-unibyte-buffer + (cond + ((cdr (assq 'buffer cont)) + (insert-buffer-substring (cdr (assq 'buffer cont)))) + ((and (setq filename (cdr (assq 'filename cont))) + (not (equal (cdr (assq 'nofile cont)) "yes"))) + (let ((coding-system-for-read mm-binary-coding-system)) + (mm-insert-file-contents filename nil nil nil nil t))) + (t + (insert (cdr (assq 'contents cont))))) + (setq encoding (mm-encode-buffer type) + coded (mm-string-as-multibyte (buffer-string)))) + (mml-insert-mime-headers cont type charset encoding nil) + (insert "\n") + (mm-with-unibyte-current-buffer + (insert coded))))) + ((eq (car cont) 'external) + (insert "Content-Type: message/external-body") + (let ((parameters (mml-parameter-string + cont '(expiration size permission))) + (name (cdr (assq 'name cont))) + (url (cdr (assq 'url cont)))) + (when name + (setq name (mml-parse-file-name name)) + (if (stringp name) + (mml-insert-parameter + (mail-header-encode-parameter "name" name) + "access-type=local-file") + (mml-insert-parameter + (mail-header-encode-parameter + "name" (file-name-nondirectory (nth 2 name))) + (mail-header-encode-parameter "site" (nth 1 name)) + (mail-header-encode-parameter + "directory" (file-name-directory (nth 2 name)))) + (mml-insert-parameter + (concat "access-type=" + (if (member (nth 0 name) '("ftp@" "anonymous@")) + "anon-ftp" + "ftp"))))) + (when url + (mml-insert-parameter + (mail-header-encode-parameter "url" url) + "access-type=url")) + (when parameters + (mml-insert-parameter-string + cont '(expiration size permission)))) + (insert "\n\n") + (insert "Content-Type: " (cdr (assq 'type cont)) "\n") + (insert "Content-ID: " (message-make-message-id) "\n") + (insert "Content-Transfer-Encoding: " + (or (cdr (assq 'encoding cont)) "binary")) + (insert "\n\n") + (insert (or (cdr (assq 'contents cont)))) + (insert "\n")) + ((eq (car cont) 'multipart) + (let* ((type (or (cdr (assq 'type cont)) "mixed")) + (mml-generate-default-type (if (equal type "digest") + "message/rfc822" + "text/plain")) + (handler (assoc type mml-generate-multipart-alist))) + (if handler + (funcall (cdr handler) cont) + ;; No specific handler. Use default one. + (let ((mml-boundary (mml-compute-boundary cont))) + (insert (format "Content-Type: multipart/%s; boundary=\"%s\"\n" + type mml-boundary)) + (let ((cont cont) part) + (while (setq part (pop cont)) + ;; Skip `multipart' and attributes. + (when (and (consp part) (consp (cdr part))) + (insert "\n--" mml-boundary "\n") + (mml-generate-mime-1 part)))) + (insert "\n--" mml-boundary "--\n"))))) + (t + (error "Invalid element: %S" cont))) + ;; handle sign & encrypt tags in a semi-smart way. + (let ((sign-item (assoc (cdr (assq 'sign cont)) mml-sign-alist)) + (encrypt-item (assoc (cdr (assq 'encrypt cont)) + mml-encrypt-alist)) + sender recipients) + (when (or sign-item encrypt-item) + (if (setq sender (cdr (assq 'sender cont))) + (message-options-set 'message-sender sender)) + (if (setq recipients (cdr (assq 'recipients cont))) + (message-options-set 'message-recipients recipients)) + (let ((style (mml-signencrypt-style (first (or sign-item encrypt-item))))) + ;; check if: we're both signing & encrypting, both methods + ;; are the same (why would they be different?!), and that + ;; the signencrypt style allows for combined operation. + (if (and sign-item encrypt-item (equal (first sign-item) + (first encrypt-item)) + (equal style 'combined)) + (funcall (nth 1 encrypt-item) cont t) + ;; otherwise, revert to the old behavior. + (when sign-item + (funcall (nth 1 sign-item) cont)) + (when encrypt-item + (funcall (nth 1 encrypt-item) cont))))))))) + +(defun mml-compute-boundary (cont) + "Return a unique boundary that does not exist in CONT." + (let ((mml-boundary (funcall mml-boundary-function + (incf mml-multipart-number)))) + ;; This function tries again and again until it has found + ;; a unique boundary. + (while (not (catch 'not-unique + (mml-compute-boundary-1 cont)))) + mml-boundary)) + +(defun mml-compute-boundary-1 (cont) + (let (filename) + (cond + ((eq (car cont) 'part) (with-temp-buffer - (if (setq filename (cdr (assq 'filename cont))) - (insert-file-contents-literally filename) - (insert (cdr (assq 'contents cont)))) - (if (equal (car (split-string type "/")) "text") - (setq charset (mm-encode-body) - encoding (mm-body-encoding)) - (setq encoding (mm-encode-buffer type))) - (setq coded (buffer-string))) - (when (or charset - (not (equal type "text/plain"))) - (insert "Content-Type: " type)) + (cond + ((cdr (assq 'buffer cont)) + (insert-buffer-substring (cdr (assq 'buffer cont)))) + ((and (setq filename (cdr (assq 'filename cont))) + (not (equal (cdr (assq 'nofile cont)) "yes"))) + (mm-insert-file-contents filename)) + (t + (insert (cdr (assq 'contents cont))))) + (goto-char (point-min)) + (when (re-search-forward (concat "^--" (regexp-quote mml-boundary)) + nil t) + (setq mml-boundary (funcall mml-boundary-function + (incf mml-multipart-number))) + (throw 'not-unique nil)))) + ((eq (car cont) 'multipart) + (mapcar 'mml-compute-boundary-1 (cddr cont)))) + t)) + +(defun mml-make-boundary (number) + (concat (make-string (% number 60) ?=) + (if (> number 17) + (format "%x" number) + "") + mml-base-boundary)) + +(defun mml-insert-mime-headers (cont type charset encoding flowed) + (let (parameters disposition description) + (setq parameters + (mml-parameter-string + cont mml-content-type-parameters)) + (when (or charset + parameters + flowed + (not (equal type mml-generate-default-type))) + (when (consp charset) + (error + "Can't encode a part with several charsets.")) + (insert "Content-Type: " type) (when charset - (insert (format "; charset=\"%s\"" charset))) - (insert "\n") - (unless (eq encoding '7bit) - (insert (format "Content-Transfer-Encoding: %s\n" encoding))) - (insert "\n") - (insert coded))) - ((eq (car cont) 'multipart) - (let ((mml-boundary (concat "=" mml-boundary))) - (insert (format "Content-Type: multipart/%s; boundary=\"%s\"\n" - (or (cdr (assq 'type cont)) "mixed") - mml-boundary)) - (insert "\n") - (setq cont (cddr cont)) - (while cont - (insert "--" mml-boundary "\n") - (mml-generate-mime-1 (pop cont))) - (insert "--" mml-boundary "--\n"))) - (t - (error "%S" cont)))) + (insert "; " (mail-header-encode-parameter + "charset" (symbol-name charset)))) + (when flowed + (insert "; format=flowed")) + (when parameters + (mml-insert-parameter-string + cont mml-content-type-parameters)) + (insert "\n")) + (setq parameters + (mml-parameter-string + cont mml-content-disposition-parameters)) + (when (or (setq disposition (cdr (assq 'disposition cont))) + parameters) + (insert "Content-Disposition: " (or disposition "inline")) + (when parameters + (mml-insert-parameter-string + cont mml-content-disposition-parameters)) + (insert "\n")) + (unless (eq encoding '7bit) + (insert (format "Content-Transfer-Encoding: %s\n" encoding))) + (when (setq description (cdr (assq 'description cont))) + (insert "Content-Description: " + (mail-encode-encoded-word-string description) "\n")))) + +(defun mml-parameter-string (cont types) + (let ((string "") + value type) + (while (setq type (pop types)) + (when (setq value (cdr (assq type cont))) + ;; Strip directory component from the filename parameter. + (when (eq type 'filename) + (setq value (file-name-nondirectory value))) + (setq string (concat string "; " + (mail-header-encode-parameter + (symbol-name type) value))))) + (when (not (zerop (length string))) + string))) + +(defun mml-insert-parameter-string (cont types) + (let (value type) + (while (setq type (pop types)) + (when (setq value (cdr (assq type cont))) + ;; Strip directory component from the filename parameter. + (when (eq type 'filename) + (setq value (file-name-nondirectory value))) + (mml-insert-parameter + (mail-header-encode-parameter + (symbol-name type) value)))))) + +(eval-when-compile + (defvar ange-ftp-name-format) + (defvar efs-path-regexp)) +(defun mml-parse-file-name (path) + (if (if (boundp 'efs-path-regexp) + (string-match efs-path-regexp path) + (if (boundp 'ange-ftp-name-format) + (string-match (car ange-ftp-name-format) path))) + (list (match-string 1 path) (match-string 2 path) + (substring path (1+ (match-end 2)))) + path)) + +(defun mml-insert-buffer (buffer) + "Insert BUFFER at point and quote any MML markup." + (save-restriction + (narrow-to-region (point) (point)) + (insert-buffer-substring buffer) + (mml-quote-region (point-min) (point-max)) + (goto-char (point-max)))) + +;;; +;;; Transforming MIME to MML +;;; + +(defun mime-to-mml (&optional handles) + "Translate the current buffer (which should be a message) into MML. +If HANDLES is non-nil, use it instead reparsing the buffer." + ;; First decode the head. + (save-restriction + (message-narrow-to-head) + (mail-decode-encoded-word-region (point-min) (point-max))) + (unless handles + (setq handles (mm-dissect-buffer t))) + (goto-char (point-min)) + (search-forward "\n\n" nil t) + (delete-region (point) (point-max)) + (if (stringp (car handles)) + (mml-insert-mime handles) + (mml-insert-mime handles t)) + (mm-destroy-parts handles) + (save-restriction + (message-narrow-to-head) + ;; Remove them, they are confusing. + (message-remove-header "Content-Type") + (message-remove-header "MIME-Version") + (message-remove-header "Content-Disposition") + (message-remove-header "Content-Transfer-Encoding"))) + +(defun mml-to-mime () + "Translate the current buffer from MML to MIME." + (message-encode-message-body) + (save-restriction + (message-narrow-to-headers-or-head) + ;; Skip past any From_ headers. + (while (looking-at "From ") + (forward-line 1)) + (let ((mail-parse-charset message-default-charset)) + (mail-encode-encoded-word-buffer)))) + +(defun mml-insert-mime (handle &optional no-markup) + (let (textp buffer mmlp) + ;; Determine type and stuff. + (unless (stringp (car handle)) + (unless (setq textp (equal (mm-handle-media-supertype handle) "text")) + (save-excursion + (set-buffer (setq buffer (mml-generate-new-buffer " *mml*"))) + (mm-insert-part handle) + (if (setq mmlp (equal (mm-handle-media-type handle) + "message/rfc822")) + (mime-to-mml))))) + (if mmlp + (mml-insert-mml-markup handle nil t t) + (unless (and no-markup + (equal (mm-handle-media-type handle) "text/plain")) + (mml-insert-mml-markup handle buffer textp))) + (cond + (mmlp + (insert-buffer buffer) + (goto-char (point-max)) + (insert "<#/mml>\n")) + ((stringp (car handle)) + (mapcar 'mml-insert-mime (cdr handle)) + (insert "<#/multipart>\n")) + (textp + (let ((charset (mail-content-type-get + (mm-handle-type handle) 'charset))) + (if (eq charset 'gnus-decoded) + (mm-insert-part handle) + (insert (mm-decode-string (mm-get-part handle) charset)))) + (goto-char (point-max))) + (t + (insert "<#/part>\n"))))) + +(defun mml-insert-mml-markup (handle &optional buffer nofile mmlp) + "Take a MIME handle and insert an MML tag." + (if (stringp (car handle)) + (insert "<#multipart type=" (mm-handle-media-subtype handle) + ">\n") + (if mmlp + (insert "<#mml type=" (mm-handle-media-type handle)) + (insert "<#part type=" (mm-handle-media-type handle))) + (dolist (elem (append (cdr (mm-handle-type handle)) + (cdr (mm-handle-disposition handle)))) + (unless (symbolp (cdr elem)) + (insert " " (symbol-name (car elem)) "=\"" (cdr elem) "\""))) + (when (mm-handle-disposition handle) + (insert " disposition=" (car (mm-handle-disposition handle)))) + (when buffer + (insert " buffer=\"" (buffer-name buffer) "\"")) + (when nofile + (insert " nofile=yes")) + (when (mm-handle-description handle) + (insert " description=\"" (mm-handle-description handle) "\"")) + (insert ">\n"))) + +(defun mml-insert-parameter (&rest parameters) + "Insert PARAMETERS in a nice way." + (dolist (param parameters) + (insert ";") + (let ((point (point))) + (insert " " param) + (when (> (current-column) 71) + (goto-char point) + (insert "\n ") + (end-of-line))))) + +;;; +;;; Mode for inserting and editing MML forms +;;; + +(defvar mml-mode-map + (let ((sign (make-sparse-keymap)) + (encrypt (make-sparse-keymap)) + (signpart (make-sparse-keymap)) + (encryptpart (make-sparse-keymap)) + (map (make-sparse-keymap)) + (main (make-sparse-keymap))) + (define-key sign "p" 'mml-secure-message-sign-pgpmime) + (define-key sign "o" 'mml-secure-message-sign-pgp) + (define-key sign "s" 'mml-secure-message-sign-smime) + (define-key signpart "p" 'mml-secure-sign-pgpmime) + (define-key signpart "o" 'mml-secure-sign-pgp) + (define-key signpart "s" 'mml-secure-sign-smime) + (define-key encrypt "p" 'mml-secure-message-encrypt-pgpmime) + (define-key encrypt "o" 'mml-secure-message-encrypt-pgp) + (define-key encrypt "s" 'mml-secure-message-encrypt-smime) + (define-key encryptpart "p" 'mml-secure-encrypt-pgpmime) + (define-key encryptpart "o" 'mml-secure-encrypt-pgp) + (define-key encryptpart "s" 'mml-secure-encrypt-smime) + (define-key map "\C-n" 'mml-unsecure-message) + (define-key map "f" 'mml-attach-file) + (define-key map "b" 'mml-attach-buffer) + (define-key map "e" 'mml-attach-external) + (define-key map "q" 'mml-quote-region) + (define-key map "m" 'mml-insert-multipart) + (define-key map "p" 'mml-insert-part) + (define-key map "v" 'mml-validate) + (define-key map "P" 'mml-preview) + (define-key map "s" sign) + (define-key map "S" signpart) + (define-key map "c" encrypt) + (define-key map "C" encryptpart) + ;;(define-key map "n" 'mml-narrow-to-part) + ;; `M-m' conflicts with `back-to-indentation'. + ;; (define-key main "\M-m" map) + (define-key main "\C-c\C-m" map) + main)) + +(easy-menu-define + mml-menu mml-mode-map "" + `("Attachments" + ["Attach File..." mml-attach-file + ,@(if (featurep 'xemacs) '(t) + '(:help "Attach a file at point"))] + ["Attach Buffer..." mml-attach-buffer t] + ["Attach External..." mml-attach-external t] + ["Insert Part..." mml-insert-part t] + ["Insert Multipart..." mml-insert-multipart t] + ["PGP/MIME Sign" mml-secure-message-sign-pgpmime t] + ["PGP/MIME Encrypt" mml-secure-message-encrypt-pgpmime t] + ["PGP Sign" mml-secure-message-sign-pgp t] + ["PGP Encrypt" mml-secure-message-encrypt-pgp t] + ["S/MIME Sign" mml-secure-message-sign-smime t] + ["S/MIME Encrypt" mml-secure-message-encrypt-smime t] + ("Secure MIME part" + ["PGP/MIME Sign Part" mml-secure-sign-pgpmime t] + ["PGP/MIME Encrypt Part" mml-secure-encrypt-pgpmime t] + ["PGP Sign Part" mml-secure-sign-pgp t] + ["PGP Encrypt Part" mml-secure-encrypt-pgp t] + ["S/MIME Sign Part" mml-secure-sign-smime t] + ["S/MIME Encrypt Part" mml-secure-encrypt-smime t]) + ["Encrypt/Sign off" mml-unsecure-message t] + ;;["Narrow" mml-narrow-to-part t] + ["Quote MML" mml-quote-region t] + ["Validate MML" mml-validate t] + ["Preview" mml-preview t])) + +(defvar mml-mode nil + "Minor mode for editing MML.") + +(defun mml-mode (&optional arg) + "Minor mode for editing MML. +MML is the MIME Meta Language, a minor mode for composing MIME articles. +See Info node `(emacs-mime)Composing'. + +\\{mml-mode-map}" + (interactive "P") + (when (set (make-local-variable 'mml-mode) + (if (null arg) (not mml-mode) + (> (prefix-numeric-value arg) 0))) + (gnus-add-minor-mode 'mml-mode " MML" mml-mode-map) + (easy-menu-add mml-menu mml-mode-map) + (run-hooks 'mml-mode-hook))) + +;;; +;;; Helper functions for reading MIME stuff from the minibuffer and +;;; inserting stuff to the buffer. +;;; + +(defun mml-minibuffer-read-file (prompt) + (let ((file (read-file-name prompt nil nil t))) + ;; Prevent some common errors. This is inspired by similar code in + ;; VM. + (when (file-directory-p file) + (error "%s is a directory, cannot attach" file)) + (unless (file-exists-p file) + (error "No such file: %s" file)) + (unless (file-readable-p file) + (error "Permission denied: %s" file)) + file)) + +(defun mml-minibuffer-read-type (name &optional default) + (mailcap-parse-mimetypes) + (let* ((default (or default + (mm-default-file-encoding name) + ;; Perhaps here we should check what the file + ;; looks like, and offer text/plain if it looks + ;; like text/plain. + "application/octet-stream")) + (string (completing-read + (format "Content type (default %s): " default) + (mapcar 'list (mailcap-mime-types))))) + (if (not (equal string "")) + string + default))) + +(defun mml-minibuffer-read-description () + (let ((description (read-string "One line description: "))) + (when (string-match "\\`[ \t]*\\'" description) + (setq description nil)) + description)) + +(defun mml-quote-region (beg end) + "Quote the MML tags in the region." + (interactive "r") + (save-excursion + (save-restriction + ;; Temporarily narrow the region to defend from changes + ;; invalidating END. + (narrow-to-region beg end) + (goto-char (point-min)) + ;; Quote parts. + (while (re-search-forward + "<#!*/?\\(multipart\\|part\\|external\\|mml\\)" nil t) + ;; Insert ! after the #. + (goto-char (+ (match-beginning 0) 2)) + (insert "!"))))) + +(defun mml-insert-tag (name &rest plist) + "Insert an MML tag described by NAME and PLIST." + (when (symbolp name) + (setq name (symbol-name name))) + (insert "<#" name) + (while plist + (let ((key (pop plist)) + (value (pop plist))) + (when value + ;; Quote VALUE if it contains suspicious characters. + (when (string-match "[\"'\\~/*;() \t\n]" value) + (setq value (with-output-to-string + (let (print-escape-nonascii) + (prin1 value))))) + (insert (format " %s=%s" key value))))) + (insert ">\n")) + +(defun mml-insert-empty-tag (name &rest plist) + "Insert an empty MML tag described by NAME and PLIST." + (when (symbolp name) + (setq name (symbol-name name))) + (apply #'mml-insert-tag name plist) + (insert "<#/" name ">\n")) + +;;; Attachment functions. + +(defun mml-attach-file (file &optional type description) + "Attach a file to the outgoing MIME message. +The file is not inserted or encoded until you send the message with +`\\[message-send-and-exit]' or `\\[message-send]'. + +FILE is the name of the file to attach. TYPE is its content-type, a +string of the form \"type/subtype\". DESCRIPTION is a one-line +description of the attachment." + (interactive + (let* ((file (mml-minibuffer-read-file "Attach file: ")) + (type (mml-minibuffer-read-type file)) + (description (mml-minibuffer-read-description))) + (list file type description))) + (mml-insert-empty-tag 'part 'type type 'filename file + 'disposition "attachment" 'description description)) + +(defun mml-attach-buffer (buffer &optional type description) + "Attach a buffer to the outgoing MIME message. +See `mml-attach-file' for details of operation." + (interactive + (let* ((buffer (read-buffer "Attach buffer: ")) + (type (mml-minibuffer-read-type buffer "text/plain")) + (description (mml-minibuffer-read-description))) + (list buffer type description))) + (mml-insert-empty-tag 'part 'type type 'buffer buffer + 'disposition "attachment" 'description description)) + +(defun mml-attach-external (file &optional type description) + "Attach an external file into the buffer. +FILE is an ange-ftp/efs specification of the part location. +TYPE is the MIME type to use." + (interactive + (let* ((file (mml-minibuffer-read-file "Attach external file: ")) + (type (mml-minibuffer-read-type file)) + (description (mml-minibuffer-read-description))) + (list file type description))) + (mml-insert-empty-tag 'external 'type type 'name file + 'disposition "attachment" 'description description)) + +(defun mml-insert-multipart (&optional type) + (interactive (list (completing-read "Multipart type (default mixed): " + '(("mixed") ("alternative") ("digest") ("parallel") + ("signed") ("encrypted")) + nil nil "mixed"))) + (or type + (setq type "mixed")) + (mml-insert-empty-tag "multipart" 'type type) + (forward-line -1)) + +(defun mml-insert-part (&optional type) + (interactive + (list (mml-minibuffer-read-type ""))) + (mml-insert-tag 'part 'type type 'disposition "inline") + (forward-line -1)) + +(defun mml-preview-insert-mft () + "Insert a Mail-Followup-To header before previewing an article. +Should be adopted if code in `message-send-mail' is changed." + (when (and (message-mail-p) + (message-subscribed-p) + (not (mail-fetch-field "mail-followup-to")) + (message-make-mft)) + (message-position-on-field "Mail-Followup-To" "X-Draft-From") + (insert (message-make-mft)))) + +(defun mml-preview (&optional raw) + "Display current buffer with Gnus, in a new buffer. +If RAW, don't highlight the article." + (interactive "P") + (save-excursion + (let* ((buf (current-buffer)) + (message-options message-options) + (message-this-is-mail (message-mail-p)) + (message-this-is-news (message-news-p)) + (message-posting-charset (or (gnus-setup-posting-charset + (save-restriction + (message-narrow-to-headers-or-head) + (message-fetch-field "Newsgroups"))) + message-posting-charset))) + (message-options-set-recipient) + (switch-to-buffer (generate-new-buffer + (concat (if raw "*Raw MIME preview of " + "*MIME preview of ") (buffer-name)))) + (erase-buffer) + (insert-buffer buf) + (mml-preview-insert-mft) + (let ((message-deletable-headers (if (message-news-p) + nil + message-deletable-headers))) + (message-generate-headers + (copy-sequence (if (message-news-p) + message-required-news-headers + message-required-mail-headers)))) + (if (re-search-forward + (concat "^" (regexp-quote mail-header-separator) "\n") nil t) + (replace-match "\n")) + (let ((mail-header-separator ""));; mail-header-separator is removed. + (mml-to-mime)) + (if raw + (when (fboundp 'set-buffer-multibyte) + (let ((s (buffer-string))) + ;; Insert the content into unibyte buffer. + (erase-buffer) + (mm-disable-multibyte) + (insert s))) + (let ((gnus-newsgroup-charset (car message-posting-charset)) + gnus-article-prepare-hook gnus-original-article-buffer) + (run-hooks 'gnus-article-decode-hook) + (let ((gnus-newsgroup-name "dummy") + (gnus-newsrc-hashtb (or gnus-newsrc-hashtb + (gnus-make-hashtable 5)))) + (gnus-article-prepare-display)))) + ;; Disable article-mode-map. + (use-local-map nil) + (setq buffer-read-only t) + (local-set-key "q" (lambda () (interactive) (kill-buffer nil))) + (goto-char (point-min))))) + +(defun mml-validate () + "Validate the current MML document." + (interactive) + (mml-parse)) + +(defun mml-tweak-part (cont) + "Tweak a MML part." + (let ((tweak (cdr (assq 'tweak cont))) + func) + (cond + (tweak + (setq func + (or (cdr (assoc tweak mml-tweak-function-alist)) + (intern tweak)))) + (mml-tweak-type-alist + (let ((alist mml-tweak-type-alist) + (type (or (cdr (assq 'type cont)) "text/plain"))) + (while alist + (if (string-match (caar alist) type) + (setq func (cdar alist) + alist nil) + (setq alist (cdr alist))))))) + (if func + (funcall func cont) + cont) + (let ((alist mml-tweak-sexp-alist)) + (while alist + (if (eval (caar alist)) + (funcall (cdar alist) cont)) + (setq alist (cdr alist))))) + cont) + +(defun mml-tweak-externalize-attachments (cont) + "Tweak attached files as external parts." + (let (filename-cons) + (when (and (eq (car cont) 'part) + (not (cdr (assq 'buffer cont))) + (and (setq filename-cons (assq 'filename cont)) + (not (equal (cdr (assq 'nofile cont)) "yes")))) + (setcar cont 'external) + (setcar filename-cons 'name)))) (provide 'mml)