Update of /project/cl-smtp/cvsroot/cl-smtp In directory cl-net:/tmp/cvs-serv31991
Modified Files: CHANGELOG README attachments.lisp cl-smtp.asd cl-smtp.lisp mime-types.lisp package.lisp Removed Files: smtp-output-stream.lisp Log Message: Rewrite encoding functions, now it is possible to use non ascii characters in header values and in attachment filenames. New keyword argument external-format (default :utf-8). New class attachment, with slots attachment-name and attachment-mime-type, and a constructor function make-attachment. Rename q-encode-str to rfc2045-q-encode-string. Add rfc2231-encode-string to encode non ascii characters in attachment filenames.
--- /project/cl-smtp/cvsroot/cl-smtp/CHANGELOG 2010/04/22 10:51:34 1.16 +++ /project/cl-smtp/cvsroot/cl-smtp/CHANGELOG 2010/05/06 09:24:43 1.17 @@ -1,7 +1,23 @@ +Version 20100505.1 +2010.05.05 +Rewrite encoding functions, now it is possible to use non ascii characters in +header values and in attachment filenames. +New keyword argument external-format (default :utf-8). +New class attachment, with slots attachment-name and +attachment-mime-type, and a constructor function make-attachment. +Rename q-encode-str to rfc2045-q-encode-string. +Add rfc2231-encode-string to encode non ascii characters +in attachment filenames. +Add tests for encoding functions, see tests.lisp. +Change cl-smtp.lisp, attachment.lisp, package.lisp, + cl-smtp.asd, CHANGELOG, README +Add tests.lisp +Remove smtp-output-stream.lisp + Version 20100422.1 2010.04.22 Fixed encoding errors in header strings, -new function q-encode-str to encode strings in header +new function q-encode-str to encode strings in header. Change cl-smtp.lisp, CHANGELOG
Version 20100420.1 --- /project/cl-smtp/cvsroot/cl-smtp/README 2008/04/02 18:02:28 1.9 +++ /project/cl-smtp/cvsroot/cl-smtp/README 2010/05/06 09:24:43 1.10 @@ -21,14 +21,17 @@ (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) ssl) +Send email.
- Arguments: +Returns nil or error with message + + arguments: - host (String) : hostname or ip-adress of the smtpserver - from (String) : email adress - to (String or List of Strings) : email adress - subject (String) : subject text - message (String) : message body - keywords: + keywords: - 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 @@ -40,16 +43,70 @@ 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 - List of String/Pathnames) + - attachments (Attachment Instance or String or Pathname: attachments to send + List of Attachment/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 interpreted in KB - ssl (or t :starttls :tls) : if t or :STARTTLS: use the STARTTLS functionality if :TLS: use TLS directly + - external-format : symbol, default :utf-8
-Returns nil or error with message +------------------------------------------------ + +(cl-smtp:rfc2045-q-encode-string str &key (external-format :utf8)) + +Decodes a string to an quoted-printable string. + +Returns quoted-printable string + + arguments: + - str : the string to encode + keywords: + - external-format : symbol, default :utf-8 + +------------------------------------------------ + +(cl-smtp:rfc2231-encode-string str &key (external-format :utf8)) + +Decodes a string to an rfc2231 encode string. + +Returns rfc2231 encode string + + arguments: + - str : the string to encode + keywords: + - external-format : symbol, default :utf-8 + +------------------------------------------------ +CLASS +cl-smtp:attachment + + - accessor: attachment-name : string + - accessor: attachment-data-pathname : pathname + - accessor: attachment-mime-type : string (mime-type) + +It is now possible to send a file under a different name. +See cl-smtp:make-attachment. +------------------------------------------------ + +(cl-smtp:make-attachment data-pathname + &key (name (file-namestring data-pathname)) + (mime-type (lookup-mime-type name))) + +Create a instance of cl-smtp:attachment. + +Returns cl-smtp:attachment + + arguments: + - data-pathname : pathname + keywords: + - name : string, + default (file-namestring data-pathname) + - mime-type : string, + default (lookup-mime-type name) +------------------------------------------------
For debug output set the parameter *debug* to t (default nil) (setf cl-smtp::*debug* t) --- /project/cl-smtp/cvsroot/cl-smtp/attachments.lisp 2010/04/20 10:10:58 1.7 +++ /project/cl-smtp/cvsroot/cl-smtp/attachments.lisp 2010/05/06 09:24:43 1.8 @@ -3,7 +3,7 @@ ;;; This file is part of CL-SMTP, the Lisp SMTP Client
-;;; Copyright (C) 2004/2005/2006/2007 Jan Idzikowski +;;; Copyright (C) 2004/2005/2006/2007/2008/2009/2010 Jan Idzikowski
;;; This library is free software; you can redistribute it and/or ;;; modify it under the terms of the Lisp Lesser General Public License @@ -69,24 +69,91 @@ content-transfer-encoding))) (when include-blank-line? (write-blank-line sock)))
-(defun send-attachment-header (sock boundary name) - - (generate-message-header - sock - :boundary boundary - :content-type (format nil "~a;~%~tname="~a"" (lookup-mime-type name) name) - :content-transfer-encoding "base64" - :content-disposition (format nil "attachment; filename="~a"" name))) +(defun escape-rfc822-quoted-string (str) + (with-output-to-string (s) + (loop + for c across str do + (when (find (char-code c) '(10 13 92 34)) + (write-char #\ s)) + (write-char c s)))) + +(defun rfc2231-encode-string (string &key (external-format :utf-8)) + (with-output-to-string (s) + (format s "~A''" (string-upcase (symbol-name external-format))) + (loop for n across (string-to-octets string + :external-format external-format) + for c = (code-char n) + do (cond ((or (char<= #\0 c #\9) + (char<= #\a c #\z) + (char<= #\A c #\Z) + (find c "$-_.!*'()," :test #'char=)) + (write-char c s)) + ((char= c #\Space) + (write-char #+ s)) + (t (format s "%~2,'0x" (char-code c))))))) + +(defun send-attachment-header (sock boundary attachment external-format) + (let ((quoted-name + (escape-rfc822-quoted-string + (rfc2045-q-encode-string (attachment-name attachment) + :external-format external-format))) + (quoted-name* + (escape-rfc822-quoted-string + (rfc2231-encode-string (attachment-name attachment) + :external-format external-format)))) + (generate-message-header + sock + :boundary boundary + :content-type (format nil "~A;~%~tname*=~A;~%~tname=~S" + (attachment-mime-type attachment) + quoted-name* quoted-name) + :content-transfer-encoding "base64" + :content-disposition (format nil "attachment; filename*=~A; filename=~S" + quoted-name* quoted-name))))
(defun send-end-marker (sock boundary) ;; Note the -- at beginning and end of boundary is required (write-to-smtp sock (format nil "~%--~a--~%" boundary)))
-(defun send-attachment (sock attachment boundary buffer-size) - (when (probe-file attachment) - (let ((name (file-namestring attachment))) - (send-attachment-header sock boundary name) - (base64-encode-file attachment sock :buffer-size buffer-size)))) +(defclass attachment () + ((name :initarg :name + :accessor attachment-name) + (data-pathname :initarg :data-pathname + :accessor attachment-data-pathname) + (mime-type :initarg :mime-type + :accessor attachment-mime-type))) + +(defun make-attachment (data-pathname + &key (name (file-namestring data-pathname)) + (mime-type (lookup-mime-type name))) + (make-instance 'attachment + :data-pathname data-pathname + :name name + :mime-type mime-type)) + +(defmethod attachment-name ((attachment pathname)) + (file-namestring attachment)) + +(defmethod attachment-data-pathname ((attachment pathname)) + attachment) + +(defmethod attachment-mime-type ((attachment pathname)) + (lookup-mime-type (namestring attachment))) + +(defmethod attachment-name ((attachment string)) + (file-namestring attachment)) + +(defmethod attachment-data-pathname ((attachment string)) + attachment) + +(defmethod attachment-mime-type ((attachment string)) + (lookup-mime-type attachment)) + +(defun send-attachment (sock attachment boundary buffer-size external-format) + (send-attachment-header sock boundary attachment external-format) + (base64-encode-file (attachment-data-pathname attachment) + sock + :buffer-size buffer-size))
(defun base64-encode-file (file-in sock &key --- /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.asd 2008/04/17 08:33:55 1.16 +++ /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.asd 2010/05/06 09:24:43 1.17 @@ -2,7 +2,7 @@ ;;; This file is part of CL-SMTP, the Lisp SMTP Client
-;;; Copyright (C) 2004/2005/2006/2007/2008 Jan Idzikowski +;;; Copyright (C) 2004/2005/2006/2007/2008/2009/2010 Jan Idzikowski
;;; This library is free software; you can redistribute it and/or ;;; modify it under the terms of the Lisp Lesser General Public License @@ -17,7 +17,7 @@ ;;; Description: cl-smtp ASDF system definition file
(asdf:defsystem :cl-smtp - :version "20080417.1" + :version "20100505.1" :perform (load-op :after (op webpage) (pushnew :cl-smtp cl:*features*)) :depends-on (:usocket @@ -29,5 +29,5 @@ :components ((:file "package") (:file "attachments") (:file "cl-smtp") - (:file "smtp-output-stream") - (:file "mime-types"))) + (:file "mime-types") + (:file "tests"))) --- /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.lisp 2010/04/22 10:51:34 1.14 +++ /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.lisp 2010/05/06 09:24:43 1.15 @@ -2,7 +2,7 @@ ;;; This file is part of CL-SMTP, the Lisp SMTP Client
-;;; Copyright (C) 2004/2005/2006/2007 Jan Idzikowski +;;; Copyright (C) 2004/2005/2006/2007/2008/2009/2010 Jan Idzikowski
;;; This library is free software; you can redistribute it and/or ;;; modify it under the terms of the Lisp Lesser General Public License @@ -18,8 +18,6 @@
(in-package :cl-smtp)
-(defparameter *content-type* "text/plain; charset=ISO-8859-1") - (defparameter *x-mailer* (format nil "(~A ~A)" (lisp-implementation-type) (lisp-implementation-version))) @@ -38,7 +36,8 @@ (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))) + #\Return #\NewLine)) + (defvar *return-newline* #.(format nil "~C~C" #\Return #\NewLine)))
(defun mask-dot (str) "Replace all occurences of \r\n.\r\n in STR with \r\n..\r\n" @@ -62,19 +61,20 @@ #+allegro (excl:string-to-base64-string str) #-allegro (cl-base64:string-to-base64-string str))
-(defun q-encode-str (str &key (external-format - (flex:make-external-format :iso-8859-15))) - (let ((line-has-non-ascii nil)) +(defun rfc2045-q-encode-string (str &key (external-format :utf-8)) + (let ((line-has-non-ascii nil) + (exformat (flex:make-external-format external-format))) (with-output-to-string (s) (loop for c across str do (cond ((< 127 (char-code c)) (unless line-has-non-ascii - (format s "=?~A?Q?" (flex:external-format-name external-format)) + (format s "=?~A?Q?" + (string-upcase (symbol-name external-format))) (setf line-has-non-ascii t)) (loop for byte across (flex:string-to-octets (make-string 1 :initial-element c) - :external-format external-format) + :external-format exformat) do (format s "=~2,'0X" byte))) (t (when line-has-non-ascii @@ -84,6 +84,23 @@ (when line-has-non-ascii (format s "?=")))))
+(defun substitute-return-newline (str) + "Replace all occurences of \r\n in STR with spaces" + (let ((resultstr "")) + (labels ((mask (tempstr) + (let ((n (search *return-newline* tempstr))) + (cond + (n + (setf resultstr (concatenate 'string resultstr + (subseq tempstr 0 n) + " ")) + (mask (subseq tempstr (+ n 2)))) + (t + (setf resultstr (concatenate 'string resultstr + tempstr))))))) + (mask str)) + resultstr)) + (define-condition smtp-error (error) ())
@@ -94,9 +111,10 @@ (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" + (format stream "a command failed:~%command: ~S expected: ~A response-code: ~A response-message: ~A" (command condition) (expected-response-code condition) + (response-code condition) (response-message condition))))))
(define-condition rcpt-failed (smtp-protocol-error) @@ -125,23 +143,24 @@ :response-message msgstr)))) lines))
-(defun do-with-smtp-mail (host from to thunk &key port authentication ssl local-hostname) +(defun do-with-smtp-mail (host from to thunk &key port authentication ssl + local-hostname (external-format :utf-8)) (usocket:with-client-socket (socket stream host port :element-type '(unsigned-byte 8)) (setf stream (flexi-streams:make-flexi-stream - stream - :external-format - (flexi-streams:make-external-format - :latin-1 :eol-style :lf))) + stream + :external-format + (flexi-streams:make-external-format + external-format :eol-style :lf))) (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)) + (funcall thunk 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) +(defmacro with-smtp-mail ((stream-var host from to &key ssl (port (if (eq :tls ssl) 465 25)) authentication local-hostname (external-format :utf-8)) &body body) "Encapsulate a SMTP MAIl conversation. A connection to the SMTP server on HOST and PORT is established and a MAIL command is @@ -154,12 +173,13 @@ :port ,port :authentication ,authentication :ssl ,ssl - :local-hostname ,local-hostname)) + :local-hostname ,local-hostname + :external-format ,external-format))
(defun send-email (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 256)) + attachments (buffer-size 256) (external-format :utf-8)) (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 @@ -171,23 +191,28 @@ :buffer-size (if (numberp buffer-size) buffer-size 256) + :external-format external-format :ssl ssl))
(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))) + (local-hostname (usocket::get-host-name)) + (external-format :utf-8)) (with-smtp-mail (stream host from (append to cc bcc) :port port :authentication authentication :ssl ssl - :local-hostname local-hostname) - (setf (in-header stream) nil) + :local-hostname local-hostname + :external-format external-format) (let* ((boundary (make-random-boundary)) (html-boundary (if (and attachments html-message) (make-random-boundary) - boundary))) + boundary)) + (content-type + (format nil "text/plain; charset=~S" + (string-upcase (symbol-name external-format))))) (send-mail-headers stream :from from :to to @@ -213,19 +238,19 @@ :multipart-type "alternative") (write-blank-line stream) (generate-message-header - stream :boundary html-boundary :content-type *content-type* + 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" + :content-type content-type :content-disposition "inline" :include-blank-line? nil)) (html-message (generate-message-header - stream :boundary html-boundary :content-type *content-type* + stream :boundary html-boundary :content-type content-type :content-disposition "inline")) (t - (generate-message-header stream :content-type *content-type* + (generate-message-header stream :content-type content-type :include-blank-line? nil))) (write-blank-line stream) (write-to-smtp stream message) @@ -234,14 +259,16 @@ (when html-message (generate-message-header stream :boundary html-boundary - :content-type "text/html; charset=ISO-8859-1" + :content-type (format nil "text/html; charset=~S" + (string-upcase (symbol-name external-format))) :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-attachment stream attachment boundary buffer-size + external-format)) (send-end-marker stream boundary)))))
(define-condition no-supported-authentication-method (smtp-error) @@ -300,7 +327,7 @@ ;; Read the initial greeting from the SMTP server (smtp-command stream nil 220) (smtp-command stream (format nil "HELO ~A" - (usocket::get-host-name)) + (usocket::get-host-name)) 250) (return-from smtp-handshake stream))
@@ -362,11 +389,12 @@ is signalled. This condition may be handled by the caller in order to send the email anyway." (smtp-command stream - (format nil "MAIL FROM:<~A>" from) + (format nil "MAIL FROM:<~A>" (substitute-return-newline from)) 250) (dolist (address to) (restart-case - (smtp-command stream (format nil "RCPT TO:<~A>" address) + (smtp-command stream (format nil "RCPT TO:<~A>" + (substitute-return-newline address)) 250 :condition-class 'rcpt-failed :condition-arguments (list :recipient address)) @@ -384,23 +412,31 @@
(defun send-mail-headers (stream &key from to cc reply-to - extra-headers display-name subject) + extra-headers display-name subject + (external-format :utf-8)) "Send email headers according to the given arguments to the SMTP server connected to on STREAM. The server is expected to have previously accepted the DATA SMTP command." (write-to-smtp stream (format nil "Date: ~A" (get-email-date-string))) (if display-name (write-to-smtp stream (format nil "From: ~A <~A>" - (q-encode-str display-name) from)) + (rfc2045-q-encode-string + display-name :external-format external-format) + from)) (write-to-smtp stream (format nil "From: ~A" from))) (write-to-smtp stream (format nil "To: ~{ ~a~^,~}" to)) (when cc (write-to-smtp stream (format nil "Cc: ~{ ~a~^,~}" cc))) - (write-to-smtp stream (format nil "Subject: ~A" (q-encode-str subject))) - (write-to-smtp stream (format nil "X-Mailer: cl-smtp ~A" - (q-encode-str *x-mailer*))) + (write-to-smtp stream (format nil "Subject: ~A" + (rfc2045-q-encode-string + subject :external-format external-format))) + (write-to-smtp stream (format nil "X-Mailer: cl-smtp~A" + (rfc2045-q-encode-string + *x-mailer* :external-format external-format))) (when reply-to - (write-to-smtp stream (format nil "Reply-To: ~A" reply-to))) + (write-to-smtp stream (format nil "Reply-To: ~A" + (rfc2045-q-encode-string + reply-to :external-format external-format)))) (when (and extra-headers (listp extra-headers)) (dolist (l extra-headers) --- /project/cl-smtp/cvsroot/cl-smtp/mime-types.lisp 2007/11/03 23:53:29 1.1 +++ /project/cl-smtp/cvsroot/cl-smtp/mime-types.lisp 2010/05/06 09:24:43 1.2 @@ -3,7 +3,7 @@ ;;; This file is part of CL-SMTP, the Lisp SMTP Client
-;;; Copyright (C) 2004/2005/2006/2007 Jan Idzikowski +;;; Copyright (C) 2004/2005/2006/2007/2008/2009/2010 Jan Idzikowski
;;; This library is free software; you can redistribute it and/or ;;; modify it under the terms of the Lisp Lesser General Public License --- /project/cl-smtp/cvsroot/cl-smtp/package.lisp 2008/04/02 19:39:59 1.1 +++ /project/cl-smtp/cvsroot/cl-smtp/package.lisp 2010/05/06 09:24:43 1.2 @@ -26,7 +26,14 @@ "SMTP-PROTOCOL-ERROR" "NO-SUPPORTED-AUTHENTICATION-METHOD" "RCPT-FAILED" - "IGNORE-RECIPIENT")) + "IGNORE-RECIPIENT" + "ATTACHMENT" + "MAKE-ATTACHMENT" + "ATTACHMENT-NAME" + "ATTACHMENT-DATA-PATHNAME" + "ATTACHMENT-MIME-TYPE" + "RFC2045-Q-ENCODE-STRING" + "RFC2231-ENCODE-STRING"))
(in-package :cl-smtp)