Browse code
send-letter/send-smtp/send-smtp-auth mods
dancy authored on 26/01/2006 23:53:27
Showing 4 changed files
Showing 4 changed files
1 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,503 @@ |
1 |
+;; $Id: mime-parse.cl,v 1.1 2006/01/26 23:53:27 dancy Exp $ |
|
2 |
+ |
|
3 |
+(defpackage :net.post-office |
|
4 |
+ (:use #:lisp #:excl) |
|
5 |
+ (:export |
|
6 |
+ #:parse-mime-structure |
|
7 |
+ #:mime-dequote |
|
8 |
+ |
|
9 |
+ ;; accessors |
|
10 |
+ #:mime-part-headers-size |
|
11 |
+ #:mime-part-body-size |
|
12 |
+ #:mime-part-lines |
|
13 |
+ #:mime-part-position |
|
14 |
+ #:mime-part-body-position |
|
15 |
+ #:mime-part-message |
|
16 |
+ |
|
17 |
+ ;; class name |
|
18 |
+ #:mime-part-parsed |
|
19 |
+ |
|
20 |
+ )) |
|
21 |
+ |
|
22 |
+(in-package :net.post-office) |
|
23 |
+ |
|
24 |
+(eval-when (compile) |
|
25 |
+ (declaim (optimize (speed 3)))) |
|
26 |
+ |
|
27 |
+;;; MIME structure parser. |
|
28 |
+;;; Ref: RFC2045/2046 |
|
29 |
+ |
|
30 |
+(defconstant *whitespace* '(#\space #\tab #\return #\newline)) |
|
31 |
+ |
|
32 |
+(defclass mime-part-parsed (mime-part) |
|
33 |
+ ( |
|
34 |
+ (headers-size ;; in bytes. Includes the bytes for the blank line |
|
35 |
+ :accessor mime-part-headers-size :initform nil) |
|
36 |
+ (body-size ;; in bytes. |
|
37 |
+ :accessor mime-part-body-size :initform nil) |
|
38 |
+ (lines ;; line count of body (for non-multipart types) |
|
39 |
+ :accessor mime-part-lines :initform nil) |
|
40 |
+ (position ;; file position of start of headers |
|
41 |
+ :accessor mime-part-position :initform nil) |
|
42 |
+ (body-position ;; file position of start of body |
|
43 |
+ :accessor mime-part-body-position :initform nil) |
|
44 |
+ (message ;; for message/rfc822 encapsulated message. |
|
45 |
+ ;; This will be a mime-part |
|
46 |
+ :accessor mime-part-message :initform nil))) |
|
47 |
+ |
|
48 |
+(excl::defresource mime-line |
|
49 |
+ :constructor (lambda () (make-array 16000 :element-type 'character |
|
50 |
+ :fill-pointer 0)) |
|
51 |
+ :reinitializer (lambda (x) (setf (fill-pointer x) 0))) |
|
52 |
+ |
|
53 |
+;; Return values: |
|
54 |
+;; First is the part. |
|
55 |
+;; Second is :eof if end of file was reached or |
|
56 |
+;; :boundary if a boundary was reached |
|
57 |
+;; nil, otherwise |
|
58 |
+;; Third is new position |
|
59 |
+(defun parse-mime-structure (stream &key boundary digest (pos 0) mbox) |
|
60 |
+ (parse-mime-structure-1 stream boundary digest pos mbox)) |
|
61 |
+ |
|
62 |
+(defun parse-mime-structure-1 (stream boundary digest pos mbox) |
|
63 |
+ (let ((part (make-instance 'mime-part-parsed))) |
|
64 |
+ (setf (mime-part-position part) pos) |
|
65 |
+ (setf (mime-part-boundary part) boundary) |
|
66 |
+ (multiple-value-bind (headers bytes) |
|
67 |
+ (parse-headers stream mbox) |
|
68 |
+ (setf (mime-part-headers-size part) bytes) |
|
69 |
+ (incf pos bytes) |
|
70 |
+ (setf (mime-part-body-position part) pos) |
|
71 |
+ (setf (mime-part-headers part) headers) |
|
72 |
+ |
|
73 |
+ (let ((content-type (mime-get-header "content-type" part))) |
|
74 |
+ (setf (mime-part-id part) (mime-get-header "Content-Id" part)) |
|
75 |
+ (setf (mime-part-description part) |
|
76 |
+ (mime-get-header "Content-description" part)) |
|
77 |
+ (setf (mime-part-encoding part) |
|
78 |
+ (or (mime-get-header "Content-transfer-encoding" part) |
|
79 |
+ "7bit")) |
|
80 |
+ |
|
81 |
+ (multiple-value-bind (type subtype params) |
|
82 |
+ (parse-content-type content-type) |
|
83 |
+ |
|
84 |
+ (if* (null type) |
|
85 |
+ then |
|
86 |
+ (if* digest |
|
87 |
+ then |
|
88 |
+ (setf (mime-part-type part) "message") |
|
89 |
+ (setf (mime-part-subtype part) "rfc822") |
|
90 |
+ (setf (mime-part-parameters part) |
|
91 |
+ '(("charset" . "us-ascii"))) |
|
92 |
+ (mime-parse-message-rfc822 part stream boundary pos |
|
93 |
+ mbox) |
|
94 |
+ else |
|
95 |
+ (setup-text-plain-part part stream boundary pos mbox)) |
|
96 |
+ else |
|
97 |
+ (setf (mime-part-type part) type) |
|
98 |
+ (setf (mime-part-subtype part) subtype) |
|
99 |
+ (setf (mime-part-parameters part) params) |
|
100 |
+ |
|
101 |
+ (cond |
|
102 |
+ ((equalp type "multipart") |
|
103 |
+ (mime-parse-multipart part stream boundary pos mbox)) |
|
104 |
+ ((message-rfc822-p type subtype) |
|
105 |
+ (mime-parse-message-rfc822 part stream boundary pos mbox)) |
|
106 |
+ (t |
|
107 |
+ (mime-parse-non-multipart part stream boundary pos mbox))))))))) |
|
108 |
+ |
|
109 |
+;; OK if 'string' is nil. |
|
110 |
+;; Might return nil |
|
111 |
+;; called by parse-mime-structure-1 |
|
112 |
+(defun parse-content-type (string) |
|
113 |
+ (block nil |
|
114 |
+ (if (null string) |
|
115 |
+ (return)) |
|
116 |
+ (let ((max (length string)) |
|
117 |
+ pos type subtype) |
|
118 |
+ (multiple-value-setq (type pos) |
|
119 |
+ (mime-get-token string 0 max)) |
|
120 |
+ (if (string= type "") |
|
121 |
+ (return)) |
|
122 |
+ |
|
123 |
+ (setf pos (skip-whitespace string pos max)) |
|
124 |
+ |
|
125 |
+ (if (or (>= pos max) (char/= (char string pos) #\/)) |
|
126 |
+ (return)) ;; bogus input |
|
127 |
+ |
|
128 |
+ (multiple-value-setq (subtype pos) |
|
129 |
+ (mime-get-token string (1+ pos) max)) |
|
130 |
+ |
|
131 |
+ (if (string= subtype "") |
|
132 |
+ (return)) ;; bogus input |
|
133 |
+ |
|
134 |
+ (values type subtype (parse-parameters string pos max))))) |
|
135 |
+ |
|
136 |
+;; called by parse-content-type. |
|
137 |
+(defun parse-parameters (string pos max) |
|
138 |
+ (let (char pairs param value) |
|
139 |
+ (while (< pos max) |
|
140 |
+ (setf pos (skip-whitespace string pos max)) |
|
141 |
+ (setf char (char string pos)) |
|
142 |
+ |
|
143 |
+ (if (char/= char #\;) |
|
144 |
+ (return)) |
|
145 |
+ |
|
146 |
+ (multiple-value-setq (param pos) |
|
147 |
+ (mime-get-token string (1+ pos) max)) |
|
148 |
+ (setf pos (skip-whitespace string pos max)) |
|
149 |
+ (if (or (>= pos max) (char/= (char string pos) #\=)) |
|
150 |
+ (return)) |
|
151 |
+ (multiple-value-setq (value pos) |
|
152 |
+ (mime-get-parameter-value string (1+ pos) max)) |
|
153 |
+ |
|
154 |
+ (push (cons param value) pairs)) |
|
155 |
+ (values (nreverse pairs) pos))) |
|
156 |
+ |
|
157 |
+ |
|
158 |
+(defconstant *mime-tspecials* |
|
159 |
+ '(#\( #\) #\< #\> #\@ |
|
160 |
+ #\, #\; #\: #\\ #\" |
|
161 |
+ #\/ #\[ #\] #\? #\=)) |
|
162 |
+ |
|
163 |
+(defun skip-whitespace (string pos max) |
|
164 |
+ (declare (optimize (speed 3)) |
|
165 |
+ (fixnum pos max)) |
|
166 |
+ (while (< pos max) |
|
167 |
+ (if (not (excl::whitespace-char-p (schar string pos))) |
|
168 |
+ (return)) |
|
169 |
+ (incf pos)) |
|
170 |
+ pos) |
|
171 |
+ |
|
172 |
+(defun mime-get-parameter-value (string pos max) |
|
173 |
+ (setf pos (skip-whitespace string pos max)) |
|
174 |
+ (if* (>= pos max) |
|
175 |
+ then |
|
176 |
+ (values "" pos) |
|
177 |
+ else |
|
178 |
+ (if (char= (char string pos) #\") |
|
179 |
+ (mime-get-quoted-string string pos max) |
|
180 |
+ (mime-get-token string pos max)))) |
|
181 |
+ |
|
182 |
+(defun mime-get-token (string pos max) |
|
183 |
+ (setf pos (skip-whitespace string pos max)) |
|
184 |
+ (let ((startpos pos) |
|
185 |
+ char) |
|
186 |
+ (while (< pos max) |
|
187 |
+ (setf char (char string pos)) |
|
188 |
+ (if (or (char= #\space char) (member char *mime-tspecials*)) |
|
189 |
+ (return)) |
|
190 |
+ (incf pos)) |
|
191 |
+ (values (subseq string startpos pos) pos))) |
|
192 |
+ |
|
193 |
+;; Doesn't attempt to dequote |
|
194 |
+(defun mime-get-quoted-string (string pos max) |
|
195 |
+ (let ((res (make-string (- max pos))) |
|
196 |
+ (outpos 0) |
|
197 |
+ char inquote inbackslash) |
|
198 |
+ (while (< pos max) |
|
199 |
+ (setf char (char string pos)) |
|
200 |
+ |
|
201 |
+ (if* (and (char= char #\") (not inbackslash)) |
|
202 |
+ then |
|
203 |
+ (if* inquote |
|
204 |
+ then |
|
205 |
+ (setf (schar res outpos) char) |
|
206 |
+ (incf outpos) |
|
207 |
+ (incf pos) |
|
208 |
+ (return)) |
|
209 |
+ (setf inquote t)) |
|
210 |
+ |
|
211 |
+ (if* inbackslash |
|
212 |
+ then |
|
213 |
+ (setf inbackslash nil) |
|
214 |
+ else |
|
215 |
+ (if (char= char #\\) |
|
216 |
+ (setf inbackslash t))) |
|
217 |
+ |
|
218 |
+ (setf (schar res outpos) char) |
|
219 |
+ (incf outpos) |
|
220 |
+ (incf pos)) |
|
221 |
+ |
|
222 |
+ (values (subseq res 0 outpos) pos))) |
|
223 |
+ |
|
224 |
+(defun mime-dequote (string) |
|
225 |
+ (block nil |
|
226 |
+ (if (or (string= string "") (char/= (char string 0) #\")) |
|
227 |
+ (return string)) |
|
228 |
+ |
|
229 |
+ (let* ((max (length string)) |
|
230 |
+ (pos 1) |
|
231 |
+ (res (make-string max)) |
|
232 |
+ (outpos 0) |
|
233 |
+ char inbackslash) |
|
234 |
+ |
|
235 |
+ (while (< pos max) |
|
236 |
+ (setf char (char string pos)) |
|
237 |
+ |
|
238 |
+ (if (and (char= char #\") (not inbackslash)) |
|
239 |
+ (return)) |
|
240 |
+ |
|
241 |
+ (if* (and (not inbackslash) (char= char #\\)) |
|
242 |
+ then |
|
243 |
+ (setf inbackslash t) |
|
244 |
+ (incf pos) |
|
245 |
+ else |
|
246 |
+ (setf (schar res outpos) char) |
|
247 |
+ (incf outpos) |
|
248 |
+ (incf pos) |
|
249 |
+ (setf inbackslash nil))) |
|
250 |
+ |
|
251 |
+ (subseq res 0 outpos)))) |
|
252 |
+ |
|
253 |
+(defun setup-text-plain-part (part stream boundary pos mbox) |
|
254 |
+ (setf (mime-part-type part) "text") |
|
255 |
+ (setf (mime-part-subtype part) "plain") |
|
256 |
+ (setf (mime-part-parameters part) '(("charset" . "us-ascii"))) |
|
257 |
+ (mime-parse-non-multipart part stream boundary pos mbox)) |
|
258 |
+ |
|
259 |
+(defun mime-parse-non-multipart (part stream boundary pos mbox) |
|
260 |
+ (let ((startpos pos)) |
|
261 |
+ (multiple-value-bind (endpos lines eof pos) |
|
262 |
+ (read-until-boundary stream boundary pos mbox) |
|
263 |
+ |
|
264 |
+ (setf (mime-part-lines part) lines) |
|
265 |
+ (setf (mime-part-body-position part) startpos) |
|
266 |
+ (setf (mime-part-body-size part) (- endpos startpos)) |
|
267 |
+ |
|
268 |
+ (values part eof pos)))) |
|
269 |
+ |
|
270 |
+(defun mime-parse-message-rfc822 (part stream boundary pos mbox) |
|
271 |
+ (let ((startpos pos)) |
|
272 |
+ (multiple-value-bind (message eof pos) |
|
273 |
+ (parse-mime-structure-1 stream boundary nil pos mbox) |
|
274 |
+ |
|
275 |
+ (setf (mime-part-message part) message) |
|
276 |
+ |
|
277 |
+ (setf (mime-part-body-position part) startpos) |
|
278 |
+ (setf (mime-part-body-size part) (- pos startpos)) |
|
279 |
+ |
|
280 |
+ (values part eof pos)))) |
|
281 |
+ |
|
282 |
+ |
|
283 |
+(defun mime-parse-multipart (part stream parent-boundary pos mbox) |
|
284 |
+ (let* ((params (mime-part-parameters part)) |
|
285 |
+ (boundary (cdr (assoc "boundary" params :test #'equalp))) |
|
286 |
+ (startpos pos) |
|
287 |
+ parts eof newpart) |
|
288 |
+ |
|
289 |
+ (setf (mime-part-boundary part) parent-boundary) |
|
290 |
+ |
|
291 |
+ ;; If boundary isn't specified.. try to compensate by using our |
|
292 |
+ ;; parent's boundary. |
|
293 |
+ (if (null boundary) |
|
294 |
+ (setf boundary parent-boundary) |
|
295 |
+ (setf boundary (mime-dequote boundary))) |
|
296 |
+ |
|
297 |
+ ;; Locate the first boundary. |
|
298 |
+ (multiple-value-bind (ignore1 ignore2 ignore3 newpos) |
|
299 |
+ (read-until-boundary stream boundary pos mbox) |
|
300 |
+ (declare (ignore ignore1 ignore2 ignore3)) |
|
301 |
+ (setf pos newpos)) |
|
302 |
+ |
|
303 |
+ (until eof |
|
304 |
+ (multiple-value-setq (newpart eof pos) |
|
305 |
+ (parse-mime-structure-1 stream boundary |
|
306 |
+ (equalp (mime-part-subtype part) "digest") |
|
307 |
+ pos mbox)) |
|
308 |
+ (push newpart parts)) |
|
309 |
+ |
|
310 |
+ (setf (mime-part-parts part) (nreverse parts)) |
|
311 |
+ |
|
312 |
+ ;; Discard everything that follows until we reach the parent-boundary. |
|
313 |
+ (multiple-value-bind (ignore1 ignore2 eof pos) |
|
314 |
+ (read-until-boundary stream parent-boundary pos mbox) |
|
315 |
+ (declare (ignore ignore1 ignore2)) |
|
316 |
+ |
|
317 |
+ (setf (mime-part-body-size part) (- pos startpos)) |
|
318 |
+ |
|
319 |
+ (values part eof pos)))) |
|
320 |
+;; support |
|
321 |
+ |
|
322 |
+;; Returns headers alist and the number of bytes read. |
|
323 |
+(defun parse-headers (stream mbox) |
|
324 |
+ (declare (optimize (speed 3) (safety 0))) |
|
325 |
+ (let ((count 0) headers colonpos name value) |
|
326 |
+ (excl::with-resource (line mime-line) |
|
327 |
+ (loop |
|
328 |
+ (let ((bytes (mime-read-line line stream mbox))) |
|
329 |
+ (if (null bytes) |
|
330 |
+ (return)) |
|
331 |
+ |
|
332 |
+ (incf count bytes) |
|
333 |
+ |
|
334 |
+ (mime-line-string-right-trim line) |
|
335 |
+ (if (string= line "") |
|
336 |
+ (return)) |
|
337 |
+ |
|
338 |
+ ;; Continuation line |
|
339 |
+ (if* (and (excl::whitespace-char-p (char line 0)) headers) |
|
340 |
+ then ;; yes |
|
341 |
+ (setf (cdr (car headers)) |
|
342 |
+ (concatenate 'string (cdr (car headers)) " " |
|
343 |
+ (string-left-trim *whitespace* line))) |
|
344 |
+ else (setf colonpos (position #\: line)) |
|
345 |
+ (if (null colonpos) ;; bogus input |
|
346 |
+ (return)) |
|
347 |
+ (setf name |
|
348 |
+ (string-trim *whitespace* (subseq line 0 colonpos))) |
|
349 |
+ (let ((startpos (position-if-not #'excl::whitespace-char-p |
|
350 |
+ line :start (1+ colonpos)))) |
|
351 |
+ (setf value |
|
352 |
+ (if* (null startpos) |
|
353 |
+ then "" |
|
354 |
+ else (subseq line startpos)))) |
|
355 |
+ |
|
356 |
+ (push (cons name value) headers))))) |
|
357 |
+ |
|
358 |
+ (values (nreverse headers) count))) |
|
359 |
+ |
|
360 |
+;; Returns: (1) position of the end of the part |
|
361 |
+;; (2) number of lines read |
|
362 |
+;; (3) :eof if EOF, :boundary if close delimiter was seen, else nil |
|
363 |
+;; (4) new stream position (post boundary read) |
|
364 |
+(defun read-until-boundary (stream boundary pos mbox) |
|
365 |
+ (let ((lines 0) |
|
366 |
+ (lastpos pos) |
|
367 |
+ bytes delimiter close-delimiter) |
|
368 |
+ |
|
369 |
+ (excl::with-resource (line mime-line) |
|
370 |
+ |
|
371 |
+ (when boundary |
|
372 |
+ (setf delimiter (concatenate 'string "--" boundary)) |
|
373 |
+ (setf close-delimiter (concatenate 'string delimiter "--"))) |
|
374 |
+ |
|
375 |
+ (loop |
|
376 |
+ (setf bytes (mime-read-line line stream mbox)) |
|
377 |
+ |
|
378 |
+ (if (or (null bytes) |
|
379 |
+ (and delimiter (prefixp delimiter line))) |
|
380 |
+ (return)) |
|
381 |
+ |
|
382 |
+ (incf pos bytes) |
|
383 |
+ |
|
384 |
+ (setf lastpos pos) |
|
385 |
+ (incf lines)) |
|
386 |
+ |
|
387 |
+ (values lastpos |
|
388 |
+ lines |
|
389 |
+ (cond ((null bytes) |
|
390 |
+ :eof) |
|
391 |
+ ((and close-delimiter (prefixp close-delimiter line)) |
|
392 |
+ :boundary) |
|
393 |
+ (t nil)) |
|
394 |
+ pos)))) |
|
395 |
+ |
|
396 |
+;; Returns values: |
|
397 |
+;; Number of characters read, including CR/LFs. Returns nil if EOF. |
|
398 |
+(defun mime-read-line (buffer stream mbox) |
|
399 |
+ (declare (optimize (speed 3) (safety 0))) |
|
400 |
+ (excl::with-underlying-simple-vector (buffer sbuf) |
|
401 |
+ (declare (type string sbuf)) |
|
402 |
+ (let* ((pos 0) |
|
403 |
+ (count 0) |
|
404 |
+ (max (array-dimension buffer 0)) |
|
405 |
+ (crlf (eq (eol-convention stream) :dos)) |
|
406 |
+ char) |
|
407 |
+ (declare (fixnum pos count max)) |
|
408 |
+ (while (and (< pos max) (setf char (read-char stream nil nil))) |
|
409 |
+ (incf count) |
|
410 |
+ (when (char= char #\newline) |
|
411 |
+ (if crlf |
|
412 |
+ (incf count)) ;; account for carriage return as well |
|
413 |
+ (return)) |
|
414 |
+ |
|
415 |
+ (setf (schar sbuf pos) char) |
|
416 |
+ (incf pos)) |
|
417 |
+ |
|
418 |
+ (setf (fill-pointer buffer) pos) |
|
419 |
+ |
|
420 |
+ ;; Treat mbox "From " line as EOF |
|
421 |
+ (if (and mbox (prefixp "From " buffer)) |
|
422 |
+ (setf count 0)) |
|
423 |
+ |
|
424 |
+ (if (/= count 0) count)))) |
|
425 |
+ |
|
426 |
+(defun mime-line-string-right-trim (line) |
|
427 |
+ (let ((pos (position-if-not #'excl::whitespace-char-p line :from-end t))) |
|
428 |
+ (if pos |
|
429 |
+ (setf (fill-pointer line) (1+ pos))))) |
|
430 |
+ |
|
431 |
+;;; body streams stuff |
|
432 |
+ |
|
433 |
+(defun body-stream-func (outstream instream boundary) |
|
434 |
+ (let ((delimiter (if boundary (concatenate 'string "--" boundary))) |
|
435 |
+ line) |
|
436 |
+ |
|
437 |
+ (while (setf line (read-line instream nil nil)) |
|
438 |
+ (if (and delimiter (prefixp delimiter line)) |
|
439 |
+ (return)) |
|
440 |
+ |
|
441 |
+ (write-line line outstream)))) |
|
442 |
+ |
|
443 |
+(defun body-stream-func-with-count (outstream instream count) |
|
444 |
+ (declare (optimize (speed 3)) |
|
445 |
+ (fixnum count)) |
|
446 |
+ (let (char) |
|
447 |
+ (dotimes (n count) |
|
448 |
+ (declare (fixnum n)) |
|
449 |
+ (setf char (read-char instream nil nil)) |
|
450 |
+ (if* (null char) |
|
451 |
+ then (return) |
|
452 |
+ else (write-char char outstream))))) |
|
453 |
+ |
|
454 |
+ |
|
455 |
+(defmacro with-part-body-stream ((sym instream part &key count) &body body) |
|
456 |
+ (if* count |
|
457 |
+ then |
|
458 |
+ `(with-function-input-stream (,sym #'body-stream-func-with-count |
|
459 |
+ ,instream ,count) |
|
460 |
+ ,@body) |
|
461 |
+ else |
|
462 |
+ `(with-function-input-stream (,sym #'body-stream-func |
|
463 |
+ ,instream |
|
464 |
+ (mime-part-boundary ,part)) |
|
465 |
+ ,@body))) |
|
466 |
+ |
|
467 |
+;;; testing |
|
468 |
+ |
|
469 |
+#| |
|
470 |
+(defun test-parse-mime (file &key (pretty t)) |
|
471 |
+ (with-open-file (f file) |
|
472 |
+ (let ((res (parse-mime-structure f))) |
|
473 |
+ (if pretty |
|
474 |
+ (pprint-rfc822-part res) |
|
475 |
+ res)))) |
|
476 |
+ |
|
477 |
+(defun pprint-rfc822-part (thing &optional (prefix "")) |
|
478 |
+ (if (null thing) |
|
479 |
+ (error "Strange. something called pprint-rfc822-part with nil")) |
|
480 |
+ (let ((type (mime-part-type thing)) |
|
481 |
+ (subtype (mime-part-subtype thing))) |
|
482 |
+ (format t "~AHEADER ([RFC-2822] header of the message)~%" prefix) |
|
483 |
+ (format t "~ATEXT ([RFC-2822] text body of the message) ~A/~A~%" prefix type subtype) |
|
484 |
+ |
|
485 |
+ (if* (message-rfc822-p type subtype) |
|
486 |
+ then ;; XXX .. what should the new prefix be? |
|
487 |
+ (pprint-rfc822-part (mime-part-message thing) prefix) |
|
488 |
+ elseif (equalp type "multipart") |
|
489 |
+ then (pprint-multipart thing prefix)))) |
|
490 |
+ |
|
491 |
+(defun pprint-multipart (thing prefix) |
|
492 |
+ (let ((id 1)) |
|
493 |
+ (dolist (part (mime-part-parts thing)) |
|
494 |
+ (let ((type (mime-part-type part)) |
|
495 |
+ (subtype (mime-part-subtype part))) |
|
496 |
+ (format t "~a~a ~a/~a~%" prefix id type subtype) |
|
497 |
+ (if* (message-rfc822-p type subtype) |
|
498 |
+ then (pprint-rfc822-part (mime-part-message part) |
|
499 |
+ (format nil "~a~a." prefix id)) |
|
500 |
+ elseif (equalp type "multipart") |
|
501 |
+ then (pprint-multipart part (format nil "~a~a." prefix id))) |
|
502 |
+ (incf id))))) |
|
503 |
+|# |
0 | 504 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,239 @@ |
1 |
+;; $Id: mime-transfer-encoding.cl,v 1.1 2006/01/26 23:53:27 dancy Exp $ |
|
2 |
+ |
|
3 |
+(defpackage :net.post-office |
|
4 |
+ (:use #:lisp #:excl) |
|
5 |
+ (:export |
|
6 |
+ #:base64-encode-stream |
|
7 |
+ #:qp-encode-stream)) |
|
8 |
+ |
|
9 |
+(in-package :net.post-office) |
|
10 |
+ |
|
11 |
+;;; Supported transfer encodings |
|
12 |
+ |
|
13 |
+;; encoders |
|
14 |
+ |
|
15 |
+(defun raw-encode-stream (instream outstream) |
|
16 |
+ (declare (optimize (speed 3) (safety 0))) |
|
17 |
+ (let ((buf (make-array 4096 :element-type '(unsigned-byte 8))) |
|
18 |
+ got) |
|
19 |
+ (declare (dynamic-extent buf) |
|
20 |
+ (fixnum got)) |
|
21 |
+ (while (/= 0 (setf got (read-vector buf instream))) |
|
22 |
+ (write-vector buf outstream :end got)))) |
|
23 |
+ |
|
24 |
+;; Temporary until this change is made in excl.cl. |
|
25 |
+(defparameter excl::*to-base64* |
|
26 |
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") |
|
27 |
+ |
|
28 |
+;; This should be added to the excl package. |
|
29 |
+(defun base64-encode-stream (instream outstream &key (wrap-at 72)) |
|
30 |
+ (declare (optimize (speed 3))) |
|
31 |
+ ;; inbuf _must_ be a size which is a multiple of three. The |
|
32 |
+ ;; encoding code depends on it. outbuf must be 4/3rds bigger than |
|
33 |
+ ;; inbuf. |
|
34 |
+ (let ((inbuf (make-array #.(* 3 4096) :element-type '(unsigned-byte 8))) |
|
35 |
+ (outbuf (make-array #.(* 4 4096) :element-type 'character)) |
|
36 |
+ remaining end outpos inpos value) |
|
37 |
+ (declare (dynamic-extent inbuf outbuf) |
|
38 |
+ (fixnum remaining outpos end inpos value)) |
|
39 |
+ |
|
40 |
+ (macrolet ((outchar (char) |
|
41 |
+ `(progn |
|
42 |
+ (setf (schar outbuf outpos) ,char) |
|
43 |
+ (incf outpos))) |
|
44 |
+ (outchar-base64 (x) |
|
45 |
+ `(outchar (schar excl::*to-base64* (logand ,x 63))))) |
|
46 |
+ |
|
47 |
+ (flet ((read-full-vector (buf stream) |
|
48 |
+ (let ((pos 0) |
|
49 |
+ (max (length buf)) |
|
50 |
+ newpos) |
|
51 |
+ (declare (fixnum pos max got newpos)) |
|
52 |
+ (while (< pos max) |
|
53 |
+ (setf newpos (read-vector buf stream :start pos)) |
|
54 |
+ (if* (= newpos pos) |
|
55 |
+ then (return)) |
|
56 |
+ (setf pos newpos)) |
|
57 |
+ pos))) |
|
58 |
+ |
|
59 |
+ (while (/= 0 (setf end (read-full-vector inbuf instream))) |
|
60 |
+ (setf remaining end) |
|
61 |
+ (setf inpos 0) |
|
62 |
+ (setf outpos 0) |
|
63 |
+ (while (> remaining 0) |
|
64 |
+ (if* (>= remaining 3) |
|
65 |
+ then (setf value (logior (ash (aref inbuf inpos) 16) |
|
66 |
+ (ash (aref inbuf (+ 1 inpos)) 8) |
|
67 |
+ (aref inbuf (+ 2 inpos)))) |
|
68 |
+ (incf inpos 3) |
|
69 |
+ (decf remaining 3) |
|
70 |
+ (outchar-base64 (ash value -18)) |
|
71 |
+ (outchar-base64 (ash value -12)) |
|
72 |
+ (outchar-base64 (ash value -6)) |
|
73 |
+ (outchar-base64 value) |
|
74 |
+ elseif (= remaining 2) |
|
75 |
+ then (setf value (logior (ash (aref inbuf inpos) 16) |
|
76 |
+ (ash (aref inbuf (+ 1 inpos)) 8))) |
|
77 |
+ (incf inpos 2) |
|
78 |
+ (decf remaining 2) |
|
79 |
+ (outchar-base64 (ash value -18)) |
|
80 |
+ (outchar-base64 (ash value -12)) |
|
81 |
+ (outchar-base64 (ash value -6)) |
|
82 |
+ (outchar #\=) |
|
83 |
+ else (setf value (ash (aref inbuf inpos) 16)) |
|
84 |
+ (incf inpos) |
|
85 |
+ (decf remaining) |
|
86 |
+ (outchar-base64 (ash value -18)) |
|
87 |
+ (outchar-base64 (ash value -12)) |
|
88 |
+ (outchar #\=) |
|
89 |
+ (outchar #\=))) |
|
90 |
+ |
|
91 |
+ ;; generate output. |
|
92 |
+ (if* (null wrap-at) |
|
93 |
+ then (write-string outbuf outstream :end outpos) |
|
94 |
+ else (setf inpos 0) |
|
95 |
+ (while (< inpos outpos) |
|
96 |
+ (let ((len (min (- outpos inpos) wrap-at))) |
|
97 |
+ (write-string outbuf outstream |
|
98 |
+ :start inpos |
|
99 |
+ :end (+ inpos len)) |
|
100 |
+ (incf inpos len) |
|
101 |
+ (write-char #\newline outstream))))))))) |
|
102 |
+ |
|
103 |
+(defconstant *qp-hex-digits* "0123456789ABCDEF") |
|
104 |
+ |
|
105 |
+;; wrap-at is not a hard limit but more of a suggestion. it may be |
|
106 |
+;; late by by 3 characters. |
|
107 |
+(defun qp-encode-stream (instream outstream &key (wrap-at 72)) |
|
108 |
+ (declare (optimize (speed 3))) |
|
109 |
+ (let ((prev 0) |
|
110 |
+ (outcol 0) |
|
111 |
+ byte) |
|
112 |
+ (declare (fixnum byte prev)) |
|
113 |
+ |
|
114 |
+ (macrolet ((whitespace (x) |
|
115 |
+ (let ((xx (gensym))) |
|
116 |
+ `(let ((,xx ,x)) |
|
117 |
+ (or (= ,xx 9) (= ,xx 32)))))) |
|
118 |
+ |
|
119 |
+ (labels ((check-linewrap () |
|
120 |
+ (if* (and wrap-at (>= outcol wrap-at)) |
|
121 |
+ then (format outstream "=~%" outstream) |
|
122 |
+ (setf outcol 0))) |
|
123 |
+ (check-deferred () |
|
124 |
+ (if* (and (= prev 13) (/= byte 10)) |
|
125 |
+ then ;; previous byte was bare CR. Handle |
|
126 |
+ (check-linewrap) |
|
127 |
+ (write-string "=0D" outstream) |
|
128 |
+ (incf outcol 3)) |
|
129 |
+ |
|
130 |
+ (if* (whitespace prev) |
|
131 |
+ then (if* (or (= byte 0) (= byte 10) (= byte 13)) |
|
132 |
+ then ;; EOF, EOL, probable EOL. Encode. |
|
133 |
+ (check-linewrap) |
|
134 |
+ (format outstream "=20") |
|
135 |
+ (incf outcol 3) |
|
136 |
+ else ;; Safe to print deferred whitespace |
|
137 |
+ (check-linewrap) |
|
138 |
+ (write-char (code-char prev) outstream) |
|
139 |
+ (incf outcol 1))))) |
|
140 |
+ |
|
141 |
+ (while (setf byte (read-byte instream nil nil)) |
|
142 |
+ (check-deferred) |
|
143 |
+ |
|
144 |
+ (if* (or (and (>= byte 33) (<= byte 60)) |
|
145 |
+ (and (>= byte 62) (<= byte 126))) |
|
146 |
+ then (check-linewrap) |
|
147 |
+ (write-char (code-char byte) outstream) |
|
148 |
+ (incf outcol) |
|
149 |
+ elseif (or (= byte 13) (whitespace byte)) |
|
150 |
+ thenret ;; defer handling |
|
151 |
+ elseif (= byte 10) ;; LF |
|
152 |
+ then (write-char #\newline outstream) |
|
153 |
+ (setf outcol 0) |
|
154 |
+ else (check-linewrap) |
|
155 |
+ (format outstream "=~c~c" |
|
156 |
+ (schar *qp-hex-digits* |
|
157 |
+ (ash byte -4)) |
|
158 |
+ (schar *qp-hex-digits* |
|
159 |
+ (logand byte #xf))) |
|
160 |
+ (incf outcol 3)) |
|
161 |
+ |
|
162 |
+ (setf prev byte)) |
|
163 |
+ |
|
164 |
+ ;; Handle final deferred data |
|
165 |
+ (setf byte 0) |
|
166 |
+ (check-deferred))))) |
|
167 |
+ |
|
168 |
+ |
|
169 |
+#| |
|
170 |
+ |
|
171 |
+(defun mime-decode-transfer-encoding (outstream instream encoding) |
|
172 |
+ (cond |
|
173 |
+ ((equalp encoding "quoted-printable") |
|
174 |
+ (decode-quoted-printable outstream instream)) |
|
175 |
+ (t |
|
176 |
+ (decode-unmodified outstream instream)))) |
|
177 |
+ |
|
178 |
+(defmacro with-decoded-transfer-encoding-stream ((sym instream encoding) &body body) |
|
179 |
+ `(with-function-input-stream (,sym #'mime-decode-transfer-encoding ,instream |
|
180 |
+ ,encoding) |
|
181 |
+ ,@body)) |
|
182 |
+ |
|
183 |
+(defun decoded-part-body-stream-func (outstream instream part) |
|
184 |
+ (with-part-body-stream (part-body-stream instream part) |
|
185 |
+ (mime-decode-transfer-encoding outstream part-body-stream |
|
186 |
+ (mime-part-encoding part)))) |
|
187 |
+ |
|
188 |
+(defmacro with-decoded-part-body-stream ((sym instream part) &body body) |
|
189 |
+ `(with-function-input-stream (,sym #'decoded-part-body-stream-func |
|
190 |
+ ,instream ,part) |
|
191 |
+ ,@body)) |
|
192 |
+ |
|
193 |
+ |
|
194 |
+;; The decoders |
|
195 |
+ |
|
196 |
+(defun decode-unmodified (outstream instream) |
|
197 |
+ (let (line) |
|
198 |
+ (while (setf line (read-line instream nil nil)) |
|
199 |
+ (write-line line outstream)))) |
|
200 |
+ |
|
201 |
+(defun decode-quoted-printable (outstream instream) |
|
202 |
+ (declare (optimize (speed 3))) |
|
203 |
+ (let (line max pos char char2 softlinebreak) |
|
204 |
+ (while (setf line (read-line instream nil nil)) |
|
205 |
+ (setf max (length line)) |
|
206 |
+ (setf pos 0) |
|
207 |
+ |
|
208 |
+ (macrolet ((getchar () |
|
209 |
+ `(if* (>= pos max) |
|
210 |
+ then (setf softlinebreak t) |
|
211 |
+ (return) |
|
212 |
+ else (prog1 (schar line pos) (incf pos))))) |
|
213 |
+ |
|
214 |
+ (while (< pos max) |
|
215 |
+ (setf char (getchar)) |
|
216 |
+ |
|
217 |
+ (if* (char= char #\=) |
|
218 |
+ then ;; If EOL occurs during the attempt to get the next |
|
219 |
+ ;; two chars, it will be treated as a soft line break. |
|
220 |
+ (setf char (getchar)) |
|
221 |
+ (setf char2 (getchar)) |
|
222 |
+ |
|
223 |
+ (let ((value (logior |
|
224 |
+ (ash (or (position char *qp-hex-digits*) -1) 4) |
|
225 |
+ (or (position char2 *qp-hex-digits*) -1)))) |
|
226 |
+ (if* (> value -1) |
|
227 |
+ then |
|
228 |
+ (write-byte value outstream) |
|
229 |
+ else |
|
230 |
+ ;; We got some bogus input. Leave it untouched |
|
231 |
+ (write-char #\= outstream) |
|
232 |
+ (write-char char outstream) |
|
233 |
+ (write-char char2 outstream))) |
|
234 |
+ else (write-char char outstream))) |
|
235 |
+ |
|
236 |
+ (if* softlinebreak |
|
237 |
+ then (setf softlinebreak nil) |
|
238 |
+ else (write-char #\newline outstream)))))) |
|
239 |
+|# |
0 | 240 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,611 @@ |
1 |
+;; $Id: mime.cl,v 1.1 2006/01/26 23:53:27 dancy Exp $ |
|
2 |
+ |
|
3 |
+(defpackage :net.post-office |
|
4 |
+ (:use #:lisp #:excl) |
|
5 |
+ (:export |
|
6 |
+ ;; functions/methods |
|
7 |
+ #:make-mime-part |
|
8 |
+ #:mime-part-writer |
|
9 |
+ #:mime-part-p |
|
10 |
+ #:mime-part-constructed-p |
|
11 |
+ |
|
12 |
+ ;; macros |
|
13 |
+ #:mime-get-header |
|
14 |
+ #:with-mime-part-constructed-stream |
|
15 |
+ |
|
16 |
+ ;; slot accessors |
|
17 |
+ #:mime-part-type |
|
18 |
+ #:mime-part-subtype |
|
19 |
+ #:mime-part-parameters |
|
20 |
+ #:mime-part-id |
|
21 |
+ #:mime-part-description |
|
22 |
+ #:mime-part-encoding |
|
23 |
+ #:mime-part-headers |
|
24 |
+ #:mime-part-parts |
|
25 |
+ #:mime-part-boundary)) |
|
26 |
+ |
|
27 |
+(in-package :net.post-office) |
|
28 |
+ |
|
29 |
+(eval-when (compile load eval) |
|
30 |
+ (require :osi) |
|
31 |
+ (require :regexp2)) |
|
32 |
+ |
|
33 |
+(defclass mime-part () |
|
34 |
+ ( |
|
35 |
+ (type :accessor mime-part-type :initform nil) |
|
36 |
+ (subtype :accessor mime-part-subtype :initform nil) |
|
37 |
+ (parameters :accessor mime-part-parameters :initform nil) ;; alist |
|
38 |
+ (id :accessor mime-part-id :initform nil) |
|
39 |
+ (description :accessor mime-part-description :initform nil) |
|
40 |
+ (encoding :accessor mime-part-encoding :initform nil) |
|
41 |
+ (headers ;; parsed headers alist |
|
42 |
+ :accessor mime-part-headers :initform nil) |
|
43 |
+ (parts ;; list of subparts (for multipart types) |
|
44 |
+ :accessor mime-part-parts :initform nil) |
|
45 |
+ (boundary :accessor mime-part-boundary :initform nil))) |
|
46 |
+ |
|
47 |
+(defclass mime-part-constructed (mime-part) |
|
48 |
+ ( |
|
49 |
+ (source-type :accessor source-type) |
|
50 |
+ (source :accessor source) |
|
51 |
+ (disposition :accessor disposition :initform nil) |
|
52 |
+ (disposition-name :accessor disposition-name :initform nil))) |
|
53 |
+ |
|
54 |
+(defmacro mime-get-header (header part) |
|
55 |
+ `(cdr (assoc ,header (mime-part-headers ,part) :test #'equalp))) |
|
56 |
+ |
|
57 |
+(defun message-rfc822-p (type subtype) |
|
58 |
+ (and (equalp type "message") (equalp subtype "rfc822"))) |
|
59 |
+ |
|
60 |
+(defun multipart-p (part) |
|
61 |
+ (equalp (mime-part-type part) "multipart")) |
|
62 |
+ |
|
63 |
+(defun multipart-mixed-p (part) |
|
64 |
+ (and (equalp (mime-part-type part) "multipart") |
|
65 |
+ (equalp (mime-part-subtype part) "mixed"))) |
|
66 |
+ |
|
67 |
+(defun mime-part-p (thing) |
|
68 |
+ (typep thing 'mime-part)) |
|
69 |
+ |
|
70 |
+(defun mime-part-constructed-p (thing) |
|
71 |
+ (typep thing 'mime-part-constructed)) |
|
72 |
+ |
|
73 |
+(defun generate-boundary () |
|
74 |
+ (declare (optimize (speed 3))) |
|
75 |
+ (let ((hex "01234567890abcdef")) |
|
76 |
+ (with-output-to-string (s) |
|
77 |
+ (write-string "----------_" s) |
|
78 |
+ (dotimes (n 32) |
|
79 |
+ (write-char (schar hex (random 16)) s))))) |
|
80 |
+ |
|
81 |
+(defun make-mime-part (&key content-type encoding headers |
|
82 |
+ (attachmentp nil attachmentp-supplied) |
|
83 |
+ name text (start 0) end file |
|
84 |
+ subparts (external-format :default) |
|
85 |
+ parameters charset id description) |
|
86 |
+ (let ((part (make-instance 'mime-part-constructed)) |
|
87 |
+ type subtype multipart textp filepath) |
|
88 |
+ |
|
89 |
+ (if* (and text file) |
|
90 |
+ then (error "Only one of :text or :file may be specified")) |
|
91 |
+ |
|
92 |
+ (if* (and text (null end)) |
|
93 |
+ then (setf end (length text))) |
|
94 |
+ |
|
95 |
+ (when file |
|
96 |
+ (if* (streamp file) |
|
97 |
+ then (setf filepath (ignore-errors (namestring file))) |
|
98 |
+ else (setf filepath file))) |
|
99 |
+ |
|
100 |
+ (when (null content-type) |
|
101 |
+ (if* filepath |
|
102 |
+ then (setf content-type (lookup-mime-type filepath))) |
|
103 |
+ |
|
104 |
+ (when (null content-type) |
|
105 |
+ (setf content-type |
|
106 |
+ (if* subparts |
|
107 |
+ then "multipart/mixed" |
|
108 |
+ elseif file |
|
109 |
+ then "application/octet-stream" |
|
110 |
+ elseif (and text (stringp text)) |
|
111 |
+ then "text/plain" |
|
112 |
+ else "application/octet-stream")))) |
|
113 |
+ |
|
114 |
+ (let ((pos (position #\/ content-type))) |
|
115 |
+ (if (null pos) |
|
116 |
+ (error "Invalid content-type: ~s" content-type)) |
|
117 |
+ |
|
118 |
+ (setf type (subseq content-type 0 pos)) |
|
119 |
+ (setf subtype (subseq content-type (1+ pos)))) |
|
120 |
+ |
|
121 |
+ (setf multipart (equalp type "multipart")) |
|
122 |
+ (setf textp (or (equalp type "text") (message-rfc822-p type subtype))) |
|
123 |
+ |
|
124 |
+ (if* (and subparts (not multipart)) |
|
125 |
+ then (error "subparts may not be specified for non-multipart parts")) |
|
126 |
+ |
|
127 |
+ (if* (and (not multipart) (null text) (null file)) |
|
128 |
+ then (error "One of :text or :file must be specified")) |
|
129 |
+ |
|
130 |
+ (if* (and (null charset) textp) |
|
131 |
+ then (setf charset |
|
132 |
+ (or |
|
133 |
+ (guess-charset-from-ef (find-external-format external-format)) |
|
134 |
+ "us-ascii"))) |
|
135 |
+ |
|
136 |
+ (when (and (not multipart) (null encoding)) |
|
137 |
+ (if* textp |
|
138 |
+ then (if* (equalp charset "us-ascii") |
|
139 |
+ then (setf encoding "7bit") |
|
140 |
+ else (setf encoding "quoted-printable")) |
|
141 |
+ else (setf encoding "base64"))) |
|
142 |
+ |
|
143 |
+ (setf (mime-part-type part) type) |
|
144 |
+ (setf (mime-part-subtype part) subtype) |
|
145 |
+ (setf (mime-part-parameters part) parameters) |
|
146 |
+ (if* charset |
|
147 |
+ then (push (cons "charset" charset) (mime-part-parameters part))) |
|
148 |
+ (setf (mime-part-encoding part) encoding) |
|
149 |
+ (setf (mime-part-id part) id) |
|
150 |
+ (setf (mime-part-description part) description) |
|
151 |
+ (setf (mime-part-headers part) headers) |
|
152 |
+ |
|
153 |
+ (if* file |
|
154 |
+ then (setf (source part) file) |
|
155 |
+ (if* (streamp file) |
|
156 |
+ then (setf (source-type part) :stream) |
|
157 |
+ else (with-open-file (f file)) ;; make sure we can read it. |
|
158 |
+ (setf (source-type part) :file)) |
|
159 |
+ (if* (not attachmentp-supplied) |
|
160 |
+ then (setf attachmentp t)) |
|
161 |
+ else (setf (source-type part) :usb8) |
|
162 |
+ (setf (source part) |
|
163 |
+ (if* (stringp text) |
|
164 |
+ then (string-to-octets text :null-terminate nil |
|
165 |
+ :external-format external-format) |
|
166 |
+ else (subseq text start end)))) |
|
167 |
+ |
|
168 |
+ (if* (and (not textp) (not attachmentp) (not multipart)) |
|
169 |
+ then (setf (disposition part) "inline")) |
|
170 |
+ |
|
171 |
+ (when attachmentp |
|
172 |
+ (setf (disposition part) "attachment") |
|
173 |
+ (if* (and (null name) file) |
|
174 |
+ then (setf name (excl.osi:basename filepath)))) |
|
175 |
+ |
|
176 |
+ (if* name |
|
177 |
+ then (setf (disposition-name part) name)) |
|
178 |
+ |
|
179 |
+ (if* multipart |
|
180 |
+ then (let ((boundary (generate-boundary))) |
|
181 |
+ (setf (mime-part-boundary part) boundary) |
|
182 |
+ (push (cons "boundary" boundary) |
|
183 |
+ (mime-part-parameters part)))) |
|
184 |
+ |
|
185 |
+ (setf (mime-part-parts part) subparts) |
|
186 |
+ |
|
187 |
+ part)) |
|
188 |
+ |
|
189 |
+(defparameter *ef-nick-to-mime-charset* |
|
190 |
+ '((:ascii . "us-ascii") |
|
191 |
+ (:iso-2022-jp . "iso-2022-jp") |
|
192 |
+ (:koi8-r . "koi8-r") |
|
193 |
+ (:shiftjis . "shift_jis") |
|
194 |
+ (:euc . "euc-jp") |
|
195 |
+ (:gb2312 . "gb2312") |
|
196 |
+ (:big5 . "big5") |
|
197 |
+ (:utf8 . "utf-8"))) |
|
198 |
+ |
|
199 |
+(defun guess-charset-from-ef (ef) |
|
200 |
+ (dolist (nick (ef-nicknames (find-external-format ef))) |
|
201 |
+ (let ((charset (cdr (assoc nick *ef-nick-to-mime-charset*)))) |
|
202 |
+ (if charset (return-from guess-charset-from-ef charset)))) |
|
203 |
+ (let ((ef-name (string-downcase (symbol-name (ef-name (crlf-base-ef ef)))))) |
|
204 |
+ ;; Try iso-8559-x names. |
|
205 |
+ (multiple-value-bind (found ignore suffix) |
|
206 |
+ (match-re (load-time-value "^iso8859-(\\d+)-base") ef-name) |
|
207 |
+ (declare (ignore ignore)) |
|
208 |
+ (if found |
|
209 |
+ (return-from guess-charset-from-ef |
|
210 |
+ (format nil "iso-8859-~a" suffix)))) |
|
211 |
+ |
|
212 |
+ ;; Try windows- names. |
|
213 |
+ (multiple-value-bind (found whole value) |
|
214 |
+ (match-re (load-time-value "^(\\d+)-base$") ef-name) |
|
215 |
+ (declare (ignore whole)) |
|
216 |
+ (if found |
|
217 |
+ (return-from guess-charset-from-ef |
|
218 |
+ (format nil "windows-~a" value)))))) |
|
219 |
+ |
|
220 |
+(defmethod mime-part-writer ((part mime-part-constructed) |
|
221 |
+ &key (stream *terminal-io*)) |
|
222 |
+ (mime-part-constructed-writer part stream t)) |
|
223 |
+ |
|
224 |
+(defun mime-part-constructed-writer (part stream top-level) |
|
225 |
+ (if* top-level |
|
226 |
+ then (format stream "MIME-Version: 1.0~%")) |
|
227 |
+ |
|
228 |
+ ;; First dump user-supplied headers. |
|
229 |
+ (dolist (h (mime-part-headers part)) |
|
230 |
+ (format stream "~a: ~a~%" (car h) (cdr h))) |
|
231 |
+ |
|
232 |
+ ;; Now dump headers that are based on class fields. |
|
233 |
+ |
|
234 |
+ (let* ((type (mime-part-type part)) |
|
235 |
+ (multipart (equalp type "multipart"))) |
|
236 |
+ (format stream "Content-Type: ~a/~a" type (mime-part-subtype part)) |
|
237 |
+ (dolist (param (mime-part-parameters part)) |
|
238 |
+ (format stream ";~% ~a=~s" (car param) (cdr param))) |
|
239 |
+ (format stream "~%") |
|
240 |
+ |
|
241 |
+ (if* (mime-part-encoding part) |
|
242 |
+ then (format stream "Content-Transfer-Encoding: ~a~%" |
|
243 |
+ (mime-part-encoding part))) |
|
244 |
+ |
|
245 |
+ (if* (mime-part-id part) |
|
246 |
+ then (format stream "Content-Id: ~a~%" (mime-part-id part))) |
|
247 |
+ |
|
248 |
+ (if* (mime-part-description part) |
|
249 |
+ then (format stream "Content-Description: ~a~%" |
|
250 |
+ (mime-part-description part))) |
|
251 |
+ |
|
252 |
+ |
|
253 |
+ (if* (disposition part) |
|
254 |
+ then (format stream "Content-Disposition: ~a" (disposition part)) |
|
255 |
+ (if* (disposition-name part) |
|
256 |
+ then (format stream ";~% filename=~s" (disposition-name part))) |
|
257 |
+ (format stream "~%")) |
|
258 |
+ |
|
259 |
+ (format stream "~%") ;; terminate headers |
|
260 |
+ |
|
261 |
+ (if* multipart |
|
262 |
+ then (let ((boundary (mime-part-boundary part))) |
|
263 |
+ (if top-level |
|
264 |
+ (format stream "~ |
|
265 |
+This is a multi-part message in MIME format.~%")) |
|
266 |
+ (dolist (subpart (mime-part-parts part)) |
|
267 |
+ (format stream "~%--~a~%" boundary) |
|
268 |
+ (mime-part-constructed-writer subpart stream nil)) |
|
269 |
+ (format stream "~%--~a--~%" boundary)) |
|
270 |
+ else (let ((instream (if* (eq (source-type part) :stream) |
|
271 |
+ then (source part) |
|
272 |
+ elseif (eq (source-type part) :file) |
|
273 |
+ then (open (source part)) |
|
274 |
+ else (make-buffer-input-stream (source part))))) |
|
275 |
+ (unwind-protect |
|
276 |
+ (let ((encoding (mime-part-encoding part))) |
|
277 |
+ (if* (equalp encoding "base64") |
|
278 |
+ then (base64-encode-stream instream stream) |
|
279 |
+ elseif (equalp encoding "quoted-printable") |
|
280 |
+ then (qp-encode-stream instream stream) |
|
281 |
+ else (raw-encode-stream instream stream))) |
|
282 |
+ ;; cleanup |
|
283 |
+ (if* (not (eq (source-type part) :stream)) |
|
284 |
+ then (close instream))))))) |
|
285 |
+ |
|
286 |
+(defun mime-part-writer-1 (stream part) |
|
287 |
+ (mime-part-writer part :stream stream)) |
|
288 |
+ |
|
289 |
+(defmacro with-mime-part-constructed-stream ((stream part) &body body) |
|
290 |
+ `(excl::with-function-input-stream (,stream #'mime-part-writer-1 ,part) |
|
291 |
+ ,@body)) |
|
292 |
+ |
|
293 |
+;; Stuff ripped off from aserve |
|
294 |
+ |
|
295 |
+(defun split-namestring (file) |
|
296 |
+ ;; split the namestring into root and tail and then the tail |
|
297 |
+ ;; into name and type |
|
298 |
+ ;; |
|
299 |
+ ;; any of the return value can be nil if the corresponding item |
|
300 |
+ ;; isn't present. |
|
301 |
+ ;; |
|
302 |
+ ;; rules for splitting the tail into name and type components: |
|
303 |
+ ;; if the last period in the tail is at the beginning or end of the |
|
304 |
+ ;; tail, then the name is exactly the tail and type is nil. |
|
305 |
+ ;; Thus .foo and bar. are just names, no type |
|
306 |
+ ;; but .foo.c has a name of ".foo" and a type of "c" |
|
307 |
+ ;; Thus if there is a non-nil type then it means that |
|
308 |
+ ;; 1. there will be a non nil name as well |
|
309 |
+ ;; 2. to reconstruct the filename you need to add a period between |
|
310 |
+ ;; the name and type. |
|
311 |
+ ;; |
|
312 |
+ (let ((pos (min (or (or (position #\/ file :from-end t) most-positive-fixnum) |
|
313 |
+ #+mswindows (position #\\ file :from-end t)))) |
|
314 |
+ root |
|
315 |
+ tail) |
|
316 |
+ |
|
317 |
+ (if* (equal file "") then (return-from split-namestring nil)) |
|
318 |
+ |
|
319 |
+ (if* (and pos (< pos most-positive-fixnum)) |
|
320 |
+ then ; we have root and tail |
|
321 |
+ (if* (eql pos (1- (length file))) |
|
322 |
+ then ; just have root |
|
323 |
+ (return-from split-namestring |
|
324 |
+ (values file nil nil nil))) |
|
325 |
+ |
|
326 |
+ |
|
327 |
+ (setq root (subseq file 0 (1+ pos)) |
|
328 |
+ tail (subseq file (1+ pos))) |
|
329 |
+ else (setq tail file)) |
|
330 |
+ |
|
331 |
+ |
|
332 |
+ ; split the tail |
|
333 |
+ (let ((pos (position #\. tail :from-end t))) |
|
334 |
+ (if* (or (null pos) |
|
335 |
+ (zerop pos) |
|
336 |
+ (equal pos (1- (length tail)))) |
|
337 |
+ then ; name begins or ends with . so it's not |
|
338 |
+ ; a type separator |
|
339 |
+ (values root tail tail nil) |
|
340 |
+ else ; have all pieces |
|
341 |
+ (values root tail |
|
342 |
+ (subseq tail 0 pos) |
|
343 |
+ (subseq tail (1+ pos))))))) |
|
344 |
+ |
|
345 |
+ |
|
346 |
+; we can specify either an exact url or one that handles all |
|
347 |
+; urls with a common prefix. |
|
348 |
+;; |
|
349 |
+;; if the prefix is given as a list: e.g. ("ReadMe") then it says that |
|
350 |
+;; this mime type applie to file named ReadMe. Note that file types |
|
351 |
+;; are checked first and if no match then a filename match is done. |
|
352 |
+; |
|
353 |
+(defparameter *file-type-to-mime-type* |
|
354 |
+ ;; this list constructed by generate-mime-table in parse.cl |
|
355 |
+ '(("application/EDI-Consent") ("application/EDI-X12") ("application/EDIFACT") |
|
356 |
+ ("application/activemessage") ("application/andrew-inset" "ez") |
|
357 |
+ ("application/applefile") ("application/atomicmail") |
|
358 |
+ ("application/batch-SMTP") ("application/beep+xml") ("application/cals-1840") |
|
359 |
+ ("application/commonground") ("application/cybercash") |
|
360 |
+ ("application/dca-rft") ("application/dec-dx") ("application/dvcs") |
|
361 |
+ ("application/eshop") ("application/http") ("application/hyperstudio") |
|
362 |
+ ("application/iges") ("application/index") ("application/index.cmd") |
|
363 |
+ ("application/index.obj") ("application/index.response") |
|
364 |
+ ("application/index.vnd") ("application/iotp") ("application/ipp") |
|
365 |
+ ("application/isup") ("application/font-tdpfr") |
|
366 |
+ ("application/mac-binhex40" "hqx") ("application/mac-compactpro" "cpt") |
|
367 |
+ ("application/macwriteii") ("application/marc") ("application/mathematica") |
|
368 |
+ ("application/mathematica-old") ("application/msword" "doc") |
|
369 |
+ ("application/news-message-id") ("application/news-transmission") |
|
370 |
+ ("application/ocsp-request") ("application/ocsp-response") |
|
371 |
+ ("application/octet-stream" "bin" "dms" "lha" "lzh" "exe" "class" "so" "dll" |
|
372 |
+ "img" "iso") |
|
373 |
+ ("application/ogg" "ogg") ("application/parityfec") ("application/pdf" "pdf") |
|
374 |
+ ("application/pgp-encrypted") ("application/pgp-keys") |
|
375 |
+ ("application/pgp-signature") ("application/pkcs10") |
|
376 |
+ ("application/pkcs7-mime") ("application/pkcs7-signature") |
|
377 |
+ ("application/pkix-cert") ("application/pkix-crl") ("application/pkixcmp") |
|
378 |
+ ("application/postscript" "ai" "eps" "ps") |
|
379 |
+ ("application/prs.alvestrand.titrax-sheet") ("application/prs.cww") |
|
380 |
+ ("application/prs.nprend") ("application/qsig") |
|
381 |
+ ("application/remote-printing") ("application/riscos") |
|
382 |
+ ("application/rtf" "rtf") ("application/sdp") ("application/set-payment") |
|
383 |
+ ("application/set-payment-initiation") ("application/set-registration") |
|
384 |
+ ("application/set-registration-initiation") ("application/sgml") |
|
385 |
+ ("application/sgml-open-catalog") ("application/sieve") ("application/slate") |
|
386 |
+ ("application/smil" "smi" "smil") ("application/timestamp-query") |
|
387 |
+ ("application/timestamp-reply") ("application/vemmi") |
|
388 |
+ ("application/vnd.3M.Post-it-Notes") ("application/vnd.FloGraphIt") |
|
389 |
+ ("application/vnd.accpac.simply.aso") ("application/vnd.accpac.simply.imp") |
|
390 |
+ ("application/vnd.acucobol") ("application/vnd.aether.imp") |
|
391 |
+ ("application/vnd.anser-web-certificate-issue-initiation") |
|
392 |
+ ("application/vnd.anser-web-funds-transfer-initiation") |
|
393 |
+ ("application/vnd.audiograph") ("application/vnd.businessobjects") |
|
394 |
+ ("application/vnd.bmi") ("application/vnd.canon-cpdl") |
|
395 |
+ ("application/vnd.canon-lips") ("application/vnd.claymore") |
|
396 |
+ ("application/vnd.commerce-battelle") ("application/vnd.commonspace") |
|
397 |
+ ("application/vnd.comsocaller") ("application/vnd.contact.cmsg") |
|
398 |
+ ("application/vnd.cosmocaller") ("application/vnd.cups-postscript") |
|
399 |
+ ("application/vnd.cups-raster") ("application/vnd.cups-raw") |
|
400 |
+ ("application/vnd.ctc-posml") ("application/vnd.cybank") |
|
401 |
+ ("application/vnd.dna") ("application/vnd.dpgraph") ("application/vnd.dxr") |
|
402 |
+ ("application/vnd.ecdis-update") ("application/vnd.ecowin.chart") |
|
403 |
+ ("application/vnd.ecowin.filerequest") ("application/vnd.ecowin.fileupdate") |
|
404 |
+ ("application/vnd.ecowin.series") ("application/vnd.ecowin.seriesrequest") |
|
405 |
+ ("application/vnd.ecowin.seriesupdate") ("application/vnd.enliven") |
|
406 |
+ ("application/vnd.epson.esf") ("application/vnd.epson.msf") |
|
407 |
+ ("application/vnd.epson.quickanime") ("application/vnd.epson.salt") |
|
408 |
+ ("application/vnd.epson.ssf") ("application/vnd.ericsson.quickcall") |
|
409 |
+ ("application/vnd.eudora.data") ("application/vnd.fdf") |
|
410 |
+ ("application/vnd.ffsns") ("application/vnd.framemaker") |
|
411 |
+ ("application/vnd.fsc.weblaunch") ("application/vnd.fujitsu.oasys") |
|
412 |
+ ("application/vnd.fujitsu.oasys2") ("application/vnd.fujitsu.oasys3") |
|
413 |
+ ("application/vnd.fujitsu.oasysgp") ("application/vnd.fujitsu.oasysprs") |
|
414 |
+ ("application/vnd.fujixerox.ddd") ("application/vnd.fujixerox.docuworks") |
|
415 |
+ ("application/vnd.fujixerox.docuworks.binder") ("application/vnd.fut-misnet") |
|
416 |
+ ("application/vnd.grafeq") ("application/vnd.groove-account") |
|
417 |
+ ("application/vnd.groove-identity-message") |
|
418 |
+ ("application/vnd.groove-injector") ("application/vnd.groove-tool-message") |
|
419 |
+ ("application/vnd.groove-tool-template") ("application/vnd.groove-vcard") |
|
420 |
+ ("application/vnd.hhe.lesson-player") ("application/vnd.hp-HPGL") |
|
421 |
+ ("application/vnd.hp-PCL") ("application/vnd.hp-PCLXL") |
|
422 |
+ ("application/vnd.hp-hpid") ("application/vnd.hp-hps") |
|
423 |
+ ("application/vnd.httphone") ("application/vnd.hzn-3d-crossword") |
|
424 |
+ ("application/vnd.ibm.afplinedata") ("application/vnd.ibm.MiniPay") |
|
425 |
+ ("application/vnd.ibm.modcap") ("application/vnd.informix-visionary") |
|
426 |
+ ("application/vnd.intercon.formnet") ("application/vnd.intertrust.digibox") |
|
427 |
+ ("application/vnd.intertrust.nncp") ("application/vnd.intu.qbo") |
|
428 |
+ ("application/vnd.intu.qfx") ("application/vnd.irepository.package+xml") |
|
429 |
+ ("application/vnd.is-xpr") ("application/vnd.japannet-directory-service") |
|
430 |
+ ("application/vnd.japannet-jpnstore-wakeup") |
|
431 |
+ ("application/vnd.japannet-payment-wakeup") |
|
432 |
+ ("application/vnd.japannet-registration") |
|
433 |
+ ("application/vnd.japannet-registration-wakeup") |
|
434 |
+ ("application/vnd.japannet-setstore-wakeup") |
|
435 |
+ ("application/vnd.japannet-verification") |
|
436 |
+ ("application/vnd.japannet-verification-wakeup") ("application/vnd.koan") |
|
437 |
+ ("application/vnd.lotus-1-2-3") ("application/vnd.lotus-approach") |
|
438 |
+ ("application/vnd.lotus-freelance") ("application/vnd.lotus-notes") |
|
439 |
+ ("application/vnd.lotus-organizer") ("application/vnd.lotus-screencam") |
|
440 |
+ ("application/vnd.lotus-wordpro") ("application/vnd.mcd") |
|
441 |
+ ("application/vnd.mediastation.cdkey") ("application/vnd.meridian-slingshot") |
|
442 |
+ ("application/vnd.mif" "mif") ("application/vnd.minisoft-hp3000-save") |
|
443 |
+ ("application/vnd.mitsubishi.misty-guard.trustweb") |
|
444 |
+ ("application/vnd.mobius.daf") ("application/vnd.mobius.dis") |
|
445 |
+ ("application/vnd.mobius.msl") ("application/vnd.mobius.plc") |
|
446 |
+ ("application/vnd.mobius.txf") ("application/vnd.motorola.flexsuite") |
|
447 |
+ ("application/vnd.motorola.flexsuite.adsi") |
|
448 |
+ ("application/vnd.motorola.flexsuite.fis") |
|
449 |
+ ("application/vnd.motorola.flexsuite.gotap") |
|
450 |
+ ("application/vnd.motorola.flexsuite.kmr") |
|
451 |
+ ("application/vnd.motorola.flexsuite.ttc") |
|
452 |
+ ("application/vnd.motorola.flexsuite.wem") |
|
453 |
+ ("application/vnd.mozilla.xul+xml") ("application/vnd.ms-artgalry") |
|
454 |
+ ("application/vnd.ms-asf") ("application/vnd.ms-excel" "xls") |
|
455 |
+ ("application/vnd.ms-lrm") ("application/vnd.ms-powerpoint" "ppt") |
|
456 |
+ ("application/vnd.ms-project") ("application/vnd.ms-tnef") |
|
457 |
+ ("application/vnd.ms-works") ("application/vnd.mseq") |
|
458 |
+ ("application/vnd.msign") ("application/vnd.music-niff") |
|
459 |
+ ("application/vnd.musician") ("application/vnd.netfpx") |
|
460 |
+ ("application/vnd.noblenet-directory") ("application/vnd.noblenet-sealer") |
|
461 |
+ ("application/vnd.noblenet-web") ("application/vnd.novadigm.EDM") |
|
462 |
+ ("application/vnd.novadigm.EDX") ("application/vnd.novadigm.EXT") |
|
463 |
+ ("application/vnd.osa.netdeploy") ("application/vnd.palm") |
|
464 |
+ ("application/vnd.pg.format") ("application/vnd.pg.osasli") |
|
465 |
+ ("application/vnd.powerbuilder6") ("application/vnd.powerbuilder6-s") |
|
466 |
+ ("application/vnd.powerbuilder7") ("application/vnd.powerbuilder7-s") |
|
467 |
+ ("application/vnd.powerbuilder75") ("application/vnd.powerbuilder75-s") |
|
468 |
+ ("application/vnd.previewsystems.box") |
|
469 |
+ ("application/vnd.publishare-delta-tree") ("application/vnd.pvi.ptid1") |
|
470 |
+ ("application/vnd.pwg-xhtml-print+xml") ("application/vnd.rapid") |
|
471 |
+ ("application/vnd.s3sms") ("application/vnd.seemail") |
|
472 |
+ ("application/vnd.shana.informed.formdata") |
|
473 |
+ ("application/vnd.shana.informed.formtemplate") |
|
474 |
+ ("application/vnd.shana.informed.interchange") |
|
475 |
+ ("application/vnd.shana.informed.package") ("application/vnd.sss-cod") |
|
476 |
+ ("application/vnd.sss-dtf") ("application/vnd.sss-ntf") |
|
477 |
+ ("application/vnd.sun.xml.writer" "sxw") |
|
478 |
+ ("application/vnd.sun.xml.writer.template" "stw") |
|
479 |
+ ("application/vnd.sun.xml.calc" "sxc") |
|
480 |
+ ("application/vnd.sun.xml.calc.template" "stc") |
|
481 |
+ ("application/vnd.sun.xml.draw" "sxd") |
|
482 |
+ ("application/vnd.sun.xml.draw.template" "std") |
|
483 |
+ ("application/vnd.sun.xml.impress" "sxi") |
|
484 |
+ ("application/vnd.sun.xml.impress.template" "sti") |
|
485 |
+ ("application/vnd.sun.xml.writer.global" "sxg") |
|
486 |
+ ("application/vnd.sun.xml.math" "sxm") ("application/vnd.street-stream") |
|
487 |
+ ("application/vnd.svd") ("application/vnd.swiftview-ics") |
|
488 |
+ ("application/vnd.triscape.mxs") ("application/vnd.trueapp") |
|
489 |
+ ("application/vnd.truedoc") ("application/vnd.tve-trigger") |
|
490 |
+ ("application/vnd.ufdl") ("application/vnd.uplanet.alert") |
|
491 |
+ ("application/vnd.uplanet.alert-wbxml") |
|
492 |
+ ("application/vnd.uplanet.bearer-choice-wbxml") |
|
493 |
+ ("application/vnd.uplanet.bearer-choice") ("application/vnd.uplanet.cacheop") |
|
494 |
+ ("application/vnd.uplanet.cacheop-wbxml") ("application/vnd.uplanet.channel") |
|
495 |
+ ("application/vnd.uplanet.channel-wbxml") ("application/vnd.uplanet.list") |
|
496 |
+ ("application/vnd.uplanet.list-wbxml") ("application/vnd.uplanet.listcmd") |
|
497 |
+ ("application/vnd.uplanet.listcmd-wbxml") ("application/vnd.uplanet.signal") |
|
498 |
+ ("application/vnd.vcx") ("application/vnd.vectorworks") |
|
499 |
+ ("application/vnd.vidsoft.vidconference") ("application/vnd.visio") |
|
500 |
+ ("application/vnd.vividence.scriptfile") ("application/vnd.wap.sic") |
|
501 |
+ ("application/vnd.wap.slc") ("application/vnd.wap.wbxml" "wbxml") |
|
502 |
+ ("application/vnd.wap.wmlc" "wmlc") |
|
503 |
+ ("application/vnd.wap.wmlscriptc" "wmlsc") ("application/vnd.webturbo") |
|
504 |
+ ("application/vnd.wrq-hp3000-labelled") ("application/vnd.wt.stf") |
|
505 |
+ ("application/vnd.xara") ("application/vnd.xfdl") |
|
506 |
+ ("application/vnd.yellowriver-custom-menu") ("application/whoispp-query") |
|
507 |
+ ("application/whoispp-response") ("application/wita") |
|
508 |
+ ("application/wordperfect5.1") ("application/x-bcpio" "bcpio") |
|
509 |
+ ("application/x-bittorrent" "torrent") ("application/x-bzip2" "bz2") |
|
510 |
+ ("application/x-cdlink" "vcd") ("application/x-chess-pgn" "pgn") |
|
511 |
+ ("application/x-compress") ("application/x-cpio" "cpio") |
|
512 |
+ ("application/x-csh" "csh") ("application/x-director" "dcr" "dir" "dxr") |
|
513 |
+ ("application/x-dvi" "dvi") ("application/x-futuresplash" "spl") |
|
514 |
+ ("application/x-gtar" "gtar") ("application/x-gzip" "gz" "tgz") |
|
515 |
+ ("application/x-hdf" "hdf") ("application/x-javascript" "js") |
|
516 |
+ ("application/x-kword" "kwd" "kwt") ("application/x-kspread" "ksp") |
|
517 |
+ ("application/x-kpresenter" "kpr" "kpt") ("application/x-kchart" "chrt") |
|
518 |
+ ("application/x-killustrator" "kil") |
|
519 |
+ ("application/x-koan" "skp" "skd" "skt" "skm") |
|
520 |
+ ("application/x-latex" "latex") ("application/x-netcdf" "nc" "cdf") |
|
521 |
+ ("application/x-rpm" "rpm") ("application/x-sh" "sh") |
|
522 |
+ ("application/x-shar" "shar") ("application/x-shockwave-flash" "swf") |
|
523 |
+ ("application/x-stuffit" "sit") ("application/x-sv4cpio" "sv4cpio") |
|
524 |
+ ("application/x-sv4crc" "sv4crc") ("application/x-tar" "tar") |
|
525 |
+ ("application/x-tcl" "tcl") ("application/x-tex" "tex") |
|
526 |
+ ("application/x-texinfo" "texinfo" "texi") |
|
527 |
+ ("application/x-troff" "t" "tr" "roff") ("application/x-troff-man" "man") |
|
528 |
+ ("application/x-troff-me" "me") ("application/x-troff-ms" "ms") |
|
529 |
+ ("application/x-ustar" "ustar") ("application/x-wais-source" "src") |
|
530 |
+ ("application/x400-bp") ("application/xhtml+xml" "xhtml" "xht") |
|
531 |
+ ("application/xml") ("application/xml-dtd") |
|
532 |
+ ("application/xml-external-parsed-entity") ("application/zip" "zip") |
|
533 |
+ ("audio/32kadpcm") ("audio/basic" "au" "snd") ("audio/g.722.1") ("audio/l16") |
|
534 |
+ ("audio/midi" "mid" "midi" "kar") ("audio/mp4a-latm") ("audio/mpa-robust") |
|
535 |
+ ("audio/mpeg" "mpga" "mp2" "mp3") ("audio/parityfec") ("audio/prs.sid") |
|
536 |
+ ("audio/telephone-event") ("audio/tone") ("audio/vnd.cisco.nse") |
|
537 |
+ ("audio/vnd.cns.anp1") ("audio/vnd.cns.inf1") ("audio/vnd.digital-winds") |
|
538 |
+ ("audio/vnd.everad.plj") ("audio/vnd.lucent.voice") ("audio/vnd.nortel.vbk") |
|
539 |
+ ("audio/vnd.nuera.ecelp4800") ("audio/vnd.nuera.ecelp7470") |
|
540 |
+ ("audio/vnd.nuera.ecelp9600") ("audio/vnd.octel.sbc") ("audio/vnd.qcelp") |
|
541 |
+ ("audio/vnd.rhetorex.32kadpcm") ("audio/vnd.vmx.cvsd") |
|
542 |
+ ("audio/x-aiff" "aif" "aiff" "aifc") ("audio/x-mpegurl" "m3u") |
|
543 |
+ ("audio/x-pn-realaudio" "ram" "rm") ("audio/x-realaudio" "ra") |
|
544 |
+ ("audio/x-wav" "wav") ("chemical/x-pdb" "pdb") ("chemical/x-xyz" "xyz") |
|
545 |
+ ("image/bmp" "bmp") ("image/cgm") ("image/g3fax") ("image/gif" "gif") |
|
546 |
+ ("image/ief" "ief") ("image/jpeg" "jpeg" "jpg" "jpe") ("image/naplps") |
|
547 |
+ ("image/png" "png") ("image/prs.btif") ("image/prs.pti") |
|
548 |
+ ("image/tiff" "tiff" "tif") ("image/vnd.cns.inf2") |
|
549 |
+ ("image/vnd.djvu" "djvu" "djv") ("image/vnd.dwg") ("image/vnd.dxf") |
|
550 |
+ ("image/vnd.fastbidsheet") ("image/vnd.fpx") ("image/vnd.fst") |
|
551 |
+ ("image/vnd.fujixerox.edmics-mmr") ("image/vnd.fujixerox.edmics-rlc") |
|
552 |
+ ("image/vnd.mix") ("image/vnd.net-fpx") ("image/vnd.svf") |
|
553 |
+ ("image/vnd.wap.wbmp" "wbmp") ("image/vnd.xiff") ("image/x-cmu-raster" "ras") |
|
554 |
+ ("image/x-portable-anymap" "pnm") ("image/x-portable-bitmap" "pbm") |
|
555 |
+ ("image/x-portable-graymap" "pgm") ("image/x-portable-pixmap" "ppm") |
|
556 |
+ ("image/x-rgb" "rgb") ("image/x-xbitmap" "xbm") ("image/x-xpixmap" "xpm") |
|
557 |
+ ("image/x-xwindowdump" "xwd") ("message/delivery-status") |
|
558 |
+ ("message/disposition-notification") ("message/external-body") |
|
559 |
+ ("message/http") ("message/news") ("message/partial") ("message/rfc822") |
|
560 |
+ ("message/s-http") ("model/iges" "igs" "iges") |
|
561 |
+ ("model/mesh" "msh" "mesh" "silo") ("model/vnd.dwf") |
|
562 |
+ ("model/vnd.flatland.3dml") ("model/vnd.gdl") ("model/vnd.gs-gdl") |
|
563 |
+ ("model/vnd.gtw") ("model/vnd.mts") ("model/vnd.vtu") |
|
564 |
+ ("model/vrml" "wrl" "vrml") ("multipart/alternative") |
|
565 |
+ ("multipart/appledouble") ("multipart/byteranges") ("multipart/digest") |
|
566 |
+ ("multipart/encrypted") ("multipart/form-data") ("multipart/header-set") |
|
567 |
+ ("multipart/mixed") ("multipart/parallel") ("multipart/related") |
|
568 |
+ ("multipart/report") ("multipart/signed") ("multipart/voice-message") |
|
569 |
+ ("text/calendar") ("text/css" "css") ("text/directory") ("text/enriched") |
|
570 |
+ ("text/html" "html" "htm") ("text/parityfec") ("text/plain" "asc" "txt") |
|
571 |
+ ("text/prs.lines.tag") ("text/rfc822-headers") ("text/richtext" "rtx") |
|
572 |
+ ("text/rtf" "rtf") ("text/sgml" "sgml" "sgm") |
|
573 |
+ ("text/tab-separated-values" "tsv") ("text/t140") ("text/uri-list") |
|
574 |
+ ("text/vnd.DMClientScript") ("text/vnd.IPTC.NITF") ("text/vnd.IPTC.NewsML") |
|
575 |
+ ("text/vnd.abc") ("text/vnd.curl") ("text/vnd.flatland.3dml") |
|
576 |
+ ("text/vnd.fly") ("text/vnd.fmi.flexstor") ("text/vnd.in3d.3dml") |
|
577 |
+ ("text/vnd.in3d.spot") ("text/vnd.latex-z") ("text/vnd.motorola.reflex") |
|
578 |
+ ("text/vnd.ms-mediapackage") ("text/vnd.wap.si") ("text/vnd.wap.sl") |
|
579 |
+ ("text/vnd.wap.wml" "wml") ("text/vnd.wap.wmlscript" "wmls") |
|
580 |
+ ("text/x-setext" "etx") ("text/xml" "xml" "xsl") |
|
581 |
+ ("text/xml-external-parsed-entity") ("video/mp4v-es") |
|
582 |
+ ("video/mpeg" "mpeg" "mpg" "mpe") ("video/parityfec") ("video/pointer") |
|
583 |
+ ("video/quicktime" "qt" "mov") ("video/vnd.fvt") ("video/vnd.motorola.video") |
|
584 |
+ ("video/vnd.motorola.videop") ("video/vnd.mpegurl" "mxu") ("video/vnd.mts") |
|
585 |
+ ("video/vnd.nokia.interleaved-multimedia") ("video/vnd.vivo") |
|
586 |
+ ("video/x-msvideo" "avi") ("video/x-sgi-movie" "movie") |
|
587 |
+ ("x-conference/x-cooltalk" "ice"))) |
|
588 |
+ |
|
589 |
+(defvar *mime-types* nil) |
|
590 |
+ |
|
591 |
+(defun build-mime-types-table () |
|
592 |
+ (if* (null *mime-types*) |
|
593 |
+ then (setf *mime-types* (make-hash-table :test #'equalp)) |
|
594 |
+ (dolist (ent *file-type-to-mime-type*) |
|
595 |
+ (dolist (type (cdr ent)) |
|
596 |
+ (setf (gethash type *mime-types*) (car ent)))))) |
|
597 |
+ |
|
598 |
+ |
|
599 |
+(build-mime-types-table) ;; build the table now |
|
600 |
+ |
|
601 |
+;; return mime type if known |
|
602 |
+(defmethod lookup-mime-type (filename) |
|
603 |
+ (if* (pathnamep filename) |
|
604 |
+ then (setq filename (namestring filename))) |
|
605 |
+ (multiple-value-bind (root tail name type) |
|
606 |
+ (split-namestring filename) |
|
607 |
+ (declare (ignore root name)) |
|
608 |
+ (if* (and type (gethash type *mime-types*)) |
|
609 |
+ thenret |
|
610 |
+ elseif (gethash (list tail) *mime-types*) |
|
611 |
+ thenret))) |
... | ... |
@@ -24,7 +24,7 @@ |
24 | 24 |
;; Suite 330, Boston, MA 02111-1307 USA |
25 | 25 |
;; |
26 | 26 |
;; |
27 |
-;; $Id: smtp.cl,v 1.9 2005/08/03 05:17:29 layer Exp $ |
|
27 |
+;; $Id: smtp.cl,v 1.10 2006/01/26 23:53:27 dancy Exp $ |
|
28 | 28 |
|
29 | 29 |
;; Description: |
30 | 30 |
;; send mail to an smtp server. See rfc821 for the spec. |
... | ... |
@@ -57,7 +57,7 @@ |
57 | 57 |
;; or the final destination). "from" is the address to be given |
58 | 58 |
;; as the sender. "to" can be a string or a list of strings naming |
59 | 59 |
;; recipients. |
60 |
-;; "message" is the message to be sent |
|
60 |
+;; "message" is the message to be sent. It can be a string or a stream. |
|
61 | 61 |
;; cc and bcc can be either be a string or a list of strings |
62 | 62 |
;; naming recipients. All cc's and bcc's are sent the message |
63 | 63 |
;; but the bcc's aren't included in the header created. |
... | ... |
@@ -73,7 +73,7 @@ |
73 | 73 |
;; this is like send-letter except that it doesn't build a header. |
74 | 74 |
;; the messages should contain a header (and if not then sendmail |
75 | 75 |
;; notices this and builds one -- other MTAs may not be that smart). |
76 |
-;; The messages ia list of strings to be concatenated together |
|
76 |
+;; The messages ia list of strings or streams to be concatenated together |
|
77 | 77 |
;; and sent as one message |
78 | 78 |
;; |
79 | 79 |
;; |
... | ... |
@@ -113,11 +113,30 @@ |
113 | 113 |
|
114 | 114 |
(defun send-letter (server from to message |
115 | 115 |
&key cc bcc subject reply-to headers |
116 |
- login password) |
|
116 |
+ login password attachments) |
|
117 | 117 |
;; |
118 | 118 |
;; see documentation at the head of this file |
119 | 119 |
;; |
120 |
- (let ((header (make-string-output-stream)) |
|
120 |
+ |
|
121 |
+ (if* (mime-part-constructed-p message) |
|
122 |
+ then (if* (and (not (multipart-mixed-p message)) attachments) |
|
123 |
+ then (error "~ |
|
124 |
+attachments are not allowed for non-multipart/mixed messages.")) |
|
125 |
+ else (let ((part |
|
126 |
+ (if* (streamp message) |
|
127 |
+ then |
|
128 |
+ (make-mime-part :file message) |
|
129 |
+ elseif (stringp message) |
|
130 |
+ then (make-mime-part :text message) |
|
131 |
+ else (error "~ |
|
132 |
+message must be a string, stream, or mime-part-constructed, not ~s" message)))) |
|
133 |
+ |
|
134 |
+ (setf message |
|
135 |
+ (if* attachments |
|
136 |
+ then (make-mime-part :subparts (list part)) |
|
137 |
+ else part)))) |
|
138 |
+ |
|
139 |
+ (let ((user-header "") |
|
121 | 140 |
(tos (if* (stringp to) |
122 | 141 |
then (list to) |
123 | 142 |
elseif (consp to) |
... | ... |
@@ -138,19 +157,21 @@ |
138 | 157 |
elseif (consp bcc) |
139 | 158 |
then bcc |
140 | 159 |
else (error "bcc should be a string or list, not ~s" bcc)))) |
141 |
- (format header "From: ~a~c~cTo: " |
|
142 |
- from |
|
143 |
- #\return |
|
144 |
- #\linefeed) |
|
145 |
- (format header "~{ ~a~^,~}~c~c" tos #\return #\linefeed) |
|
160 |
+ |
|
161 |
+ (push (cons "From" from) (mime-part-headers message)) |
|
162 |
+ (push (cons "To" (list-to-delimited-string tos ", ")) |
|
163 |
+ (mime-part-headers message)) |
|
164 |
+ |
|
146 | 165 |
(if* ccs |
147 |
- then (format header "Cc: ~{ ~a~^,~}~c~c" ccs #\return #\linefeed)) |
|
166 |
+ then |
|
167 |
+ (push (cons "Cc" (list-to-delimited-string ccs ", ")) |
|
168 |
+ (mime-part-headers message))) |
|
148 | 169 |
|
149 | 170 |
(if* subject |
150 |
- then (format header "Subject: ~a~c~c" subject #\return #\linefeed)) |
|
171 |
+ then (push (cons "Subject" subject) (mime-part-headers message))) |
|
151 | 172 |
|
152 | 173 |
(if* reply-to |
153 |
- then (format header "Reply-To: ~a~c~c" reply-to #\return #\linefeed)) |
|
174 |
+ then (push (cons "Reply-To" reply-to) (mime-part-headers message))) |
|
154 | 175 |
|
155 | 176 |
(if* headers |
156 | 177 |
then (if* (stringp headers) |
... | ... |
@@ -158,27 +179,43 @@ |
158 | 179 |
elseif (consp headers) |
159 | 180 |
thenret |
160 | 181 |
else (error "Unknown headers format: ~s." headers)) |
161 |
- (dolist (h headers) |
|
162 |
- (format header "~a~c~c" h #\return #\linefeed))) |
|
163 |
- |
|
164 |
- (format header "~c~c" #\return #\linefeed) |
|
182 |
+ (setf user-header |
|
183 |
+ (with-output-to-string (header) |
|
184 |
+ (dolist (h headers) |
|
185 |
+ (format header "~a~%" h))))) |
|
186 |
+ |
|
187 |
+ (if* attachments |
|
188 |
+ then (if (not (consp attachments)) |
|
189 |
+ (setf attachments (list attachments))) |
|
190 |
+ |
|
191 |
+ (dolist (attachment attachments) |
|
192 |
+ (if* (mime-part-constructed-p attachment) |
|
193 |
+ thenret |
|
194 |
+ elseif (or (streamp attachment) (stringp attachment) |
|
195 |
+ (pathnamep attachment)) |
|
196 |
+ then (setf attachment (make-mime-part :file attachment)) |
|
197 |
+ else (error "~ |
|
198 |
+Attachments must be filenames, streams, or mime-part-constructed, not ~s" |
|
199 |
+ attachment)) |
|
200 |
+ (nconc (mime-part-parts message) (list attachment)))) |
|
165 | 201 |
|
166 |
- (send-smtp-auth server from (append tos ccs bccs) |
|
167 |
- login password |
|
168 |
- (get-output-stream-string header) |
|
169 |
- message))) |
|
202 |
+ (with-mime-part-constructed-stream (s message) |
|
203 |
+ (send-smtp-auth server from (append tos ccs bccs) |
|
204 |
+ login password |
|
205 |
+ user-header |
|
206 |
+ s)))) |
|
170 | 207 |
|
171 | 208 |
|
172 |
-(defun send-smtp(server from to &rest messages) |
|
209 |
+(defun send-smtp (server from to &rest messages) |
|
173 | 210 |
(send-smtp-1 server from to nil nil messages)) |
174 | 211 |
|
175 | 212 |
(defun send-smtp-auth (server from to login password &rest messages) |
176 | 213 |
(send-smtp-1 server from to login password messages)) |
177 |
- |
|
214 |
+ |
|
178 | 215 |
(defun send-smtp-1 (server from to login password messages) |
179 | 216 |
;; send the effective concatenation of the messages via |
180 | 217 |
;; smtp to the mail server |
181 |
- ;; Each message should be a string |
|
218 |
+ ;; Each message should be a string or a stream. |
|
182 | 219 |
;; |
183 | 220 |
;; 'to' can be a single string or a list of strings. |
184 | 221 |
;; each string should be in the official rfc822 format "foo@bar.com" |
... | ... |
@@ -188,7 +225,7 @@ |
188 | 225 |
|
189 | 226 |
(unwind-protect |
190 | 227 |
(progn |
191 |
- |
|
228 |
+ |
|
192 | 229 |
(smtp-command sock "MAIL from:<~a>" from) |
193 | 230 |
(response-case (sock msg) |
194 | 231 |
(2 ;; cool |
... | ... |
@@ -219,21 +256,29 @@ |
219 | 256 |
|
220 | 257 |
|
221 | 258 |
(let ((at-bol t) |
222 |
- (prev-ch nil)) |
|
259 |
+ (prev-ch nil) |
|
260 |
+ ch stream) |
|
223 | 261 |
(dolist (message messages) |
224 |
- (dotimes (i (length message)) |
|
225 |
- (let ((ch (aref message i))) |
|
226 |
- (if* (and at-bol (eq ch #\.)) |
|
227 |
- then ; to prevent . from being interpreted as eol |
|
228 |
- (write-char #\. sock)) |
|
229 |
- (if* (eq ch #\newline) |
|
230 |
- then (setq at-bol t) |
|
231 |
- (if* (not (eq prev-ch #\return)) |
|
232 |
- then (write-char #\return sock)) |
|
233 |
- else (setq at-bol nil)) |
|
234 |
- (write-char ch sock) |
|
235 |
- (setq prev-ch ch))))) |
|
236 |
- |
|
262 |
+ (setf stream (if* (streamp message) |
|
263 |
+ then message |
|
264 |
+ else (make-string-input-stream message))) |
|
265 |
+ (unwind-protect |
|
266 |
+ (progn |
|
267 |
+ (while (setf ch (read-char stream nil nil)) |
|
268 |
+ (if* (and at-bol (eq ch #\.)) |
|
269 |
+ then ;; to prevent . from being interpreted as eol |
|
270 |
+ (write-char #\. sock)) |
|
271 |
+ (if* (eq ch #\newline) |
|
272 |
+ then (setq at-bol t) |
|
273 |
+ (if* (not (eq prev-ch #\return)) |
|
274 |
+ then (write-char #\return sock)) |
|
275 |
+ else (setq at-bol nil)) |
|
276 |
+ (write-char ch sock) |
|
277 |
+ (setq prev-ch ch))) |
|
278 |
+ ;; unwind-protect |
|
279 |
+ (if* (not (streamp message)) |
|
280 |
+ then (close stream))))) |
|
281 |
+ |
|
237 | 282 |
(write-char #\return sock) (write-char #\linefeed sock) |
238 | 283 |
(write-char #\. sock) |
239 | 284 |
(write-char #\return sock) (write-char #\linefeed sock) |