Don't split imap messages back into original group
[gnus] / lisp / nnimap.el
index 983d80e..c476be6 100644 (file)
@@ -1,6 +1,6 @@
 ;;; nnimap.el --- IMAP interface for Gnus
 
-;; Copyright (C) 2010-2011 Free Software Foundation, Inc.
+;; Copyright (C) 2010-2015 Free Software Foundation, Inc.
 
 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
 ;;         Simon Josefsson <simon@josefsson.org>
 
 ;;; Code:
 
-;; For Emacs <22.2 and XEmacs.
-(eval-and-compile
-  (unless (fboundp 'declare-function) (defmacro declare-function (&rest r))))
-
 (eval-and-compile
   (require 'nnheader)
   ;; In Emacs 24, `open-protocol-stream' is an autoloaded alias for
@@ -82,7 +78,8 @@ back on `network'.")
 
 (defvoo nnimap-inbox nil
   "The mail box where incoming mail arrives and should be split out of.
-For example, \"INBOX\".")
+This can be a string or a list of strings
+For example, \"INBOX\" or (\"INBOX\" \"SENT\").")
 
 (defvoo nnimap-split-methods nil
   "How mail is split.
@@ -94,12 +91,13 @@ Uses the same syntax as `nnmail-split-methods'.")
 (defvoo nnimap-unsplittable-articles '(%Deleted %Seen)
   "Articles with the flags in the list will not be considered when splitting.")
 
-(make-obsolete-variable 'nnimap-split-rule "see `nnimap-split-methods'"
+(make-obsolete-variable 'nnimap-split-rule "see `nnimap-split-methods'."
                        "Emacs 24.1")
 
 (defvoo nnimap-authenticator nil
   "How nnimap authenticate itself to the server.
-Possible choices are nil (use default methods) or `anonymous'.")
+Possible choices are nil (use default methods), `anonymous',
+`login', `plain' and `cram-md5'.")
 
 (defvoo nnimap-expunge t
   "If non-nil, expunge articles after deleting them.
@@ -117,11 +115,21 @@ some servers.")
 
 (defvoo nnimap-fetch-partial-articles nil
   "If non-nil, Gnus will fetch partial articles.
-If t, nnimap will fetch only the first part.  If a string, it
+If t, Gnus will fetch only the first part.  If a string, it
 will fetch all parts that have types that match that string.  A
 likely value would be \"text/\" to automatically fetch all
 textual parts.")
 
+(defgroup nnimap nil
+  "IMAP for Gnus."
+  :group 'gnus)
+
+(defcustom nnimap-request-articles-find-limit nil
+  "Limit the number of articles to look for after moving an article."
+  :type '(choice (const nil) integer)
+  :version "24.4"
+  :group 'nnimap)
+
 (defvar nnimap-process nil)
 
 (defvar nnimap-status-string "")
@@ -134,7 +142,7 @@ textual parts.")
 
 (defstruct nnimap
   group process commands capabilities select-result newlinep server
-  last-command-time greeting examined stream-type)
+  last-command-time greeting examined stream-type initial-resync)
 
 (defvar nnimap-object nil)
 
@@ -158,19 +166,28 @@ textual parts.")
   (nnimap-find-process-buffer nntp-server-buffer))
 
 (defun nnimap-header-parameters ()
-  (format "(UID RFC822.SIZE BODYSTRUCTURE %s)"
-         (format
+  (let (params)
+    (push "UID" params)
+    (push "RFC822.SIZE" params)
+    (when (nnimap-capability "X-GM-EXT-1")
+      (push "X-GM-LABELS" params))
+    (push "BODYSTRUCTURE" params)
+    (push (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))))
+                  nnmail-extra-headers))
+         params)
+    (format "%s" (nreverse params))))
 
 (deffoo nnimap-retrieve-headers (articles &optional group server fetch-old)
+  (when group
+    (setq group (nnimap-decode-gnus-group group)))
   (with-current-buffer nntp-server-buffer
     (erase-buffer)
-    (when (nnimap-possibly-change-group group server)
+    (when (nnimap-change-group group server)
       (with-current-buffer (nnimap-buffer)
        (erase-buffer)
        (nnimap-wait-for-response
@@ -179,6 +196,8 @@ textual parts.")
          (nnimap-article-ranges (gnus-compress-sequence articles))
          (nnimap-header-parameters))
         t)
+       (unless (process-live-p (get-buffer-process (current-buffer)))
+         (error "Server closed connection"))
        (nnimap-transform-headers)
        (nnheader-remove-cr-followed-by-lf))
       (insert-buffer-substring
@@ -187,38 +206,56 @@ textual parts.")
 
 (defun nnimap-transform-headers ()
   (goto-char (point-min))
-  (let (article bytes lines size string)
+  (let (article lines size string labels)
     (block nil
       (while (not (eobp))
-       (while (not (looking-at "\\* [0-9]+ FETCH.+UID \\([0-9]+\\)"))
+       (while (not (looking-at "\\* [0-9]+ FETCH"))
          (delete-region (point) (progn (forward-line 1) (point)))
          (when (eobp)
            (return)))
-       (setq article (match-string 1))
+       (goto-char (match-end 0))
        ;; Unfold quoted {number} strings.
-       (while (re-search-forward "[^]][ (]{\\([0-9]+\\)}\r?\n"
-                                 (1+ (line-end-position)) t)
+       (while (re-search-forward
+               "[^]][ (]{\\([0-9]+\\)}\r?\n"
+               (save-excursion
+                 ;; Start of the header section.
+                 (or (re-search-forward "] {[0-9]+}\r?\n" nil t)
+                     ;; Start of the next FETCH.
+                     (re-search-forward "\\* [0-9]+ FETCH" nil t)
+                     (point-max)))
+               t)
          (setq size (string-to-number (match-string 1)))
          (delete-region (+ (match-beginning 0) 2) (point))
          (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" (mm-subst-char-in-string ?\n ?\s string))))
        (beginning-of-line)
+       (setq article
+             (and (re-search-forward "UID \\([0-9]+\\)" (line-end-position)
+                                     t)
+                  (match-string 1)))
+       (setq lines nil)
        (setq size
              (and (re-search-forward "RFC822.SIZE \\([0-9]+\\)"
                                      (line-end-position)
                                      t)
                   (match-string 1)))
        (beginning-of-line)
+       (when (search-forward "X-GM-LABELS" (line-end-position) t)
+         (setq labels (ignore-errors (read (current-buffer)))))
+       (beginning-of-line)
        (when (search-forward "BODYSTRUCTURE" (line-end-position) t)
          (let ((structure (ignore-errors
                             (read (current-buffer)))))
            (while (and (consp structure)
-                       (not (stringp (car structure))))
+                       (not (atom (car structure))))
              (setq structure (car structure)))
-           (setq lines (nth 7 structure))))
+           (setq lines (if (and
+                            (stringp (car structure))
+                            (equal (upcase (nth 0 structure)) "MESSAGE")
+                            (equal (upcase (nth 1 structure)) "RFC822"))
+                           (nth 9 structure)
+                         (nth 7 structure)))))
        (delete-region (line-beginning-position) (line-end-position))
        (insert (format "211 %s Article retrieved." article))
        (forward-line 1)
@@ -226,7 +263,11 @@ textual parts.")
          (insert (format "Chars: %s\n" size)))
        (when lines
          (insert (format "Lines: %s\n" lines)))
-       (unless (re-search-forward "^\r$" nil t)
+       (when labels
+         (insert (format "X-GM-LABELS: %s\n" labels)))
+       ;; Most servers have a blank line after the headers, but
+       ;; Davmail doesn't.
+       (unless (re-search-forward "^\r$\\|^)\r?$" nil t)
          (goto-char (point-max)))
        (delete-region (line-beginning-position) (line-end-position))
        (insert ".")
@@ -262,18 +303,20 @@ textual parts.")
         result))
       (mapconcat #'identity (nreverse result) ",")))))
 
-(deffoo nnimap-open-server (server &optional defs)
+(deffoo nnimap-open-server (server &optional defs no-reconnect)
   (if (nnimap-server-opened server)
       t
     (unless (assq 'nnimap-address defs)
       (setq defs (append defs (list (list 'nnimap-address server)))))
     (nnoo-change-server 'nnimap server defs)
-    (or (nnimap-find-connection nntp-server-buffer)
-       (nnimap-open-connection nntp-server-buffer))))
+    (if no-reconnect
+       (nnimap-find-connection nntp-server-buffer)
+      (or (nnimap-find-connection nntp-server-buffer)
+         (nnimap-open-connection nntp-server-buffer)))))
 
 (defun nnimap-make-process-buffer (buffer)
   (with-current-buffer
-      (generate-new-buffer (format "*nnimap %s %s %s*"
+      (generate-new-buffer (format " *nnimap %s %s %s*"
                                   nnimap-address nnimap-server-port
                                   (gnus-buffer-exists-p buffer)))
     (mm-disable-multibyte)
@@ -281,7 +324,8 @@ textual parts.")
     (gnus-add-buffer)
     (set (make-local-variable 'after-change-functions) nil)
     (set (make-local-variable 'nnimap-object)
-        (make-nnimap :server (nnoo-current-server 'nnimap)))
+        (make-nnimap :server (nnoo-current-server 'nnimap)
+                     :initial-resync 0))
     (push (list buffer (current-buffer)) nnimap-connection-alist)
     (push (current-buffer) nnimap-process-buffers)
     (current-buffer)))
@@ -318,7 +362,8 @@ textual parts.")
                          (nnimap-last-command-time nnimap-object)))
                        ;; More than five minutes since the last command.
                        (* 5 60)))
-           (nnimap-send-command "NOOP")))))))
+            (ignore-errors              ;E.g. "buffer foo has no process".
+              (nnimap-send-command "NOOP"))))))))
 
 (defun nnimap-open-connection (buffer)
   ;; Be backwards-compatible -- the earlier value of nnimap-stream was
@@ -338,14 +383,18 @@ textual parts.")
        nil
       stream)))
 
+(defun nnimap-map-port (port)
+  (if (equal port "imaps")
+      "993"
+    port))
+
 (defun nnimap-open-connection-1 (buffer)
   (unless nnimap-keepalive-timer
     (setq nnimap-keepalive-timer (run-at-time (* 60 15) (* 60 15)
-                                             'nnimap-keepalive)))
+                                             #'nnimap-keepalive)))
   (with-current-buffer (nnimap-make-process-buffer buffer)
     (let* ((coding-system-for-read 'binary)
           (coding-system-for-write 'binary)
-          (port nil)
           (ports
            (cond
             ((memq nnimap-stream '(network plain starttls))
@@ -367,8 +416,10 @@ textual parts.")
        (push nnimap-server-port ports))
       (let* ((stream-list
              (open-protocol-stream
-              "*nnimap*" (current-buffer) nnimap-address (car ports)
+              "*nnimap*" (current-buffer) nnimap-address
+              (nnimap-map-port (car ports))
               :type nnimap-stream
+              :warn-unless-encrypted t
               :return-list t
               :shell-command nnimap-shell-program
               :capability-command "1 CAPABILITY\r\n"
@@ -385,12 +436,20 @@ textual parts.")
             (stream-type (plist-get props :type)))
        (when (and stream (not (memq (process-status stream) '(open run))))
          (setq stream nil))
+
+        (when (and (fboundp 'set-network-process-option) ;; Not in XEmacs.
+                   (fboundp 'process-type) ;; Emacs 22 doesn't provide it.
+                   (eq (process-type stream) 'network))
+          ;; Use TCP-keepalive so that connections that pass through a NAT
+          ;; router don't hang when left idle.
+          (set-network-process-option stream :keepalive t))
+
        (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)
+                              nnimap-address (car ports) nnimap-stream)
              'no-connect)
          (gnus-set-process-query-on-exit-flag stream nil)
          (if (not (gnus-string-match-p "[*.] \\(OK\\|PREAUTH\\)" greeting))
@@ -410,8 +469,8 @@ textual parts.")
                                (nnimap-credentials
                                (gnus-delete-duplicates
                                 (list
-                                 nnimap-address
-                                 (nnoo-current-server 'nnimap)))
+                                  (nnoo-current-server 'nnimap)
+                                 nnimap-address))
                                 ports
                                 nnimap-user))))
                  (setq nnimap-object nil)
@@ -442,6 +501,8 @@ textual parts.")
            (when nnimap-object
              (when (nnimap-capability "QRESYNC")
                (nnimap-command "ENABLE QRESYNC"))
+              (nnheader-message 7 "Opening connection to %s...done"
+                               nnimap-address)
              (nnimap-process nnimap-object))))))))
 
 (autoload 'rfc2104-hash "rfc2104")
@@ -452,9 +513,13 @@ textual parts.")
    ;; 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))
+        (eq (nnimap-stream-type nnimap-object) 'tls)
+        (or (null nnimap-authenticator)
+            (eq nnimap-authenticator 'login)))
     (nnimap-command "LOGIN %S %S" user password))
-   ((nnimap-capability "AUTH=CRAM-MD5")
+   ((and (nnimap-capability "AUTH=CRAM-MD5")
+        (or (null nnimap-authenticator)
+            (eq nnimap-authenticator 'cram-md5)))
     (erase-buffer)
     (let ((sequence (nnimap-send-command "AUTHENTICATE CRAM-MD5"))
          (challenge (nnimap-wait-for-line "^\\+\\(.*\\)\n")))
@@ -467,9 +532,13 @@ textual parts.")
                               (base64-decode-string challenge))))
        "\r\n"))
       (nnimap-wait-for-response sequence)))
-   ((not (nnimap-capability "LOGINDISABLED"))
+   ((and (not (nnimap-capability "LOGINDISABLED"))
+        (or (null nnimap-authenticator)
+            (eq nnimap-authenticator 'login)))
     (nnimap-command "LOGIN %S %S" user password))
-   ((nnimap-capability "AUTH=PLAIN")
+   ((and (nnimap-capability "AUTH=PLAIN")
+        (or (null nnimap-authenticator)
+            (eq nnimap-authenticator 'plain)))
     (nnimap-command
      "AUTHENTICATE PLAIN %s"
      (base64-encode-string
@@ -519,11 +588,13 @@ textual parts.")
   nnimap-status-string)
 
 (deffoo nnimap-request-article (article &optional group server to-buffer)
+  (when group
+    (setq group (nnimap-decode-gnus-group group)))
   (with-current-buffer nntp-server-buffer
-    (let ((result (nnimap-possibly-change-group group server))
+    (let ((result (nnimap-change-group group server))
          parts structure)
       (when (stringp article)
-       (setq article (nnimap-find-article-by-message-id group article)))
+       (setq article (nnimap-find-article-by-message-id group server article