;;; -*- Mode:Lisp; Readtable:T; Package:USER; Base:10; Patch-File:T -*-
;;; Patch file for Mailer version 2.2
;;; Reason:
;;;  Make the mailer more general, in preparation for SMTP support.
;;; Written 3-Sep-86 18:08:09 by RpK (Robert P. Krajewski) at site LMI Cambridge
;;; while running on Hastur the Unspeakable from band 2
;;; with System 110.232, Lambda-Diag 7.17, Experimental Local-File 68.7, FILE-Server 18.4, Unix-Interface 9.1, ZMail 65.14, Object Lisp 3.4, Tape 6.39, Site Data Editor 3.3, Tiger 24.0, KERMIT 31.3, Window-Maker 1.1, Gateway 4.8, TCP-Kernel 39.7, TCP-User 62.7, TCP-Server 45.5, MEDIUM-RESOLUTION-COLOR 3.4, MICRO-COMPILATION-TOOLS 3.2, System Revision Level 3.167, Experimental Mailer 2.1, microcode 1563, SDU Boot Tape 3.12, SDU ROM 102.



; From modified file LAD: RELEASE-3.NETWORK.MAILER; MAIN.LISP#5 at 3-Sep-86 18:08:10
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; MAIN  "


(defun terminal-warn-function (severity stream &rest format-args)
  (fresh-line stream)
  (write-string (symbol-name severity) stream)
  (write-string ": " stream)
  (apply #'format stream format-args)
  (fresh-line stream))

(defvar *warn-function* 'terminal-warn-function
  "Should take arguments of SEVERITY STREAM &REST FORMAT-ARGS")

(defun warn-stream (type stream &rest format-args)
  "Print a warning onto STREAM, which may be a server stream.
The FORMAT output should not output a newline.  TYPE is a keyword,
in the set {:DISK-FULL, :RANDOM-ERROR, :APPEND-ERROR}."
  (apply *warn-function* type stream format-args))

;;; 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 (warn-stream :append-error STREAM
				"Error opening for append: ~A" (SEND INFILE :REPORT-STRING))
		   (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
     (warn-stream :disk-full stream "Disk full, please try later.")
     (RETURN OUTFILE))
    (ERROR
     (warn-stream :random-error STREAM "Unexpected error for ~A: ~A"
		  ADDRESS (SEND OUTFILE :REPORT-STRING))
     (FORCE-OUTPUT STREAM)
     (RETURN OUTFILE))
    (:NO-ERROR
     (IF INFILE (STREAM-COPY-UNTIL-EOF INFILE OUTFILE))
     (write-STRING TEXT OUTFILE)
     (IF INCLUDE-TAIL (write-LINE "" OUTFILE))
     (IF INFILE (SEND INFILE :DELETE))
     (FORCE-OUTPUT STREAM))
     (RETURN T)))

))

; From modified file LAD: RELEASE-3.NETWORK.MAILER; MAIN.LISP#5 at 3-Sep-86 18:08:34
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; MAIN  "


