(nnimap-update-info): Clean up slightly.
[gnus] / lisp / nnimap.el
index 3795605..6de49eb 100644 (file)
@@ -1,6 +1,6 @@
 ;;; nnimap.el --- IMAP interface for Gnus
 
 ;;; nnimap.el --- IMAP interface for Gnus
 
-;; Copyright (C) 2010 Free Software Foundation, Inc.
+;; Copyright (C) 2010-2011 Free Software Foundation, Inc.
 
 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
 ;;         Simon Josefsson <simon@josefsson.org>
 
 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
 ;;         Simon Josefsson <simon@josefsson.org>
@@ -44,6 +44,8 @@
 (require 'utf7)
 (require 'tls)
 (require 'parse-time)
 (require 'utf7)
 (require 'tls)
 (require 'parse-time)
+(require 'nnmail)
+(require 'proto-stream)
 
 (autoload 'auth-source-forget-user-or-password "auth-source")
 (autoload 'auth-source-user-or-password "auth-source")
 
 (autoload 'auth-source-forget-user-or-password "auth-source")
 (autoload 'auth-source-user-or-password "auth-source")
 If nnimap-stream is `ssl', this will default to `imaps'.  If not,
 it will default to `imap'.")
 
 If nnimap-stream is `ssl', this will default to `imaps'.  If not,
 it will default to `imap'.")
 
-(defvoo nnimap-stream 'ssl
+(defvoo nnimap-stream 'undecided
   "How nnimap will talk to the IMAP server.
   "How nnimap will talk to the IMAP server.
-Values are `ssl', `network', `starttls' or `shell'.")
+Values are `ssl', `network', `starttls' or `shell'.
+The default is to try `ssl' first, and then `network'.")
 
 (defvoo nnimap-shell-program (if (boundp 'imap-shell-program)
                                 (if (listp imap-shell-program)
 
 (defvoo nnimap-shell-program (if (boundp 'imap-shell-program)
                                 (if (listp imap-shell-program)
@@ -121,7 +124,7 @@ textual parts.")
 
 (defstruct nnimap
   group process commands capabilities select-result newlinep server
 
 (defstruct nnimap
   group process commands capabilities select-result newlinep server
-  last-command-time greeting)
+  last-command-time greeting examined stream-type)
 
 (defvar nnimap-object nil)
 
 
 (defvar nnimap-object nil)
 
@@ -136,6 +139,9 @@ textual parts.")
     (download "gnus-download")
     (forward "gnus-forward")))
 
     (download "gnus-download")
     (forward "gnus-forward")))
 
+(defvar nnimap-quirks
+  '(("QRESYNC" "Zimbra" "QRESYNC ")))
+
 (defun nnimap-buffer ()
   (nnimap-find-process-buffer nntp-server-buffer))
 
 (defun nnimap-buffer ()
   (nnimap-find-process-buffer nntp-server-buffer))
 
@@ -161,7 +167,8 @@ textual parts.")
          (nnimap-article-ranges (gnus-compress-sequence articles))
          (nnimap-header-parameters))
         t)
          (nnimap-article-ranges (gnus-compress-sequence articles))
          (nnimap-header-parameters))
         t)
-       (nnimap-transform-headers))
+       (nnimap-transform-headers)
+       (nnheader-remove-cr-followed-by-lf))
       (insert-buffer-substring
        (nnimap-find-process-buffer (current-buffer))))
     'headers))
       (insert-buffer-substring
        (nnimap-find-process-buffer (current-buffer))))
     'headers))
@@ -177,11 +184,12 @@ textual parts.")
            (return)))
        (setq article (match-string 1))
        ;; Unfold quoted {number} strings.
            (return)))
        (setq article (match-string 1))
        ;; Unfold quoted {number} strings.
-       (while (re-search-forward "[^]][ (]{\\([0-9]+\\)}\r\n"
+       (while (re-search-forward "[^]][ (]{\\([0-9]+\\)}\r?\n"
                                  (1+ (line-end-position)) t)
          (setq size (string-to-number (match-string 1)))
          (delete-region (+ (match-beginning 0) 2) (point))
                                  (1+ (line-end-position)) t)
          (setq size (string-to-number (match-string 1)))
          (delete-region (+ (match-beginning 0) 2) (point))
-         (setq string (delete-region (point) (+ (point) size)))
+         (setq string (buffer-substring (point) (+ (point) size)))
+         (delete-region (point) (+ (point) size))
          (insert (format "%S" string)))
        (setq bytes (nnimap-get-length)
              lines nil)
          (insert (format "%S" string)))
        (setq bytes (nnimap-get-length)
              lines nil)
@@ -212,6 +220,16 @@ textual parts.")
        (insert ".")
        (forward-line 1)))))
 
        (insert ".")
        (forward-line 1)))))
 
+(defun nnimap-unfold-quoted-lines ()
+  ;; Unfold quoted {number} strings.
+  (let (size string)
+    (while (re-search-forward " {\\([0-9]+\\)}\r?\n" nil t)
+      (setq size (string-to-number (match-string 1)))
+      (delete-region (1+ (match-beginning 0)) (point))
+      (setq string (buffer-substring (point) (+ (point) size)))
+      (delete-region (point) (+ (point) size))
+      (insert (format "%S" string)))))
+
 (defun nnimap-get-length ()
   (and (re-search-forward "{\\([0-9]+\\)}" (line-end-position) t)
        (string-to-number (match-string 1))))
 (defun nnimap-get-length ()
   (and (re-search-forward "{\\([0-9]+\\)}" (line-end-position) t)
        (string-to-number (match-string 1))))
