(nnimap-update-info): Clean up slightly.
[gnus] / lisp / nnimap.el
index 38f8004..6de49eb 100644 (file)
@@ -1,6 +1,6 @@
 ;;; 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>
@@ -47,9 +47,6 @@
 (require 'nnmail)
 (require 'proto-stream)
 
-(eval-when-compile
-  (require 'gnus-sum))
-
 (autoload 'auth-source-forget-user-or-password "auth-source")
 (autoload 'auth-source-user-or-password "auth-source")
 
@@ -127,7 +124,7 @@ textual parts.")
 
 (defstruct nnimap
   group process commands capabilities select-result newlinep server
-  last-command-time greeting examined)
+  last-command-time greeting examined stream-type)
 
 (defvar nnimap-object nil)
 
@@ -142,6 +139,9 @@ textual parts.")
     (download "gnus-download")
     (forward "gnus-forward")))
 
+(defvar nnimap-quirks
+  '(("QRESYNC" "Zimbra" "QRESYNC ")))
+
 (defun nnimap-buffer ()
   (nnimap-find-process-buffer nntp-server-buffer))
 
@@ -167,7 +167,8 @@ textual parts.")
          (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))
@@ -346,36 +347,36 @@ textual parts.")
             (t
              (error "Unknown stream type: %s" nnimap-stream))))
           (proto-stream-always-use-starttls t)
-          connection-result login-result credentials)
+           login-result credentials)
       (when nnimap-server-port
-       (setq ports (append ports (list (format "%s" nnimap-server-port)))))
-      (destructuring-bind (stream greeting capabilities)
+       (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)
-            (if (not (string-match "STARTTLS" capabilities))
-                ;; Not a STARTTLS-capable server.
-                nil
+            (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 (string-match "[*.] OK" greeting))
+         (if (not (gnus-string-match-p "[*.] \\(OK\\|PREAUTH\\)" greeting))
              (nnheader-report 'nnimap "%s" greeting)
            ;; Store the greeting (for debugging purposes).
            (setf (nnimap-greeting nnimap-object) greeting)
            (setf (nnimap-capabilities nnimap-object)
                  (mapcar #'upcase
                          (split-string capabilities)))
-           (unless (equal connection-result "PREAUTH")
+           (unless (gnus-string-match-p "[*.] PREAUTH" greeting)
              (if (not (setq credentials
                             (if (eq nnimap-authenticator 'anonymous)
                                 (list "anonymous"
@@ -390,17 +391,7 @@ textual parts.")
                                (nnimap-credentials nnimap-address ports)))))
                  (setq nnimap-object nil)
                (setq login-result
-                     (if (and (nnimap-capability "AUTH=PLAIN")
-                              (nnimap-capability "LOGINDISABLED"))
-                         (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.
@@ -417,6 +408,39 @@ textual parts.")
                (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)
@@ -495,15 +519,17 @@ textual parts.")
     (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
@@ -565,7 +591,7 @@ textual parts.")
     ;; 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)))
@@ -639,7 +665,7 @@ textual parts.")
   (let ((result (nnimap-possibly-change-group
                 ;; Don't SELECT the group if we're going to select it
                 ;; later, anyway.
-                (if (and dont-check
+                (if (and (not dont-check)
                          (assoc group nnimap-current-infos))
                     nil
                   group)
@@ -794,22 +820,42 @@ textual parts.")
 
 (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))
@@ -881,6 +927,16 @@ textual parts.")
        (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)
@@ -929,15 +985,20 @@ textual parts.")
                                 "\n"
                               "\r\n"))
        (let ((result (nnimap-get-response sequence)))
-         (if (not (car result))
+         (if (not (nnimap-ok-p result))
              (progn
-               (nnheader-message 7 "%s" (nnheader-get-report-string 'nnimap))
+               (nnheader-report 'nnimap "%s" result)
                nil)
            (cons group
                  (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
@@ -1064,8 +1125,9 @@ textual parts.")
                   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)
+                                         (nnimap-quirk "QRESYNC")
                                          uidvalidity modseq)
                     'qresync
                     nil group 'qresync)
@@ -1091,6 +1153,15 @@ textual parts.")
                    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))
@@ -1159,27 +1230,26 @@ textual parts.")
        (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
-                             (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)))
-         (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.
@@ -1245,6 +1315,16 @@ textual parts.")
                    (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)
@@ -1368,7 +1448,7 @@ textual parts.")
                 (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)
@@ -1407,9 +1487,13 @@ 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))
+(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
@@ -1500,8 +1584,9 @@ textual parts.")
   (nnimap-parse-response))
 
 (defun nnimap-wait-for-connection (&optional regexp)
-  (unless regexp
-    (setq regexp "^[*.] .*\n"))
+  (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)
@@ -1510,7 +1595,7 @@ textual parts.")
       (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)
@@ -1521,12 +1606,14 @@ textual parts.")
          (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)