git.fiddlerwoaroof.com
imap.cl
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))))