3 ;;; This file is part of CL-SMTP, the Lisp SMTP Client
5 ;;; Copyright (C) 2004/2005 Jan Idzikowski
7 ;;; This library is free software; you can redistribute it and/or
8 ;;; modify it under the terms of the Lisp Lesser General Public License
9 ;;; (http://opensource.franz.com/preamble.html), known as the LLGPL.
11 ;;; This library is distributed in the hope that it will be useful,
12 ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 ;;; Lisp Lesser GNU General Public License for more details.
16 ;;; File: cl-smtp.lisp
17 ;;; Description: main smtp client logic
19 (in-package :net.smtp-client
)
21 (defparameter *debug
* nil
)
22 (defparameter *x-mailer
*
24 (lisp-implementation-type)
25 (lisp-implementation-version)))
27 (defun check-arg (arg name
)
35 (error "the \"~A\" argument is not a string or cons" name
))))
38 "replace \r\n.\r\n with \r\n..\r\n"
39 (let ((dotstr (format nil
"~C.~C" #\NewLine
#\NewLine
))
40 (maskdotsr (format nil
"~C..~C" #\NewLine
#\NewLine
))
42 (labels ((mask (tempstr)
43 (let ((n (search dotstr tempstr
)))
46 (setf resultstr
(concatenate 'string resultstr
49 (mask (subseq tempstr
(+ n
3))))
51 (setf resultstr
(concatenate 'string resultstr
56 (defun string-to-base64-string (str)
57 (cl-base64:string-to-base64-string str
))
59 (defun send-email (host from to subject message
60 &key
(port 25) cc bcc reply-to extra-headers
61 display-name authentication
62 attachments
(buffer-size 256))
63 (send-smtp host from
(check-arg to
"to") subject
(mask-dot message
)
64 :port port
:cc
(check-arg cc
"cc") :bcc
(check-arg bcc
"bcc")
66 :extra-headers extra-headers
67 :display-name display-name
68 :authentication authentication
69 :attachments
(check-arg attachments
"attachments")
70 :buffer-size
(if (numberp buffer-size
)
74 (defun make-smtp-socket (host port
)
75 (make-socket :address-family
:internet
:type
:stream
:connect
:active
76 :remote-host host
:remote-port port
77 :external-format
'(:iso-8859-1
:line-terminator
:dos
)))
79 (defun compute-rcpt-command (sock adresses
)
81 (write-to-smtp sock
(format nil
"RCPT TO: <~A>" to
))
82 (multiple-value-bind (code msgstr
)
85 (error "in RCPT TO command: ~A" msgstr
)))))
87 (defun write-to-smtp (sock command
)
88 (write-string command sock
)
92 (defun read-from-smtp (sock)
93 (let* ((line (read-line sock
))
94 (response-code (parse-integer line
:start
0 :junk-allowed t
)))
95 (if (= (char-code (elt line
3)) (char-code #\-
))
97 (values response-code line
))))
99 (defun send-smtp (host from to subject message
100 &key
(port 25) cc bcc reply-to extra-headers
101 display-name authentication attachments buffer-size
)
102 (let ((boundary (make-random-boundary)))
103 (with-open-stream (sock (make-smtp-socket host port
))
104 (open-smtp-connection sock
:authentication authentication
)
105 (write-to-smtp sock
(format nil
"MAIL FROM: ~@[~A ~]<~A>" display-name from
))
106 (multiple-value-bind (code msgstr
)
107 (read-from-smtp sock
)
109 (error "in MAIL FROM command: ~A" msgstr
)))
110 (compute-rcpt-command sock to
)
111 (compute-rcpt-command sock cc
)
112 (compute-rcpt-command sock bcc
)
113 (write-to-smtp sock
"DATA")
114 (multiple-value-bind (code msgstr
)
115 (read-from-smtp sock
)
117 (error "in DATA command: ~A" msgstr
)))
118 (write-to-smtp sock
(format nil
"Date: ~A" (get-email-date-string)))
119 (write-to-smtp sock
(format nil
"From: ~@[~A <~]~A~@[>~]"
120 display-name from display-name
))
121 (write-to-smtp sock
(format nil
"To: ~{ ~a~^,~}" to
))
123 (write-to-smtp sock
(format nil
"Cc: ~{ ~a~^,~}" cc
)))
124 (write-to-smtp sock
(format nil
"Subject: ~A" subject
))
125 (write-to-smtp sock
(format nil
"X-Mailer: cl-smtp ~A"
128 (write-to-smtp sock
(format nil
"Reply-To: ~A" reply-to
)))
129 (when (and extra-headers
130 (listp extra-headers
))
131 (dolist (l extra-headers
)
133 (format nil
"~A: ~{~a~^,~}" (car l
) (rest l
)))))
134 (write-to-smtp sock
"Mime-Version: 1.0")
136 (generate-multipart-header sock boundary
))
139 (setq message
(wrap-message-with-multipart-dividers
141 (write-to-smtp sock message
)
143 (dolist (attachment attachments
)
144 (send-attachment sock attachment boundary buffer-size
))
145 (send-attachments-end-marker sock boundary
))
146 (write-char #\. sock
)
149 (multiple-value-bind (code msgstr
)
150 (read-from-smtp sock
)
152 (error "Message send failed: ~A" msgstr
)))
153 (write-to-smtp sock
"QUIT")
154 (multiple-value-bind (code msgstr
)
155 (read-from-smtp sock
)
157 (error "in QUIT command:: ~A" msgstr
))))))
159 (defun open-smtp-connection (sock &key authentication
)
160 (multiple-value-bind (code msgstr
)
161 (read-from-smtp sock
)
163 (error "wrong response from smtp server: ~A" msgstr
)))
166 (write-to-smtp sock
(format nil
"EHLO ~A" (et:get-host-name
)))
167 (multiple-value-bind (code msgstr
)
168 (read-from-smtp sock
)
170 (error "wrong response from smtp server: ~A" msgstr
)))
172 ((eq (car authentication
) :plain
)
173 (write-to-smtp sock
(format nil
"AUTH PLAIN ~A"
174 (string-to-base64-string
175 (format nil
"~A~C~A~C~A" (cadr authentication
)
176 #\null
(cadr authentication
) #\null
177 (caddr authentication
)))))
178 (multiple-value-bind (code msgstr
)
179 (read-from-smtp sock
)
181 (error "plain authentication failed: ~A" msgstr
))))
182 ((eq (car authentication
) :login
)
183 (write-to-smtp sock
"AUTH LOGIN")
184 (multiple-value-bind (code msgstr
)
185 (read-from-smtp sock
)
187 (error "login authentication failed: ~A" msgstr
)))
188 (write-to-smtp sock
(string-to-base64-string (cadr authentication
)))
189 (multiple-value-bind (code msgstr
)
190 (read-from-smtp sock
)
192 (error "login authentication send username failed: ~A" msgstr
)))
193 (write-to-smtp sock
(string-to-base64-string (caddr authentication
)))
194 (multiple-value-bind (code msgstr
)
195 (read-from-smtp sock
)
197 (error "login authentication send password failed: ~A" msgstr
))))
199 (error "authentication ~A is not supported in cl-smtp"
200 (car authentication
)))))
202 (write-to-smtp sock
(format nil
"HELO ~A" (et:get-host-name
)))
203 (multiple-value-bind (code msgstr
)
204 (read-from-smtp sock
)
206 (error "wrong response from smtp server: ~A" msgstr
))))))
208 (defun get-email-date-string ()
209 (multiple-value-bind (sec min h d m y wd
) (get-decoded-time)
210 (let* ((month (elt '("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec") (- m
1)))
211 (weekday (elt '("Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun") wd
))
212 (timezone (get-timezone-from-integer
213 (- (encode-universal-time sec min h d m y
0)
214 (get-universal-time)))))
215 (format nil
"~A, ~2,'0d ~A ~d ~2,'0d:~2,'0d:~2,'0d ~D"
216 weekday d month y h min sec timezone
))))
218 (defun get-timezone-from-integer (x)
224 (format nil
"+~2,'0d00" hour
))
226 (format nil
"-~2,'0d00" (* -
1 hour
))))
227 (multiple-value-bind (h m
) (truncate min
60)
230 (format nil
"+~2,'0d~2,'0d" h
(truncate m
)))
232 (format nil
"-~2,'0d~2,'0d" (* -
1 h
) (* -
1 (truncate m
)))))))))