X-Git-Url: http://git.chise.org/gitweb/?a=blobdiff_plain;f=lisp%2Fmml.el;h=5c5efdc12b43dfc641141bc75e02446c905e6ce6;hb=f9c8170d647a9e61dd1d8bb7c4f7d4d8c6721280;hp=a7f7ffc4ff4f38b77d575c27959db19de20c5c4e;hpb=0d1720aee995af053638966ad6bcf16698575735;p=elisp%2Fgnus.git- diff --git a/lisp/mml.el b/lisp/mml.el index a7f7ffc..5c5efdc 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,99 Free Software Foundation, Inc. ;; Author: Lars Magne Ingebrigtsen ;; This file is part of GNU Emacs. @@ -52,43 +52,113 @@ (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) (while (and (not (eobp)) (not (looking-at "<#/multipart"))) (cond ((looking-at "<#multipart") (push (nconc (mml-read-tag) (mml-parse-1)) struct)) - ((looking-at "<#part") - (push (nconc (mml-read-tag) (list (cons 'contents (mml-read-part)))) - struct)) ((looking-at "<#external") (push (nconc (mml-read-tag) (list (cons 'contents (mml-read-part)))) struct)) (t - (push (list 'part '(type . "text/plain") - (cons 'contents (mml-read-part))) struct)))) + (if (looking-at "<#part") + (setq tag (mml-read-tag)) + (setq tag (list 'part '(type . "text/plain")) + warn t)) + (setq point (point) + contents (mml-read-part) + charsets (mm-find-mime-charset-region point (point))) + (if (< (length charsets) 2) + (push (nconc tag (list (cons 'contents contents))) + struct) + (let ((nstruct (mml-parse-singlepart-with-multiple-charsets + tag point (point)))) + (when (and warn + (not + (y-or-n-p + (format + "Warning: Your message contains %d parts. Really send? " + (length nstruct))))) + (error "Edit your message to use only one charset")) + (setq struct (nconc nstruct struct))))))) (unless (eobp) (forward-line 1)) (nreverse struct))) +(defun mml-parse-singlepart-with-multiple-charsets (orig-tag beg end) + (save-excursion + (narrow-to-region beg end) + (goto-char (point-min)) + (let ((current (mm-mime-charset (char-charset (following-char)))) + charset struct space newline paragraph) + (while (not (eobp)) + (cond + ;; The charset remains the same. + ((or (eq (setq charset (mm-mime-charset + (char-charset (following-char)))) 'us-ascii) + (eq charset current))) + ;; The initial charset was ascii. + ((eq current 'us-ascii) + (setq current charset + space nil + newline nil + paragraph nil)) + ;; We have a change in charsets. + (t + (push (append + orig-tag + (list (cons 'contents + (buffer-substring-no-properties + beg (or paragraph newline space (point)))))) + struct) + (setq beg (or paragraph newline space (point)) + current charset + space nil + newline nil + paragraph nil))) + ;; Compute places where it might be nice to break the part. + (cond + ((memq (following-char) '(? ?\t)) + (setq space (1+ (point)))) + ((eq (following-char) ?\n) + (setq newline (1+ (point)))) + ((and (eq (following-char) ?\n) + (not (bobp)) + (eq (char-after (1- (point))) ?\n)) + (setq paragraph (point)))) + (forward-char 1)) + ;; Do the final part. + (unless (= beg (point)) + (push (append orig-tag + (list (cons 'contents + (buffer-substring-no-properties + beg (point))))) + struct)) + struct))) + (defun mml-read-tag () "Read a tag and return the contents." (let (contents name elem val) (forward-char 2) - (setq name (buffer-substring (point) (progn (forward-sexp 1) (point)))) + (setq name (buffer-substring-no-properties + (point) (progn (forward-sexp 1) (point)))) (skip-chars-forward " \t\n") (while (not (looking-at ">")) - (setq elem (buffer-substring (point) (progn (forward-sexp 1) (point)))) + (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) + (skip-chars-forward " \t\n") (cons (intern name) (nreverse contents)))) (defun mml-read-part () @@ -97,17 +167,19 @@ ;; 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 (re-search-forward "<#/?\\(multipart\\|part\\|external\\)." nil t) + (if (re-search-forward + "<#\\(/\\)?\\(multipart\\|part\\|external\\)." nil t) (prog1 - (buffer-substring beg (match-beginning 0)) - (if (equal (match-string 0) "<#/multipart>") + (buffer-substring-no-properties 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)))) - (buffer-substring beg (goto-char (point-max)))))) + (buffer-substring-no-properties beg (goto-char (point-max)))))) (defvar mml-boundary nil) -(defvar mml-base-boundary "=-=-=") +(defvar mml-base-boundary "-=-=") (defvar mml-multipart-number 0) (defun mml-generate-mime () @@ -127,28 +199,36 @@ (defun mml-generate-mime-1 (cont) (cond ((eq (car cont) 'part) - (let (coded encoding charset filename type parameters) + (let (coded encoding charset filename type) (setq type (or (cdr (assq 'type cont)) "text/plain")) (if (equal (car (split-string type "/")) "text") (with-temp-buffer - (if (setq filename (cdr (assq 'filename cont))) - (insert-file-contents-literally filename) + (cond + ((cdr (assq 'buffer cont)) + (insert-buffer-substring (cdr (assq 'buffer cont)))) + ((setq filename (cdr (assq 'filename cont))) + (insert-file-contents-literally filename)) + (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\\)" nil t) + "<#!+/?\\(part\\|multipart\\|external\\)" nil t) (delete-region (+ (match-beginning 0) 2) - (+ (match-beginning 0) 3))))) - (setq charset (mm-encode-body) - encoding (mm-body-encoding)) + (+ (match-beginning 0) 3)))))) + (setq charset (mm-encode-body)) + (setq encoding (mm-body-encoding charset)) (setq coded (buffer-string))) (mm-with-unibyte-buffer - (if (setq filename (cdr (assq 'filename cont))) - (insert-file-contents-literally filename) - (insert (cdr (assq 'contents cont)))) + (cond + ((cdr (assq 'buffer cont)) + (insert-buffer-substring (cdr (assq 'buffer cont)))) + ((setq filename (cdr (assq 'filename cont))) + (insert-file-contents-literally filename)) + (t + (insert (cdr (assq 'contents cont))))) (setq encoding (mm-encode-buffer type) coded (buffer-string)))) (mml-insert-mime-headers cont type charset encoding) @@ -162,13 +242,15 @@ (when name (setq name (mml-parse-file-name name)) (if (stringp name) - (insert ";\n name=\"" (prin1-to-string name) + (insert ";\n " (mail-header-encode-parameter "name" name) "\";\n access-type=local-file") (insert - (format ";\n name=%S;\n site=%S;\n directory=%S" - (file-name-nondirectory (nth 2 name)) - (nth 1 name) - (file-name-directory (nth 2 name)))) + (format ";\n " + (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))))) (insert ";\n access-type=" (if (member (nth 0 name) '("ftp@" "anonymous@")) "anon-ftp" @@ -211,11 +293,16 @@ (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)))) + (cond + ((cdr (assq 'buffer cont)) + (insert-buffer-substring (cdr (assq 'buffer cont)))) + ((setq filename (cdr (assq 'filename cont))) + (insert-file-contents-literally filename)) + (t + (insert (cdr (assq 'contents cont))))) (goto-char (point-min)) - (when (re-search-forward (concat "^--" mml-boundary) nil t) + (when (re-search-forward (concat "^--" (regexp-quote mml-boundary)) + nil t) (setq mml-boundary (mml-make-boundary)) (throw 'not-unique nil)))) ((eq (car cont) 'multipart) @@ -237,22 +324,27 @@ (defun mml-insert-mime-headers (cont type charset encoding) (let (parameters disposition description) + (setq parameters + (mml-parameter-string + cont '(name access-type expiration size permission))) (when (or charset - (setq parameters - (mml-parameter-string - cont '(name access-type expiration size permission))) + parameters (not (equal type "text/plain"))) + (when (consp charset) + (error + "Can't encode a part with several charsets.")) (insert "Content-Type: " type) (when charset - (insert (format "; charset=\"%s\"" charset))) + (insert "; " (mail-header-encode-parameter + "charset" (symbol-name charset)))) (when parameters (insert parameters)) (insert "\n")) + (setq parameters + (mml-parameter-string + cont '(filename creation-date modification-date read-date))) (when (or (setq disposition (cdr (assq 'disposition cont))) - (setq parameters - (mml-parameter-string - cont '(filename creation-date modification-date - read-date)))) + parameters) (insert "Content-Disposition: " (or disposition "inline")) (when parameters (insert parameters)) @@ -260,18 +352,20 @@ (unless (eq encoding '7bit) (insert (format "Content-Transfer-Encoding: %s\n" encoding))) (when (setq description (cdr (assq 'description cont))) - (insert "Content-Description: " description "\n")) - )) + (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))) - (setq string (concat string ";\n " (symbol-name type) "=" - (if (string-match "[^_0-9A-Za-z]" value) - (prin1-to-string value) - value))))) + ;; Strip directory component from the filename parameter. + (when (eq type 'filename) + (setq value (file-name-nondirectory value))) + (setq string (concat string ";\n " + (mail-header-encode-parameter + (symbol-name type) value))))) (when (not (zerop (length string))) string))) @@ -286,6 +380,207 @@ (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 () + "Translate the current buffer (which should be a message) into MML." + ;; First decode the head. + (save-restriction + (message-narrow-to-head) + (mail-decode-encoded-word-region (point-min) (point-max))) + (let ((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))) + +(defun mml-to-mime () + "Translate the current buffer from MML to MIME." + (message-encode-message-body) + (save-restriction + (message-narrow-to-headers) + (mail-encode-encoded-word-buffer))) + +(defun mml-insert-mime (handle &optional no-markup) + (let (textp buffer) + ;; Determine type and stuff. + (unless (stringp (car handle)) + (unless (setq textp (equal + (car (split-string + (car (mm-handle-type handle)) "/")) + "text")) + (save-excursion + (set-buffer (setq buffer (generate-new-buffer " *mml*"))) + (mm-insert-part handle)))) + (unless no-markup + (mml-insert-mml-markup handle buffer)) + (cond + ((stringp (car handle)) + (mapcar 'mml-insert-mime (cdr handle)) + (insert "<#/multipart>\n")) + (textp + (mm-insert-part handle) + (goto-char (point-max))) + (t + (insert "<#/part>\n"))))) + +(defun mml-insert-mml-markup (handle &optional buffer) + "Take a MIME handle and insert an MML tag." + (if (stringp (car handle)) + (insert "<#multipart type=" (cadr (split-string (car handle) "/")) + ">\n") + (insert "<#part type=" (car (mm-handle-type handle))) + (dolist (elem (append (cdr (mm-handle-type handle)) + (cdr (mm-handle-disposition handle)))) + (insert " " (symbol-name (car elem)) "=\"" (cdr elem) "\"")) + (when buffer + (insert " buffer=\"" (buffer-name buffer) "\"")) + (when (mm-handle-description handle) + (insert " description=\"" (mm-handle-description handle) "\"")) + (equal (split-string (car (mm-handle-type handle)) "/") "text") + (insert ">\n"))) + +;;; +;;; Mode for inserting and editing MML forms +;;; + +(defvar mml-mode-map + (let ((map (make-sparse-keymap)) + (main (make-sparse-keymap))) + (define-key map "f" 'mml-attach-file) + (define-key map "b" 'mml-attach-buffer) + (define-key map "q" 'mml-quote-region) + (define-key map "m" 'mml-insert-multipart) + (define-key map "q" 'mml-insert-part) + (define-key map "v" 'mml-validate) + (define-key main "\M-m" map) + main)) + +(easy-menu-define + mml-menu mml-mode-map "" + '("MML" + ("Attach" + ["File" mml-attach-file t] + ["Buffer" mml-attach-buffer t]) + ("Insert" + ["Multipart" mml-insert-multipart t] + ["Part" mml-insert-part t]) + ["Quote" mml-quote-region t] + ["Validate" mml-validate t])) + +(defvar mml-mode nil + "Minor mode for editing MML.") + +(defun mml-mode (&optional arg) + "Minor mode for editing MML. + +\\{mml-mode-map}" + (interactive "P") + (if (not (set (make-local-variable 'mml-mode) + (if (null arg) (not mml-mode) + (> (prefix-numeric-value arg) 0)))) + nil + (set (make-local-variable 'mml-mode) t) + (unless (assq 'mml-mode minor-mode-alist) + (push `(mml-mode " MML") minor-mode-alist)) + (unless (assq 'mml-mode minor-mode-map-alist) + (push (cons 'mml-mode mml-mode-map) + minor-mode-map-alist))) + (run-hooks 'mml-mode-hook)) + +(defun mml-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-read-type (file) + (let* ((default (or (mm-default-file-encoding file) + ;; 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) + (delete-duplicates + (mapcar (lambda (m) (list (cdr m))) mailcap-mime-extensions) + :test 'equal)))) + (if (not (equal string "")) + string + default))) + +(defun mml-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 + (goto-char beg) + ;; Quote parts. + (while (re-search-forward + "<#/?!*\\(multipart\\|part\\|external\\)" end t) + (goto-char (match-beginning 1)) + (insert "!")))) + +(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-read-file "Attach file: ")) + (type (mml-read-type file)) + (description (mml-read-description))) + (list file type description))) + (insert + (format + "<#part type=%s name=%s filename=%s%s disposition=attachment><#/part>\n" + type (prin1-to-string (file-name-nondirectory file)) + (prin1-to-string file) + (if description + (format " description=%s" (prin1-to-string 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-read-file "Attach external file: ")) + (type (mml-read-type file)) + (description (mml-read-description))) + (list file type description))) + (insert (format + "<#external type=%s name=%s disposition=attachment><#/external>\n" + type (prin1-to-string file)))) + + (provide 'mml) ;;; mml.el ends here