git.fiddlerwoaroof.com
imap.lisp
36b59036
 ;; -*- mode: common-lisp; package: net.post-office -*-
 ;;
 ;; imap.cl
 ;; imap and pop interface
 ;;
 ;; copyright (c) 1999-2002 Franz Inc, Berkeley, CA - All rights reserved.
32ad9729
 ;; copyright (c) 2002-2012 Franz Inc, Oakland, CA - All rights reserved.
36b59036
 ;;
 ;; This code is free software; you can redistribute it and/or
 ;; modify it under the terms of the version 2.1 of
0f9b7410
 ;; the GNU Lesser General Public License as published by
36b59036
 ;; the Free Software Foundation, as clarified by the AllegroServe
 ;; prequel found in license-allegroserve.txt.
 ;;
 ;; This code is distributed in the hope that it will be useful,
 ;; but without any warranty; without even the implied warranty of
 ;; merchantability or fitness for a particular purpose.  See the GNU
 ;; Lesser General Public License for more details.
 ;;
 ;; $Id: imap.cl,v 1.32 2009/03/25 22:46:02 layer Exp $
 
 ;; Description:
 ;;- This code in this file obeys the Lisp Coding Standard found in
 ;;- http://www.franz.com/~jkf/coding_standards.html
 ;;-
 
 
 (defpackage :net.post-office
89818e18
   (:nicknames #:post-office)
   (:use :cl :excl)
0f9b7410
   (:export
36b59036
    #:address-name
    #:address-additional
    #:address-mailbox
    #:address-host
0f9b7410
 
36b59036
    #:alter-flags
    #:close-connection
    #:close-mailbox
    #:copy-to-mailbox
    #:create-mailbox
    #:delete-letter
    #:delete-mailbox
0f9b7410
 
36b59036
    #:envelope-date
    #:envelope-subject
    #:envelope-from
    #:envelope-sender
    #:envelope-reply-to
    #:envelope-to
    #:envelope-cc
    #:envelope-bcc
    #:envelope-in-reply-to
    #:envelope-message-id
0f9b7410
 
36b59036
    #:expunge-mailbox
    #:fetch-field
    #:fetch-letter
    #:fetch-letter-sequence
    #:end-of-letter-p
    #:with-fetch-letter-sequence
    #:fetch-parts
    #:*imap-version-number*
    #:make-envelope-from-text
    #: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
    #:mailbox-uidnext
    #:make-imap-connection
    #:make-pop-connection
    #:with-imap-connection
    #:with-pop-connection
    #:noop
    #:parse-mail-header
3f9f016a
    #:top-lines  ; pop only
36b59036
    #:unique-id  ; pop only
0f9b7410
 
36b59036
    #:po-condition
    #:po-condition-identifier
    #:po-condition-server-string
    #:po-error
0f9b7410
 
36b59036
    #:rename-mailbox
    #:reset-mailbox
    #:search-mailbox
    #:select-mailbox
0f9b7410
 
36b59036
    )
   )
 
 (in-package :net.post-office)
 
 (provide :imap)
 
 (defparameter *imap-version-number* '(:major 1 :minor 14)) ; major.minor
 
 ;; todo
 ;;  have the list of tags selected done on a per connection basis to
 ;;  eliminate any possible multithreading problems
 ;;
 ;;
 
 (defvar *debug-imap* nil)
 
 
 
 
 
 (defclass post-office ()
   ((socket :initarg :socket
3f9f016a
            :accessor post-office-socket)
0f9b7410
 
36b59036
    (host :initarg :host
3f9f016a
          :accessor  post-office-host
          :initform nil)
36b59036
    (user  :initarg :user
3f9f016a
           :accessor post-office-user
           :initform nil)
0f9b7410
 
36b59036
    (state :accessor post-office-state
3f9f016a
           :initarg :state
           :initform :unconnected)
0f9b7410
 
    (timeout
36b59036
     ;; time to wait for network activity for actions that should
     ;; happen very quickly when things are operating normally
     :initarg :timeout
     :initform 60
0f9b7410
     :accessor timeout)
36b59036
   ))
 
 (defclass imap-mailbox (post-office)
   ((mailbox-name   ; currently selected mailbox
     :accessor mailbox-name
     :initform nil)
 
0f9b7410
    (separator
36b59036
     ;; string that separates mailbox names in the hierarchy
     :accessor mailbox-separator
     :initform "")
0f9b7410
 
36b59036
    ;;; these slots hold information about the currently selected mailbox:
0f9b7410
 
36b59036
     (message-count  ; how many in the mailbox
     :accessor mailbox-message-count
     :initform 0)
0f9b7410
 
36b59036
    (recent-messages ; how many messages since we last checked
     :accessor mailbox-recent-messages
     :initform 0)
0f9b7410
 
36b59036
    (uidvalidity  ; used to denote messages uniquely
0f9b7410
     :accessor mailbox-uidvalidity
36b59036
     :initform 0)
0f9b7410
 
    (uidnext
36b59036
     :accessor mailbox-uidnext ;; predicted next uid
     :initform 0)
0f9b7410
 
3f9f016a
    (flags       ; list of flags that can be stored in a message
0f9b7410
     :accessor mailbox-flags
36b59036
     :initform nil)
0f9b7410
 
36b59036
    (permanent-flags  ; list of flags that be stored permanently
     :accessor mailbox-permanent-flags
     :initform nil)
0f9b7410
 
36b59036
    (first-unseen   ; number of the first unseen message
     :accessor first-unseen
     :initform 0)
0f9b7410
 
36b59036
    ;;; end list of values for the currently selected mailbox
0f9b7410
 
36b59036
    ;;; state information for fetch-letter-sequence
0f9b7410
    (fetch-letter-offset
36b59036
     :accessor fetch-letter-offset)
0f9b7410
    (fetch-letter-number
36b59036
     :accessor fetch-letter-number)
    (fetch-letter-uid
     :accessor fetch-letter-uid)
    (fetch-letter-finished
     :accessor fetch-letter-finished)
    )
   )
 
 
 (defclass pop-mailbox (post-office)
   ((message-count  ; how many in the mailbox
     :accessor mailbox-message-count
     :initform 0)
0f9b7410
    (fetch-letter-state
36b59036
     :accessor state
     :initform :invalid)))
0f9b7410
 
36b59036
 
 
 
 (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
3f9f016a
   host     ;; the name of the machine
36b59036
   )
 
 
 
 ;--------------------------------
 ; 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).
 ;
0f9b7410
 ; A condition will have a server-string value if it as initiated by
36b59036
 ; something returned by the server.
0f9b7410
 ; A condition will have a format-control value if we want to display
 ; something we generated in response to
 ;
36b59036
 ;
 ;
 ;; identifiers used in conditions/errors
 
 ; :problem  condition
3f9f016a
 ;       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).
