1 | ;;; |
---|
2 | ;;; mime.scm - parsing MIME (rfc2045) message |
---|
3 | ;;; |
---|
4 | ;;; Copyright (c) 2000-2004 Shiro Kawai, All rights reserved. |
---|
5 | ;;; |
---|
6 | ;;; Redistribution and use in source and binary forms, with or without |
---|
7 | ;;; modification, are permitted provided that the following conditions |
---|
8 | ;;; are met: |
---|
9 | ;;; |
---|
10 | ;;; 1. Redistributions of source code must retain the above copyright |
---|
11 | ;;; notice, this list of conditions and the following disclaimer. |
---|
12 | ;;; |
---|
13 | ;;; 2. Redistributions in binary form must reproduce the above copyright |
---|
14 | ;;; notice, this list of conditions and the following disclaimer in the |
---|
15 | ;;; documentation and/or other materials provided with the distribution. |
---|
16 | ;;; |
---|
17 | ;;; 3. Neither the name of the authors nor the names of its contributors |
---|
18 | ;;; may be used to endorse or promote products derived from this |
---|
19 | ;;; software without specific prior written permission. |
---|
20 | ;;; |
---|
21 | ;;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
---|
22 | ;;; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
---|
23 | ;;; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
---|
24 | ;;; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
---|
25 | ;;; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
---|
26 | ;;; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED |
---|
27 | ;;; TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
---|
28 | ;;; PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
---|
29 | ;;; LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
---|
30 | ;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
---|
31 | ;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
---|
32 | ;;; |
---|
33 | ;;; $Id: mime.scm,v 1.11 2004/11/25 22:00:43 shirok Exp $ |
---|
34 | ;;; |
---|
35 | ;;; Ported to Chicken by Hans Bulfone <jsb@nil.at> |
---|
36 | |
---|
37 | ;; RFC2045 Multipurpose Internet Mail Extensions (MIME) |
---|
38 | ;; Part One: Format of Internet Message Bodies |
---|
39 | ;; RFC2046 Multipurpose Internet Mail Extensions (MIME) |
---|
40 | ;; Part Two: Media Types |
---|
41 | ;; RFC2047 Multipurpose Internet Mail Extensions (MIME) |
---|
42 | ;; Part Three: Message Header Extensions for Non-ASCII Text |
---|
43 | ;; RFC2048 |
---|
44 | ;; RFC2049 |
---|
45 | |
---|
46 | (define-extension mime) |
---|
47 | (declare |
---|
48 | (usual-integrations) |
---|
49 | (uses mime:quoted-printable) |
---|
50 | (export mime-token-chars mime-parse-version mime-get-attributes |
---|
51 | mime-parse-content-type mime-decode-word |
---|
52 | mime-parse-message mime-retrieve-body |
---|
53 | mime-body->string mime-body->file |
---|
54 | make-mime-part mime-part? |
---|
55 | mime-part:type mime-part:subtype mime-part:parameters |
---|
56 | mime-part:transfer-encoding mime-part:parent mime-part:index |
---|
57 | mime-part:headers mime-part:content mime-part:attrs |
---|
58 | mime-part:type-set! mime-part:subtype-set! mime-part:parameters-set! |
---|
59 | mime-part:transfer-encoding-set! mime-part:parent-set! mime-part:index-set! |
---|
60 | mime-part:headers-set! mime-part:content-set! mime-part:attrs-set! |
---|
61 | mime-part-write mime-part->string)) |
---|
62 | (require-extension lolevel regex |
---|
63 | srfi-1 srfi-13 srfi-14 |
---|
64 | rfc822 base64 iconv) |
---|
65 | |
---|
66 | (define-macro (let1 name val . body) |
---|
67 | `(let ((,name ,val)) |
---|
68 | ,@body)) |
---|
69 | |
---|
70 | ;;=============================================================== |
---|
71 | ;; Basic utility |
---|
72 | ;; |
---|
73 | |
---|
74 | (define mime-token-chars |
---|
75 | (char-set-difference |
---|
76 | (ucs-range->char-set #x21 #x7f) |
---|
77 | (char-set #\( #\) #\< #\> #\@ #\, #\; #\: #\\ #\" #\/ #\[ #\] #\? #\=))) |
---|
78 | |
---|
79 | ;; returns list of major and minor versions in integers |
---|
80 | (define mime-parse-version |
---|
81 | (let ((re-version (regexp "^([0-9]+)\\.([0-9]+)$"))) |
---|
82 | (lambda (field) |
---|
83 | (and field |
---|
84 | (let1 s |
---|
85 | (string-concatenate (map ->string (rfc822-field->tokens field))) |
---|
86 | (cond ((string-search re-version s) => |
---|
87 | (lambda (m) (map (lambda (s) (string->number s)) (cdr m)))) |
---|
88 | (else #f))))))) |
---|
89 | |
---|
90 | ;; reads attributes in the form ;attr1=val1;attr2=val2;... from input |
---|
91 | ;; and returns them as alist |
---|
92 | (define (mime-get-attributes input) |
---|
93 | (define cs-quote (char-set #\")) |
---|
94 | (define (get-attributes input r) |
---|
95 | (cond ((and-let* (((eqv? #\; (rfc822-next-token input '()))) |
---|
96 | (attr (rfc822-next-token input `(,mime-token-chars))) |
---|
97 | ((string? attr)) |
---|
98 | ((eqv? #\= (rfc822-next-token input '()))) |
---|
99 | (val (rfc822-next-token |
---|
100 | input |
---|
101 | `(,mime-token-chars |
---|
102 | (,cs-quote . ,rfc822-quoted-string)))) |
---|
103 | ((string? val))) |
---|
104 | (cons attr val)) |
---|
105 | => (lambda (p) (get-attributes input (cons p r)))) |
---|
106 | (else (reverse! r)))) |
---|
107 | (get-attributes input '())) |
---|
108 | |
---|
109 | ;; returns (<type> <subtype> (<attribute> . <value>) ...) |
---|
110 | (define (mime-parse-content-type field) |
---|
111 | (and field |
---|
112 | (call-with-input-string field |
---|
113 | (lambda (input) |
---|
114 | (let* ((type (rfc822-next-token input `(,mime-token-chars))) |
---|
115 | (slash (rfc822-next-token input '())) |
---|
116 | (subtype (rfc822-next-token input `(,mime-token-chars)))) |
---|
117 | (and (string? type) |
---|
118 | (eqv? #\/ slash) |
---|
119 | (string? subtype) |
---|
120 | (cons* (string-downcase type) |
---|
121 | (string-downcase subtype) |
---|
122 | (mime-get-attributes input)))))))) |
---|
123 | |
---|
124 | ;; decode rfc2047-encoded word, i.e. "=?...?=" |
---|
125 | ;; if word isn't legal encoded word, it is returned as is. |
---|
126 | (define mime-decode-word |
---|
127 | (letrec |
---|
128 | ((re-word |
---|
129 | (regexp "^=\\?([-!#-'*+[:alnum:]^-~]+)\\?([-!#-'*+[:alnum:]^-~]+)\\?([!->@-~]+)\\?=$")) |
---|
130 | ; (regexp "^=\\?([^? ]+)\\?([^? ]+)\\?([^? ]+)\\?=$")) |
---|
131 | ;; this is taken from alex shinn's charconv egg: |
---|
132 | (aliases (make-hash-table string=?)) |
---|
133 | (ces-normalize-name |
---|
134 | (lambda (str) |
---|
135 | (let ((str2 (string-upcase str))) |
---|
136 | (hash-table-ref/default aliases str2 str2)))) |
---|
137 | (%ces-upper-compatible? |
---|
138 | (lambda (a b) |
---|
139 | (cond |
---|
140 | ((not b) #t) |
---|
141 | ((string=? b "UTF8") (string=? a "UTF8")) |
---|
142 | ((string=? b "ASCII") (not (member a '("UTF16" "UTF32")))) |
---|
143 | (else #f)))) |
---|
144 | (ces-upper-compatible? |
---|
145 | (lambda (a b) |
---|
146 | (%ces-upper-compatible? (ces-normalize-name a) (ces-normalize-name b)))) |
---|
147 | (ces-convert |
---|
148 | (lambda (str from . o) |
---|
149 | (let ((to (or (and (pair? o) (car o)) "UTF8"))) |
---|
150 | (if (ces-upper-compatible? to from) |
---|
151 | str |
---|
152 | (let ((cd (iconv-open to from))) |
---|
153 | (iconv cd str))))))) |
---|
154 | |
---|
155 | (for-each |
---|
156 | (lambda (ls) |
---|
157 | (for-each |
---|
158 | (cute hash-table-set! aliases <> (car ls)) |
---|
159 | (cdr ls))) |
---|
160 | '(("UTF8" "UTF-8") |
---|
161 | ("ISO-8859-1" "LATIN-1") |
---|
162 | ("SHIFT_JIS" "SJIS" "SHIFTJIS" "SHIFT-JIS") |
---|
163 | ("EUC-JP" "EUCJP" "EUC_JP"))) |
---|
164 | |
---|
165 | (lambda (word) |
---|
166 | (cond |
---|
167 | ((string-search re-word word) |
---|
168 | => (match-lambda |
---|
169 | ((_ charset encoding body) |
---|
170 | (handle-exceptions |
---|
171 | exn |
---|
172 | word ; capture illegal encoding |
---|
173 | (cond |
---|
174 | ((string-ci=? encoding "q") |
---|
175 | (ces-convert |
---|
176 | (quoted-printable-decode-string body) |
---|
177 | charset #f)) |
---|
178 | ((string-ci=? encoding "b") |
---|
179 | (ces-convert |
---|
180 | (base64-decode-string body) |
---|
181 | charset #f)) |
---|
182 | (else word)))))) ; unsupported encoding |
---|
183 | (else word))))) |
---|
184 | |
---|
185 | |
---|
186 | ;;=============================================================== |
---|
187 | ;; Virtual port to recognize mime boundary |
---|
188 | ;; |
---|
189 | |
---|
190 | (define read-byte |
---|
191 | (let ((read-char read-char) |
---|
192 | (char->integer char->integer)) |
---|
193 | (lambda (port) |
---|
194 | (let ((ch (read-char port))) |
---|
195 | (if (eof-object? ch) |
---|
196 | ch |
---|
197 | (char->integer ch)))))) |
---|
198 | |
---|
199 | (define peek-byte |
---|
200 | (let ((peek-char peek-char) |
---|
201 | (char->integer char->integer)) |
---|
202 | (lambda (port) |
---|
203 | (let ((ch (peek-char port))) |
---|
204 | (if (eof-object? ch) |
---|
205 | ch |
---|
206 | (char->integer ch)))))) |
---|
207 | |
---|
208 | ;; Creates a procedural port, which reads from SRCPORT until it reaches |
---|
209 | ;; either EOF or MIME boundary. Basically it runs a DFA. |
---|
210 | (define (make-mime-port boundary srcport) |
---|
211 | (define q (make-queue)) |
---|
212 | (define (enqueue! queue . items) |
---|
213 | (if (null? items) |
---|
214 | (void) |
---|
215 | (begin |
---|
216 | (queue-add! queue (car items)) |
---|
217 | (apply enqueue! queue (cdr items))))) |
---|
218 | (define dequeue! queue-remove!) |
---|
219 | (define --boundary (string->byte-vector (string-append "--" boundary))) |
---|
220 | |
---|
221 | (define state 'prologue) |
---|
222 | |
---|
223 | (define eof #!eof) |
---|
224 | |
---|
225 | (define (deq! q) |
---|
226 | (if (queue-empty? q) eof (dequeue! q))) |
---|
227 | |
---|
228 | (define (fifo! q b) |
---|
229 | (enqueue! q b) (dequeue! q)) |
---|
230 | |
---|
231 | (define (getb) |
---|
232 | (if (queue-empty? q) |
---|
233 | (case state |
---|
234 | ((prologue) (skip-prologue)) |
---|
235 | ((boundary eof) eof) |
---|
236 | (else (newb))) |
---|
237 | (dequeue! q))) |
---|
238 | |
---|
239 | (define (newb) |
---|
240 | (let1 b (read-byte srcport) |
---|
241 | (cond |
---|
242 | ((eof-object? b) |
---|
243 | (set! state 'eof) |
---|
244 | eof) |
---|
245 | (else |
---|
246 | (case b |
---|
247 | ((#x0d) ;; CR, check to see LF |
---|
248 | (let1 b2 (peek-byte srcport) |
---|
249 | (if (eqv? b2 #x0a) |
---|
250 | (begin |
---|
251 | (read-byte srcport) |
---|
252 | (enqueue! q b #x0a) |
---|
253 | (check-boundary)) |
---|
254 | b))) |
---|
255 | ((#x0a) ;; LF, check boundary |
---|
256 | (enqueue! q b) (check-boundary)) |
---|
257 | (else b)))))) |
---|
258 | |
---|
259 | (define (check-boundary) |
---|
260 | (let loop ((b (peek-byte srcport)) |
---|
261 | (ind 0) |
---|
262 | (max (byte-vector-length --boundary))) |
---|
263 | (cond ((eof-object? b) |
---|
264 | (deq! q)) |
---|
265 | ((= ind max) |
---|
266 | (cond ((memv b '(#x0d #x0a)) ;;found boundary |
---|
267 | (read-byte srcport) ;;consume LF or CRLF |
---|
268 | (when (and (eqv? #x0d b) |
---|
269 | (eqv? #x0a (peek-byte srcport))) |
---|
270 | (read-byte srcport)) |
---|
271 | (set! q (make-queue)) |
---|
272 | (set! state 'boundary) |
---|
273 | eof) |
---|
274 | ((eqv? b #x2d) ;; maybe end boundary |
---|
275 | (enqueue! q (read-byte srcport)) |
---|
276 | (cond ((eqv? (peek-byte srcport) #x2d);; yes, end boundary |
---|
277 | (read-byte srcport) |
---|
278 | (set! q (make-queue)) |
---|
279 | (skip-epilogue)) |
---|
280 | (else |
---|
281 | (deq! q)))) |
---|
282 | (else |
---|
283 | (deq! q)))) |
---|
284 | ((= b (byte-vector-ref --boundary ind)) |
---|
285 | (enqueue! q (read-byte srcport)) |
---|
286 | (loop (peek-byte srcport) (+ ind 1) max)) |
---|
287 | ((queue-empty? q) |
---|
288 | (newb)) |
---|
289 | (else |
---|
290 | (dequeue! q))))) |
---|
291 | |
---|
292 | ;; Reads past the first boundary. The first boundary may appear |
---|
293 | ;; at the beginning of the message (instead of after CRLF), so |
---|
294 | ;; we need slightly different handling than the normal getb. |
---|
295 | (define (skip-prologue) |
---|
296 | (let loop ((b (check-boundary))) |
---|
297 | (cond |
---|
298 | ((eof-object? b) |
---|
299 | (cond ((eq? state 'boundary) ; we've found the boundary |
---|
300 | (set! state 'body) |
---|
301 | (getb)) |
---|
302 | (else ; no boundary found |
---|
303 | (set! state 'eof) |
---|
304 | eof))) |
---|
305 | ((queue-empty? q) |
---|
306 | (loop (newb))) |
---|
307 | (else |
---|
308 | (set! q (make-queue)) |
---|
309 | (loop (newb)))))) |
---|
310 | |
---|
311 | (define (skip-epilogue) |
---|
312 | (let loop ((b (read-byte srcport))) |
---|
313 | (if (eof-object? b) |
---|
314 | (begin (set! state 'eof) |
---|
315 | b) |
---|
316 | (loop (read-byte srcport))))) |
---|
317 | |
---|
318 | (values |
---|
319 | (make-input-port |
---|
320 | (lambda () |
---|
321 | (let ((ch (getb))) |
---|
322 | (if (eof-object? ch) |
---|
323 | ch |
---|
324 | (integer->char ch)))) |
---|
325 | (lambda () |
---|
326 | (or (not (queue-empty? q)) |
---|
327 | (char-ready? srcport))) |
---|
328 | void) |
---|
329 | (lambda () state) |
---|
330 | (lambda (s) (set! state s)))) |
---|
331 | |
---|
332 | ;;=============================================================== |
---|
333 | ;; Basic streaming parser |
---|
334 | ;; |
---|
335 | |
---|
336 | (define-record-type :mime-part |
---|
337 | (*make-mime-part type subtype parameters transfer-encoding |
---|
338 | parent index headers content attrs) |
---|
339 | mime-part? |
---|
340 | (type mime-part:type mime-part:type-set!) |
---|
341 | (subtype mime-part:subtype mime-part:subtype-set!) |
---|
342 | (parameters mime-part:parameters mime-part:parameters-set!) |
---|
343 | (transfer-encoding mime-part:transfer-encoding mime-part:transfer-encoding-set!) |
---|
344 | (parent mime-part:parent mime-part:parent-set!) |
---|
345 | (index mime-part:index mime-part:index-set!) |
---|
346 | (headers mime-part:headers mime-part:headers-set!) |
---|
347 | (content mime-part:content mime-part:content-set!) |
---|
348 | (attrs mime-part:attrs mime-part:attrs-set!)) |
---|
349 | |
---|
350 | (define (make-mime-part #!key (type "text") (subtype "plain") |
---|
351 | (parameters '()) |
---|
352 | (transfer-encoding "7bit") |
---|
353 | (parent #f) (index 0) (headers '()) |
---|
354 | (content #f) (attrs '())) |
---|
355 | (*make-mime-part type subtype parameters transfer-encoding |
---|
356 | parent index headers content attrs)) |
---|
357 | |
---|
358 | (define (mime-parse-message port headers handler) |
---|
359 | (internal-parse port headers handler #f 0 |
---|
360 | '("text" "plain" ("charset" . "us-ascii")))) |
---|
361 | |
---|
362 | (define (internal-parse port headers handler parent index default-type) |
---|
363 | (let* ((ctype (or (mime-parse-content-type |
---|
364 | (rfc822-header-ref headers "content-type")) |
---|
365 | default-type)) |
---|
366 | (enc (rfc822-header-ref headers "content-transfer-encoding" "7bit")) |
---|
367 | (packet (*make-mime-part |
---|
368 | (car ctype) (cadr ctype) (cddr ctype) |
---|
369 | enc parent index headers #f '())) |
---|
370 | ) |
---|
371 | (cond |
---|
372 | ((equal? (car ctype) "multipart") |
---|
373 | (multipart-parse port packet handler)) |
---|
374 | ((equal? (car ctype) "message") |
---|
375 | (message-parse port packet handler)) |
---|
376 | (else |
---|
377 | ;; normal body |
---|
378 | (mime-part:content-set! packet (handler packet port)) |
---|
379 | packet) |
---|
380 | ))) |
---|
381 | |
---|
382 | (define (multipart-parse port packet handler) |
---|
383 | (let ((boundary (or (alist-ref "boundary" (mime-part:parameters packet) equal?) |
---|
384 | (error "No boundary given for multipart message"))) |
---|
385 | (default-type (if (equal? (mime-part:subtype packet) "digest") |
---|
386 | '("message" "rfc822") |
---|
387 | '("text" "plain" ("charset" . "us-ascii"))))) |
---|
388 | (receive (mime-port mime-port-state mime-port-state-set!) |
---|
389 | (make-mime-port boundary port) |
---|
390 | (let loop ((index 0) |
---|
391 | (contents '())) |
---|
392 | (let* ((headers (rfc822-header->list mime-port)) |
---|
393 | (r (internal-parse mime-port headers handler |
---|
394 | packet index |
---|
395 | default-type))) |
---|
396 | (case (mime-port-state) |
---|
397 | ((boundary) |
---|
398 | (mime-port-state-set! 'body) |
---|
399 | (loop (+ index 1) (cons r contents))) |
---|
400 | ((eof) |
---|
401 | (mime-part:content-set! packet (reverse! (cons r contents))) |
---|
402 | packet) |
---|
403 | (else |
---|
404 | ;; parser returned without readling entire part. |
---|
405 | ;; discard the rest of the part. |
---|
406 | (let loop ((b (read-char port))) |
---|
407 | (unless (eof-object? b) |
---|
408 | (loop (read-char port)))) |
---|
409 | packet))))))) |
---|
410 | |
---|
411 | (define (message-parse port packet handler) |
---|
412 | (let* ((headers (rfc822-header->list port)) |
---|
413 | (r (internal-parse port headers handler packet 0 |
---|
414 | '("text" "plain" ("charset" . "us-ascii"))))) |
---|
415 | (mime-part:content-set! packet (list r))) |
---|
416 | packet) |
---|
417 | |
---|
418 | ;;=============================================================== |
---|
419 | ;; Body readers |
---|
420 | ;; |
---|
421 | |
---|
422 | (define (mime-retrieve-body packet inp outp) |
---|
423 | |
---|
424 | (define (read-line/nl) |
---|
425 | (let loop ((c (read-char inp)) |
---|
426 | (chars '())) |
---|
427 | (cond ((eof-object? c) |
---|
428 | (if (null? chars) |
---|
429 | c |
---|
430 | (reverse-list->string chars))) |
---|
431 | ((char=? c #\newline) |
---|
432 | (reverse-list->string (cons c chars))) |
---|
433 | ((char=? c #\return) |
---|
434 | (let1 c (peek-char inp) |
---|
435 | (if (char=? c #\newline) |
---|
436 | (reverse-list->string |
---|
437 | (cons* (read-char inp) #\return chars)) |
---|
438 | (reverse-list->string (cons #\return chars))))) |
---|
439 | (else (loop (read-char inp) (cons c chars)))))) |
---|
440 | |
---|
441 | (define (read-text decoder) |
---|
442 | (let loop ((line (read-line/nl))) |
---|
443 | (unless (eof-object? line) |
---|
444 | (display (decoder line) outp) |
---|
445 | (loop (read-line/nl))))) |
---|
446 | |
---|
447 | (define (read-base64) |
---|
448 | (display |
---|
449 | (base64:decode |
---|
450 | (with-output-to-string |
---|
451 | (lambda () |
---|
452 | (let loop ((line (read-line inp))) |
---|
453 | (unless (eof-object? line) |
---|
454 | (display line) |
---|
455 | (loop (read-line inp))))))) |
---|
456 | outp)) |
---|
457 | |
---|
458 | (let ((enc (mime-part:transfer-encoding packet))) |
---|
459 | (cond |
---|
460 | ((string-ci=? enc "base64") (read-base64)) |
---|
461 | ((string-ci=? enc "quoted-printable") |
---|
462 | (read-text quoted-printable-decode-string)) |
---|
463 | ((member enc '("7bit" "8bit" "binary") string-ci=?) |
---|
464 | (let loop ((b (read-char inp))) |
---|
465 | (unless (eof-object? b) |
---|
466 | (write-char b outp) |
---|
467 | (loop (read-char inp))))) |
---|
468 | )) |
---|
469 | ) |
---|
470 | |
---|
471 | (define (mime-body->string packet inp) |
---|
472 | (let ((s (open-output-string))) |
---|
473 | (mime-retrieve-body packet inp s) |
---|
474 | (get-output-string s))) |
---|
475 | |
---|
476 | (define (mime-body->file packet inp filename) |
---|
477 | (call-with-output-file filename |
---|
478 | (lambda (outp) |
---|
479 | (mime-retrieve-body packet inp outp))) |
---|
480 | filename) |
---|
481 | |
---|
482 | |
---|
483 | ;;=============================================================== |
---|
484 | ;; RFC2822/MIME message generator |
---|
485 | ;; |
---|
486 | |
---|
487 | (define (rfc822-header-fold h #!optional (col 78)) |
---|
488 | (define ws (char-set #\space #\tab)) |
---|
489 | (define (fold-pos s) |
---|
490 | (let loop ((i (string-index s ws)) (p #f)) |
---|
491 | (cond |
---|
492 | ((not i) p) |
---|
493 | ((= i 0) |
---|
494 | (loop (string-index s ws 1 (string-length s)) p)) |
---|
495 | ((and (> i 0) (char=? (string-ref s (- i 1)) #\\)) |
---|
496 | (loop (string-index s ws (+ i 1) (string-length s)) p)) |
---|
497 | ; ((> i 998) p) |
---|
498 | ((and p (> i col)) p) |
---|
499 | ((> i col) i) |
---|
500 | (else |
---|
501 | (loop (string-index s ws (+ i 1) (string-length s)) i))))) |
---|
502 | (define (fold1 s) |
---|
503 | (cond |
---|
504 | ((<= (string-length s) col) |
---|
505 | (display s) #f) |
---|
506 | ((fold-pos s) |
---|
507 | => (lambda (p) |
---|
508 | (display (substring s 0 p)) |
---|
509 | (display "\r\n") |
---|
510 | (substring s p (string-length s)))) |
---|
511 | (else |
---|
512 | ;; line is too long but we can't fold it (no whitespace) |
---|
513 | (display s) #f))) |
---|
514 | |
---|
515 | (if (string-any #\newline h) |
---|
516 | h |
---|
517 | (with-output-to-string |
---|
518 | (lambda () |
---|
519 | (let loop ((h (fold1 h))) |
---|
520 | (if h (loop (fold1 h)))))))) |
---|
521 | |
---|
522 | (define (rfc822-header-upcase h) |
---|
523 | (with-output-to-string |
---|
524 | (lambda () |
---|
525 | (string-fold |
---|
526 | (lambda (ch u) |
---|
527 | (cond |
---|
528 | ((char=? ch #\-) (display ch) #t) |
---|
529 | (u (display (char-upcase ch)) #f) |
---|
530 | (else (display ch) #f))) |
---|
531 | #t h)))) |
---|
532 | |
---|
533 | (define (rfc822-headers-write headers) |
---|
534 | (for-each |
---|
535 | (lambda (h) |
---|
536 | (printf "~a: ~a\r\n" |
---|
537 | (rfc822-header-upcase (car h)) |
---|
538 | (rfc822-header-fold (cadr h) (- 78 2 (string-length (car h)))))) |
---|
539 | headers)) |
---|
540 | |
---|
541 | (define (alist-subst key value alist #!optional (test eqv?)) |
---|
542 | (let loop ((alist alist) (r '()) (f? #f)) |
---|
543 | (cond |
---|
544 | ((null? alist) |
---|
545 | (if f? (reverse r) (cons (cons key value) (reverse r)))) |
---|
546 | (f? |
---|
547 | (loop (cdr alist) (cons (car alist) r) #t)) |
---|
548 | ((test key (caar alist)) |
---|
549 | (loop (cdr alist) (cons (cons (caar alist) value) r) #t)) |
---|
550 | (else |
---|
551 | (loop (cdr alist) (cons (car alist) r) #f))))) |
---|
552 | |
---|
553 | (define (alist-subst* alist pairs #!optional (test eqv?)) |
---|
554 | (define (pairs-without pairs key) |
---|
555 | (filter (lambda (x) (not (test (car x) key))) pairs)) |
---|
556 | (let loop ((alist alist) (r '()) (pairs pairs)) |
---|
557 | (cond |
---|
558 | ((null? alist) |
---|
559 | (append (reverse r) pairs)) |
---|
560 | ((null? pairs) |
---|
561 | (loop (cdr alist) (cons (car alist) r) pairs)) |
---|
562 | ((assoc (caar alist) pairs test) |
---|
563 | => (lambda (p) (loop (cdr alist) (cons (cons (caar alist) (cdr p)) r) |
---|
564 | (pairs-without pairs (car p))))) |
---|
565 | (else |
---|
566 | (loop (cdr alist) (cons (car alist) r) pairs))))) |
---|
567 | |
---|
568 | (define (content-type->string type subtype params) |
---|
569 | (string-append |
---|
570 | type "/" |
---|
571 | subtype |
---|
572 | (if (pair? params) ";" "") |
---|
573 | (string-intersperse |
---|
574 | (map (lambda (x) (string-append (car x) "=\"" (cdr x) "\"")) params) |
---|
575 | ";"))) |
---|
576 | |
---|
577 | (define (mime-part:modified-headers part #!optional new-boundary) |
---|
578 | (alist-subst* |
---|
579 | (mime-part:headers part) |
---|
580 | `(("content-type" |
---|
581 | ,(content-type->string |
---|
582 | (mime-part:type part) (mime-part:subtype part) |
---|
583 | (if new-boundary |
---|
584 | (alist-subst "boundary" new-boundary (mime-part:parameters part) string-ci=?) |
---|
585 | (mime-part:parameters part)))) |
---|
586 | ("content-transfer-encoding" ,(mime-part:transfer-encoding part))) |
---|
587 | string=?)) |
---|
588 | |
---|
589 | (define (multipart-write part #!optional boundary) |
---|
590 | (let ((parts (map mime-part->string (mime-part:content part))) |
---|
591 | (boundary (or boundary |
---|
592 | (alist-ref "boundary" (mime-part:parameters part) string-ci=?) |
---|
593 | "MIME-Message-Boundary-"))) |
---|
594 | |
---|
595 | (define (find-boundary) |
---|
596 | (let loop ((boundary boundary) |
---|
597 | (parts parts)) |
---|
598 | (cond |
---|
599 | ((null? parts) boundary) |
---|
600 | ((string-contains (car parts) (string-append "--" boundary)) |
---|
601 | (loop (string-append boundary "a") parts)) |
---|
602 | (else |
---|
603 | (loop boundary (cdr parts)))))) |
---|
604 | |
---|
605 | (let ((boundary (find-boundary))) |
---|
606 | |
---|
607 | (define (write-boundary end?) |
---|
608 | (display "\r\n--") |
---|
609 | (display boundary) |
---|
610 | (if end? (display "--\r\n") (display "\r\n"))) |
---|
611 | |
---|
612 | (rfc822-headers-write |
---|
613 | (mime-part:modified-headers part boundary)) |
---|
614 | (display "\r\nThis message is in MIME format.\r\n") |
---|
615 | (do ((parts parts (cdr parts))) |
---|
616 | ((null? parts) (write-boundary #t)) |
---|
617 | (write-boundary #f) |
---|
618 | (display (car parts)))))) |
---|
619 | |
---|
620 | (define (message-write part) |
---|
621 | (rfc822-headers-write (mime-part:modified-headers part)) |
---|
622 | (display "\r\n") |
---|
623 | (mime-part-write (car (mime-part:content part)))) |
---|
624 | |
---|
625 | (define (other-part-write part) |
---|
626 | (define (write-base64) |
---|
627 | (for-each |
---|
628 | (lambda (s) |
---|
629 | (display s) |
---|
630 | (display "\r\n")) |
---|
631 | (string-chop |
---|
632 | (base64:encode (mime-part:content part)) |
---|
633 | 76))) |
---|
634 | |
---|
635 | (define (write-qp) |
---|
636 | (with-input-from-string (mime-part:content part) |
---|
637 | (lambda () |
---|
638 | (quoted-printable-encode |
---|
639 | #:binary? (alist-ref 'qp-encode-binary? |
---|
640 | (mime-part:attrs part) |
---|
641 | eq? #f))))) |
---|
642 | |
---|
643 | (rfc822-headers-write (mime-part:modified-headers part)) |
---|
644 | (display "\r\n") |
---|
645 | |
---|
646 | (let ((enc (mime-part:transfer-encoding part))) |
---|
647 | (cond |
---|
648 | ((string-ci=? enc "base64") |
---|
649 | (write-base64)) |
---|
650 | ((string-ci=? enc "quoted-printable") |
---|
651 | (write-qp)) |
---|
652 | ((member enc '("7bit" "8bit" "binary") string-ci=?) |
---|
653 | (display (mime-part:content part)))))) |
---|
654 | |
---|
655 | |
---|
656 | (define (mime-part-write part) |
---|
657 | (let ((type (mime-part:type part))) |
---|
658 | (cond |
---|
659 | ((string-ci=? type "multipart") |
---|
660 | (multipart-write part)) |
---|
661 | ((string-ci=? type "message") |
---|
662 | (message-write part)) |
---|
663 | (else |
---|
664 | (other-part-write part))))) |
---|
665 | |
---|
666 | (define (mime-part->string part) |
---|
667 | (with-output-to-string |
---|
668 | (cut mime-part-write part))) |
---|