Add disk cache for IMAP folders.
authorChris Hanson <org/chris-hanson/cph>
Fri, 28 Sep 2001 19:18:30 +0000 (19:18 +0000)
committerChris Hanson <org/chris-hanson/cph>
Fri, 28 Sep 2001 19:18:30 +0000 (19:18 +0000)
v7/src/imail/imail-imap.scm

index 05ad08f1e8589562400b9036213f560827331152..2c6e00e6d5575a328524c2313925155bf09ec632 100644 (file)
@@ -1,6 +1,6 @@
 ;;; -*-Scheme-*-
 ;;;
-;;; $Id: imail-imap.scm,v 1.185 2001/09/14 02:06:53 cph Exp $
+;;; $Id: imail-imap.scm,v 1.186 2001/09/28 19:18:30 cph Exp $
 ;;;
 ;;; Copyright (c) 1999-2001 Massachusetts Institute of Technology
 ;;;
      (if (imap-folder-uidvalidity folder)
         (set-imap-folder-unseen! folder #f))
      (set-imap-folder-uidvalidity! folder uidvalidity)))
-  (read-message-headers! folder 0))
+  (read-message-headers! folder 0)
+  (clean-cache-directory folder))
 
 (define (detach-all-messages! folder)
   (let ((v (imap-folder-messages folder))
                                   start #f '(UID FLAGS))))))
 \f
 (define (remove-imap-folder-message folder index)
+  (delete-cached-message (%get-message folder index))
   (without-interrupts
    (lambda ()
      (let ((v (imap-folder-messages folder))
                                     (imap-message-uid m*))
                                  (error "Message inserted into folder:" m*))
                              (loop (fix:+ i 1) i*)))))))