0f9b7410
 ;
36b59036
 ; :unknown-ok   condition
3f9f016a
 ;       the server responded with an 'ok' followed by something
 ;       we don't recognize.  It's probably safe to ignore this.
36b59036
 ;
 ;  :unknown-untagged condition
3f9f016a
 ;       the server responded with some untagged command we don't
 ;       recognize.  it's probaby ok to ignore this.
36b59036
 ;
 ;  :error-response  error
3f9f016a
 ;       the command failed.
36b59036
 ;
 ;  :syntax-error   error
3f9f016a
 ;       the data passed to a function in this interface was malformed
36b59036
 ;
 ;  :unexpected    error
3f9f016a
 ;       the server responded an unexpected way.
36b59036
 ;
 ;  :server-shutdown-connection error
3f9f016a
 ;       the server has shut down the connection, don't attempt to
36b59036
 ;       send any more commands to this connection, or even close it.
 ;
 ;  :timeout  error
3f9f016a
 ;       server failed to respond within the timeout period
36b59036
 ;
 ;  :response-too-large error
3f9f016a
 ;       contents of a response is too large to store in a Lisp array.
36b59036
 
 
 ;; conditions
 (define-condition po-condition ()
   ;; used to notify user of things that shouldn't necessarily stop
   ;; program flow
0f9b7410
   ((identifier
36b59036
     ;; keyword identifying the error (or :unknown)
0f9b7410
     :reader po-condition-identifier
36b59036
     :initform :unknown
996cc728
     :initarg :identifier)
0f9b7410
    (server-string
36b59036
     ;; message from the imap server
     :reader po-condition-server-string
     :initform ""
996cc728
     :initarg :server-string)
    (message
     :reader po-condition-message
     :initform ""
     :initarg :message)
    (arguments
     :reader po-condition-arguments
     :initform nil
     :initarg :arguments))
   
36b59036
   (:report
    (lambda (con stream)
      (with-slots (identifier server-string) con
0f9b7410
        ;; a condition either has a server-string or it has a
36b59036
        ;; format-control string
        (format stream "Post Office condition: ~s~%" identifier)
996cc728
        (unless (string= (po-condition-message con)
                         "")
          (apply #'format stream
                 (po-condition-message con)
                 (po-condition-arguments con)))
        (when server-string
          (format stream
                  "~&Message from server: ~s"
                  (string-left-trim " " server-string)))))))
36b59036
 
0f9b7410
 
 
 (define-condition po-error (po-condition error)
36b59036
   ;; used to denote things that should stop program flow
   ())
 
 
 
 ;; aignalling the conditions
 
0f9b7410
 (defun po-condition (identifier &key server-string format-control
3f9f016a
                           format-arguments)
36b59036
   (signal (make-instance 'po-condition
3f9f016a
             :identifier identifier
             :server-string server-string
996cc728
             :message format-control
             :arguments format-arguments)))
0f9b7410
 
36b59036
 (defun po-error (identifier &key server-string
3f9f016a
                       format-control format-arguments)
36b59036
   (error (make-instance 'po-error
3f9f016a
             :identifier identifier
             :server-string server-string
996cc728
             :message format-control
             :arguments format-arguments)))
36b59036
 
0f9b7410
 
36b59036
 
 ;----------------------------------------------
 
 
 
 
 
 
 (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))
 
 ;; 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)))
