git.fiddlerwoaroof.com
smtp.cl
a5c2fe04
 ;; mail sending package
 ;;
 ;; smtp -  rfc821
 ;;
8dd85436
 (defpackage :net.post-office
a5c2fe04
   (:use #:lisp #:excl)
   (:export 
    #:send-letter
    #:send-smtp))
 
8dd85436
 (in-package :net.post-office)
a5c2fe04
 
 ;; the exported functions:
 
 ;; (send-letter "mail-server" "from" "to" "message" &key subject reply-to)
 ;;  
 ;;    sends a message to the mail server (which may be a relay server
 ;;    or the final destination).  "from" is the address to be given
 ;;    as the sender.  "to" can be a string or a list of strings naming
 ;;    recipients.   
 ;;    "message" is the message to be sent
 ;;    This builds a header and inserts the optional subject and reply-to
 ;;    lines.
 ;;
 ;; (send-smtp "mail-server" "from" "to" &rest messages)
 ;;    this is like send-letter except that it doesn't build a header.
 ;;    the messages should contain a header (and if not then sendmail
 ;;    notices this and builds one -- other MTAs may not be that smart).
 ;;    The messages ia  list of string to be concatenated together
 ;;    and sent as one message
 ;;
 ;;
 
 
 
 
 
 
 
 
 
 
 
 (defmacro response-case ((ftp-stream) &rest case-clauses)
   (let ((response-class (gensym)))
     `(multiple-value-bind (,response-class ftp-response)
 	 (progn (force-output ,ftp-stream)
 		(wait-for-response ,ftp-stream))
        (declare (ignorable ftp-response))
        (case ,response-class
 	 ,@case-clauses))))
 
 (defvar *smtp-debug* nil)
 
 
 
 (defun send-letter (server from to message
 		    &key subject
 			 reply-to)
   (let ((header (make-string-output-stream)))
     (format header "From: ~a~c~cTo: "
 	    from
 	    #\return
 	    #\linefeed)
     (let ((tos (if* (stringp to) 
 			then (list to) 
 		      elseif (consp to)
 			then to
 			else (error "to should be a string or list, not ~s"
 				    to))))
       (format header "~{ ~a~^,~}~c~c" tos #\return #\linefeed))
     
     (if* subject
        then (format header "Subject: ~a~c~c" subject #\return #\linefeed))
     
     (if* reply-to
        then (format header "Reply-To: ~a~c~c" reply-to #\return #\linefeed))
     
     
     (format header "~c~c" #\return #\linefeed)
     
     (send-smtp server from to (get-output-stream-string header) message)
   
   
   
     ))
     
     
 	  
 		    
 
 (defun send-smtp (server from to &rest messages)
   ;; send the effective concatenation of the messages via
   ;; smtp to the mail server
   ;; Each message should be a string
   ;;
   ;; 'to' can be a single string or a list of strings.
   ;; each string should be in the official rfc822 format  "foo@bar.com"
   ;;
   (let ((sock (socket:make-socket :remote-host server
 				  :remote-port 25  ; smtp
 				  )))
     (unwind-protect
 	(progn
 	  (response-case (sock)
 			 (2 ;; to the initial connect
 			  nil)
 			 (t (error "initial connect failed")))
 	  
 	  ;; now that we're connected we can compute our hostname
 	  (let ((hostname (socket:ipaddr-to-hostname
 			   (socket:local-host sock))))
 	    (if* (null hostname)
 	       then (format nil "[~a]" (socket:ipaddr-to-dotted
 					(socket:local-host sock))))
 	    (smtp-command sock "HELO ~a" hostname)
 	    (response-case (sock)
 			   (2 ;; ok
 			    nil)
 			   (t (error "hello greeting failed"))))
 	    
 	  (smtp-command sock "MAIL from:<~a>" from)
 	  (response-case (sock)
 			 (2 ;; cool
 			  nil
 			  )
 			 (t (error "Mail from command failed")))
 
 	  (let ((tos (if* (stringp to) 
 			then (list to) 
 		      elseif (consp to)
 			then to
 			else (error "to should be a string or list, not ~s"
 				    to))))
 	    (dolist (to tos)
 	      (smtp-command sock "RCPT to:<~a>" to)
 	      (response-case (sock)
 			     (2 ;; cool
 			      nil
 			      )
 			     (t (error "rcpt to command failed")))))
 	
 	  (smtp-command sock "DATA")
 	  (response-case (sock)
 			 (3 ;; cool
 			  nil)
 			 (t (error "Data command failed")))
 	  ;(format t "sending message~%") (force-output t)	
 	  
 	  
 	  (let ((at-bol t))
 	    (dolist (message messages)
 	      (dotimes (i (length message))
 		(let ((ch (aref message i)))
 		  (if* (and at-bol (eq ch #\.))
 		     then ; to prevent . from being interpreted as eol
 			  (write-char #\. sock))
 		  (if* (eq ch #\newline)
 		     then (setq at-bol t)
 			  (write-char #\return sock)
 		     else (setq at-bol nil))
 		  (write-char ch sock)))))
 	
 	  (write-char #\return sock) (write-char #\linefeed sock)
 	  (write-char #\. sock)
 	  (write-char #\return sock) (write-char #\linefeed sock)
 	
 	  (response-case (sock)
 			 (2 nil ; (format t "Message sent to ~a~%" to)
 			    )
 			 
 			 (t (error "message not sent")))
 
 	  (force-output t)
 	  
 	  (smtp-command sock "QUIT")
 	  (response-case (sock)
 			 (2 ;; cool
 			  nil)
 			 (t (error "quit failed"))))
       (close sock))))
 
 
 
 
 
 
 
 	
       
 (defun wait-for-response (stream)
   ;; read the response of the ftp server.
   ;; collect it all in a string.
   ;; Return two values:
   ;; 	response class
   ;;    whole string
   ;; The string should begin with a decimal digit, and that is converted
   ;; into a number which is returned as the response class.
   ;; If the string doesn't begin with a decimal digit then the
   ;; response class is -1.
   ;;
   (flet ((match-chars (string pos1 pos2 count)
 	   ;; like strncmp
 	   (dotimes (i count t)
 	     (if* (not (eq (aref string (+ pos1 i))
 			   (aref string (+ pos2 i))))
 		then (return nil)))))
 
     (let ((res (make-array 20 :element-type 'character
 			   :adjustable t
 			   :fill-pointer 0)))
       (if* (null (read-a-line stream res))
 	 then ; eof encountered before end of line
 	      (return-from wait-for-response (values -1 res)))
 
       ;; a multi-line response begins with line containing
       ;; a hyphen in the 4th column:
       ;; xyz-  some text
       ;;
       ;;  and ends with a line containing the same reply code but no
       ;;  hyphen.
       ;; xyz  some text
       ;;
 
       (if* (and (>= (length res) 4) (eq #\- (aref res 3)))
 	 then ;; multi line response
 	      (let ((old-length (length res))
 		    (new-length nil))
 		(loop
 		  (if* (null (read-a-line stream res))
 		     then ; eof encountered before end of line
 			  (return-from wait-for-response (values -1 res)))
 		  (setq new-length (length res))
 		  ;; see if this is the last line
 		  (if* (and (>= (- new-length old-length) 4)
 			    (eq (aref res (+ old-length 3)) #\space)
 			    (match-chars res 0 old-length 3))
 		     then (return))
 
 		  (setq old-length new-length))))
 
       ;; complete response is in res
       ;; compute class and return the whole thing
       (let ((class (or (and (> (length res) 0)
 			    (digit-char-p (aref res 0)))
 		       -1)))
 	(values class res)))))      
 
 (defun smtp-command (stream &rest format-args)
   ;; send a command to the smtp server
   (let ((command (apply #'format nil format-args)))
     (if* *smtp-debug*
        then (format *smtp-debug* "to smtp command: ~s~%" command)
 	    (force-output *smtp-debug*))
     (write-string command stream)
     (write-char #\return stream)
     (write-char #\newline stream)
     (force-output stream)))
 
 (defun read-a-line (stream res)
   ;; read from stream and put the result in the adjust able array res
   ;; if line ends in cr-lf, only put a newline in res.
   ;; If we get an eof before the line finishes, return nil,
   ;; else return t if all is ok
   (let (ch last-ch)
     (loop
       (setq ch (read-char stream nil nil))
       (if* (null ch)
 	 then ; premature eof
 	      (return nil))
 
       (if* *smtp-debug*
 	 then (format *smtp-debug* "~c" ch)
 	      (force-output *smtp-debug*)
 	      )
 
       (if* (eq last-ch #\return)
 	 then (if* (eq ch #\linefeed)
 		 then (vector-push-extend #\newline res)
 		      (return t)
 		 else (vector-push-extend last-ch res))
        elseif (eq ch #\linefeed)
 	 then ; line ends with just lf, not cr-lf
 	      (vector-push-extend #\newline res)
 	      (return t)
        elseif (not (eq ch #\return))
 	 then (vector-push-extend ch res))
 
       (setq last-ch ch))))