Mercurial > emacs
diff lisp/gnus/nnimap.el @ 31717:6b20b7e85e3c
*** empty log message ***
author | Gerd Moellmann <gerd@gnu.org> |
---|---|
date | Tue, 19 Sep 2000 13:40:08 +0000 |
parents | |
children | fc62cbb87aa5 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lisp/gnus/nnimap.el Tue Sep 19 13:40:08 2000 +0000 @@ -0,0 +1,1332 @@ +;;; nnimap.el --- imap backend for Gnus +;; Copyright (C) 1998, 1999, 2000 Free Software Foundation, Inc. + +;; Author: Simon Josefsson <jas@pdc.kth.se> +;; Jim Radford <radford@robby.caltech.edu> +;; Keywords: mail + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 2, or (at your option) +;; any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; Todo, major things: +;; +;; o Fix Gnus to view correct number of unread/total articles in group buffer +;; o Fix Gnus to handle leading '.' in group names (fixed?) +;; o Finish disconnected mode (moving articles between mailboxes unplugged) +;; o Sieve +;; o MIME (partial article fetches) +;; o Split to other backends, different split rules for different +;; servers/inboxes +;; +;; Todo, minor things: +;; +;; 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 Add support for the following: (if applicable) +;; request-list-newsgroups, request-regenerate +;; list-active-group, +;; request-associate-buffer, request-restore-buffer, +;; o Do The Right Thing when UIDVALIDITY changes (what's the right thing?) +;; o Support RFC2221 (Login referrals) +;; 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? NO! +;; o Use \Draft to support the draft group?? +;; o Duplicate suppression + +;;; Code: + +(eval-and-compile + (require 'imap)) + +(require 'nnoo) +(require 'nnmail) +(require 'nnheader) +(require 'mm-util) +(require 'gnus) +(require 'gnus-range) +(require 'gnus-start) +(require 'gnus-int) + +(nnoo-declare nnimap) + +(defconst nnimap-version "nnimap 0.131") + +(defvoo nnimap-address nil + "Address of physical IMAP server. If nil, use the virtual server's name.") + +(defvoo nnimap-server-port nil + "Port number on physical IMAP server. +If nil, defaults to 993 for SSL connections and 143 otherwise.") + +;; Splitting variables + +(defvar 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.") + +(defvar 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'. + +This can be a string or a list of strings.") + +(defvar nnimap-split-rule nil + "*Mail will be split according to theese rules. + +Mail is read from mailbox(es) specified in `nnimap-split-inbox'. + +If you'd like, for instance, one mail group for mail from the +\"gnus-imap\" mailing list, one group for junk mail and leave +everything else in the incoming mailbox, you could do something like +this: + +(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 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. + +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.") + +(defvar 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.") + +;; Authorization / Privacy variables + +(defvoo nnimap-auth-method nil + "Obsolete.") + +(defvoo nnimap-stream nil + "How nnimap will connect to the server. + +The default, nil, will try to use the \"best\" method the server can +handle. + +Change this if + +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 + server accept Kerberos login's but you haven't installed the + `imtest' program or your machine isn't configured for Kerberos. + +Possible choices: kerberos4, ssl, network") + +(defvoo nnimap-authenticator nil + "How nnimap authenticate itself to the server. + +The default, nil, will try to use the \"best\" method the server can +handle. + +There is only one reason for fiddling with this variable, and that is +if your server is more capable than your environment -- i.e. you +connect to a server that accept Kerberos login's but you haven't +installed the `imtest' program or your machine isn't configured for +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'.") + +(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 +~/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-expunge-on-close 'always ; 'ask, 'never + "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. + +If always, nnimap always perform a expunge when closing the group. +If never, nnimap never expunges articles marked for deletion. +If ask, nnimap will ask you if you wish to expunge marked articles. + +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. +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 +semantics of this are server specific, on the University of Washington +server you can specify a directory. + +Example: + '(\"INBOX\" \"mail/*\" (\"~friend/mail/\" . \"list/*\")) + +There are two wildcards * and %. * matches everything, % matches +everything in the current hierarchy.") + +(defvoo nnimap-news-groups nil + "IMAP support a news-like mode, also known as bulletin board mode, where replies is sent via IMAP instead of SMTP. + +This variable should contain a regexp matching groups where you wish +replies to be stored to the mailbox directly. + +Example: + '(\"^[^I][^N][^B][^O][^X].*$\") + +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 +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-server-address nil + "Obsolete. Use `nnimap-address'.") + +(defcustom nnimap-authinfo-file "~/.authinfo" + "Authorization information for IMAP servers. In .netrc format." + :type + '(choice file + (repeat :tag "Entries" + :menu-tag "Inline" + (list :format "%v" + :value ("" ("login" . "") ("password" . "")) + (string :tag "Host") + (checklist :inline t + (cons :format "%v" + (const :format "" "login") + (string :format "Login: %v")) + (cons :format "%v" + (const :format "" "password") + (string :format "Password: %v"))))))) + +(defcustom nnimap-prune-cache t + "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 + "Method to use to request a list of all folders from the server. +If this is 'imap-mailbox-lsub, then use a server-side subscription list to +restrict visible folders.") + +;; Internal variables: + +(defvar nnimap-debug nil + "Name of buffer to record debugging info. +For example: (setq nnimap-debug \"*nnimap-debug*\")") +(defvar nnimap-current-move-server nil) +(defvar nnimap-current-move-group nil) +(defvar nnimap-current-move-article nil) +(defvar nnimap-length) +(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.") +(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: + +(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))) + +(defun nnimap-possibly-change-server (server) + "Return buffer for SERVER, changing the current server as a side-effect. +If SERVER is nil, uses the current server." + (setq nnimap-current-server (or server nnimap-current-server) + nnimap-server-buffer (nnimap-get-server-buffer nnimap-current-server))) + +(defun nnimap-verify-uidvalidity (group server) + "Verify stored uidvalidity match current one in GROUP on SERVER." + (let* ((gnusgroup (gnus-group-prefixed-name + group (gnus-server-to-method + (format "nnimap:%s" server)))) + (new-uidvalidity (imap-mailbox-get 'uidvalidity)) + (old-uidvalidity (gnus-group-get-parameter gnusgroup 'uidvalidity))) + (if old-uidvalidity + (if (not (equal old-uidvalidity new-uidvalidity)) + nil ;; uidvalidity clash + (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'." + ;; 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) + (let (minuid maxuid) + (when (> (imap-mailbox-get 'exists) 0) + (imap-fetch "1,*" "UID" nil 'nouidfetch) + (imap-message-map (lambda (uid Uid) + (setq minuid (if minuid (min minuid uid) uid) + 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) + (with-current-buffer nnimap-server-buffer + (if (or (null group) (imap-current-mailbox-p group)) + imap-current-mailbox + (if (imap-mailbox-select group) + (if (or (nnimap-verify-uidvalidity + group (or server nnimap-current-server)) + (zerop (imap-mailbox-get 'exists group)) + (yes-or-no-p + (format + "nnimap: Group %s is not uidvalid. Continue? " group))) + imap-current-mailbox + (imap-mailbox-unselect) + (error "nnimap: Group %s is not uid-valid." group)) + (nnheader-report 'nnimap (imap-error-text))))))) + +(defun nnimap-replace-whitespace (string) + "Return STRING with all whitespace replaced with space." + (when string + (while (string-match "[\r\n\t]+" string) + (setq string (replace-match " " t t string))) + string)) + +;; Required backend functions + +(defun nnimap-retrieve-headers-progress () + "Hook to insert NOV line for current article into `nntp-server-buffer'." + (and (numberp nnmail-large-newsgroup) + (zerop (% (incf nnimap-counter) nnimap-progress-how-often)) + (> nnimap-length nnmail-large-newsgroup) + (nnheader-message 6 "nnimap: Retrieving headers... %c" + (nth (/ (% nnimap-counter + (* (length nnimap-progress-chars) + nnimap-progress-how-often)) + nnimap-progress-how-often) + nnimap-progress-chars))) + (with-current-buffer nntp-server-buffer + (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) + (nnheader-ms-strip-cr) + (nnheader-fold-continuation-lines) + (subst-char-in-region (point-min) (point-max) ?\t ? ) + (let ((head (nnheader-parse-head 'naked))) + (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." + (with-current-buffer nnimap-server-buffer + (if (numberp (car-safe articles)) + (imap-search + (concat "UID " + (imap-range-to-message-set + (gnus-compress-sequence + (append (gnus-uncompress-sequence + (and fetch-old + (cons (if (numberp fetch-old) + (max 1 (- (car articles) fetch-old)) + 1) + (1- (car articles))))) + articles))))) + (mapcar (lambda (msgid) + (imap-search + (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))))) + +(defun nnimap-retrieve-headers-from-file (group server) + (with-current-buffer nntp-server-buffer + (let ((nov (nnimap-group-overview-filename group server))) + (when (file-exists-p nov) + (mm-insert-file-contents nov) + (set-buffer-modified-p nil) + (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 + (erase-buffer) + nil)))))) + +(defun nnimap-retrieve-headers-from-server (articles group server) + (with-current-buffer nnimap-server-buffer + (let ((imap-fetch-data-hook '(nnimap-retrieve-headers-progress)) + (nnimap-length (gnus-range-length articles)) + (nnimap-counter 0)) + (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) + (or gnus-nov-is-evil nnimap-nov-is-evil + (unless (and (gnus-make-directory + (file-name-directory + (nnimap-group-overview-filename group server))) + (file-writable-p + (nnimap-group-overview-filename group server))) + (message "nnimap: Nov cache not writable, %s" + (nnimap-group-overview-filename group server))))) + +(deffoo nnimap-retrieve-headers (articles &optional group server fetch-old) + (when (nnimap-possibly-change-group group server) + (with-current-buffer nntp-server-buffer + (erase-buffer) + (if (nnimap-use-nov-p group server) + (nnimap-retrieve-headers-from-server + (gnus-compress-sequence articles) group server) + (let (uids cached low high) + (when (setq uids (nnimap-retrieve-which-headers articles fetch-old) + low (car uids) + high (car (last uids))) + (if (setq cached (nnimap-retrieve-headers-from-file group server)) + (progn + ;; fetch articles with uids before cache block + (when (< low (car cached)) + (goto-char (point-min)) + (nnimap-retrieve-headers-from-server + (cons low (1- (car cached))) group server)) + ;; fetch articles with uids after cache block + (when (> high (cdr cached)) + (goto-char (point-max)) + (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 + (goto-char (point-min)) + (dolist (uid (gnus-set-difference articles uids)) + (when (re-search-forward (format "^%d\t" uid) nil t) + (gnus-delete-line))))) + ;; nothing cached, fetch whole range from server + (nnimap-retrieve-headers-from-server + (cons low high) group server)) + (when (buffer-modified-p) + (nnmail-write-region + 1 (point-max) (nnimap-group-overview-filename group server) + nil 'nomesg)) + (nnheader-nov-delete-outside-range low high)))) + 'nov))) + +(defun nnimap-open-connection (server) + (if (not (imap-open nnimap-address nnimap-server-port nnimap-stream + nnimap-authenticator nnimap-server-buffer)) + (nnheader-report 'nnimap "Can't open connection to server %s" server) + (unless (or (imap-capability 'IMAP4 nnimap-server-buffer) + (imap-capability 'IMAP4rev1 nnimap-server-buffer)) + (imap-close nnimap-server-buffer) + (nnheader-report 'nnimap "Server %s is not IMAP4 compliant" server)) + (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) + nnimap-server-buffer-alist) + (nnimap-possibly-change-server server)) + (imap-close nnimap-server-buffer) + (kill-buffer nnimap-server-buffer) + (nnheader-report 'nnimap "Could not authenticate to %s" server))))) + +(deffoo nnimap-open-server (server &optional defs) + (nnheader-init-server-buffer) + (if (nnimap-server-opened server) + t + (unless (assq 'nnimap-server-buffer defs) + (push (list 'nnimap-server-buffer (concat " *nnimap* " server)) defs)) + ;; translate `nnimap-server-address' to `nnimap-address' in defs + ;; for people that configured nnimap with a very old version + (unless (assq 'nnimap-address defs) + (if (assq 'nnimap-server-address defs) + (push (list 'nnimap-address + (cadr (assq 'nnimap-server-address defs))) defs) + (push (list 'nnimap-address server) defs))) + (nnoo-change-server 'nnimap server 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)) + (nnimap-open-connection server)))) + +(deffoo nnimap-server-opened (&optional server) + "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) + (nnoo-server-opened 'nnimap (or server nnimap-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." + (let ((server (or server nnimap-current-server))) + (when (or (nnimap-server-opened server) + (imap-opened (nnimap-get-server-buffer server))) + (imap-close (nnimap-get-server-buffer server)) + (kill-buffer (nnimap-get-server-buffer server)) + (setq nnimap-server-buffer nil + nnimap-current-server nil + nnimap-server-buffer-alist + (delq server nnimap-server-buffer-alist))) + (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 +function is generally only called when Gnus is shutting down." + (mapcar (lambda (server) (nnimap-close-server (car server))) + nnimap-server-buffer-alist) + (setq nnimap-server-buffer-alist nil)) + +(deffoo nnimap-status-message (&optional server) + "This function returns the last error message from server." + (when (nnimap-possibly-change-server server) + (nnoo-status-message 'nnimap server))) + +(defun nnimap-demule (string) + (funcall (if (and (fboundp 'string-as-multibyte) + (subrp (symbol-function 'string-as-multibyte))) + 'string-as-multibyte + '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 + (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 (imap-current-message) 'BODYDETAIL))) + (imap-message-get (imap-current-message) 'RFC822))))) + (nnheader-ms-strip-cr) + (funcall nnimap-callback-callback-function 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) + nnimap-server-buffer)) + article))) + (when article + (gnus-message 10 "nnimap: Fetching (part of) article %d..." article) + (if (not nnheader-callback-function) + (with-current-buffer (or to-buffer nntp-server-buffer) + (erase-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 10 "nnimap: Fetching (part of) article %d...done" + article) + (if (bobp) + (nnheader-report 'nnimap "No such article: %s" + (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) + (imap-fetch-asynch article part nil nnimap-server-buffer) + (cons group article)))))) + +(deffoo nnimap-asynchronous-p () + t) + +(deffoo nnimap-request-article (article &optional 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) + (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) + (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 + group + (gnus-get-info (gnus-group-prefixed-name + 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))) + (nnheader-report 'nnimap "Could not get active info for %s" + group)) + (t + (nnheader-insert "211 %d %d %d %s\n" (or (nth 0 info) 0) + (max 1 (or (nth 1 info) 1)) + (or (nth 2 info) 0) group) + (nnheader-report 'nnimap "Group %s selected" group) + t))))) + +(defun nnimap-close-group (group &optional server) + (with-current-buffer nnimap-server-buffer + (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))) + (t (imap-mailbox-unselect))) + (not imap-current-mailbox)))) + +(defun nnimap-pattern-to-list-arguments (pattern) + (mapcar (lambda (p) + (cons (car-safe p) (or (cdr-safe p) p))) + (if (and (listp pattern) + (listp (cdr pattern))) + pattern + (list pattern)))) + +(deffoo nnimap-request-list (&optional server) + (when (nnimap-possibly-change-server server) + (with-current-buffer nntp-server-buffer + (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 + (cdr pattern) (car pattern))) + (or (member "\\NoSelect" (imap-mailbox-get 'list-flags mbx)) + (let ((info (nnimap-find-minmax-uid mbx 'examine))) + (when info + (with-current-buffer nntp-server-buffer + (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) "")) + t)) + +(deffoo nnimap-request-post (&optional server) + (let ((success t)) + (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) + (if (gnus-y-or-n-p (format "No such group: %s. Create it? " + to-newsgroup)) + (or (and (gnus-request-create-group + to-newsgroup gnus-command-method) + (gnus-activate-group to-newsgroup nil nil + gnus-command-method)) + (error "Couldn't create group %s" to-newsgroup))) + (error "No such group: %s" to-newsgroup)) + (unless (nnimap-request-accept-article mbx (nth 1 gnus-command-method)) + (setq success nil)))))) + +;; Optional backend functions + +(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))) + (insert (format "\"%s\" %d %d y\n" group + (or (nth 2 info) 0) + (max 1 (or (nth 1 info) 1)))))))) + (gnus-message 5 "nnimap: Checking mailboxes...done") + 'active)) + +(deffoo nnimap-request-update-info-internal (group info &optional server) + (when (nnimap-possibly-change-group group server) + (when info;; xxx what does this mean? should we create a info? + (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 + ;; imap clients! we correct this + (setq seen (gnus-uncompress-range (gnus-info-read info)) + unseen (imap-search "UNSEEN UNDELETED") + seen (gnus-set-difference seen unseen) + ;; seen might lack articles marked as read by other + ;; imap clients! we correct this + seen (append seen (imap-search "SEEN")) + ;; 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 + ;; is buggy so we return '((1 . 1)). + seen (if (and (integerp (car seen)) + (null (cdr seen))) + (list (cons (car seen) (car seen))) + seen)) + (gnus-info-set-read info seen))) + + (mapcar (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) + + ;; 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 + (nnimap-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)) + + info)))) + +(deffoo nnimap-request-type (group &optional article) + (if (and nnimap-news-groups (string-match nnimap-news-groups group)) + 'news + 'mail)) + +(deffoo nnimap-request-set-mark (group actions &optional server) + (when (nnimap-possibly-change-group group server) + (with-current-buffer nnimap-server-buffer + (let (action) + (gnus-message 7 "nnimap: Setting marks in %s..." group) + (while (setq action (pop actions)) + (let ((range (nth 0 action)) + (what (nth 1 action)) + (cmdmarks (nth 2 action)) + marks) + ;; 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))) + ;; remove stuff we are forbidden to store + (mapcar (lambda (mark) + (if (imap-message-flag-permanent-p + (nnimap-mark-to-flag mark)) + (setq marks (cons mark marks)))) + cmdmarks) + (when (and range marks) + (cond ((eq what 'del) + (imap-message-flags-del + (imap-range-to-message-set range) + (nnimap-mark-to-flag marks nil t))) + ((eq what 'add) + (imap-message-flags-add + (imap-range-to-message-set range) + (nnimap-mark-to-flag marks nil t))) + ((eq what 'set) + (imap-message-flags-set + (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 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. + (with-current-buffer nntp-server-buffer + ;; Fold continuation lines. + (goto-char (point-min)) + (while (re-search-forward "\\(\r?\n[ \t]+\\)+" nil t) + (replace-match " " t t)) + (if (functionp rules) + (funcall rules) + (let (to-groups regrepp) + (catch 'split-done + (dolist (rule rules to-groups) + (let ((group (car rule)) + (regexp (cadr rule))) + (goto-char (point-min)) + (when (and (if (stringp regexp) + (progn + (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. + (not (assoc group to-groups))) + (push (if regrepp + (nnmail-expand-newtext group) + group) + 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) + (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) + nnimap-split-inbox + (list nnimap-split-inbox))) + +(defun nnimap-split-articles (&optional group server) + (when (nnimap-possibly-change-server server) + (with-current-buffer nnimap-server-buffer + (let (rule inbox removeorig (inboxes (nnimap-split-find-inbox server))) + ;; iterate over inboxes + (while (and (setq inbox (pop inboxes)) + (nnimap-possibly-change-group inbox));; SELECT + ;; find split rule for this server / inbox + (when (setq rule (nnimap-split-find-rule server inbox)) + ;; iterate over articles + (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))) + ;; 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 + (imap-mailbox-expunge) + (imap-mailbox-close))) + t)))) + +(deffoo nnimap-request-scan (&optional group server) + (nnimap-split-articles group server)) + +(deffoo nnimap-request-newgroups (date &optional server) + (when (nnimap-possibly-change-server server) + (with-current-buffer nntp-server-buffer + (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 + nnimap-server-buffer)) + (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" + 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) + (imap-mailbox-create group nnimap-server-buffer)))) + +(defun nnimap-time-substract (time1 time2) + "Return TIME for TIME1 - TIME2." + (let* ((ms (- (car time1) (car time2))) + (ls (- (nth 1 time1) (nth 1 time2)))) + (if (< ls 0) + (list (- ms 1) (+ (expt 2 16) ls)) + (list ms ls)))) + +(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))))) + (if (eq ?0 (string-to-char date)) + (substring date 1) + date))) + +(defun nnimap-request-expire-articles-progress () + (gnus-message 5 "nnimap: Marking article %d for deletion..." + imap-current-message)) + +;; 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 + (imap-range-to-message-set 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 + (imap-range-to-message-set artseq) "\\Deleted") + (setq articles nil))) + ((numberp days) + (let ((oldarts (imap-search + (format "UID %s NOT SINCE %s" + (imap-range-to-message-set artseq) + (nnimap-date-days-ago days)))) + (imap-fetch-data-hook + '(nnimap-request-expire-articles-progress))) + (and oldarts + (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) + +(deffoo nnimap-request-move-article (article group server + accept-form &optional last) + (when (nnimap-possibly-change-server server) + (save-excursion + (let ((buf (get-buffer-create " *nnimap move*")) + (nnimap-current-move-article article) + (nnimap-current-move-group group) + (nnimap-current-move-server nnimap-current-server) + result) + (and (nnimap-request-article article group server) + (save-excursion + (set-buffer buf) + (buffer-disable-undo (current-buffer)) + (insert-buffer-substring nntp-server-buffer) + (setq result (eval accept-form)) + (kill-buffer buf) + result) + (nnimap-request-expire-articles (list article) group server t)) + result)))) + +(deffoo nnimap-request-accept-article (group &optional server last) + (when (nnimap-possibly-change-server server) + (let (uid) + (if (setq uid + (if (string= nnimap-current-server nnimap-current-move-server) + ;; moving article within same server, speed it up... + (and (nnimap-possibly-change-group + nnimap-current-move-group) + (imap-message-copy (number-to-string + 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)) + (while (search-forward "\n" nil t) + (replace-match "\r\n"))) + ;; 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)) + (nnheader-report 'nnimap (imap-error-text nnimap-server-buffer)))))) + +(deffoo nnimap-request-delete-group (group force &optional server) + (when (nnimap-possibly-change-server server) + (with-current-buffer nnimap-server-buffer + (if force + (or (null (imap-mailbox-status group 'uidvalidity)) + (imap-mailbox-delete group)) + ;; UNSUBSCRIBE? + t)))) + +(deffoo nnimap-request-rename-group (group new-name &optional server) + (when (nnimap-possibly-change-server server) + (imap-mailbox-rename group new-name nnimap-server-buffer))) + +(defun nnimap-expunge (mailbox server) + (when (nnimap-possibly-change-group mailbox server) + (imap-mailbox-expunge nnimap-server-buffer))) + +(defun nnimap-acl-get (mailbox server) + (when (nnimap-possibly-change-server server) + (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)) + (unless (imap-capability 'ACL nnimap-server-buffer) + (error "Your server does not support ACL editing")) + (with-current-buffer nnimap-server-buffer + ;; 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))))) + 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))))) + new-acls) + t))) + + +;;; Internal functions + +;; +;; This is confusing. +;; +;; mark => read, tick, draft, reply etc +;; flag => "\\Seen", "\\Flagged", "\\Draft", "gnus-expire" etc +;; 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). +;; + +(defconst nnimap-mark-to-predicate-alist + (mapcar + (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)))))) + (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. +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)))))) + (cons '(read . read) gnus-article-mark-lists))) + +(defun nnimap-mark-to-flag-1 (preds) + (if (and (not (null preds)) (listp preds)) + (cons (nnimap-mark-to-flag (car preds)) + (nnimap-mark-to-flag (cdr preds))) + (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. +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) + (not (listp result))) + (list result) + result)) + (if make-string + (mapconcat (lambda (flag) + (if (listp flag) + (mapconcat 'identity flag " ") + flag)) + result " ") + result))) + +(defun nnimap-mark-permanent-p (mark &optional 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))) + +(when nnimap-debug + (require 'trace) + (buffer-disable-undo (get-buffer-create nnimap-debug)) + (mapcar (lambda (f) (trace-function-background f nnimap-debug)) + '( + 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 + nnimap-remassoc + nnimap-update-alist-soft + ))) + +(provide 'nnimap) + +;;; nnimap.el ends here