1 ;;; mml.el --- A package for parsing and validating MML documents
2 ;; Copyright (C) 1998 Free Software Foundation, Inc.
4 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
5 ;; This file is part of GNU Emacs.
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)
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.
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.
31 (autoload 'message-make-message-id "message"))
33 (defvar mml-syntax-table
34 (let ((table (copy-syntax-table emacs-lisp-mode-syntax-table)))
35 (modify-syntax-entry ?\\ "/" table)
36 (modify-syntax-entry ?< "(" table)
37 (modify-syntax-entry ?> ")" table)
38 (modify-syntax-entry ?@ "w" table)
39 (modify-syntax-entry ?/ "w" table)
40 (modify-syntax-entry ?= " " table)
41 (modify-syntax-entry ?* " " table)
42 (modify-syntax-entry ?\; " " table)
43 (modify-syntax-entry ?\' " " table)
47 "Parse the current buffer as an MML document."
48 (goto-char (point-min))
49 (let ((table (syntax-table)))
52 (set-syntax-table mml-syntax-table)
54 (set-syntax-table table))))
57 "Parse the current buffer as an MML document."
58 (let (struct tag point contents charsets warn)
59 (while (and (not (eobp))
60 (not (looking-at "<#/multipart")))
62 ((looking-at "<#multipart")
63 (push (nconc (mml-read-tag) (mml-parse-1)) struct))
64 ((looking-at "<#external")
65 (push (nconc (mml-read-tag) (list (cons 'contents (mml-read-part))))
68 (if (looking-at "<#part")
69 (setq tag (mml-read-tag))
70 (setq tag (list 'part '(type . "text/plain"))
73 contents (mml-read-part)
74 charsets (delq 'ascii (mm-find-charset-region point (point))))
75 (if (< (length charsets) 2)
76 (push (nconc tag (list (cons 'contents contents)))
78 (let ((nstruct (mml-parse-singlepart-with-multiple-charsets
84 "Warning: Your message contains %d parts. Really send? "
86 (error "Edit your message to use only one charset"))
87 (setq struct (nconc nstruct struct)))))))
92 (defun mml-parse-singlepart-with-multiple-charsets (orig-tag beg end)
94 (narrow-to-region beg end)
95 (goto-char (point-min))
96 (let ((current (char-charset (following-char)))
97 charset struct space newline paragraph)
100 ;; The charset remains the same.
101 ((or (eq (setq charset (char-charset (following-char))) 'ascii)
102 (eq charset current)))
103 ;; The initial charset was ascii.
105 (setq current charset))
106 ;; We have a change in charsets.
110 (list (cons 'contents
111 (buffer-substring-no-properties
112 beg (or paragraph newline space (point))))))
114 (setq beg (or paragraph newline space (point))
119 ;; Compute places where it might be nice to break the part.
121 ((memq (following-char) '(? ?\t))
122 (setq space (1+ (point))))
123 ((eq (following-char) ?\n)
124 (setq newline (1+ (point))))
125 ((and (eq (following-char) ?\n)
127 (eq (char-after (1- (point))) ?\n))
128 (setq paragraph (point))))
130 ;; Do the final part.
131 (unless (= beg (point))
132 (push (append orig-tag
133 (list (cons 'contents
134 (buffer-substring-no-properties
139 (defun mml-read-tag ()
140 "Read a tag and return the contents."
141 (let (contents name elem val)
143 (setq name (buffer-substring-no-properties
144 (point) (progn (forward-sexp 1) (point))))
145 (skip-chars-forward " \t\n")
146 (while (not (looking-at ">"))
147 (setq elem (buffer-substring-no-properties
148 (point) (progn (forward-sexp 1) (point))))
149 (skip-chars-forward "= \t\n")
150 (setq val (buffer-substring-no-properties
151 (point) (progn (forward-sexp 1) (point))))
152 (when (string-match "^\"\\(.*\\)\"$" val)
153 (setq val (match-string 1 val)))
154 (push (cons (intern elem) val) contents)
155 (skip-chars-forward " \t\n"))
157 (cons (intern name) (nreverse contents))))
159 (defun mml-read-part ()
160 "Return the buffer up till the next part, multipart or closing part or multipart."
162 ;; If the tag ended at the end of the line, we go to the next line.
163 (when (looking-at "[ \t]*\n")
165 (if (re-search-forward
166 "<#\\(/\\)?\\(multipart\\|part\\|external\\)." nil t)
168 (buffer-substring-no-properties beg (match-beginning 0))
169 (if (or (not (match-beginning 1))
170 (equal (match-string 2) "multipart"))
171 (goto-char (match-beginning 0))
172 (when (looking-at "[ \t]*\n")
174 (buffer-substring-no-properties beg (goto-char (point-max))))))
176 (defvar mml-boundary nil)
177 (defvar mml-base-boundary "=-=-=")
178 (defvar mml-multipart-number 0)
180 (defun mml-generate-mime ()
181 "Generate a MIME message based on the current MML document."
182 (let ((cont (mml-parse))
183 (mml-multipart-number 0))
187 (if (and (consp (car cont))
189 (mml-generate-mime-1 (car cont))
190 (mml-generate-mime-1 (nconc (list 'multipart '(type . "mixed"))
194 (defun mml-generate-mime-1 (cont)
196 ((eq (car cont) 'part)
197 (let (coded encoding charset filename type)
198 (setq type (or (cdr (assq 'type cont)) "text/plain"))
199 (if (equal (car (split-string type "/")) "text")
201 (if (setq filename (cdr (assq 'filename cont)))
202 (insert-file-contents-literally filename)
204 (narrow-to-region (point) (point))
205 (insert (cdr (assq 'contents cont)))
206 ;; Remove quotes from quoted tags.
207 (goto-char (point-min))
208 (while (re-search-forward
209 "<#!+/?\\(part\\|multipart\\|external\\)" nil t)
210 (delete-region (+ (match-beginning 0) 2)
211 (+ (match-beginning 0) 3)))))
212 (setq charset (mm-encode-body)
213 encoding (mm-body-encoding))
214 (setq coded (buffer-string)))
215 (mm-with-unibyte-buffer
216 (if (setq filename (cdr (assq 'filename cont)))
217 (insert-file-contents filename)
218 (insert (cdr (assq 'contents cont))))
219 (setq encoding (mm-encode-buffer type)
220 coded (buffer-string))))
221 (mml-insert-mime-headers cont type charset encoding)
224 ((eq (car cont) 'external)
225 (insert "Content-Type: message/external-body")
226 (let ((parameters (mml-parameter-string
227 cont '(expiration size permission)))
228 (name (cdr (assq 'name cont))))
230 (setq name (mml-parse-file-name name))
232 (insert ";\n " (mail-header-encode-parameter "name" name)
233 "\";\n access-type=local-file")
236 (mail-header-encode-parameter
237 "name" (file-name-nondirectory (nth 2 name)))
238 (mail-header-encode-parameter "site" (nth 1 name))
239 (mail-header-encode-parameter
240 "directory" (file-name-directory (nth 2 name)))))
241 (insert ";\n access-type="
242 (if (member (nth 0 name) '("ftp@" "anonymous@"))
246 (insert parameters)))
248 (insert "Content-Type: " (cdr (assq 'type cont)) "\n")
249 (insert "Content-ID: " (message-make-message-id) "\n")
250 (insert "Content-Transfer-Encoding: "
251 (or (cdr (assq 'encoding cont)) "binary"))
253 (insert (or (cdr (assq 'contents cont))))
255 ((eq (car cont) 'multipart)
256 (let ((mml-boundary (mml-compute-boundary cont)))
257 (insert (format "Content-Type: multipart/%s; boundary=\"%s\"\n"
258 (or (cdr (assq 'type cont)) "mixed")
261 (setq cont (cddr cont))
263 (insert "\n--" mml-boundary "\n")
264 (mml-generate-mime-1 (pop cont)))
265 (insert "\n--" mml-boundary "--\n")))
267 (error "Invalid element: %S" cont))))
269 (defun mml-compute-boundary (cont)
270 "Return a unique boundary that does not exist in CONT."
271 (let ((mml-boundary (mml-make-boundary)))
272 ;; This function tries again and again until it has found
273 ;; a unique boundary.
274 (while (not (catch 'not-unique
275 (mml-compute-boundary-1 cont))))
278 (defun mml-compute-boundary-1 (cont)
281 ((eq (car cont) 'part)
283 (if (setq filename (cdr (assq 'filename cont)))
284 (insert-file-contents-literally filename)
285 (insert (cdr (assq 'contents cont))))
286 (goto-char (point-min))
287 (when (re-search-forward (concat "^--" (regexp-quote mml-boundary))
289 (setq mml-boundary (mml-make-boundary))
290 (throw 'not-unique nil))))
291 ((eq (car cont) 'multipart)
292 (mapcar 'mml-compute-boundary-1 (cddr cont))))
295 (defun mml-make-boundary ()
296 (concat (make-string (% (incf mml-multipart-number) 60) ?=)
297 (if (> mml-multipart-number 17)
298 (format "%x" mml-multipart-number)
302 (defun mml-make-string (num string)
304 (while (not (zerop (decf num)))
305 (setq out (concat out string)))
308 (defun mml-insert-mime-headers (cont type charset encoding)
309 (let (parameters disposition description)
312 (mml-parameter-string
313 cont '(name access-type expiration size permission)))
314 (not (equal type "text/plain")))
315 (when (consp charset)
318 "Can't encode a part with several charsets. Insert a <#part>."))
319 (insert "Content-Type: " type)
321 (insert "; " (mail-header-encode-parameter
322 "charset" (symbol-name charset))))
326 (when (or (setq disposition (cdr (assq 'disposition cont)))
328 (mml-parameter-string
329 cont '(filename creation-date modification-date
331 (insert "Content-Disposition: " (or disposition "inline"))
335 (unless (eq encoding '7bit)
336 (insert (format "Content-Transfer-Encoding: %s\n" encoding)))
337 (when (setq description (cdr (assq 'description cont)))
338 (insert "Content-Description: " description "\n"))))
340 (defun mml-parameter-string (cont types)
343 (while (setq type (pop types))
344 (when (setq value (cdr (assq type cont)))
345 (setq string (concat string ";\n "
346 (mail-header-encode-parameter
347 (symbol-name type) value)))))
348 (when (not (zerop (length string)))
351 (defvar ange-ftp-path-format)
352 (defvar efs-path-regexp)
353 (defun mml-parse-file-name (path)
354 (if (if (boundp 'efs-path-regexp)
355 (string-match efs-path-regexp path)
356 (if (boundp 'ange-ftp-path-format)
357 (string-match (car ange-ftp-path-format))))
358 (list (match-string 1 path) (match-string 2 path)
359 (substring path (1+ (match-end 2))))