1 ;;; mml.el --- A package for parsing and validating MML documents
2 ;; Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005
3 ;; Free Software Foundation, Inc.
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6 ;; This file is part of GNU Emacs.
8 ;; GNU Emacs is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation; either version 2, or (at your option)
13 ;; GNU Emacs is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with GNU Emacs; see the file COPYING. If not, write to the
20 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 ;; Boston, MA 02110-1301, USA.
32 (eval-when-compile (require 'cl))
35 (autoload 'message-make-message-id "message")
36 (autoload 'gnus-setup-posting-charset "gnus-msg")
37 (autoload 'gnus-add-minor-mode "gnus-ems")
38 (autoload 'gnus-make-local-hook "gnus-util")
39 (autoload 'message-fetch-field "message")
40 (autoload 'fill-flowed-encode "flow-fill")
41 (autoload 'message-posting-charset "message"))
43 (defcustom mml-content-type-parameters
44 '(name access-type expiration size permission format)
45 "*A list of acceptable parameters in MML tag.
46 These parameters are generated in Content-Type header if exists."
48 :type '(repeat (symbol :tag "Parameter"))
51 (defcustom mml-content-disposition-parameters
52 '(filename creation-date modification-date read-date)
53 "*A list of acceptable parameters in MML tag.
54 These parameters are generated in Content-Disposition header if exists."
56 :type '(repeat (symbol :tag "Parameter"))
59 (defcustom mml-insert-mime-headers-always nil
60 "If non-nil, always put Content-Type: text/plain at top of empty parts.
61 It is necessary to work against a bug in certain clients."
66 (defvar mml-tweak-type-alist nil
67 "A list of (TYPE . FUNCTION) for tweaking MML parts.
68 TYPE is a string containing a regexp to match the MIME type. FUNCTION
69 is a Lisp function which is called with the MML handle to tweak the
70 part. This variable is used only when no TWEAK parameter exists in
73 (defvar mml-tweak-function-alist nil
74 "A list of (NAME . FUNCTION) for tweaking MML parts.
75 NAME is a string containing the name of the TWEAK parameter in the MML
76 handle. FUNCTION is a Lisp function which is called with the MML
77 handle to tweak the part.")
79 (defvar mml-tweak-sexp-alist
80 '((mml-externalize-attachments . mml-tweak-externalize-attachments))
81 "A list of (SEXP . FUNCTION) for tweaking MML parts.
82 SEXP is an s-expression. If the evaluation of SEXP is non-nil, FUNCTION
83 is called. FUNCTION is a Lisp function which is called with the MML
84 handle to tweak the part.")
86 (defvar mml-externalize-attachments nil
87 "*If non-nil, local-file attachments are generated as external parts.")
89 (defvar mml-generate-multipart-alist nil
90 "*Alist of multipart generation functions.
91 Each entry has the form (NAME . FUNCTION), where
92 NAME is a string containing the name of the part (without the
93 leading \"/multipart/\"),
94 FUNCTION is a Lisp function which is called to generate the part.
96 The Lisp function has to supply the appropriate MIME headers and the
97 contents of this part.")
99 (defvar mml-syntax-table
100 (let ((table (copy-syntax-table emacs-lisp-mode-syntax-table)))
101 (modify-syntax-entry ?\\ "/" table)
102 (modify-syntax-entry ?< "(" table)
103 (modify-syntax-entry ?> ")" table)
104 (modify-syntax-entry ?@ "w" table)
105 (modify-syntax-entry ?/ "w" table)
106 (modify-syntax-entry ?= " " table)
107 (modify-syntax-entry ?* " " table)
108 (modify-syntax-entry ?\; " " table)
109 (modify-syntax-entry ?\' " " table)
112 (defvar mml-boundary-function 'mml-make-boundary
113 "A function called to suggest a boundary.
114 The function may be called several times, and should try to make a new
115 suggestion each time. The function is called with one parameter,
116 which is a number that says how many times the function has been
117 called for this message.")
119 (defvar mml-confirmation-set nil
120 "A list of symbols, each of which disables some warning.
121 `unknown-encoding': always send messages contain characters with
122 unknown encoding; `use-ascii': always use ASCII for those characters
123 with unknown encoding; `multipart': always send messages with more than
126 (defvar mml-generate-default-type "text/plain"
127 "Content type by which the Content-Type header can be omitted.
128 The Content-Type header will not be put in the MIME part if the type
129 equals the value and there's no parameter (e.g. charset, format, etc.)
130 and `mml-insert-mime-headers-always' is nil. The value will be bound
131 to \"message/rfc822\" when encoding an article to be forwarded as a MIME
132 part. This is for the internal use, you should never modify the value.")
134 (defvar mml-buffer-list nil)
136 (defun mml-generate-new-buffer (name)
137 (let ((buf (generate-new-buffer name)))
138 (push buf mml-buffer-list)
141 (defun mml-destroy-buffers ()
142 (let (kill-buffer-hook)
143 (mapcar 'kill-buffer mml-buffer-list)
144 (setq mml-buffer-list nil)))
147 "Parse the current buffer as an MML document."
149 (goto-char (point-min))
150 (let ((table (syntax-table)))
153 (set-syntax-table mml-syntax-table)
155 (set-syntax-table table)))))
157 (defun mml-parse-1 ()
158 "Parse the current buffer as an MML document."
159 (let (struct tag point contents charsets warn use-ascii no-markup-p raw)
160 (while (and (not (eobp))
161 (not (looking-at "<#/multipart")))
163 ((looking-at "<#secure")
164 ;; The secure part is essentially a meta-meta tag, which
165 ;; expands to either a part tag if there are no other parts in
166 ;; the document or a multipart tag if there are other parts
167 ;; included in the message
169 (taginfo (mml-read-tag))
170 (recipients (cdr (assq 'recipients taginfo)))
171 (sender (cdr (assq 'sender taginfo)))
172 (location (cdr (assq 'tag-location taginfo)))
173 (mode (cdr (assq 'mode taginfo)))
174 (method (cdr (assq 'method taginfo)))
179 "<#\\(/\\)?\\(multipart\\|part\\|external\\|mml\\)." nil t)
180 (setq secure-mode "multipart")
181 (setq secure-mode "part")))
184 (re-search-forward "<#secure[^\n]*>\n"))
185 (delete-region (match-beginning 0) (match-end 0))
186 (cond ((string= mode "sign")
187 (setq tags (list "sign" method)))
188 ((string= mode "encrypt")
189 (setq tags (list "encrypt" method)))
190 ((string= mode "signencrypt")
191 (setq tags (list "sign" method "encrypt" method))))
192 (eval `(mml-insert-tag ,secure-mode
194 ,(if recipients "recipients")
196 ,(if sender "sender")
199 (goto-char location)))
200 ((looking-at "<#multipart")
201 (push (nconc (mml-read-tag) (mml-parse-1)) struct))
202 ((looking-at "<#external")
203 (push (nconc (mml-read-tag) (list (cons 'contents (mml-read-part))))
206 (if (or (looking-at "<#part") (looking-at "<#mml"))
207 (setq tag (mml-read-tag)
210 (setq tag (list 'part '(type . "text/plain"))
213 (setq raw (cdr (assq 'raw tag))
215 contents (mml-read-part (eq 'mml (car tag)))
220 (intern (downcase (cdr (assq 'charset tag))))))
222 (mm-find-mime-charset-region point (point)
224 (when (and (not raw) (memq nil charsets))
225 (if (or (memq 'unknown-encoding mml-confirmation-set)
226 (message-options-get 'unknown-encoding)
228 Message contains characters with unknown encoding. Really send? ")
229 (message-options-set 'unknown-encoding t)))
231 (or (memq 'use-ascii mml-confirmation-set)
232 (message-options-get 'use-ascii)
233 (and (y-or-n-p "Use ASCII as charset? ")
234 (message-options-set 'use-ascii t))))
235 (setq charsets (delq nil charsets))
237 (error "Edit your message to remove those characters")))
240 (< (length charsets) 2))
241 (if (or (not no-markup-p)
242 (string-match "[^ \t\r\n]" contents))
243 ;; Don't create blank parts.
244 (push (nconc tag (list (cons 'contents contents)))
246 (let ((nstruct (mml-parse-singlepart-with-multiple-charsets
247 tag point (point) use-ascii)))
249 (not (memq 'multipart mml-confirmation-set))
250 (not (message-options-get 'multipart))
251 (not (and (y-or-n-p (format "\
252 A message part needs to be split into %d charset parts. Really send? "
254 (message-options-set 'multipart t))))
255 (error "Edit your message to use only one charset"))
256 (setq struct (nconc nstruct struct)))))))
261 (defun mml-parse-singlepart-with-multiple-charsets
262 (orig-tag beg end &optional use-ascii)
265 (narrow-to-region beg end)
266 (goto-char (point-min))
267 (let ((current (or (mm-mime-charset (mm-charset-after))
268 (and use-ascii 'us-ascii)))
269 charset struct space newline paragraph)
271 (setq charset (mm-mime-charset (mm-charset-after)))
273 ;; The charset remains the same.
274 ((eq charset 'us-ascii))
275 ((or (and use-ascii (not charset))
276 (eq charset current))
280 ;; The initial charset was ascii.
281 ((eq current 'us-ascii)
282 (setq current charset
286 ;; We have a change in charsets.
290 (list (cons 'contents
291 (buffer-substring-no-properties
292 beg (or paragraph newline space (point))))))
294 (setq beg (or paragraph newline space (point))
299 ;; Compute places where it might be nice to break the part.
301 ((memq (following-char) '(? ?\t))
302 (setq space (1+ (point))))
303 ((and (eq (following-char) ?\n)
305 (eq (char-after (1- (point))) ?\n))
306 (setq paragraph (point)))
307 ((eq (following-char) ?\n)
308 (setq newline (1+ (point)))))
310 ;; Do the final part.
311 (unless (= beg (point))
312 (push (append orig-tag
313 (list (cons 'contents
314 (buffer-substring-no-properties
319 (defun mml-read-tag ()
320 "Read a tag and return the contents."
321 (let ((orig-point (point))
322 contents name elem val)
324 (setq name (buffer-substring-no-properties
325 (point) (progn (forward-sexp 1) (point))))
326 (skip-chars-forward " \t\n")
327 (while (not (looking-at ">[ \t]*\n?"))
328 (setq elem (buffer-substring-no-properties
329 (point) (progn (forward-sexp 1) (point))))
330 (skip-chars-forward "= \t\n")
331 (setq val (buffer-substring-no-properties
332 (point) (progn (forward-sexp 1) (point))))
333 (when (string-match "^\"\\(.*\\)\"$" val)
334 (setq val (match-string 1 val)))
335 (push (cons (intern elem) val) contents)
336 (skip-chars-forward " \t\n"))
337 (goto-char (match-end 0))
338 ;; Don't skip the leading space.
339 ;;(skip-chars-forward " \t\n")
340 ;; Put the tag location into the returned contents
341 (setq contents (append (list (cons 'tag-location orig-point)) contents))
342 (cons (intern name) (nreverse contents))))
344 (defun mml-buffer-substring-no-properties-except-hard-newlines (start end)
345 (let ((str (buffer-substring-no-properties start end))
346 (bufstart start) tmp)
347 (while (setq tmp (text-property-any start end 'hard 't))
348 (set-text-properties (- tmp bufstart) (- tmp bufstart -1)
350 (setq start (1+ tmp)))
353 (defun mml-read-part (&optional mml)
354 "Return the buffer up till the next part, multipart or closing part or multipart.
355 If MML is non-nil, return the buffer up till the correspondent mml tag."
356 (let ((beg (point)) (count 1))
357 ;; If the tag ended at the end of the line, we go to the next line.
358 (when (looking-at "[ \t]*\n")
362 (while (and (> count 0) (not (eobp)))
363 (if (re-search-forward "<#\\(/\\)?mml." nil t)
364 (setq count (+ count (if (match-beginning 1) -1 1)))
365 (goto-char (point-max))))
366 (mml-buffer-substring-no-properties-except-hard-newlines
369 (match-beginning 0))))
370 (if (re-search-forward
371 "<#\\(/\\)?\\(multipart\\|part\\|external\\|mml\\)." nil t)
373 (mml-buffer-substring-no-properties-except-hard-newlines
374 beg (match-beginning 0))
375 (if (or (not (match-beginning 1))
376 (equal (match-string 2) "multipart"))
377 (goto-char (match-beginning 0))
378 (when (looking-at "[ \t]*\n")
380 (mml-buffer-substring-no-properties-except-hard-newlines
381 beg (goto-char (point-max)))))))
383 (defvar mml-boundary nil)
384 (defvar mml-base-boundary "-=-=")
385 (defvar mml-multipart-number 0)
387 (defun mml-generate-mime ()
388 "Generate a MIME message based on the current MML document."
389 (let ((cont (mml-parse))
390 (mml-multipart-number mml-multipart-number))
394 (if (and (consp (car cont))
396 (mml-generate-mime-1 (car cont))
397 (mml-generate-mime-1 (nconc (list 'multipart '(type . "mixed"))
401 (defun mml-generate-mime-1 (cont)
402 (let ((mm-use-ultra-safe-encoding
403 (or mm-use-ultra-safe-encoding (assq 'sign cont))))
405 (narrow-to-region (point) (point))
406 (mml-tweak-part cont)
408 ((or (eq (car cont) 'part) (eq (car cont) 'mml))
409 (let* ((raw (cdr (assq 'raw cont)))
410 (filename (cdr (assq 'filename cont)))
411 (type (or (cdr (assq 'type cont))
412 (and filename (mm-default-file-encoding filename))
413 "application/octet-stream"))
414 coded encoding charset flowed)
416 (member (car (split-string type "/")) '("text" "message")))
419 (setq charset (mm-charset-to-coding-system
420 (cdr (assq 'charset cont))))
421 (when (eq charset 'ascii)
424 ((cdr (assq 'buffer cont))
425 (insert-buffer-substring (cdr (assq 'buffer cont))))
427 (not (equal (cdr (assq 'nofile cont)) "yes")))
428 (let ((coding-system-for-read charset))
429 (mm-insert-file-contents filename)))
430 ((eq 'mml (car cont))
431 (insert (cdr (assq 'contents cont))))
434 (narrow-to-region (point) (point))
435 (insert (cdr (assq 'contents cont)))
436 ;; Remove quotes from quoted tags.
437 (goto-char (point-min))
438 (while (re-search-forward
439 "<#!+/?\\(part\\|multipart\\|external\\|mml\\)"
441 (delete-region (+ (match-beginning 0) 2)
442 (+ (match-beginning 0) 3))))))
444 ((eq (car cont) 'mml)
445 (let ((mml-boundary (mml-compute-boundary cont))
446 ;; It is necessary for the case where this
447 ;; function is called recursively since
448 ;; `m-g-d-t' will be bound to "message/rfc822"
449 ;; when encoding an article to be forwarded.
450 (mml-generate-default-type "text/plain"))
452 (let ((mm-7bit-chars (concat mm-7bit-chars "\x1b")))
453 ;; ignore 0x1b, it is part of iso-2022-jp
454 (setq encoding (mm-body-7-or-8))))
455 ((string= (car (split-string type "/")) "message")
456 (let ((mm-7bit-chars (concat mm-7bit-chars "\x1b")))
457 ;; ignore 0x1b, it is part of iso-2022-jp
458 (setq encoding (mm-body-7-or-8))))
460 ;; Only perform format=flowed filling on text/plain
461 ;; parts where there either isn't a format parameter
462 ;; in the mml tag or it says "flowed" and there
463 ;; actually are hard newlines in the text.
464 (let (use-hard-newlines)
465 (when (and (string= type "text/plain")
466 (not (string= (cdr (assq 'sign cont)) "pgp"))
467 (or (null (assq 'format cont))
468 (string= (cdr (assq 'format cont))
470 (setq use-hard-newlines
472 (point-min) (point-max) 'hard 't)))
474 ;; Indicate that `mml-insert-mime-headers' should
475 ;; insert a "; format=flowed" string unless the
476 ;; user has already specified it.
477 (setq flowed (null (assq 'format cont)))))
478 (setq charset (mm-encode-body charset))
479 (setq encoding (mm-body-encoding
480 charset (cdr (assq 'encoding cont))))))
481 (setq coded (buffer-string)))
482 (mml-insert-mime-headers cont type charset encoding flowed)
485 (mm-with-unibyte-buffer
487 ((cdr (assq 'buffer cont))
488 (insert (with-current-buffer (cdr (assq 'buffer cont))
489 (mm-with-unibyte-current-buffer
492 (not (equal (cdr (assq 'nofile cont)) "yes")))
493 (let ((coding-system-for-read mm-binary-coding-system))
494 (mm-insert-file-contents filename nil nil nil nil t)))
496 (insert (cdr (assq 'contents cont)))))
497 (setq encoding (mm-encode-buffer type)
498 coded (mm-string-as-multibyte (buffer-string))))
499 (mml-insert-mime-headers cont type charset encoding nil)
501 (mm-with-unibyte-current-buffer
503 ((eq (car cont) 'external)
504 (insert "Content-Type: message/external-body")
505 (let ((parameters (mml-parameter-string
506 cont '(expiration size permission)))
507 (name (cdr (assq 'name cont)))
508 (url (cdr (assq 'url cont))))
510 (setq name (mml-parse-file-name name))
512 (mml-insert-parameter
513 (mail-header-encode-parameter "name" name)
514 "access-type=local-file")
515 (mml-insert-parameter
516 (mail-header-encode-parameter
517 "name" (file-name-nondirectory (nth 2 name)))
518 (mail-header-encode-parameter "site" (nth 1 name))
519 (mail-header-encode-parameter
520 "directory" (file-name-directory (nth 2 name))))
521 (mml-insert-parameter
522 (concat "access-type="
523 (if (member (nth 0 name) '("ftp@" "anonymous@"))
527 (mml-insert-parameter
528 (mail-header-encode-parameter "url" url)
531 (mml-insert-parameter-string
532 cont '(expiration size permission)))
534 (insert "Content-Type: "
535 (or (cdr (assq 'type cont))
536 (and name (mm-default-file-encoding name))
537 "application/octet-stream")
539 (insert "Content-ID: " (message-make-message-id) "\n")
540 (insert "Content-Transfer-Encoding: "
541 (or (cdr (assq 'encoding cont)) "binary"))
543 (insert (or (cdr (assq 'contents cont))))
545 ((eq (car cont) 'multipart)
546 (let* ((type (or (cdr (assq 'type cont)) "mixed"))
547 (mml-generate-default-type (if (equal type "digest")
550 (handler (assoc type mml-generate-multipart-alist)))
552 (funcall (cdr handler) cont)
553 ;; No specific handler. Use default one.
554 (let ((mml-boundary (mml-compute-boundary cont)))
555 (insert (format "Content-Type: multipart/%s; boundary=\"%s\""
557 (if (cdr (assq 'start cont))
558 (format "; start=\"%s\"\n" (cdr (assq 'start cont)))
560 (let ((cont cont) part)
561 (while (setq part (pop cont))
562 ;; Skip `multipart' and attributes.
563 (when (and (consp part) (consp (cdr part)))
564 (insert "\n--" mml-boundary "\n")
565 (mml-generate-mime-1 part))))
566 (insert "\n--" mml-boundary "--\n")))))
568 (error "Invalid element: %S" cont)))
569 ;; handle sign & encrypt tags in a semi-smart way.
570 (let ((sign-item (assoc (cdr (assq 'sign cont)) mml-sign-alist))
571 (encrypt-item (assoc (cdr (assq 'encrypt cont))
574 (when (or sign-item encrypt-item)
575 (when (setq sender (cdr (assq 'sender cont)))
576 (message-options-set 'mml-sender sender)
577 (message-options-set 'message-sender sender))
578 (if (setq recipients (cdr (assq 'recipients cont)))
579 (message-options-set 'message-recipients recipients))
580 (let ((style (mml-signencrypt-style
581 (first (or sign-item encrypt-item)))))
582 ;; check if: we're both signing & encrypting, both methods
583 ;; are the same (why would they be different?!), and that
584 ;; the signencrypt style allows for combined operation.
585 (if (and sign-item encrypt-item (equal (first sign-item)
586 (first encrypt-item))
587 (equal style 'combined))
588 (funcall (nth 1 encrypt-item) cont t)
589 ;; otherwise, revert to the old behavior.
591 (funcall (nth 1 sign-item) cont))
593 (funcall (nth 1 encrypt-item) cont)))))))))
595 (defun mml-compute-boundary (cont)
596 "Return a unique boundary that does not exist in CONT."
597 (let ((mml-boundary (funcall mml-boundary-function
598 (incf mml-multipart-number))))
599 ;; This function tries again and again until it has found
600 ;; a unique boundary.
601 (while (not (catch 'not-unique
602 (mml-compute-boundary-1 cont))))
605 (defun mml-compute-boundary-1 (cont)
608 ((eq (car cont) 'part)
611 ((cdr (assq 'buffer cont))
612 (insert-buffer-substring (cdr (assq 'buffer cont))))
613 ((and (setq filename (cdr (assq 'filename cont)))
614 (not (equal (cdr (assq 'nofile cont)) "yes")))
615 (mm-insert-file-contents filename nil nil nil nil t))
617 (insert (cdr (assq 'contents cont)))))
618 (goto-char (point-min))
619 (when (re-search-forward (concat "^--" (regexp-quote mml-boundary))
621 (setq mml-boundary (funcall mml-boundary-function
622 (incf mml-multipart-number)))
623 (throw 'not-unique nil))))
624 ((eq (car cont) 'multipart)
625 (mapcar 'mml-compute-boundary-1 (cddr cont))))
628 (defun mml-make-boundary (number)
629 (concat (make-string (% number 60) ?=)
635 (defun mml-insert-mime-headers (cont type charset encoding flowed)
636 (let (parameters id disposition description)
638 (mml-parameter-string
639 cont mml-content-type-parameters))
643 (not (equal type mml-generate-default-type))
644 mml-insert-mime-headers-always)
645 (when (consp charset)
647 "Can't encode a part with several charsets"))
648 (insert "Content-Type: " type)
650 (insert "; " (mail-header-encode-parameter
651 "charset" (symbol-name charset))))
653 (insert "; format=flowed"))
655 (mml-insert-parameter-string
656 cont mml-content-type-parameters))
658 (when (setq id (cdr (assq 'id cont)))
659 (insert "Content-ID: " id "\n"))
661 (mml-parameter-string
662 cont mml-content-disposition-parameters))
663 (when (or (setq disposition (cdr (assq 'disposition cont)))
665 (insert "Content-Disposition: " (or disposition "inline"))
667 (mml-insert-parameter-string
668 cont mml-content-disposition-parameters))
670 (unless (eq encoding '7bit)
671 (insert (format "Content-Transfer-Encoding: %s\n" encoding)))
672 (when (setq description (cdr (assq 'description cont)))
673 (insert "Content-Description: "
674 (mail-encode-encoded-word-string description) "\n"))))
676 (defun mml-parameter-string (cont types)
679 (while (setq type (pop types))
680 (when (setq value (cdr (assq type cont)))
681 ;; Strip directory component from the filename parameter.
682 (when (eq type 'filename)
683 (setq value (file-name-nondirectory value)))
684 (setq string (concat string "; "
685 (mail-header-encode-parameter
686 (symbol-name type) value)))))
687 (when (not (zerop (length string)))
690 (defun mml-insert-parameter-string (cont types)
692 (while (setq type (pop types))
693 (when (setq value (cdr (assq type cont)))
694 ;; Strip directory component from the filename parameter.
695 (when (eq type 'filename)
696 (setq value (file-name-nondirectory value)))
697 (mml-insert-parameter
698 (mail-header-encode-parameter
699 (symbol-name type) value))))))
702 (defvar ange-ftp-name-format)
703 (defvar efs-path-regexp))
704 (defun mml-parse-file-name (path)
705 (if (if (boundp 'efs-path-regexp)
706 (string-match efs-path-regexp path)
707 (if (boundp 'ange-ftp-name-format)
708 (string-match (car ange-ftp-name-format) path)))
709 (list (match-string 1 path) (match-string 2 path)
710 (substring path (1+ (match-end 2))))
713 (defun mml-insert-buffer (buffer)
714 "Insert BUFFER at point and quote any MML markup."
716 (narrow-to-region (point) (point))
717 (insert-buffer-substring buffer)
718 (mml-quote-region (point-min) (point-max))
719 (goto-char (point-max))))
722 ;;; Transforming MIME to MML
725 (defun mime-to-mml (&optional handles)
726 "Translate the current buffer (which should be a message) into MML.
727 If HANDLES is non-nil, use it instead reparsing the buffer."
728 ;; First decode the head.
730 (message-narrow-to-head)
731 (let ((rfc2047-quote-decoded-words-containing-tspecials t))
732 (mail-decode-encoded-word-region (point-min) (point-max))))
734 (setq handles (mm-dissect-buffer t)))
735 (goto-char (point-min))
736 (search-forward "\n\n" nil t)
737 (delete-region (point) (point-max))
738 (if (stringp (car handles))
739 (mml-insert-mime handles)
740 (mml-insert-mime handles t))
741 (mm-destroy-parts handles)
743 (message-narrow-to-head)
744 ;; Remove them, they are confusing.
745 (message-remove-header "Content-Type")
746 (message-remove-header "MIME-Version")
747 (message-remove-header "Content-Disposition")
748 (message-remove-header "Content-Transfer-Encoding")))
750 (defun mml-to-mime ()
751 "Translate the current buffer from MML to MIME."
752 (message-encode-message-body)
754 (message-narrow-to-headers-or-head)
755 ;; Skip past any From_ headers.
756 (while (looking-at "From ")
758 (let ((mail-parse-charset message-default-charset))
759 (mail-encode-encoded-word-buffer))))
761 (defun mml-insert-mime (handle &optional no-markup)
762 (let (textp buffer mmlp)
763 ;; Determine type and stuff.
764 (unless (stringp (car handle))
765 (unless (setq textp (equal (mm-handle-media-supertype handle) "text"))
767 (set-buffer (setq buffer (mml-generate-new-buffer " *mml*")))
768 (mm-insert-part handle)
769 (if (setq mmlp (equal (mm-handle-media-type handle)
773 (mml-insert-mml-markup handle nil t t)
774 (unless (and no-markup
775 (equal (mm-handle-media-type handle) "text/plain"))
776 (mml-insert-mml-markup handle buffer textp)))
779 (insert-buffer-substring buffer)
780 (goto-char (point-max))
781 (insert "<#/mml>\n"))
782 ((stringp (car handle))
783 (mapcar 'mml-insert-mime (cdr handle))
784 (insert "<#/multipart>\n"))
786 (let ((charset (mail-content-type-get
787 (mm-handle-type handle) 'charset))
789 (if (eq charset 'gnus-decoded)
790 (mm-insert-part handle)
791 (insert (mm-decode-string (mm-get-part handle) charset)))
792 (mml-quote-region start (point)))
793 (goto-char (point-max)))
795 (insert "<#/part>\n")))))
797 (defun mml-insert-mml-markup (handle &optional buffer nofile mmlp)
798 "Take a MIME handle and insert an MML tag."
799 (if (stringp (car handle))
801 (insert "<#multipart type=" (mm-handle-media-subtype handle))
802 (let ((start (mm-handle-multipart-ctl-parameter handle 'start)))
804 (insert " start=\"" start "\"")))
807 (insert "<#mml type=" (mm-handle-media-type handle))
808 (insert "<#part type=" (mm-handle-media-type handle)))
809 (dolist (elem (append (cdr (mm-handle-type handle))
810 (cdr (mm-handle-disposition handle))))
811 (unless (symbolp (cdr elem))
812 (insert " " (symbol-name (car elem)) "=\"" (cdr elem) "\"")))
813 (when (mm-handle-id handle)
814 (insert " id=\"" (mm-handle-id handle) "\""))
815 (when (mm-handle-disposition handle)
816 (insert " disposition=" (car (mm-handle-disposition handle))))
818 (insert " buffer=\"" (buffer-name buffer) "\""))
820 (insert " nofile=yes"))
821 (when (mm-handle-description handle)
822 (insert " description=\"" (mm-handle-description handle) "\""))
825 (defun mml-insert-parameter (&rest parameters)
826 "Insert PARAMETERS in a nice way."
827 (dolist (param parameters)
829 (let ((point (point)))
831 (when (> (current-column) 71)
837 ;;; Mode for inserting and editing MML forms
841 (let ((sign (make-sparse-keymap))
842 (encrypt (make-sparse-keymap))
843 (signpart (make-sparse-keymap))
844 (encryptpart (make-sparse-keymap))
845 (map (make-sparse-keymap))
846 (main (make-sparse-keymap)))
847 (define-key sign "p" 'mml-secure-message-sign-pgpmime)
848 (define-key sign "o" 'mml-secure-message-sign-pgp)
849 (define-key sign "s" 'mml-secure-message-sign-smime)
850 (define-key signpart "p" 'mml-secure-sign-pgpmime)
851 (define-key signpart "o" 'mml-secure-sign-pgp)
852 (define-key signpart "s" 'mml-secure-sign-smime)
853 (define-key encrypt "p" 'mml-secure-message-encrypt-pgpmime)
854 (define-key encrypt "o" 'mml-secure-message-encrypt-pgp)
855 (define-key encrypt "s" 'mml-secure-message-encrypt-smime)
856 (define-key encryptpart "p" 'mml-secure-encrypt-pgpmime)
857 (define-key encryptpart "o" 'mml-secure-encrypt-pgp)
858 (define-key encryptpart "s" 'mml-secure-encrypt-smime)
859 (define-key map "\C-n" 'mml-unsecure-message)
860 (define-key map "f" 'mml-attach-file)
861 (define-key map "b" 'mml-attach-buffer)
862 (define-key map "e" 'mml-attach-external)
863 (define-key map "q" 'mml-quote-region)
864 (define-key map "m" 'mml-insert-multipart)
865 (define-key map "p" 'mml-insert-part)
866 (define-key map "v" 'mml-validate)
867 (define-key map "P" 'mml-preview)
868 (define-key map "s" sign)
869 (define-key map "S" signpart)
870 (define-key map "c" encrypt)
871 (define-key map "C" encryptpart)
872 ;;(define-key map "n" 'mml-narrow-to-part)
873 ;; `M-m' conflicts with `back-to-indentation'.
874 ;; (define-key main "\M-m" map)
875 (define-key main "\C-c\C-m" map)
879 mml-menu mml-mode-map ""
881 ["Attach File..." mml-attach-file
882 ,@(if (featurep 'xemacs) '(t)
883 '(:help "Attach a file at point"))]
884 ["Attach Buffer..." mml-attach-buffer t]
885 ["Attach External..." mml-attach-external t]
886 ["Insert Part..." mml-insert-part t]
887 ["Insert Multipart..." mml-insert-multipart t]
888 ["PGP/MIME Sign" mml-secure-message-sign-pgpmime t]
889 ["PGP/MIME Encrypt" mml-secure-message-encrypt-pgpmime t]
890 ["PGP Sign" mml-secure-message-sign-pgp t]
891 ["PGP Encrypt" mml-secure-message-encrypt-pgp t]
892 ["S/MIME Sign" mml-secure-message-sign-smime t]
893 ["S/MIME Encrypt" mml-secure-message-encrypt-smime t]
895 ["PGP/MIME Sign Part" mml-secure-sign-pgpmime t]
896 ["PGP/MIME Encrypt Part" mml-secure-encrypt-pgpmime t]
897 ["PGP Sign Part" mml-secure-sign-pgp t]
898 ["PGP Encrypt Part" mml-secure-encrypt-pgp t]
899 ["S/MIME Sign Part" mml-secure-sign-smime t]
900 ["S/MIME Encrypt Part" mml-secure-encrypt-smime t])
901 ["Encrypt/Sign off" mml-unsecure-message t]
902 ;;["Narrow" mml-narrow-to-part t]
903 ["Quote MML" mml-quote-region t]
904 ["Validate MML" mml-validate t]
905 ["Preview" mml-preview t]))
908 "Minor mode for editing MML.")
910 (defun mml-mode (&optional arg)
911 "Minor mode for editing MML.
912 MML is the MIME Meta Language, a minor mode for composing MIME articles.
913 See Info node `(emacs-mime)Composing'.
917 (when (set (make-local-variable 'mml-mode)
918 (if (null arg) (not mml-mode)
919 (> (prefix-numeric-value arg) 0)))
920 (gnus-add-minor-mode 'mml-mode " MML" mml-mode-map)
921 (easy-menu-add mml-menu mml-mode-map)
922 (run-hooks 'mml-mode-hook)))
925 ;;; Helper functions for reading MIME stuff from the minibuffer and
926 ;;; inserting stuff to the buffer.
929 (defun mml-minibuffer-read-file (prompt)
930 (let* ((completion-ignored-extensions nil)
931 (file (read-file-name prompt nil nil t)))
932 ;; Prevent some common errors. This is inspired by similar code in
934 (when (file-directory-p file)
935 (error "%s is a directory, cannot attach" file))
936 (unless (file-exists-p file)
937 (error "No such file: %s" file))
938 (unless (file-readable-p file)
939 (error "Permission denied: %s" file))
942 (defun mml-minibuffer-read-type (name &optional default)
943 (mailcap-parse-mimetypes)
944 (let* ((default (or default
945 (mm-default-file-encoding name)
946 ;; Perhaps here we should check what the file
947 ;; looks like, and offer text/plain if it looks
949 "application/octet-stream"))
950 (string (completing-read
951 (format "Content type (default %s): " default)
952 (mapcar 'list (mailcap-mime-types)))))
953 (if (not (equal string ""))
957 (defun mml-minibuffer-read-description ()
958 (let ((description (read-string "One line description: ")))
959 (when (string-match "\\`[ \t]*\\'" description)
960 (setq description nil))
963 (defun mml-minibuffer-read-disposition (type &optional default)
964 (unless default (setq default
965 (if (and (string-match "\\`text/" type)
966 (not (string-match "\\`text/rtf\\'" type)))
969 (let ((disposition (completing-read
970 (format "Disposition (default %s): " default)
971 '(("attachment") ("inline") (""))
972 nil t nil nil default)))
973 (if (not (equal disposition ""))
977 (defun mml-quote-region (beg end)
978 "Quote the MML tags in the region."
982 ;; Temporarily narrow the region to defend from changes
984 (narrow-to-region beg end)
985 (goto-char (point-min))
987 (while (re-search-forward
988 "<#!*/?\\(multipart\\|part\\|external\\|mml\\)" nil t)
989 ;; Insert ! after the #.
990 (goto-char (+ (match-beginning 0) 2))
993 (defun mml-insert-tag (name &rest plist)
994 "Insert an MML tag described by NAME and PLIST."
996 (setq name (symbol-name name)))
999 (let ((key (pop plist))
1000 (value (pop plist)))
1002 ;; Quote VALUE if it contains suspicious characters.
1003 (when (string-match "[\"'\\~/*;() \t\n]" value)
1004 (setq value (with-output-to-string
1005 (let (print-escape-nonascii)
1007 (insert (format " %s=%s" key value)))))
1010 (defun mml-insert-empty-tag (name &rest plist)
1011 "Insert an empty MML tag described by NAME and PLIST."
1012 (when (symbolp name)
1013 (setq name (symbol-name name)))
1014 (apply #'mml-insert-tag name plist)
1015 (insert "<#/" name ">\n"))
1017 ;;; Attachment functions.
1019 (defun mml-attach-file (file &optional type description disposition)
1020 "Attach a file to the outgoing MIME message.
1021 The file is not inserted or encoded until you send the message with
1022 `\\[message-send-and-exit]' or `\\[message-send]'.
1024 FILE is the name of the file to attach. TYPE is its content-type, a
1025 string of the form \"type/subtype\". DESCRIPTION is a one-line
1026 description of the attachment."
1028 (let* ((file (mml-minibuffer-read-file "Attach file: "))
1029 (type (mml-minibuffer-read-type file))
1030 (description (mml-minibuffer-read-description))
1031 (disposition (mml-minibuffer-read-disposition type)))
1032 (list file type description disposition)))
1033 (mml-insert-empty-tag 'part
1036 'disposition (or disposition "attachment")
1037 'description description))
1039 (defun mml-attach-buffer (buffer &optional type description)
1040 "Attach a buffer to the outgoing MIME message.
1041 See `mml-attach-file' for details of operation."
1043 (let* ((buffer (read-buffer "Attach buffer: "))
1044 (type (mml-minibuffer-read-type buffer "text/plain"))
1045 (description (mml-minibuffer-read-description)))
1046 (list buffer type description)))
1047 (mml-insert-empty-tag 'part 'type type 'buffer buffer
1048 'disposition "attachment" 'description description))
1050 (defun mml-attach-external (file &optional type description)
1051 "Attach an external file into the buffer.
1052 FILE is an ange-ftp/efs specification of the part location.
1053 TYPE is the MIME type to use."
1055 (let* ((file (mml-minibuffer-read-file "Attach external file: "))
1056 (type (mml-minibuffer-read-type file))
1057 (description (mml-minibuffer-read-description)))
1058 (list file type description)))
1059 (mml-insert-empty-tag 'external 'type type 'name file
1060 'disposition "attachment" 'description description))
1062 (defun mml-insert-multipart (&optional type)
1063 (interactive (list (completing-read "Multipart type (default mixed): "
1064 '(("mixed") ("alternative") ("digest") ("parallel")
1065 ("signed") ("encrypted"))
1068 (setq type "mixed"))
1069 (mml-insert-empty-tag "multipart" 'type type)
1072 (defun mml-insert-part (&optional type)
1074 (list (mml-minibuffer-read-type "")))
1075 (mml-insert-tag 'part 'type type 'disposition "inline")
1078 (defun mml-preview-insert-mail-followup-to ()
1079 "Insert a Mail-Followup-To header before previewing an article.
1080 Should be adopted if code in `message-send-mail' is changed."
1081 (when (and (message-mail-p)
1082 (message-subscribed-p)
1083 (not (mail-fetch-field "mail-followup-to"))
1084 (message-make-mail-followup-to))
1085 (message-position-on-field "Mail-Followup-To" "X-Draft-From")
1086 (insert (message-make-mail-followup-to))))
1088 (defun mml-preview (&optional raw)
1089 "Display current buffer with Gnus, in a new buffer.
1090 If RAW, don't highlight the article."
1093 (let* ((buf (current-buffer))
1094 (message-options message-options)
1095 (message-this-is-mail (message-mail-p))
1096 (message-this-is-news (message-news-p))
1097 (message-posting-charset (or (gnus-setup-posting-charset
1099 (message-narrow-to-headers-or-head)
1100 (message-fetch-field "Newsgroups")))
1101 message-posting-charset)))
1102 (message-options-set-recipient)
1103 (pop-to-buffer (generate-new-buffer
1104 (concat (if raw "*Raw MIME preview of "
1105 "*MIME preview of ") (buffer-name))))
1106 (when (boundp 'gnus-buffers)
1107 (push (current-buffer) gnus-buffers))
1109 (insert-buffer-substring buf)
1110 (mml-preview-insert-mail-followup-to)
1111 (let ((message-deletable-headers (if (message-news-p)
1113 message-deletable-headers)))
1114 (message-generate-headers
1115 (copy-sequence (if (message-news-p)
1116 message-required-news-headers
1117 message-required-mail-headers))))
1118 (if (re-search-forward
1119 (concat "^" (regexp-quote mail-header-separator) "\n") nil t)
1120 (replace-match "\n"))
1121 (let ((mail-header-separator ""));; mail-header-separator is removed.
1124 (when (fboundp 'set-buffer-multibyte)
1125 (let ((s (buffer-string)))
1126 ;; Insert the content into unibyte buffer.
1128 (mm-disable-multibyte)
1130 (let ((gnus-newsgroup-charset (car message-posting-charset))
1131 gnus-article-prepare-hook gnus-original-article-buffer)
1132 (run-hooks 'gnus-article-decode-hook)
1133 (let ((gnus-newsgroup-name "dummy")
1134 (gnus-newsrc-hashtb (or gnus-newsrc-hashtb
1135 (gnus-make-hashtable 5))))
1136 (gnus-article-prepare-display))))
1137 ;; Disable article-mode-map.
1139 (gnus-make-local-hook 'kill-buffer-hook)
1140 (add-hook 'kill-buffer-hook
1142 (mm-destroy-parts gnus-article-mime-handles)) nil t)
1143 (setq buffer-read-only t)
1144 (local-set-key "q" (lambda () (interactive) (kill-buffer nil)))
1145 (local-set-key "=" (lambda () (interactive) (delete-other-windows)))
1149 (widget-button-press (point))))
1150 (local-set-key gnus-mouse-2
1153 (widget-button-press (widget-event-point event) event)))
1154 (goto-char (point-min)))))
1156 (defun mml-validate ()
1157 "Validate the current MML document."
1161 (defun mml-tweak-part (cont)
1163 (let ((tweak (cdr (assq 'tweak cont)))
1168 (or (cdr (assoc tweak mml-tweak-function-alist))
1170 (mml-tweak-type-alist
1171 (let ((alist mml-tweak-type-alist)
1172 (type (or (cdr (assq 'type cont)) "text/plain")))
1174 (if (string-match (caar alist) type)
1175 (setq func (cdar alist)
1177 (setq alist (cdr alist)))))))
1181 (let ((alist mml-tweak-sexp-alist))
1183 (if (eval (caar alist))
1184 (funcall (cdar alist) cont))
1185 (setq alist (cdr alist)))))
1188 (defun mml-tweak-externalize-attachments (cont)
1189 "Tweak attached files as external parts."
1190 (let (filename-cons)
1191 (when (and (eq (car cont) 'part)
1192 (not (cdr (assq 'buffer cont)))
1193 (and (setq filename-cons (assq 'filename cont))
1194 (not (equal (cdr (assq 'nofile cont)) "yes"))))
1195 (setcar cont 'external)
1196 (setcar filename-cons 'name))))
1200 ;;; arch-tag: 583c96cf-1ffe-451b-a5e5-4733ae9ddd12
1201 ;;; mml.el ends here