2f95fd31 |
;; -*- mode: common-lisp; package: net.post-office -*-
|
1bb44177 |
;;
;; imap.cl
;; imap and pop interface
;;
|
48cdc2ae |
;; See the file LICENSE for the full license governing this code.
|
e8bb50bd |
#+(version= 7 0)
(sys:defpatch "imap" 1
"v1: fetch-letter-sequence support."
:type :system
:post-loadable t)
#+(version= 8 0)
(sys:defpatch "imap" 1
"v1: fetch-letter-sequence support."
:type :system
:post-loadable t)
#+(version= 8 1)
(sys:defpatch "imap" 1
"v1: Add ssl/tls support for both imap/pop connections."
:type :system
:post-loadable t)
|
1bb44177 |
;; Description:
|
46f65921 |
;;- This code in this file obeys the Lisp Coding Standard found in
;;- http://www.franz.com/~jkf/coding_standards.html
;;-
|
1bb44177 |
|
58857608 |
|
8dd85436 |
(defpackage :net.post-office
|
58857608 |
(:use :lisp :excl)
(:export
#:address-name
#:address-additional
#:address-mailbox
#:address-host
#:alter-flags
|
6fa1b4ab |
#:close-connection
|
58857608 |
#:close-mailbox
#:copy-to-mailbox
#:create-mailbox
#:delete-letter
#:delete-mailbox
#:envelope-date
#:envelope-subject
#:envelope-from
#:envelope-sender
#:envelope-reply-to
#:envelope-to
#:envelope-cc
#:envelope-bcc
#:envelope-in-reply-to
#:envelope-message-id
#:expunge-mailbox
#:fetch-field
#:fetch-letter
|
9705345d |
#:fetch-letter-sequence
#:end-of-letter-p
#:with-fetch-letter-sequence
|
6fa1b4ab |
#:fetch-parts
#:*imap-version-number*
|
8b09f124 |
#:make-envelope-from-text
|
58857608 |
#:mailbox-flags ; accessor
#:mailbox-permanent-flags ; acc
#:mailbox-list
#:mailbox-list-flags
#:mailbox-list-separator
#:mailbox-list-name
#:mailbox-message-count ; accessor
#:mailbox-recent-messages ; ac
#:mailbox-separator ; accessor
#:mailbox-uidvalidity
|
6d610934 |
#:mailbox-uidnext
|
58857608 |
#:make-imap-connection
|
6fa1b4ab |
#:make-pop-connection
|
9705345d |
#:with-imap-connection
#:with-pop-connection
|
58857608 |
#:noop
|
eaa80812 |
#:parse-mail-header
|
8b09f124 |
#:top-lines ; pop only
#:unique-id ; pop only
|
90494367 |
#:po-condition
|
b8706301 |
#:po-condition-identifier
|
90494367 |
#:po-condition-server-string
#:po-error
|
58857608 |
#:rename-mailbox
|
a3139825 |
#:reset-mailbox
|
58857608 |
#:search-mailbox
#:select-mailbox
|
8b09f124 |
|
58857608 |
)
)
|
8dd85436 |
(in-package :net.post-office)
|
6fa1b4ab |
|
90494367 |
(provide :imap)
|
6fa1b4ab |
|
f13625a5 |
(defparameter *imap-version-number* '(:major 1 :minor 14)) ; major.minor
|
58857608 |
|
3dc3e039 |
;; todo
;; have the list of tags selected done on a per connection basis to
;; eliminate any possible multithreading problems
;;
;;
|
58857608 |
(defvar *debug-imap* nil)
|
3dc3e039 |
|
6fa1b4ab |
(defclass post-office ()
|
58857608 |
((socket :initarg :socket
|
6fa1b4ab |
:accessor post-office-socket)
|
58857608 |
(host :initarg :host
|
6fa1b4ab |
:accessor post-office-host
|
58857608 |
:initform nil)
(user :initarg :user
|
6fa1b4ab |
:accessor post-office-user
|
58857608 |
:initform nil)
|
6fa1b4ab |
(state :accessor post-office-state
|
58857608 |
:initarg :state
:initform :unconnected)
(timeout
;; time to wait for network activity for actions that should
;; happen very quickly when things are operating normally
:initarg :timeout
:initform 60
:accessor timeout)
))
|
6fa1b4ab |
(defclass imap-mailbox (post-office)
|
58857608 |
((mailbox-name ; currently selected mailbox
:accessor mailbox-name
:initform nil)
(separator
;; string that separates mailbox names in the hierarchy
:accessor mailbox-separator
:initform "")
|
21b58a10 |
;;; these slots hold information about the currently selected mailbox:
|
58857608 |
(message-count ; how many in the mailbox
:accessor mailbox-message-count
:initform 0)
(recent-messages ; how many messages since we last checked
:accessor mailbox-recent-messages
:initform 0)
(uidvalidity ; used to denote messages uniquely
:accessor mailbox-uidvalidity
:initform 0)
(uidnext
:accessor mailbox-uidnext ;; predicted next uid
:initform 0)
(flags ; list of flags that can be stored in a message
:accessor mailbox-flags
:initform nil)
(permanent-flags ; list of flags that be stored permanently
:accessor mailbox-permanent-flags
:initform nil)
(first-unseen ; number of the first unseen message
:accessor first-unseen
:initform 0)
|
6fa1b4ab |
;;; end list of values for the currently selected mailbox
|
5a390910 |
;;; state information for fetch-letter-sequence
(fetch-letter-offset
:accessor fetch-letter-offset)
(fetch-letter-number
:accessor fetch-letter-number)
(fetch-letter-uid
:accessor fetch-letter-uid)
(fetch-letter-finished
:accessor fetch-letter-finished)
|
58857608 |
)
)
|
6fa1b4ab |
(defclass pop-mailbox (post-office)
((message-count ; how many in the mailbox
:accessor mailbox-message-count
|
9705345d |
:initform 0)
(fetch-letter-state
:accessor state
:initform :invalid)))
|
58857608 |
(defstruct (mailbox-list (:type list))
;; a list of these are returned by mailbox-list
flags
separator
name)
(defstruct (envelope (:type list))
;; returned by fetch-letter as the value of the envelope property
date
subject
from
sender
reply-to
to
cc
bcc
in-reply-to
message-id)
(defstruct (address (:type list))
name ;; often the person's full name
additional
mailbox ;; the login name
host ;; the name of the machine
)
|
90494367 |
;--------------------------------
; conditions
;
; We define a set of conditions that are signalled due to events
; in the imap interface.
; Each condition has an indentifier which is a keyword. That can
; be used in the handling code to identify the class of error.
; All our conditions are po-condition or po-error (which is a subclass of
; po-condition).
;
; A condition will have a server-string value if it as initiated by
; something returned by the server.
; A condition will have a format-control value if we want to display
; something we generated in response to
;
;
;
;; identifiers used in conditions/errors
; :problem condition
; the server responded with 'no' followed by an explanation.
; this mean that something unusual happend and doesn't necessarily
; mean that the command has completely failed (but it might).
;
; :unknown-ok condition
; the server responded with an 'ok' followed by something
; we don't recognize. It's probably safe to ignore this.
;
; :unknown-untagged condition
; the server responded with some untagged command we don't
; recognize. it's probaby ok to ignore this.
;
; :error-response error
; the command failed.
;
; :syntax-error error
; the data passed to a function in this interface was malformed
;
; :unexpected error
; the server responded an unexpected way.
;
; :server-shutdown-connection error
; the server has shut down the connection, don't attempt to
; send any more commands to this connection, or even close it.
;
; :timeout error
; server failed to respond within the timeout period
|
8559c73a |
;
; :response-too-large error
; contents of a response is too large to store in a Lisp array.
|
90494367 |
;; conditions
(define-condition po-condition ()
;; used to notify user of things that shouldn't necessarily stop
;; program flow
((identifier
;; keyword identifying the error (or :unknown)
:reader po-condition-identifier
:initform :unknown
:initarg :identifier
)
(server-string
;; message from the imap server
:reader po-condition-server-string
:initform ""
:initarg :server-string
))
(:report
(lambda (con stream)
(with-slots (identifier server-string) con
;; a condition either has a server-string or it has a
;; format-control string
(format stream "Post Office condition: ~s~%" identifier)
(if* (and (slot-boundp con 'excl::format-control)
(excl::simple-condition-format-control con))
then (apply #'format stream
(excl::simple-condition-format-control con)
(excl::simple-condition-format-arguments con)))
(if* server-string
then (format stream
"~&Message from server: ~s"
(string-left-trim " " server-string)))))))
(define-condition po-error (po-condition error)
;; used to denote things that should stop program flow
())
;; aignalling the conditions
(defun po-condition (identifier &key server-string format-control
format-arguments)
(signal (make-instance 'po-condition
:identifier identifier
:server-string server-string
:format-control format-control
:format-arguments format-arguments
)))
(defun po-error (identifier &key server-string
format-control format-arguments)
(error (make-instance 'po-error
:identifier identifier
:server-string server-string
:format-control format-control
:format-arguments format-arguments)))
;----------------------------------------------
|
58857608 |
(defparameter *imap-tags* '("t01" "t02" "t03" "t04" "t05" "t06" "t07"))
(defvar *cur-imap-tags* nil)
(defvar *crlf*
(let ((str (make-string 2)))
(setf (aref str 0) #\return)
(setf (aref str 1) #\linefeed)
str))
|
4e6f0603 |
;; returns values: socket starttls
;; server is a cons of the form:
;; (server-name &key (port 25) (ssl nil) (starttls nil) ...ssl-client-keywords...)
(defun connect-to-imap/pop-server (server-info server-type)
(macrolet ((pop-keyword (k l) `(prog1 (getf ,l ,k) (remf ,l ,k)))
(server-port (ssl type) `(cond ((eq ,type :imap) (if ,ssl 993 143))
((eq ,type :pop) (if ,ssl 995 110)))))
(let* ((server (car server-info))
(ssl-args (cdr server-info))
ssl port starttls sock)
(setq ssl (pop-keyword :ssl ssl-args))
(setq port (or (pop-keyword :port ssl-args) (server-port ssl server-type)))
(setq starttls (pop-keyword :starttls ssl-args))
(setq sock (socket:make-socket :remote-host server
:remote-port port))
(when ssl
(setq sock (apply #'socket:make-ssl-client-stream sock ssl-args)))
(values sock starttls))) )
|
58857608 |
(defun make-imap-connection (host &key (port 143)
user
password
(timeout 30))
|
4e6f0603 |
(multiple-value-bind (sock starttls)
(if (consp host)
(connect-to-imap/pop-server host :imap)
(socket:make-socket :remote-host host :remote-port port))
(let ((imap (make-instance 'imap-mailbox
:socket sock
:host host
:timeout timeout
:state :unauthorized)))
|
58857608 |
|
90494367 |
(multiple-value-bind (tag cmd count extra comment)
|
58857608 |
(get-and-parse-from-imap-server imap)
|
4e6f0603 |
(declare (ignorable cmd count extra))
|
58857608 |
(if* (not (eq :untagged tag))
|
90494367 |
then (po-error :error-response
:server-string comment)))
|
58857608 |
|
4e6f0603 |
; check for starttls negotiation
(when starttls
(let (capabilities)
(send-command-get-results
imap "CAPABILITY"
#'(lambda (mb cmd count extra comment)
(declare (ignorable mb cmd count extra))
(setq capabilities comment))
#'(lambda (mb cmd count extra comment)
(check-for-success mb cmd count extra comment
"CAPABILITY")))
(when (and capabilities (match-re "STARTTLS" capabilities :case-fold t
:return nil))
;; negotiate starttls
(send-command-get-results imap "STARTTLS"
#'handle-untagged-response
#'(lambda (mb cmd count extra comment)
(check-for-success mb cmd count extra comment
"STARTTLS")
(setf (post-office-socket mb)
(socket:make-ssl-client-stream
(post-office-socket mb) :method :tlsv1)))))))
|
58857608 |
; now login
(send-command-get-results imap
(format nil "login ~a ~a" user password)
#'handle-untagged-response
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(check-for-success mb command count extra
|
90494367 |
comment
|
58857608 |
"login")))
; find the separator character
(let ((res (mailbox-list imap)))
;;
(let ((sep (cadr (car res))))
(if* sep
then (setf (mailbox-separator imap) sep))))
|
4e6f0603 |
imap)))
|
58857608 |
|
6fa1b4ab |
(defmethod close-connection ((mb imap-mailbox))
|
58857608 |
|
6fa1b4ab |
(let ((sock (post-office-socket mb)))
|
58857608 |
(if* sock
then (ignore-errors
(send-command-get-results
mb
"logout"
; don't want to get confused by untagged
; bye command, which is expected here
#'(lambda (mb command count extra)
(declare (ignore mb command count extra))
nil)
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(check-for-success mb command count extra
|
90494367 |
comment
|
58857608 |
"logout")))))
|
6fa1b4ab |
(setf (post-office-socket mb) nil)
(if* sock then (ignore-errors (close sock)))
t))
(defmethod close-connection ((pb pop-mailbox))
(let ((sock (post-office-socket pb)))
(if* sock
then (ignore-errors
(send-pop-command-get-results
pb
"QUIT")))
(setf (post-office-socket pb) nil)
|
58857608 |
(if* sock then (ignore-errors (close sock)))
t))
|
6fa1b4ab |
(defun make-pop-connection (host &key (port 110)
user
password
(timeout 30))
|
4e6f0603 |
(multiple-value-bind (sock starttls)
(if (consp host)
(connect-to-imap/pop-server host :pop)
(socket:make-socket :remote-host host :remote-port port))
(let ((pop (make-instance 'pop-mailbox
|
6fa1b4ab |
:socket sock
:host host
:timeout timeout
:state :unauthorized)))
(multiple-value-bind (result)
(get-and-parse-from-pop-server pop)
(if* (not (eq :ok result))
|
90494367 |
then (po-error :error-response
:format-control
"unexpected line from server after connect")))
|
6fa1b4ab |
|
4e6f0603 |
; check for starttls negotiation
(when starttls
(let ((capabilities (send-pop-command-get-results pop "capa" t)))
(when (and capabilities (match-re "STLS" capabilities :case-fold t
:return nil))
(send-pop-command-get-results pop "STLS")
(setf (post-office-socket pop) (socket:make-ssl-client-stream
(post-office-socket pop) :method :tlsv1)))))
|
6fa1b4ab |
; now login
(send-pop-command-get-results pop (format nil "user ~a" user))
(send-pop-command-get-results pop (format nil "pass ~a" password))
(let ((res (send-pop-command-get-results pop "stat")))
(setf (mailbox-message-count pop) (car res)))
|
4e6f0603 |
pop)))
|
6fa1b4ab |
|
58857608 |
(defmethod send-command-get-results ((mb imap-mailbox)
command untagged-handler tagged-handler)
;; send a command and retrieve results until we get the tagged
;; response for the command we sent
;;
(let ((tag (get-next-tag)))
|
6fa1b4ab |
(format (post-office-socket mb)
|
58857608 |
"~a ~a~a" tag command *crlf*)
|
6fa1b4ab |
(force-output (post-office-socket mb))
|
58857608 |
(if* *debug-imap*
then (format t
"~a ~a~a" tag command *crlf*)
(force-output))
(loop
|
90494367 |
(multiple-value-bind (got-tag cmd count extra comment)
|
58857608 |
(get-and-parse-from-imap-server mb)
(if* (eq got-tag :untagged)
|
90494367 |
then (funcall untagged-handler mb cmd count extra comment)
|
58857608 |
elseif (equal tag got-tag)
|
90494367 |
then (funcall tagged-handler mb cmd count extra comment)
|
58857608 |
(return)
|
64f441e3 |
else (po-error :error-response
:format-control "received tag ~s out of order"
:format-arguments (list got-tag)
:server-string comment))))))
|
58857608 |
(defun get-next-tag ()
(let ((tag (pop *cur-imap-tags*)))
(if* tag
thenret
else (setq *cur-imap-tags* *imap-tags*)
(pop *cur-imap-tags*))))
|
90494367 |
(defun handle-untagged-response (mb command count extra comment)
|
58857608 |
;; default function to handle untagged responses, which are
;; really just returning general state information about
;; the mailbox
(case command
(:exists (setf (mailbox-message-count mb) count))
(:recent (setf (mailbox-recent-messages mb) count))
|
9acef702 |
(:flags (setf (mailbox-flags mb) (kwd-intern-possible-list extra)))
|
58857608 |
(:bye ; occurs when connection times out or mailbox lock is stolen
|
6fa1b4ab |
(ignore-errors (close (post-office-socket mb)))
|
90494367 |
(po-error :server-shutdown-connection
:server-string "server shut down the connection"))
|
58857608 |
(:no ; used when grabbing a lock from another process
|
90494367 |
(po-condition :problem :server-string comment))
|
58857608 |
(:ok ; a whole variety of things
(if* extra
then (if* (equalp (car extra) "unseen")
then (setf (first-unseen mb) (cadr extra))
elseif (equalp (car extra) "uidvalidity")
then (setf (mailbox-uidvalidity mb) (cadr extra))
elseif (equalp (car extra) "uidnext")
then (setf (mailbox-uidnext mb) (cadr extra))
elseif (equalp (car extra) "permanentflags")
then (setf (mailbox-permanent-flags mb)
|
9acef702 |
(kwd-intern-possible-list (cadr extra)))
|
90494367 |
else (po-condition :unknown-ok :server-string comment))))
(t (po-condition :unknown-untagged :server-string comment)))
|
58857608 |
)
|
6fa1b4ab |
|
9705345d |
(defmethod begin-extended-results-sequence ((mb pop-mailbox))
(setf (state mb) 1))
(defmethod get-extended-results-sequence ((mb pop-mailbox) buffer &key (start 0) (end (length buffer)))
(declare (optimize (speed 3) (safety 1)))
(let ((inpos start)
(outpos start)
(sock (post-office-socket mb))
ch
stop)
(macrolet ((add-to-buffer ()
`(progn
(setf (schar buffer outpos) ch)
(incf outpos))))
(while (and (< inpos end) (/= (state mb) 4))
(setf stop (read-sequence buffer sock :start inpos :end end :partial-fill t))
(while (< inpos stop)
(setf ch (schar buffer inpos))
(if* (eq ch #\return)
thenret ; ignore crs
else (ecase (state mb)
(1 (if* (eq ch #\.) ; at beginning of line
then (setf (state mb) 2)
elseif (eq ch #\linefeed)
then
(add-to-buffer) ; state stays at 1
else
(setf (state mb) 3)
(add-to-buffer)))
(2 ; seen first dot
(if* (eq ch #\linefeed)
then ; end of results
(setf (state mb) 4)
(return)
else
(setf (state mb) 3)
(add-to-buffer))) ; normal reading
(3 ; middle of line
(if* (eq ch #\linefeed)
then (setf (state mb) 1))
(add-to-buffer))))
(incf inpos))
(setf inpos outpos))
outpos)))
(defmacro end-of-extended-results-p (mb)
`(= (state ,mb) 4))
(defmethod end-extended-results-sequence ((mb pop-mailbox))
(declare (optimize (speed 3) (safety 1)))
(let ((buffer (make-string 4096)))
(until (end-of-extended-results-p mb)
(get-extended-results-sequence mb buffer)))
(setf (state mb) :invalid-state)
t)
(defmacro with-extended-results-sequence ((mailbox) &body body)
(let ((mb (gensym)))
`(let ((,mb ,mailbox))
(begin-extended-results-sequence ,mb)
(unwind-protect
(progn
,@body)
;; cleanup
(end-extended-results-sequence ,mb)))))
|
6fa1b4ab |
(defun send-pop-command-get-results (pop command &optional extrap)
|
9705345d |
(declare (optimize (speed 3) (safety 1)))
|
8b09f124 |
;; send the given command to the pop server
;; if extrap is true and if the response is +ok, then data
;; will follow the command (up to and excluding the first line consisting
;; of just a period)
;;
;; if the pop server returns an error code we signal a lisp error.
;; otherwise
;; return
;; extrap is nil -- return the list of tokens on the line after +ok
;; extrap is true -- return the extra object (a big string)
;;
|
6fa1b4ab |
(format (post-office-socket pop) "~a~a" command *crlf*)
(force-output (post-office-socket pop))
(if* *debug-imap*
then (format t "~a~a" command *crlf*)
(force-output t))
(multiple-value-bind (result parsed line)
(get-and-parse-from-pop-server pop)
(if* (not (eq result :ok))
|
90494367 |
then (po-error :error-response
:server-string line))
|
6fa1b4ab |
(if* extrap
|
8b09f124 |
then ;; get the rest of the data
;; many but not all pop servers return the size of the data
;; after the +ok, so we use that to initially size the
;; retreival buffer.
|
9705345d |
(let* ((buf (get-line-buffer (+ (if* (fixnump (car parsed))
then (car parsed)
else 2048 ; reasonable size
)
50)))
(buflen (length buf))
(pos 0))
(with-extended-results-sequence (pop)
(until (end-of-extended-results-p pop)
(if* (>= pos buflen)
then ;; grow buffer
(if* (>= buflen (1- array-total-size-limit))
then ; can't grow it any further
(po-error
:response-too-large
:format-control
"response from mail server is too large to hold in a lisp array"))
(let ((new-buf (get-line-buffer (* buflen 2))))
(init-line-buffer new-buf buf)
(free-line-buffer buf)
(setq buf new-buf)
(setq buflen (length buf))))
(setf pos (get-extended-results-sequence pop buf :start pos :end buflen))))
|
6fa1b4ab |
(prog1 (subseq buf 0 pos)
(free-line-buffer buf)))
else parsed)))
|
9705345d |
|
58857608 |
(defun convert-flags-plist (plist)
;; scan the plist looking for "flags" indicators and
;; turn value into a list of symbols rather than strings
(do ((xx plist (cddr xx)))
((null xx) plist)
(if* (equalp "flags" (car xx))
then (setf (cadr xx) (mapcar #'kwd-intern (cadr xx))))))
|
6fa1b4ab |
(defmethod select-mailbox ((mb imap-mailbox) name)
;; select the given mailbox
|
58857608 |
(send-command-get-results mb
(format nil "select ~a" name)
#'handle-untagged-response
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(declare (ignore mb count extra))
(if* (not (eq command :ok))
|
90494367 |
then (po-error
:problem
:format-control
"imap mailbox select failed"
:server-string comment))))
|
58857608 |
(setf (mailbox-name mb) name)
t
)
|
6fa1b4ab |
(defmethod fetch-letter ((mb imap-mailbox) number &key uid)
;; return the whole letter
(fetch-field number "body[]"
(fetch-parts mb number "body[]" :uid uid)
:uid uid))
|
58857608 |
|
6fa1b4ab |
(defmethod fetch-letter ((pb pop-mailbox) number &key uid)
(declare (ignore uid))
(send-pop-command-get-results pb
(format nil "RETR ~d" number)
t ; extra stuff
))
|
5a390910 |
(defmethod begin-fetch-letter-sequence ((mb imap-mailbox) number &key uid)
(setf (fetch-letter-offset mb) 0)
(setf (fetch-letter-number mb) number)
(setf (fetch-letter-uid mb) uid)
(setf (fetch-letter-finished mb) nil))
|
9705345d |
(defmethod begin-fetch-letter-sequence ((mb pop-mailbox) number &key uid)
(declare (ignore uid))
(send-pop-command-get-results mb (format nil "RETR ~d" number))
(begin-extended-results-sequence mb))
|
5a390910 |
(defmethod fetch-letter-sequence ((mb imap-mailbox) buffer
&key (start 0) (end (length buffer)))
(let* ((num (fetch-letter-number mb))
(offset (fetch-letter-offset mb))
(uid (fetch-letter-uid mb))
(buflen (- end start))
(data (fetch-field num (format nil "body[]<~d>" offset)
(fetch-parts mb num
(format nil "body[]<~d.~d>" offset buflen)
:uid uid)
:uid uid))
(datalen (length data)))
(setf (subseq buffer start end) data)
(if* (and (> buflen 0) (= datalen 0))
then (setf (fetch-letter-finished mb) t))
(setf (fetch-letter-offset mb) (+ offset buflen))
(+ start datalen)))
|
9705345d |
(defmethod fetch-letter-sequence ((mb pop-mailbox) buffer &key (start 0) (end (length buffer)))
(get-extended-results-sequence mb buffer :start start :end end))
|
5a390910 |
(defmethod end-fetch-letter-sequence ((mb imap-mailbox))
)
|
9705345d |
(defmethod end-fetch-letter-sequence ((mb pop-mailbox))
(end-extended-results-sequence mb))
|
5a390910 |
(defmethod end-of-letter-p ((mb imap-mailbox))
(fetch-letter-finished mb))
|
9705345d |
(defmethod end-of-letter-p ((mb pop-mailbox))
(end-of-extended-results-p mb))
(defmacro with-fetch-letter-sequence ((mailbox &rest args) &body body)
(let ((mb (gensym)))
`(let ((,mb ,mailbox))
(begin-fetch-letter-sequence ,mb ,@args)
(unwind-protect
(progn
,@body)
;; cleanup
(end-fetch-letter-sequence ,mb)))))
|
6fa1b4ab |
(defmethod fetch-parts ((mb imap-mailbox) number parts &key uid)
|
58857608 |
(let (res)
(send-command-get-results
mb
(format nil "~afetch ~a ~a"
(if* uid then "uid " else "")
(message-set-string number)
(or parts "body[]")
)
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(if* (eq command :fetch)
then (push (list count (internalize-flags extra)) res)
else (handle-untagged-response
|
90494367 |
mb command count extra comment)))
#'(lambda (mb command count extra comment)
|
58857608 |
(declare (ignore mb count extra))
(if* (not (eq command :ok))
|
90494367 |
then (po-error :problem
:format-control "imap mailbox fetch failed"
:server-string comment))))
|
58857608 |
res))
(defun fetch-field (letter-number field-name info &key uid)
;; given the information from a fetch-letter, return the
;; particular field for the particular letter
;;
;; info is as returned by fetch
;; field-name is a string, case doesn't matter.
;;
(dolist (item info)
;; item is (messagenumber plist-info)
;; the same messagenumber may appear in multiple items
(let (use-this)
(if* uid
then ; uid appears as a property in the value, not
; as the top level message sequence number
(do ((xx (cadr item) (cddr xx)))
((null xx))
(if* (equalp "uid" (car xx))
then (if* (eql letter-number (cadr xx))
then (return (setq use-this t))
else (return))))
else ; just a message sequence number
(setq use-this (eql letter-number (car item))))
(if* use-this
then (do ((xx (cadr item) (cddr xx)))
((null xx))
(if* (equalp field-name (car xx))
then (return-from fetch-field (cadr xx))))))))
(defun internalize-flags (stuff)
;; given a plist like object, look for items labelled "flags" and
;; convert the contents to internal flags objects
(do ((xx stuff (cddr xx)))
((null xx))
(if* (equalp (car xx) "flags")
|
9acef702 |
then ; we can end up with sublists of forms if we
; do add-flags with a list of flags. this seems like
; a bug in the imap server.. but we have to deal with it
(setf (cadr xx) (kwd-intern-possible-list (cadr xx)))
(return)))
|
58857608 |
stuff)
|
6fa1b4ab |
(defmethod delete-letter ((mb imap-mailbox) messages &key (expunge t) uid)
|
58857608 |
;; delete all the mesasges and do the expunge to make
;; it permanent if expunge is true
(alter-flags mb messages :add-flags :\\deleted :uid uid)
(if* expunge then (expunge-mailbox mb)))
|
6fa1b4ab |
(defmethod delete-letter ((pb pop-mailbox) messages &key (expunge nil) uid)
;; delete all the messages. We can't expunge without quitting so
;; we don't expunge
(declare (ignore expunge uid))
(if* (or (numberp messages)
(and (consp messages) (eq :seq (car messages))))
then (setq messages (list messages)))
(if* (not (consp messages))
|
90494367 |
then (po-error :syntax-error
:format-control "expect a mesage number or list of messages, not ~s"
:format-arguments (list messages)))
|
6fa1b4ab |
(dolist (message messages)
(if* (numberp message)
then (send-pop-command-get-results pb
(format nil "DELE ~d" message))
elseif (and (consp message) (eq :seq (car message)))
then (do ((start (cadr message) (1+ start))
(end (caddr message)))
((> start end))
(send-pop-command-get-results pb
(format nil "DELE ~d" start)))
|
90494367 |
else (po-error :syntax-error
:format-control "bad message number ~s"
:format-arguments (list message)))))
|
6fa1b4ab |
|
58857608 |
|
6fa1b4ab |
(defmethod noop ((mb imap-mailbox))
|
58857608 |
;; just poke the server... keeping it awake and checking for
;; new letters
(send-command-get-results mb
"noop"
#'handle-untagged-response
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(check-for-success
mb command count extra
|
90494367 |
comment
|
58857608 |
"noop"))))
|
6fa1b4ab |
(defmethod noop ((pb pop-mailbox))
;; send the stat command instead so we can update the message count
(let ((res (send-pop-command-get-results pb "stat")))
(setf (mailbox-message-count pb) (car res)))
)
|
8b09f124 |
(defmethod unique-id ((pb pop-mailbox) &optional message)
;; if message is given, return the unique id of that
;; message,
;; if message is not given then return a list of lists:
;; (message unique-id)
;; for all messages not marked as deleted
;;
(if* message
then (let ((res (send-pop-command-get-results pb
(format nil
"UIDL ~d"
message))))
(cadr res))
else ; get all of them
(let* ((res (send-pop-command-get-results pb "UIDL" t))
(end (length res))
kind
mnum
mid
(next 0))
(let ((coll))
(loop
(multiple-value-setq (kind mnum next)
(get-next-token res next end))
(if* (eq :eof kind) then (return))
(if* (not (eq :number kind))
then ; hmm. bogus
(po-error :unexpected
:format-control "uidl returned illegal message number in ~s"
:format-arguments (list res)))
; now get message id
(multiple-value-setq (kind mid next)
(get-next-token res next end))
(if* (eq :number kind)
then ; looked like a number to the tokenizer,
; make it a string to be consistent
(setq mid (format nil "~d" mid))
elseif (not (eq :string kind))
then ; didn't find the uid
(po-error :unexpected
:format-control "uidl returned illegal message id in ~s"
:format-arguments (list res)))
(push (list mnum mid) coll))
(nreverse coll)))))
(defmethod top-lines ((pb pop-mailbox) message lines)
;; return the header and the given number of top lines of the message
(let ((res (send-pop-command-get-results pb
(format nil
"TOP ~d ~d"
message
lines)
t ; extra
)))
res))
|
a3139825 |
(defmethod reset-mailbox ((pb pop-mailbox))
;; undo's deletes
(send-pop-command-get-results pb "RSET")
)
|
8b09f124 |
|
90494367 |
(defun check-for-success (mb command count extra comment command-string )
|
58857608 |
(declare (ignore mb count extra))
(if* (not (eq command :ok))
|
90494367 |
then (po-error :error-response
:format-control "imap ~a failed"
:format-arguments (list command-string)
:server-string comment)))
|
58857608 |
|
6fa1b4ab |
(defmethod mailbox-list ((mb imap-mailbox) &key (reference "") (pattern ""))
|
58857608 |
;; return a list of mailbox names with respect to a given
(let (res)
(send-command-get-results mb
(format nil "list ~s ~s" reference pattern)
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(if* (eq command :list)
then (push extra res)
else (handle-untagged-response
|
90494367 |
mb command count extra
comment)))
#'(lambda (mb command count extra comment)
|
58857608 |
(check-for-success
|
90494367 |
mb command count extra
comment "list")))
|
58857608 |
;; the car of each list is a set of keywords, make that so
(dolist (rr res)
(setf (car rr) (mapcar #'kwd-intern (car rr))))
res
))
|
6fa1b4ab |
(defmethod create-mailbox ((mb imap-mailbox) mailbox-name)
|
58857608 |
;; create a mailbox name of the given name.
;; use mailbox-separator if you want to create a hierarchy
(send-command-get-results mb
(format nil "create ~s" mailbox-name)
#'handle-untagged-response
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(check-for-success
|
90494367 |
mb command count extra
comment "create")))
|
58857608 |
t)
|
6fa1b4ab |
(defmethod delete-mailbox ((mb imap-mailbox) mailbox-name)
|
58857608 |
;; create a mailbox name of the given name.
;; use mailbox-separator if you want to create a hierarchy
(send-command-get-results mb
(format nil "delete ~s" mailbox-name)
#'handle-untagged-response
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(check-for-success
|
90494367 |
mb command count extra
comment "delete"))))
|
58857608 |
|
6fa1b4ab |
(defmethod rename-mailbox ((mb imap-mailbox) old-mailbox-name new-mailbox-name)
|
58857608 |
;; create a mailbox name of the given name.
;; use mailbox-separator if you want to create a hierarchy
(send-command-get-results mb
(format nil "rename ~s ~s"
old-mailbox-name
new-mailbox-name)
#'handle-untagged-response
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(check-for-success
|
90494367 |
mb command count extra
comment
"rename"))))
|
58857608 |
|
6fa1b4ab |
(defmethod alter-flags ((mb imap-mailbox)
messages &key (flags nil flags-p)
add-flags remove-flags
|
58857608 |
silent uid)
;;
;; change the flags using the store command
;;
(let (cmd val res)
(if* flags-p
then (setq cmd "flags" val flags)
elseif add-flags
then (setq cmd "+flags" val add-flags)
elseif remove-flags
then (setq cmd "-flags" val remove-flags)
else (return-from alter-flags nil))
(if* (atom val) then (setq val (list val)))
(send-command-get-results mb
(format nil "~astore ~a ~a~a ~a"
(if* uid then "uid " else "")
(message-set-string messages)
cmd
(if* silent
then ".silent"
else "")
(if* val
thenret
else "()"))
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(if* (eq command :fetch)
then (push (list count
(convert-flags-plist
extra))
res)
else (handle-untagged-response
|
90494367 |
mb command count extra
comment)))
|
58857608 |
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(check-for-success
|
90494367 |
mb command count extra
comment "store")))
|
58857608 |
res))
(defun message-set-string (messages)
;; return a string that describes the messages which may be a
;; single number or a sequence of numbers
(if* (atom messages)
then (format nil "~a" messages)
else (if* (and (consp messages)
(eq :seq (car messages)))
then (format nil "~a:~a" (cadr messages) (caddr messages))
else (let ((str (make-string-output-stream))
(precomma nil))
(dolist (msg messages)
(if* precomma then (format str ","))
(if* (atom msg)
then (format str "~a" msg)
elseif (eq :seq (car msg))
then (format str
"~a:~a" (cadr msg) (caddr msg))
|
90494367 |
else (po-error :syntax-error
:format-control "bad message list ~s"
:format-arguments (list msg)))
|
58857608 |
(setq precomma t))
(get-output-stream-string str)))))
(defmethod expunge-mailbox ((mb imap-mailbox))
;; remove messages marked as deleted
(let (res)
(send-command-get-results mb
"expunge"
|
90494367 |
#'(lambda (mb command count extra
comment)
|
58857608 |
(if* (eq command :expunge)
then (push count res)
else (handle-untagged-response
|
90494367 |
mb command count extra
comment)))
#'(lambda (mb command count extra comment)
|
58857608 |
(check-for-success
|
90494367 |
mb command count extra
comment "expunge")))
|
58857608 |
(nreverse res)))
(defmethod close-mailbox ((mb imap-mailbox))
;; remove messages marked as deleted
(send-command-get-results mb
"close"
#'handle-untagged-response
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(check-for-success
|
90494367 |
mb command count extra
comment "close")))
|
58857608 |
t)
(defmethod copy-to-mailbox ((mb imap-mailbox) message-list destination
&key uid)
(send-command-get-results mb
(format nil "~acopy ~a ~s"
(if* uid then "uid " else "")
(message-set-string message-list)
destination)
#'handle-untagged-response
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(check-for-success
|
90494367 |
mb command count extra
comment "copy")))
|
58857608 |
t)
;; search command
|
6fa1b4ab |
(defmethod search-mailbox ((mb imap-mailbox) search-expression &key uid)
|
58857608 |
(let (res)
(send-command-get-results mb
(format nil "~asearch ~a"
(if* uid then "uid " else "")
(build-search-string search-expression))
|
90494367 |
#'(lambda (mb command count extra comment)
|
58857608 |
(if* (eq command :search)
then (setq res (append res extra))
else (handle-untagged-response
|
90494367 |
mb command count extra
comment)))
#'(lambda (mb command count extra comment)
|
58857608 |
(check-for-success
|
90494367 |
mb command count extra
comment "search")))
|
58857608 |
res))
(defmacro defsearchop (name &rest operands)
(if* (null operands)
then `(setf (get ',name 'imap-search-no-args) t)
else `(setf (get ',name 'imap-search-args) ',operands)))
(defsearchop :all)
(defsearchop :answered)
(defsearchop :bcc :str)
(defsearchop :before :date)
(defsearchop :body :str)
(defsearchop :cc :str)
(defsearchop :deleted)
(defsearchop :draft)
(defsearchop :flagged)
(defsearchop :from :str)
(defsearchop :header :str :str)
(defsearchop :keyword :flag)
(defsearchop :larger :number)
(defsearchop :new)
(defsearchop :old)
(defsearchop :on :date)
(defsearchop :recent)
(defsearchop :seen)
(defsearchop :sentbefore :date)
(defsearchop :senton :date)
(defsearchop :sentsince :date)
(defsearchop :since :date)
(defsearchop :smaller :number)
(defsearchop :subject :str)
(defsearchop :text :str)
(defsearchop :to :str)
(defsearchop :uid :messageset)
(defsearchop :unanswered)
(defsearchop :undeleted)
(defsearchop :undraft)
(defsearchop :unflagged)
(defsearchop :unkeyword :flag)
(defsearchop :unseen)
(defun build-search-string (search)
;; take the lisp search form and turn it into a string that can be
;; passed to imap
(if* (null search)
then ""
else (let ((str (make-string-output-stream)))
(bss-int search str)
(get-output-stream-string str))))
(defun bss-int (search str)
;;* it turns out that imap (on linux) is very picky about spaces....
;; any extra whitespace will result in failed searches
;;
(labels ((and-ify (srch str)
(let ((spaceout nil))
(dolist (xx srch)
(if* spaceout then (format str " "))
(bss-int xx str)
(setq spaceout t))))
(or-ify (srch str)
; only binary or allowed in imap but we support n-ary
; or in this interface
(if* (null (cdr srch))
then (bss-int (car srch) str)
elseif (cddr srch)
then ; over two clauses
(format str "or (")
(bss-int (car srch) str)
(format str ") (")
(or-ify (cdr srch) str)
(format str ")")
else ; 2 args
(format str "or (" )
(bss-int (car srch) str)
(format str ") (")
(bss-int (cadr srch) str)
(format str ")")))
(set-ify (srch str)
;; a sequence of messages
(do* ((xsrch srch (cdr xsrch))
(val (car xsrch) (car xsrch)))
((null xsrch))
(if* (integerp val)
then (format str "~s" val)
elseif (and (consp val)
(eq :seq (car val))
(eq 3 (length val)))
then (format str "~s:~s" (cadr val) (caddr val))
|
90494367 |
else (po-error :syntax-error
:format-control "illegal set format ~s"
:format-arguments (list val)))
|
58857608 |
(if* (cdr xsrch) then (format str ","))))
(arg-process (str args arginfo)
;; process and print each arg to str
;; assert (length of args and arginfo are the same)
(do* ((x-args args (cdr x-args))
(val (car x-args) (car x-args))
(x-arginfo arginfo (cdr x-arginfo)))
((null x-args))
(ecase (car x-arginfo)
(:str
; print it as a string
(format str " \"~a\"" (car x-args)))
(:date
(if* (integerp val)
then (setq val (universal-time-to-rfc822-date
val))
elseif (not (stringp val))
|
90494367 |
then (po-error :syntax-error
:format-control "illegal value for date search ~s"
:format-arguments (list val)))
|
58857608 |
;; val is now a string
(format str " ~s" val))
(:number
(if* (not (integerp val))
|
90494367 |
then (po-error :syntax-error
:format-control "illegal value for number in search ~s"
:format-arguments (list val)))
|
58857608 |
(format str " ~s" val))
(:flag
;; should be a symbol in the kwd package
(setq val (string val))
(format str " ~s" val))
(:messageset
(if* (numberp val)
then (format str " ~s" val)
elseif (consp val)
then (set-ify val str)
|
90494367 |
else (po-error :syntax-error
:format-control "illegal message set ~s"
:format-arguments (list val))))
|
58857608 |
))))
(if* (symbolp search)
then (if* (get search 'imap-search-no-args)
then (format str "~a" (string-upcase
|
90494367 |
(string search)))
else (po-error :syntax-error
:format-control "illegal search word: ~s"
:format-arguments (list search)))
|
58857608 |
elseif (consp search)
then (case (car search)
(and (if* (null (cdr search))
then (bss-int :all str)
elseif (null (cddr search))
then (bss-int (cadr search) str)
else (and-ify (cdr search) str)))
(or (if* (null (cdr search))
then (bss-int :all str)
elseif (null (cddr search))
then (bss-int (cadr search) str)
else (or-ify (cdr search) str)))
(not (if* (not (eql (length search) 2))
|
90494367 |
then (po-error :syntax-error
:format-control "not takes one argument: ~s"
:format-arguments (list search)))
|
58857608 |
(format str "not (" )
(bss-int (cadr search) str)
(format str ")"))
(:seq
(set-ify (list search) str))
(t (let (arginfo)
(if* (and (symbolp (car search))
(setq arginfo (get (car search)
'imap-search-args)))
then
(format str "~a" (string-upcase
|
90494367 |
(string (car search))))
|
58857608 |
(if* (not (equal (length (cdr search))
(length arginfo)))
|
90494367 |
then (po-error :syntax-error
:format-control "wrong number of arguments to ~s"
:format-arguments search))
|
58857608 |
(arg-process str (cdr search) arginfo)
elseif (integerp (car search))
then (set-ify search str)
|
90494367 |
else (po-error :syntax-error
:format-control "Illegal form ~s in search string"
:format-arguments (list search))))))
|
58857608 |
elseif (integerp search)
then ; a message number
(format str "~s" search)
|
90494367 |
else (po-error :syntax-error
:format-control "Illegal form ~s in search string"
:format-arguments (list search)))))
|
58857608 |
|
8b09f124 |
|
eaa80812 |
(defun parse-mail-header (text)
;; given the partial text of a mail message that includes
;; at least the header part, return an assoc list of
;; (header . content) items
;; Note that the header is string with most likely mixed case names
;; as it's conventional to capitalize header names.
|
8b09f124 |
(let ((next 0)
(end (length text))
header
value
kind
headers)
(labels ((next-header-line ()
;; find the next header line return
;; :eof - no more
;; :start - beginning of header value, header and
;; value set
;; :continue - continuation of previous header line
(let ((state 1)
beginv ; charpos beginning value
beginh ; charpos beginning header
ch
)
(tagbody again
|
58857608 |
|
8b09f124 |
(return-from next-header-line
(loop ; for each character
(if* (>= next end)
then (return :eof))
(setq ch (char text next))
(if* (eq ch #\return)
thenret ; ignore return, (handle following linefeed)
else (case state
(1 ; no characters seen
(if* (eq ch #\linefeed)
then (incf next)
(return :eof)
elseif (member ch
'(#\space
#\tab))
then ; continuation
(setq state 2)
else (setq beginh next)
(setq state 3)
))
(2 ; looking for first non blank in value
(if* (eq ch #\linefeed)
then ; empty continuation line, ignore
|
925381dd |
(incf next)
|
ef11d1c0 |
(if* header
then ; header and no value
(setq value "")
(return :start))
|
1170a0d0 |
(setq state 1)
|
8b09f124 |
(go again)
elseif (not (member ch
(member ch
'(#\space
#\tab))))
then ; begin value part
(setq beginv next)
(setq state 4)))
(3 ; reading the header
(if* (eq ch #\linefeed)
then ; bogus header line, ignore
|
ef11d1c0 |
(setq state 1)
|
8b09f124 |
(go again)
elseif (eq ch #\:)
then (setq header
(subseq text beginh next))
(setq state 2)))
(4 ; looking for the end of the value
(if* (eq ch #\linefeed)
then (setq value
(subseq text beginv
(if* (eq #\return
(char text
(1- next)))
then (1- next)
else next)))
(incf next)
(return (if* header
then :start
|
f8eb1c74 |
else :continue))))))
(incf next)))))))
|
8b09f124 |
(loop ; for each header line
(setq header nil)
(if* (eq :eof (setq kind (next-header-line)))
then (return))
(case kind
(:start (push (cons header value) headers))
(:continue
(if* headers
then ; append to previous one
(setf (cdr (car headers))
(concatenate 'string (cdr (car headers))
" "
|
eaa80812 |
value)))))))
|
539b7bff |
(values headers
(subseq text next end))))
|
eaa80812 |
(defun make-envelope-from-text (text)
;; given at least the headers part of a message return
;; an envelope structure containing the contents
;; This is useful for parsing the headers of things returned by
;; a pop server
;;
(let ((headers (parse-mail-header text)))
(make-envelope
:date (cdr (assoc "date" headers :test #'equalp))
:subject (cdr (assoc "subject" headers :test #'equalp))
:from (cdr (assoc "from" headers :test #'equalp))
:sender (cdr (assoc "sender" headers :test #'equalp))
:reply-to (cdr (assoc "reply-to" headers :test #'equalp))
:to (cdr (assoc "to" headers :test #'equalp))
:cc (cdr (assoc "cc" headers :test #'equalp))
:bcc (cdr (assoc "bcc" headers :test #'equalp))
:in-reply-to (cdr (assoc "in-reply-to" headers :test #'equalp))
:message-id (cdr (assoc "message-id" headers :test #'equalp))
)))
|
8b09f124 |
|
58857608 |
(defmethod get-and-parse-from-imap-server ((mb imap-mailbox))
|
8b09f124 |
;; read the next line and parse it
;;
|
58857608 |
;;
(multiple-value-bind (line count)
(get-line-from-server mb)
(if* *debug-imap*
|
925381dd |
then (format t "from server: ")
|
58857608 |
(dotimes (i count)(write-char (schar line i)))
|
925381dd |
(terpri)
(force-output))
|
58857608 |
(parse-imap-response line count)
))
|
6fa1b4ab |
(defmethod get-and-parse-from-pop-server ((mb pop-mailbox))
;; read the next line from the pop server
|
8b09f124 |
;;
;; return 3 values:
;; :ok or :error
;; a list of rest of the tokens on the line
;; the whole line after the +ok or -err
|
6fa1b4ab |
(multiple-value-bind (line count)
(get-line-from-server mb)
(if* *debug-imap*
then (format t "from server: " count)
(dotimes (i count)(write-char (schar line i)))
(terpri))
(parse-pop-response line count)))
|
58857608 |
;; Parse and return the data from each line
;; values returned
;; tag -- either a string or the symbol :untagged
;; command -- a keyword symbol naming the command, like :ok
;; count -- a number which preceeded the command, or nil if
;; there wasn't a command
;; bracketted - a list of objects found in []'s after the command
;; or in ()'s after the command or sometimes just
;; out in the open after the command (like the search)
|
90494367 |
;; comment -- the whole of the part after the command
|
58857608 |
;;
(defun parse-imap-response (line end)
(let (kind value next
|
90494367 |
tag count command extra-data
comment)
|
58857608 |
;; get tag
(multiple-value-setq (kind value next)
(get-next-token line 0 end))
(case kind
(:string (setq tag (if* (equal value "*")
then :untagged
else value)))
|
90494367 |
(t (po-error :unexpected
:format-control "Illegal tag on response: ~s"
:format-arguments (list (subseq line 0 count))
:server-string (subseq line 0 end)
)))
|
58857608 |
;; get command
(multiple-value-setq (kind value next)
(get-next-token line next end))
(tagbody again
(case kind
(:number (setq count value)
(multiple-value-setq (kind value next)
(get-next-token line next end))
(go again))
(:string (setq command (kwd-intern value)))
|
90494367 |
(t (po-error :unexpected
:format-control "Illegal command on response: ~s"
:format-arguments (list (subseq line 0 count))
:server-string (subseq line 0 end)))))
(setq comment (subseq line next end))
|
58857608 |
;; now the part after the command... this gets tricky
(loop
(multiple-value-setq (kind value next)
(get-next-token line next end))
(case kind
((:lbracket :lparen)
(multiple-value-setq (kind value next)
(get-next-sexpr line (1- next) end))
(case kind
(:sexpr (push value extra-data))
|
90494367 |
(t (po-error :syntax-error :format-control "bad sexpr form"))))
|
58857608 |
(:eof (return nil))
((:number :string :nil) (push value extra-data))
(t ; should never happen
(return)))
(if* (not (member command '(:list :search) :test #'eq))
then ; only one item returned
(setq extra-data (car extra-data))
(return)))
(if* (member command '(:list :search) :test #'eq)
then (setq extra-data (nreverse extra-data)))
|
90494367 |
(values tag command count extra-data comment)))
|
58857608 |
(defun get-next-sexpr (line start end)
;; read a whole s-expression
;; return 3 values
;; kind -- :sexpr or :rparen or :rbracket
;; value - the sexpr value
;; next - next charpos to scan
;;
(let ( kind value next)
(multiple-value-setq (kind value next) (get-next-token line start end))
(case kind
((:string :number :nil)
|
90494367 |
(values :sexpr value next))
(:eof (po-error :syntax-error
:format-control "eof inside sexpr"))
|
58857608 |
((:lbracket :lparen)
(let (res)
(loop
(multiple-value-setq (kind value next)
(get-next-sexpr line next end))
(case kind
(:sexpr (push value res))
((:rparen :rbracket)
(return (values :sexpr (nreverse res) next)))
|
90494367 |
(t (po-error :syntax-error
:format-control "bad sexpression"))))))
|
58857608 |
((:rbracket :rparen)
(values kind nil next))
|
90494367 |
(t (po-error :syntax-error
:format-control "bad sexpression")))))
|
58857608 |
|
6fa1b4ab |
(defun parse-pop-response (line end)
|
8b09f124 |
;; return 3 values:
|
6fa1b4ab |
;; :ok or :error
|
8b09f124 |
;; a list of rest of the tokens on the line, the tokens
;; being either strings or integers
|
6fa1b4ab |
;; the whole line after the +ok or -err
;;
(let (res lineres result)
(multiple-value-bind (kind value next)
(get-next-token line 0 end)
(case kind
(:string (setq result (if* (equal "+OK" value)
then :ok
else :error)))
|
90494367 |
(t (po-error :unexpected
:format-control "bad response from server"
:server-string (subseq line 0 end))))
|
58857608 |
|
6fa1b4ab |
(setq lineres (subseq line next end))
(loop
(multiple-value-setq (kind value next)
(get-next-token line next end))
(case kind
(:eof (return))
((:string :number) (push value res))))
(values result (nreverse res) lineres))))
|
58857608 |
(defparameter *char-to-kind*
(let ((arr (make-array 256 :initial-element nil)))
(do ((i #.(char-code #\0) (1+ i)))
((> i #.(char-code #\9)))
(setf (aref arr i) :number))
(setf (aref arr #.(char-code #\space)) :space)
(setf (aref arr #.(char-code #\tab)) :space)
|
8b09f124 |
(setf (aref arr #.(char-code #\return)) :space)
(setf (aref arr #.(char-code #\linefeed)) :space)
|
58857608 |
(setf (aref arr #.(char-code #\[)) :lbracket)
(setf (aref arr #.(char-code #\])) :rbracket)
(setf (aref arr #.(char-code #\()) :lparen)
(setf (aref arr #.(char-code #\))) :rparen)
(setf (aref arr #.(char-code #\")) :dquote)
(setf (aref arr #.(char-code #\^b)) :big-string) ; our own invention
arr))
(defun get-next-token (line start end)
;; scan past whitespace for the next token
;; return three values:
;; kind: :string , :number, :eof, :lbracket, :rbracket,
;; :lparen, :rparen
;; value: the value, either a string or number or nil
;; next: the character pos to start scanning for the next token
;;
(let (ch chkind colstart (count 0) (state :looking)
collector right-bracket-is-normal)
(loop
; pick up the next character
(if* (>= start end)
then (if* (eq state :looking)
then (return (values :eof nil start))
else (setq ch #\space))
else (setq ch (schar line start)))
(setq chkind (aref *char-to-kind* (char-code ch)))
(case state
(:looking
(case chkind
(:space nil)
(:number (setq state :number)
(setq colstart start)
(setq count (- (char-code ch) #.(char-code #\0))))
((:lbracket :lparen :rbracket :rparen)
(return (values chkind nil (1+ start))))
(:dquote
(setq collector (make-array 10
:element-type 'character
:adjustable t
:fill-pointer 0))
(setq state :qstring))
(:big-string
(setq colstart (1+ start))
(setq state :big-string))
(t (setq colstart start)
(setq state :literal))))
(:number
(case chkind
((:space :lbracket :lparen :rbracket :rparen
:dquote) ; end of number
(return (values :number count start)))
(:number ; more number
(setq count (+ (* count 10)
(- (char-code ch) #.(char-code #\0)))))
(t ; turn into an literal
(setq state :literal))))
(:literal
(case chkind
((:space :rbracket :lparen :rparen :dquote) ; end of literal
(if* (and (eq chkind :rbracket)
right-bracket-is-normal)
then nil ; don't stop now
else (let ((seq (subseq line colstart start)))
(if* (equal "NIL" seq)
then (return (values :nil
nil
start))
else (return (values :string
seq
start))))))
(t (if* (eq chkind :lbracket)
then ; imbedded left bracket so right bracket isn't
; a break char
(setq right-bracket-is-normal t))
nil)))
(:qstring
;; quoted string
; (format t "start is ~s kind is ~s~%" start chkind)
(case chkind
(:dquote
;; end of string
(return (values :string collector (1+ start))))
(t (if* (eq ch #\\)
then ; escaping the next character
(incf start)
(if* (>= start end)
|
90494367 |
then (po-error :unexpected
:format-control "eof in string returned"))
|
58857608 |
(setq ch (schar line start)))
(vector-push-extend ch collector)
(if* (>= start end)
then ; we overran the end of the input
|
90494367 |
(po-error :unexpected
:format-control "eof in string returned")))))
|
58857608 |
(:big-string
;; super string... just a block of data
; (format t "start is ~s kind is ~s~%" start chkind)
(case chkind
(:big-string
;; end of string
(return (values :string
(subseq line colstart start)
(1+ start))))
(t nil)))
)
(incf start))))
|
2f95fd31 |
; this used to be exported from the excl package
#+(version>= 6 0)
(defvar *keyword-package* (find-package :keyword))
|
9acef702 |
(defun kwd-intern-possible-list (form)
(if* (null form)
then nil
elseif (atom form)
then (kwd-intern form)
else (mapcar #'kwd-intern-possible-list form)))
|
58857608 |
(defun kwd-intern (string)
;; convert the string to the current preferred case
;; and then intern
(intern (case excl::*current-case-mode*
((:case-sensitive-lower
:case-insensitive-lower) (string-downcase string))
(t (string-upcase string)))
*keyword-package*))
;; low level i/o to server
(defun get-line-from-server (mailbox)
;; Return two values: a buffer and a character count.
;; The character count includes up to but excluding the cr lf that
;; was read from the socket.
;;
(let* ((buff (get-line-buffer 0))
(len (length buff))
(i 0)
|
6fa1b4ab |
(p (post-office-socket mailbox))
|
58857608 |
(ch nil)
(whole-count)
)
|
90494367 |
(handler-case
(flet ((grow-buffer (size)
(let ((newbuff (get-line-buffer size)))
(dotimes (j i)
(setf (schar newbuff j) (schar buff j)))
(free-line-buffer buff)
(setq buff newbuff)
(setq len (length buff)))))
|
58857608 |
|
90494367 |
;; increase the buffer to at least size
;; this is somewhat complex to ensure that we aren't doing
;; buffer allocation within the with-timeout form, since
;; that could trigger a gc which could then cause the
;; with-timeout form to expire.
(loop
|
58857608 |
|
90494367 |
(if* whole-count
then ; we should now read in this may bytes and
; append it to this buffer
(multiple-value-bind (ans this-count)
(get-block-of-data-from-server mailbox whole-count)
; now put this data in the current buffer
(if* (> (+ i whole-count 5) len)
then ; grow the initial buffer
(grow-buffer (+ i whole-count 100)))
|
58857608 |
|
90494367 |
(dotimes (ind this-count)
(setf (schar buff i) (schar ans ind))
(incf i))
(setf (schar buff i) #\^b) ; end of inset string
(incf i)
(free-line-buffer ans)
|
64f441e3 |
(setq whole-count nil)
|
90494367 |
)
elseif ch
then ; we're growing the buffer holding the line data
(grow-buffer (+ len 200))
(setf (schar buff i) ch)
|
58857608 |
(incf i))
|
90494367 |
|
64f441e3 |
|
90494367 |
(block timeout
(mp:with-timeout ((timeout mailbox)
(po-error :timeout
:format-control "imap server failed to respond"))
;; read up to lf (lf most likely preceeded by cr)
(loop
(setq ch (read-char p))
(if* (eq #\linefeed ch)
then ; end of line. Don't save the return
(if* (and (> i 0)
(eq (schar buff (1- i)) #\return))
then ; remove #\return, replace with newline
(decf i)
(setf (schar buff i) #\newline)
)
;; must check for an extended return value which
;; is indicated by a {nnn} at the end of the line
(block count-check
(let ((ind (1- i)))
(if* (and (>= i 0) (eq (schar buff ind) #\}))
then (let ((count 0)
(mult 1))
(loop
(decf ind)
(if* (< ind 0)
then ; no of the form {nnn}
(return-from count-check))
(setf ch (schar buff ind))
(if* (eq ch #\{)
then ; must now read that many bytes
(setf (schar buff ind) #\^b)
(setq whole-count count)
(setq i (1+ ind))
(return-from timeout)
elseif (<= #.(char-code #\0)
(char-code ch)
#.(char-code #\9))
then ; is a digit
(setq count
(+ count
(* mult
(- (char-code ch)
#.(char-code #\0)))))
(setq mult (* 10 mult))
else ; invalid form, get out
(return-from count-check)))))))
|
58857608 |
|
90494367 |
(return-from get-line-from-server
(values buff i))
else ; save character
(if* (>= i len)
then ; need bigger buffer
(return))
(setf (schar buff i) ch)
(incf i)))))))
(error (con)
;; most likely error is that the server went away
(ignore-errors (close p))
(po-error :server-shutdown-connection
:format-control "condition signalled: ~a~%most likely server shut down the connection."
:format-arguments (list con)))
)))
|
58857608 |
(defun get-block-of-data-from-server (mb count &key save-returns)
;; read count bytes from the server returning it in a line buffer object
;; return as a second value the number of characters saved
|
9705345d |
;; (we drop #\return's so that lines are separated by a #\newline
|
58857608 |
;; like lisp likes).
;;
(let ((buff (get-line-buffer count))
|
6fa1b4ab |
(p (post-office-socket mb))
|
58857608 |
(ind 0))
(mp:with-timeout ((timeout mb)
|
90494367 |
(po-error :timeout
:format-control "imap server timed out"))
|
58857608 |
(dotimes (i count)
(if* (eq #\return (setf (schar buff ind) (read-char p)))
then (if* save-returns then (incf ind)) ; drop #\returns
else (incf ind)))
(values buff ind))))
;;-- reusable line buffers
(defvar *line-buffers* nil)
|
f13625a5 |
#+(version>= 8 1)
|
fc164f43 |
(defvar *line-buffers-lock* (make-basic-lock :name "line-buffers"))
|
58857608 |
|
f13625a5 |
(defmacro with-locked-line-buffers (&rest body)
#+(version>= 8 1)
`(with-locked-structure (*line-buffers-lock*
:non-smp :without-scheduling)
,@body)
#-(version>= 8 1)
|
5c445f02 |
`(sys::without-scheduling ,@body)
|
f13625a5 |
)
|
58857608 |
(defun get-line-buffer (size)
;; get a buffer of at least size bytes
|
8559c73a |
(setq size (min size (1- array-total-size-limit)))
|
fc164f43 |
(let ((found
|
f13625a5 |
(with-locked-line-buffers
|
fc164f43 |
(dolist (buff *line-buffers*)
(if* (>= (length buff) size)
then ;; use this one
(setq *line-buffers* (delete buff *line-buffers*))
(return buff))))))
(or found (make-string size))))
|
58857608 |
(defun free-line-buffer (buff)
|
f13625a5 |
(with-locked-line-buffers
|
58857608 |
(push buff *line-buffers*)))
|
8b09f124 |
(defun init-line-buffer (new old)
;; copy old into new
(declare (optimize (speed 3)))
(dotimes (i (length old))
(declare (fixnum i))
(setf (schar new i) (schar old i))))
|
58857608 |
|
8b09f124 |
;;;;;;;
|
58857608 |
; date functions
(defun universal-time-to-rfc822-date (ut)
;; convert a lisp universal time to rfc 822 date
;;
(multiple-value-bind
(sec min hour date month year day-of-week dsp time-zone)
(decode-universal-time ut 0)
(declare (ignore time-zone sec min hour day-of-week dsp time-zone))
(format nil "~d-~a-~d"
date
(svref
'#(nil "Jan" "Feb" "Mar" "Apr" "May" "Jun"
"Jul" "Aug" "Sep" "Oct" "Nov" "Dec")
month
)
year)))
|
9705345d |
;; utility
(defmacro with-imap-connection ((mb &rest options) &body body)
`(let ((,mb (make-imap-connection ,@options)))
(unwind-protect
(progn
,@body)
(close-connection ,mb))))
(defmacro with-pop-connection ((mb &rest options) &body body)
`(let ((,mb (make-pop-connection ,@options)))
(unwind-protect
(progn
,@body)
(close-connection ,mb))))
|