;; o What about Gnus's article editing, can we support it? NO!
;; o Use \Draft to support the draft group??
;; o Duplicate suppression
+;; o Rewrite UID SEARCH UID X as UID FETCH X (UID) for those with slow servers
;;; Code:
-(eval-and-compile
- (require 'cl)
- (require 'imap))
-
+(require 'imap)
(require 'nnoo)
(require 'nnmail)
(require 'nnheader)
(defconst nnimap-version "nnimap 1.0")
+(defgroup nnimap nil
+ "Reading IMAP mail with Gnus."
+ :group 'gnus)
+
(defvoo nnimap-address nil
"Address of physical IMAP server. If nil, use the virtual server's name.")
;; Splitting variables
-(defvar nnimap-split-crosspost t
+(defcustom nnimap-split-crosspost t
"If non-nil, do crossposting if several split methods match the mail.
-If nil, the first match found will be used.")
+If nil, the first match found will be used."
+ :group 'nnimap
+ :type 'boolean)
-(defvar nnimap-split-inbox nil
- "*Name of mailbox to split mail from.
+(defcustom nnimap-split-inbox nil
+ "Name of mailbox to split mail from.
Mail is read from this mailbox and split according to rules in
`nnimap-split-rule'.
-This can be a string or a list of strings.")
+This can be a string or a list of strings."
+ :group 'nnimap
+ :type '(choice (string)
+ (repeat string)))
+
+(define-widget 'nnimap-strict-function 'function
+ "This widget only matches values that are functionp.
+
+Warning: This means that a value that is the symbol of a not yet
+loaded function will not match. Use with care."
+ :match 'nnimap-strict-function-match)
-(defvar nnimap-split-rule nil
- "*Mail will be split according to theese rules.
+(defun nnimap-strict-function-match (widget value)
+ "Ignoring WIDGET, match if VALUE is a function."
+ (functionp value))
+
+(defcustom nnimap-split-rule nil
+ "Mail will be split according to theese rules.
Mail is read from mailbox(es) specified in `nnimap-split-inbox'.
everything else in the incoming mailbox, you could do something like
this:
-(setq nnimap-split-rule '((\"INBOX.gnus-imap\" \"From:.*gnus-imap\")
+\(setq nnimap-split-rule '((\"INBOX.gnus-imap\" \"From:.*gnus-imap\")
(\"INBOX.junk\" \"Subject:.*buy\")))
As you can see, `nnimap-split-rule' is a list of lists, where the first
even different split rules in different inboxes on the same server,
the syntax of this variable have been extended along the lines of:
-(setq nnimap-split-rule
+\(setq nnimap-split-rule
'((\"my1server\" (\".*\" ((\"ding\" \"ding@gnus.org\")
(\"junk\" \"From:.*Simon\")))
(\"my2server\" (\"INBOX\" nnimap-split-fancy))
\"my3server\" and \"my4server\" both use the same rules. Similarly,
the inbox string is also a regexp. The actual splitting rules are as
before, either a function, or a list with group/regexp or
-group/function elements.")
-
-(defvar nnimap-split-predicate "UNSEEN UNDELETED"
+group/function elements."
+ :group 'nnimap
+ :type '(choice :tag "Rule type"
+ (repeat :menu-tag "Single-server"
+ :tag "Single-server list"
+ (list (string :tag "Mailbox")
+ (choice :tag "Predicate"
+ (regexp :tag "A regexp")
+ (nnimap-strict-function :tag "A function"))))
+ (choice :menu-tag "A function"
+ :tag "A function"
+ (function-item nnimap-split-fancy)
+ (function-item nnmail-split-fancy)
+ (nnimap-strict-function :tag "User-defined function"))
+ (repeat :menu-tag "Multi-server (extended)"
+ :tag "Multi-server list"
+ (list (regexp :tag "Server regexp")
+ (list (regexp :tag "Incoming Mailbox regexp")
+ (repeat :tag "Rules for matching server(s) and mailbox(es)"
+ (list (string :tag "Destination mailbox")
+ (choice :tag "Predicate"
+ (regexp :tag "A Regexp")
+ (nnimap-strict-function :tag "A Function")))))))))
+
+(defcustom nnimap-split-predicate "UNSEEN UNDELETED"
"The predicate used to find articles to split.
If you use another IMAP client to peek on articles but always would
like nnimap to split them once it's started, you could change this to
\"UNDELETED\". Other available predicates are available in
-RFC2060 section 6.4.4.")
-
-(defvar nnimap-split-fancy nil
- "Like `nnmail-split-fancy', which see.")
+RFC2060 section 6.4.4."
+ :group 'nnimap
+ :type 'string)
+
+(defcustom nnimap-split-fancy nil
+ "Like the variable `nnmail-split-fancy', which see."
+ :group 'nnimap
+ :type 'sexp)
+
+;; Performance / bug workaround variables
+
+(defcustom nnimap-close-asynchronous t
+ "Close mailboxes asynchronously in `nnimap-close-group'.
+This means that errors cought by nnimap when closing the mailbox will
+not prevent Gnus from updating the group status, which may be harmful.
+However, it increases speed."
+ :type 'boolean
+ :group 'nnimap)
+
+(defcustom nnimap-dont-close t
+ "Never close mailboxes.
+This increases the speed of closing mailboxes (quiting group) but may
+decrease the speed of selecting another mailbox later. Re-selecting
+the same mailbox will be faster though."
+ :type 'boolean
+ :group 'nnimap)
+
+(defcustom nnimap-retrieve-groups-asynchronous t
+ "Send asynchronous STATUS commands for each mailbox before checking mail.
+If you have mailboxes that rarely receives mail, this speeds up new
+mail checking. It works by first sending STATUS commands for each
+mailbox, and then only checking groups which has a modified UIDNEXT
+more carefully for new mail.
+
+In summary, the default is O((1-p)*k+p*n) and changing it to nil makes
+it O(n). If p is small, then the default is probably faster."
+ :type 'boolean
+ :group 'nnimap)
+
+(defvoo nnimap-need-unselect-to-notice-new-mail nil
+ "Unselect mailboxes before looking for new mail in them.
+Some servers seem to need this under some circumstances.")
;; Authorization / Privacy variables
(defvoo nnimap-nov-file-name-suffix ".novcache"
"Suffix for NOV cache base filename.")
-(defvoo nnimap-nov-is-evil nil
- "If non-nil, nnimap will never generate or use a local nov database for this backend.
-Using nov databases will speed up header fetching considerably.
+(defvoo nnimap-nov-is-evil gnus-agent
+ "If non-nil, never generate or use a local nov database for this backend.
+Using nov databases should speed up header fetching considerably.
+However, it will invoke a UID SEARCH UID command on the server, and
+some servers implement this command inefficiently by opening each and
+every message in the group, thus making it quite slow.
Unlike other backends, you do not need to take special care if you
flip this variable.")
use this to make replies go directly to the group.")
(defvoo nnimap-expunge-search-string "UID %s NOT SINCE %s"
- "*IMAP search command to use for articles that are to be expired.
+ "IMAP search command to use for articles that are to be expired.
The first %s is replaced by a UID set of articles to search on,
and the second %s is replaced by a date criterium.
2060 for more information on valid strings.")
(defvoo nnimap-importantize-dormant t
- "*If non-nil, mark \"dormant\" articles as \"ticked\" for other IMAP clients.
+ "If non-nil, mark \"dormant\" articles as \"ticked\" for other IMAP clients.
Note that within Gnus, dormant articles will still (only) be
marked as ticked. This is to make \"dormant\" articles stand out,
just like \"ticked\" articles, in other IMAP clients.")
;; Internal variables:
+(defvar nnimap-mailbox-info (gnus-make-hashtable 997))
(defvar nnimap-debug nil
"Name of buffer to record debugging info.
For example: (setq nnimap-debug \"*nnimap-debug*\")")
(defun nnimap-before-find-minmax-bugworkaround ()
"Function called before iterating through mailboxes with
`nnimap-find-minmax-uid'."
- ;; XXX this is for UoW imapd problem, it doesn't notice new mail in
- ;; currently selected mailbox without a re-select/examine.
- (or (null (imap-current-mailbox nnimap-server-buffer))
- (imap-mailbox-unselect nnimap-server-buffer)))
+ (when nnimap-need-unselect-to-notice-new-mail
+ ;; XXX this is for UoW imapd problem, it doesn't notice new mail in
+ ;; currently selected mailbox without a re-select/examine.
+ (or (null (imap-current-mailbox nnimap-server-buffer))
+ (imap-mailbox-unselect nnimap-server-buffer))))
(defun nnimap-find-minmax-uid (group &optional examine)
"Find lowest and highest active article nummber in GROUP.
If EXAMINE is non-nil the group is selected read-only."
(with-current-buffer nnimap-server-buffer
- (when (imap-mailbox-select group examine)
+ (when (or (string= group (imap-current-mailbox))
+ (imap-mailbox-select group examine))
(let (minuid maxuid)
(when (> (imap-mailbox-get 'exists) 0)
(imap-fetch "1,*" "UID" nil 'nouidfetch)
mbx imap-current-mailbox
headers (nnimap-demule
(if (imap-capability 'IMAP4rev1)
- ;; xxx don't just use car? alist doesn't contain
+ ;; xxx don't just use car? alist doesn't contain
;; anything else now, but it might...
(nth 2 (car (imap-message-get uid 'BODYDETAIL)))
(imap-message-get uid 'RFC822.HEADER)))
(nnimap-retrieve-headers-from-server
(cons (1+ (cdr cached)) high) group server))
(when nnimap-prune-cache
- ;; remove nov's for articles which has expired on server
+ ;; remove nov's for articles which has expired on server
(goto-char (point-min))
(dolist (uid (gnus-set-difference articles uids))
(when (re-search-forward (format "^%d\t" uid) nil t)
(cons low high) group server))
(when (buffer-modified-p)
(nnmail-write-region
- 1 (point-max) (nnimap-group-overview-filename group server)
- nil 'nomesg))
+ (point-min) (point-max)
+ (nnimap-group-overview-filename group server) nil 'nomesg))
(nnheader-nov-delete-outside-range low high))))
'nov)))
(when (and (imap-opened)
(nnimap-possibly-change-group group server))
(case nnimap-expunge-on-close
- ('always (imap-mailbox-expunge)
- (imap-mailbox-close))
- ('ask (if (and (imap-search "DELETED")
- (gnus-y-or-n-p (format
- "Expunge articles in group `%s'? "
- imap-current-mailbox)))
- (progn (imap-mailbox-expunge)
- (imap-mailbox-close))
- (imap-mailbox-unselect)))
+ (always (progn
+ (imap-mailbox-expunge nnimap-close-asynchronous)
+ (unless nnimap-dont-close
+ (imap-mailbox-close nnimap-close-asynchronous))))
+ (ask (if (and (imap-search "DELETED")
+ (gnus-y-or-n-p (format "Expunge articles in group `%s'? "
+ imap-current-mailbox)))
+ (progn
+ (imap-mailbox-expunge nnimap-close-asynchronous)
+ (unless nnimap-dont-close
+ (imap-mailbox-close nnimap-close-asynchronous)))
+ (imap-mailbox-unselect)))
(t (imap-mailbox-unselect)))
(not imap-current-mailbox))))
;; Optional backend functions
+(defun nnimap-string-lessp-numerical (s1 s2)
+ "Return t if first arg string is less than second in numerical order."
+ (cond ((string= s1 s2)
+ nil)
+ ((> (length s1) (length s2))
+ nil)
+ ((< (length s1) (length s2))
+ t)
+ ((< (string-to-number (substring s1 0 1))
+ (string-to-number (substring s2 0 1)))
+ t)
+ ((> (string-to-number (substring s1 0 1))
+ (string-to-number (substring s2 0 1)))
+ nil)
+ (t
+ (nnimap-string-lessp-numerical (substring s1 1) (substring s2 1)))))
+
(deffoo nnimap-retrieve-groups (groups &optional server)
(when (nnimap-possibly-change-server server)
(gnus-message 5 "nnimap: Checking mailboxes...")
(with-current-buffer nntp-server-buffer
(erase-buffer)
(nnimap-before-find-minmax-bugworkaround)
- (dolist (group groups)
- (gnus-message 7 "nnimap: Checking mailbox %s" group)
- (or (member "\\NoSelect"
- (imap-mailbox-get 'list-flags group nnimap-server-buffer))
- (let ((info (nnimap-find-minmax-uid group 'examine)))
- (when (> (or (imap-mailbox-get 'recent group
- nnimap-server-buffer) 0)
- 0)
- (push (list (cons group 0)) nnmail-split-history))
- (insert (format "\"%s\" %d %d y\n" group
- (or (nth 2 info) 0)
- (max 1 (or (nth 1 info) 1))))))))
+ (let (asyncgroups slowgroups)
+ (if (null nnimap-retrieve-groups-asynchronous)
+ (setq slowgroups groups)
+ (dolist (group groups)
+ (gnus-message 7 "nnimap: Checking mailbox %s" group)
+ (add-to-list (if (gnus-gethash-safe (concat server group)
+ nnimap-mailbox-info)
+ 'asyncgroups
+ 'slowgroups)
+ (list group (imap-mailbox-status-asynch
+ group 'uidnext nnimap-server-buffer))))
+ (dolist (asyncgroup asyncgroups)
+ (let ((group (nth 0 asyncgroup))
+ (tag (nth 1 asyncgroup))
+ new old)
+ (when (imap-ok-p (imap-wait-for-tag tag nnimap-server-buffer))
+ (if (nnimap-string-lessp-numerical
+ (car (gnus-gethash
+ (concat server group) nnimap-mailbox-info))
+ (imap-mailbox-get 'uidnext group nnimap-server-buffer))
+ (push (list group) slowgroups)
+ (insert (cdr (gnus-gethash (concat server group)
+ nnimap-mailbox-info))))))))
+ (dolist (group slowgroups)
+ (if nnimap-retrieve-groups-asynchronous
+ (setq group (car group)))
+ (gnus-message 7 "nnimap: Rechecking mailbox %s" group)
+ (imap-mailbox-put 'uidnext nil group nnimap-server-buffer)
+ (or (member "\\NoSelect" (imap-mailbox-get 'list-flags group
+ nnimap-server-buffer))
+ (let* ((info (nnimap-find-minmax-uid group 'examine))
+ (str (format "\"%s\" %d %d y\n" group
+ (or (nth 2 info) 0)
+ (max 1 (or (nth 1 info) 1)))))
+ (when (> (or (imap-mailbox-get 'recent group
+ nnimap-server-buffer) 0)
+ 0)
+ (push (list (cons group 0)) nnmail-split-history))
+ (insert str)
+ (when nnimap-retrieve-groups-asynchronous
+ (gnus-sethash
+ (concat server group)
+ (cons (or (imap-mailbox-get
+ 'uidnext group nnimap-server-buffer)
+ (imap-mailbox-status
+ group 'uidnext nnimap-server-buffer))
+ str)
+ nnimap-mailbox-info)))))))
(gnus-message 5 "nnimap: Checking mailboxes...done")
'active))
;; remove dupes
seen (sort seen '<)
seen (gnus-compress-sequence seen t)
- ;; we can't return '(1) since this isn't a "list of ranges",
- ;; and we can't return '((1)) since g-list-of-unread-articles
+ ;; we can't return '(1) since this isn't a "list of ranges",
+ ;; and we can't return '((1)) since g-list-of-unread-articles
;; is buggy so we return '((1 . 1)).
seen (if (and (integerp (car seen))
(null (cdr seen)))
gnus-article-mark-lists)
(when nnimap-importantize-dormant
- ;; nnimap mark dormant article as ticked too (for other clients)
+ ;; nnimap mark dormant article as ticked too (for other clients)
;; so we remove that mark for gnus since we support dormant
(gnus-info-set-marks
info
nil)
(defun nnimap-split-fancy ()
- "Like nnmail-split-fancy, but uses nnimap-split-fancy."
+ "Like the function `nnmail-split-fancy', but uses `nnimap-split-fancy'."
(let ((nnmail-split-fancy nnimap-split-fancy))
(nnmail-split-fancy)))
(goto-char (point-min))
(when (and (if (stringp regexp)
(progn
- (setq regrepp (string-match "\\\\[0-9&]" group))
+ (if (not (stringp group))
+ (setq group (eval group))
+ (setq regrepp
+ (string-match "\\\\[0-9&]" group)))
(re-search-forward regexp nil t))
(funcall regexp group))
- ;; Don't enter the article into the same group twice.
+ ;; Don't enter the article into the same group twice.
(not (assoc group to-groups)))
(push (if regrepp
(nnmail-expand-newtext group)
(message "IMAP split moved %s:%s:%d to %s" server
inbox article to-group)
(setq removeorig t)
- ;; Add the group-art list to the history list.
+ (when nnmail-cache-accepted-message-ids
+ (with-current-buffer nntp-server-buffer
+ (let (msgid)
+ (and (setq msgid
+ (nnmail-fetch-field "message-id"))
+ (nnmail-cache-insert msgid to-group)))))
+ ;; Add the group-art list to the history list.
(push (list (cons to-group 0)) nnmail-split-history))
(t
(message "IMAP split failed to move %s:%s:%d to %s"
server inbox article to-group))))
- ;; remove article if it was successfully copied somewhere
+ ;; remove article if it was successfully copied somewhere
(and removeorig
(imap-message-flags-add (format "%d" article)
"\\Seen \\Deleted")))))
(when (imap-mailbox-select inbox) ;; just in case
- ;; todo: UID EXPUNGE (if available) to remove splitted articles
+ ;; todo: UID EXPUNGE (if available) to remove splitted articles
(imap-mailbox-expunge)
(imap-mailbox-close)))
+ (when nnmail-cache-accepted-message-ids
+ (nnmail-cache-close))
t))))
(deffoo nnimap-request-scan (&optional group server)
(gnus-message 5 "nnimap: Marking article %d for deletion..."
imap-current-message))
-
(defun nnimap-expiry-target (arts group server)
(unless (eq nnmail-expiry-target 'delete)
(with-temp-buffer
(let ((nnimap-current-move-article art)
(nnimap-current-move-group group)
(nnimap-current-move-server server))
- (nnmail-expiry-target-group nnmail-expiry-target group))))))
+ (nnmail-expiry-target-group nnmail-expiry-target group))))
+ ;; It is not clear if `nnmail-expiry-target' somehow cause the
+ ;; current group to be changed or not, so we make sure here.
+ (nnimap-possibly-change-group group server)))
;; Notice that we don't actually delete anything, we just mark them deleted.
(deffoo nnimap-request-expire-articles (articles group &optional server force)
(setq result (eval accept-form))
(kill-buffer buf)
result)
- (nnimap-request-expire-articles (list article) group server t))
+ (imap-message-flags-add
+ (imap-range-to-message-set (list article))
+ "\\Deleted" 'silent nnimap-server-buffer))
result))))
(deffoo nnimap-request-accept-article (group &optional server last)
(let (uid)
(if (setq uid
(if (string= nnimap-current-server nnimap-current-move-server)
- ;; moving article within same server, speed it up...
+ ;; moving article within same server, speed it up...
(and (nnimap-possibly-change-group
nnimap-current-move-group)
(imap-message-copy (number-to-string
nnimap-server-buffer))
(with-current-buffer (current-buffer)
(goto-char (point-min))
- ;; remove any 'From blabla' lines, some IMAP servers
+ ;; remove any 'From blabla' lines, some IMAP servers
;; reject the entire message otherwise.
(when (looking-at "^From[^:]")
(kill-region (point) (progn (forward-line) (point))))
;; turn into rfc822 format (\r\n eol's)
(while (search-forward "\n" nil t)
- (replace-match "\r\n")))
+ (replace-match "\r\n"))
+ (when nnmail-cache-accepted-message-ids
+ (nnmail-cache-insert (nnmail-fetch-field "message-id")
+ group)))
+ (when (and last nnmail-cache-accepted-message-ids)
+ (nnmail-cache-close))
;; this 'or' is for Cyrus server bug
(or (null (imap-current-mailbox nnimap-server-buffer))
(imap-mailbox-unselect nnimap-server-buffer))
(defun nnimap-expunge (mailbox server)
(when (nnimap-possibly-change-group mailbox server)
- (imap-mailbox-expunge nnimap-server-buffer)))
+ (imap-mailbox-expunge nil nnimap-server-buffer)))
(defun nnimap-acl-get (mailbox server)
(when (nnimap-possibly-change-server server)