3f9f016a
              (server-port (ssl type) `(cond ((eq ,type :imap) (if ,ssl 993 143))
                                             ((eq ,type :pop) (if ,ssl 995 110)))))
36b59036
     (let* ((server (car server-info))
3f9f016a
            (ssl-args (cdr server-info))
            ssl port starttls sock)
36b59036
       (setq ssl (pop-keyword :ssl ssl-args))
996cc728
       (setq port (or (pop-keyword :port ssl-args)
                      (server-port ssl server-type)))
36b59036
       (setq starttls (pop-keyword :starttls ssl-args))
       (setq sock (socket:make-socket :remote-host server
3f9f016a
                                      :remote-port port))
36b59036
       (when ssl
996cc728
         (setq sock (apply #'cl+ssl:make-ssl-client-stream
                           sock
                           :external-format :iso-8859-1
                           ssl-args)))
0f9b7410
 
36b59036
       (values sock starttls))) )
 
0f9b7410
 (defun make-imap-connection (host &key (port 143)
3f9f016a
                                        user
                                        password
                                        (timeout 30))
36b59036
   (multiple-value-bind (sock starttls)
       (if (consp host)
3f9f016a
           (connect-to-imap/pop-server host :imap)
         (socket:make-socket :remote-host host :remote-port port))
36b59036
     (let ((imap (make-instance 'imap-mailbox
3f9f016a
                   :socket sock
                   :host   host
                   :timeout timeout
                   :state :unauthorized)))
0f9b7410
 
36b59036
     (multiple-value-bind (tag cmd count extra comment)
3f9f016a
         (get-and-parse-from-imap-server imap)
36b59036
       (declare (ignorable cmd count extra))
       (if* (not (eq :untagged tag))
3f9f016a
          then  (po-error :error-response
                          :server-string comment)))
0f9b7410
 
36b59036
     ; check for starttls negotiation
     (when starttls
       (let (capabilities)
3f9f016a
         (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)))))))
36b59036
 
     ; now login
0f9b7410
     (send-command-get-results imap
3f9f016a
                               (format nil "login ~a ~a" user password)
                               #'handle-untagged-response
                               #'(lambda (mb command count extra comment)
                                   (check-for-success mb command count extra
                                                      comment
                                                      "login")))
0f9b7410
 
36b59036
     ; find the separator character
     (let ((res (mailbox-list imap)))
0f9b7410
       ;;
36b59036
       (let ((sep (cadr  (car res))))
3f9f016a
         (if* sep
            then (setf (mailbox-separator imap) sep))))
0f9b7410
 
 
 
36b59036
     imap)))
 
 
 (defmethod close-connection ((mb imap-mailbox))
0f9b7410
 
36b59036
   (let ((sock (post-office-socket mb)))
     (if* sock
        then (ignore-errors
3f9f016a
              (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)
               #'(lambda (mb command count extra comment)
                   (check-for-success mb command count extra
                                      comment
                                      "logout")))))
36b59036
     (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
3f9f016a
              (send-pop-command-get-results
               pb
               "QUIT")))
36b59036
     (setf (post-office-socket pb) nil)
     (if* sock then (ignore-errors (close sock)))
     t))
 
 
 
 (defun make-pop-connection (host &key (port 110)
3f9f016a
                                       user
                                       password
                                       (timeout 30))
36b59036
   (multiple-value-bind (sock starttls)
       (if (consp host)
3f9f016a
           (connect-to-imap/pop-server host :pop)
         (socket:make-socket :remote-host host :remote-port port))
36b59036
     (let ((pop (make-instance 'pop-mailbox
3f9f016a
                 :socket sock
                 :host   host
                 :timeout timeout
                 :state :unauthorized)))
0f9b7410
 
36b59036
     (multiple-value-bind (result)
3f9f016a
         (get-and-parse-from-pop-server pop)
36b59036
       (if* (not (eq :ok result))
3f9f016a
          then  (po-error :error-response
                          :format-control
                          "unexpected line from server after connect")))
0f9b7410
 
36b59036
     ; check for starttls negotiation
     (when starttls
       (let ((capabilities (send-pop-command-get-results pop "capa" t)))
3f9f016a
         (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)))))
0f9b7410
 
36b59036
     ; 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)))
0f9b7410
 
 
 
36b59036
     pop)))
 
0f9b7410
 
 (defmethod send-command-get-results ((mb imap-mailbox)
3f9f016a
                                      command untagged-handler tagged-handler)
36b59036
   ;; send a command and retrieve results until we get the tagged
   ;; response for the command we sent
   ;;
   (let ((tag (get-next-tag)))
     (format (post-office-socket mb)
3f9f016a
             "~a ~a~a" tag command *crlf*)
36b59036
     (force-output (post-office-socket mb))
0f9b7410
 
996cc728
     (when *debug-imap*
       (format t
               "~a ~a~a" tag command *crlf*)
       (force-output))
     
36b59036
     (loop
996cc728
        (multiple-value-bind (got-tag cmd count extra comment)
            (get-and-parse-from-imap-server mb)
          (if* (eq got-tag :untagged)
             then (funcall untagged-handler mb cmd count extra comment)
           elseif (equal tag got-tag)
             then (funcall tagged-handler mb cmd count extra comment)
                  (return)
             else (po-error :error-response
                            :format-control "received tag ~s out of order"
                            :format-arguments (list got-tag)
                            :server-string comment))))))
36b59036
 
 
 (defun get-next-tag ()
   (let ((tag (pop *cur-imap-tags*)))
     (if*  tag
        thenret
        else (setq *cur-imap-tags* *imap-tags*)
3f9f016a
             (pop *cur-imap-tags*))))
36b59036
 
 (defun handle-untagged-response (mb command count extra comment)
0f9b7410
   ;; default function to handle untagged responses, which are
36b59036
   ;; 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))
     (:flags  (setf (mailbox-flags mb) (kwd-intern-possible-list extra)))
     (:bye ; occurs when connection times out or mailbox lock is stolen
      (ignore-errors (close (post-office-socket mb)))
      (po-error :server-shutdown-connection
3f9f016a
                  :server-string "server shut down the connection"))
36b59036
     (:no ; used when grabbing a lock from another process
      (po-condition :problem :server-string comment))
     (:ok ; a whole variety of things
      (if* extra
3f9f016a
         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)
                        (kwd-intern-possible-list (cadr extra)))
                 else (po-condition :unknown-ok :server-string comment))))
36b59036
     (t (po-condition :unknown-untagged :server-string comment)))
0f9b7410
 
36b59036
   )
 
 
 (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)
3f9f016a
         (outpos start)
         (sock (post-office-socket mb))
         ch
         stop)
0f9b7410
     (macrolet ((add-to-buffer ()
3f9f016a
                  `(progn
                     (setf (schar buffer outpos) ch)
                     (incf outpos))))
36b59036
       (while (and (< inpos end) (/= (state mb) 4))
3f9f016a
         (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))
36b59036
       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
3f9f016a
            (progn
              ,@body)
          ;; cleanup
          (end-extended-results-sequence ,mb)))))
36b59036
 
 
0f9b7410
 
36b59036
 
 (defun send-pop-command-get-results (pop command &optional extrap)
   (declare (optimize (speed 3) (safety 1)))
   ;; send the given command to the pop server
   ;; if extrap is true and if the response is +ok, then data
0f9b7410
   ;;  will follow the command (up to and excluding the first line consisting
36b59036
   ;;  of just a period)
0f9b7410
   ;;
36b59036
   ;; 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)
   ;;
   (format (post-office-socket pop) "~a~a" command *crlf*)
   (force-output (post-office-socket pop))
0f9b7410
 
36b59036
   (if* *debug-imap*
      then (format t "~a~a" command *crlf*)
3f9f016a
           (force-output t))
