Make internal nnimap moving slightly faster.
[gnus] / lisp / nnimap.el
index 6a1c3f5..d7dc1fb 100644 (file)
 
 ;;; Code:
 
+;; For Emacs <22.2 and XEmacs.
+(eval-and-compile
+  (unless (fboundp 'declare-function) (defmacro declare-function (&rest r))))
+
 (eval-and-compile
   (require 'nnheader))
 
@@ -132,6 +136,16 @@ textual parts.")
 (defun nnimap-buffer ()
   (nnimap-find-process-buffer nntp-server-buffer))
 
+(defun nnimap-header-parameters ()
+  (format "(UID RFC822.SIZE BODYSTRUCTURE %s)"
+         (format
+          (if (nnimap-ver4-p)
+              "BODY.PEEK[HEADER.FIELDS %s]"
+            "RFC822.HEADER.LINES %s")
+          (append '(Subject From Date Message-Id
+                            References In-Reply-To Xref)
+                  nnmail-extra-headers))))
+
 (deffoo nnimap-retrieve-headers (articles &optional group server fetch-old)
   (with-current-buffer nntp-server-buffer
     (erase-buffer)
@@ -142,14 +156,7 @@ textual parts.")
         (nnimap-send-command
          "UID FETCH %s %s"
          (nnimap-article-ranges (gnus-compress-sequence articles))
-         (format "(UID RFC822.SIZE BODYSTRUCTURE %s)"
-                 (format
-                  (if (nnimap-ver4-p)
-                      "BODY.PEEK[HEADER.FIELDS %s]"
-                    "RFC822.HEADER.LINES %s")
-                  (append '(Subject From Date Message-Id
-                                    References In-Reply-To Xref)
-                          nnmail-extra-headers))))
+         (nnimap-header-parameters))
         t)
        (nnimap-transform-headers))
       (insert-buffer-substring
@@ -284,6 +291,8 @@ textual parts.")
                        (* 5 60)))
            (nnimap-send-command "NOOP")))))))
 
