Update of /project/cl-smtp/cvsroot/cl-smtp In directory clnet:/tmp/cvs-serv19188
Modified Files: CHANGELOG README cl-smtp.asd cl-smtp.lisp Log Message: Add SSL support, thank Timothy Ritchey for the suggestions. New boolean keyword argument ssl added to send-email.
--- /project/cl-smtp/cvsroot/cl-smtp/CHANGELOG 2007/11/03 23:53:29 1.9 +++ /project/cl-smtp/cvsroot/cl-smtp/CHANGELOG 2007/11/11 23:10:21 1.10 @@ -1,3 +1,9 @@ +Version 20071113.1 +2007.11.13 +Add SSL support, thank Timothy Ritchey for the suggestions. +New boolean keyword argument ssl added to send-email. +Change cl-smtp.lisp, cl-smtp.asd, README, CHANGELOG + Version 20071104.1 2007.11.04 Fixed bug with the file attachments to solve corrupted files when @@ -5,7 +11,7 @@ Added automatically including mime types for attachesments of common known extensions. (Brian Sorg) Added Html-messages option to send-mail function. (Brian Sorg) -Change attachments.lisp, cl-smtp.asd, cl-smtp.lisp, README, CHANGLOG +Change attachments.lisp, cl-smtp.asd, cl-smtp.lisp, README, CHANGELOG Add mime-type.lisp
Version 20071018.1 --- /project/cl-smtp/cvsroot/cl-smtp/README 2007/11/03 23:53:29 1.7 +++ /project/cl-smtp/cvsroot/cl-smtp/README 2007/11/11 23:10:21 1.8 @@ -6,6 +6,8 @@
with authentication support for PLAIN and LOGIN authentication method
+and ssl support with cl+ssl package + used cl-base64 and usocket packages (cl-base64 isn't a requirement on ACL)
See INSTALL for prerequisites and build details. @@ -18,7 +20,7 @@
(cl-smtp:send-email host from to subject message &key (port 25) cc bcc reply-to extra-headers html-message - authentication attachments (buffer-size 256)) + authentication attachments (buffer-size 256) ssl)
Arguments: - host (String) : hostname or ip-adress of the smtpserver @@ -41,7 +43,8 @@ - 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 + the number is interpretted in KB + - ssl (Boolean) : if true than use the STARTTLS functionality to make a ssl connection
Returns nil or error with message
--- /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.asd 2007/11/05 19:58:24 1.11 +++ /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.asd 2007/11/11 23:10:21 1.12 @@ -29,10 +29,12 @@ (print ,str)))
(asdf:defsystem :cl-smtp - :version "20071105.1" + :version "20071113.1" :perform (load-op :after (op webpage) (pushnew :cl-smtp cl:*features*)) - :depends-on (:usocket #-allegro :cl-base64) + :depends-on (:usocket #-allegro :cl-base64 + #-allegro :flexi-streams + #-allegro :cl+ssl) :components ((:file "cl-smtp" :depends-on ("attachments")) (:file "attachments") (:file "mime-types"))) --- /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.lisp 2007/11/05 19:58:24 1.10 +++ /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.lisp 2007/11/11 23:10:21 1.11 @@ -63,8 +63,8 @@
(defun send-email (host from to subject message &key (port 25) cc bcc reply-to extra-headers - html-message display-name authentication - attachments (buffer-size 256)) + html-message display-name authentication + attachments (buffer-size 256) ssl) (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 @@ -75,206 +75,244 @@ :attachments (check-arg attachments "attachments") :buffer-size (if (numberp buffer-size) buffer-size - 256))) + 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) + 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 - (progn - (open-smtp-connection sock :authentication authentication) - (send-smtp-headers sock :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 - sock :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 - sock :boundary boundary :include-blank-line? nil) - (generate-multipart-header sock html-boundary - :multipart-type "alternative") - (write-blank-line sock) - (generate-message-header - sock :boundary html-boundary :content-type *content-type* - :content-disposition "inline" :include-blank-line? nil))) - (attachments - (generate-message-header - sock :boundary boundary - :content-type *content-type* :content-disposition "inline" - :include-blank-line? nil)) - (html-message - (generate-message-header - sock :boundary html-boundary :content-type *content-type* - :content-disposition "inline")) - (t - (generate-message-header sock :content-type *content-type* - :include-blank-line? nil))) - (write-blank-line sock) - (write-to-smtp sock message) - (write-blank-line sock) - ;;---------- Send Html text if needed ------------------------- - (when html-message - (generate-message-header - sock :boundary html-boundary - :content-type "text/html; charset=ISO-8859-1" - :content-disposition "inline") - (write-to-smtp sock html-message) - (send-end-marker sock html-boundary)) - ;;---------- Send Attachments ----------------------------------- - (when attachments - (dolist (attachment attachments) - (send-attachment sock attachment boundary buffer-size)) - (send-end-marker sock boundary)) - (write-char #. sock) - (write-blank-line sock) - (force-output sock) - (multiple-value-bind (code msgstr) - (read-from-smtp sock) - (when (/= code 250) - (error "Message send failed: ~A" msgstr))) - (write-to-smtp sock "QUIT") - (multiple-value-bind (code msgstr) - (read-from-smtp sock) - (when (/= code 221) - (error "in QUIT command:: ~A" msgstr)))) + (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 (sock &key authentication) +(defun open-smtp-connection (stream &key authentication ssl) (multiple-value-bind (code msgstr) - (read-from-smtp sock) + (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 sock (format nil "EHLO ~A" (usocket::get-host-name))) - (multiple-value-bind (code msgstr) - (read-from-smtp sock) + (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 sock (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 sock) - (when (/= code 235) - (error "plain authentication failed: ~A" msgstr)))) - ((eq (car authentication) :login) - (write-to-smtp sock "AUTH LOGIN") - (multiple-value-bind (code msgstr) - (read-from-smtp sock) - (when (/= code 334) - (error "login authentication failed: ~A" msgstr))) - (write-to-smtp sock (string-to-base64-string (cadr authentication))) - (multiple-value-bind (code msgstr) - (read-from-smtp sock) - (when (/= code 334) - (error "login authentication send username failed: ~A" msgstr))) - (write-to-smtp sock (string-to-base64-string (caddr authentication))) - (multiple-value-bind (code msgstr) - (read-from-smtp sock) - (when (/= code 235) - (error "login authentication send password failed: ~A" msgstr)))) - (t - (error "authentication ~A is not supported in cl-smtp" - (car authentication))))) + (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 sock (format nil "HELO ~A" (usocket::get-host-name))) + (write-to-smtp stream (format nil "HELO ~A" (usocket::get-host-name))) (multiple-value-bind (code msgstr) - (read-from-smtp sock) + (read-from-smtp stream) (when (/= code 250) - (error "wrong response from smtp server: ~A" msgstr)))))) + (error "wrong response from smtp server: ~A" msgstr))))) + stream)
-(defun send-smtp-headers (sock +(defun send-smtp-headers (stream &key from to cc bcc reply-to extra-headers display-name subject) - (write-to-smtp sock + (write-to-smtp stream (format nil "MAIL FROM:~@[~A ~]<~A>" display-name from)) (multiple-value-bind (code msgstr) - (read-from-smtp sock) + (read-from-smtp stream) (when (/= code 250) (error "in MAIL FROM command: ~A" msgstr))) - (compute-rcpt-command sock to) - (compute-rcpt-command sock cc) - (compute-rcpt-command sock bcc) - (write-to-smtp sock "DATA") + (compute-rcpt-command stream to) + (compute-rcpt-command stream cc) + (compute-rcpt-command stream bcc) + (write-to-smtp stream "DATA") (multiple-value-bind (code msgstr) - (read-from-smtp sock) + (read-from-smtp stream) (when (/= code 354) (error "in DATA command: ~A" msgstr))) - (write-to-smtp sock (format nil "Date: ~A" (get-email-date-string))) - (write-to-smtp sock (format nil "From: ~@[~A <~]~A~@[>~]" - display-name from display-name)) - (write-to-smtp sock (format nil "To: ~{ ~a~^,~}" to)) + (write-to-smtp stream (format nil "Date: ~A" (get-email-date-string))) + (write-to-smtp stream (format nil "From: ~@[~A <~]~A~@[>~]" + display-name from display-name)) + (write-to-smtp stream (format nil "To: ~{ ~a~^,~}" to)) (when cc - (write-to-smtp sock (format nil "Cc: ~{ ~a~^,~}" cc))) - (write-to-smtp sock (format nil "Subject: ~A" subject)) - (write-to-smtp sock (format nil "X-Mailer: cl-smtp ~A" - *x-mailer*)) + (write-to-smtp stream (format nil "Cc: ~{ ~a~^,~}" cc))) + (write-to-smtp stream (format nil "Subject: ~A" subject)) + (write-to-smtp stream (format nil "X-Mailer: cl-smtp ~A" + *x-mailer*)) (when reply-to - (write-to-smtp sock (format nil "Reply-To: ~A" reply-to))) + (write-to-smtp stream (format nil "Reply-To: ~A" reply-to))) (when (and extra-headers (listp extra-headers)) (dolist (l extra-headers) - (write-to-smtp sock + (write-to-smtp stream (format nil "~A: ~{~a~^,~}" (car l) (rest l))))) - (write-to-smtp sock "Mime-Version: 1.0")) + (write-to-smtp stream "Mime-Version: 1.0"))
-(defun send-multipart-headers (sock &key attachment-boundary html-boundary) +(defun send-multipart-headers (stream &key attachment-boundary html-boundary) (cond (attachment-boundary - (generate-multipart-header sock attachment-boundary + (generate-multipart-header stream attachment-boundary :multipart-type "mixed")) (html-boundary (generate-multipart-header - sock html-boundary + stream html-boundary :multipart-type "alternative")) (t nil)))
-(defun compute-rcpt-command (sock adresses) +(defun compute-rcpt-command (stream adresses) (dolist (to adresses) - (write-to-smtp sock (format nil "RCPT TO:<~A>" to)) + (write-to-smtp stream (format nil "RCPT TO:<~A>" to)) (multiple-value-bind (code msgstr) - (read-from-smtp sock) + (read-from-smtp stream) (when (/= code 250) (error "in RCPT TO command: ~A" msgstr)))))
-(defun write-to-smtp (sock command) +(defun write-to-smtp (stream command) (print-debug (format nil "to server: ~A" command)) - (write-string command sock) - (write-char #\Return sock) - (write-char #\NewLine sock) - (force-output sock)) - -(defun write-blank-line (sock) - (write-char #\Return sock) - (write-char #\NewLine sock) - (force-output sock)) - -(defun read-from-smtp (sock) - (let* ((line (read-line sock)) + (write-string command stream) + (write-char #\Return stream) + (write-char #\NewLine stream) + (force-output stream)) + +(defun write-blank-line (stream) + (write-char #\Return stream) + (write-char #\NewLine stream) + (force-output stream)) + +(defun read-from-smtp (stream &optional lines) + (let* ((line (read-line stream)) + (response (string-trim '(#\Return #\NewLine) (subseq line 4))) (response-code (parse-integer line :start 0 :junk-allowed t))) (print-debug (format nil "from server: ~A" line)) (if (= (char-code (elt line 3)) (char-code #-)) - (read-from-smtp sock) - (values response-code line))))
[5 lines skipped]