Implement IMAP keepalive.
[gnus] / lisp / nnimap.el
1 ;;; nnimap.el --- IMAP interface for Gnus
2
3 ;; Copyright (C) 2010 Free Software Foundation, Inc.
4
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6 ;;         Simon Josefsson <simon@josefsson.org>
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; nnimap interfaces Gnus with IMAP servers.
26
27 ;;; Code:
28
29 (eval-and-compile
30   (require 'nnheader))
31
32 (eval-when-compile
33   (require 'cl))
34
35 (require 'nnheader)
36 (require 'gnus-util)
37 (require 'gnus)
38 (require 'nnoo)
39 (require 'netrc)
40 (require 'parse-time)
41
42 (nnoo-declare nnimap)
43
44 (defvoo nnimap-address nil
45   "The address of the IMAP server.")
46
47 (defvoo nnimap-server-port nil
48   "The IMAP port used.
49 If nnimap-stream is `ssl', this will default to `imaps'.  If not,
50 it will default to `imap'.")
51
52 (defvoo nnimap-stream 'ssl
53   "How nnimap will talk to the IMAP server.
54 Values are `ssl', `network', `starttls' or `shell'.")
55
56 (defvoo nnimap-shell-program (if (boundp 'imap-shell-program)
57                                  (if (listp imap-shell-program)
58                                      (car imap-shell-program)
59                                    imap-shell-program)
60                                "ssh %s imapd"))
61
62 (defvoo nnimap-inbox nil
63   "The mail box where incoming mail arrives and should be split out of.")
64
65 (defvoo nnimap-authenticator nil
66   "How nnimap authenticate itself to the server.
67 Possible choices are nil (use default methods) or `anonymous'.")
68
69 (defvoo nnimap-fetch-partial-articles nil
70   "If non-nil, nnimap will fetch partial articles.
71 If t, nnimap will fetch only the first part.  If a string, it
72 will fetch all parts that have types that match that string.  A
73 likely value would be \"text/\" to automatically fetch all
74 textual parts.")
75
76 (defvoo nnimap-expunge t
77   "If non-nil, expunge articles after deleting them.
78 This is always done if the server supports UID EXPUNGE, but it's
79 not done by default on servers that doesn't support that command.")
80
81
82 (defvoo nnimap-connection-alist nil)
83
84 (defvoo nnimap-current-infos nil)
85
86 (defvar nnimap-process nil)
87
88 (defvar nnimap-status-string "")
89
90 (defvar nnimap-split-download-body-default nil
91   "Internal variable with default value for `nnimap-split-download-body'.")
92
93 (defvar nnimap-keepalive-timer nil)
94 (defvar nnimap-process-buffers nil)
95
96 (defstruct nnimap
97   group process commands capabilities select-result newlinep server
98   last-command-time)
99
100 (defvar nnimap-object nil)
101
102 (defvar nnimap-mark-alist
103   '((read "\\Seen" %Seen)
104     (tick "\\Flagged" %Flagged)
105     (reply "\\Answered" %Answered)
106     (expire "gnus-expire")
107     (dormant "gnus-dormant")
108     (score "gnus-score")
109     (save "gnus-save")
110     (download "gnus-download")
111     (forward "gnus-forward")))
112
113 (defvar nnimap-split-methods nil)
114
115 (defun nnimap-buffer ()
116   (nnimap-find-process-buffer nntp-server-buffer))
117
118 (deffoo nnimap-retrieve-headers (articles &optional group server fetch-old)
119   (with-current-buffer nntp-server-buffer
120     (erase-buffer)
121     (when (nnimap-possibly-change-group group server)
122       (with-current-buffer (nnimap-buffer)
123         (nnimap-send-command "SELECT %S" (utf7-encode group t))
124         (erase-buffer)
125         (nnimap-wait-for-response
126          (nnimap-send-command
127           "UID FETCH %s %s"
128           (nnimap-article-ranges (gnus-compress-sequence articles))
129           (format "(UID RFC822.SIZE BODYSTRUCTURE %s)"
130                   (format
131                    (if (member "IMAP4REV1"
132                                (nnimap-capabilities nnimap-object))
133                        "BODY.PEEK[HEADER.FIELDS %s]"
134                      "RFC822.HEADER.LINES %s")
135                    (append '(Subject From Date Message-Id
136                                      References In-Reply-To Xref)
137                            nnmail-extra-headers))))
138          t)
139         (nnimap-transform-headers))
140       (insert-buffer-substring
141        (nnimap-find-process-buffer (current-buffer))))
142     'headers))
143
144 (defun nnimap-transform-headers ()
145   (goto-char (point-min))
146   (let (article bytes lines size string)
147     (block nil
148       (while (not (eobp))
149         (while (not (looking-at "^\\* [0-9]+ FETCH.*UID \\([0-9]+\\)"))
150           (delete-region (point) (progn (forward-line 1) (point)))
151           (when (eobp)
152             (return)))
153         (setq article (match-string 1))
154         ;; Unfold quoted {number} strings.
155         (while (re-search-forward "[^]] {\\([0-9]+\\)}\r\n"
156                                   (1+ (line-end-position)) t)
157           (setq size (string-to-number (match-string 1)))
158           (delete-region (+ (match-beginning 0) 2) (point))
159           (setq string (delete-region (point) (+ (point) size)))
160           (insert (format "%S" string)))
161         (setq bytes (nnimap-get-length)
162               lines nil)
163         (beginning-of-line)
164         (setq size
165               (and (re-search-forward "RFC822.SIZE \\([0-9]+\\)"
166                                       (line-end-position)
167                                       t)
168                    (match-string 1)))
169         (beginning-of-line)
170         (when (search-forward "BODYSTRUCTURE" (line-end-position) t)
171           (let ((structure (ignore-errors
172                              (read (current-buffer)))))
173             (while (and (consp structure)
174                         (not (stringp (car structure))))
175               (setq structure (car structure)))
176             (setq lines (nth 7 structure))))
177         (delete-region (line-beginning-position) (line-end-position))
178         (insert (format "211 %s Article retrieved." article))
179         (forward-line 1)
180         (when size
181           (insert (format "Chars: %s\n" size)))
182         (when lines
183           (insert (format "Lines: %s\n" lines)))
184         (re-search-forward "^\r$")
185         (delete-region (line-beginning-position) (line-end-position))
186         (insert ".")
187         (forward-line 1)))))
188
189 (defun nnimap-get-length ()
190   (and (re-search-forward "{\\([0-9]+\\)}" (line-end-position) t)
191        (string-to-number (match-string 1))))
192
193 (defun nnimap-article-ranges (ranges)
194   (let (result)
195     (cond
196      ((numberp ranges)
197       (number-to-string ranges))
198      ((numberp (cdr ranges))
199       (format "%d:%d" (car ranges) (cdr ranges)))
200      (t
201       (dolist (elem ranges)
202         (push
203          (if (consp elem)
204              (format "%d:%d" (car elem) (cdr elem))
205            (number-to-string elem))
206          result))
207       (mapconcat #'identity (nreverse result) ",")))))
208
209 (deffoo nnimap-open-server (server &optional defs)
210   (if (nnimap-server-opened server)
211       t
212     (unless (assq 'nnimap-address defs)
213       (setq defs (append defs (list (list 'nnimap-address server)))))
214     (nnoo-change-server 'nnimap server defs)
215     (or (nnimap-find-connection nntp-server-buffer)
216         (nnimap-open-connection nntp-server-buffer))))
217
218 (defun nnimap-make-process-buffer (buffer)
219   (with-current-buffer
220       (generate-new-buffer (format "*nnimap %s %s %s*"
221                                    nnimap-address nnimap-server-port
222                                    (gnus-buffer-exists-p buffer)))
223     (mm-disable-multibyte)
224     (buffer-disable-undo)
225     (gnus-add-buffer)
226     (set (make-local-variable 'after-change-functions) nil)
227     (set (make-local-variable 'nnimap-object)
228          (make-nnimap :server (nnoo-current-server 'nnimap)))
229     (push (list buffer (current-buffer)) nnimap-connection-alist)
230     (push (current-buffer) nnimap-process-buffers)
231     (current-buffer)))
232
233 (defun nnimap-open-shell-stream (name buffer host port)
234   (let ((process-connection-type nil))
235     (start-process name buffer shell-file-name
236                    shell-command-switch
237                    (format-spec
238                     nnimap-shell-program
239                     (format-spec-make
240                      ?s host
241                      ?p port)))))
242
243 (defun nnimap-credentials (address ports)
244   (let (port credentials)
245     ;; Request the credentials from all ports, but only query on the
246     ;; last port if all the previous ones have failed.
247     (while (and (null credentials)
248                 (setq port (pop ports)))
249       (setq credentials
250             (auth-source-user-or-password
251              '("login" "password") address port nil (null ports))))
252     credentials))
253
254 (defun nnimap-keepalive ()
255   (let ((now (current-time)))
256     (dolist (buffer nnimap-process-buffers)
257       (when (buffer-name buffer)
258         (with-current-buffer buffer
259           (when (and nnimap-object
260                      (nnimap-last-command-time nnimap-object)
261                      (> (time-to-seconds
262                          (time-subtract
263                           now
264                           (nnimap-last-command-time nnimap-object)))
265                         ;; More than five minutes since the last command.
266                         (* 5 60)))
267             (nnimap-send-command "NOOP")))))))
268
269 (defun nnimap-open-connection (buffer)
270   (unless nnimap-keepalive-timer
271     (setq nnimap-keepalive-timer (run-at-time (* 60 15) (* 60 15)
272                                               'nnimap-keepalive)))
273   (with-current-buffer (nnimap-make-process-buffer buffer)
274     (let* ((coding-system-for-read 'binary)
275            (coding-system-for-write 'binary)
276            (ports
277             (cond
278              ((eq nnimap-stream 'network)
279               (open-network-stream
280                "*nnimap*" (current-buffer) nnimap-address
281                (or nnimap-server-port
282                    (if (netrc-find-service-number "imap")
283                        "imap"
284                      "143")))
285               '("143" "imap"))
286              ((eq nnimap-stream 'shell)
287               (nnimap-open-shell-stream
288                "*nnimap*" (current-buffer) nnimap-address
289                (or nnimap-server-port "imap"))
290               '("imap"))
291              ((eq nnimap-stream 'starttls)
292               (starttls-open-stream
293                "*nnimap*" (current-buffer) nnimap-address
294                (or nnimap-server-port "imap"))
295               '("imap"))
296              ((eq nnimap-stream 'ssl)
297               (open-tls-stream
298                "*nnimap*" (current-buffer) nnimap-address
299                (or nnimap-server-port
300                    (if (netrc-find-service-number "imaps")
301                        "imaps"
302                      "993")))
303               '("143" "993" "imap" "imaps"))))
304            connection-result login-result credentials)
305       (setf (nnimap-process nnimap-object)
306             (get-buffer-process (current-buffer)))
307       (when (and (nnimap-process nnimap-object)
308                  (memq (process-status (nnimap-process nnimap-object))
309                        '(open run)))
310         (gnus-set-process-query-on-exit-flag (nnimap-process nnimap-object) nil)
311         (when (setq connection-result (nnimap-wait-for-connection))
312           (when (eq nnimap-stream 'starttls)
313             (nnimap-send-command "STARTTLS")
314             (starttls-negotiate (nnimap-process nnimap-object)))
315           (unless (equal connection-result "PREAUTH")
316             (if (not (setq credentials
317                            (if (eq nnimap-authenticator 'anonymous)
318                                (list "anonymous"
319                                      (message-make-address))
320                              (nnimap-credentials
321                               nnimap-address
322                               (if nnimap-server-port
323                                   (cons (format "%s" nnimap-server-port) ports)
324                                 ports)))))
325                 (setq nnimap-object nil)
326               (setq login-result (nnimap-command "LOGIN %S %S"
327                                                  (car credentials)
328                                                  (cadr credentials)))
329               (unless (car login-result)
330                 (delete-process (nnimap-process nnimap-object))
331                 (setq nnimap-object nil))))
332           (when nnimap-object
333             (setf (nnimap-capabilities nnimap-object)
334                   (mapcar
335                    #'upcase
336                    (or (nnimap-find-parameter "CAPABILITY" (cdr login-result))
337                        (nnimap-find-parameter
338                         "CAPABILITY" (cdr (nnimap-command "CAPABILITY"))))))
339             (when (member "QRESYNC" (nnimap-capabilities nnimap-object))
340               (nnimap-command "ENABLE QRESYNC"))
341             t))))))
342
343 (defun nnimap-find-parameter (parameter elems)
344   (let (result)
345     (dolist (elem elems)
346       (cond
347        ((equal (car elem) parameter)
348         (setq result (cdr elem)))
349        ((and (equal (car elem) "OK")
350              (consp (cadr elem))
351              (equal (caadr elem) parameter))
352         (setq result (cdr (cadr elem))))))
353     result))
354
355 (deffoo nnimap-close-server (&optional server)
356   t)
357
358 (deffoo nnimap-request-close ()
359   t)
360
361 (deffoo nnimap-server-opened (&optional server)
362   (and (nnoo-current-server-p 'nnimap server)
363        nntp-server-buffer
364        (gnus-buffer-live-p nntp-server-buffer)
365        (nnimap-find-connection nntp-server-buffer)))
366
367 (deffoo nnimap-status-message (&optional server)
368   nnimap-status-string)
369
370 (deffoo nnimap-request-article (article &optional group server to-buffer)
371   (with-current-buffer nntp-server-buffer
372     (let ((result (nnimap-possibly-change-group group server))
373           parts)
374       (when (stringp article)
375         (setq article (nnimap-find-article-by-message-id group article)))
376       (when (and result
377                  article)
378         (erase-buffer)
379         (with-current-buffer (nnimap-buffer)
380           (erase-buffer)
381           (when nnimap-fetch-partial-articles
382             (if (eq nnimap-fetch-partial-articles t)
383                 (setq parts '(1))
384               (nnimap-command "UID FETCH %d (BODYSTRUCTURE)" article)
385               (goto-char (point-min))
386               (when (re-search-forward "FETCH.*BODYSTRUCTURE" nil t)
387                 (let ((structure (ignore-errors (read (current-buffer)))))
388                   (setq parts (nnimap-find-wanted-parts structure))))))
389           (setq result
390                 (nnimap-command
391                  (if (member "IMAP4REV1" (nnimap-capabilities nnimap-object))
392                      "UID FETCH %d BODY.PEEK[]"
393                    "UID FETCH %d RFC822.PEEK")
394                  article))
395           ;; Check that we really got an article.
396           (goto-char (point-min))
397           (unless (looking-at "\\* [0-9]+ FETCH")
398             (setq result nil)))
399         (let ((buffer (nnimap-find-process-buffer (current-buffer))))
400           (when (car result)
401             (with-current-buffer (or to-buffer nntp-server-buffer)
402               (insert-buffer-substring buffer)
403               (goto-char (point-min))
404               (let ((bytes (nnimap-get-length)))
405                 (delete-region (line-beginning-position)
406                                (progn (forward-line 1) (point)))
407                 (goto-char (+ (point) bytes))
408                 (delete-region (point) (point-max))
409                 (nnheader-ms-strip-cr))
410               (cons group article))))))))
411
412 (defun nnimap-find-wanted-parts (structure)
413   (message-flatten-list (nnimap-find-wanted-parts-1 structure "")))
414
415 (defun nnimap-find-wanted-parts-1 (structure prefix)
416   (let ((num 1)
417         parts)
418     (while (consp (car structure))
419       (let ((sub (pop structure)))
420         (if (consp (car sub))
421             (push (nnimap-find-wanted-parts-1
422                    sub (if (string= prefix "")
423                            (number-to-string num)
424                          (format "%s.%s" prefix num)))
425                   parts)
426           (let ((type (format "%s/%s" (nth 0 sub) (nth 1 sub))))
427             (when (string-match nnimap-fetch-partial-articles type)
428               (push (if (string= prefix "")
429                         (number-to-string num)
430                       (format "%s.%s" prefix num))
431                     parts)))
432           (incf num))))
433     (nreverse parts)))
434
435 (deffoo nnimap-request-group (group &optional server dont-check info)
436   (let ((result (nnimap-possibly-change-group group server))
437         articles active marks high low)
438     (with-current-buffer nntp-server-buffer
439       (when result
440         (if (and dont-check
441                  (setq active (nth 2 (assoc group nnimap-current-infos))))
442             (insert (format "211 %d %d %d %S\n"
443                             (- (cdr active) (car active))
444                             (car active)
445                             (cdr active)
446                             group))
447           (with-current-buffer (nnimap-buffer)
448             (erase-buffer)
449             (let ((group-sequence
450                    (nnimap-send-command "SELECT %S" (utf7-encode group t)))
451                   (flag-sequence
452                    (nnimap-send-command "UID FETCH 1:* FLAGS")))
453               (nnimap-wait-for-response flag-sequence)
454               (setq marks
455                     (nnimap-flags-to-marks
456                      (nnimap-parse-flags
457                       (list (list group-sequence flag-sequence 1 group)))))
458               (when info
459                 (nnimap-update-infos marks (list info)))
460               (goto-char (point-max))
461               (let ((uidnext (nth 5 (car marks))))
462                 (setq high (if uidnext
463                                (1- uidnext)
464                              (nth 3 (car marks)))
465                       low (or (nth 4 (car marks)) uidnext)))))
466           (erase-buffer)
467           (insert
468            (format
469             "211 %d %d %d %S\n" (1+ (- high low)) low high group)))
470         t))))
471
472 (deffoo nnimap-request-create-group (group &optional server args)
473   (when (nnimap-possibly-change-group nil server)
474     (with-current-buffer (nnimap-buffer)
475       (car (nnimap-command "CREATE %S" (utf7-encode group t))))))
476
477 (deffoo nnimap-request-delete-group (group &optional force server)
478   (when (nnimap-possibly-change-group nil server)
479     (with-current-buffer (nnimap-buffer)
480       (car (nnimap-command "DELETE %S" (utf7-encode group t))))))
481
482 (deffoo nnimap-request-expunge-group (group &optional server)
483   (when (nnimap-possibly-change-group group server)
484     (with-current-buffer (nnimap-buffer)
485       (car (nnimap-command "EXPUNGE")))))
486
487 (defun nnimap-get-flags (spec)
488   (let ((articles nil)
489         elems)
490     (with-current-buffer (nnimap-buffer)
491       (erase-buffer)
492       (nnimap-wait-for-response (nnimap-send-command
493                                  "UID FETCH %s FLAGS" spec))
494       (goto-char (point-min))
495       (while (re-search-forward "^\\* [0-9]+ FETCH (\\(.*\\))" nil t)
496         (setq elems (nnimap-parse-line (match-string 1)))
497         (push (cons (string-to-number (cadr (member "UID" elems)))
498                     (cadr (member "FLAGS" elems)))
499               articles)))
500     (nreverse articles)))
501
502 (deffoo nnimap-close-group (group &optional server)
503   t)
504
505 (deffoo nnimap-request-move-article (article group server accept-form
506                                              &optional last internal-move-group)
507   (with-temp-buffer
508     (when (nnimap-request-article article group server (current-buffer))
509       ;; If the move is internal (on the same server), just do it the easy
510       ;; way.
511       (let ((message-id (message-field-value "message-id")))
512         (if internal-move-group
513             (let ((result
514                    (with-current-buffer (nnimap-buffer)
515                      (nnimap-command "UID COPY %d %S"
516                                      article
517                                      (utf7-encode internal-move-group t)))))
518               (when (car result)
519                 (nnimap-delete-article article)
520                 (cons internal-move-group
521                       (nnimap-find-article-by-message-id
522                        internal-move-group message-id))))
523           ;; Move the article to a different method.
524           (let ((result (eval accept-form)))
525             (when result
526               (nnimap-delete-article article)
527               result)))))))
528
529 (deffoo nnimap-request-expire-articles (articles group &optional server force)
530   (cond
531    ((null articles)
532     nil)
533    ((not (nnimap-possibly-change-group group server))
534     articles)
535    ((and force
536          (eq nnmail-expiry-target 'delete))
537     (unless (nnimap-delete-article articles)
538       (message "Article marked for deletion, but not expunged."))
539     nil)
540    (t
541     (let ((deletable-articles
542            (if (or force
543                    (eq nnmail-expiry-wait 'immediate))
544                articles
545              (gnus-sorted-intersection
546               articles
547               (nnimap-find-expired-articles group)))))
548       (if (null deletable-articles)
549           articles
550         (if (eq nnmail-expiry-target 'delete)
551             (nnimap-delete-article deletable-articles)
552           (setq deletable-articles
553                 (nnimap-process-expiry-targets
554                  deletable-articles group server)))
555         ;; Return the articles we didn't delete.
556         (gnus-sorted-complement articles deletable-articles))))))
557
558 (defun nnimap-process-expiry-targets (articles group server)
559   (let ((deleted-articles nil))
560     (dolist (article articles)
561       (let ((target nnmail-expiry-target))
562         (with-temp-buffer
563           (when (nnimap-request-article article group server (current-buffer))
564             (message "Expiring article %s:%d" group article)
565             (when (functionp target)
566               (setq target (funcall target group)))
567             (when (and target
568                        (not (eq target 'delete)))
569               (if (or (gnus-request-group target t)
570                       (gnus-request-create-group target))
571                   (nnmail-expiry-target-group target group)
572                 (setq target nil)))
573             (when target
574               (push article deleted-articles))))))
575     ;; Change back to the current group again.
576     (nnimap-possibly-change-group group server)
577     (setq deleted-articles (nreverse deleted-articles))
578     (nnimap-delete-article deleted-articles)
579     deleted-articles))
580
581 (defun nnimap-find-expired-articles (group)
582   (let ((cutoff (nnmail-expired-article-p group nil nil)))
583     (with-current-buffer (nnimap-buffer)
584       (let ((result
585              (nnimap-command
586               "UID SEARCH SENTBEFORE %s"
587               (format-time-string
588                (format "%%d-%s-%%Y"
589                        (upcase
590                         (car (rassoc (nth 4 (decode-time cutoff))
591                                      parse-time-months))))
592                cutoff))))
593         (and (car result)
594              (delete 0 (mapcar #'string-to-number
595                                (cdr (assoc "SEARCH" (cdr result))))))))))
596
597
598 (defun nnimap-find-article-by-message-id (group message-id)
599   (when (nnimap-possibly-change-group group nil)
600     (with-current-buffer (nnimap-buffer)
601       (let ((result
602              (nnimap-command "UID SEARCH HEADER Message-Id %S" message-id))
603             article)
604         (when (car result)
605           ;; Select the last instance of the message in the group.
606           (and (setq article
607                      (car (last (assoc "SEARCH" (cdr result)))))
608                (string-to-number article)))))))
609
610 (defun nnimap-delete-article (articles)
611   (with-current-buffer (nnimap-buffer)
612     (nnimap-command "UID STORE %s +FLAGS.SILENT (\\Deleted)"
613                     (nnimap-article-ranges articles))
614     (cond
615      ((member "UIDPLUS" (nnimap-capabilities nnimap-object))
616       (nnimap-command "UID EXPUNGE %s"
617                       (nnimap-article-ranges articles))
618       t)
619      (nnimap-expunge
620       (nnimap-command "EXPUNGE")
621       t)
622      (t (gnus-message 7 (concat "nnimap: nnimap-expunge is not set and the "
623                                 "server doesn't support UIDPLUS, so we won't "
624                                 "delete this article now"))))))
625
626 (deffoo nnimap-request-scan (&optional group server)
627   (when (and (nnimap-possibly-change-group nil server)
628              nnimap-inbox
629              nnimap-split-methods)
630     (message "nnimap %s splitting mail..." server)
631     (nnimap-split-incoming-mail)))
632
633 (defun nnimap-marks-to-flags (marks)
634   (let (flags flag)
635     (dolist (mark marks)
636       (when (setq flag (cadr (assq mark nnimap-mark-alist)))
637         (push flag flags)))
638     flags))
639
640 (deffoo nnimap-request-set-mark (group actions &optional server)
641   (when (nnimap-possibly-change-group group server)
642     (let (sequence)
643       (with-current-buffer (nnimap-buffer)
644         ;; Just send all the STORE commands without waiting for
645         ;; response.  If they're successful, they're successful.
646         (dolist (action actions)
647           (destructuring-bind (range action marks) action
648             (let ((flags (nnimap-marks-to-flags marks)))
649               (when flags
650                 (setq sequence (nnimap-send-command
651                                 "UID STORE %s %sFLAGS.SILENT (%s)"
652                                 (nnimap-article-ranges range)
653                                 (if (eq action 'del)
654                                     "-"
655                                   "+")
656                                 (mapconcat #'identity flags " ")))))))
657         ;; Wait for the last command to complete to avoid later
658         ;; syncronisation problems with the stream.
659         (when sequence
660           (nnimap-wait-for-response sequence))))))
661
662 (deffoo nnimap-request-accept-article (group &optional server last)
663   (when (nnimap-possibly-change-group nil server)
664     (nnmail-check-syntax)
665     (let ((message (buffer-string))
666           (message-id (message-field-value "message-id"))
667           sequence)
668       (with-current-buffer (nnimap-buffer)
669         (setq sequence (nnimap-send-command
670                         "APPEND %S {%d}" (utf7-encode group t)
671                         (length message)))
672         (process-send-string (get-buffer-process (current-buffer)) message)
673         (process-send-string (get-buffer-process (current-buffer))
674                              (if (nnimap-newlinep nnimap-object)
675                                  "\n"
676                                "\r\n"))
677         (let ((result (nnimap-get-response sequence)))
678           (when result
679             (cons group
680                   (nnimap-find-article-by-message-id group message-id))))))))
681
682 (defun nnimap-add-cr ()
683   (goto-char (point-min))
684   (while (re-search-forward "\r?\n" nil t)
685     (replace-match "\r\n" t t)))
686
687 (defun nnimap-get-groups ()
688   (let ((result (nnimap-command "LIST \"\" \"*\""))
689         groups)
690     (when (car result)
691       (dolist (line (cdr result))
692         (when (and (equal (car line) "LIST")
693                    (not (and (caadr line)
694                              (string-match "noselect" (caadr line)))))
695           (push (car (last line)) groups)))
696       (nreverse groups))))
697
698 (deffoo nnimap-request-list (&optional server)
699   (nnimap-possibly-change-group nil server)
700   (with-current-buffer nntp-server-buffer
701     (erase-buffer)
702     (let ((groups
703            (with-current-buffer (nnimap-buffer)
704              (nnimap-get-groups)))
705           sequences responses)
706       (when groups
707         (with-current-buffer (nnimap-buffer)
708           (setf (nnimap-group nnimap-object) nil)
709           (dolist (group groups)
710             (push (list (nnimap-send-command "EXAMINE %S" (utf7-encode group t))
711                         group)
712                   sequences))
713           (nnimap-wait-for-response (caar sequences))
714           (setq responses
715                 (nnimap-get-responses (mapcar #'car sequences))))
716         (dolist (response responses)
717           (let* ((sequence (car response))
718                  (response (cadr response))
719                  (group (cadr (assoc sequence sequences))))
720             (when (and group
721                        (equal (caar response) "OK"))
722               (let ((uidnext (nnimap-find-parameter "UIDNEXT" response))
723                     highest exists)
724                 (dolist (elem response)
725                   (when (equal (cadr elem) "EXISTS")
726                     (setq exists (string-to-number (car elem)))))
727                 (when uidnext
728                   (setq highest (1- (string-to-number (car uidnext)))))
729                 (cond
730                  ((null highest)
731                   (insert (format "%S 0 1 y\n" (utf7-decode group t))))
732                  ((zerop exists)
733                   ;; Empty group.
734                   (insert (format "%S %d %d y\n"
735                                   (utf7-decode group t) highest (1+ highest))))
736                  (t
737                   ;; Return the widest possible range.
738                   (insert (format "%S %d 1 y\n" (utf7-decode group t)
739                                   (or highest exists)))))))))
740         t))))
741
742 (deffoo nnimap-retrieve-group-data-early (server infos)
743   (when (nnimap-possibly-change-group nil server)
744     (with-current-buffer (nnimap-buffer)
745       ;; QRESYNC handling isn't implemented.
746       (let ((qresyncp (member "notQRESYNC" (nnimap-capabilities nnimap-object)))
747             marks groups sequences)
748         ;; Go through the infos and gather the data needed to know
749         ;; what and how to request the data.
750         (dolist (info infos)
751           (setq marks (gnus-info-marks info))
752           (push (list (gnus-group-real-name (gnus-info-group info))
753                       (cdr (assq 'active marks))
754                       (cdr (assq 'uid marks)))
755                 groups))
756         ;; Then request the data.
757         (erase-buffer)
758         (setf (nnimap-group nnimap-object) nil)
759         (dolist (elem groups)
760           (if (and qresyncp
761                    (nth 2 elem))
762               (push
763                (list 'qresync
764                      (nnimap-send-command "EXAMINE %S (QRESYNC (%s %s))"
765                                           (car elem)
766                                           (car (nth 2 elem))
767                                           (cdr (nth 2 elem)))
768                      nil
769                      (car elem))
770                sequences)
771             (let ((start
772                    (if (nth 1 elem)
773                        ;; Fetch the last 100 flags.
774                        (max 1 (- (cdr (nth 1 elem)) 100))
775                      1)))
776               (push (list (nnimap-send-command "EXAMINE %S" (car elem))
777                           (nnimap-send-command "UID FETCH %d:* FLAGS" start)
778                           start
779                           (car elem))
780                     sequences))))
781         sequences))))
782
783 (deffoo nnimap-finish-retrieve-group-infos (server infos sequences)
784   (when (and sequences
785              (nnimap-possibly-change-group nil server))
786     (with-current-buffer (nnimap-buffer)
787       ;; Wait for the final data to trickle in.
788       (nnimap-wait-for-response (cadar sequences))
789       ;; Now we should have all the data we need, no matter whether
790       ;; we're QRESYNCING, fetching all the flags from scratch, or
791       ;; just fetching the last 100 flags per group.
792       (nnimap-update-infos (nnimap-flags-to-marks
793                             (nnimap-parse-flags
794                              (nreverse sequences)))
795                            infos)
796       ;; Finally, just return something resembling an active file in
797       ;; the nntp buffer, so that the agent can save the info, too.
798       (with-current-buffer nntp-server-buffer
799         (erase-buffer)
800         (dolist (info infos)
801           (let* ((group (gnus-info-group info))
802                  (active (gnus-active group)))
803             (when active
804               (insert (format "%S %d %d y\n"
805                               (gnus-group-real-name group)
806                               (cdr active)
807                               (car active))))))))))
808
809 (defun nnimap-update-infos (flags infos)
810   (dolist (info infos)
811     (let ((group (gnus-group-real-name (gnus-info-group info))))
812       (nnimap-update-info info (cdr (assoc group flags))))))
813
814 (defun nnimap-update-info (info marks)
815   (when marks
816     (destructuring-bind (existing flags high low uidnext start-article
817                                   permanent-flags) marks
818       (let ((group (gnus-info-group info))
819             (completep (and start-article
820                             (= start-article 1))))
821         (when uidnext
822           (setq high (1- uidnext)))
823         ;; First set the active ranges based on high/low.
824         (if (or completep
825                 (not (gnus-active group)))
826             (gnus-set-active group
827                              (if (and low high)
828                                  (cons low high)
829                                ;; No articles in this group.
830                                (cons uidnext (1- uidnext))))
831           (setcdr (gnus-active group) (or high (1- uidnext))))
832         (unless high
833           (setq high (1- uidnext)))
834         ;; Then update the list of read articles.
835         (let* ((unread
836                 (gnus-compress-sequence
837                  (gnus-set-difference
838                   (gnus-set-difference
839                    existing
840                    (cdr (assoc '%Seen flags)))
841                   (cdr (assoc '%Flagged flags)))))
842                (read (gnus-range-difference
843                       (cons start-article high) unread)))
844           (when (> start-article 1)
845             (setq read
846                   (gnus-range-nconcat
847                    (if (> start-article 1)
848                        (gnus-sorted-range-intersection
849                         (cons 1 (1- start-article))
850                         (gnus-info-read info))
851                      (gnus-info-read info))
852                    read)))
853           (gnus-info-set-read info read)
854           ;; Update the marks.
855           (setq marks (gnus-info-marks info))
856           ;; Note the active level for the next run-through.
857           (let ((active (assq 'active marks)))
858             (if active
859                 (setcdr active (gnus-active group))
860               (push (cons 'active (gnus-active group)) marks)))
861           (dolist (type (cdr nnimap-mark-alist))
862             (let ((old-marks (assoc (car type) marks))
863                   (new-marks
864                    (gnus-compress-sequence
865                     (cdr (or (assoc (caddr type) flags)     ; %Flagged
866                              (assoc (intern (cadr type) obarray) flags)
867                              (assoc (cadr type) flags)))))) ; "\Flagged"
868               (setq marks (delq old-marks marks))
869               (pop old-marks)
870               (when (and old-marks
871                          (> start-article 1))
872                 (setq old-marks (gnus-range-difference
873                                  old-marks
874                                  (cons start-article high)))
875                 (setq new-marks (gnus-range-nconcat old-marks new-marks)))
876               (when new-marks
877                 (push (cons (car type) new-marks) marks)))
878             (gnus-info-set-marks info marks t)
879             (nnimap-store-info info (gnus-active group))))))))
880
881 (defun nnimap-store-info (info active)
882   (let* ((group (gnus-group-real-name (gnus-info-group info)))
883          (entry (assoc group nnimap-current-infos)))
884     (if entry
885         (setcdr entry (list info active))
886       (push (list group info active) nnimap-current-infos))))
887
888 (defun nnimap-flags-to-marks (groups)
889   (let (data group totalp uidnext articles start-article mark permanent-flags)
890     (dolist (elem groups)
891       (setq group (car elem)
892             uidnext (nth 1 elem)
893             start-article (nth 2 elem)
894             permanent-flags (nth 3 elem)
895             articles (nthcdr 4 elem))
896       (let ((high (caar articles))
897             marks low existing)
898         (dolist (article articles)
899           (setq low (car article))
900           (push (car article) existing)
901           (dolist (flag (cdr article))
902             (setq mark (assoc flag marks))
903             (if (not mark)
904                 (push (list flag (car article)) marks)
905               (setcdr mark (cons (car article) (cdr mark))))))
906         (push (list group existing marks high low uidnext start-article
907                     permanent-flags)
908               data)))
909     data))
910
911 (defun nnimap-parse-flags (sequences)
912   (goto-char (point-min))
913   ;; Change \Delete etc to %Delete, so that the reader can read it.
914   (subst-char-in-region (point-min) (point-max)
915                         ?\\ ?% t)
916   (let (start end articles groups uidnext elems permanent-flags)
917     (dolist (elem sequences)
918       (destructuring-bind (group-sequence flag-sequence totalp group) elem
919         (setq start (point))
920         ;; The EXAMINE was successful.
921         (when (and (search-forward (format "\n%d OK " group-sequence) nil t)
922                    (progn
923                      (forward-line 1)
924                      (setq end (point))
925                      (goto-char start)
926                      (setq permanent-flags
927                            (and (search-forward "PERMANENTFLAGS "
928                                                  (or end (point-min)) t)
929                                 (read (current-buffer))))
930                      (goto-char start)
931                      (setq uidnext
932                            (and (search-forward "UIDNEXT "
933                                                  (or end (point-min)) t)
934                                 (read (current-buffer))))
935                      (goto-char end)
936                      (forward-line -1))
937                    ;; The UID FETCH FLAGS was successful.
938                    (search-forward (format "\n%d OK " flag-sequence) nil t))
939           (setq start (point))
940           (goto-char end)
941           (while (search-forward " FETCH " start t)
942             (setq elems (read (current-buffer)))
943             (push (cons (cadr (memq 'UID elems))
944                         (cadr (memq 'FLAGS elems)))
945                   articles))
946           (push (nconc (list group uidnext totalp permanent-flags) articles)
947                 groups)
948           (setq articles nil))))
949     groups))
950
951 (defun nnimap-find-process-buffer (buffer)
952   (cadr (assoc buffer nnimap-connection-alist)))
953
954 (deffoo nnimap-request-post (&optional server)
955   (setq nnimap-status-string "Read-only server")
956   nil)
957
958 (defun nnimap-possibly-change-group (group server)
959   (let ((open-result t))
960     (when (and server
961                (not (nnimap-server-opened server)))
962       (setq open-result (nnimap-open-server server)))
963     (cond
964      ((not open-result)
965       nil)
966      ((not group)
967       t)
968      (t
969       (with-current-buffer (nnimap-buffer)
970         (if (equal group (nnimap-group nnimap-object))
971             t
972           (let ((result (nnimap-command "SELECT %S" (utf7-encode group t))))
973             (when (car result)
974               (setf (nnimap-group nnimap-object) group
975                     (nnimap-select-result nnimap-object) result)
976               result))))))))
977
978 (defun nnimap-find-connection (buffer)
979   "Find the connection delivering to BUFFER."
980   (let ((entry (assoc buffer nnimap-connection-alist)))
981     (when entry
982       (if (and (buffer-name (cadr entry))
983                (get-buffer-process (cadr entry))
984                (memq (process-status (get-buffer-process (cadr entry)))
985                      '(open run)))
986           (get-buffer-process (cadr entry))
987         (setq nnimap-connection-alist (delq entry nnimap-connection-alist))
988         nil))))
989
990 (defvar nnimap-sequence 0)
991
992 (defun nnimap-send-command (&rest args)
993   (process-send-string
994    (get-buffer-process (current-buffer))
995    (nnimap-log-command
996     (format "%d %s%s\n"
997             (incf nnimap-sequence)
998             (apply #'format args)
999             (if (nnimap-newlinep nnimap-object)
1000                 ""
1001               "\r"))))
1002   nnimap-sequence)
1003
1004 (defun nnimap-log-command (command)
1005   (with-current-buffer (get-buffer-create "*imap log*")
1006     (goto-char (point-max))
1007     (insert (format-time-string "%H:%M:%S") " " command))
1008   command)
1009
1010 (defun nnimap-command (&rest args)
1011   (erase-buffer)
1012   (setf (nnimap-last-command-time nnimap-object) (current-time))
1013   (let* ((sequence (apply #'nnimap-send-command args))
1014          (response (nnimap-get-response sequence)))
1015     (if (equal (caar response) "OK")
1016         (cons t response)
1017       (nnheader-report 'nnimap "%s"
1018                        (mapconcat (lambda (a)
1019                                     (format "%s" a))
1020                                   (car response) " "))
1021       nil)))
1022
1023 (defun nnimap-get-response (sequence)
1024   (nnimap-wait-for-response sequence)
1025   (nnimap-parse-response))
1026
1027 (defun nnimap-wait-for-connection ()
1028   (let ((process (get-buffer-process (current-buffer))))
1029     (goto-char (point-min))
1030     (while (and (memq (process-status process)
1031                       '(open run))
1032                 (not (re-search-forward "^\\* .*\n" nil t)))
1033       (nnheader-accept-process-output process)
1034       (goto-char (point-min)))
1035     (forward-line -1)
1036     (and (looking-at "\\* \\([A-Z0-9]+\\)")
1037          (match-string 1))))
1038
1039 (defun nnimap-wait-for-response (sequence &optional messagep)
1040   (let ((process (get-buffer-process (current-buffer))))
1041     (goto-char (point-max))
1042     (while (and (memq (process-status process)
1043                       '(open run))
1044                 (not (re-search-backward (format "^%d .*\n" sequence)
1045                                          (max (point-min) (- (point) 500))
1046                                          t)))
1047       (when messagep
1048         (message "Read %dKB" (/ (buffer-size) 1000)))
1049       (nnheader-accept-process-output process)
1050       (goto-char (point-max)))))
1051
1052 (defun nnimap-parse-response ()
1053   (let ((lines (split-string (nnimap-last-response-string) "\r\n" t))
1054         result)
1055     (dolist (line lines)
1056       (push (cdr (nnimap-parse-line line)) result))
1057     ;; Return the OK/error code first, and then all the "continuation
1058     ;; lines" afterwards.
1059     (cons (pop result)
1060           (nreverse result))))
1061
1062 ;; Parse an IMAP response line lightly.  They look like
1063 ;; "* OK [UIDVALIDITY 1164213559] UIDs valid", typically, so parse
1064 ;; the lines into a list of strings and lists of string.
1065 (defun nnimap-parse-line (line)
1066   (let (char result)
1067     (with-temp-buffer
1068       (insert line)
1069       (goto-char (point-min))
1070       (while (not (eobp))
1071         (if (eql (setq char (following-char)) ? )
1072             (forward-char 1)
1073           (push
1074            (cond
1075             ((eql char ?\[)
1076              (split-string (buffer-substring
1077                             (1+ (point)) (1- (search-forward "]")))))
1078             ((eql char ?\()
1079              (split-string (buffer-substring
1080                             (1+ (point)) (1- (search-forward ")")))))
1081             ((eql char ?\")
1082              (forward-char 1)
1083              (buffer-substring (point) (1- (search-forward "\""))))
1084             (t
1085              (buffer-substring (point) (if (search-forward " " nil t)
1086                                            (1- (point))
1087                                          (goto-char (point-max))))))
1088            result)))
1089       (nreverse result))))
1090
1091 (defun nnimap-last-response-string ()
1092   (save-excursion
1093     (forward-line 1)
1094     (let ((end (point)))
1095       (forward-line -1)
1096       (when (not (bobp))
1097         (forward-line -1)
1098         (while (and (not (bobp))
1099                     (eql (following-char) ?*))
1100           (forward-line -1))
1101         (unless (eql (following-char) ?*)
1102           (forward-line 1)))
1103       (buffer-substring (point) end))))
1104
1105 (defun nnimap-get-responses (sequences)
1106   (let (responses)
1107     (dolist (sequence sequences)
1108       (goto-char (point-min))
1109       (when (re-search-forward (format "^%d " sequence) nil t)
1110         (push (list sequence (nnimap-parse-response))
1111               responses)))
1112     responses))
1113
1114 (defvar nnimap-incoming-split-list nil)
1115
1116 (defun nnimap-fetch-inbox (articles)
1117   (erase-buffer)
1118   (nnimap-wait-for-response
1119    (nnimap-send-command
1120     "UID FETCH %s %s"
1121     (nnimap-article-ranges articles)
1122     (format "(UID %s%s)"
1123             (format
1124              (if (member "IMAP4REV1"
1125                          (nnimap-capabilities nnimap-object))
1126                  "BODY.PEEK[HEADER] BODY.PEEK"
1127                "RFC822.PEEK"))
1128             (if nnimap-split-download-body-default
1129                 "[]"
1130               "[1]")))
1131    t))
1132
1133 (defun nnimap-split-incoming-mail ()
1134   (with-current-buffer (nnimap-buffer)
1135     (let ((nnimap-incoming-split-list nil)
1136           (nnmail-split-methods nnimap-split-methods)
1137           (nnmail-inhibit-default-split-group t)
1138           (groups (nnimap-get-groups))
1139           new-articles)
1140       (erase-buffer)
1141       (nnimap-command "SELECT %S" nnimap-inbox)
1142       (setq new-articles (nnimap-new-articles (nnimap-get-flags "1:*")))
1143       (when new-articles
1144         (nnimap-fetch-inbox new-articles)
1145         (nnimap-transform-split-mail)
1146         (nnheader-ms-strip-cr)
1147         (nnmail-cache-open)
1148         (nnmail-split-incoming (current-buffer)
1149                                #'nnimap-save-mail-spec
1150                                nil nil
1151                                #'nnimap-dummy-active-number
1152                                #'nnimap-save-mail-spec)
1153         (when nnimap-incoming-split-list
1154           (let ((specs (nnimap-make-split-specs nnimap-incoming-split-list))
1155                 sequences junk-articles)
1156             ;; Create any groups that doesn't already exist on the
1157             ;; server first.
1158             (dolist (spec specs)
1159               (when (and (not (member (car spec) groups))
1160                          (not (eq (car spec) 'junk)))
1161                 (nnimap-command "CREATE %S" (utf7-encode (car spec) t))))
1162             ;; Then copy over all the messages.
1163             (erase-buffer)
1164             (dolist (spec specs)
1165               (let ((group (car spec))
1166                     (ranges (cdr spec)))
1167                 (if (eq group 'junk)
1168                     (setq junk-articles ranges)
1169                   (push (list (nnimap-send-command
1170                                "UID COPY %s %S"
1171                                (nnimap-article-ranges ranges)
1172                                (utf7-encode group t))
1173                               ranges)
1174                         sequences))))
1175             ;; Wait for the last COPY response...
1176             (when sequences
1177               (nnimap-wait-for-response (caar sequences))
1178               ;; And then mark the successful copy actions as deleted,
1179               ;; and possibly expunge them.
1180               (nnimap-mark-and-expunge-incoming
1181                (nnimap-parse-copied-articles sequences)))
1182             (nnimap-mark-and-expunge-incoming junk-articles)))))))
1183
1184 (defun nnimap-mark-and-expunge-incoming (range)
1185   (when range
1186     (setq range (nnimap-article-ranges range))
1187     (let ((sequence
1188            (nnimap-send-command
1189             "UID STORE %s +FLAGS.SILENT (\\Deleted)" range)))
1190       (cond
1191        ;; If the server supports it, we now delete the message we have
1192        ;; just copied over.
1193        ((member "UIDPLUS" (nnimap-capabilities nnimap-object))
1194         (setq sequence (nnimap-send-command "UID EXPUNGE %s" range)))
1195        ;; If it doesn't support UID EXPUNGE, then we only expunge if the
1196        ;; user has configured it.
1197        (nnimap-expunge
1198         (setq sequence (nnimap-send-command "EXPUNGE"))))
1199       (nnimap-wait-for-response sequence))))
1200
1201 (defun nnimap-parse-copied-articles (sequences)
1202   (let (sequence copied range)
1203     (goto-char (point-min))
1204     (while (re-search-forward "^\\([0-9]+\\) OK " nil t)
1205       (setq sequence (string-to-number (match-string 1)))
1206       (when (setq range (cadr (assq sequence sequences)))
1207         (push (gnus-uncompress-range range) copied)))
1208     (gnus-compress-sequence (sort (apply #'nconc copied) #'<))))
1209
1210 (defun nnimap-new-articles (flags)
1211   (let (new)
1212     (dolist (elem flags)
1213       (when (or (null (cdr elem))
1214                 (and (not (memq '%Deleted (cdr elem)))
1215                      (not (memq '%Seen (cdr elem)))))
1216         (push (car elem) new)))
1217     (gnus-compress-sequence (nreverse new))))
1218
1219 (defun nnimap-make-split-specs (list)
1220   (let ((specs nil)
1221         entry)
1222     (dolist (elem list)
1223       (destructuring-bind (article spec) elem
1224         (dolist (group (delete nil (mapcar #'car spec)))
1225           (unless (setq entry (assoc group specs))
1226             (push (setq entry (list group)) specs))
1227           (setcdr entry (cons article (cdr entry))))))
1228     (dolist (entry specs)
1229       (setcdr entry (gnus-compress-sequence (sort (cdr entry) #'<))))
1230     specs))
1231
1232 (defun nnimap-transform-split-mail ()
1233   (goto-char (point-min))
1234   (let (article bytes)
1235     (block nil
1236       (while (not (eobp))
1237         (while (not (looking-at "^\\* [0-9]+ FETCH.*UID \\([0-9]+\\)"))
1238           (delete-region (point) (progn (forward-line 1) (point)))
1239           (when (eobp)
1240             (return)))
1241         (setq article (match-string 1)
1242               bytes (nnimap-get-length))
1243         (delete-region (line-beginning-position) (line-end-position))
1244         ;; Insert MMDF separator, and a way to remember what this
1245         ;; article UID is.
1246         (insert (format "\^A\^A\^A\^A\n\nX-nnimap-article: %s" article))
1247         (forward-char (1+ bytes))
1248         (setq bytes (nnimap-get-length))
1249         (delete-region (line-beginning-position) (line-end-position))
1250         (forward-char (1+ bytes))
1251         (delete-region (line-beginning-position) (line-end-position))))))
1252
1253 (defun nnimap-dummy-active-number (group &optional server)
1254   1)
1255
1256 (defun nnimap-save-mail-spec (group-art &optional server full-nov)
1257   (let (article)
1258     (goto-char (point-min))
1259     (if (not (re-search-forward "X-nnimap-article: \\([0-9]+\\)" nil t))
1260         (error "Invalid nnimap mail")
1261       (setq article (string-to-number (match-string 1))))
1262     (push (list article
1263                 (if (eq group-art 'junk)
1264                     (list (cons 'junk 1))
1265                   group-art))
1266           nnimap-incoming-split-list)))
1267
1268 (provide 'nnimap)
1269
1270 ;;; nnimap.el ends here