X-Git-Url: http://git.chise.org/gitweb/?a=blobdiff_plain;f=lisp%2Fnnimap.el;h=f4e9c35bf7308d50edeed96e9d6339b5068e8cb0;hb=ebdecdf203f300217a9a7f533dcf43fec5d427b4;hp=c8c57f974a1dbfee133c15304b73f45a704b1a0e;hpb=22d67eb69d38558636bccda449388713db84b44d;p=elisp%2Fgnus.git- diff --git a/lisp/nnimap.el b/lisp/nnimap.el index c8c57f9..f4e9c35 100644 --- a/lisp/nnimap.el +++ b/lisp/nnimap.el @@ -1,5 +1,5 @@ ;;; nnimap.el --- imap backend for Gnus -;; Copyright (C) 1998,1999 Free Software Foundation, Inc. +;; Copyright (C) 1998, 1999, 2000, 2001, 2002 Free Software Foundation, Inc. ;; Author: Simon Josefsson ;; Jim Radford @@ -36,15 +36,13 @@ ;; ;; Todo, minor things: ;; -;; o Support escape characters in `message-tokenize-header' -;; o Split-fancy. -;; o Support NOV nnmail-extra-headers. +;; o Don't require half of Gnus -- backends should be standalone ;; o Verify that we don't use IMAP4rev1 specific things (RFC2060 App B) ;; o Dont uid fetch 1,* in nnimap-retrive-groups (slow) ;; o Split up big fetches (1,* header especially) in smaller chunks ;; o What do I do with gnus-newsgroup-*? ;; o Tell Gnus about new groups (how can we tell?) -;; o Respooling (fix Gnus?) (unnecessery?) +;; o Respooling (fix Gnus?) (unnecessary?) ;; o Add support for the following: (if applicable) ;; request-list-newsgroups, request-regenerate ;; list-active-group, @@ -54,27 +52,30 @@ ;; o IMAP2BIS compatibility? (RFC2061) ;; o ACAP stuff (perhaps a different project, would be nice to ACAPify ;; .newsrc.eld) -;; o What about Gnus's article editing, can we support it? +;; 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 'imap)) - +(require 'imap) (require 'nnoo) (require 'nnmail) (require 'nnheader) (require 'mm-util) (require 'gnus) -(require 'gnus-async) (require 'gnus-range) (require 'gnus-start) (require 'gnus-int) (nnoo-declare nnimap) -(defconst nnimap-version "nnimap 0.131") +(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.") @@ -85,20 +86,36 @@ If nil, defaults to 993 for SSL connections and 143 otherwise.") ;; 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-rules'. +`nnimap-split-rule'. + +This can be a string or a list of strings." + :group 'nnimap + :type '(choice (string) + (repeat string))) -This can be a string or a list of strings.") +(define-widget 'nnimap-strict-function 'function + "This widget only matches values that are functionp. -(defvar nnimap-split-rule nil - "*Mail will be split according to theese rules. +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) + +(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 these rules. Mail is read from mailbox(es) specified in `nnimap-split-inbox'. @@ -107,22 +124,110 @@ If you'd like, for instance, one mail group for mail from the 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 -element in each \"rule\" is the name of the IMAP mailbox, and the -second is a regexp that nnimap will try to match on the header to find -a fit. - -The first element can also be a list. In that case, the first element -is the server the second element is the group on that server in which -the matching article will be stored. +As you can see, `nnimap-split-rule' is a list of lists, where the +first element in each \"rule\" is the name of the IMAP mailbox (or the +symbol `junk' if you want to remove the mail), and the second is a +regexp that nnimap will try to match on the header to find a fit. The second element can also be a function. In that case, it will be called narrowed to the headers with the first element of the rule as the argument. It should return a non-nil value if it thinks that the -mail belongs in that group.") +mail belongs in that group. + +This variable can also have a function as its value, the function will +be called with the headers narrowed and should return a group where it +thinks the article should be splitted to. See `nnimap-split-fancy'. + +To allow for different split rules on different virtual servers, and +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 + '((\"my1server\" (\".*\" ((\"ding\" \"ding@gnus.org\") + (\"junk\" \"From:.*Simon\"))) + (\"my2server\" (\"INBOX\" nnimap-split-fancy)) + (\"my[34]server\" (\".*\" ((\"private\" \"To:.*Simon\") + (\"junk\" my-junk-func))))) + +The virtual server name is in fact a regexp, so that the same rules +may apply to several servers. In the example, the servers +\"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." + :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." + :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 @@ -137,7 +242,7 @@ handle. Change this if -1) you want to connect with SSL. The SSL integration with IMAP is +1) you want to connect with SSL. The SSL integration with IMAP is brain-dead so you'll have to tell it specifically. 2) your server is more capable than your environment -- i.e. your @@ -161,27 +266,32 @@ Kerberos. Possible choices: kerberos4, cram-md5, login, anonymous.") (defvoo nnimap-directory (nnheader-concat gnus-directory "overview/") - "Directory to keep NOV cache files for nnimap groups. See also -`nnimap-nov-file-name'.") + "Directory to keep NOV cache files for nnimap groups. +See also `nnimap-nov-file-name'.") (defvoo nnimap-nov-file-name "nnimap." - "NOV cache base filename. The group name and -`nnimap-nov-file-name-suffix' will be appended. A typical complete -file name would be ~/News/overview/nnimap.pdc.INBOX.ding.nov, or + "NOV cache base filename. +The group name and `nnimap-nov-file-name-suffix' will be appended. A +typical complete file name would be +~/News/overview/nnimap.pdc.INBOX.ding.nov, or ~/News/overview/nnimap/pdc/INBOX/ding/nov if `nnmail-use-long-file-names' is nil") (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. Unlike other backends, you do not need to take special -care if you flip this variable.") +(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.") (defvoo nnimap-expunge-on-close 'always ; 'ask, 'never - "When a IMAP group with articles marked for deletion is closed, this + "Whether to expunge a group when it is closed. +When a IMAP group with articles marked for deletion is closed, this variable determine if nnimap should actually remove the articles or not. @@ -193,11 +303,11 @@ When setting this variable to `never', you can only expunge articles by using `G x' (gnus-group-nnimap-expunge) from the Group buffer.") (defvoo nnimap-list-pattern "*" - "A string LIMIT or list of strings with mailbox wildcards used to -limit available groups. Se below for available wildcards. + "A string LIMIT or list of strings with mailbox wildcards used to limit available groups. +See below for available wildcards. The LIMIT string can be a cons cell (REFERENCE . LIMIT), where -REFERENCE will be passed as the first parameter to LIST/LSUB. The +REFERENCE will be passed as the first parameter to LIST/LSUB. The semantics of this are server specific, on the University of Washington server you can specify a directory. @@ -220,12 +330,28 @@ Example: This will match all groups not beginning with \"INBOX\". Note that there is nothing technically different between mail-like and -news-like mailboxes. If you wish to have a group with todo items or +news-like mailboxes. If you wish to have a group with todo items or similar which you wouldn't want to set up a mailing list for, you can 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. +The first %s is replaced by a UID set of articles to search on, +and the second %s is replaced by a date criterium. + +One useful (and perhaps the only useful) value to change this to would +be `UID %s NOT SENTSINCE %s' to make nnimap use the Date: header +instead of the internal date of messages. See section 6.4.4 of RFC +2060 for more information on valid strings.") + +(defvoo nnimap-importantize-dormant t + "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.") + (defvoo nnimap-server-address nil - "Obsolete. Use `nnimap-address'.") + "Obsolete. Use `nnimap-address'.") (defcustom nnimap-authinfo-file "~/.authinfo" "Authorization information for IMAP servers. In .netrc format." @@ -245,8 +371,7 @@ use this to make replies go directly to the group.") (string :format "Password: %v"))))))) (defcustom nnimap-prune-cache t - "If non-nil, nnimap check wheter articles still exist on server -before using data stored in NOV cache." + "If non-nil, nnimap check whether articles still exist on server before using data stored in NOV cache." :type 'boolean) (defvar nnimap-request-list-method 'imap-mailbox-list @@ -254,9 +379,15 @@ before using data stored in NOV cache." If this is 'imap-mailbox-lsub, then use a server-side subscription list to restrict visible folders.") +(defcustom nnimap-debug nil + "If non-nil, random debug spews are placed in *nnimap-debug* buffer." + :group 'nnimap + :type 'boolean) + ;; Internal variables: -(defvar nnimap-debug "*nnimap-debug*") +(defvar nnimap-debug-buffer "*nnimap-debug*") +(defvar nnimap-mailbox-info (gnus-make-hashtable 997)) (defvar nnimap-current-move-server nil) (defvar nnimap-current-move-group nil) (defvar nnimap-current-move-article nil) @@ -264,32 +395,16 @@ restrict visible folders.") (defvar nnimap-progress-chars '(?| ?/ ?- ?\\)) (defvar nnimap-progress-how-often 20) (defvar nnimap-counter) -(defvar nnimap-callback-callback-function nil - "Gnus callback the nnimap asynchronous callback should call.") -(defvar nnimap-callback-buffer nil - "Which buffer the asynchronous article prefetch callback should work in.") - -;; Various server variables. +(defvar nnimap-server-buffer-alist nil) ;; Map server name to buffers. +(defvar nnimap-current-server nil) ;; Current server +(defvar nnimap-server-buffer nil) ;; Current servers' buffer -;; Internal variables. -(defvar nnimap-server-buffer-alist nil) ;; Map server name to buffers. -(defvar nnimap-current-server nil) ;; Current server -(defvar nnimap-server-buffer nil) ;; Current servers' buffer (nnoo-define-basics nnimap) ;; Utility functions: -(defun nnimap-replace-in-string (string regexp to) - "Replace substrings in STRING matching REGEXP with TO." - (if (string-match regexp string) - (concat (substring string 0 (match-beginning 0)) - to - (nnimap-replace-in-string (substring string (match-end 0)) - regexp to)) - string)) - (defsubst nnimap-get-server-buffer (server) "Return buffer for SERVER, if nil use current server." (cadr (assoc (or server nnimap-current-server) nnimap-server-buffer-alist))) @@ -306,20 +421,46 @@ If SERVER is nil, uses the current server." group (gnus-server-to-method (format "nnimap:%s" server)))) (new-uidvalidity (imap-mailbox-get 'uidvalidity)) - (old-uidvalidity (gnus-group-get-parameter gnusgroup 'uidvalidity))) + (old-uidvalidity (gnus-group-get-parameter gnusgroup 'uidvalidity)) + (dir (file-name-as-directory (expand-file-name nnimap-directory))) + (nameuid (nnheader-translate-file-chars + (concat nnimap-nov-file-name + (if (equal server "") + "unnamed" + server) "." group "." old-uidvalidity + nnimap-nov-file-name-suffix) t)) + (file (if (or nnmail-use-long-file-names + (file-exists-p (expand-file-name nameuid dir))) + (expand-file-name nameuid dir) + (expand-file-name + (mm-encode-coding-string + (nnheader-replace-chars-in-string nameuid ?. ?/) + nnmail-pathname-coding-system) + dir)))) (if old-uidvalidity (if (not (equal old-uidvalidity new-uidvalidity)) - nil ;; uidvalidity clash + ;; uidvalidity clash + (gnus-delete-file file) (gnus-group-set-parameter gnusgroup 'uidvalidity new-uidvalidity) t) (gnus-group-add-parameter gnusgroup (cons 'uidvalidity new-uidvalidity)) t))) +(defun nnimap-before-find-minmax-bugworkaround () + "Function called before iterating through mailboxes with +`nnimap-find-minmax-uid'." + (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. + "Find lowest and highest active article number 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) @@ -328,7 +469,7 @@ If EXAMINE is non-nil the group is selected read-only." maxuid (if maxuid (max maxuid uid) uid))) 'UID)) (list (imap-mailbox-get 'exists) minuid maxuid))))) - + (defun nnimap-possibly-change-group (group &optional server) "Make GROUP the current group, and SERVER the current server." (when (nnimap-possibly-change-server server) @@ -339,12 +480,14 @@ If EXAMINE is non-nil the group is selected read-only." (if (or (nnimap-verify-uidvalidity group (or server nnimap-current-server)) (zerop (imap-mailbox-get 'exists group)) + t ;; for OGnus to see if ignoring uidvalidity + ;; changes has any bad effects. (yes-or-no-p (format - "nnimap: Group %s is not uidvalid. Continue? " group))) + "nnimap: Group %s is not uidvalid. Continue? " group))) imap-current-mailbox (imap-mailbox-unselect) - (error "nnimap: Group %s is not uid-valid." group)) + (error "nnimap: Group %s is not uid-valid" group)) (nnheader-report 'nnimap (imap-error-text))))))) (defun nnimap-replace-whitespace (string) @@ -368,37 +511,29 @@ If EXAMINE is non-nil the group is selected read-only." nnimap-progress-how-often) nnimap-progress-chars))) (with-current-buffer nntp-server-buffer - (nnheader-insert-nov - (with-current-buffer nnimap-server-buffer - (vector imap-current-message - (nnimap-replace-whitespace - (imap-message-envelope-subject imap-current-message)) - (nnimap-replace-whitespace - (imap-envelope-from - (car-safe (imap-message-envelope-from - imap-current-message)))) - (nnimap-replace-whitespace - (imap-message-envelope-date imap-current-message)) - (nnimap-replace-whitespace - (imap-message-envelope-message-id imap-current-message)) - (nnimap-replace-whitespace - (let ((str (if (imap-capability 'IMAP4rev1) - (nth 2 (assoc - "HEADER.FIELDS REFERENCES" - (imap-message-get - imap-current-message 'BODYDETAIL))) - (imap-message-get imap-current-message - 'RFC822.HEADER)))) - (if (> (length str) (length "References: ")) - (substring str (length "References: ")) - (if (and (setq str (imap-message-envelope-in-reply-to - imap-current-message)) - (string-match "<[^>]+>" str)) - (substring str (match-beginning 0) (match-end 0)))))) - (imap-message-get imap-current-message 'RFC822.SIZE) - (imap-body-lines (imap-message-body imap-current-message)) - nil ;; xref - nil))))) ;; extra-headers + (let (headers lines chars uid mbx) + (with-current-buffer nnimap-server-buffer + (setq uid imap-current-message + mbx imap-current-mailbox + headers (nnimap-demule + (if (imap-capability 'IMAP4rev1) + ;; 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))) + lines (imap-body-lines (imap-message-body imap-current-message)) + chars (imap-message-get imap-current-message 'RFC822.SIZE))) + (nnheader-insert-nov + (with-temp-buffer + (buffer-disable-undo) + (insert headers) + (let ((head (nnheader-parse-naked-head))) + (mail-header-set-number head uid) + (mail-header-set-chars head chars) + (mail-header-set-lines head lines) + (mail-header-set-xref + head (format "%s %s:%d" (system-name) mbx uid)) + head)))))) (defun nnimap-retrieve-which-headers (articles fetch-old) "Get a range of articles to fetch based on ARTICLES and FETCH-OLD." @@ -406,7 +541,7 @@ If EXAMINE is non-nil the group is selected read-only." (if (numberp (car-safe articles)) (imap-search (concat "UID " - (nnimap-range-to-string + (imap-range-to-message-set (gnus-compress-sequence (append (gnus-uncompress-sequence (and fetch-old @@ -417,23 +552,53 @@ If EXAMINE is non-nil the group is selected read-only." articles))))) (mapcar (lambda (msgid) (imap-search - (format "HEADER Message-Id %s" msgid))) + (format "HEADER Message-Id \"%s\"" msgid))) articles)))) (defun nnimap-group-overview-filename (group server) "Make pathname for GROUP on SERVER." - (let ((dir (file-name-as-directory (expand-file-name nnimap-directory))) - (file (nnheader-translate-file-chars - (concat nnimap-nov-file-name - (if (equal server "") - "unnamed" - server) "." group nnimap-nov-file-name-suffix) t))) - (if (or nnmail-use-long-file-names - (file-exists-p (concat dir file))) - (concat dir file) - (concat dir (mm-encode-coding-string - (nnheader-replace-chars-in-string file ?. ?/) - nnmail-pathname-coding-system))))) + (let* ((dir (file-name-as-directory (expand-file-name nnimap-directory))) + (uidvalidity (gnus-group-get-parameter + (gnus-group-prefixed-name + group (gnus-server-to-method + (format "nnimap:%s" server))) + 'uidvalidity)) + (name (nnheader-translate-file-chars + (concat nnimap-nov-file-name + (if (equal server "") + "unnamed" + server) "." group nnimap-nov-file-name-suffix) t)) + (nameuid (nnheader-translate-file-chars + (concat nnimap-nov-file-name + (if (equal server "") + "unnamed" + server) "." group "." uidvalidity + nnimap-nov-file-name-suffix) t)) + (oldfile (if (or nnmail-use-long-file-names + (file-exists-p (expand-file-name name dir))) + (expand-file-name name dir) + (expand-file-name + (mm-encode-coding-string + (nnheader-replace-chars-in-string name ?. ?/) + nnmail-pathname-coding-system) + dir))) + (newfile (if (or nnmail-use-long-file-names + (file-exists-p (expand-file-name nameuid dir))) + (expand-file-name nameuid dir) + (expand-file-name + (mm-encode-coding-string + (nnheader-replace-chars-in-string nameuid ?. ?/) + nnmail-pathname-coding-system) + dir)))) + (when (and (file-exists-p oldfile) (not (file-exists-p newfile))) + (message "nnimap: Upgrading novcache filename...") + (sit-for 1) + (gnus-make-directory (file-name-directory newfile)) + (unless (ignore-errors (rename-file oldfile newfile) t) + (if (ignore-errors (copy-file oldfile newfile) t) + (delete-file oldfile) + (error "Can't rename `%s' to `%s'" oldfile newfile)))) + newfile)) (defun nnimap-retrieve-headers-from-file (group server) (with-current-buffer nntp-server-buffer @@ -441,13 +606,11 @@ If EXAMINE is non-nil the group is selected read-only." (when (file-exists-p nov) (mm-insert-file-contents nov) (set-buffer-modified-p nil) - (let ((min (progn (goto-char (point-min)) - (when (not (eobp)) - (read (current-buffer))))) - (max (progn (goto-char (point-max)) - (forward-line -1) - (when (not (bobp)) - (read (current-buffer)))))) + (let ((min (ignore-errors (goto-char (point-min)) + (read (current-buffer)))) + (max (ignore-errors (goto-char (point-max)) + (forward-line -1) + (read (current-buffer))))) (if (and (numberp min) (numberp max)) (cons min max) ;; junk, remove it, it's saved later @@ -459,16 +622,21 @@ If EXAMINE is non-nil the group is selected read-only." (let ((imap-fetch-data-hook '(nnimap-retrieve-headers-progress)) (nnimap-length (gnus-range-length articles)) (nnimap-counter 0)) - (imap-fetch (nnimap-range-to-string articles) - (concat "(UID RFC822.SIZE ENVELOPE BODY " - (if (imap-capability 'IMAP4rev1) - "BODY.PEEK[HEADER.FIELDS (References)])" - "RFC822.HEADER.LINES (References))"))) + (imap-fetch (imap-range-to-message-set articles) + (concat "(UID RFC822.SIZE BODY " + (let ((headers + (append '(Subject From Date Message-Id + References In-Reply-To Xref) + (copy-sequence + nnmail-extra-headers)))) + (if (imap-capability 'IMAP4rev1) + (format "BODY.PEEK[HEADER.FIELDS %s])" headers) + (format "RFC822.HEADER.LINES %s)" headers))))) (and (numberp nnmail-large-newsgroup) (> nnimap-length nnmail-large-newsgroup) (nnheader-message 6 "nnimap: Retrieving headers...done"))))) -(defun nnimap-use-nov-p (group server) +(defun nnimap-dont-use-nov-p (group server) (or gnus-nov-is-evil nnimap-nov-is-evil (unless (and (gnus-make-directory (file-name-directory @@ -482,7 +650,7 @@ If EXAMINE is non-nil the group is selected read-only." (when (nnimap-possibly-change-group group server) (with-current-buffer nntp-server-buffer (erase-buffer) - (if (nnimap-use-nov-p group server) + (if (nnimap-dont-use-nov-p group server) (nnimap-retrieve-headers-from-server (gnus-compress-sequence articles) group server) (let (uids cached low high) @@ -512,8 +680,8 @@ If EXAMINE is non-nil the group is selected read-only." (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))) @@ -525,15 +693,15 @@ If EXAMINE is non-nil the group is selected read-only." (imap-capability 'IMAP4rev1 nnimap-server-buffer)) (imap-close nnimap-server-buffer) (nnheader-report 'nnimap "Server %s is not IMAP4 compliant" server)) - (let (list alist user passwd) - (and (fboundp 'gnus-parse-netrc) - (setq list (gnus-parse-netrc nnimap-authinfo-file) - alist (or (and (gnus-netrc-get - (gnus-netrc-machine list server) "machine") - (gnus-netrc-machine list server)) - (gnus-netrc-machine list nnimap-address)) - user (gnus-netrc-get alist "login") - passwd (gnus-netrc-get alist "password"))) + (let* ((list (gnus-parse-netrc nnimap-authinfo-file)) + (port (if nnimap-server-port + (int-to-string nnimap-server-port) + "imap")) + (alist (gnus-netrc-machine list (or nnimap-server-address + nnimap-address server) + port "imap")) + (user (gnus-netrc-get alist "login")) + (passwd (gnus-netrc-get alist "password"))) (if (imap-authenticate user passwd nnimap-server-buffer) (prog1 (push (list server nnimap-server-buffer) @@ -557,14 +725,23 @@ If EXAMINE is non-nil the group is selected read-only." (cadr (assq 'nnimap-server-address defs))) defs) (push (list 'nnimap-address server) defs))) (nnoo-change-server 'nnimap server defs) - (if (null nnimap-server-buffer) - (error "this shouldn't happen")) - (or (imap-opened nnimap-server-buffer) + (or nnimap-server-buffer + (setq nnimap-server-buffer (cadr (assq 'nnimap-server-buffer defs)))) + (with-current-buffer (get-buffer-create nnimap-server-buffer) + (nnoo-change-server 'nnimap server defs)) + (or (and nnimap-server-buffer + (imap-opened nnimap-server-buffer) + (if (with-current-buffer nnimap-server-buffer + (memq imap-state '(auth select examine))) + t + (imap-close nnimap-server-buffer) + (nnimap-open-connection server))) (nnimap-open-connection server)))) (deffoo nnimap-server-opened (&optional server) - "If SERVER is the current virtual server, and the connection to the -physical server is alive, this function return a non-nil value. If + "Whether SERVER is opened. +If SERVER is the current virtual server, and the connection to the +physical server is alive, this function return a non-nil value. If SERVER is nil, it is treated as the current server." ;; clean up autologouts?? (and (or server nnimap-current-server) @@ -572,8 +749,8 @@ SERVER is nil, it is treated as the current server." (imap-opened (nnimap-get-server-buffer server)))) (deffoo nnimap-close-server (&optional server) - "Close connection to server and free all resources connected to -it. Return nil if the server couldn't be closed for some reason." + "Close connection to server and free all resources connected to it. +Return nil if the server couldn't be closed for some reason." (let ((server (or server nnimap-current-server))) (when (or (nnimap-server-opened server) (imap-opened (nnimap-get-server-buffer server))) @@ -586,9 +763,9 @@ it. Return nil if the server couldn't be closed for some reason." (nnoo-close-server 'nnimap server))) (deffoo nnimap-request-close () - "Close connection to all servers and free all resources that the -backend have reserved. All buffers that have been created by that -backend should be killed. (Not the nntp-server-buffer, though.) This + "Close connection to all servers and free all resources that the backend have reserved. +All buffers that have been created by that +backend should be killed. (Not the nntp-server-buffer, though.) This function is generally only called when Gnus is shutting down." (mapcar (lambda (server) (nnimap-close-server (car server))) nnimap-server-buffer-alist) @@ -606,40 +783,61 @@ function is generally only called when Gnus is shutting down." 'identity) (or string ""))) -(defun nnimap-callback () - (remove-hook 'imap-fetch-data-hook 'nnimap-callback) - (with-current-buffer nnimap-callback-buffer - (insert - (with-current-buffer nnimap-server-buffer - (nnimap-demule (imap-message-get (imap-current-message) 'RFC822)))) ;xxx - (nnheader-ms-strip-cr) - (funcall nnimap-callback-callback-function t))) - -(defun nnimap-request-article-part (article part prop - &optional group server to-buffer) +(defun nnimap-make-callback (article gnus-callback buffer) + "Return a callback function." + `(lambda () + (nnimap-callback ,article ,gnus-callback ,buffer))) + +(defun nnimap-callback (article gnus-callback buffer) + (when (eq article (imap-current-message)) + (remove-hook 'imap-fetch-data-hook + (nnimap-make-callback article gnus-callback buffer)) + (with-current-buffer buffer + (insert + (with-current-buffer nnimap-server-buffer + (nnimap-demule + (if (imap-capability 'IMAP4rev1) + ;; xxx don't just use car? alist doesn't contain + ;; anything else now, but it might... + (nth 2 (car (imap-message-get article 'BODYDETAIL))) + (imap-message-get article 'RFC822))))) + (nnheader-ms-strip-cr) + (funcall gnus-callback t)))) + +(defun nnimap-request-article-part (article part prop &optional + group server to-buffer detail) (when (nnimap-possibly-change-group group server) (let ((article (if (stringp article) (car-safe (imap-search - (format "HEADER Message-Id %s" article) + (format "HEADER Message-Id \"%s\"" article) nnimap-server-buffer)) article))) (when article - (gnus-message 9 "nnimap: Fetching (part of) article %d..." article) + (gnus-message 10 "nnimap: Fetching (part of) article %d from %s..." + article (or group imap-current-mailbox + gnus-newsgroup-name)) (if (not nnheader-callback-function) (with-current-buffer (or to-buffer nntp-server-buffer) (erase-buffer) - (insert (nnimap-demule (imap-fetch article part prop nil - nnimap-server-buffer))) + (let ((data (imap-fetch article part prop nil + nnimap-server-buffer))) + (insert (nnimap-demule (if detail + (nth 2 (car data)) + data)))) (nnheader-ms-strip-cr) - (gnus-message 9 "nnimap: Fetching (part of) article %d...done" - article) + (gnus-message + 10 "nnimap: Fetching (part of) article %d from %s...done" + article (or group imap-current-mailbox gnus-newsgroup-name)) (if (bobp) - (nnheader-report 'nnimap "No such article: %s" + (nnheader-report 'nnimap "No such article %d in %s: %s" + article (or group imap-current-mailbox + gnus-newsgroup-name) (imap-error-text nnimap-server-buffer)) (cons group article))) - (add-hook 'imap-fetch-data-hook 'nnimap-callback) - (setq nnimap-callback-callback-function nnheader-callback-function - nnimap-callback-buffer nntp-server-buffer) + (add-hook 'imap-fetch-data-hook + (nnimap-make-callback article + nnheader-callback-function + nntp-server-buffer)) (imap-fetch-asynch article part nil nnimap-server-buffer) (cons group article)))))) @@ -647,16 +845,25 @@ function is generally only called when Gnus is shutting down." t) (deffoo nnimap-request-article (article &optional group server to-buffer) - (nnimap-request-article-part - article "RFC822.PEEK" 'RFC822 group server to-buffer)) + (if (imap-capability 'IMAP4rev1 nnimap-server-buffer) + (nnimap-request-article-part + article "BODY.PEEK[]" 'BODYDETAIL group server to-buffer 'detail) + (nnimap-request-article-part + article "RFC822.PEEK" 'RFC822 group server to-buffer))) (deffoo nnimap-request-head (article &optional group server to-buffer) - (nnimap-request-article-part - article "RFC822.HEADER" 'RFC822.HEADER group server to-buffer)) + (if (imap-capability 'IMAP4rev1 nnimap-server-buffer) + (nnimap-request-article-part + article "BODY.PEEK[HEADER]" 'BODYDETAIL group server to-buffer 'detail) + (nnimap-request-article-part + article "RFC822.HEADER" 'RFC822.HEADER group server to-buffer))) (deffoo nnimap-request-body (article &optional group server to-buffer) - (nnimap-request-article-part - article "RFC822.TEXT.PEEK" 'RFC822.TEXT group server to-buffer)) + (if (imap-capability 'IMAP4rev1 nnimap-server-buffer) + (nnimap-request-article-part + article "BODY.PEEK[TEXT]" 'BODYDETAIL group server to-buffer 'detail) + (nnimap-request-article-part + article "RFC822.TEXT.PEEK" 'RFC822.TEXT group server to-buffer))) (deffoo nnimap-request-group (group &optional server fast) (nnimap-request-update-info-internal @@ -665,6 +872,7 @@ function is generally only called when Gnus is shutting down." group (gnus-server-to-method (format "nnimap:%s" server)))) server) (when (nnimap-possibly-change-group group server) + (nnimap-before-find-minmax-bugworkaround) (let (info) (cond (fast group) ((null (setq info (nnimap-find-minmax-uid group t))) @@ -677,20 +885,35 @@ function is generally only called when Gnus is shutting down." (nnheader-report 'nnimap "Group %s selected" group) t))))) +(defun nnimap-update-unseen (group &optional server) + "Update the unseen count in `nnimap-mailbox-info'." + (gnus-sethash + (gnus-group-prefixed-name group server) + (let ((old (gnus-gethash-safe (gnus-group-prefixed-name group server) + nnimap-mailbox-info))) + (list (nth 0 old) (nth 1 old) + (imap-mailbox-status group 'unseen nnimap-server-buffer) + (nth 3 old))) + nnimap-mailbox-info)) + (defun nnimap-close-group (group &optional server) (with-current-buffer nnimap-server-buffer (when (and (imap-opened) (nnimap-possibly-change-group group server)) + (nnimap-update-unseen 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)))) @@ -708,6 +931,7 @@ function is generally only called when Gnus is shutting down." (erase-buffer)) (gnus-message 5 "nnimap: Generating active list%s..." (if (> (length server) 0) (concat " for " server) "")) + (nnimap-before-find-minmax-bugworkaround) (with-current-buffer nnimap-server-buffer (dolist (pattern (nnimap-pattern-to-list-arguments nnimap-list-pattern)) (dolist (mbx (funcall nnimap-request-list-method @@ -715,11 +939,9 @@ function is generally only called when Gnus is shutting down." (or (member "\\NoSelect" (imap-mailbox-get 'list-flags mbx)) (let ((info (nnimap-find-minmax-uid mbx 'examine))) (when info - ;; Escape SPC in mailboxes xxx relies on gnus internals (with-current-buffer nntp-server-buffer - (insert (format "%s %d %d y\n" - (nnimap-replace-in-string mbx " " "\\ ") - (or (nth 2 info) 0) + (insert (format "\"%s\" %d %d y\n" + mbx (or (nth 2 info) 0) (max 1 (or (nth 1 info) 1))))))))))) (gnus-message 5 "nnimap: Generating active list%s...done" (if (> (length server) 0) (concat " for " server) "")) @@ -727,8 +949,9 @@ function is generally only called when Gnus is shutting down." (deffoo nnimap-request-post (&optional server) (let ((success t)) - (dolist (mbx (message-tokenize-header - (message-fetch-field "Newsgroups")) success) + (dolist (mbx (message-unquote-tokens + (message-tokenize-header + (message-fetch-field "Newsgroups") ", ")) success) (let ((to-newsgroup (gnus-group-prefixed-name mbx gnus-command-method))) (or (gnus-active to-newsgroup) (gnus-activate-group to-newsgroup) @@ -745,23 +968,98 @@ function is generally only called when Gnus is shutting down." ;; 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) - (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))) - ;; Escape SPC in mailboxes xxx relies on gnus internals - (insert (format "211 %d %d %d %s\n" (or (nth 0 info) 0) - (max 1 (or (nth 1 info) 1)) - (or (nth 2 info) 0) - (nnimap-replace-in-string group " " "\\ "))))))) + (nnimap-before-find-minmax-bugworkaround) + (let (asyncgroups slowgroups) + (if (null nnimap-retrieve-groups-asynchronous) + (setq slowgroups groups) + (dolist (group groups) + (gnus-message 9 "nnimap: Quickly checking mailbox %s" group) + (add-to-list (if (gnus-gethash-safe + (gnus-group-prefixed-name group server) + nnimap-mailbox-info) + 'asyncgroups + 'slowgroups) + (list group (imap-mailbox-status-asynch + group '(uidvalidity uidnext unseen) + 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 (or (not (string= + (nth 0 (gnus-gethash (gnus-group-prefixed-name + group server) + nnimap-mailbox-info)) + (imap-mailbox-get 'uidvalidity group + nnimap-server-buffer))) + (not (string= + (nth 1 (gnus-gethash (gnus-group-prefixed-name + group server) + nnimap-mailbox-info)) + (imap-mailbox-get 'uidnext group + nnimap-server-buffer)))) + (push (list group) slowgroups) + (insert (nth 3 (gnus-gethash (gnus-group-prefixed-name + group server) + nnimap-mailbox-info)))))))) + (dolist (group slowgroups) + (if nnimap-retrieve-groups-asynchronous + (setq group (car group))) + (gnus-message 7 "nnimap: Mailbox %s modified" 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 + (gnus-group-prefixed-name group server) + (list (or (imap-mailbox-get + 'uidvalidity group nnimap-server-buffer) + (imap-mailbox-status + group 'uidvalidity nnimap-server-buffer)) + (or (imap-mailbox-get + 'uidnext group nnimap-server-buffer) + (imap-mailbox-status + group 'uidnext nnimap-server-buffer)) + (or (imap-mailbox-get + 'unseen group nnimap-server-buffer) + (imap-mailbox-status + group 'unseen nnimap-server-buffer)) + str) + nnimap-mailbox-info))))))) (gnus-message 5 "nnimap: Checking mailboxes...done") - 'groups)) + 'active)) (deffoo nnimap-request-update-info-internal (group info &optional server) (when (nnimap-possibly-change-group group server) @@ -769,7 +1067,7 @@ function is generally only called when Gnus is shutting down." (with-current-buffer nnimap-server-buffer (gnus-message 5 "nnimap: Updating info for %s..." (gnus-info-group info)) - + (when (nnimap-mark-permanent-p 'read) (let (seen unseen) ;; read info could contain articles marked unread by other @@ -792,20 +1090,34 @@ function is generally only called when Gnus is shutting down." seen)) (gnus-info-set-read info seen))) - (mapc (lambda (pred) - (when (and (nnimap-mark-permanent-p (cdr pred)) - (member (nnimap-mark-to-flag (cdr pred)) - (imap-mailbox-get 'flags))) - (gnus-info-set-marks - info - (nnimap-update-alist-soft - (cdr pred) - (gnus-compress-sequence - (imap-search (nnimap-mark-to-predicate (cdr pred)))) - (gnus-info-marks info)) - t))) - gnus-article-mark-lists) - + (mapcar (lambda (pred) + (when (or (eq (cdr pred) 'recent) + (and (nnimap-mark-permanent-p (cdr pred)) + (member (nnimap-mark-to-flag (cdr pred)) + (imap-mailbox-get 'flags)))) + (gnus-info-set-marks + info + (gnus-update-alist-soft + (cdr pred) + (gnus-compress-sequence + (imap-search (nnimap-mark-to-predicate (cdr pred)))) + (gnus-info-marks info)) + t))) + gnus-article-mark-lists) + + (when nnimap-importantize-dormant + ;; 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 + (gnus-update-alist-soft + 'tick + (gnus-remove-from-range + (cdr-safe (assoc 'tick (gnus-info-marks info))) + (cdr-safe (assoc 'dormant (gnus-info-marks info)))) + (gnus-info-marks info)) + t)) + (gnus-message 5 "nnimap: Updating info for %s...done" (gnus-info-group info)) @@ -826,11 +1138,22 @@ function is generally only called when Gnus is shutting down." (what (nth 1 action)) (cmdmarks (nth 2 action)) marks) + ;; bookmark can't be stored (not list/range + (setq cmdmarks (delq 'bookmark cmdmarks)) + ;; killed can't be stored (not list/range + (setq cmdmarks (delq 'killed cmdmarks)) + ;; unsent are for nndraft groups only + (setq cmdmarks (delq 'unsent cmdmarks)) ;; cache flags are pointless on the server (setq cmdmarks (delq 'cache cmdmarks)) - ;; flag dormant articles as ticked - (if (memq 'dormant cmdmarks) - (setq cmdmarks (cons 'tick cmdmarks))) + ;; seen flags are local to each gnus + (setq cmdmarks (delq 'seen cmdmarks)) + ;; recent marks can't be set + (setq cmdmarks (delq 'recent cmdmarks)) + (when nnimap-importantize-dormant + ;; flag dormant articles as ticked + (if (memq 'dormant cmdmarks) + (setq cmdmarks (cons 'tick cmdmarks)))) ;; remove stuff we are forbidden to store (mapcar (lambda (mark) (if (imap-message-flag-permanent-p @@ -840,19 +1163,24 @@ function is generally only called when Gnus is shutting down." (when (and range marks) (cond ((eq what 'del) (imap-message-flags-del - (nnimap-range-to-string range) + (imap-range-to-message-set range) (nnimap-mark-to-flag marks nil t))) ((eq what 'add) (imap-message-flags-add - (nnimap-range-to-string range) + (imap-range-to-message-set range) (nnimap-mark-to-flag marks nil t))) ((eq what 'set) (imap-message-flags-set - (nnimap-range-to-string range) + (imap-range-to-message-set range) (nnimap-mark-to-flag marks nil t))))))) (gnus-message 7 "nnimap: Setting marks in %s...done" group)))) nil) +(defun nnimap-split-fancy () + "Like the function `nnmail-split-fancy', but uses `nnimap-split-fancy'." + (let ((nnmail-split-fancy nnimap-split-fancy)) + (nnmail-split-fancy))) + (defun nnimap-split-to-groups (rules) ;; tries to match all rules in nnimap-split-rule against content of ;; nntp-server-buffer, returns a list of groups that matched. @@ -871,7 +1199,10 @@ function is generally only called when Gnus is shutting down." (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. @@ -882,9 +1213,22 @@ function is generally only called when Gnus is shutting down." to-groups) (or nnimap-split-crosspost (throw 'split-done to-groups)))))))))) - + +(defun nnimap-assoc-match (key alist) + (let (element) + (while (and alist (not element)) + (if (string-match (car (car alist)) key) + (setq element (car alist))) + (setq alist (cdr alist))) + element)) + (defun nnimap-split-find-rule (server inbox) - nnimap-split-rule) + (if (and (listp nnimap-split-rule) (listp (car nnimap-split-rule)) + (list (cdar nnimap-split-rule)) (listp (cadar nnimap-split-rule))) + ;; extended format + (cadr (nnimap-assoc-match inbox (cdr (nnimap-assoc-match + server nnimap-split-rule)))) + nnimap-split-rule)) (defun nnimap-split-find-inbox (server) (if (listp nnimap-split-inbox) @@ -901,21 +1245,31 @@ function is generally only called when Gnus is shutting down." ;; find split rule for this server / inbox (when (setq rule (nnimap-split-find-rule server inbox)) ;; iterate over articles - (dolist (article (imap-search "UNSEEN UNDELETED")) + (dolist (article (imap-search nnimap-split-predicate)) (when (nnimap-request-head article) ;; copy article to right group(s) (setq removeorig nil) (dolist (to-group (nnimap-split-to-groups rule)) - (if (imap-message-copy (number-to-string article) - to-group nil 'nocopyuid) - (progn - (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. - (push (list (cons to-group 0)) nnmail-split-history)) - (message "IMAP split failed to move %s:%s:%d to %s" server - inbox article to-group))) + (cond ((eq to-group 'junk) + (message "IMAP split removed %s:%s:%d" server inbox + article) + (setq removeorig t)) + ((imap-message-copy (number-to-string article) + to-group nil 'nocopyuid) + (message "IMAP split moved %s:%s:%d to %s" server + inbox article to-group) + (setq removeorig t) + (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 (and removeorig (imap-message-flags-add (format "%d" article) @@ -924,6 +1278,8 @@ function is generally only called when Gnus is shutting down." ;; 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) @@ -935,25 +1291,26 @@ function is generally only called when Gnus is shutting down." (gnus-message 5 "nnimap: Listing subscribed mailboxes%s%s..." (if (> (length server) 0) " on " "") server) (erase-buffer) + (nnimap-before-find-minmax-bugworkaround) (dolist (pattern (nnimap-pattern-to-list-arguments nnimap-list-pattern)) - (dolist (mbx (imap-mailbox-lsub "*" (car pattern) nil + (dolist (mbx (imap-mailbox-lsub "*" (car pattern) nil nnimap-server-buffer)) - (or (member-if (lambda (mailbox) - (string= (downcase mailbox) "\\noselect")) - (imap-mailbox-get 'list-flags mbx - nnimap-server-buffer)) - ;; Escape SPC in mailboxes xxx relies on gnus internals + (or (catch 'found + (dolist (mailbox (imap-mailbox-get 'list-flags mbx + nnimap-server-buffer)) + (if (string= (downcase mailbox) "\\noselect") + (throw 'found t))) + nil) (let ((info (nnimap-find-minmax-uid mbx 'examine))) (when info - (insert (format "%s %d %d y\n" - (nnimap-replace-in-string mbx " " "\\ ") - (or (nth 2 info) 0) + (insert (format "\"%s\" %d %d y\n" + mbx (or (nth 2 info) 0) (max 1 (or (nth 1 info) 1))))))))) (gnus-message 5 "nnimap: Listing subscribed mailboxes%s%s...done" (if (> (length server) 0) " on " "") server)) t)) - + (deffoo nnimap-request-create-group (group &optional server args) (when (nnimap-possibly-change-server server) (or (imap-mailbox-status group 'uidvalidity nnimap-server-buffer) @@ -969,10 +1326,12 @@ function is generally only called when Gnus is shutting down." (defun nnimap-date-days-ago (daysago) "Return date, in format \"3-Aug-1998\", for DAYSAGO days ago." - (let ((date (format-time-string "%d-%b-%Y" - (nnimap-time-substract - (current-time) - (days-to-time daysago))))) + (let* ((time (nnimap-time-substract (current-time) (days-to-time daysago))) + (date (format-time-string + (format "%%d-%s-%%Y" + (capitalize (car (rassoc (nth 4 (decode-time time)) + parse-time-months)))) + time))) (if (eq ?0 (string-to-char date)) (substring date 1) date))) @@ -981,36 +1340,53 @@ function is generally only called when Gnus is shutting down." (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 + (dolist (art arts) + (nnimap-request-article art group server (current-buffer)) + ;; hints for optimization in `nnimap-request-accept-article' + (let ((nnimap-current-move-article art) + (nnimap-current-move-group group) + (nnimap-current-move-server server)) + (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) (let ((artseq (gnus-compress-sequence articles))) (when (and artseq (nnimap-possibly-change-group group server)) (with-current-buffer nnimap-server-buffer - (if force - (and (imap-message-flags-add - (nnimap-range-to-string artseq) "\\Deleted") - (setq articles nil)) - (let ((days (or (and nnmail-expiry-wait-function - (funcall nnmail-expiry-wait-function group)) - nnmail-expiry-wait))) - (cond ((eq days 'immediate) - (and (imap-message-flags-add - (nnimap-range-to-string artseq) "\\Deleted") - (setq articles nil))) - ((numberp days) - (let ((oldarts (imap-search - (format "UID %s NOT SINCE %s" - (nnimap-range-to-string artseq) - (nnimap-date-days-ago days)))) - (imap-fetch-data-hook - '(nnimap-request-expire-articles-progress))) - (and oldarts - (imap-message-flags-add - (nnimap-range-to-string - (gnus-compress-sequence oldarts)) - "\\Deleted") - (setq articles (gnus-set-difference - articles oldarts))))))))))) + (let ((days (or (and nnmail-expiry-wait-function + (funcall nnmail-expiry-wait-function group)) + nnmail-expiry-wait))) + (cond ((or force (eq days 'immediate)) + (let ((oldarts (imap-search + (concat "UID " + (imap-range-to-message-set artseq))))) + (when oldarts + (nnimap-expiry-target oldarts group server) + (when (imap-message-flags-add + (imap-range-to-message-set + (gnus-compress-sequence oldarts)) "\\Deleted") + (setq articles (gnus-set-difference + articles oldarts)))))) + ((numberp days) + (let ((oldarts (imap-search + (format nnimap-expunge-search-string + (imap-range-to-message-set artseq) + (nnimap-date-days-ago days)))) + (imap-fetch-data-hook + '(nnimap-request-expire-articles-progress))) + (when oldarts + (nnimap-expiry-target oldarts group server) + (when (imap-message-flags-add + (imap-range-to-message-set + (gnus-compress-sequence oldarts)) "\\Deleted") + (setq articles (gnus-set-difference + articles oldarts))))))))))) ;; return articles not deleted articles) @@ -1031,9 +1407,11 @@ function is generally only called when Gnus is shutting down." (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) (when (nnimap-possibly-change-server server) (let (uid) @@ -1046,13 +1424,23 @@ function is generally only called when Gnus is shutting down." nnimap-current-move-article) group 'dontcreate nil nnimap-server-buffer)) - ;; turn into rfc822 format (\r\n eol's) (with-current-buffer (current-buffer) (goto-char (point-min)) + ;; 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"))) - ;; next line for Cyrus server bug - (imap-mailbox-unselect nnimap-server-buffer) + (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)) (imap-message-append group (current-buffer) nil nil nnimap-server-buffer))) (cons group (nth 1 uid)) @@ -1073,11 +1461,12 @@ function is generally only called when Gnus is shutting down." (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) - (imap-mailbox-acl-get mailbox nnimap-server-buffer))) + (and (imap-capability 'ACL nnimap-server-buffer) + (imap-mailbox-acl-get mailbox nnimap-server-buffer)))) (defun nnimap-acl-edit (mailbox method old-acls new-acls) (when (nnimap-possibly-change-server (cadr method)) @@ -1087,18 +1476,18 @@ function is generally only called when Gnus is shutting down." ;; delete all removed identifiers (mapcar (lambda (old-acl) (unless (assoc (car old-acl) new-acls) - (or (imap-mailbox-acl-delete (car old-acl) mailbox) - (error "Can't delete ACL for %s" (car old-acl))))) + (or (imap-mailbox-acl-delete (car old-acl) mailbox) + (error "Can't delete ACL for %s" (car old-acl))))) old-acls) ;; set all changed acl's (mapcar (lambda (new-acl) (let ((new-rights (cdr new-acl)) (old-rights (cdr (assoc (car new-acl) old-acls)))) - (unless (and old-rights new-rights - (string= old-rights new-rights)) - (or (imap-mailbox-acl-set (car new-acl) new-rights mailbox) - (error "Can't set ACL for %s to %s" (car new-acl) - new-rights))))) + (unless (and old-rights new-rights + (string= old-rights new-rights)) + (or (imap-mailbox-acl-set (car new-acl) new-rights mailbox) + (error "Can't set ACL for %s to %s" (car new-acl) + new-rights))))) new-acls) t))) @@ -1113,37 +1502,39 @@ function is generally only called when Gnus is shutting down." ;; predicate => "SEEN", "FLAGGED", "DRAFT", "KEYWORD gnus-expire" etc ;; ;; Mark should not really contain 'read since it's not a "mark" in the Gnus -;; world, but we cheat. Mark == gnus-article-mark-lists + '(read . read). +;; world, but we cheat. Mark == gnus-article-mark-lists + '(read . read). ;; (defconst nnimap-mark-to-predicate-alist (mapcar - (lambda (pair) ; cdr is the mark + (lambda (pair) ; cdr is the mark (or (assoc (cdr pair) - '((read . "SEEN") - (tick . "FLAGGED") - (draft . "DRAFT") - (reply . "ANSWERED"))) - (cons (cdr pair) - (format "KEYWORD gnus-%s" (symbol-name (cdr pair)))))) + '((read . "SEEN") + (tick . "FLAGGED") + (draft . "DRAFT") + (recent . "RECENT") + (reply . "ANSWERED"))) + (cons (cdr pair) + (format "KEYWORD gnus-%s" (symbol-name (cdr pair)))))) (cons '(read . read) gnus-article-mark-lists))) (defun nnimap-mark-to-predicate (pred) - "Convert a Gnus mark (a symbol such as read, tick, expire) to a IMAP -predicate (a string such as \"SEEN\", \"FLAGGED\", \"KEYWORD -gnus-expire\") to be used within a IMAP SEARCH query." + "Convert a Gnus mark (a symbol such as read, tick, expire) to a IMAP predicate. +This is a string such as \"SEEN\", \"FLAGGED\", \"KEYWORD gnus-expire\", +to be used within a IMAP SEARCH query." (cdr (assq pred nnimap-mark-to-predicate-alist))) (defconst nnimap-mark-to-flag-alist (mapcar (lambda (pair) (or (assoc (cdr pair) - '((read . "\\Seen") - (tick . "\\Flagged") - (draft . "\\Draft") - (reply . "\\Answered"))) - (cons (cdr pair) - (format "gnus-%s" (symbol-name (cdr pair)))))) + '((read . "\\Seen") + (tick . "\\Flagged") + (draft . "\\Draft") + (recent . "\\Recent") + (reply . "\\Answered"))) + (cons (cdr pair) + (format "gnus-%s" (symbol-name (cdr pair)))))) (cons '(read . read) gnus-article-mark-lists))) (defun nnimap-mark-to-flag-1 (preds) @@ -1153,8 +1544,8 @@ gnus-expire\") to be used within a IMAP SEARCH query." (cdr (assoc preds nnimap-mark-to-flag-alist)))) (defun nnimap-mark-to-flag (preds &optional always-list make-string) - "Convert a Gnus mark (a symbol such as read, tick, expire) to a IMAP -flag (a string such as \"\\Seen\", \"\\Flagged\", \"gnus-expire\") to + "Convert a Gnus mark (a symbol such as read, tick, expire) to a IMAP flag. +This is a string such as \"\\Seen\", \"\\Flagged\", \"gnus-expire\", to be used in a STORE FLAGS command." (let ((result (nnimap-mark-to-flag-1 preds))) (setq result (if (and (or make-string always-list) @@ -1170,103 +1561,70 @@ be used in a STORE FLAGS command." result))) (defun nnimap-mark-permanent-p (mark &optional group) - "Return t iff MARK can be permanently (between IMAP sessions) saved -on articles, in GROUP." + "Return t iff MARK can be permanently (between IMAP sessions) saved on articles, in GROUP." (imap-message-flag-permanent-p (nnimap-mark-to-flag mark))) -(defun nnimap-remassoc (key alist) - "Delete by side effect any elements of LIST whose car is -`equal' to KEY. The modified LIST is returned. If the first member -of LIST has a car that is `equal' to KEY, there is no way to remove it -by side effect; therefore, write `(setq foo (remassoc key foo))' to be -sure of changing the value of `foo'." - (when alist - (if (equal key (caar alist)) - (cdr alist) - (setcdr alist (nnimap-remassoc key (cdr alist))) - alist))) - -(defun nnimap-update-alist-soft (key value alist) - (if value - (cons (cons key value) (nnimap-remassoc key alist)) - (nnimap-remassoc key alist))) - -(defun nnimap-range-to-string (range) - (mapconcat - (lambda (item) - (if (consp item) - (format "%d:%d" - (car item) (cdr item)) - (format "%d" item))) - (if (and (listp range) (not (listp (cdr range)))) - (list range) ;; make (1 . 2) into ((1 . 2)) - range) - ",")) - (when nnimap-debug (require 'trace) - (buffer-disable-undo (get-buffer-create nnimap-debug)) - (mapc (lambda (f) (trace-function-background f nnimap-debug)) - '( -nnimap-replace-in-string -nnimap-possibly-change-server -nnimap-verify-uidvalidity -nnimap-find-minmax-uid -nnimap-possibly-change-group -;nnimap-replace-whitespace -nnimap-retrieve-headers-progress -nnimap-retrieve-which-headers -nnimap-group-overview-filename -nnimap-retrieve-headers-from-file -nnimap-retrieve-headers-from-server -nnimap-retrieve-headers -nnimap-open-connection -nnimap-open-server -nnimap-server-opened -nnimap-close-server -nnimap-request-close -nnimap-status-message -;nnimap-demule -nnimap-request-article-part -nnimap-request-article -nnimap-request-head -nnimap-request-body -nnimap-request-group -nnimap-close-group -nnimap-pattern-to-list-arguments -nnimap-request-list -nnimap-request-post -nnimap-retrieve-groups -nnimap-request-update-info-internal -nnimap-request-type -nnimap-request-set-mark -nnimap-split-to-groups -nnimap-split-find-rule -nnimap-split-find-inbox -nnimap-split-articles -nnimap-request-scan -nnimap-request-newgroups -nnimap-request-create-group -nnimap-time-substract -nnimap-date-days-ago -nnimap-request-expire-articles-progress -nnimap-request-expire-articles -nnimap-request-move-article -nnimap-request-accept-article -nnimap-request-delete-group -nnimap-request-rename-group -gnus-group-nnimap-expunge -gnus-group-nnimap-edit-acl -gnus-group-nnimap-edit-acl-done -nnimap-group-mode-hook -nnimap-mark-to-predicate -nnimap-mark-to-flag-1 -nnimap-mark-to-flag -nnimap-mark-permanent-p -nnimap-remassoc -nnimap-update-alist-soft -nnimap-range-to-string - ))) + (buffer-disable-undo (get-buffer-create nnimap-debug-buffer)) + (mapcar (lambda (f) (trace-function-background f nnimap-debug-buffer)) + '( + nnimap-possibly-change-server + nnimap-verify-uidvalidity + nnimap-find-minmax-uid + nnimap-before-find-minmax-bugworkaround + nnimap-possibly-change-group + ;;nnimap-replace-whitespace + nnimap-retrieve-headers-progress + nnimap-retrieve-which-headers + nnimap-group-overview-filename + nnimap-retrieve-headers-from-file + nnimap-retrieve-headers-from-server + nnimap-retrieve-headers + nnimap-open-connection + nnimap-open-server + nnimap-server-opened + nnimap-close-server + nnimap-request-close + nnimap-status-message + ;;nnimap-demule + nnimap-request-article-part + nnimap-request-article + nnimap-request-head + nnimap-request-body + nnimap-request-group + nnimap-close-group + nnimap-pattern-to-list-arguments + nnimap-request-list + nnimap-request-post + nnimap-retrieve-groups + nnimap-request-update-info-internal + nnimap-request-type + nnimap-request-set-mark + nnimap-split-to-groups + nnimap-split-find-rule + nnimap-split-find-inbox + nnimap-split-articles + nnimap-request-scan + nnimap-request-newgroups + nnimap-request-create-group + nnimap-time-substract + nnimap-date-days-ago + nnimap-request-expire-articles-progress + nnimap-request-expire-articles + nnimap-request-move-article + nnimap-request-accept-article + nnimap-request-delete-group + nnimap-request-rename-group + gnus-group-nnimap-expunge + gnus-group-nnimap-edit-acl + gnus-group-nnimap-edit-acl-done + nnimap-group-mode-hook + nnimap-mark-to-predicate + nnimap-mark-to-flag-1 + nnimap-mark-to-flag + nnimap-mark-permanent-p + ))) (provide 'nnimap)