| 1 |
;;; -*- mode: Lisp -*- |
|---|
| 2 |
|
|---|
| 3 |
;;; This file is part of CL-SMTP, the Lisp SMTP Client |
|---|
| 4 |
|
|---|
| 5 |
;;; Copyright (C) 2004/2005/2006/2007 Jan Idzikowski |
|---|
| 6 |
|
|---|
| 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. |
|---|
| 10 |
|
|---|
| 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. |
|---|
| 15 |
|
|---|
| 16 |
;;; File: cl-smtp.lisp |
|---|
| 17 |
;;; Description: main smtp client logic |
|---|
| 18 |
|
|---|
| 19 |
(in-package :cl-smtp) |
|---|
| 20 |
|
|---|
| 21 |
(defparameter *content-type* "text/plain; charset=ISO-8859-1") |
|---|
| 22 |
|
|---|
| 23 |
(defparameter *x-mailer* (format nil "(~A ~A)" |
|---|
| 24 |
(lisp-implementation-type) |
|---|
| 25 |
(lisp-implementation-version))) |
|---|
| 26 |
|
|---|
| 27 |
(defun check-arg (arg name) |
|---|
| 28 |
(cond |
|---|
| 29 |
((or (stringp arg) |
|---|
| 30 |
(pathnamep arg)) |
|---|
| 31 |
(list arg)) |
|---|
| 32 |
((listp arg) |
|---|
| 33 |
arg) |
|---|
| 34 |
(t |
|---|
| 35 |
(error "the \"~A\" argument is not a string or cons" name)))) |
|---|
| 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 |
|
|---|
| 43 |
(defun mask-dot (str) |
|---|
| 44 |
"Replace all occurences of \r\n.\r\n in STR with \r\n..\r\n" |
|---|
| 45 |
(let ((resultstr "")) |
|---|
| 46 |
(labels ((mask (tempstr) |
|---|
| 47 |
(let ((n (search *line-with-one-dot* tempstr))) |
|---|
| 48 |
(cond |
|---|
| 49 |
(n |
|---|
| 50 |
(setf resultstr (concatenate 'string resultstr |
|---|
| 51 |
(subseq tempstr 0 n) |
|---|
| 52 |
*line-with-two-dots*)) |
|---|
| 53 |
(mask (subseq tempstr (+ n #.(length *line-with-one-dot*))))) |
|---|
| 54 |
(t |
|---|
| 55 |
(setf resultstr (concatenate 'string resultstr |
|---|
| 56 |
tempstr))))))) |
|---|
| 57 |
(mask str)) |
|---|
| 58 |
resultstr)) |
|---|
| 59 |
|
|---|
| 60 |
(defun string-to-base64-string (str) |
|---|
| 61 |
(declare (ignorable str)) |
|---|
| 62 |
#+allegro (excl:string-to-base64-string str) |
|---|
| 63 |
#-allegro (cl-base64:string-to-base64-string str)) |
|---|
| 64 |
|
|---|
| 65 |
(define-condition smtp-error (error) |
|---|
| 66 |
()) |
|---|
| 67 |
|
|---|
| 68 |
(define-condition smtp-protocol-error (smtp-error) |
|---|
| 69 |
((command :initarg :command :reader command) |
|---|
| 70 |
(expected-response-code :initarg :expected-response-code :reader expected-response-code) |
|---|
| 71 |
(response-code :initarg :response-code :reader response-code) |
|---|
| 72 |
(response-message :initarg :response-message :reader response-message)) |
|---|
| 73 |
(:report (lambda (condition stream) |
|---|
| 74 |
(print-unreadable-object (condition stream :type t) |
|---|
| 75 |
(format stream "a command failed:~%command: ~S expected: ~A response: ~A" |
|---|
| 76 |
(command condition) |
|---|
| 77 |
(expected-response-code condition) |
|---|
| 78 |
(response-message condition)))))) |
|---|
| 79 |
|
|---|
| 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-protocol-error) |
|---|
| 91 |
condition-arguments) |
|---|
| 92 |
(when command |
|---|
| 93 |
(write-to-smtp stream command)) |
|---|
| 94 |
(multiple-value-bind (code msgstr lines) |
|---|
| 95 |
(read-from-smtp stream) |
|---|
| 96 |
(when (/= code expected-response-code) |
|---|
| 97 |
(apply #'error |
|---|
| 98 |
condition-class |
|---|
| 99 |
(append condition-arguments |
|---|
| 100 |
(list :command command |
|---|
| 101 |
:expected-response-code expected-response-code |
|---|
| 102 |
:response-code code |
|---|
| 103 |
:response-message msgstr)))) |
|---|
| 104 |
lines)) |
|---|
| 105 |
|
|---|
| 106 |
(defun do-with-smtp-mail (host from to thunk &key port authentication ssl local-hostname) |
|---|
| 107 |
(usocket:with-client-socket (socket stream host port) |
|---|
| 108 |
(let ((stream (smtp-handshake stream |
|---|
| 109 |
:authentication authentication |
|---|
| 110 |
:ssl ssl |
|---|
| 111 |
:local-hostname local-hostname))) |
|---|
| 112 |
(initiate-smtp-mail stream from to) |
|---|
| 113 |
(funcall thunk (make-instance 'smtp-output-stream :encapsulated-stream stream)) |
|---|
| 114 |
(finish-smtp-mail stream)))) |
|---|
| 115 |
|
|---|
| 116 |
(defmacro with-smtp-mail ((stream-var host from to &key ssl (port (if (eq :tls ssl) 465 25)) authentication local-hostname) |
|---|
| 117 |
&body body) |
|---|
| 118 |
"Encapsulate a SMTP MAIl conversation. A connection to the SMTP |
|---|
| 119 |
server on HOST and PORT is established and a MAIL command is |
|---|
| 120 |
initiated with FROM being the mail sender and TO being the list of |
|---|
| 121 |
recipients. BODY is evaluated with STREAM-VAR being the stream |
|---|
| 122 |
connected to the remote SMTP server. BODY is expected to write the |
|---|
| 123 |
RFC2821 message (headers and body) to STREAM-VAR." |
|---|
| 124 |
`(do-with-smtp-mail ,host ,from ,to |
|---|
| 125 |
(lambda (,stream-var) ,@body) |
|---|
| 126 |
:port ,port |
|---|
| 127 |
:authentication ,authentication |
|---|
| 128 |
:ssl ,ssl |
|---|
| 129 |
:local-hostname ,local-hostname)) |
|---|
| 130 |
|
|---|
| 131 |
(defun send-email (host from to subject message |
|---|
| 132 |
&key ssl (port (if (eq :tls ssl) 465 25)) cc bcc reply-to extra-headers |
|---|
| 133 |
html-message display-name authentication |
|---|
| 134 |
attachments (buffer-size 256)) |
|---|
| 135 |
(send-smtp host from (check-arg to "to") subject (mask-dot message) |
|---|
| 136 |
:port port :cc (check-arg cc "cc") :bcc (check-arg bcc "bcc") |
|---|
| 137 |
:reply-to reply-to |
|---|
| 138 |
:extra-headers extra-headers |
|---|
| 139 |
:html-message html-message |
|---|
| 140 |
:display-name display-name |
|---|
| 141 |
:authentication authentication |
|---|
| 142 |
:attachments (check-arg attachments "attachments") |
|---|
| 143 |
:buffer-size (if (numberp buffer-size) |
|---|
| 144 |
buffer-size |
|---|
| 145 |
256) |
|---|
| 146 |
:ssl ssl)) |
|---|
| 147 |
|
|---|
| 148 |
(defun send-smtp (host from to subject message |
|---|
| 149 |
&key ssl (port (if (eq :tls ssl) 465 25)) cc bcc |
|---|
| 150 |
reply-to extra-headers html-message display-name |
|---|
| 151 |
authentication attachments buffer-size |
|---|
| 152 |
(local-hostname (usocket::get-host-name))) |
|---|
| 153 |
(with-smtp-mail (stream host from (append to cc bcc) |
|---|
| 154 |
:port port |
|---|
| 155 |
:authentication authentication |
|---|
| 156 |
:ssl ssl |
|---|
| 157 |
:local-hostname local-hostname) |
|---|
| 158 |
(let* ((boundary (make-random-boundary)) |
|---|
| 159 |
(html-boundary (if (and attachments html-message) |
|---|
| 160 |
(make-random-boundary) |
|---|
| 161 |
boundary))) |
|---|
| 162 |
(send-mail-headers stream |
|---|
| 163 |
:from from |
|---|
| 164 |
:to to |
|---|
| 165 |
:cc cc |
|---|
| 166 |
:reply-to reply-to |
|---|
| 167 |
:display-name display-name |
|---|
| 168 |
:extra-headers extra-headers :subject subject) |
|---|
| 169 |
(when (or attachments html-message) |
|---|
| 170 |
(send-multipart-headers stream |
|---|
| 171 |
:attachment-boundary (when attachments boundary) |
|---|
| 172 |
:html-boundary html-boundary)) |
|---|
| 173 |
;;----------- Send the body Message --------------------------- |
|---|
| 174 |
;;--- Send the proper headers depending on plain-text, |
|---|
| 175 |
;;--- multi-part or html email |
|---|
| 176 |
(cond ((and attachments html-message) |
|---|
| 177 |
;; if both present, start attachment section, |
|---|
| 178 |
;; then define alternative section, |
|---|
| 179 |
;; then write alternative header |
|---|
| 180 |
(progn |
|---|
| 181 |
(generate-message-header |
|---|
| 182 |
stream :boundary boundary :include-blank-line? nil) |
|---|
| 183 |
(generate-multipart-header stream html-boundary |
|---|
| 184 |
:multipart-type "alternative") |
|---|
| 185 |
(write-blank-line stream) |
|---|
| 186 |
(generate-message-header |
|---|
| 187 |
stream :boundary html-boundary :content-type *content-type* |
|---|
| 188 |
:content-disposition "inline" :include-blank-line? nil))) |
|---|
| 189 |
(attachments |
|---|
| 190 |
(generate-message-header |
|---|
| 191 |
stream :boundary boundary |
|---|
| 192 |
:content-type *content-type* :content-disposition "inline" |
|---|
| 193 |
:include-blank-line? nil)) |
|---|
| 194 |
(html-message |
|---|
| 195 |
(generate-message-header |
|---|
| 196 |
stream :boundary html-boundary :content-type *content-type* |
|---|
| 197 |
:content-disposition "inline")) |
|---|
| 198 |
(t |
|---|
| 199 |
(generate-message-header stream :content-type *content-type* |
|---|
| 200 |
:include-blank-line? nil))) |
|---|
| 201 |
(write-blank-line stream) |
|---|
| 202 |
(write-to-smtp stream message) |
|---|
| 203 |
(write-blank-line stream) |
|---|
| 204 |
;;---------- Send Html text if needed ------------------------- |
|---|
| 205 |
(when html-message |
|---|
| 206 |
(generate-message-header |
|---|
| 207 |
stream :boundary html-boundary |
|---|
| 208 |
:content-type "text/html; charset=ISO-8859-1" |
|---|
| 209 |
:content-disposition "inline") |
|---|
| 210 |
(write-to-smtp stream html-message) |
|---|
| 211 |
(send-end-marker stream html-boundary)) |
|---|
| 212 |
;;---------- Send Attachments ----------------------------------- |
|---|
| 213 |
(when attachments |
|---|
| 214 |
(dolist (attachment attachments) |
|---|
| 215 |
(send-attachment stream attachment boundary buffer-size)) |
|---|
| 216 |
(send-end-marker stream boundary))))) |
|---|
| 217 |
|
|---|
| 218 |
(define-condition no-supported-authentication-method (smtp-error) |
|---|
| 219 |
((features :initarg :features :reader features)) |
|---|
| 220 |
(:report (lambda (condition stream) |
|---|
| 221 |
(print-unreadable-object (condition stream :type t) |
|---|
| 222 |
(format stream "SMTP authentication has been requested, but the SMTP server did not advertise any ~ |
|---|
| 223 |
supported authentication scheme. Features announced: ~{~S~^, ~}" |
|---|
| 224 |
(features condition)))))) |
|---|
| 225 |
|
|---|
| 226 |
(defun smtp-authenticate (stream authentication features) |
|---|
| 227 |
"Authenticate to the SMTP server connected on STREAM. |
|---|
| 228 |
AUTHENTICATION is a list of two or three elements. If the first |
|---|
| 229 |
element is a keyword, it specifies the desired authentication |
|---|
| 230 |
method (:PLAIN or :LOGIN), which is currently ignored. The actual |
|---|
| 231 |
method used is determined by looking at the advertised features of |
|---|
| 232 |
the SMTP server. The (other) two elements of the AUTHENTICATION |
|---|
| 233 |
list are the login username and password. FEATURES is the list of |
|---|
| 234 |
features announced by the SMTP server. |
|---|
| 235 |
|
|---|
| 236 |
If the server does not announce any compatible authentication scheme, |
|---|
| 237 |
the NO-SUPPORTED-AUTHENTICATION-METHOD error is signalled." |
|---|
| 238 |
(when (keywordp (car authentication)) |
|---|
| 239 |
(pop authentication)) |
|---|
| 240 |
(let ((server-authentication (loop for i in features |
|---|
| 241 |
for e = (search "AUTH " i :test #'equal) |
|---|
| 242 |
when (and e (= e 0)) |
|---|
| 243 |
return i))) |
|---|
| 244 |
(destructuring-bind (username password) authentication |
|---|
| 245 |
(cond |
|---|
| 246 |
((search " PLAIN" server-authentication :test #'equal) |
|---|
| 247 |
(smtp-command stream (format nil "AUTH PLAIN ~A" |
|---|
| 248 |
(string-to-base64-string |
|---|
| 249 |
(format nil "~A~C~A~C~A" |
|---|
| 250 |
username |
|---|
| 251 |
#\null username |
|---|
| 252 |
#\null password))) |
|---|
| 253 |
235)) |
|---|
| 254 |
((search " LOGIN" server-authentication :test #'equal) |
|---|
| 255 |
(smtp-command stream "AUTH LOGIN" |
|---|
| 256 |
334) |
|---|
| 257 |
(smtp-command stream (string-to-base64-string username) |
|---|
| 258 |
334) |
|---|
| 259 |
(smtp-command stream (string-to-base64-string password) |
|---|
| 260 |
235)) |
|---|
| 261 |
(t |
|---|
| 262 |
(error 'no-supported-authentication-method :features features)))))) |
|---|
| 263 |
|
|---|
| 264 |
(defun smtp-handshake (stream &key authentication ssl local-hostname) |
|---|
| 265 |
"Perform the initial SMTP handshake on STREAM. Returns the stream |
|---|
| 266 |
to use further down in the conversation, which may be different from |
|---|
| 267 |
the original stream if we switched to SSL." |
|---|
| 268 |
|
|---|
| 269 |
;; Read the initial greeting from the SMTP server |
|---|
| 270 |
(smtp-command stream nil |
|---|
| 271 |
220) |
|---|
| 272 |
|
|---|
| 273 |
(unless (or ssl authentication) |
|---|
| 274 |
;; Unless we want ESMTP features, perform classic SMTP handshake and return |
|---|
| 275 |
(smtp-command stream (format nil "HELO ~A" |
|---|
| 276 |
(usocket::get-host-name)) |
|---|
| 277 |
250) |
|---|
| 278 |
(return-from smtp-handshake stream)) |
|---|
| 279 |
|
|---|
| 280 |
;; When SSL or authentication requested, perform ESMTP EHLO |
|---|
| 281 |
(let (features) |
|---|
| 282 |
(labels |
|---|
| 283 |
((do-ehlo () |
|---|
| 284 |
(setf features (rest (smtp-command stream (format nil "EHLO ~A" local-hostname) |
|---|
| 285 |
250)))) |
|---|
| 286 |
(convert-connection-to-ssl () |
|---|
| 287 |
(setf stream |
|---|
| 288 |
#+allegro (socket:make-ssl-client-stream stream) |
|---|
| 289 |
#-allegro |
|---|
| 290 |
(let ((s stream)) |
|---|
| 291 |
(cl+ssl:make-ssl-client-stream |
|---|
| 292 |
(cl+ssl:stream-fd stream) |
|---|
| 293 |
:close-callback (lambda () (close s))))) |
|---|
| 294 |
#-allegro |
|---|
| 295 |
(setf stream (flexi-streams:make-flexi-stream |
|---|
| 296 |
stream |
|---|
| 297 |
:external-format |
|---|
| 298 |
(flexi-streams:make-external-format |
|---|
| 299 |
:latin-1 :eol-style :lf))))) |
|---|
| 300 |
(ecase ssl |
|---|
| 301 |
((or t :starttls) |
|---|
| 302 |
(do-ehlo) |
|---|
| 303 |
(unless (find "STARTTLS" features :test #'equal) |
|---|
| 304 |
(error "this server does not supports TLS")) |
|---|
| 305 |
(print-debug "this server supports TLS") |
|---|
| 306 |
(smtp-command stream "STARTTLS" |
|---|
| 307 |
220) |
|---|
| 308 |
(convert-connection-to-ssl) |
|---|
| 309 |
;; After STARTTLS, the connection is "like new". Re-do the |
|---|
| 310 |
;; EHLO command to switch the server to ESMTP mode and read |
|---|
| 311 |
;; the list of announced features again. |
|---|
| 312 |
(do-ehlo)) |
|---|
| 313 |
(:tls |
|---|
| 314 |
;; Plain SSL connection |
|---|
| 315 |
(convert-connection-to-ssl) |
|---|
| 316 |
(do-ehlo)) |
|---|
| 317 |
((nil) |
|---|
| 318 |
(do-ehlo)))) |
|---|
| 319 |
(when authentication |
|---|
| 320 |
(smtp-authenticate stream authentication features))) |
|---|
| 321 |
stream) |
|---|
| 322 |
|
|---|
| 323 |
(defun initiate-smtp-mail (stream from to) |
|---|
| 324 |
"Initiate an SMTP MAIL command, sending a MAIL FROM command for the |
|---|
| 325 |
email address in FROM and RCPT commands for all receipients in TO, |
|---|
| 326 |
which is expected to be a list. |
|---|
| 327 |
|
|---|
| 328 |
If any of the TO addresses is not accepted, a RCPT-FAILED condition |
|---|
| 329 |
is signalled. This condition may be handled by the caller in order |
|---|
| 330 |
to send the email anyway." |
|---|
| 331 |
(smtp-command stream |
|---|
| 332 |
(format nil "MAIL FROM:<~A>" from) |
|---|
| 333 |
250) |
|---|
| 334 |
(dolist (address to) |
|---|
| 335 |
(restart-case |
|---|
| 336 |
(smtp-command stream (format nil "RCPT TO:<~A>" address) |
|---|
| 337 |
250 |
|---|
| 338 |
:condition-class 'rcpt-failed |
|---|
| 339 |
:condition-arguments (list :recipient address)) |
|---|
| 340 |
(ignore-recipient ()))) |
|---|
| 341 |
(smtp-command stream "DATA" |
|---|
| 342 |
354)) |
|---|
| 343 |
|
|---|
| 344 |
(defun finish-smtp-mail (stream) |
|---|
| 345 |
"Finish sending an email to the SMTP server connected to on STREAM. |
|---|
| 346 |
The server is expected to be inside of the DATA SMTP command. The |
|---|
| 347 |
connection is then terminated by sending a QUIT command." |
|---|
| 348 |
(fresh-line stream) |
|---|
| 349 |
(smtp-command stream "." 250) |
|---|
| 350 |
(smtp-command stream "QUIT" 221)) |
|---|
| 351 |
|
|---|
| 352 |
(defun send-mail-headers (stream |
|---|
| 353 |
&key from to cc reply-to |
|---|
| 354 |
extra-headers display-name subject) |
|---|
| 355 |
"Send email headers according to the given arguments to the SMTP |
|---|
| 356 |
server connected to on STREAM. The server is expected to have |
|---|
| 357 |
previously accepted the DATA SMTP command." |
|---|
| 358 |
(write-to-smtp stream (format nil "Date: ~A" (get-email-date-string))) |
|---|
| 359 |
(write-to-smtp stream (format nil "From: ~@[~A <~]~A~@[>~]" |
|---|
| 360 |
display-name from display-name)) |
|---|
| 361 |
(write-to-smtp stream (format nil "To: ~{ ~a~^,~}" to)) |
|---|
| 362 |
(when cc |
|---|
| 363 |
(write-to-smtp stream (format nil "Cc: ~{ ~a~^,~}" cc))) |
|---|
| 364 |
(write-to-smtp stream (format nil "Subject: ~A" subject)) |
|---|
| 365 |
(write-to-smtp stream (format nil "X-Mailer: cl-smtp ~A" |
|---|
| 366 |
*x-mailer*)) |
|---|
| 367 |
(when reply-to |
|---|
| 368 |
(write-to-smtp stream (format nil "Reply-To: ~A" reply-to))) |
|---|
| 369 |
(when (and extra-headers |
|---|
| 370 |
(listp extra-headers)) |
|---|
| 371 |
(dolist (l extra-headers) |
|---|
| 372 |
(write-to-smtp stream |
|---|
| 373 |
(format nil "~A: ~{~a~^,~}" (car l) (rest l))))) |
|---|
| 374 |
(write-to-smtp stream "Mime-Version: 1.0")) |
|---|
| 375 |
|
|---|
| 376 |
(defun send-multipart-headers (stream &key attachment-boundary html-boundary) |
|---|
| 377 |
(cond (attachment-boundary |
|---|
| 378 |
(generate-multipart-header stream attachment-boundary |
|---|
| 379 |
:multipart-type "mixed")) |
|---|
| 380 |
(html-boundary (generate-multipart-header |
|---|
| 381 |
stream html-boundary |
|---|
| 382 |
:multipart-type "alternative")) |
|---|
| 383 |
(t nil))) |
|---|
| 384 |
|
|---|
| 385 |
(defun write-to-smtp (stream command) |
|---|
| 386 |
(print-debug (format nil "to server: ~A" command)) |
|---|
| 387 |
(write-string command stream) |
|---|
| 388 |
(write-char #\Return stream) |
|---|
| 389 |
(write-char #\NewLine stream) |
|---|
| 390 |
(force-output stream)) |
|---|
| 391 |
|
|---|
| 392 |
(defun write-blank-line (stream) |
|---|
| 393 |
(write-char #\Return stream) |
|---|
| 394 |
(write-char #\NewLine stream) |
|---|
| 395 |
(force-output stream)) |
|---|
| 396 |
|
|---|
| 397 |
(defun read-from-smtp (stream &optional lines) |
|---|
| 398 |
(let* ((line (read-line stream)) |
|---|
| 399 |
(response (string-trim '(#\Return #\NewLine) (subseq line 4))) |
|---|
| 400 |
(response-code (parse-integer line :start 0 :junk-allowed t))) |
|---|
| 401 |
(print-debug (format nil "from server: ~A" line)) |
|---|
| 402 |
(if (= (char-code (elt line 3)) (char-code #\-)) |
|---|
| 403 |
(read-from-smtp stream (append lines (list response))) |
|---|
| 404 |
(values response-code response lines)))) |
|---|
| 405 |
|
|---|
| 406 |
(defun get-email-date-string () |
|---|
| 407 |
(multiple-value-bind (sec min h d m y wd) (get-decoded-time) |
|---|
| 408 |
(let* ((month (elt '("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec") (- m 1))) |
|---|
| 409 |
(weekday (elt '("Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun") wd)) |
|---|
| 410 |
(timezone (get-timezone-from-integer |
|---|
| 411 |
(- (encode-universal-time sec min h d m y 0) |
|---|
| 412 |
(get-universal-time))))) |
|---|
| 413 |
(format nil "~A, ~2,'0d ~A ~d ~2,'0d:~2,'0d:~2,'0d ~D" |
|---|
| 414 |
weekday d month y h min sec timezone)))) |
|---|
| 415 |
|
|---|
| 416 |
(defun get-timezone-from-integer (x) |
|---|
| 417 |
(let ((min (/ x 60)) |
|---|
| 418 |
(hour (/ x 3600))) |
|---|
| 419 |
(if (integerp hour) |
|---|
| 420 |
(cond |
|---|
| 421 |
((>= hour 0) |
|---|
| 422 |
(format nil "+~2,'0d00" hour)) |
|---|
| 423 |
((< hour 0) |
|---|
| 424 |
(format nil "-~2,'0d00" (* -1 hour)))) |
|---|
| 425 |
(multiple-value-bind (h m) (truncate min 60) |
|---|
| 426 |
(cond |
|---|
| 427 |
((>= hour 0) |
|---|
| 428 |
(format nil "+~2,'0d~2,'0d" h (truncate m))) |
|---|
| 429 |
((< hour 0) |
|---|
| 430 |
(format nil "-~2,'0d~2,'0d" (* -1 h) (* -1 (truncate m))))))))) |
|---|