* gnus-art.el (gnus-mime-delete-part): Error message when no MIME
[gnus] / lisp / gnus-agent.el
index eaf69d9..2366306 100644 (file)
@@ -1,5 +1,5 @@
 ;;; gnus-agent.el --- unplugged support for Gnus
-;; Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002
+;; Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004
 ;;        Free Software Foundation, Inc.
 
 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
 
 (require 'gnus)
 (require 'gnus-cache)
+(require 'nnmail)
 (require 'nnvirtual)
 (require 'gnus-sum)
 (require 'gnus-score)
 (require 'gnus-srvr)
+(require 'gnus-util)
 (eval-when-compile
   (if (featurep 'xemacs)
       (require 'itimer)
@@ -37,7 +39,9 @@
   (require 'cl))
 
 (eval-and-compile
-  (autoload 'gnus-server-update-server "gnus-srvr"))
+  (autoload 'gnus-server-update-server "gnus-srvr")
+  (autoload 'gnus-agent-customize-category "gnus-cus")
+)
 
 (defcustom gnus-agent-directory (nnheader-concat gnus-directory "agent/")
   "Where the Gnus agent will store its files."
@@ -55,7 +59,7 @@
   :type 'hook)
 
 (defcustom gnus-agent-fetched-hook nil
-  "Hook run after finishing fetching articles."
+  "Hook run when finished fetching articles."
   :group 'gnus-agent
   :type 'hook)
 
 
 (defcustom gnus-agent-expire-days 7
   "Read articles older than this will be expired.
-This can also be a list of regexp/day pairs.  The regexps will
-be matched against group names."
+If you wish to disable Agent expiring, see `gnus-agent-enable-expiration'."
   :group 'gnus-agent
-  :type 'integer)
+  :type '(number :tag "days"))
 
 (defcustom gnus-agent-expire-all nil
   "If non-nil, also expire unread, ticked and dormant articles.
@@ -110,7 +113,7 @@ If nil, only read articles will be expired."
   :group 'gnus-agent
   :type 'function)
 
-(defcustom gnus-agent-synchronize-flags 'ask
+(defcustom gnus-agent-synchronize-flags nil
   "Indicate if flags are synchronized when you plug in.
 If this is `ask' the hook will query the user."
   :version "21.1"
@@ -140,11 +143,81 @@ If this is `ask' the hook will query the user."
   :type '(repeat (symbol :tag "Mark"))
   :group 'gnus-agent)
 
+(defcustom gnus-agent-consider-all-articles nil
+  "When non-nil, the agent will let the agent predicate decide
+whether articles need to be downloaded or not, for all articles.  When
+nil, the default, the agent will only let the predicate decide
+whether unread articles are downloaded or not.  If you enable this,
+groups with large active ranges may open slower and you may also want
+to look into the agent expiry settings to block the expiration of
+read articles as they would just be downloaded again."
+  :version "21.4"
+  :type 'boolean
+  :group 'gnus-agent)
+
+(defcustom gnus-agent-max-fetch-size 10000000 ;; 10 Mb
+  "Chunk size for `gnus-agent-fetch-session'.
+The function will split its article fetches into chunks smaller than
+this limit."
+  :group 'gnus-agent
+  :type 'integer)
+
+(defcustom gnus-agent-enable-expiration 'ENABLE
+  "The default expiration state for each group.
+When set to ENABLE, the default, `gnus-agent-expire' will expire old
+contents from a group's local storage.  This value may be overridden
+to disable expiration in specific categories, topics, and groups.  Of
+course, you could change gnus-agent-enable-expiration to DISABLE then
+enable expiration per categories, topics, and groups."
+  :group 'gnus-agent
+  :type '(radio (const :format "Enable " ENABLE)
+                (const :format "Disable " DISABLE)))
+
+(defcustom gnus-agent-expire-unagentized-dirs t
+  "*Whether expiration should expire in unagentized directories.
+Have gnus-agent-expire scan the directories under
+\(gnus-agent-directory) for groups that are no longer agentized.
+When found, offer to remove them."
+  :type 'boolean
+  :group 'gnus-agent)
+
+(defcustom gnus-agent-auto-agentize-methods '(nntp nnimap)
+  "Initially, all servers from these methods are agentized.
+The user may remove or add servers using the Server buffer.
+See Info node `(gnus)Server Buffer'."
+  :type '(repeat symbol)
+  :group 'gnus-agent)
+
+(defcustom gnus-agent-queue-mail t
+  "Whether and when outgoing mail should be queued by the agent.
+When `always', always queue outgoing mail.  When nil, never
+queue.  Otherwise, queue if and only if unplugged."
+  :group 'gnus-agent
+  :type '(radio (const :format "Always" always)
+               (const :format "Never" nil)
+               (const :format "When plugged" t)))
+
+(defcustom gnus-agent-prompt-send-queue nil
+  "If non-nil, `gnus-group-send-queue' will prompt if called when
+unplugged."
+  :group 'gnus-agent
+  :type 'boolean)
+
 ;;; Internal variables
 
 (defvar gnus-agent-history-buffers nil)
 (defvar gnus-agent-buffer-alist nil)
-(defvar gnus-agent-article-alist nil)
+(defvar gnus-agent-article-alist nil
+  "An assoc list identifying the articles whose headers have been fetched.  
+If successfully fetched, these headers will be stored in the group's overview
+file.  The key of each assoc pair is the article ID, the value of each assoc
+pair is a flag indicating whether the identified article has been downloaded
+\(gnus-agent-fetch-articles sets the value to the day of the download).
+NOTES:
+1) The last element of this list can not be expired as some 
+   routines (for example, get-agent-fetch-headers) use the last
+   value to track which articles have had their headers retrieved.
+2) The function `gnus-agent-regenerate' may destructively modify the value.")
 (defvar gnus-agent-group-alist nil)
 (defvar gnus-category-alist nil)
 (defvar gnus-agent-current-history nil)
@@ -156,6 +229,9 @@ If this is `ask' the hook will query the user."
 (defvar gnus-agent-send-mail-function nil)
 (defvar gnus-agent-file-coding-system 'raw-text)
 (defvar gnus-agent-file-loading-cache nil)
+(defvar gnus-agent-total-fetched-hashtb nil)
+(defvar gnus-agent-inhibit-update-total-fetched-for nil)
+(defvar gnus-agent-need-update-total-fetched-for nil)
 
 ;; Dynamic variables
 (defvar gnus-headers)
@@ -186,8 +262,7 @@ If this is `ask' the hook will query the user."
 (gnus-add-shutdown 'gnus-close-agent 'gnus)
 
 (defun gnus-close-agent ()
-  (setq gnus-agent-covered-methods nil
-       gnus-category-predicate-cache nil
+  (setq gnus-category-predicate-cache nil
        gnus-category-group-cache nil
        gnus-agent-spam-hashtb nil)
   (gnus-kill-buffer gnus-agent-overview-buffer))
@@ -196,6 +271,17 @@ If this is `ask' the hook will query the user."
 ;;; Utility functions
 ;;;
 
+(defmacro gnus-agent-with-refreshed-group (group &rest body)
+  "Performs the body then updates the group's line in the group
+buffer.  Automatically blocks multiple updates due to recursion."
+`(prog1 (let ((gnus-agent-inhibit-update-total-fetched-for t)) ,@body)
+     (when (and gnus-agent-need-update-total-fetched-for
+               (not gnus-agent-inhibit-update-total-fetched-for))
+       (save-excursion
+         (set-buffer gnus-group-buffer)
+         (setq gnus-agent-need-update-total-fetched-for nil)
+         (gnus-group-update-group ,group t)))))
+
 (defun gnus-agent-read-file (file)
   "Load FILE and do a `read' there."
   (with-temp-buffer
@@ -211,28 +297,130 @@ If this is `ask' the hook will query the user."
            (cadr gnus-command-method))))
 
 (defsubst gnus-agent-directory ()
-  "Path of the Gnus agent directory."
+  "The name of the Gnus agent directory."
   (nnheader-concat gnus-agent-directory
                   (nnheader-translate-file-chars (gnus-agent-method)) "/"))
 
 (defun gnus-agent-lib-file (file)
-  "The full path of the Gnus agent library FILE."
+  "The full name of the Gnus agent library FILE."
   (expand-file-name file
                    (file-name-as-directory
                     (expand-file-name "agent.lib" (gnus-agent-directory)))))
 
+(defun gnus-agent-cat-set-property (category property value)
+  (if value
+      (setcdr (or (assq property category)
+              (let ((cell (cons property nil)))
+                    (setcdr category (cons cell (cdr category)))
+                    cell)) value)
+    (let ((category category))
+      (while (cond ((eq property (caadr category))
+                    (setcdr category (cddr category))
+                    nil)
+                   (t
+                    (setq category (cdr category)))))))
+  category)
+
+(eval-when-compile
+  (defmacro gnus-agent-cat-defaccessor (name prop-name)
+    "Define accessor and setter methods for manipulating a list of the form
+\(NAME (PROPERTY1 VALUE1) ... (PROPERTY_N VALUE_N)).
+Given the call (gnus-agent-cat-defaccessor func PROPERTY1), the list may be
+manipulated as follows:
+  (func LIST): Returns VALUE1
+  (setf (func LIST) NEW_VALUE1): Replaces VALUE1 with NEW_VALUE1."
+    `(progn (defmacro ,name (category)
+              (list (quote cdr) (list (quote assq)
+                                      (quote (quote ,prop-name)) category)))
+
+            (define-setf-method ,name (category)
+              (let* ((--category--temp-- (make-symbol "--category--"))
+                     (--value--temp-- (make-symbol "--value--")))
+                (list (list --category--temp--) ; temporary-variables
+                      (list category)   ; value-forms
+                      (list --value--temp--) ; store-variables
+                      (let* ((category --category--temp--) ; store-form
+                             (value --value--temp--))
+                        (list (quote gnus-agent-cat-set-property)
+                              category
+                              (quote (quote ,prop-name))
+                              value))
+                      (list (quote ,name) --category--temp--) ; access-form
+                      )))))
+  )
+
+(defmacro gnus-agent-cat-name (category)
+  `(car ,category))
+
+(gnus-agent-cat-defaccessor
+ gnus-agent-cat-days-until-old             agent-days-until-old)
+(gnus-agent-cat-defaccessor
+ gnus-agent-cat-enable-expiration          agent-enable-expiration)
+(gnus-agent-cat-defaccessor
+ gnus-agent-cat-groups                     agent-groups)
+(gnus-agent-cat-defaccessor
+ gnus-agent-cat-high-score                 agent-high-score)
+(gnus-agent-cat-defaccessor
+ gnus-agent-cat-length-when-long           agent-length-when-long)
+(gnus-agent-cat-defaccessor
+ gnus-agent-cat-length-when-short          agent-length-when-short)
+(gnus-agent-cat-defaccessor
+ gnus-agent-cat-low-score                  agent-low-score)
+(gnus-agent-cat-defaccessor
+ gnus-agent-cat-predicate                  agent-predicate)
+(gnus-agent-cat-defaccessor
+ gnus-agent-cat-score-file                 agent-score-file)
+(gnus-agent-cat-defaccessor
+ gnus-agent-cat-enable-undownloaded-faces agent-enable-undownloaded-faces)
+
+(eval-and-compile
+  (defsetf gnus-agent-cat-groups (category) (groups)
+    (list 'gnus-agent-set-cat-groups category groups)))
+
+(defun gnus-agent-set-cat-groups (category groups)
+  (unless (eq groups 'ignore)
+    (let ((new-g groups)
+          (old-g (gnus-agent-cat-groups category)))
+      (cond ((eq new-g old-g)
+             ;; gnus-agent-add-group is fiddling with the group
+             ;; list. Still, Im done.
+             nil
+             )
+            ((eq new-g (cdr old-g))
+             ;; gnus-agent-add-group is fiddling with the group list
+             (setcdr (or (assq 'agent-groups category)
+                         (let ((cell (cons 'agent-groups nil)))
+                           (setcdr category (cons cell (cdr category)))
+                           cell)) new-g))
+            (t
+             (let ((groups groups))
+               (while groups
+                 (let* ((group        (pop groups))
+                        (old-category (gnus-group-category group)))
+                   (if (eq category old-category)
+                       nil
+                     (setf (gnus-agent-cat-groups old-category)
+                           (delete group (gnus-agent-cat-groups
+                                          old-category))))))
+               ;; Purge cache as preceeding loop invalidated it.
+               (setq gnus-category-group-cache nil))
+
+             (setcdr (or (assq 'agent-groups category)
+                         (let ((cell (cons 'agent-groups nil)))
+                           (setcdr category (cons cell (cdr category)))
+                           cell)) groups))))))
+
+(defsubst gnus-agent-cat-make (name &optional default-agent-predicate)
+  (list name `(agent-predicate . ,(or default-agent-predicate 'false))))
+
 ;;; Fetching setup functions.
 
 (defun gnus-agent-start-fetch ()
   "Initialize data structures for efficient fetching."
-  (gnus-agent-open-history)
-  (setq gnus-agent-current-history (gnus-agent-history-buffer))
   (gnus-agent-create-buffer))
 
 (defun gnus-agent-stop-fetch ()
   "Save all data structures and clean up."
-  (gnus-agent-save-history)
-  (gnus-agent-close-history)
   (setq gnus-agent-spam-hashtb nil)
   (save-excursion
     (set-buffer nntp-server-buffer)
@@ -249,6 +437,13 @@ If this is `ask' the hook will query the user."
 (put 'gnus-agent-with-fetch 'lisp-indent-function 0)
 (put 'gnus-agent-with-fetch 'edebug-form-spec '(body))
 
+(defmacro gnus-agent-append-to-list (tail value)
+  `(setq ,tail (setcdr ,tail (cons ,value nil))))
+
+(defmacro gnus-agent-message (level &rest args)
+  `(if (<= ,level gnus-verbose)
+       (message ,@args)))
+
 ;;;
 ;;; Mode infestation
 ;;;
@@ -278,7 +473,13 @@ If this is `ask' the hook will query the user."
                                                     buffer))))
            minor-mode-map-alist))
     (when (eq major-mode 'gnus-group-mode)
-      (gnus-agent-toggle-plugged gnus-plugged))
+      (let ((init-plugged gnus-plugged)
+            (gnus-agent-go-online nil))
+        ;; g-a-t-p does nothing when gnus-plugged isn't changed.
+        ;; Therefore, make certain that the current value does not
+        ;; match the desired initial value.
+        (setq gnus-plugged :unknown)
+        (gnus-agent-toggle-plugged init-plugged)))
     (gnus-run-hooks 'gnus-agent-mode-hook
                    (intern (format "gnus-agent-%s-mode-hook" buffer)))))
 
@@ -302,15 +503,21 @@ If this is `ask' the hook will query the user."
        ["Toggle plugged" gnus-agent-toggle-plugged t]
        ["Toggle group plugged" gnus-agent-toggle-group-plugged t]
        ["List categories" gnus-enter-category-buffer t]
+       ["Add (current) group to category" gnus-agent-add-group t]
+       ["Remove (current) group from category" gnus-agent-remove-group t]
        ["Send queue" gnus-group-send-queue gnus-plugged]
        ("Fetch"
        ["All" gnus-agent-fetch-session gnus-plugged]
-       ["Group" gnus-agent-fetch-group gnus-plugged])))))
+       ["Group" gnus-agent-fetch-group gnus-plugged])
+       ["Synchronize flags" gnus-agent-synch