;;; nnimap.el --- IMAP interface for Gnus
-;; Copyright (C) 2010 Free Software Foundation, Inc.
+;; Copyright (C) 2010-2011 Free Software Foundation, Inc.
;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
;; Simon Josefsson <simon@josefsson.org>
(require 'nnmail)
(require 'proto-stream)
-(eval-when-compile
- (require 'gnus-sum))
-
(autoload 'auth-source-forget-user-or-password "auth-source")
(autoload 'auth-source-user-or-password "auth-source")
(defstruct nnimap
group process commands capabilities select-result newlinep server
- last-command-time greeting examined)
+ last-command-time greeting examined stream-type)
(defvar nnimap-object nil)
(download "gnus-download")
(forward "gnus-forward")))
+(defvar nnimap-quirks
+ '(("QRESYNC" "Zimbra" "QRESYNC ")))
+
(defun nnimap-buffer ()
(nnimap-find-process-buffer nntp-server-buffer))
(nnimap-article-ranges (gnus-compress-sequence articles))
(nnimap-header-parameters))
t)
- (nnimap-transform-headers))
+ (nnimap-transform-headers)
+ (nnheader-remove-cr-followed-by-lf))
(insert-buffer-substring
(nnimap-find-process-buffer (current-buffer))))
'headers))
'("143" "993" "imap" "imaps"))
(t
(error "Unknown stream type: %s" nnimap-stream))))
- connection-result login-result credentials)
+ (proto-stream-always-use-starttls t)
+ login-result credentials)
(when nnimap-server-port
- (push (format "%s" nnimap-server-port) ports))
- (destructuring-bind (stream greeting capabilities)
- (open-proto-stream
+ (setq ports (append ports (list nnimap-server-port))))
+ (destructuring-bind (stream greeting capabilities stream-type)
+ (open-protocol-stream
"*nnimap*" (current-buffer) nnimap-address (car (last ports))
:type nnimap-stream
:shell-command nnimap-shell-program
:capability-command "1 CAPABILITY\r\n"
+ :success " OK "
:starttls-function
(lambda (capabilities)
- (if (not (string-match "STARTTLS" capabilities))
- ;; Not a STARTTLS-capable server.
- nil
+ (when (gnus-string-match-p "STARTTLS" capabilities)
"1 STARTTLS\r\n")))
(setf (nnimap-process nnimap-object) stream)
+ (setf (nnimap-stream-type nnimap-object) stream-type)
(if (not stream)
(progn
(nnheader-report 'nnimap "Unable to contact %s:%s via %s"
nnimap-address port nnimap-stream)
'no-connect)
(gnus-set-process-query-on-exit-flag stream nil)
- (if (not (string-match "[*.] OK" greeting))
+ (if (not (gnus-string-match-p "[*.] \\(OK\\|PREAUTH\\)" greeting))
(nnheader-report 'nnimap "%s" greeting)
;; Store the greeting (for debugging purposes).
(setf (nnimap-greeting nnimap-object) greeting)
(setf (nnimap-capabilities nnimap-object)
(mapcar #'upcase
(split-string capabilities)))
- (unless (equal connection-result "PREAUTH")
+ (unless (gnus-string-match-p "[*.] PREAUTH" greeting)
(if (not (setq credentials
(if (eq nnimap-authenticator 'anonymous)
(list "anonymous"
(nnimap-credentials nnimap-address ports)))))
(setq nnimap-object nil)
(setq login-result
- (if (and (nnimap-capability "AUTH=PLAIN")
- (nnimap-capability "LOGINDISABLED"))
- (nnimap-command
- "AUTHENTICATE PLAIN %s"
- (base64-encode-string
- (format "\000%s\000%s"
- (nnimap-quote-specials (car credentials))
- (nnimap-quote-specials (cadr credentials)))))
- (nnimap-command "LOGIN %S %S"
- (car credentials)
- (cadr credentials))))
+ (nnimap-login (car credentials) (cadr credentials)))
(unless (car login-result)
;; If the login failed, then forget the credentials
;; that are now possibly cached.
(nnimap-command "ENABLE QRESYNC"))
(nnimap-process nnimap-object))))))))
+(autoload 'rfc2104-hash "rfc2104")
+
+(defun nnimap-login (user password)
+ (cond
+ ;; Prefer plain LOGIN if it's enabled (since it requires fewer
+ ;; round trips than CRAM-MD5, and it's less likely to be buggy),
+ ;; and we're using an encrypted connection.
+ ((and (not (nnimap-capability "LOGINDISABLED"))
+ (eq (nnimap-stream-type nnimap-object) 'tls))
+ (nnimap-command "LOGIN %S %S" user password))
+ ((nnimap-capability "AUTH=CRAM-MD5")
+ (erase-buffer)
+ (let ((sequence (nnimap-send-command "AUTHENTICATE CRAM-MD5"))
+ (challenge (nnimap-wait-for-line "^\\+\\(.*\\)\n")))
+ (process-send-string
+ (get-buffer-process (current-buffer))
+ (concat
+ (base64-encode-string
+ (concat user " "
+ (rfc2104-hash 'md5 64 16 password
+ (base64-decode-string challenge))))
+ "\r\n"))
+ (nnimap-wait-for-response sequence)))
+ ((not (nnimap-capability "LOGINDISABLED"))
+ (nnimap-command "LOGIN %S %S" user password))
+ ((nnimap-capability "AUTH=PLAIN")
+ (nnimap-command
+ "AUTHENTICATE PLAIN %s"
+ (base64-encode-string
+ (format "\000%s\000%s"
+ (nnimap-quote-specials user)
+ (nnimap-quote-specials password)))))))
+
(defun nnimap-quote-specials (string)
(with-temp-buffer
(insert string)
(with-current-buffer (nnimap-buffer)
(when (stringp article)
(setq article (nnimap-find-article-by-message-id group article)))
- (nnimap-get-whole-article
- article (format "UID FETCH %%d %s"
- (nnimap-header-parameters)))
- (let ((buffer (current-buffer)))
- (with-current-buffer (or to-buffer nntp-server-buffer)
- (erase-buffer)
- (insert-buffer-substring buffer)
- (nnheader-ms-strip-cr)
- (cons group article))))))
+ (if (null article)
+ nil
+ (nnimap-get-whole-article
+ article (format "UID FETCH %%d %s"
+ (nnimap-header-parameters)))
+ (let ((buffer (current-buffer)))
+ (with-current-buffer (or to-buffer nntp-server-buffer)
+ (erase-buffer)
+ (insert-buffer-substring buffer)
+ (nnheader-ms-strip-cr)
+ (cons group article)))))))
(defun nnimap-get-whole-article (article &optional command)
(let ((result
;; Collect all the body parts.
(while (looking-at ".*BODY\\[\\([.0-9]+\\)\\]")
(setq id (match-string 1)
- bytes (nnimap-get-length))
+ bytes (or (nnimap-get-length) 0))
(beginning-of-line)
(delete-region (point) (progn (forward-line 1) (point)))
(push (list id (buffer-substring (point) (+ (point) bytes)))
(let ((result (nnimap-possibly-change-group
;; Don't SELECT the group if we're going to select it
;; later, anyway.
- (if (and dont-check
+ (if (and (not dont-check)
(assoc group nnimap-current-infos))
nil
group)
(defun nnimap-process-expiry-targets (articles group server)
(let ((deleted-articles nil))
- (dolist (article articles)
- (let ((target nnmail-expiry-target))
- (with-temp-buffer
- (mm-disable-multibyte)
- (when (nnimap-request-article article group server (current-buffer))
- (nnheader-message 7 "Expiring article %s:%d" group article)
- (when (functionp target)
- (setq target (funcall target group)))
- (when (and target
- (not (eq target 'delete)))
- (if (or (gnus-request-group target t)
- (gnus-request-create-group target))
- (nnmail-expiry-target-group target group)
- (setq target nil)))
- (when target
- (push article deleted-articles))))))
+ (cond
+ ;; shortcut further processing if we're going to delete the articles
+ ((eq nnmail-expiry-target 'delete)
+ (setq deleted-articles articles)
+ t)
+ ;; or just move them to another folder on the same IMAP server
+ ((and (not (functionp nnmail-expiry-target))
+ (gnus-server-equal (gnus-group-method nnmail-expiry-target)
+ (gnus-server-to-method
+ (format "nnimap:%s" server))))
+ (and (nnimap-possibly-change-group group server)
+ (with-current-buffer (nnimap-buffer)
+ (nnheader-message 7 "Expiring articles from %s: %s" group articles)
+ (nnimap-command
+ "UID COPY %s %S"
+ (nnimap-article-ranges (gnus-compress-sequence articles))
+ (utf7-encode (gnus-group-real-name nnmail-expiry-target) t))
+ (setq deleted-articles articles)))
+ t)
+ (t
+ (dolist (article articles)
+ (let ((target nnmail-expiry-target))
+ (with-temp-buffer
+ (mm-disable-multibyte)
+ (when (nnimap-request-article article group server (current-buffer))
+ (nnheader-message 7 "Expiring article %s:%d" group article)
+ (when (functionp target)
+ (setq target (funcall target group)))
+ (when (and target
+ (not (eq target 'delete)))
+ (if (or (gnus-request-group target t)
+ (gnus-request-create-group target))
+ (nnmail-expiry-target-group target group)
+ (setq target nil)))
+ (when target
+ (push article deleted-articles))))))))
;; Change back to the current group again.
(nnimap-possibly-change-group group server)
(setq deleted-articles (nreverse deleted-articles))
(push flag flags)))
flags))
+(deffoo nnimap-request-update-group-status (group status &optional server)
+ (when (nnimap-possibly-change-group nil server)
+ (let ((command (assoc
+ status
+ '((subscribe "SUBSCRIBE")
+ (unsubscribe "UNSUBSCRIBE")))))
+ (when command
+ (with-current-buffer (nnimap-buffer)
+ (nnimap-command "%s %S" (cadr command) (utf7-encode group t)))))))
+
(deffoo nnimap-request-set-mark (group actions &optional server)
(when (nnimap-possibly-change-group group server)
(let (sequence)
"\n"
"\r\n"))
(let ((result (nnimap-get-response sequence)))
- (if (not (car result))
+ (if (not (nnimap-ok-p result))
(progn
- (nnheader-message 7 "%s" (nnheader-get-report-string 'nnimap))
+ (nnheader-report 'nnimap "%s" result)
nil)
(cons group
(or (nnimap-find-uid-response "APPENDUID" (car result))
(nnimap-find-article-by-message-id
group message-id)))))))))
+(defun nnimap-ok-p (value)
+ (and (consp value)
+ (consp (car value))
+ (equal (caar value) "OK")))
+
(defun nnimap-find-uid-response (name list)
(let ((result (car (last (nnimap-find-response-element name list)))))
(and result
uidvalidity
modseq)
(push
- (list (nnimap-send-command "EXAMINE %S (QRESYNC (%s %s))"
+ (list (nnimap-send-command "EXAMINE %S (%s (%s %s))"
(utf7-encode group t)
+ (nnimap-quirk "QRESYNC")
uidvalidity modseq)
'qresync
nil group 'qresync)
sequences))))
sequences))))
+(defun nnimap-quirk (command)
+ (let ((quirk (assoc command nnimap-quirks)))
+ ;; If this server is of a type that matches a quirk, then return
+ ;; the "quirked" command instead of the proper one.
+ (if (or (null quirk)
+ (not (string-match (nth 1 quirk) (nnimap-greeting nnimap-object))))
+ command
+ (nth 2 quirk))))
+
(deffoo nnimap-finish-retrieve-group-infos (server infos sequences)
(when (and sequences
(nnimap-possibly-change-group nil server))
(when uidnext
(setq high (1- uidnext)))
;; First set the active ranges based on high/low.
- (if (or completep
- (not (gnus-active group)))
- (gnus-set-active group
- (cond
- (active
- (cons (min (or low (car active))
- (car active))
- (max (or high (cdr active))
- (cdr active))))
- ((and low high)
- (cons low high))
- (uidnext
- ;; No articles in this group.
- (cons uidnext (1- uidnext)))
- (start-article
- (cons start-article (1- start-article)))
- (t
- ;; No articles and no uidnext.
- nil)))
- (gnus-set-active
- group
+ (gnus-set-active
+ group
+ (if (or completep
+ (not (gnus-active group)))
+ (cond
+ (active
+ (cons (min (or low (car active))
+ (car active))
+ (max (or high (cdr active))
+ (cdr active))))
+ ((and low high)
+ (cons low high))
+ (uidnext
+ ;; No articles in this group.
+ (cons uidnext (1- uidnext)))
+ (start-article
+ (cons start-article (1- start-article)))
+ (t
+ ;; No articles and no uidnext.
+ nil))
(cons (car active)
(or high (1- uidnext)))))
;; See whether this is a read-only group.
(when new-marks
(push (cons (car type) new-marks) marks)))))
(gnus-info-set-marks info marks t))))
+ ;; Tell Gnus whether there are any \Recent messages in any of
+ ;; the groups.
+ (let ((recent (cdr (assoc '%Recent flags))))
+ (when (and active recent)
+ (while recent
+ (when (> (car recent) (cdr active))
+ (push (list (cons (gnus-group-real-name group) 0))
+ nnmail-split-history)
+ (setq recent nil))
+ (pop recent))))
;; Note the active level for the next run-through.
(gnus-group-set-parameter info 'active (gnus-active group))
(gnus-group-set-parameter info 'uidvalidity uidvalidity)
(goto-char start)
(setq vanished
(and (eq flag-sequence 'qresync)
- (re-search-forward "VANISHED.* \\([0-9:,]+\\)"
+ (re-search-forward "^\\* VANISHED .* \\([0-9:,]+\\)"
(or end (point-min)) t)
(match-string 1)))
(goto-char start)
(setq nnimap-status-string "Read-only server")
nil)
-(deffoo nnimap-request-thread (id)
- (let* ((refs (split-string
- (or (mail-header-references (gnus-summary-article-header))
+(declare-function gnus-fetch-headers "gnus-sum"
+ (articles &optional limit force-new dependencies))
+
+(deffoo nnimap-request-thread (header)
+ (let* ((id (mail-header-id header))
+ (refs (split-string
+ (or (mail-header-references header)
"")))
(cmd (let ((value
(format
(nnimap-parse-response))
(defun nnimap-wait-for-connection (&optional regexp)
- (unless regexp
- (setq regexp "^[*.] .*\n"))
+ (nnimap-wait-for-line (or regexp "^[*.] .*\n") "[*.] \\([A-Z0-9]+\\)"))
+
+(defun nnimap-wait-for-line (regexp &optional response-regexp)
(let ((process (get-buffer-process (current-buffer))))
(goto-char (point-min))
(while (and (memq (process-status process)
(nnheader-accept-process-output process)
(goto-char (point-min)))
(forward-line -1)
- (and (looking-at "[*.] \\([A-Z0-9]+\\)")
+ (and (looking-at (or response-regexp regexp))
(match-string 1))))
(defun nnimap-wait-for-response (sequence &optional messagep)
(goto-char (point-max))
(while (and (setq openp (memq (process-status process)
'(open run)))
- (not (re-search-backward
- (format "^%d .*\n" sequence)
- (if nnimap-streaming
- (max (point-min) (- (point) 500))
- (point-min))
- t)))
+ (progn
+ ;; Skip past any "*" lines that the server has
+ ;; output.
+ (while (and (not (bobp))
+ (progn
+ (forward-line -1)
+ (looking-at "\\*"))))
+ (not (looking-at (format "%d " sequence)))))
(when messagep
(nnheader-message 7 "nnimap read %dk" (/ (buffer-size) 1000)))
(nnheader-accept-process-output process)