Update of /project/cl-smtp/cvsroot/cl-smtp
In directory clnet:/tmp/cvs-serv3901
Modified Files:
CHANGELOG README cl-smtp.asd cl-smtp.lisp
Added Files:
smtp-output-stream.lisp
Log Message:
A lot of changes:
- add support for sending raw messages
- add character quoting in email headers (according to RFC2047)
- add condition classes for error reporting
- fixed STARTTLS
- change authentication functionality
See CHANGELOG and source.
Thanks Hans Huebner for these changes.
--- /project/cl-smtp/cvsroot/cl-smtp/CHANGELOG 2007/11/11 23:10:21 1.10
+++ /project/cl-smtp/cvsroot/cl-smtp/CHANGELOG 2008/04/02 18:02:28 1.11
@@ -1,3 +1,13 @@
+Version 20080202.1
+2007.02.02
+Added support for sending raw messages. (Hans Huebner)
+Fixed STARTTLS so that an EHLO command is sent after STARTTLS. (Hans Huebner)
+Changed Authentication functionality, the actual authentication method used is determined by looking at the advertised features of the SMTP server. (Hans Huebner)
+Added non-ASCII character quoting in email headers (according to RFC2047). (Hans Huebner)
+Added condition classes for error reporting. (Hans Huebner)
+Change cl-smtp.lisp, cl-smtp.asd, CHANGELOG
+Add smtp-output-stream.lisp
+
Version 20071113.1
2007.11.13
Add SSL support, thank Timothy Ritchey for the suggestions.
--- /project/cl-smtp/cvsroot/cl-smtp/README 2007/11/11 23:10:21 1.8
+++ /project/cl-smtp/cvsroot/cl-smtp/README 2008/04/02 18:02:28 1.9
@@ -25,26 +25,29 @@
Arguments:
- host (String) : hostname or ip-adress of the smtpserver
- from (String) : email adress
- - to (String or Cons of Strings) : email adress
+ - to (String or List of Strings) : email adress
- subject (String) : subject text
- message (String) : message body
keywords:
- - cc (String or Cons of Strings) : email adress carbon copy
- - bcc (String or Cons of Strings): email adress blind carbon copy
+ - cc (String or List of Strings) : email adress carbon copy
+ - bcc (String or List of Strings): email adress blind carbon copy
- reply-to (String) : email adress
- displayname (String) : displayname of the sender
- - extra-headers (Cons) : extra headers as alist
+ - extra-headers (List) : extra headers as alist
- html-message (String) : message body formatted with HTML tags
- - authentication (Cons) : list with 3 elements
- (:method "username" "password")
+ - authentication (List) : list with 2 or elements
+ ([:method] "username" "password")
method is a keyword :plain or :login
+ If the method is not specified, the
+ proper method is determined automatically.
- attachments (String or Pathname: attachments to send
- Cons of String/Pathnames)
+ List of String/Pathnames)
- buffer-size (Number default 256): controls how much of a attachment file
is read on each loop before encoding
and transmitting the contents,
- the number is interpretted in KB
- - ssl (Boolean) : if true than use the STARTTLS functionality to make a ssl connection
+ the number is interpreted in KB
+ - ssl (or t :starttls :tls) : if t or :STARTTLS: use the STARTTLS functionality
+ if :TLS: use TLS directly
Returns nil or error with message
--- /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.asd 2007/11/11 23:10:21 1.12
+++ /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.asd 2008/04/02 18:02:29 1.13
@@ -16,25 +16,18 @@
;;; File: cl-smtp.asd
;;; Description: cl-smtp ASDF system definition file
-(defpackage :cl-smtp
- (:use :cl :asdf)
- (:export :send-email))
-
-(in-package :cl-smtp)
-
-(defparameter *debug* nil)
-
-(defmacro print-debug (str)
- `(when *debug*
- (print ,str)))
-
(asdf:defsystem :cl-smtp
- :version "20071113.1"
- :perform (load-op :after (op webpage)
- (pushnew :cl-smtp cl:*features*))
- :depends-on (:usocket #-allegro :cl-base64
- #-allegro :flexi-streams
- #-allegro :cl+ssl)
- :components ((:file "cl-smtp" :depends-on ("attachments"))
- (:file "attachments")
- (:file "mime-types")))
+ :version "20080202.1"
+ :perform (load-op :after (op webpage)
+ (pushnew :cl-smtp cl:*features*))
+ :depends-on (:usocket
+ :trivial-gray-streams
+ :flexi-streams
+ #-allegro :cl-base64
+ #-allegro :cl+ssl)
+ :serial t
+ :components ((:file "package")
+ (:file "attachments")
+ (:file "cl-smtp")
+ (:file "smtp-output-stream")
+ (:file "mime-types")))
--- /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.lisp 2007/11/11 23:10:21 1.11
+++ /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.lisp 2008/04/02 18:02:29 1.12
@@ -34,21 +34,23 @@
(t
(error "the \"~A\" argument is not a string or cons" name))))
+(eval-when (:compile-toplevel :load-toplevel :execute)
+ (defvar *line-with-one-dot* #.(format nil "~C~C.~C~C" #\Return #\NewLine
+ #\Return #\NewLine))
+ (defvar *line-with-two-dots* #.(format nil "~C~C..~C~C" #\Return #\NewLine
+ #\Return #\NewLine)))
+
(defun mask-dot (str)
- "replace \r\n.\r\n with \r\n..\r\n"
- (let ((dotstr (format nil "~C~C.~C~C" #\Return #\NewLine
- #\Return #\NewLine))
- (maskdotsr (format nil "~C~C..~C~C" #\Return #\NewLine
- #\Return #\NewLine))
- (resultstr ""))
+ "Replace all occurences of \r\n.\r\n in STR with \r\n..\r\n"
+ (let ((resultstr ""))
(labels ((mask (tempstr)
- (let ((n (search dotstr tempstr)))
+ (let ((n (search *line-with-one-dot* tempstr)))
(cond
(n
(setf resultstr (concatenate 'string resultstr
(subseq tempstr 0 n)
- maskdotsr))
- (mask (subseq tempstr (+ n 5))))
+ *line-with-two-dots*))
+ (mask (subseq tempstr (+ n #.(length *line-with-one-dot*)))))
(t
(setf resultstr (concatenate 'string resultstr
tempstr)))))))
@@ -60,11 +62,76 @@
#+allegro (excl:string-to-base64-string str)
#-allegro (cl-base64:string-to-base64-string str))
+(define-condition smtp-error (error)
+ ())
+
+(define-condition smtp-protocol-error (smtp-error)
+ ((command :initarg :command :reader command)
+ (expected-response-code :initarg :expected-response-code :reader expected-response-code)
+ (response-code :initarg :response-code :reader response-code)
+ (response-message :initarg :response-message :reader response-message))
+ (:report (lambda (condition stream)
+ (print-unreadable-object (condition stream :type t)
+ (format stream "a command failed:~%command: ~S expected: ~A response: ~A"
+ (command condition)
+ (expected-response-code condition)
+ (response-message condition))))))
+
+(define-condition rcpt-failed (smtp-protocol-error)
+ ((recipient :initarg :recipient
+ :reader recipient))
+ (:report (lambda (condition stream)
+ (print-unreadable-object (condition stream :type t)
+ (format stream "while trying to send email through SMTP, the server rejected the recipient ~A: ~A"
+ (recipient condition)
+ (response-message condition))))))
+
+(defun smtp-command (stream command expected-response-code
+ &key (condition-class 'smtp-protocol-error)
+ condition-arguments)
+ (when command
+ (write-to-smtp stream command))
+ (multiple-value-bind (code msgstr lines)
+ (read-from-smtp stream)
+ (when (/= code expected-response-code)
+ (apply #'error
+ condition-class
+ (append condition-arguments
+ (list :command command
+ :expected-response-code expected-response-code
+ :response-code code
+ :response-message msgstr))))
+ lines))
+
+(defun do-with-smtp-mail (host from to thunk &key port authentication ssl local-hostname)
+ (usocket:with-client-socket (socket stream host port)
+ (let ((stream (smtp-handshake stream
+ :authentication authentication
+ :ssl ssl
+ :local-hostname local-hostname)))
+ (initiate-smtp-mail stream from to)
+ (funcall thunk (make-instance 'smtp-output-stream :encapsulated-stream stream))
+ (finish-smtp-mail stream))))
+
+(defmacro with-smtp-mail ((stream-var host from to &key ssl (port (if (eq :tls ssl) 465 25)) authentication local-hostname)
+ &body body)
+ "Encapsulate a SMTP MAIl conversation. A connection to the SMTP
+ server on HOST and PORT is established and a MAIL command is
+ initiated with FROM being the mail sender and TO being the list of
+ recipients. BODY is evaluated with STREAM-VAR being the stream
+ connected to the remote SMTP server. BODY is expected to write the
+ RFC2821 message (headers and body) to STREAM-VAR."
+ `(do-with-smtp-mail ,host ,from ,to
+ (lambda (,stream-var) ,@body)
+ :port ,port
+ :authentication ,authentication
+ :ssl ,ssl
+ :local-hostname ,local-hostname))
(defun send-email (host from to subject message
- &key (port 25) cc bcc reply-to extra-headers
+ &key ssl (port (if (eq :tls ssl) 465 25)) cc bcc reply-to extra-headers
html-message display-name authentication
- attachments (buffer-size 256) ssl)
+ attachments (buffer-size 256))
(send-smtp host from (check-arg to "to") subject (mask-dot message)
:port port :cc (check-arg cc "cc") :bcc (check-arg bcc "bcc")
:reply-to reply-to
@@ -78,186 +145,216 @@
256)
:ssl ssl))
-
-(defun send-smtp (host from to subject message
- &key (port 25) cc bcc reply-to extra-headers html-message
- display-name authentication attachments buffer-size ssl)
- (let* ((sock (usocket:socket-stream (usocket:socket-connect host port)))
- (boundary (make-random-boundary))
- (html-boundary (if (and attachments html-message)
- (make-random-boundary)
- boundary)))
- (unwind-protect
- (let ((stream (open-smtp-connection sock
- :authentication authentication
- :ssl ssl)))
- (send-smtp-headers stream :from from :to to :cc cc :bcc bcc
- :reply-to reply-to
- :display-name display-name
- :extra-headers extra-headers :subject subject)
- (when (or attachments html-message)
- (send-multipart-headers
- stream :attachment-boundary (when attachments boundary)
- :html-boundary html-boundary))
- ;;----------- Send the body Message ---------------------------
- ;;--- Send the proper headers depending on plain-text,
- ;;--- multi-part or html email
- (cond ((and attachments html-message)
- ;; if both present, start attachment section,
- ;; then define alternative section,
- ;; then write alternative header
- (progn
- (generate-message-header
- stream :boundary boundary :include-blank-line? nil)
- (generate-multipart-header stream html-boundary
- :multipart-type "alternative")
- (write-blank-line stream)
- (generate-message-header
- stream :boundary html-boundary :content-type *content-type*
- :content-disposition "inline" :include-blank-line? nil)))
- (attachments
- (generate-message-header
- stream :boundary boundary
- :content-type *content-type* :content-disposition "inline"
- :include-blank-line? nil))
- (html-message
- (generate-message-header
- stream :boundary html-boundary :content-type *content-type*
- :content-disposition "inline"))
- (t
- (generate-message-header stream :content-type *content-type*
- :include-blank-line? nil)))
- (write-blank-line stream)
- (write-to-smtp stream message)
- (write-blank-line stream)
- ;;---------- Send Html text if needed -------------------------
- (when html-message
- (generate-message-header
- stream :boundary html-boundary
- :content-type "text/html; charset=ISO-8859-1"
- :content-disposition "inline")
- (write-to-smtp stream html-message)
- (send-end-marker stream html-boundary))
- ;;---------- Send Attachments -----------------------------------
- (when attachments
- (dolist (attachment attachments)
- (send-attachment stream attachment boundary buffer-size))
- (send-end-marker stream boundary))
- (write-char #\. stream)
- (write-blank-line stream)
- (force-output stream)
- (multiple-value-bind (code msgstr)
- (read-from-smtp stream)
- (when (/= code 250)
- (error "Message send failed: ~A" msgstr)))
- (write-to-smtp stream "QUIT")
- (multiple-value-bind (code msgstr)
- (read-from-smtp stream)
- (when (/= code 221)
- (error "in QUIT command:: ~A" msgstr))))
- (close sock))))
-
-(defun open-smtp-connection (stream &key authentication ssl)
- (multiple-value-bind (code msgstr)
- (read-from-smtp stream)
- (when (/= code 220)
- (error "wrong response from smtp server: ~A" msgstr)))
- (when ssl
- (write-to-smtp stream (format nil "EHLO ~A"
- (usocket::get-host-name)))
- (multiple-value-bind (code msgstr lines)
- (read-from-smtp stream)
- (when (/= code 250)
- (error "wrong response from smtp server: ~A" msgstr))
- (when ssl
- (cond
- ((find "STARTTLS" lines :test #'equal)
- (print-debug "this server supports TLS")
- (write-to-smtp stream "STARTTLS")
- (multiple-value-bind (code msgstr)
- (read-from-smtp stream)
- (when (/= code 220)
- (error "Unable to start TLS: ~A" msgstr))
- (setf stream
- #+allegro (socket:make-ssl-client-stream stream)
- #-allegro
- (let ((s stream))
- (cl+ssl:make-ssl-client-stream
- (cl+ssl:stream-fd stream)
- :close-callback (lambda () (close s)))))
- #-allegro
- (setf stream (flexi-streams:make-flexi-stream
- stream
- :external-format
- (flexi-streams:make-external-format
- :latin-1 :eol-style :lf)))))
- (t
- (error "this server does not supports TLS"))))))
- (cond
- (authentication
- (write-to-smtp stream (format nil "EHLO ~A"
- (usocket::get-host-name)))
- (multiple-value-bind (code msgstr)
- (read-from-smtp stream)
- (when (/= code 250)
- (error "wrong response from smtp server: ~A" msgstr)))
- (cond
- ((eq (car authentication) :plain)
- (write-to-smtp stream (format nil "AUTH PLAIN ~A"
- (string-to-base64-string
- (format nil "~A~C~A~C~A"
- (cadr authentication)
- #\null (cadr authentication)
- #\null
- (caddr authentication)))))
- (multiple-value-bind (code msgstr)
- (read-from-smtp stream)
- (when (/= code 235)
- (error "plain authentication failed: ~A" msgstr))))
- ((eq (car authentication) :login)
- (write-to-smtp stream "AUTH LOGIN")
- (multiple-value-bind (code msgstr)
- (read-from-smtp stream)
- (when (/= code 334)
- (error "login authentication failed: ~A" msgstr)))
- (write-to-smtp stream (string-to-base64-string (cadr authentication)))
- (multiple-value-bind (code msgstr)
- (read-from-smtp stream)
- (when (/= code 334)
- (error "login authentication send username failed: ~A" msgstr)))
- (write-to-smtp stream (string-to-base64-string (caddr authentication)))
- (multiple-value-bind (code msgstr)
- (read-from-smtp stream)
- (when (/= code 235)
- (error "login authentication send password failed: ~A" msgstr))))
- (t
- (error "authentication ~A is not supported in cl-smtp"
- (car authentication)))))
- (t
- (write-to-smtp stream (format nil "HELO ~A" (usocket::get-host-name)))
- (multiple-value-bind (code msgstr)
- (read-from-smtp stream)
- (when (/= code 250)
- (error "wrong response from smtp server: ~A" msgstr)))))
+(defun send-smtp (host from to subject message
+ &key ssl (port (if (eq :tls ssl) 465 25)) cc bcc
+ reply-to extra-headers html-message display-name
+ authentication attachments buffer-size
+ (local-hostname (usocket::get-host-name)))
+ (with-smtp-mail (stream host from (append to cc bcc)
+ :port port
+ :authentication authentication
+ :ssl ssl
+ :local-hostname local-hostname)
+ (let* ((boundary (make-random-boundary))
+ (html-boundary (if (and attachments html-message)
+ (make-random-boundary)
+ boundary)))
+ (send-mail-headers stream
+ :from from
+ :to to
+ :cc cc
+ :reply-to reply-to
+ :display-name display-name
+ :extra-headers extra-headers :subject subject)
+ (when (or attachments html-message)
+ (send-multipart-headers stream
+ :attachment-boundary (when attachments boundary)
+ :html-boundary html-boundary))
+ ;;----------- Send the body Message ---------------------------
+ ;;--- Send the proper headers depending on plain-text,
+ ;;--- multi-part or html email
+ (cond ((and attachments html-message)
+ ;; if both present, start attachment section,
+ ;; then define alternative section,
+ ;; then write alternative header
+ (progn
+ (generate-message-header
+ stream :boundary boundary :include-blank-line? nil)
+ (generate-multipart-header stream html-boundary
+ :multipart-type "alternative")
+ (write-blank-line stream)
+ (generate-message-header
+ stream :boundary html-boundary :content-type *content-type*
+ :content-disposition "inline" :include-blank-line? nil)))
+ (attachments
+ (generate-message-header
+ stream :boundary boundary
+ :content-type *content-type* :content-disposition "inline"
+ :include-blank-line? nil))
+ (html-message
+ (generate-message-header
+ stream :boundary html-boundary :content-type *content-type*
+ :content-disposition "inline"))
+ (t
+ (generate-message-header stream :content-type *content-type*
+ :include-blank-line? nil)))
+ (write-blank-line stream)
+ (write-to-smtp stream message)
+ (write-blank-line stream)
+ ;;---------- Send Html text if needed -------------------------
+ (when html-message
+ (generate-message-header
+ stream :boundary html-boundary
+ :content-type "text/html; charset=ISO-8859-1"
+ :content-disposition "inline")
+ (write-to-smtp stream html-message)
+ (send-end-marker stream html-boundary))
+ ;;---------- Send Attachments -----------------------------------
+ (when attachments
+ (dolist (attachment attachments)
+ (send-attachment stream attachment boundary buffer-size))
+ (send-end-marker stream boundary)))))
+
+(define-condition no-supported-authentication-method (smtp-error)
+ ((features :initarg :features :reader features))
+ (:report (lambda (condition stream)
+ (print-unreadable-object (condition stream :type t)
+ (format stream "SMTP authentication has been requested, but the SMTP server did not advertise any ~
+ supported authentication scheme. Features announced: ~{~S~^, ~}"
+ (features condition))))))
+
+(defun smtp-authenticate (stream authentication features)
+ "Authenticate to the SMTP server connected on STREAM.
+ AUTHENTICATION is a list of two or three elements. If the first
+ element is a keyword, it specifies the desired authentication
+ method (:PLAIN or :LOGIN), which is currently ignored. The actual
+ method used is determined by looking at the advertised features of
+ the SMTP server. The (other) two elements of the AUTHENTICATION
+ list are the login username and password. FEATURES is the list of
+ features announced by the SMTP server.
+
+ If the server does not announce any compatible authentication scheme,
+ the NO-SUPPORTED-AUTHENTICATION-METHOD error is signalled."
+ (when (keywordp (car authentication))
+ (pop authentication))
+ (let ((server-authentication (loop for i in features
+ for e = (search "AUTH " i :test #'equal)
+ when (and e (= e 0))
+ return i)))
+ (destructuring-bind (username password) authentication
+ (cond
+ ((search " PLAIN" server-authentication :test #'equal)
+ (smtp-command stream (format nil "AUTH PLAIN ~A"
+ (string-to-base64-string
+ (format nil "~A~C~A~C~A"
+ username
+ #\null username
+ #\null password)))
+ 235))
+ ((search " LOGIN" server-authentication :test #'equal)
+ (smtp-command stream "AUTH LOGIN"
+ 334)
+ (smtp-command stream (string-to-base64-string username)
+ 334)
+ (smtp-command stream (string-to-base64-string password)
+ 235))
+ (t
+ (error 'no-supported-authentication-method :features features))))))
+
+(defun smtp-handshake (stream &key authentication ssl local-hostname)
+ "Perform the initial SMTP handshake on STREAM. Returns the stream
+ to use further down in the conversation, which may be different from
+ the original stream if we switched to SSL."
+
[150 lines skipped]
--- /project/cl-smtp/cvsroot/cl-smtp/smtp-output-stream.lisp 2008/04/02 18:02:29 NONE
+++ /project/cl-smtp/cvsroot/cl-smtp/smtp-output-stream.lisp 2008/04/02 18:02:29 1.1
[237 lines skipped]