source: project/mime/trunk/mime.scm @ 5248

Last change on this file since 5248 was 5248, checked in by hans, 13 years ago

version 1.3: factored out mime-get-attributes from mime-parse-content-type, added a missing backslash in mime-parse-version regexp

File size: 21.2 KB
Line 
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)))
Note: See TracBrowser for help on using the repository browser.