36b59036
 
   (multiple-value-bind (result parsed line)
       (get-and-parse-from-pop-server pop)
     (if* (not (eq result :ok))
        then (po-error :error-response
3f9f016a
                       :server-string line))
36b59036
 
     (if* extrap
        then ;; get the rest of the data
3f9f016a
             ;; 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.
             (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))))
               (prog1 (subseq buf 0 pos)
                 (free-line-buffer buf)))
36b59036
        else parsed)))
0f9b7410
 
36b59036
 
 
 
 (defun convert-flags-plist (plist)
0f9b7410
   ;; scan the plist looking for "flags" indicators and
36b59036
   ;; 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))))))
 
 
 (defmethod select-mailbox ((mb imap-mailbox) name)
   ;; select the given mailbox
   (send-command-get-results mb
3f9f016a
                             (format nil "select ~a" name)
                             #'handle-untagged-response
                             #'(lambda (mb command count extra comment)
                                 (declare (ignore mb count extra))
                                 (if* (not (eq command :ok))
                                    then (po-error
                                          :problem
                                          :format-control
                                          "imap mailbox select failed"
                                          :server-string comment))))
36b59036
   (setf (mailbox-name mb) name)
   t
   )
 
 
 (defmethod fetch-letter ((mb imap-mailbox) number &key uid)
   ;; return the whole letter
   (fetch-field number "body[]"
3f9f016a
                (fetch-parts mb number "body[]" :uid uid)
                :uid uid))
36b59036
 
 
 (defmethod fetch-letter ((pb pop-mailbox) number &key uid)
   (declare (ignore uid))
0f9b7410
   (send-pop-command-get-results pb
3f9f016a
                                 (format nil "RETR ~d" number)
                                 t ; extra stuff
                                 ))
36b59036
 
 (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))
 
 
 (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))
 
0f9b7410
 (defmethod fetch-letter-sequence ((mb imap-mailbox) buffer
3f9f016a
                                   &key (start 0) (end (length buffer)))
36b59036
   (let* ((num (fetch-letter-number mb))
3f9f016a
          (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)))
36b59036
 
     (setf (subseq buffer start end) data)
0f9b7410
 
36b59036
     (if* (and (> buflen 0) (= datalen 0))
        then (setf (fetch-letter-finished mb) t))
0f9b7410
 
36b59036
     (setf (fetch-letter-offset mb) (+ offset buflen))
0f9b7410
 
36b59036
     (+ start datalen)))
0f9b7410
 
36b59036
 
 (defmethod fetch-letter-sequence ((mb pop-mailbox) buffer &key (start 0) (end (length buffer)))
   (get-extended-results-sequence mb buffer :start start :end end))
 
 (defmethod end-fetch-letter-sequence ((mb imap-mailbox))
   )
 
 (defmethod end-fetch-letter-sequence ((mb pop-mailbox))
   (end-extended-results-sequence mb))
 
 (defmethod end-of-letter-p ((mb imap-mailbox))
   (fetch-letter-finished mb))
 
 (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
3f9f016a
            (progn
              ,@body)
          ;; cleanup
          (end-fetch-letter-sequence ,mb)))))
0f9b7410
 
36b59036
 (defmethod fetch-parts ((mb imap-mailbox) number parts &key uid)
   (let (res)
0f9b7410
     (send-command-get-results
36b59036
      mb
      (format nil "~afetch ~a ~a"
3f9f016a
              (if* uid then "uid " else "")
              (message-set-string number)
              (or parts "body[]")
              )
36b59036
      #'(lambda (mb command count extra comment)
3f9f016a
          (if* (eq command :fetch)
             then (push (list count (internalize-flags extra)) res)
             else (handle-untagged-response
                   mb command count extra comment)))
36b59036
      #'(lambda (mb command count extra comment)
3f9f016a
          (declare (ignore mb count extra))
          (if* (not (eq command :ok))
             then (po-error :problem
                            :format-control "imap mailbox fetch failed"
                            :server-string comment))))
36b59036
     res))
 
0f9b7410
 
36b59036
 (defun fetch-field (letter-number field-name info &key uid)
0f9b7410
   ;; given the information from a fetch-letter, return the
36b59036
   ;; 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
3f9f016a
          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))))
0f9b7410
 
36b59036
       (if* use-this
3f9f016a
          then (do ((xx (cadr item) (cddr xx)))
                   ((null xx))
                 (if* (equalp field-name (car xx))
                    then (return-from fetch-field (cadr xx))))))))
36b59036
 
0f9b7410
 
36b59036
 
 (defun internalize-flags (stuff)
0f9b7410
   ;; given a plist like object, look for items labelled "flags" and
36b59036
   ;; convert the contents to internal flags objects
   (do ((xx stuff (cddr xx)))
       ((null xx))
     (if* (equalp (car xx) "flags")
0f9b7410
        then ; we can end up with sublists of forms if we
3f9f016a
             ; 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)))
0f9b7410
 
36b59036
   stuff)
 
0f9b7410
 
36b59036
 
 
 (defmethod delete-letter ((mb imap-mailbox) messages &key (expunge t) uid)
0f9b7410
   ;; delete all the mesasges and do the expunge to make
36b59036
   ;; it permanent if expunge is true
   (alter-flags mb messages :add-flags :\\deleted :uid uid)
   (if* expunge then (expunge-mailbox mb)))
 
 (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))
0f9b7410
 
   (if* (or (numberp messages)
3f9f016a
            (and (consp messages) (eq :seq (car messages))))
36b59036
      then (setq messages (list messages)))
0f9b7410
 
36b59036
   (if* (not (consp messages))
      then (po-error :syntax-error
3f9f016a
                     :format-control "expect a mesage number or list of messages, not ~s"
                  :format-arguments (list messages)))
0f9b7410
 
36b59036
   (dolist (message messages)
     (if* (numberp message)
        then (send-pop-command-get-results pb
3f9f016a
                                           (format nil "DELE ~d" message))
36b59036
      elseif (and (consp message) (eq :seq (car message)))
        then (do ((start (cadr message) (1+ start))
3f9f016a
                  (end (caddr message)))
                 ((> start end))
               (send-pop-command-get-results pb
                                             (format nil "DELE ~d" start)))
36b59036
        else (po-error :syntax-error
3f9f016a
                       :format-control "bad message number ~s"
                       :format-arguments (list message)))))
