;;; pop3.scm --- implement the POP3 maildrop protocol in the Scheme Shell ;;; This file is part of the Scheme Untergrund Networking package. ;;; Copyright (c) 1998 by Eric Marsden ;;; Copyright (c) 2003 by Mike Sperber ;;; For copyright information, see the file COPYING which comes with ;;; the distribution. ;;; Related work ===================================================== ;; ;; * Emacs is distributed with a C program called movemail which can ;; be compiled with support for the POP protocol. There is also an ;; Emacs Lisp library called pop3.el by Richard Pieri which includes ;; APOP support. ;; ;; * Shriram Krishnamurthi has written a POP3 library for MzScheme (as ;; well as support for the NNTP protocol, for SMTP, ...). ;; ;; * Siod (a small-footprint Scheme implementation by George Carette) ;; includes support for the POP3 protocol. ;; ;; * rfc1939 describes the POP3 protocol. ;; http://www.ietf.org/rfc/rfc1939.txt ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Communication is initiated by the client. The server responds to ;; each request with a status indicator and an explanatory message. ;; The client starts off by opening a connection to a well known port ;; on the server machine (typically TCP 110, or 109 on some broken ;; systems). Messages sent to the server are of the form ;; ;; CMD [ arg ] ;; ;; Replies from the server are of the form ;; ;; status [ Informative message ] ;; ;; where status is either "+OK" or "-ERR". If the server is sending ;; data (the contents of a message for example), it marks the end of ;; the data by a line consisting only of a decimal point (thus the ;; bytes to look out for are .. Any lines in the data ;; starting with a . have an additional . added to the beginning, to ;; avoid the client thinking that the line marks the end of the ;; message. The client should therefore replace double decimal points ;; at the beginning of a line by a single decimal point. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (pop3-connect . args) (let-optionals args ((host-arg #f) (login #f) (password #f) (log #f)) (let* ((host (or host-arg (getenv "MAILHOST"))) (hst-info (host-info host)) (hostname (host-info:name hst-info)) (srvc-info (service-info "pop3" "tcp")) (sock (socket-connect protocol-family/internet socket-type/stream hostname (service-info:port srvc-info))) (connection (make-pop3-connection hostname sock log "" "" #f #f))) (pop3-log connection (string-append "-- " (date->string (date)) ": opened POP3 connection to " hostname)) ;; read the challenge the server sends in its welcome banner (let* ((banner (read-response connection)) (match (regexp-search (rx (: "+OK " (* (~ #\<)) #\< (submatch (+ (~ #\>))) #\>)) banner)) (challenge (and match (match:substring match 1)))) (set-pop3-connection-challenge! connection challenge)) (pop3-login connection login password) connection))) ;; first try standard USER/PASS authentication, and switch to APOP ;; authentication if the server prefers. (define (pop3-login connection login password) (let* ((netrc-record #f) (get-netrc-record (lambda () (cond (netrc-record) (else (set! netrc-record (netrc-machine-entry (pop3-connection-host-name connection) #f)) netrc-record))))) (let ((login (or login (begin (if (or (not (get-netrc-record)) (not (netrc-entry-login (get-netrc-record)))) (signal 'pop3-error "no login record specified and no netrc entry")) (netrc-entry-login (get-netrc-record))))) (password (or password (begin (if (not (netrc-entry-password (get-netrc-record))) (signal 'pop3-error "no password record specified and no netrc entry")) (netrc-entry-password (get-netrc-record)))))) (with-fatal-error-handler* (lambda (result punt) (cond ((not (pop3-error? result)) (punt)) ((pop3-connection-challenge connection) (pop3-apop-login connection login password)))) (lambda () (send-command connection (build-command "USER" login)) (send-command connection (build-command "PASS" password)) (set-pop3-connection-login! connection login) (set-pop3-connection-password! connection password) (set-pop3-connection-state! connection 'connected)))))) ;; Login to the server using APOP authentication (no cleartext ;; passwords are sent over the network). The server appends a token to ;; its welcome message, which is built from the server's fully ;; qualified domain name and a unique serial number. The client ;; concatenates this token and the pass phrase and applies the MD5 ;; digest algorithm (a one-way hash) to produce a digest. The user ;; name and the digest are sent to the server to authenticate the ;; user. The following example comes from the RFC: ;; ;; S: +OK POP3 server ready <1896.697170952@dbc.mtview.ca.us> ;; C: APOP mrose c4c9334bac560ecc979e58001b3e22fb ;; S: +OK maildrop has 1 message (369 octets) ;; ;; In this example, the shared secret is the string `tan- ;; staaf'. Hence, the MD5 algorithm is applied to the string ;; ;; <1896.697170952@dbc.mtview.ca.us>tanstaaf ;; ;; which produces a digest value of ;; ;; c4c9334bac560ecc979e58001b3e22fb ;; (define (pop3-apop-login connection login password) (let* ((key (string-append (pop3-connection-challenge connection) password)) (digest (number->string (md5-digest->number (md5-digest-for-string key)) 16)) (status (send-command connection (build-command "APOP" login digest)))) (set-pop3-connection-login! connection login) (set-pop3-connection-password! connection password) (set-pop3-connection-state! connection 'connected) status)) ;; return number of messages and number of bytes waiting at the maildrop (define (pop3-stat connection) (check-transaction-state connection pop3-stat) (let* ((response (send-command connection "STAT")) (match (regexp-search (rx (posix-string "([0-9]+) ([0-9]+)")) response))) (values (string->number (match:substring match 1)) (string->number (match:substring match 2))))) (define (pop3-retrieve-message connection msgid) (check-transaction-state connection pop3-retrieve-message) (let* ((status (send-command connection (build-command "RETR" (number->string msgid)))) (port (socket:inport (pop3-connection-command-socket connection))) (headers (read-rfc822-headers port read-crlf-line)) (body (multiline-response->lines port))) (values headers body))) (define (pop3-retrieve-headers connection msgid) (check-transaction-state connection pop3-retrieve-headers) (let* ((status (send-command connection (build-command "TOP" (number->string msgid) "0"))) (port (socket:inport (pop3-connection-command-socket connection))) (headers (read-rfc822-headers port read-crlf-line))) (exhaust-multiline-response port) headers)) ;; Return highest accessed message-id number for the session. This ;; ain't in the RFC, but seems to be supported by several servers. (define (pop3-last connection) (check-transaction-state connection pop3-last) (let ((response (send-command connection "LAST"))) (string->number (car ((infix-splitter) response))))) ;; mark the message number MSGID for deletion. Note that the messages ;; are not truly deleted until the QUIT command is sent, and messages ;; can be undeleted using the RSET command. (define (pop3-delete connection msgid) (check-transaction-state connection pop3-delete) (send-command connection (build-command "DELE" (number->string msgid))) (values)) ;; any messages which have been marked for deletion are unmarked (define (pop3-reset connection) (check-transaction-state connection pop3-reset) (send-command connection "RSET") (values)) (define (pop3-quit connection) (check-transaction-state connection pop3-quit) (let ((status (send-command connection "QUIT"))) (close-socket (pop3-connection-command-socket connection)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Nothing exported below. (define-record-type pop3-connection :pop3-connection (make-pop3-connection host-name command-socket log-port login password challenge state) pop3-connection (host-name pop3-connection-host-name) (command-socket pop3-connection-command-socket) (log-port pop3-connection-log-port) (login pop3-connection-login set-pop3-connection-login!) (password pop3-connection-password set-pop3-connection-password!) (challenge pop3-connection-challenge set-pop3-connection-challenge!) (state pop3-connection-state set-pop3-connection-state!)) (define-condition-type 'pop3-error '(error)) (define pop3-error? (condition-predicate 'pop3-error)) (define (check-transaction-state connection caller) (if (not (eq? (pop3-connection-state connection) 'connected)) (error "not in transaction state" caller))) (define (read-response connection) (let* ((sock (pop3-connection-command-socket connection)) (in (socket:inport sock)) (line (read-crlf-line in))) (pop3-log connection (string-append "-> " line)) line)) ;; this could perhaps be improved (define (handle-response response command) (let ((match (regexp-search (rx (posix-string "^\\+OK(.*)")) response))) (if match (match:substring match 1) (let ((match2 (regexp-search (rx (posix-string "^-ERR(.*)")) response))) (if match2 (signal 'pop3-error (match:substring match2 1) command) (signal 'pop3-error response command)))))) (define (pop3-log connection line) (let ((log (pop3-connection-log-port connection))) (if log (begin (write-string line log) (newline log) (force-output log))))) (define (send-command connection command) (let* ((sock (pop3-connection-command-socket connection)) (out (socket:outport sock))) (write-string command out) (write-crlf out) (pop3-log connection (string-append "<- " command)) (handle-response (read-response connection) command))) (define (multiline-response->lines port) (let loop ((reverse-lines '())) (let ((line (read-crlf-line port))) (if (and (not (eof-object? line)) (not (string=? line "."))) (let ((line (if (string-prefix? ".." line) (substring line 1 (string-length line)) line))) (loop (cons line reverse-lines))) (reverse reverse-lines))))) (define (exhaust-multiline-response port) (let loop () (let ((line (read-crlf-line port))) (if (and (not (eof-object? line)) (not (string=? line "."))) (loop))))) (define (build-command str . opt-args) (string-join (cons str opt-args)))