Changeset 2784
- Timestamp:
- 03/23/08 15:19:51 (8 months ago)
- Files:
-
- trunk/thirdparty/cl-smtp/README (modified) (1 diff)
- trunk/thirdparty/cl-smtp/cl-smtp.asd (modified) (2 diffs)
- trunk/thirdparty/cl-smtp/cl-smtp.lisp (modified) (7 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/thirdparty/cl-smtp/README
r2782 r2784 26 26 - host (String) : hostname or ip-adress of the smtpserver 27 27 - from (String) : email adress 28 - to (String or Consof Strings) : email adress28 - to (String or List of Strings) : email adress 29 29 - subject (String) : subject text 30 30 - message (String) : message body 31 31 keywords: 32 - cc (String or Consof Strings) : email adress carbon copy33 - bcc (String or Consof Strings): email adress blind carbon copy32 - cc (String or List of Strings) : email adress carbon copy 33 - bcc (String or List of Strings): email adress blind carbon copy 34 34 - reply-to (String) : email adress 35 35 - displayname (String) : displayname of the sender 36 - extra-headers ( Cons) : extra headers as alist36 - extra-headers (List) : extra headers as alist 37 37 - html-message (String) : message body formatted with HTML tags 38 - authentication ( Cons) : list with 3elements39 ( :method"username" "password")38 - authentication (List) : list with 2 or elements 39 ([:method] "username" "password") 40 40 method is a keyword :plain or :login 41 If the method is not specified, the 42 proper method is determined automatically. 41 43 - attachments (String or Pathname: attachments to send 42 Consof String/Pathnames)44 List of String/Pathnames) 43 45 - buffer-size (Number default 256): controls how much of a attachment file 44 46 is read on each loop before encoding 45 47 and transmitting the contents, 46 48 the number is interpretted in KB 47 - ssl (Boolean) : if true than use the STARTTLS functionality to make a ssl connection 49 - ssl (Boolean) : if true than use the STARTTLS functionality 50 to make a ssl connection 48 51 49 52 Returns nil or error with message trunk/thirdparty/cl-smtp/cl-smtp.asd
r2782 r2784 18 18 19 19 (defpackage :cl-smtp 20 (:use :cl :asdf) 21 (:export :send-email)) 20 (:use :cl :asdf) 21 (:export "SEND-EMAIL" 22 "WITH-SMTP-MAIL" 23 24 "SMTP-ERROR" 25 "SMTP-PROTOCOL-ERROR" 26 "NO-SUPPORTED-AUTHENTICATION-METHOD" 27 "RCPT-FAILED" 28 "IGNORE-RECIPIENT")) 22 29 23 30 (in-package :cl-smtp) … … 30 37 31 38 (asdf:defsystem :cl-smtp 32 :version "20071113.1"33 :perform (load-op :after (op webpage)34 (pushnew :cl-smtp cl:*features*))35 :depends-on (:usocket #-allegro :cl-base6436 #-allegro :flexi-streams37 #-allegro :cl+ssl)38 :components ((:file "cl-smtp" :depends-on ("attachments"))39 (:file "attachments")40 (:file "mime-types")))39 :version "20071113.1" 40 :perform (load-op :after (op webpage) 41 (pushnew :cl-smtp cl:*features*)) 42 :depends-on (:usocket #-allegro :cl-base64 43 #-allegro :flexi-streams 44 #-allegro :cl+ssl) 45 :components ((:file "cl-smtp" :depends-on ("attachments")) 46 (:file "attachments") 47 (:file "mime-types"))) trunk/thirdparty/cl-smtp/cl-smtp.lisp
r2783 r2784 35 35 (error "the \"~A\" argument is not a string or cons" name)))) 36 36 37 (eval-when (:compile-toplevel :load-toplevel :execute) 38 (defvar *line-with-one-dot* #.(format nil "~C~C.~C~C" #\Return #\NewLine 39 #\Return #\NewLine)) 40 (defvar *line-with-two-dots* #.(format nil "~C~C..~C~C" #\Return #\NewLine 41 #\Return #\NewLine))) 42 37 43 (defun mask-dot (str) 38 "replace \r\n.\r\n with \r\n..\r\n" 39 (let ((dotstr (format nil "~C~C.~C~C" #\Return #\NewLine 40 #\Return #\NewLine)) 41 (maskdotsr (format nil "~C~C..~C~C" #\Return #\NewLine 42 #\Return #\NewLine)) 43 (resultstr "")) 44 "Replace all occurences of \r\n.\r\n in STR with \r\n..\r\n" 45 (let ((resultstr "")) 44 46 (labels ((mask (tempstr) 45 (let ((n (search dotstrtempstr)))47 (let ((n (search *line-with-one-dot* tempstr))) 46 48 (cond 47 49 (n 48 50 (setf resultstr (concatenate 'string resultstr 49 51 (subseq tempstr 0 n) 50 maskdotsr))51 (mask (subseq tempstr (+ n 5))))52 *line-with-two-dots*)) 53 (mask (subseq tempstr (+ n #.(length *line-with-one-dot*))))) 52 54 (t 53 55 (setf resultstr (concatenate 'string resultstr … … 62 64 63 65 (define-condition smtp-error (error) 64 ( (host :initarg :host :reader host)))66 ()) 65 67 66 68 (define-condition smtp-protocol-error (smtp-error) … … 71 73 (:report (lambda (condition stream) 72 74 (print-unreadable-object (condition stream :type t) 73 (format stream "while talking to smtp server ~A, a command failed:~%command: ~S expected: ~A response: ~A" 74 (host condition) 75 (format stream "a command failed:~%command: ~S expected: ~A response: ~A" 75 76 (command condition) 76 77 (expected-response-code condition) 77 78 (response-message condition)))))) 78 79 79 (defun smtp-command (stream command expected-response-code &optional (condition-class 'smtp-protocol-error)) 80 (define-condition rcpt-failed (smtp-protocol-error) 81 ((recipient :initarg :recipient 82 :reader recipient)) 83 (:report (lambda (condition stream) 84 (print-unreadable-object (condition stream :type t) 85 (format stream "while trying to send email through SMTP, the server rejected the recipient ~A: ~A" 86 (recipient condition) 87 (response-message condition)))))) 88 89 (defun smtp-command (stream command expected-response-code 90 &key (condition-class 'smtp-error) condition-arguments) 80 91 (when command 81 92 (write-to-smtp stream command)) … … 83 94 (read-from-smtp stream) 84 95 (when (/= code expected-response-code) 85 (error condition-class 86 :command command 87 :expected-response-code expected-response-code 88 :response-code code 89 :response-message msgstr)) 96 (apply #'error 97 condition-class 98 (append condition-arguments 99 (list :command command 100 :expected-response-code expected-response-code 101 :response-code code 102 :response-message msgstr)))) 90 103 lines)) 104 105 (defun do-with-smtp-mail (host port from to thunk &key authentication ssl local-hostname) 106 (usocket:with-client-socket (socket stream host port) 107 (let ((stream (smtp-handshake stream 108 :authentication authentication 109 :ssl ssl 110 :local-hostname local-hostname))) 111 (initiate-smtp-mail stream from to) 112 (funcall thunk stream) 113 (finish-smtp-mail stream)))) 114 115 (defmacro with-smtp-mail ((stream-var host port from to &key authentication ssl local-hostname) 116 &body body) 117 "Encapsulate a SMTP MAIl conversation. A connection to the SMTP 118 server on HOST and PORT is established and a MAIL command is 119 initiated with FROM being the mail sender and TO being the list of 120 recipients. BODY is evaluated with STREAM-VAR being the stream 121 connected to the remote SMTP server. BODY is expected to write the 122 RFC2821 message (headers and body) to STREAM-VAR." 123 `(do-with-smtp-mail ,host ,port ,from ,to 124 (lambda (,stream-var) ,@body) 125 :authentication ,authentication 126 :ssl ,ssl 127 :local-hostname ,local-hostname)) 91 128 92 129 (defun send-email (host from to subject message … … 111 148 display-name authentication attachments buffer-size ssl 112 149 (local-hostname (usocket::get-host-name))) 113 (let* ((sock (usocket:socket-stream (usocket:socket-connect host port))) 114 (boundary (make-random-boundary)) 115 (html-boundary (if (and attachments html-message) 116 (make-random-boundary) 117 boundary))) 118 (unwind-protect 119 (let ((stream (open-smtp-connection sock 120 :authentication authentication 121 :ssl ssl 122 :local-hostname local-hostname))) 123 (send-smtp-headers stream :from from :to to :cc cc :bcc bcc 124 :reply-to reply-to 125 :display-name display-name 126 :extra-headers extra-headers :subject subject) 127 (when (or attachments html-message) 128 (send-multipart-headers 129 stream :attachment-boundary (when attachments boundary) 130 :html-boundary html-boundary)) 131 ;;----------- Send the body Message --------------------------- 132 ;;--- Send the proper headers depending on plain-text, 133 ;;--- multi-part or html email 134 (cond ((and attachments html-message) 135 ;; if both present, start attachment section, 136 ;; then define alternative section, 137 ;; then write alternative header 138 (progn 139 (generate-message-header 140 stream :boundary boundary :include-blank-line? nil) 141 (generate-multipart-header stream html-boundary 142 :multipart-type "alternative") 143 (write-blank-line stream) 144 (generate-message-header 145 stream :boundary html-boundary :content-type *content-type* 146 :content-disposition "inline" :include-blank-line? nil))) 147 (attachments 148 (generate-message-header 149 stream :boundary boundary 150 :content-type *content-type* :content-disposition "inline" 151 :include-blank-line? nil)) 152 (html-message 153 (generate-message-header 154 stream :boundary html-boundary :content-type *content-type* 155 :content-disposition "inline")) 156 (t 157 (generate-message-header stream :content-type *content-type* 158 :include-blank-line? nil))) 159 (write-blank-line stream) 160 (write-to-smtp stream message) 161 (write-blank-line stream) 162 ;;---------- Send Html text if needed ------------------------- 163 (when html-message 164 (generate-message-header 165 stream :boundary html-boundary 166 :content-type "text/html; charset=ISO-8859-1" 167 :content-disposition "inline") 168 (write-to-smtp stream html-message) 169 (send-end-marker stream html-boundary)) 170 ;;---------- Send Attachments ----------------------------------- 171 (when attachments 172 (dolist (attachment attachments) 173 (send-attachment stream attachment boundary buffer-size)) 174 (send-end-marker stream boundary)) 175 (smtp-command stream "." 250) 176 (smtp-command stream "QUIT" 221)) 177 (close sock)))) 178 179 (defun open-smtp-connection (stream &key authentication ssl local-hostname) 150 (with-smtp-mail (stream host port from (append to cc bcc) 151 :authentication authentication 152 :ssl ssl 153 :local-hostname local-hostname) 154 (let* ((boundary (make-random-boundary)) 155 (html-boundary (if (and attachments html-message) 156 (make-random-boundary) 157 boundary))) 158 (send-mail-headers stream 159 :from from 160 :to to 161 :cc cc 162 :reply-to reply-to 163 :display-name display-name 164 :extra-headers extra-headers :subject subject) 165 (when (or attachments html-message) 166 (send-multipart-headers stream 167 :attachment-boundary (when attachments boundary) 168 :html-boundary html-boundary)) 169 ;;----------- Send the body Message --------------------------- 170 ;;--- Send the proper headers depending on plain-text, 171 ;;--- multi-part or html email 172 (cond ((and attachments html-message) 173 ;; if both present, start attachment section, 174 ;; then define alternative section, 175 ;; then write alternative header 176 (progn 177 (generate-message-header 178 stream :boundary boundary :include-blank-line? nil) 179 (generate-multipart-header stream html-boundary 180 :multipart-type "alternative") 181 (write-blank-line stream) 182 (generate-message-header 183 stream :boundary html-boundary :content-type *content-type* 184 :content-disposition "inline" :include-blank-line? nil))) 185 (attachments 186 (generate-message-header 187 stream :boundary boundary 188 :content-type *content-type* :content-disposition "inline" 189 :include-blank-line? nil)) 190 (html-message 191 (generate-message-header 192 stream :boundary html-boundary :content-type *content-type* 193 :content-disposition "inline")) 194 (t 195 (generate-message-header stream :content-type *content-type* 196 :include-blank-line? nil))) 197 (write-blank-line stream) 198 (write-to-smtp stream message) 199 (write-blank-line stream) 200 ;;---------- Send Html text if needed ------------------------- 201 (when html-message 202 (generate-message-header 203 stream :boundary html-boundary 204 :content-type "text/html; charset=ISO-8859-1" 205 :content-disposition "inline") 206 (write-to-smtp stream html-message) 207 (send-end-marker stream html-boundary)) 208 ;;---------- Send Attachments ----------------------------------- 209 (when attachments 210 (dolist (attachment attachments) 211 (send-attachment stream attachment boundary buffer-size)) 212 (send-end-marker stream boundary))))) 213 214 (define-condition no-supported-authentication-method (smtp-error) 215 ((features :initarg :features :reader features)) 216 (:report (lambda (condition stream) 217 (print-unreadable-object (condition stream :type t) 218 (format stream "SMTP authentication has been requested, but the SMTP server did not advertise any ~ 219 supported authentication scheme. Features announced: ~{~S~^, ~}" 220 (features condition)))))) 221 222 (defun smtp-authenticate (stream authentication features) 223 "Authenticate to the SMTP server connected on STREAM. 224 AUTHENTICATION is a list of two or three elements. If the first 225 element is a keyword, it specifies the desired authentication 226 method (:PLAIN or :LOGIN), which is currently ignored. The actual 227 method used is determined by looking at the advertised features of 228 the SMTP server. The (other) two elements of the AUTHENTICATION 229 list are the login username and password. FEATURES is the list of 230 features announced by the SMTP server. 231 232 If the server does not announce any compatible authentication scheme, 233 the NO-SUPPORTED-AUTHENTICATION-METHOD error is signalled." 234 (when (keywordp (car authentication)) 235 (pop authentication)) 236 (destructuring-bind (username password) authentication 237 (cond 238 ((member "AUTH PLAIN" features :test #'equal) 239 (smtp-command stream (format nil "AUTH PLAIN ~A" 240 (string-to-base64-string 241 (format nil "~A~C~A~C~A" 242 username 243 #\null username 244 #\null password))) 245 235)) 246 ((member "AUTH LOGIN" features :test #'equal) 247 (smtp-command stream "AUTH LOGIN" 248 334) 249 (smtp-command stream (string-to-base64-string username) 250 334) 251 (smtp-command stream (string-to-base64-string password) 252 235)) 253 (t 254 (error 'no-supported-authentication-method :features features))))) 255 256 (defun smtp-handshake (stream &key authentication ssl local-hostname) 257 "Perform the initial SMTP handshake on STREAM. Returns the stream 258 to use further down in the conversation, which may be different from 259 the original stream if we switched to SSL." 260 261 ;; Read the initial greeting from the SMTP server 180 262 (smtp-command stream nil 181 263 220) 182 (if (or ssl authentication) 183 ;; When SSL or authentication requested, perform ESMTP EHLO 184 (let ((lines (smtp-command stream (format nil "EHLO ~A" local-hostname) 185 250))) 186 (when ssl 187 (unless (find "STARTTLS" lines :test #'equal) 188 (error "this server does not supports TLS")) 189 (print-debug "this server supports TLS") 190 (smtp-command stream "STARTTLS" 191 220) 192 (setf stream 193 #+allegro (socket:make-ssl-client-stream stream) 194 #-allegro 195 (let ((s stream)) 196 (cl+ssl:make-ssl-client-stream 197 (cl+ssl:stream-fd stream) 198 :close-callback (lambda () (close s))))) 199 #-allegro 200 (setf stream (flexi-streams:make-flexi-stream 201 stream 202 :external-format 203 (flexi-streams:make-external-format 204 :latin-1 :eol-style :lf)))) 205 (when authentication 206 (ecase (car authentication) 207 (:plain 208 (smtp-command stream (format nil "AUTH PLAIN ~A" 209 (string-to-base64-string 210 (format nil "~A~C~A~C~A" 211 (cadr authentication) 212 #\null (cadr authentication) 213 #\null 214 (caddr authentication)))) 215 235)) 216 (:login 217 (smtp-command stream "AUTH LOGIN" 218 334) 219 (smtp-command stream (string-to-base64-string (cadr authentication)) 220 334) 221 (smtp-command stream (string-to-base64-string (caddr authentication)) 222 235)) 223 (t 224 (smtp-command stream (format nil "HELO ~A" local-hostname) 225 250))))) 226 ;; No authentication or SSL requested, perform classic SMTP HELO 227 (smtp-command stream (format nil "HELO ~A" 264 265 (unless (or ssl authentication) 266 ;; Unless we want ESMTP features, perform classic SMTP handshake and return 267 (smtp-command stream (format nil "HELO ~A" 228 268 (usocket::get-host-name)) 229 250)) 269 250) 270 (return-from smtp-handshake stream)) 271 272 ;; When SSL or authentication requested, perform ESMTP EHLO 273 (let (features) 274 (labels 275 ((do-ehlo () 276 (setf features (rest (smtp-command stream (format nil "EHLO ~A" local-hostname) 277 250)))) 278 (convert-connection-to-ssl () 279 (setf stream 280 #+allegro (socket:make-ssl-client-stream stream) 281 #-allegro 282 (let ((s stream)) 283 (cl+ssl:make-ssl-client-stream 284 (cl+ssl:stream-fd stream) 285 :close-callback (lambda () (close s))))) 286 #-allegro 287 (setf stream (flexi-streams:make-flexi-stream 288 stream 289 :external-format 290 (flexi-streams:make-external-format 291 :latin-1 :eol-style :lf))))) 292 (ecase ssl 293 ((or t :starttls) 294 (do-ehlo) 295 (unless (find "STARTTLS" features :test #'equal) 296 (error "this server does not supports TLS")) 297 (print-debug "this server supports TLS") 298 (smtp-command stream "STARTTLS" 299 220) 300 (convert-connection-to-ssl) 301 ;; After STARTTLS, the connection is "like new". Re-do the 302 ;; EHLO command to switch the server to ESMTP mode and read 303 ;; the list of announced features again. 304 (do-ehlo)) 305 (:tls 306 ;; Plain SSL connection 307 (convert-connection-to-ssl) 308 (do-ehlo)))) 309 (when authentication 310 (smtp-authenticate stream authentication features))) 230 311 stream) 231 312 232 (defun send-smtp-headers (stream 233 &key from to cc bcc reply-to 313 (defun initiate-smtp-mail (stream from to) 314 "Initiate an SMTP MAIL command, sending a MAIL FROM command for the 315 email address in FROM and RCPT commands for all receipients in TO, 316 which is expected to be a list. 317 318 If any of the TO addresses is not accepted, a RCPT-FAILED condition 319 is signalled. This condition may be handled by the caller in order 320 to send the email anyway." 321 (smtp-command stream 322 (format nil "MAIL FROM:<~A>" from) 323 250) 324 (dolist (address to) 325 (restart-case 326 (smtp-command stream (format nil "RCPT TO:<~A>" address) 327 250 328 :condition-class 'rcpt-failed 329 :condition-arguments (list :recipient address)) 330 (ignore-recipient ()))) 331 (smtp-command stream "DATA" 332 354)) 333 334 (defun finish-smtp-mail (stream) 335 "Finish sending an email to the SMTP server connected to on STREAM. 336 The server is expected to be inside of the DATA SMTP command. The 337 connection is then terminated by sending a QUIT command." 338 (smtp-command stream "." 250) 339 (smtp-command stream "QUIT" 221)) 340 341 (defun send-mail-headers (stream 342 &key from to cc reply-to 234 343 extra-headers display-name subject) 235 (smtp-command stream 236 (format nil "MAIL FROM:~@[~A ~]<~A>" display-name from) 237 250) 238 (compute-rcpt-command stream to) 239 (compute-rcpt-command stream cc) 240 (compute-rcpt-command stream bcc) 241 (smtp-command stream "DATA" 242 354) 344 "Send email headers according to the given arguments to the SMTP 345 server connected to on STREAM. The server is expected to have 346 previously accepted the DATA SMTP command." 243 347 (write-to-smtp stream (format nil "Date: ~A" (get-email-date-string))) 244 348 (write-to-smtp stream (format nil "From: ~@[~A <~]~A~@[>~]" … … 268 372 (t nil))) 269 373 270 (defun compute-rcpt-command (stream adresses)271 (dolist (to adresses)272 (smtp-command stream (format nil "RCPT TO:<~A>" to)273 250)))274 275 374 (defun write-to-smtp (stream command) 276 375 (print-debug (format nil "to server: ~A" command)) … … 308 407 (hour (/ x 3600))) 309 408 (if (integerp hour) 310 (cond409 (cond 311 410 ((>= hour 0) 312 411 (format nil "+~2,'0d00" hour)) 313 412 ((< hour 0) 314 (format nil "-~2,'0d00" (* -1 hour))))315 (multiple-value-bind (h m) (truncate min 60)316 (cond413 (format nil "-~2,'0d00" (* -1 hour)))) 414 (multiple-value-bind (h m) (truncate min 60) 415 (cond 317 416 ((>= hour 0) 318 417 (format nil "+~2,'0d~2,'0d" h (truncate m)))