(defmacro define-delivery-method (type (address) filter (addresses plist text report-stream) &body delivery)
  `(progn
     (defun (:property ,type deliver-filter) (,address)
       ,filter)
     (defun (:property ,type deliver-driver) (,addresses ,plist ,text ,report-stream)
       ,@delivery)
     (pushnew ',type *delivery-methods*)))

(defun deliver-mail (addresses plist 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 plist text report-stream)))))
  (if addresses
      (format report-stream "Warning: These addresses appear unreachable:~%~S~%" addresses))
  (setq failed (nconc addresses failed)))

))

; From modified file LAD: RELEASE-3.NETWORK.MAILER; MAIN.LISP#5 at 3-Sep-86 18:09:31
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; MAIN  "

(define-delivery-method append-to-file (address)
			(memq (address-type address) '(:name :file))
			(addresses plist text report-stream)
  (declare (ignore plist))
  (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))

))

; From modified file LAD: RELEASE-3.NETWORK.MAILER; MAIN.LISP#5 at 3-Sep-86 18:09:58
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; MAIN  "

(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 (cdr properties) 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.~%"))))))

))

; From modified file LAD: RELEASE-3.NETWORK.MAILER; MAIN.LISP#5 at 3-Sep-86 18:10:11
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; MAIN  "


(defun reception-line (server-stream &optional (protocol "CHAOS-MAIL"))
  (explicit-reception-line (send (send server-stream :foreign-host) :name) protocol))

(defun explicit-reception-line (fromstring protocol)
  (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"
	    fromstring
	    (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))))

))

; From modified file LAD: RELEASE-3.NETWORK.MAILER; MAIN.LISP#5 at 3-Sep-86 18:10:15
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; MAIN  "

(defun deliver-to-sorted-hosts (delivery-name function addresses plist text report-stream)
  "Returned a list of failed addresses.
FUNCTION should take the arguments (HOST ADDRESSES PLIST TEXT REPORT-STREAM) and
return a list of failed addresses."
  (format report-stream "~&Attempting ~A delivery (~D addresses):"
	  delivery-name (length addresses))
  (do ((addrs addresses (cdr addrs)) address host failed (to-deliver nil nil))
      ((null addrs) failed)
    (setq address (car addrs) host (foreign-class address))
    (cond ((null (setq host (si:parse-host host t)))
	   (format report-stream "~&Unknown host ~A" (foreign-class address))
	   (push address failed))
	  (t
	   (setq to-deliver (ncons address))
	   (dolist (a (cdr addrs))
	     (when (eq (si:parse-host (foreign-class a) t) host)
	       (push a to-deliver)
	       (setq addrs (delq a addrs))))
	   (setq failed (nconc failed
			       (funcall function host to-deliver plist text report-stream)))))))

))

; From modified file LAD: RELEASE-3.NETWORK.MAILER; MAIN.LISP#5 at 3-Sep-86 18:10:48
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; MAIN  "

(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) (read-line S))
      (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)))))))
	  (typecase Z-UNAME
	    (null (setq uname "unknown"))
	    (string ; error
	     (setq uname "Unknown"))
	    (t
	     (let ((z-address (first z-uname)))
	       (SETQ UNAME (getf z-address :NAME))
	       (LET ((HOST (getf z-address :host)))
		 (when 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))))))))))))))

))

; From modified file LAD: RELEASE-3.NETWORK.MAILER; MAIN.LISP#5 at 3-Sep-86 18:11:14
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; MAIN  "

(defvar *simple-forwarding-alist* '()
  "An alist of hosts to which to forward, and what hosts are reachable through forwarding")

(defun set-simple-forwarding (host host-strings)
  (setq host (si:parse-host host))
  (assert (reachable-host-p host) (host) "Mail can't be forwarded to ~A" host)
  (let ((entry (lisp:assoc host *simple-forwarding-alist*)))
    (if entry
	(setf (cdr entry) host-strings)
      (push (cons host host-strings) *simple-forwarding-alist*)))
  host)

(defun load-simple-forwarding-data (host file)
  "Sets up the mailer so that the mail destined for hosts listed in FILE are sent to HOST.
FILE is simply names, one to a line."
  (set-simple-forwarding host (read-host-names file)))

(defun read-host-names (file)
  (with-open-file (in file)
    (do (line eofp names)
	(eofp (remove-duplicates names :test #'string-equal))
      (multiple-value-setq (line eofp) (read-line in nil))
      (when line
	(setq line (string-trim '(#\Space #\Tab) line))
	(unless (zerop (string-length line))
	  (push line names))))))


(defun find-forwarding-host (hostname)
  (let ((entry (lisp:rassoc hostname *simple-forwarding-alist*
			    :test #'(lambda (name list)
				      (lisp:member name list :test #'string-equal)))))
    (and entry (car entry))))

(defvar *direct-mail-connected-networks* '())

(defun add-direct-mail-connected-network (network)
  (pushnew network *direct-mail-connected-networks*))

(defun reachable-host-p (host)
  (dolist (network *direct-mail-connected-networks*)
    (when (send host :network-typep network)
      (return t))))

(defun host-name-on-network-p (name network-type)
  "Returns non-NIL if NAME names a host on NETWORK-TYPE."
  (let ((host (si:parse-host name t nil)))
    (when host
      (send host :network-typep network-type))))

(defun foreign-network-address-p (address network)
  (and (eq (address-type address) :foreign)
       (stringp (foreign-class address))
       (host-name-on-network-p (foreign-class address) network)))

))

; From modified file LAD: RELEASE-3.NETWORK.MAILER; MAIN.LISP#5 at 3-Sep-86 18:11:33
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; MAIN  "

(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))))))

))

; From modified file LAD: RELEASE-3.NETWORK.MAILER; MAIN.LISP#5 at 3-Sep-86 18:11:38
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; MAIN  "

(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)
	      (let ((fwd-host (find-forwarding-host last-host-string)))
		(if fwd-host
		    (values nil
			    (make-foreign-address :mailer-address original
						  :original-address original
						  :class (send fwd-host :name)))
		  (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))))))))

))

; From modified file LAD: RELEASE-3.NETWORK.MAILER; CHAOS.LISP#6 at 3-Sep-86 18:12:40
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; CHAOS  "


(defun chaos-mail-warn-function (type stream &rest format-args)
  (flet ((gwan (string)
	   (write-string string stream)
	   (apply #'format stream format-args)
	   (terpri stream)
	   (force-output stream)))
    (case type
      (:disk-full (gwan "%"))
      (:random-error (gwan "-"))
      (:append-error (gwan "-"))
      (otherwise (gwan (format nil "-[~A]" type))))))

;;; The Store and Forward server
(defun mail-server-chaos (&aux final-ok qfile)
  (let ((user-id "Mail-Server")
	(*warn-function* 'chaos-mail-warn-function))
    (ignoring-errors
      (as-mail-server (stream (chaos:open-stream nil "MAIL"))
	(send stream :add-as-server "MAIL")
	(ignoring-errors ; catch network lossage
	  (with-text-buffer (text stream)
	    (let ((recipients (expand-addresses (get-mail-recipients stream))))
	      (with-output-to-string (s text)
		(write-line (reception-line stream) s))
	      (get-mail-text stream text)
	      (finish-output stream)
	      (let ((result
		      (write-queue-file (setq qfile (qfile-name))
					recipients `(:failures 0
						     :source-network-type :chaos
						     :source-protocol :chaos-mail
						     :from ,(from-uname-for-delivery text)
						     :source-host ,(send stream :foreign-host))
					      TEXT STREAM)))
		(cond ((errorp result)
		       (setq final-ok ())
		       (if (probef qfile) (deletef qfile)))
		      (t
		       (setq final-ok :queued-ok))))))
	  (if final-ok (format stream "+Message sent successfully.~%"))
	  (force-output stream)))
	(when (and (eq final-ok :queued-ok)
		   (not *delay-delivery*))
	  (mail-deliver-qfile qfile)))))

))

