Synch with Oort Gnus.
[elisp/gnus.git-] / lisp / nnimap.el
index a556bc5..2623a3f 100644 (file)
@@ -55,6 +55,7 @@
 ;;   o What about Gnus's article editing, can we support it?  NO!
 ;;   o Use \Draft to support the draft group??
 ;;   o Duplicate suppression
+;;   o Rewrite UID SEARCH UID X as UID FETCH X (UID) for those with slow servers
 
 ;;; Code:
 
@@ -128,10 +129,10 @@ this:
 \(setq nnimap-split-rule '((\"INBOX.gnus-imap\"   \"From:.*gnus-imap\")
                          (\"INBOX.junk\"        \"Subject:.*buy\")))
 
-As you can see, `nnimap-split-rule' is a list of lists, where the first
-element in each \"rule\" is the name of the IMAP mailbox, and the
-second is a regexp that nnimap will try to match on the header to find
-a fit.
+As you can see, `nnimap-split-rule' is a list of lists, where the
+first element in each \"rule\" is the name of the IMAP mailbox (or the
+symbol `junk' if you want to remove the mail), and the second is a
+regexp that nnimap will try to match on the header to find a fit.
 
 The second element can also be a function.  In that case, it will be
 called narrowed to the headers with the first element of the rule as
@@ -192,7 +193,7 @@ RFC2060 section 6.4.4."
   :type 'string)
 
 (defcustom nnimap-split-fancy nil
-  "Like `nnmail-split-fancy', which see."
+  "Like the variable `nnmail-split-fancy', which see."
   :group 'nnimap
   :type 'sexp)
 
@@ -214,6 +215,18 @@ the same mailbox will be faster though."
   :type 'boolean
   :group 'nnimap)
 
+(defcustom nnimap-retrieve-groups-asynchronous t
+  "Send asynchronous STATUS commands for each mailbox before checking mail.
+If you have mailboxes that rarely receives mail, this speeds up new
+mail checking.  It works by first sending STATUS commands for each
+mailbox, and then only checking groups which has a modified UIDNEXT
+more carefully for new mail.
+
+In summary, the default is O((1-p)*k+p*n) and changing it to nil makes
+it O(n).  If p is small, then the default is probably faster."
+  :type 'boolean
+  :group 'nnimap)
+
 (defvoo nnimap-need-unselect-to-notice-new-mail nil
   "Unselect mailboxes before looking for new mail in them.
 Some servers seem to need this under some circumstances.")
@@ -269,9 +282,12 @@ typical complete file name would be
 (defvoo nnimap-nov-file-name-suffix ".novcache"
   "Suffix for NOV cache base filename.")
 
-(defvoo nnimap-nov-is-evil nil
-  "If non-nil, nnimap will never generate or use a local nov database for this backend.
-Using nov databases will speed up header fetching considerably.
+(defvoo nnimap-nov-is-evil gnus-agent
+  "If non-nil, never generate or use a local nov database for this backend.
+Using nov databases should speed up header fetching considerably.
+However, it will invoke a UID SEARCH UID command on the server, and
+some servers implement this command inefficiently by opening each and
+every message in the group, thus making it quite slow.
 Unlike other backends, you do not need to take special care if you
 flip this variable.")
 
@@ -367,6 +383,7 @@ restrict visible folders.")
 
 ;; Internal variables:
 
+(defvar nnimap-mailbox-info (gnus-make-hashtable 997))
 (defvar nnimap-debug nil
   "Name of buffer to record debugging info.
 For example: (setq nnimap-debug \"*nnimap-debug*\")")
@@ -513,12 +530,7 @@ If EXAMINE is non-nil the group is selected read-only."
        (with-temp-buffer
         (buffer-disable-undo)
         (insert headers)
-        (nnheader-fold-continuation-lines)
-        (subst-char-in-region (point-min) (point-max) ?\t ? )
-        (nnheader-ms-strip-cr)
-        (nnheader-fold-continuation-lines)
-        (subst-char-in-region (point-min) (point-max) ?\t ? )
-        (let ((head (nnheader-parse-head 'naked)))
+        (let ((head (nnheader-parse-naked-head)))
           (mail-header-set-number head uid)
           (mail-header-set-chars head chars)
           (mail-header-set-lines head lines)
@@ -721,7 +733,12 @@ If EXAMINE is non-nil the group is selected read-only."
     (with-current-buffer (get-buffer-create nnimap-server-buffer)
       (nnoo-change-server 'nnimap server defs))
     (or (and nnimap-server-buffer
-            (imap-opened nnimap-server-buffer))
+            (imap-opened nnimap-server-buffer)
+            (if (with-current-buffer nnimap-server-buffer
+                  (memq imap-state '(auth select examine)))
+                t
+              (imap-close nnimap-server-buffer)
+              (nnimap-open-connection server)))
        (nnimap-open-connection server))))
 
 (deffoo nnimap-server-opened (&optional server)
@@ -857,10 +874,22 @@ function is generally only called when Gnus is shutting down."
             (nnheader-report 'nnimap "Group %s selected" group)
             t)))))
 
+(defun nnimap-update-unseen (group &optional server)
+  "Update the unseen count in `nnimap-mailbox-info'."
+  (gnus-sethash
+   (gnus-group-prefixed-name group server)
+   (let ((old (gnus-gethash-safe (gnus-group-prefixed-name group server) 
+                                nnimap-mailbox-info)))
+     (list (nth 0 old) (nth 1 old)
+          (imap-mailbox-status group 'unseen nnimap-server-buffer)
+          (nth 3 old)))
+   nnimap-mailbox-info))
+
 (defun nnimap-close-group (group &optional server)
   (with-current-buffer nnimap-server-buffer
     (when (and (imap-opened)
               (nnimap-possibly-change-group group server))
+      (nnimap-update-unseen group server)
       (case nnimap-expunge-on-close
        (always (progn
                  (imap-mailbox-expunge nnimap-close-asynchronous)
@@ -928,24 +957,96 @@ function is generally only called when Gnus is shutting down."
 
 ;; Optional backend functions
 
+(defun nnimap-string-lessp-numerical (s1 s2)
+  "Return t if first arg string is less than second in numerical order."
+  (cond ((string= s1 s2)
+        nil)
+       ((> (length s1) (length s2))
+        nil)
+       ((< (length s1) (length s2))
+        t)
+       ((< (string-to-number (substring s1 0 1))
+           (string-to-number (substring s2 0 1)))
+        t)
+       ((> (string-to-number (substring s1 0 1))
+           (string-to-number (substring s2 0 1)))
+        nil)
+       (t
+        (nnimap-string-lessp-numerical (substring s1 1) (substring s2 1)))))
+
 (deffoo nnimap-retrieve-groups (groups &optional server)
   (when (nnimap-possibly-change-server server)
     (gnus-message 5 "nnimap: Checking mailboxes...")
     (with-current-buffer nntp-server-buffer
       (erase-buffer)
       (nnimap-before-find-minmax-bugworkaround)
-      (dolist (group groups)
-       (gnus-message 7 "nnimap: Checking mailbox %s" group)
-       (or (member "\\NoSelect"
-                   (imap-mailbox-get 'list-flags group nnimap-server-buffer))
-           (let ((info (nnimap-find-minmax-uid group 'examine)))
-             (when (> (or (imap-mailbox-get 'recent group
-                                            nnimap-server-buffer) 0)
-                      0)
-               (push (list (cons group 0)) nnmail-split-history))
-             (insert (format "\"%s\" %d %d y\n" group
-                             (or (nth 2 info) 0)
-                             (max 1 (or (nth 1 info) 1))))))))
+      (let (asyncgroups slowgroups)
+       (if (null nnimap-retrieve-groups-asynchronous)
+           (setq slowgroups groups)
+         (dolist (group groups)
+           (gnus-message 9 "nnimap: Quickly checking mailbox %s" group)
+           (add-to-list (if (gnus-gethash-safe
+                             (gnus-group-prefixed-name group server)
+                             nnimap-mailbox-info)
+                            'asyncgroups
+                          'slowgroups)
+                        (list group (imap-mailbox-status-asynch
+                                     group '(uidvalidity uidnext unseen) 
+                                     nnimap-server-buffer))))
+         (dolist (asyncgroup asyncgroups)
+           (let ((group (nth 0 asyncgroup))
+                 (tag   (nth 1 asyncgroup))
+                 new old)
+             (when (imap-ok-p (imap-wait-for-tag tag nnimap-server-buffer))
+               (if (or (not (string=
+                             (nth 0 (gnus-gethash (gnus-group-prefixed-name
+                                                   group server)
+                                                  nnimap-mailbox-info))
+                             (imap-mailbox-get 'uidvalidity group 
+                                               nnimap-server-buffer)))
+                       (not (string=
+                             (nth 1 (gnus-gethash (gnus-group-prefixed-name
+                                                   group server)
+                                                  nnimap-mailbox-info))
+                             (imap-mailbox-get 'uidnext group
+                                               nnimap-server-buffer))))
+                   (push (list group) slowgroups)
+                 (insert (nth 3 (gnus-gethash (gnus-group-prefixed-name
+                                               group server)
+                                              nnimap-mailbox-info))))))))
+       (dolist (group slowgroups)
+         (if nnimap-retrieve-groups-asynchronous
+             (setq group (car group)))
+         (gnus-message 7 "nnimap: Mailbox %s modified" group)
+         (imap-mailbox-put 'uidnext nil group nnimap-server-buffer)
+         (or (member "\\NoSelect" (imap-mailbox-get 'list-flags group
+                                                    nnimap-server-buffer))
+             (let* ((info (nnimap-find-minmax-uid group 'examine))
+                    (str (format "\"%s\" %d %d y\n" group
+                                 (or (nth 2 info) 0)
+                                 (max 1 (or (nth 1 info) 1)))))
+               (when (> (or (imap-mailbox-get 'recent group
+                                              nnimap-server-buffer) 0)
+                        0)
+                 (push (list (cons group 0)) nnmail-split-history))
+               (insert str)
+               (when nnimap-retrieve-groups-asynchronous
+                 (gnus-sethash
+                  (gnus-group-prefixed-name group server)
+                  (list (or (imap-mailbox-get
+                             'uidvalidity group nnimap-server-buffer)
+                            (imap-mailbox-status
+                             group 'uidvalidity nnimap-server-buffer))
+                        (or (imap-mailbox-get
+                             'uidnext group nnimap-server-buffer)
+                            (imap-mailbox-status
+                             group 'uidnext nnimap-server-buffer))
+                        (or (imap-mailbox-get
+                             'unseen group nnimap-server-buffer)
+                            (imap-mailbox-status
+                             group 'unseen nnimap-server-buffer))
+                        str)
+                  nnimap-mailbox-info)))))))
     (gnus-message 5 "nnimap: Checking mailboxes...done")
     'active))
 
@@ -1065,7 +1166,7 @@ function is generally only called when Gnus is shutting down."
   nil)
 
 (defun nnimap-split-fancy ()
-  "Like nnmail-split-fancy, but uses nnimap-split-fancy."
+  "Like the function `nnmail-split-fancy', but uses `nnimap-split-fancy'."
   (let ((nnmail-split-fancy nnimap-split-fancy))
     (nnmail-split-fancy)))
 
@@ -1087,7 +1188,10 @@ function is generally only called when Gnus is shutting down."
              (goto-char (point-min))
              (when (and (if (stringp regexp)
                             (progn
-                              (setq regrepp (string-match "\\\\[0-9&]" group))
+                              (if (not (stringp group))
+                                  (setq group (eval group))
+                                (setq regrepp
+                                      (string-match "\\\\[0-9&]" group)))
                               (re-search-forward regexp nil t))
                           (funcall regexp group))
                         ;; Don't enter the article into the same group twice.
@@ -1144,6 +1248,12 @@ function is generally only called when Gnus is shutting down."
                         (message "IMAP split moved %s:%s:%d to %s" server
                                  inbox article to-group)
                         (setq removeorig t)
+                        (when nnmail-cache-accepted-message-ids
+                          (with-current-buffer nntp-server-buffer
+                             (let (msgid)
+                               (and (setq msgid
+                                         (nnmail-fetch-field "message-id"))
+                                    (nnmail-cache-insert msgid to-group)))))
                         ;; Add the group-art list to the history list.
                         (push (list (cons to-group 0)) nnmail-split-history))
                        (t
@@ -1157,6 +1267,8 @@ function is generally only called when Gnus is shutting down."
            ;; todo: UID EXPUNGE (if available) to remove splitted articles
            (imap-mailbox-expunge)
            (imap-mailbox-close)))
+       (when nnmail-cache-accepted-message-ids
+         (nnmail-cache-close))
        t))))
 
 (deffoo nnimap-request-scan (&optional group server)
@@ -1217,11 +1329,10 @@ function is generally only called when Gnus is shutting down."
   (gnus-message 5 "nnimap: Marking article %d for deletion..."
                imap-current-message))
 
-
 (defun nnimap-expiry-target (arts group server)
   (unless (eq nnmail-expiry-target 'delete)
     (with-temp-buffer
-      (dolist (art (gnus-uncompress-sequence arts))
+      (dolist (art arts)
        (nnimap-request-article art group server (current-buffer))
        ;; hints for optimization in `nnimap-request-accept-article'
        (let ((nnimap-current-move-article art)
@@ -1237,35 +1348,32 @@ function is generally only called when Gnus is shutting down."
   (let ((artseq (gnus-compress-sequence articles)))
     (when (and artseq (nnimap-possibly-change-group group server))
       (with-current-buffer nnimap-server-buffer
-       (if force
-           (progn
-             (nnimap-expiry-target artseq group server)
-             (when (imap-message-flags-add (imap-range-to-message-set artseq)
-                                           "\\Deleted")
-               (setq articles nil)))
-         (let ((days (or (and nnmail-expiry-wait-function
-                              (funcall nnmail-expiry-wait-function group))
-                         nnmail-expiry-wait)))
-           (cond ((eq days 'immediate)
-                  (nnimap-expiry-target artseq group server)
-                  (when (imap-message-flags-add
-                         (imap-range-to-message-set artseq) "\\Deleted")
-                    (setq articles nil)))
-                 ((numberp days)
-                  (let ((oldarts (imap-search
-                                  (format nnimap-expunge-search-string
-                                          (imap-range-to-message-set artseq)
-                                          (nnimap-date-days-ago days))))
-                        (imap-fetch-data-hook
-                         '(nnimap-request-expire-articles-progress)))
+       (let ((days (or (and nnmail-expiry-wait-function
+                            (funcall nnmail-expiry-wait-function group))
+                       nnmail-expiry-wait)))
+         (cond ((or force (eq days 'immediate))
+                (let ((oldarts (imap-search
+                                (concat "UID " 
+                                        (imap-range-to-message-set artseq)))))
+                  (when oldarts
+                    (nnimap-expiry-target oldarts group server)
+                    (when (imap-message-flags-add
+                           (imap-range-to-message-set oldarts) "\\Deleted")
+                      (setq articles (gnus-set-difference
+                                      articles oldarts))))))
+               ((numberp days)
+                (let ((oldarts (imap-search
+                                (format nnimap-expunge-search-string
+                                        (imap-range-to-message-set artseq)
+                                        (nnimap-date-days-ago days))))
+                      (imap-fetch-data-hook
+                       '(nnimap-request-expire-articles-progress)))
+                  (when oldarts
                     (nnimap-expiry-target oldarts group server)
-                    (and oldarts
-                         (imap-message-flags-add
-                          (imap-range-to-message-set
-                           (gnus-compress-sequence oldarts))
-                          "\\Deleted")
-                         (setq articles (gnus-set-difference
-                                         articles oldarts)))))))))))
+                    (when (imap-message-flags-add
+                           (imap-range-to-message-set oldarts) "\\Deleted")
+                      (setq articles (gnus-set-difference 
+                                      articles oldarts)))))))))))
   ;; return articles not deleted
   articles)
 
@@ -1311,7 +1419,12 @@ function is generally only called when Gnus is shutting down."
                      (kill-region (point) (progn (forward-line) (point))))
                    ;; turn into rfc822 format (\r\n eol's)
                    (while (search-forward "\n" nil t)
-                     (replace-match "\r\n")))
+                     (replace-match "\r\n"))
+                   (when nnmail-cache-accepted-message-ids
+                     (nnmail-cache-insert (nnmail-fetch-field "message-id")
+                                          group)))
+                 (when (and last nnmail-cache-accepted-message-ids)
+                   (nnmail-cache-close))
                  ;; this 'or' is for Cyrus server bug
                  (or (null (imap-current-mailbox nnimap-server-buffer))
                      (imap-mailbox-unselect nnimap-server-buffer))