(nnimap-update-info): Protect against nil uidnexts.
[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                              (cond
828                               ((and low high)
829                                (cons low high))
830                               (uidnext
831                                ;; No articles in this group.
832                                (cons uidnext (1- uidnext)))
833                               (start-article
834                                (cons start-article (1- start-article)))
835                               (t
836                                ;; No articles and no uidnext.
837                                nil)))
838           (setcdr (gnus-active group) (or high (1- uidnext))))
839         (when (and (not high)
840                    uidnext)
841           (setq high (1- uidnext)))
842         ;; Then update the list of read articles.
843         (let* ((unread
844                 (gnus-compress-sequence
845                  (gnus-set-difference
846                   (gnus-set-difference
847                    existing
848                    (cdr (assoc '%Seen flags)))
849                   (cdr (assoc '%Flagged flags)))))
850                (read (gnus-range-difference
851                       (cons start-article high) unread)))
852           (when (> start-article 1)
853             (setq read
854                   (gnus-range-nconcat
855                    (if (> start-article 1)
856                        (gnus-sorted-range-intersection
857                         (cons 1 (1- start-article))
858                         (gnus-info-read info))
859                      (gnus-info-read info))
860                    read)))
861           (gnus-info-set-read info read)
862           ;; Update the marks.
863           (setq marks (gnus-info-marks info))
864           ;; Note the active level for the next run-through.
865           (let ((active (assq 'active marks)))
866             (if active
867                 (setcdr active (gnus-active group))
868               (push (cons 'active (gnus-active group)) marks)))
869           (dolist (type (cdr nnimap-mark-alist))
870             (let ((old-marks (assoc (car type) marks))
871                   (new-marks
872                    (gnus-compress-sequence
873                     (cdr (or (assoc (caddr type) flags)     ; %Flagged
874                              (assoc (intern (cadr type) obarray) flags)
875                              (assoc (cadr type) flags)))))) ; "\Flagged"
876               (setq marks (delq old-marks marks))
877               (pop old-marks)
878               (when (and old-marks
879                          (> start-article 1))
880                 (setq old-marks (gnus-range-difference
881                                  old-marks
882                                  (cons start-article high)))
883                 (setq new-marks (gnus-range-nconcat old-marks new-marks)))
884               (when new-marks
885                 (push (cons (car type) new-marks) marks)))
886             (gnus-info-set-marks info marks t)
887             (nnimap-store-info info (gnus-active group))))))))
888
889 (defun nnimap-store-info (info active)
890   (let* ((group (gnus-group-real-name (gnus-info-group info)))
891          (entry (assoc group nnimap-current-infos)))
892     (if entry
893         (setcdr entry (list info active))
894       (push (list group info active) nnimap-current-infos))))
895
896 (defun nnimap-flags-to-marks (groups)
897   (let (data group totalp uidnext articles start-article mark permanent-flags)
898     (dolist (elem groups)
899       (setq group (car elem)
900             uidnext (nth 1 elem)
901             start-article (nth 2 elem)
902             permanent-flags (nth 3 elem)
903             articles (nthcdr 4 elem))
904       (let ((high (caar articles))
905             marks low existing)
906         (dolist (article articles)
907           (setq low (car article))
908           (push (car article) existing)
909           (dolist (flag (cdr article))
910             (setq mark (assoc flag marks))
911             (if (not mark)
912                 (push (list flag (car article)) marks)
913               (setcdr mark (cons (car article) (cdr mark))))))
914         (push (list group existing marks high low uidnext start-article
915                     permanent-flags)
916               data)))
917     data))
918
919 (defun nnimap-parse-flags (sequences)
920   (goto-char (point-min))
921   ;; Change \Delete etc to %Delete, so that the reader can read it.
922   (subst-char-in-region (point-min) (point-max)
923                         ?\\ ?% t)
924   (let (start end articles groups uidnext elems permanent-flags)
925     (dolist (elem sequences)
926       (destructuring-bind (group-sequence flag-sequence totalp group) elem
927         (setq start (point))
928         ;; The EXAMINE was successful.
929         (when (and (search-forward (format "\n%d OK " group-sequence) nil t)
930                    (progn
931                      (forward-line 1)
932                      (setq end (point))
933                      (goto-char start)
934                      (setq permanent-flags
935                            (and (search-forward "PERMANENTFLAGS "
936                                                  (or end (point-min)) t)
937                                 (read (current-buffer))))
938                      (goto-char start)
939                      (setq uidnext
940                            (and (search-forward "UIDNEXT "
941                                                  (or end (point-min)) t)
942                                 (read (current-buffer))))
943                      (goto-char end)
944                      (forward-line -1))
945                    ;; The UID FETCH FLAGS was successful.
946                    (search-forward (format "\n%d OK " flag-sequence) nil t))
947           (setq start (point))
948           (goto-char end)
949           (while (search-forward " FETCH " start t)
950             (setq elems (read (current-buffer)))
951             (push (cons (cadr (memq 'UID elems))
952                         (cadr (memq 'FLAGS elems)))
953                   articles))
954           (push (nconc (list group uidnext totalp permanent-flags) articles)
955                 groups)
956           (setq articles nil))))
957     groups))
958
959 (defun nnimap-find-process-buffer (buffer)
960   (cadr (assoc buffer nnimap-connection-alist)))
961
962 (deffoo nnimap-request-post (&optional server)
963   (setq nnimap-status-string "Read-only server")
964   nil)
965
966 (defun nnimap-possibly-change-group (group server)
967   (let ((open-result t))
968     (when (and server
969                (not (nnimap-server-opened server)))
970       (setq open-result (nnimap-open-server server)))
971     (cond
972      ((not open-result)
973       nil)
974      ((not group)
975       t)
976      (t
977       (with-current-buffer (nnimap-buffer)
978         (if (equal group (nnimap-group nnimap-object))
979             t
980           (let ((result (nnimap-command "SELECT %S" (utf7-encode group t))))
981             (when (car result)
982               (setf (nnimap-group nnimap-object) group
983                     (nnimap-select-result nnimap-object) result)
984               result))))))))
985
986 (defun nnimap-find-connection (buffer)
987   "Find the connection delivering to BUFFER."
988   (let ((entry (assoc buffer nnimap-connection-alist)))
989     (when entry
990       (if (and (buffer-name (cadr entry))
991                (get-buffer-process (cadr entry))
992                (memq (process-status (get-buffer-process (cadr entry)))
993                      '(open run)))
994           (get-buffer-process (cadr entry))
995         (setq nnimap-connection-alist (delq entry nnimap-connection-alist))
996         nil))))
997
998 (defvar nnimap-sequence 0)
999
1000 (defun nnimap-send-command (&rest args)
1001   (process-send-string
1002    (get-buffer-process (current-buffer))
1003    (nnimap-log-command
1004     (format "%d %s%s\n"
1005             (incf nnimap-sequence)
1006             (apply #'format args)
1007             (if (nnimap-newlinep nnimap-object)
1008                 ""
1009               "\r"))))
1010   nnimap-sequence)
1011
1012 (defun nnimap-log-command (command)
1013   (with-current-buffer (get-buffer-create "*imap log*")
1014     (goto-char (point-max))
1015     (insert (format-time-string "%H:%M:%S") " " command))
1016   command)
1017
1018 (defun nnimap-command (&rest args)
1019   (erase-buffer)
1020   (setf (nnimap-last-command-time nnimap-object) (current-time))
1021   (let* ((sequence (apply #'nnimap-send-command args))
1022          (response (nnimap-get-response sequence)))
1023     (if (equal (caar response) "OK")
1024         (cons t response)
1025       (nnheader-report 'nnimap "%s"
1026                        (mapconcat (lambda (a)
1027                                     (format "%s" a))
1028                                   (car response) " "))
1029       nil)))
1030
1031 (defun nnimap-get-response (sequence)
1032   (nnimap-wait-for-response sequence)
1033   (nnimap-parse-response))
1034
1035 (defun nnimap-wait-for-connection ()
1036   (let ((process (get-buffer-process (current-buffer))))
1037     (goto-char (point-min))
1038     (while (and (memq (process-status process)
1039                       '(open run))
1040                 (not (re-search-forward "^\\* .*\n" nil t)))
1041       (nnheader-accept-process-output process)
1042       (goto-char (point-min)))
1043     (forward-line -1)
1044     (and (looking-at "\\* \\([A-Z0-9]+\\)")
1045          (match-string 1))))
1046
1047 (defun nnimap-wait-for-response (sequence &optional messagep)
1048   (let ((process (get-buffer-process (current-buffer))))
1049     (goto-char (point-max))
1050     (while (and (memq (process-status process)
1051                       '(open run))
1052                 (not (re-search-backward (format "^%d .*\n" sequence)
1053                                          (max (point-min) (- (point) 500))
1054                                          t)))
1055       (when messagep
1056         (message "Read %dKB" (/ (buffer-size) 1000)))
1057       (nnheader-accept-process-output process)
1058       (goto-char (point-max)))))
1059
1060 (defun nnimap-parse-response ()
1061   (let ((lines (split-string (nnimap-last-response-string) "\r\n" t))
1062         result)
1063     (dolist (line lines)
1064       (push (cdr (nnimap-parse-line line)) result))
1065     ;; Return the OK/error code first, and then all the "continuation
1066     ;; lines" afterwards.
1067     (cons (pop result)
1068           (nreverse result))))
1069
1070 ;; Parse an IMAP response line lightly.  They look like
1071 ;; "* OK [UIDVALIDITY 1164213559] UIDs valid", typically, so parse
1072 ;; the lines into a list of strings and lists of string.
1073 (defun nnimap-parse-line (line)
1074   (let (char result)
1075     (with-temp-buffer
1076       (insert line)
1077       (goto-char (point-min))
1078       (while (not (eobp))
1079         (if (eql (setq char (following-char)) ? )
1080             (forward-char 1)
1081           (push
1082            (cond
1083             ((eql char ?\[)
1084              (split-string (buffer-substring
1085                             (1+ (point)) (1- (search-forward "]")))))
1086             ((eql char ?\()
1087              (split-string (buffer-substring
1088                             (1+ (point)) (1- (search-forward ")")))))
1089             ((eql char ?\")
1090              (forward-char 1)
1091              (buffer-substring (point) (1- (search-forward "\""))))
1092             (t
1093              (buffer-substring (point) (if (search-forward " " nil t)
1094                                            (1- (point))
1095                                          (goto-char (point-max))))))
1096            result)))
1097       (nreverse result))))
1098
1099 (defun nnimap-last-response-string ()
1100   (save-excursion
1101     (forward-line 1)
1102     (let ((end (point)))
1103       (forward-line -1)
1104       (when (not (bobp))
1105         (forward-line -1)
1106         (while (and (not (bobp))
1107                     (eql (following-char) ?*))
1108           (forward-line -1))
1109         (unless (eql (following-char) ?*)
1110           (forward-line 1)))
1111       (buffer-substring (point) end))))
1112
1113 (defun nnimap-get-responses (sequences)
1114   (let (responses)
1115     (dolist (sequence sequences)
1116       (goto-char (point-min))
1117       (when (re-search-forward (format "^%d " sequence) nil t)
1118         (push (list sequence (nnimap-parse-response))
1119               responses)))
1120     responses))
1121
1122 (defvar nnimap-incoming-split-list nil)
1123
1124 (defun nnimap-fetch-inbox (articles)
1125   (erase-buffer)
1126   (nnimap-wait-for-response
1127    (nnimap-send-command
1128     "UID FETCH %s %s"
1129     (nnimap-article-ranges articles)
1130     (format "(UID %s%s)"
1131             (format
1132              (if (member "IMAP4REV1"
1133                          (nnimap-capabilities nnimap-object))
1134                  "BODY.PEEK[HEADER] BODY.PEEK"
1135                "RFC822.PEEK"))
1136             (if nnimap-split-download-body-default
1137                 "[]"
1138               "[1]")))
1139    t))
1140
1141 (defun nnimap-split-incoming-mail ()
1142   (with-current-buffer (nnimap-buffer)
1143     (let ((nnimap-incoming-split-list nil)
1144           (nnmail-split-methods nnimap-split-methods)
1145           (nnmail-inhibit-default-split-group t)
1146           (groups (nnimap-get-groups))
1147           new-articles)
1148       (erase-buffer)
1149       (nnimap-command "SELECT %S" nnimap-inbox)
1150       (setq new-articles (nnimap-new-articles (nnimap-get-flags "1:*")))
1151       (when new-articles
1152         (nnimap-fetch-inbox new-articles)
1153         (nnimap-transform-split-mail)
1154         (nnheader-ms-strip-cr)
1155         (nnmail-cache-open)
1156         (nnmail-split-incoming (current-buffer)
1157                                #'nnimap-save-mail-spec
1158                                nil nil
1159                                #'nnimap-dummy-active-number
1160                                #'nnimap-save-mail-spec)
1161         (when nnimap-incoming-split-list
1162           (let ((specs (nnimap-make-split-specs nnimap-incoming-split-list))
1163                 sequences junk-articles)
1164             ;; Create any groups that doesn't already exist on the
1165             ;; server first.
1166             (dolist (spec specs)
1167               (when (and (not (member (car spec) groups))
1168                          (not (eq (car spec) 'junk)))
1169                 (nnimap-command "CREATE %S" (utf7-encode (car spec) t))))
1170             ;; Then copy over all the messages.
1171             (erase-buffer)
1172             (dolist (spec specs)
1173               (let ((group (car spec))
1174                     (ranges (cdr spec)))
1175                 (if (eq group 'junk)
1176                     (setq junk-articles ranges)
1177                   (push (list (nnimap-send-command
1178                                "UID COPY %s %S"
1179                                (nnimap-article-ranges ranges)
1180                                (utf7-encode group t))
1181                               ranges)
1182                         sequences))))
1183             ;; Wait for the last COPY response...
1184             (when sequences
1185               (nnimap-wait-for-response (caar sequences))
1186               ;; And then mark the successful copy actions as deleted,
1187               ;; and possibly expunge them.
1188               (nnimap-mark-and-expunge-incoming
1189                (nnimap-parse-copied-articles sequences)))
1190             (nnimap-mark-and-expunge-incoming junk-articles)))))))
1191
1192 (defun nnimap-mark-and-expunge-incoming (range)
1193   (when range
1194     (setq range (nnimap-article-ranges range))
1195     (let ((sequence
1196            (nnimap-send-command
1197             "UID STORE %s +FLAGS.SILENT (\\Deleted)" range)))
1198       (cond
1199        ;; If the server supports it, we now delete the message we have
1200        ;; just copied over.
1201        ((member "UIDPLUS" (nnimap-capabilities nnimap-object))
1202         (setq sequence (nnimap-send-command "UID EXPUNGE %s" range)))
1203        ;; If it doesn't support UID EXPUNGE, then we only expunge if the
1204        ;; user has configured it.
1205        (nnimap-expunge
1206         (setq sequence (nnimap-send-command "EXPUNGE"))))
1207       (nnimap-wait-for-response sequence))))
1208
1209 (defun nnimap-parse-copied-articles (sequences)
1210   (let (sequence copied range)
1211     (goto-char (point-min))
1212     (while (re-search-forward "^\\([0-9]+\\) OK " nil t)
1213       (setq sequence (string-to-number (match-string 1)))
1214       (when (setq range (cadr (assq sequence sequences)))
1215         (push (gnus-uncompress-range range) copied)))
1216     (gnus-compress-sequence (sort (apply #'nconc copied) #'<))))
1217
1218 (defun nnimap-new-articles (flags)
1219   (let (new)
1220     (dolist (elem flags)
1221       (when (or (null (cdr elem))
1222                 (and (not (memq '%Deleted (cdr elem)))
1223                      (not (memq '%Seen (cdr elem)))))
1224         (push (car elem) new)))
1225     (gnus-compress-sequence (nreverse new))))
1226
1227 (defun nnimap-make-split-specs (list)
1228   (let ((specs nil)
1229         entry)
1230     (dolist (elem list)
1231       (destructuring-bind (article spec) elem
1232         (dolist (group (delete nil (mapcar #'car spec)))
1233           (unless (setq entry (assoc group specs))
1234             (push (setq entry (list group)) specs))
1235           (setcdr entry (cons article (cdr entry))))))
1236     (dolist (entry specs)
1237       (setcdr entry (gnus-compress-sequence (sort (cdr entry) #'<))))
1238     specs))
1239
1240 (defun nnimap-transform-split-mail ()
1241   (goto-char (point-min))
1242   (let (article bytes)
1243     (block nil
1244       (while (not (eobp))
1245         (while (not (looking-at "^\\* [0-9]+ FETCH.*UID \\([0-9]+\\)"))
1246           (delete-region (point) (progn (forward-line 1) (point)))
1247           (when (eobp)
1248             (return)))
1249         (setq article (match-string 1)
1250               bytes (nnimap-get-length))
1251         (delete-region (line-beginning-position) (line-end-position))
1252         ;; Insert MMDF separator, and a way to remember what this
1253         ;; article UID is.
1254         (insert (format "\^A\^A\^A\^A\n\nX-nnimap-article: %s" article))
1255         (forward-char (1+ bytes))
1256         (setq bytes (nnimap-get-length))
1257         (delete-region (line-beginning-position) (line-end-position))
1258         (forward-char (1+ bytes))
1259         (delete-region (line-beginning-position) (line-end-position))))))
1260
1261 (defun nnimap-dummy-active-number (group &optional server)
1262   1)
1263
1264 (defun nnimap-save-mail-spec (group-art &optional server full-nov)
1265   (let (article)
1266     (goto-char (point-min))
1267     (if (not (re-search-forward "X-nnimap-article: \\([0-9]+\\)" nil t))
1268         (error "Invalid nnimap mail")
1269       (setq article (string-to-number (match-string 1))))
1270     (push (list article
1271                 (if (eq group-art 'junk)
1272                     (list (cons 'junk 1))
1273                   group-art))
1274           nnimap-incoming-split-list)))
1275
1276 (provide 'nnimap)
1277
1278 ;;; nnimap.el ends here