@@ -256,16 +274,6 @@ textual parts.")
     (push (current-buffer) nnimap-process-buffers)
     (current-buffer)))
 
     (push (current-buffer) nnimap-process-buffers)
     (current-buffer)))
 
-(defun nnimap-open-shell-stream (name buffer host port)
-  (let ((process-connection-type nil))
-    (start-process name buffer shell-file-name
-                  shell-command-switch
-                  (format-spec
-                   nnimap-shell-program
-                   (format-spec-make
-                    ?s host
-                    ?p port)))))
-
 (defun nnimap-credentials (address ports &optional inhibit-create)
   (let (port credentials)
     ;; Request the credentials from all ports, but only query on the
 (defun nnimap-credentials (address ports &optional inhibit-create)
   (let (port credentials)
     ;; Request the credentials from all ports, but only query on the
@@ -287,7 +295,7 @@ textual parts.")
        (with-current-buffer buffer
          (when (and nnimap-object
                     (nnimap-last-command-time nnimap-object)
        (with-current-buffer buffer
          (when (and nnimap-object
                     (nnimap-last-command-time nnimap-object)
-                    (> (time-to-seconds
+                    (> (gnus-float-time
                         (time-subtract
                          now
                          (nnimap-last-command-time nnimap-object)))
                         (time-subtract
                          now
                          (nnimap-last-command-time nnimap-object)))
@@ -295,113 +303,80 @@ textual parts.")
                        (* 5 60)))
            (nnimap-send-command "NOOP")))))))
 
                        (* 5 60)))
            (nnimap-send-command "NOOP")))))))
 
