Synch with Oort Gnus.
[elisp/gnus.git-] / lisp / mml.el
1 ;;; mml.el --- A package for parsing and validating MML documents
2 ;; Copyright (C) 1998, 1999, 2000, 2001 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
5 ;; This file is part of GNU Emacs.
6
7 ;; GNU Emacs is free software; you can redistribute it and/or modify
8 ;; it under the terms of the GNU General Public License as published by
9 ;; the Free Software Foundation; either version 2, or (at your option)
10 ;; any later version.
11
12 ;; GNU Emacs is distributed in the hope that it will be useful,
13 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 ;; GNU General Public License for more details.
16
17 ;; You should have received a copy of the GNU General Public License
18 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
19 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
20 ;; Boston, MA 02111-1307, USA.
21
22 ;;; Commentary:
23
24 ;;; Code:
25
26 (require 'mm-util)
27 (require 'mm-bodies)
28 (require 'mm-encode)
29 (require 'mm-decode)
30 (require 'mml-sec)
31 (eval-when-compile (require 'cl))
32
33 (eval-and-compile
34   (autoload 'message-make-message-id "message")
35   (autoload 'gnus-setup-posting-charset "gnus-msg")
36   (autoload 'gnus-add-minor-mode "gnus-ems")
37   (autoload 'message-fetch-field "message")
38   (autoload 'message-posting-charset "message"))
39
40 (defcustom mml-content-type-parameters
41   '(name access-type expiration size permission format)
42   "*A list of acceptable parameters in MML tag.
43 These parameters are generated in Content-Type header if exists."
44   :type '(repeat (symbol :tag "Parameter"))
45   :group 'message)
46
47 (defcustom mml-content-disposition-parameters
48   '(filename creation-date modification-date read-date)
49   "*A list of acceptable parameters in MML tag.
50 These parameters are generated in Content-Disposition header if exists."
51   :type '(repeat (symbol :tag "Parameter"))
52   :group 'message)
53
54 (defvar mml-generate-multipart-alist nil
55   "*Alist of multipart generation functions.
56 Each entry has the form (NAME . FUNCTION), where
57 NAME is a string containing the name of the part (without the
58 leading \"/multipart/\"),
59 FUNCTION is a Lisp function which is called to generate the part.
60
61 The Lisp function has to supply the appropriate MIME headers and the
62 contents of this part.")
63
64 (defvar mml-syntax-table
65   (let ((table (copy-syntax-table emacs-lisp-mode-syntax-table)))
66     (modify-syntax-entry ?\\ "/" table)
67     (modify-syntax-entry ?< "(" table)
68     (modify-syntax-entry ?> ")" table)
69     (modify-syntax-entry ?@ "w" table)
70     (modify-syntax-entry ?/ "w" table)
71     (modify-syntax-entry ?= " " table)
72     (modify-syntax-entry ?* " " table)
73     (modify-syntax-entry ?\; " " table)
74     (modify-syntax-entry ?\' " " table)
75     table))
76
77 (defvar mml-boundary-function 'mml-make-boundary
78   "A function called to suggest a boundary.
79 The function may be called several times, and should try to make a new
80 suggestion each time.  The function is called with one parameter,
81 which is a number that says how many times the function has been
82 called for this message.")
83
84 (defvar mml-confirmation-set nil
85   "A list of symbols, each of which disables some warning.
86 `unknown-encoding': always send messages contain characters with
87 unknown encoding; `use-ascii': always use ASCII for those characters
88 with unknown encoding; `multipart': always send messages with more than
89 one charsets.")
90
91 (defvar mml-generate-default-type "text/plain")
92
93 (defvar mml-buffer-list nil)
94
95 (defun mml-generate-new-buffer (name)
96   (let ((buf (generate-new-buffer name)))
97     (push buf mml-buffer-list)
98     buf))
99
100 (defun mml-destroy-buffers ()
101   (let (kill-buffer-hook)
102     (mapcar 'kill-buffer mml-buffer-list)
103     (setq mml-buffer-list nil)))
104
105 (defun mml-parse ()
106   "Parse the current buffer as an MML document."
107   (save-excursion
108     (goto-char (point-min))
109     (let ((table (syntax-table)))
110       (unwind-protect
111           (progn
112             (set-syntax-table mml-syntax-table)
113             (mml-parse-1))
114         (set-syntax-table table)))))
115
116 (defun mml-parse-1 ()
117   "Parse the current buffer as an MML document."
118   (let (struct tag point contents charsets warn use-ascii no-markup-p raw)
119     (while (and (not (eobp))
120                 (not (looking-at "<#/multipart")))
121       (cond
122        ((looking-at "<#multipart")
123         (push (nconc (mml-read-tag) (mml-parse-1)) struct))
124        ((looking-at "<#external")
125         (push (nconc (mml-read-tag) (list (cons 'contents (mml-read-part))))
126               struct))
127        (t
128         (if (or (looking-at "<#part") (looking-at "<#mml"))
129             (setq tag (mml-read-tag)
130                   no-markup-p nil
131                   warn nil)
132           (setq tag (list 'part '(type . "text/plain"))
133                 no-markup-p t
134                 warn t))
135         (setq raw (cdr (assq 'raw tag))
136               point (point)
137               contents (mml-read-part (eq 'mml (car tag)))
138               charsets (cond
139                         (raw nil)
140                         ((assq 'charset tag)
141                          (list
142                           (intern (downcase (cdr (assq 'charset tag))))))
143                         (t
144                          (mm-find-mime-charset-region point (point)))))
145         (when (and (not raw) (memq nil charsets))
146           (if (or (memq 'unknown-encoding mml-confirmation-set)
147                   (message-options-get 'unknown-encoding)
148                   (and (y-or-n-p "\
149 Message contains characters with unknown encoding.  Really send?")
150                        (message-options-set 'unknown-encoding t)))
151               (if (setq use-ascii
152                         (or (memq 'use-ascii mml-confirmation-set)
153                             (message-options-get 'use-ascii)
154                             (and (y-or-n-p "Use ASCII as charset?")
155                                  (message-options-set 'use-ascii t))))
156                   (setq charsets (delq nil charsets))
157                 (setq warn nil))
158             (error "Edit your message to remove those characters")))
159         (if (or raw
160                 (eq 'mml (car tag))
161                 (< (length charsets) 2))
162             (if (or (not no-markup-p)
163                     (string-match "[^ \t\r\n]" contents))
164                 ;; Don't create blank parts.
165                 (push (nconc tag (list (cons 'contents contents)))
166                       struct))
167           (let ((nstruct (mml-parse-singlepart-with-multiple-charsets
168                           tag point (point) use-ascii)))
169             (when (and warn
170                        (not (memq 'multipart mml-confirmation-set))
171                        (not (message-options-get 'multipart))
172                        (not (and (y-or-n-p (format "\
173 A message part needs to be split into %d charset parts.  Really send? "
174                                                    (length nstruct)))
175                                  (message-options-set 'multipart t))))
176               (error "Edit your message to use only one charset"))
177             (setq struct (nconc nstruct struct)))))))
178     (unless (eobp)
179       (forward-line 1))
180     (nreverse struct)))
181
182 (defun mml-parse-singlepart-with-multiple-charsets
183   (orig-tag beg end &optional use-ascii)
184   (save-excursion
185     (save-restriction
186       (narrow-to-region beg end)
187       (goto-char (point-min))
188       (let ((current (or (mm-mime-charset (mm-charset-after))
189                          (and use-ascii 'us-ascii)))
190             charset struct space newline paragraph)
191         (while (not (eobp))
192           (setq charset (mm-mime-charset (mm-charset-after)))
193           (cond
194            ;; The charset remains the same.
195            ((eq charset 'us-ascii))
196            ((or (and use-ascii (not charset))
197                 (eq charset current))
198             (setq space nil
199                   newline nil
200                   paragraph nil))
201            ;; The initial charset was ascii.
202            ((eq current 'us-ascii)
203             (setq current charset
204                   space nil
205                   newline nil
206                   paragraph nil))
207            ;; We have a change in charsets.
208            (t
209             (push (append
210                    orig-tag
211                    (list (cons 'contents
212                                (buffer-substring-no-properties
213                                 beg (or paragraph newline space (point))))))
214                   struct)
215             (setq beg (or paragraph newline space (point))
216                   current charset
217                   space nil
218                   newline nil
219                   paragraph nil)))
220           ;; Compute places where it might be nice to break the part.
221           (cond
222            ((memq (following-char) '(?  ?\t))
223             (setq space (1+ (point))))
224            ((and (eq (following-char) ?\n)
225                  (not (bobp))
226                  (eq (char-after (1- (point))) ?\n))
227             (setq paragraph (point)))
228            ((eq (following-char) ?\n)
229             (setq newline (1+ (point)))))
230           (forward-char 1))
231         ;; Do the final part.
232         (unless (= beg (point))
233           (push (append orig-tag
234                         (list (cons 'contents
235                                     (buffer-substring-no-properties
236                                      beg (point)))))
237                 struct))
238         struct))))
239
240 (defun mml-read-tag ()
241   "Read a tag and return the contents."
242   (let ((orig-point (point))
243         contents name elem val)
244     (forward-char 2)
245     (setq name (buffer-substring-no-properties
246                 (point) (progn (forward-sexp 1) (point))))
247     (skip-chars-forward " \t\n")
248     (while (not (looking-at ">[ \t]*\n?"))
249       (setq elem (buffer-substring-no-properties
250                   (point) (progn (forward-sexp 1) (point))))
251       (skip-chars-forward "= \t\n")
252       (setq val (buffer-substring-no-properties
253                  (point) (progn (forward-sexp 1) (point))))
254       (when (string-match "^\"\\(.*\\)\"$" val)
255         (setq val (match-string 1 val)))
256       (push (cons (intern elem) val) contents)
257       (skip-chars-forward " \t\n"))
258     (goto-char (match-end 0))
259     ;; Don't skip the leading space.
260     ;;(skip-chars-forward " \t\n")
261     ;; Put the tag location into the returned contents
262     (setq contents (append (list (cons 'tag-location orig-point)) contents))
263     (cons (intern name) (nreverse contents))))
264
265 (defun mml-read-part (&optional mml)
266   "Return the buffer up till the next part, multipart or closing part or multipart.
267 If MML is non-nil, return the buffer up till the correspondent mml tag."
268   (let ((beg (point)) (count 1))
269     ;; If the tag ended at the end of the line, we go to the next line.
270     (when (looking-at "[ \t]*\n")
271       (forward-line 1))
272     (if mml
273         (progn
274           (while (and (> count 0) (not (eobp)))
275             (if (re-search-forward "<#\\(/\\)?mml." nil t)
276                 (setq count (+ count (if (match-beginning 1) -1 1)))
277               (goto-char (point-max))))
278           (buffer-substring-no-properties beg (if (> count 0)
279                                                   (point)
280                                                 (match-beginning 0))))
281       (if (re-search-forward
282            "<#\\(/\\)?\\(multipart\\|part\\|external\\|mml\\)." nil t)
283           (prog1
284               (buffer-substring-no-properties beg (match-beginning 0))
285             (if (or (not (match-beginning 1))
286                     (equal (match-string 2) "multipart"))
287                 (goto-char (match-beginning 0))
288               (when (looking-at "[ \t]*\n")
289                 (forward-line 1))))
290         (buffer-substring-no-properties beg (goto-char (point-max)))))))
291
292 (defvar mml-boundary nil)
293 (defvar mml-base-boundary "-=-=")
294 (defvar mml-multipart-number 0)
295
296 (defun mml-generate-mime ()
297   "Generate a MIME message based on the current MML document."
298   (let ((cont (mml-parse))
299         (mml-multipart-number mml-multipart-number))
300     (if (not cont)
301         nil
302       (with-temp-buffer
303         (if (and (consp (car cont))
304                  (= (length cont) 1))
305             (mml-generate-mime-1 (car cont))
306           (mml-generate-mime-1 (nconc (list 'multipart '(type . "mixed"))
307                                       cont)))
308         (buffer-string)))))
309
310 (defun mml-generate-mime-1 (cont)
311   (let ((mm-use-ultra-safe-encoding
312          (or mm-use-ultra-safe-encoding (assq 'sign cont))))
313     (save-restriction
314       (narrow-to-region (point) (point))
315       (cond
316        ((or (eq (car cont) 'part) (eq (car cont) 'mml))
317         (let ((raw (cdr (assq 'raw cont)))
318               coded encoding charset filename type)
319           (setq type (or (cdr (assq 'type cont)) "text/plain"))
320           (if (and (not raw)
321                    (member (car (split-string type "/")) '("text" "message")))
322               (with-temp-buffer
323                 (setq charset (mm-charset-to-coding-system
324                                (cdr (assq 'charset cont))))
325                 (when (eq charset 'ascii)
326                   (setq charset nil))
327                 (cond
328                  ((cdr (assq 'buffer cont))
329                   (insert-buffer-substring (cdr (assq 'buffer cont))))
330                  ((and (setq filename (cdr (assq 'filename cont)))
331                        (not (equal (cdr (assq 'nofile cont)) "yes")))
332                   (let ((coding-system-for-read charset))
333                     (mm-insert-file-contents filename)))
334                  ((eq 'mml (car cont))
335                   (insert (cdr (assq 'contents cont))))
336                  (t
337                   (save-restriction
338                     (narrow-to-region (point) (point))
339                     (insert (cdr (assq 'contents cont)))
340                     ;; Remove quotes from quoted tags.
341                     (goto-char (point-min))
342                     (while (re-search-forward
343                             "<#!+/?\\(part\\|multipart\\|external\\|mml\\)"
344                             nil t)
345                       (delete-region (+ (match-beginning 0) 2)
346                                      (+ (match-beginning 0) 3))))))
347                 (cond
348                  ((eq (car cont) 'mml)
349                   (let ((mml-boundary (funcall mml-boundary-function
350                                                (incf mml-multipart-number)))
351                         (mml-generate-default-type "text/plain"))
352                     (mml-to-mime))
353                   (let ((mm-7bit-chars (concat mm-7bit-chars "\x1b")))
354                     ;; ignore 0x1b, it is part of iso-2022-jp
355                     (setq encoding (mm-body-7-or-8))))
356                  ((string= (car (split-string type "/")) "message")
357                   (let ((mm-7bit-chars (concat mm-7bit-chars "\x1b")))
358                     ;; ignore 0x1b, it is part of iso-2022-jp
359                     (setq encoding (mm-body-7-or-8))))
360                  (t
361                   (setq charset (mm-encode-body charset))
362                   (setq encoding (mm-body-encoding
363                                   charset (cdr (assq 'encoding cont))))))
364                 (setq coded (buffer-string)))
365             (mm-with-unibyte-buffer
366               (cond
367                ((cdr (assq 'buffer cont))
368                 (insert-buffer-substring (cdr (assq 'buffer cont))))
369                ((and (setq filename (cdr (assq 'filename cont)))
370                      (not (equal (cdr (assq 'nofile cont)) "yes")))
371                 (let ((coding-system-for-read mm-binary-coding-system))
372                   (mm-insert-file-contents filename nil nil nil nil t)))
373                (t
374                 (insert (cdr (assq 'contents cont)))))
375               (setq encoding (mm-encode-buffer type)
376                     coded (buffer-string))))
377           (mml-insert-mime-headers cont type charset encoding)
378           (insert "\n")
379           (insert coded)))
380        ((eq (car cont) 'external)
381         (insert "Content-Type: message/external-body")
382         (let ((parameters (mml-parameter-string
383                            cont '(expiration size permission)))
384               (name (cdr (assq 'name cont)))
385               (url (cdr (assq 'url cont))))
386           (when name
387             (setq name (mml-parse-file-name name))
388             (if (stringp name)
389                 (mml-insert-parameter
390                  (mail-header-encode-parameter "name" name)
391                  "access-type=local-file")
392               (mml-insert-parameter
393                (mail-header-encode-parameter
394                 "name" (file-name-nondirectory (nth 2 name)))
395                (mail-header-encode-parameter "site" (nth 1 name))
396                (mail-header-encode-parameter
397                 "directory" (file-name-directory (nth 2 name))))
398               (mml-insert-parameter
399                (concat "access-type="
400                        (if (member (nth 0 name) '("ftp@" "anonymous@"))
401                            "anon-ftp"
402                          "ftp")))))
403           (when url
404             (mml-insert-parameter
405              (mail-header-encode-parameter "url" url)
406              "access-type=url"))
407           (when parameters
408             (mml-insert-parameter-string
409              cont '(expiration size permission))))
410         (insert "\n\n")
411         (insert "Content-Type: " (cdr (assq 'type cont)) "\n")
412         (insert "Content-ID: " (message-make-message-id) "\n")
413         (insert "Content-Transfer-Encoding: "
414                 (or (cdr (assq 'encoding cont)) "binary"))
415         (insert "\n\n")
416         (insert (or (cdr (assq 'contents cont))))
417         (insert "\n"))
418        ((eq (car cont) 'multipart)
419         (let* ((type (or (cdr (assq 'type cont)) "mixed"))
420                (mml-generate-default-type (if (equal type "digest")
421                                               "message/rfc822"
422                                             "text/plain"))
423                (handler (assoc type mml-generate-multipart-alist)))
424           (if handler
425               (funcall (cdr handler) cont)
426             ;; No specific handler.  Use default one.
427             (let ((mml-boundary (mml-compute-boundary cont)))
428               (insert (format "Content-Type: multipart/%s; boundary=\"%s\"\n"
429                               type mml-boundary))
430               (let ((cont cont) part)
431                 (while (setq part (pop cont))
432                   ;; Skip `multipart' and attributes.
433                   (when (and (consp part) (consp (cdr part)))
434                     (insert "\n--" mml-boundary "\n")
435                     (mml-generate-mime-1 part))))
436               (insert "\n--" mml-boundary "--\n")))))
437        (t
438         (error "Invalid element: %S" cont)))
439       (let ((item (assoc (cdr (assq 'sign cont)) mml-sign-alist))
440             sender recipients)
441         (when item
442           (if (setq sender (cdr (assq 'sender cont)))
443               (message-options-set 'message-sender sender))
444           (if (setq recipients (cdr (assq 'recipients cont)))
445               (message-options-set 'message-sender recipients))
446           (funcall (nth 1 item) cont)))
447       (let ((item (assoc (cdr (assq 'encrypt cont)) mml-encrypt-alist))
448             sender recipients)
449         (when item
450           (if (setq sender (cdr (assq 'sender cont)))
451               (message-options-set 'message-sender sender))
452           (if (setq recipients (cdr (assq 'recipients cont)))
453               (message-options-set 'message-sender recipients))
454           (funcall (nth 1 item) cont))))))
455
456 (defun mml-compute-boundary (cont)
457   "Return a unique boundary that does not exist in CONT."
458   (let ((mml-boundary (funcall mml-boundary-function
459                                (incf mml-multipart-number))))
460     ;; This function tries again and again until it has found
461     ;; a unique boundary.
462     (while (not (catch 'not-unique
463                   (mml-compute-boundary-1 cont))))
464     mml-boundary))
465
466 (defun mml-compute-boundary-1 (cont)
467   (let (filename)
468     (cond
469      ((eq (car cont) 'part)
470       (with-temp-buffer
471         (cond
472          ((cdr (assq 'buffer cont))
473           (insert-buffer-substring (cdr (assq 'buffer cont))))
474          ((and (setq filename (cdr (assq 'filename cont)))
475                (not (equal (cdr (assq 'nofile cont)) "yes")))
476           (mm-insert-file-contents filename))
477          (t
478           (insert (cdr (assq 'contents cont)))))
479         (goto-char (point-min))
480         (when (re-search-forward (concat "^--" (regexp-quote mml-boundary))
481                                  nil t)
482           (setq mml-boundary (funcall mml-boundary-function
483                                       (incf mml-multipart-number)))
484           (throw 'not-unique nil))))
485      ((eq (car cont) 'multipart)
486       (mapcar 'mml-compute-boundary-1 (cddr cont))))
487     t))
488
489 (defun mml-make-boundary (number)
490   (concat (make-string (% number 60) ?=)
491           (if (> number 17)
492               (format "%x" number)
493             "")
494           mml-base-boundary))
495
496 (defun mml-insert-mime-headers (cont type charset encoding)
497   (let (parameters disposition description)
498     (setq parameters
499           (mml-parameter-string
500            cont mml-content-type-parameters))
501     (when (or charset
502               parameters
503               (not (equal type mml-generate-default-type)))
504       (when (consp charset)
505         (error
506          "Can't encode a part with several charsets."))
507       (insert "Content-Type: " type)
508       (when charset
509         (insert "; " (mail-header-encode-parameter
510                       "charset" (symbol-name charset))))
511       (when parameters
512         (mml-insert-parameter-string
513          cont mml-content-type-parameters))
514       (insert "\n"))
515     (setq parameters
516           (mml-parameter-string
517            cont mml-content-disposition-parameters))
518     (when (or (setq disposition (cdr (assq 'disposition cont)))
519               parameters)
520       (insert "Content-Disposition: " (or disposition "inline"))
521       (when parameters
522         (mml-insert-parameter-string
523          cont mml-content-disposition-parameters))
524       (insert "\n"))
525     (unless (eq encoding '7bit)
526       (insert (format "Content-Transfer-Encoding: %s\n" encoding)))
527     (when (setq description (cdr (assq 'description cont)))
528       (insert "Content-Description: "
529               (mail-encode-encoded-word-string description) "\n"))))
530
531 (defun mml-parameter-string (cont types)
532   (let ((string "")
533         value type)
534     (while (setq type (pop types))
535       (when (setq value (cdr (assq type cont)))
536         ;; Strip directory component from the filename parameter.
537         (when (eq type 'filename)
538           (setq value (file-name-nondirectory value)))
539         (setq string (concat string "; "
540                              (mail-header-encode-parameter
541                               (symbol-name type) value)))))
542     (when (not (zerop (length string)))
543       string)))
544
545 (defun mml-insert-parameter-string (cont types)
546   (let (value type)
547     (while (setq type (pop types))
548       (when (setq value (cdr (assq type cont)))
549         ;; Strip directory component from the filename parameter.
550         (when (eq type 'filename)
551           (setq value (file-name-nondirectory value)))
552         (mml-insert-parameter
553          (mail-header-encode-parameter
554           (symbol-name type) value))))))
555
556 (eval-when-compile
557   (defvar ange-ftp-name-format)
558   (defvar efs-path-regexp))
559 (defun mml-parse-file-name (path)
560   (if (if (boundp 'efs-path-regexp)
561           (string-match efs-path-regexp path)
562         (if (boundp 'ange-ftp-name-format)
563             (string-match (car ange-ftp-name-format) path)))
564       (list (match-string 1 path) (match-string 2 path)
565             (substring path (1+ (match-end 2))))
566     path))
567
568 (defun mml-insert-buffer (buffer)
569   "Insert BUFFER at point and quote any MML markup."
570   (save-restriction
571     (narrow-to-region (point) (point))
572     (insert-buffer-substring buffer)
573     (mml-quote-region (point-min) (point-max))
574     (goto-char (point-max))))
575
576 ;;;
577 ;;; Transforming MIME to MML
578 ;;;
579
580 (defun mime-to-mml (&optional handles)
581   "Translate the current buffer (which should be a message) into MML.
582 If HANDLES is non-nil, use it instead reparsing the buffer."
583   ;; First decode the head.
584   (save-restriction
585     (message-narrow-to-head)
586     (mail-decode-encoded-word-region (point-min) (point-max)))
587   (unless handles
588     (setq handles (mm-dissect-buffer t)))
589   (goto-char (point-min))
590   (search-forward "\n\n" nil t)
591   (delete-region (point) (point-max))
592   (if (stringp (car handles))
593       (mml-insert-mime handles)
594     (mml-insert-mime handles t))
595   (mm-destroy-parts handles)
596   (save-restriction
597     (message-narrow-to-head)
598     ;; Remove them, they are confusing.
599     (message-remove-header "Content-Type")
600     (message-remove-header "MIME-Version")
601     (message-remove-header "Content-Transfer-Encoding")))
602
603 (defun mml-to-mime ()
604   "Translate the current buffer from MML to MIME."
605   (message-encode-message-body)
606   (save-restriction
607     (message-narrow-to-headers-or-head)
608     (let ((mail-parse-charset message-default-charset))
609       (mail-encode-encoded-word-buffer))))
610
611 (defun mml-insert-mime (handle &optional no-markup)
612   (let (textp buffer mmlp)
613     ;; Determine type and stuff.
614     (unless (stringp (car handle))
615       (unless (setq textp (equal (mm-handle-media-supertype handle) "text"))
616         (save-excursion
617           (set-buffer (setq buffer (mml-generate-new-buffer " *mml*")))
618           (mm-insert-part handle)
619           (if (setq mmlp (equal (mm-handle-media-type handle)
620                                 "message/rfc822"))
621               (mime-to-mml)))))
622     (if mmlp
623         (mml-insert-mml-markup handle nil t t)
624       (unless (and no-markup
625                    (equal (mm-handle-media-type handle) "text/plain"))
626         (mml-insert-mml-markup handle buffer textp)))
627     (cond
628      (mmlp
629       (insert-buffer buffer)
630       (goto-char (point-max))
631       (insert "<#/mml>\n"))
632      ((stringp (car handle))
633       (mapcar 'mml-insert-mime (cdr handle))
634       (insert "<#/multipart>\n"))
635      (textp
636       (let ((charset (mail-content-type-get
637                       (mm-handle-type handle) 'charset)))
638         (if (eq charset 'gnus-decoded)
639             (mm-insert-part handle)
640           (insert (mm-decode-string (mm-get-part handle) charset))))
641       (goto-char (point-max)))
642      (t
643       (insert "<#/part>\n")))))
644
645 (defun mml-insert-mml-markup (handle &optional buffer nofile mmlp)
646   "Take a MIME handle and insert an MML tag."
647   (if (stringp (car handle))
648       (insert "<#multipart type=" (mm-handle-media-subtype handle)
649               ">\n")
650     (if mmlp
651         (insert "<#mml type=" (mm-handle-media-type handle))
652       (insert "<#part type=" (mm-handle-media-type handle)))
653     (dolist (elem (append (cdr (mm-handle-type handle))
654                           (cdr (mm-handle-disposition handle))))
655       (unless (symbolp (cdr elem))
656         (insert " " (symbol-name (car elem)) "=\"" (cdr elem) "\"")))
657     (when (mm-handle-disposition handle)
658       (insert " disposition=" (car (mm-handle-disposition handle))))
659     (when buffer
660       (insert " buffer=\"" (buffer-name buffer) "\""))
661     (when nofile
662       (insert " nofile=yes"))
663     (when (mm-handle-description handle)
664       (insert " description=\"" (mm-handle-description handle) "\""))
665     (insert ">\n")))
666
667 (defun mml-insert-parameter (&rest parameters)
668   "Insert PARAMETERS in a nice way."
669   (dolist (param parameters)
670     (insert ";")
671     (let ((point (point)))
672       (insert " " param)
673       (when (> (current-column) 71)
674         (goto-char point)
675         (insert "\n ")
676         (end-of-line)))))
677
678 ;;;
679 ;;; Mode for inserting and editing MML forms
680 ;;;
681
682 (defvar mml-mode-map
683   (let ((sign (make-sparse-keymap))
684         (encrypt (make-sparse-keymap))
685         (map (make-sparse-keymap))
686         (main (make-sparse-keymap)))
687     (define-key sign "p" 'mml-secure-sign-pgpmime)
688     (define-key sign "s" 'mml-secure-sign-smime)
689     (define-key encrypt "p" 'mml-secure-encrypt-pgpmime)
690     (define-key encrypt "s" 'mml-secure-encrypt-smime)
691     (define-key map "f" 'mml-attach-file)
692     (define-key map "b" 'mml-attach-buffer)
693     (define-key map "e" 'mml-attach-external)
694     (define-key map "q" 'mml-quote-region)
695     (define-key map "m" 'mml-insert-multipart)
696     (define-key map "p" 'mml-insert-part)
697     (define-key map "v" 'mml-validate)
698     (define-key map "P" 'mml-preview)
699     (define-key map "s" sign)
700     (define-key map "c" encrypt)
701     ;;(define-key map "n" 'mml-narrow-to-part)
702     ;; `M-m' conflicts with `back-to-indentation'.
703     ;; (define-key main "\M-m" map)
704     (define-key main "\C-c\C-m" map)
705     main))
706
707 (easy-menu-define
708  mml-menu mml-mode-map ""
709  '("MML"
710    ("Attach"
711     ["File" mml-attach-file t]
712     ["Buffer" mml-attach-buffer t]
713     ["External" mml-attach-external t])
714    ("Insert"
715     ["Multipart" mml-insert-multipart t]
716     ["Part" mml-insert-part t])
717    ("Security"
718     ("Sign"
719      ["PGP/MIME" mml-secure-sign-pgpmime t]
720      ["S/MIME" mml-secure-sign-smime t])
721     ("Encrypt"
722      ["PGP/MIME" mml-secure-encrypt-pgpmime t]
723      ["S/MIME" mml-secure-encrypt-smime t]))
724    ;;["Narrow" mml-narrow-to-part t]
725    ["Quote" mml-quote-region t]
726    ["Validate" mml-validate t]
727    ["Preview" mml-preview t]))
728
729 (defvar mml-mode nil
730   "Minor mode for editing MML.")
731
732 (defun mml-mode (&optional arg)
733   "Minor mode for editing MML.
734
735 \\{mml-mode-map}"
736   (interactive "P")
737   (when (set (make-local-variable 'mml-mode)
738              (if (null arg) (not mml-mode)
739                (> (prefix-numeric-value arg) 0)))
740     (gnus-add-minor-mode 'mml-mode " MML" mml-mode-map)
741     (easy-menu-add mml-menu mml-mode-map)
742     (run-hooks 'mml-mode-hook)))
743
744 ;;;
745 ;;; Helper functions for reading MIME stuff from the minibuffer and
746 ;;; inserting stuff to the buffer.
747 ;;;
748
749 (defun mml-minibuffer-read-file (prompt)
750   (let ((file (read-file-name prompt nil nil t)))
751     ;; Prevent some common errors.  This is inspired by similar code in
752     ;; VM.
753     (when (file-directory-p file)
754       (error "%s is a directory, cannot attach" file))
755     (unless (file-exists-p file)
756       (error "No such file: %s" file))
757     (unless (file-readable-p file)
758       (error "Permission denied: %s" file))
759     file))
760
761 (defun mml-minibuffer-read-type (name &optional default)
762   (mailcap-parse-mimetypes)
763   (let* ((default (or default
764                       (mm-default-file-encoding name)
765                       ;; Perhaps here we should check what the file
766                       ;; looks like, and offer text/plain if it looks
767                       ;; like text/plain.
768                       "application/octet-stream"))
769          (string (completing-read
770                   (format "Content type (default %s): " default)
771                   (mapcar 'list (mailcap-mime-types)))))
772     (if (not (equal string ""))
773         string
774       default)))
775
776 (defun mml-minibuffer-read-description ()
777   (let ((description (read-string "One line description: ")))
778     (when (string-match "\\`[ \t]*\\'" description)
779       (setq description nil))
780     description))
781
782 (defun mml-quote-region (beg end)
783   "Quote the MML tags in the region."
784   (interactive "r")
785   (save-excursion
786     (save-restriction
787       ;; Temporarily narrow the region to defend from changes
788       ;; invalidating END.
789       (narrow-to-region beg end)
790       (goto-char (point-min))
791       ;; Quote parts.
792       (while (re-search-forward
793               "<#!*/?\\(multipart\\|part\\|external\\|mml\\)" nil t)
794         ;; Insert ! after the #.
795         (goto-char (+ (match-beginning 0) 2))
796         (insert "!")))))
797
798 (defun mml-insert-tag (name &rest plist)
799   "Insert an MML tag described by NAME and PLIST."
800   (when (symbolp name)
801     (setq name (symbol-name name)))
802   (insert "<#" name)
803   (while plist
804     (let ((key (pop plist))
805           (value (pop plist)))
806       (when value
807         ;; Quote VALUE if it contains suspicious characters.
808         (when (string-match "[\"'\\~/*;() \t\n]" value)
809           (setq value (prin1-to-string value)))
810         (insert (format " %s=%s" key value)))))
811   (insert ">\n"))
812
813 (defun mml-insert-empty-tag (name &rest plist)
814   "Insert an empty MML tag described by NAME and PLIST."
815   (when (symbolp name)
816     (setq name (symbol-name name)))
817   (apply #'mml-insert-tag name plist)
818   (insert "<#/" name ">\n"))
819
820 ;;; Attachment functions.
821
822 (defun mml-attach-file (file &optional type description)
823   "Attach a file to the outgoing MIME message.
824 The file is not inserted or encoded until you send the message with
825 `\\[message-send-and-exit]' or `\\[message-send]'.
826
827 FILE is the name of the file to attach.  TYPE is its content-type, a
828 string of the form \"type/subtype\".  DESCRIPTION is a one-line
829 description of the attachment."
830   (interactive
831    (let* ((file (mml-minibuffer-read-file "Attach file: "))
832           (type (mml-minibuffer-read-type file))
833           (description (mml-minibuffer-read-description)))
834      (list file type description)))
835   (mml-insert-empty-tag 'part 'type type 'filename file
836                         'disposition "attachment" 'description description))
837
838 (defun mml-attach-buffer (buffer &optional type description)
839   "Attach a buffer to the outgoing MIME message.
840 See `mml-attach-file' for details of operation."
841   (interactive
842    (let* ((buffer (read-buffer "Attach buffer: "))
843           (type (mml-minibuffer-read-type buffer "text/plain"))
844           (description (mml-minibuffer-read-description)))
845      (list buffer type description)))
846   (mml-insert-empty-tag 'part 'type type 'buffer buffer
847                         'disposition "attachment" 'description description))
848
849 (defun mml-attach-external (file &optional type description)
850   "Attach an external file into the buffer.
851 FILE is an ange-ftp/efs specification of the part location.
852 TYPE is the MIME type to use."
853   (interactive
854    (let* ((file (mml-minibuffer-read-file "Attach external file: "))
855           (type (mml-minibuffer-read-type file))
856           (description (mml-minibuffer-read-description)))
857      (list file type description)))
858   (mml-insert-empty-tag 'external 'type type 'name file
859                         'disposition "attachment" 'description description))
860
861 (defun mml-insert-multipart (&optional type)
862   (interactive (list (completing-read "Multipart type (default mixed): "
863                                       '(("mixed") ("alternative") ("digest") ("parallel")
864                                         ("signed") ("encrypted"))
865                                       nil nil "mixed")))
866   (or type
867       (setq type "mixed"))
868   (mml-insert-empty-tag "multipart" 'type type)
869   (forward-line -1))
870
871 (defun mml-insert-part (&optional type)
872   (interactive
873    (list (mml-minibuffer-read-type "")))
874   (mml-insert-tag 'part 'type type 'disposition "inline")
875   (forward-line -1))
876
877 (defun mml-preview (&optional raw)
878   "Display current buffer with Gnus, in a new buffer.
879 If RAW, don't highlight the article."
880   (interactive "P")
881   (let ((buf (current-buffer))
882         (message-options message-options)
883         (message-posting-charset (or (gnus-setup-posting-charset
884                                       (save-restriction
885                                         (message-narrow-to-headers-or-head)
886                                         (message-fetch-field "Newsgroups")))
887                                      message-posting-charset)))
888     (message-options-set-recipient)
889     (switch-to-buffer (generate-new-buffer
890                        (concat (if raw "*Raw MIME preview of "
891                                  "*MIME preview of ") (buffer-name))))
892     (erase-buffer)
893     (insert-buffer buf)
894     (if (re-search-forward
895          (concat "^" (regexp-quote mail-header-separator) "\n") nil t)
896         (replace-match "\n"))
897     (mml-to-mime)
898     (if raw
899         (when (fboundp 'set-buffer-multibyte)
900           (let ((s (buffer-string)))
901             ;; Insert the content into unibyte buffer.
902             (erase-buffer)
903             (mm-disable-multibyte)
904             (insert s)))
905       (let ((gnus-newsgroup-charset (car message-posting-charset)))
906         (run-hooks 'gnus-article-decode-hook)
907         (let ((gnus-newsgroup-name "dummy"))
908           (gnus-article-prepare-display))))
909     ;; Disable article-mode-map.
910     (use-local-map nil)
911     (setq buffer-read-only t)
912     (local-set-key "q" (lambda () (interactive) (kill-buffer nil)))
913     (goto-char (point-min))))
914
915 (defun mml-validate ()
916   "Validate the current MML document."
917   (interactive)
918   (mml-parse))
919
920 (provide 'mml)
921
922 ;;; mml.el ends here