| 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 | (ignore-errors |
|---|
| 351 | ;; For now, we ignore errors that are signalled after the mail has |
|---|
| 352 | ;; successfully been sent. Some servers close the connection |
|---|
| 353 | ;; after having received the QUIT command without properly saying |
|---|
| 354 | ;; good bye. |
|---|
| 355 | (smtp-command stream "QUIT" 221))) |
|---|
| 356 | |
|---|
| 357 | (defun send-mail-headers (stream |
|---|
| 358 | &key from to cc reply-to |
|---|
| 359 | extra-headers display-name subject) |
|---|
| 360 | "Send email headers according to the given arguments to the SMTP |
|---|
| 361 | server connected to on STREAM. The server is expected to have |
|---|
| 362 | previously accepted the DATA SMTP command." |
|---|
| 363 | (write-to-smtp stream (format nil "Date: ~A" (get-email-date-string))) |
|---|
| 364 | (write-to-smtp stream (format nil "From: ~@[~A <~]~A~@[>~]" |
|---|
| 365 | display-name from display-name)) |
|---|
| 366 | (write-to-smtp stream (format nil "To: ~{ ~a~^,~}" to)) |
|---|
| 367 | (when cc |
|---|
| 368 | (write-to-smtp stream (format nil "Cc: ~{ ~a~^,~}" cc))) |
|---|
| 369 | (write-to-smtp stream (format nil "Subject: ~A" subject)) |
|---|
| 370 | (write-to-smtp stream (format nil "X-Mailer: cl-smtp ~A" |
|---|
| 371 | *x-mailer*)) |
|---|
| 372 | (when reply-to |
|---|
| 373 | (write-to-smtp stream (format nil "Reply-To: ~A" reply-to))) |
|---|
| 374 | (when (and extra-headers |
|---|
| 375 | (listp extra-headers)) |
|---|
| 376 | (dolist (l extra-headers) |
|---|
| 377 | (write-to-smtp stream |
|---|
| 378 | (format nil "~A: ~{~a~^,~}" (car l) (rest l))))) |
|---|
| 379 | (write-to-smtp stream "Mime-Version: 1.0")) |
|---|
| 380 | |
|---|
| 381 | (defun send-multipart-headers (stream &key attachment-boundary html-boundary) |
|---|
| 382 | (cond (attachment-boundary |
|---|
| 383 | (generate-multipart-header stream attachment-boundary |
|---|
| 384 | :multipart-type "mixed")) |
|---|
| 385 | (html-boundary (generate-multipart-header |
|---|
| 386 | stream html-boundary |
|---|
| 387 | :multipart-type "alternative")) |
|---|
| 388 | (t nil)) |
|---|
| 389 | (write-blank-line stream)) |
|---|
| 390 | |
|---|
| 391 | (defun write-to-smtp (stream command) |
|---|
| 392 | (print-debug (format nil "to server: ~A" command)) |
|---|
| 393 | (write-string command stream) |
|---|
| 394 | (write-char #\Return stream) |
|---|
| 395 | (write-char #\NewLine stream) |
|---|
| 396 | (force-output stream)) |
|---|
| 397 | |
|---|
| 398 | (defun write-blank-line (stream) |
|---|
| 399 | (write-char #\Return stream) |
|---|
| 400 | (write-char #\NewLine stream) |
|---|
| 401 | (force-output stream)) |
|---|
| 402 | |
|---|
| 403 | (defun read-from-smtp (stream &optional lines) |
|---|
| 404 | (let* ((line (read-line stream)) |
|---|
| 405 | (response (string-trim '(#\Return #\NewLine) (subseq line 4))) |
|---|
| 406 | (response-code (parse-integer line :start 0 :junk-allowed t))) |
|---|
| 407 | (print-debug (format nil "from server: ~A" line)) |
|---|
| 408 | (if (= (char-code (elt line 3)) (char-code #\-)) |
|---|
| 409 | (read-from-smtp stream (append lines (list response))) |
|---|
| 410 | (values response-code response lines)))) |
|---|
| 411 | |
|---|
| 412 | (defun get-email-date-string () |
|---|
| 413 | (multiple-value-bind (sec min h d m y wd) (get-decoded-time) |
|---|
| 414 | (let* ((month (elt '("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec") (- m 1))) |
|---|
| 415 | (weekday (elt '("Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun") wd)) |
|---|
| 416 | (timezone (get-timezone-from-integer |
|---|
| 417 | (- (encode-universal-time sec min h d m y 0) |
|---|
| 418 | (get-universal-time))))) |
|---|
| 419 | (format nil "~A, ~2,'0d ~A ~d ~2,'0d:~2,'0d:~2,'0d ~D" |
|---|
| 420 | weekday d month y h min sec timezone)))) |
|---|
| 421 | |
|---|
| 422 | (defun get-timezone-from-integer (x) |
|---|
| 423 | (let ((min (/ x 60)) |
|---|
| 424 | (hour (/ x 3600))) |
|---|
| 425 | (if (integerp hour) |
|---|
| 426 | (cond |
|---|
| 427 | ((>= hour 0) |
|---|
| 428 | (format nil "+~2,'0d00" hour)) |
|---|
| 429 | ((< hour 0) |
|---|
| 430 | (format nil "-~2,'0d00" (* -1 hour)))) |
|---|
| 431 | (multiple-value-bind (h m) (truncate min 60) |
|---|
| 432 | (cond |
|---|
| 433 | ((>= hour 0) |
|---|
| 434 | (format nil "+~2,'0d~2,'0d" h (truncate m))) |
|---|
| 435 | ((< hour 0) |
|---|
| 436 | (format nil "-~2,'0d~2,'0d" (* -1 h) (* -1 (truncate m))))))))) |
|---|