0f9b7410
 
 
 
 
36b59036
 
 (defmethod noop ((mb imap-mailbox))
   ;; just poke the server... keeping it awake and checking for
   ;; new letters
   (send-command-get-results mb
3f9f016a
                             "noop"
                             #'handle-untagged-response
                             #'(lambda (mb command count extra comment)
                                 (check-for-success
                                  mb command count extra
                                  comment
                                  "noop"))))
36b59036
 
 
 (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)))
   )
 
 
 (defmethod unique-id ((pb pop-mailbox) &optional message)
   ;; if message is given, return the unique id of that
0f9b7410
   ;; message,
36b59036
   ;; 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
3f9f016a
                                                    (format nil
                                                            "UIDL ~d"
                                                            message))))
             (cadr res))
36b59036
      else ; get all of them
3f9f016a
           (let* ((res (send-pop-command-get-results pb "UIDL" t))
                  (end (length res))
                  kind
                  mnum
                  mid
                  (next 0))
0f9b7410
 
 
3f9f016a
             (let ((coll))
               (loop
                 (multiple-value-setq (kind mnum next)
                   (get-next-token res next end))
0f9b7410
 
3f9f016a
                 (if* (eq :eof kind) then (return))
0f9b7410
 
3f9f016a
                 (if* (not (eq :number kind))
                    then ; hmm. bogus
                         (po-error :unexpected
                                   :format-control "uidl returned illegal message number in ~s"
                                   :format-arguments (list res)))
0f9b7410
 
3f9f016a
                 ; now get message id
0f9b7410
 
3f9f016a
                 (multiple-value-setq (kind mid next)
                     (get-next-token res next end))
0f9b7410
 
3f9f016a
                 (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)))
0f9b7410
 
3f9f016a
                 (push (list mnum mid) coll))
0f9b7410
 
3f9f016a
               (nreverse coll)))))
36b59036
 
 (defmethod top-lines ((pb pop-mailbox) message lines)
   ;; return the header and the given number of top lines of the message
0f9b7410
 
36b59036
   (let ((res (send-pop-command-get-results pb
3f9f016a
                                            (format nil
                                                    "TOP ~d ~d"
                                                    message
                                                    lines)
                                            t ; extra
                                            )))
36b59036
     res))
0f9b7410
 
 
36b59036
 
 
 (defmethod reset-mailbox ((pb pop-mailbox))
   ;; undo's deletes
   (send-pop-command-get-results pb "RSET")
   )
0f9b7410
 
36b59036
 
 
 (defun check-for-success (mb command count extra comment command-string )
   (declare (ignore mb count extra))
   (if* (not (eq command :ok))
      then (po-error :error-response
3f9f016a
                     :format-control "imap ~a failed"
                     :format-arguments (list command-string)
                     :server-string comment)))
36b59036
 
0f9b7410
 
 
36b59036
 
 
 (defmethod mailbox-list ((mb imap-mailbox) &key (reference "") (pattern ""))
   ;; return a list of mailbox names with respect to a given
   (let (res)
     (send-command-get-results mb
3f9f016a
                               (format nil "list ~s ~s" reference pattern)
                               #'(lambda (mb command count extra comment)
                                   (if* (eq command :list)
                                      then (push extra res)
                                      else (handle-untagged-response
                                            mb command count extra
                                            comment)))
                               #'(lambda (mb command count extra comment)
                                   (check-for-success
                                    mb command count extra
                                    comment "list")))
0f9b7410
 
36b59036
     ;; the car of each list is a set of keywords, make that so
     (dolist (rr res)
       (setf (car rr) (mapcar #'kwd-intern (car rr))))
0f9b7410
 
36b59036
     res
0f9b7410
 
 
36b59036
     ))
 
 
 (defmethod create-mailbox ((mb imap-mailbox) mailbox-name)
   ;; create a mailbox name of the given name.
   ;; use mailbox-separator if you want to create a hierarchy
   (send-command-get-results mb
3f9f016a
                             (format nil "create ~s" mailbox-name)
                             #'handle-untagged-response
                             #'(lambda (mb command count extra comment)
                                   (check-for-success
                                    mb command count extra
                                    comment "create")))
36b59036
   t)
 
 
 (defmethod delete-mailbox ((mb imap-mailbox) mailbox-name)
   ;; create a mailbox name of the given name.
   ;; use mailbox-separator if you want to create a hierarchy
   (send-command-get-results mb
3f9f016a
                             (format nil "delete ~s" mailbox-name)
                             #'handle-untagged-response
                             #'(lambda (mb command count extra comment)
                                   (check-for-success
                                    mb command count extra
                                    comment "delete"))))
36b59036
 
 (defmethod rename-mailbox ((mb imap-mailbox) old-mailbox-name new-mailbox-name)
   ;; create a mailbox name of the given name.
   ;; use mailbox-separator if you want to create a hierarchy
   (send-command-get-results mb
3f9f016a
                             (format nil "rename ~s ~s"
                                     old-mailbox-name
                                     new-mailbox-name)
                             #'handle-untagged-response
                             #'(lambda (mb command count extra comment)
                                   (check-for-success
                                    mb command count extra
                                    comment
                                    "rename"))))
36b59036
 
 
 
 (defmethod alter-flags ((mb imap-mailbox)
3f9f016a
                         messages &key (flags nil flags-p)
                                       add-flags remove-flags
                                       silent uid)
36b59036
   ;;
   ;; 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))
0f9b7410
 
36b59036
     (if* (atom val) then (setq val (list val)))
0f9b7410
 
36b59036
     (send-command-get-results mb
3f9f016a
                               (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 "()"))
                               #'(lambda (mb command count extra comment)
                                   (if* (eq command :fetch)
                                      then (push (list count
                                                       (convert-flags-plist
                                                        extra))
                                                 res)
                                      else (handle-untagged-response
                                            mb command count extra
                                            comment)))
 
                               #'(lambda (mb command count extra comment)
                                   (check-for-success
                                    mb command count extra
                                    comment "store")))
