Implement partial IMAP article fetch.
[gnus] / lisp / nnimap.el
index a8902b2..1b9b0e7 100644 (file)
@@ -51,7 +51,7 @@ it will default to `imap'.")
 
 (defvoo nnimap-stream 'ssl
   "How nnimap will talk to the IMAP server.
-Values are `ssl' and `network'.")
+Values are `ssl', `network', `starttls' or `shell'.")
 
 (defvoo nnimap-shell-program (if (boundp 'imap-shell-program)
                                 (if (listp imap-shell-program)
@@ -90,8 +90,12 @@ not done by default on servers that doesn't support that command.")
 (defvar nnimap-split-download-body-default nil
   "Internal variable with default value for `nnimap-split-download-body'.")
 
+(defvar nnimap-keepalive-timer nil)
+(defvar nnimap-process-buffers nil)
+
 (defstruct nnimap
-  group process commands capabilities select-result newlinep server)
+  group process commands capabilities select-result newlinep server
+  last-command-time)
 
 (defvar nnimap-object nil)
 
@@ -124,8 +128,7 @@ not done by default on servers that doesn't support that command.")
          (nnimap-article-ranges (gnus-compress-sequence articles))
          (format "(UID RFC822.SIZE BODYSTRUCTURE %s)"
                  (format
-                  (if (member "IMAP4REV1"
-                              (nnimap-capabilities nnimap-object))
+                  (if (nnimap-ver4-p)
                       "BODY.PEEK[HEADER.FIELDS %s]"
                     "RFC822.HEADER.LINES %s")
                   (append '(Subject From Date Message-Id
@@ -135,19 +138,26 @@ not done by default on servers that doesn't support that command.")
        (nnimap-transform-headers))
       (insert-buffer-substring
        (nnimap-find-process-buffer (current-buffer))))
-    t))
+    'headers))
 
 (defun nnimap-transform-headers ()
   (goto-char (point-min))
-  (let (article bytes lines size)
+  (let (article bytes lines size string)
     (block nil
       (while (not (eobp))
        (while (not (looking-at "^\\* [0-9]+ FETCH.*UID \\([0-9]+\\)"))
          (delete-region (point) (progn (forward-line 1) (point)))
          (when (eobp)
            (return)))
-       (setq article (match-string 1)
-             bytes (nnimap-get-length)
+       (setq article (match-string 1))
+       ;; Unfold quoted {number} strings.
+       (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))
+         (setq string (delete-region (point) (+ (point) size)))
+         (insert (format "%S" string)))
+       (setq bytes (nnimap-get-length)
              lines nil)
        (beginning-of-line)
        (setq size
@@ -157,7 +167,8 @@ not done by default on servers that doesn't support that command.")
                   (match-string 1)))
        (beginning-of-line)
        (when (search-forward "BODYSTRUCTURE" (line-end-position) t)
-         (let ((structure (ignore-errors (read (current-buffer)))))
+         (let ((structure (ignore-errors
+                            (read (current-buffer)))))
            (while (and (consp structure)
                        (not (stringp (car structure))))
              (setq structure (car structure)))
@@ -215,6 +226,7 @@ not done by default on servers that doesn't support that command.")
     (set (make-local-variable 'nnimap-object)
         (make-nnimap :server (nnoo-current-server 'nnimap)))
     (push (list buffer (current-buffer)) nnimap-connection-alist)
+    (push (current-buffer) nnimap-process-buffers)
     (current-buffer)))
 
 (defun nnimap-open-shell-stream (name buffer host port)
@@ -238,7 +250,25 @@ not done by default on servers that doesn't support that command.")
             '("login" "password") address port nil (null ports))))
     credentials))
 
+(defun nnimap-keepalive ()
+  (let ((now (current-time)))
+    (dolist (buffer nnimap-process-buffers)
+      (when (buffer-name buffer)
+       (with-current-buffer buffer
+         (when (and nnimap-object
+                    (nnimap-last-command-time nnimap-object)
+                    (> (time-to-seconds
+                        (time-subtract
+                         now
+                         (nnimap-last-command-time nnimap-object)))
+                       ;; More than five minutes since the last command.
+                       (* 5 60)))
+           (nnimap-send-command "NOOP")))))))
+
 (defun nnimap-open-connection (buffer)
+  (unless nnimap-keepalive-timer
+    (setq nnimap-keepalive-timer (run-at-time (* 60 15) (* 60 15)
+                                             'nnimap-keepalive)))
   (with-current-buffer (nnimap-make-process-buffer buffer)
     (let* ((coding-system-for-read 'binary)
           (coding-system-for-write 'binary)
@@ -257,6 +287,11 @@ not done by default on servers that doesn't support that command.")
               "*nnimap*" (current-buffer) nnimap-address
               (or nnimap-server-port "imap"))
              '("imap"))
+            ((eq nnimap-stream 'starttls)
+             (starttls-open-stream
+              "*nnimap*" (current-buffer) nnimap-address
+              (or nnimap-server-port "imap"))
+             '("imap"))
             ((eq nnimap-stream 'ssl)
              (open-tls-stream
               "*nnimap*" (current-buffer) nnimap-address
@@ -273,6 +308,9 @@ not done by default on servers that doesn't support that command.")
                       '(open run)))
        (gnus-set-process-query-on-exit-flag (nnimap-process nnimap-object) nil)
        (when (setq connection-result (nnimap-wait-for-connection))
+         (when (eq nnimap-stream 'starttls)
+           (nnimap-command "STARTTLS")
+           (starttls-negotiate (nnimap-process nnimap-object)))
          (unless (equal connection-result "PREAUTH")
            (if (not (setq credentials
                           (if (eq nnimap-authenticator 'anonymous)
@@ -331,7 +369,7 @@ not done by default on servers that doesn't support that command.")
 (deffoo nnimap-request-article (article &optional group server to-buffer)
   (with-current-buffer nntp-server-buffer
     (let ((result (nnimap-possibly-change-group group server))
-         parts)
+         parts structure)
       (when (stringp article)
        (setq article (nnimap-find-article-by-message-id group article)))
       (when (and result
@@ -345,30 +383,107 @@ not done by default on servers that doesn't support that command.")
              (nnimap-command "UID FETCH %d (BODYSTRUCTURE)" article)
              (goto-char (point-min))
              (when (re-search-forward "FETCH.*BODYSTRUCTURE" nil t)
-               (let ((structure (ignore-errors (read (current-buffer)))))
-                 (setq parts (nnimap-find-wanted-parts structure))))))
-         (setq result
-               (nnimap-command
-                (if (member "IMAP4REV1" (nnimap-capabilities nnimap-object))
-                    "UID FETCH %d BODY.PEEK[]"
-                  "UID FETCH %d RFC822.PEEK")
-                article))
-         ;; Check that we really got an article.
-         (goto-char (point-min))
-         (unless (looking-at "\\* [0-9]+ FETCH")
-           (setq result nil)))
-       (let ((buffer (nnimap-find-process-buffer (current-buffer))))
-         (when (car result)
-           (with-current-buffer (or to-buffer nntp-server-buffer)
-             (insert-buffer-substring buffer)
-             (goto-char (point-min))
-             (let ((bytes (nnimap-get-length)))
-               (delete-region (line-beginning-position)
-                              (progn (forward-line 1) (point)))
-               (goto-char (+ (point) bytes))
-               (delete-region (point) (point-max))
-               (nnheader-ms-strip-cr))
-             (cons group article))))))))
+               (setq structure (ignore-errors (read (current-buffer)))
+                     parts (nnimap-find-wanted-parts structure)))))
+         (when (if parts
+                   (nnimap-get-partial-article article parts structure)
+                 (nnimap-get-whole-article article))
+           (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)
+  (let ((result
+        (nnimap-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))
+    (unless (looking-at "\\* [0-9]+ FETCH")
+      (setq result nil))
+    (when result
+      (goto-char (point-min))
+      (let ((bytes (nnimap-get-length)))
+       (delete-region (line-beginning-position)
+                      (progn (forward-line 1) (point)))
+       (goto-char (+ (point) bytes))
+       (delete-region (point) (point-max)))
+      t)))
+
+(defun nnimap-ver4-p ()
+  (member "IMAP4REV1" (nnimap-capabilities nnimap-object)))
+
+(defun nnimap-get-partial-article (article parts structure)
+  (let ((result
+        (nnimap-command
+         "UID FETCH %d (%s %s)"
+         article
+         (if (nnimap-ver4-p)
+             "BODY.PEEK[HEADER]"
+           "RFC822.HEADER")
+         (if (nnimap-ver4-p)
+             (mapconcat (lambda (part)
+                          (format "BODY.PEEK[%s]" part))
+                        parts " ")
+           (mapconcat (lambda (part)
+                        (format "RFC822.PEEK[%s]" part))
+                      parts " ")))))
+    (when result
+      (nnimap-convert-partial-article structure))))
+
+(defun nnimap-convert-partial-article (structure)
+  ;; First just skip past the headers.
+  (goto-char (point-min))
+  (let ((bytes (nnimap-get-length))
+       id parts)
+    ;; Delete "FETCH" line.
+    (delete-region (line-beginning-position)
+                  (progn (forward-line 1) (point)))
+    (goto-char (+ (point) bytes))
+    ;; Collect all the body parts.
+    (while (looking-at ".*BODY\\[\\([.0-9]+\\)\\]")
+      (setq id (match-string 1)
+           bytes (nnimap-get-length))
+      (beginning-of-line)
+      (delete-region (point) (progn (forward-line 1) (point)))
+      (push (list id (buffer-substring (point) (+ (point) bytes)))
+           parts)
+      (delete-region (point) (+ (point) bytes)))
+    ;; Delete trailing junk.
+    (delete-region (point) (point-max))
+    ;; Now insert all the parts again where they fit in the structure.
+    (nnimap-insert-partial-structure structure parts)
+    t))
+
+(defun nnimap-insert-partial-structure (structure parts &optional subp)
+  (let ((type (car (last structure 4)))
+       (boundary (cadr (member "BOUNDARY" (car (last structure 3))))))
+    (when subp
+      (insert (format "Content-type: multipart/%s; boundary=%S\n\n"
+                     (downcase type) boundary)))
+    (while (not (stringp (car structure)))
+      (insert "\n--" boundary "\n")
+      (if (consp (caar structure))
+         (nnimap-insert-partial-structure (pop structure) parts t)
+       (let ((bit (pop structure)))
+         (insert (format  "Content-type: %s/%s"
+                          (downcase (nth 0 bit))
+                          (downcase (nth 1 bit))))
+         (if (member "CHARSET" (nth 2 bit))
+             (insert (format
+                      "; charset=%S\n" (cadr (member "CHARSET" (nth 2 bit)))))
+           (insert "\n"))
+         (insert (format "Content-transfer-encoding: %s\n"
+                         (nth 5 bit)))
+         (insert "\n")
+         (when (assoc (nth 9 bit) parts)
+           (insert (cadr (assoc (nth 9 bit) parts)))))))
+    (insert "\n--" boundary "--\n")))
 
 (defun nnimap-find-wanted-parts (structure)
   (message-flatten-list (nnimap-find-wanted-parts-1 structure "")))
@@ -384,13 +499,14 @@ not done by default on servers that doesn't support that command.")
                           (number-to-string num)
                         (format "%s.%s" prefix num)))
                  parts)
-         (let ((type (format "%s/%s" (nth 0 sub) (nth 1 sub))))
-           (when (string-match nnimap-fetch-partial-articles type)
-             (push (if (string= prefix "")
+         (let ((type (format "%s/%s" (nth 0 sub) (nth 1 sub)))
+               (id (if (string= prefix "")
                        (number-to-string num)
-                     (format "%s.%s" prefix num))
-                   parts)))
-         (incf num))))
+                     (format "%s.%s" prefix num))))
+           (setcar (nthcdr 9 sub) id)
+           (when (string-match nnimap-fetch-partial-articles type)
+             (push id parts))))
+       (incf num)))
     (nreverse parts)))
 
 (deffoo nnimap-request-group (group &optional server dont-check info)
@@ -419,13 +535,11 @@ not done by default on servers that doesn't support that command.")
              (when info
                (nnimap-update-infos marks (list info)))
              (goto-char (point-max))
-             (cond
-              (marks
-               (setq high (nth 3 (car marks))
-                     low (nth 4 (car marks))))
-              ((re-search-backward "UIDNEXT \\([0-9]+\\)" nil t)
-               (setq high (1- (string-to-number (match-string 1)))
-                     low 1)))))
+             (let ((uidnext (nth 5 (car marks))))
+               (setq high (if uidnext
+                              (1- uidnext)
+                            (nth 3 (car marks)))
+                     low (or (nth 4 (car marks)) uidnext)))))
          (erase-buffer)
          (insert
           (format
@@ -502,7 +616,8 @@ not done by default on servers that doesn't support that command.")
     nil)
    (t
     (let ((deletable-articles
-          (if force
+          (if (or force
+                  (eq nnmail-expiry-wait 'immediate))
               articles
             (gnus-sorted-intersection
              articles
@@ -780,15 +895,27 @@ not done by default on servers that doesn't support that command.")
       (let ((group (gnus-info-group info))
            (completep (and start-article
                            (= start-article 1))))
+       (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
-                            (if high
-                                (cons low high)
+                            (cond
+                             ((and low high)
+                              (cons low high))
+                             (uidnext
                               ;; No articles in this group.
-                              (cons (1- uidnext) uidnext)))
-         (setcdr (gnus-active group) high))
+                              (cons uidnext (1- uidnext)))
+                             (start-article
+                              (cons start-article (1- start-article)))
+                             (t
+                              ;; No articles and no uidnext.
+                              nil)))
+         (setcdr (gnus-active group) (or high (1- uidnext))))
+       (when (and (not high)
+                  uidnext)
+         (setq high (1- uidnext)))
        ;; Then update the list of read articles.
        (let* ((unread
                (gnus-compress-sequence
@@ -821,6 +948,7 @@ not done by default on servers that doesn't support that command.")
                  (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)
@@ -859,10 +987,10 @@ not done by default on servers that doesn't support that command.")
            (setq mark (assoc flag marks))
            (if (not mark)
                (push (list flag (car article)) marks)
-             (setcdr mark (cons (car article) (cdr mark)))))
-         (push (list group existing marks high low uidnext start-article
-                     permanent-flags)
-               data))))
+             (setcdr mark (cons (car article) (cdr mark))))))
+       (push (list group existing marks high low uidnext start-article
+                   permanent-flags)
+             data)))
     data))
 
 (defun nnimap-parse-flags (sequences)
@@ -884,11 +1012,13 @@ not done by default on servers that doesn't support that command.")
                           (and (search-forward "PERMANENTFLAGS "
                                                 (or end (point-min)) t)
                                (read (current-buffer))))
+                    (goto-char start)
                     (setq uidnext
                           (and (search-forward "UIDNEXT "
                                                 (or end (point-min)) t)
                                (read (current-buffer))))
-                    (goto-char end))
+                    (goto-char end)
+                    (forward-line -1))
                   ;; The UID FETCH FLAGS was successful.
                   (search-forward (format "\n%d OK " flag-sequence) nil t))
          (setq start (point))
@@ -964,6 +1094,7 @@ not done by default on servers that doesn't support that command.")
 
 (defun nnimap-command (&rest args)
   (erase-buffer)
+  (setf (nnimap-last-command-time nnimap-object) (current-time))
   (let* ((sequence (apply #'nnimap-send-command args))
         (response (nnimap-get-response sequence)))
     (if (equal (caar response) "OK")
@@ -1075,8 +1206,7 @@ not done by default on servers that doesn't support that command.")
     (nnimap-article-ranges articles)
     (format "(UID %s%s)"
            (format
-            (if (member "IMAP4REV1"
-                        (nnimap-capabilities nnimap-object))
+            (if (nnimap-ver4-p)
                 "BODY.PEEK[HEADER] BODY.PEEK"
               "RFC822.PEEK"))
            (if nnimap-split-download-body-default
@@ -1132,8 +1262,8 @@ not done by default on servers that doesn't support that command.")
              ;; And then mark the successful copy actions as deleted,
              ;; and possibly expunge them.
              (nnimap-mark-and-expunge-incoming
-              (nnimap-parse-copied-articles sequences))
-             (nnimap-mark-and-expunge-incoming junk-articles))))))))
+              (nnimap-parse-copied-articles sequences)))
+            (nnimap-mark-and-expunge-incoming junk-articles)))))))
 
 (defun nnimap-mark-and-expunge-incoming (range)
   (when range