1;;; nnimap.el --- imap backend for Gnus 2 3;; Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 4;; 2005, 2006, 2007 Free Software Foundation, Inc. 5 6;; Author: Simon Josefsson <jas@pdc.kth.se> 7;; Jim Radford <radford@robby.caltech.edu> 8;; Keywords: mail 9 10;; This file is part of GNU Emacs. 11 12;; GNU Emacs is free software; you can redistribute it and/or modify 13;; it under the terms of the GNU General Public License as published by 14;; the Free Software Foundation; either version 2, or (at your option) 15;; any later version. 16 17;; GNU Emacs is distributed in the hope that it will be useful, 18;; but WITHOUT ANY WARRANTY; without even the implied warranty of 19;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20;; GNU General Public License for more details. 21 22;; You should have received a copy of the GNU General Public License 23;; along with GNU Emacs; see the file COPYING. If not, write to the 24;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 25;; Boston, MA 02110-1301, USA. 26 27;;; Commentary: 28 29;; Todo, major things: 30;; 31;; o Fix Gnus to view correct number of unread/total articles in group buffer 32;; o Fix Gnus to handle leading '.' in group names (fixed?) 33;; o Finish disconnected mode (moving articles between mailboxes unplugged) 34;; o Sieve 35;; o MIME (partial article fetches) 36;; o Split to other backends, different split rules for different 37;; servers/inboxes 38;; 39;; Todo, minor things: 40;; 41;; o Don't require half of Gnus -- backends should be standalone 42;; o Verify that we don't use IMAP4rev1 specific things (RFC2060 App B) 43;; o Dont uid fetch 1,* in nnimap-retrive-groups (slow) 44;; o Split up big fetches (1,* header especially) in smaller chunks 45;; o What do I do with gnus-newsgroup-*? 46;; o Tell Gnus about new groups (how can we tell?) 47;; o Respooling (fix Gnus?) (unnecessary?) 48;; o Add support for the following: (if applicable) 49;; request-list-newsgroups, request-regenerate 50;; list-active-group, 51;; request-associate-buffer, request-restore-buffer, 52;; o Do The Right Thing when UIDVALIDITY changes (what's the right thing?) 53;; o Support RFC2221 (Login referrals) 54;; o IMAP2BIS compatibility? (RFC2061) 55;; o ACAP stuff (perhaps a different project, would be nice to ACAPify 56;; .newsrc.eld) 57;; o What about Gnus's article editing, can we support it? NO! 58;; o Use \Draft to support the draft group?? 59;; o Duplicate suppression 60;; o Rewrite UID SEARCH UID X as UID FETCH X (UID) for those with slow servers 61 62;;; Code: 63 64(require 'imap) 65(require 'nnoo) 66(require 'nnmail) 67(require 'nnheader) 68(require 'mm-util) 69(require 'gnus) 70(require 'gnus-range) 71(require 'gnus-start) 72(require 'gnus-int) 73 74(eval-when-compile (require 'cl)) 75 76(nnoo-declare nnimap) 77 78(defconst nnimap-version "nnimap 1.0") 79 80(defgroup nnimap nil 81 "Reading IMAP mail with Gnus." 82 :group 'gnus) 83 84(defvoo nnimap-address nil 85 "Address of physical IMAP server. If nil, use the virtual server's name.") 86 87(defvoo nnimap-server-port nil 88 "Port number on physical IMAP server. 89If nil, defaults to 993 for TLS/SSL connections and 143 otherwise.") 90 91;; Splitting variables 92 93(defcustom nnimap-split-crosspost t 94 "If non-nil, do crossposting if several split methods match the mail. 95If nil, the first match found will be used." 96 :group 'nnimap 97 :type 'boolean) 98 99(defcustom nnimap-split-inbox nil 100 "Name of mailbox to split mail from. 101 102Mail is read from this mailbox and split according to rules in 103`nnimap-split-rule'. 104 105This can be a string or a list of strings." 106 :group 'nnimap 107 :type '(choice (string) 108 (repeat string))) 109 110(define-widget 'nnimap-strict-function 'function 111 "This widget only matches values that are functionp. 112 113Warning: This means that a value that is the symbol of a not yet 114loaded function will not match. Use with care." 115 :match 'nnimap-strict-function-match) 116 117(defun nnimap-strict-function-match (widget value) 118 "Ignoring WIDGET, match if VALUE is a function." 119 (functionp value)) 120 121(defcustom nnimap-split-rule nil 122 "Mail will be split according to these rules. 123 124Mail is read from mailbox(es) specified in `nnimap-split-inbox'. 125 126If you'd like, for instance, one mail group for mail from the 127\"gnus-imap\" mailing list, one group for junk mail and leave 128everything else in the incoming mailbox, you could do something like 129this: 130 131\(setq nnimap-split-rule '((\"INBOX.gnus-imap\" \"From:.*gnus-imap\") 132 (\"INBOX.junk\" \"Subject:.*buy\"))) 133 134As you can see, `nnimap-split-rule' is a list of lists, where the 135first element in each \"rule\" is the name of the IMAP mailbox (or the 136symbol `junk' if you want to remove the mail), and the second is a 137regexp that nnimap will try to match on the header to find a fit. 138 139The second element can also be a function. In that case, it will be 140called narrowed to the headers with the first element of the rule as 141the argument. It should return a non-nil value if it thinks that the 142mail belongs in that group. 143 144This variable can also have a function as its value, the function will 145be called with the headers narrowed and should return a group where it 146thinks the article should be splitted to. See `nnimap-split-fancy'. 147 148To allow for different split rules on different virtual servers, and 149even different split rules in different inboxes on the same server, 150the syntax of this variable have been extended along the lines of: 151 152\(setq nnimap-split-rule 153 '((\"my1server\" (\".*\" ((\"ding\" \"ding@gnus.org\") 154 (\"junk\" \"From:.*Simon\"))) 155 (\"my2server\" (\"INBOX\" nnimap-split-fancy)) 156 (\"my[34]server\" (\".*\" ((\"private\" \"To:.*Simon\") 157 (\"junk\" my-junk-func))))) 158 159The virtual server name is in fact a regexp, so that the same rules 160may apply to several servers. In the example, the servers 161\"my3server\" and \"my4server\" both use the same rules. Similarly, 162the inbox string is also a regexp. The actual splitting rules are as 163before, either a function, or a list with group/regexp or 164group/function elements." 165 :group 'nnimap 166 :type '(choice :tag "Rule type" 167 (repeat :menu-tag "Single-server" 168 :tag "Single-server list" 169 (list (string :tag "Mailbox") 170 (choice :tag "Predicate" 171 (regexp :tag "A regexp") 172 (nnimap-strict-function :tag "A function")))) 173 (choice :menu-tag "A function" 174 :tag "A function" 175 (function-item nnimap-split-fancy) 176 (function-item nnmail-split-fancy) 177 (nnimap-strict-function :tag "User-defined function")) 178 (repeat :menu-tag "Multi-server (extended)" 179 :tag "Multi-server list" 180 (list (regexp :tag "Server regexp") 181 (list (regexp :tag "Incoming Mailbox regexp") 182 (repeat :tag "Rules for matching server(s) and mailbox(es)" 183 (list (string :tag "Destination mailbox") 184 (choice :tag "Predicate" 185 (regexp :tag "A Regexp") 186 (nnimap-strict-function :tag "A Function"))))))))) 187 188(defcustom nnimap-split-predicate "UNSEEN UNDELETED" 189 "The predicate used to find articles to split. 190If you use another IMAP client to peek on articles but always would 191like nnimap to split them once it's started, you could change this to 192\"UNDELETED\". Other available predicates are available in 193RFC2060 section 6.4.4." 194 :group 'nnimap 195 :type 'string) 196 197(defcustom nnimap-split-fancy nil 198 "Like the variable `nnmail-split-fancy'." 199 :group 'nnimap 200 :type 'sexp) 201 202(defvar nnimap-split-download-body-default nil 203 "Internal variable with default value for `nnimap-split-download-body'.") 204 205(defcustom nnimap-split-download-body 'default 206 "Whether to download entire articles during splitting. 207This is generally not required, and will slow things down considerably. 208You may need it if you want to use an advanced splitting function that 209analyzes the body before splitting the article. 210If this variable is nil, bodies will not be downloaded; if this 211variable is the symbol `default' the default behaviour is 212used (which currently is nil, unless you use a statistical 213spam.el test); if this variable is another non-nil value bodies 214will be downloaded." 215 :version "22.1" 216 :group 'nnimap 217 :type '(choice (const :tag "Let system decide" deault) 218 boolean)) 219 220;; Performance / bug workaround variables 221 222(defcustom nnimap-close-asynchronous t 223 "Close mailboxes asynchronously in `nnimap-close-group'. 224This means that errors caught by nnimap when closing the mailbox will 225not prevent Gnus from updating the group status, which may be harmful. 226However, it increases speed." 227 :version "22.1" 228 :type 'boolean 229 :group 'nnimap) 230 231(defcustom nnimap-dont-close t 232 "Never close mailboxes. 233This increases the speed of closing mailboxes (quiting group) but may 234decrease the speed of selecting another mailbox later. Re-selecting 235the same mailbox will be faster though." 236 :version "22.1" 237 :type 'boolean 238 :group 'nnimap) 239 240(defcustom nnimap-retrieve-groups-asynchronous t 241 "Send asynchronous STATUS commands for each mailbox before checking mail. 242If you have mailboxes that rarely receives mail, this speeds up new 243mail checking. It works by first sending STATUS commands for each 244mailbox, and then only checking groups which has a modified UIDNEXT 245more carefully for new mail. 246 247In summary, the default is O((1-p)*k+p*n) and changing it to nil makes 248it O(n). If p is small, then the default is probably faster." 249 :version "22.1" 250 :type 'boolean 251 :group 'nnimap) 252 253(defvoo nnimap-need-unselect-to-notice-new-mail nil 254 "Unselect mailboxes before looking for new mail in them. 255Some servers seem to need this under some circumstances.") 256 257;; Authorization / Privacy variables 258 259(defvoo nnimap-auth-method nil 260 "Obsolete.") 261 262(defvoo nnimap-stream nil 263 "How nnimap will connect to the server. 264 265The default, nil, will try to use the \"best\" method the server can 266handle. 267 268Change this if 269 2701) you want to connect with TLS/SSL. The TLS/SSL integration 271 with IMAP is suboptimal so you'll have to tell it 272 specifically. 273 2742) your server is more capable than your environment -- i.e. your 275 server accept Kerberos login's but you haven't installed the 276 `imtest' program or your machine isn't configured for Kerberos. 277 278Possible choices: gssapi, kerberos4, starttls, tls, ssl, network, shell. 279See also `imap-streams' and `imap-stream-alist'.") 280 281(defvoo nnimap-authenticator nil 282 "How nnimap authenticate itself to the server. 283 284The default, nil, will try to use the \"best\" method the server can 285handle. 286 287There is only one reason for fiddling with this variable, and that is 288if your server is more capable than your environment -- i.e. you 289connect to a server that accept Kerberos login's but you haven't 290installed the `imtest' program or your machine isn't configured for 291Kerberos. 292 293Possible choices: gssapi, kerberos4, digest-md5, cram-md5, login, anonymous. 294See also `imap-authenticators' and `imap-authenticator-alist'") 295 296(defvoo nnimap-directory (nnheader-concat gnus-directory "overview/") 297 "Directory to keep NOV cache files for nnimap groups. 298See also `nnimap-nov-file-name'.") 299 300(defvoo nnimap-nov-file-name "nnimap." 301 "NOV cache base filename. 302The group name and `nnimap-nov-file-name-suffix' will be appended. A 303typical complete file name would be 304~/News/overview/nnimap.pdc.INBOX.ding.nov, or 305~/News/overview/nnimap/pdc/INBOX/ding/nov if 306`nnmail-use-long-file-names' is nil") 307 308(defvoo nnimap-nov-file-name-suffix ".novcache" 309 "Suffix for NOV cache base filename.") 310 311(defvoo nnimap-nov-is-evil gnus-agent 312 "If non-nil, never generate or use a local nov database for this backend. 313Using nov databases should speed up header fetching considerably. 314However, it will invoke a UID SEARCH UID command on the server, and 315some servers implement this command inefficiently by opening each and 316every message in the group, thus making it quite slow. 317Unlike other backends, you do not need to take special care if you 318flip this variable.") 319 320(defvoo nnimap-search-uids-not-since-is-evil nil 321 "If non-nil, avoid \"UID SEARCH UID ... NOT SINCE\" queries when expiring. 322Instead, use \"UID SEARCH SINCE\" to prune the list of expirable 323articles within Gnus. This seems to be faster on Courier in some cases.") 324 325(defvoo nnimap-expunge-on-close 'always ; 'ask, 'never 326 "Whether to expunge a group when it is closed. 327When a IMAP group with articles marked for deletion is closed, this 328variable determine if nnimap should actually remove the articles or 329not. 330 331If always, nnimap always perform a expunge when closing the group. 332If never, nnimap never expunges articles marked for deletion. 333If ask, nnimap will ask you if you wish to expunge marked articles. 334 335When setting this variable to `never', you can only expunge articles 336by using `G x' (gnus-group-nnimap-expunge) from the Group buffer.") 337 338(defvoo nnimap-list-pattern "*" 339 "A string LIMIT or list of strings with mailbox wildcards used to limit available groups. 340See below for available wildcards. 341 342The LIMIT string can be a cons cell (REFERENCE . LIMIT), where 343REFERENCE will be passed as the first parameter to LIST/LSUB. The 344semantics of this are server specific, on the University of Washington 345server you can specify a directory. 346 347Example: 348 '(\"INBOX\" \"mail/*\" (\"~friend/mail/\" . \"list/*\")) 349 350There are two wildcards * and %. * matches everything, % matches 351everything in the current hierarchy.") 352 353(defvoo nnimap-news-groups nil 354 "IMAP support a news-like mode, also known as bulletin board mode, 355where replies is sent via IMAP instead of SMTP. 356 357This variable should contain a regexp matching groups where you wish 358replies to be stored to the mailbox directly. 359 360Example: 361 '(\"^[^I][^N][^B][^O][^X].*$\") 362 363This will match all groups not beginning with \"INBOX\". 364 365Note that there is nothing technically different between mail-like and 366news-like mailboxes. If you wish to have a group with todo items or 367similar which you wouldn't want to set up a mailing list for, you can 368use this to make replies go directly to the group.") 369 370(defvoo nnimap-expunge-search-string "UID %s NOT SINCE %s" 371 "IMAP search command to use for articles that are to be expired. 372The first %s is replaced by a UID set of articles to search on, 373and the second %s is replaced by a date criterium. 374 375One useful (and perhaps the only useful) value to change this to would 376be `UID %s NOT SENTSINCE %s' to make nnimap use the Date: header 377instead of the internal date of messages. See section 6.4.4 of RFC 3782060 for more information on valid strings. 379 380However, if `nnimap-search-uids-not-since-is-evil' is true, this 381variable has no effect since the search logic is reversed.") 382 383(defvoo nnimap-importantize-dormant t 384 "If non-nil, mark \"dormant\" articles as \"ticked\" for other IMAP clients. 385Note that within Gnus, dormant articles will still (only) be 386marked as ticked. This is to make \"dormant\" articles stand out, 387just like \"ticked\" articles, in other IMAP clients.") 388 389(defvoo nnimap-server-address nil 390 "Obsolete. Use `nnimap-address'.") 391 392(defcustom nnimap-authinfo-file "~/.authinfo" 393 "Authorization information for IMAP servers. In .netrc format." 394 :type 395 '(choice file 396 (repeat :tag "Entries" 397 :menu-tag "Inline" 398 (list :format "%v" 399 :value ("" ("login" . "") ("password" . "")) 400 (string :tag "Host") 401 (checklist :inline t 402 (cons :format "%v" 403 (const :format "" "login") 404 (string :format "Login: %v")) 405 (cons :format "%v" 406 (const :format "" "password") 407 (string :format "Password: %v")))))) 408 :group 'nnimap) 409 410(defcustom nnimap-prune-cache t 411 "If non-nil, nnimap check whether articles still exist on server before using data stored in NOV cache." 412 :type 'boolean 413 :group 'nnimap) 414 415(defvar nnimap-request-list-method 'imap-mailbox-list 416 "Method to use to request a list of all folders from the server. 417If this is 'imap-mailbox-lsub, then use a server-side subscription list to 418restrict visible folders.") 419 420(defcustom nnimap-debug nil 421 "If non-nil, random debug spews are placed in *nnimap-debug* buffer. 422Note that username, passwords and other privacy sensitive 423information (such as e-mail) may be stored in the *nnimap-debug* 424buffer. It is not written to disk, however. Do not enable this 425variable unless you are comfortable with that." 426 :group 'nnimap 427 :type 'boolean) 428 429;; Internal variables: 430 431(defvar nnimap-debug-buffer "*nnimap-debug*") 432(defvar nnimap-mailbox-info (gnus-make-hashtable 997)) 433(defvar nnimap-current-move-server nil) 434(defvar nnimap-current-move-group nil) 435(defvar nnimap-current-move-article nil) 436(defvar nnimap-length) 437(defvar nnimap-progress-chars '(?| ?/ ?- ?\\)) 438(defvar nnimap-progress-how-often 20) 439(defvar nnimap-counter) 440(defvar nnimap-server-buffer-alist nil) ;; Map server name to buffers. 441(defvar nnimap-current-server nil) ;; Current server 442(defvar nnimap-server-buffer nil) ;; Current servers' buffer 443 444 445 446(nnoo-define-basics nnimap) 447 448;; Utility functions: 449 450(defsubst nnimap-get-server-buffer (server) 451 "Return buffer for SERVER, if nil use current server." 452 (cadr (assoc (or server nnimap-current-server) nnimap-server-buffer-alist))) 453 454(defun nnimap-possibly-change-server (server) 455 "Return buffer for SERVER, changing the current server as a side-effect. 456If SERVER is nil, uses the current server." 457 (setq nnimap-current-server (or server nnimap-current-server) 458 nnimap-server-buffer (nnimap-get-server-buffer nnimap-current-server))) 459 460(defun nnimap-verify-uidvalidity (group server) 461 "Verify stored uidvalidity match current one in GROUP on SERVER." 462 (let* ((gnusgroup (gnus-group-prefixed-name 463 group (gnus-server-to-method 464 (format "nnimap:%s" server)))) 465 (new-uidvalidity (imap-mailbox-get 'uidvalidity)) 466 (old-uidvalidity (gnus-group-get-parameter gnusgroup 'uidvalidity)) 467 (dir (file-name-as-directory (expand-file-name nnimap-directory))) 468 (nameuid (nnheader-translate-file-chars 469 (concat nnimap-nov-file-name 470 (if (equal server "") 471 "unnamed" 472 server) "." group "." old-uidvalidity 473 nnimap-nov-file-name-suffix) t)) 474 (file (if (or nnmail-use-long-file-names 475 (file-exists-p (expand-file-name nameuid dir))) 476 (expand-file-name nameuid dir) 477 (expand-file-name 478 (mm-encode-coding-string 479 (nnheader-replace-chars-in-string nameuid ?. ?/) 480 nnmail-pathname-coding-system) 481 dir)))) 482 (if old-uidvalidity 483 (if (not (equal old-uidvalidity new-uidvalidity)) 484 ;; uidvalidity clash 485 (gnus-delete-file file) 486 (gnus-group-set-parameter gnusgroup 'uidvalidity new-uidvalidity) 487 t) 488 (gnus-group-add-parameter gnusgroup (cons 'uidvalidity new-uidvalidity)) 489 t))) 490 491(defun nnimap-before-find-minmax-bugworkaround () 492 "Function called before iterating through mailboxes with 493`nnimap-find-minmax-uid'." 494 (when nnimap-need-unselect-to-notice-new-mail 495 ;; XXX this is for UoW imapd problem, it doesn't notice new mail in 496 ;; currently selected mailbox without a re-select/examine. 497 (or (null (imap-current-mailbox nnimap-server-buffer)) 498 (imap-mailbox-unselect nnimap-server-buffer)))) 499 500(defun nnimap-find-minmax-uid (group &optional examine) 501 "Find lowest and highest active article number in GROUP. 502If EXAMINE is non-nil the group is selected read-only." 503 (with-current-buffer nnimap-server-buffer 504 (when (or (string= group (imap-current-mailbox)) 505 (imap-mailbox-select group examine)) 506 (let (minuid maxuid) 507 (when (> (imap-mailbox-get 'exists) 0) 508 (imap-fetch "1,*" "UID" nil 'nouidfetch) 509 (imap-message-map (lambda (uid Uid) 510 (setq minuid (if minuid (min minuid uid) uid) 511 maxuid (if maxuid (max maxuid uid) uid))) 512 'UID)) 513 (list (imap-mailbox-get 'exists) minuid maxuid))))) 514 515(defun nnimap-possibly-change-group (group &optional server) 516 "Make GROUP the current group, and SERVER the current server." 517 (when (nnimap-possibly-change-server server) 518 (with-current-buffer nnimap-server-buffer 519 (if (or (null group) (imap-current-mailbox-p group)) 520 imap-current-mailbox 521 (if (imap-mailbox-select group) 522 (if (or (nnimap-verify-uidvalidity 523 group (or server nnimap-current-server)) 524 (zerop (imap-mailbox-get 'exists group)) 525 t ;; for OGnus to see if ignoring uidvalidity 526 ;; changes has any bad effects. 527 (yes-or-no-p 528 (format 529 "nnimap: Group %s is not uidvalid. Continue? " group))) 530 imap-current-mailbox 531 (imap-mailbox-unselect) 532 (error "nnimap: Group %s is not uid-valid" group)) 533 (nnheader-report 'nnimap (imap-error-text))))))) 534 535(defun nnimap-replace-whitespace (string) 536 "Return STRING with all whitespace replaced with space." 537 (when string 538 (while (string-match "[\r\n\t]+" string) 539 (setq string (replace-match " " t t string))) 540 string)) 541 542;; Required backend functions 543 544(defun nnimap-retrieve-headers-progress () 545 "Hook to insert NOV line for current article into `nntp-server-buffer'." 546 (and (numberp nnmail-large-newsgroup) 547 (zerop (% (incf nnimap-counter) nnimap-progress-how-often)) 548 (> nnimap-length nnmail-large-newsgroup) 549 (nnheader-message 6 "nnimap: Retrieving headers... %c" 550 (nth (/ (% nnimap-counter 551 (* (length nnimap-progress-chars) 552 nnimap-progress-how-often)) 553 nnimap-progress-how-often) 554 nnimap-progress-chars))) 555 (with-current-buffer nntp-server-buffer 556 (let (headers lines chars uid mbx) 557 (with-current-buffer nnimap-server-buffer 558 (setq uid imap-current-message 559 mbx imap-current-mailbox 560 headers (nnimap-demule 561 (if (imap-capability 'IMAP4rev1) 562 ;; xxx don't just use car? alist doesn't contain 563 ;; anything else now, but it might... 564 (nth 2 (car (imap-message-get uid 'BODYDETAIL))) 565 (imap-message-get uid 'RFC822.HEADER))) 566 lines (imap-body-lines (imap-message-body imap-current-message)) 567 chars (imap-message-get imap-current-message 'RFC822.SIZE))) 568 (nnheader-insert-nov 569 (with-temp-buffer 570 (buffer-disable-undo) 571 (insert headers) 572 (let ((head (nnheader-parse-naked-head))) 573 (mail-header-set-number head uid) 574 (mail-header-set-chars head chars) 575 (mail-header-set-lines head lines) 576 (mail-header-set-xref 577 head (format "%s %s:%d" (system-name) mbx uid)) 578 head)))))) 579 580(defun nnimap-retrieve-which-headers (articles fetch-old) 581 "Get a range of articles to fetch based on ARTICLES and FETCH-OLD." 582 (with-current-buffer nnimap-server-buffer 583 (if (numberp (car-safe articles)) 584 (imap-search 585 (concat "UID " 586 (imap-range-to-message-set 587 (gnus-compress-sequence 588 (append (gnus-uncompress-sequence 589 (and fetch-old 590 (cons (if (numberp fetch-old) 591 (max 1 (- (car articles) fetch-old)) 592 1) 593 (1- (car articles))))) 594 articles))))) 595 (mapcar (lambda (msgid) 596 (imap-search 597 (format "HEADER Message-Id \"%s\"" msgid))) 598 articles)))) 599 600(defun nnimap-group-overview-filename (group server) 601 "Make file name for GROUP on SERVER." 602 (let* ((dir (file-name-as-directory (expand-file-name nnimap-directory))) 603 (uidvalidity (gnus-group-get-parameter 604 (gnus-group-prefixed-name 605 group (gnus-server-to-method 606 (format "nnimap:%s" server))) 607 'uidvalidity)) 608 (name (nnheader-translate-file-chars 609 (concat nnimap-nov-file-name 610 (if (equal server "") 611 "unnamed" 612 server) "." group nnimap-nov-file-name-suffix) t)) 613 (nameuid (nnheader-translate-file-chars 614 (concat nnimap-nov-file-name 615 (if (equal server "") 616 "unnamed" 617 server) "." group "." uidvalidity 618 nnimap-nov-file-name-suffix) t)) 619 (oldfile (if (or nnmail-use-long-file-names 620 (file-exists-p (expand-file-name name dir))) 621 (expand-file-name name dir) 622 (expand-file-name 623 (mm-encode-coding-string 624 (nnheader-replace-chars-in-string name ?. ?/) 625 nnmail-pathname-coding-system) 626 dir))) 627 (newfile (if (or nnmail-use-long-file-names 628 (file-exists-p (expand-file-name nameuid dir))) 629 (expand-file-name nameuid dir) 630 (expand-file-name 631 (mm-encode-coding-string 632 (nnheader-replace-chars-in-string nameuid ?. ?/) 633 nnmail-pathname-coding-system) 634 dir)))) 635 (when (and (file-exists-p oldfile) (not (file-exists-p newfile))) 636 (message "nnimap: Upgrading novcache filename...") 637 (sit-for 1) 638 (gnus-make-directory (file-name-directory newfile)) 639 (unless (ignore-errors (rename-file oldfile newfile) t) 640 (if (ignore-errors (copy-file oldfile newfile) t) 641 (delete-file oldfile) 642 (error "Can't rename `%s' to `%s'" oldfile newfile)))) 643 newfile)) 644 645(defun nnimap-retrieve-headers-from-file (group server) 646 (with-current-buffer nntp-server-buffer 647 (let ((nov (nnimap-group-overview-filename group server))) 648 (when (file-exists-p nov) 649 (mm-insert-file-contents nov) 650 (set-buffer-modified-p nil) 651 (let ((min (ignore-errors (goto-char (point-min)) 652 (read (current-buffer)))) 653 (max (ignore-errors (goto-char (point-max)) 654 (forward-line -1) 655 (read (current-buffer))))) 656 (if (and (numberp min) (numberp max)) 657 (cons min max) 658 ;; junk, remove it, it's saved later 659 (erase-buffer) 660 nil)))))) 661 662(defun nnimap-retrieve-headers-from-server (articles group server) 663 (with-current-buffer nnimap-server-buffer 664 (let ((imap-fetch-data-hook '(nnimap-retrieve-headers-progress)) 665 (nnimap-length (gnus-range-length articles)) 666 (nnimap-counter 0)) 667 (imap-fetch (imap-range-to-message-set articles) 668 (concat "(UID RFC822.SIZE BODY " 669 (let ((headers 670 (append '(Subject From Date Message-Id 671 References In-Reply-To Xref) 672 (copy-sequence 673 nnmail-extra-headers)))) 674 (if (imap-capability 'IMAP4rev1) 675 (format "BODY.PEEK[HEADER.FIELDS %s])" headers) 676 (format "RFC822.HEADER.LINES %s)" headers))))) 677 (with-current-buffer nntp-server-buffer 678 (sort-numeric-fields 1 (point-min) (point-max))) 679 (and (numberp nnmail-large-newsgroup) 680 (> nnimap-length nnmail-large-newsgroup) 681 (nnheader-message 6 "nnimap: Retrieving headers...done"))))) 682 683(defun nnimap-dont-use-nov-p (group server) 684 (or gnus-nov-is-evil nnimap-nov-is-evil 685 (unless (and (gnus-make-directory 686 (file-name-directory 687 (nnimap-group-overview-filename group server))) 688 (file-writable-p 689 (nnimap-group-overview-filename group server))) 690 (message "nnimap: Nov cache not writable, %s" 691 (nnimap-group-overview-filename group server))))) 692 693(deffoo nnimap-retrieve-headers (articles &optional group server fetch-old) 694 (when (nnimap-possibly-change-group group server) 695 (with-current-buffer nntp-server-buffer 696 (erase-buffer) 697 (if (nnimap-dont-use-nov-p group server) 698 (nnimap-retrieve-headers-from-server 699 (gnus-compress-sequence articles) group server) 700 (let (uids cached low high) 701 (when (setq uids (nnimap-retrieve-which-headers articles fetch-old) 702 low (car uids) 703 high (car (last uids))) 704 (if (setq cached (nnimap-retrieve-headers-from-file group server)) 705 (progn 706 ;; fetch articles with uids before cache block 707 (when (< low (car cached)) 708 (goto-char (point-min)) 709 (nnimap-retrieve-headers-from-server 710 (cons low (1- (car cached))) group server)) 711 ;; fetch articles with uids after cache block 712 (when (> high (cdr cached)) 713 (goto-char (point-max)) 714 (nnimap-retrieve-headers-from-server 715 (cons (1+ (cdr cached)) high) group server)) 716 (when nnimap-prune-cache 717 ;; remove nov's for articles which has expired on server 718 (goto-char (point-min)) 719 (dolist (uid (gnus-set-difference articles uids)) 720 (when (re-search-forward (format "^%d\t" uid) nil t) 721 (gnus-delete-line))))) 722 ;; nothing cached, fetch whole range from server 723 (nnimap-retrieve-headers-from-server 724 (cons low high) group server)) 725 (when (buffer-modified-p) 726 (nnmail-write-region 727 (point-min) (point-max) 728 (nnimap-group-overview-filename group server) nil 'nomesg)) 729 (nnheader-nov-delete-outside-range low high)))) 730 'nov))) 731 732(defun nnimap-open-connection (server) 733 (if (not (imap-open nnimap-address nnimap-server-port nnimap-stream 734 nnimap-authenticator nnimap-server-buffer)) 735 (nnheader-report 'nnimap "Can't open connection to server %s" server) 736 (unless (or (imap-capability 'IMAP4 nnimap-server-buffer) 737 (imap-capability 'IMAP4rev1 nnimap-server-buffer)) 738 (imap-close nnimap-server-buffer) 739 (nnheader-report 'nnimap "Server %s is not IMAP4 compliant" server)) 740 (let* ((list (progn (gnus-message 7 "Parsing authinfo file `%s'." 741 nnimap-authinfo-file) 742 (gnus-parse-netrc nnimap-authinfo-file))) 743 (port (if nnimap-server-port 744 (int-to-string nnimap-server-port) 745 "imap")) 746 (alist (or (gnus-netrc-machine list server port "imap") 747 (gnus-netrc-machine list server port "imaps") 748 (gnus-netrc-machine list 749 (or nnimap-server-address 750 nnimap-address) 751 port "imap") 752 (gnus-netrc-machine list 753 (or nnimap-server-address 754 nnimap-address) 755 port "imaps"))) 756 (user (gnus-netrc-get alist "login")) 757 (passwd (gnus-netrc-get alist "password"))) 758 (if (imap-authenticate user passwd nnimap-server-buffer) 759 (prog1 760 (push (list server nnimap-server-buffer) 761 nnimap-server-buffer-alist) 762 (nnimap-possibly-change-server server)) 763 (imap-close nnimap-server-buffer) 764 (kill-buffer nnimap-server-buffer) 765 (nnheader-report 'nnimap "Could not authenticate to %s" server))))) 766 767(deffoo nnimap-open-server (server &optional defs) 768 (nnheader-init-server-buffer) 769 (if (nnimap-server-opened server) 770 t 771 (unless (assq 'nnimap-server-buffer defs) 772 (push (list 'nnimap-server-buffer (concat " *nnimap* " server)) defs)) 773 ;; translate `nnimap-server-address' to `nnimap-address' in defs 774 ;; for people that configured nnimap with a very old version 775 (unless (assq 'nnimap-address defs) 776 (if (assq 'nnimap-server-address defs) 777 (push (list 'nnimap-address 778 (cadr (assq 'nnimap-server-address defs))) defs) 779 (push (list 'nnimap-address server) defs))) 780 (nnoo-change-server 'nnimap server defs) 781 (or nnimap-server-buffer 782 (setq nnimap-server-buffer (cadr (assq 'nnimap-server-buffer defs)))) 783 (with-current-buffer (get-buffer-create nnimap-server-buffer) 784 (nnoo-change-server 'nnimap server defs)) 785 (or (and nnimap-server-buffer 786 (imap-opened nnimap-server-buffer) 787 (if (with-current-buffer nnimap-server-buffer 788 (memq imap-state '(auth select examine))) 789 t 790 (imap-close nnimap-server-buffer) 791 (nnimap-open-connection server))) 792 (nnimap-open-connection server)))) 793 794(deffoo nnimap-server-opened (&optional server) 795 "Whether SERVER is opened. 796If SERVER is the current virtual server, and the connection to the 797physical server is alive, this function return a non-nil value. If 798SERVER is nil, it is treated as the current server." 799 ;; clean up autologouts?? 800 (and (or server nnimap-current-server) 801 (nnoo-server-opened 'nnimap (or server nnimap-current-server)) 802 (imap-opened (nnimap-get-server-buffer server)))) 803 804(deffoo nnimap-close-server (&optional server) 805 "Close connection to server and free all resources connected to it. 806Return nil if the server couldn't be closed for some reason." 807 (let ((server (or server nnimap-current-server))) 808 (when (or (nnimap-server-opened server) 809 (imap-opened (nnimap-get-server-buffer server))) 810 (imap-close (nnimap-get-server-buffer server)) 811 (kill-buffer (nnimap-get-server-buffer server)) 812 (setq nnimap-server-buffer nil 813 nnimap-current-server nil 814 nnimap-server-buffer-alist 815 (delq server nnimap-server-buffer-alist))) 816 (nnoo-close-server 'nnimap server))) 817 818(deffoo nnimap-request-close () 819 "Close connection to all servers and free all resources that the backend have reserved. 820All buffers that have been created by that 821backend should be killed. (Not the nntp-server-buffer, though.) This 822function is generally only called when Gnus is shutting down." 823 (mapcar (lambda (server) (nnimap-close-server (car server))) 824 nnimap-server-buffer-alist) 825 (setq nnimap-server-buffer-alist nil)) 826 827(deffoo nnimap-status-message (&optional server) 828 "This function returns the last error message from server." 829 (when (nnimap-possibly-change-server server) 830 (nnoo-status-message 'nnimap server))) 831 832(defun nnimap-demule (string) 833 ;; BEWARE: we used to use string-as-multibyte here which is braindead 834 ;; because it will turn accidental emacs-mule-valid byte sequences 835 ;; into multibyte chars. --Stef 836 ;; Reverted, braindead got 7.5 out of 10 on imdb, so it can't be 837 ;; that bad. --Simon 838 (funcall (if (and (fboundp 'string-as-multibyte) 839 (subrp (symbol-function 'string-as-multibyte))) 840 'string-as-multibyte 841 'identity) 842 (or string ""))) 843 844(defun nnimap-make-callback (article gnus-callback buffer) 845 "Return a callback function." 846 `(lambda () 847 (nnimap-callback ,article ,gnus-callback ,buffer))) 848 849(defun nnimap-callback (article gnus-callback buffer) 850 (when (eq article (imap-current-message)) 851 (remove-hook 'imap-fetch-data-hook 852 (nnimap-make-callback article gnus-callback buffer)) 853 (with-current-buffer buffer 854 (insert 855 (with-current-buffer nnimap-server-buffer 856 (nnimap-demule 857 (if (imap-capability 'IMAP4rev1) 858 ;; xxx don't just use car? alist doesn't contain 859 ;; anything else now, but it might... 860 (nth 2 (car (imap-message-get article 'BODYDETAIL))) 861 (imap-message-get article 'RFC822))))) 862 (nnheader-ms-strip-cr) 863 (funcall gnus-callback t)))) 864 865(defun nnimap-request-article-part (article part prop &optional 866 group server to-buffer detail) 867 (when (nnimap-possibly-change-group group server) 868 (let ((article (if (stringp article) 869 (car-safe (imap-search 870 (format "HEADER Message-Id \"%s\"" article) 871 nnimap-server-buffer)) 872 article))) 873 (when article 874 (gnus-message 10 "nnimap: Fetching (part of) article %d from %s..." 875 article (or group imap-current-mailbox 876 gnus-newsgroup-name)) 877 (if (not nnheader-callback-function) 878 (with-current-buffer (or to-buffer nntp-server-buffer) 879 (erase-buffer) 880 (let ((data (imap-fetch article part prop nil 881 nnimap-server-buffer))) 882 (insert (nnimap-demule (if detail 883 (nth 2 (car data)) 884 data)))) 885 (nnheader-ms-strip-cr) 886 (gnus-message 887 10 "nnimap: Fetching (part of) article %d from %s...done" 888 article (or group imap-current-mailbox gnus-newsgroup-name)) 889 (if (bobp) 890 (nnheader-report 'nnimap "No such article %d in %s: %s" 891 article (or group imap-current-mailbox 892 gnus-newsgroup-name) 893 (imap-error-text nnimap-server-buffer)) 894 (cons group article))) 895 (add-hook 'imap-fetch-data-hook 896 (nnimap-make-callback article 897 nnheader-callback-function 898 nntp-server-buffer)) 899 (imap-fetch-asynch article part nil nnimap-server-buffer) 900 (cons group article)))))) 901 902(deffoo nnimap-asynchronous-p () 903 t) 904 905(deffoo nnimap-request-article (article &optional group server to-buffer) 906 (if (imap-capability 'IMAP4rev1 nnimap-server-buffer) 907 (nnimap-request-article-part 908 article "BODY.PEEK[]" 'BODYDETAIL group server to-buffer 'detail) 909 (nnimap-request-article-part 910 article "RFC822.PEEK" 'RFC822 group server to-buffer))) 911 912(deffoo nnimap-request-head (article &optional group server to-buffer) 913 (if (imap-capability 'IMAP4rev1 nnimap-server-buffer) 914 (nnimap-request-article-part 915 article "BODY.PEEK[HEADER]" 'BODYDETAIL group server to-buffer 'detail) 916 (nnimap-request-article-part 917 article "RFC822.HEADER" 'RFC822.HEADER group server to-buffer))) 918 919(deffoo nnimap-request-body (article &optional group server to-buffer) 920 (if (imap-capability 'IMAP4rev1 nnimap-server-buffer) 921 (nnimap-request-article-part 922 article "BODY.PEEK[TEXT]" 'BODYDETAIL group server to-buffer 'detail) 923 (nnimap-request-article-part 924 article "RFC822.TEXT.PEEK" 'RFC822.TEXT group server to-buffer))) 925 926(deffoo nnimap-request-group (group &optional server fast) 927 (nnimap-request-update-info-internal 928 group 929 (gnus-get-info (gnus-group-prefixed-name 930 group (gnus-server-to-method (format "nnimap:%s" server)))) 931 server) 932 (when (nnimap-possibly-change-group group server) 933 (nnimap-before-find-minmax-bugworkaround) 934 (let (info) 935 (cond (fast group) 936 ((null (setq info (nnimap-find-minmax-uid group t))) 937 (nnheader-report 'nnimap "Could not get active info for %s" 938 group)) 939 (t 940 (nnheader-insert "211 %d %d %d %s\n" (or (nth 0 info) 0) 941 (max 1 (or (nth 1 info) 1)) 942 (or (nth 2 info) 0) group) 943 (nnheader-report 'nnimap "Group %s selected" group) 944 t))))) 945 946(defun nnimap-update-unseen (group &optional server) 947 "Update the unseen count in `nnimap-mailbox-info'." 948 (gnus-sethash 949 (gnus-group-prefixed-name group server) 950 (let ((old (gnus-gethash-safe (gnus-group-prefixed-name group server) 951 nnimap-mailbox-info))) 952 (list (nth 0 old) (nth 1 old) 953 (imap-mailbox-status group 'unseen nnimap-server-buffer) 954 (nth 3 old))) 955 nnimap-mailbox-info)) 956 957(defun nnimap-close-group (group &optional server) 958 (with-current-buffer nnimap-server-buffer 959 (when (and (imap-opened) 960 (nnimap-possibly-change-group group server)) 961 (nnimap-update-unseen group server) 962 (case nnimap-expunge-on-close 963 (always (progn 964 (imap-mailbox-expunge nnimap-close-asynchronous) 965 (unless nnimap-dont-close 966 (imap-mailbox-close nnimap-close-asynchronous)))) 967 (ask (if (and (imap-search "DELETED") 968 (gnus-y-or-n-p (format "Expunge articles in group `%s'? " 969 imap-current-mailbox))) 970 (progn 971 (imap-mailbox-expunge nnimap-close-asynchronous) 972 (unless nnimap-dont-close 973 (imap-mailbox-close nnimap-close-asynchronous))) 974 (imap-mailbox-unselect))) 975 (t (imap-mailbox-unselect))) 976 (not imap-current-mailbox)))) 977 978(defun nnimap-pattern-to-list-arguments (pattern) 979 (mapcar (lambda (p) 980 (cons (car-safe p) (or (cdr-safe p) p))) 981 (if (and (listp pattern) 982 (listp (cdr pattern))) 983 pattern 984 (list pattern)))) 985 986(deffoo nnimap-request-list (&optional server) 987 (when (nnimap-possibly-change-server server) 988 (with-current-buffer nntp-server-buffer 989 (erase-buffer)) 990 (gnus-message 5 "nnimap: Generating active list%s..." 991 (if (> (length server) 0) (concat " for " server) "")) 992 (nnimap-before-find-minmax-bugworkaround) 993 (with-current-buffer nnimap-server-buffer 994 (dolist (pattern (nnimap-pattern-to-list-arguments nnimap-list-pattern)) 995 (dolist (mbx (funcall nnimap-request-list-method 996 (cdr pattern) (car pattern))) 997 (or (member "\\NoSelect" (imap-mailbox-get 'list-flags mbx)) 998 (let ((info (nnimap-find-minmax-uid mbx 'examine))) 999 (when info 1000 (with-current-buffer nntp-server-buffer 1001 (insert (format "\"%s\" %d %d y\n" 1002 mbx (or (nth 2 info) 0) 1003 (max 1 (or (nth 1 info) 1))))))))))) 1004 (gnus-message 5 "nnimap: Generating active list%s...done" 1005 (if (> (length server) 0) (concat " for " server) "")) 1006 t)) 1007 1008(deffoo nnimap-request-post (&optional server) 1009 (let ((success t)) 1010 (dolist (mbx (message-unquote-tokens 1011 (message-tokenize-header 1012 (message-fetch-field "Newsgroups") ", ")) success) 1013 (let ((to-newsgroup (gnus-group-prefixed-name mbx gnus-command-method))) 1014 (or (gnus-active to-newsgroup) 1015 (gnus-activate-group to-newsgroup) 1016 (if (gnus-y-or-n-p (format "No such group: %s. Create it? " 1017 to-newsgroup)) 1018 (or (and (gnus-request-create-group 1019 to-newsgroup gnus-command-method) 1020 (gnus-activate-group to-newsgroup nil nil 1021 gnus-command-method)) 1022 (error "Couldn't create group %s" to-newsgroup))) 1023 (error "No such group: %s" to-newsgroup)) 1024 (unless (nnimap-request-accept-article mbx (nth 1 gnus-command-method)) 1025 (setq success nil)))))) 1026 1027;; Optional backend functions 1028 1029(defun nnimap-string-lessp-numerical (s1 s2) 1030 "Return t if first arg string is less than second in numerical order." 1031 (cond ((string= s1 s2) 1032 nil) 1033 ((> (length s1) (length s2)) 1034 nil) 1035 ((< (length s1) (length s2)) 1036 t) 1037 ((< (string-to-number (substring s1 0 1)) 1038 (string-to-number (substring s2 0 1))) 1039 t) 1040 ((> (string-to-number (substring s1 0 1)) 1041 (string-to-number (substring s2 0 1))) 1042 nil) 1043 (t 1044 (nnimap-string-lessp-numerical (substring s1 1) (substring s2 1))))) 1045 1046(deffoo nnimap-retrieve-groups (groups &optional server) 1047 (when (nnimap-possibly-change-server server) 1048 (gnus-message 5 "nnimap: Checking mailboxes...") 1049 (with-current-buffer nntp-server-buffer 1050 (erase-buffer) 1051 (nnimap-before-find-minmax-bugworkaround) 1052 (let (asyncgroups slowgroups) 1053 (if (null nnimap-retrieve-groups-asynchronous) 1054 (setq slowgroups groups) 1055 (dolist (group groups) 1056 (gnus-message 9 "nnimap: Quickly checking mailbox %s" group) 1057 (add-to-list (if (gnus-gethash-safe 1058 (gnus-group-prefixed-name group server) 1059 nnimap-mailbox-info) 1060 'asyncgroups 1061 'slowgroups) 1062 (list group (imap-mailbox-status-asynch 1063 group '(uidvalidity uidnext unseen) 1064 nnimap-server-buffer)))) 1065 (dolist (asyncgroup asyncgroups) 1066 (let ((group (nth 0 asyncgroup)) 1067 (tag (nth 1 asyncgroup)) 1068 new old) 1069 (when (imap-ok-p (imap-wait-for-tag tag nnimap-server-buffer)) 1070 (if (or (not (string= 1071 (nth 0 (gnus-gethash (gnus-group-prefixed-name 1072 group server) 1073 nnimap-mailbox-info)) 1074 (imap-mailbox-get 'uidvalidity group 1075 nnimap-server-buffer))) 1076 (not (string= 1077 (nth 1 (gnus-gethash (gnus-group-prefixed-name 1078 group server) 1079 nnimap-mailbox-info)) 1080 (imap-mailbox-get 'uidnext group 1081 nnimap-server-buffer)))) 1082 (push (list group) slowgroups) 1083 (insert (nth 3 (gnus-gethash (gnus-group-prefixed-name 1084 group server) 1085 nnimap-mailbox-info)))))))) 1086 (dolist (group slowgroups) 1087 (if nnimap-retrieve-groups-asynchronous 1088 (setq group (car group))) 1089 (gnus-message 7 "nnimap: Mailbox %s modified" group) 1090 (imap-mailbox-put 'uidnext nil group nnimap-server-buffer) 1091 (or (member "\\NoSelect" (imap-mailbox-get 'list-flags group 1092 nnimap-server-buffer)) 1093 (let* ((info (nnimap-find-minmax-uid group 'examine)) 1094 (str (format "\"%s\" %d %d y\n" group 1095 (or (nth 2 info) 0) 1096 (max 1 (or (nth 1 info) 1))))) 1097 (when (> (or (imap-mailbox-get 'recent group 1098 nnimap-server-buffer) 0) 1099 0) 1100 (push (list (cons group 0)) nnmail-split-history)) 1101 (insert str) 1102 (when nnimap-retrieve-groups-asynchronous 1103 (gnus-sethash 1104 (gnus-group-prefixed-name group server) 1105 (list (or (imap-mailbox-get 1106 'uidvalidity group nnimap-server-buffer) 1107 (imap-mailbox-status 1108 group 'uidvalidity nnimap-server-buffer)) 1109 (or (imap-mailbox-get 1110 'uidnext group nnimap-server-buffer) 1111 (imap-mailbox-status 1112 group 'uidnext nnimap-server-buffer)) 1113 (or (imap-mailbox-get 1114 'unseen group nnimap-server-buffer) 1115 (imap-mailbox-status 1116 group 'unseen nnimap-server-buffer)) 1117 str) 1118 nnimap-mailbox-info))))))) 1119 (gnus-message 5 "nnimap: Checking mailboxes...done") 1120 'active)) 1121 1122(deffoo nnimap-request-update-info-internal (group info &optional server) 1123 (when (nnimap-possibly-change-group group server) 1124 (when info ;; xxx what does this mean? should we create a info? 1125 (with-current-buffer nnimap-server-buffer 1126 (gnus-message 5 "nnimap: Updating info for %s..." 1127 (gnus-info-group info)) 1128 1129 (when (nnimap-mark-permanent-p 'read) 1130 (let (seen unseen) 1131 ;; read info could contain articles marked unread by other 1132 ;; imap clients! we correct this 1133 (setq unseen (gnus-compress-sequence 1134 (imap-search "UNSEEN UNDELETED")) 1135 seen (gnus-range-difference (gnus-info-read info) unseen) 1136 seen (gnus-range-add seen 1137 (gnus-compress-sequence 1138 (imap-search "SEEN"))) 1139 seen (if (and (integerp (car seen)) 1140 (null (cdr seen))) 1141 (list (cons (car seen) (car seen))) 1142 seen)) 1143 (gnus-info-set-read info seen))) 1144 1145 (mapcar (lambda (pred) 1146 (when (or (eq (cdr pred) 'recent) 1147 (and (nnimap-mark-permanent-p (cdr pred)) 1148 (member (nnimap-mark-to-flag (cdr pred)) 1149 (imap-mailbox-get 'flags)))) 1150 (gnus-info-set-marks 1151 info 1152 (gnus-update-alist-soft 1153 (cdr pred) 1154 (gnus-compress-sequence 1155 (imap-search (nnimap-mark-to-predicate (cdr pred)))) 1156 (gnus-info-marks info)) 1157 t))) 1158 gnus-article-mark-lists) 1159 1160 (when nnimap-importantize-dormant 1161 ;; nnimap mark dormant article as ticked too (for other clients) 1162 ;; so we remove that mark for gnus since we support dormant 1163 (gnus-info-set-marks 1164 info 1165 (gnus-update-alist-soft 1166 'tick 1167 (gnus-remove-from-range 1168 (cdr-safe (assoc 'tick (gnus-info-marks info))) 1169 (cdr-safe (assoc 'dormant (gnus-info-marks info)))) 1170 (gnus-info-marks info)) 1171 t)) 1172 1173 (gnus-message 5 "nnimap: Updating info for %s...done" 1174 (gnus-info-group info)) 1175 1176 info)))) 1177 1178(deffoo nnimap-request-type (group &optional article) 1179 (if (and nnimap-news-groups (string-match nnimap-news-groups group)) 1180 'news 1181 'mail)) 1182 1183(deffoo nnimap-request-set-mark (group actions &optional server) 1184 (when (nnimap-possibly-change-group group server) 1185 (with-current-buffer nnimap-server-buffer 1186 (let (action) 1187 (gnus-message 7 "nnimap: Setting marks in %s..." group) 1188 (while (setq action (pop actions)) 1189 (let ((range (nth 0 action)) 1190 (what (nth 1 action)) 1191 (cmdmarks (nth 2 action)) 1192 marks) 1193 ;; bookmark can't be stored (not list/range 1194 (setq cmdmarks (delq 'bookmark cmdmarks)) 1195 ;; killed can't be stored (not list/range 1196 (setq cmdmarks (delq 'killed cmdmarks)) 1197 ;; unsent are for nndraft groups only 1198 (setq cmdmarks (delq 'unsent cmdmarks)) 1199 ;; cache flags are pointless on the server 1200 (setq cmdmarks (delq 'cache cmdmarks)) 1201 ;; seen flags are local to each gnus 1202 (setq cmdmarks (delq 'seen cmdmarks)) 1203 ;; recent marks can't be set 1204 (setq cmdmarks (delq 'recent cmdmarks)) 1205 (when nnimap-importantize-dormant 1206 ;; flag dormant articles as ticked 1207 (if (memq 'dormant cmdmarks) 1208 (setq cmdmarks (cons 'tick cmdmarks)))) 1209 ;; remove stuff we are forbidden to store 1210 (mapcar (lambda (mark) 1211 (if (imap-message-flag-permanent-p 1212 (nnimap-mark-to-flag mark)) 1213 (setq marks (cons mark marks)))) 1214 cmdmarks) 1215 (when (and range marks) 1216 (cond ((eq what 'del) 1217 (imap-message-flags-del 1218 (imap-range-to-message-set range) 1219 (nnimap-mark-to-flag marks nil t))) 1220 ((eq what 'add) 1221 (imap-message-flags-add 1222 (imap-range-to-message-set range) 1223 (nnimap-mark-to-flag marks nil t))) 1224 ((eq what 'set) 1225 (imap-message-flags-set 1226 (imap-range-to-message-set range) 1227 (nnimap-mark-to-flag marks nil t))))))) 1228 (gnus-message 7 "nnimap: Setting marks in %s...done" group)))) 1229 nil) 1230 1231(defun nnimap-split-fancy () 1232 "Like the function `nnmail-split-fancy', but uses `nnimap-split-fancy'." 1233 (let ((nnmail-split-fancy nnimap-split-fancy)) 1234 (nnmail-split-fancy))) 1235 1236(defun nnimap-split-to-groups (rules) 1237 ;; tries to match all rules in nnimap-split-rule against content of 1238 ;; nntp-server-buffer, returns a list of groups that matched. 1239 (with-current-buffer nntp-server-buffer 1240 ;; Fold continuation lines. 1241 (goto-char (point-min)) 1242 (while (re-search-forward "\\(\r?\n[ \t]+\\)+" nil t) 1243 (replace-match " " t t)) 1244 (if (functionp rules) 1245 (funcall rules) 1246 (let (to-groups regrepp) 1247 (catch 'split-done 1248 (dolist (rule rules to-groups) 1249 (let ((group (car rule)) 1250 (regexp (cadr rule))) 1251 (goto-char (point-min)) 1252 (when (and (if (stringp regexp) 1253 (progn 1254 (if (not (stringp group)) 1255 (setq group (eval group)) 1256 (setq regrepp 1257 (string-match "\\\\[0-9&]" group))) 1258 (re-search-forward regexp nil t)) 1259 (funcall regexp group)) 1260 ;; Don't enter the article into the same group twice. 1261 (not (assoc group to-groups))) 1262 (push (if regrepp 1263 (nnmail-expand-newtext group) 1264 group) 1265 to-groups) 1266 (or nnimap-split-crosspost 1267 (throw 'split-done to-groups)))))))))) 1268 1269(defun nnimap-assoc-match (key alist) 1270 (let (element) 1271 (while (and alist (not element)) 1272 (if (string-match (car (car alist)) key) 1273 (setq element (car alist))) 1274 (setq alist (cdr alist))) 1275 element)) 1276 1277(defun nnimap-split-find-rule (server inbox) 1278 (if (and (listp nnimap-split-rule) (listp (car nnimap-split-rule)) 1279 (list (cdar nnimap-split-rule)) (listp (cadar nnimap-split-rule))) 1280 ;; extended format 1281 (cadr (nnimap-assoc-match inbox (cdr (nnimap-assoc-match 1282 server nnimap-split-rule)))) 1283 nnimap-split-rule)) 1284 1285(defun nnimap-split-find-inbox (server) 1286 (if (listp nnimap-split-inbox) 1287 nnimap-split-inbox 1288 (list nnimap-split-inbox))) 1289 1290(defun nnimap-split-articles (&optional group server) 1291 (when (nnimap-possibly-change-server server) 1292 (with-current-buffer nnimap-server-buffer 1293 (let (rule inbox removeorig (inboxes (nnimap-split-find-inbox server))) 1294 ;; iterate over inboxes 1295 (while (and (setq inbox (pop inboxes)) 1296 (nnimap-possibly-change-group inbox)) ;; SELECT 1297 ;; find split rule for this server / inbox 1298 (when (setq rule (nnimap-split-find-rule server inbox)) 1299 ;; iterate over articles 1300 (dolist (article (imap-search nnimap-split-predicate)) 1301 (when (if (if (eq nnimap-split-download-body 'default) 1302 nnimap-split-download-body-default 1303 nnimap-split-download-body) 1304 (and (nnimap-request-article article) 1305 (with-current-buffer nntp-server-buffer (mail-narrow-to-head))) 1306 (nnimap-request-head article)) 1307 ;; copy article to right group(s) 1308 (setq removeorig nil) 1309 (dolist (to-group (nnimap-split-to-groups rule)) 1310 (cond ((eq to-group 'junk) 1311 (message "IMAP split removed %s:%s:%d" server inbox 1312 article) 1313 (setq removeorig t)) 1314 ((imap-message-copy (number-to-string article) 1315 to-group nil 'nocopyuid) 1316 (message "IMAP split moved %s:%s:%d to %s" server 1317 inbox article to-group) 1318 (setq removeorig t) 1319 (when nnmail-cache-accepted-message-ids 1320 (with-current-buffer nntp-server-buffer 1321 (let (msgid) 1322 (and (setq msgid 1323 (nnmail-fetch-field "message-id")) 1324 (nnmail-cache-insert msgid 1325 to-group 1326 (nnmail-fetch-field "subject")))))) 1327 ;; Add the group-art list to the history list. 1328 (push (list (cons to-group 0)) nnmail-split-history)) 1329 (t 1330 (message "IMAP split failed to move %s:%s:%d to %s" 1331 server inbox article to-group)))) 1332 (if (if (eq nnimap-split-download-body 'default) 1333 nnimap-split-download-body-default 1334 nnimap-split-download-body) 1335 (widen)) 1336 ;; remove article if it was successfully copied somewhere 1337 (and removeorig 1338 (imap-message-flags-add (format "%d" article) 1339 "\\Seen \\Deleted"))))) 1340 (when (imap-mailbox-select inbox) ;; just in case 1341 ;; todo: UID EXPUNGE (if available) to remove splitted articles 1342 (imap-mailbox-expunge) 1343 (imap-mailbox-close))) 1344 (when nnmail-cache-accepted-message-ids 1345 (nnmail-cache-close)) 1346 t)))) 1347 1348(deffoo nnimap-request-scan (&optional group server) 1349 (nnimap-split-articles group server)) 1350 1351(deffoo nnimap-request-newgroups (date &optional server) 1352 (when (nnimap-possibly-change-server server) 1353 (with-current-buffer nntp-server-buffer 1354 (gnus-message 5 "nnimap: Listing subscribed mailboxes%s%s..." 1355 (if (> (length server) 0) " on " "") server) 1356 (erase-buffer) 1357 (nnimap-before-find-minmax-bugworkaround) 1358 (dolist (pattern (nnimap-pattern-to-list-arguments 1359 nnimap-list-pattern)) 1360 (dolist (mbx (imap-mailbox-lsub (cdr pattern) (car pattern) nil 1361 nnimap-server-buffer)) 1362 (or (catch 'found 1363 (dolist (mailbox (imap-mailbox-get 'list-flags mbx 1364 nnimap-server-buffer)) 1365 (if (string= (downcase mailbox) "\\noselect") 1366 (throw 'found t))) 1367 nil) 1368 (let ((info (nnimap-find-minmax-uid mbx 'examine))) 1369 (when info 1370 (insert (format "\"%s\" %d %d y\n" 1371 mbx (or (nth 2 info) 0) 1372 (max 1 (or (nth 1 info) 1))))))))) 1373 (gnus-message 5 "nnimap: Listing subscribed mailboxes%s%s...done" 1374 (if (> (length server) 0) " on " "") server)) 1375 t)) 1376 1377(deffoo nnimap-request-create-group (group &optional server args) 1378 (when (nnimap-possibly-change-server server) 1379 (or (imap-mailbox-status group 'uidvalidity nnimap-server-buffer) 1380 (imap-mailbox-create group nnimap-server-buffer) 1381 (nnheader-report 'nnimap "%S" 1382 (imap-error-text nnimap-server-buffer))))) 1383 1384(defun nnimap-time-substract (time1 time2) 1385 "Return TIME for TIME1 - TIME2." 1386 (let* ((ms (- (car time1) (car time2))) 1387 (ls (- (nth 1 time1) (nth 1 time2)))) 1388 (if (< ls 0) 1389 (list (- ms 1) (+ (expt 2 16) ls)) 1390 (list ms ls)))) 1391 1392(eval-when-compile (require 'parse-time)) 1393(defun nnimap-date-days-ago (daysago) 1394 "Return date, in format \"3-Aug-1998\", for DAYSAGO days ago." 1395 (require 'parse-time) 1396 (let* ((time (nnimap-time-substract (current-time) (days-to-time daysago))) 1397 (date (format-time-string 1398 (format "%%d-%s-%%Y" 1399 (capitalize (car (rassoc (nth 4 (decode-time time)) 1400 parse-time-months)))) 1401 time))) 1402 (if (eq ?0 (string-to-char date)) 1403 (substring date 1) 1404 date))) 1405 1406(defun nnimap-request-expire-articles-progress () 1407 (gnus-message 5 "nnimap: Marking article %d for deletion..." 1408 imap-current-message)) 1409 1410(defun nnimap-expiry-target (arts group server) 1411 (unless (eq nnmail-expiry-target 'delete) 1412 (with-temp-buffer 1413 (dolist (art arts) 1414 (nnimap-request-article art group server (current-buffer)) 1415 ;; hints for optimization in `nnimap-request-accept-article' 1416 (let ((nnimap-current-move-article art) 1417 (nnimap-current-move-group group) 1418 (nnimap-current-move-server server)) 1419 (nnmail-expiry-target-group nnmail-expiry-target group)))) 1420 ;; It is not clear if `nnmail-expiry-target' somehow cause the 1421 ;; current group to be changed or not, so we make sure here. 1422 (nnimap-possibly-change-group group server))) 1423 1424;; Notice that we don't actually delete anything, we just mark them deleted. 1425(deffoo nnimap-request-expire-articles (articles group &optional server force) 1426 (let ((artseq (gnus-compress-sequence articles))) 1427 (when (and artseq (nnimap-possibly-change-group group server)) 1428 (with-current-buffer nnimap-server-buffer 1429 (let ((days (or (and nnmail-expiry-wait-function 1430 (funcall nnmail-expiry-wait-function group)) 1431 nnmail-expiry-wait))) 1432 (cond ((or force (eq days 'immediate)) 1433 (let ((oldarts (imap-search 1434 (concat "UID " 1435 (imap-range-to-message-set artseq))))) 1436 (when oldarts 1437 (nnimap-expiry-target oldarts group server) 1438 (when (imap-message-flags-add 1439 (imap-range-to-message-set 1440 (gnus-compress-sequence oldarts)) "\\Deleted") 1441 (setq articles (gnus-set-difference 1442 articles oldarts)))))) 1443 ((and nnimap-search-uids-not-since-is-evil (numberp days)) 1444 (let* ((all-new-articles 1445 (gnus-compress-sequence 1446 (imap-search (format "SINCE %s" 1447 (nnimap-date-days-ago days))))) 1448 (oldartseq 1449 (gnus-range-difference artseq all-new-articles)) 1450 (oldarts (gnus-uncompress-range oldartseq))) 1451 (when oldarts 1452 (nnimap-expiry-target oldarts group server) 1453 (when (imap-message-flags-add 1454 (imap-range-to-message-set oldartseq) 1455 "\\Deleted") 1456 (setq articles (gnus-set-difference 1457 articles oldarts)))))) 1458 ((numberp days) 1459 (let ((oldarts (imap-search 1460 (format nnimap-expunge-search-string 1461 (imap-range-to-message-set artseq) 1462 (nnimap-date-days-ago days)))) 1463 (imap-fetch-data-hook 1464 '(nnimap-request-expire-articles-progress))) 1465 (when oldarts 1466 (nnimap-expiry-target oldarts group server) 1467 (when (imap-message-flags-add 1468 (imap-range-to-message-set 1469 (gnus-compress-sequence oldarts)) "\\Deleted") 1470 (setq articles (gnus-set-difference 1471 articles oldarts))))))))))) 1472 ;; return articles not deleted 1473 articles) 1474 1475(deffoo nnimap-request-move-article (article group server 1476 accept-form &optional last) 1477 (when (nnimap-possibly-change-server server) 1478 (save-excursion 1479 (let ((buf (get-buffer-create " *nnimap move*")) 1480 (nnimap-current-move-article article) 1481 (nnimap-current-move-group group) 1482 (nnimap-current-move-server nnimap-current-server) 1483 result) 1484 (and (nnimap-request-article article group server) 1485 (save-excursion 1486 (set-buffer buf) 1487 (buffer-disable-undo (current-buffer)) 1488 (insert-buffer-substring nntp-server-buffer) 1489 (setq result (eval accept-form)) 1490 (kill-buffer buf) 1491 result) 1492 (nnimap-possibly-change-group group server) 1493 (imap-message-flags-add 1494 (imap-range-to-message-set (list article)) 1495 "\\Deleted" 'silent nnimap-server-buffer)) 1496 result)))) 1497 1498(deffoo nnimap-request-accept-article (group &optional server last) 1499 (when (nnimap-possibly-change-server server) 1500 (let (uid) 1501 (if (setq uid 1502 (if (string= nnimap-current-server nnimap-current-move-server) 1503 ;; moving article within same server, speed it up... 1504 (and (nnimap-possibly-change-group 1505 nnimap-current-move-group) 1506 (imap-message-copy (number-to-string 1507 nnimap-current-move-article) 1508 group 'dontcreate nil 1509 nnimap-server-buffer)) 1510 (with-current-buffer (current-buffer) 1511 (goto-char (point-min)) 1512 ;; remove any 'From blabla' lines, some IMAP servers 1513 ;; reject the entire message otherwise. 1514 (when (looking-at "^From[^:]") 1515 (delete-region (point) (progn (forward-line) (point)))) 1516 ;; turn into rfc822 format (\r\n eol's) 1517 (while (search-forward "\n" nil t) 1518 (replace-match "\r\n")) 1519 (when nnmail-cache-accepted-message-ids 1520 (nnmail-cache-insert (nnmail-fetch-field "message-id") 1521 group 1522 (nnmail-fetch-field "subject")))) 1523 (when (and last nnmail-cache-accepted-message-ids) 1524 (nnmail-cache-close)) 1525 ;; this 'or' is for Cyrus server bug 1526 (or (null (imap-current-mailbox nnimap-server-buffer)) 1527 (imap-mailbox-unselect nnimap-server-buffer)) 1528 (imap-message-append group (current-buffer) nil nil 1529 nnimap-server-buffer))) 1530 (cons group (nth 1 uid)) 1531 (nnheader-report 'nnimap (imap-error-text nnimap-server-buffer)))))) 1532 1533(deffoo nnimap-request-delete-group (group force &optional server) 1534 (when (nnimap-possibly-change-server server) 1535 (with-current-buffer nnimap-server-buffer 1536 (if force 1537 (or (null (imap-mailbox-status group 'uidvalidity)) 1538 (imap-mailbox-delete group)) 1539 ;; UNSUBSCRIBE? 1540 t)))) 1541 1542(deffoo nnimap-request-rename-group (group new-name &optional server) 1543 (when (nnimap-possibly-change-server server) 1544 (imap-mailbox-rename group new-name nnimap-server-buffer))) 1545 1546(defun nnimap-expunge (mailbox server) 1547 (when (nnimap-possibly-change-group mailbox server) 1548 (imap-mailbox-expunge nil nnimap-server-buffer))) 1549 1550(defun nnimap-acl-get (mailbox server) 1551 (when (nnimap-possibly-change-server server) 1552 (and (imap-capability 'ACL nnimap-server-buffer) 1553 (imap-mailbox-acl-get mailbox nnimap-server-buffer)))) 1554 1555(defun nnimap-acl-edit (mailbox method old-acls new-acls) 1556 (when (nnimap-possibly-change-server (cadr method)) 1557 (unless (imap-capability 'ACL nnimap-server-buffer) 1558 (error "Your server does not support ACL editing")) 1559 (with-current-buffer nnimap-server-buffer 1560 ;; delete all removed identifiers 1561 (mapcar (lambda (old-acl) 1562 (unless (assoc (car old-acl) new-acls) 1563 (or (imap-mailbox-acl-delete (car old-acl) mailbox) 1564 (error "Can't delete ACL for %s" (car old-acl))))) 1565 old-acls) 1566 ;; set all changed acl's 1567 (mapcar (lambda (new-acl) 1568 (let ((new-rights (cdr new-acl)) 1569 (old-rights (cdr (assoc (car new-acl) old-acls)))) 1570 (unless (and old-rights new-rights 1571 (string= old-rights new-rights)) 1572 (or (imap-mailbox-acl-set (car new-acl) new-rights mailbox) 1573 (error "Can't set ACL for %s to %s" (car new-acl) 1574 new-rights))))) 1575 new-acls) 1576 t))) 1577 1578 1579;;; Internal functions 1580 1581;; 1582;; This is confusing. 1583;; 1584;; mark => read, tick, draft, reply etc 1585;; flag => "\\Seen", "\\Flagged", "\\Draft", "gnus-expire" etc 1586;; predicate => "SEEN", "FLAGGED", "DRAFT", "KEYWORD gnus-expire" etc 1587;; 1588;; Mark should not really contain 'read since it's not a "mark" in the Gnus 1589;; world, but we cheat. Mark == gnus-article-mark-lists + '(read . read). 1590;; 1591 1592(defconst nnimap-mark-to-predicate-alist 1593 (mapcar 1594 (lambda (pair) ; cdr is the mark 1595 (or (assoc (cdr pair) 1596 '((read . "SEEN") 1597 (tick . "FLAGGED") 1598 (draft . "DRAFT") 1599 (recent . "RECENT") 1600 (reply . "ANSWERED"))) 1601 (cons (cdr pair) 1602 (format "KEYWORD gnus-%s" (symbol-name (cdr pair)))))) 1603 (cons '(read . read) gnus-article-mark-lists))) 1604 1605(defun nnimap-mark-to-predicate (pred) 1606 "Convert a Gnus mark (a symbol such as read, tick, expire) to a IMAP predicate. 1607This is a string such as \"SEEN\", \"FLAGGED\", \"KEYWORD gnus-expire\", 1608to be used within a IMAP SEARCH query." 1609 (cdr (assq pred nnimap-mark-to-predicate-alist))) 1610 1611(defconst nnimap-mark-to-flag-alist 1612 (mapcar 1613 (lambda (pair) 1614 (or (assoc (cdr pair) 1615 '((read . "\\Seen") 1616 (tick . "\\Flagged") 1617 (draft . "\\Draft") 1618 (recent . "\\Recent") 1619 (reply . "\\Answered"))) 1620 (cons (cdr pair) 1621 (format "gnus-%s" (symbol-name (cdr pair)))))) 1622 (cons '(read . read) gnus-article-mark-lists))) 1623 1624(defun nnimap-mark-to-flag-1 (preds) 1625 (if (and (not (null preds)) (listp preds)) 1626 (cons (nnimap-mark-to-flag (car preds)) 1627 (nnimap-mark-to-flag (cdr preds))) 1628 (cdr (assoc preds nnimap-mark-to-flag-alist)))) 1629 1630(defun nnimap-mark-to-flag (preds &optional always-list make-string) 1631 "Convert a Gnus mark (a symbol such as read, tick, expire) to a IMAP flag. 1632This is a string such as \"\\Seen\", \"\\Flagged\", \"gnus-expire\", to 1633be used in a STORE FLAGS command." 1634 (let ((result (nnimap-mark-to-flag-1 preds))) 1635 (setq result (if (and (or make-string always-list) 1636 (not (listp result))) 1637 (list result) 1638 result)) 1639 (if make-string 1640 (mapconcat (lambda (flag) 1641 (if (listp flag) 1642 (mapconcat 'identity flag " ") 1643 flag)) 1644 result " ") 1645 result))) 1646 1647(defun nnimap-mark-permanent-p (mark &optional group) 1648 "Return t iff MARK can be permanently (between IMAP sessions) saved on articles, in GROUP." 1649 (imap-message-flag-permanent-p (nnimap-mark-to-flag mark))) 1650 1651(when nnimap-debug 1652 (require 'trace) 1653 (buffer-disable-undo (get-buffer-create nnimap-debug-buffer)) 1654 (mapcar (lambda (f) (trace-function-background f nnimap-debug-buffer)) 1655 '( 1656 nnimap-possibly-change-server 1657 nnimap-verify-uidvalidity 1658 nnimap-find-minmax-uid 1659 nnimap-before-find-minmax-bugworkaround 1660 nnimap-possibly-change-group 1661 ;;nnimap-replace-whitespace 1662 nnimap-retrieve-headers-progress 1663 nnimap-retrieve-which-headers 1664 nnimap-group-overview-filename 1665 nnimap-retrieve-headers-from-file 1666 nnimap-retrieve-headers-from-server 1667 nnimap-retrieve-headers 1668 nnimap-open-connection 1669 nnimap-open-server 1670 nnimap-server-opened 1671 nnimap-close-server 1672 nnimap-request-close 1673 nnimap-status-message 1674 ;;nnimap-demule 1675 nnimap-request-article-part 1676 nnimap-request-article 1677 nnimap-request-head 1678 nnimap-request-body 1679 nnimap-request-group 1680 nnimap-close-group 1681 nnimap-pattern-to-list-arguments 1682 nnimap-request-list 1683 nnimap-request-post 1684 nnimap-retrieve-groups 1685 nnimap-request-update-info-internal 1686 nnimap-request-type 1687 nnimap-request-set-mark 1688 nnimap-split-to-groups 1689 nnimap-split-find-rule 1690 nnimap-split-find-inbox 1691 nnimap-split-articles 1692 nnimap-request-scan 1693 nnimap-request-newgroups 1694 nnimap-request-create-group 1695 nnimap-time-substract 1696 nnimap-date-days-ago 1697 nnimap-request-expire-articles-progress 1698 nnimap-request-expire-articles 1699 nnimap-request-move-article 1700 nnimap-request-accept-article 1701 nnimap-request-delete-group 1702 nnimap-request-rename-group 1703 gnus-group-nnimap-expunge 1704 gnus-group-nnimap-edit-acl 1705 gnus-group-nnimap-edit-acl-done 1706 nnimap-group-mode-hook 1707 nnimap-mark-to-predicate 1708 nnimap-mark-to-flag-1 1709 nnimap-mark-to-flag 1710 nnimap-mark-permanent-p 1711 ))) 1712 1713(provide 'nnimap) 1714 1715;; arch-tag: 2b001f20-3ff9-4094-a0ad-46807c1ba70b 1716;;; nnimap.el ends here 1717