36b59036
     res))
 
 
 (defun message-set-string (messages)
   ;; return a string that describes the messages which may be a
   ;; single number or a sequence of numbers
0f9b7410
 
36b59036
   (if* (atom messages)
      then (format nil "~a" messages)
      else (if* (and (consp messages)
3f9f016a
                     (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))
                          else (po-error :syntax-error
                                         :format-control "bad message list ~s"
                                         :format-arguments (list msg)))
                       (setq precomma t))
                     (get-output-stream-string str)))))
0f9b7410
 
 
 
 
 
 
36b59036
 (defmethod expunge-mailbox ((mb imap-mailbox))
   ;; remove messages marked as deleted
   (let (res)
     (send-command-get-results mb
3f9f016a
                               "expunge"
                               #'(lambda (mb command count extra
                                          comment)
                                   (if* (eq command :expunge)
                                      then (push count res)
                                      else (handle-untagged-response
                                            mb command count extra
                                            comment)))
                               #'(lambda (mb command count extra comment)
                                   (check-for-success
                                    mb command count extra
                                    comment "expunge")))
36b59036
     (nreverse res)))
0f9b7410
 
 
 
36b59036
 (defmethod close-mailbox ((mb imap-mailbox))
   ;; remove messages marked as deleted
   (send-command-get-results mb
3f9f016a
                             "close"
                             #'handle-untagged-response
0f9b7410
 
3f9f016a
                             #'(lambda (mb command count extra comment)
                                 (check-for-success
                                  mb command count extra
                                  comment "close")))
36b59036
   t)
0f9b7410
 
36b59036
 
 
 (defmethod copy-to-mailbox ((mb imap-mailbox) message-list destination
3f9f016a
                             &key uid)
36b59036
   (send-command-get-results mb
3f9f016a
                             (format nil "~acopy ~a ~s"
                                     (if* uid then "uid " else "")
                                     (message-set-string message-list)
                                     destination)
                             #'handle-untagged-response
                             #'(lambda (mb command count extra comment)
                                 (check-for-success
                                  mb command count extra
                                  comment "copy")))
36b59036
   t)
 
 
 ;; search command
 
 (defmethod search-mailbox ((mb imap-mailbox) search-expression &key uid)
   (let (res)
     (send-command-get-results mb
3f9f016a
                               (format nil "~asearch ~a"
                                       (if* uid then "uid " else "")
                                       (build-search-string search-expression))
                               #'(lambda (mb command count extra comment)
                                   (if* (eq command :search)
                                      then (setq res (append res extra))
                                      else (handle-untagged-response
                                            mb command count extra
                                            comment)))
                               #'(lambda (mb command count extra comment)
                                   (check-for-success
                                    mb command count extra
                                    comment "search")))
36b59036
     res))
0f9b7410
 
 
36b59036
 (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)))
3f9f016a
             (bss-int search str)
             (get-output-stream-string str))))
36b59036
 
 (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)
3f9f016a
              (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))
                   else (po-error :syntax-error
                                  :format-control "illegal set format ~s"
                                  :format-arguments (list val)))
                (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))
                      then (po-error :syntax-error
                                     :format-control "illegal value for date search ~s"
                                     :format-arguments (list val)))
                   ;; val is now a string
                   (format str " ~s" val))
                  (:number
 
                   (if* (not (integerp val))
                      then (po-error :syntax-error
                                     :format-control "illegal value for number in search ~s"
                                     :format-arguments (list val)))
                   (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)
                      else (po-error :syntax-error
                                     :format-control "illegal message set ~s"
                                     :format-arguments (list val))))
 
                  ))))
0f9b7410
 
36b59036
     (if* (symbolp search)
        then (if* (get search 'imap-search-no-args)
3f9f016a
                then (format str "~a"  (string-upcase
                                        (string search)))
                else (po-error :syntax-error
                               :format-control "illegal search word: ~s"
                               :format-arguments (list search)))
36b59036
      elseif (consp search)
        then (case (car search)
3f9f016a
               (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))
                       then (po-error :syntax-error
                                      :format-control "not takes one argument: ~s"
                                      :format-arguments (list search)))
                    (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
                                              (string (car search))))
                            (if* (not (equal (length (cdr search))
                                             (length arginfo)))
                               then (po-error :syntax-error
                                              :format-control "wrong number of arguments to ~s"
                                              :format-arguments search))
 
                            (arg-process str (cdr search) arginfo)
 
                     elseif (integerp (car search))
                       then (set-ify search str)
                       else (po-error :syntax-error
                                      :format-control "Illegal form ~s in search string"
                                      :format-arguments (list search))))))
36b59036
      elseif (integerp search)
        then ;  a message number
3f9f016a
             (format str "~s" search)
36b59036
        else (po-error :syntax-error
3f9f016a
                       :format-control "Illegal form ~s in search string"
                       :format-arguments (list search)))))
36b59036
 
 
 
 
 
0f9b7410
 (defun parse-mail-header (text)
36b59036
   ;; 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.
   (let ((next 0)
3f9f016a
         (end (length text))
         header
         value
         kind
         headers)
36b59036
     (labels ((next-header-line ()
3f9f016a
                ;; 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
 
                    (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
                                           (incf next)
                                           (if* header
                                              then ; header and no value
                                                   (setq value "")
                                                   (return :start))
                                           (setq state 1)
                                           (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
                                           (setq state 1)
                                           (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
                                                      else :continue))))))
                        (incf next)))))))
0f9b7410
 
 
 
36b59036
       (loop ; for each header line
3f9f016a
         (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))
                                   " "
                                   value)))))))
36b59036
     (values headers
3f9f016a
             (subseq text next end))))
36b59036
 
 
 (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)))
0f9b7410
 
36b59036
     (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))
      )))
 
 
 
 
 
0f9b7410
 
 
 
 
 