-(declare-function gnutls-negotiate "subr" (fn file &optional arglist fileonly))
-
 (defun nnimap-open-connection (buffer)
 (defun nnimap-open-connection (buffer)
+  ;; Be backwards-compatible -- the earlier value of nnimap-stream was
+  ;; `ssl' when nnimap-server-port was nil.  Sort of.
+  (when (and nnimap-server-port
+            (eq nnimap-stream 'undecided))
+    (setq nnimap-stream 'ssl))
+  (let ((stream
+        (if (eq nnimap-stream 'undecided)
+            (loop for type in '(ssl network)
+                  for stream = (let ((nnimap-stream type))
+                                 (nnimap-open-connection-1 buffer))
+                  while (eq stream 'no-connect)
+                  finally (return stream))
+          (nnimap-open-connection-1 buffer))))
+    (if (eq stream 'no-connect)
+       nil
+      stream)))
+
+(defun nnimap-open-connection-1 (buffer)
   (unless nnimap-keepalive-timer
     (setq nnimap-keepalive-timer (run-at-time (* 60 15) (* 60 15)
                                              'nnimap-keepalive)))
   (unless nnimap-keepalive-timer
     (setq nnimap-keepalive-timer (run-at-time (* 60 15) (* 60 15)
                                              'nnimap-keepalive)))
-  (block nil
-    (with-current-buffer (nnimap-make-process-buffer buffer)
-      (let* ((coding-system-for-read 'binary)
-            (coding-system-for-write 'binary)
-            (port nil)
-            (ports
-             (cond
-              ((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
-                      (or nnimap-server-port
-                          (if (netrc-find-service-number "imap")
-                              "imap"
-                            "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)
-               (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"))))
-               '("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)
-                        "*nnimap*" (current-buffer) nnimap-address
-                        (setq port
-                              (or nnimap-server-port
-                                  (if (netrc-find-service-number "imaps")
-                                      "imaps"
-                                    "993"))))
-               '("143" "993" "imap" "imaps"))
-              (t
-               (error "Unknown stream type: %s" nnimap-stream))))
-            connection-result login-result credentials)
-       (setf (nnimap-process nnimap-object)
-             (get-buffer-process (current-buffer)))
-       (if (not (and (nnimap-process nnimap-object)
-                     (memq (process-status (nnimap-process nnimap-object))
-                           '(open run))))
-           (nnheader-report 'nnimap "Unable to contact %s:%s via %s"
-                            nnimap-address port nnimap-stream)
-         (gnus-set-process-query-on-exit-flag
-          (nnimap-process nnimap-object) nil)
-         (if (not (setq connection-result (nnimap-wait-for-connection)))
-             (nnheader-report 'nnimap
-                              "%s" (buffer-substring
-                                    (point) (line-end-position)))
+  (with-current-buffer (nnimap-make-process-buffer buffer)
+    (let* ((coding-system-for-read 'binary)
+          (coding-system-for-write 'binary)
+          (port nil)
+          (ports
+           (cond
+            ((or (eq nnimap-stream 'network)
+                 (eq nnimap-stream 'starttls))
+             (nnheader-message 7 "Opening connection to %s..."
+                               nnimap-address)
+             '("143" "imap"))
+            ((eq nnimap-stream 'shell)
+             (nnheader-message 7 "Opening connection to %s via shell..."
+                               nnimap-address)
+             '("imap"))
+            ((memq nnimap-stream '(ssl tls))
+             (nnheader-message 7 "Opening connection to %s via tls..."
+                               nnimap-address)
+             '("143" "993" "imap" "imaps"))
+            (t
+             (error "Unknown stream type: %s" nnimap-stream))))
+          (proto-stream-always-use-starttls t)
+           login-result credentials)
+      (when nnimap-server-port
+       (setq ports (append ports (list nnimap-server-port))))
+      (destructuring-bind (stream greeting capabilities stream-type)
+         (open-protocol-stream
+          "*nnimap*" (current-buffer) nnimap-address (car (last ports))
+          :type nnimap-stream
+          :shell-command nnimap-shell-program
+          :capability-command "1 CAPABILITY\r\n"
+          :success " OK "
+          :starttls-function
+          (lambda (capabilities)
+            (when (gnus-string-match-p "STARTTLS" capabilities)
+              "1 STARTTLS\r\n")))
+       (setf (nnimap-process nnimap-object) stream)
+       (setf (nnimap-stream-type nnimap-object) stream-type)
+       (if (not stream)
+           (progn
+             (nnheader-report 'nnimap "Unable to contact %s:%s via %s"
+                              nnimap-address port nnimap-stream)
+             'no-connect)
+         (gnus-set-process-query-on-exit-flag stream nil)
+         (if (not (gnus-string-match-p "[*.] \\(OK\\|PREAUTH\\)" greeting))
+             (nnheader-report 'nnimap "%s" greeting)
            ;; Store the greeting (for debugging purposes).
            ;; Store the greeting (for debugging purposes).
-           (setf (nnimap-greeting nnimap-object)
-                 (buffer-substring (line-beginning-position)
-                                   (line-end-position)))
-           ;; Store the capabilities.
+           (setf (nnimap-greeting nnimap-object) greeting)
            (setf (nnimap-capabilities nnimap-object)
            (setf (nnimap-capabilities nnimap-object)
-                 (mapcar
-                  #'upcase
-                  (nnimap-find-parameter
-                   "CAPABILITY" (cdr (nnimap-command "CAPABILITY")))))
-           (when nnimap-server-port
-             (push (format "%s" nnimap-server-port) ports))
-           ;; If this is a STARTTLS-capable server, then sever the
-           ;; connection and start a STARTTLS connection instead.
-           (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)))
-                 ;; If the STARTTLS connection was successful, we
-                 ;; kill our first non-encrypted connection.  If it
-                 ;; wasn't successful, we just use our unencrypted
-                 ;; connection.
-                 (when (memq (process-status tls-process) '(open run))
-                   (delete-process (nnimap-process nnimap-object))
-                   (kill-buffer (current-buffer))
-                   (return tls-process))))))
-           (unless (equal connection-result "PREAUTH")
+                 (mapcar #'upcase
+                         (split-string capabilities)))
+           (unless (gnus-string-match-p "[*.] PREAUTH" greeting)
              (if (not (setq credentials
                             (if (eq nnimap-authenticator 'anonymous)
                                 (list "anonymous"
              (if (not (setq credentials
                             (if (eq nnimap-authenticator 'anonymous)
                                 (list "anonymous"
@@ -416,17 +391,7 @@ textual parts.")
                                (nnimap-credentials nnimap-address ports)))))
                  (setq nnimap-object nil)
                (setq login-result
                                (nnimap-credentials nnimap-address ports)))))
                  (setq nnimap-object nil)
                (setq login-result
-                     (if (member "AUTH=PLAIN"
-                                 (nnimap-capabilities nnimap-object))
-                         (nnimap-command
-                          "AUTHENTICATE PLAIN %s"
-                          (base64-encode-string
-                           (format "\000%s\000%s"
-                                   (nnimap-quote-specials (car credentials))
-                                   (nnimap-quote-specials (cadr credentials)))))
-                       (nnimap-command "LOGIN %S %S"
-                                       (car credentials)
-                                       (cadr credentials))))
+                     (nnimap-login (car credentials) (cadr credentials)))
                (unless (car login-result)
                  ;; If the login failed, then forget the credentials
                  ;; that are now possibly cached.
                (unless (car login-result)
                  ;; If the login failed, then forget the credentials
                  ;; that are now possibly cached.
@@ -439,10 +404,43 @@ textual parts.")
                  (delete-process (nnimap-process nnimap-object))
                  (setq nnimap-object nil))))
            (when nnimap-object
                  (delete-process (nnimap-process nnimap-object))
                  (setq nnimap-object nil))))
            (when nnimap-object
-             (when (member "QRESYNC" (nnimap-capabilities nnimap-object))
+             (when (nnimap-capability "QRESYNC")
                (nnimap-command "ENABLE QRESYNC"))
              (nnimap-process nnimap-object))))))))
 
                (nnimap-command "ENABLE QRESYNC"))
              (nnimap-process nnimap-object))))))))
 
+(autoload 'rfc2104-hash "rfc2104")
+
+(defun nnimap-login (user password)
+  (cond
+   ;; Prefer plain LOGIN if it's enabled (since it requires fewer
+   ;; round trips than CRAM-MD5, and it's less likely to be buggy),
+   ;; and we're using an encrypted connection.
+   ((and (not (nnimap-capability "LOGINDISABLED"))
+        (eq (nnimap-stream-type nnimap-object) 'tls))
+    (nnimap-command "LOGIN %S %S" user password))
+   ((nnimap-capability "AUTH=CRAM-MD5")
+    (erase-buffer)
+    (let ((sequence (nnimap-send-command "AUTHENTICATE CRAM-MD5"))
+         (challenge (nnimap-wait-for-line "^\\+\\(.*\\)\n")))
+      (process-send-string
+       (get-buffer-process (current-buffer))
+       (concat
+       (base64-encode-string
+        (concat user " "
+                (rfc2104-hash 'md5 64 16 password
+                              (base64-decode-string challenge))))
+       "\r\n"))
+      (nnimap-wait-for-response sequence)))
+   ((not (nnimap-capability "LOGINDISABLED"))
+    (nnimap-command "LOGIN %S %S" user password))
+   ((nnimap-capability "AUTH=PLAIN")
+    (nnimap-command
+     "AUTHENTICATE PLAIN %s"
+     (base64-encode-string
+      (format "\000%s\000%s"
+             (nnimap-quote-specials user)
+             (nnimap-quote-specials password)))))))
+
 (defun nnimap-quote-specials (string)
   (with-temp-buffer
     (insert string)
 (defun nnimap-quote-specials (string)
   (with-temp-buffer
     (insert string)
@@ -521,15 +519,17 @@ textual parts.")
     (with-current-buffer (nnimap-buffer)
       (when (stringp article)
        (setq article (nnimap-find-article-by-message-id group article)))
     (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))))))
+      (if (null article)
+         nil
+       (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
 
 (defun nnimap-get-whole-article (article &optional command)
   (let ((result
@@ -555,8 +555,11 @@ textual parts.")
        (delete-region (point) (point-max)))
       t)))
 
        (delete-region (point) (point-max)))
       t)))
 
+(defun nnimap-capability (capability)
+  (member capability (nnimap-capabilities nnimap-object)))
+
 (defun nnimap-ver4-p ()
 (defun nnimap-ver4-p ()
-  (member "IMAP4REV1" (nnimap-capabilities nnimap-object)))
+  (nnimap-capability "IMAP4REV1"))
 
 (defun nnimap-get-partial-article (article parts structure)
   (let ((result
 
 (defun nnimap-get-partial-article (article parts structure)
   (let ((result
@@ -588,7 +591,7 @@ textual parts.")
     ;; Collect all the body parts.
     (while (looking-at ".*BODY\\[\\([.0-9]+\\)\\]")
       (setq id (match-string 1)
     ;; Collect all the body parts.
     (while (looking-at ".*BODY\\[\\([.0-9]+\\)\\]")
       (setq id (match-string 1)
-           bytes (nnimap-get-length))
+           bytes (or (nnimap-get-length) 0))
       (beginning-of-line)
       (delete-region (point) (progn (forward-line 1) (point)))
       (push (list id (buffer-substring (point) (+ (point) bytes)))
       (beginning-of-line)
       (delete-region (point) (progn (forward-line 1) (point)))
       (push (list id (buffer-substring (point) (+ (point) bytes)))
@@ -662,7 +665,8 @@ textual parts.")
   (let ((result (nnimap-possibly-change-group
                 ;; Don't SELECT the group if we're going to select it
                 ;; later, anyway.
   (let ((result (nnimap-possibly-change-group
                 ;; Don't SELECT the group if we're going to select it
                 ;; later, anyway.
-                (if dont-check
+                (if (and (not dont-check)
+                         (assoc group nnimap-current-infos))
                     nil
                   group)
                 server))
                     nil
                   group)
                 server))
@@ -691,7 +695,8 @@ textual parts.")
                                  1 group "SELECT")))))
              (when (and info
                         marks)
                                  1 group "SELECT")))))
              (when (and info
                         marks)
-               (nnimap-update-infos marks (list info)))
+               (nnimap-update-infos marks (list info))
+               (nnimap-store-info info (gnus-active (gnus-info-group info))))
              (goto-char (point-max))
              (let ((uidnext (nth 5 (car marks))))
                (setq high (or (if uidnext
              (goto-char (point-max))
              (let ((uidnext (nth 5 (car marks))))
                (setq high (or (if uidnext
@@ -718,15 +723,17 @@ textual parts.")
 (deffoo nnimap-request-rename-group (group new-name &optional server)
   (when (nnimap-possibly-change-group nil server)
     (with-current-buffer (nnimap-buffer)
 (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)
+      (nnimap-unselect-group)
       (car (nnimap-command "RENAME %S %S"
                           (utf7-encode group t) (utf7-encode new-name t))))))
 
       (car (nnimap-command "RENAME %S %S"
                           (utf7-encode group t) (utf7-encode new-name t))))))
 
+(defun nnimap-unselect-group ()
+  ;; 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"))
+
 (deffoo nnimap-request-expunge-group (group &optional server)
   (when (nnimap-possibly-change-group group server)
     (with-current-buffer (nnimap-buffer)
 (deffoo nnimap-request-expunge-group (group &optional server)
   (when (nnimap-possibly-change-group group server)
     (with-current-buffer (nnimap-buffer)
@@ -773,8 +780,9 @@ textual parts.")
              (when (car result)
                (nnimap-delete-article article)
                (cons internal-move-group
              (when (car result)
                (nnimap-delete-article article)
                (cons internal-move-group
-                     (nnimap-find-article-by-message-id
-                      internal-move-group message-id))))
+                     (or (nnimap-find-uid-response "COPYUID" (cadr result))
+                         (nnimap-find-article-by-message-id
+                          internal-move-group message-id)))))
          ;; Move the article to a different method.
          (let ((result (eval accept-form)))
            (when result
          ;; Move the article to a different method.
          (let ((result (eval accept-form)))
            (when result
@@ -812,22 +820,42 @@ textual parts.")
 
 (defun nnimap-process-expiry-targets (articles group server)
   (let ((deleted-articles nil))
 
 (defun nnimap-process-expiry-targets (articles group server)
   (let ((deleted-articles nil))
-    (dolist (article articles)
-      (let ((target nnmail-expiry-target))
-       (with-temp-buffer
-          (mm-disable-multibyte)
-         (when (nnimap-request-article article group server (current-buffer))
-           (nnheader-message 7 "Expiring article %s:%d" group article)
-           (when (functionp target)
-             (setq target (funcall target group)))
-           (when (and target
-                      (not (eq target 'delete)))
-             (if (or (gnus-request-group target t)
-                     (gnus-request-create-group target))
-                 (nnmail-expiry-target-group target group)
-               (setq target nil)))
-           (when target
-             (push article deleted-articles))))))
+    (cond
+     ;; shortcut further processing if we're going to delete the articles
+     ((eq nnmail-expiry-target 'delete)
+      (setq deleted-articles articles)
+      t)
+     ;; or just move them to another folder on the same IMAP server
+     ((and (not (functionp nnmail-expiry-target))
+          (gnus-server-equal (gnus-group-method nnmail-expiry-target)
+                             (gnus-server-to-method
+                              (format "nnimap:%s" server))))
+      (and (nnimap-possibly-change-group group server)
+          (with-current-buffer (nnimap-buffer)
+            (nnheader-message 7 "Expiring articles from %s: %s" group articles)
+            (nnimap-command
+             "UID COPY %s %S"
+             (nnimap-article-ranges (gnus-compress-sequence articles))
+             (utf7-encode (gnus-group-real-name nnmail-expiry-target) t))
+            (setq deleted-articles articles)))
+      t)
+     (t
+      (dolist (article articles)
+       (let ((target nnmail-expiry-target))
+         (with-temp-buffer
+           (mm-disable-multibyte)
+           (when (nnimap-request-article article group server (current-buffer))
+             (nnheader-message 7 "Expiring article %s:%d" group article)
+             (when (functionp target)
+               (setq target (funcall target group)))
+             (when (and target
+                        (not (eq target 'delete)))
+               (if (or (gnus-request-group target t)
+                       (gnus-request-create-group target))
+                   (nnmail-expiry-target-group target group)
+                 (setq target nil)))
+             (when target
+               (push article deleted-articles))))))))
     ;; Change back to the current group again.
     (nnimap-possibly-change-group group server)
     (setq deleted-articles (nreverse deleted-articles))
     ;; Change back to the current group again.
     (nnimap-possibly-change-group group server)
     (setq deleted-articles (nreverse deleted-articles))
@@ -854,8 +882,10 @@ textual parts.")
 (defun nnimap-find-article-by-message-id (group message-id)
   (with-current-buffer (nnimap-buffer)
     (erase-buffer)
 (defun nnimap-find-article-by-message-id (group message-id)
   (with-current-buffer (nnimap-buffer)
     (erase-buffer)
-    (setf (nnimap-group nnimap-object) nil)
-    (nnimap-send-command "EXAMINE %S" (utf7-encode group t))
+    (unless (equal group (nnimap-group nnimap-object))
+      (setf (nnimap-group nnimap-object) nil)
+      (setf (nnimap-examined nnimap-object) group)
+      (nnimap-send-command "EXAMINE %S" (utf7-encode group t)))
     (let ((sequence
           (nnimap-send-command "UID SEARCH HEADER Message-Id %S" message-id))
          article result)
     (let ((sequence
           (nnimap-send-command "UID SEARCH HEADER Message-Id %S" message-id))
          article result)
@@ -872,7 +902,7 @@ textual parts.")
     (nnimap-command "UID STORE %s +FLAGS.SILENT (\\Deleted)"
                    (nnimap-article-ranges articles))
     (cond
     (nnimap-command "UID STORE %s +FLAGS.SILENT (\\Deleted)"
                    (nnimap-article-ranges articles))
     (cond
-     ((member "UIDPLUS" (nnimap-capabilities nnimap-object))
+     ((nnimap-capability "UIDPLUS")
       (nnimap-command "UID EXPUNGE %s"
                      (nnimap-article-ranges articles))
       t)
       (nnimap-command "UID EXPUNGE %s"
                      (nnimap-article-ranges articles))
       t)
@@ -897,6 +927,16 @@ textual parts.")
        (push flag flags)))
     flags))
 
        (push flag flags)))
     flags))
 
+(deffoo nnimap-request-update-group-status (group status &optional server)
+  (when (nnimap-possibly-change-group nil server)
+    (let ((command (assoc
+                   status
+                   '((subscribe "SUBSCRIBE")
+                     (unsubscribe "UNSUBSCRIBE")))))
+      (when command
+       (with-current-buffer (nnimap-buffer)
+         (nnimap-command "%s %S" (cadr command) (utf7-encode group t)))))))
+
 (deffoo nnimap-request-set-mark (group actions &optional server)
   (when (nnimap-possibly-change-group group server)
     (let (sequence)
 (deffoo nnimap-request-set-mark (group actions &optional server)
   (when (nnimap-possibly-change-group group server)
     (let (sequence)
@@ -911,9 +951,10 @@ textual parts.")
                (setq sequence (nnimap-send-command
                                "UID STORE %s %sFLAGS.SILENT (%s)"
                                (nnimap-article-ranges range)
                (setq sequence (nnimap-send-command
                                "UID STORE %s %sFLAGS.SILENT (%s)"
                                (nnimap-article-ranges range)
-                               (if (eq action 'del)
-                                   "-"
-                                 "+")
+                               (cond
+                                ((eq action 'del) "-")
+                                ((eq action 'add) "+")
+                                ((eq action 'set) ""))
                                (mapconcat #'identity flags " ")))))))
        ;; Wait for the last command to complete to avoid later
        ;; syncronisation problems with the stream.
                                (mapconcat #'identity flags " ")))))))
        ;; Wait for the last command to complete to avoid later
        ;; syncronisation problems with the stream.
@@ -928,21 +969,48 @@ textual parts.")
       (nnimap-add-cr)
       (setq message (buffer-substring-no-properties (point-min) (point-max)))
       (with-current-buffer (nnimap-buffer)
       (nnimap-add-cr)
       (setq message (buffer-substring-no-properties (point-min) (point-max)))
       (with-current-buffer (nnimap-buffer)
+       ;; If we have this group open read-only, then unselect it
+       ;; before appending to it.
+       (when (equal (nnimap-examined nnimap-object) group)
+         (nnimap-unselect-group))
+       (erase-buffer)
        (setq sequence (nnimap-send-command
                        "APPEND %S {%d}" (utf7-encode group t)
                        (length message)))
        (setq sequence (nnimap-send-command
                        "APPEND %S {%d}" (utf7-encode group t)
                        (length message)))
+       (unless nnimap-streaming
+         (nnimap-wait-for-connection "^[+]"))
        (process-send-string (get-buffer-process (current-buffer)) message)
        (process-send-string (get-buffer-process (current-buffer))
                             (if (nnimap-newlinep nnimap-object)
                                 "\n"
                               "\r\n"))
        (let ((result (nnimap-get-response sequence)))
        (process-send-string (get-buffer-process (current-buffer)) message)
        (process-send-string (get-buffer-process (current-buffer))
                             (if (nnimap-newlinep nnimap-object)
                                 "\n"
                               "\r\n"))
        (let ((result (nnimap-get-response sequence)))
-         (if (not (car result))
+         (if (not (nnimap-ok-p result))
              (progn
              (progn
-               (nnheader-message 7 "%s" (nnheader-get-report-string 'nnimap))
+               (nnheader-report 'nnimap "%s" result)
                nil)
            (cons group
                nil)
            (cons group
-                 (nnimap-find-article-by-message-id group message-id))))))))
+                 (or (nnimap-find-uid-response "APPENDUID" (car result))
+                     (nnimap-find-article-by-message-id
+                      group message-id)))))))))
+
+(defun nnimap-ok-p (value)
+  (and (consp value)
+       (consp (car value))
+       (equal (caar value) "OK")))
+
+(defun nnimap-find-uid-response (name list)
+  (let ((result (car (last (nnimap-find-response-element name list)))))
+    (and result
+        (string-to-number result))))
+
+(defun nnimap-find-response-element (name list)
+  (let (result)
+    (dolist (elem list)
+      (when (and (consp elem)
+                (equal name (car elem)))
+       (setq result elem)))
+    result))
 
 (deffoo nnimap-request-replace-article (article group buffer)
   (let (group-art)
 
 (deffoo nnimap-request-replace-article (article group buffer)
   (let (group-art)
@@ -961,15 +1029,25 @@ textual parts.")
     (replace-match "\r\n" t t)))
 
 (defun nnimap-get-groups ()
     (replace-match "\r\n" t t)))
 
 (defun nnimap-get-groups ()
-  (let ((result (nnimap-command "LIST \"\" \"*\""))
+  (erase-buffer)
+  (let ((sequence (nnimap-send-command "LIST \"\" \"*\""))
        groups)
        groups)
-    (when (car result)
-      (dolist (line (cdr result))
-       (when (and (equal (car line) "LIST")
-                  (not (and (caadr line)
-                            (string-match "noselect" (caadr line)))))
-         (push (car (last line)) groups)))
-      (nreverse groups))))
+    (nnimap-wait-for-response sequence)
+    (subst-char-in-region (point-min) (point-max)
+                         ?\\ ?% t)
+    (goto-char (point-min))
+    (nnimap-unfold-quoted-lines)
+    (goto-char (point-min))
+    (while (search-forward "* LIST " nil t)
+      (let ((flags (read (current-buffer)))
+           (separator (read (current-buffer)))
+           (group (read (current-buffer))))
+       (unless (member '%NoSelect flags)
+         (push (if (stringp group)
+                   group
+                 (format "%s" group))
+               groups))))
+    (nreverse groups)))
 
 (deffoo nnimap-request-list (&optional server)
   (nnimap-possibly-change-group nil server)
 
 (deffoo nnimap-request-list (&optional server)
   (nnimap-possibly-change-group nil server)
@@ -983,6 +1061,7 @@ textual parts.")
        (with-current-buffer (nnimap-buffer)
          (setf (nnimap-group nnimap-object) nil)
          (dolist (group groups)
        (with-current-buffer (nnimap-buffer)
          (setf (nnimap-group nnimap-object) nil)
          (dolist (group groups)
+           (setf (nnimap-examined nnimap-object) group)
            (push (list (nnimap-send-command "EXAMINE %S" (utf7-encode group t))
                        group)
                  sequences))
            (push (list (nnimap-send-command "EXAMINE %S" (utf7-encode group t))
                        group)
                  sequences))
@@ -1031,7 +1110,7 @@ textual parts.")
     (with-current-buffer (nnimap-buffer)
       (erase-buffer)
       (setf (nnimap-group nnimap-object) nil)
     (with-current-buffer (nnimap-buffer)
       (erase-buffer)
       (setf (nnimap-group nnimap-object) nil)
-      (let ((qresyncp (member "QRESYNC" (nnimap-capabilities nnimap-object)))
+      (let ((qresyncp (nnimap-capability "QRESYNC"))
            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.
            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.
@@ -1041,12 +1120,14 @@ textual parts.")
                active (cdr (assq 'active params))
                uidvalidity (cdr (assq 'uidvalidity params))
                modseq (cdr (assq 'modseq params)))
                active (cdr (assq 'active params))
                uidvalidity (cdr (assq 'uidvalidity params))
                modseq (cdr (assq 'modseq params)))
+         (setf (nnimap-examined nnimap-object) group)
          (if (and qresyncp
                   uidvalidity
                   modseq)
              (push
          (if (and qresyncp
                   uidvalidity
                   modseq)
              (push
-              (list (nnimap-send-command "EXAMINE %S (QRESYNC (%s %s))"
+              (list (nnimap-send-command "EXAMINE %S (%s (%s %s))"
                                          (utf7-encode group t)
                                          (utf7-encode group t)
+                                         (nnimap-quirk "QRESYNC")
                                          uidvalidity modseq)
                     'qresync
                     nil group 'qresync)
                                          uidvalidity modseq)
                     'qresync
                     nil group 'qresync)
@@ -1072,6 +1153,15 @@ textual parts.")
                    sequences))))
        sequences))))
 
                    sequences))))
        sequences))))
 
+(defun nnimap-quirk (command)
+  (let ((quirk (assoc command nnimap-quirks)))
+    ;; If this server is of a type that matches a quirk, then return
+    ;; the "quirked" command instead of the proper one.
+    (if (or (null quirk)
+           (not (string-match (nth 1 quirk) (nnimap-greeting nnimap-object))))
+       command
+      (nth 2 quirk))))
+
 (deffoo nnimap-finish-retrieve-group-infos (server infos sequences)
   (when (and sequences
             (nnimap-possibly-change-group nil server))
 (deffoo nnimap-finish-retrieve-group-infos (server infos sequences)
   (when (and sequences
             (nnimap-possibly-change-group nil server))
@@ -1140,24 +1230,26 @@ textual parts.")
        (when uidnext
          (setq high (1- uidnext)))
        ;; First set the active ranges based on high/low.
        (when uidnext
          (setq high (1- uidnext)))
        ;; First set the active ranges based on high/low.
-       (if (or completep
-               (not (gnus-active group)))
-           (gnus-set-active group
-                            (cond
-                             ((and low high)
-                              (cons low high))
-                             (uidnext
-                              ;; No articles in this group.
-                              (cons uidnext (1- uidnext)))
-                             (active
-                              active)
-                             (start-article
-                              (cons start-article (1- start-article)))
-                             (t
-                              ;; No articles and no uidnext.
-                              nil)))
-         (gnus-set-active
-          group
+       (gnus-set-active
+        group
+        (if (or completep
+                (not (gnus-active group)))
+            (cond
+             (active
+              (cons (min (or low (car active))
+                         (car active))
+                    (max (or high (cdr active))
+                         (cdr active))))
+             ((and low high)
+              (cons low high))
+             (uidnext
+              ;; No articles in this group.
+              (cons uidnext (1- uidnext)))
+             (start-article
+              (cons start-article (1- start-article)))
+             (t
+              ;; No articles and no uidnext.
+              nil))
           (cons (car active)
                 (or high (1- uidnext)))))
        ;; See whether this is a read-only group.
           (cons (car active)
                 (or high (1- uidnext)))))
        ;; See whether this is a read-only group.
@@ -1203,7 +1295,8 @@ textual parts.")
              (setq marks (gnus-info-marks info))
              (dolist (type (cdr nnimap-mark-alist))
                (when (or (not (listp permanent-flags))
              (setq marks (gnus-info-marks info))
              (dolist (type (cdr nnimap-mark-alist))
                (when (or (not (listp permanent-flags))
-                         (memq (assoc (caddr type) flags) permanent-flags)
+                         (memq (car (assoc (caddr type) flags))
+                               permanent-flags)
                          (memq '%* permanent-flags))
                  (let ((old-marks (assoc (car type) marks))
                        (new-marks
                          (memq '%* permanent-flags))
                  (let ((old-marks (assoc (car type) marks))
                        (new-marks
@@ -1222,6 +1315,16 @@ textual parts.")
                    (when new-marks
                      (push (cons (car type) new-marks) marks)))))
              (gnus-info-set-marks info marks t))))
                    (when new-marks
                      (push (cons (car type) new-marks) marks)))))
              (gnus-info-set-marks info marks t))))
+       ;; Tell Gnus whether there are any \Recent messages in any of
+       ;; the groups.
+       (let ((recent (cdr (assoc '%Recent flags))))
+         (when (and active recent)
+           (while recent
+             (when (> (car recent) (cdr active))
+               (push (list (cons (gnus-group-real-name group) 0))
+                     nnmail-split-history)
+               (setq recent nil))
+             (pop recent))))
        ;; 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)
        ;; 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)
@@ -1345,7 +1448,7 @@ textual parts.")
                 (goto-char start)
                 (setq vanished
                       (and (eq flag-sequence 'qresync)
                 (goto-char start)
                 (setq vanished
                       (and (eq flag-sequence 'qresync)
-                           (re-search-forward "VANISHED.* \\([0-9:,]+\\)"
+                           (re-search-forward "^\\* VANISHED .* \\([0-9:,]+\\)"
                                               (or end (point-min)) t)
                            (match-string 1)))
                 (goto-char start)
                                               (or end (point-min)) t)
                            (match-string 1)))
                 (goto-char start)
@@ -1384,24 +1487,28 @@ textual parts.")
   (setq nnimap-status-string "Read-only server")
   nil)
 
   (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)))))))))
+(declare-function gnus-fetch-headers "gnus-sum"
+                 (articles &optional limit force-new dependencies))
+
+(deffoo nnimap-request-thread (header)
+  (let* ((id (mail-header-id header))
+        (refs (split-string
+               (or (mail-header-references 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))))))
+     nil t)))
 
 (defun nnimap-possibly-change-group (group server)
   (let ((open-result t))
 
 (defun nnimap-possibly-change-group (group server)
   (let ((open-result t))
@@ -1476,16 +1583,19 @@ textual parts.")
   (nnimap-wait-for-response sequence)
   (nnimap-parse-response))
 
   (nnimap-wait-for-response sequence)
   (nnimap-parse-response))
 
-(defun nnimap-wait-for-connection ()
+(defun nnimap-wait-for-connection (&optional regexp)
+  (nnimap-wait-for-line (or regexp "^[*.] .*\n") "[*.] \\([A-Z0-9]+\\)"))
+
+(defun nnimap-wait-for-line (regexp &optional response-regexp)
   (let ((process (get-buffer-process (current-buffer))))
     (goto-char (point-min))
     (while (and (memq (process-status process)
                      '(open run))
   (let ((process (get-buffer-process (current-buffer))))
     (goto-char (point-min))
     (while (and (memq (process-status process)
                      '(open run))
-               (not (re-search-forward "^[*.] .*\n" nil t)))
+               (not (re-search-forward regexp nil t)))
       (nnheader-accept-process-output process)
       (goto-char (point-min)))
     (forward-line -1)
       (nnheader-accept-process-output process)
       (goto-char (point-min)))
     (forward-line -1)
-    (and (looking-at "[*.] \\([A-Z0-9]+\\)")
+    (and (looking-at (or response-regexp regexp))
         (match-string 1))))
 
 (defun nnimap-wait-for-response (sequence &optional messagep)
         (match-string 1))))
 
 (defun nnimap-wait-for-response (sequence &optional messagep)
@@ -1496,12 +1606,14 @@ textual parts.")
          (goto-char (point-max))
          (while (and (setq openp (memq (process-status process)
                                        '(open run)))
          (goto-char (point-max))
          (while (and (setq openp (memq (process-status process)
                                        '(open run)))
-                     (not (re-search-backward
-                           (format "^%d .*\n" sequence)
-                           (if nnimap-streaming
-                               (max (point-min) (- (point) 500))
-                             (point-min))
-                           t)))
+                     (progn
+                       ;; Skip past any "*" lines that the server has
+                       ;; output.
+                       (while (and (not (bobp))
+                                   (progn
+                                     (forward-line -1)
+                                     (looking-at "\\*"))))
+                       (not (looking-at (format "%d " sequence)))))
            (when messagep
              (nnheader-message 7 "nnimap read %dk" (/ (buffer-size) 1000)))
            (nnheader-accept-process-output process)
            (when messagep
              (nnheader-message 7 "nnimap read %dk" (/ (buffer-size) 1000)))
            (nnheader-accept-process-output process)
@@ -1542,12 +1654,16 @@ textual parts.")
             (split-string
              (buffer-substring
               (1+ (point))
             (split-string
              (buffer-substring
               (1+ (point))
-              (1- (search-forward "]" (line-end-position) 'move)))))
+              (if (search-forward "]" (line-end-position) 'move)
+                  (1- (point))
+                (point)))))
            ((eql char ?\()
             (split-string
              (buffer-substring
               (1+ (point))
            ((eql char ?\()
             (split-string
              (buffer-substring
               (1+ (point))
-              (1- (search-forward ")" (line-end-position) 'move)))))
+              (if (search-forward ")" (line-end-position) 'move)
+                  (1- (point))
+                (point)))))
            ((eql char ?\")
             (forward-char 1)
             (buffer-substring
            ((eql char ?\")
             (forward-char 1)
             (buffer-substring
@@ -1668,7 +1784,7 @@ textual parts.")
       (cond
        ;; If the server supports it, we now delete the message we have
        ;; just copied over.
       (cond
        ;; If the server supports it, we now delete the message we have
        ;; just copied over.
-       ((member "UIDPLUS" (nnimap-capabilities nnimap-object))
+       ((nnimap-capability "UIDPLUS")
        (setq sequence (nnimap-send-command "UID EXPUNGE %s" range)))
        ;; If it doesn't support UID EXPUNGE, then we only expunge if the
        ;; user has configured it.
        (setq sequence (nnimap-send-command "UID EXPUNGE %s" range)))
        ;; If it doesn't support UID EXPUNGE, then we only expunge if the
        ;; user has configured it.