;;; -*- Mode:LISP; Package:TCP-APPLICATION; Base:10; Readtable:CL -*- #|| Copyright LISP Machine, Inc. 1985, 1986, 1987 See filename "Copyright.Text" for licensing and release information. Simple implementation of SMTP. ||# (define-network-service *tcp-smtp-service* :smtp :tcp "Simple Mail Transfer Protocol (RFC 821)" :listen-port (sym ipport-smtp) :toplevel-function 'smtp-server-function :auto-enable? t) (defvar *smtp-server-state* nil "This is a plist") (defmacro smtp-server-state (keyword) `(get *smtp-server-state* ,keyword)) (defun smtp-server-function (stream) (smtp-reply stream 220 "~A Simple Mail Transfer Service Ready" (send si:local-host :name)) (catch 'smtp-server-function (do ((*smtp-server-state* nil) (si:user-id "SMTP_SERVER") (string (ftp:ftp-readline stream) (ftp:ftp-readline stream))) ((null string)) (smtp-server-command string stream)))) (defun smtp-reply (stream code &rest format-args) (apply #'format stream "~D ~@?" code format-args) (smtp-terpri stream) (send stream :force-output)) (defun smtp-terpri (stream) (send stream :tyo #o15) (send stream :tyo #o12)) (defvar *smtp-commands* '( ;;The following are the SMTP commands: (helo " ") (mail " FROM: ") (rcpt " TO: ") (data "") (rset "") (send " FROM: ") (soml " FROM: ") (saml " FROM: ") (vrfy " ") (expn " ") (help "[ ] ") (noop "") (quit "") (turn ""))) (defun smtp-server-command (string stream) (let ((command (global:intern-soft (string-upcase (if (string-search " " string) (substring string 0 (string-search " " string)) string)) "TCP-APPLICATION"))) (cond ((and command (get command 'smtp-command)) (funcall (get command 'smtp-command) (string-trim " " (substring string (string-length command))) stream)) ((and command (assoc command *smtp-commands* :test #'eq)) (smtp-reply stream 502 "Command not implemented")) (t (smtp-reply stream 500 "Syntax error, command unrecognized: ~A" string))))) (defun (helo smtp-command) (string stream) (setf (smtp-server-state :correspondant-host) string) (smtp-reply stream 250 "~A" (send si:local-host :name))) (defun (help smtp-command) (string stream &aux temp) (cond ((string-equal string "") (format stream "211- Commands==> (* means not implemented)" stream) (smtp-terpri stream) (princ "211- " stream) (do ((j 0 (1+ j)) (l *smtp-commands* (cdr l))) ((null l) (smtp-terpri stream)) (princ (caar l) stream) (or (get (caar l) 'smtp-command) (princ "*" stream)) (cond ((null (cdr l))) ((= 5 (mod j 6)) (smtp-terpri stream) (princ "211- " stream)) (t (princ " " stream)))) (smtp-reply stream 211 "HELP for more information")) ((setq temp (assoc string *smtp-commands* :test #'string-equal)) (smtp-reply stream 214 "~A ~A" (car temp) (cadr temp))) (t (smtp-reply stream 214 "Unknown command: ~A" string)))) (defun (noop smtp-command) (string stream) string (smtp-reply-ok stream)) (defun smtp-reply-ok (stream) (smtp-reply stream 250 "Ok")) (defun (quit smtp-command) (string stream) string (smtp-reply stream 221 "~A Service closing transmission channel" (send si:local-host :name)) (throw 'smtp-server-function nil)) (defun (rset smtp-command) (string stream) string (setq *smtp-server-state* nil) (smtp-reply-ok stream)) (defun (mail smtp-command) (string stream) (smtp-mail-cmd string stream nil)) (defun smtp-mail-cmd (string stream terminal-modep) (let ((f (string-search "FROM:" string))) (cond ((null f) (smtp-reply stream 501 "No FROM: field found")) (t (setf (smtp-server-state 'from) (list (substring string 0) (string-right-trim ">" (string-left-trim "<" (substring string (+ f (length "FROM:"))))))) (dolist (x '(to data)) (setf (smtp-server-state x) nil)) (setf (smtp-server-state 'terminal-modep) terminal-modep) (smtp-reply-ok stream))))) (defun (send smtp-command) (string stream) (smtp-mail-cmd string stream t)) (defvar *smtp-mailer* :simple "A keyword identifying the mailer for use with the server, when not using interactive ``terminal'' messages. It should have two properties: SMTP-RCPT-VERIFIER: A function which takes arguments of the original argument string, the address string and SMTP stream. It should frob the SMTP-SERVER-STATE, and do replies with SMTP-REPLY{-OK}. SMTP-MAILER: A function which delivers mail. The arguments are the transmitting host as a string, a list of the lines of the message, and the SMTP stream. It should also look at SMTP-SERVER-STATE and do the replies with SMTP-REPLY{-OK}.") (defun simple-rcpt-verifier (string address-string stream) (let ((addr (smtp-parse-to-address address-string))) (if (null addr) (smtp-reply stream 501 "Syntax error in parameters or arguments") (let ((mb (smtp-lookup-mailbox addr))) (cond ((null mb) (smtp-reply stream 550 "unknown local user")) ((not (smtp-mailbox-probe mb)) (smtp-reply stream 450 "mailbox unavailable")) (t (push (list (string-append string "") addr mb) (smtp-server-state 'to)) (smtp-reply-ok stream))))))) (setf (get :simple 'smtp-rcpt-verifier) 'simple-rcpt-verifier) (defun (rcpt smtp-command) (string stream) (let ((f (string-search "TO:" string))) (if (null f) (smtp-reply stream 501 "No TO: field found") ;; Only use the mailer hook when not sending to the user. (funcall (if (smtp-server-state 'terminal-modep) #'simple-rcpt-verifier (get *smtp-mailer* 'smtp-rcpt-verifier)) (copy-seq string) ; original argument, copied (string-left-trim " <" (string-right-trim "> " (substring string (+ f (length "TO:"))))) stream)))) (defun smtp-parse-to-address (st &aux n host) (cond ((setq n (string-search "@" st)) (setq host (si:parse-host (substring st (1+ n)) t nil)) (if host (list (substring st 0 n) host))) (t (list st si:local-host)))) (defun smtp-lookup-mailbox (address) (cond ((smtp-server-state 'terminal-modep) (if (and (string-equal (car address) (si:symbol-value-globally 'si:user-id)) (eq (cadr address) si:local-host)) ;; in fact, not correct, we must also handle the si:*other-processors* address)) (t (smtp-lookup-mailbox-1 address)))) (defun smtp-lookup-mailbox-1 (address &aux f) ;; this could probably be modified to work off of a list of ;; users names and mailboxes. In fact, without that there is ;; no way of telling the difference between an unknown user ;; and a SYS:REMOTE-NETWORK-ERROR (that is, if there is a remote ;; network error due to host unavailability then we cannot tell if ;; the name is legal and can be retried). (or (eq (cadr address) si:local-host) (do ((l si:*other-processors* (cdr l))) ((null l) (return-from smtp-lookup-mailbox-1 nil)) (let ((h (si:get-host-from-address (si:%processor-conf-chaos-address (si:op-proc-conf (car l))) :chaos))) (and (eq (send h :system-type) :lispm) (eq (cadr address) h) (return))))) (setf f (fs:make-pathname :host (cadr address) :directory (car address) :name "MAIL" :type "TEXT")) (global:condition-case () (list address (probe-file f) f) (fs:directory-not-found nil) (sys:remote-network-error (list address :host-not-available)))) (defun smtp-mailbox-probe (mb) (cond ((smtp-server-state 'terminal-modep) (cond (zwei:*converse-gagged* nil) (t mb))) ((eq (cadr mb) :host-not-available) nil) (t mb))) (defun simple-local-mailer (host data stream) (unless (smtp-server-state 'terminal-modep) (push (format nil "Received: from ~A by ~A with SMTP; ~\\time\\" host si:local-host (time:get-universal-time)) data)) (dolist (mbx (smtp-server-state 'to)) (smtp-send-one-message mbx (smtp-server-state 'from) (mapcar 'car (smtp-server-state 'to)) data)) (smtp-reply-ok stream)) (setf (get :simple 'smtp-mailer) 'simple-local-mailer) (defun (data smtp-command) (IGNORE stream &aux data) (cond ((not (smtp-server-state 'from)) (smtp-reply stream 554 "no from: given")) ((not (smtp-server-state 'to)) (smtp-reply stream 554 "no legal to: given; nobody to send it to")) (t (let ((string (make-array 80 :element-type 'string-char :fill-pointer 0 :adjustable t))) (smtp-reply stream 354 "Start mail input; end with .") (do () ((progn (setf (fill-pointer string) 0) (do ((c)) ((null (setq c (send stream :tyi))) (throw 'smtp-server-function nil)) (cond ((and (= c #\.) (= (length string) 1))) ((= c #o15) (send stream :tyi) (if (string-equal string ".") (return t)) (push (substring string 0) data) (return nil)) (t (vector-push-extend (if (member c '(12 9) :test #'eq) (+ c #o200) c) string))))))) (setq data (nreverse data)) (funcall (if (smtp-server-state 'terminal-modep) #'simple-local-mailer (get *smtp-mailer* 'smtp-mailer)) (smtp-identify-corresponding-host) data stream) ;; Reset the state -- there can be more than one session. (setf (smtp-server-state 'from) nil) (setf (smtp-server-state 'to) nil))))) (defun smtp-identify-corresponding-host () (if (get *smtp-server-state* :correspondant-host) (format nil "~A(~A)" (get *smtp-server-state* :correspondant-host) (network-server-client)) (network-server-client))) (defun smtp-send-one-message (desc from to data) (cond ((smtp-server-state 'terminal-modep) (smtp-send-one-message-to-terminal desc (cadr from) to data)) (t (smtp-send-one-message-to-mailbox desc (car from) to data)))) (defun smtp-send-one-message-to-mailbox (desc from to data) ;; actually need a lock on the FILE in question. ;; e.g. ZMAIL might be hacking this file too. (with-lock ((get 'smtp-send-one-message-to-mailbox 'lock)) (let ((mbx (caddr desc))) (let ((pfile (cadr mbx)) (file (caddr mbx))) ;; if (probe-file file) is not eq to pfile ;; then there has been activity. could be interesting. (cond ((not (setq pfile (probe-file file))) (with-open-file (stream file :out) (smtp-output-one-message from to data stream))) (t ;; But we would also really like to use APPEND mode ;; on this file! (with-open-file (stream-out (send pfile :new-version (1+ (send pfile :version))) :out) (with-open-file (stream-in pfile :in) (global:stream-copy-until-eof stream-in stream-out)) (smtp-output-one-message from to data stream-out)) (delete-file pfile) (fs:expunge-directory pfile))))))) (defun smtp-send-one-message-to-terminal (desc from to data) desc to (zwei:converse-receive-from-network (format nil "~{~A~^~%~}" data) :sender from)) (defvar *smtp-output-synthetic-header* nil) (defun smtp-output-one-message (from to data stream) (when *smtp-output-synthetic-header* (send stream :line-out from) (dolist (x to) (send stream :line-out x))) (dolist (x data) (send stream :line-out x)) (send stream :line-out "")) (defun valid-lispm-host (host) (and (si:parse-host host t nil) (eq :lispm (send (si:parse-host host t nil) :system-type)))) (defun make-lispm-zmail-init-file (username host) "This function makes a ZMAIL init file for a user on a lispmachine" (check-type username string) (check-type host (satisfies valid-lispm-host) "the name of a lispmachine host") (setq host (send (si:parse-host host) :name)) (let ((mail-file (fs:make-pathname :host host :directory username :name "MAIL" :type "TEXT")) (babyl-file (fs:make-pathname :host host :directory username :name "BABYL" :type "TEXT")) (zmail-init (fs:make-pathname :host host :directory username :name "ZMAIL" :type "INIT"))) (with-open-file (stream zmail-init :out) (format stream ";;-*-mode:lisp;package:zwei;base:10-*-~ ~%(login-setq *zmail-startup-file-name* ~S)~ ~%(login-setq *from-user-id* ~S)~ ~%(login-setq *from-host* ~S)~%" (send babyl-file :string-for-printing) username host)) (with-open-file (stream babyl-file :out) (format stream "Babyl Options:~ ~%Append:1~ ~%Version:5~ ~%Mail: ~A~ ~%Owner:~A~ ~%Summary-window-format: T~ ~% " (send mail-file :string-for-printing) username))))