Merge changes made in Gnus trunk.
[bpt/emacs.git] / lisp / gnus / nnimap.el
CommitLineData
20a673b2 1;;; nnimap.el --- IMAP interface for Gnus
e84b4b86 2
20a673b2 3;; Copyright (C) 2010 Free Software Foundation, Inc.
c113de23 4
20a673b2
KY
5;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6;; Simon Josefsson <simon@josefsson.org>
c113de23
GM
7
8;; This file is part of GNU Emacs.
9
5e809f55 10;; GNU Emacs is free software: you can redistribute it and/or modify
c113de23 11;; it under the terms of the GNU General Public License as published by
5e809f55
GM
12;; the Free Software Foundation, either version 3 of the License, or
13;; (at your option) any later version.
c113de23
GM
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
5e809f55 17;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
c113de23
GM
18;; GNU General Public License for more details.
19
20;; You should have received a copy of the GNU General Public License
5e809f55 21;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
c113de23
GM
22
23;;; Commentary:
24
20a673b2 25;; nnimap interfaces Gnus with IMAP servers.
c113de23
GM
26
27;;; Code:
28
f0b7f5a8
KY
29;; For Emacs <22.2 and XEmacs.
30(eval-and-compile
31 (unless (fboundp 'declare-function) (defmacro declare-function (&rest r))))
32
aa8f8277 33(eval-and-compile
20a673b2 34 (require 'nnheader))
aa8f8277 35
20a673b2
KY
36(eval-when-compile
37 (require 'cl))
c113de23 38
f58208b1
LMI
39(require 'nnheader)
40(require 'gnus-util)
41(require 'gnus)
42(require 'nnoo)
20a673b2 43(require 'netrc)
14db1c41 44(require 'utf7)
6b958814 45(require 'tls)
0617bb00 46(require 'parse-time)
e952b711 47
635be05a
KY
48(autoload 'auth-source-forget-user-or-password "auth-source")
49(autoload 'auth-source-user-or-password "auth-source")
50
c113de23
GM
51(nnoo-declare nnimap)
52
c113de23 53(defvoo nnimap-address nil
20a673b2 54 "The address of the IMAP server.")
c113de23
GM
55
56(defvoo nnimap-server-port nil
20a673b2
KY
57 "The IMAP port used.
58If nnimap-stream is `ssl', this will default to `imaps'. If not,
59it will default to `imap'.")
60
61(defvoo nnimap-stream 'ssl
62 "How nnimap will talk to the IMAP server.
b1ae92ba 63Values are `ssl', `network', `starttls' or `shell'.")
20a673b2
KY
64
65(defvoo nnimap-shell-program (if (boundp 'imap-shell-program)
66 (if (listp imap-shell-program)
67 (car imap-shell-program)
68 imap-shell-program)
69 "ssh %s imapd"))
70
71(defvoo nnimap-inbox nil
72 "The mail box where incoming mail arrives and should be split out of.")
73
8ccbef23
G
74(defvoo nnimap-split-methods nil
75 "How mail is split.
76Uses the same syntax as nnmail-split-methods")
77
6b958814
G
78(defvoo nnimap-split-fancy nil
79 "Uses the same syntax as nnmail-split-fancy.")
80
99e65b2d
G
81(defvoo nnimap-unsplittable-articles '(%Deleted %Seen)
82 "Articles with the flags in the list will not be considered when splitting.")
83
229b59da 84(make-obsolete-variable 'nnimap-split-rule "see `nnimap-split-methods'"
6b958814 85 "Emacs 24.1")
229b59da 86
bdaa75c7
LMI
87(defvoo nnimap-authenticator nil
88 "How nnimap authenticate itself to the server.
89Possible choices are nil (use default methods) or `anonymous'.")
90
b069e5a6
G
91(defvoo nnimap-expunge t
92 "If non-nil, expunge articles after deleting them.
93This is always done if the server supports UID EXPUNGE, but it's
94not done by default on servers that doesn't support that command.")
95
8ccbef23
G
96(defvoo nnimap-streaming t
97 "If non-nil, try to use streaming commands with IMAP servers.
98Switching this off will make nnimap slower, but it helps with
99some servers.")
0617bb00 100
20a673b2 101(defvoo nnimap-connection-alist nil)
286c4fc2
LMI
102
103(defvoo nnimap-current-infos nil)
104
9f2d52e7
G
105(defvoo nnimap-fetch-partial-articles nil
106 "If non-nil, Gnus will fetch partial articles.
107If t, nnimap will fetch only the first part. If a string, it
108will fetch all parts that have types that match that string. A
109likely value would be \"text/\" to automatically fetch all
110textual parts.")
111
20a673b2
KY
112(defvar nnimap-process nil)
113
114(defvar nnimap-status-string "")
23f87bed
MB
115
116(defvar nnimap-split-download-body-default nil
117 "Internal variable with default value for `nnimap-split-download-body'.")
118
61b1af82
G
119(defvar nnimap-keepalive-timer nil)
120(defvar nnimap-process-buffers nil)
121
20a673b2 122(defstruct nnimap
61b1af82 123 group process commands capabilities select-result newlinep server
4478e074 124 last-command-time greeting)
c113de23 125
20a673b2
KY
126(defvar nnimap-object nil)
127
128(defvar nnimap-mark-alist
b069e5a6
G
129 '((read "\\Seen" %Seen)
130 (tick "\\Flagged" %Flagged)
131 (reply "\\Answered" %Answered)
20a673b2
KY
132 (expire "gnus-expire")
133 (dormant "gnus-dormant")
134 (score "gnus-score")
135 (save "gnus-save")
136 (download "gnus-download")
137 (forward "gnus-forward")))
138
20a673b2
KY
139(defun nnimap-buffer ()
140 (nnimap-find-process-buffer nntp-server-buffer))
141
b5c575e6
G
142(defun nnimap-header-parameters ()
143 (format "(UID RFC822.SIZE BODYSTRUCTURE %s)"
144 (format
145 (if (nnimap-ver4-p)
146 "BODY.PEEK[HEADER.FIELDS %s]"
147 "RFC822.HEADER.LINES %s")
148 (append '(Subject From Date Message-Id
149 References In-Reply-To Xref)
150 nnmail-extra-headers))))
151
286c4fc2 152(deffoo nnimap-retrieve-headers (articles &optional group server fetch-old)
c113de23 153 (with-current-buffer nntp-server-buffer
20a673b2
KY
154 (erase-buffer)
155 (when (nnimap-possibly-change-group group server)
156 (with-current-buffer (nnimap-buffer)
20a673b2
KY
157 (erase-buffer)
158 (nnimap-wait-for-response
159 (nnimap-send-command
160 "UID FETCH %s %s"
161 (nnimap-article-ranges (gnus-compress-sequence articles))
b5c575e6 162 (nnimap-header-parameters))
20a673b2
KY
163 t)
164 (nnimap-transform-headers))
165 (insert-buffer-substring
166 (nnimap-find-process-buffer (current-buffer))))
b1ae92ba 167 'headers))
20a673b2
KY
168
169(defun nnimap-transform-headers ()
170 (goto-char (point-min))
b1ae92ba 171 (let (article bytes lines size string)
20a673b2
KY
172 (block nil
173 (while (not (eobp))
174 (while (not (looking-at "^\\* [0-9]+ FETCH.*UID \\([0-9]+\\)"))
175 (delete-region (point) (progn (forward-line 1) (point)))
176 (when (eobp)
177 (return)))
b1ae92ba
G
178 (setq article (match-string 1))
179 ;; Unfold quoted {number} strings.
b5c575e6 180 (while (re-search-forward "[^]][ (]{\\([0-9]+\\)}\r\n"
b1ae92ba
G
181 (1+ (line-end-position)) t)
182 (setq size (string-to-number (match-string 1)))
183 (delete-region (+ (match-beginning 0) 2) (point))
184 (setq string (delete-region (point) (+ (point) size)))
185 (insert (format "%S" string)))
186 (setq bytes (nnimap-get-length)
20a673b2
KY
187 lines nil)
188 (beginning-of-line)
a46359d4
LMI
189 (setq size
190 (and (re-search-forward "RFC822.SIZE \\([0-9]+\\)"
191 (line-end-position)
192 t)
193 (match-string 1)))
194 (beginning-of-line)
20a673b2 195 (when (search-forward "BODYSTRUCTURE" (line-end-position) t)
b1ae92ba
G
196 (let ((structure (ignore-errors
197 (read (current-buffer)))))
20a673b2
KY
198 (while (and (consp structure)
199 (not (stringp (car structure))))
200 (setq structure (car structure)))
201 (setq lines (nth 7 structure))))
202 (delete-region (line-beginning-position) (line-end-position))
203 (insert (format "211 %s Article retrieved." article))
204 (forward-line 1)
a46359d4
LMI
205 (when size
206 (insert (format "Chars: %s\n" size)))
20a673b2
KY
207 (when lines
208 (insert (format "Lines: %s\n" lines)))
b5c575e6
G
209 (unless (re-search-forward "^\r$" nil t)
210 (goto-char (point-max)))
20a673b2
KY
211 (delete-region (line-beginning-position) (line-end-position))
212 (insert ".")
213 (forward-line 1)))))
214
215(defun nnimap-get-length ()
216 (and (re-search-forward "{\\([0-9]+\\)}" (line-end-position) t)
217 (string-to-number (match-string 1))))
218
219(defun nnimap-article-ranges (ranges)
220 (let (result)
221 (cond
222 ((numberp ranges)
223 (number-to-string ranges))
224 ((numberp (cdr ranges))
225 (format "%d:%d" (car ranges) (cdr ranges)))
226 (t
227 (dolist (elem ranges)
228 (push
229 (if (consp elem)
230 (format "%d:%d" (car elem) (cdr elem))
231 (number-to-string elem))
232 result))
233 (mapconcat #'identity (nreverse result) ",")))))
234
286c4fc2 235(deffoo nnimap-open-server (server &optional defs)
c113de23
GM
236 (if (nnimap-server-opened server)
237 t
c113de23 238 (unless (assq 'nnimap-address defs)
20a673b2 239 (setq defs (append defs (list (list 'nnimap-address server)))))
c113de23 240 (nnoo-change-server 'nnimap server defs)
20a673b2
KY
241 (or (nnimap-find-connection nntp-server-buffer)
242 (nnimap-open-connection nntp-server-buffer))))
243
244(defun nnimap-make-process-buffer (buffer)
245 (with-current-buffer
246 (generate-new-buffer (format "*nnimap %s %s %s*"
247 nnimap-address nnimap-server-port
248 (gnus-buffer-exists-p buffer)))
249 (mm-disable-multibyte)
250 (buffer-disable-undo)
251 (gnus-add-buffer)
252 (set (make-local-variable 'after-change-functions) nil)
b069e5a6
G
253 (set (make-local-variable 'nnimap-object)
254 (make-nnimap :server (nnoo-current-server 'nnimap)))
20a673b2 255 (push (list buffer (current-buffer)) nnimap-connection-alist)
61b1af82 256 (push (current-buffer) nnimap-process-buffers)
20a673b2
KY
257 (current-buffer)))
258
259(defun nnimap-open-shell-stream (name buffer host port)
e8861cd2
LMI
260 (let ((process-connection-type nil))
261 (start-process name buffer shell-file-name
262 shell-command-switch
263 (format-spec
264 nnimap-shell-program
265 (format-spec-make
266 ?s host
267 ?p port)))))
20a673b2 268
2696d88f 269(defun nnimap-credentials (address ports &optional inhibit-create)
286c4fc2
LMI
270 (let (port credentials)
271 ;; Request the credentials from all ports, but only query on the
272 ;; last port if all the previous ones have failed.
273 (while (and (null credentials)
274 (setq port (pop ports)))
275 (setq credentials
276 (auth-source-user-or-password
2696d88f
G
277 '("login" "password") address port nil
278 (if inhibit-create
279 nil
280 (null ports)))))
286c4fc2
LMI
281 credentials))
282
61b1af82
G
283(defun nnimap-keepalive ()
284 (let ((now (current-time)))
285 (dolist (buffer nnimap-process-buffers)
286 (when (buffer-name buffer)
287 (with-current-buffer buffer
288 (when (and nnimap-object
289 (nnimap-last-command-time nnimap-object)
290 (> (time-to-seconds
291 (time-subtract
292 now
293 (nnimap-last-command-time nnimap-object)))
294 ;; More than five minutes since the last command.
295 (* 5 60)))
296 (nnimap-send-command "NOOP")))))))
297
f0b7f5a8
KY
298(declare-function gnutls-negotiate "subr" (fn file &optional arglist fileonly))
299
20a673b2 300(defun nnimap-open-connection (buffer)
61b1af82
G
301 (unless nnimap-keepalive-timer
302 (setq nnimap-keepalive-timer (run-at-time (* 60 15) (* 60 15)
303 'nnimap-keepalive)))
9f2d52e7
G
304 (block nil
305 (with-current-buffer (nnimap-make-process-buffer buffer)
306 (let* ((coding-system-for-read 'binary)
307 (coding-system-for-write 'binary)
308 (port nil)
309 (ports
310 (cond
dab0271f
G
311 ((or (eq nnimap-stream 'network)
312 (and (eq nnimap-stream 'starttls)
313 (fboundp 'open-gnutls-stream)))
283f7b93
G
314 (nnheader-message 7 "Opening connection to %s..."
315 nnimap-address)
9f2d52e7
G
316 (open-network-stream
317 "*nnimap*" (current-buffer) nnimap-address
318 (setq port
319 (or nnimap-server-port
320 (if (netrc-find-service-number "imap")
321 "imap"
322 "143"))))
323 '("143" "imap"))
324 ((eq nnimap-stream 'shell)
283f7b93
G
325 (nnheader-message 7 "Opening connection to %s via shell..."
326 nnimap-address)
9f2d52e7
G
327 (nnimap-open-shell-stream
328 "*nnimap*" (current-buffer) nnimap-address
329 (setq port (or nnimap-server-port "imap")))
330 '("imap"))
331 ((eq nnimap-stream 'starttls)
283f7b93 332 (nnheader-message 7 "Opening connection to %s via starttls..."
030158f3 333 nnimap-address)
0d2d1bdc 334 (let ((tls-program
030158f3 335 '("openssl s_client -connect %h:%p -no_ssl2 -ign_eof -starttls imap")))
6b958814
G
336 (open-tls-stream
337 "*nnimap*" (current-buffer) nnimap-address
a1d16a7b 338 (setq port (or nnimap-server-port "imap"))))
9f2d52e7 339 '("imap"))
71e691a5 340 ((memq nnimap-stream '(ssl tls))
283f7b93
G
341 (nnheader-message 7 "Opening connection to %s via tls..."
342 nnimap-address)
f7aa248a 343 (funcall (if (fboundp 'open-gnutls-stream)
66627fa9
G
344 'open-gnutls-stream
345 'open-tls-stream)
346 "*nnimap*" (current-buffer) nnimap-address
347 (setq port
348 (or nnimap-server-port
349 (if (netrc-find-service-number "imaps")
350 "imaps"
351 "993"))))
71e691a5
G
352 '("143" "993" "imap" "imaps"))
353 (t
354 (error "Unknown stream type: %s" nnimap-stream))))
9f2d52e7
G
355 connection-result login-result credentials)
356 (setf (nnimap-process nnimap-object)
357 (get-buffer-process (current-buffer)))
358 (if (not (and (nnimap-process nnimap-object)
359 (memq (process-status (nnimap-process nnimap-object))
360 '(open run))))
361 (nnheader-report 'nnimap "Unable to contact %s:%s via %s"
362 nnimap-address port nnimap-stream)
f7aa248a
G
363 (gnus-set-process-query-on-exit-flag
364 (nnimap-process nnimap-object) nil)
9f2d52e7
G
365 (if (not (setq connection-result (nnimap-wait-for-connection)))
366 (nnheader-report 'nnimap
367 "%s" (buffer-substring
368 (point) (line-end-position)))
369 ;; Store the greeting (for debugging purposes).
370 (setf (nnimap-greeting nnimap-object)
371 (buffer-substring (line-beginning-position)
372 (line-end-position)))
373 ;; Store the capabilities.
20a673b2
KY
374 (setf (nnimap-capabilities nnimap-object)
375 (mapcar
376 #'upcase
9f2d52e7
G
377 (nnimap-find-parameter
378 "CAPABILITY" (cdr (nnimap-command "CAPABILITY")))))
9f2d52e7
G
379 (when nnimap-server-port
380 (push (format "%s" nnimap-server-port) ports))
6b958814
G
381 ;; If this is a STARTTLS-capable server, then sever the
382 ;; connection and start a STARTTLS connection instead.
dab0271f
G
383 (cond
384 ((and (or (and (eq nnimap-stream 'network)
389b76fa 385 (nnimap-capability "STARTTLS"))
dab0271f
G
386 (eq nnimap-stream 'starttls))
387 (fboundp 'open-gnutls-stream))
388 (nnimap-command "STARTTLS")
389 (gnutls-negotiate (nnimap-process nnimap-object) nil))
390 ((and (eq nnimap-stream 'network)
389b76fa 391 (nnimap-capability "STARTTLS"))
6b958814
G
392 (let ((nnimap-stream 'starttls))
393 (let ((tls-process
394 (nnimap-open-connection buffer)))
395 ;; If the STARTTLS connection was successful, we
396 ;; kill our first non-encrypted connection. If it
397 ;; wasn't successful, we just use our unencrypted
398 ;; connection.
399 (when (memq (process-status tls-process) '(open run))
400 (delete-process (nnimap-process nnimap-object))
401 (kill-buffer (current-buffer))
dab0271f 402 (return tls-process))))))
9f2d52e7
G
403 (unless (equal connection-result "PREAUTH")
404 (if (not (setq credentials
405 (if (eq nnimap-authenticator 'anonymous)
406 (list "anonymous"
407 (message-make-address))
408 (or
409 ;; First look for the credentials based
410 ;; on the virtual server name.
411 (nnimap-credentials
412 (nnoo-current-server 'nnimap) ports t)
413 ;; Then look them up based on the
414 ;; physical address.
415 (nnimap-credentials nnimap-address ports)))))
416 (setq nnimap-object nil)
99e65b2d 417 (setq login-result
389b76fa
G
418 (if (and (nnimap-capability "AUTH=PLAIN")
419 (nnimap-capability "LOGINDISABLED"))
99e65b2d
G
420 (nnimap-command
421 "AUTHENTICATE PLAIN %s"
422 (base64-encode-string
423 (format "\000%s\000%s"
424 (nnimap-quote-specials (car credentials))
425 (nnimap-quote-specials (cadr credentials)))))
426 (nnimap-command "LOGIN %S %S"
427 (car credentials)
428 (cadr credentials))))
9f2d52e7
G
429 (unless (car login-result)
430 ;; If the login failed, then forget the credentials
431 ;; that are now possibly cached.
432 (dolist (host (list (nnoo-current-server 'nnimap)
433 nnimap-address))
434 (dolist (port ports)
435 (dolist (element '("login" "password"))
436 (auth-source-forget-user-or-password
437 element host port))))
438 (delete-process (nnimap-process nnimap-object))
439 (setq nnimap-object nil))))
440 (when nnimap-object
389b76fa 441 (when (nnimap-capability "QRESYNC")
9f2d52e7 442 (nnimap-command "ENABLE QRESYNC"))
6b958814
G
443 (nnimap-process nnimap-object))))))))
444
99e65b2d
G
445(defun nnimap-quote-specials (string)
446 (with-temp-buffer
447 (insert string)
448 (goto-char (point-min))
449 (while (re-search-forward "[\\\"]" nil t)
450 (forward-char -1)
451 (insert "\\")
452 (forward-char 1))
453 (buffer-string)))
454
20a673b2
KY
455(defun nnimap-find-parameter (parameter elems)
456 (let (result)
457 (dolist (elem elems)
458 (cond
459 ((equal (car elem) parameter)
460 (setq result (cdr elem)))
461 ((and (equal (car elem) "OK")
462 (consp (cadr elem))
463 (equal (caadr elem) parameter))
464 (setq result (cdr (cadr elem))))))
465 result))
466
286c4fc2 467(deffoo nnimap-close-server (&optional server)
71e691a5
G
468 (when (nnoo-change-server 'nnimap server nil)
469 (ignore-errors
470 (delete-process (get-buffer-process (nnimap-buffer))))
d1090fe8 471 (nnoo-close-server 'nnimap server)
71e691a5 472 t))
c113de23 473
286c4fc2 474(deffoo nnimap-request-close ()
20a673b2 475 t)
23f87bed 476
286c4fc2 477(deffoo nnimap-server-opened (&optional server)
20a673b2
KY
478 (and (nnoo-current-server-p 'nnimap server)
479 nntp-server-buffer
480 (gnus-buffer-live-p nntp-server-buffer)
481 (nnimap-find-connection nntp-server-buffer)))
c113de23 482
286c4fc2 483(deffoo nnimap-status-message (&optional server)
20a673b2 484 nnimap-status-string)
c113de23 485
286c4fc2 486(deffoo nnimap-request-article (article &optional group server to-buffer)
c113de23 487 (with-current-buffer nntp-server-buffer
bdaa75c7 488 (let ((result (nnimap-possibly-change-group group server))
8ccbef23 489 parts structure)
20a673b2
KY
490 (when (stringp article)
491 (setq article (nnimap-find-article-by-message-id group article)))
492 (when (and result
493 article)
494 (erase-buffer)
495 (with-current-buffer (nnimap-buffer)
496 (erase-buffer)
9f2d52e7
G
497 (when nnimap-fetch-partial-articles
498 (nnimap-command "UID FETCH %d (BODYSTRUCTURE)" article)
499 (goto-char (point-min))
500 (when (re-search-forward "FETCH.*BODYSTRUCTURE" nil t)
229b59da
G
501 (setq structure (ignore-errors
502 (let ((start (point)))
503 (forward-sexp 1)
504 (downcase-region start (point))
9d1bf25d 505 (goto-char start)
229b59da 506 (read (current-buffer))))
9f2d52e7 507 parts (nnimap-find-wanted-parts structure))))
8ccbef23
G
508 (when (if parts
509 (nnimap-get-partial-article article parts structure)
510 (nnimap-get-whole-article article))
511 (let ((buffer (current-buffer)))
512 (with-current-buffer (or to-buffer nntp-server-buffer)
513 (erase-buffer)
514 (insert-buffer-substring buffer)
515 (nnheader-ms-strip-cr)
516 (cons group article)))))))))
517
b5c575e6
G
518(deffoo nnimap-request-head (article &optional group server to-buffer)
519 (when (nnimap-possibly-change-group group server)
520 (with-current-buffer (nnimap-buffer)
521 (when (stringp article)
522 (setq article (nnimap-find-article-by-message-id group article)))
523 (nnimap-get-whole-article
524 article (format "UID FETCH %%d %s"
525 (nnimap-header-parameters)))
526 (let ((buffer (current-buffer)))
527 (with-current-buffer (or to-buffer nntp-server-buffer)
528 (erase-buffer)
529 (insert-buffer-substring buffer)
530 (nnheader-ms-strip-cr)
531 (cons group article))))))
532
533(defun nnimap-get-whole-article (article &optional command)
8ccbef23
G
534 (let ((result
535 (nnimap-command
b5c575e6
G
536 (or command
537 (if (nnimap-ver4-p)
538 "UID FETCH %d BODY.PEEK[]"
539 "UID FETCH %d RFC822.PEEK"))
8ccbef23
G
540 article)))
541 ;; Check that we really got an article.
542 (goto-char (point-min))
4478e074 543 (unless (re-search-forward "\\* [0-9]+ FETCH" nil t)
8ccbef23
G
544 (setq result nil))
545 (when result
4478e074
G
546 ;; Remove any data that may have arrived before the FETCH data.
547 (beginning-of-line)
548 (unless (bobp)
549 (delete-region (point-min) (point)))
8ccbef23
G
550 (let ((bytes (nnimap-get-length)))
551 (delete-region (line-beginning-position)
552 (progn (forward-line 1) (point)))
553 (goto-char (+ (point) bytes))
554 (delete-region (point) (point-max)))
555 t)))
556
389b76fa
G
557(defun nnimap-capability (capability)
558 (member capability (nnimap-capabilities nnimap-object)))
559
8ccbef23 560(defun nnimap-ver4-p ()
389b76fa 561 (nnimap-capability "IMAP4REV1"))
8ccbef23
G
562
563(defun nnimap-get-partial-article (article parts structure)
564 (let ((result
565 (nnimap-command
566 "UID FETCH %d (%s %s)"
567 article
568 (if (nnimap-ver4-p)
569 "BODY.PEEK[HEADER]"
570 "RFC822.HEADER")
571 (if (nnimap-ver4-p)
572 (mapconcat (lambda (part)
573 (format "BODY.PEEK[%s]" part))
574 parts " ")
575 (mapconcat (lambda (part)
576 (format "RFC822.PEEK[%s]" part))
577 parts " ")))))
578 (when result
579 (nnimap-convert-partial-article structure))))
580
581(defun nnimap-convert-partial-article (structure)
582 ;; First just skip past the headers.
583 (goto-char (point-min))
584 (let ((bytes (nnimap-get-length))
585 id parts)
586 ;; Delete "FETCH" line.
587 (delete-region (line-beginning-position)
588 (progn (forward-line 1) (point)))
589 (goto-char (+ (point) bytes))
590 ;; Collect all the body parts.
591 (while (looking-at ".*BODY\\[\\([.0-9]+\\)\\]")
592 (setq id (match-string 1)
593 bytes (nnimap-get-length))
594 (beginning-of-line)
595 (delete-region (point) (progn (forward-line 1) (point)))
596 (push (list id (buffer-substring (point) (+ (point) bytes)))
597 parts)
598 (delete-region (point) (+ (point) bytes)))
599 ;; Delete trailing junk.
600 (delete-region (point) (point-max))
601 ;; Now insert all the parts again where they fit in the structure.
602 (nnimap-insert-partial-structure structure parts)
603 t))
604
605(defun nnimap-insert-partial-structure (structure parts &optional subp)
229b59da
G
606 (let (type boundary)
607 (let ((bstruc structure))
608 (while (consp (car bstruc))
609 (pop bstruc))
610 (setq type (car bstruc))
611 (setq bstruc (car (cdr bstruc)))
2526f423
G
612 (let ((has-boundary (member "boundary" bstruc)))
613 (when has-boundary
614 (setq boundary (cadr has-boundary)))))
8ccbef23
G
615 (when subp
616 (insert (format "Content-type: multipart/%s; boundary=%S\n\n"
617 (downcase type) boundary)))
618 (while (not (stringp (car structure)))
619 (insert "\n--" boundary "\n")
620 (if (consp (caar structure))
621 (nnimap-insert-partial-structure (pop structure) parts t)
622 (let ((bit (pop structure)))
623 (insert (format "Content-type: %s/%s"
624 (downcase (nth 0 bit))
625 (downcase (nth 1 bit))))
626 (if (member "CHARSET" (nth 2 bit))
627 (insert (format
628 "; charset=%S\n" (cadr (member "CHARSET" (nth 2 bit)))))
629 (insert "\n"))
630 (insert (format "Content-transfer-encoding: %s\n"
631 (nth 5 bit)))
632 (insert "\n")
633 (when (assoc (nth 9 bit) parts)
634 (insert (cadr (assoc (nth 9 bit) parts)))))))
635 (insert "\n--" boundary "--\n")))
bdaa75c7
LMI
636
637(defun nnimap-find-wanted-parts (structure)
638 (message-flatten-list (nnimap-find-wanted-parts-1 structure "")))
639
640(defun nnimap-find-wanted-parts-1 (structure prefix)
641 (let ((num 1)
642 parts)
643 (while (consp (car structure))
644 (let ((sub (pop structure)))
645 (if (consp (car sub))
646 (push (nnimap-find-wanted-parts-1
647 sub (if (string= prefix "")
648 (number-to-string num)
649 (format "%s.%s" prefix num)))
650 parts)
8ccbef23
G
651 (let ((type (format "%s/%s" (nth 0 sub) (nth 1 sub)))
652 (id (if (string= prefix "")
bdaa75c7 653 (number-to-string num)
8ccbef23
G
654 (format "%s.%s" prefix num))))
655 (setcar (nthcdr 9 sub) id)
9f2d52e7
G
656 (when (if (eq nnimap-fetch-partial-articles t)
657 (equal id "1")
658 (string-match nnimap-fetch-partial-articles type))
8ccbef23
G
659 (push id parts))))
660 (incf num)))
bdaa75c7 661 (nreverse parts)))
20a673b2 662
286c4fc2 663(deffoo nnimap-request-group (group &optional server dont-check info)
7cad71ad
G
664 (let ((result (nnimap-possibly-change-group
665 ;; Don't SELECT the group if we're going to select it
666 ;; later, anyway.
667 (if dont-check
668 nil
669 group)
670 server))
a46359d4
LMI
671 articles active marks high low)
672 (with-current-buffer nntp-server-buffer
20a673b2 673 (when result
286c4fc2
LMI
674 (if (and dont-check
675 (setq active (nth 2 (assoc group nnimap-current-infos))))
676 (insert (format "211 %d %d %d %S\n"
677 (- (cdr active) (car active))
678 (car active)
679 (cdr active)
680 group))
681 (with-current-buffer (nnimap-buffer)
682 (erase-buffer)
683 (let ((group-sequence
0617bb00 684 (nnimap-send-command "SELECT %S" (utf7-encode group t)))
286c4fc2
LMI
685 (flag-sequence
686 (nnimap-send-command "UID FETCH 1:* FLAGS")))
7cad71ad 687 (setf (nnimap-group nnimap-object) group)
286c4fc2
LMI
688 (nnimap-wait-for-response flag-sequence)
689 (setq marks
690 (nnimap-flags-to-marks
691 (nnimap-parse-flags
f7aa248a
G
692 (list (list group-sequence flag-sequence
693 1 group "SELECT")))))
694 (when (and info
695 marks)
286c4fc2
LMI
696 (nnimap-update-infos marks (list info)))
697 (goto-char (point-max))
b1ae92ba 698 (let ((uidnext (nth 5 (car marks))))
a3f57c41
G
699 (setq high (or (if uidnext
700 (1- uidnext)
701 (nth 3 (car marks)))
702 0)
703 low (or (nth 4 (car marks)) uidnext 1)))))
286c4fc2
LMI
704 (erase-buffer)
705 (insert
706 (format
0617bb00
LMI
707 "211 %d %d %d %S\n" (1+ (- high low)) low high group)))
708 t))))
709
710(deffoo nnimap-request-create-group (group &optional server args)
711 (when (nnimap-possibly-change-group nil server)
712 (with-current-buffer (nnimap-buffer)
713 (car (nnimap-command "CREATE %S" (utf7-encode group t))))))
c113de23 714
a46359d4
LMI
715(deffoo nnimap-request-delete-group (group &optional force server)
716 (when (nnimap-possibly-change-group nil server)
717 (with-current-buffer (nnimap-buffer)
0617bb00
LMI
718 (car (nnimap-command "DELETE %S" (utf7-encode group t))))))
719
a7dcc87b
G
720(deffoo nnimap-request-rename-group (group new-name &optional server)
721 (when (nnimap-possibly-change-group nil server)
722 (with-current-buffer (nnimap-buffer)
ab67634f
G
723 ;; Make sure we don't have this group open read/write by asking
724 ;; to examine a mailbox that doesn't exist. This seems to be
725 ;; the only way that allows us to reliably go back to unselected
726 ;; state on Courier.
727 (nnimap-command "EXAMINE DOES.NOT.EXIST")
6b7df8d3 728 (setf (nnimap-group nnimap-object) nil)
f7aa248a
G
729 (car (nnimap-command "RENAME %S %S"
730 (utf7-encode group t) (utf7-encode new-name t))))))
a7dcc87b 731
0617bb00
LMI
732(deffoo nnimap-request-expunge-group (group &optional server)
733 (when (nnimap-possibly-change-group group server)
734 (with-current-buffer (nnimap-buffer)
735 (car (nnimap-command "EXPUNGE")))))
a46359d4 736
20a673b2
KY
737(defun nnimap-get-flags (spec)
738 (let ((articles nil)
f7aa248a 739 elems end)
20a673b2 740 (with-current-buffer (nnimap-buffer)
c113de23 741 (erase-buffer)
20a673b2
KY
742 (nnimap-wait-for-response (nnimap-send-command
743 "UID FETCH %s FLAGS" spec))
f7aa248a
G
744 (setq end (point))
745 (subst-char-in-region (point-min) (point-max)
746 ?\\ ?% t)
20a673b2 747 (goto-char (point-min))
f7aa248a
G
748 (while (search-forward " FETCH " end t)
749 (setq elems (read (current-buffer)))
750 (push (cons (cadr (memq 'UID elems))
751 (cadr (memq 'FLAGS elems)))
20a673b2
KY
752 articles)))
753 (nreverse articles)))
a1506d29 754
286c4fc2 755(deffoo nnimap-close-group (group &optional server)
20a673b2 756 t)
c113de23 757
01c52d31 758(deffoo nnimap-request-move-article (article group server accept-form
20a673b2 759 &optional last internal-move-group)
0617bb00 760 (with-temp-buffer
a04f9e26 761 (mm-disable-multibyte)
b5c575e6
G
762 (when (funcall (if internal-move-group
763 'nnimap-request-head
764 'nnimap-request-article)
765 article group server (current-buffer))
0617bb00
LMI
766 ;; If the move is internal (on the same server), just do it the easy
767 ;; way.
768 (let ((message-id (message-field-value "message-id")))
769 (if internal-move-group
770 (let ((result
771 (with-current-buffer (nnimap-buffer)
772 (nnimap-command "UID COPY %d %S"
773 article
774 (utf7-encode internal-move-group t)))))
775 (when (car result)
a46359d4 776 (nnimap-delete-article article)
0617bb00
LMI
777 (cons internal-move-group
778 (nnimap-find-article-by-message-id
779 internal-move-group message-id))))
780 ;; Move the article to a different method.
781 (let ((result (eval accept-form)))
782 (when result
783 (nnimap-delete-article article)
784 result)))))))
20a673b2
KY
785
786(deffoo nnimap-request-expire-articles (articles group &optional server force)
787 (cond
0617bb00
LMI
788 ((null articles)
789 nil)
20a673b2
KY
790 ((not (nnimap-possibly-change-group group server))
791 articles)
0617bb00
LMI
792 ((and force
793 (eq nnmail-expiry-target 'delete))
4478e074 794 (unless (nnimap-delete-article (gnus-compress-sequence articles))
283f7b93 795 (nnheader-message 7 "Article marked for deletion, but not expunged."))
20a673b2
KY
796 nil)
797 (t
0617bb00 798 (let ((deletable-articles
b069e5a6
G
799 (if (or force
800 (eq nnmail-expiry-wait 'immediate))
0617bb00
LMI
801 articles
802 (gnus-sorted-intersection
803 articles
804 (nnimap-find-expired-articles group)))))
805 (if (null deletable-articles)
806 articles
807 (if (eq nnmail-expiry-target 'delete)
4478e074 808 (nnimap-delete-article (gnus-compress-sequence deletable-articles))
0617bb00
LMI
809 (setq deletable-articles
810 (nnimap-process-expiry-targets
811 deletable-articles group server)))
812 ;; Return the articles we didn't delete.
813 (gnus-sorted-complement articles deletable-articles))))))
814
815(defun nnimap-process-expiry-targets (articles group server)
816 (let ((deleted-articles nil))
817 (dolist (article articles)
818 (let ((target nnmail-expiry-target))
819 (with-temp-buffer
a04f9e26 820 (mm-disable-multibyte)
0617bb00 821 (when (nnimap-request-article article group server (current-buffer))
283f7b93 822 (nnheader-message 7 "Expiring article %s:%d" group article)
0617bb00
LMI
823 (when (functionp target)
824 (setq target (funcall target group)))
825 (when (and target
826 (not (eq target 'delete)))
827 (if (or (gnus-request-group target t)
828 (gnus-request-create-group target))
829 (nnmail-expiry-target-group target group)
830 (setq target nil)))
831 (when target
832 (push article deleted-articles))))))
833 ;; Change back to the current group again.
834 (nnimap-possibly-change-group group server)
835 (setq deleted-articles (nreverse deleted-articles))
4478e074 836 (nnimap-delete-article (gnus-compress-sequence deleted-articles))
0617bb00
LMI
837 deleted-articles))
838
839(defun nnimap-find-expired-articles (group)
840 (let ((cutoff (nnmail-expired-article-p group nil nil)))
841 (with-current-buffer (nnimap-buffer)
842 (let ((result
843 (nnimap-command
844 "UID SEARCH SENTBEFORE %s"
845 (format-time-string
846 (format "%%d-%s-%%Y"
847 (upcase
848 (car (rassoc (nth 4 (decode-time cutoff))
849 parse-time-months))))
850 cutoff))))
851 (and (car result)
852 (delete 0 (mapcar #'string-to-number
853 (cdr (assoc "SEARCH" (cdr result))))))))))
854
20a673b2
KY
855
856(defun nnimap-find-article-by-message-id (group message-id)
6b958814
G
857 (with-current-buffer (nnimap-buffer)
858 (erase-buffer)
859 (setf (nnimap-group nnimap-object) nil)
860 (nnimap-send-command "EXAMINE %S" (utf7-encode group t))
861 (let ((sequence
862 (nnimap-send-command "UID SEARCH HEADER Message-Id %S" message-id))
863 article result)
864 (setq result (nnimap-wait-for-response sequence))
865 (when (and result
866 (car (setq result (nnimap-parse-response))))
867 ;; Select the last instance of the message in the group.
868 (and (setq article
869 (car (last (assoc "SEARCH" (cdr result)))))
870 (string-to-number article))))))
20a673b2
KY
871
872(defun nnimap-delete-article (articles)
873 (with-current-buffer (nnimap-buffer)
874 (nnimap-command "UID STORE %s +FLAGS.SILENT (\\Deleted)"
875 (nnimap-article-ranges articles))
0617bb00 876 (cond
389b76fa 877 ((nnimap-capability "UIDPLUS")
0617bb00
LMI
878 (nnimap-command "UID EXPUNGE %s"
879 (nnimap-article-ranges articles))
880 t)
881 (nnimap-expunge
882 (nnimap-command "EXPUNGE")
7390c1cd
TZ
883 t)
884 (t (gnus-message 7 (concat "nnimap: nnimap-expunge is not set and the "
885 "server doesn't support UIDPLUS, so we won't "
886 "delete this article now"))))))
20a673b2
KY
887
888(deffoo nnimap-request-scan (&optional group server)
889 (when (and (nnimap-possibly-change-group nil server)
20a673b2
KY
890 nnimap-inbox
891 nnimap-split-methods)
283f7b93 892 (nnheader-message 7 "nnimap %s splitting mail..." server)
20a673b2
KY
893 (nnimap-split-incoming-mail)))
894
895(defun nnimap-marks-to-flags (marks)
896 (let (flags flag)
897 (dolist (mark marks)
898 (when (setq flag (cadr (assq mark nnimap-mark-alist)))
899 (push flag flags)))
900 flags))
901
286c4fc2 902(deffoo nnimap-request-set-mark (group actions &optional server)
20a673b2
KY
903 (when (nnimap-possibly-change-group group server)
904 (let (sequence)
905 (with-current-buffer (nnimap-buffer)
229b59da 906 (erase-buffer)
20a673b2
KY
907 ;; Just send all the STORE commands without waiting for
908 ;; response. If they're successful, they're successful.
909 (dolist (action actions)
910 (destructuring-bind (range action marks) action
911 (let ((flags (nnimap-marks-to-flags marks)))
912 (when flags
913 (setq sequence (nnimap-send-command
914 "UID STORE %s %sFLAGS.SILENT (%s)"
915 (nnimap-article-ranges range)
916 (if (eq action 'del)
917 "-"
918 "+")
919 (mapconcat #'identity flags " ")))))))
920 ;; Wait for the last command to complete to avoid later
921 ;; syncronisation problems with the stream.
a46359d4
LMI
922 (when sequence
923 (nnimap-wait-for-response sequence))))))
a1506d29 924
c113de23 925(deffoo nnimap-request-accept-article (group &optional server last)
20a673b2
KY
926 (when (nnimap-possibly-change-group nil server)
927 (nnmail-check-syntax)
6b958814
G
928 (let ((message-id (message-field-value "message-id"))
929 sequence message)
930 (nnimap-add-cr)
728fd3b9 931 (setq message (buffer-substring-no-properties (point-min) (point-max)))
20a673b2 932 (with-current-buffer (nnimap-buffer)
389b76fa 933 (erase-buffer)
20a673b2
KY
934 (setq sequence (nnimap-send-command
935 "APPEND %S {%d}" (utf7-encode group t)
936 (length message)))
389b76fa
G
937 (unless nnimap-streaming
938 (nnimap-wait-for-connection "^[+]"))
20a673b2 939 (process-send-string (get-buffer-process (current-buffer)) message)
286c4fc2
LMI
940 (process-send-string (get-buffer-process (current-buffer))
941 (if (nnimap-newlinep nnimap-object)
942 "\n"
943 "\r\n"))
20a673b2 944 (let ((result (nnimap-get-response sequence)))
062eae99
G
945 (if (not (car result))
946 (progn
283f7b93 947 (nnheader-message 7 "%s" (nnheader-get-report-string 'nnimap))
062eae99 948 nil)
20a673b2
KY
949 (cons group
950 (nnimap-find-article-by-message-id group message-id))))))))
951
728fd3b9
LMI
952(deffoo nnimap-request-replace-article (article group buffer)
953 (let (group-art)
954 (when (and (nnimap-possibly-change-group group nil)
955 ;; Put the article into the group.
956 (with-current-buffer buffer
957 (setq group-art
958 (nnimap-request-accept-article group nil t))))
959 (nnimap-delete-article (list article))
960 ;; Return the new article number.
961 (cdr group-art))))
962
20a673b2
KY
963(defun nnimap-add-cr ()
964 (goto-char (point-min))
965 (while (re-search-forward "\r?\n" nil t)
966 (replace-match "\r\n" t t)))
967
968(defun nnimap-get-groups ()
969 (let ((result (nnimap-command "LIST \"\" \"*\""))
970 groups)
971 (when (car result)
972 (dolist (line (cdr result))
973 (when (and (equal (car line) "LIST")
974 (not (and (caadr line)
975 (string-match "noselect" (caadr line)))))
976 (push (car (last line)) groups)))
977 (nreverse groups))))
978
286c4fc2 979(deffoo nnimap-request-list (&optional server)
20a673b2
KY
980 (nnimap-possibly-change-group nil server)
981 (with-current-buffer nntp-server-buffer
982 (erase-buffer)
983 (let ((groups
984 (with-current-buffer (nnimap-buffer)
985 (nnimap-get-groups)))
986 sequences responses)
987 (when groups
988 (with-current-buffer (nnimap-buffer)
b069e5a6 989 (setf (nnimap-group nnimap-object) nil)
20a673b2
KY
990 (dolist (group groups)
991 (push (list (nnimap-send-command "EXAMINE %S" (utf7-encode group t))
992 group)
993 sequences))
994 (nnimap-wait-for-response (caar sequences))
995 (setq responses
996 (nnimap-get-responses (mapcar #'car sequences))))
997 (dolist (response responses)
998 (let* ((sequence (car response))
999 (response (cadr response))
1000 (group (cadr (assoc sequence sequences))))
1001 (when (and group
1002 (equal (caar response) "OK"))
1003 (let ((uidnext (nnimap-find-parameter "UIDNEXT" response))
1004 highest exists)
1005 (dolist (elem response)
1006 (when (equal (cadr elem) "EXISTS")
1007 (setq exists (string-to-number (car elem)))))
1008 (when uidnext
1009 (setq highest (1- (string-to-number (car uidnext)))))
1010 (cond
1011 ((null highest)
1012 (insert (format "%S 0 1 y\n" (utf7-decode group t))))
1013 ((zerop exists)
1014 ;; Empty group.
1015 (insert (format "%S %d %d y\n"
1016 (utf7-decode group t) highest (1+ highest))))
1017 (t
1018 ;; Return the widest possible range.
1019 (insert (format "%S %d 1 y\n" (utf7-decode group t)
1020 (or highest exists)))))))))
c113de23
GM
1021 t))))
1022
a3f57c41
G
1023(deffoo nnimap-request-newgroups (date &optional server)
1024 (nnimap-possibly-change-group nil server)
1025 (with-current-buffer nntp-server-buffer
1026 (erase-buffer)
1027 (dolist (group (with-current-buffer (nnimap-buffer)
1028 (nnimap-get-groups)))
1029 (unless (assoc group nnimap-current-infos)
1030 ;; Insert dummy numbers here -- they don't matter.
130e977f
LMI
1031 (insert (format "%S 0 1 y\n" group))))
1032 t))
a3f57c41 1033
286c4fc2 1034(deffoo nnimap-retrieve-group-data-early (server infos)
20a673b2
KY
1035 (when (nnimap-possibly-change-group nil server)
1036 (with-current-buffer (nnimap-buffer)
f7aa248a
G
1037 (erase-buffer)
1038 (setf (nnimap-group nnimap-object) nil)
389b76fa 1039 (let ((qresyncp (nnimap-capability "QRESYNC"))
f7aa248a 1040 params groups sequences active uidvalidity modseq group)
20a673b2
KY
1041 ;; Go through the infos and gather the data needed to know
1042 ;; what and how to request the data.
1043 (dolist (info infos)
f7aa248a
G
1044 (setq params (gnus-info-params info)
1045 group (gnus-group-real-name (gnus-info-group info))
1046 active (cdr (assq 'active params))
1047 uidvalidity (cdr (assq 'uidvalidity params))
1048 modseq (cdr (assq 'modseq params)))
20a673b2 1049 (if (and qresyncp
f7aa248a
G
1050 uidvalidity
1051 modseq)
20a673b2 1052 (push
f7aa248a 1053 (list (nnimap-send-command "EXAMINE %S (QRESYNC (%s %s))"
dab0271f
G
1054 (utf7-encode group t)
1055 uidvalidity modseq)
f7aa248a
G
1056 'qresync
1057 nil group 'qresync)
20a673b2
KY
1058 sequences)
1059 (let ((start
f7aa248a 1060 (if (and active uidvalidity)
20a673b2 1061 ;; Fetch the last 100 flags.
f7aa248a
G
1062 (max 1 (- (cdr active) 100))
1063 1))
1064 (command
1065 (if uidvalidity
1066 "EXAMINE"
1067 ;; If we don't have a UIDVALIDITY, then this is
1068 ;; the first time we've seen the group, so we
1069 ;; have to do a SELECT (which is slower than an
1070 ;; examine), but will tell us whether the group
1071 ;; is read-only or not.
1072 "SELECT")))
dab0271f
G
1073 (push (list (nnimap-send-command "%s %S" command
1074 (utf7-encode group t))
20a673b2 1075 (nnimap-send-command "UID FETCH %d:* FLAGS" start)
f7aa248a 1076 start group command)
b5c575e6 1077 sequences))))
20a673b2
KY
1078 sequences))))
1079
286c4fc2 1080(deffoo nnimap-finish-retrieve-group-infos (server infos sequences)
20a673b2
KY
1081 (when (and sequences
1082 (nnimap-possibly-change-group nil server))
1083 (with-current-buffer (nnimap-buffer)
1084 ;; Wait for the final data to trickle in.
f7aa248a
G
1085 (when (nnimap-wait-for-response (if (eq (cadar sequences) 'qresync)
1086 (caar sequences)
1087 (cadar sequences))
1088 t)
1089 ;; Now we should have most of the data we need, no matter
1090 ;; whether we're QRESYNCING, fetching all the flags from
1091 ;; scratch, or just fetching the last 100 flags per group.
8ccbef23
G
1092 (nnimap-update-infos (nnimap-flags-to-marks
1093 (nnimap-parse-flags
1094 (nreverse sequences)))
1095 infos)
1096 ;; Finally, just return something resembling an active file in
1097 ;; the nntp buffer, so that the agent can save the info, too.
1098 (with-current-buffer nntp-server-buffer
1099 (erase-buffer)
1100 (dolist (info infos)
1101 (let* ((group (gnus-info-group info))
1102 (active (gnus-active group)))
1103 (when active
1104 (insert (format "%S %d %d y\n"
1105 (gnus-group-real-name group)
1106 (cdr active)
1107 (car active)))))))))))
20a673b2
KY
1108
1109(defun nnimap-update-infos (flags infos)
1110 (dolist (info infos)
f7aa248a
G
1111 (let* ((group (gnus-group-real-name (gnus-info-group info)))
1112 (marks (cdr (assoc group flags))))
1113 (when marks
1114 (nnimap-update-info info marks)))))
20a673b2
KY
1115
1116(defun nnimap-update-info (info marks)
f7aa248a
G
1117 (destructuring-bind (existing flags high low uidnext start-article
1118 permanent-flags uidvalidity
1119 vanished highestmodseq) marks
1120 (cond
1121 ;; Ignore groups with no UIDNEXT/marks. This happens for
1122 ;; completely empty groups.
1123 ((and (not existing)
1124 (not uidnext))
dab0271f
G
1125 (let ((active (cdr (assq 'active (gnus-info-params info)))))
1126 (when active
1127 (gnus-set-active (gnus-info-group info) active))))
f7aa248a
G
1128 ;; We have a mismatch between the old and new UIDVALIDITY
1129 ;; identifiers, so we have to re-request the group info (the next
1130 ;; time). This virtually never happens.
1131 ((let ((old-uidvalidity
1132 (cdr (assq 'uidvalidity (gnus-info-params info)))))
1133 (and old-uidvalidity
1134 (not (equal old-uidvalidity uidvalidity))
1135 (> start-article 1)))
1136 (gnus-group-remove-parameter info 'uidvalidity)
1137 (gnus-group-remove-parameter info 'modseq))
1138 ;; We have the data needed to update.
1139 (t
dab0271f
G
1140 (let* ((group (gnus-info-group info))
1141 (completep (and start-article
1142 (= start-article 1)))
1143 (active (or (gnus-active group)
1144 (cdr (assq 'active (gnus-info-params info))))))
b1ae92ba
G
1145 (when uidnext
1146 (setq high (1- uidnext)))
20a673b2
KY
1147 ;; First set the active ranges based on high/low.
1148 (if (or completep
1149 (not (gnus-active group)))
1150 (gnus-set-active group
61b1af82
G
1151 (cond
1152 ((and low high)
1153 (cons low high))
1154 (uidnext
20a673b2 1155 ;; No articles in this group.
61b1af82 1156 (cons uidnext (1- uidnext)))
dab0271f
G
1157 (active
1158 active)
61b1af82
G
1159 (start-article
1160 (cons start-article (1- start-article)))
1161 (t
1162 ;; No articles and no uidnext.
1163 nil)))
9f2d52e7
G
1164 (gnus-set-active
1165 group
dab0271f 1166 (cons (car active)
9f2d52e7 1167 (or high (1- uidnext)))))
f7aa248a
G
1168 ;; See whether this is a read-only group.
1169 (unless (eq permanent-flags 'not-scanned)
1170 (gnus-group-set-parameter
1171 info 'permanent-flags
7cad71ad
G
1172 (and (or (memq '%* permanent-flags)
1173 (memq '%Seen permanent-flags))
1174 permanent-flags)))
f7aa248a
G
1175 ;; Update marks and read articles if this isn't a
1176 ;; read-only IMAP group.
7cad71ad
G
1177 (when (setq permanent-flags
1178 (cdr (assq 'permanent-flags (gnus-info-params info))))
f7aa248a
G
1179 (if (and highestmodseq
1180 (not start-article))
1181 ;; We've gotten the data by QRESYNCing.
1182 (nnimap-update-qresync-info
dab0271f 1183 info existing (nnimap-imap-ranges-to-gnus-ranges vanished) flags)
f7aa248a
G
1184 ;; Do normal non-QRESYNC flag updates.
1185 ;; Update the list of read articles.
1186 (let* ((unread
1187 (gnus-compress-sequence
1188 (gnus-set-difference
1189 (gnus-set-difference
1190 existing
1191 (cdr (assoc '%Seen flags)))
1192 (cdr (assoc '%Flagged flags)))))
1193 (read (gnus-range-difference
1194 (cons start-article high) unread)))
1195 (when (> start-article 1)
1196 (setq read
1197 (gnus-range-nconcat
1198 (if (> start-article 1)
1199 (gnus-sorted-range-intersection
1200 (cons 1 (1- start-article))
1201 (gnus-info-read info))
1202 (gnus-info-read info))
1203 read)))
7cad71ad
G
1204 (when (or (not (listp permanent-flags))
1205 (memq '%Seen permanent-flags))
1206 (gnus-info-set-read info read))
f7aa248a
G
1207 ;; Update the marks.
1208 (setq marks (gnus-info-marks info))
1209 (dolist (type (cdr nnimap-mark-alist))
7cad71ad 1210 (when (or (not (listp permanent-flags))
1e961f10
KAH
1211 (memq (car (assoc (caddr type) flags))
1212 permanent-flags)
7cad71ad
G
1213 (memq '%* permanent-flags))
1214 (let ((old-marks (assoc (car type) marks))
1215 (new-marks
1216 (gnus-compress-sequence
1217 (cdr (or (assoc (caddr type) flags) ; %Flagged
1218 (assoc (intern (cadr type) obarray) flags)
1219 (assoc (cadr type) flags)))))) ; "\Flagged"
1220 (setq marks (delq old-marks marks))
1221 (pop old-marks)
1222 (when (and old-marks
1223 (> start-article 1))
1224 (setq old-marks (gnus-range-difference
1225 old-marks
1226 (cons start-article high)))
1227 (setq new-marks (gnus-range-nconcat old-marks new-marks)))
1228 (when new-marks
1229 (push (cons (car type) new-marks) marks)))))
1230 (gnus-info-set-marks info marks t))))
f7aa248a
G
1231 ;; Note the active level for the next run-through.
1232 (gnus-group-set-parameter info 'active (gnus-active group))
1233 (gnus-group-set-parameter info 'uidvalidity uidvalidity)
1234 (gnus-group-set-parameter info 'modseq highestmodseq)
1235 (nnimap-store-info info (gnus-active group)))))))
1236
dab0271f 1237(defun nnimap-update-qresync-info (info existing vanished flags)
f7aa248a
G
1238 ;; Add all the vanished articles to the list of read articles.
1239 (gnus-info-set-read
1240 info
dab0271f
G
1241 (gnus-add-to-range
1242 (gnus-add-to-range
1243 (gnus-range-add (gnus-info-read info)
1244 vanished)
1245 (cdr (assq '%Flagged flags)))
1246 (cdr (assq '%Seen flags))))
1247 (let ((marks (gnus-info-marks info)))
1248 (dolist (type (cdr nnimap-mark-alist))
1249 (let ((ticks (assoc (car type) marks))
1250 (new-marks
1251 (cdr (or (assoc (caddr type) flags) ; %Flagged
1252 (assoc (intern (cadr type) obarray) flags)
1253 (assoc (cadr type) flags))))) ; "\Flagged"
1254 (setq marks (delq ticks marks))
1255 (pop ticks)
1256 ;; Add the new marks we got.
1257 (setq ticks (gnus-add-to-range ticks new-marks))
1258 ;; Remove the marks from messages that don't have them.
1259 (setq ticks (gnus-remove-from-range
1260 ticks
1261 (gnus-compress-sequence
1262 (gnus-sorted-complement existing new-marks))))
1263 (when ticks
1264 (push (cons (car type) ticks) marks)))
1265 (gnus-info-set-marks info marks t))))
f7aa248a
G
1266
1267(defun nnimap-imap-ranges-to-gnus-ranges (irange)
1268 (if (zerop (length irange))
1269 nil
1270 (let ((result nil))
1271 (dolist (elem (split-string irange ","))
1272 (push
1273 (if (string-match ":" elem)
1274 (let ((numbers (split-string elem ":")))
1275 (cons (string-to-number (car numbers))
1276 (string-to-number (cadr numbers))))
1277 (string-to-number elem))
1278 result))
1279 (nreverse result))))
286c4fc2
LMI
1280
1281(defun nnimap-store-info (info active)
1282 (let* ((group (gnus-group-real-name (gnus-info-group info)))
1283 (entry (assoc group nnimap-current-infos)))
1284 (if entry
1285 (setcdr entry (list info active))
1286 (push (list group info active) nnimap-current-infos))))
20a673b2
KY
1287
1288(defun nnimap-flags-to-marks (groups)
f7aa248a
G
1289 (let (data group totalp uidnext articles start-article mark permanent-flags
1290 uidvalidity vanished highestmodseq)
20a673b2
KY
1291 (dolist (elem groups)
1292 (setq group (car elem)
b069e5a6
G
1293 uidnext (nth 1 elem)
1294 start-article (nth 2 elem)
1295 permanent-flags (nth 3 elem)
f7aa248a
G
1296 uidvalidity (nth 4 elem)
1297 vanished (nth 5 elem)
1298 highestmodseq (nth 6 elem)
1299 articles (nthcdr 7 elem))
20a673b2
KY
1300 (let ((high (caar articles))
1301 marks low existing)
1302 (dolist (article articles)
1303 (setq low (car article))
1304 (push (car article) existing)
1305 (dolist (flag (cdr article))
1306 (setq mark (assoc flag marks))
1307 (if (not mark)
1308 (push (list flag (car article)) marks)
b069e5a6
G
1309 (setcdr mark (cons (car article) (cdr mark))))))
1310 (push (list group existing marks high low uidnext start-article
f7aa248a 1311 permanent-flags uidvalidity vanished highestmodseq)
b069e5a6 1312 data)))
20a673b2
KY
1313 data))
1314
1315(defun nnimap-parse-flags (sequences)
1316 (goto-char (point-min))
b069e5a6
G
1317 ;; Change \Delete etc to %Delete, so that the reader can read it.
1318 (subst-char-in-region (point-min) (point-max)
1319 ?\\ ?% t)
f7aa248a
G
1320 (let (start end articles groups uidnext elems permanent-flags
1321 uidvalidity vanished highestmodseq)
20a673b2 1322 (dolist (elem sequences)
f7aa248a
G
1323 (destructuring-bind (group-sequence flag-sequence totalp group command)
1324 elem
b069e5a6 1325 (setq start (point))
f7aa248a
G
1326 (when (and
1327 ;; The EXAMINE was successful.
1328 (search-forward (format "\n%d OK " group-sequence) nil t)
1329 (progn
1330 (forward-line 1)
1331 (setq end (point))
1332 (goto-char start)
1333 (setq permanent-flags
1334 (if (equal command "SELECT")
b069e5a6 1335 (and (search-forward "PERMANENTFLAGS "
f7aa248a
G
1336 (or end (point-min)) t)
1337 (read (current-buffer)))
1338 'not-scanned))
1339 (goto-char start)
1340 (setq uidnext
1341 (and (search-forward "UIDNEXT "
1342 (or end (point-min)) t)
1343 (read (current-buffer))))
1344 (goto-char start)
1345 (setq uidvalidity
1346 (and (re-search-forward "UIDVALIDITY \\([0-9]+\\)"
1347 (or end (point-min)) t)
1348 ;; Store UIDVALIDITY as a string, as it's
1349 ;; too big for 32-bit Emacsen, usually.
1350 (match-string 1)))
1351 (goto-char start)
1352 (setq vanished
1353 (and (eq flag-sequence 'qresync)
1354 (re-search-forward "VANISHED.* \\([0-9:,]+\\)"
1355 (or end (point-min)) t)
1356 (match-string 1)))
1357 (goto-char start)
1358 (setq highestmodseq
1359 (and (search-forward "HIGHESTMODSEQ "
1360 (or end (point-min)) t)
1361 (read (current-buffer))))
1362 (goto-char end)
1363 (forward-line -1))
1364 ;; The UID FETCH FLAGS was successful.
1365 (or (eq flag-sequence 'qresync)
1366 (search-forward (format "\n%d OK " flag-sequence) nil t)))
1367 (if (eq flag-sequence 'qresync)
1368 (progn
1369 (goto-char start)
1370 (setq start end))
1371 (setq start (point))
1372 (goto-char end))
a1d16a7b 1373 (while (re-search-forward "^\\* [0-9]+ FETCH " start t)
b069e5a6
G
1374 (setq elems (read (current-buffer)))
1375 (push (cons (cadr (memq 'UID elems))
1376 (cadr (memq 'FLAGS elems)))
20a673b2 1377 articles))
f7aa248a
G
1378 (push (nconc (list group uidnext totalp permanent-flags uidvalidity
1379 vanished highestmodseq)
1380 articles)
b069e5a6 1381 groups)
f7aa248a 1382 (goto-char end)
20a673b2
KY
1383 (setq articles nil))))
1384 groups))
1385
1386(defun nnimap-find-process-buffer (buffer)
1387 (cadr (assoc buffer nnimap-connection-alist)))
1388
286c4fc2 1389(deffoo nnimap-request-post (&optional server)
20a673b2
KY
1390 (setq nnimap-status-string "Read-only server")
1391 nil)
c113de23 1392
030158f3
G
1393(deffoo nnimap-request-thread (id)
1394 (let* ((refs (split-string
1395 (or (mail-header-references (gnus-summary-article-header))
1396 "")))
1397 (cmd (let ((value
1398 (format
1399 "(OR HEADER REFERENCES %s HEADER Message-Id %s)"
1400 id id)))
1401 (dolist (refid refs value)
1402 (setq value (format
1403 "(OR (OR HEADER Message-Id %s HEADER REFERENCES %s) %s)"
1404 refid refid value)))))
1405 (result
1406 (with-current-buffer (nnimap-buffer)
1407 (nnimap-command "UID SEARCH %s" cmd))))
1408 (gnus-fetch-headers (and (car result)
1409 (delete 0 (mapcar #'string-to-number
1410 (cdr (assoc "SEARCH" (cdr result)))))))))
1411
20a673b2
KY
1412(defun nnimap-possibly-change-group (group server)
1413 (let ((open-result t))
1414 (when (and server
1415 (not (nnimap-server-opened server)))
1416 (setq open-result (nnimap-open-server server)))
1417 (cond
1418 ((not open-result)
1419 nil)
1420 ((not group)
1421 t)
1422 (t
1423 (with-current-buffer (nnimap-buffer)
1424 (if (equal group (nnimap-group nnimap-object))
1425 t
1426 (let ((result (nnimap-command "SELECT %S" (utf7-encode group t))))
1427 (when (car result)
286c4fc2
LMI
1428 (setf (nnimap-group nnimap-object) group
1429 (nnimap-select-result nnimap-object) result)
20a673b2
KY
1430 result))))))))
1431
1432(defun nnimap-find-connection (buffer)
1433 "Find the connection delivering to BUFFER."
1434 (let ((entry (assoc buffer nnimap-connection-alist)))
1435 (when entry
1436 (if (and (buffer-name (cadr entry))
1437 (get-buffer-process (cadr entry))
1438 (memq (process-status (get-buffer-process (cadr entry)))
1439 '(open run)))
1440 (get-buffer-process (cadr entry))
1441 (setq nnimap-connection-alist (delq entry nnimap-connection-alist))
1442 nil))))
1443
1444(defvar nnimap-sequence 0)
1445
1446(defun nnimap-send-command (&rest args)
1447 (process-send-string
1448 (get-buffer-process (current-buffer))
1449 (nnimap-log-command
286c4fc2 1450 (format "%d %s%s\n"
20a673b2 1451 (incf nnimap-sequence)
286c4fc2
LMI
1452 (apply #'format args)
1453 (if (nnimap-newlinep nnimap-object)
1454 ""
1455 "\r"))))
b5c575e6
G
1456 ;; Some servers apparently can't have many outstanding
1457 ;; commands, so throttle them.
1458 (unless nnimap-streaming
1459 (nnimap-wait-for-response nnimap-sequence))
20a673b2
KY
1460 nnimap-sequence)
1461
1462(defun nnimap-log-command (command)
1463 (with-current-buffer (get-buffer-create "*imap log*")
1464 (goto-char (point-max))
1465 (insert (format-time-string "%H:%M:%S") " " command))
1466 command)
1467
1468(defun nnimap-command (&rest args)
1469 (erase-buffer)
61b1af82 1470 (setf (nnimap-last-command-time nnimap-object) (current-time))
20a673b2
KY
1471 (let* ((sequence (apply #'nnimap-send-command args))
1472 (response (nnimap-get-response sequence)))
1473 (if (equal (caar response) "OK")
1474 (cons t response)
1475 (nnheader-report 'nnimap "%s"
a46359d4
LMI
1476 (mapconcat (lambda (a)
1477 (format "%s" a))
1478 (car response) " "))
20a673b2
KY
1479 nil)))
1480
1481(defun nnimap-get-response (sequence)
1482 (nnimap-wait-for-response sequence)
1483 (nnimap-parse-response))
1484
389b76fa
G
1485(defun nnimap-wait-for-connection (&optional regexp)
1486 (unless regexp
1487 (setq regexp "^[*.] .*\n"))
286c4fc2
LMI
1488 (let ((process (get-buffer-process (current-buffer))))
1489 (goto-char (point-min))
1490 (while (and (memq (process-status process)
1491 '(open run))
389b76fa 1492 (not (re-search-forward regexp nil t)))
286c4fc2
LMI
1493 (nnheader-accept-process-output process)
1494 (goto-char (point-min)))
bdaa75c7 1495 (forward-line -1)
6b958814 1496 (and (looking-at "[*.] \\([A-Z0-9]+\\)")
bdaa75c7 1497 (match-string 1))))
286c4fc2 1498
20a673b2 1499(defun nnimap-wait-for-response (sequence &optional messagep)
8ccbef23
G
1500 (let ((process (get-buffer-process (current-buffer)))
1501 openp)
dab0271f
G
1502 (condition-case nil
1503 (progn
1504 (goto-char (point-max))
1505 (while (and (setq openp (memq (process-status process)
1506 '(open run)))
1507 (not (re-search-backward
1508 (format "^%d .*\n" sequence)
1509 (if nnimap-streaming
1510 (max (point-min) (- (point) 500))
1511 (point-min))
1512 t)))
1513 (when messagep
283f7b93 1514 (nnheader-message 7 "nnimap read %dk" (/ (buffer-size) 1000)))
dab0271f
G
1515 (nnheader-accept-process-output process)
1516 (goto-char (point-max)))
1517 openp)
1518 (quit
1519 ;; The user hit C-g while we were waiting: kill the process, in case
1520 ;; it's a gnutls-cli process that's stuck (tends to happen a lot behind
1521 ;; NAT routers).
1522 (delete-process process)
1523 nil))))
20a673b2
KY
1524
1525(defun nnimap-parse-response ()
1526 (let ((lines (split-string (nnimap-last-response-string) "\r\n" t))
1527 result)
1528 (dolist (line lines)
1529 (push (cdr (nnimap-parse-line line)) result))
1530 ;; Return the OK/error code first, and then all the "continuation
1531 ;; lines" afterwards.
1532 (cons (pop result)
1533 (nreverse result))))
1534
1535;; Parse an IMAP response line lightly. They look like
1536;; "* OK [UIDVALIDITY 1164213559] UIDs valid", typically, so parse
1537;; the lines into a list of strings and lists of string.
1538(defun nnimap-parse-line (line)
1539 (let (char result)
1540 (with-temp-buffer
a04f9e26 1541 (mm-disable-multibyte)
20a673b2
KY
1542 (insert line)
1543 (goto-char (point-min))
1544 (while (not (eobp))
1545 (if (eql (setq char (following-char)) ? )
1546 (forward-char 1)
1547 (push
1548 (cond
1549 ((eql char ?\[)
f7aa248a
G
1550 (split-string
1551 (buffer-substring
1552 (1+ (point))
1553 (1- (search-forward "]" (line-end-position) 'move)))))
20a673b2 1554 ((eql char ?\()
f7aa248a
G
1555 (split-string
1556 (buffer-substring
1557 (1+ (point))
1558 (1- (search-forward ")" (line-end-position) 'move)))))
20a673b2
KY
1559 ((eql char ?\")
1560 (forward-char 1)
9f2d52e7
G
1561 (buffer-substring
1562 (point)
1563 (1- (or (search-forward "\"" (line-end-position) 'move)
1564 (point)))))
20a673b2
KY
1565 (t
1566 (buffer-substring (point) (if (search-forward " " nil t)
1567 (1- (point))
1568 (goto-char (point-max))))))
1569 result)))
1570 (nreverse result))))
1571
1572(defun nnimap-last-response-string ()
1573 (save-excursion
1574 (forward-line 1)
1575 (let ((end (point)))
1576 (forward-line -1)
1577 (when (not (bobp))
1578 (forward-line -1)
1579 (while (and (not (bobp))
1580 (eql (following-char) ?*))
1581 (forward-line -1))
1582 (unless (eql (following-char) ?*)
1583 (forward-line 1)))
1584 (buffer-substring (point) end))))
1585
1586(defun nnimap-get-responses (sequences)
1587 (let (responses)
1588 (dolist (sequence sequences)
1589 (goto-char (point-min))
1590 (when (re-search-forward (format "^%d " sequence) nil t)
1591 (push (list sequence (nnimap-parse-response))
1592 responses)))
1593 responses))
1594
1595(defvar nnimap-incoming-split-list nil)
1596
1597(defun nnimap-fetch-inbox (articles)
1598 (erase-buffer)
1599 (nnimap-wait-for-response
1600 (nnimap-send-command
1601 "UID FETCH %s %s"
1602 (nnimap-article-ranges articles)
1603 (format "(UID %s%s)"
1604 (format
8ccbef23 1605 (if (nnimap-ver4-p)
20a673b2
KY
1606 "BODY.PEEK[HEADER] BODY.PEEK"
1607 "RFC822.PEEK"))
1608 (if nnimap-split-download-body-default
a46359d4 1609 "[]"
20a673b2
KY
1610 "[1]")))
1611 t))
1612
1613(defun nnimap-split-incoming-mail ()
1614 (with-current-buffer (nnimap-buffer)
1615 (let ((nnimap-incoming-split-list nil)
229b59da
G
1616 (nnmail-split-methods (if (eq nnimap-split-methods 'default)
1617 nnmail-split-methods
1618 nnimap-split-methods))
6b958814
G
1619 (nnmail-split-fancy (or nnimap-split-fancy
1620 nnmail-split-fancy))
20a673b2
KY
1621 (nnmail-inhibit-default-split-group t)
1622 (groups (nnimap-get-groups))
1623 new-articles)
1624 (erase-buffer)
1625 (nnimap-command "SELECT %S" nnimap-inbox)
99e65b2d 1626 (setf (nnimap-group nnimap-object) nnimap-inbox)
20a673b2
KY
1627 (setq new-articles (nnimap-new-articles (nnimap-get-flags "1:*")))
1628 (when new-articles
1629 (nnimap-fetch-inbox new-articles)
1630 (nnimap-transform-split-mail)
1631 (nnheader-ms-strip-cr)
1632 (nnmail-cache-open)
1633 (nnmail-split-incoming (current-buffer)
1634 #'nnimap-save-mail-spec
1635 nil nil
b069e5a6
G
1636 #'nnimap-dummy-active-number
1637 #'nnimap-save-mail-spec)
20a673b2
KY
1638 (when nnimap-incoming-split-list
1639 (let ((specs (nnimap-make-split-specs nnimap-incoming-split-list))
b069e5a6 1640 sequences junk-articles)
20a673b2
KY
1641 ;; Create any groups that doesn't already exist on the
1642 ;; server first.
1643 (dolist (spec specs)
b069e5a6
G
1644 (when (and (not (member (car spec) groups))
1645 (not (eq (car spec) 'junk)))
20a673b2
KY
1646 (nnimap-command "CREATE %S" (utf7-encode (car spec) t))))
1647 ;; Then copy over all the messages.
1648 (erase-buffer)
1649 (dolist (spec specs)
1650 (let ((group (car spec))
1651 (ranges (cdr spec)))
b069e5a6
G
1652 (if (eq group 'junk)
1653 (setq junk-articles ranges)
1654 (push (list (nnimap-send-command
1655 "UID COPY %s %S"
1656 (nnimap-article-ranges ranges)
1657 (utf7-encode group t))
1658 ranges)
1659 sequences))))
20a673b2
KY
1660 ;; Wait for the last COPY response...
1661 (when sequences
1662 (nnimap-wait-for-response (caar sequences))
1663 ;; And then mark the successful copy actions as deleted,
1664 ;; and possibly expunge them.
1665 (nnimap-mark-and-expunge-incoming
61b1af82
G
1666 (nnimap-parse-copied-articles sequences)))
1667 (nnimap-mark-and-expunge-incoming junk-articles)))))))
20a673b2
KY
1668
1669(defun nnimap-mark-and-expunge-incoming (range)
1670 (when range
1671 (setq range (nnimap-article-ranges range))
229b59da 1672 (erase-buffer)
0617bb00
LMI
1673 (let ((sequence
1674 (nnimap-send-command
1675 "UID STORE %s +FLAGS.SILENT (\\Deleted)" range)))
1676 (cond
1677 ;; If the server supports it, we now delete the message we have
1678 ;; just copied over.
389b76fa 1679 ((nnimap-capability "UIDPLUS")
0617bb00
LMI
1680 (setq sequence (nnimap-send-command "UID EXPUNGE %s" range)))
1681 ;; If it doesn't support UID EXPUNGE, then we only expunge if the
1682 ;; user has configured it.
b069e5a6 1683 (nnimap-expunge
0617bb00
LMI
1684 (setq sequence (nnimap-send-command "EXPUNGE"))))
1685 (nnimap-wait-for-response sequence))))
20a673b2
KY
1686
1687(defun nnimap-parse-copied-articles (sequences)
1688 (let (sequence copied range)
1689 (goto-char (point-min))
1690 (while (re-search-forward "^\\([0-9]+\\) OK " nil t)
1691 (setq sequence (string-to-number (match-string 1)))
1692 (when (setq range (cadr (assq sequence sequences)))
1693 (push (gnus-uncompress-range range) copied)))
1694 (gnus-compress-sequence (sort (apply #'nconc copied) #'<))))
1695
1696(defun nnimap-new-articles (flags)
1697 (let (new)
1698 (dolist (elem flags)
99e65b2d
G
1699 (unless (gnus-list-memq-of-list nnimap-unsplittable-articles
1700 (cdr elem))
20a673b2
KY
1701 (push (car elem) new)))
1702 (gnus-compress-sequence (nreverse new))))
1703
1704(defun nnimap-make-split-specs (list)
1705 (let ((specs nil)
1706 entry)
1707 (dolist (elem list)
1708 (destructuring-bind (article spec) elem
1709 (dolist (group (delete nil (mapcar #'car spec)))
1710 (unless (setq entry (assoc group specs))
1711 (push (setq entry (list group)) specs))
1712 (setcdr entry (cons article (cdr entry))))))
1713 (dolist (entry specs)
1714 (setcdr entry (gnus-compress-sequence (sort (cdr entry) #'<))))
1715 specs))
1716
1717(defun nnimap-transform-split-mail ()
1718 (goto-char (point-min))
1719 (let (article bytes)
1720 (block nil
1721 (while (not (eobp))
1722 (while (not (looking-at "^\\* [0-9]+ FETCH.*UID \\([0-9]+\\)"))
1723 (delete-region (point) (progn (forward-line 1) (point)))
1724 (when (eobp)
1725 (return)))
1726 (setq article (match-string 1)
1727 bytes (nnimap-get-length))
1728 (delete-region (line-beginning-position) (line-end-position))
1729 ;; Insert MMDF separator, and a way to remember what this
1730 ;; article UID is.
1731 (insert (format "\^A\^A\^A\^A\n\nX-nnimap-article: %s" article))
1732 (forward-char (1+ bytes))
1733 (setq bytes (nnimap-get-length))
1734 (delete-region (line-beginning-position) (line-end-position))
6b7df8d3
G
1735 ;; There's a body; skip past that.
1736 (when bytes
1737 (forward-char (1+ bytes))
1738 (delete-region (line-beginning-position) (line-end-position)))))))
20a673b2
KY
1739
1740(defun nnimap-dummy-active-number (group &optional server)
1741 1)
1742
1743(defun nnimap-save-mail-spec (group-art &optional server full-nov)
1744 (let (article)
1745 (goto-char (point-min))
1746 (if (not (re-search-forward "X-nnimap-article: \\([0-9]+\\)" nil t))
1747 (error "Invalid nnimap mail")
1748 (setq article (string-to-number (match-string 1))))
b069e5a6
G
1749 (push (list article
1750 (if (eq group-art 'junk)
1751 (list (cons 'junk 1))
1752 group-art))
20a673b2 1753 nnimap-incoming-split-list)))
c113de23 1754
c113de23
GM
1755(provide 'nnimap)
1756
1757;;; nnimap.el ends here