+(declare-function gnutls-negotiate "subr" (fn file &optional arglist fileonly))
+
 (defun nnimap-open-connection (buffer)
   (unless nnimap-keepalive-timer
     (setq nnimap-keepalive-timer (run-at-time (* 60 15) (* 60 15)
@@ -295,7 +304,11 @@ textual parts.")
             (port nil)
             (ports
              (cond
-              ((eq nnimap-stream 'network)
+              ((or (eq nnimap-stream 'network)
+                   (and (eq nnimap-stream 'starttls)
+                        (fboundp 'open-gnutls-stream)))
+               (nnheader-message 7 "Opening connection to %s..."
+                                 nnimap-address)
                (open-network-stream
                 "*nnimap*" (current-buffer) nnimap-address
                 (setq port
@@ -305,18 +318,24 @@ textual parts.")
                             "143"))))
                '("143" "imap"))
               ((eq nnimap-stream 'shell)
+               (nnheader-message 7 "Opening connection to %s via shell..."
+                                 nnimap-address)
                (nnimap-open-shell-stream
                 "*nnimap*" (current-buffer) nnimap-address
                 (setq port (or nnimap-server-port "imap")))
                '("imap"))
               ((eq nnimap-stream 'starttls)
-               (let ((tls-program (nnimap-extend-tls-programs)))
+               (nnheader-message 7 "Opening connection to %s via starttls..."
+                        nnimap-address)
+               (let ((tls-program
+                      '("openssl s_client -connect %h:%p -no_ssl2 -ign_eof -starttls imap")))
                  (open-tls-stream
                   "*nnimap*" (current-buffer) nnimap-address
-                  (setq port (or nnimap-server-port "imap"))
-                  'starttls))
+                  (setq port (or nnimap-server-port "imap"))))
                '("imap"))
               ((memq nnimap-stream '(ssl tls))
+               (nnheader-message 7 "Opening connection to %s via tls..."
+                                 nnimap-address)
                (funcall (if (fboundp 'open-gnutls-stream)
                             'open-gnutls-stream
                           'open-tls-stream)
@@ -357,8 +376,16 @@ textual parts.")
              (push (format "%s" nnimap-server-port) ports))
            ;; If this is a STARTTLS-capable server, then sever the
            ;; connection and start a STARTTLS connection instead.
-           (when (and (eq nnimap-stream 'network)
-                      (member "STARTTLS" (nnimap-capabilities nnimap-object)))
+           (cond
+            ((and (or (and (eq nnimap-stream 'network)
+                           (member "STARTTLS"
+                                   (nnimap-capabilities nnimap-object)))
+                      (eq nnimap-stream 'starttls))
+                  (fboundp 'open-gnutls-stream))
+             (nnimap-command "STARTTLS")
+             (gnutls-negotiate (nnimap-process nnimap-object) nil))
+            ((and (eq nnimap-stream 'network)
+                  (member "STARTTLS" (nnimap-capabilities nnimap-object)))
              (let ((nnimap-stream 'starttls))
                (let ((tls-process
                       (nnimap-open-connection buffer)))
@@ -369,7 +396,7 @@ textual parts.")
                  (when (memq (process-status tls-process) '(open run))
                    (delete-process (nnimap-process nnimap-object))
                    (kill-buffer (current-buffer))
-                   (return tls-process)))))
+                   (return tls-process))))))
            (unless (equal connection-result "PREAUTH")
              (if (not (setq credentials
                             (if (eq nnimap-authenticator 'anonymous)
@@ -403,19 +430,6 @@ textual parts.")
                (nnimap-command "ENABLE QRESYNC"))
              (nnimap-process nnimap-object))))))))
 
-(defun nnimap-extend-tls-programs ()
-  (let ((programs tls-program)
-       result)
-    (unless (consp programs)
-      (setq programs (list programs)))
-    (dolist (program programs)
-      (when (assoc (car (split-string program)) tls-starttls-switches)
-       (push (if (not (string-match "%s" program))
-                 (concat program " " "%s")
-               program)
-             result)))
-    (nreverse result)))
-
 (defun nnimap-find-parameter (parameter elems)
   (let (result)
     (dolist (elem elems)
@@ -432,6 +446,7 @@ textual parts.")
   (when (nnoo-change-server 'nnimap server nil)
     (ignore-errors
       (delete-process (get-buffer-process (nnimap-buffer))))
+    (nnoo-close-server 'nnimap server)
     t))
 
 (deffoo nnimap-request-close ()
@@ -465,7 +480,7 @@ textual parts.")
                                (let ((start (point)))
                                  (forward-sexp 1)
                                  (downcase-region start (point))
-                                 (goto-char (point))
+                                 (goto-char start)
                                  (read (current-buffer))))
                    parts (nnimap-find-wanted-parts structure))))
          (when (if parts
@@ -478,12 +493,28 @@ textual parts.")
                (nnheader-ms-strip-cr)
                (cons group article)))))))))
 
-(defun nnimap-get-whole-article (article)
+(deffoo nnimap-request-head (article &optional group server to-buffer)
+  (when (nnimap-possibly-change-group group server)
+    (with-current-buffer (nnimap-buffer)
+      (when (stringp article)
+       (setq article (nnimap-find-article-by-message-id group article)))
+      (nnimap-get-whole-article
+       article (format "UID FETCH %%d %s"
+                      (nnimap-header-parameters)))
+      (let ((buffer (current-buffer)))
+       (with-current-buffer (or to-buffer nntp-server-buffer)
+         (erase-buffer)
+         (insert-buffer-substring buffer)
+         (nnheader-ms-strip-cr)
+         (cons group article))))))
+
+(defun nnimap-get-whole-article (article &optional command)
   (let ((result
         (nnimap-command
-         (if (nnimap-ver4-p)
-             "UID FETCH %d BODY.PEEK[]"
-           "UID FETCH %d RFC822.PEEK")
+         (or command
+             (if (nnimap-ver4-p)
+                 "UID FETCH %d BODY.PEEK[]"
+               "UID FETCH %d RFC822.PEEK"))
          article)))
     ;; Check that we really got an article.
     (goto-char (point-min))
@@ -553,9 +584,9 @@ textual parts.")
        (pop bstruc))
       (setq type (car bstruc))
       (setq bstruc (car (cdr bstruc)))
-      (when (and (stringp (car bstruc))
-                (string= (downcase (car bstruc)) "boundary"))
-       (setq boundary (cadr bstruc))))
+      (let ((has-boundary (member "boundary" bstruc)))
+        (when has-boundary
+          (setq boundary (cadr has-boundary)))))
     (when subp
       (insert (format "Content-type: multipart/%s; boundary=%S\n\n"
                      (downcase type) boundary)))
@@ -605,7 +636,13 @@ textual parts.")
     (nreverse parts)))
 
 (deffoo nnimap-request-group (group &optional server dont-check info)
-  (let ((result (nnimap-possibly-change-group group server))
+  (let ((result (nnimap-possibly-change-group
+                ;; Don't SELECT the group if we're going to select it
+                ;; later, anyway.
+                (if dont-check
+                    nil
+                  group)
+                server))
        articles active marks high low)
     (with-current-buffer nntp-server-buffer
       (when result
@@ -622,6 +659,7 @@ textual parts.")
                   (nnimap-send-command "SELECT %S" (utf7-encode group t)))
                  (flag-sequence
                   (nnimap-send-command "UID FETCH 1:* FLAGS")))
+             (setf (nnimap-group nnimap-object) group)
              (nnimap-wait-for-response flag-sequence)
              (setq marks
                    (nnimap-flags-to-marks
@@ -657,6 +695,12 @@ textual parts.")
 (deffoo nnimap-request-rename-group (group new-name &optional server)
   (when (nnimap-possibly-change-group nil server)
     (with-current-buffer (nnimap-buffer)
+      ;; Make sure we don't have this group open read/write by asking
+      ;; to examine a mailbox that doesn't exist.  This seems to be
+      ;; the only way that allows us to reliably go back to unselected
+      ;; state on Courier.
+      (nnimap-command "EXAMINE DOES.NOT.EXIST")
+      (setf (nnimap-group nnimap-object) nil)
       (car (nnimap-command "RENAME %S %S"
                           (utf7-encode group t) (utf7-encode new-name t))))))
 
@@ -689,7 +733,11 @@ textual parts.")
 (deffoo nnimap-request-move-article (article group server accept-form
                                             &optional last internal-move-group)
   (with-temp-buffer
-    (when (nnimap-request-article article group server (current-buffer))
+    (mm-disable-multibyte)
+    (when (funcall (if internal-move-group
+                      'nnimap-request-head
+                    'nnimap-request-article)
+                  article group server (current-buffer))
       ;; If the move is internal (on the same server), just do it the easy
       ;; way.
       (let ((message-id (message-field-value "message-id")))
@@ -719,7 +767,7 @@ textual parts.")
    ((and force
         (eq nnmail-expiry-target 'delete))
     (unless (nnimap-delete-article (gnus-compress-sequence articles))
-      (message "Article marked for deletion, but not expunged."))
+      (nnheader-message 7 "Article marked for deletion, but not expunged."))
     nil)
    (t
     (let ((deletable-articles
@@ -744,8 +792,9 @@ textual parts.")
     (dolist (article articles)
       (let ((target nnmail-expiry-target))
        (with-temp-buffer
+          (mm-disable-multibyte)
          (when (nnimap-request-article article group server (current-buffer))
-           (message "Expiring article %s:%d" group article)
+           (nnheader-message 7 "Expiring article %s:%d" group article)
            (when (functionp target)
              (setq target (funcall target group)))
            (when (and target
@@ -815,7 +864,7 @@ textual parts.")
   (when (and (nnimap-possibly-change-group nil server)
             nnimap-inbox
             nnimap-split-methods)
-    (message "nnimap %s splitting mail..." server)
+    (nnheader-message 7 "nnimap %s splitting mail..." server)
     (nnimap-split-incoming-mail)))
 
 (defun nnimap-marks-to-flags (marks)
@@ -854,7 +903,7 @@ textual parts.")
     (let ((message-id (message-field-value "message-id"))
          sequence message)
       (nnimap-add-cr)
-      (setq message (buffer-string))
+      (setq message (buffer-substring-no-properties (point-min) (point-max)))
       (with-current-buffer (nnimap-buffer)
        (setq sequence (nnimap-send-command
                        "APPEND %S {%d}" (utf7-encode group t)
@@ -867,11 +916,22 @@ textual parts.")
        (let ((result (nnimap-get-response sequence)))
          (if (not (car result))
              (progn
-               (message "%s" (nnheader-get-report-string 'nnimap))
+               (nnheader-message 7 "%s" (nnheader-get-report-string 'nnimap))
                nil)
            (cons group
                  (nnimap-find-article-by-message-id group message-id))))))))
 
+(deffoo nnimap-request-replace-article (article group buffer)
+  (let (group-art)
+    (when (and (nnimap-possibly-change-group group nil)
+              ;; Put the article into the group.
+              (with-current-buffer buffer
+                (setq group-art
+                      (nnimap-request-accept-article group nil t))))
+      (nnimap-delete-article (list article))
+      ;; Return the new article number.
+      (cdr group-art))))
+
 (defun nnimap-add-cr ()
   (goto-char (point-min))
   (while (re-search-forward "\r?\n" nil t)
@@ -949,7 +1009,7 @@ textual parts.")
       (erase-buffer)
       (setf (nnimap-group nnimap-object) nil)
       ;; QRESYNC handling isn't implemented.
-      (let ((qresyncp (member "notQRESYNC" (nnimap-capabilities nnimap-object)))
+      (let ((qresyncp (member "QRESYNC" (nnimap-capabilities nnimap-object)))
            params groups sequences active uidvalidity modseq group)
        ;; Go through the infos and gather the data needed to know
        ;; what and how to request the data.
@@ -964,7 +1024,8 @@ textual parts.")
                   modseq)
              (push
               (list (nnimap-send-command "EXAMINE %S (QRESYNC (%s %s))"
-                                         group uidvalidity modseq)
+                                         (utf7-encode group t)
+                                         uidvalidity modseq)
                     'qresync
                     nil group 'qresync)
               sequences)
@@ -982,7 +1043,8 @@ textual parts.")
                     ;; examine), but will tell us whether the group
                     ;; is read-only or not.
                     "SELECT")))
-             (push (list (nnimap-send-command "%s %S" command group)
+             (push (list (nnimap-send-command "%s %S" command
+                                              (utf7-encode group t))
                          (nnimap-send-command "UID FETCH %d:* FLAGS" start)
                          start group command)
                    sequences)))
@@ -1038,7 +1100,9 @@ textual parts.")
      ;; completely empty groups.
      ((and (not existing)
           (not uidnext))
-      )
+      (let ((active (cdr (assq 'active (gnus-info-params info)))))
+       (when active
+         (gnus-set-active (gnus-info-group info) active))))
      ;; We have a mismatch between the old and new UIDVALIDITY
      ;; identifiers, so we have to re-request the group info (the next
      ;; time).  This virtually never happens.
@@ -1051,9 +1115,11 @@ textual parts.")
       (gnus-group-remove-parameter info 'modseq))
      ;; We have the data needed to update.
      (t
-      (let ((group (gnus-info-group info))
-           (completep (and start-article
-                           (= start-article 1))))
+      (let* ((group (gnus-info-group info))
+            (completep (and start-article
+                            (= start-article 1)))
+            (active (or (gnus-active group)
+                        (cdr (assq 'active (gnus-info-params info))))))
        (when uidnext
          (setq high (1- uidnext)))
        ;; First set the active ranges based on high/low.
@@ -1066,6 +1132,8 @@ textual parts.")
                              (uidnext
                               ;; No articles in this group.
                               (cons uidnext (1- uidnext)))
+                             (active
+                              active)
                              (start-article
                               (cons start-article (1- start-article)))
                              (t
@@ -1073,23 +1141,24 @@ textual parts.")
                               nil)))
          (gnus-set-active
           group
-          (cons (car (gnus-active group))
+          (cons (car active)
                 (or high (1- uidnext)))))
        ;; See whether this is a read-only group.
        (unless (eq permanent-flags 'not-scanned)
          (gnus-group-set-parameter
           info 'permanent-flags
-          (if (memq '%* permanent-flags)
-              t
-            nil)))
+          (and (or (memq '%* permanent-flags)
+                   (memq '%Seen permanent-flags))
+               permanent-flags)))
        ;; Update marks and read articles if this isn't a
        ;; read-only IMAP group.
-       (when (cdr (assq 'permanent-flags (gnus-info-params info)))
+       (when (setq permanent-flags
+                   (cdr (assq 'permanent-flags (gnus-info-params info))))
          (if (and highestmodseq
                   (not start-article))
              ;; We've gotten the data by QRESYNCing.
              (nnimap-update-qresync-info
-              info (nnimap-imap-ranges-to-gnus-ranges vanished) flags)
+              info existing (nnimap-imap-ranges-to-gnus-ranges vanished) flags)
            ;; Do normal non-QRESYNC flag updates.
            ;; Update the list of read articles.
            (let* ((unread
@@ -1110,40 +1179,67 @@ textual parts.")
                            (gnus-info-read info))
                         (gnus-info-read info))
                       read)))
-             (gnus-info-set-read info read)
+             (when (or (not (listp permanent-flags))
+                       (memq '%Seen permanent-flags))
+               (gnus-info-set-read info read))
              ;; Update the marks.
              (setq marks (gnus-info-marks info))
              (dolist (type (cdr nnimap-mark-alist))
-               (let ((old-marks (assoc (car type) marks))
-                     (new-marks
-                      (gnus-compress-sequence
-                       (cdr (or (assoc (caddr type) flags) ; %Flagged
-                                (assoc (intern (cadr type) obarray) flags)
-                                (assoc (cadr type) flags)))))) ; "\Flagged"
-                 (setq marks (delq old-marks marks))
-                 (pop old-marks)
-                 (when (and old-marks
-                            (> start-article 1))
-                   (setq old-marks (gnus-range-difference
-                                    old-marks
-                                    (cons start-article high)))
-                   (setq new-marks (gnus-range-nconcat old-marks new-marks)))
-                 (when new-marks
-                   (push (cons (car type) new-marks) marks)))
-               (gnus-info-set-marks info marks t)))))
+               (when (or (not (listp permanent-flags))
+                         (memq (assoc (caddr type) flags) permanent-flags)
+                         (memq '%* permanent-flags))
+                 (let ((old-marks (assoc (car type) marks))
+                       (new-marks
+                        (gnus-compress-sequence
+                         (cdr (or (assoc (caddr type) flags) ; %Flagged
+                                  (assoc (intern (cadr type) obarray) flags)
+                                  (assoc (cadr type) flags)))))) ; "\Flagged"
+                   (setq marks (delq old-marks marks))
+                   (pop old-marks)
+                   (when (and old-marks
+                              (> start-article 1))
+                     (setq old-marks (gnus-range-difference
+                                      old-marks
+                                      (cons start-article high)))
+                     (setq new-marks (gnus-range-nconcat old-marks new-marks)))
+                   (when new-marks
+                     (push (cons (car type) new-marks) marks)))))
+             (gnus-info-set-marks info marks t))))
        ;; Note the active level for the next run-through.
        (gnus-group-set-parameter info 'active (gnus-active group))
        (gnus-group-set-parameter info 'uidvalidity uidvalidity)
        (gnus-group-set-parameter info 'modseq highestmodseq)
        (nnimap-store-info info (gnus-active group)))))))
 
-(defun nnimap-update-qresync-info (info vanished flags)
+(defun nnimap-update-qresync-info (info existing vanished flags)
   ;; Add all the vanished articles to the list of read articles.
   (gnus-info-set-read
    info
-   (gnus-range-add (gnus-info-read info)
-                  vanished))
-  )
+   (gnus-add-to-range
+    (gnus-add-to-range
+     (gnus-range-add (gnus-info-read info)
+                    vanished)
+     (cdr (assq '%Flagged flags)))
+    (cdr (assq '%Seen flags))))
+  (let ((marks (gnus-info-marks info)))
+    (dolist (type (cdr nnimap-mark-alist))
+      (let ((ticks (assoc (car type) marks))
+           (new-marks
+            (cdr (or (assoc (caddr type) flags) ; %Flagged
+                     (assoc (intern (cadr type) obarray) flags)
+                     (assoc (cadr type) flags))))) ; "\Flagged"
+       (setq marks (delq ticks marks))
+       (pop ticks)
+       ;; Add the new marks we got.
+       (setq ticks (gnus-add-to-range ticks new-marks))
+       ;; Remove the marks from messages that don't have them.
+       (setq ticks (gnus-remove-from-range
+                    ticks
+                    (gnus-compress-sequence
+                     (gnus-sorted-complement existing new-marks))))
+       (when ticks
+         (push (cons (car type) ticks) marks)))
+      (gnus-info-set-marks info marks t))))
 
 (defun nnimap-imap-ranges-to-gnus-ranges (irange)
   (if (zerop (length irange))
@@ -1251,7 +1347,7 @@ textual parts.")
                (setq start end))
            (setq start (point))
            (goto-char end))
-         (while (search-forward " FETCH " start t)
+         (while (re-search-forward "^\\* [0-9]+ FETCH " start t)
            (setq elems (read (current-buffer)))
            (push (cons (cadr (memq 'UID elems))
                        (cadr (memq 'FLAGS elems)))
@@ -1271,6 +1367,25 @@ textual parts.")
   (setq nnimap-status-string "Read-only server")
   nil)
 
+(deffoo nnimap-request-thread (id)
+    (let* ((refs (split-string
+              (or (mail-header-references (gnus-summary-article-header))
+                  "")))
+          (cmd (let ((value
+                      (format
+                       "(OR HEADER REFERENCES %s HEADER Message-Id %s)"
+                       id id)))
+                 (dolist (refid refs value)
+                   (setq value (format
+                                "(OR (OR HEADER Message-Id %s HEADER REFERENCES %s) %s)"
+                                refid refid value)))))
+          (result
+           (with-current-buffer (nnimap-buffer)
+             (nnimap-command  "UID SEARCH %s" cmd))))
+      (gnus-fetch-headers (and (car result)
+          (delete 0 (mapcar #'string-to-number
+                            (cdr (assoc "SEARCH" (cdr result)))))))))
+
 (defun nnimap-possibly-change-group (group server)
   (let ((open-result t))
     (when (and server
@@ -1367,7 +1482,7 @@ textual parts.")
                              (point-min))
                            t)))
            (when messagep
-             (message "nnimap read %dk" (/ (buffer-size) 1000)))
+             (nnheader-message 7 "nnimap read %dk" (/ (buffer-size) 1000)))
            (nnheader-accept-process-output process)
            (goto-char (point-max)))
           openp)
@@ -1394,6 +1509,7 @@ textual parts.")
 (defun nnimap-parse-line (line)
   (let (char result)
     (with-temp-buffer
+      (mm-disable-multibyte)
       (insert line)
       (goto-char (point-min))
       (while (not (eobp))
@@ -1587,8 +1703,10 @@ textual parts.")
        (forward-char (1+ bytes))
        (setq bytes (nnimap-get-length))
        (delete-region (line-beginning-position) (line-end-position))
-       (forward-char (1+ bytes))
-       (delete-region (line-beginning-position) (line-end-position))))))
+       ;; There's a body; skip past that.
+       (when bytes
+         (forward-char (1+ bytes))
+         (delete-region (line-beginning-position) (line-end-position)))))))
 
 (defun nnimap-dummy-active-number (group &optional server)
   1)