Fix typo in last checkin.
[gnus] / lisp / nnimap.el
1 ;;; nnimap.el --- IMAP interface for Gnus
2
3 ;; Copyright (C) 2010 Free Software Foundation, Inc.
4
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6 ;;         Simon Josefsson <simon@josefsson.org>
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; nnimap interfaces Gnus with IMAP servers.
26
27 ;;; Code:
28
29 (eval-and-compile
30   (require 'nnheader))
31
32 (eval-when-compile
33   (require 'cl))
34
35 (require 'nnheader)
36 (require 'gnus-util)
37 (require 'gnus)
38 (require 'nnoo)
39 (require 'netrc)
40 (require 'parse-time)
41
42 (nnoo-declare nnimap)
43
44 (defvoo nnimap-address nil
45   "The address of the IMAP server.")
46
47 (defvoo nnimap-server-port nil
48   "The IMAP port used.
49 If nnimap-stream is `ssl', this will default to `imaps'.  If not,
50 it will default to `imap'.")
51
52 (defvoo nnimap-stream 'ssl
53   "How nnimap will talk to the IMAP server.
54 Values are `ssl', `network', `starttls' or `shell'.")
55
56 (defvoo nnimap-shell-program (if (boundp 'imap-shell-program)
57                                  (if (listp imap-shell-program)
58                                      (car imap-shell-program)
59                                    imap-shell-program)
60                                "ssh %s imapd"))
61
62 (defvoo nnimap-inbox nil
63   "The mail box where incoming mail arrives and should be split out of.")
64
65 (defvoo nnimap-authenticator nil
66   "How nnimap authenticate itself to the server.
67 Possible choices are nil (use default methods) or `anonymous'.")
68
69 (defvoo nnimap-fetch-partial-articles nil
70   "If non-nil, nnimap will fetch partial articles.
71 If t, nnimap will fetch only the first part.  If a string, it
72 will fetch all parts that have types that match that string.  A
73 likely value would be \"text/\" to automatically fetch all
74 textual parts.")
75
76 (defvoo nnimap-expunge t
77   "If non-nil, expunge articles after deleting them.
78 This is always done if the server supports UID EXPUNGE, but it's
79 not done by default on servers that doesn't support that command.")
80
81
82 (defvoo nnimap-connection-alist nil)
83
84 (defvoo nnimap-current-infos nil)
85
86 (defvar nnimap-process nil)
87
88 (defvar nnimap-status-string "")
89
90 (defvar nnimap-split-download-body-default nil
91   "Internal variable with default value for `nnimap-split-download-body'.")
92
93 (defstruct nnimap
94   group process commands capabilities select-result newlinep server
95   last-command-time)
96
97 (defvar nnimap-object nil)
98
99 (defvar nnimap-mark-alist
100   '((read "\\Seen" %Seen)
101     (tick "\\Flagged" %Flagged)
102     (reply "\\Answered" %Answered)
103     (expire "gnus-expire")
104     (dormant "gnus-dormant")
105     (score "gnus-score")
106     (save "gnus-save")
107     (download "gnus-download")
108     (forward "gnus-forward")))
109
110 (defvar nnimap-split-methods nil)
111
112 (defun nnimap-buffer ()
113   (nnimap-find-process-buffer nntp-server-buffer))
114
115 (deffoo nnimap-retrieve-headers (articles &optional group server fetch-old)
116   (with-current-buffer nntp-server-buffer
117     (erase-buffer)
118     (when (nnimap-possibly-change-group group server)
119       (with-current-buffer (nnimap-buffer)
120         (nnimap-send-command "SELECT %S" (utf7-encode group t))
121         (erase-buffer)
122         (nnimap-wait-for-response
123          (nnimap-send-command
124           "UID FETCH %s %s"
125           (nnimap-article-ranges (gnus-compress-sequence articles))
126           (format "(UID RFC822.SIZE BODYSTRUCTURE %s)"
127                   (format
128                    (if (member "IMAP4REV1"
129                                (nnimap-capabilities nnimap-object))
130                        "BODY.PEEK[HEADER.FIELDS %s]"
131                      "RFC822.HEADER.LINES %s")
132                    (append '(Subject From Date Message-Id
133                                      References In-Reply-To Xref)
134                            nnmail-extra-headers))))
135          t)
136         (nnimap-transform-headers))
137       (insert-buffer-substring
138        (nnimap-find-process-buffer (current-buffer))))
139     'headers))
140
141 (defun nnimap-transform-headers ()
142   (goto-char (point-min))
143   (let (article bytes lines size string)
144     (block nil
145       (while (not (eobp))
146         (while (not (looking-at "^\\* [0-9]+ FETCH.*UID \\([0-9]+\\)"))
147           (delete-region (point) (progn (forward-line 1) (point)))
148           (when (eobp)
149             (return)))
150         (setq article (match-string 1))
151         ;; Unfold quoted {number} strings.
152         (while (re-search-forward "[^]] {\\([0-9]+\\)}\r\n"
153                                   (1+ (line-end-position)) t)
154           (setq size (string-to-number (match-string 1)))
155           (delete-region (+ (match-beginning 0) 2) (point))
156           (setq string (delete-region (point) (+ (point) size)))
157           (insert (format "%S" string)))
158         (setq bytes (nnimap-get-length)
159               lines nil)
160         (beginning-of-line)
161         (setq size
162               (and (re-search-forward "RFC822.SIZE \\([0-9]+\\)"
163                                       (line-end-position)
164                                       t)
165                    (match-string 1)))
166         (beginning-of-line)
167         (when (search-forward "BODYSTRUCTURE" (line-end-position) t)
168           (let ((structure (ignore-errors
169                              (read (current-buffer)))))
170             (while (and (consp structure)
171                         (not (stringp (car structure))))
172               (setq structure (car structure)))
173             (setq lines (nth 7 structure))))
174         (delete-region (line-beginning-position) (line-end-position))
175         (insert (format "211 %s Article retrieved." article))
176         (forward-line 1)
177         (when size
178           (insert (format "Chars: %s\n" size)))
179         (when lines
180           (insert (format "Lines: %s\n" lines)))
181         (re-search-forward "^\r$")
182         (delete-region (line-beginning-position) (line-end-position))
183         (insert ".")
184         (forward-line 1)))))
185
186 (defun nnimap-get-length ()
187   (and (re-search-forward "{\\([0-9]+\\)}" (line-end-position) t)
188        (string-to-number (match-string 1))))
189
190 (defun nnimap-article-ranges (ranges)
191   (let (result)
192     (cond
193      ((numberp ranges)
194       (number-to-string ranges))
195      ((numberp (cdr ranges))
196       (format "%d:%d" (car ranges) (cdr ranges)))
197      (t
198       (dolist (elem ranges)
199         (push
200          (if (consp elem)
201              (format "%d:%d" (car elem) (cdr elem))
202            (number-to-string elem))
203          result))
204       (mapconcat #'identity (nreverse result) ",")))))
205
206 (deffoo nnimap-open-server (server &optional defs)
207   (if (nnimap-server-opened server)
208       t
209     (unless (assq 'nnimap-address defs)
210       (setq defs (append defs (list (list 'nnimap-address server)))))
211     (nnoo-change-server 'nnimap server defs)
212     (or (nnimap-find-connection nntp-server-buffer)
213         (nnimap-open-connection nntp-server-buffer))))
214
215 (defun nnimap-make-process-buffer (buffer)
216   (with-current-buffer
217       (generate-new-buffer (format "*nnimap %s %s %s*"
218                                    nnimap-address nnimap-server-port
219                                    (gnus-buffer-exists-p buffer)))
220     (mm-disable-multibyte)
221     (buffer-disable-undo)
222     (gnus-add-buffer)
223     (set (make-local-variable 'after-change-functions) nil)
224     (set (make-local-variable 'nnimap-object)
225          (make-nnimap :server (nnoo-current-server 'nnimap)))
226     (push (list buffer (current-buffer)) nnimap-connection-alist)
227     (current-buffer)))
228
229 (defun nnimap-open-shell-stream (name buffer host port)
230   (let ((process-connection-type nil))
231     (start-process name buffer shell-file-name
232                    shell-command-switch
233                    (format-spec
234                     nnimap-shell-program
235                     (format-spec-make
236                      ?s host
237                      ?p port)))))
238
239 (defun nnimap-credentials (address ports)
240   (let (port credentials)
241     ;; Request the credentials from all ports, but only query on the
242     ;; last port if all the previous ones have failed.
243     (while (and (null credentials)
244                 (setq port (pop ports)))
245       (setq credentials
246             (auth-source-user-or-password
247              '("login" "password") address port nil (null ports))))
248     credentials))
249
250 (defun nnimap-open-connection (buffer)
251   (with-current-buffer (nnimap-make-process-buffer buffer)
252     (let* ((coding-system-for-read 'binary)
253            (coding-system-for-write 'binary)
254            (ports
255             (cond
256              ((eq nnimap-stream 'network)
257               (open-network-stream
258                "*nnimap*" (current-buffer) nnimap-address
259                (or nnimap-server-port
260                    (if (netrc-find-service-number "imap")
261                        "imap"
262                      "143")))
263               '("143" "imap"))
264              ((eq nnimap-stream 'shell)
265               (nnimap-open-shell-stream
266                "*nnimap*" (current-buffer) nnimap-address
267                (or nnimap-server-port "imap"))
268               '("imap"))
269              ((eq nnimap-stream 'starttls)
270               (starttls-open-stream
271                "*nnimap*" (current-buffer) nnimap-address
272                (or nnimap-server-port "imap"))
273               '("imap"))
274              ((eq nnimap-stream 'ssl)
275               (open-tls-stream
276                "*nnimap*" (current-buffer) nnimap-address
277                (or nnimap-server-port
278                    (if (netrc-find-service-number "imaps")
279                        "imaps"
280                      "993")))
281               '("143" "993" "imap" "imaps"))))
282            connection-result login-result credentials)
283       (setf (nnimap-process nnimap-object)
284             (get-buffer-process (current-buffer)))
285       (when (and (nnimap-process nnimap-object)
286                  (memq (process-status (nnimap-process nnimap-object))
287                        '(open run)))
288         (gnus-set-process-query-on-exit-flag (nnimap-process nnimap-object) nil)
289         (when (setq connection-result (nnimap-wait-for-connection))
290           (when (eq nnimap-stream 'starttls)
291             (nnimap-send-command "STARTTLS")
292             (starttls-negotiate (nnimap-process nnimap-object)))
293           (unless (equal connection-result "PREAUTH")
294             (if (not (setq credentials
295                            (if (eq nnimap-authenticator 'anonymous)
296                                (list "anonymous"
297                                      (message-make-address))
298                              (nnimap-credentials
299                               nnimap-address
300                               (if nnimap-server-port
301                                   (cons (format "%s" nnimap-server-port) ports)
302                                 ports)))))
303                 (setq nnimap-object nil)
304               (setq login-result (nnimap-command "LOGIN %S %S"
305                                                  (car credentials)
306                                                  (cadr credentials)))
307               (unless (car login-result)
308                 (delete-process (nnimap-process nnimap-object))
309                 (setq nnimap-object nil))))
310           (when nnimap-object
311             (setf (nnimap-capabilities nnimap-object)
312                   (mapcar
313                    #'upcase
314                    (or (nnimap-find-parameter "CAPABILITY" (cdr login-result))
315                        (nnimap-find-parameter
316                         "CAPABILITY" (cdr (nnimap-command "CAPABILITY"))))))
317             (when (member "QRESYNC" (nnimap-capabilities nnimap-object))
318               (nnimap-command "ENABLE QRESYNC"))
319             t))))))
320
321 (defun nnimap-find-parameter (parameter elems)
322   (let (result)
323     (dolist (elem elems)
324       (cond
325        ((equal (car elem) parameter)
326         (setq result (cdr elem)))
327        ((and (equal (car elem) "OK")
328              (consp (cadr elem))
329              (equal (caadr elem) parameter))
330         (setq result (cdr (cadr elem))))))
331     result))
332
333 (deffoo nnimap-close-server (&optional server)
334   t)
335
336 (deffoo nnimap-request-close ()
337   t)
338
339 (deffoo nnimap-server-opened (&optional server)
340   (and (nnoo-current-server-p 'nnimap server)
341        nntp-server-buffer
342        (gnus-buffer-live-p nntp-server-buffer)
343        (nnimap-find-connection nntp-server-buffer)))
344
345 (deffoo nnimap-status-message (&optional server)
346   nnimap-status-string)
347
348 (deffoo nnimap-request-article (article &optional group server to-buffer)
349   (with-current-buffer nntp-server-buffer
350     (let ((result (nnimap-possibly-change-group group server))
351           parts)
352       (when (stringp article)
353         (setq article (nnimap-find-article-by-message-id group article)))
354       (when (and result
355                  article)
356         (erase-buffer)
357         (with-current-buffer (nnimap-buffer)
358           (erase-buffer)
359           (when nnimap-fetch-partial-articles
360             (if (eq nnimap-fetch-partial-articles t)
361                 (setq parts '(1))
362               (nnimap-command "UID FETCH %d (BODYSTRUCTURE)" article)
363               (goto-char (point-min))
364               (when (re-search-forward "FETCH.*BODYSTRUCTURE" nil t)
365                 (let ((structure (ignore-errors (read (current-buffer)))))
366                   (setq parts (nnimap-find-wanted-parts structure))))))
367           (setq result
368                 (nnimap-command
369                  (if (member "IMAP4REV1" (nnimap-capabilities nnimap-object))
370                      "UID FETCH %d BODY.PEEK[]"
371                    "UID FETCH %d RFC822.PEEK")
372                  article))
373           ;; Check that we really got an article.
374           (goto-char (point-min))
375           (unless (looking-at "\\* [0-9]+ FETCH")
376             (setq result nil)))
377         (let ((buffer (nnimap-find-process-buffer (current-buffer))))
378           (when (car result)
379             (with-current-buffer (or to-buffer nntp-server-buffer)
380               (insert-buffer-substring buffer)
381               (goto-char (point-min))
382               (let ((bytes (nnimap-get-length)))
383                 (delete-region (line-beginning-position)
384                                (progn (forward-line 1) (point)))
385                 (goto-char (+ (point) bytes))
386                 (delete-region (point) (point-max))
387                 (nnheader-ms-strip-cr))
388               (cons group article))))))))
389
390 (defun nnimap-find-wanted-parts (structure)
391   (message-flatten-list (nnimap-find-wanted-parts-1 structure "")))
392
393 (defun nnimap-find-wanted-parts-1 (structure prefix)
394   (let ((num 1)
395         parts)
396     (while (consp (car structure))
397       (let ((sub (pop structure)))
398         (if (consp (car sub))
399             (push (nnimap-find-wanted-parts-1
400                    sub (if (string= prefix "")
401                            (number-to-string num)
402                          (format "%s.%s" prefix num)))
403                   parts)
404           (let ((type (format "%s/%s" (nth 0 sub) (nth 1 sub))))
405             (when (string-match nnimap-fetch-partial-articles type)
406               (push (if (string= prefix "")
407                         (number-to-string num)
408                       (format "%s.%s" prefix num))
409                     parts)))
410           (incf num))))
411     (nreverse parts)))
412
413 (deffoo nnimap-request-group (group &optional server dont-check info)
414   (let ((result (nnimap-possibly-change-group group server))
415         articles active marks high low)
416     (with-current-buffer nntp-server-buffer
417       (when result
418         (if (and dont-check
419                  (setq active (nth 2 (assoc group nnimap-current-infos))))
420             (insert (format "211 %d %d %d %S\n"
421                             (- (cdr active) (car active))
422                             (car active)
423                             (cdr active)
424                             group))
425           (with-current-buffer (nnimap-buffer)
426             (erase-buffer)
427             (let ((group-sequence
428                    (nnimap-send-command "SELECT %S" (utf7-encode group t)))
429                   (flag-sequence
430                    (nnimap-send-command "UID FETCH 1:* FLAGS")))
431               (nnimap-wait-for-response flag-sequence)
432               (setq marks
433                     (nnimap-flags-to-marks
434                      (nnimap-parse-flags
435                       (list (list group-sequence flag-sequence 1 group)))))
436               (when info
437                 (nnimap-update-infos marks (list info)))
438               (goto-char (point-max))
439               (let ((uidnext (nth 5 (car marks))))
440                 (setq high (if uidnext
441                                (1- uidnext)
442                              (nth 3 (car marks)))
443                       low (or (nth 4 (car marks)) uidnext)))))
444           (erase-buffer)
445           (insert
446            (format
447             "211 %d %d %d %S\n" (1+ (- high low)) low high group)))
448         t))))
449
450 (deffoo nnimap-request-create-group (group &optional server args)
451   (when (nnimap-possibly-change-group nil server)
452     (with-current-buffer (nnimap-buffer)
453       (car (nnimap-command "CREATE %S" (utf7-encode group t))))))
454
455 (deffoo nnimap-request-delete-group (group &optional force server)
456   (when (nnimap-possibly-change-group nil server)
457     (with-current-buffer (nnimap-buffer)
458       (car (nnimap-command "DELETE %S" (utf7-encode group t))))))
459
460 (deffoo nnimap-request-expunge-group (group &optional server)
461   (when (nnimap-possibly-change-group group server)
462     (with-current-buffer (nnimap-buffer)
463       (car (nnimap-command "EXPUNGE")))))
464
465 (defun nnimap-get-flags (spec)
466   (let ((articles nil)
467         elems)
468     (with-current-buffer (nnimap-buffer)
469       (erase-buffer)
470       (nnimap-wait-for-response (nnimap-send-command
471                                  "UID FETCH %s FLAGS" spec))
472       (goto-char (point-min))
473       (while (re-search-forward "^\\* [0-9]+ FETCH (\\(.*\\))" nil t)
474         (setq elems (nnimap-parse-line (match-string 1)))
475         (push (cons (string-to-number (cadr (member "UID" elems)))
476                     (cadr (member "FLAGS" elems)))
477               articles)))
478     (nreverse articles)))
479
480 (deffoo nnimap-close-group (group &optional server)
481   t)
482
483 (deffoo nnimap-request-move-article (article group server accept-form
484                                              &optional last internal-move-group)
485   (with-temp-buffer
486     (when (nnimap-request-article article group server (current-buffer))
487       ;; If the move is internal (on the same server), just do it the easy
488       ;; way.
489       (let ((message-id (message-field-value "message-id")))
490         (if internal-move-group
491             (let ((result
492                    (with-current-buffer (nnimap-buffer)
493                      (nnimap-command "UID COPY %d %S"
494                                      article
495                                      (utf7-encode internal-move-group t)))))
496               (when (car result)
497                 (nnimap-delete-article article)
498                 (cons internal-move-group
499                       (nnimap-find-article-by-message-id
500                        internal-move-group message-id))))
501           ;; Move the article to a different method.
502           (let ((result (eval accept-form)))
503             (when result
504               (nnimap-delete-article article)
505               result)))))))
506
507 (deffoo nnimap-request-expire-articles (articles group &optional server force)
508   (cond
509    ((null articles)
510     nil)
511    ((not (nnimap-possibly-change-group group server))
512     articles)
513    ((and force
514          (eq nnmail-expiry-target 'delete))
515     (unless (nnimap-delete-article articles)
516       (message "Article marked for deletion, but not expunged."))
517     nil)
518    (t
519     (let ((deletable-articles
520            (if (or force
521                    (eq nnmail-expiry-wait 'immediate))
522                articles
523              (gnus-sorted-intersection
524               articles
525               (nnimap-find-expired-articles group)))))
526       (if (null deletable-articles)
527           articles
528         (if (eq nnmail-expiry-target 'delete)
529             (nnimap-delete-article deletable-articles)
530           (setq deletable-articles
531                 (nnimap-process-expiry-targets
532                  deletable-articles group server)))
533         ;; Return the articles we didn't delete.
534         (gnus-sorted-complement articles deletable-articles))))))
535
536 (defun nnimap-process-expiry-targets (articles group server)
537   (let ((deleted-articles nil))
538     (dolist (article articles)
539       (let ((target nnmail-expiry-target))
540         (with-temp-buffer
541           (when (nnimap-request-article article group server (current-buffer))
542             (message "Expiring article %s:%d" group article)
543             (when (functionp target)
544               (setq target (funcall target group)))
545             (when (and target
546                        (not (eq target 'delete)))
547               (if (or (gnus-request-group target t)
548                       (gnus-request-create-group target))
549                   (nnmail-expiry-target-group target group)
550                 (setq target nil)))
551             (when target
552               (push article deleted-articles))))))
553     ;; Change back to the current group again.
554     (nnimap-possibly-change-group group server)
555     (setq deleted-articles (nreverse deleted-articles))
556     (nnimap-delete-article deleted-articles)
557     deleted-articles))
558
559 (defun nnimap-find-expired-articles (group)
560   (let ((cutoff (nnmail-expired-article-p group nil nil)))
561     (with-current-buffer (nnimap-buffer)
562       (let ((result
563              (nnimap-command
564               "UID SEARCH SENTBEFORE %s"
565               (format-time-string
566                (format "%%d-%s-%%Y"
567                        (upcase
568                         (car (rassoc (nth 4 (decode-time cutoff))
569                                      parse-time-months))))
570                cutoff))))
571         (and (car result)
572              (delete 0 (mapcar #'string-to-number
573                                (cdr (assoc "SEARCH" (cdr result))))))))))
574
575
576 (defun nnimap-find-article-by-message-id (group message-id)
577   (when (nnimap-possibly-change-group group nil)
578     (with-current-buffer (nnimap-buffer)
579       (let ((result
580              (nnimap-command "UID SEARCH HEADER Message-Id %S" message-id))
581             article)
582         (when (car result)
583           ;; Select the last instance of the message in the group.
584           (and (setq article
585                      (car (last (assoc "SEARCH" (cdr result)))))
586                (string-to-number article)))))))
587
588 (defun nnimap-delete-article (articles)
589   (with-current-buffer (nnimap-buffer)
590     (nnimap-command "UID STORE %s +FLAGS.SILENT (\\Deleted)"
591                     (nnimap-article-ranges articles))
592     (cond
593      ((member "UIDPLUS" (nnimap-capabilities nnimap-object))
594       (nnimap-command "UID EXPUNGE %s"
595                       (nnimap-article-ranges articles))
596       t)
597      (nnimap-expunge
598       (nnimap-command "EXPUNGE")
599       t)
600      (t (gnus-message 7 (concat "nnimap: nnimap-expunge is not set and the "
601                                 "server doesn't support UIDPLUS, so we won't "
602                                 "delete this article now"))))))
603
604 (deffoo nnimap-request-scan (&optional group server)
605   (when (and (nnimap-possibly-change-group nil server)
606              nnimap-inbox
607              nnimap-split-methods)
608     (message "nnimap %s splitting mail..." server)
609     (nnimap-split-incoming-mail)))
610
611 (defun nnimap-marks-to-flags (marks)
612   (let (flags flag)
613     (dolist (mark marks)
614       (when (setq flag (cadr (assq mark nnimap-mark-alist)))
615         (push flag flags)))
616     flags))
617
618 (deffoo nnimap-request-set-mark (group actions &optional server)
619   (when (nnimap-possibly-change-group group server)
620     (let (sequence)
621       (with-current-buffer (nnimap-buffer)
622         ;; Just send all the STORE commands without waiting for
623         ;; response.  If they're successful, they're successful.
624         (dolist (action actions)
625           (destructuring-bind (range action marks) action
626             (let ((flags (nnimap-marks-to-flags marks)))
627               (when flags
628                 (setq sequence (nnimap-send-command
629                                 "UID STORE %s %sFLAGS.SILENT (%s)"
630                                 (nnimap-article-ranges range)
631                                 (if (eq action 'del)
632                                     "-"
633                                   "+")
634                                 (mapconcat #'identity flags " ")))))))
635         ;; Wait for the last command to complete to avoid later
636         ;; syncronisation problems with the stream.
637         (when sequence
638           (nnimap-wait-for-response sequence))))))
639
640 (deffoo nnimap-request-accept-article (group &optional server last)
641   (when (nnimap-possibly-change-group nil server)
642     (nnmail-check-syntax)
643     (let ((message (buffer-string))
644           (message-id (message-field-value "message-id"))
645           sequence)
646       (with-current-buffer (nnimap-buffer)
647         (setq sequence (nnimap-send-command
648                         "APPEND %S {%d}" (utf7-encode group t)
649                         (length message)))
650         (process-send-string (get-buffer-process (current-buffer)) message)
651         (process-send-string (get-buffer-process (current-buffer))
652                              (if (nnimap-newlinep nnimap-object)
653                                  "\n"
654                                "\r\n"))
655         (let ((result (nnimap-get-response sequence)))
656           (when result
657             (cons group
658                   (nnimap-find-article-by-message-id group message-id))))))))
659
660 (defun nnimap-add-cr ()
661   (goto-char (point-min))
662   (while (re-search-forward "\r?\n" nil t)
663     (replace-match "\r\n" t t)))
664
665 (defun nnimap-get-groups ()
666   (let ((result (nnimap-command "LIST \"\" \"*\""))
667         groups)
668     (when (car result)
669       (dolist (line (cdr result))
670         (when (and (equal (car line) "LIST")
671                    (not (and (caadr line)
672                              (string-match "noselect" (caadr line)))))
673           (push (car (last line)) groups)))
674       (nreverse groups))))
675
676 (deffoo nnimap-request-list (&optional server)
677   (nnimap-possibly-change-group nil server)
678   (with-current-buffer nntp-server-buffer
679     (erase-buffer)
680     (let ((groups
681            (with-current-buffer (nnimap-buffer)
682              (nnimap-get-groups)))
683           sequences responses)
684       (when groups
685         (with-current-buffer (nnimap-buffer)
686           (setf (nnimap-group nnimap-object) nil)
687           (dolist (group groups)
688             (push (list (nnimap-send-command "EXAMINE %S" (utf7-encode group t))
689                         group)
690                   sequences))
691           (nnimap-wait-for-response (caar sequences))
692           (setq responses
693                 (nnimap-get-responses (mapcar #'car sequences))))
694         (dolist (response responses)
695           (let* ((sequence (car response))
696                  (response (cadr response))
697                  (group (cadr (assoc sequence sequences))))
698             (when (and group
699                        (equal (caar response) "OK"))
700               (let ((uidnext (nnimap-find-parameter "UIDNEXT" response))
701                     highest exists)
702                 (dolist (elem response)
703                   (when (equal (cadr elem) "EXISTS")
704                     (setq exists (string-to-number (car elem)))))
705                 (when uidnext
706                   (setq highest (1- (string-to-number (car uidnext)))))
707                 (cond
708                  ((null highest)
709                   (insert (format "%S 0 1 y\n" (utf7-decode group t))))
710                  ((zerop exists)
711                   ;; Empty group.
712                   (insert (format "%S %d %d y\n"
713                                   (utf7-decode group t) highest (1+ highest))))
714                  (t
715                   ;; Return the widest possible range.
716                   (insert (format "%S %d 1 y\n" (utf7-decode group t)
717                                   (or highest exists)))))))))
718         t))))
719
720 (deffoo nnimap-retrieve-group-data-early (server infos)
721   (when (nnimap-possibly-change-group nil server)
722     (with-current-buffer (nnimap-buffer)
723       ;; QRESYNC handling isn't implemented.
724       (let ((qresyncp (member "notQRESYNC" (nnimap-capabilities nnimap-object)))
725             marks groups sequences)
726         ;; Go through the infos and gather the data needed to know
727         ;; what and how to request the data.
728         (dolist (info infos)
729           (setq marks (gnus-info-marks info))
730           (push (list (gnus-group-real-name (gnus-info-group info))
731                       (cdr (assq 'active marks))
732                       (cdr (assq 'uid marks)))
733                 groups))
734         ;; Then request the data.
735         (erase-buffer)
736         (setf (nnimap-group nnimap-object) nil)
737         (dolist (elem groups)
738           (if (and qresyncp
739                    (nth 2 elem))
740               (push
741                (list 'qresync
742                      (nnimap-send-command "EXAMINE %S (QRESYNC (%s %s))"
743                                           (car elem)
744                                           (car (nth 2 elem))
745                                           (cdr (nth 2 elem)))
746                      nil
747                      (car elem))
748                sequences)
749             (let ((start
750                    (if (nth 1 elem)
751                        ;; Fetch the last 100 flags.
752                        (max 1 (- (cdr (nth 1 elem)) 100))
753                      1)))
754               (push (list (nnimap-send-command "EXAMINE %S" (car elem))
755                           (nnimap-send-command "UID FETCH %d:* FLAGS" start)
756                           start
757                           (car elem))
758                     sequences))))
759         sequences))))
760
761 (deffoo nnimap-finish-retrieve-group-infos (server infos sequences)
762   (when (and sequences
763              (nnimap-possibly-change-group nil server))
764     (with-current-buffer (nnimap-buffer)
765       ;; Wait for the final data to trickle in.
766       (nnimap-wait-for-response (cadar sequences))
767       ;; Now we should have all the data we need, no matter whether
768       ;; we're QRESYNCING, fetching all the flags from scratch, or
769       ;; just fetching the last 100 flags per group.
770       (nnimap-update-infos (nnimap-flags-to-marks
771                             (nnimap-parse-flags
772                              (nreverse sequences)))
773                            infos)
774       ;; Finally, just return something resembling an active file in
775       ;; the nntp buffer, so that the agent can save the info, too.
776       (with-current-buffer nntp-server-buffer
777         (erase-buffer)
778         (dolist (info infos)
779           (let* ((group (gnus-info-group info))
780                  (active (gnus-active group)))
781             (when active
782               (insert (format "%S %d %d y\n"
783                               (gnus-group-real-name group)
784                               (cdr active)
785                               (car active))))))))))
786
787 (defun nnimap-update-infos (flags infos)
788   (dolist (info infos)
789     (let ((group (gnus-group-real-name (gnus-info-group info))))
790       (nnimap-update-info info (cdr (assoc group flags))))))
791
792 (defun nnimap-update-info (info marks)
793   (when marks
794     (destructuring-bind (existing flags high low uidnext start-article
795                                   permanent-flags) marks
796       (let ((group (gnus-info-group info))
797             (completep (and start-article
798                             (= start-article 1))))
799         (when uidnext
800           (setq high (1- uidnext)))
801         ;; First set the active ranges based on high/low.
802         (if (or completep
803                 (not (gnus-active group)))
804             (gnus-set-active group
805                              (if (and low high)
806                                  (cons low high)
807                                ;; No articles in this group.
808                                (cons uidnext (1- uidnext))))
809           (setcdr (gnus-active group) (or high (1- uidnext))))
810         (unless high
811           (setq high (1- uidnext)))
812         ;; Then update the list of read articles.
813         (let* ((unread
814                 (gnus-compress-sequence
815                  (gnus-set-difference
816                   (gnus-set-difference
817                    existing
818                    (cdr (assoc '%Seen flags)))
819                   (cdr (assoc '%Flagged flags)))))
820                (read (gnus-range-difference
821                       (cons start-article high) unread)))
822           (when (> start-article 1)
823             (setq read
824                   (gnus-range-nconcat
825                    (if (> start-article 1)
826                        (gnus-sorted-range-intersection
827                         (cons 1 (1- start-article))
828                         (gnus-info-read info))
829                      (gnus-info-read info))
830                    read)))
831           (gnus-info-set-read info read)
832           ;; Update the marks.
833           (setq marks (gnus-info-marks info))
834           ;; Note the active level for the next run-through.
835           (let ((active (assq 'active marks)))
836             (if active
837                 (setcdr active (gnus-active group))
838               (push (cons 'active (gnus-active group)) marks)))
839           (dolist (type (cdr nnimap-mark-alist))
840             (let ((old-marks (assoc (car type) marks))
841                   (new-marks
842                    (gnus-compress-sequence
843                     (cdr (or (assoc (caddr type) flags)     ; %Flagged
844                              (assoc (intern (cadr type) obarray) flags)
845                              (assoc (cadr type) flags)))))) ; "\Flagged"
846               (setq marks (delq old-marks marks))
847               (pop old-marks)
848               (when (and old-marks
849                          (> start-article 1))
850                 (setq old-marks (gnus-range-difference
851                                  old-marks
852                                  (cons start-article high)))
853                 (setq new-marks (gnus-range-nconcat old-marks new-marks)))
854               (when new-marks
855                 (push (cons (car type) new-marks) marks)))
856             (gnus-info-set-marks info marks t)
857             (nnimap-store-info info (gnus-active group))))))))
858
859 (defun nnimap-store-info (info active)
860   (let* ((group (gnus-group-real-name (gnus-info-group info)))
861          (entry (assoc group nnimap-current-infos)))
862     (if entry
863         (setcdr entry (list info active))
864       (push (list group info active) nnimap-current-infos))))
865
866 (defun nnimap-flags-to-marks (groups)
867   (let (data group totalp uidnext articles start-article mark permanent-flags)
868     (dolist (elem groups)
869       (setq group (car elem)
870             uidnext (nth 1 elem)
871             start-article (nth 2 elem)
872             permanent-flags (nth 3 elem)
873             articles (nthcdr 4 elem))
874       (let ((high (caar articles))
875             marks low existing)
876         (dolist (article articles)
877           (setq low (car article))
878           (push (car article) existing)
879           (dolist (flag (cdr article))
880             (setq mark (assoc flag marks))
881             (if (not mark)
882                 (push (list flag (car article)) marks)
883               (setcdr mark (cons (car article) (cdr mark))))))
884         (push (list group existing marks high low uidnext start-article
885                     permanent-flags)
886               data)))
887     data))
888
889 (defun nnimap-parse-flags (sequences)
890   (goto-char (point-min))
891   ;; Change \Delete etc to %Delete, so that the reader can read it.
892   (subst-char-in-region (point-min) (point-max)
893                         ?\\ ?% t)
894   (let (start end articles groups uidnext elems permanent-flags)
895     (dolist (elem sequences)
896       (destructuring-bind (group-sequence flag-sequence totalp group) elem
897         (setq start (point))
898         ;; The EXAMINE was successful.
899         (when (and (search-forward (format "\n%d OK " group-sequence) nil t)
900                    (progn
901                      (forward-line 1)
902                      (setq end (point))
903                      (goto-char start)
904                      (setq permanent-flags
905                            (and (search-forward "PERMANENTFLAGS "
906                                                  (or end (point-min)) t)
907                                 (read (current-buffer))))
908                      (goto-char start)
909                      (setq uidnext
910                            (and (search-forward "UIDNEXT "
911                                                  (or end (point-min)) t)
912                                 (read (current-buffer))))
913                      (goto-char end)
914                      (forward-line -1))
915                    ;; The UID FETCH FLAGS was successful.
916                    (search-forward (format "\n%d OK " flag-sequence) nil t))
917           (setq start (point))
918           (goto-char end)
919           (while (search-forward " FETCH " start t)
920             (setq elems (read (current-buffer)))
921             (push (cons (cadr (memq 'UID elems))
922                         (cadr (memq 'FLAGS elems)))
923                   articles))
924           (push (nconc (list group uidnext totalp permanent-flags) articles)
925                 groups)
926           (setq articles nil))))
927     groups))
928
929 (defun nnimap-find-process-buffer (buffer)
930   (cadr (assoc buffer nnimap-connection-alist)))
931
932 (deffoo nnimap-request-post (&optional server)
933   (setq nnimap-status-string "Read-only server")
934   nil)
935
936 (defun nnimap-possibly-change-group (group server)
937   (let ((open-result t))
938     (when (and server
939                (not (nnimap-server-opened server)))
940       (setq open-result (nnimap-open-server server)))
941     (cond
942      ((not open-result)
943       nil)
944      ((not group)
945       t)
946      (t
947       (with-current-buffer (nnimap-buffer)
948         (if (equal group (nnimap-group nnimap-object))
949             t
950           (let ((result (nnimap-command "SELECT %S" (utf7-encode group t))))
951             (when (car result)
952               (setf (nnimap-group nnimap-object) group
953                     (nnimap-select-result nnimap-object) result)
954               result))))))))
955
956 (defun nnimap-find-connection (buffer)
957   "Find the connection delivering to BUFFER."
958   (let ((entry (assoc buffer nnimap-connection-alist)))
959     (when entry
960       (if (and (buffer-name (cadr entry))
961                (get-buffer-process (cadr entry))
962                (memq (process-status (get-buffer-process (cadr entry)))
963                      '(open run)))
964           (get-buffer-process (cadr entry))
965         (setq nnimap-connection-alist (delq entry nnimap-connection-alist))
966         nil))))
967
968 (defvar nnimap-sequence 0)
969
970 (defun nnimap-send-command (&rest args)
971   (process-send-string
972    (get-buffer-process (current-buffer))
973    (nnimap-log-command
974     (format "%d %s%s\n"
975             (incf nnimap-sequence)
976             (apply #'format args)
977             (if (nnimap-newlinep nnimap-object)
978                 ""
979               "\r"))))
980   nnimap-sequence)
981
982 (defun nnimap-log-command (command)
983   (with-current-buffer (get-buffer-create "*imap log*")
984     (goto-char (point-max))
985     (insert (format-time-string "%H:%M:%S") " " command))
986   command)
987
988 (defun nnimap-command (&rest args)
989   (erase-buffer)
990   (setf (nnimap-last-command-time nnimap-object) (current-time))
991   (let* ((sequence (apply #'nnimap-send-command args))
992          (response (nnimap-get-response sequence)))
993     (if (equal (caar response) "OK")
994         (cons t response)
995       (nnheader-report 'nnimap "%s"
996                        (mapconcat (lambda (a)
997                                     (format "%s" a))
998                                   (car response) " "))
999       nil)))
1000
1001 (defun nnimap-get-response (sequence)
1002   (nnimap-wait-for-response sequence)
1003   (nnimap-parse-response))
1004
1005 (defun nnimap-wait-for-connection ()
1006   (let ((process (get-buffer-process (current-buffer))))
1007     (goto-char (point-min))
1008     (while (and (memq (process-status process)
1009                       '(open run))
1010                 (not (re-search-forward "^\\* .*\n" nil t)))
1011       (nnheader-accept-process-output process)
1012       (goto-char (point-min)))
1013     (forward-line -1)
1014     (and (looking-at "\\* \\([A-Z0-9]+\\)")
1015          (match-string 1))))
1016
1017 (defun nnimap-wait-for-response (sequence &optional messagep)
1018   (let ((process (get-buffer-process (current-buffer))))
1019     (goto-char (point-max))
1020     (while (and (memq (process-status process)
1021                       '(open run))
1022                 (not (re-search-backward (format "^%d .*\n" sequence)
1023                                          (max (point-min) (- (point) 500))
1024                                          t)))
1025       (when messagep
1026         (message "Read %dKB" (/ (buffer-size) 1000)))
1027       (nnheader-accept-process-output process)
1028       (goto-char (point-max)))))
1029
1030 (defun nnimap-parse-response ()
1031   (let ((lines (split-string (nnimap-last-response-string) "\r\n" t))
1032         result)
1033     (dolist (line lines)
1034       (push (cdr (nnimap-parse-line line)) result))
1035     ;; Return the OK/error code first, and then all the "continuation
1036     ;; lines" afterwards.
1037     (cons (pop result)
1038           (nreverse result))))
1039
1040 ;; Parse an IMAP response line lightly.  They look like
1041 ;; "* OK [UIDVALIDITY 1164213559] UIDs valid", typically, so parse
1042 ;; the lines into a list of strings and lists of string.
1043 (defun nnimap-parse-line (line)
1044   (let (char result)
1045     (with-temp-buffer
1046       (insert line)
1047       (goto-char (point-min))
1048       (while (not (eobp))
1049         (if (eql (setq char (following-char)) ? )
1050             (forward-char 1)
1051           (push
1052            (cond
1053             ((eql char ?\[)
1054              (split-string (buffer-substring
1055                             (1+ (point)) (1- (search-forward "]")))))
1056             ((eql char ?\()
1057              (split-string (buffer-substring
1058                             (1+ (point)) (1- (search-forward ")")))))
1059             ((eql char ?\")
1060              (forward-char 1)
1061              (buffer-substring (point) (1- (search-forward "\""))))
1062             (t
1063              (buffer-substring (point) (if (search-forward " " nil t)
1064                                            (1- (point))
1065                                          (goto-char (point-max))))))
1066            result)))
1067       (nreverse result))))
1068
1069 (defun nnimap-last-response-string ()
1070   (save-excursion
1071     (forward-line 1)
1072     (let ((end (point)))
1073       (forward-line -1)
1074       (when (not (bobp))
1075         (forward-line -1)
1076         (while (and (not (bobp))
1077                     (eql (following-char) ?*))
1078           (forward-line -1))
1079         (unless (eql (following-char) ?*)
1080           (forward-line 1)))
1081       (buffer-substring (point) end))))
1082
1083 (defun nnimap-get-responses (sequences)
1084   (let (responses)
1085     (dolist (sequence sequences)
1086       (goto-char (point-min))
1087       (when (re-search-forward (format "^%d " sequence) nil t)
1088         (push (list sequence (nnimap-parse-response))
1089               responses)))
1090     responses))
1091
1092 (defvar nnimap-incoming-split-list nil)
1093
1094 (defun nnimap-fetch-inbox (articles)
1095   (erase-buffer)
1096   (nnimap-wait-for-response
1097    (nnimap-send-command
1098     "UID FETCH %s %s"
1099     (nnimap-article-ranges articles)
1100     (format "(UID %s%s)"
1101             (format
1102              (if (member "IMAP4REV1"
1103                          (nnimap-capabilities nnimap-object))
1104                  "BODY.PEEK[HEADER] BODY.PEEK"
1105                "RFC822.PEEK"))
1106             (if nnimap-split-download-body-default
1107                 "[]"
1108               "[1]")))
1109    t))
1110
1111 (defun nnimap-split-incoming-mail ()
1112   (with-current-buffer (nnimap-buffer)
1113     (let ((nnimap-incoming-split-list nil)
1114           (nnmail-split-methods nnimap-split-methods)
1115           (nnmail-inhibit-default-split-group t)
1116           (groups (nnimap-get-groups))
1117           new-articles)
1118       (erase-buffer)
1119       (nnimap-command "SELECT %S" nnimap-inbox)
1120       (setq new-articles (nnimap-new-articles (nnimap-get-flags "1:*")))
1121       (when new-articles
1122         (nnimap-fetch-inbox new-articles)
1123         (nnimap-transform-split-mail)
1124         (nnheader-ms-strip-cr)
1125         (nnmail-cache-open)
1126         (nnmail-split-incoming (current-buffer)
1127                                #'nnimap-save-mail-spec
1128                                nil nil
1129                                #'nnimap-dummy-active-number
1130                                #'nnimap-save-mail-spec)
1131         (when nnimap-incoming-split-list
1132           (let ((specs (nnimap-make-split-specs nnimap-incoming-split-list))
1133                 sequences junk-articles)
1134             ;; Create any groups that doesn't already exist on the
1135             ;; server first.
1136             (dolist (spec specs)
1137               (when (and (not (member (car spec) groups))
1138                          (not (eq (car spec) 'junk)))
1139                 (nnimap-command "CREATE %S" (utf7-encode (car spec) t))))
1140             ;; Then copy over all the messages.
1141             (erase-buffer)
1142             (dolist (spec specs)
1143               (let ((group (car spec))
1144                     (ranges (cdr spec)))
1145                 (if (eq group 'junk)
1146                     (setq junk-articles ranges)
1147                   (push (list (nnimap-send-command
1148                                "UID COPY %s %S"
1149                                (nnimap-article-ranges ranges)
1150                                (utf7-encode group t))
1151                               ranges)
1152                         sequences))))
1153             ;; Wait for the last COPY response...
1154             (when sequences
1155               (nnimap-wait-for-response (caar sequences))
1156               ;; And then mark the successful copy actions as deleted,
1157               ;; and possibly expunge them.
1158               (nnimap-mark-and-expunge-incoming
1159                (nnimap-parse-copied-articles sequences)))
1160             (nnimap-mark-and-expunge-incoming junk-articles)))))))
1161
1162 (defun nnimap-mark-and-expunge-incoming (range)
1163   (when range
1164     (setq range (nnimap-article-ranges range))
1165     (let ((sequence
1166            (nnimap-send-command
1167             "UID STORE %s +FLAGS.SILENT (\\Deleted)" range)))
1168       (cond
1169        ;; If the server supports it, we now delete the message we have
1170        ;; just copied over.
1171        ((member "UIDPLUS" (nnimap-capabilities nnimap-object))
1172         (setq sequence (nnimap-send-command "UID EXPUNGE %s" range)))
1173        ;; If it doesn't support UID EXPUNGE, then we only expunge if the
1174        ;; user has configured it.
1175        (nnimap-expunge
1176         (setq sequence (nnimap-send-command "EXPUNGE"))))
1177       (nnimap-wait-for-response sequence))))
1178
1179 (defun nnimap-parse-copied-articles (sequences)
1180   (let (sequence copied range)
1181     (goto-char (point-min))
1182     (while (re-search-forward "^\\([0-9]+\\) OK " nil t)
1183       (setq sequence (string-to-number (match-string 1)))
1184       (when (setq range (cadr (assq sequence sequences)))
1185         (push (gnus-uncompress-range range) copied)))
1186     (gnus-compress-sequence (sort (apply #'nconc copied) #'<))))
1187
1188 (defun nnimap-new-articles (flags)
1189   (let (new)
1190     (dolist (elem flags)
1191       (when (or (null (cdr elem))
1192                 (and (not (memq '%Deleted (cdr elem)))
1193                      (not (memq '%Seen (cdr elem)))))
1194         (push (car elem) new)))
1195     (gnus-compress-sequence (nreverse new))))
1196
1197 (defun nnimap-make-split-specs (list)
1198   (let ((specs nil)
1199         entry)
1200     (dolist (elem list)
1201       (destructuring-bind (article spec) elem
1202         (dolist (group (delete nil (mapcar #'car spec)))
1203           (unless (setq entry (assoc group specs))
1204             (push (setq entry (list group)) specs))
1205           (setcdr entry (cons article (cdr entry))))))
1206     (dolist (entry specs)
1207       (setcdr entry (gnus-compress-sequence (sort (cdr entry) #'<))))
1208     specs))
1209
1210 (defun nnimap-transform-split-mail ()
1211   (goto-char (point-min))
1212   (let (article bytes)
1213     (block nil
1214       (while (not (eobp))
1215         (while (not (looking-at "^\\* [0-9]+ FETCH.*UID \\([0-9]+\\)"))
1216           (delete-region (point) (progn (forward-line 1) (point)))
1217           (when (eobp)
1218             (return)))
1219         (setq article (match-string 1)
1220               bytes (nnimap-get-length))
1221         (delete-region (line-beginning-position) (line-end-position))
1222         ;; Insert MMDF separator, and a way to remember what this
1223         ;; article UID is.
1224         (insert (format "\^A\^A\^A\^A\n\nX-nnimap-article: %s" article))
1225         (forward-char (1+ bytes))
1226         (setq bytes (nnimap-get-length))
1227         (delete-region (line-beginning-position) (line-end-position))
1228         (forward-char (1+ bytes))
1229         (delete-region (line-beginning-position) (line-end-position))))))
1230
1231 (defun nnimap-dummy-active-number (group &optional server)
1232   1)
1233
1234 (defun nnimap-save-mail-spec (group-art &optional server full-nov)
1235   (let (article)
1236     (goto-char (point-min))
1237     (if (not (re-search-forward "X-nnimap-article: \\([0-9]+\\)" nil t))
1238         (error "Invalid nnimap mail")
1239       (setq article (string-to-number (match-string 1))))
1240     (push (list article
1241                 (if (eq group-art 'junk)
1242                     (list (cons 'junk 1))
1243                   group-art))
1244           nnimap-incoming-split-list)))
1245
1246 (provide 'nnimap)
1247
1248 ;;; nnimap.el ends here