36b59036
 (defmethod get-and-parse-from-imap-server ((mb imap-mailbox))
   ;; read the next line and parse it
   ;;
   ;;
   (multiple-value-bind (line count)
       (get-line-from-server mb)
0f9b7410
     (if* *debug-imap*
36b59036
        then (format t "from server: ")
3f9f016a
             (dotimes (i count)(write-char (schar line i)))
             (terpri)
             (force-output))
0f9b7410
 
36b59036
     (parse-imap-response line count)
     ))
 
 
 
 (defmethod get-and-parse-from-pop-server ((mb pop-mailbox))
   ;; read the next line from the pop server
   ;;
   ;; return 3 values:
0f9b7410
   ;;   :ok or :error
36b59036
   ;;   a list of rest of the tokens on the line
   ;;   the whole line after the +ok or -err
 
   (multiple-value-bind (line count)
       (get-line-from-server mb)
0f9b7410
 
     (if* *debug-imap*
89818e18
        then (format t "from server: ")
3f9f016a
             (dotimes (i count)(write-char (schar line i)))
             (terpri))
0f9b7410
 
36b59036
     (parse-pop-response line count)))
 
0f9b7410
 
 
36b59036
 ;; 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
3f9f016a
 ;;           there wasn't a command
36b59036
 ;;  bracketted - a list of objects found in []'s after the command
0f9b7410
 ;;            or in ()'s after the command  or sometimes just
3f9f016a
 ;;            out in the open after the command (like the search)
36b59036
 ;;  comment  -- the whole of the part after the command
 ;;
 (defun parse-imap-response (line end)
   (let (kind value next
3f9f016a
         tag count command extra-data
         comment)
0f9b7410
 
36b59036
     ;; get tag
     (multiple-value-setq (kind value next)
       (get-next-token line 0 end))
0f9b7410
 
36b59036
     (case kind
       (:string (setq tag (if* (equal value "*")
3f9f016a
                             then :untagged
                             else value)))
36b59036
       (t (po-error :unexpected
3f9f016a
                    :format-control "Illegal tag on response: ~s"
                    :format-arguments (list (subseq line 0 count))
                    :server-string (subseq line 0 end)
                    )))
0f9b7410
 
36b59036
     ;; get command
     (multiple-value-setq (kind value next)
       (get-next-token line next end))
0f9b7410
 
36b59036
     (tagbody again
       (case kind
3f9f016a
         (:number (setq count value)
                  (multiple-value-setq (kind value next)
                    (get-next-token line next end))
                  (go again))
         (:string (setq command (kwd-intern value)))
         (t (po-error :unexpected
                      :format-control "Illegal command on response: ~s"
                      :format-arguments (list (subseq line 0 count))
                      :server-string (subseq line 0 end)))))
36b59036
 
     (setq comment (subseq line next end))
0f9b7410
 
36b59036
     ;; now the part after the command... this gets tricky
     (loop
       (multiple-value-setq (kind value next)
3f9f016a
         (get-next-token line next end))
0f9b7410
 
36b59036
       (case kind
3f9f016a
         ((:lbracket :lparen)
          (multiple-value-setq (kind value next)
            (get-next-sexpr line (1- next) end))
          (case kind
            (:sexpr (push value extra-data))
            (t (po-error :syntax-error :format-control "bad sexpr form"))))
         (:eof (return nil))
         ((:number :string :nil) (push value extra-data))
         (t  ; should never happen
          (return)))
0f9b7410
 
36b59036
       (if* (not (member command '(:list :search) :test #'eq))
3f9f016a
          then ; only one item returned
               (setq extra-data (car extra-data))
               (return)))
36b59036
 
     (if* (member command '(:list :search) :test #'eq)
        then (setq extra-data (nreverse extra-data)))
0f9b7410
 
 
36b59036
     (values tag command count extra-data comment)))
0f9b7410
 
36b59036
 
 
 (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
0f9b7410
   ;;
36b59036
   (let ( kind value next)
     (multiple-value-setq (kind value next) (get-next-token line start end))
0f9b7410
 
36b59036
     (case kind
       ((:string :number :nil)
        (values :sexpr value next))
0f9b7410
       (:eof (po-error :syntax-error
3f9f016a
                       :format-control "eof inside sexpr"))
36b59036
       ((:lbracket :lparen)
        (let (res)
3f9f016a
          (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)))
              (t (po-error :syntax-error
                           :format-control "bad sexpression"))))))
36b59036
       ((:rbracket :rparen)
        (values kind nil next))
       (t (po-error :syntax-error
3f9f016a
                    :format-control "bad sexpression")))))
36b59036
 
 
 (defun parse-pop-response (line end)
   ;; return 3 values:
0f9b7410
   ;;   :ok or :error
36b59036
   ;;   a list of rest of the tokens on the line, the tokens
3f9f016a
   ;;     being either strings or integers
36b59036
   ;;   the whole line after the +ok or -err
   ;;
   (let (res lineres result)
     (multiple-value-bind (kind value next)
3f9f016a
         (get-next-token line 0 end)
0f9b7410
 
36b59036
       (case kind
3f9f016a
         (:string (setq result (if* (equal "+OK" value)
                                  then :ok
                                  else :error)))
         (t (po-error :unexpected
                      :format-control "bad response from server"
                      :server-string (subseq line 0 end))))
0f9b7410
 
36b59036
       (setq lineres (subseq line next end))
 
       (loop
3f9f016a
         (multiple-value-setq (kind value next)
           (get-next-token line next end))
0f9b7410
 
3f9f016a
         (case kind
           (:eof (return))
           ((:string :number) (push value res))))
0f9b7410
 
36b59036
       (values result (nreverse res) lineres))))
