ChangeLog fixes
[bpt/emacs.git] / lisp / gnus / nnimap.el
CommitLineData
20a673b2 1;;; nnimap.el --- IMAP interface for Gnus
e84b4b86 2
ba318903 3;; Copyright (C) 2010-2014 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
da91b5f2
CY
34 (require 'nnheader)
35 ;; In Emacs 24, `open-protocol-stream' is an autoloaded alias for
36 ;; `make-network-stream'.
37 (unless (fboundp 'open-protocol-stream)
38 (require 'proto-stream)))
aa8f8277 39
20a673b2
KY
40(eval-when-compile
41 (require 'cl))
c113de23 42
f58208b1
LMI
43(require 'nnheader)
44(require 'gnus-util)
45(require 'gnus)
46(require 'nnoo)
20a673b2 47(require 'netrc)
14db1c41 48(require 'utf7)
6b958814 49(require 'tls)
0617bb00 50(require 'parse-time)
2b1e1ff4
GM
51(require 'nnmail)
52
b8e0f0cd
G
53(autoload 'auth-source-forget+ "auth-source")
54(autoload 'auth-source-search "auth-source")
635be05a 55
c113de23
GM
56(nnoo-declare nnimap)
57
c113de23 58(defvoo nnimap-address nil
20a673b2 59 "The address of the IMAP server.")
c113de23 60
5e68f861
TZ
61(defvoo nnimap-user nil
62 "Username to use for authentication to the IMAP server.")
63
c113de23 64(defvoo nnimap-server-port nil
20a673b2
KY
65 "The IMAP port used.
66If nnimap-stream is `ssl', this will default to `imaps'. If not,
67it will default to `imap'.")
68
ed797193 69(defvoo nnimap-stream 'undecided
e742e117
CY
70 "How nnimap talks to the IMAP server.
71The value should be either `undecided', `ssl' or `tls',
72`network', `starttls', `plain', or `shell'.
73
74If the value is `undecided', nnimap tries `ssl' first, then falls
75back on `network'.")
20a673b2
KY
76
77(defvoo nnimap-shell-program (if (boundp 'imap-shell-program)
78 (if (listp imap-shell-program)
79 (car imap-shell-program)
80 imap-shell-program)
81 "ssh %s imapd"))
82
83(defvoo nnimap-inbox nil
8e22bee0 84 "The mail box where incoming mail arrives and should be split out of.
eaa610c3
KY
85This can be a string or a list of strings
86For example, \"INBOX\" or (\"INBOX\" \"SENT\").")
20a673b2 87
8ccbef23
G
88(defvoo nnimap-split-methods nil
89 "How mail is split.
8e22bee0 90Uses the same syntax as `nnmail-split-methods'.")
8ccbef23 91
6b958814 92(defvoo nnimap-split-fancy nil
8e22bee0 93 "Uses the same syntax as `nnmail-split-fancy'.")
6b958814 94
99e65b2d
G
95(defvoo nnimap-unsplittable-articles '(%Deleted %Seen)
96 "Articles with the flags in the list will not be considered when splitting.")
97
229b59da 98(make-obsolete-variable 'nnimap-split-rule "see `nnimap-split-methods'"
6b958814 99 "Emacs 24.1")
229b59da 100
bdaa75c7
LMI
101(defvoo nnimap-authenticator nil
102 "How nnimap authenticate itself to the server.
45dd6480
LI
103Possible choices are nil (use default methods), `anonymous',
104`login', `plain' and `cram-md5'.")
bdaa75c7 105
b069e5a6
G
106(defvoo nnimap-expunge t
107 "If non-nil, expunge articles after deleting them.
108This is always done if the server supports UID EXPUNGE, but it's
109not done by default on servers that doesn't support that command.")
110
8ccbef23
G
111(defvoo nnimap-streaming t
112 "If non-nil, try to use streaming commands with IMAP servers.
113Switching this off will make nnimap slower, but it helps with
114some servers.")
0617bb00 115
20a673b2 116(defvoo nnimap-connection-alist nil)
286c4fc2
LMI
117
118(defvoo nnimap-current-infos nil)
119
9f2d52e7
G
120(defvoo nnimap-fetch-partial-articles nil
121 "If non-nil, Gnus will fetch partial articles.
89b163db 122If t, Gnus will fetch only the first part. If a string, it
9f2d52e7
G
123will fetch all parts that have types that match that string. A
124likely value would be \"text/\" to automatically fetch all
125textual parts.")
126
eaa610c3
KY
127(defgroup nnimap nil
128 "IMAP for Gnus."
129 :group 'gnus)
130
131(defcustom nnimap-request-articles-find-limit nil
132 "Limit the number of articles to look for after moving an article."
af1c6c84
GM
133 :type '(choice (const nil) integer)
134 :version "24.4"
eaa610c3
KY
135 :group 'nnimap)
136
20a673b2
KY
137(defvar nnimap-process nil)
138
139(defvar nnimap-status-string "")
23f87bed
MB
140
141(defvar nnimap-split-download-body-default nil
142 "Internal variable with default value for `nnimap-split-download-body'.")
143
61b1af82
G
144(defvar nnimap-keepalive-timer nil)
145(defvar nnimap-process-buffers nil)
146
20a673b2 147(defstruct nnimap
61b1af82 148 group process commands capabilities select-result newlinep server
b5235dd9 149 last-command-time greeting examined stream-type initial-resync)
c113de23 150
20a673b2
KY
151(defvar nnimap-object nil)
152
153(defvar nnimap-mark-alist
b069e5a6
G
154 '((read "\\Seen" %Seen)
155 (tick "\\Flagged" %Flagged)
156 (reply "\\Answered" %Answered)
20a673b2
KY
157 (expire "gnus-expire")
158 (dormant "gnus-dormant")
159 (score "gnus-score")
160 (save "gnus-save")
161 (download "gnus-download")
162 (forward "gnus-forward")))
163
549c9aed
G
164(defvar nnimap-quirks
165 '(("QRESYNC" "Zimbra" "QRESYNC ")))
166
d5e9a4e9
LI
167(defvar nnimap-inhibit-logging nil)
168
20a673b2
KY
169(defun nnimap-buffer ()
170 (nnimap-find-process-buffer nntp-server-buffer))
171
b5c575e6
G
172(defun nnimap-header-parameters ()
173 (format "(UID RFC822.SIZE BODYSTRUCTURE %s)"
174 (format
175 (if (nnimap-ver4-p)
176 "BODY.PEEK[HEADER.FIELDS %s]"
177 "RFC822.HEADER.LINES %s")
178 (append '(Subject From Date Message-Id
179 References In-Reply-To Xref)
180 nnmail-extra-headers))))
181
286c4fc2 182(deffoo nnimap-retrieve-headers (articles &optional group server fetch-old)
e21bac42
G
183 (when group
184 (setq group (nnimap-decode-gnus-group group)))
c113de23 185 (with-current-buffer nntp-server-buffer
20a673b2 186 (erase-buffer)
eaa610c3 187 (when (nnimap-change-group group server)
20a673b2 188 (with-current-buffer (nnimap-buffer)
20a673b2
KY
189 (erase-buffer)
190 (nnimap-wait-for-response
191 (nnimap-send-command
192 "UID FETCH %s %s"
193 (nnimap-article-ranges (gnus-compress-sequence articles))
b5c575e6 194 (nnimap-header-parameters))
20a673b2 195 t)
70041e9a
G
196 (nnimap-transform-headers)
197 (nnheader-remove-cr-followed-by-lf))
20a673b2
KY
198 (insert-buffer-substring
199 (nnimap-find-process-buffer (current-buffer))))
b1ae92ba 200 'headers))
20a673b2
KY
201
202(defun nnimap-transform-headers ()
203 (goto-char (point-min))
827235c3 204 (let (article lines size string)
20a673b2
KY
205 (block nil
206 (while (not (eobp))
827235c3 207 (while (not (looking-at "\\* [0-9]+ FETCH"))
20a673b2
KY
208 (delete-region (point) (progn (forward-line 1) (point)))
209 (when (eobp)
210 (return)))
827235c3 211 (goto-char (match-end 0))
733de8e2 212 ;; Unfold quoted {number} strings.
827235c3
LI
213 (while (re-search-forward
214 "[^]][ (]{\\([0-9]+\\)}\r?\n"
215 (save-excursion
20d6487e
LI
216 ;; Start of the header section.
217 (or (re-search-forward "] {[0-9]+}\r?\n" nil t)
218 ;; Start of the next FETCH.
219 (re-search-forward "\\* [0-9]+ FETCH" nil t)
827235c3
LI
220 (point-max)))
221 t)
733de8e2
LMI
222 (setq size (string-to-number (match-string 1)))
223 (delete-region (+ (match-beginning 0) 2) (point))
224 (setq string (buffer-substring (point) (+ (point) size)))
225 (delete-region (point) (+ (point) size))
1f0f21c5 226 (insert (format "%S" (mm-subst-char-in-string ?\n ?\s string))))
20a673b2 227 (beginning-of-line)
827235c3
LI
228 (setq article
229 (and (re-search-forward "UID \\([0-9]+\\)" (line-end-position)
230 t)
231 (match-string 1)))
232 (setq lines nil)
a46359d4
LMI
233 (setq size
234 (and (re-search-forward "RFC822.SIZE \\([0-9]+\\)"
235 (line-end-position)
236 t)
237 (match-string 1)))
238 (beginning-of-line)
20a673b2 239 (when (search-forward "BODYSTRUCTURE" (line-end-position) t)
b1ae92ba
G
240 (let ((structure (ignore-errors
241 (read (current-buffer)))))
20a673b2 242 (while (and (consp structure)
4d9db491 243 (not (atom (car structure))))
20a673b2 244 (setq structure (car structure)))
fb568e63 245 (setq lines (if (and
4d9db491 246 (stringp (car structure))
fb568e63
AC
247 (equal (upcase (nth 0 structure)) "MESSAGE")
248 (equal (upcase (nth 1 structure)) "RFC822"))
249 (nth 9 structure)
250 (nth 7 structure)))))
20a673b2
KY
251 (delete-region (line-beginning-position) (line-end-position))
252 (insert (format "211 %s Article retrieved." article))
253 (forward-line 1)
a46359d4
LMI
254 (when size
255 (insert (format "Chars: %s\n" size)))
20a673b2
KY
256 (when lines
257 (insert (format "Lines: %s\n" lines)))
b5c575e6
G
258 (unless (re-search-forward "^\r$" nil t)
259 (goto-char (point-max)))
20a673b2
KY
260 (delete-region (line-beginning-position) (line-end-position))
261 (insert ".")
262 (forward-line 1)))))
263
a56a1cce
LMI
264(defun nnimap-unfold-quoted-lines ()
265 ;; Unfold quoted {number} strings.
733de8e2
LMI
266 (let (size string)
267 (while (re-search-forward " {\\([0-9]+\\)}\r?\n" nil t)
268 (setq size (string-to-number (match-string 1)))
269 (delete-region (1+ (match-beginning 0)) (point))
270 (setq string (buffer-substring (point) (+ (point) size)))
271 (delete-region (point) (+ (point) size))
272 (insert (format "%S" string)))))
a56a1cce 273
20a673b2
KY
274(defun nnimap-get-length ()
275 (and (re-search-forward "{\\([0-9]+\\)}" (line-end-position) t)
276 (string-to-number (match-string 1))))
277
278(defun nnimap-article-ranges (ranges)
279 (let (result)
280 (cond
281 ((numberp ranges)
282 (number-to-string ranges))
283 ((numberp (cdr ranges))
284 (format "%d:%d" (car ranges) (cdr ranges)))
285 (t
286 (dolist (elem ranges)
287 (push
288 (if (consp elem)
289 (format "%d:%d" (car elem) (cdr elem))
290 (number-to-string elem))
291 result))
292 (mapconcat #'identity (nreverse result) ",")))))
293
98366438 294(deffoo nnimap-open-server (server &optional defs no-reconnect)
c113de23
GM
295 (if (nnimap-server-opened server)
296 t
c113de23 297 (unless (assq 'nnimap-address defs)
20a673b2 298 (setq defs (append defs (list (list 'nnimap-address server)))))
c113de23 299 (nnoo-change-server 'nnimap server defs)
98366438
LI
300 (if no-reconnect
301 (nnimap-find-connection nntp-server-buffer)
302 (or (nnimap-find-connection nntp-server-buffer)
303 (nnimap-open-connection nntp-server-buffer)))))
20a673b2
KY
304
305(defun nnimap-make-process-buffer (buffer)
306 (with-current-buffer
3d2af193 307 (generate-new-buffer (format " *nnimap %s %s %s*"
20a673b2
KY
308 nnimap-address nnimap-server-port
309 (gnus-buffer-exists-p buffer)))
310 (mm-disable-multibyte)
311 (buffer-disable-undo)
312 (gnus-add-buffer)
313 (set (make-local-variable 'after-change-functions) nil)
b069e5a6 314 (set (make-local-variable 'nnimap-object)
b5235dd9
LI
315 (make-nnimap :server (nnoo-current-server 'nnimap)
316 :initial-resync 0))
20a673b2 317 (push (list buffer (current-buffer)) nnimap-connection-alist)
61b1af82 318 (push (current-buffer) nnimap-process-buffers)
20a673b2
KY
319 (current-buffer)))
320
5e68f861 321(defun nnimap-credentials (address ports user)
733afdf4
TZ
322 (let* ((auth-source-creation-prompts
323 '((user . "IMAP user at %h: ")
324 (secret . "IMAP password for %u@%h: ")))
325 (found (nth 0 (auth-source-search :max 1
326 :host address
327 :port ports
5e68f861 328 :user user
733afdf4
TZ
329 :require '(:user :secret)
330 :create t))))
b8e0f0cd 331 (if found
c13bc26b
LI
332 (list (plist-get found :user)
333 (let ((secret (plist-get found :secret)))
334 (if (functionp secret)
335 (funcall secret)
733afdf4
TZ
336 secret))
337 (plist-get found :save-function))
b8e0f0cd 338 nil)))
286c4fc2 339
61b1af82
G
340(defun nnimap-keepalive ()
341 (let ((now (current-time)))
342 (dolist (buffer nnimap-process-buffers)
343 (when (buffer-name buffer)
344 (with-current-buffer buffer
345 (when (and nnimap-object
346 (nnimap-last-command-time nnimap-object)
2b1e1ff4 347 (> (gnus-float-time
61b1af82
G
348 (time-subtract
349 now
350 (nnimap-last-command-time nnimap-object)))
351 ;; More than five minutes since the last command.
352 (* 5 60)))
ef821434
SM
353 (ignore-errors ;E.g. "buffer foo has no process".
354 (nnimap-send-command "NOOP"))))))))
61b1af82 355
20a673b2 356(defun nnimap-open-connection (buffer)
ed797193
G
357 ;; Be backwards-compatible -- the earlier value of nnimap-stream was
358 ;; `ssl' when nnimap-server-port was nil. Sort of.
359 (when (and nnimap-server-port
360 (eq nnimap-stream 'undecided))
361 (setq nnimap-stream 'ssl))
362 (let ((stream
363 (if (eq nnimap-stream 'undecided)
e742e117 364 (loop for type in '(ssl network)
ed797193
G
365 for stream = (let ((nnimap-stream type))
366 (nnimap-open-connection-1 buffer))
367 while (eq stream 'no-connect)
368 finally (return stream))
369 (nnimap-open-connection-1 buffer))))
370 (if (eq stream 'no-connect)
371 nil
372 stream)))
373
a5166359
LMI
374(defun nnimap-map-port (port)
375 (if (equal port "imaps")
376 "993"
377 port))
378
ed797193 379(defun nnimap-open-connection-1 (buffer)
61b1af82
G
380 (unless nnimap-keepalive-timer
381 (setq nnimap-keepalive-timer (run-at-time (* 60 15) (* 60 15)
ef821434 382 #'nnimap-keepalive)))
ed797193
G
383 (with-current-buffer (nnimap-make-process-buffer buffer)
384 (let* ((coding-system-for-read 'binary)
385 (coding-system-for-write 'binary)
ed797193 386 (ports
dab0271f 387 (cond
e742e117 388 ((memq nnimap-stream '(network plain starttls))
ed797193
G
389 (nnheader-message 7 "Opening connection to %s..."
390 nnimap-address)
003522ce 391 '("imap" "143"))
ed797193
G
392 ((eq nnimap-stream 'shell)
393 (nnheader-message 7 "Opening connection to %s via shell..."
394 nnimap-address)
395 '("imap"))
396 ((memq nnimap-stream '(ssl tls))
397 (nnheader-message 7 "Opening connection to %s via tls..."
398 nnimap-address)
003522ce 399 '("imaps" "imap" "993" "143"))
ed797193
G
400 (t
401 (error "Unknown stream type: %s" nnimap-stream))))
ed797193
G
402 login-result credentials)
403 (when nnimap-server-port
003522ce 404 (push nnimap-server-port ports))
f2eefd24
CY
405 (let* ((stream-list
406 (open-protocol-stream
a5166359
LMI
407 "*nnimap*" (current-buffer) nnimap-address
408 (nnimap-map-port (car ports))
f2eefd24
CY
409 :type nnimap-stream
410 :return-list t
411 :shell-command nnimap-shell-program
412 :capability-command "1 CAPABILITY\r\n"
da91b5f2 413 :end-of-command "\r\n"
f2eefd24
CY
414 :success " OK "
415 :starttls-function
416 (lambda (capabilities)
417 (when (gnus-string-match-p "STARTTLS" capabilities)
418 "1 STARTTLS\r\n"))))
419 (stream (car stream-list))
420 (props (cdr stream-list))
421 (greeting (plist-get props :greeting))
422 (capabilities (plist-get props :capabilities))
423 (stream-type (plist-get props :type)))
424 (when (and stream (not (memq (process-status stream) '(open run))))
425 (setq stream nil))
3ccc1742
SM
426
427 (when (and (fboundp 'set-network-process-option) ;; Not in XEmacs.
428 (fboundp 'process-type) ;; Emacs 22 doesn't provide it.
429 (eq (process-type stream) 'network))
430 ;; Use TCP-keepalive so that connections that pass through a NAT
431 ;; router don't hang when left idle.
432 (set-network-process-option stream :keepalive t))
433
ed797193 434 (setf (nnimap-process nnimap-object) stream)
008cad90 435 (setf (nnimap-stream-type nnimap-object) stream-type)
ed797193
G
436 (if (not stream)
437 (progn
438 (nnheader-report 'nnimap "Unable to contact %s:%s via %s"
bc320087 439 nnimap-address (car ports) nnimap-stream)
ed797193
G
440 'no-connect)
441 (gnus-set-process-query-on-exit-flag stream nil)
442 (if (not (gnus-string-match-p "[*.] \\(OK\\|PREAUTH\\)" greeting))
443 (nnheader-report 'nnimap "%s" greeting)
444 ;; Store the greeting (for debugging purposes).
445 (setf (nnimap-greeting nnimap-object) greeting)
446 (setf (nnimap-capabilities nnimap-object)
447 (mapcar #'upcase
448 (split-string capabilities)))
449 (unless (gnus-string-match-p "[*.] PREAUTH" greeting)
9f2d52e7
G
450 (if (not (setq credentials
451 (if (eq nnimap-authenticator 'anonymous)
452 (list "anonymous"
453 (message-make-address))
b8e0f0cd
G
454 ;; Look for the credentials based on
455 ;; the virtual server name and the address
456 (nnimap-credentials
7ba93e94
G
457 (gnus-delete-duplicates
458 (list
0bdb7b45
TZ
459 (nnoo-current-server 'nnimap)
460 nnimap-address))
5e68f861
TZ
461 ports
462 nnimap-user))))
9f2d52e7 463 (setq nnimap-object nil)
d5e9a4e9
LI
464 (let ((nnimap-inhibit-logging t))
465 (setq login-result
466 (nnimap-login (car credentials) (cadr credentials))))
733afdf4 467 (if (car login-result)
a5954fa5 468 (progn
b534ed40
LMI
469 ;; Save the credentials if a save function exists
470 ;; (such a function will only be passed if a new
471 ;; token was created).
a5954fa5
G
472 (when (functionp (nth 2 credentials))
473 (funcall (nth 2 credentials)))
474 ;; See if CAPABILITY is set as part of login
475 ;; response.
476 (dolist (response (cddr login-result))
477 (when (string= "CAPABILITY" (upcase (car response)))
478 (setf (nnimap-capabilities nnimap-object)
479 (mapcar #'upcase (cdr response))))))
9f2d52e7
G
480 ;; If the login failed, then forget the credentials
481 ;; that are now possibly cached.
482 (dolist (host (list (nnoo-current-server 'nnimap)
483 nnimap-address))
484 (dolist (port ports)
35123c04 485 (auth-source-forget+ :host host :port port)))
9f2d52e7
G
486 (delete-process (nnimap-process nnimap-object))
487 (setq nnimap-object nil))))
488 (when nnimap-object
389b76fa 489 (when (nnimap-capability "QRESYNC")
9f2d52e7 490 (nnimap-command "ENABLE QRESYNC"))
c8f8221f
SM
491 (nnheader-message 7 "Opening connection to %s...done"
492 nnimap-address)
6b958814
G
493 (nnimap-process nnimap-object))))))))
494
84d89ede
LMI
495(autoload 'rfc2104-hash "rfc2104")
496
497(defun nnimap-login (user password)
498 (cond
008cad90
G
499 ;; Prefer plain LOGIN if it's enabled (since it requires fewer
500 ;; round trips than CRAM-MD5, and it's less likely to be buggy),
501 ;; and we're using an encrypted connection.
502 ((and (not (nnimap-capability "LOGINDISABLED"))
45dd6480
LI
503 (eq (nnimap-stream-type nnimap-object) 'tls)
504 (or (null nnimap-authenticator)
505 (eq nnimap-authenticator 'login)))
008cad90 506 (nnimap-command "LOGIN %S %S" user password))
45dd6480
LI
507 ((and (nnimap-capability "AUTH=CRAM-MD5")
508 (or (null nnimap-authenticator)
509 (eq nnimap-authenticator 'cram-md5)))
84d89ede
LMI
510 (erase-buffer)
511 (let ((sequence (nnimap-send-command "AUTHENTICATE CRAM-MD5"))
512 (challenge (nnimap-wait-for-line "^\\+\\(.*\\)\n")))
513 (process-send-string
514 (get-buffer-process (current-buffer))
515 (concat
516 (base64-encode-string
517 (concat user " "
518 (rfc2104-hash 'md5 64 16 password
519 (base64-decode-string challenge))))
520 "\r\n"))
521 (nnimap-wait-for-response sequence)))
45dd6480
LI
522 ((and (not (nnimap-capability "LOGINDISABLED"))
523 (or (null nnimap-authenticator)
524 (eq nnimap-authenticator 'login)))
84d89ede 525 (nnimap-command "LOGIN %S %S" user password))
45dd6480
LI
526 ((and (nnimap-capability "AUTH=PLAIN")
527 (or (null nnimap-authenticator)
528 (eq nnimap-authenticator 'plain)))
84d89ede
LMI
529 (nnimap-command
530 "AUTHENTICATE PLAIN %s"
531 (base64-encode-string
532 (format "\000%s\000%s"
533 (nnimap-quote-specials user)
534 (nnimap-quote-specials password)))))))
535
99e65b2d
G
536(defun nnimap-quote-specials (string)
537 (with-temp-buffer
538 (insert string)
539 (goto-char (point-min))
540 (while (re-search-forward "[\\\"]" nil t)
541 (forward-char -1)
542 (insert "\\")
543 (forward-char 1))
544 (buffer-string)))
545
20a673b2
KY
546(defun nnimap-find-parameter (parameter elems)
547 (let (result)
548 (dolist (elem elems)
549 (cond
550 ((equal (car elem) parameter)
551 (setq result (cdr elem)))
552 ((and (equal (car elem) "OK")
553 (consp (cadr elem))
554 (equal (caadr elem) parameter))
555 (setq result (cdr (cadr elem))))))
556 result))
557
286c4fc2 558(deffoo nnimap-close-server (&optional server)
71e691a5
G
559 (when (nnoo-change-server 'nnimap server nil)
560 (ignore-errors
561 (delete-process (get-buffer-process (nnimap-buffer))))
d1090fe8 562 (nnoo-close-server 'nnimap server)
71e691a5 563 t))
c113de23 564
286c4fc2 565(deffoo nnimap-request-close ()
20a673b2 566 t)
23f87bed 567
286c4fc2 568(deffoo nnimap-server-opened (&optional server)
20a673b2
KY
569 (and (nnoo-current-server-p 'nnimap server)
570 nntp-server-buffer
571 (gnus-buffer-live-p nntp-server-buffer)
572 (nnimap-find-connection nntp-server-buffer)))
c113de23 573
286c4fc2 574(deffoo nnimap-status-message (&optional server)
20a673b2 575 nnimap-status-string)
c113de23 576
286c4fc2 577(deffoo nnimap-request-article (article &optional group server to-buffer)
e21bac42
G
578 (when group
579 (setq group (nnimap-decode-gnus-group group)))
c113de23 580 (with-current-buffer nntp-server-buffer
eaa610c3 581 (let ((result (nnimap-change-group group server))
8ccbef23 582 parts structure)
20a673b2 583 (when (stringp article)
eaa610c3 584 (setq article (nnimap-find-article-by-message-id group server article)))
20a673b2
KY
585 (when (and result
586 article)
587 (erase-buffer)
588 (with-current-buffer (nnimap-buffer)
589 (erase-buffer)
9f2d52e7
G
590 (when nnimap-fetch-partial-articles
591 (nnimap-command "UID FETCH %d (BODYSTRUCTURE)" article)
592 (goto-char (point-min))
593 (when (re-search-forward "FETCH.*BODYSTRUCTURE" nil t)
229b59da
G
594 (setq structure (ignore-errors
595 (let ((start (point)))
596 (forward-sexp 1)
597 (downcase-region start (point))
9d1bf25d 598 (goto-char start)
229b59da 599 (read (current-buffer))))
9f2d52e7 600 parts (nnimap-find-wanted-parts structure))))
8ccbef23
G
601 (when (if parts
602 (nnimap-get-partial-article article parts structure)
603 (nnimap-get-whole-article article))
604 (let ((buffer (current-buffer)))
605 (with-current-buffer (or to-buffer nntp-server-buffer)
9f5e78f7 606 (nnheader-insert-buffer-substring buffer)
b87f32fc
G
607 (nnheader-ms-strip-cr)))
608 (cons group article)))))))
8ccbef23 609
b5c575e6 610(deffoo nnimap-request-head (article &optional group server to-buffer)
e21bac42
G
611 (when group
612 (setq group (nnimap-decode-gnus-group group)))
eaa610c3 613 (when (nnimap-change-group group server)
b5c575e6
G
614 (with-current-buffer (nnimap-buffer)
615 (when (stringp article)
eaa610c3 616 (setq article (nnimap-find-article-by-message-id group server article)))
228724bc
LI
617 (if (null article)
618 nil
619 (nnimap-get-whole-article
620 article (format "UID FETCH %%d %s"
621 (nnimap-header-parameters)))
622 (let ((buffer (current-buffer)))
623 (with-current-buffer (or to-buffer nntp-server-buffer)
624 (erase-buffer)
625 (insert-buffer-substring buffer)
626 (nnheader-ms-strip-cr)
627 (cons group article)))))))
b5c575e6
G
628
629(defun nnimap-get-whole-article (article &optional command)
8ccbef23
G
630 (let ((result
631 (nnimap-command
b5c575e6
G
632 (or command
633 (if (nnimap-ver4-p)
634 "UID FETCH %d BODY.PEEK[]"
635 "UID FETCH %d RFC822.PEEK"))
8ccbef23
G
636 article)))
637 ;; Check that we really got an article.
638 (goto-char (point-min))
4478e074 639 (unless (re-search-forward "\\* [0-9]+ FETCH" nil t)
8ccbef23
G
640 (setq result nil))
641 (when result
4478e074
G
642 ;; Remove any data that may have arrived before the FETCH data.
643 (beginning-of-line)
644 (unless (bobp)
645 (delete-region (point-min) (point)))
8ccbef23
G
646 (let ((bytes (nnimap-get-length)))
647 (delete-region (line-beginning-position)
648 (progn (forward-line 1) (point)))
649 (goto-char (+ (point) bytes))
650 (delete-region (point) (point-max)))
651 t)))
652
389b76fa
G
653(defun nnimap-capability (capability)
654 (member capability (nnimap-capabilities nnimap-object)))
655
8ccbef23 656(defun nnimap-ver4-p ()
389b76fa 657 (nnimap-capability "IMAP4REV1"))
8ccbef23
G
658
659(defun nnimap-get-partial-article (article parts structure)
660 (let ((result
661 (nnimap-command
662 "UID FETCH %d (%s %s)"
663 article
664 (if (nnimap-ver4-p)
665 "BODY.PEEK[HEADER]"
666 "RFC822.HEADER")
667 (if (nnimap-ver4-p)
668 (mapconcat (lambda (part)
669 (format "BODY.PEEK[%s]" part))
670 parts " ")
671 (mapconcat (lambda (part)
672 (format "RFC822.PEEK[%s]" part))
673 parts " ")))))
674 (when result
675 (nnimap-convert-partial-article structure))))
676
677(defun nnimap-convert-partial-article (structure)
678 ;; First just skip past the headers.
679 (goto-char (point-min))
680 (let ((bytes (nnimap-get-length))
681 id parts)
682 ;; Delete "FETCH" line.
683 (delete-region (line-beginning-position)
684 (progn (forward-line 1) (point)))
685 (goto-char (+ (point) bytes))
686 ;; Collect all the body parts.
687 (while (looking-at ".*BODY\\[\\([.0-9]+\\)\\]")
688 (setq id (match-string 1)
c516cd6d 689 bytes (or (nnimap-get-length) 0))
8ccbef23
G
690 (beginning-of-line)
691 (delete-region (point) (progn (forward-line 1) (point)))
692 (push (list id (buffer-substring (point) (+ (point) bytes)))
693 parts)
694 (delete-region (point) (+ (point) bytes)))
695 ;; Delete trailing junk.
696 (delete-region (point) (point-max))
697 ;; Now insert all the parts again where they fit in the structure.
698 (nnimap-insert-partial-structure structure parts)
699 t))
700
701(defun nnimap-insert-partial-structure (structure parts &optional subp)
229b59da
G
702 (let (type boundary)
703 (let ((bstruc structure))
704 (while (consp (car bstruc))
705 (pop bstruc))
706 (setq type (car bstruc))
707 (setq bstruc (car (cdr bstruc)))
2526f423
G
708 (let ((has-boundary (member "boundary" bstruc)))
709 (when has-boundary
710 (setq boundary (cadr has-boundary)))))
8ccbef23
G
711 (when subp
712 (insert (format "Content-type: multipart/%s; boundary=%S\n\n"
713 (downcase type) boundary)))
714 (while (not (stringp (car structure)))
715 (insert "\n--" boundary "\n")
716 (if (consp (caar structure))
717 (nnimap-insert-partial-structure (pop structure) parts t)
718 (let ((bit (pop structure)))
943399bc
LMI
719 (insert (format "Content-type: %s/%s"
720 (downcase (nth 0 bit))
721 (downcase (nth 1 bit))))
722 (if (member-ignore-case "CHARSET" (nth 2 bit))
8ccbef23 723 (insert (format
943399bc
LMI
724 "; charset=%S\n"
725 (cadr (member-ignore-case "CHARSET" (nth 2 bit)))))
8ccbef23
G
726 (insert "\n"))
727 (insert (format "Content-transfer-encoding: %s\n"
728 (nth 5 bit)))
729 (insert "\n")
730 (when (assoc (nth 9 bit) parts)
731 (insert (cadr (assoc (nth 9 bit) parts)))))))
732 (insert "\n--" boundary "--\n")))
bdaa75c7
LMI
733
734(defun nnimap-find-wanted-parts (structure)
735 (message-flatten-list (nnimap-find-wanted-parts-1 structure "")))
736
737(defun nnimap-find-wanted-parts-1 (structure prefix)
738 (let ((num 1)
739 parts)
740 (while (consp (car structure))
741 (let ((sub (pop structure)))
742 (if (consp (car sub))
743 (push (nnimap-find-wanted-parts-1
744 sub (if (string= prefix "")
745 (number-to-string num)
746 (format "%s.%s" prefix num)))
747 parts)
8ccbef23
G
748 (let ((type (format "%s/%s" (nth 0 sub) (nth 1 sub)))
749 (id (if (string= prefix "")
bdaa75c7 750 (number-to-string num)
8ccbef23
G
751 (format "%s.%s" prefix num))))
752 (setcar (nthcdr 9 sub) id)
9f2d52e7
G
753 (when (if (eq nnimap-fetch-partial-articles t)
754 (equal id "1")
755 (string-match nnimap-fetch-partial-articles type))
8ccbef23
G
756 (push id parts))))
757 (incf num)))
bdaa75c7 758 (nreverse parts)))
20a673b2 759
e21bac42
G
760(defun nnimap-decode-gnus-group (group)
761 (decode-coding-string group 'utf-8))
762
286c4fc2 763(deffoo nnimap-request-group (group &optional server dont-check info)
e21bac42 764 (setq group (nnimap-decode-gnus-group group))
eaa610c3 765 (let ((result (nnimap-change-group
7cad71ad
G
766 ;; Don't SELECT the group if we're going to select it
767 ;; later, anyway.
bb7f5cbc 768 (if (and (not dont-check)
9310f19d 769 (assoc group nnimap-current-infos))
7cad71ad
G
770 nil
771 group)
772 server))
a46359d4
LMI
773 articles active marks high low)
774 (with-current-buffer nntp-server-buffer
20a673b2 775 (when result
286c4fc2
LMI
776 (if (and dont-check
777 (setq active (nth 2 (assoc group nnimap-current-infos))))
778 (insert (format "211 %d %d %d %S\n"
779 (- (cdr active) (car active))
780 (car active)
781 (cdr active)
782 group))
783 (with-current-buffer (nnimap-buffer)
784 (erase-buffer)
785 (let ((group-sequence
0617bb00 786 (nnimap-send-command "SELECT %S" (utf7-encode group t)))
286c4fc2
LMI
787 (flag-sequence
788 (nnimap-send-command "UID FETCH 1:* FLAGS")))
7cad71ad 789 (setf (nnimap-group nnimap-object) group)
286c4fc2
LMI
790 (nnimap-wait-for-response flag-sequence)
791 (setq marks
792 (nnimap-flags-to-marks
793 (nnimap-parse-flags
f7aa248a
G
794 (list (list group-sequence flag-sequence
795 1 group "SELECT")))))
796 (when (and info
797 marks)
9310f19d
LMI
798 (nnimap-update-infos marks (list info))
799 (nnimap-store-info info (gnus-active (gnus-info-group info))))
286c4fc2 800 (goto-char (point-max))
b1ae92ba 801 (let ((uidnext (nth 5 (car marks))))
a3f57c41
G
802 (setq high (or (if uidnext
803 (1- uidnext)
804 (nth 3 (car marks)))
805 0)
806 low (or (nth 4 (car marks)) uidnext 1)))))
286c4fc2
LMI
807 (erase-buffer)
808 (insert
809 (format
0617bb00
LMI
810 "211 %d %d %d %S\n" (1+ (- high low)) low high group)))
811 t))))
812
813(deffoo nnimap-request-create-group (group &optional server args)
e21bac42 814 (setq group (nnimap-decode-gnus-group group))
eaa610c3 815 (when (nnimap-change-group nil server)
0617bb00
LMI
816 (with-current-buffer (nnimap-buffer)
817 (car (nnimap-command "CREATE %S" (utf7-encode group t))))))
c113de23 818
a46359d4 819(deffoo nnimap-request-delete-group (group &optional force server)
e21bac42 820 (setq group (nnimap-decode-gnus-group group))
eaa610c3 821 (when (nnimap-change-group nil server)
a46359d4 822 (with-current-buffer (nnimap-buffer)
0617bb00
LMI
823 (car (nnimap-command "DELETE %S" (utf7-encode group t))))))
824
a7dcc87b 825(deffoo nnimap-request-rename-group (group new-name &optional server)
e21bac42 826 (setq group (nnimap-decode-gnus-group group))
eaa610c3 827 (when (nnimap-change-group nil server)
a7dcc87b 828 (with-current-buffer (nnimap-buffer)
e39a5583 829 (nnimap-unselect-group)
f7aa248a
G
830 (car (nnimap-command "RENAME %S %S"
831 (utf7-encode group t) (utf7-encode new-name t))))))
a7dcc87b 832
e39a5583
LMI
833(defun nnimap-unselect-group ()
834 ;; Make sure we don't have this group open read/write by asking
835 ;; to examine a mailbox that doesn't exist. This seems to be
836 ;; the only way that allows us to reliably go back to unselected
837 ;; state on Courier.
838 (nnimap-command "EXAMINE DOES.NOT.EXIST"))
839
0617bb00 840(deffoo nnimap-request-expunge-group (group &optional server)
e21bac42 841 (setq group (nnimap-decode-gnus-group group))
eaa610c3 842 (when (nnimap-change-group group server)
0617bb00
LMI
843 (with-current-buffer (nnimap-buffer)
844 (car (nnimap-command "EXPUNGE")))))
a46359d4 845
20a673b2
KY
846(defun nnimap-get-flags (spec)
847 (let ((articles nil)
f7aa248a 848 elems end)
20a673b2 849 (with-current-buffer (nnimap-buffer)
c113de23 850 (erase-buffer)
20a673b2
KY
851 (nnimap-wait-for-response (nnimap-send-command
852 "UID FETCH %s FLAGS" spec))
f7aa248a
G
853 (setq end (point))
854 (subst-char-in-region (point-min) (point-max)
855 ?\\ ?% t)
20a673b2 856 (goto-char (point-min))
f7aa248a
G
857 (while (search-forward " FETCH " end t)
858 (setq elems (read (current-buffer)))
859 (push (cons (cadr (memq 'UID elems))
860 (cadr (memq 'FLAGS elems)))
20a673b2
KY
861 articles)))
862 (nreverse articles)))
a1506d29 863
286c4fc2 864(deffoo nnimap-close-group (group &optional server)
20a673b2 865 t)
c113de23 866
01c52d31 867(deffoo nnimap-request-move-article (article group server accept-form
20a673b2 868 &optional last internal-move-group)
e21bac42 869 (setq group (nnimap-decode-gnus-group group))
eaa610c3
KY
870 (when internal-move-group
871 (setq internal-move-group (nnimap-decode-gnus-group internal-move-group)))
0617bb00 872 (with-temp-buffer
a04f9e26 873 (mm-disable-multibyte)
b5c575e6
G
874 (when (funcall (if internal-move-group
875 'nnimap-request-head
876 'nnimap-request-article)
877 article group server (current-buffer))
0617bb00
LMI
878 ;; If the move is internal (on the same server), just do it the easy
879 ;; way.
880 (let ((message-id (message-field-value "message-id")))
881 (if internal-move-group
882 (let ((result
883 (with-current-buffer (nnimap-buffer)
884 (nnimap-command "UID COPY %d %S"
885 article
886 (utf7-encode internal-move-group t)))))
887 (when (car result)
a46359d4 888 (nnimap-delete-article article)
0617bb00 889 (cons internal-move-group
cccb4b4c
LMI
890 (or (nnimap-find-uid-response "COPYUID" (cadr result))
891 (nnimap-find-article-by-message-id
eaa610c3
KY
892 internal-move-group server message-id
893 nnimap-request-articles-find-limit)))))
0617bb00
LMI
894 ;; Move the article to a different method.
895 (let ((result (eval accept-form)))
896 (when result
eaa610c3 897 (nnimap-change-group group server)
0617bb00
LMI
898 (nnimap-delete-article article)
899 result)))))))
20a673b2
KY
900
901(deffoo nnimap-request-expire-articles (articles group &optional server force)
e21bac42 902 (setq group (nnimap-decode-gnus-group group))
20a673b2 903 (cond
0617bb00
LMI
904 ((null articles)
905 nil)
eaa610c3 906 ((not (nnimap-change-group group server))
20a673b2 907 articles)
0617bb00
LMI
908 ((and force
909 (eq nnmail-expiry-target 'delete))
4478e074 910 (unless (nnimap-delete-article (gnus-compress-sequence articles))
283f7b93 911 (nnheader-message 7 "Article marked for deletion, but not expunged."))
20a673b2
KY
912 nil)
913 (t
0617bb00 914 (let ((deletable-articles
b069e5a6
G
915 (if (or force
916 (eq nnmail-expiry-wait 'immediate))
0617bb00
LMI
917 articles
918 (gnus-sorted-intersection
919 articles
920 (nnimap-find-expired-articles group)))))
921 (if (null deletable-articles)
922 articles
923 (if (eq nnmail-expiry-target 'delete)
4478e074 924 (nnimap-delete-article (gnus-compress-sequence deletable-articles))
0617bb00
LMI
925 (setq deletable-articles
926 (nnimap-process-expiry-targets
927 deletable-articles group server)))
928 ;; Return the articles we didn't delete.
929 (gnus-sorted-complement articles deletable-articles))))))
930
931(defun nnimap-process-expiry-targets (articles group server)
932 (let ((deleted-articles nil))
04db63bc
G
933 (cond
934 ;; shortcut further processing if we're going to delete the articles
935 ((eq nnmail-expiry-target 'delete)
936 (setq deleted-articles articles)
937 t)
938 ;; or just move them to another folder on the same IMAP server
939 ((and (not (functionp nnmail-expiry-target))
940 (gnus-server-equal (gnus-group-method nnmail-expiry-target)
941 (gnus-server-to-method
942 (format "nnimap:%s" server))))
eaa610c3 943 (and (nnimap-change-group group server)
04db63bc
G
944 (with-current-buffer (nnimap-buffer)
945 (nnheader-message 7 "Expiring articles from %s: %s" group articles)
946 (nnimap-command
947 "UID COPY %s %S"
948 (nnimap-article-ranges (gnus-compress-sequence articles))
949 (utf7-encode (gnus-group-real-name nnmail-expiry-target) t))
950 (setq deleted-articles articles)))
951 t)
952 (t
953 (dolist (article articles)
954 (let ((target nnmail-expiry-target))
955 (with-temp-buffer
956 (mm-disable-multibyte)
957 (when (nnimap-request-article article group server (current-buffer))
04db63bc
G
958 (when (functionp target)
959 (setq target (funcall target group)))
d0b36cbe
LMI
960 (if (and target
961 (not (eq target 'delete)))
962 (if (or (gnus-request-group target t)
963 (gnus-request-create-group target))
964 (progn
965 (nnmail-expiry-target-group target group)
966 (nnheader-message 7 "Expiring article %s:%d to %s"
967 group article target))
968 (setq target nil))
969 (nnheader-message 7 "Expiring article %s:%d" group article))
04db63bc
G
970 (when target
971 (push article deleted-articles))))))))
0617bb00 972 ;; Change back to the current group again.
eaa610c3 973 (nnimap-change-group group server)
0617bb00 974 (setq deleted-articles (nreverse deleted-articles))
4478e074 975 (nnimap-delete-article (gnus-compress-sequence deleted-articles))
0617bb00
LMI
976 deleted-articles))
977
978(defun nnimap-find-expired-articles (group)
979 (let ((cutoff (nnmail-expired-article-p group nil nil)))
980 (with-current-buffer (nnimap-buffer)
981 (let ((result
982 (nnimap-command
983 "UID SEARCH SENTBEFORE %s"
984 (format-time-string
985 (format "%%d-%s-%%Y"
986 (upcase
987 (car (rassoc (nth 4 (decode-time cutoff))
988 parse-time-months))))
989 cutoff))))
990 (and (car result)
991 (delete 0 (mapcar #'string-to-number
992 (cdr (assoc "SEARCH" (cdr result))))))))))
993
20a673b2 994
eaa610c3
KY
995(defun nnimap-find-article-by-message-id (group server message-id
996 &optional limit)
997 "Search for message with MESSAGE-ID in GROUP from SERVER.
998If LIMIT, first try to limit the search to the N last articles."
6b958814
G
999 (with-current-buffer (nnimap-buffer)
1000 (erase-buffer)
eaa610c3
KY
1001 (let* ((change-group-result (nnimap-change-group group server nil t))
1002 (number-of-article
1003 (and (listp change-group-result)
1004 (catch 'found
1005 (dolist (result (cdr change-group-result))
1006 (when (equal "EXISTS" (cadr result))
1007 (throw 'found (car result)))))))
1008 (sequence
1009 (nnimap-send-command
1010 "UID SEARCH%s HEADER Message-Id %S"
1011 (if (and limit number-of-article)
1012 ;; The -1 is because IMAP message
1013 ;; numbers are one-based rather than
1014 ;; zero-based.
1015 (format " %s:*" (- (string-to-number number-of-article)
1016 limit -1))
1017 "")
1018 message-id)))
1019 (when (nnimap-wait-for-response sequence)
1020 (let ((article (car (last (cdr (assoc "SEARCH"
1021 (nnimap-parse-response)))))))
1022 (if article
1023 (string-to-number article)
1024 (when (and limit number-of-article)
1025 (nnimap-find-article-by-message-id group server message-id))))))))
20a673b2
KY
1026
1027(defun nnimap-delete-article (articles)
1028 (with-current-buffer (nnimap-buffer)
1029 (nnimap-command "UID STORE %s +FLAGS.SILENT (\\Deleted)"
1030 (nnimap-article-ranges articles))
0617bb00 1031 (cond
389b76fa 1032 ((nnimap-capability "UIDPLUS")
0617bb00
LMI
1033 (nnimap-command "UID EXPUNGE %s"
1034 (nnimap-article-ranges articles))
1035 t)
1036 (nnimap-expunge
1037 (nnimap-command "EXPUNGE")
7390c1cd
TZ
1038 t)
1039 (t (gnus-message 7 (concat "nnimap: nnimap-expunge is not set and the "
1040 "server doesn't support UIDPLUS, so we won't "
1041 "delete this article now"))))))
20a673b2
KY
1042
1043(deffoo nnimap-request-scan (&optional group server)
e21bac42
G
1044 (when group
1045 (setq group (nnimap-decode-gnus-group group)))
eaa610c3 1046 (when (and (nnimap-change-group nil server)
20a673b2
KY
1047 nnimap-inbox
1048 nnimap-split-methods)
283f7b93 1049 (nnheader-message 7 "nnimap %s splitting mail..." server)
eaa610c3
KY
1050 (if (listp nnimap-inbox)
1051 (dolist (nnimap-inbox nnimap-inbox)
1052 (nnimap-split-incoming-mail))
1053 (nnimap-split-incoming-mail))
4d19331f 1054 (nnheader-message 7 "nnimap %s splitting mail...done" server)))
20a673b2
KY
1055
1056(defun nnimap-marks-to-flags (marks)
1057 (let (flags flag)
1058 (dolist (mark marks)
1059 (when (setq flag (cadr (assq mark nnimap-mark-alist)))
1060 (push flag flags)))
1061 flags))
1062
549c9aed 1063(deffoo nnimap-request-update-group-status (group status &optional server)
e21bac42 1064 (setq group (nnimap-decode-gnus-group group))
eaa610c3 1065 (when (nnimap-change-group nil server)
549c9aed
G
1066 (let ((command (assoc
1067 status
1068 '((subscribe "SUBSCRIBE")
1069 (unsubscribe "UNSUBSCRIBE")))))
1070 (when command
1071 (with-current-buffer (nnimap-buffer)
1072 (nnimap-command "%s %S" (cadr command) (utf7-encode group t)))))))
1073
286c4fc2 1074(deffoo nnimap-request-set-mark (group actions &optional server)
e21bac42 1075 (setq group (nnimap-decode-gnus-group group))
eaa610c3 1076 (when (nnimap-change-group group server)
20a673b2
KY
1077 (let (sequence)
1078 (with-current-buffer (nnimap-buffer)
229b59da 1079 (erase-buffer)
20a673b2
KY
1080 ;; Just send all the STORE commands without waiting for
1081 ;; response. If they're successful, they're successful.
1082 (dolist (action actions)
1083 (destructuring-bind (range action marks) action
1084 (let ((flags (nnimap-marks-to-flags marks)))
1085 (when flags
1086 (setq sequence (nnimap-send-command
1087 "UID STORE %s %sFLAGS.SILENT (%s)"
1088 (nnimap-article-ranges range)
5f285722
LMI
1089 (cond
1090 ((eq action 'del) "-")
57cc52be 1091 ((eq action 'add) "+")
5f285722 1092 ((eq action 'set) ""))
20a673b2
KY
1093 (mapconcat #'identity flags " ")))))))
1094 ;; Wait for the last command to complete to avoid later
e1dbe924 1095 ;; synchronization problems with the stream.
a46359d4
LMI
1096 (when sequence
1097 (nnimap-wait-for-response sequence))))))
a1506d29 1098
c113de23 1099(deffoo nnimap-request-accept-article (group &optional server last)
e21bac42 1100 (setq group (nnimap-decode-gnus-group group))
eaa610c3 1101 (when (nnimap-change-group nil server)
20a673b2 1102 (nnmail-check-syntax)
6b958814
G
1103 (let ((message-id (message-field-value "message-id"))
1104 sequence message)
1105 (nnimap-add-cr)
728fd3b9 1106 (setq message (buffer-substring-no-properties (point-min) (point-max)))
20a673b2 1107 (with-current-buffer (nnimap-buffer)
114fe546
G
1108 (when (setq message (or (nnimap-process-quirk "OK Gimap " 'append message)
1109 message))
41d579ce
LI
1110 ;; If we have this group open read-only, then unselect it
1111 ;; before appending to it.
1112 (when (equal (nnimap-examined nnimap-object) group)
1113 (nnimap-unselect-group))
1114 (erase-buffer)
1115 (setq sequence (nnimap-send-command
1116 "APPEND %S {%d}" (utf7-encode group t)
1117 (length message)))
1118 (unless nnimap-streaming
1119 (nnimap-wait-for-connection "^[+]"))
1120 (process-send-string (get-buffer-process (current-buffer)) message)
1121 (process-send-string (get-buffer-process (current-buffer))
1122 (if (nnimap-newlinep nnimap-object)
1123 "\n"
1124 "\r\n"))
1125 (let ((result (nnimap-get-response sequence)))
1126 (if (not (nnimap-ok-p result))
1127 (progn
1128 (nnheader-report 'nnimap "%s" result)
1129 nil)
1130 (cons group
1131 (or (nnimap-find-uid-response "APPENDUID" (car result))
1132 (nnimap-find-article-by-message-id
eaa610c3
KY
1133 group server message-id
1134 nnimap-request-articles-find-limit))))))))))
41d579ce
LI
1135
1136(defun nnimap-process-quirk (greeting-match type data)
1137 (when (and (nnimap-greeting nnimap-object)
114fe546 1138 (string-match greeting-match (nnimap-greeting nnimap-object))
41d579ce
LI
1139 (eq type 'append)
1140 (string-match "\000" data))
1141 (let ((choice (gnus-multiple-choice
1142 "Message contains NUL characters. Delete, continue, abort? "
1143 '((?d "Delete NUL characters")
1144 (?c "Try to APPEND the message as is")
1145 (?a "Abort")))))
1146 (cond
1147 ((eq choice ?a)
1148 (nnheader-report 'nnimap "Aborted APPEND due to NUL characters"))
1149 ((eq choice ?c)
1150 data)
1151 (t
1152 (with-temp-buffer
1153 (insert data)
1154 (goto-char (point-min))
1155 (while (search-forward "\000" nil t)
1156 (replace-match "" t t))
1157 (buffer-string)))))))
cccb4b4c 1158
0d1c2cc8
G
1159(defun nnimap-ok-p (value)
1160 (and (consp value)
1161 (consp (car value))
1162 (equal (caar value) "OK")))
1163
cccb4b4c 1164(defun nnimap-find-uid-response (name list)
17dd2281 1165 (let ((result (car (last (nnimap-find-response-element name list)))))
cccb4b4c
LMI
1166 (and result
1167 (string-to-number result))))
1168
1169(defun nnimap-find-response-element (name list)
1170 (let (result)
1171 (dolist (elem list)
1172 (when (and (consp elem)
1173 (equal name (car elem)))
1174 (setq result elem)))
1175 result))
20a673b2 1176
728fd3b9 1177(deffoo nnimap-request-replace-article (article group buffer)
e21bac42 1178 (setq group (nnimap-decode-gnus-group group))
728fd3b9 1179 (let (group-art)
eaa610c3 1180 (when (and (nnimap-change-group group)
728fd3b9
LMI
1181 ;; Put the article into the group.
1182 (with-current-buffer buffer
1183 (setq group-art
1184 (nnimap-request-accept-article group nil t))))
1185 (nnimap-delete-article (list article))
1186 ;; Return the new article number.
1187 (cdr group-art))))
1188
20a673b2
KY
1189(defun nnimap-add-cr ()
1190 (goto-char (point-min))
1191 (while (re-search-forward "\r?\n" nil t)
1192 (replace-match "\r\n" t t)))
1193
1194(defun nnimap-get-groups ()
cccb4b4c
LMI
1195 (erase-buffer)
1196 (let ((sequence (nnimap-send-command "LIST \"\" \"*\""))
20a673b2 1197 groups)
cccb4b4c
LMI
1198 (nnimap-wait-for-response sequence)
1199 (subst-char-in-region (point-min) (point-max)
1200 ?\\ ?% t)
1201 (goto-char (point-min))
1202 (nnimap-unfold-quoted-lines)
1203 (goto-char (point-min))
1204 (while (search-forward "* LIST " nil t)
1205 (let ((flags (read (current-buffer)))
1206 (separator (read (current-buffer)))
1207 (group (read (current-buffer))))
1208 (unless (member '%NoSelect flags)
bca46f6b
G
1209 (push (utf7-decode (if (stringp group)
1210 group
1211 (format "%s" group)) t)
36af6c65 1212 groups))))
cccb4b4c 1213 (nreverse groups)))
20a673b2 1214
eaa610c3
KY
1215(defun nnimap-get-responses (sequences)
1216 (let (responses)
1217 (dolist (sequence sequences)
1218 (goto-char (point-min))
1219 (when (re-search-forward (format "^%d " sequence) nil t)
1220 (push (list sequence (nnimap-parse-response))
1221 responses)))
1222 responses))
1223
286c4fc2 1224(deffoo nnimap-request-list (&optional server)
eaa610c3 1225 (when (nnimap-change-group nil server)
c13bc26b
LI
1226 (with-current-buffer nntp-server-buffer
1227 (erase-buffer)
1228 (let ((groups
1229 (with-current-buffer (nnimap-buffer)
1230 (nnimap-get-groups)))
1231 sequences responses)
1232 (when groups
1233 (with-current-buffer (nnimap-buffer)
1234 (setf (nnimap-group nnimap-object) nil)
1235 (dolist (group groups)
1236 (setf (nnimap-examined nnimap-object) group)
1237 (push (list (nnimap-send-command "EXAMINE %S"
1238 (utf7-encode group t))
1239 group)
1240 sequences))
1241 (nnimap-wait-for-response (caar sequences))
1242 (setq responses
1243 (nnimap-get-responses (mapcar #'car sequences))))
1244 (dolist (response responses)
1245 (let* ((sequence (car response))
1246 (response (cadr response))
75a4d9cc
LI
1247 (group (cadr (assoc sequence sequences)))
1248 (egroup (encode-coding-string group 'utf-8)))
c13bc26b
LI
1249 (when (and group
1250 (equal (caar response) "OK"))
1251 (let ((uidnext (nnimap-find-parameter "UIDNEXT" response))
1252 highest exists)
1253 (dolist (elem response)
1254 (when (equal (cadr elem) "EXISTS")
1255 (setq exists (string-to-number (car elem)))))
1256 (when uidnext
1257 (setq highest (1- (string-to-number (car uidnext)))))
1258 (cond
1259 ((null highest)
75a4d9cc 1260 (insert (format "%S 0 1 y\n" egroup)))
c13bc26b
LI
1261 ((zerop exists)
1262 ;; Empty group.
75a4d9cc 1263 (insert (format "%S %d %d y\n" egroup
c13bc26b
LI
1264 highest (1+ highest))))
1265 (t
1266 ;; Return the widest possible range.
75a4d9cc 1267 (insert (format "%S %d 1 y\n" egroup
c13bc26b
LI
1268 (or highest exists)))))))))
1269 t)))))
c113de23 1270
a3f57c41 1271(deffoo nnimap-request-newgroups (date &optional server)
eaa610c3 1272 (when (nnimap-change-group nil server)
c13bc26b
LI
1273 (with-current-buffer nntp-server-buffer
1274 (erase-buffer)
1275 (dolist (group (with-current-buffer (nnimap-buffer)
1276 (nnimap-get-groups)))
1277 (unless (assoc group nnimap-current-infos)
1278 ;; Insert dummy numbers here -- they don't matter.
75a4d9cc 1279 (insert (format "%S 0 1 y\n" (encode-coding-string group 'utf-8)))))
c13bc26b 1280 t)))
a3f57c41 1281
286c4fc2 1282(deffoo nnimap-retrieve-group-data-early (server infos)
eaa610c3 1283 (when (and (nnimap-change-group nil server)
964646c4 1284 infos)
20a673b2 1285 (with-current-buffer (nnimap-buffer)
f7aa248a
G
1286 (erase-buffer)
1287 (setf (nnimap-group nnimap-object) nil)
b5235dd9 1288 (setf (nnimap-initial-resync nnimap-object) 0)
389b76fa 1289 (let ((qresyncp (nnimap-capability "QRESYNC"))
eaa610c3
KY
1290 params groups sequences active uidvalidity modseq group
1291 unexist)
20a673b2
KY
1292 ;; Go through the infos and gather the data needed to know
1293 ;; what and how to request the data.
1294 (dolist (info infos)
f7aa248a 1295 (setq params (gnus-info-params info)
e21bac42
G
1296 group (nnimap-decode-gnus-group
1297 (gnus-group-real-name (gnus-info-group info)))
f7aa248a 1298 active (cdr (assq 'active params))
eaa610c3 1299 unexist (assq 'unexist (gnus-info-marks info))
f7aa248a
G
1300 uidvalidity (cdr (assq 'uidvalidity params))
1301 modseq (cdr (assq 'modseq params)))
e39a5583 1302 (setf (nnimap-examined nnimap-object) group)
20a673b2 1303 (if (and qresyncp
f7aa248a 1304 uidvalidity
4a3988d5 1305 active
eaa610c3
KY
1306 modseq
1307 unexist)
20a673b2 1308 (push
549c9aed 1309 (list (nnimap-send-command "EXAMINE %S (%s (%s %s))"
dab0271f 1310 (utf7-encode group t)
549c9aed 1311 (nnimap-quirk "QRESYNC")
dab0271f 1312 uidvalidity modseq)
f7aa248a
G
1313 'qresync
1314 nil group 'qresync)
20a673b2 1315 sequences)
b7645a9d 1316 (let ((command
f7aa248a
G
1317 (if uidvalidity
1318 "EXAMINE"
1319 ;; If we don't have a UIDVALIDITY, then this is
1320 ;; the first time we've seen the group, so we
1321 ;; have to do a SELECT (which is slower than an
1322 ;; examine), but will tell us whether the group
1323 ;; is read-only or not.
b7645a9d
LI
1324 "SELECT"))
1325 start)
eaa610c3 1326 (if (and active uidvalidity unexist)
b7645a9d
LI
1327 ;; Fetch the last 100 flags.
1328 (setq start (max 1 (- (cdr active) 100)))
eaa610c3 1329 (incf (nnimap-initial-resync nnimap-object))
b7645a9d 1330 (setq start 1))
dab0271f
G
1331 (push (list (nnimap-send-command "%s %S" command
1332 (utf7-encode group t))
20a673b2 1333 (nnimap-send-command "UID FETCH %d:* FLAGS" start)
f7aa248a 1334 start group command)
b5c575e6 1335 sequences))))
20a673b2
KY
1336 sequences))))
1337
549c9aed
G
1338(defun nnimap-quirk (command)
1339 (let ((quirk (assoc command nnimap-quirks)))
1340 ;; If this server is of a type that matches a quirk, then return
1341 ;; the "quirked" command instead of the proper one.
1342 (if (or (null quirk)
1343 (not (string-match (nth 1 quirk) (nnimap-greeting nnimap-object))))
1344 command
1345 (nth 2 quirk))))
1346
286c4fc2 1347(deffoo nnimap-finish-retrieve-group-infos (server infos sequences)
20a673b2 1348 (when (and sequences
eaa610c3 1349 (nnimap-change-group nil server t)
3451795c
LMI
1350 ;; Check that the process is still alive.
1351 (get-buffer-process (nnimap-buffer))
1352 (memq (process-status (get-buffer-process (nnimap-buffer)))
7e67562f 1353 '(open run)))
20a673b2
KY
1354 (with-current-buffer (nnimap-buffer)
1355 ;; Wait for the final data to trickle in.
f7aa248a
G
1356 (when (nnimap-wait-for-response (if (eq (cadar sequences) 'qresync)
1357 (caar sequences)
1358 (cadar sequences))
1359 t)
1360 ;; Now we should have most of the data we need, no matter
1361 ;; whether we're QRESYNCING, fetching all the flags from
1362 ;; scratch, or just fetching the last 100 flags per group.
8ccbef23
G
1363 (nnimap-update-infos (nnimap-flags-to-marks
1364 (nnimap-parse-flags
1365 (nreverse sequences)))
1366 infos)
1367 ;; Finally, just return something resembling an active file in
1368 ;; the nntp buffer, so that the agent can save the info, too.
1369 (with-current-buffer nntp-server-buffer
1370 (erase-buffer)
1371 (dolist (info infos)
1372 (let* ((group (gnus-info-group info))
1373 (active (gnus-active group)))
1374 (when active
1375 (insert (format "%S %d %d y\n"
e21bac42
G
1376 (decode-coding-string
1377 (gnus-group-real-name group) 'utf-8)
8ccbef23
G
1378 (cdr active)
1379 (car active)))))))))))
20a673b2
KY
1380
1381(defun nnimap-update-infos (flags infos)
1382 (dolist (info infos)
e21bac42
G
1383 (let* ((group (nnimap-decode-gnus-group
1384 (gnus-group-real-name (gnus-info-group info))))
f7aa248a
G
1385 (marks (cdr (assoc group flags))))
1386 (when marks
1387 (nnimap-update-info info marks)))))
20a673b2
KY
1388
1389(defun nnimap-update-info (info marks)
f7aa248a
G
1390 (destructuring-bind (existing flags high low uidnext start-article
1391 permanent-flags uidvalidity
1392 vanished highestmodseq) marks
1393 (cond
1394 ;; Ignore groups with no UIDNEXT/marks. This happens for
1395 ;; completely empty groups.
1396 ((and (not existing)
1397 (not uidnext))
dab0271f
G
1398 (let ((active (cdr (assq 'active (gnus-info-params info)))))
1399 (when active
1400 (gnus-set-active (gnus-info-group info) active))))
f7aa248a
G
1401 ;; We have a mismatch between the old and new UIDVALIDITY
1402 ;; identifiers, so we have to re-request the group info (the next
1403 ;; time). This virtually never happens.
1404 ((let ((old-uidvalidity
1405 (cdr (assq 'uidvalidity (gnus-info-params info)))))
1406 (and old-uidvalidity
1407 (not (equal old-uidvalidity uidvalidity))
7e67562f
G
1408 (or (not start-article)
1409 (> start-article 1))))
f7aa248a
G
1410 (gnus-group-remove-parameter info 'uidvalidity)
1411 (gnus-group-remove-parameter info 'modseq))
1412 ;; We have the data needed to update.
1413 (t
dab0271f
G
1414 (let* ((group (gnus-info-group info))
1415 (completep (and start-article
1416 (= start-article 1)))
1417 (active (or (gnus-active group)
1418 (cdr (assq 'active (gnus-info-params info))))))
b1ae92ba
G
1419 (when uidnext
1420 (setq high (1- uidnext)))
20a673b2
KY
1421 ;; First set the active ranges based on high/low.
1422 (if (or completep
1423 (not (gnus-active group)))
1424 (gnus-set-active group
61b1af82 1425 (cond
a7f6e5b9
LMI
1426 (active
1427 (cons (min (or low (car active))
1428 (car active))
1429 (max (or high (cdr active))
1430 (cdr active))))
61b1af82
G
1431 ((and low high)
1432 (cons low high))
1433 (uidnext
20a673b2 1434 ;; No articles in this group.
61b1af82
G
1435 (cons uidnext (1- uidnext)))
1436 (start-article
1437 (cons start-article (1- start-article)))
1438 (t
1439 ;; No articles and no uidnext.
1440 nil)))
41d579ce
LI
1441 (gnus-set-active group
1442 (cons (car active)
1443 (or high (1- uidnext)))))
f7aa248a
G
1444 ;; See whether this is a read-only group.
1445 (unless (eq permanent-flags 'not-scanned)
1446 (gnus-group-set-parameter
1447 info 'permanent-flags
7cad71ad
G
1448 (and (or (memq '%* permanent-flags)
1449 (memq '%Seen permanent-flags))
1450 permanent-flags)))
f7aa248a
G
1451 ;; Update marks and read articles if this isn't a
1452 ;; read-only IMAP group.
7cad71ad
G
1453 (when (setq permanent-flags
1454 (cdr (assq 'permanent-flags (gnus-info-params info))))
f7aa248a
G
1455 (if (and highestmodseq
1456 (not start-article))
1457 ;; We've gotten the data by QRESYNCing.
1458 (nnimap-update-qresync-info
dab0271f 1459 info existing (nnimap-imap-ranges-to-gnus-ranges vanished) flags)
f7aa248a
G
1460 ;; Do normal non-QRESYNC flag updates.
1461 ;; Update the list of read articles.
1462 (let* ((unread
1463 (gnus-compress-sequence
1464 (gnus-set-difference
1465 (gnus-set-difference
1466 existing
9d9ffa13
LMI
1467 (gnus-sorted-union
1468 (cdr (assoc '%Seen flags))
1469 (cdr (assoc '%Deleted flags))))
f7aa248a
G
1470 (cdr (assoc '%Flagged flags)))))
1471 (read (gnus-range-difference
1472 (cons start-article high) unread)))
1473 (when (> start-article 1)
1474 (setq read
1475 (gnus-range-nconcat
1476 (if (> start-article 1)
1477 (gnus-sorted-range-intersection
1478 (cons 1 (1- start-article))
1479 (gnus-info-read info))
1480 (gnus-info-read info))
1481 read)))
7cad71ad
G
1482 (when (or (not (listp permanent-flags))
1483 (memq '%Seen permanent-flags))
1484 (gnus-info-set-read info read))
f7aa248a
G
1485 ;; Update the marks.
1486 (setq marks (gnus-info-marks info))
1487 (dolist (type (cdr nnimap-mark-alist))
7cad71ad 1488 (when (or (not (listp permanent-flags))
1e961f10
KAH
1489 (memq (car (assoc (caddr type) flags))
1490 permanent-flags)
7cad71ad
G
1491 (memq '%* permanent-flags))
1492 (let ((old-marks (assoc (car type) marks))
1493 (new-marks
1494 (gnus-compress-sequence
1495 (cdr (or (assoc (caddr type) flags) ; %Flagged
1496 (assoc (intern (cadr type) obarray) flags)
1497 (assoc (cadr type) flags)))))) ; "\Flagged"
1498 (setq marks (delq old-marks marks))
1499 (pop old-marks)
1500 (when (and old-marks
1501 (> start-article 1))
1502 (setq old-marks (gnus-range-difference
1503 old-marks
1504 (cons start-article high)))
1505 (setq new-marks (gnus-range-nconcat old-marks new-marks)))
1506 (when new-marks
1507 (push (cons (car type) new-marks) marks)))))
eaa610c3
KY
1508 ;; Keep track of non-existing articles.
1509 (let* ((old-unexists (assq 'unexist marks))
1510 (active (gnus-active group))
1511 (unexists
1512 (if completep
1513 (gnus-range-difference
1514 active
1515 (gnus-compress-sequence existing))
1516 (gnus-add-to-range
1517 (cdr old-unexists)
1518 (gnus-list-range-difference
1519 existing (gnus-active group))))))
1520 (when (> (car active) 1)
1521 (setq unexists (gnus-range-add
1522 (cons 1 (1- (car active)))
1523 unexists)))
1524 (if old-unexists
1525 (setcdr old-unexists unexists)
1526 (push (cons 'unexist unexists) marks)))
7cad71ad 1527 (gnus-info-set-marks info marks t))))
41d579ce
LI
1528 ;; Tell Gnus whether there are any \Recent messages in any of
1529 ;; the groups.
1530 (let ((recent (cdr (assoc '%Recent flags))))
b52daf3d
LI
1531 (when (and active
1532 recent
1533 (> (car (last recent)) (cdr active)))
1534 (push (list (cons (gnus-group-real-name group) 0))
1535 nnmail-split-history)))
f7aa248a
G
1536 ;; Note the active level for the next run-through.
1537 (gnus-group-set-parameter info 'active (gnus-active group))
1538 (gnus-group-set-parameter info 'uidvalidity uidvalidity)
1539 (gnus-group-set-parameter info 'modseq highestmodseq)
1540 (nnimap-store-info info (gnus-active group)))))))
1541
dab0271f 1542(defun nnimap-update-qresync-info (info existing vanished flags)
f7aa248a
G
1543 ;; Add all the vanished articles to the list of read articles.
1544 (gnus-info-set-read
1545 info
dab0271f
G
1546 (gnus-add-to-range
1547 (gnus-add-to-range
1548 (gnus-range-add (gnus-info-read info)
1549 vanished)
1550 (cdr (assq '%Flagged flags)))
1551 (cdr (assq '%Seen flags))))
1552 (let ((marks (gnus-info-marks info)))
1553 (dolist (type (cdr nnimap-mark-alist))
1554 (let ((ticks (assoc (car type) marks))
1555 (new-marks
1556 (cdr (or (assoc (caddr type) flags) ; %Flagged
1557 (assoc (intern (cadr type) obarray) flags)
1558 (assoc (cadr type) flags))))) ; "\Flagged"
1559 (setq marks (delq ticks marks))
1560 (pop ticks)
1561 ;; Add the new marks we got.
1562 (setq ticks (gnus-add-to-range ticks new-marks))
1563 ;; Remove the marks from messages that don't have them.
1564 (setq ticks (gnus-remove-from-range
1565 ticks
1566 (gnus-compress-sequence
1567 (gnus-sorted-complement existing new-marks))))
1568 (when ticks
1569 (push (cons (car type) ticks) marks)))
eaa610c3
KY
1570 (gnus-info-set-marks info marks t))
1571 ;; Add vanished to the list of unexisting articles.
1572 (when vanished
1573 (let* ((old-unexists (assq 'unexist marks))
1574 (unexists (gnus-range-add (cdr old-unexists) vanished)))
1575 (if old-unexists
1576 (setcdr old-unexists unexists)
1577 (push (cons 'unexist unexists) marks)))
dab0271f 1578 (gnus-info-set-marks info marks t))))
f7aa248a
G
1579
1580(defun nnimap-imap-ranges-to-gnus-ranges (irange)
1581 (if (zerop (length irange))
1582 nil
1583 (let ((result nil))
1584 (dolist (elem (split-string irange ","))
1585 (push
1586 (if (string-match ":" elem)
1587 (let ((numbers (split-string elem ":")))
1588 (cons (string-to-number (car numbers))
1589 (string-to-number (cadr numbers))))
1590 (string-to-number elem))
1591 result))
1592 (nreverse result))))
286c4fc2
LMI
1593
1594(defun nnimap-store-info (info active)
1595 (let* ((group (gnus-group-real-name (gnus-info-group info)))
1596 (entry (assoc group nnimap-current-infos)))
1597 (if entry
1598 (setcdr entry (list info active))
1599 (push (list group info active) nnimap-current-infos))))
20a673b2
KY
1600
1601(defun nnimap-flags-to-marks (groups)
f7aa248a
G
1602 (let (data group totalp uidnext articles start-article mark permanent-flags
1603 uidvalidity vanished highestmodseq)
20a673b2
KY
1604 (dolist (elem groups)
1605 (setq group (car elem)
b069e5a6
G
1606 uidnext (nth 1 elem)
1607 start-article (nth 2 elem)
1608 permanent-flags (nth 3 elem)
f7aa248a
G
1609 uidvalidity (nth 4 elem)
1610 vanished (nth 5 elem)
1611 highestmodseq (nth 6 elem)
1612 articles (nthcdr 7 elem))
20a673b2
KY
1613 (let ((high (caar articles))
1614 marks low existing)
1615 (dolist (article articles)
1616 (setq low (car article))
1617 (push (car article) existing)
1618 (dolist (flag (cdr article))
1619 (setq mark (assoc flag marks))
1620 (if (not mark)
1621 (push (list flag (car article)) marks)
b069e5a6
G
1622 (setcdr mark (cons (car article) (cdr mark))))))
1623 (push (list group existing marks high low uidnext start-article
f7aa248a 1624 permanent-flags uidvalidity vanished highestmodseq)
b069e5a6 1625 data)))
20a673b2
KY
1626 data))
1627
1628(defun nnimap-parse-flags (sequences)
1629 (goto-char (point-min))
4eff9c1a
LI
1630 ;; Change \Delete etc to %Delete, so that the Emacs Lisp reader can
1631 ;; read it.
b069e5a6
G
1632 (subst-char-in-region (point-min) (point-max)
1633 ?\\ ?% t)
a123622d
G
1634 ;; Remove any MODSEQ entries in the buffer, because they may contain
1635 ;; numbers that are too large for 32-bit Emacsen.
1636 (while (re-search-forward " MODSEQ ([0-9]+)" nil t)
1637 (replace-match "" t t))
1638 (goto-char (point-min))
f7aa248a
G
1639 (let (start end articles groups uidnext elems permanent-flags
1640 uidvalidity vanished highestmodseq)
20a673b2 1641 (dolist (elem sequences)
f7aa248a
G
1642 (destructuring-bind (group-sequence flag-sequence totalp group command)
1643 elem
b069e5a6 1644 (setq start (point))
f7aa248a
G
1645 (when (and
1646 ;; The EXAMINE was successful.
1647 (search-forward (format "\n%d OK " group-sequence) nil t)
1648 (progn
1649 (forward-line 1)
1650 (setq end (point))
1651 (goto-char start)
1652 (setq permanent-flags
1653 (if (equal command "SELECT")
b069e5a6 1654 (and (search-forward "PERMANENTFLAGS "
f7aa248a
G
1655 (or end (point-min)) t)
1656 (read (current-buffer)))
1657 'not-scanned))
1658 (goto-char start)
1659 (setq uidnext
1660 (and (search-forward "UIDNEXT "
1661 (or end (point-min)) t)
1662 (read (current-buffer))))
1663 (goto-char start)
1664 (setq uidvalidity
1665 (and (re-search-forward "UIDVALIDITY \\([0-9]+\\)"
1666 (or end (point-min)) t)
1667 ;; Store UIDVALIDITY as a string, as it's
1668 ;; too big for 32-bit Emacsen, usually.
1669 (match-string 1)))
1670 (goto-char start)
1671 (setq vanished
1672 (and (eq flag-sequence 'qresync)
a5166359 1673 (re-search-forward "^\\* VANISHED .*? \\([0-9:,]+\\)"
f7aa248a
G
1674 (or end (point-min)) t)
1675 (match-string 1)))
1676 (goto-char start)
1677 (setq highestmodseq
a123622d 1678 (and (re-search-forward "HIGHESTMODSEQ \\([0-9]+\\)"
f7aa248a 1679 (or end (point-min)) t)
a123622d 1680 (match-string 1)))
f7aa248a
G
1681 (goto-char end)
1682 (forward-line -1))
1683 ;; The UID FETCH FLAGS was successful.
1684 (or (eq flag-sequence 'qresync)
1685 (search-forward (format "\n%d OK " flag-sequence) nil t)))
1686 (if (eq flag-sequence 'qresync)
1687 (progn
1688 (goto-char start)
1689 (setq start end))
1690 (setq start (point))
1691 (goto-char end))
a1d16a7b 1692 (while (re-search-forward "^\\* [0-9]+ FETCH " start t)
71f8b7ed 1693 (let ((p (point)))
a123622d 1694 (setq elems (read (current-buffer)))
71f8b7ed
G
1695 (push (cons (cadr (memq 'UID elems))
1696 (cadr (memq 'FLAGS elems)))
1697 articles)))
f7aa248a
G
1698 (push (nconc (list group uidnext totalp permanent-flags uidvalidity
1699 vanished highestmodseq)
1700 articles)
b069e5a6 1701 groups)
4eff9c1a
LI
1702 (if (eq flag-sequence 'qresync)
1703 (goto-char end)
1704 (setq end (point)))
20a673b2
KY
1705 (setq articles nil))))
1706 groups))
1707
1708(defun nnimap-find-process-buffer (buffer)
1709 (cadr (assoc buffer nnimap-connection-alist)))
1710
286c4fc2 1711(deffoo nnimap-request-post (&optional server)
20a673b2
KY
1712 (setq nnimap-status-string "Read-only server")
1713 nil)
c113de23 1714
eaa610c3 1715(defvar gnus-refer-thread-use-nnir) ;; gnus-sum.el
5a6a61f7
GM
1716(declare-function gnus-fetch-headers "gnus-sum"
1717 (articles &optional limit force-new dependencies))
1718
18cd34c1
LI
1719(autoload 'nnir-search-thread "nnir")
1720
0c136286 1721(deffoo nnimap-request-thread (header &optional group server)
e21bac42
G
1722 (when group
1723 (setq group (nnimap-decode-gnus-group group)))
bca46f6b 1724 (if gnus-refer-thread-use-nnir
47f0b35e 1725 (nnir-search-thread header)
eaa610c3 1726 (when (nnimap-change-group group server)
47f0b35e
AC
1727 (let* ((cmd (nnimap-make-thread-query header))
1728 (result (with-current-buffer (nnimap-buffer)
1729 (nnimap-command "UID SEARCH %s" cmd))))
1730 (when result
1731 (gnus-fetch-headers
7e67562f
G
1732 (and (car result)
1733 (delete 0 (mapcar #'string-to-number
1734 (cdr (assoc "SEARCH" (cdr result))))))
47f0b35e 1735 nil t))))))
030158f3 1736
eaa610c3
KY
1737(defun nnimap-change-group (group &optional server no-reconnect read-only)
1738 "Change group to GROUP if non-nil.
1739If SERVER is set, check that server is connected, otherwise retry
1740to reconnect, unless NO-RECONNECT is set to t. Return nil if
1741unsuccessful in connecting.
1742If GROUP is nil, return t.
1743If READ-ONLY is set, send EXAMINE rather than SELECT to the server.
1744Return the server's response to the SELECT or EXAMINE command."
20a673b2
KY
1745 (let ((open-result t))
1746 (when (and server
1747 (not (nnimap-server-opened server)))
98366438 1748 (setq open-result (nnimap-open-server server nil no-reconnect)))
20a673b2
KY
1749 (cond
1750 ((not open-result)
1751 nil)
1752 ((not group)
1753 t)
1754 (t
1755 (with-current-buffer (nnimap-buffer)
eaa610c3
KY
1756 (let ((result (nnimap-command "%s %S"
1757 (if read-only
1758 "EXAMINE"
1759 "SELECT")
1760 (utf7-encode group t))))
1761 (when (car result)
1762 (setf (nnimap-group nnimap-object) group
1763 (nnimap-select-result nnimap-object) result)
1764 result)))))))
20a673b2
KY
1765
1766(defun nnimap-find-connection (buffer)
1767 "Find the connection delivering to BUFFER."
1768 (let ((entry (assoc buffer nnimap-connection-alist)))
1769 (when entry
1770 (if (and (buffer-name (cadr entry))
1771 (get-buffer-process (cadr entry))
1772 (memq (process-status (get-buffer-process (cadr entry)))
1773 '(open run)))
1774 (get-buffer-process (cadr entry))
1775 (setq nnimap-connection-alist (delq entry nnimap-connection-alist))
1776 nil))))
1777
1778(defvar nnimap-sequence 0)
1779
1780(defun nnimap-send-command (&rest args)
d5e9a4e9 1781 (setf (nnimap-last-command-time nnimap-object) (current-time))
20a673b2
KY
1782 (process-send-string
1783 (get-buffer-process (current-buffer))
1784 (nnimap-log-command
286c4fc2 1785 (format "%d %s%s\n"
20a673b2 1786 (incf nnimap-sequence)
286c4fc2
LMI
1787 (apply #'format args)
1788 (if (nnimap-newlinep nnimap-object)
1789 ""
1790 "\r"))))
b5c575e6
G
1791 ;; Some servers apparently can't have many outstanding
1792 ;; commands, so throttle them.
1793 (unless nnimap-streaming
1794 (nnimap-wait-for-response nnimap-sequence))
20a673b2
KY
1795 nnimap-sequence)
1796
3d2af193
LI
1797(defvar nnimap-record-commands nil
1798 "If non-nil, log commands to the \"*imap log*\" buffer.")
1799
eaa610c3
KY
1800(defun nnimap-log-buffer ()
1801 (let ((name "*imap log*"))
1802 (or (get-buffer name)
1803 (with-current-buffer (get-buffer-create name)
1804 (when (boundp 'window-point-insertion-type)
1805 (make-local-variable 'window-point-insertion-type)
1806 (setq window-point-insertion-type t))
1807 (current-buffer)))))
1808
20a673b2 1809(defun nnimap-log-command (command)
3d2af193 1810 (when nnimap-record-commands
eaa610c3 1811 (with-current-buffer (nnimap-log-buffer)
3d2af193 1812 (goto-char (point-max))
b0668aa8 1813 (insert (format-time-string "%H:%M:%S")
eaa610c3
KY
1814 " [" nnimap-address "] "
1815 (if nnimap-inhibit-logging
1816 "(inhibited)\n"
1817 command))))
20a673b2
KY
1818 command)
1819
1820(defun nnimap-command (&rest args)
1821 (erase-buffer)
1822 (let* ((sequence (apply #'nnimap-send-command args))
1823 (response (nnimap-get-response sequence)))
1824 (if (equal (caar response) "OK")
1825 (cons t response)
1826 (nnheader-report 'nnimap "%s"
a46359d4
LMI
1827 (mapconcat (lambda (a)
1828 (format "%s" a))
1829 (car response) " "))
20a673b2
KY
1830 nil)))
1831
1832(defun nnimap-get-response (sequence)
1833 (nnimap-wait-for-response sequence)
1834 (nnimap-parse-response))
1835
389b76fa 1836(defun nnimap-wait-for-connection (&optional regexp)
84d89ede
LMI
1837 (nnimap-wait-for-line (or regexp "^[*.] .*\n") "[*.] \\([A-Z0-9]+\\)"))
1838
1839(defun nnimap-wait-for-line (regexp &optional response-regexp)
286c4fc2
LMI
1840 (let ((process (get-buffer-process (current-buffer))))
1841 (goto-char (point-min))
1842 (while (and (memq (process-status process)
1843 '(open run))
389b76fa 1844 (not (re-search-forward regexp nil t)))
286c4fc2
LMI
1845 (nnheader-accept-process-output process)
1846 (goto-char (point-min)))
bdaa75c7 1847 (forward-line -1)
84d89ede 1848 (and (looking-at (or response-regexp regexp))
bdaa75c7 1849 (match-string 1))))
286c4fc2 1850
20a673b2 1851(defun nnimap-wait-for-response (sequence &optional messagep)
8ccbef23
G
1852 (let ((process (get-buffer-process (current-buffer)))
1853 openp)
dab0271f
G
1854 (condition-case nil
1855 (progn
1856 (goto-char (point-max))
1857 (while (and (setq openp (memq (process-status process)
1858 '(open run)))
0832490d
LI
1859 (progn
1860 ;; Skip past any "*" lines that the server has
1861 ;; output.
1862 (while (and (not (bobp))
1863 (progn
1864 (forward-line -1)
1865 (looking-at "\\*"))))
01baa1e6 1866 (not (looking-at (format "%d .*\n" sequence)))))
dab0271f 1867 (when messagep
2146e256 1868 (nnheader-message-maybe
b5235dd9
LI
1869 7 "nnimap read %dk from %s%s" (/ (buffer-size) 1000)
1870 nnimap-address
1871 (if (not (zerop (nnimap-initial-resync nnimap-object)))
65e6fb28
LI
1872 (format " (initial sync of %d group%s; please wait)"
1873 (nnimap-initial-resync nnimap-object)
1874 (if (= (nnimap-initial-resync nnimap-object) 1)
1875 ""
1876 "s"))
b5235dd9 1877 "")))
dab0271f
G
1878 (nnheader-accept-process-output process)
1879 (goto-char (point-max)))
b5235dd9 1880 (setf (nnimap-initial-resync nnimap-object) 0)
dab0271f
G
1881 openp)
1882 (quit
a123622d
G
1883 (when debug-on-quit
1884 (debug "Quit"))
dab0271f
G
1885 ;; The user hit C-g while we were waiting: kill the process, in case
1886 ;; it's a gnutls-cli process that's stuck (tends to happen a lot behind
1887 ;; NAT routers).
1888 (delete-process process)
1889 nil))))
20a673b2
KY
1890
1891(defun nnimap-parse-response ()
1892 (let ((lines (split-string (nnimap-last-response-string) "\r\n" t))
1893 result)
1894 (dolist (line lines)
1895 (push (cdr (nnimap-parse-line line)) result))
1896 ;; Return the OK/error code first, and then all the "continuation
1897 ;; lines" afterwards.
1898 (cons (pop result)
1899 (nreverse result))))
1900
1901;; Parse an IMAP response line lightly. They look like
1902;; "* OK [UIDVALIDITY 1164213559] UIDs valid", typically, so parse
1903;; the lines into a list of strings and lists of string.
1904(defun nnimap-parse-line (line)
1905 (let (char result)
1906 (with-temp-buffer
a04f9e26 1907 (mm-disable-multibyte)
20a673b2
KY
1908 (insert line)
1909 (goto-char (point-min))
1910 (while (not (eobp))
1911 (if (eql (setq char (following-char)) ? )
1912 (forward-char 1)
1913 (push
1914 (cond
1915 ((eql char ?\[)
f7aa248a
G
1916 (split-string
1917 (buffer-substring
1918 (1+ (point))
9310f19d
LMI
1919 (if (search-forward "]" (line-end-position) 'move)
1920 (1- (point))
1921 (point)))))
20a673b2 1922 ((eql char ?\()
f7aa248a
G
1923 (split-string
1924 (buffer-substring
1925 (1+ (point))
9310f19d
LMI
1926 (if (search-forward ")" (line-end-position) 'move)
1927 (1- (point))
1928 (point)))))
20a673b2
KY
1929 ((eql char ?\")
1930 (forward-char 1)
9f2d52e7
G
1931 (buffer-substring
1932 (point)
1933 (1- (or (search-forward "\"" (line-end-position) 'move)
1934 (point)))))
20a673b2
KY
1935 (t
1936 (buffer-substring (point) (if (search-forward " " nil t)
1937 (1- (point))
1938 (goto-char (point-max))))))
1939 result)))
1940 (nreverse result))))
1941
1942(defun nnimap-last-response-string ()
1943 (save-excursion
1944 (forward-line 1)
1945 (let ((end (point)))
1946 (forward-line -1)
1947 (when (not (bobp))
1948 (forward-line -1)
1949 (while (and (not (bobp))
1950 (eql (following-char) ?*))
1951 (forward-line -1))
1952 (unless (eql (following-char) ?*)
1953 (forward-line 1)))
1954 (buffer-substring (point) end))))
1955
20a673b2
KY
1956(defvar nnimap-incoming-split-list nil)
1957
1958(defun nnimap-fetch-inbox (articles)
1959 (erase-buffer)
1960 (nnimap-wait-for-response
1961 (nnimap-send-command
1962 "UID FETCH %s %s"
1963 (nnimap-article-ranges articles)
1964 (format "(UID %s%s)"
1965 (format
8ccbef23 1966 (if (nnimap-ver4-p)
1518e4f0 1967 "BODY.PEEK"
20a673b2 1968 "RFC822.PEEK"))
1518e4f0
G
1969 (cond
1970 (nnimap-split-download-body-default
1971 "[]")
1972 ((nnimap-ver4-p)
1973 "[HEADER]")
1974 (t
1975 "[1]"))))
20a673b2
KY
1976 t))
1977
1978(defun nnimap-split-incoming-mail ()
1979 (with-current-buffer (nnimap-buffer)
1980 (let ((nnimap-incoming-split-list nil)
656e1aab
LMI
1981 (nnmail-split-methods
1982 (cond
1983 ((eq nnimap-split-methods 'default)
1984 nnmail-split-methods)
1985 (nnimap-split-methods
1986 nnimap-split-methods)
1987 (nnimap-split-fancy
1988 'nnmail-split-fancy)))
6b958814
G
1989 (nnmail-split-fancy (or nnimap-split-fancy
1990 nnmail-split-fancy))
20a673b2
KY
1991 (nnmail-inhibit-default-split-group t)
1992 (groups (nnimap-get-groups))
1993 new-articles)
1994 (erase-buffer)
1995 (nnimap-command "SELECT %S" nnimap-inbox)
99e65b2d 1996 (setf (nnimap-group nnimap-object) nnimap-inbox)
20a673b2
KY
1997 (setq new-articles (nnimap-new-articles (nnimap-get-flags "1:*")))
1998 (when new-articles
1999 (nnimap-fetch-inbox new-articles)
2000 (nnimap-transform-split-mail)
2001 (nnheader-ms-strip-cr)
2002 (nnmail-cache-open)
2003 (nnmail-split-incoming (current-buffer)
2004 #'nnimap-save-mail-spec
2005 nil nil
b069e5a6
G
2006 #'nnimap-dummy-active-number
2007 #'nnimap-save-mail-spec)
20a673b2
KY
2008 (when nnimap-incoming-split-list
2009 (let ((specs (nnimap-make-split-specs nnimap-incoming-split-list))
b069e5a6 2010 sequences junk-articles)
20a673b2
KY
2011 ;; Create any groups that doesn't already exist on the
2012 ;; server first.
2013 (dolist (spec specs)
b069e5a6
G
2014 (when (and (not (member (car spec) groups))
2015 (not (eq (car spec) 'junk)))
20a673b2
KY
2016 (nnimap-command "CREATE %S" (utf7-encode (car spec) t))))
2017 ;; Then copy over all the messages.
2018 (erase-buffer)
2019 (dolist (spec specs)
2020 (let ((group (car spec))
2021 (ranges (cdr spec)))
b069e5a6
G
2022 (if (eq group 'junk)
2023 (setq junk-articles ranges)
2024 (push (list (nnimap-send-command
2025 "UID COPY %s %S"
2026 (nnimap-article-ranges ranges)
2027 (utf7-encode group t))
2028 ranges)
2029 sequences))))
20a673b2
KY
2030 ;; Wait for the last COPY response...
2031 (when sequences
2032 (nnimap-wait-for-response (caar sequences))
2033 ;; And then mark the successful copy actions as deleted,
2034 ;; and possibly expunge them.
2035 (nnimap-mark-and-expunge-incoming
61b1af82
G
2036 (nnimap-parse-copied-articles sequences)))
2037 (nnimap-mark-and-expunge-incoming junk-articles)))))))
20a673b2
KY
2038
2039(defun nnimap-mark-and-expunge-incoming (range)
2040 (when range
2041 (setq range (nnimap-article-ranges range))
229b59da 2042 (erase-buffer)
0617bb00
LMI
2043 (let ((sequence
2044 (nnimap-send-command
2045 "UID STORE %s +FLAGS.SILENT (\\Deleted)" range)))
2046 (cond
2047 ;; If the server supports it, we now delete the message we have
2048 ;; just copied over.
389b76fa 2049 ((nnimap-capability "UIDPLUS")
0617bb00
LMI
2050 (setq sequence (nnimap-send-command "UID EXPUNGE %s" range)))
2051 ;; If it doesn't support UID EXPUNGE, then we only expunge if the
2052 ;; user has configured it.
b069e5a6 2053 (nnimap-expunge
0617bb00
LMI
2054 (setq sequence (nnimap-send-command "EXPUNGE"))))
2055 (nnimap-wait-for-response sequence))))
20a673b2
KY
2056
2057(defun nnimap-parse-copied-articles (sequences)
2058 (let (sequence copied range)
2059 (goto-char (point-min))
56e96bed 2060 (while (re-search-forward "^\\([0-9]+\\) OK\\b" nil t)
20a673b2
KY
2061 (setq sequence (string-to-number (match-string 1)))
2062 (when (setq range (cadr (assq sequence sequences)))
2063 (push (gnus-uncompress-range range) copied)))
2064 (gnus-compress-sequence (sort (apply #'nconc copied) #'<))))
2065
2066(defun nnimap-new-articles (flags)
2067 (let (new)
2068 (dolist (elem flags)
99e65b2d
G
2069 (unless (gnus-list-memq-of-list nnimap-unsplittable-articles
2070 (cdr elem))
20a673b2
KY
2071 (push (car elem) new)))
2072 (gnus-compress-sequence (nreverse new))))
2073
2074(defun nnimap-make-split-specs (list)
2075 (let ((specs nil)
2076 entry)
2077 (dolist (elem list)
2078 (destructuring-bind (article spec) elem
2079 (dolist (group (delete nil (mapcar #'car spec)))
2080 (unless (setq entry (assoc group specs))
2081 (push (setq entry (list group)) specs))
2082 (setcdr entry (cons article (cdr entry))))))
2083 (dolist (entry specs)
2084 (setcdr entry (gnus-compress-sequence (sort (cdr entry) #'<))))
2085 specs))
2086
2087(defun nnimap-transform-split-mail ()
2088 (goto-char (point-min))
2089 (let (article bytes)
2090 (block nil
2091 (while (not (eobp))
b5244046 2092 (while (not (looking-at "\\* [0-9]+ FETCH.+UID \\([0-9]+\\)"))
20a673b2
KY
2093 (delete-region (point) (progn (forward-line 1) (point)))
2094 (when (eobp)
2095 (return)))
2096 (setq article (match-string 1)
2097 bytes (nnimap-get-length))
2098 (delete-region (line-beginning-position) (line-end-position))
2099 ;; Insert MMDF separator, and a way to remember what this
2100 ;; article UID is.
2101 (insert (format "\^A\^A\^A\^A\n\nX-nnimap-article: %s" article))
2102 (forward-char (1+ bytes))
2103 (setq bytes (nnimap-get-length))
2104 (delete-region (line-beginning-position) (line-end-position))
6b7df8d3
G
2105 ;; There's a body; skip past that.
2106 (when bytes
2107 (forward-char (1+ bytes))
2108 (delete-region (line-beginning-position) (line-end-position)))))))
20a673b2
KY
2109
2110(defun nnimap-dummy-active-number (group &optional server)
2111 1)
2112
2113(defun nnimap-save-mail-spec (group-art &optional server full-nov)
2114 (let (article)
2115 (goto-char (point-min))
2116 (if (not (re-search-forward "X-nnimap-article: \\([0-9]+\\)" nil t))
2117 (error "Invalid nnimap mail")
2118 (setq article (string-to-number (match-string 1))))
b069e5a6
G
2119 (push (list article
2120 (if (eq group-art 'junk)
2121 (list (cons 'junk 1))
2122 group-art))
20a673b2 2123 nnimap-incoming-split-list)))
c113de23 2124
af92e247
AC
2125(defun nnimap-make-thread-query (header)
2126 (let* ((id (mail-header-id header))
2127 (refs (split-string
2128 (or (mail-header-references header)
2129 "")))
43a0a4fa
AC
2130 (value
2131 (format
2132 "(OR HEADER REFERENCES %S HEADER Message-Id %S)"
2133 id id)))
af92e247
AC
2134 (dolist (refid refs value)
2135 (setq value (format
43a0a4fa 2136 "(OR (OR HEADER Message-Id %S HEADER REFERENCES %S) %s)"
af92e247
AC
2137 refid refid value)))))
2138
2139
c113de23
GM
2140(provide 'nnimap)
2141
2142;;; nnimap.el ends here