diff --git a/collects/net/cgi.rkt b/collects/net/cgi.rkt
index b848d16f0e..9612982942 100644
--- a/collects/net/cgi.rkt
+++ b/collects/net/cgi.rkt
@@ -1,6 +1,227 @@
#lang racket/base
-(require racket/unit "cgi-sig.rkt" "cgi-unit.rkt")
-(define-values/invoke-unit/infer cgi@)
+(require "uri-codec.rkt")
-(provide-signature-elements cgi^)
+(provide
+ ;; -- exceptions raised --
+ (struct-out cgi-error)
+ (struct-out incomplete-%-suffix)
+ (struct-out invalid-%-suffix)
+
+ ;; -- cgi methods --
+ get-bindings
+ get-bindings/post
+ get-bindings/get
+ output-http-headers
+ generate-html-output
+ generate-error-output
+ bindings-as-html
+ extract-bindings
+ extract-binding/single
+ get-cgi-method
+
+ ;; -- general HTML utilities --
+ string->html
+ generate-link-text)
+
+;; type bindings = list ((symbol . string))
+
+;; --------------------------------------------------------------------
+
+;; Exceptions:
+
+(define-struct cgi-error ())
+
+;; chars : list (char)
+;; -- gives the suffix which is invalid, not including the `%'
+
+(define-struct (incomplete-%-suffix cgi-error) (chars))
+
+;; char : char
+;; -- an invalid character in a hex string
+
+(define-struct (invalid-%-suffix cgi-error) (char))
+
+;; --------------------------------------------------------------------
+
+;; query-string->string : string -> string
+
+;; -- The input is the string post-processed as per Web specs, which
+;; is as follows:
+;; spaces are turned into "+"es and lots of things are turned into %XX, where
+;; XX are hex digits, eg, %E7 for ~. The output is a regular Scheme string
+;; with all the characters converted back.
+
+(define query-string->string form-urlencoded-decode)
+
+;; string->html : string -> string
+;; -- the input is raw text, the output is HTML appropriately quoted
+
+(define (string->html s)
+ (apply string-append
+ (map (lambda (c)
+ (case c
+ [(#\<) "<"]
+ [(#\>) ">"]
+ [(#\&) "&"]
+ [else (string c)]))
+ (string->list s))))
+
+(define default-text-color "#000000")
+(define default-bg-color "#ffffff")
+(define default-link-color "#cc2200")
+(define default-vlink-color "#882200")
+(define default-alink-color "#444444")
+
+;; generate-html-output :
+;; html-string x list (html-string) x ... -> ()
+
+(define (generate-html-output title body-lines
+ [text-color default-text-color]
+ [bg-color default-bg-color]
+ [link-color default-link-color]
+ [vlink-color default-vlink-color]
+ [alink-color default-alink-color])
+ (let ([sa string-append])
+ (for ([l `("Content-type: text/html"
+ ""
+ ""
+ ""
+ "
"
+ ,(sa "" title "")
+ ""
+ ""
+ ,(sa "")
+ ""
+ ,@body-lines
+ ""
+ ""
+ "")])
+ (display l)
+ (newline))))
+
+;; output-http-headers : -> void
+(define (output-http-headers)
+ (printf "Content-type: text/html\r\n\r\n"))
+
+;; delimiter->predicate : symbol -> regexp
+;; returns a regexp to read a chunk of text up to a delimiter (excluding it)
+(define (delimiter->rx delimiter)
+ (case delimiter
+ [(amp) #rx#"^[^&]*"]
+ [(semi) #rx#"^[^;]*"]
+ [(amp-or-semi) #rx#"^[^&;]*"]
+ [else (error 'delimiter->rx
+ "internal-error, unknown delimiter: ~e" delimiter)]))
+
+;; get-bindings* : iport -> (listof (cons symbol string))
+;; Reads all bindings from the input port. The strings are processed to
+;; remove the CGI spec "escape"s.
+;; This code is _slightly_ lax: it allows an input to end in
+;; (current-alist-separator-mode). It's not clear this is legal by the
+;; CGI spec, which suggests that the last value binding must end in an
+;; EOF. It doesn't look like this matters.
+;; ELI: * Keeping this behavior for now, maybe better to remove it?
+;; * Looks like `form-urlencoded->alist' is doing almost exactly
+;; the same job this code does.
+(define (get-bindings* method ip)
+ (define (err fmt . xs)
+ (generate-error-output
+ (list (format "Server generated malformed input for ~a method:" method)
+ (apply format fmt xs))))
+ (define value-rx (delimiter->rx (current-alist-separator-mode)))
+ (define (process str) (query-string->string (bytes->string/utf-8 str)))
+ (let loop ([bindings '()])
+ (if (eof-object? (peek-char ip))
+ (reverse bindings)
+ (let ()
+ (define name (car (or (regexp-match #rx"^[^=]+" ip)
+ (err "Missing field name before `='"))))
+ (unless (eq? #\= (read-char ip))
+ (err "No binding for `~a' field." name))
+ (define value (car (regexp-match value-rx ip)))
+ (read-char ip) ; consume the delimiter, possibly eof (retested above)
+ (loop (cons (cons (string->symbol (process name)) (process value))
+ bindings))))))
+
+;; get-bindings/post : () -> bindings
+(define (get-bindings/post)
+ (get-bindings* "POST" (current-input-port)))
+
+;; get-bindings/get : () -> bindings
+(define (get-bindings/get)
+ (get-bindings* "GET" (open-input-string (getenv "QUERY_STRING"))))
+
+;; get-bindings : () -> bindings
+(define (get-bindings)
+ (if (string=? (get-cgi-method) "POST")
+ (get-bindings/post)
+ (get-bindings/get)))
+
+;; generate-error-output : list (html-string) ->
+(define (generate-error-output error-message-lines)
+ (generate-html-output "Internal Error" error-message-lines)
+ (exit))
+
+;; bindings-as-html : bindings -> list (html-string)
+;; -- formats name-value bindings as HTML appropriate for displaying
+(define (bindings-as-html bindings)
+ `(""
+ ,@(map (lambda (bind)
+ (string-append (symbol->string (car bind))
+ " --> "
+ (cdr bind)
+ "
"))
+ bindings)
+ "
"))
+
+;; extract-bindings : (string + symbol) x bindings -> list (string)
+;; -- Extracts the bindings associated with a given name. The semantics of
+;; forms states that a CHECKBOX may use the same NAME field multiple times.
+;; Hence, a list of strings is returned. Note that the result may be the
+;; empty list.
+(define (extract-bindings field-name bindings)
+ (let ([field-name (if (symbol? field-name)
+ field-name (string->symbol field-name))])
+ (let loop ([found null] [bindings bindings])
+ (if (null? bindings)
+ found
+ (if (equal? field-name (caar bindings))
+ (loop (cons (cdar bindings) found) (cdr bindings))
+ (loop found (cdr bindings)))))))
+
+;; extract-binding/single : (string + symbol) x bindings -> string
+;; -- used in cases where only one binding is supposed to occur
+(define (extract-binding/single field-name bindings)
+ (let* ([field-name (if (symbol? field-name)
+ field-name (string->symbol field-name))]
+ [result (extract-bindings field-name bindings)])
+ (cond
+ [(null? result)
+ (generate-error-output
+ (cons (format "No binding for field `~a':
" field-name)
+ (bindings-as-html bindings)))]
+ [(null? (cdr result))
+ (car result)]
+ [else
+ (generate-error-output
+ (cons (format "Multiple bindings for field `~a' where one expected:
"
+ field-name)
+ (bindings-as-html bindings)))])))
+
+;; get-cgi-method : () -> string
+;; -- string is either GET or POST (though future extension is possible)
+(define (get-cgi-method)
+ (or (getenv "REQUEST_METHOD")
+ (error 'get-cgi-method "no REQUEST_METHOD environment variable")))
+
+;; generate-link-text : string x html-string -> html-string
+(define (generate-link-text url anchor-text)
+ (string-append "" anchor-text ""))
diff --git a/collects/net/dns.rkt b/collects/net/dns.rkt
index 901649091f..6496204bb7 100644
--- a/collects/net/dns.rkt
+++ b/collects/net/dns.rkt
@@ -1,6 +1,341 @@
#lang racket/base
-(require racket/unit "dns-sig.rkt" "dns-unit.rkt")
-(define-values/invoke-unit/infer dns@)
+(require racket/udp
+ racket/system)
-(provide-signature-elements dns^)
+(provide dns-get-address
+ dns-get-name
+ dns-get-mail-exchanger
+ dns-find-nameserver)
+
+;; UDP retry timeout:
+(define INIT-TIMEOUT 50)
+
+(define types
+ '((a 1)
+ (ns 2)
+ (md 3)
+ (mf 4)
+ (cname 5)
+ (soa 6)
+ (mb 7)
+ (mg 8)
+ (mr 9)
+ (null 10)
+ (wks 11)
+ (ptr 12)
+ (hinfo 13)
+ (minfo 14)
+ (mx 15)
+ (txt 16)))
+
+(define classes
+ '((in 1)
+ (cs 2)
+ (ch 3)
+ (hs 4)))
+
+(define (cossa i l)
+ (cond [(null? l) #f]
+ [(equal? (cadar l) i) (car l)]
+ [else (cossa i (cdr l))]))
+
+(define (number->octet-pair n)
+ (list (arithmetic-shift n -8)
+ (modulo n 256)))
+
+(define (octet-pair->number a b)
+ (+ (arithmetic-shift a 8) b))
+
+(define (octet-quad->number a b c d)
+ (+ (arithmetic-shift a 24)
+ (arithmetic-shift b 16)
+ (arithmetic-shift c 8)
+ d))
+
+(define (name->octets s)
+ (let ([do-one (lambda (s) (cons (bytes-length s) (bytes->list s)))])
+ (let loop ([s s])
+ (let ([m (regexp-match #rx#"^([^.]*)[.](.*)" s)])
+ (if m
+ (append (do-one (cadr m)) (loop (caddr m)))
+ (append (do-one s) (list 0)))))))
+
+(define (make-std-query-header id question-count)
+ (append (number->octet-pair id)
+ (list 1 0) ; Opcode & flags (recusive flag set)
+ (number->octet-pair question-count)
+ (number->octet-pair 0)
+ (number->octet-pair 0)
+ (number->octet-pair 0)))
+
+(define (make-query id name type class)
+ (append (make-std-query-header id 1)
+ (name->octets name)
+ (number->octet-pair (cadr (assoc type types)))
+ (number->octet-pair (cadr (assoc class classes)))))
+
+(define (add-size-tag m)
+ (append (number->octet-pair (length m)) m))
+
+(define (rr-data rr)
+ (cadddr (cdr rr)))
+
+(define (rr-type rr)
+ (cadr rr))
+
+(define (rr-name rr)
+ (car rr))
+
+(define (parse-name start reply)
+ (let ([v (car start)])
+ (cond
+ [(zero? v)
+ ;; End of name
+ (values #f (cdr start))]
+ [(zero? (bitwise-and #xc0 v))
+ ;; Normal label
+ (let loop ([len v][start (cdr start)][accum null])
+ (if (zero? len)
+ (let-values ([(s start) (parse-name start reply)])
+ (let ([s0 (list->bytes (reverse accum))])
+ (values (if s (bytes-append s0 #"." s) s0)
+ start)))
+ (loop (sub1 len) (cdr start) (cons (car start) accum))))]
+ [else
+ ;; Compression offset
+ (let ([offset (+ (arithmetic-shift (bitwise-and #x3f v) 8)
+ (cadr start))])
+ (let-values ([(s ignore-start)
+ (parse-name (list-tail reply offset) reply)])
+ (values s (cddr start))))])))
+
+(define (parse-rr start reply)
+ (let-values ([(name start) (parse-name start reply)])
+ (let* ([type (car (cossa (octet-pair->number (car start) (cadr start))
+ types))]
+ [start (cddr start)]
+ ;;
+ [class (car (cossa (octet-pair->number (car start) (cadr start))
+ classes))]
+ [start (cddr start)]
+ ;;
+ [ttl (octet-quad->number (car start) (cadr start)
+ (caddr start) (cadddr start))]
+ [start (cddddr start)]
+ ;;
+ [len (octet-pair->number (car start) (cadr start))]
+ [start (cddr start)])
+ ;; Extract next len bytes for data:
+ (let loop ([len len] [start start] [accum null])
+ (if (zero? len)
+ (values (list name type class ttl (reverse accum))
+ start)
+ (loop (sub1 len) (cdr start) (cons (car start) accum)))))))
+
+(define (parse-ques start reply)
+ (let-values ([(name start) (parse-name start reply)])
+ (let* ([type (car (cossa (octet-pair->number (car start) (cadr start))
+ types))]
+ [start (cddr start)]
+ ;;
+ [class (car (cossa (octet-pair->number (car start) (cadr start))
+ classes))]
+ [start (cddr start)])
+ (values (list name type class) start))))
+
+(define (parse-n parse start reply n)
+ (let loop ([n n][start start][accum null])
+ (if (zero? n)
+ (values (reverse accum) start)
+ (let-values ([(rr start) (parse start reply)])
+ (loop (sub1 n) start (cons rr accum))))))
+
+(define (dns-query nameserver addr type class)
+ (unless (assoc type types)
+ (raise-type-error 'dns-query "DNS query type" type))
+ (unless (assoc class classes)
+ (raise-type-error 'dns-query "DNS query class" class))
+
+ (let* ([query (make-query (random 256) (string->bytes/latin-1 addr)
+ type class)]
+ [udp (udp-open-socket)]
+ [reply
+ (dynamic-wind
+ void
+ (lambda ()
+ (let ([s (make-bytes 512)])
+ (let retry ([timeout INIT-TIMEOUT])
+ (udp-send-to udp nameserver 53 (list->bytes query))
+ (sync (handle-evt (udp-receive!-evt udp s)
+ (lambda (r)
+ (bytes->list (subbytes s 0 (car r)))))
+ (handle-evt (alarm-evt (+ (current-inexact-milliseconds)
+ timeout))
+ (lambda (v)
+ (retry (* timeout 2))))))))
+ (lambda () (udp-close udp)))])
+
+ ;; First two bytes must match sent message id:
+ (unless (and (= (car reply) (car query))
+ (= (cadr reply) (cadr query)))
+ (error 'dns-query "bad reply id from server"))
+
+ (let ([v0 (caddr reply)]
+ [v1 (cadddr reply)])
+ ;; Check for error code:
+ (let ([rcode (bitwise-and #xf v1)])
+ (unless (zero? rcode)
+ (error 'dns-query "error from server: ~a"
+ (case rcode
+ [(1) "format error"]
+ [(2) "server failure"]
+ [(3) "name error"]
+ [(4) "not implemented"]
+ [(5) "refused"]))))
+
+ (let ([qd-count (octet-pair->number (list-ref reply 4) (list-ref reply 5))]
+ [an-count (octet-pair->number (list-ref reply 6) (list-ref reply 7))]
+ [ns-count (octet-pair->number (list-ref reply 8) (list-ref reply 9))]
+ [ar-count (octet-pair->number (list-ref reply 10) (list-ref reply 11))])
+
+ (let ([start (list-tail reply 12)])
+ (let*-values ([(qds start) (parse-n parse-ques start reply qd-count)]
+ [(ans start) (parse-n parse-rr start reply an-count)]
+ [(nss start) (parse-n parse-rr start reply ns-count)]
+ [(ars start) (parse-n parse-rr start reply ar-count)])
+ (unless (null? start)
+ (error 'dns-query "error parsing server reply"))
+ (values (positive? (bitwise-and #x4 v0))
+ qds ans nss ars reply)))))))
+
+(define cache (make-hasheq))
+(define (dns-query/cache nameserver addr type class)
+ (let ([key (string->symbol (format "~a;~a;~a;~a" nameserver addr type class))])
+ (let ([v (hash-ref cache key (lambda () #f))])
+ (if v
+ (apply values v)
+ (let-values ([(auth? qds ans nss ars reply)
+ (dns-query nameserver addr type class)])
+ (hash-set! cache key (list auth? qds ans nss ars reply))
+ (values auth? qds ans nss ars reply))))))
+
+(define (ip->string s)
+ (format "~a.~a.~a.~a"
+ (list-ref s 0) (list-ref s 1) (list-ref s 2) (list-ref s 3)))
+
+(define (try-forwarding k nameserver)
+ (let loop ([nameserver nameserver][tried (list nameserver)])
+ ;; Normally the recusion is done for us, but it's technically optional
+ (let-values ([(v ars auth?) (k nameserver)])
+ (or v
+ (and (not auth?)
+ (let* ([ns (ormap (lambda (ar)
+ (and (eq? (rr-type ar) 'a)
+ (ip->string (rr-data ar))))
+ ars)])
+ (and ns
+ (not (member ns tried))
+ (loop ns (cons ns tried)))))))))
+
+(define (ip->in-addr.arpa ip)
+ (let ([result (regexp-match #rx"^([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)$"
+ ip)])
+ (format "~a.~a.~a.~a.in-addr.arpa"
+ (list-ref result 4)
+ (list-ref result 3)
+ (list-ref result 2)
+ (list-ref result 1))))
+
+(define (get-ptr-list-from-ans ans)
+ (filter (lambda (ans-entry) (eq? (list-ref ans-entry 1) 'ptr)) ans))
+
+(define (dns-get-name nameserver ip)
+ (or (try-forwarding
+ (lambda (nameserver)
+ (let-values ([(auth? qds ans nss ars reply)
+ (dns-query/cache nameserver (ip->in-addr.arpa ip) 'ptr 'in)])
+ (values (and (positive? (length (get-ptr-list-from-ans ans)))
+ (let ([s (rr-data (car (get-ptr-list-from-ans ans)))])
+ (let-values ([(name null) (parse-name s reply)])
+ (bytes->string/latin-1 name))))
+ ars auth?)))
+ nameserver)
+ (error 'dns-get-name "bad ip address")))
+
+(define (get-a-list-from-ans ans)
+ (filter (lambda (ans-entry) (eq? (list-ref ans-entry 1) 'a))
+ ans))
+
+(define (dns-get-address nameserver addr)
+ (or (try-forwarding
+ (lambda (nameserver)
+ (let-values ([(auth? qds ans nss ars reply) (dns-query/cache nameserver addr 'a 'in)])
+ (values (and (positive? (length (get-a-list-from-ans ans)))
+ (let ([s (rr-data (car (get-a-list-from-ans ans)))])
+ (ip->string s)))
+ ars auth?)))
+ nameserver)
+ (error 'dns-get-address "bad address")))
+
+(define (dns-get-mail-exchanger nameserver addr)
+ (or (try-forwarding
+ (lambda (nameserver)
+ (let-values ([(auth? qds ans nss ars reply) (dns-query/cache nameserver addr 'mx 'in)])
+ (values (let loop ([ans ans][best-pref +inf.0][exchanger #f])
+ (cond
+ [(null? ans)
+ (or exchanger
+ ;; Does 'soa mean that the input address is fine?
+ (and (ormap (lambda (ns) (eq? (rr-type ns) 'soa))
+ nss)
+ addr))]
+ [else
+ (let ([d (rr-data (car ans))])
+ (let ([pref (octet-pair->number (car d) (cadr d))])
+ (if (< pref best-pref)
+ (let-values ([(name start) (parse-name (cddr d) reply)])
+ (loop (cdr ans) pref name))
+ (loop (cdr ans) best-pref exchanger))))]))
+ ars auth?)))
+ nameserver)
+ (error 'dns-get-mail-exchanger "bad address")))
+
+(define (dns-find-nameserver)
+ (case (system-type)
+ [(unix macosx)
+ (with-handlers ([void (lambda (x) #f)])
+ (with-input-from-file "/etc/resolv.conf"
+ (lambda ()
+ (let loop ()
+ (let ([l (read-line)])
+ (or (and (string? l)
+ (let ([m (regexp-match
+ #rx"nameserver[ \t]+([0-9]+[.][0-9]+[.][0-9]+[.][0-9]+)"
+ l)])
+ (and m (cadr m))))
+ (and (not (eof-object? l))
+ (loop))))))))]
+ [(windows)
+ (let ([nslookup (find-executable-path "nslookup.exe" #f)])
+ (and nslookup
+ (let-values ([(pin pout pid perr proc)
+ (apply
+ values
+ (process/ports
+ #f (open-input-file "NUL") (current-error-port)
+ nslookup))])
+ (let loop ([name #f] [ip #f] [try-ip? #f])
+ (let ([line (read-line pin 'any)])
+ (cond [(eof-object? line)
+ (close-input-port pin)
+ (proc 'wait)
+ (or ip name)]
+ [(and (not name)
+ (regexp-match #rx"^Default Server: +(.*)$" line))
+ => (lambda (m) (loop (cadr m) #f #t))]
+ [(and try-ip?
+ (regexp-match #rx"^Address: +(.*)$" line))
+ => (lambda (m) (loop name (cadr m) #f))]
+ [else (loop name ip #f)]))))))]
+ [else #f]))
diff --git a/collects/net/ftp.rkt b/collects/net/ftp.rkt
index 5e4ff2a349..6702448612 100644
--- a/collects/net/ftp.rkt
+++ b/collects/net/ftp.rkt
@@ -1,6 +1,215 @@
#lang racket/base
-(require racket/unit "ftp-sig.rkt" "ftp-unit.rkt")
-(define-values/invoke-unit/infer ftp@)
+(require racket/date racket/file racket/port racket/tcp)
-(provide-signature-elements ftp^)
+(provide ftp-connection?
+ ftp-cd
+ ftp-establish-connection ftp-establish-connection*
+ ftp-close-connection
+ ftp-directory-list
+ ftp-download-file
+ ftp-make-file-seconds)
+
+;; opqaue record to represent an FTP connection:
+(define-struct ftp-connection (in out))
+
+(define re:multi-response-start #rx#"^[0-9][0-9][0-9]-")
+(define re:response-end #rx#"^[0-9][0-9][0-9] ")
+
+(define (check-expected-result line expected)
+ (when expected
+ (unless (ormap (lambda (expected)
+ (bytes=? expected (subbytes line 0 3)))
+ (if (bytes? expected)
+ (list expected)
+ expected))
+ (error 'ftp "expected result code ~a, got ~a" expected line))))
+
+;; ftp-check-response : input-port output-port bytes-or-byteslist-or-#f (bytes any -> any) any -> any
+;;
+;; Checks a standard-format response, checking for the given
+;; expected 3-digit result code if expected is not #f.
+;;
+;; While checking, the function sends response lines to
+;; diagnostic-accum. This function -accum functions can return a
+;; value that accumulates over multiple calls to the function, and
+;; accum-start is used as the initial value. Use `void' and
+;; `(void)' to ignore the response info.
+;;
+;; If an unexpected result is found, an exception is raised, and the
+;; stream is left in an undefined state.
+(define (ftp-check-response tcpin tcpout expected diagnostic-accum accum-start)
+ (flush-output tcpout)
+ (let ([line (read-bytes-line tcpin 'any)])
+ (cond
+ [(eof-object? line)
+ (error 'ftp "unexpected EOF")]
+ [(regexp-match re:multi-response-start line)
+ (check-expected-result line expected)
+ (let ([re:done (regexp (format "^~a " (subbytes line 0 3)))])
+ (let loop ([accum (diagnostic-accum line accum-start)])
+ (let ([line (read-bytes-line tcpin 'any)])
+ (cond [(eof-object? line)
+ (error 'ftp "unexpected EOF")]
+ [(regexp-match re:done line)
+ (diagnostic-accum line accum)]
+ [else
+ (loop (diagnostic-accum line accum))]))))]
+ [(regexp-match re:response-end line)
+ (check-expected-result line expected)
+ (diagnostic-accum line accum-start)]
+ [else
+ (error 'ftp "unexpected result: ~e" line)])))
+
+(define (get-month month-bytes)
+ (cond [(assoc month-bytes
+ '((#"Jan" 1) (#"Feb" 2) (#"Mar" 3) (#"Apr" 4) (#"May" 5)
+ (#"Jun" 6) (#"Jul" 7) (#"Aug" 8) (#"Sep" 9) (#"Oct" 10)
+ (#"Nov" 11) (#"Dec" 12)))
+ => cadr]
+ [else (error 'get-month "bad month: ~s" month-bytes)]))
+
+(define (bytes->number bytes)
+ (string->number (bytes->string/latin-1 bytes)))
+
+(define re:date #rx#"(...) *(.*) (..):(..)|(...) *([0-9]*) +(....)")
+
+(define (ftp-make-file-seconds ftp-date-str)
+ (define date-list (regexp-match re:date (string->bytes/utf-8 ftp-date-str)))
+ (if (not (list-ref date-list 4))
+ (find-seconds 0 0 0
+ (bytes->number (list-ref date-list 6))
+ (get-month (list-ref date-list 5))
+ (bytes->number (list-ref date-list 7)))
+ (let* ([cur-secs (current-seconds)]
+ [cur-date (seconds->date cur-secs)]
+ [cur-year (date-year cur-date)]
+ [tzofs (date-time-zone-offset cur-date)]
+ [minute (bytes->number (list-ref date-list 4))]
+ [hour (bytes->number (list-ref date-list 3))]
+ [day (bytes->number (list-ref date-list 2))]
+ [month (get-month (list-ref date-list 1))]
+ [guess (+ (find-seconds 0 minute hour day month cur-year) tzofs)])
+ (if (guess . <= . cur-secs)
+ guess
+ (+ (find-seconds 0 minute hour day month (sub1 cur-year)) tzofs)))))
+
+(define re:passive #rx#"\\((.*),(.*),(.*),(.*),(.*),(.*)\\)")
+
+(define (establish-data-connection tcp-ports)
+ (fprintf (ftp-connection-out tcp-ports) "PASV\r\n")
+ (let ([response (ftp-check-response
+ (ftp-connection-in tcp-ports)
+ (ftp-connection-out tcp-ports)
+ #"227"
+ (lambda (s ignore) s) ; should be the only response
+ (void))])
+ (let* ([reg-list (regexp-match re:passive response)]
+ [pn1 (and reg-list
+ (bytes->number (list-ref reg-list 5)))]
+ [pn2 (bytes->number (list-ref reg-list 6))])
+ (unless (and reg-list pn1 pn2)
+ (error 'ftp "can't understand PASV response: ~e" response))
+ (let-values ([(tcp-data tcp-data-out)
+ (tcp-connect (format "~a.~a.~a.~a"
+ (list-ref reg-list 1)
+ (list-ref reg-list 2)
+ (list-ref reg-list 3)
+ (list-ref reg-list 4))
+ (+ (* 256 pn1) pn2))])
+ (fprintf (ftp-connection-out tcp-ports) "TYPE I\r\n")
+ (ftp-check-response (ftp-connection-in tcp-ports)
+ (ftp-connection-out tcp-ports)
+ #"200" void (void))
+ (tcp-abandon-port tcp-data-out)
+ tcp-data))))
+
+;; Used where version 0.1a printed responses:
+(define (print-msg s ignore)
+ ;; (printf "~a\n" s)
+ (void))
+
+(define (ftp-establish-connection* in out username password)
+ (ftp-check-response in out #"220" print-msg (void))
+ (fprintf out "USER ~a\r\n" username)
+ (let ([no-password? (ftp-check-response
+ in out (list #"331" #"230")
+ (lambda (line 230?)
+ (or 230? (regexp-match #rx#"^230" line)))
+ #f)])
+ (unless no-password?
+ (fprintf out "PASS ~a\r\n" password)
+ (ftp-check-response in out #"230" void (void))))
+ (make-ftp-connection in out))
+
+(define (ftp-establish-connection server-address server-port username password)
+ (let-values ([(tcpin tcpout) (tcp-connect server-address server-port)])
+ (ftp-establish-connection* tcpin tcpout username password)))
+
+(define (ftp-close-connection tcp-ports)
+ (fprintf (ftp-connection-out tcp-ports) "QUIT\r\n")
+ (ftp-check-response (ftp-connection-in tcp-ports)
+ (ftp-connection-out tcp-ports)
+ #"221" void (void))
+ (close-input-port (ftp-connection-in tcp-ports))
+ (close-output-port (ftp-connection-out tcp-ports)))
+
+(define (ftp-cd ftp-ports new-dir)
+ (fprintf (ftp-connection-out ftp-ports) "CWD ~a\r\n" new-dir)
+ (ftp-check-response (ftp-connection-in ftp-ports)
+ (ftp-connection-out ftp-ports)
+ #"250" void (void)))
+
+(define re:dir-line
+ (regexp (string-append
+ "^(.)(.*) ((?i:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)"
+ " .* [0-9][0-9]:?[0-9][0-9]) (.*)$")))
+
+(define (ftp-directory-list tcp-ports [path #f])
+ (define tcp-data (establish-data-connection tcp-ports))
+ (if path
+ (fprintf (ftp-connection-out tcp-ports) "LIST ~a\r\n" path)
+ (fprintf (ftp-connection-out tcp-ports) "LIST\r\n"))
+ (ftp-check-response (ftp-connection-in tcp-ports)
+ (ftp-connection-out tcp-ports)
+ (list #"150" #"125") void (void))
+ (define lines (port->lines tcp-data))
+ (close-input-port tcp-data)
+ (ftp-check-response (ftp-connection-in tcp-ports)
+ (ftp-connection-out tcp-ports)
+ #"226" print-msg (void))
+ (for*/list ([l (in-list lines)]
+ [m (in-value (cond [(regexp-match re:dir-line l) => cdr]
+ [else #f]))]
+ #:when m)
+ (define size (cond [(and (equal? "-" (car m))
+ (regexp-match #rx"([0-9]+) *$" (cadr m)))
+ => cadr]
+ [else #f]))
+ (define r `(,(car m) ,@(cddr m)))
+ (if size `(,@r ,size) r)))
+
+(define (ftp-download-file tcp-ports folder filename)
+ ;; Save the file under the name tmp.file, rename it once download is
+ ;; complete this assures we don't over write any existing file without
+ ;; having a good file down
+ (let* ([tmpfile (make-temporary-file
+ (string-append
+ (regexp-replace
+ #rx"~"
+ (path->string (build-path folder "ftptmp"))
+ "~~")
+ "~a"))]
+ [new-file (open-output-file tmpfile #:exists 'replace)]
+ [tcp-data (establish-data-connection tcp-ports)])
+ (fprintf (ftp-connection-out tcp-ports) "RETR ~a\r\n" filename)
+ (ftp-check-response (ftp-connection-in tcp-ports)
+ (ftp-connection-out tcp-ports)
+ (list #"125" #"150") print-msg (void))
+ (copy-port tcp-data new-file)
+ (close-output-port new-file)
+ (close-input-port tcp-data)
+ (ftp-check-response (ftp-connection-in tcp-ports)
+ (ftp-connection-out tcp-ports)
+ #"226" print-msg (void))
+ (rename-file-or-directory tmpfile (build-path folder filename) #t)))
diff --git a/collects/net/imap.rkt b/collects/net/imap.rkt
index 6c02a92485..37e8b00be7 100644
--- a/collects/net/imap.rkt
+++ b/collects/net/imap.rkt
@@ -1,7 +1,12 @@
#lang racket/base
-(require racket/unit racket/contract "imap-sig.rkt" "imap-unit.rkt")
-(define-values/invoke-unit/infer imap@)
+(require racket/contract/base racket/tcp "private/rbtree.rkt")
+
+;; define the imap struct and its predicate here, for use in the contract, below
+(define-struct imap (r w exists recent unseen uidnext uidvalidity
+ expunges fetches new?)
+ #:mutable)
+(define (imap-connection? v) (imap? v))
(provide/contract
[imap-get-hierarchy-delimiter (imap-connection? . -> . bytes?)]
@@ -48,3 +53,546 @@
imap-create-mailbox
imap-mailbox-flags)
+
+(define debug-via-stdio? #f)
+
+(define eol (if debug-via-stdio? 'linefeed 'return-linefeed))
+
+(define (tag-eq? a b)
+ (or (eq? a b)
+ (and (symbol? a)
+ (symbol? b)
+ (string-ci=? (symbol->string a) (symbol->string b)))))
+
+(define field-names
+ (list (list 'uid (string->symbol "UID"))
+ (list 'header (string->symbol "RFC822.HEADER"))
+ (list 'body (string->symbol "RFC822.TEXT"))
+ (list 'size (string->symbol "RFC822.SIZE"))
+ (list 'flags (string->symbol "FLAGS"))))
+
+(define flag-names
+ (list (list 'seen (string->symbol "\\Seen"))
+ (list 'answered (string->symbol "\\Answered"))
+ (list 'flagged (string->symbol "\\Flagged"))
+ (list 'deleted (string->symbol "\\Deleted"))
+ (list 'draft (string->symbol "\\Draft"))
+ (list 'recent (string->symbol "\\Recent"))
+
+ (list 'noinferiors (string->symbol "\\Noinferiors"))
+ (list 'noselect (string->symbol "\\Noselect"))
+ (list 'marked (string->symbol "\\Marked"))
+ (list 'unmarked (string->symbol "\\Unmarked"))
+
+ (list 'hasnochildren (string->symbol "\\HasNoChildren"))
+ (list 'haschildren (string->symbol "\\HasChildren"))))
+
+(define (imap-flag->symbol f)
+ (or (ormap (lambda (a) (and (tag-eq? f (cadr a)) (car a))) flag-names)
+ f))
+
+(define (symbol->imap-flag s)
+ (cond [(assoc s flag-names) => cadr] [else s]))
+
+(define (log-warning . args)
+ ;; (apply printf args)
+ (void))
+(define log log-warning)
+
+(define make-msg-id
+ (let ([id 0])
+ (lambda ()
+ (begin0 (string->bytes/latin-1 (format "a~a " id))
+ (set! id (add1 id))))))
+
+(define (starts-with? l n)
+ (and (>= (bytes-length l) (bytes-length n))
+ (bytes=? n (subbytes l 0 (bytes-length n)))))
+
+(define (skip s n)
+ (subbytes s (if (number? n) n (bytes-length n))))
+
+(define (splice l sep)
+ (if (null? l)
+ ""
+ (format "~a~a"
+ (car l)
+ (apply string-append
+ (map (lambda (n) (format "~a~a" sep n)) (cdr l))))))
+
+(define (imap-read s r)
+ (let loop ([s s]
+ [r r]
+ [accum null]
+ [eol-k (lambda (accum) (reverse accum))]
+ [eop-k (lambda (s accum) (error 'imap-read "unxpected close parenthesis"))])
+ (cond
+ [(bytes=? #"" s)
+ (eol-k accum)]
+ [(char-whitespace? (integer->char (bytes-ref s 0)))
+ (loop (skip s 1) r accum eol-k eop-k)]
+ [else
+ (case (integer->char (bytes-ref s 0))
+ [(#\")
+ (let ([m (regexp-match #rx#"\"([^\"]*)\"(.*)" s)])
+ (if m
+ (loop (caddr m) r (cons (cadr m) accum) eol-k eop-k)
+ (error 'imap-read "didn't find end of quoted string in: ~a" s)))]
+ [(#\))
+ (eop-k (skip s 1) accum)]
+ [(#\() (letrec ([next-line
+ (lambda (accum)
+ (loop (read-bytes-line r eol) r
+ accum
+ next-line
+ finish-parens))]
+ [finish-parens
+ (lambda (s laccum)
+ (loop s r
+ (cons (reverse laccum) accum)
+ eol-k eop-k))])
+ (loop (skip s 1) r null next-line finish-parens))]
+ [(#\{) (let ([m (regexp-match #rx#"{([0-9]+)}(.*)" s)])
+ (cond
+ [(not m) (error 'imap-read "couldn't read {} number: ~a" s)]
+ [(not (bytes=? (caddr m) #"")) (error 'imap-read "{} not at end-of-line: ~a" s)]
+ [else
+ (loop #"" r
+ (cons (read-bytes (string->number
+ (bytes->string/latin-1 (cadr m)))
+ r)
+ accum)
+ eol-k eop-k)]))]
+ [else (let ([m (regexp-match #rx#"([^ (){}]+)(.*)" s)])
+ (if m
+ (loop (caddr m) r
+ (cons (let ([v (cadr m)])
+ (if (regexp-match #rx#"^[0-9]*$" v)
+ (string->number (bytes->string/latin-1 v))
+ (string->symbol (bytes->string/latin-1 v))))
+ accum)
+ eol-k eop-k)
+ (error 'imap-read "failure reading atom: ~a" s)))])])))
+
+(define (get-response r id info-handler continuation-handler)
+ (let loop ()
+ (let ([l (read-bytes-line r eol)])
+ (log "raw-reply: ~s\n" l)
+ (cond [(eof-object? l)
+ (error 'imap-send "unexpected end-of-file from server")]
+ [(and id (starts-with? l id))
+ (let ([reply (imap-read (skip l id) r)])
+ (log "response: ~a\n" reply)
+ reply)]
+ [(starts-with? l #"* ")
+ (let ([info (imap-read (skip l 2) r)])
+ (log "info: ~s\n" info)
+ (info-handler info))
+ (when id (loop))]
+ [(starts-with? l #"+ ")
+ (if (null? continuation-handler)
+ (error 'imap-send "unexpected continuation request: ~a" l)
+ ((car continuation-handler) loop (imap-read (skip l 2) r)))]
+ [else
+ (log-warning "warning: unexpected response for ~a: ~a\n" id l)
+ (when id (loop))]))))
+
+;; A cmd is
+;; * (box v) - send v literally via ~a
+;; * string or bytes - protect as necessary
+;; * (cons cmd null) - same as cmd
+;; * (cons cmd cmd) - send cmd, space, cmd
+
+(define (imap-send imap cmd info-handler . continuation-handler)
+ (let ([r (imap-r imap)]
+ [w (imap-w imap)]
+ [id (make-msg-id)])
+ (log "sending ~a~a\n" id cmd)
+ (fprintf w "~a" id)
+ (let loop ([cmd cmd])
+ (cond
+ [(box? cmd) (fprintf w "~a" (unbox cmd))]
+ [(string? cmd) (loop (string->bytes/utf-8 cmd))]
+ [(bytes? cmd)
+ (if (or (regexp-match #rx#"[ *\"\r\n]" cmd)
+ (equal? cmd #""))
+ (if (regexp-match #rx#"[\"\r\n]" cmd)
+ (begin
+ ;; Have to send size, then continue if the
+ ;; server consents
+ (fprintf w "{~a}\r\n" (bytes-length cmd))
+ (flush-output w)
+ (get-response r #f void (list (lambda (gloop data) (void))))
+ ;; Continue by writing the data
+ (write-bytes cmd w))
+ (fprintf w "\"~a\"" cmd))
+ (fprintf w "~a" cmd))]
+ [(and (pair? cmd) (null? (cdr cmd))) (loop (car cmd))]
+ [(pair? cmd) (begin (loop (car cmd))
+ (fprintf w " ")
+ (loop (cdr cmd)))]))
+ (fprintf w "\r\n")
+ (flush-output w)
+ (get-response r id (wrap-info-handler imap info-handler)
+ continuation-handler)))
+
+(define (check-ok reply)
+ (unless (and (pair? reply) (tag-eq? (car reply) 'OK))
+ (error 'check-ok "server error: ~s" reply)))
+
+(define (ok-tag-eq? i t)
+ (and (tag-eq? (car i) 'OK)
+ ((length i) . >= . 3)
+ (tag-eq? (cadr i) (string->symbol (format "[~a" t)))))
+
+(define (ok-tag-val i)
+ (let ([v (caddr i)])
+ (and (symbol? v)
+ (let ([v (symbol->string v)])
+ (regexp-match #rx"[]]$" v)
+ (string->number (substring v 0 (sub1 (string-length v))))))))
+
+(define (wrap-info-handler imap info-handler)
+ (lambda (i)
+ (when (and (list? i) ((length i) . >= . 2))
+ (cond
+ [(tag-eq? (cadr i) 'EXISTS)
+ (when (> (car i) (or (imap-exists imap) 0))
+ (set-imap-new?! imap #t))
+ (set-imap-exists! imap (car i))]
+ [(tag-eq? (cadr i) 'RECENT)
+ (set-imap-recent! imap (car i))]
+ [(tag-eq? (cadr i) 'EXPUNGE)
+ (let ([n (car i)])
+ (log "Recording expunge: ~s\n" n)
+ ;; add it to the tree of expunges
+ (expunge-insert! (imap-expunges imap) n)
+ ;; decrement exists count:
+ (set-imap-exists! imap (sub1 (imap-exists imap)))
+ ;; adjust ids for any remembered fetches:
+ (fetch-shift! (imap-fetches imap) n))]
+ [(tag-eq? (cadr i) 'FETCH)
+ (fetch-insert!
+ (imap-fetches imap)
+ ;; Convert result to assoc list:
+ (cons (car i)
+ (let ([new
+ (let loop ([l (caddr i)])
+ (if (null? l)
+ null
+ (cons (cons (car l) (cadr l))
+ (loop (cddr l)))))])
+ ;; Keep anything not overridden:
+ (let ([old (cdr (or (fetch-find (imap-fetches imap) (car i))
+ '(0)))])
+ (let loop ([old old][new new])
+ (cond
+ [(null? old) new]
+ [(assq (caar old) new)
+ (loop (cdr old) new)]
+ [else (loop (cdr old) (cons (car old) new))]))))))]
+ [(ok-tag-eq? i 'UIDNEXT)
+ (set-imap-uidnext! imap (ok-tag-val i))]
+ [(ok-tag-eq? i 'UIDVALIDITY)
+ (set-imap-uidvalidity! imap (ok-tag-val i))]
+ [(ok-tag-eq? i 'UNSEEN)
+ (set-imap-uidvalidity! imap (ok-tag-val i))]))
+ (info-handler i)))
+
+(define imap-port-number
+ (make-parameter 143
+ (lambda (v)
+ (unless (and (number? v)
+ (exact? v)
+ (integer? v)
+ (<= 1 v 65535))
+ (raise-type-error 'imap-port-number
+ "exact integer in [1,65535]"
+ v))
+ v)))
+
+(define (imap-connect* r w username password inbox)
+ (with-handlers ([void
+ (lambda (x)
+ (close-input-port r)
+ (close-output-port w)
+ (raise x))])
+
+ (let ([imap (make-imap r w #f #f #f #f #f
+ (new-tree) (new-tree) #f)])
+ (check-ok (imap-send imap "NOOP" void))
+ (let ([reply (imap-send imap (list "LOGIN" username password) void)])
+ (if (and (pair? reply) (tag-eq? 'NO (car reply)))
+ (error 'imap-connect
+ "username or password rejected by server: ~s" reply)
+ (check-ok reply)))
+ (let-values ([(init-count init-recent) (imap-reselect imap inbox)])
+ (values imap init-count init-recent)))))
+
+(define (imap-connect server username password inbox)
+ ;; => imap count-k recent-k
+ (let-values ([(r w)
+ (if debug-via-stdio?
+ (begin
+ (printf "stdin == ~a\n" server)
+ (values (current-input-port) (current-output-port)))
+ (tcp-connect server (imap-port-number)))])
+ (imap-connect* r w username password inbox)))
+
+(define (imap-reselect imap inbox)
+ (imap-selectish-command imap (list "SELECT" inbox) #t))
+
+(define (imap-examine imap inbox)
+ (imap-selectish-command imap (list "EXAMINE" inbox) #t))
+
+;; Used to return (values #f #f) if no change since last check?
+(define (imap-noop imap)
+ (imap-selectish-command imap "NOOP" #f))
+
+(define (imap-selectish-command imap cmd reset?)
+ (let ([init-count #f]
+ [init-recent #f])
+ (check-ok (imap-send imap cmd void))
+ (when reset?
+ (set-imap-expunges! imap (new-tree))
+ (set-imap-fetches! imap (new-tree))
+ (set-imap-new?! imap #f))
+ (values (imap-exists imap) (imap-recent imap))))
+
+(define (imap-status imap inbox flags)
+ (unless (and (list? flags)
+ (andmap (lambda (s)
+ (memq s '(messages recent uidnext uidvalidity unseen)))
+ flags))
+ (raise-type-error 'imap-status "list of status flag symbols" flags))
+ (let ([results null])
+ (check-ok (imap-send imap (list "STATUS" inbox (box (format "~a" flags)))
+ (lambda (i)
+ (when (and (list? i) (= 3 (length i))
+ (tag-eq? (car i) 'STATUS))
+ (set! results (caddr i))))))
+ (map (lambda (f)
+ (let loop ([l results])
+ (cond
+ [(or (null? l) (null? (cdr l))) #f]
+ [(tag-eq? f (car l)) (cadr l)]
+ [else (loop (cdr l))])))
+ flags)))
+
+(define (imap-poll imap)
+ (when (and ;; Check for async messages from the server
+ (char-ready? (imap-r imap))
+ ;; It has better start with "*"...
+ (= (peek-byte (imap-r imap)) (char->integer #\*)))
+ ;; May set fields in `imap':
+ (get-response (imap-r imap) #f (wrap-info-handler imap void) null)
+ (void)))
+
+(define (imap-get-updates imap)
+ (no-expunges 'imap-updates imap)
+ (let ([l (fetch-tree->list (imap-fetches imap))])
+ (set-imap-fetches! imap (new-tree))
+ l))
+
+(define (imap-pending-updates? imap)
+ (not (tree-empty? (imap-fetches imap))))
+
+(define (imap-get-expunges imap)
+ (let ([l (expunge-tree->list (imap-expunges imap))])
+ (set-imap-expunges! imap (new-tree))
+ l))
+
+(define (imap-pending-expunges? imap)
+ (not (tree-empty? (imap-expunges imap))))
+
+(define (imap-reset-new! imap)
+ (set-imap-new?! imap #f))
+
+(define (imap-messages imap)
+ (imap-exists imap))
+
+(define (imap-disconnect imap)
+ (let ([r (imap-r imap)]
+ [w (imap-w imap)])
+ (check-ok (imap-send imap "LOGOUT" void))
+ (close-input-port r)
+ (close-output-port w)))
+
+(define (imap-force-disconnect imap)
+ (let ([r (imap-r imap)]
+ [w (imap-w imap)])
+ (close-input-port r)
+ (close-output-port w)))
+
+(define (no-expunges who imap)
+ (unless (tree-empty? (imap-expunges imap))
+ (raise-mismatch-error who "session has pending expunge reports: " imap)))
+
+(define (msg-set msgs)
+ (apply
+ string-append
+ (let loop ([prev #f][msgs msgs])
+ (cond
+ [(null? msgs) null]
+ [(and prev
+ (pair? (cdr msgs))
+ (= (add1 prev) (car msgs)))
+ (loop (car msgs) (cdr msgs))]
+ [prev (cons (format ":~a," prev)
+ (loop #f msgs))]
+ [(null? (cdr msgs)) (list (format "~a" (car msgs)))]
+ [(= (add1 (car msgs)) (cadr msgs))
+ (cons (format "~a" (car msgs))
+ (loop (car msgs) (cdr msgs)))]
+ [else (cons (format "~a," (car msgs))
+ (loop #f (cdr msgs)))]))))
+
+(define (imap-get-messages imap msgs field-list)
+ (no-expunges 'imap-get-messages imap)
+ (when (or (not (list? msgs))
+ (not (andmap integer? msgs)))
+ (raise-type-error 'imap-get-messages "non-empty message list" msgs))
+ (when (or (null? field-list)
+ (not (list? field-list))
+ (not (andmap (lambda (f) (assoc f field-names)) field-list)))
+ (raise-type-error 'imap-get-messages "non-empty field list" field-list))
+
+ (if (null? msgs)
+ null
+ (begin
+ ;; FETCH request adds info to `(imap-fectches imap)':
+ (imap-send imap
+ (list "FETCH"
+ (box (msg-set msgs))
+ (box
+ (format "(~a)"
+ (splice (map (lambda (f)
+ (cadr (assoc f field-names)))
+ field-list)
+ " "))))
+ void)
+ ;; Sort out the collected info:
+ (let ([flds (map (lambda (f) (cadr (assoc f field-names)))
+ field-list)])
+ (begin0
+ ;; For each msg, try to get each field value:
+ (map
+ (lambda (msg)
+ (let ([m (or (fetch-find (imap-fetches imap) msg)
+ (error 'imap-get-messages "no result for message ~a" msg))])
+ (let loop ([flds flds][m (cdr m)])
+ (cond
+ [(null? flds)
+ (if (null? m)
+ (fetch-delete! (imap-fetches imap) msg)
+ (fetch-insert! (imap-fetches imap) (cons msg m)))
+ null]
+ [else
+ (let ([a (assoc (car flds) m)])
+ (cons (and a (cdr a))
+ (loop (cdr flds) (if a (remq a m) m))))]))))
+ msgs))))))
+
+(define (imap-store imap mode msgs flags)
+ (no-expunges 'imap-store imap)
+ (check-ok
+ (imap-send imap
+ (list "STORE"
+ (box (msg-set msgs))
+ (case mode
+ [(+) "+FLAGS.SILENT"]
+ [(-) "-FLAGS.SILENT"]
+ [(!) "FLAGS.SILENT"]
+ [else (raise-type-error 'imap-store
+ "mode: '!, '+, or '-" mode)])
+ (box (format "~a" flags)))
+ void)))
+
+(define (imap-copy imap msgs dest-mailbox)
+ (no-expunges 'imap-copy imap)
+ (check-ok
+ (imap-send imap (list "COPY" (box (msg-set msgs)) dest-mailbox) void)))
+
+(define (imap-append imap dest-mailbox msg)
+ (no-expunges 'imap-append imap)
+ (let ([msg (if (bytes? msg) msg (string->bytes/utf-8 msg))])
+ (check-ok
+ (imap-send imap (list "APPEND"
+ dest-mailbox
+ (box "(\\Seen)")
+ (box (format "{~a}" (bytes-length msg))))
+ void
+ (lambda (loop contin)
+ (fprintf (imap-w imap) "~a\r\n" msg)
+ (loop))))))
+
+(define (imap-expunge imap)
+ (check-ok (imap-send imap "EXPUNGE" void)))
+
+(define (imap-mailbox-exists? imap mailbox)
+ (let ([exists? #f])
+ (check-ok (imap-send imap
+ (list "LIST" "" mailbox)
+ (lambda (i)
+ (when (and (pair? i) (tag-eq? (car i) 'LIST))
+ (set! exists? #t)))))
+ exists?))
+
+(define (imap-create-mailbox imap mailbox)
+ (check-ok (imap-send imap (list "CREATE" mailbox) void)))
+
+(define (imap-get-hierarchy-delimiter imap)
+ (let ([result #f])
+ (check-ok
+ (imap-send imap (list "LIST" "" "")
+ (lambda (i)
+ (when (and (pair? i) (tag-eq? (car i) 'LIST))
+ (set! result (caddr i))))))
+ result))
+
+(define imap-list-child-mailboxes
+ (case-lambda
+ [(imap mailbox)
+ (imap-list-child-mailboxes imap mailbox #f)]
+ [(imap mailbox raw-delimiter)
+ (let* ([delimiter (or raw-delimiter (imap-get-hierarchy-delimiter imap))]
+ [mailbox-name (and mailbox (bytes-append mailbox delimiter))]
+ [pattern (if mailbox
+ (bytes-append mailbox-name #"%")
+ #"%")])
+ (map (lambda (p)
+ (list (car p)
+ (cond
+ [(symbol? (cadr p))
+ (string->bytes/utf-8 (symbol->string (cadr p)))]
+ [(string? (cadr p))
+ (string->bytes/utf-8 (symbol->string (cadr p)))]
+ [(bytes? (cadr p))
+ (cadr p)])))
+ (imap-list-mailboxes imap pattern mailbox-name)))]))
+
+(define (imap-mailbox-flags imap mailbox)
+ (let ([r (imap-list-mailboxes imap mailbox #f)])
+ (if (= (length r) 1)
+ (caar r)
+ (error 'imap-mailbox-flags "could not get flags for ~s (~a)"
+ mailbox
+ (if (null? r) "no matches" "multiple matches")))))
+
+(define (imap-list-mailboxes imap pattern except)
+ (let* ([sub-folders null])
+ (check-ok
+ (imap-send imap (list "LIST" "" pattern)
+ (lambda (x)
+ (when (and (pair? x)
+ (tag-eq? (car x) 'LIST))
+ (let* ([flags (cadr x)]
+ [name (cadddr x)]
+ [bytes-name (if (symbol? name)
+ (string->bytes/utf-8 (symbol->string name))
+ name)])
+ (unless (and except
+ (bytes=? bytes-name except))
+ (set! sub-folders
+ (cons (list flags name) sub-folders))))))))
+ (reverse sub-folders)))
diff --git a/collects/net/mime.rkt b/collects/net/mime.rkt
index 9714637433..508a656597 100644
--- a/collects/net/mime.rkt
+++ b/collects/net/mime.rkt
@@ -23,21 +23,725 @@
;;; Author: Francisco Solsona
;;
-;;
-;; Commentary:
+;; Commentary: MIME support for PLT Scheme: an implementation of
+;; rfc2045, rfc2046, rfc2047, rfc2048, and rfc2049.
#lang racket/base
-(require racket/unit
- "mime-sig.rkt" "mime-unit.rkt" "qp.rkt" "base64.rkt" "head.rkt")
-;(define-unit-from-context base64@ base64^)
-;(define-unit-from-context qp@ qp^)
-;(define-unit-from-context head@ head^)
+(require racket/port "mime-util.rkt" "qp.rkt" "base64.rkt" "head.rkt")
-(define-values/invoke-unit/infer
- (export mime^)
- (link mime@))
+(provide
+ ;; -- exceptions raised --
+ (struct-out mime-error)
+ (struct-out unexpected-termination)
+ (struct-out missing-multipart-boundary-parameter)
+ (struct-out malformed-multipart-entity)
+ (struct-out empty-mechanism)
+ (struct-out empty-type)
+ (struct-out empty-subtype)
+ (struct-out empty-disposition-type)
-(provide-signature-elements mime^)
+ ;; -- basic mime structures --
+ (struct-out message)
+ (struct-out entity)
+ (struct-out disposition)
-;;; mime.rkt ends here
+ ;; -- mime methods --
+ mime-analyze)
+
+;; Constants:
+(define discrete-alist
+ '(("text" . text)
+ ("image" . image)
+ ("audio" . audio)
+ ("video" . video)
+ ("application" . application)))
+
+(define disposition-alist
+ '(("inline" . inline)
+ ("attachment" . attachment)
+ ("file" . attachment) ;; This is used (don't know why) by
+ ;; multipart/form-data
+ ("messagetext" . inline)
+ ("form-data" . form-data)))
+
+(define composite-alist
+ '(("message" . message)
+ ("multipart" . multipart)))
+
+(define mechanism-alist
+ '(("7bit" . 7bit)
+ ("8bit" . 8bit)
+ ("binary" . binary)
+ ("quoted-printable" . quoted-printable)
+ ("base64" . base64)))
+
+(define ietf-extensions '())
+(define iana-extensions
+ '(;; text
+ ("plain" . plain)
+ ("html" . html)
+ ("enriched" . enriched) ; added 5/2005 - probably not iana
+ ("richtext" . richtext)
+ ("tab-separated-values" . tab-separated-values)
+ ;; Multipart
+ ("mixed" . mixed)
+ ("alternative" . alternative)
+ ("digest" . digest)
+ ("parallel" . parallel)
+ ("appledouble" . appledouble)
+ ("header-set" . header-set)
+ ("form-data" . form-data)
+ ;; Message
+ ("rfc822" . rfc822)
+ ("partial" . partial)
+ ("external-body" . external-body)
+ ("news" . news)
+ ;; Application
+ ("octet-stream" . octet-stream)
+ ("postscript" . postscript)
+ ("oda" . oda)
+ ("atomicmail" . atomicmail)
+ ("andrew-inset" . andrew-inset)
+ ("slate" . slate)
+ ("wita" . wita)
+ ("dec-dx" . dec-dx)
+ ("dca-rf" . dca-rf)
+ ("activemessage" . activemessage)
+ ("rtf" . rtf)
+ ("applefile" . applefile)
+ ("mac-binhex40" . mac-binhex40)
+ ("news-message-id" . news-message-id)
+ ("news-transmissio" . news-transmissio)
+ ("wordperfect5.1" . wordperfect5.1)
+ ("pdf" . pdf)
+ ("zip" . zip)
+ ("macwritei" . macwritei)
+ ;; "image"
+ ("jpeg" . jpeg)
+ ("gif" . gif)
+ ("ief" . ief)
+ ("tiff" . tiff)
+ ;; "audio"
+ ("basic" . basic)
+ ;; "video" .
+ ("mpeg" . mpeg)
+ ("quicktime" . quicktime)))
+
+;; Basic structures
+(define-struct message (version entity fields)
+ #:mutable)
+(define-struct entity
+ (type subtype charset encoding disposition params id description other
+ fields parts body)
+ #:mutable)
+(define-struct disposition
+ (type filename creation modification read size params)
+ #:mutable)
+
+;; Exceptions
+(define-struct mime-error ())
+(define-struct (unexpected-termination mime-error) (msg))
+(define-struct (missing-multipart-boundary-parameter mime-error) ())
+(define-struct (malformed-multipart-entity mime-error) (msg))
+(define-struct (empty-mechanism mime-error) ())
+(define-struct (empty-type mime-error) ())
+(define-struct (empty-subtype mime-error) ())
+(define-struct (empty-disposition-type mime-error) ())
+
+;; *************************************
+;; Practical stuff, aka MIME in action:
+;; *************************************
+(define CRLF (format "~a~a" #\return #\newline))
+(define CRLF-binary "=0D=0A") ;; quoted printable representation
+
+;; get-headers : input-port -> string
+;; returns the header part of a message/part conforming to rfc822, and
+;; rfc2045.
+(define (get-headers in)
+ (let loop ([headers ""] [ln (read-line in 'any)])
+ (cond [(eof-object? ln)
+ ;; (raise (make-unexpected-termination "eof reached! while parsing headers"))
+ (warning "premature eof while parsing headers")
+ headers]
+ [(string=? ln "") headers]
+ [else
+ ;; Quoting rfc822:
+ ;; " Headers occur before the message body and are
+ ;; terminated by a null line (i.e., two contiguous
+ ;; CRLFs)."
+ ;; That is: Two empty lines. But most MUAs seem to count
+ ;; the CRLF ending the last field (header) as the first
+ ;; CRLF of the null line.
+ (loop (string-append headers ln CRLF)
+ (read-line in 'any))])))
+
+(define (make-default-disposition)
+ (make-disposition
+ 'inline ;; type
+ "" ;; filename
+ #f ;; creation
+ #f ;; modification
+ #f ;; read
+ #f ;; size
+ null ;; params
+ ))
+
+(define (make-default-entity)
+ (make-entity
+ 'text ;; type
+ 'plain ;; subtype
+ 'us-ascii ;; charset
+ '7bit ;; encoding
+ (make-default-disposition) ;; disposition
+ null ;; params
+ "" ;; id
+ "" ;; description
+ null ;; other MIME fields (MIME-extension-fields)
+ null ;; fields
+ null ;; parts
+ null ;; body
+ ))
+
+(define (make-default-message)
+ (make-message 1.0 (make-default-entity) null))
+
+(define (mime-decode entity input)
+ (set-entity-body!
+ entity
+ (case (entity-encoding entity)
+ [(quoted-printable)
+ (lambda (output)
+ (qp-decode-stream input output))]
+ [(base64)
+ (lambda (output)
+ (base64-decode-stream input output))]
+ [else ;; 7bit, 8bit, binary
+ (lambda (output)
+ (copy-port input output))])))
+
+(define (mime-analyze input [part #f])
+ (let* ([iport (if (bytes? input)
+ (open-input-bytes input)
+ input)]
+ [headers (get-headers iport)]
+ [msg (if part
+ (MIME-part-headers headers)
+ (MIME-message-headers headers))]
+ [entity (message-entity msg)])
+ ;; OK we have in msg a MIME-message structure, lets see what we have:
+ (case (entity-type entity)
+ [(text image audio video application)
+ ;; decode part, and save port and thunk
+ (mime-decode entity iport)]
+ [(message multipart)
+ (let ([boundary (entity-boundary entity)])
+ (when (not boundary)
+ (when (eq? 'multipart (entity-type entity))
+ (raise (make-missing-multipart-boundary-parameter))))
+ (set-entity-parts! entity
+ (map (lambda (part)
+ (mime-analyze part #t))
+ (if boundary
+ (multipart-body iport boundary)
+ (list iport)))))]
+ [else
+ ;; Unrecognized type, you're on your own! (sorry)
+ (mime-decode entity iport)])
+ ;; return mime structure
+ msg))
+
+(define (entity-boundary entity)
+ (let* ([params (entity-params entity)]
+ [ans (assoc "boundary" params)])
+ (and ans (cdr ans))))
+
+;; *************************************************
+;; MIME Specific: rfc2045-2049, and rfc0822, rfc2183
+;; *************************************************
+
+;;multipart-body := [preamble CRLF]
+;; dash-boundary transport-padding CRLF
+;; body-part *encapsulation
+;; close-delimiter transport-padding
+;; [CRLF epilogue]
+;; Returns a list of input ports, each one containing the correspongind part.
+(define (multipart-body input boundary)
+ (let* ([make-re (lambda (prefix)
+ (regexp (string-append prefix "--" (regexp-quote boundary) "(--)?\r\n")))]
+ [re (make-re "\r\n")])
+ (letrec ([eat-part (lambda ()
+ (let-values ([(pin pout) (make-pipe)])
+ (let ([m (regexp-match re input 0 #f pout)])
+ (cond
+ [(not m)
+ (close-output-port pout)
+ (values pin ;; part
+ #f ;; close-delimiter?
+ #t ;; eof reached?
+ )]
+ [(cadr m)
+ (close-output-port pout)
+ (values pin #t #f)]
+ [else
+ (close-output-port pout)
+ (values pin #f #f)]))))])
+ ;; pre-amble is allowed to be completely empty:
+ (if (regexp-match-peek (make-re "^") input)
+ ;; No \r\f before first separator:
+ (read-line input)
+ ;; non-empty preamble:
+ (eat-part))
+ (let loop ()
+ (let-values ([(part close? eof?) (eat-part)])
+ (cond [close? (list part)]
+ [eof? (list part)]
+ [else (cons part (loop))]))))))
+
+;; MIME-message-headers := entity-headers
+;; fields
+;; version CRLF
+;; ; The ordering of the header
+;; ; fields implied by this BNF
+;; ; definition should be ignored.
+(define (MIME-message-headers headers)
+ (let ([message (make-default-message)])
+ (entity-headers headers message #t)
+ message))
+
+;; MIME-part-headers := entity-headers
+;; [ fields ]
+;; ; Any field not beginning with
+;; ; "content-" can have no defined
+;; ; meaning and may be ignored.
+;; ; The ordering of the header
+;; ; fields implied by this BNF
+;; ; definition should be ignored.
+(define (MIME-part-headers headers)
+ (let ([message (make-default-message)])
+ (entity-headers headers message #f)
+ message))
+
+;; entity-headers := [ content CRLF ]
+;; [ encoding CRLF ]
+;; [ id CRLF ]
+;; [ description CRLF ]
+;; *( MIME-extension-field CRLF )
+(define (entity-headers headers message version?)
+ (let ([entity (message-entity message)])
+ (let-values ([(mime non-mime) (get-fields headers)])
+ (let loop ([fields mime])
+ (unless (null? fields)
+ ;; Process MIME field
+ (let ([trimmed-h (trim-comments (car fields))])
+ (or (and version? (version trimmed-h message))
+ (content trimmed-h entity)
+ (encoding trimmed-h entity)
+ (dispositione trimmed-h entity)
+ (id trimmed-h entity)
+ (description trimmed-h entity)
+ (MIME-extension-field trimmed-h entity))
+ ;; keep going
+ (loop (cdr fields)))))
+ ;; NON-mime headers (or semantically incorrect). In order to make
+ ;; this implementation of rfc2045 robuts, we will save the header in
+ ;; the fields field of the message struct:
+ (set-message-fields! message non-mime)
+ ;; Return message
+ message)))
+
+(define (get-fields headers)
+ (let ([mime null] [non-mime null])
+ (letrec ([store-field
+ (lambda (f)
+ (unless (string=? f "")
+ (if (mime-header? f)
+ (set! mime (append mime (list (trim-spaces f))))
+ (set! non-mime (append non-mime (list (trim-spaces f)))))))])
+ (let ([fields (extract-all-fields headers)])
+ (for-each (lambda (p)
+ (store-field (format "~a: ~a" (car p) (cdr p))))
+ fields))
+ (values mime non-mime))))
+
+(define re:content #rx"^(?i:content-)")
+(define re:mime #rx"^(?i:mime-version):")
+
+(define (mime-header? h)
+ (or (regexp-match? re:content h)
+ (regexp-match? re:mime h)))
+
+;;; Headers
+;;; Content-type follows this BNF syntax:
+;; content := "Content-Type" ":" type "/" subtype
+;; *(";" parameter)
+;; ; Matching of media type and subtype
+;; ; is ALWAYS case-insensitive.
+(define re:content-type #rx"^(?i:content-type):([^/]+)/([^/]+)$")
+(define (content header entity)
+ (let* ([params (string-tokenizer #\; header)]
+ [one re:content-type]
+ [h (trim-all-spaces (car params))]
+ [target (regexp-match one h)]
+ [old-param (entity-params entity)])
+ (and target
+ (set-entity-type! entity
+ (type (regexp-replace one h "\\1"))) ;; type
+ (set-entity-subtype! entity
+ (subtype (regexp-replace one h "\\2"))) ;; subtype
+ (set-entity-params!
+ entity
+ (append old-param
+ (let loop ([p (cdr params)] ;; parameters
+ [ans null])
+ (cond [(null? p) ans]
+ [else
+ (let ([par-pair (parameter (trim-all-spaces (car p)))])
+ (cond [par-pair
+ (when (string=? (car par-pair) "charset")
+ (set-entity-charset! entity (cdr par-pair)))
+ (loop (cdr p) (append ans (list par-pair)))]
+ [else
+ (warning "Invalid parameter for Content-Type: `~a'" (car p))
+ ;; go on...
+ (loop (cdr p) ans)]))])))))))
+
+;; From rfc2183 Content-Disposition
+;; disposition := "Content-Disposition" ":"
+;; disposition-type
+;; *(";" disposition-parm)
+(define re:content-disposition #rx"^(?i:content-disposition):(.+)$")
+(define (dispositione header entity)
+ (let* ([params (string-tokenizer #\; header)]
+ [reg re:content-disposition]
+ [h (trim-all-spaces (car params))]
+ [target (regexp-match reg h)]
+ [disp-struct (entity-disposition entity)])
+ (and target
+ (set-disposition-type!
+ disp-struct
+ (disp-type (regexp-replace reg h "\\1")))
+ (disp-params (cdr params) disp-struct))))
+
+;; version := "MIME-Version" ":" 1*DIGIT "." 1*DIGIT
+(define re:mime-version #rx"^(?i:MIME-Version):([0-9]+)\\.([0-9]+)$")
+(define (version header message)
+ (let* ([reg re:mime-version]
+ [h (trim-all-spaces header)]
+ [target (regexp-match reg h)])
+ (and target
+ (set-message-version!
+ message
+ (string->number (regexp-replace reg h "\\1.\\2"))))))
+
+;; description := "Content-Description" ":" *text
+(define re:content-description #rx"^(?i:content-description):[ \t\r\n]*(.*)$")
+(define (description header entity)
+ (let* ([reg re:content-description]
+ [target (regexp-match reg header)])
+ (and target
+ (set-entity-description!
+ entity
+ (trim-spaces (regexp-replace reg header "\\1"))))))
+
+;; encoding := "Content-Transfer-Encoding" ":" mechanism
+(define re:content-transfer-encoding #rx"^(?i:content-transfer-encoding):(.+)$")
+(define (encoding header entity)
+ (let* ([reg re:content-transfer-encoding]
+ [h (trim-all-spaces header)]
+ [target (regexp-match reg h)])
+ (and target
+ (set-entity-encoding!
+ entity
+ (mechanism (regexp-replace reg h "\\1"))))))
+
+;; id := "Content-ID" ":" msg-id
+(define re:content-id #rx"^(?i:content-id):(.+)$")
+(define (id header entity)
+ (let* ([reg re:content-id]
+ [h (trim-all-spaces header)]
+ [target (regexp-match reg h)])
+ (and target
+ (set-entity-id!
+ entity
+ (msg-id (regexp-replace reg h "\\1"))))))
+
+;; From rfc822:
+;; msg-id = "<" addr-spec ">" ; Unique message id
+;; addr-spec = local-part "@" domain ; global address
+;; local-part = word *("." word) ; uninterpreted
+;; ; case-preserved
+;; domain = sub-domain *("." sub-domain)
+;; sub-domain = domain-ref / domain-literal
+;; domain-literal = "[" *(dtext / quoted-pair) "]"
+;; domain-ref = atom ; symbolic reference
+(define (msg-id str)
+ (let* ([r (regexp "^<[^@>]+@[^.]+(\\.[^.]+)*>$")]
+ [ans (regexp-match r str)])
+ (if ans
+ str
+ (begin (warning "Invalid msg-id: ~a" str) str))))
+
+;; mechanism := "7bit" / "8bit" / "binary" /
+;; "quoted-printable" / "base64" /
+;; ietf-token / x-token
+(define (mechanism mech)
+ (if (not mech)
+ (raise (make-empty-mechanism))
+ (let ([val (assoc (lowercase mech) mechanism-alist)])
+ (or (and val (cdr val))
+ (ietf-token mech)
+ (x-token mech)))))
+
+;; MIME-extension-field :=
+;;
+(define (MIME-extension-field header entity)
+ (let* ([reg (regexp "^[Cc]ontent-(.+):[ \t]*(.+)$")]
+ [target (regexp-match reg header)])
+ (and target
+ (set-entity-other!
+ entity
+ (append (entity-other entity)
+ (list (cons (regexp-replace reg header "\\1")
+ (trim-spaces (regexp-replace reg header "\\2")))))))))
+
+;; type := discrete-type / composite-type
+(define (type value)
+ (if (not value)
+ (raise (make-empty-type))
+ (or (discrete-type value)
+ (composite-type value))))
+
+;; disposition-type := "inline" / "attachment" / extension-token
+(define (disp-type value)
+ (if (not value)
+ (raise (make-empty-disposition-type))
+ (let ([val (assoc (lowercase (trim-spaces value)) disposition-alist)])
+ (if val (cdr val) (extension-token value)))))
+
+;; discrete-type := "text" / "image" / "audio" / "video" /
+;; "application" / extension-token
+(define (discrete-type value)
+ (let ([val (assoc (lowercase (trim-spaces value)) discrete-alist)])
+ (if val (cdr val) (extension-token value))))
+
+;; composite-type := "message" / "multipart" / extension-token
+(define (composite-type value)
+ (let ([val (assoc (lowercase (trim-spaces value)) composite-alist)])
+ (if val (cdr val) (extension-token value))))
+
+;; extension-token := ietf-token / x-token
+(define (extension-token value)
+ (or (ietf-token value)
+ (x-token value)))
+
+;; ietf-token :=
+(define (ietf-token value)
+ (let ([ans (assoc (lowercase (trim-spaces value)) ietf-extensions)])
+ (and ans (cdr ans))))
+
+;; Directly from RFC 1700:
+;; Type Subtype Description Reference
+;; ---- ------- ----------- ---------
+;; text plain [RFC1521,NSB]
+;; richtext [RFC1521,NSB]
+;; tab-separated-values [Paul Lindner]
+;;
+;; multipart mixed [RFC1521,NSB]
+;; alternative [RFC1521,NSB]
+;; digest [RFC1521,NSB]
+;; parallel [RFC1521,NSB]
+;; appledouble [MacMime,Patrik Faltstrom]
+;; header-set [Dave Crocker]
+;;
+;; message rfc822 [RFC1521,NSB]
+;; partial [RFC1521,NSB]
+;; external-body [RFC1521,NSB]
+;; news [RFC 1036, Henry Spencer]
+;;
+;; application octet-stream [RFC1521,NSB]
+;; postscript [RFC1521,NSB]
+;; oda [RFC1521,NSB]
+;; atomicmail [atomicmail,NSB]
+;; andrew-inset [andrew-inset,NSB]
+;; slate [slate,terry crowley]
+;; wita [Wang Info Transfer,Larry Campbell]
+;; dec-dx [Digital Doc Trans, Larry Campbell]
+;; dca-rft [IBM Doc Content Arch, Larry Campbell]
+;; activemessage [Ehud Shapiro]
+;; rtf [Paul Lindner]
+;; applefile [MacMime,Patrik Faltstrom]
+;; mac-binhex40 [MacMime,Patrik Faltstrom]
+;; news-message-id [RFC1036, Henry Spencer]
+;; news-transmission [RFC1036, Henry Spencer]
+;; wordperfect5.1 [Paul Lindner]
+;; pdf [Paul Lindner]
+;; zip [Paul Lindner]
+;; macwriteii [Paul Lindner]
+;; msword [Paul Lindner]
+;; remote-printing [RFC1486,MTR]
+;;
+;; image jpeg [RFC1521,NSB]
+;; gif [RFC1521,NSB]
+;; ief Image Exchange Format [RFC1314]
+;; tiff Tag Image File Format [MTR]
+;;
+;; audio basic [RFC1521,NSB]
+;;
+;; video mpeg [RFC1521,NSB]
+;; quicktime [Paul Lindner]
+
+;; x-token :=
+(define (x-token value)
+ (let* ([r #rx"^[xX]-(.*)"]
+ [h (trim-spaces value)]
+ [ans (regexp-match r h)])
+ (and ans
+ (token (regexp-replace r h "\\1"))
+ h)))
+
+;; subtype := extension-token / iana-token
+(define (subtype value)
+ (if (not value)
+ (raise (make-empty-subtype))
+ (or (extension-token value)
+ (iana-token value))))
+
+;; iana-token :=
+(define (iana-token value)
+ (let ([ans (assoc (lowercase (trim-spaces value)) iana-extensions)])
+ (and ans (cdr ans))))
+
+;; parameter := attribute "=" value
+(define re:parameter (regexp "([^=]+)=(.+)"))
+(define (parameter par)
+ (let* ([r re:parameter]
+ [att (attribute (regexp-replace r par "\\1"))]
+ [val (value (regexp-replace r par "\\2"))])
+ (if (regexp-match r par)
+ (cons (if att (lowercase att) "???") val)
+ (cons "???" par))))
+
+;; value := token / quoted-string
+(define (value val)
+ (or (token val)
+ (quoted-string val)
+ val))
+
+;; token := 1*
+;; tspecials := "(" / ")" / "<" / ">" / "@" /
+;; "," / ";" / ":" / "\" / <">
+;; "/" / "[" / "]" / "?" / "="
+;; ; Must be in quoted-string,
+;; ; to use within parameter values
+(define (token value)
+ (let* ([tspecials (regexp "[^][()<>@,;:\\\"/?= ]+")]
+ [ans (regexp-match tspecials value)])
+ (and ans
+ (string=? value (car ans))
+ (car ans))))
+
+;; attribute := token
+;; ; Matching of attributes
+;; ; is ALWAYS case-insensitive.
+(define attribute token)
+
+(define re:quotes (regexp "\"(.+)\""))
+(define (quoted-string str)
+ (let* ([quotes re:quotes]
+ [ans (regexp-match quotes str)])
+ (and ans (regexp-replace quotes str "\\1"))))
+
+;; disposition-parm := filename-parm
+;; / creation-date-parm
+;; / modification-date-parm
+;; / read-date-parm
+;; / size-parm
+;; / parameter
+;;
+;; filename-parm := "filename" "=" value
+;;
+;; creation-date-parm := "creation-date" "=" quoted-date-time
+;;
+;; modification-date-parm := "modification-date" "=" quoted-date-time
+;;
+;; read-date-parm := "read-date" "=" quoted-date-time
+;;
+;; size-parm := "size" "=" 1*DIGIT
+(define (disp-params lst disp)
+ (let loop ([lst lst])
+ (unless (null? lst)
+ (let* ([p (parameter (trim-all-spaces (car lst)))]
+ [parm (car p)]
+ [value (cdr p)])
+ (cond [(string=? parm "filename")
+ (set-disposition-filename! disp value)]
+ [(string=? parm "creation-date")
+ (set-disposition-creation!
+ disp
+ (disp-quoted-data-time value))]
+ [(string=? parm "modification-date")
+ (set-disposition-modification!
+ disp
+ (disp-quoted-data-time value))]
+ [(string=? parm "read-date")
+ (set-disposition-read!
+ disp
+ (disp-quoted-data-time value))]
+ [(string=? parm "size")
+ (set-disposition-size!
+ disp
+ (string->number value))]
+ [else
+ (set-disposition-params!
+ disp
+ (append (disposition-params disp) (list p)))])
+ (loop (cdr lst))))))
+
+;; date-time = [ day "," ] date time ; dd mm yy
+;; ; hh:mm:ss zzz
+;;
+;; day = "Mon" / "Tue" / "Wed" / "Thu"
+;; / "Fri" / "Sat" / "Sun"
+;;
+;; date = 1*2DIGIT month 2DIGIT ; day month year
+;; ; e.g. 20 Jun 82
+;;
+;; month = "Jan" / "Feb" / "Mar" / "Apr"
+;; / "May" / "Jun" / "Jul" / "Aug"
+;; / "Sep" / "Oct" / "Nov" / "Dec"
+;;
+;; time = hour zone ; ANSI and Military
+;;
+;; hour = 2DIGIT ":" 2DIGIT [":" 2DIGIT]
+;; ; 00:00:00 - 23:59:59
+;;
+;; zone = "UT" / "GMT" ; Universal Time
+;; ; North American : UT
+;; / "EST" / "EDT" ; Eastern: - 5/ - 4
+;; / "CST" / "CDT" ; Central: - 6/ - 5
+;; / "MST" / "MDT" ; Mountain: - 7/ - 6
+;; / "PST" / "PDT" ; Pacific: - 8/ - 7
+;; / 1ALPHA ; Military: Z = UT;
+;; ; A:-1; (J not used)
+;; ; M:-12; N:+1; Y:+12
+;; / ( ("+" / "-") 4DIGIT ) ; Local differential
+;; ; hours+min. (HHMM)
+(define date-time
+ (lambda (str)
+ ;; Fix Me: I have to return a date structure, or time in seconds.
+ str))
+
+;; quoted-date-time := quoted-string
+;; ; contents MUST be an RFC 822 `date-time'
+;; ; numeric timezones (+HHMM or -HHMM) MUST be used
+
+(define disp-quoted-data-time date-time)
diff --git a/collects/net/nntp.rkt b/collects/net/nntp.rkt
index 816aa5047d..da8ae8dfdb 100644
--- a/collects/net/nntp.rkt
+++ b/collects/net/nntp.rkt
@@ -1,6 +1,325 @@
#lang racket/base
-(require racket/unit "nntp-sig.rkt" "nntp-unit.rkt")
-(define-values/invoke-unit/infer nntp@)
+(require racket/tcp)
-(provide-signature-elements nntp^)
+(provide (struct-out communicator)
+ connect-to-server connect-to-server* disconnect-from-server
+ authenticate-user open-news-group
+ head-of-message body-of-message
+ newnews-since generic-message-command
+ make-desired-header extract-desired-headers
+
+ (struct-out nntp)
+ (struct-out unexpected-response)
+ (struct-out bad-status-line)
+ (struct-out premature-close)
+ (struct-out bad-newsgroup-line)
+ (struct-out non-existent-group)
+ (struct-out article-not-in-group)
+ (struct-out no-group-selected)
+ (struct-out article-not-found)
+ (struct-out authentication-rejected))
+
+;; sender : oport
+;; receiver : iport
+;; server : string
+;; port : number
+
+(define-struct communicator (sender receiver server port))
+
+;; code : number
+;; text : string
+;; line : string
+;; communicator : communicator
+;; group : string
+;; article : number
+
+(define-struct (nntp exn) ())
+(define-struct (unexpected-response nntp) (code text))
+(define-struct (bad-status-line nntp) (line))
+(define-struct (premature-close nntp) (communicator))
+(define-struct (bad-newsgroup-line nntp) (line))
+(define-struct (non-existent-group nntp) (group))
+(define-struct (article-not-in-group nntp) (article))
+(define-struct (no-group-selected nntp) ())
+(define-struct (article-not-found nntp) (article))
+(define-struct (authentication-rejected nntp) ())
+
+;; signal-error :
+;; (exn-args ... -> exn) x format-string x values ... ->
+;; exn-args -> ()
+
+;; - throws an exception
+
+(define (signal-error constructor format-string . args)
+ (lambda exn-args
+ (raise (apply constructor
+ (apply format format-string args)
+ (current-continuation-marks)
+ exn-args))))
+
+;; default-nntpd-port-number :
+;; number
+
+(define default-nntpd-port-number 119)
+
+;; connect-to-server*:
+;; input-port output-port -> communicator
+
+(define connect-to-server*
+ (case-lambda
+ [(receiver sender)
+ (connect-to-server* receiver sender "unspecified" "unspecified")]
+ [(receiver sender server-name port-number)
+ (file-stream-buffer-mode sender 'line)
+ (let ([communicator (make-communicator sender receiver server-name
+ port-number)])
+ (let-values ([(code response)
+ (get-single-line-response communicator)])
+ (case code
+ [(200 201) communicator]
+ [else ((signal-error make-unexpected-response
+ "unexpected connection response: ~s ~s"
+ code response)
+ code response)])))]))
+
+;; connect-to-server :
+;; string [x number] -> commnicator
+
+(define connect-to-server
+ (lambda (server-name (port-number default-nntpd-port-number))
+ (let-values ([(receiver sender)
+ (tcp-connect server-name port-number)])
+ (connect-to-server* receiver sender server-name port-number))))
+
+;; close-communicator :
+;; communicator -> ()
+
+(define (close-communicator communicator)
+ (close-input-port (communicator-receiver communicator))
+ (close-output-port (communicator-sender communicator)))
+
+;; disconnect-from-server :
+;; communicator -> ()
+
+(define (disconnect-from-server communicator)
+ (send-to-server communicator "QUIT")
+ (let-values ([(code response)
+ (get-single-line-response communicator)])
+ (case code
+ [(205)
+ (close-communicator communicator)]
+ [else
+ ((signal-error make-unexpected-response
+ "unexpected dis-connect response: ~s ~s"
+ code response)
+ code response)])))
+
+;; authenticate-user :
+;; communicator x user-name x password -> ()
+;; the password is not used if the server does not ask for it.
+
+(define (authenticate-user communicator user password)
+ (define (reject code response)
+ ((signal-error make-authentication-rejected
+ "authentication rejected (~s ~s)"
+ code response)))
+ (define (unexpected code response)
+ ((signal-error make-unexpected-response
+ "unexpected response for authentication: ~s ~s"
+ code response)
+ code response))
+ (send-to-server communicator "AUTHINFO USER ~a" user)
+ (let-values ([(code response) (get-single-line-response communicator)])
+ (case code
+ [(281) (void)] ; server doesn't ask for a password
+ [(381)
+ (send-to-server communicator "AUTHINFO PASS ~a" password)
+ (let-values ([(code response)
+ (get-single-line-response communicator)])
+ (case code
+ [(281) (void)] ; done
+ [(502) (reject code response)]
+ [else (unexpected code response)]))]
+ [(502) (reject code response)]
+ [else (reject code response)
+ (unexpected code response)])))
+
+;; send-to-server :
+;; communicator x format-string x list (values) -> ()
+
+(define (send-to-server communicator message-template . rest)
+ (let ([sender (communicator-sender communicator)])
+ (apply fprintf sender
+ (string-append message-template "\r\n")
+ rest)
+ (flush-output sender)))
+
+;; parse-status-line :
+;; string -> number x string
+
+(define (parse-status-line line)
+ (if (eof-object? line)
+ ((signal-error make-bad-status-line "eof instead of a status line")
+ line)
+ (let ([match (cdr (or (regexp-match #rx"([0-9]+) (.*)" line)
+ ((signal-error make-bad-status-line
+ "malformed status line: ~s" line)
+ line)))])
+ (values (string->number (car match))
+ (cadr match)))))
+
+;; get-one-line-from-server :
+;; iport -> string
+
+(define (get-one-line-from-server server->client-port)
+ (read-line server->client-port 'return-linefeed))
+
+;; get-single-line-response :
+;; communicator -> number x string
+
+(define (get-single-line-response communicator)
+ (let* ([receiver (communicator-receiver communicator)]
+ [status-line (get-one-line-from-server receiver)])
+ (parse-status-line status-line)))
+
+;; get-rest-of-multi-line-response :
+;; communicator -> list (string)
+
+(define (get-rest-of-multi-line-response communicator)
+ (let ([receiver (communicator-receiver communicator)])
+ (let loop ([r '()])
+ (let ([l (get-one-line-from-server receiver)])
+ (cond
+ [(eof-object? l)
+ ((signal-error make-premature-close
+ "port prematurely closed during multi-line response")
+ communicator)]
+ [(string=? l ".") (reverse r)]
+ [(string=? l "..") (loop (cons "." r))]
+ [else (loop (cons l r))])))))
+
+;; get-multi-line-response :
+;; communicator -> number x string x list (string)
+
+;; -- The returned values are the status code, the rest of the status
+;; response line, and the remaining lines.
+
+(define (get-multi-line-response communicator)
+ (let* ([receiver (communicator-receiver communicator)]
+ [status-line (get-one-line-from-server receiver)])
+ (let-values ([(code rest-of-line)
+ (parse-status-line status-line)])
+ (values code rest-of-line (get-rest-of-multi-line-response communicator)))))
+
+;; open-news-group :
+;; communicator x string -> number x number x number
+
+;; -- The returned values are the number of articles, the first
+;; article number, and the last article number for that group.
+
+(define (open-news-group communicator group-name)
+ (send-to-server communicator "GROUP ~a" group-name)
+ (let-values ([(code rest-of-line)
+ (get-single-line-response communicator)])
+ (case code
+ [(211)
+ (let ([match (map string->number
+ (cdr
+ (or (regexp-match #rx"([0-9]+) ([0-9]+) ([0-9]+)" rest-of-line)
+ ((signal-error make-bad-newsgroup-line
+ "malformed newsgroup open response: ~s"
+ rest-of-line)
+ rest-of-line))))])
+ (let ([number-of-articles (car match)]
+ [first-article-number (cadr match)]
+ [last-article-number (caddr match)])
+ (values number-of-articles
+ first-article-number
+ last-article-number)))]
+ [(411)
+ ((signal-error make-non-existent-group
+ "group ~s does not exist on server ~s"
+ group-name (communicator-server communicator))
+ group-name)]
+ [else
+ ((signal-error make-unexpected-response
+ "unexpected group opening response: ~s" code)
+ code rest-of-line)])))
+
+;; generic-message-command :
+;; string x number -> communicator x (number U string) -> list (string)
+
+(define (generic-message-command command ok-code)
+ (lambda (communicator message-index)
+ (send-to-server communicator (string-append command " ~a")
+ (if (number? message-index)
+ (number->string message-index)
+ message-index))
+ (let-values ([(code response)
+ (get-single-line-response communicator)])
+ (if (= code ok-code)
+ (get-rest-of-multi-line-response communicator)
+ (case code
+ [(423)
+ ((signal-error make-article-not-in-group
+ "article id ~s not in group" message-index)
+ message-index)]
+ [(412)
+ ((signal-error make-no-group-selected
+ "no group selected"))]
+ [(430)
+ ((signal-error make-article-not-found
+ "no article id ~s found" message-index)
+ message-index)]
+ [else
+ ((signal-error make-unexpected-response
+ "unexpected message access response: ~s" code)
+ code response)])))))
+
+;; head-of-message :
+;; communicator x (number U string) -> list (string)
+
+(define head-of-message
+ (generic-message-command "HEAD" 221))
+
+;; body-of-message :
+;; communicator x (number U string) -> list (string)
+
+(define body-of-message
+ (generic-message-command "BODY" 222))
+
+;; newnews-since :
+;; communicator x (number U string) -> list (string)
+
+(define newnews-since
+ (generic-message-command "NEWNEWS" 230))
+
+;; make-desired-header :
+;; string -> desired
+
+(define (make-desired-header raw-header)
+ (regexp
+ (string-append
+ "^"
+ (list->string
+ (apply append
+ (map (lambda (c)
+ (cond
+ [(char-lower-case? c)
+ (list #\[ (char-upcase c) c #\])]
+ [(char-upper-case? c)
+ (list #\[ c (char-downcase c) #\])]
+ [else
+ (list c)]))
+ (string->list raw-header))))
+ ":")))
+
+;; extract-desired-headers :
+;; list (string) x list (desired) -> list (string)
+
+(define (extract-desired-headers headers desireds)
+ (filter (lambda (header)
+ (ormap (lambda (matcher) (regexp-match matcher header))
+ desireds))
+ headers))
diff --git a/collects/net/pop3.rkt b/collects/net/pop3.rkt
index 099a9fa14a..2142b8cd8d 100644
--- a/collects/net/pop3.rkt
+++ b/collects/net/pop3.rkt
@@ -1,13 +1,9 @@
#lang racket/base
-(require racket/unit "pop3-sig.rkt" "pop3-unit.rkt")
-(define-values/invoke-unit/infer pop3@)
-
-(provide-signature-elements pop3^)
+;; Implements RFC 1939, Post Office Protocol - Version 3, Myers & Rose
#|
-
-> (require-library "pop3.rkt" "net")
+> (require net/pop3)
> (define c (connect-to-server "cs.rice.edu"))
> (authenticate/plain-text "scheme" "********" c)
> (get-mailbox-status c)
@@ -28,3 +24,408 @@
("some body" "text" "goes" "." "here" "." "")
> (disconnect-from-server c)
|#
+
+(require racket/tcp)
+
+(provide (struct-out communicator)
+ connect-to-server connect-to-server* disconnect-from-server
+ authenticate/plain-text
+ get-mailbox-status
+ get-message/complete get-message/headers get-message/body
+ delete-message
+ get-unique-id/single get-unique-id/all
+
+ make-desired-header extract-desired-headers
+
+ (struct-out pop3)
+ (struct-out cannot-connect)
+ (struct-out username-rejected)
+ (struct-out password-rejected)
+ (struct-out not-ready-for-transaction)
+ (struct-out not-given-headers)
+ (struct-out illegal-message-number)
+ (struct-out cannot-delete-message)
+ (struct-out disconnect-not-quiet)
+ (struct-out malformed-server-response))
+
+;; sender : oport
+;; receiver : iport
+;; server : string
+;; port : number
+;; state : symbol = (disconnected, authorization, transaction)
+
+(define-struct communicator (sender receiver server port [state #:mutable]))
+
+(define-struct (pop3 exn) ())
+(define-struct (cannot-connect pop3) ())
+(define-struct (username-rejected pop3) ())
+(define-struct (password-rejected pop3) ())
+(define-struct (not-ready-for-transaction pop3) (communicator))
+(define-struct (not-given-headers pop3) (communicator message))
+(define-struct (illegal-message-number pop3) (communicator message))
+(define-struct (cannot-delete-message exn) (communicator message))
+(define-struct (disconnect-not-quiet pop3) (communicator))
+(define-struct (malformed-server-response pop3) (communicator))
+
+;; signal-error :
+;; (exn-args ... -> exn) x format-string x values ... ->
+;; exn-args -> ()
+
+(define (signal-error constructor format-string . args)
+ (lambda exn-args
+ (raise (apply constructor
+ (apply format format-string args)
+ (current-continuation-marks)
+ exn-args))))
+
+;; signal-malformed-response-error :
+;; exn-args -> ()
+
+;; -- in practice, it takes only one argument: a communicator.
+
+(define signal-malformed-response-error
+ (signal-error make-malformed-server-response
+ "malformed response from server"))
+
+;; confirm-transaction-mode :
+;; communicator x string -> ()
+
+;; -- signals an error otherwise.
+
+(define (confirm-transaction-mode communicator error-message)
+ (unless (eq? (communicator-state communicator) 'transaction)
+ ((signal-error make-not-ready-for-transaction error-message)
+ communicator)))
+
+;; default-pop-port-number :
+;; number
+
+(define default-pop-port-number 110)
+
+(define-struct server-responses ())
+(define-struct (+ok server-responses) ())
+(define-struct (-err server-responses) ())
+
+;; connect-to-server*:
+;; input-port output-port -> communicator
+
+(define connect-to-server*
+ (case-lambda
+ [(receiver sender) (connect-to-server* receiver sender "unspecified" "unspecified")]
+ [(receiver sender server-name port-number)
+ (let ([communicator (make-communicator sender receiver server-name port-number
+ 'authorization)])
+ (let ([response (get-status-response/basic communicator)])
+ (cond
+ [(+ok? response) communicator]
+ [(-err? response)
+ ((signal-error make-cannot-connect
+ "cannot connect to ~a on port ~a"
+ server-name port-number))])))]))
+
+;; connect-to-server :
+;; string [x number] -> communicator
+
+(define connect-to-server
+ (lambda (server-name (port-number default-pop-port-number))
+ (let-values ([(receiver sender) (tcp-connect server-name port-number)])
+ (connect-to-server* receiver sender server-name port-number))))
+
+;; authenticate/plain-text :
+;; string x string x communicator -> ()
+
+;; -- if authentication succeeds, sets the communicator's state to
+;; transaction.
+
+(define (authenticate/plain-text username password communicator)
+ (let ([sender (communicator-sender communicator)])
+ (send-to-server communicator "USER ~a" username)
+ (let ([status (get-status-response/basic communicator)])
+ (cond
+ [(+ok? status)
+ (send-to-server communicator "PASS ~a" password)
+ (let ([status (get-status-response/basic communicator)])
+ (cond
+ [(+ok? status)
+ (set-communicator-state! communicator 'transaction)]
+ [(-err? status)
+ ((signal-error make-password-rejected
+ "password was rejected"))]))]
+ [(-err? status)
+ ((signal-error make-username-rejected
+ "username was rejected"))]))))
+
+;; get-mailbox-status :
+;; communicator -> number x number
+
+;; -- returns number of messages and number of octets.
+
+(define (get-mailbox-status communicator)
+ (confirm-transaction-mode
+ communicator
+ "cannot get mailbox status unless in transaction mode")
+ (send-to-server communicator "STAT")
+ (apply values
+ (map string->number
+ (let-values ([(status result)
+ (get-status-response/match
+ communicator
+ #rx"([0-9]+) ([0-9]+)"
+ #f)])
+ result))))
+
+;; get-message/complete :
+;; communicator x number -> list (string) x list (string)
+
+(define (get-message/complete communicator message)
+ (confirm-transaction-mode
+ communicator
+ "cannot get message headers unless in transaction state")
+ (send-to-server communicator "RETR ~a" message)
+ (let ([status (get-status-response/basic communicator)])
+ (cond
+ [(+ok? status)
+ (split-header/body (get-multi-line-response communicator))]
+ [(-err? status)
+ ((signal-error make-illegal-message-number
+ "not given message ~a" message)
+ communicator message)])))
+
+;; get-message/headers :
+;; communicator x number -> list (string)
+
+(define (get-message/headers communicator message)
+ (confirm-transaction-mode
+ communicator
+ "cannot get message headers unless in transaction state")
+ (send-to-server communicator "TOP ~a 0" message)
+ (let ([status (get-status-response/basic communicator)])
+ (cond
+ [(+ok? status)
+ (let-values ([(headers body)
+ (split-header/body
+ (get-multi-line-response communicator))])
+ headers)]
+ [(-err? status)
+ ((signal-error make-not-given-headers
+ "not given headers to message ~a" message)
+ communicator message)])))
+
+;; get-message/body :
+;; communicator x number -> list (string)
+
+(define (get-message/body communicator message)
+ (let-values ([(headers body) (get-message/complete communicator message)])
+ body))
+
+;; split-header/body :
+;; list (string) -> list (string) x list (string)
+
+;; -- returns list of headers and list of body lines.
+
+(define (split-header/body lines)
+ (let loop ([lines lines] [header null])
+ (if (null? lines)
+ (values (reverse header) null)
+ (let ([first (car lines)]
+ [rest (cdr lines)])
+ (if (string=? first "")
+ (values (reverse header) rest)
+ (loop rest (cons first header)))))))
+
+;; delete-message :
+;; communicator x number -> ()
+
+(define (delete-message communicator message)
+ (confirm-transaction-mode
+ communicator
+ "cannot delete message unless in transaction state")
+ (send-to-server communicator "DELE ~a" message)
+ (let ([status (get-status-response/basic communicator)])
+ (cond
+ [(-err? status)
+ ((signal-error make-cannot-delete-message
+ "no message numbered ~a available to be deleted" message)
+ communicator message)]
+ [(+ok? status)
+ 'deleted])))
+
+;; regexp for UIDL responses
+
+(define uidl-regexp #rx"([0-9]+) (.*)")
+
+;; get-unique-id/single :
+;; communicator x number -> string
+
+(define (get-unique-id/single communicator message)
+ (confirm-transaction-mode
+ communicator
+ "cannot get unique message id unless in transaction state")
+ (send-to-server communicator "UIDL ~a" message)
+ (let-values ([(status result)
+ (get-status-response/match communicator uidl-regexp ".*")])
+ ;; The server response is of the form
+ ;; +OK 2 QhdPYR:00WBw1Ph7x7
+ (cond
+ [(-err? status)
+ ((signal-error make-illegal-message-number
+ "no message numbered ~a available for unique id" message)
+ communicator message)]
+ [(+ok? status)
+ (cadr result)])))
+
+;; get-unique-id/all :
+;; communicator -> list(number x string)
+
+(define (get-unique-id/all communicator)
+ (confirm-transaction-mode communicator
+ "cannot get unique message ids unless in transaction state")
+ (send-to-server communicator "UIDL")
+ (let ([status (get-status-response/basic communicator)])
+ ;; The server response is of the form
+ ;; +OK
+ ;; 1 whqtswO00WBw418f9t5JxYwZ
+ ;; 2 QhdPYR:00WBw1Ph7x7
+ ;; .
+ (map (lambda (l)
+ (let ([m (regexp-match uidl-regexp l)])
+ (cons (string->number (cadr m)) (caddr m))))
+ (get-multi-line-response communicator))))
+
+;; close-communicator :
+;; communicator -> ()
+
+(define (close-communicator communicator)
+ (close-input-port (communicator-receiver communicator))
+ (close-output-port (communicator-sender communicator)))
+
+;; disconnect-from-server :
+;; communicator -> ()
+
+(define (disconnect-from-server communicator)
+ (send-to-server communicator "QUIT")
+ (set-communicator-state! communicator 'disconnected)
+ (let ([response (get-status-response/basic communicator)])
+ (close-communicator communicator)
+ (cond
+ [(+ok? response) (void)]
+ [(-err? response)
+ ((signal-error make-disconnect-not-quiet
+ "got error status upon disconnect")
+ communicator)])))
+
+;; send-to-server :
+;; communicator x format-string x list (values) -> ()
+
+(define (send-to-server communicator message-template . rest)
+ (apply fprintf (communicator-sender communicator)
+ (string-append message-template "\r\n")
+ rest)
+ (flush-output (communicator-sender communicator)))
+
+;; get-one-line-from-server :
+;; iport -> string
+
+(define (get-one-line-from-server server->client-port)
+ (read-line server->client-port 'return-linefeed))
+
+;; get-server-status-response :
+;; communicator -> server-responses x string
+
+;; -- provides the low-level functionality of checking for +OK
+;; and -ERR, returning an appropriate structure, and returning the
+;; rest of the status response as a string to be used for further
+;; parsing, if necessary.
+
+(define (get-server-status-response communicator)
+ (let* ([receiver (communicator-receiver communicator)]
+ [status-line (get-one-line-from-server receiver)]
+ [r (regexp-match #rx"^\\+OK(.*)" status-line)])
+ (if r
+ (values (make-+ok) (cadr r))
+ (let ([r (regexp-match #rx"^\\-ERR(.*)" status-line)])
+ (if r
+ (values (make--err) (cadr r))
+ (signal-malformed-response-error communicator))))))
+
+;; get-status-response/basic :
+;; communicator -> server-responses
+
+;; -- when the only thing to determine is whether the response
+;; was +OK or -ERR.
+
+(define (get-status-response/basic communicator)
+ (let-values ([(response rest)
+ (get-server-status-response communicator)])
+ response))
+
+;; get-status-response/match :
+;; communicator x regexp x regexp -> (status x list (string))
+
+;; -- when further parsing of the status response is necessary.
+;; Strips off the car of response from regexp-match.
+
+(define (get-status-response/match communicator +regexp -regexp)
+ (let-values ([(response rest)
+ (get-server-status-response communicator)])
+ (if (and +regexp (+ok? response))
+ (let ([r (regexp-match +regexp rest)])
+ (if r (values response (cdr r))
+ (signal-malformed-response-error communicator)))
+ (if (and -regexp (-err? response))
+ (let ([r (regexp-match -regexp rest)])
+ (if r (values response (cdr r))
+ (signal-malformed-response-error communicator)))
+ (signal-malformed-response-error communicator)))))
+
+;; get-multi-line-response :
+;; communicator -> list (string)
+
+(define (get-multi-line-response communicator)
+ (let ([receiver (communicator-receiver communicator)])
+ (let loop ()
+ (let ([l (get-one-line-from-server receiver)])
+ (cond
+ [(eof-object? l)
+ (signal-malformed-response-error communicator)]
+ [(string=? l ".")
+ '()]
+ [(and (> (string-length l) 1)
+ (char=? (string-ref l 0) #\.))
+ (cons (substring l 1 (string-length l)) (loop))]
+ [else
+ (cons l (loop))])))))
+
+;; make-desired-header :
+;; string -> desired
+
+(define (make-desired-header raw-header)
+ (regexp
+ (string-append
+ "^"
+ (list->string
+ (apply append
+ (map (lambda (c)
+ (cond
+ [(char-lower-case? c)
+ (list #\[ (char-upcase c) c #\])]
+ [(char-upper-case? c)
+ (list #\[ c (char-downcase c) #\])]
+ [else
+ (list c)]))
+ (string->list raw-header))))
+ ":")))
+
+;; extract-desired-headers :
+;; list (string) x list (desired) -> list (string)
+
+(define (extract-desired-headers headers desireds)
+ (let loop ([headers headers])
+ (if (null? headers) null
+ (let ([first (car headers)]
+ [rest (cdr headers)])
+ (if (ormap (lambda (matcher)
+ (regexp-match matcher first))
+ desireds)
+ (cons first (loop rest))
+ (loop rest))))))
diff --git a/collects/net/qp.rkt b/collects/net/qp.rkt
index c5267fb6b1..7522a6c863 100644
--- a/collects/net/qp.rkt
+++ b/collects/net/qp.rkt
@@ -6,31 +6,166 @@
;;;
;;; This file is part of mime-plt.
-;;; mime-plt is free software; you can redistribute it and/or
+;;; qp is free software; you can redistribute it and/or
;;; modify it under the terms of the GNU Lesser General Public
;;; License as published by the Free Software Foundation; either
;;; version 2.1 of the License, or (at your option) any later version.
-;;; mime-plt is distributed in the hope that it will be useful,
+;;; qp is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;;; Lesser General Public License for more details.
;;; You should have received a copy of the GNU Lesser General Public
-;;; License along with mime-plt; if not, write to the Free Software
+;;; License along with qp; if not, write to the Free Software
;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
;;; 02110-1301 USA.
;;; Author: Francisco Solsona
;;
-;;
;; Commentary:
#lang racket/base
-(require racket/unit "qp-sig.rkt" "qp-unit.rkt")
-(define-values/invoke-unit/infer qp@)
+(provide
+ ;; -- exceptions raised --
+ (struct-out qp-error)
+ (struct-out qp-wrong-input)
+ (struct-out qp-wrong-line-size)
-(provide-signature-elements qp^)
+ ;; -- qp methods --
+ qp-encode
+ qp-decode
+ qp-encode-stream
+ qp-decode-stream)
+
+;; Exceptions:
+;; String or input-port expected:
+(define-struct qp-error ())
+(define-struct (qp-wrong-input qp-error) ())
+(define-struct (qp-wrong-line-size qp-error) (size))
+
+;; qp-encode : bytes -> bytes
+;; returns the quoted printable representation of STR.
+(define (qp-encode str)
+ (let ([out (open-output-bytes)])
+ (qp-encode-stream (open-input-bytes str) out #"\r\n")
+ (get-output-bytes out)))
+
+;; qp-decode : string -> string
+;; returns STR unqp.
+(define (qp-decode str)
+ (let ([out (open-output-bytes)])
+ (qp-decode-stream (open-input-bytes str) out)
+ (get-output-bytes out)))
+
+(define (qp-decode-stream in out)
+ (let loop ([ch (read-byte in)])
+ (unless (eof-object? ch)
+ (case ch
+ [(61) ;; A "=", which is quoted-printable stuff
+ (let ([next (read-byte in)])
+ (cond
+ [(eq? next 10)
+ ;; Soft-newline -- drop it
+ (void)]
+ [(eq? next 13)
+ ;; Expect a newline for a soft CRLF...
+ (let ([next-next (read-byte in)])
+ (if (eq? next-next 10)
+ ;; Good.
+ (loop (read-byte in))
+ ;; Not a LF? Well, ok.
+ (loop next-next)))]
+ [(hex-digit? next)
+ (let ([next-next (read-byte in)])
+ (cond [(eof-object? next-next)
+ (warning "Illegal qp sequence: `=~a'" next)
+ (display "=" out)
+ (display next out)]
+ [(hex-digit? next-next)
+ ;; qp-encoded
+ (write-byte (hex-bytes->byte next next-next)
+ out)]
+ [else
+ (warning "Illegal qp sequence: `=~a~a'" next next-next)
+ (write-byte 61 out)
+ (write-byte next out)
+ (write-byte next-next out)]))]
+ [else
+ ;; Warning: invalid
+ (warning "Illegal qp sequence: `=~a'" next)
+ (write-byte 61 out)
+ (write-byte next out)])
+ (loop (read-byte in)))]
+ [else
+ (write-byte ch out)
+ (loop (read-byte in))]))))
+
+(define (warning msg . args)
+ (when #f
+ (fprintf (current-error-port)
+ (apply format msg args))
+ (newline (current-error-port))))
+
+(define (hex-digit? i)
+ (vector-ref hex-values i))
+
+(define (hex-bytes->byte b1 b2)
+ (+ (* 16 (vector-ref hex-values b1))
+ (vector-ref hex-values b2)))
+
+(define (write-hex-bytes byte p)
+ (write-byte 61 p)
+ (write-byte (vector-ref hex-bytes (arithmetic-shift byte -4)) p)
+ (write-byte (vector-ref hex-bytes (bitwise-and byte 15)) p))
+
+(define (qp-encode-stream in out [newline-string #"\n"])
+ (let loop ([col 0])
+ (if (= col 75)
+ (begin
+ ;; Soft newline:
+ (write-byte 61 out)
+ (display newline-string out)
+ (loop 0))
+ (let ([i (read-byte in)])
+ (cond
+ [(eof-object? i) (void)]
+ [(or (= i 10) (= i 13))
+ (write-byte i out)
+ (loop 0)]
+ [(or (<= 33 i 60) (<= 62 i 126)
+ (and (or (= i 32) (= i 9))
+ (not (let ([next (peek-byte in)])
+ (or (eof-object? next) (= next 10) (= next 13))))))
+ ;; single-byte mode:
+ (write-byte i out)
+ (loop (add1 col))]
+ [(>= col 73)
+ ;; need a soft newline first
+ (write-byte 61 out)
+ (display newline-string out)
+ ;; now the octect
+ (write-hex-bytes i out)
+ (loop 3)]
+ [else
+ ;; an octect
+ (write-hex-bytes i out)
+ (loop (+ col 3))])))))
+
+;; Tables
+(define hex-values (make-vector 256 #f))
+(define hex-bytes (make-vector 16))
+(let loop ([i 0])
+ (unless (= i 10)
+ (vector-set! hex-values (+ i 48) i)
+ (vector-set! hex-bytes i (+ i 48))
+ (loop (add1 i))))
+(let loop ([i 0])
+ (unless (= i 6)
+ (vector-set! hex-values (+ i 65) (+ 10 i))
+ (vector-set! hex-values (+ i 97) (+ 10 i))
+ (vector-set! hex-bytes (+ 10 i) (+ i 65))
+ (loop (add1 i))))
;;; qp.rkt ends here
diff --git a/collects/net/scribblings/base64.scrbl b/collects/net/scribblings/base64.scrbl
index 7d74db1fd6..0d3b6293ed 100644
--- a/collects/net/scribblings/base64.scrbl
+++ b/collects/net/scribblings/base64.scrbl
@@ -46,6 +46,10 @@ end-of-file or Base 64 terminator @litchar{=} from @racket[in].}
@section{Base64 Unit}
+@margin-note{@racket[base64@] and @racket[base64^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/base64] module.}
+
@defmodule[net/base64-unit]
@defthing[base64@ unit?]{
diff --git a/collects/net/scribblings/cgi.scrbl b/collects/net/scribblings/cgi.scrbl
index 562d659d92..169ccef9b0 100644
--- a/collects/net/scribblings/cgi.scrbl
+++ b/collects/net/scribblings/cgi.scrbl
@@ -140,6 +140,10 @@ query is invalid.}
@section{CGI Unit}
+@margin-note{@racket[cgi@] and @racket[cgi^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/cgi] module.}
+
@defmodule[net/cgi-unit]
@defthing[cgi@ unit?]{
diff --git a/collects/net/scribblings/dns.scrbl b/collects/net/scribblings/dns.scrbl
index 016a237413..552c59d202 100644
--- a/collects/net/scribblings/dns.scrbl
+++ b/collects/net/scribblings/dns.scrbl
@@ -56,6 +56,10 @@ extract the first nameserver address. On Windows, it runs
@section{DNS Unit}
+@margin-note{@racket[dns@] and @racket[dns^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/dns] module.}
+
@defmodule[net/dns-unit]
@defthing[dns@ unit?]{
diff --git a/collects/net/scribblings/ftp.scrbl b/collects/net/scribblings/ftp.scrbl
index 7c8fc253db..1bbcaf0387 100644
--- a/collects/net/scribblings/ftp.scrbl
+++ b/collects/net/scribblings/ftp.scrbl
@@ -88,6 +88,10 @@ file, then moved into place on success).}
@section{FTP Unit}
+@margin-note{@racket[ftp@] and @racket[ftp^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/ftp] module.}
+
@defmodule[net/ftp-unit]
@defthing[ftp@ unit?]{
diff --git a/collects/net/scribblings/head.scrbl b/collects/net/scribblings/head.scrbl
index d93cba84f0..440612e680 100644
--- a/collects/net/scribblings/head.scrbl
+++ b/collects/net/scribblings/head.scrbl
@@ -222,6 +222,10 @@ are comma-separated, and possibly broken into multiple lines.
@section{Header Unit}
+@margin-note{@racket[head@] and @racket[head^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/head] module.}
+
@defmodule[net/head-unit]
@defthing[head@ unit?]{
diff --git a/collects/net/scribblings/imap.scrbl b/collects/net/scribblings/imap.scrbl
index e4f835a4d5..da85e9565d 100644
--- a/collects/net/scribblings/imap.scrbl
+++ b/collects/net/scribblings/imap.scrbl
@@ -497,6 +497,10 @@ Returns a list of IMAP flags for the given mailbox. See also
@section{IMAP Unit}
+@margin-note{@racket[imap@] and @racket[imap^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/imap] module.}
+
@defmodule[net/imap-unit]
@defthing[imap@ unit?]{
diff --git a/collects/net/scribblings/mime.scrbl b/collects/net/scribblings/mime.scrbl
index 30b8c38026..f611e1e641 100644
--- a/collects/net/scribblings/mime.scrbl
+++ b/collects/net/scribblings/mime.scrbl
@@ -236,6 +236,10 @@ field, or when the specification is incorrectly formatted.}
@section{MIME Unit}
+@margin-note{@racket[mime@] and @racket[mime^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/mime] module.}
+
@defmodule[net/mime-unit]
@defthing[mime@ unit?]{
diff --git a/collects/net/scribblings/nntp.scrbl b/collects/net/scribblings/nntp.scrbl
index d63ceca3c2..dbace6999d 100644
--- a/collects/net/scribblings/nntp.scrbl
+++ b/collects/net/scribblings/nntp.scrbl
@@ -135,6 +135,10 @@ Raised when the server reject an authentication attempt.}
@section{NNTP Unit}
+@margin-note{@racket[nntp@] and @racket[nntp^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/nntp] module.}
+
@defmodule[net/nntp-unit]
@defthing[nntp@ unit?]{
diff --git a/collects/net/scribblings/pop3.scrbl b/collects/net/scribblings/pop3.scrbl
index 99b3669c32..9a78f86769 100644
--- a/collects/net/scribblings/pop3.scrbl
+++ b/collects/net/scribblings/pop3.scrbl
@@ -184,6 +184,10 @@ Raised when the server produces a malformed response.}
@section{POP3 Unit}
+@margin-note{@racket[pop3@] and @racket[pop3^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/pop3] module.}
+
@defmodule[net/pop3-unit]
@defthing[pop3@ unit?]{
diff --git a/collects/net/scribblings/qp.scrbl b/collects/net/scribblings/qp.scrbl
index c5f0a6d608..18ef49e957 100644
--- a/collects/net/scribblings/qp.scrbl
+++ b/collects/net/scribblings/qp.scrbl
@@ -66,6 +66,10 @@ backward compatibility.}
@section{Quoted-Printable Unit}
+@margin-note{@racket[qp@] and @racket[qp^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/qp] module.}
+
@defmodule[net/qp-unit]
@defthing[qp@ unit?]{
diff --git a/collects/net/scribblings/sendmail.scrbl b/collects/net/scribblings/sendmail.scrbl
index cef43caa42..d034f15662 100644
--- a/collects/net/scribblings/sendmail.scrbl
+++ b/collects/net/scribblings/sendmail.scrbl
@@ -63,6 +63,10 @@ Raised when no mail recipients were specified for
@section{Sendmail Unit}
+@margin-note{@racket[sendmail@] and @racket[sendmail^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/sendmail] module.}
+
@defmodule[net/sendmail-unit]
@defthing[sendmail@ unit?]{
diff --git a/collects/net/scribblings/smtp.scrbl b/collects/net/scribblings/smtp.scrbl
index 46cc382838..a6f768f300 100644
--- a/collects/net/scribblings/smtp.scrbl
+++ b/collects/net/scribblings/smtp.scrbl
@@ -102,6 +102,10 @@ probably will not).}
@section{SMTP Unit}
+@margin-note{@racket[smtp@] and @racket[smtp^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/smtp] module.}
+
@defmodule[net/smtp-unit]
@defthing[smtp@ unit?]{
diff --git a/collects/net/scribblings/uri-codec.scrbl b/collects/net/scribblings/uri-codec.scrbl
index 57f3c30ebc..17a8da688c 100644
--- a/collects/net/scribblings/uri-codec.scrbl
+++ b/collects/net/scribblings/uri-codec.scrbl
@@ -154,3 +154,27 @@ use/recognize only of the separators.
(form-urlencoded->alist "x=foo;y=bar;z=baz")
(alist->form-urlencoded ex)
]}
+
+@; ----------------------------------------
+
+@section{URI Codec Unit}
+
+@margin-note{@racket[uri-codec@] and @racket[uri-codec^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/uri-codec] module.}
+
+@defmodule[net/uri-codec-unit]
+
+@defthing[uri-codec@ unit?]{
+
+Imports nothing, exports @racket[uri-codec^].}
+
+@; ----------------------------------------
+
+@section{URI Codec Signature}
+
+@defmodule[net/uri-codec-sig]
+
+@defsignature[uri-codec^ ()]{}
+
+Includes everything exported by the @racketmodname[net/uri-codec] module.
diff --git a/collects/net/scribblings/url.scrbl b/collects/net/scribblings/url.scrbl
index ed8ca9b98b..e1b2db5f32 100644
--- a/collects/net/scribblings/url.scrbl
+++ b/collects/net/scribblings/url.scrbl
@@ -361,6 +361,10 @@ as described with @racket[get-pure-port].}
@section{URL Unit}
+@margin-note{@racket[url@], @racket[url^], and @racket[url+scheme^] are deprecated.
+They exist for backward-compatibility and will likely be removed in
+the future. New code should use the @racketmodname[net/url] module.}
+
@defmodule[net/url-unit]
@defthing[url@ unit?]{
diff --git a/collects/net/sendmail.rkt b/collects/net/sendmail.rkt
index e759519616..025aec0454 100644
--- a/collects/net/sendmail.rkt
+++ b/collects/net/sendmail.rkt
@@ -1,6 +1,120 @@
#lang racket/base
-(require racket/unit "sendmail-sig.rkt" "sendmail-unit.rkt")
-(define-values/invoke-unit/infer sendmail@)
+(require racket/system)
-(provide-signature-elements sendmail^)
+(provide send-mail-message/port
+ send-mail-message
+ (struct-out no-mail-recipients))
+
+(define-struct (no-mail-recipients exn) ())
+
+(define sendmail-search-path
+ '("/usr/lib" "/usr/sbin"))
+
+(define sendmail-program-file
+ (if (or (eq? (system-type) 'unix)
+ (eq? (system-type) 'macosx))
+ (let loop ([paths sendmail-search-path])
+ (if (null? paths)
+ (raise (make-exn:fail:unsupported
+ "unable to find sendmail on this Unix variant"
+ (current-continuation-marks)))
+ (let ([p (build-path (car paths) "sendmail")])
+ (if (and (file-exists? p)
+ (memq 'execute (file-or-directory-permissions p)))
+ p
+ (loop (cdr paths))))))
+ (raise (make-exn:fail:unsupported
+ "sendmail only available under Unix"
+ (current-continuation-marks)))))
+
+;; send-mail-message/port :
+;; string x string x list (string) x list (string) x list (string)
+;; [x list (string)] -> oport
+
+;; -- sender can be anything, though spoofing is not recommended.
+;; The recipients must all be pure email addresses. Note that
+;; everything is expected to follow RFC conventions. If any other
+;; headers are specified, they are expected to be completely
+;; formatted already. Clients are urged to use close-output-port on
+;; the port returned by this procedure as soon as the necessary text
+;; has been written, so that the sendmail process can complete.
+
+(define (send-mail-message/port
+ sender subject to-recipients cc-recipients bcc-recipients
+ . other-headers)
+ (when (and (null? to-recipients) (null? cc-recipients)
+ (null? bcc-recipients))
+ (raise (make-no-mail-recipients
+ "no mail recipients were specified"
+ (current-continuation-marks))))
+ (let ([return (apply process* sendmail-program-file "-i"
+ (append to-recipients cc-recipients bcc-recipients))])
+ (let ([reader (car return)]
+ [writer (cadr return)]
+ [pid (caddr return)]
+ [error-reader (cadddr return)])
+ (close-input-port reader)
+ (close-input-port error-reader)
+ (fprintf writer "From: ~a\n" sender)
+ (letrec ([write-recipient-header
+ (lambda (header-string recipients)
+ (let ([header-space
+ (+ (string-length header-string) 2)])
+ (fprintf writer "~a: " header-string)
+ (let loop ([to recipients] [indent header-space])
+ (if (null? to)
+ (newline writer)
+ (let ([first (car to)]
+ [rest (cdr to)])
+ (let ([len (string-length first)])
+ (if (>= (+ len indent) 80)
+ (begin
+ (fprintf writer
+ (if (null? rest)
+ "\n ~a"
+ "\n ~a, ")
+ first)
+ (loop (cdr to)
+ (+ len header-space 2)))
+ (begin
+ (fprintf writer
+ (if (null? rest)
+ "~a "
+ "~a, ")
+ first)
+ (loop (cdr to)
+ (+ len indent 2))))))))))])
+ (write-recipient-header "To" to-recipients)
+ (unless (null? cc-recipients)
+ (write-recipient-header "CC" cc-recipients)))
+ (fprintf writer "Subject: ~a\n" subject)
+ (fprintf writer "X-Mailer: Racket (racket-lang.org)\n")
+ (for-each (lambda (s)
+ (display s writer)
+ (newline writer))
+ other-headers)
+ (newline writer)
+ writer)))
+
+;; send-mail-message :
+;; string x string x list (string) x list (string) x list (string) x
+;; list (string) [x list (string)] -> ()
+
+;; -- sender can be anything, though spoofing is not recommended. The
+;; recipients must all be pure email addresses. The text is expected
+;; to be pre-formatted. Note that everything is expected to follow
+;; RFC conventions. If any other headers are specified, they are
+;; expected to be completely formatted already.
+
+(define (send-mail-message
+ sender subject to-recipients cc-recipients bcc-recipients text
+ . other-headers)
+ (let ([writer (apply send-mail-message/port sender subject
+ to-recipients cc-recipients bcc-recipients
+ other-headers)])
+ (for-each (lambda (s)
+ (display s writer) ; We use -i, so "." is not a problem
+ (newline writer))
+ text)
+ (close-output-port writer)))
diff --git a/collects/net/smtp.rkt b/collects/net/smtp.rkt
index 4e213d4701..c635a4580b 100644
--- a/collects/net/smtp.rkt
+++ b/collects/net/smtp.rkt
@@ -1,6 +1,166 @@
#lang racket/base
-(require racket/unit "smtp-sig.rkt" "smtp-unit.rkt")
-(define-values/invoke-unit/infer smtp@)
+(require racket/tcp "base64.rkt")
-(provide-signature-elements smtp^)
+(provide smtp-sending-server
+ smtp-send-message
+ smtp-send-message*
+ smtp-sending-end-of-message)
+
+(define smtp-sending-server (make-parameter "localhost"))
+
+(define debug-via-stdio? #f)
+
+;; (define log printf)
+(define log void)
+
+(define (starts-with? l n)
+ (and (>= (string-length l) (string-length n))
+ (string=? n (substring l 0 (string-length n)))))
+
+(define (check-reply/accum r v w a)
+ (flush-output w)
+ (let ([l (read-line r (if debug-via-stdio? 'linefeed 'return-linefeed))])
+ (log "server: ~a\n" l)
+ (if (eof-object? l)
+ (error 'check-reply "got EOF")
+ (let ([n (number->string v)])
+ (unless (starts-with? l n)
+ (error 'check-reply "expected reply ~a; got: ~a" v l))
+ (let ([n- (string-append n "-")])
+ (if (starts-with? l n-)
+ ;; Multi-line reply. Go again.
+ (check-reply/accum r v w (if a (cons (substring l 4) a) #f))
+ ;; We're finished, so add the last and reverse the result
+ (when a
+ (reverse (cons (substring l 4) a)))))))))
+
+(define (check-reply/commands r v w . commands)
+ ;; drop the first response, which is just the flavor text -- we expect the rest to
+ ;; be a list of supported ESMTP commands.
+ (let ([cmdlist (cdr (check-reply/accum r v w '()))])
+ (for-each (lambda (c1)
+ (unless (findf (lambda (c2) (string=? c1 c2)) cmdlist)
+ (error "expected advertisement of ESMTP command ~a" c1)))
+ commands)))
+
+(define (check-reply r v w)
+ (check-reply/accum r v w #f))
+
+(define (protect-line l)
+ ;; If begins with a dot, add one more
+ (if (or (equal? l #"")
+ (equal? l "")
+ (and (string? l)
+ (not (char=? #\. (string-ref l 0))))
+ (and (bytes? l)
+ (not (= (char->integer #\.) (bytes-ref l 0)))))
+ l
+ (if (bytes? l)
+ (bytes-append #"." l)
+ (string-append "." l))))
+
+(define smtp-sending-end-of-message
+ (make-parameter void
+ (lambda (f)
+ (unless (and (procedure? f)
+ (procedure-arity-includes? f 0))
+ (raise-type-error 'smtp-sending-end-of-message "thunk" f))
+ f)))
+
+(define (smtp-send-message* r w sender recipients header message-lines
+ auth-user auth-passwd tls-encode)
+ (with-handlers ([void (lambda (x)
+ (close-input-port r)
+ (close-output-port w)
+ (raise x))])
+ (check-reply r 220 w)
+ (log "hello\n")
+ (fprintf w "EHLO ~a\r\n" (smtp-sending-server))
+ (when tls-encode
+ (check-reply/commands r 250 w "STARTTLS")
+ (log "starttls\n")
+ (fprintf w "STARTTLS\r\n")
+ (check-reply r 220 w)
+ (let-values ([(ssl-r ssl-w)
+ (tls-encode r w
+ #:mode 'connect
+ #:encrypt 'tls
+ #:close-original? #t)])
+ (set! r ssl-r)
+ (set! w ssl-w))
+ ;; According to RFC 3207 Sec 4.2, we must start anew with the EHLO.
+ (log "tls hello\n")
+ (fprintf w "EHLO ~a\r\n" (smtp-sending-server)))
+ (check-reply r 250 w)
+
+ (when auth-user
+ (log "auth\n")
+ (fprintf w "AUTH PLAIN ~a"
+ ;; Encoding adds CRLF
+ (base64-encode
+ (string->bytes/latin-1
+ (format "~a\0~a\0~a" auth-user auth-user auth-passwd))))
+ (check-reply r 235 w))
+
+ (log "from\n")
+ (fprintf w "MAIL FROM:<~a>\r\n" sender)
+ (check-reply r 250 w)
+
+ (log "to\n")
+ (for-each
+ (lambda (dest)
+ (fprintf w "RCPT TO:<~a>\r\n" dest)
+ (check-reply r 250 w))
+ recipients)
+
+ (log "header\n")
+ (fprintf w "DATA\r\n")
+ (check-reply r 354 w)
+ (fprintf w "~a" header)
+ (for-each
+ (lambda (l)
+ (log "body: ~a\n" l)
+ (fprintf w "~a\r\n" (protect-line l)))
+ message-lines)
+
+ ;; After we send the ".", then only break in an emergency
+ ((smtp-sending-end-of-message))
+
+ (log "dot\n")
+ (fprintf w ".\r\n")
+ (flush-output w)
+ (check-reply r 250 w)
+
+ ;; Once a 250 has been received in response to the . at the end of
+ ;; the DATA block, the email has been sent successfully and out of our
+ ;; hands. This function should thus indicate success at this point
+ ;; no matter what else happens.
+ ;;
+ ;; Some servers (like smtp.gmail.com) will just close the connection
+ ;; on a QUIT, so instead of causing any QUIT errors to look like the
+ ;; email failed, we'll just log them.
+ (with-handlers ([void (lambda (x)
+ (log "error after send: ~a\n" (exn-message x)))])
+ (log "quit\n")
+ (fprintf w "QUIT\r\n")
+ (check-reply r 221 w))
+
+ (close-output-port w)
+ (close-input-port r)))
+
+(define smtp-send-message
+ (lambda (server sender recipients header message-lines
+ #:port-no [port-no 25]
+ #:auth-user [auth-user #f]
+ #:auth-passwd [auth-passwd #f]
+ #:tcp-connect [tcp-connect tcp-connect]
+ #:tls-encode [tls-encode #f]
+ [opt-port-no port-no])
+ (when (null? recipients)
+ (error 'send-smtp-message "no receivers"))
+ (let-values ([(r w) (if debug-via-stdio?
+ (values (current-input-port) (current-output-port))
+ (tcp-connect server opt-port-no))])
+ (smtp-send-message* r w sender recipients header message-lines
+ auth-user auth-passwd tls-encode))))
diff --git a/collects/tests/net/url-port.rkt b/collects/tests/net/url-port.rkt
index 526542258d..d7e4cdda9e 100644
--- a/collects/tests/net/url-port.rkt
+++ b/collects/tests/net/url-port.rkt
@@ -47,6 +47,11 @@
=>
"This is the data in the first chunk and this is the second one"
+ (get-pure
+ "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n20\r\nThis is the data in the first ch\r\n21\r\nand this is the second oneXXXXXXX\r\n0\r\n")
+ =>
+ "This is the data in the first chand this is the second oneXXXXXXX"
+
(get-impure
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n23\r\nThis is the data in the first chunk\r\n1A\r\nand this is the second one\r\n0\r\n")
=>