; From modified file LAD: RELEASE-3.NETWORK.MAILER; CHAOS.LISP#6 at 3-Sep-86 18:13:01
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; CHAOS  "


(defun chaos-delivery-single-host (host plist addresses text report-stream &aux s failed)
  (declare (ignore plist))
  (format report-stream "~&  Attempting delivery to ~A (~D addresses):~%"
	  host (length addresses))
  (condition-case (error)
      (unwind-protect
	  (progn
	    (setq s (chaos:open-stream host "MAIL"))
	    (dolist (a addresses)
	      (write-line (foreign-mailer-address a) s)
	      (force-output s)
	      (if (char= #\+ (read-char s))
		  (format report-stream "~&OK: ~A~%" (read-line s))
	       (format report-stream "~&Failure for ~S: ~A" a (read-line s))
	       (push a failed)))
	    (terpri s)
	    (write-string text s)
	    ;; >> No Common Lisp way of doing this.
	    (send s :eof)
	    (let ((first (read-char s)))
	      (if (char= #\+ first)
		  (format report-stream "~&Message queued~%")
	       (format report-stream "~&~:[Permanent~;Temporary~] failure: ~A~%"
		       (char= first #\-) (read-line s))
	       (setq failed addresses))))
	(when s (lisp:close s :abort t)))
    (error
     (format report-stream "Error while delivering: ~A" error)
     addresses) ; all addresses considered failed
    (:no-error failed)))

(defun chaos-delivery-method (addresses plist text report-stream)
  (deliver-to-sorted-hosts "Chaosnet" #'chaos-delivery-single-host
			   addresses plist text report-stream))

(define-delivery-method chaos-mail (address)
			(foreign-network-address-p address :chaos)
			(addresses plist text report-stream)
  (chaos-delivery-method addresses plist text report-stream))

))

; From modified file LAD: RELEASE-3.NETWORK.MAILER; CHAOS.LISP#6 at 3-Sep-86 18:13:06
#10R MAILER#: 
(COMPILER-LET ((*PACKAGE* (PKG-FIND-PACKAGE "MAILER")))
  (COMPILER::PATCH-SOURCE-FILE "SYS: NETWORK; MAILER; CHAOS  "

(add-direct-mail-connected-network :chaos)

))