-             (object-modified! folder 'SET-LENGTH n count)))))))
+             (object-modified! folder 'SET-LENGTH n count))))))
+  (clean-cache-directory folder))
 \f
 ;;;; Message datatype
 
        '(\SEEN \ANSWERED \FLAGGED \DELETED \DRAFT \RECENT)))
 
 (define-method message-internal-time ((message <imap-message>))
-  (with-imap-message-open message
-    (lambda (connection)
-      (imap:response:fetch-attribute
-       (imap:command:uid-fetch connection
-                              (imap-message-uid message)
-                              '(INTERNALDATE))
-       'INTERNALDATE))))
+  (imap:response:fetch-attribute (fetch-message-items message '(INTERNALDATE))
+                                'INTERNALDATE))
 
 (define-method message-length ((message <imap-message>))
   (with-imap-message-open message
 
 (define (guarantee-slot-initialized message initpred noun keywords)
   (if (not (initpred message))
-      (with-imap-message-open message
-       (lambda (connection)
-         (let ((uid (imap-message-uid message)))
-           (let ((suffix
-                  (string-append
-                   " " noun " for message "
-                   (number->string (+ (%message-index message) 1)))))
-             ((imail-ui:message-wrapper "Reading" suffix)
-              (lambda ()
-                (imap:read-literal-progress-hook imail-ui:progress-meter
-                  (lambda ()
-                    (imap:command:uid-fetch connection uid keywords)
-                    (if (not (initpred message))
-                        (error
-                         (string-append "Unable to obtain" suffix)))))))))))))
+      (let ((suffix
+            (string-append " " noun " for message "
+                           (number->string (+ (%message-index message) 1)))))
+       ((imail-ui:message-wrapper "Reading" suffix)
+        (lambda ()
+          (imap:read-literal-progress-hook imail-ui:progress-meter
+            (lambda ()
+              (fetch-message-items message keywords)
+              (if (not (initpred message))
+                  (error (string-append "Unable to obtain" suffix))))))))))
 
 (let ((reflector
        (lambda (generic-procedure slot-name guarantee)
                                  '(BODYSTRUCTURE)))))
 \f
 (define-method preload-folder-outlines ((folder <imap-folder>))
+  (for-each-message folder
+    (lambda (message)
+      (if (not (imap-message-header-fields-initialized? message))
+         (preload-cached-message-item message 'RFC822.HEADER))
+      (if (not (imap-message-length-initialized? message))
+         (preload-cached-message-item message 'RFC822.SIZE))))
   (let* ((connection (guarantee-imap-folder-open folder))
         (messages
          (messages-satisfying folder
              (not (and (imap-message-header-fields-initialized? message)
                        (imap-message-length-initialized? message)))))))
     (if (pair? messages)
-       ((imail-ui:message-wrapper "Reading message headers")
-        (lambda ()
-          (imap:command:fetch-set connection
-                                  (message-list->set messages)
-                                  '(RFC822.HEADER RFC822.SIZE)))))))
-       
+       (let ((keywords '(RFC822.HEADER RFC822.SIZE)))
+         (cache-preload-responses folder keywords
+           ((imail-ui:message-wrapper "Reading message headers")
+            (lambda ()
+              (imap:command:fetch-set connection
+                                      (message-list->set messages)
+                                      keywords))))))))
 
 (define imap-message-header-fields-initialized?
   (slot-initpred <imap-message> 'HEADER-FIELDS))
                      messages)))
          (reverse! messages)))))
 
+(define (for-each-message folder procedure)
+  (let ((n (folder-length folder)))
+    (do ((i 0 (+ i 1)))
+       ((= i n))
+      (procedure (get-message folder i)))))
+
 (define (message-list->set messages)
   (let loop ((indexes (map %message-index messages)) (groups '()))
     (if (pair? indexes)
                                  (exact-nonnegative-integer? limit))
                             (< cache? limit)
                             #t))))
-            (let ((part (%imap-message-body-part message section)))
+            (let ((part (fetch-message-body-part message section)))
               (set-imap-message-body-parts!
                message
                (cons (cons section part)
            (else
             (imap:bind-fetch-body-part-port port
               (lambda ()
-                (%imap-message-body-part message section))))))))
-
-(define (%imap-message-body-part message section)
-  (imap:response:fetch-body-part
-   (let ((suffix 
-         (string-append " body"
-                        (if (equal? section '(TEXT)) "" " part")
-                        " for message "
-                        (number->string (+ (%message-index message) 1)))))
-     ((imail-ui:message-wrapper "Reading" suffix)
-      (lambda ()
-       (imap:read-literal-progress-hook imail-ui:progress-meter
-         (lambda ()
-           (with-imap-message-open message
-             (lambda (connection)
-               (imap:command:uid-fetch
-                connection
-                (imap-message-uid message)
-                `(',(string-append "body["
-                                   (decorated-string-append
-                                    "" "." ""
-                                    (map (lambda (x)
-                                           (if (exact-nonnegative-integer? x)
-                                               (number->string x)
-                                               (symbol->string x)))
-                                         section))
-                                   "]"))))))))))
-   section
-   #f))
+                (fetch-message-body-part message section))))))))
 \f
 (define (parse-mime-body body)
   (cond ((not (and (pair? body) (list? body))) (parse-mime-body:lose body))
        (loop addr-list '() '()))
       '()))
 \f
+;;;; IMAP disk cache
+
+;; The disk cache has following structure:
+;;
+;; There is a root directory for the cache.  Under this directory,
+;; there is one subdirectory for each server.  The server directory
+;; name is a variant of the server information from the URL
+;;
+;; Under each server directory, there is one subdirectory for each
+;; folder on that server.  The folder directory name is formed by
+;; taking the folder's mailbox name and mapping the characters into a
+;; safe subset.  The safe subset preserves all alphanumeric
+;; characters, hypens, and underscores, converts "/" to ".", and
+;; converts everything else to "=XX" form.
+;;
+;; Under each folder directory, there is a file called "uidvalidity"
+;; that contains the UIDVALIDITY number, as a text string.  For each
+;; message in the folder, there is a subdirectory whose name is the
+;; UID of the message.
+;;
+;; Under each message directory, there is a file called
+;; "rfc822.header" that contains the header information.  There may
+;; also be files called "envelope", "bodystructure", "rfc822.size",
+;; "internaldate", "text", and "body[...]", all corresponding to the
+;; IMAP FETCH keys.
+
+(define (clean-cache-directory folder)
+  (let ((directory (imap-folder-cache-pathname folder))
+       (uidvalidity (imap-folder-uidvalidity folder)))
+    (if uidvalidity
+       (let ((up (merge-pathnames "uidvalidity" directory)))
+         (if (file-directory? directory)
+             (let ((uidvalidity* (simple-read-file up)))
+               (if (and (file-regular? up)
+                        (eqv? uidvalidity* uidvalidity))
+                   (remove-expunged-messages folder directory)
+                   (begin
+                     (call-with-append-file "/tmp/foo"
+                       (lambda (port)
+                         (write `(uidvalidity= ,uidvalidity ,uidvalidity*)
+                                port)
+                         (newline port)
+                         (write `(delete-directory-contents ,directory) port)
+                         (newline port)))
+                     (delete-directory-contents directory)
+                     (simple-write-file uidvalidity up))))
+             (begin
+               (delete-file-no-errors directory)
+               (guarantee-init-file-directory directory)
+               (simple-write-file uidvalidity up)))))))
+
+(define (remove-expunged-messages folder directory)
+  (call-with-append-file "/tmp/foo"
+    (lambda (port)
+      (write `(remove-expunged-messages ,folder ,directory) port)
+      (newline port)))
+  (for-each (lambda (pathname)
+             (let ((ns (file-namestring pathname)))
+               (if (not (or (string=? ns ".")
+                            (string=? ns "..")
+                            (string=? ns "uidvalidity")
+                            (let ((uid (string->number ns 10)))
+                              (and uid
+                                   (get-imap-message-by-uid folder uid)
+                                   (file-directory? pathname)))))
+                   (delete-file-recursively pathname))))
+           (directory-read directory #f)))
+
+(define (get-imap-message-by-uid folder uid)
+  (let loop ((low 0) (high (folder-length folder)))
+    (if (fix:< low high)
+       (let ((index (fix:quotient (fix:+ low high) 2)))
+         (let ((message (%get-message folder index)))
+           (let ((uid* (imap-message-uid message)))
+             (cond ((< uid uid*) (loop low index))
+                   ((> uid uid*) (loop (fix:+ index 1) high))
+                   (else message)))))
+       #f)))
+\f
+(define (fetch-message-items message keywords)
+  (if (equal? keywords '(FLAGS))
+      (fetch-message-items-1 message keywords)
+      (let ((alist
+            (map (lambda (keyword)
+                   (cons keyword
+                         (let ((pathname
+                                (message-item-pathname message keyword)))
+                           (if (file-exists? pathname)
+                               (list
+                                (read-cached-message-item message
+                                                          keyword
+                                                          pathname))
+                               '()))))
+                 keywords)))
+       (let ((uncached
+              (list-transform-positive alist
+                (lambda (entry)
+                  (null? (cdr entry))))))
+         (if (pair? uncached)
+             (let ((response
+                    (fetch-message-items-1 message
+                                           (map car uncached))))
+               (cache-fetch-response message response
+                 (lambda (keyword)
+                   (assq keyword alist))
+                 (lambda (keyword item)
+                   (set-cdr! (assq keyword alist) (list item)))))))
+       `(FETCH ,(+ (message-index message) 1) ,@alist))))
+
+(define (cache-fetch-response message response keyword-predicate save-item)
+  (for-each (lambda (keyword)
+             (if (keyword-predicate keyword)
+                 (let ((item (imap:response:fetch-attribute response keyword))
+                       (pathname (message-item-pathname message keyword)))
+                   (guarantee-init-file-directory pathname)
+                   (if (memq keyword message-items-cached-as-string)
+                       (string->file item pathname)
+                       (simple-write-file item pathname))
+                   (save-item keyword item))))
+           (imap:response:fetch-attribute-keywords response)))
+
+(define message-items-cached-as-string
+  '(RFC822.HEADER))
+
+(define (fetch-message-items-1 message keywords)
+  (with-imap-message-open message
+    (lambda (connection)
+      (imap:command:uid-fetch connection
+                             (imap-message-uid message)
+                             keywords))))
+\f
+(define (fetch-message-body-part message section)
+  (let ((keyword (imap-body-section->keyword section)))
+    (let ((pathname (message-item-pathname message keyword)))
+      (if (file-exists? pathname)
+         (file->string pathname)
+         (let ((part (fetch-message-body-part-1 message section keyword)))
+           (guarantee-init-file-directory pathname)
+           (string->file part pathname)
+           part)))))
+
+(define (fetch-message-body-part-1 message section keyword)
+  (imap:response:fetch-body-part
+   (let ((suffix 
+         (string-append " body"
+                        (if (equal? section '(TEXT)) "" " part")
+                        " for message "
+                        (number->string (+ (%message-index message) 1)))))
+     ((imail-ui:message-wrapper "Reading" suffix)
+      (lambda ()
+       (imap:read-literal-progress-hook imail-ui:progress-meter
+         (lambda ()
+           (with-imap-message-open message
+             (lambda (connection)
+               (imap:command:uid-fetch connection
+                                       (imap-message-uid message)
+                                       `(',keyword)))))))))
+   section
+   #f))
+
+(define (imap-body-section->keyword section)
+  (string-append "body["
+                (decorated-string-append
+                 "" "." ""
+                 (map (lambda (x)
+                        (if (exact-nonnegative-integer? x)
+                            (number->string x)
+                            (symbol-name x)))
+                      section))
+                "]"))
+
+(define (preload-cached-message-item message keyword)
+  (let ((pathname (message-item-pathname message keyword)))
+    (if (file-exists? pathname)
+       (read-cached-message-item message keyword pathname))))
+
+(define (cache-preload-responses folder keywords responses)
+  (for-each
+   (lambda (response)
+     (cache-fetch-response
+      (%get-message folder (- (imap:response:fetch-index response) 1))
+      response
+      (lambda (keyword) (memq keyword keywords))
+      (lambda (keyword item) keyword item unspecific)))
+   responses))
+
+(define (delete-cached-message message)
+  (delete-file-recursively (imap-message-cache-pathname message)))
+\f
+(define (message-item-pathname message keyword)
+  (init-file-specifier->pathname
+   `(,@(imap-message-cache-specifier message)
+     ,(if (symbol? keyword) (symbol-name keyword) keyword))))
+
+(define (imap-message-cache-pathname message)
+  (pathname-as-directory
+   (init-file-specifier->pathname (imap-message-cache-specifier message))))
+
+(define (imap-message-cache-specifier message)
+  `(,@(imap-folder-cache-specifier (message-folder message))
+    ,(write-to-string (imap-message-uid message))))
+
+(define (imap-folder-cache-pathname folder)
+  (pathname-as-directory
+   (init-file-specifier->pathname (imap-folder-cache-specifier folder))))
+
+(define (imap-folder-cache-specifier folder)
+  (let ((url (resource-locator folder)))
+    (list "imail-cache"
+         (string-append (encode-cache-namestring (imap-url-user-id url))
+                        "@"
+                        (string-downcase (imap-url-host url))
+                        ":"
+                        (number->string (imap-url-port url)))
+         (encode-cache-namestring (imap-url-mailbox url)))))
+
+(define (encode-cache-namestring string)
+  (with-string-output-port
+    (lambda (port)
+      (let ((n (string-length string)))
+       (do ((i 0 (fix:+ i 1)))
+           ((fix:= i n))
+         (let ((char (string-ref string i)))
+           (cond ((char-set-member? char-set:cache-namestring-safe char)
+                  (write-char char port))
+                 ((char=? char #\/)
+                  (write-char #\. port))
+                 (else
+                  (write-char #\= port)
+                  (let ((n (char->integer char)))
+                    (if (fix:< n #x10)
+                        (write-char #\0 port))
+                    (write n port))))))))))
+
+(define char-set:cache-namestring-safe
+  (char-set-union char-set:alphanumeric (string->char-set "-_")))
+\f
+(define (read-cached-message-item message keyword pathname)
+  (let ((item
+        (if (memq keyword message-items-cached-as-string)
+            (file->string pathname)
+            (simple-read-file pathname))))
+    (process-fetch-attribute message keyword item)
+    item))
+
+(define (simple-read-file pathname)
+  (call-with-input-file pathname read))
+
+(define (simple-write-file object pathname)
+  (call-with-output-file pathname
+    (lambda (port)
+      (write object port)
+      (newline port))))
+
+(define (string->file string pathname)
+  (call-with-output-file pathname
+    (lambda (port)
+      (write-string string port))))
+
+(define (file->string pathname)
+  (call-with-input-file pathname
+    (lambda (port)
+      ((input-port/custom-operation port 'REST->STRING) port))))
+
+(define (simple-write-file object pathname)
+  (call-with-output-file pathname
+    (lambda (port)
+      (write object port)
+      (newline port))))
+
+(define (delete-file-recursively pathname)
+  (call-with-append-file "/tmp/foo"
+    (lambda (port)
+      (write `(delete-file-recursively ,pathname) port)
+      (newline port)))
+  (if (file-directory? pathname)
+      (begin
+       (delete-directory-contents (pathname-as-directory pathname))
+       (delete-directory pathname))
+      (delete-file-no-errors pathname)))
+
+(define (delete-directory-contents directory)
+  (call-with-append-file "/tmp/foo"
+    (lambda (port)
+      (write `(delete-directory-contents ,directory) port)
+      (newline port)))
+  (for-each (lambda (pathname)
+             (if (not (let ((ns (file-namestring pathname)))
+                        (or (string=? ns ".")
+                            (string=? ns ".."))))
+                 (delete-file-recursively pathname)))
+           (directory-read directory #f)))
+\f
 ;;;; Server operations
 
 (define-method %create-resource ((url <imap-url>))