404 lines
12 KiB
Scheme
404 lines
12 KiB
Scheme
; Time-stamp: <98/10/09 19:19:06 shriram>
|
|
|
|
(unit/sig mzlib:pop3^
|
|
(import)
|
|
|
|
;; Implements RFC 1939, Post Office Protocol - Version 3, Myers & Rose
|
|
|
|
;; sender : oport
|
|
;; receiver : iport
|
|
;; server : string
|
|
;; port : number
|
|
;; state : symbol = (disconnected, authorization, transaction)
|
|
|
|
(define-struct communicator (sender receiver server port state))
|
|
|
|
(define-struct (pop3 struct:exn) ())
|
|
(define-struct (cannot-connect struct:pop3) ())
|
|
(define-struct (username-rejected struct:pop3) ())
|
|
(define-struct (password-rejected struct:pop3) ())
|
|
(define-struct (not-ready-for-transaction struct:pop3) (communicator))
|
|
(define-struct (not-given-headers struct:pop3) (communicator message))
|
|
(define-struct (illegal-message-number struct:pop3) (communicator message))
|
|
(define-struct (cannot-delete-message struct:exn) (communicator message))
|
|
(define-struct (disconnect-not-quiet struct:pop3) (communicator))
|
|
(define-struct (malformed-server-response struct:pop3) (communicator))
|
|
|
|
;; signal-error :
|
|
;; (exn-args ... -> exn) x format-string x values ... ->
|
|
;; exn-args -> ()
|
|
|
|
(define signal-error
|
|
(lambda (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
|
|
(lambda (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 struct:server-responses) ())
|
|
(define-struct (-err struct:server-responses) ())
|
|
|
|
(define +ok (make-+ok))
|
|
(define -err (make--err))
|
|
|
|
;; connect-to-server :
|
|
;; string [x number] -> communicator
|
|
|
|
(define connect-to-server
|
|
(opt-lambda (server-name (port-number default-pop-port-number))
|
|
(let-values (((receiver sender)
|
|
(tcp-connect 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)))))))))
|
|
|
|
;; authenticate/plain-text :
|
|
;; string x string x communicator -> ()
|
|
|
|
;; -- if authentication succeeds, sets the communicator's state to
|
|
;; transaction.
|
|
|
|
(define authenticate/plain-text
|
|
(lambda (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
|
|
(let ((stat-regexp (regexp "([0-9]+) ([0-9]+)")))
|
|
(lambda (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
|
|
stat-regexp #f)))
|
|
result))))))
|
|
|
|
;; get-message/complete :
|
|
;; communicator x number -> list (string) x list (string)
|
|
|
|
(define get-message/complete
|
|
(lambda (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
|
|
(lambda (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
|
|
(lambda (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
|
|
(lambda (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
|
|
(lambda (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 (regexp "([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
|
|
(lambda (communicator)
|
|
(close-input-port (communicator-receiver communicator))
|
|
(close-output-port (communicator-sender communicator))))
|
|
|
|
;; disconnect-from-server :
|
|
;; communicator -> ()
|
|
|
|
(define disconnect-from-server
|
|
(lambda (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
|
|
(lambda (communicator message-template . rest)
|
|
(apply fprintf (communicator-sender communicator)
|
|
(string-append message-template "~n")
|
|
rest)))
|
|
|
|
;; get-one-line-from-server :
|
|
;; iport -> string
|
|
|
|
(define get-one-line-from-server
|
|
(lambda (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
|
|
(let ((+ok-regexp (regexp "^\\+OK (.*)"))
|
|
(-err-regexp (regexp "^\\-ERR (.*)")))
|
|
(lambda (communicator)
|
|
(let ((receiver (communicator-receiver communicator)))
|
|
(let ((status-line (get-one-line-from-server receiver)))
|
|
(let ((r (regexp-match +ok-regexp status-line)))
|
|
(if r
|
|
(values +ok (cadr r))
|
|
(let ((r (regexp-match -err-regexp status-line)))
|
|
(if r
|
|
(values -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
|
|
(lambda (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
|
|
(lambda (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
|
|
(lambda (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
|
|
(lambda (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
|
|
(lambda (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)))))))
|
|
|
|
)
|