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