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