0f9b7410
 
 
 
 
 
 
 
 
 
 
36b59036
 (defparameter *char-to-kind*
     (let ((arr (make-array 256 :initial-element nil)))
0f9b7410
 
36b59036
       (do ((i #.(char-code #\0) (1+ i)))
3f9f016a
           ((> i #.(char-code #\9)))
         (setf (aref arr i) :number))
0f9b7410
 
36b59036
       (setf (aref arr #.(char-code #\space)) :space)
       (setf (aref arr #.(char-code #\tab)) :space)
       (setf (aref arr #.(char-code #\return)) :space)
       (setf (aref arr #.(char-code #\linefeed)) :space)
0f9b7410
 
36b59036
       (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)
0f9b7410
 
36b59036
       (setf (aref arr #.(char-code #\^b)) :big-string) ; our own invention
0f9b7410
 
36b59036
       arr))
0f9b7410
 
 
36b59036
 (defun get-next-token (line start end)
   ;; scan past whitespace for the next token
   ;; return three values:
   ;;  kind:  :string , :number, :eof, :lbracket, :rbracket,
3f9f016a
   ;;            :lparen, :rparen
36b59036
   ;;  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)
3f9f016a
         collector right-bracket-is-normal)
0f9b7410
     (loop
36b59036
       ; pick up the next character
       (if* (>= start end)
3f9f016a
          then (if* (eq state :looking)
                  then (return (values :eof nil start))
                  else (setq ch #\space))
          else (setq ch (schar line start)))
0f9b7410
 
36b59036
       (setq chkind (aref *char-to-kind* (char-code ch)))
0f9b7410
 
36b59036
       (case state
3f9f016a
         (: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)
                          then (po-error :unexpected
                                         :format-control "eof in string returned"))
                       (setq ch (schar line start)))
               (vector-push-extend ch collector)
 
               (if* (>= start end)
                  then ; we overran the end of the input
                       (po-error :unexpected
                                 :format-control "eof in string returned")))))
         (: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)))
 
 
         )
0f9b7410
 
36b59036
       (incf start))))
0f9b7410
 
 
36b59036
 
 ;  this used to be exported from the excl package
958944bd
 #+(or (and allegro (version>= 6 0))
       (not allegro))
36b59036
 (defvar *keyword-package* (find-package :keyword))
 
 (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)))
 
0f9b7410
 
36b59036
 (defun kwd-intern (string)
   ;; convert the string to the current preferred case
   ;; and then intern
   (intern (case excl::*current-case-mode*
3f9f016a
             ((:case-sensitive-lower
               :case-insensitive-lower) (string-downcase string))
             (t (string-upcase string)))
           *keyword-package*))
0f9b7410
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36b59036
 ;; 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.
0f9b7410
   ;;
996cc728
   ;; TODO: make it able to read from ssl socket
36b59036
   (let* ((buff (get-line-buffer 0))
3f9f016a
          (len  (length buff))
          (i 0)
          (p (post-office-socket mailbox))
          (ch nil)
          (whole-count)
          )
36b59036
 
0f9b7410
     (handler-case
3f9f016a
         (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)))))
 
           ;; 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
 
996cc728
              (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)))
 
                                           (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)
                                           (setq whole-count nil)
                                           )
               elseif ch
                 then                    ; we're growing the buffer holding the line data
                      (grow-buffer (+ len 200))
                      (setf (schar buff i) ch)
                      (incf i))
 
 
              (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)))))))
 
 
                             (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)))))))
36b59036
       (error (con)
3f9f016a
         ;; 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)))
36b59036
       )))
 
 
 (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
0f9b7410
   ;; return as a second value the number of characters saved
36b59036
   ;; (we drop #\return's so that lines are separated by a #\newline
   ;; like lisp likes).
   ;;
   (let ((buff (get-line-buffer count))
3f9f016a
         (p (post-office-socket mb))
         (ind 0))
36b59036
     (mp:with-timeout ((timeout mb)
3f9f016a
                       (po-error :timeout
                                 :format-control "imap server timed out"))
0f9b7410
 
36b59036
       (dotimes (i count)
3f9f016a
         (if* (eq #\return (setf (schar buff ind) (read-char p)))
            then (if* save-returns then (incf ind)) ; drop #\returns
            else (incf ind)))
0f9b7410
 
 
36b59036
       (values buff ind))))
0f9b7410
 
 
36b59036
 ;;-- reusable line buffers
 
 (defvar *line-buffers* nil)
 
958944bd
 #+(and allegro (version>= 8 1))
 (defvar *line-buffers-lock* (make-basic-lock :name "line-buffers"))
36b59036
 
 (defmacro with-locked-line-buffers (&rest body)
958944bd
   #+(and allegro (version>= 8 1))
   `(with-locked-structure (*line-buffers-lock*
                         :non-smp :without-scheduling)
      ,@body)
   #-(and allegro (version>= 8 1))
89818e18
   `(mp:without-scheduling ,@body)
36b59036
   )
 
 (defun get-line-buffer (size)
   ;; get a buffer of at least size bytes
   (setq size (min size (1- array-total-size-limit)))
0f9b7410
   (let ((found
3f9f016a
          (with-locked-line-buffers
            (dolist (buff *line-buffers*)
              (if* (>= (length buff) size)
                 then ;; use this one
                      (setq *line-buffers* (delete buff *line-buffers*))
                      (return buff))))))
36b59036
     (or found  (make-string size))))
 
 (defun free-line-buffer (buff)
   (with-locked-line-buffers
     (push buff *line-buffers*)))
 
 (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))))
 
0f9b7410
 
36b59036
 
   ;;;;;;;
 
 ; 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"
3f9f016a
             date
             (svref
              '#(nil "Jan" "Feb" "Mar" "Apr" "May" "Jun"
                 "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")
              month
              )
             year)))
0f9b7410
 
 
36b59036
 
 
 ;; utility
 
 (defmacro with-imap-connection ((mb &rest options) &body body)
   `(let ((,mb (make-imap-connection ,@options)))
      (unwind-protect
3f9f016a
          (progn
            ,@body)
36b59036
        (close-connection ,mb))))
 
 
 (defmacro with-pop-connection ((mb &rest options) &body body)
   `(let ((,mb (make-pop-connection ,@options)))
      (unwind-protect
3f9f016a
          (progn
            ,@body)
36b59036
        (close-connection ,mb))))