;;; -*- Mode:LISP; Package:MAIL; Base:10; Readtable:T; Lowercase:T -*- ;;; ;;; Lisp Machine Mailer ;;; ;;; (c) 1985 Lisp Machine Incorporated ;;; ;;; Changes: ;;; Sometime, someone: written ;;; RpK, 7 July 1983: Made to use NES, WITH-OPEN-FILE-CASE ;;; Rpk, 8 July 1983: Works. Delayed delivery implemented for more than ;;; *DELAY-DELIVERY-THRESHOLD* (3 is the default). ;;; RpK, 20 December 1984: Clean up for the modern world, customers. ;;; Now it always queues... ;;; RpK, 4 January 1985: Support Chaosnet. *delay-delivery* helps debugging ;;; RpK, 8 January 1985: Be a real system. (defvar *debug-server* ()) (defvar *delay-delivery* () "If T, do not actually deliver") (defvar *mailing-lists* nil) (define-site-variable *mail-server-p* :mail-server "Am I a mail server ?") (defmacro find-in-mailing-lists (string) #+explorer `(ass #'string-equal ,string *mailing-lists*) #-explorer `(cli:assoc ,string *mailing-lists* :test #'string-equal)) (defvar *mailing-list-file* "LM:MAIL;LIST.LISP") (defvar *mailing-list-version-number* nil) (defvar *unknown-addresses* ()) (defvar *mailer-initialized* ()) ;;; For now only allow one file for all servers. (defvar *mail-server-lock* nil) (defvar *server-enabled* nil) (defvar *server-disable-reason* "Mailer not initialized") (defvar *history* nil) (add-initialization "Reset Mailer History" '(setq *history* nil) '(:before-cold)) (defvar *notify-mode* nil "Can be NIL, meaning not to record anything, or: :NOTIFY Do a window notification :OUTPUT Print the notification on *TERMINAL-IO* :HISTORY Push the note on the history. Use PRINT-HISTORY") (defun notify (format-string &rest format-args) (case *notify-mode* (:notify (tv:notify () format-string format-args)) (:output (fresh-line *terminal-io*) (write-string "[Mailer: " *terminal-io*) (apply #'format *terminal-io* format-string format-args) (write-line "]" *terminal-io*)) (:history (push (cons (get-universal-time) (apply #'format nil format-string format-args)) *history*)))) (defun print-history () "Print out the activity of the Mailer" (dolist (h *history*) (fresh-line) (time:print-universal-time (car h)) (write-string " ") (write-string (cdr h)))) ;;; The queue file format is depressingly simple. The first part is a cons, whose CDR is ;;; unused and whose CAR is a list of recipients of the form (type . options). Later on the ;;; CDR will have more interesting information in it (for use with GET). ;;; Standard mailer per-message properties: ;;; :FAILURES, a number ;;; :FIRST-FAILURE-DATE also a number, a universal time (defvar *qfile-template* () "Set by initialising the mailer") (defvar *qfile-name-counter* 0) (defun qfile-name (&optional (name (format () "~16R-~D" (time:get-universal-time) (without-interrupts (incf *qfile-name-counter*))))) (send *qfile-template* :new-name (string name))) ;;; Currently we have just (:NAME . uname), (:FILE . filename) and ;;; (:FOREIGN address-for-host original-address host-or-class) (see below) (defstruct (address (:type :list*) :conc-name) type options) ;;; A class symbol describes what kind of delivery to use. No classes are currently implemented, ;;; but there will be some provided so that the Lisp Machine can act as a mail gateway. (defstruct (foreign-address (:type :list) (:conc-name foreign-) (:but-first address-options)) (mailer-address () :documentation "what to use when contacting a mail server") (original-address () :read-only t :documentation "what was received by the mailer, usually containing @") (class () :documentation "either a delivery class or a host to contact")) (defvar *failed-address* (make-address :type :file :options "MAIL;FAILED-MAIL.TEXT")) ;;; Buffer resource (defresource text-buffer (size) :constructor (make-array (or size 1000.) :fill-pointer 0 :element-type 'string-char) :initializer (setf (fill-pointer object) 0) :matcher (or (null size) (<= size (array-total-size object)))) (defmacro with-text-buffer ((buffer stream) &body body) "This is very useful with GET-MAIL-TEXT" `(using-resource (,buffer text-buffer (send ,stream :send-if-handles :length)) ,@body)) (DEFUN GET-MAIL-TEXT (STREAM buffer) (WITH-OUTPUT-TO-STRING (SSTREAM buffer) (STREAM-COPY-UNTIL-EOF STREAM SSTREAM) (FUNCALL SSTREAM :FRESH-LINE))) ;;; This ought to be globalized (net-ized ?), really (defmacro with-open-network-stream ((stream form) &body body) "Like WITH-OPEN-STREAM, but ALWAYS uses abort mode." `(let ((,stream)) (unwind-protect (progn (setq ,stream ,form) ,@body) (when ,stream (send ,stream :close :abort))))) (defmacro as-mail-server ((stream form) &body body) `(with-open-network-stream (,stream ,form) (cond (*server-enabled* (send ,stream :accept) ,@body) (t ;; Any :close after :reject should be harmless (send ,stream :reject *server-disable-reason*))))) ;;; This always appends new mail. ;;; Retuns either T (for success) or an error instance. In the latter case, the ;;; negative or temporary negative message has already been sent. ;;; Does not rename the mail file at the present. Too bad LMFS can't append. ;;; Locks out all other MAIL SERVER  FILE System activity. (DEFUN FILE-DELIVER-MAIL (ADDRESS PATHNAME TEXT STREAM &OPTIONAL (INCLUDE-TAIL T)) (WITH-LOCK (*MAIL-SERVER-LOCK*) (PROG () (WITH-OPEN-FILE-CASE (INFILE PATHNAME :DIRECTION :INPUT) (FS:FILE-NOT-FOUND (RETURN (FILE-DELIVER-MAIL-INTERNAL ADDRESS () PATHNAME TEXT STREAM INCLUDE-TAIL))) (ERROR (FORMAT STREAM "-Error opening for append: ~A" (SEND INFILE :REPORT-STRING)) (SEND STREAM :FORCE-OUTPUT) (RETURN INFILE)) (:NO-ERROR (RETURN (FILE-DELIVER-MAIL-INTERNAL ADDRESS INFILE PATHNAME TEXT STREAM INCLUDE-TAIL))))))) (DEFUNP FILE-DELIVER-MAIL-INTERNAL (ADDRESS INFILE PATHNAME TEXT STREAM INCLUDE-TAIL) (WITH-OPEN-FILE-CASE (OUTFILE PATHNAME :DIRECTION :OUTPUT) (FS:NO-MORE-ROOM (FORMAT STREAM "%Disk full, please try later.~%") (SEND STREAM :FORCE-OUTPUT) (RETURN OUTFILE)) (ERROR (FORMAT STREAM "-Unexpected error for ~A: ~A~%" ADDRESS (SEND OUTFILE :REPORT-STRING)) (FUNCALL STREAM :FORCE-OUTPUT) (RETURN OUTFILE)) (:NO-ERROR (IF INFILE (STREAM-COPY-UNTIL-EOF INFILE OUTFILE)) (FUNCALL OUTFILE :STRING-OUT TEXT) (IF INCLUDE-TAIL (FUNCALL OUTFILE :LINE-OUT "")) (IF INFILE (SEND INFILE :DELETE)) (FUNCALL STREAM :FORCE-OUTPUT)) (RETURN T))) (defun write-queue-file (qfile recipients properties text stream) (let ((*print-level* ()) (*print-length* ()) (*print-base* 10.) (*package* si:pkg-user-package) (*print-array* t) (*READTABLE* SI:STANDARD-READTABLE)) (file-deliver-mail "LIST" qfile (format () "~S~A" (cons recipients properties) text) stream ()))) (defvar *file-defaults* (make-pathname :host si:local-host :directory '("MAIL") :name "MAIL" :type "TEXT" :version :newest)) (defun mail-file-for-address (address) (ecase (address-type address) (:name (make-pathname :host si:local-host :directory (address-options address) :name "MAIL" :TYPE "TEXT" :VERSION :NEWEST)) (:FILE (merge-pathnames (address-options address) *file-defaults*)))) ;;; Delivery (defvar *delivery-methods* nil) (defmacro define-delivery-method (type (address) filter (addresses text report-stream) &body delivery) `(progn (defun (:property ,type deliver-filter) (,address) ,filter) (defun (:property ,type deliver-driver) (,addresses ,text ,report-stream) ,@delivery) (pushnew ',type *delivery-methods*))) (defun deliver-mail (addresses text report-stream &aux failed filter to-deliver) (dolist (method *delivery-methods*) (setq to-deliver nil) (setq filter (get method 'deliver-filter)) (dolist (address addresses) (when (funcall filter address) (push address to-deliver) (setq addresses (delq address addresses)))) (when to-deliver (setq failed (nconc failed (funcall (get method 'deliver-driver) to-deliver text report-stream))))) (if addresses (format report-stream "Warning: These addresses appear unreachable:~%~S~%" addresses)) (setq failed (nconc addresses failed))) (define-delivery-method append-to-file (address) (memq (address-type address) '(:name :file)) (addresses text report-stream) (let ((failed ())) (dolist (address addresses) (if (errorp (file-deliver-mail address (mail-file-for-address address) text report-stream)) (push address failed) (format report-stream "~&OK: ~S (local)" address))) failed)) (defun mail-deliver-qfile (qfile &optional (report-stream #'si:null-stream) &aux recipients failed-recipients properties) (format report-stream "~&Attempting delivery of ~A:~%" qfile) (with-open-file-case (msg qfile) (error ()) ; don't have to try anything (:no-error (with-text-buffer (text msg) (setq properties (let ((*read-base* 10.) (*package* si:pkg-user-package) (*readtable* si:standard-readtable)) (read msg))) (setq recipients (first properties)) (get-mail-text msg text) (setq failed-recipients (deliver-mail recipients text report-stream)) (send msg :delete) (when failed-recipients (when (= (incf (get properties :failures)) 1) (setf (get properties :first-failure-date) (get-universal-time))) (write-queue-file (send qfile :new-pathname :name (let ((name (send qfile :NAME))) (if (string-equal "FAILED_" name :end2 7) name (string-append "FAILED_" name))) :type "-Q-" :version :newest) failed-recipients (cdr properties) text report-stream) (format report-stream "~& Requeueing file.~%")))))) (defun reception-line (server-stream &optional (protocol "CHAOS-MAIL")) (multiple-value-bind (seconds minutes hours date month year dow dst-p) (time:get-time) (format () "Received: from ~A by ~A with ~A; ~A ~D-~A-~2,'0D ~2,'0D:~2,'0D:~2,'0D-~A" (send (send server-stream :foreign-host) :name) (send si:local-host :name) protocol (time:day-of-the-week-string dow :short) date (time:month-string month :short) year hours minutes seconds (time:timezone-string time:*timezone* dst-p)))) (defun address-existent-p (string) "Returns NIL, a string (a directory), or a list of addresses." (or (let ((dir (fs:lookup-directory string t))) (and dir (fs:directory-name dir))) (cdr (find-in-mailing-lists string)))) ;; Deal with mailing lists. (DEFUN EXPAND-ADDRESSES (ADDRESS-LIST &AUX RECIPIENTS LIST-ADDRESSES) (REMOVE-DUPLICATES (DOLIST (ADDRESS ADDRESS-LIST RECIPIENTS) (COND ((CONSP ADDRESS) ; should only get :FILE or :FOREIGN here (PUSH ADDRESS RECIPIENTS)) ((FS:LOOKUP-DIRECTORY ADDRESS T) (PUSH (MAKE-ADDRESS :TYPE :NAME :OPTIONS ADDRESS) RECIPIENTS)) ((AND (STRINGP ADDRESS) (SETQ LIST-ADDRESSES (CDR (find-in-mailing-lists ADDRESS)))) (SETQ RECIPIENTS (NCONC RECIPIENTS (EXPAND-ADDRESSES LIST-ADDRESSES)))) ((STRING-SEARCH-CHAR #/; ADDRESS) (PUSH (MAKE-ADDRESS :TYPE :FILE :OPTIONS ADDRESS) RECIPIENTS)) (T (PUSH ADDRESS *UNKNOWN-ADDRESSES*) (PUSH *FAILED-ADDRESS* RECIPIENTS)))) :TEST #'EQUALP)) (DEFUN READ-MAILING-LIST-FILE (&optional output-p &aux version NEW-LIST (count-errors 0) (count-lists 0) (count-names 0)) (let ((mailing-list-pathname (probef (fs:parse-pathname *Mailing-List-FILE* si:local-host)))) (when (typep mailing-list-pathname 'pathname) (setq version (send mailing-list-pathname :version)) (if (eql version *Mailing-List-Version-Number*) (format (if output-p *terminal-io* nil) "Mailing list file already updated.") (with-lock (*mail-server-lock*) (fs:reading-from-file (list mailing-list-pathname) (incf count-lists) (do* ((l (cdr list) (cdr l)) ; munge it in place (address (car l) (car l))) ((null l)) (incf count-names) (unless (consp address) (setf (car l) (multiple-value-bind (error-p result) (mailer-parse-address address) (if (not error-p) result (when output-p (format *error-output* "~&Bad address ~A in ~A list: ~A~%" address (car list) result)) (decf count-names) (incf count-errors) *failed-address*))))) (PUSH LIST NEW-LIST)) (setq *Mailing-List-Version-Number* version) (setq *mailing-lists* new-list)) (format (if output-p *terminal-io* nil) "~&Finished: ~[No~;one~:;~:*~D~] list~:P, ~[no~;one~:;~:*~D~] name~:P, ~[no~;one~:;~:*~D~] error~:P." count-lists count-names count-errors))))) (defun main-lm-mail-server-host () (dolist (h (si:get-site-option :chaos-mail-server-hosts)) (if (eq :lispm (send (setq h (si:parse-host h)) :system-type)) (return h)))) (defun lm-mail-server-p (host) (setq host (si:parse-host host)) (dolist (h (si:get-site-option :chaos-mail-server-hosts)) (if (eq host (si:parse-host h)) (return host)))) (DEFUN MESSAGE-HEADERS-AND-TEXT (TEXT) "Returns two values: a string with the headers, and a string with the message. If no header was found, () is returned as the second value. The header string will not end with a CR, or will the message string begin with one (unless there are 2 blank lines at the start of the message text)." (LET ((IDX (STRING-SEARCH #.(FORMAT () "~C~C" #\CR #\CR) TEXT))) (IF (NULL IDX) TEXT (VALUES (SUBSTRING TEXT 0 IDX) (SUBSTRING TEXT (+ 2 IDX)))))) (DEFUN FROM-UNAME-FOR-DELIVERY (TEXT &AUX (UNAME "???") HTEXT EOF-P) (WITH-INPUT-FROM-STRING (S (MESSAGE-HEADERS-AND-TEXT TEXT)) (DO () (EOF-P UNAME) (MULTIPLE-VALUE (HTEXT EOF-P) (SEND S :LINE-IN)) (WHEN (STRING-EQUAL "From:" (SUBSTRING HTEXT 0 5)) (LET ((Z-UNAME (ZWEI:PARSE-ADDRESSES (STRING-TRIM '(#\SP #\TAB) (SUBSTRING HTEXT 5 (OR (STRING-SEARCH-CHAR #\CR HTEXT) (STRING-LENGTH HTEXT))))))) (COND ((NOT Z-UNAME) (SETQ UNAME "Unknown")) ((STRINGP Z-UNAME) (SETQ UNAME "Unknown")) (T (SETQ UNAME (SECOND (MEMQ :NAME (FIRST Z-UNAME)))) (LET ((HOST (SECOND (MEMQ :HOST (FIRST Z-UNAME))))) (IF HOST (IF (LISTP HOST) (DOLIST (H HOST) (IF (NON-LOCAL-MAIL-HOST-P H) (SETQ UNAME (STRING-APPEND UNAME #/@ H)))) (IF (NON-LOCAL-MAIL-HOST-P HOST) (SETQ UNAME (STRING-APPEND UNAME #/@ HOST))))))))))))) (DEFUN NON-LOCAL-MAIL-HOST-P (HOST-STRING) (NOT (OR (MEM #'STRING-EQUAL HOST-STRING (SEND SI:LOCAL-HOST :HOST-NAMES)) (STRING-EQUAL HOST-STRING (SEND SI:LOCAL-HOST :NAME-AS-FILE-COMPUTER))))) (DEFUN MAIL-HOST-NAME (STRING) (STRING (OR (SI:PARSE-HOST STRING T ()) (FS:GET-PATHNAME-HOST STRING T) STRING))) (DEFUN FORCE-DELIVERY (&OPTIONAL (STREAM STANDARD-OUTPUT)) (LET ((DIR (FS:DIRECTORY-LIST (SEND *QFILE-TEMPLATE* :NEW-NAME :WILD)))) (DOLIST (ELEM DIR) (if (FIRST ELEM) (MAIL-DELIVER-QFILE (FIRST ELEM) STREAM)))) (FS:EXPUNGE-DIRECTORY *QFILE-TEMPLATE*)) (DEFUN INITIALIZE-MAILER () (when *mail-server-p* (UNLESS *MAILER-INITIALIZED* (send *terminal-io* :fresh-line) (send *terminal-io* :line-out "[Mailer: First initializations]") (setq *qfile-template* (fs:make-pathname :host (fs:get-pathname-host si:local-host) :directory '("MAIL" "QUEUE") :type "-Q-" :version :newest)) (load "LM:MAIL.COM;BOOT" :if-does-not-exist nil :verbose nil :package "MAIL" :set-default-pathname nil)) (send *terminal-io* :fresh-line) (send *terminal-io* :string-out "[Mailer: ") (read-mailing-list-file t) (send *terminal-io* :tyo #\]) (format t "~%[Mailer: Checking for mail to deliver]~%") (force-delivery) (setq *mailer-initialized* t) (enable-mail-server))) (defun disable-mail-server (&optional (why "Mailer disabled")) (setq *server-enabled* nil) (setq *server-disable-reason* why)) (defun enable-mail-server () (setq *server-enabled* t)) (add-initialization "Initialize Mailer" '(initialize-mailer)) (add-initialization "Reset Mailer State" '(progn (disable-mail-server "Mailer not intialized") (SETQ *MAILER-INITIALIZED* ())) () 'si:before-cold-initialization-list) ;;; Testing functions and the like (defun _hack-it () (setq *mail-server-p* t zwei:*mail-chaos-hosts* (delq si:local-host zwei:*mail-chaos-hosts*)) (push si:local-host zwei:*mail-chaos-hosts*)) (defun _send-test-message (&rest addresses &aux s) (unwind-protect (progn (setq s (chaos:open-stream si:local-host "MAIL")) (dolist (a addresses) (send s :line-out a) (send s :force-output) (format t "~% Response for ~A: ~A" a (send s :line-in))) (send s :tyo #\Newline) (format s "From: ~A <~A@~A>~%To: Nobody in particular~%Subject: This is a test~2%" fs:user-personal-name-first-name-first (fs:uname-on-host fs:user-login-machine) fs:user-login-machine) (dotimes (i 10) (format s "Blah~%")) (format s "~% --- F I N I S ---~%") (send s :eof) (format t "~%Response for text: ~A" (send s :line-in))) (when s (send s :close :abort)))) ;;; Here are typical address and the corresponding structures from ;;; (car (zwei:parse-addresses address)). (getf * :interval) returns an interval ;;; so you can use the ZWEI:INTERVAL functions/accessors ;;; ;;; foo : (:NAME "foo" :HOST NIL ;;; :INTERVAL (("foo" 0.) ("foo" 3.))) ;;; foo@cap : (:NAME "foo" :HOST ("cap") ;;; :INTERVAL (("foo@cap" 0.) ("foo@cap" 7.))) ;;; "rpk@ccc"@cap : (:NAME "/"rpk@ccc/"" :HOST ("cap") ;;; :INTERVAL (("/"rpk@ccc/"@cap" 0.) ("/"rpk@ccc/"@cap" 13.))) ;;; rpk%mc@cap : (:NAME "rpk" :HOST ("mc" "cap") ;;; :INTERVAL (("rpk%mc@cap" 0.) ("rpk%mc@cap" 10.))) ;;; mitccc!rpk@eddie : (:NAME "mitccc!rpk" :HOST ("eddie") ;;; :INTERVAL (("mitccc!rpk@eddie" 0.) ("mitccc!rpk@eddie" 16.))) ;;; rpk%ccc : (:NAME "rpk" :HOST ("ccc") ;;; :INTERVAL (("rpk%ccc" 0.) ("rpk%ccc" 7.)))) ;;; Returns two values: errorp and address (or error string). The address returned ;;; is either a string (to be expanded into a mailing list, because it is actually ;;; local) or an address structure. Note that :FOREIGN is canonicalised for taking ;;; out the local host, but initially the class slot is either a host or list of hosts. ;;; When the mailer gets more complex, this will get processed for a real class slot. (defun reachable-host-p (host) ; this will get hairier later on (send host :network-typep :chaos)) (defun mailer-parse-address (string) ;; Speed up the most common case (if (not (string-search-set "%@()[]<>" string)) (values nil string) (condition-case (a) (zwei:parse-addresses string) (error (values t (format () "Parse error for ~A" string))) (:no-error (if (stringp a) (values t a) ; Error message (massage-parsed-address (car a) string)))))) (defun massage-parsed-address (a original) (let ((hs (getf a :host))) (if (null hs) (values nil (getf a :name)) (let* ((last-host-string (car (last hs))) (last-host (si:parse-host last-host-string t))) (cond ((null last-host) (values t (format nil "Unknown host ~A" last-host-string))) ((eq last-host si:local-host) (setf (getf a :host) (nbutlast hs)) (massage-parsed-address a original)) ((reachable-host-p last-host) (values nil (make-address :type :foreign :options (make-foreign-address :mailer-address (mailer-address-for-other-host (butlast hs) (getf a :name)) :original-address original :class (send last-host :name))))) (t (values t (format () "Unreachable host ~A" last-host-string)))))))) (defun mailer-address-for-other-host (hosts name &aux (strings (ncons name))) (dolist (h hosts) (push "%" strings) (push h strings)) (when (> (length strings) 2) (setf (cadr strings) "@")) (apply #'string-append (nreverse strings))) (defun address-as-string (address) "Returns a fully-qualified address string (user@host)" (case (address-type address) (:name (format () "~A@~A" (address-options address) si:local-host)) (:file (format () "/"~A/"@~A" (address-options address) si:local-host)) (:foreign (foreign-original-address address))))