From jidzikowski at common-lisp.net Sat Nov 3 23:53:29 2007 From: jidzikowski at common-lisp.net (jidzikowski) Date: Sat, 3 Nov 2007 18:53:29 -0500 (EST) Subject: [cl-smtp-cvs] CVS cl-smtp Message-ID: <20071103235329.DBEC15310D@common-lisp.net> Update of /project/cl-smtp/cvsroot/cl-smtp In directory clnet:/tmp/cvs-serv13213 Modified Files: CHANGELOG README attachments.lisp cl-smtp.asd cl-smtp.lisp Added Files: mime-types.lisp Log Message: Fixed bug with the file attachments to solve corrupted files when processed with chunking turned on. (Brian Sorg) Added automatically including mime types for attachesments of common known extensions. (Brian Sorg) Added Html-messages option to send-mail function. (Brian Sorg) --- /project/cl-smtp/cvsroot/cl-smtp/CHANGELOG 2007/10/18 19:10:37 1.8 +++ /project/cl-smtp/cvsroot/cl-smtp/CHANGELOG 2007/11/03 23:53:29 1.9 @@ -1,3 +1,13 @@ +Version 20071104.1 +2007.11.04 +Fixed bug with the file attachments to solve corrupted files when +processed with chunking turned on. (Brian Sorg) +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 +Add mime-type.lisp + Version 20071018.1 2007.10.18 Reverted the non allegro base64 functionality in attachment.lisp, --- /project/cl-smtp/cvsroot/cl-smtp/README 2007/09/04 18:54:06 1.6 +++ /project/cl-smtp/cvsroot/cl-smtp/README 2007/11/03 23:53:29 1.7 @@ -17,8 +17,8 @@ ------------------------------------------------ (cl-smtp:send-email host from to subject message - &key (port 25) cc bcc reply-to extra-headers authentication - attachments (buffer-size 256)) + &key (port 25) cc bcc reply-to extra-headers html-message + authentication attachments (buffer-size 256)) Arguments: - host (String) : hostname or ip-adress of the smtpserver @@ -32,6 +32,7 @@ - reply-to (String) : email adress - displayname (String) : displayname of the sender - extra-headers (Cons) : extra headers as alist + - html-message (String) : message body formatted with HTML tags - authentication (Cons) : list with 3 elements (:method "username" "password") method is a keyword :plain or :login --- /project/cl-smtp/cvsroot/cl-smtp/attachments.lisp 2007/10/16 17:33:19 1.4 +++ /project/cl-smtp/cvsroot/cl-smtp/attachments.lisp 2007/11/03 23:53:29 1.5 @@ -37,98 +37,131 @@ (dotimes (i (length chars) arr) (setf (aref arr i) (pop chars))))) -(defun make-random-boundary (&optional (length 50) (boundary-chars *boundary-chars*)) +(defun make-random-boundary (&optional (length 30) + (boundary-chars *boundary-chars*)) (let ((boundary (make-string length)) + (prefix "_---------_") (chars-length (length boundary-chars))) - (dotimes (i length boundary) - (setf (aref boundary i) (svref *boundary-chars* (random chars-length)))))) + (dotimes (i length (concatenate 'string prefix boundary)) + (setf (aref boundary i) + (svref *boundary-chars* (random chars-length)))))) -(defun generate-multipart-header (sock boundary) +(defun generate-multipart-header (sock boundary &key (multipart-type "mixed")) (write-to-smtp sock - (format nil "Content-type: multipart/mixed;~%~tBoundary=\"~a\"" - boundary))) + (format nil "Content-type: multipart/~a;~%~tBoundary=\"~a\"" + multipart-type boundary))) +(defun generate-message-header (sock + &key boundary ;; uniques string of character -- see make-random-boundary + content-type ;; "text/plain; charset=ISO-8859-1" + content-disposition ;; inline attachment + content-transfer-encoding ;; 7 bit or 8 bit + (include-blank-line? t)) + (when boundary + (write-to-smtp sock (format nil "--~a" boundary))) + (when content-type + (write-to-smtp sock (format nil "Content-type: ~a" content-type))) + (when content-disposition + (write-to-smtp sock (format nil "Content-Disposition: ~A" + content-disposition))) + (when content-transfer-encoding + (write-to-smtp sock (format nil "Content-Transfer-Encoding: ~A" + content-transfer-encoding))) + (when include-blank-line? (write-blank-line sock))) -(defun wrap-message-with-multipart-dividers (message boundary) +(defun send-attachment-header (sock boundary name) - (concatenate 'string (format nil "--~a~%" boundary) - (format nil "Content-type: text/plain~%") - (format nil "Content-Disposition: inline~%") - (format nil "~%") - message (format nil "~%"))) + (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 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) - (print-debug (format nil "Sending attachment: ~a" attachment)) (when (probe-file attachment) (let ((name (file-namestring attachment))) (send-attachment-header sock boundary name) - (base64-encode-file attachment sock :buffer-size buffer-size) - ))) - -(defun send-attachment-header (sock boundary name) + (base64-encode-file attachment sock :buffer-size buffer-size)))) - (write-to-smtp sock - (format nil "~%--~a~%Content-type: application/octet-stream;~%~tname=\"~a\"~%Content-Transfer-Encoding: base64~%Content-Disposition: attachment; filename=\"~a\"~%" - boundary - name - name))) - -(defun send-attachments-end-marker (sock boundary) - (write-to-smtp sock - (format nil "~%--~a--~%" boundary))) - (defun base64-encode-file (file-in sock &key (buffer-size 256) ;; in KB - (wrap-at-column 76)) - (declare (ignorable wrap-at-column)) - (let* ((max-buffer-size (* buffer-size 1024)) - (byte-count 0) - (buffer (make-array max-buffer-size - :element-type '(unsigned-byte 8)))) + (wrap-at-column 70)) + "Encodes the file contents given by file-in, which can be of any form appropriate to with-open-file, and write the base-64 encoded version to sock, which is a socket. + +Buffer-size, given in KB, controls how much of the file is processed and written to the socket at one time. A buffer-size of 0, processes the file all at once, regardless of its size. One will have to weigh the speed vs, memory consuption risks when chosing which way is best. + +Wrap-at-column controls where the encode string is divided for line breaks." (when (probe-file file-in) ;;-- open filein --------- (with-open-file (strm-in file-in :element-type '(unsigned-byte 8)) - - (loop - (setq byte-count 0) - ;; read a portion of the file into the buffer - (setq byte-count (dotimes (i max-buffer-size max-buffer-size) - (let ((bchar (read-byte strm-in nil 'EOF))) - (if (eql bchar 'EOF) - (return i) - (setf (aref buffer i) bchar))))) - (print-debug (format nil "~%** Byte Count ~a~%" byte-count)) - ;; encode the buffer and write out to stream - #+allegro - (write-string (excl:usb8-array-to-base64-string - (if (< byte-count max-buffer-size) - (trimmed-buffer byte-count buffer) - buffer) - wrap-at-column) sock) - #-allegro - (cl-base64:usb8-array-to-base64-stream - (if (< byte-count max-buffer-size) - (trimmed-buffer byte-count buffer) - buffer) - sock :columns wrap-at-column) - (force-output sock) - ;;-- when finished reading exit do loop - (when (< byte-count max-buffer-size) - (return))))))) - -(defun trimmed-buffer (byte-count buffer) - "Creates an array the length of byte-count and copies contents of buffer into it. -Needed in Lispworks, Lispworks initialized all elements of the buffer array when it was made, allegro doesn't -seem to have this behavior" - (let ((trimmed-buffer (make-array byte-count :element-type '(unsigned-byte 8)))) - (dotimes (i byte-count trimmed-buffer) - (setf (aref trimmed-buffer i) (aref buffer i))))) - - - - - - - + (let* ((;; convert buffer size given to bytes + ;; or compute bytes based on file + max-buffer-size + (if (zerop buffer-size) + (file-length strm-in) + ;; Ensures 64 bit encoding is properly + ;; divided so that filler + ;; characters are not required between chunks + (* 24 (truncate (/ (* buffer-size 1024) 24))))) + (column-count 0) + (eof? nil) + (buffer (make-array max-buffer-size + :element-type '(unsigned-byte 8)))) + (loop + (print-debug (format nil "~%Process attachment ~a~%" file-in)) + (let* ((;; read a portion of the file into the buffer arrary and + ;; returns the index where it stopped + byte-count (dotimes (i max-buffer-size max-buffer-size) + (let ((bchar (read-byte strm-in nil 'EOF))) + (if (eql bchar 'EOF) + (progn + (setq eof? t) + (return i)) + (setf (aref buffer i) bchar)))))) + (if (zerop buffer-size) + ;; send file all at once to socket. + #+allegro + (write-string (excl:usb8-array-to-base64-string + buffer wrap-at-column) sock) + #-allegro + (cl-base64:usb8-array-to-base64-stream + buffer sock :columns wrap-at-column) + ;; otherwise process file in chunks. + ;; The extra encoded-string, + ;; and its subseq functions are brute force methods + ;; to properly handle the wrap-at-column feature + ;; between buffers. + ;; Not the most efficient way, + ;; but it works and uses existing functions + ;; in the cl-base64 package. + (let* ((;; drops off extra elements that were not filled in in reading, this is important for lisp systems that default a value into + ;; the array when it is created. -- ie Lispworks, SBCL + trimmed-buffer (if eof? + (subseq buffer 0 byte-count) + buffer)) + (encoded-string + #+allegro + (cl-base64:usb8-array-to-base64-string + trimmed-buffer) + #-allegro + (cl-base64:usb8-array-to-base64-string + trimmed-buffer))) + (loop for ch across encoded-string + do (progn + (write-char ch sock) + (incf column-count) + (when (= column-count wrap-at-column) + (setq column-count 0) + (write-char #\Newline sock)))))) + (force-output sock) + (print-debug (format nil "~% Eof is ~a~%" eof?)) + (when (or (zerop buffer-size) + eof?) + (return)))))))) --- /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.asd 2007/10/18 19:10:37 1.9 +++ /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.asd 2007/11/03 23:53:29 1.10 @@ -29,7 +29,10 @@ (print ,str))) (asdf:defsystem :cl-smtp - :version "20071018.1" + :version "20071104.1" + :perform (load-op :after (op webpage) + (pushnew :cl-smtp cl:*features*)) :depends-on (:usocket #-allegro :cl-base64) :components ((:file "cl-smtp" :depends-on ("attachments")) - (:file "attachments"))) + (:file "attachments") + (:file "mime-types"))) --- /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.lisp 2007/09/04 18:56:58 1.7 +++ /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.lisp 2007/11/03 23:53:29 1.8 @@ -18,6 +18,8 @@ (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))) @@ -61,12 +63,13 @@ (defun send-email (host from to subject message &key (port 25) cc bcc reply-to extra-headers - display-name authentication + html-message display-name authentication 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 :extra-headers extra-headers + :html-message html-message :display-name display-name :authentication authentication :attachments (check-arg attachments "attachments") @@ -76,56 +79,64 @@ (defun send-smtp (host from to subject message - &key (port 25) cc bcc reply-to extra-headers + &key (port 25) cc bcc reply-to extra-headers html-message display-name authentication attachments buffer-size) - (let ((sock (usocket:socket-stream (usocket:socket-connect host port))) - (boundary (make-random-boundary))) + (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) - (write-to-smtp sock - (format nil "MAIL FROM:~@[~A ~]<~A>" display-name from)) - (multiple-value-bind (code msgstr) - (read-from-smtp sock) - (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") - (multiple-value-bind (code msgstr) - (read-from-smtp sock) - (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)) - (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*)) - (when reply-to - (write-to-smtp sock (format nil "Reply-To: ~A" reply-to))) - (when (and extra-headers - (listp extra-headers)) - (dolist (l extra-headers) - (write-to-smtp sock - (format nil "~A: ~{~a~^,~}" (car l) (rest l))))) - (write-to-smtp sock "Mime-Version: 1.0") - (when attachments - (generate-multipart-header sock boundary)) - (write-char #\Return sock) - (write-char #\NewLine sock) - (when attachments - (setq message (wrap-message-with-multipart-dividers - message boundary))) + (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) + (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-attachments-end-marker sock boundary)) + (send-end-marker sock boundary)) (write-char #\. sock) (write-char #\Return sock) (write-char #\NewLine sock) @@ -190,6 +201,49 @@ (when (/= code 250) (error "wrong response from smtp server: ~A" msgstr)))))) +(defun send-smtp-headers (sock + &key from to cc bcc reply-to + extra-headers display-name subject) + (write-to-smtp sock + (format nil "MAIL FROM:~@[~A ~]<~A>" display-name from)) + (multiple-value-bind (code msgstr) + (read-from-smtp sock) + (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") + (multiple-value-bind (code msgstr) + (read-from-smtp sock) + (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)) + (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*)) + (when reply-to + (write-to-smtp sock (format nil "Reply-To: ~A" reply-to))) + (when (and extra-headers + (listp extra-headers)) + (dolist (l extra-headers) + (write-to-smtp sock + (format nil "~A: ~{~a~^,~}" (car l) (rest l))))) + (write-to-smtp sock "Mime-Version: 1.0")) + +(defun send-multipart-headers (sock &key attachment-boundary html-boundary) + (cond (attachment-boundary + (generate-multipart-header sock attachment-boundary + :multipart-type "mixed")) + (html-boundary (generate-multipart-header + sock html-boundary + :multipart-type "alternative")) + (t nil))) (defun compute-rcpt-command (sock adresses) (dolist (to adresses) @@ -198,7 +252,6 @@ (read-from-smtp sock) (when (/= code 250) (error "in RCPT TO command: ~A" msgstr))))) - (defun write-to-smtp (sock command) (print-debug (format nil "to server: ~A" command)) @@ -207,6 +260,11 @@ (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)) (response-code (parse-integer line :start 0 :junk-allowed t))) @@ -214,7 +272,7 @@ (if (= (char-code (elt line 3)) (char-code #\-)) (read-from-smtp sock) (values response-code line)))) - + (defun get-email-date-string () (multiple-value-bind (sec min h d m y wd) (get-decoded-time) (let* ((month (elt '("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec") (- m 1))) @@ -224,8 +282,7 @@ (get-universal-time))))) (format nil "~A, ~2,'0d ~A ~d ~2,'0d:~2,'0d:~2,'0d ~D" weekday d month y h min sec timezone)))) - - + (defun get-timezone-from-integer (x) (let ((min (/ x 60)) (hour (/ x 3600))) --- /project/cl-smtp/cvsroot/cl-smtp/mime-types.lisp 2007/11/03 23:53:29 NONE +++ /project/cl-smtp/cvsroot/cl-smtp/mime-types.lisp 2007/11/03 23:53:29 1.1 ;;; -*- mode: Lisp -*- ;;; This file is part of CL-SMTP, the Lisp SMTP Client ;;; Copyright (C) 2004/2005/2006/2007 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 ;;; (http://opensource.franz.com/preamble.html), known as the LLGPL. ;;; This library is distributed in the hope that it will be useful, ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;;; Lisp Lesser GNU General Public License for more details. ;;; File: attachments.lisp ;;; Description: encoding and transmitting login to include a mime attachment ;;; ;;; Contributed by Brian Sorg ;;; (in-package :cl-smtp) (eval-when (:compile-toplevel :load-toplevel) ;;; Some of the most common file extensions with the mime types and descriptions. ;;; Extracted from numberous webpages. (defparameter *mime-type-descriptions* '(("386" "application/octet-stream" "Windows Enhanced Mode Driver or Swap File") ("001" "application/x-001" "FAX Datafile") ("3GPP" "audio/3gpp" "3rd Generation Partnership Project. Multimedia over 3rd generation wireless networks. H.263 video is the mandatory video format in 3GPP and AMR is the main audio/speech format.") ("7CB" "application/vnd.ecdis-update" "Electronic Chart Display and Information System (ECDIS)") ("aa" "audio/audible" "Audible file format (audio books)") ("aab" "application/x-authorware-bin" "Macromedia Authorware Binary") ("aac" "audio/aac" "Advanced Audio Coding File. Part of MPEG-2 and MPEG-4 standard. (Apple iTunes Store)") ("aam" "application/x-authorware-map" "Authorware Map (Shockwave?)") ("aas" "application/x-authorware-seg" "Authorware Shocked Packet (Segment) ") ("aba" "text/x-palm-aba" "AddressBook Archive (Palm)") ("ac3" "audio/ac3" "Adaptive Transform Coder 3 (relates to the bitstream format of Dolby Digital)") ("adr" "application/x-msaddr" "Address Book") ("aexpk" "application/pgp-keys" "Armored extracted public key (PGP)") ("afl" "video/animaflex" "Font file (for Allways) (Lotus 1-2-3)") ("ahtml" "magnus-internal/cgi-advertiser" " ") ("ai" "application/postscript" "Encapsulated PostScript (metafile) (Adobe Illustrator)") ("aif" "audio/x-aiff" "Audio Interchange File Format") ("aifc" "audio/x-aiff" "Audio Interchange File Format") ("aiff" "audio/x-aiff" "Audio Interchange File Format") ("aim" "application/x-aim" "AIM file - AOL Instant Messanger") ("alt" "application/x-up-alert" "Menu file (WordPerfect Library)") ("aos" "application/x-nokia-" "Add-On Software (Nokia 9000)") ("arj" "application/x-arj" "Compressed file archive created by ARJ or winzip") ("art" "image/x-jg" "AOL Johnson-Grace Compressed File and Another Ray Tracer Format") ("asc" "application/pgp-encrypted" "Armored Encrypted file (PGP)") ("asd" "application/astound" "Autosave file (Word for Windows)") ("asf" "application/vnd.ms-asf video/x-ms-asf video/x-ms-wm" "Windows Media file - Advanced Streaming Format (ASF), NetShow") ("asn" "application/astound" " ") ("asp" "text/html" "Active Server Pages - standard HTML documents interlaced with ActiveX script code ") ("asr" "video/x-ms-asf" "Microsoft NetShow") ("asx" "video/x-ms-asf application/x-mplayer2" "VXtreme (Microsoft streaming AV)") ("asz" "application/astound" " ") ("au" "audio/basic" "8-bit u-law [PCM] / 8000 Hz") ("avi" "video/x-msvideo" "Windows Video file") ("axs" "application/olescript" " ") ("bas" "text/plain" "BASIC program") ("bat" "application/octet-stream" "DOS BAT (Batch) file.") ("bcpio" "application/x-bcpio" "Old Binary CPIO") ("bexpk" "application/pgp-keys" "binary extracted public key (PGP)") ("bin" "application/octet-stream" "Uninterpreted Binary Data") ("bk" "application/vnd.framemaker" "FrameMaker book ") ("bleep" "application/bleeper" " ") ("bmp" "image/x-bmp" "Windows Bitmap (PaintBrush)") ("btf" "image/prs.btf" "NationsBank Check Images (also .btif)") ("c" "text/plain" "C program") ("c++" "text/plain" "C program") ("cab" "application/cab" "Cabinet file Microsoft installation archive. opersyss=win32, mac cpu=x86, ppc, mips, alpha") ("cal" "application/x-msschedplus" "MS schedplus or calendar") ("cat" "application/pdf" "PDF Catalog (Used with Acrobat Reader and Search plug-in)") ("cat" "application/vnd.ms-pki.seccat" "Security Catalog") ("ccs" "text/ccs" "Cluster Configuration System used with the Global File System (GFS) in Red Hat Linux") ("cdda" "audio/aiff" "CD Audio Track") ("cda" "audio/x-cda" "CD Audio Track") ("cdf" "text/plain" "Channel Definition Format - MS push std") ("cdr" "application/x-coreldrw" "Corel Draw (metafile)") ("cer" " application/pkix-cert" "Certificatefile") ("cfm" "wwwserver/wsapi" "Cold Fusion Markup") ("cgi" "magnus-internal/cgi" "Common Gateway Interface") ("cgm" "image/cgm" "Computer Graphics Metafile ") ("chat" "application/x-chat" " ") ("che" "application/x-up-cacheop" " ") ("cht" "audio/x-dspeech" "Chart (Harvard Graphics 2.0 - SoftCraft Presenter)") ("cil" "application/vnd.ms-artgalry" "Clip Gallery Download Packages") ("class" "application/java-vm" "Java") ("cli" "application/vnd.ms-artgalry" " ") ("clp" "application/x-msclip" "Windows Clipboard (metafile)") ("cmx" "image/x-cmx" " ") ("cnc" "application/x-cnc" "CNC general program data") ("cod" "image/cis-cod" "Datafile (Forecast Plus - MS Multiplan - StatPac Gold)") ("coda" "application/x-coda" " ") ("com" "application/octet-stream" "DOS COM Executable (similar to exe, but a direct memory image)") ("cpi" "image/cpi" "ColorLab Processed Image ") ("cpio" "application/x-cpio" "IEEE Std1003.2 (`POSIX') CPIO") ("cpt" "application/mac-compactpro" "Compact Pro Archive") ("crd" "application/x-mscardfile" "MS cardfile") ("crt" "application/x-x509-ca-cert" "Certificatefile") ("csh" "application/x-csh" "CSH Script") ("csm" "application/x-cu-seeme" "Precompiled headers (Borland C++ 4.5)") ("css" "text/css" "Cascading Style Sheets") ("csv" "text/csv" "Comma-Separated Values (Excel, Lotus 123, FoxPro, MS Outlook)") ("ct" "image/" "Iris CT Graphic or Scitex CT Handshake Bitmap ") ("cu" "application/x-cu-seeme" " ") ("cut" "image/x-halo-cut" "Bitmap graphics") ("dat" "application/octet-stream" "Data file. Can be anything, text, graphics, binary, ...") ("dba" "text/x-palm-dba" "DateBook Archive (Palm)") ("dbf" "application/octet-stream" "DataBase File (FoxPro, dBase) ") ("dbm" "wwwserver/wsapi" "ColdFusion IIS Plugin") ("dca" "application/dca-rft" "IBM Doc Content Arch") ("dcr" "application/x-director" "Macromedia Director (Shockwave)") ("deb" "application/octet-stream" "Binary for debian UNIX") ("der" "application/x-x509-ca-cert" "Certificatefile") ("dir" "application/x-director" "Macromedia Director (Shockwave)") ("dll" "application/x-msdownload" "Dynamically Linked Library (DOS) pe-portable executable opersys=win32, mac cpu=x86, ppc, mips, alpha") ("dms" "application/octet-stream" "Compressed Amiga file archive created by DISKMASHER") ("doc" "application/msword" "MS Word") ("dot" "application/msword" "MS Word (Template)") ("dsf" "image/x-mgx-dsf" "Micrografx Designer 6 (metafile)") ("dst" "application/tajima" "PC-RDist Distribution file ") ("dtd" "text/xml" "SGML Document (Type) Definition file") ("dus" "audio/x-dspeech" "Readiris font dictionary") ("dvi" "application/x-dvi" "TeX DVI (Device Independent)") ("dwc" "application/dwc" "compressed archive") ("dwf" "drawing/x-dwf" "Autodesk WHIP! Drawing Web file") ("dwg" "application/x-acad" "AutoCAD Drawing") ("dxf" "application/vnd.dxf" "Drawing eXchange Format, Data Exchange File, AutoCAD (vector)") ("dxr" "application/x-director" "Macromedia Director (Shockwave)") ("ebk" "application/x-expandedbook" " ") ("emf" "image/x-emf" "Enhanced metafile created in Microsoft Windows and Visio 2002 applications") ("eml" "message/rfc822" "MS Internet Mail Message (Outlook Express and others)") ("enc" "application/pre-encrypted" "Pre-encrypted Data (also Sniffer trace)") ("eps" "application/postscript" "Encapsulated PostScript (raster)") ("erf" "application/x-hsp-erf" " ") ("es" "audio/echospeech" " ") ("etf" "image/x-etf" "Enriched Text file") ("etx" "text/x-setext" "Structure Enchanced Text") ("evy" "application/x-envoy" "Document (WordPerfect Envoy)") ("exe" "application/x-pe-" "pe-portable executable opersys=win32, mac cpu=x86, ppc, mips, alpha") ("fdf" "application/vnd.fdf" "acrobat reader") ("fh4" "image/x-freehand" "Vector graphics (Aldus FreeHand 4.x)") ("fh5" "image/x-freehand" "Freehand 5") [378 lines skipped] From jidzikowski at common-lisp.net Sun Nov 4 00:01:50 2007 From: jidzikowski at common-lisp.net (jidzikowski) Date: Sat, 3 Nov 2007 19:01:50 -0500 (EST) Subject: [cl-smtp-cvs] CVS cl-smtp Message-ID: <20071104000150.155AC59094@common-lisp.net> Update of /project/cl-smtp/cvsroot/cl-smtp In directory clnet:/tmp/cvs-serv13911 Modified Files: cl-smtp.lisp Log Message: don't set multipart header when send pure text message --- /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.lisp 2007/11/03 23:53:29 1.8 +++ /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.lisp 2007/11/04 00:01:49 1.9 @@ -90,9 +90,10 @@ (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) - (send-multipart-headers - sock :attachment-boundary (when attachments boundary) - :html-boundary html-boundary) + (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 From jidzikowski at common-lisp.net Mon Nov 5 19:58:24 2007 From: jidzikowski at common-lisp.net (jidzikowski) Date: Mon, 5 Nov 2007 14:58:24 -0500 (EST) Subject: [cl-smtp-cvs] CVS cl-smtp Message-ID: <20071105195824.47D3655395@common-lisp.net> Update of /project/cl-smtp/cvsroot/cl-smtp In directory clnet:/tmp/cvs-serv8540 Modified Files: attachments.lisp cl-smtp.asd cl-smtp.lisp Added Files: index.html style.css Log Message: - for allegro cl don't use cl-base64 - use write-blank-line in send-smtp --- /project/cl-smtp/cvsroot/cl-smtp/attachments.lisp 2007/11/03 23:53:29 1.5 +++ /project/cl-smtp/cvsroot/cl-smtp/attachments.lisp 2007/11/05 19:58:24 1.6 @@ -133,9 +133,9 @@ #-allegro (cl-base64:usb8-array-to-base64-stream buffer sock :columns wrap-at-column) - ;; otherwise process file in chunks. + ;; otherwise process file in chunks. ;; The extra encoded-string, - ;; and its subseq functions are brute force methods + ;; and its subseq functions are brute force methods ;; to properly handle the wrap-at-column feature ;; between buffers. ;; Not the most efficient way, @@ -148,7 +148,7 @@ buffer)) (encoded-string #+allegro - (cl-base64:usb8-array-to-base64-string + (excl:usb8-array-to-base64-string trimmed-buffer) #-allegro (cl-base64:usb8-array-to-base64-string --- /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.asd 2007/11/03 23:53:29 1.10 +++ /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.asd 2007/11/05 19:58:24 1.11 @@ -29,10 +29,10 @@ (print ,str))) (asdf:defsystem :cl-smtp - :version "20071104.1" + :version "20071105.1" :perform (load-op :after (op webpage) (pushnew :cl-smtp cl:*features*)) :depends-on (:usocket #-allegro :cl-base64) :components ((:file "cl-smtp" :depends-on ("attachments")) - (:file "attachments") - (:file "mime-types"))) + (:file "attachments") + (:file "mime-types"))) --- /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.lisp 2007/11/04 00:01:49 1.9 +++ /project/cl-smtp/cvsroot/cl-smtp/cl-smtp.lisp 2007/11/05 19:58:24 1.10 @@ -89,7 +89,10 @@ (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) + (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) @@ -139,8 +142,7 @@ (send-attachment sock attachment boundary buffer-size)) (send-end-marker sock boundary)) (write-char #\. sock) - (write-char #\Return sock) - (write-char #\NewLine sock) + (write-blank-line sock) (force-output sock) (multiple-value-bind (code msgstr) (read-from-smtp sock) --- /project/cl-smtp/cvsroot/cl-smtp/index.html 2007/11/05 19:58:24 NONE +++ /project/cl-smtp/cvsroot/cl-smtp/index.html 2007/11/05 19:58:24 1.1 CL-SMTP

CL-SMTP at common-lisp.net

Introduction

CL-SMTP is a simple lisp Networking Library that provides SMTP client protocol, supported LOGIN and PLAIN authentication methods.

New Version [20071018.1] Reverted the non allegro base64 functionality in attachment.lisp, now it is used cl-base64 again. Thanks Attila Lendvai for the bug report.

Download

ASDF package cl-smtp.tar.gz

CVS

You can browse our CVS repository or download the current development tree via anonymous cvs, as described here

Portability

CL-SMTP requires USOCKET and CL-BASE64 (CL-BASE64 isn't a requirement on ACL)

It works in all implementations supported by its dependencies (Allegro, SBCL, CMU CL, OpenMCL, Lispworks, CLISP and ECL).

Test results for Linux/x86/amd64:

Lisp Implementation Status Comments
Allegro working
CLISP working
CMU CL working
Lispworks working
SBCL working
OpemMCL working

Mailing Lists

Valid XHTML 1.0 Strict
--- /project/cl-smtp/cvsroot/cl-smtp/style.css 2007/11/05 19:58:24 NONE +++ /project/cl-smtp/cvsroot/cl-smtp/style.css 2007/11/05 19:58:24 1.1 .header { font-size: medium; background-color:#336699; color:#ffffff; border-style:solid; border-width: 5px; border-color:#002244; padding: 1mm 1mm 1mm 5mm; } .footer { font-size: small; font-style: italic; text-align: right; background-color:#336699; color:#ffffff; border-style:solid; border-width: 2px; border-color:#002244; padding: 1mm 1mm 1mm 1mm; } .footer a:link { font-weight:bold; color:#ffffff; text-decoration:underline; } .footer a:visited { font-weight:bold; color:#ffffff; text-decoration:underline; } .footer a:hover { font-weight:bold; color:#002244; text-decoration:underline; } .check {font-size: x-small; text-align:right;} .check a:link { font-weight:bold; color:#a0a0ff; text-decoration:underline; } .check a:visited { font-weight:bold; color:#a0a0ff; text-decoration:underline; } .check a:hover { font-weight:bold; color:#000000; text-decoration:underline; } th { background-color: #8b0000; color: white; text-align: left; } .working { background-color: #90ee90; } .broken { background-color: #c5c5c5; } From jidzikowski at common-lisp.net Sun Nov 11 23:10:21 2007 From: jidzikowski at common-lisp.net (jidzikowski) Date: Sun, 11 Nov 2007 18:10:21 -0500 (EST) Subject: [cl-smtp-cvs] CVS cl-smtp Message-ID: <20071111231021.E788161051@common-lisp.net> 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]