;;; gnus-agent.el --- unplugged support for Gnus
-;; Copyright (C) 1997, 1998, 1999, 2000, 2001
+;; 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)
(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."
:group 'gnus-agent
:type 'hook)
+(defcustom gnus-agent-fetched-hook nil
+ "Hook run when finished fetching articles."
+ :group 'gnus-agent
+ :type 'hook)
+
(defcustom gnus-agent-handle-level gnus-level-subscribed
"Groups on levels higher than this variable will be ignored by the Agent."
:group 'gnus-agent
:type 'integer)
(defcustom gnus-agent-expire-days 7
- "Read articles older than this will be expired."
+ "Read articles older than this will be expired.
+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.
: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"
(const :tag "Ask" ask))
:group 'gnus-agent)
+(defcustom gnus-agent-go-online 'ask
+ "Indicate if offline servers go online when you plug in.
+If this is `ask' the hook will query the user."
+ :version "21.1"
+ :type '(choice (const :tag "Always" t)
+ (const :tag "Never" nil)
+ (const :tag "Ask" ask))
+ :group 'gnus-agent)
+
+(defcustom gnus-agent-mark-unread-after-downloaded t
+ "Indicate whether to mark articles unread after downloaded."
+ :version "21.1"
+ :type 'boolean
+ :group 'gnus-agent)
+
+(defcustom gnus-agent-download-marks '(download)
+ "Marks for downloading."
+ :version "21.1"
+ :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-agent-covered-methods nil)
(defvar gnus-category-alist nil)
(defvar gnus-agent-current-history nil)
(defvar gnus-agent-overview-buffer nil)
(defvar gnus-agent-file-name nil)
(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)
(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))
;;; 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
(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)
(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
;;;
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)))))
"Jj" gnus-agent-toggle-plugged
"Js" gnus-agent-fetch-session
"JY" gnus-agent-synchronize-flags
- "JS" gnus-group-send-drafts
+ "JS" gnus-group-send-queue
"Ja" gnus-agent-add-group
- "Jr" gnus-agent-remove-group)
+ "Jr" gnus-agent-remove-group
+ "Jo" gnus-agent-toggle-group-plugged)
(defun gnus-agent-group-make-menu-bar ()
(unless (boundp 'gnus-agent-group-menu)
gnus-agent-group-menu gnus-agent-group-mode-map ""
'("Agent"
["Toggle plugged" gnus-agent-toggle-plugged t]
+ ["Toggle group plugged" gnus-agent-toggle-group-plugged t]
["List categories" gnus-enter-category-buffer t]
- ["Send drafts" gnus-group-send-drafts gnus-plugged]
+ ["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-synchronize-flags t]
+ ))))
(defvar gnus-agent-summary-mode-map (make-sparse-keymap))
(gnus-define-keys gnus-agent-summary-mode-map
"Jj" gnus-agent-toggle-plugged
+ "Ju" gnus-agent-summary-fetch-group
+ "JS" gnus-agent-fetch-group
+ "Js" gnus-agent-summary-fetch-series
"J#" gnus-agent-mark-article
"J\M-#" gnus-agent-unmark-article
"@" gnus-agent-toggle-mark
["Mark as downloadable" gnus-agent-mark-article t]
["Unmark as downloadable" gnus-agent-unmark-article t]
["Toggle mark" gnus-agent-toggle-mark t]
+ ["Fetch downloadable" gnus-agent-summary-fetch-group t]
["Catchup undownloaded" gnus-agent-catchup t]))))
(defvar gnus-agent-server-mode-map (make-sparse-keymap))
["Add" gnus-agent-add-server t]
["Remove" gnus-agent-remove-server t]))))
-(defun gnus-agent-toggle-plugged (plugged)
+(defun gnus-agent-make-mode-line-string (string mouse-button mouse-func)
+ (if (and (fboundp 'propertize)
+ (fboundp 'make-mode-line-mouse-map))
+ (propertize string 'local-map
+ (make-mode-line-mouse-map mouse-button mouse-func))
+ string))
+
+(defun gnus-agent-toggle-plugged (set-to)
"Toggle whether Gnus is unplugged or not."
(interactive (list (not gnus-plugged)))
- (if plugged
- (progn
- (setq gnus-plugged plugged)
- (gnus-agent-possibly-synchronize-flags)
- (gnus-run-hooks 'gnus-agent-plugged-hook)
- (setcar (cdr gnus-agent-mode-status) " Plugged"))
- (gnus-agent-close-connections)
- (setq gnus-plugged plugged)
- (gnus-run-hooks 'gnus-agent-unplugged-hook)
- (setcar (cdr gnus-agent-mode-status) " Unplugged"))
+ (cond ((eq set-to gnus-plugged)
+ nil)
+ (set-to
+ (setq gnus-plugged set-to)
+ (gnus-run-hooks 'gnus-agent-plugged-hook)
+ (setcar (cdr gnus-agent-mode-status)
+ (gnus-agent-make-mode-line-string " Plugged"
+ 'mouse-2
+ 'gnus-agent-toggle-plugged))
+ (gnus-agent-go-online gnus-agent-go-online)
+ (gnus-agent-possibly-synchronize-flags))
+ (t
+ (gnus-agent-close-connections)
+ (setq gnus-plugged set-to)
+ (gnus-run-hooks 'gnus-agent-unplugged-hook)
+ (setcar (cdr gnus-agent-mode-status)
+ (gnus-agent-make-mode-line-string " Unplugged"
+ 'mouse-2
+ 'gnus-agent-toggle-plugged))))
(set-buffer-modified-p t))
+(defmacro gnus-agent-while-plugged (&rest body)
+ `(let ((original-gnus-plugged gnus-plugged))
+ (unwind-protect
+ (progn (gnus-agent-toggle-plugged t)
+ ,@body)
+ (gnus-agent-toggle-plugged original-gnus-plugged))))
+
+(put 'gnus-agent-while-plugged 'lisp-indent-function 0)
+(put 'gnus-agent-while-plugged 'edebug-form-spec '(body))
+
(defun gnus-agent-close-connections ()
"Close all methods covered by the Gnus agent."
- (let ((methods gnus-agent-covered-methods))
+ (let ((methods (gnus-agent-covered-methods)))
(while methods
(gnus-close-server (pop methods)))))
(setq gnus-plugged t)
(gnus))
+;;;###autoload
+(defun gnus-slave-unplugged (&optional arg)
+ "Read news as a slave unplugged."
+ (interactive "P")
+ (setq gnus-plugged nil)
+ (gnus arg nil 'slave))
+
;;;###autoload
(defun gnus-agentize ()
"Allow Gnus to be an offline newsreader.
-The normal usage of this command is to put the following as the
-last form in your `.gnus.el' file:
-\(gnus-agentize)
+The gnus-agentize function is now called internally by gnus when
+gnus-agent is set. If you wish to avoid calling gnus-agentize,
+customize gnus-agent to nil.
This will modify the `gnus-setup-news-hook', and
-`message-send-mail-function' variables, and install the Gnus agent
+`message-send-mail-real-function' variables, and install the Gnus agent
minor mode in all Gnus buffers."
(interactive)
(gnus-open-agent)
(add-hook 'gnus-setup-news-hook 'gnus-agent-queue-setup)
(unless gnus-agent-send-mail-function
- (setq gnus-agent-send-mail-function message-send-mail-function
- message-send-mail-function 'gnus-agent-send-mail))
- (unless gnus-agent-covered-methods
- (setq gnus-agent-covered-methods (list gnus-select-method))))
-
-(defun gnus-agent-queue-setup ()
- "Make sure the queue group exists."
- (unless (gnus-gethash "nndraft:queue" gnus-newsrc-hashtb)
- (gnus-request-create-group "queue" '(nndraft ""))
+ (setq gnus-agent-send-mail-function
+ (or message-send-mail-real-function
+ (function (lambda () (funcall message-send-mail-function))))
+ message-send-mail-real-function 'gnus-agent-send-mail))
+
+ ;; If the servers file doesn't exist, auto-agentize some servers and
+ ;; save the servers file so this auto-agentizing isn't invoked
+ ;; again.
+ (unless (file-exists-p (nnheader-concat gnus-agent-directory "lib/servers"))
+ (gnus-message 3 "First time agent user, agentizing remote groups...")
+ (mapc
+ (lambda (server-or-method)
+ (let ((method (gnus-server-to-method server-or-method)))
+ (when (memq (car method)
+ gnus-agent-auto-agentize-methods)
+ (push (gnus-method-to-server method)
+ gnus-agent-covered-methods)
+ (setq gnus-agent-method-p-cache nil))))
+ (cons gnus-select-method gnus-secondary-select-methods))
+ (gnus-agent-write-servers)))
+
+(defun gnus-agent-queue-setup (&optional group-name)
+ "Make sure the queue group exists.
+Optional arg GROUP-NAME allows to specify another group."
+ (unless (gnus-gethash (format "nndraft:%s" (or group-name "queue"))
+ gnus-newsrc-hashtb)
+ (gnus-request-create-group (or group-name "queue") '(nndraft ""))
(let ((gnus-level-default-subscribed 1))
- (gnus-subscribe-group "nndraft:queue" nil '(nndraft "")))
+ (gnus-subscribe-group (format "nndraft:%s" (or group-name "queue"))
+ nil '(nndraft "")))
(gnus-group-set-parameter
- "nndraft:queue" 'gnus-dummy '((gnus-draft-mode)))))
+ (format "nndraft:%s" (or group-name "queue"))
+ 'gnus-dummy '((gnus-draft-mode)))))
(defun gnus-agent-send-mail ()
- (if gnus-plugged
+ (if (or (not gnus-agent-queue-mail)
+ (and gnus-plugged (not (eq gnus-agent-queue-mail 'always))))
(funcall gnus-agent-send-mail-function)
(goto-char (point-min))
(re-search-forward
gcc " ,")))))
covered)
(while (and (not covered) methods)
- (setq covered
- (member (car methods) gnus-agent-covered-methods)
+ (setq covered (gnus-agent-method-p (car methods))
methods (cdr methods)))
covered)))
+;;;###autoload
(defun gnus-agent-possibly-save-gcc ()
"Save GCC if Gnus is unplugged."
(when (and (not gnus-plugged) (gnus-agent-any-covered-gcc))
(error "Groups can't be fetched when Gnus is unplugged"))
(gnus-group-iterate n 'gnus-agent-fetch-group))
-(defun gnus-agent-fetch-group (group)
+(defun gnus-agent-fetch-group (&optional group)
"Put all new articles in GROUP into the Agent."
(interactive (list (gnus-group-group-name)))
- (unless gnus-plugged
- (error "Groups can't be fetched when Gnus is unplugged"))
+ (setq group (or group gnus-newsgroup-name))
(unless group
(error "No group on the current line"))
- (let ((gnus-command-method (gnus-find-method-for-group group)))
- (gnus-agent-with-fetch
- (gnus-agent-fetch-group-1 group gnus-command-method)
- (gnus-message 5 "Fetching %s...done" group))))
+
+ (gnus-agent-while-plugged
+ (let ((gnus-command-method (gnus-find-method-for-group group)))
+ (gnus-agent-with-fetch
+ (gnus-agent-fetch-group-1 group gnus-command-method)
+ (gnus-message 5 "Fetching %s...done" group)))))
(defun gnus-agent-add-group (category arg)
"Add the current group to an agent category."
c groups)
(gnus-group-iterate arg
(lambda (group)
- (when (cadddr (setq c (gnus-group-category group)))
- (setf (cadddr c) (delete group (cadddr c))))
+ (when (gnus-agent-cat-groups (setq c (gnus-group-category group)))
+ (setf (gnus-agent-cat-groups c)
+ (delete group (gnus-agent-cat-groups c))))
(push group groups)))
- (setf (cadddr cat) (nconc (cadddr cat) groups))
+ (setf (gnus-agent-cat-groups cat)
+ (nconc (gnus-agent-cat-groups cat) groups))
(gnus-category-write)))
(defun gnus-agent-remove-group (arg)
(let (c)
(gnus-group-iterate arg
(lambda (group)
- (when (cadddr (setq c (gnus-group-category group)))
- (setf (cadddr c) (delete group (cadddr c))))))
+ (when (gnus-agent-cat-groups (setq c (gnus-group-category group)))
+ (setf (gnus-agent-cat-groups c)
+ (delete group (gnus-agent-cat-groups c))))))
(gnus-category-write)))
(defun gnus-agent-synchronize-flags ()
"Synchronize unplugged flags with servers."
(interactive)
(save-excursion
- (dolist (gnus-command-method gnus-agent-covered-methods)
+ (dolist (gnus-command-method (gnus-agent-covered-methods))
(when (file-exists-p (gnus-agent-lib-file "flags"))
(gnus-agent-synchronize-flags-server gnus-command-method)))))
"Synchronize flags according to `gnus-agent-synchronize-flags'."
(interactive)
(save-excursion
- (dolist (gnus-command-method gnus-agent-covered-methods)
+ (dolist (gnus-command-method (gnus-agent-covered-methods))
(when (file-exists-p (gnus-agent-lib-file "flags"))
(gnus-agent-possibly-synchronize-flags-server gnus-command-method)))))
(erase-buffer)
(nnheader-insert-file-contents (gnus-agent-lib-file "flags"))
(if (null (gnus-check-server gnus-command-method))
- (message "Couldn't open server %s" (nth 1 gnus-command-method))
+ (gnus-message 1 "Couldn't open server %s" (nth 1 gnus-command-method))
(while (not (eobp))
(if (null (eval (read (current-buffer))))
- (progn (forward-line)
- (kill-line -1))
+ (gnus-delete-line)
(write-file (gnus-agent-lib-file "flags"))
(error "Couldn't set flags from file %s"
(gnus-agent-lib-file "flags"))))
(cadr method)))))
(gnus-agent-synchronize-flags-server method)))
+;;;###autoload
+(defun gnus-agent-rename-group (old-group new-group)
+ "Rename fully-qualified OLD-GROUP as NEW-GROUP. Always updates the agent, even when
+disabled, as the old agent files would corrupt gnus when the agent was
+next enabled. Depends upon the caller to determine whether group renaming is supported."
+ (let* ((old-command-method (gnus-find-method-for-group old-group))
+ (old-path (directory-file-name
+ (let (gnus-command-method old-command-method)
+ (gnus-agent-group-pathname old-group))))
+ (new-command-method (gnus-find-method-for-group new-group))
+ (new-path (directory-file-name
+ (let (gnus-command-method new-command-method)
+ (gnus-agent-group-pathname new-group)))))
+ (gnus-rename-file old-path new-path t)
+
+ (let* ((old-real-group (gnus-group-real-name old-group))
+ (new-real-group (gnus-group-real-name new-group))
+ (old-active (gnus-agent-get-group-info old-command-method old-real-group)))
+ (gnus-agent-save-group-info old-command-method old-real-group nil)
+ (gnus-agent-save-group-info new-command-method new-real-group old-active)
+
+ (let ((old-local (gnus-agent-get-local old-group
+ old-real-group old-command-method)))
+ (gnus-agent-set-local old-group
+ nil nil
+ old-real-group old-command-method)
+ (gnus-agent-set-local new-group
+ (car old-local) (cdr old-local)
+ new-real-group new-command-method)))))
+
+;;;###autoload
+(defun gnus-agent-delete-group (group)
+ "Delete fully-qualified GROUP. Always updates the agent, even when
+disabled, as the old agent files would corrupt gnus when the agent was
+next enabled. Depends upon the caller to determine whether group deletion is supported."
+ (let* ((command-method (gnus-find-method-for-group group))
+ (path (directory-file-name
+ (let (gnus-command-method command-method)
+ (gnus-agent-group-pathname group)))))
+ (gnus-delete-file path)
+
+ (let* ((real-group (gnus-group-real-name group)))
+ (gnus-agent-save-group-info command-method real-group nil)
+
+ (let ((local (gnus-agent-get-local group
+ real-group command-method)))
+ (gnus-agent-set-local group
+ nil nil
+ real-group command-method)))))
+
;;;
;;; Server mode commands
;;;
-(defun gnus-agent-add-server (server)
+(defun gnus-agent-add-server ()
"Enroll SERVER in the agent program."
- (interactive (list (gnus-server-server-name)))
- (unless server
- (error "No server on the current line"))
- (let ((method (gnus-server-get-method nil (gnus-server-server-name))))
- (when (member method gnus-agent-covered-methods)
+ (interactive)
+ (let* ((server (gnus-server-server-name))
+ (named-server (gnus-server-named-server))
+ (method (and server
+ (gnus-server-get-method nil server))))
+ (unless server
+ (error "No server on the current line"))
+
+ (when (gnus-agent-method-p method)
(error "Server already in the agent program"))
- (push method gnus-agent-covered-methods)
+
+ (push named-server gnus-agent-covered-methods)
+
+ (setq gnus-agent-method-p-cache nil)
(gnus-server-update-server server)
(gnus-agent-write-servers)
- (message "Entered %s into the Agent" server)))
+ (gnus-message 1 "Entered %s into the Agent" server)))
-(defun gnus-agent-remove-server (server)
+(defun gnus-agent-remove-server ()
"Remove SERVER from the agent program."
- (interactive (list (gnus-server-server-name)))
- (unless server
- (error "No server on the current line"))
- (let ((method (gnus-server-get-method nil (gnus-server-server-name))))
- (unless (member method gnus-agent-covered-methods)
+ (interactive)
+ (let* ((server (gnus-server-server-name))
+ (named-server (gnus-server-named-server)))
+ (unless server
+ (error "No server on the current line"))
+
+ (unless (member named-server gnus-agent-covered-methods)
(error "Server not in the agent program"))
- (setq gnus-agent-covered-methods
- (delete method gnus-agent-covered-methods))
+
+ (setq gnus-agent-covered-methods
+ (delete named-server gnus-agent-covered-methods)
+ gnus-agent-method-p-cache nil)
+
(gnus-server-update-server server)
(gnus-agent-write-servers)
- (message "Removed %s from the agent" server)))
+ (gnus-message 1 "Removed %s from the agent" server)))
(defun gnus-agent-read-servers ()
"Read the alist of covered servers."
+ (setq gnus-agent-covered-methods
+ (gnus-agent-read-file
+ (nnheader-concat gnus-agent-directory "lib/servers"))
+ gnus-agent-method-p-cache nil)
+
+ ;; I am called so early in start-up that I can not validate server
+ ;; names. When that is the case, I skip the validation. That is
+ ;; alright as the gnus startup code calls the validate methods
+ ;; directly.
+ (if gnus-server-alist
+ (gnus-agent-read-servers-validate)))
+
+(defun gnus-agent-read-servers-validate ()
+ (mapcar (lambda (server-or-method)
+ (let* ((server (if (stringp server-or-method)
+ server-or-method
+ (gnus-method-to-server server-or-method)))
+ (method (gnus-server-to-method server)))
+ (if method
+ (unless (member server gnus-agent-covered-methods)
+ (push server gnus-agent-covered-methods)
+ (setq gnus-agent-method-p-cache nil))
+ (gnus-message 1 "Ignoring disappeared server `%s'" server))))
+ (prog1 gnus-agent-covered-methods
+ (setq gnus-agent-covered-methods nil))))
+
+(defun gnus-agent-read-servers-validate-native (native-method)
(setq gnus-agent-covered-methods
- (gnus-agent-read-file
- (nnheader-concat gnus-agent-directory "lib/servers"))))
+ (mapcar (lambda (method)
+ (if (or (not method)
+ (equal method native-method))
+ "native"
+ method)) gnus-agent-covered-methods)))
(defun gnus-agent-write-servers ()
"Write the alist of covered servers."
(let ((coding-system-for-write nnheader-file-coding-system)
(file-name-coding-system nnmail-pathname-coding-system))
(with-temp-file (nnheader-concat gnus-agent-directory "lib/servers")
- (prin1 gnus-agent-covered-methods (current-buffer)))))
+ (prin1 gnus-agent-covered-methods
+ (current-buffer)))))
;;;
;;; Summary commands
(gnus-agent-mark-article n 'toggle))
(defun gnus-summary-set-agent-mark (article &optional unmark)
- "Mark ARTICLE as downloadable."
- (let ((unmark (if (and (not (null unmark)) (not (eq t unmark)))
- (memq article gnus-newsgroup-downloadable)
- unmark)))
- (if unmark
- (progn
- (setq gnus-newsgroup-downloadable
- (delq article gnus-newsgroup-downloadable))
- (push article gnus-newsgroup-undownloaded))
- (setq gnus-newsgroup-undownloaded
- (delq article gnus-newsgroup-undownloaded))
- (push article gnus-newsgroup-downloadable))
- (gnus-summary-update-mark
- (if unmark gnus-undownloaded-mark gnus-downloadable-mark)
- 'unread)))
+ "Mark ARTICLE as downloadable. If UNMARK is nil, article is marked.
+When UNMARK is t, the article is unmarked. For any other value, the
+article's mark is toggled."
+ (let ((unmark (cond ((eq nil unmark)
+ nil)
+ ((eq t unmark)
+ t)
+ (t
+ (memq article gnus-newsgroup-downloadable)))))
+ (when (gnus-summary-goto-subject article nil t)
+ (gnus-summary-update-mark
+ (if unmark
+ (progn
+ (setq gnus-newsgroup-downloadable
+ (delq article gnus-newsgroup-downloadable))
+ (gnus-article-mark article))
+ (setq gnus-newsgroup-downloadable
+ (gnus-add-to-sorted-list gnus-newsgroup-downloadable article))
+ gnus-downloadable-mark)
+ 'unread))))
(defun gnus-agent-get-undownloaded-list ()
- "Mark all unfetched articles as read."
+ "Construct list of articles that have not been downloaded."
(let ((gnus-command-method (gnus-find-method-for-group gnus-newsgroup-name)))
- (when (and (not gnus-plugged)
- (gnus-agent-method-p gnus-command-method))
- (gnus-agent-load-alist gnus-newsgroup-name)
- ;; First mark all undownloaded articles as undownloaded.
- (let ((articles (append gnus-newsgroup-unreads
- gnus-newsgroup-marked
- gnus-newsgroup-dormant))
- article)
- (while (setq article (pop articles))
- (unless (or (cdr (assq article gnus-agent-article-alist))
- (memq article gnus-newsgroup-downloadable)
- (memq article gnus-newsgroup-cached))
- (push article gnus-newsgroup-undownloaded))))
- ;; Then mark downloaded downloadable as not-downloadable,
- ;; if you get my drift.
- (let ((articles gnus-newsgroup-downloadable)
- article)
- (while (setq article (pop articles))
- (when (cdr (assq article gnus-agent-article-alist))
- (setq gnus-newsgroup-downloadable
- (delq article gnus-newsgroup-downloadable))))))))
+ (when (set (make-local-variable 'gnus-newsgroup-agentized)
+ (gnus-agent-method-p gnus-command-method))
+ (let* ((alist (gnus-agent-load-alist gnus-newsgroup-name))
+ (headers (sort (mapcar (lambda (h)
+ (mail-header-number h))
+ gnus-newsgroup-headers) '<))
+ (cached (and gnus-use-cache gnus-newsgroup-cached))
+ (undownloaded (list nil))
+ (tail-undownloaded undownloaded)
+ (unfetched (list nil))
+ (tail-unfetched unfetched))
+ (while (and alist headers)
+ (let ((a (caar alist))
+ (h (car headers)))
+ (cond ((< a h)
+ ;; Ignore IDs in the alist that are not being
+ ;; displayed in the summary.
+ (setq alist (cdr alist)))
+ ((> a h)
+ ;; Headers that are not in the alist should be
+ ;; fictious (see nnagent-retrieve-headers); they
+ ;; imply that this article isn't in the agent.
+ (gnus-agent-append-to-list tail-undownloaded h)
+ (gnus-agent-append-to-list tail-unfetched h)
+ (setq headers (cdr headers)))
+ ((cdar alist)
+ (setq alist (cdr alist))
+ (setq headers (cdr headers))
+ nil ; ignore already downloaded
+ )
+ (t
+ (setq alist (cdr alist))
+ (setq headers (cdr headers))
+
+ ;; This article isn't in the agent. Check to see
+ ;; if it is in the cache. If it is, it's been
+ ;; downloaded.
+ (while (and cached (< (car cached) a))
+ (setq cached (cdr cached)))
+ (unless (equal a (car cached))
+ (gnus-agent-append-to-list tail-undownloaded a))))))
+
+ (while headers
+ (let ((num (pop headers)))
+ (gnus-agent-append-to-list tail-undownloaded num)
+ (gnus-agent-append-to-list tail-unfetched num)))
+
+ (setq gnus-newsgroup-undownloaded (cdr undownloaded)
+ gnus-newsgroup-unfetched (cdr unfetched))))))
(defun gnus-agent-catchup ()
- "Mark all undownloaded articles as read."
+ "Mark as read all unhandled articles.
+An article is unhandled if it is neither cached, nor downloaded, nor
+downloadable."
(interactive)
(save-excursion
- (while gnus-newsgroup-undownloaded
- (gnus-summary-mark-article
- (pop gnus-newsgroup-undownloaded) gnus-catchup-mark)))
- (gnus-summary-position-point))
+ (let ((articles gnus-newsgroup-undownloaded))
+ (when (or gnus-newsgroup-downloadable
+ gnus-newsgroup-cached)
+ (setq articles (gnus-sorted-ndifference
+ (gnus-sorted-ndifference
+ (gnus-copy-sequence articles)
+ gnus-newsgroup-downloadable)
+ gnus-newsgroup-cached)))
+
+ (while articles
+ (gnus-summary-mark-article
+ (pop articles) gnus-catchup-mark)))
+ (gnus-summary-position-point)))
+
+(defun gnus-agent-summary-fetch-series ()
+ (interactive)
+ (when gnus-newsgroup-processable
+ (setq gnus-newsgroup-downloadable
+ (let* ((dl gnus-newsgroup-downloadable)
+ (gnus-newsgroup-downloadable
+ (sort (gnus-copy-sequence gnus-newsgroup-processable) '<))
+ (fetched-articles (gnus-agent-summary-fetch-group)))
+ ;; The preceeding call to (gnus-agent-summary-fetch-group)
+ ;; updated gnus-newsgroup-downloadable to remove each
+ ;; article successfully fetched.
+
+ ;; For each article that I processed, remove its
+ ;; processable mark IF the article is no longer
+ ;; downloadable (i.e. it's already downloaded)
+ (dolist (article gnus-newsgroup-processable)
+ (unless (memq article gnus-newsgroup-downloadable)
+ (gnus-summary-remove-process-mark article)))
+ (gnus-sorted-ndifference dl fetched-articles)))))
+
+(defun gnus-agent-summary-fetch-group (&optional all)
+ "Fetch the downloadable articles in the group.
+Optional arg ALL, if non-nil, means to fetch all articles."
+ (interactive "P")
+ (let ((articles
+ (if all gnus-newsgroup-articles
+ gnus-newsgroup-downloadable))
+ (gnus-command-method (gnus-find-method-for-group gnus-newsgroup-name))
+ fetched-articles)
+ (gnus-agent-while-plugged
+ (unless articles
+ (error "No articles to download"))
+ (gnus-agent-with-fetch
+ (setq gnus-newsgroup-undownloaded
+ (gnus-sorted-ndifference
+ gnus-newsgroup-undownloaded
+ (setq fetched-articles
+ (gnus-agent-fetch-articles
+ gnus-newsgroup-name articles)))))
+ (save-excursion
+ (dolist (article articles)
+ (let ((was-marked-downloadable
+ (memq article gnus-newsgroup-downloadable)))
+ (cond (gnus-agent-mark-unread-after-downloaded
+ (setq gnus-newsgroup-downloadable
+ (delq article gnus-newsgroup-downloadable))
+
+ (gnus-summary-mark-article article gnus-unread-mark))
+ (was-marked-downloadable
+ (gnus-summary-set-agent-mark article t)))
+ (when (gnus-summary-goto-subject article nil t)
+ (gnus-summary-update-download-mark article))))))
+ fetched-articles))
+
+(defun gnus-agent-fetch-selected-article ()
+ "Fetch the current article as it is selected.
+This can be added to `gnus-select-article-hook' or
+`gnus-mark-article-hook'."
+ (let ((gnus-command-method gnus-current-select-method))
+ (when (and gnus-plugged (gnus-agent-method-p gnus-command-method))
+ (when (gnus-agent-fetch-articles
+ gnus-newsgroup-name
+ (list gnus-current-article))
+ (setq gnus-newsgroup-undownloaded
+ (delq gnus-current-article gnus-newsgroup-undownloaded))
+ (gnus-summary-update-download-mark gnus-current-article)))))
;;;
;;; Internal functions
;;;
(defun gnus-agent-save-active (method)
- (gnus-agent-save-active-1 method 'gnus-active-to-gnus-format))
-
-(defun gnus-agent-save-active-1 (method function)
(when (gnus-agent-method-p method)
(let* ((gnus-command-method method)
(new (gnus-make-hashtable (count-lines (point-min) (point-max))))
(file (gnus-agent-lib-file "active")))
- (funcall function nil new)
+ (gnus-active-to-gnus-format nil new)
(gnus-agent-write-active file new)
(erase-buffer)
(nnheader-insert-file-contents file))))
(defun gnus-agent-write-active (file new)
- (let ((orig (gnus-make-hashtable (count-lines (point-min) (point-max))))
- (file (gnus-agent-lib-file "active"))
- elem osym)
- (when (file-exists-p file)
- (with-temp-buffer
- (nnheader-insert-file-contents file)
- (gnus-active-to-gnus-format nil orig))
- (mapatoms
- (lambda (sym)
- (when (and sym (boundp sym))
- (if (and (boundp (setq osym (intern (symbol-name sym) orig)))
- (setq elem (symbol-value osym)))
- (setcdr elem (cdr (symbol-value sym)))
- (set (intern (symbol-name sym) orig) (symbol-value sym)))))
- new))
(gnus-make-directory (file-name-directory file))
- (let ((coding-system-for-write gnus-agent-file-coding-system))
- ;; The hashtable contains real names of groups, no more prefix
- ;; removing, so set `full' to `t'.
- (gnus-write-active-file file orig t))))
-
-(defun gnus-agent-save-groups (method)
- (gnus-agent-save-active-1 method 'gnus-groups-to-gnus-format))
+ (let ((nnmail-active-file-coding-system gnus-agent-file-coding-system))
+ ;; The hashtable contains real names of groups. However, do NOT
+ ;; add the foreign server prefix as gnus-active-to-gnus-format
+ ;; will add it while reading the file.
+ (gnus-write-active-file file new nil)))
+
+(defun gnus-agent-possibly-alter-active (group active &optional info)
+ "Possibly expand a group's active range to include articles
+downloaded into the agent."
+ (let* ((gnus-command-method (or gnus-command-method
+ (gnus-find-method-for-group group))))
+ (when (gnus-agent-method-p gnus-command-method)
+ (let* ((local (gnus-agent-get-local group))
+ (active-min (or (car active) 0))
+ (active-max (or (cdr active) 0))
+ (agent-min (or (car local) active-min))
+ (agent-max (or (cdr local) active-max)))
+
+ (when (< agent-min active-min)
+ (setcar active agent-min))
+
+ (when (> agent-max active-max)
+ (setcdr active agent-max))
+
+ (when (and info (< agent-max (- active-min 100)))
+ ;; I'm expanding the active range by such a large amount
+ ;; that there is a gap of more than 100 articles between the
+ ;; last article known to the agent and the first article
+ ;; currently available on the server. This gap contains
+ ;; articles that have been lost, mark them as read so that
+ ;; gnus doesn't waste resources trying to fetch them.
+
+ ;; NOTE: I don't do this for smaller gaps (< 100) as I don't
+ ;; want to modify the local file everytime someone restarts
+ ;; gnus. The small gap will cause a tiny performance hit
+ ;; when gnus tries, and fails, to retrieve the articles.
+ ;; Still that should be smaller than opening a buffer,
+ ;; printing this list to the buffer, and then writing it to a
+ ;; file.
+
+ (let ((read (gnus-info-read info)))
+ (gnus-info-set-read
+ info
+ (gnus-range-add
+ read
+ (list (cons (1+ agent-max)
+ (1- active-min))))))
+
+ ;; Lie about the agent's local range for this group to
+ ;; disable the set read each time this server is opened.
+ ;; NOTE: Opening this group will restore the valid local
+ ;; range but it will also expand the local range to
+ ;; incompass the new active range.
+ (gnus-agent-set-local group agent-min (1- active-min)))))))
(defun gnus-agent-save-group-info (method group active)
+ "Update a single group's active range in the agent's copy of the server's active file."
(when (gnus-agent-method-p method)
- (let* ((gnus-command-method method)
+ (let* ((gnus-command-method (or method gnus-command-method))
(coding-system-for-write nnheader-file-coding-system)
(file-name-coding-system nnmail-pathname-coding-system)
(file (gnus-agent-lib-file "active"))
- oactive)
+ oactive-min oactive-max)
(gnus-make-directory (file-name-directory file))
(with-temp-file file
;; Emacs got problem to match non-ASCII group in multibyte buffer.
(mm-disable-multibyte)
(when (file-exists-p file)
- (nnheader-insert-file-contents file))
- (goto-char (point-min))
- (when (re-search-forward
- (concat "^" (regexp-quote group) " ") nil t)
- (save-excursion
- (save-restriction
- (narrow-to-region (match-beginning 0)
- (progn
- (forward-line 1)
- (point)))
- (setq oactive (car (nnmail-parse-active)))))
- (gnus-delete-line))
- (insert (format "%S %d %d y\n" (intern group)
- (cdr active)
- (or (car oactive) (car active))))
- (goto-char (point-max))
- (while (search-backward "\\." nil t)
- (delete-char 1))))))
+ (nnheader-insert-file-contents file)
+
+ (goto-char (point-min))
+ (when (re-search-forward
+ (concat "^" (regexp-quote group) " ") nil t)
+ (save-excursion
+ (setq oactive-max (read (current-buffer)) ;; max
+ oactive-min (read (current-buffer)))) ;; min
+ (gnus-delete-line)))
+ (when active
+ (insert (format "%S %d %d y\n" (intern group)
+ (max (or oactive-max (cdr active)) (cdr active))
+ (min (or oactive-min (car active)) (car active))))
+ (goto-char (point-max))
+ (while (search-backward "\\." nil t)
+ (delete-char 1)))))))
+
+(defun gnus-agent-get-group-info (method group)
+ "Get a single group's active range in the agent's copy of the server's active file."
+ (when (gnus-agent-method-p method)
+ (let* ((gnus-command-method (or method gnus-command-method))
+ (coding-system-for-write nnheader-file-coding-system)
+ (file-name-coding-system nnmail-pathname-coding-system)
+ (file (gnus-agent-lib-file "active"))
+ oactive-min oactive-max)
+ (gnus-make-directory (file-name-directory file))
+ (with-temp-buffer
+ ;; Emacs got problem to match non-ASCII group in multibyte buffer.
+ (mm-disable-multibyte)
+ (when (file-exists-p file)
+ (nnheader-insert-file-contents file)
+
+ (goto-char (point-min))
+ (when (re-search-forward
+ (concat "^" (regexp-quote group) " ") nil t)
+ (save-excursion
+ (setq oactive-max (read (current-buffer)) ;; max
+ oactive-min (read (current-buffer))) ;; min
+ (cons oactive-min oactive-max))))))))
(defun gnus-agent-group-path (group)
- "Translate GROUP into a path."
- (if nnmail-use-long-file-names
- (gnus-group-real-name group)
- (nnheader-translate-file-chars
- (nnheader-replace-chars-in-string
- (nnheader-replace-duplicate-chars-in-string
- (nnheader-replace-chars-in-string
- (gnus-group-real-name group)
- ?/ ?_)
- ?. ?_)
- ?. ?/))))
-
-\f
-
-(defun gnus-agent-method-p (method)
- "Say whether METHOD is covered by the agent."
- (member method gnus-agent-covered-methods))
+ "Translate GROUP into a file name."
+
+ ;; NOTE: This is what nnmail-group-pathname does as of Apr 2003.
+ ;; The two methods must be kept synchronized, which is why
+ ;; gnus-agent-group-pathname was added.
+
+ (setq group
+ (nnheader-translate-file-chars
+ (nnheader-replace-duplicate-chars-in-string
+ (nnheader-replace-chars-in-string
+ (gnus-group-real-name group)
+ ?/ ?_)
+ ?. ?_)))
+ (if (or nnmail-use-long-file-names
+ (file-directory-p (expand-file-name group (gnus-agent-directory))))
+ group
+ (mm-encode-coding-string
+ (nnheader-replace-chars-in-string group ?. ?/)
+ nnmail-pathname-coding-system)))
+
+(defun gnus-agent-group-pathname (group)
+ "Translate GROUP into a file name."
+ ;; nnagent uses nnmail-group-pathname to read articles while
+ ;; unplugged. The agent must, therefore, use the same directory
+ ;; while plugged.
+ (let ((gnus-command-method (or gnus-command-method
+ (gnus-find-method-for-group group))))
+ (nnmail-group-pathname (gnus-group-real-name group) (gnus-agent-directory))))
(defun gnus-agent-get-function (method)
- (if (and (not gnus-plugged)
- (gnus-agent-method-p method))
- (progn
- (require 'nnagent)
- 'nnagent)
- (car method)))
+ (if (gnus-online method)
+ (car method)
+ (require 'nnagent)
+ 'nnagent))
+
+(defun gnus-agent-covered-methods ()
+ "Return the subset of methods that are covered by the agent."
+ (delq nil (mapcar #'gnus-server-to-method gnus-agent-covered-methods)))
;;; History functions
(nnheader-insert-file-contents file))
(set (make-local-variable 'gnus-agent-file-name) file))))
-(defun gnus-agent-save-history ()
- (save-excursion
- (set-buffer gnus-agent-current-history)
- (gnus-make-directory (file-name-directory gnus-agent-file-name))
- (let ((coding-system-for-write gnus-agent-file-coding-system))
- (write-region (1+ (point-min)) (point-max)
- gnus-agent-file-name nil 'silent))))
-
(defun gnus-agent-close-history ()
(when (gnus-buffer-live-p gnus-agent-current-history)
(kill-buffer gnus-agent-current-history)
(delq (assoc (gnus-agent-method) gnus-agent-history-buffers)
gnus-agent-history-buffers))))
-(defun gnus-agent-enter-history (id group-arts date)
- (save-excursion
- (set-buffer gnus-agent-current-history)
- (goto-char (point-max))
- (let ((p (point)))
- (insert id "\t" (number-to-string date) "\t")
- (while group-arts
- (insert (format "%S" (intern (caar group-arts)))
- " " (number-to-string (cdr (pop group-arts)))
- " "))
- (insert "\n")
- (while (search-backward "\\." p t)
- (delete-char 1)))))
-
-(defun gnus-agent-article-in-history-p (id)
- (save-excursion
- (set-buffer (gnus-agent-history-buffer))
- (goto-char (point-min))
- (search-forward (concat "\n" id "\t") nil t)))
-
-(defun gnus-agent-history-path (id)
- (save-excursion
- (set-buffer (gnus-agent-history-buffer))
- (goto-char (point-min))
- (when (search-forward (concat "\n" id "\t") nil t)
- (let ((method (gnus-agent-method)))
- (let (paths group)
- (while (not (numberp (setq group (read (current-buffer)))))
- (push (concat method "/" group) paths))
- (nreverse paths))))))
-
;;;
;;; Fetching
;;;
(defun gnus-agent-fetch-articles (group articles)
"Fetch ARTICLES from GROUP and put them into the Agent."
(when articles
- ;; Prune off articles that we have already fetched.
- (while (and articles
- (cdr (assq (car articles) gnus-agent-article-alist)))
- (pop articles))
- (let ((arts articles))
- (while (cdr arts)
- (if (cdr (assq (cadr arts) gnus-agent-article-alist))
- (setcdr arts (cddr arts))
- (setq arts (cdr arts)))))
- (when articles
- (let ((dir (concat
- (gnus-agent-directory)
- (gnus-agent-group-path group) "/"))
- (date (time-to-days (current-time)))
- (case-fold-search t)
- pos crosses id elem)
- (gnus-make-directory dir)
- (gnus-message 7 "Fetching articles for %s..." group)
- ;; Fetch the articles from the backend.
- (if (gnus-check-backend-function 'retrieve-articles group)
- (setq pos (gnus-retrieve-articles articles group))
- (with-temp-buffer
- (let (article)
- (while (setq article (pop articles))
- (when (or
- (gnus-backlog-request-article group article
- nntp-server-buffer)
- (gnus-request-article article group))
- (goto-char (point-max))
- (push (cons article (point)) pos)
- (insert-buffer-substring nntp-server-buffer)))
- (copy-to-buffer nntp-server-buffer (point-min) (point-max))
- (setq pos (nreverse pos)))))
- ;; Then save these articles into the Agent.
- (save-excursion
- (set-buffer nntp-server-buffer)
- (while pos
- (narrow-to-region (cdar pos) (or (cdadr pos) (point-max)))
- (goto-char (point-min))
- (when (search-forward "\n\n" nil t)
- (when (search-backward "\nXrefs: " nil t)
- ;; Handle crossposting.
- (skip-chars-forward "^ ")
- (skip-chars-forward " ")
- (setq crosses nil)
- (while (looking-at "\\([^: \n]+\\):\\([0-9]+\\) +")
- (push (cons (buffer-substring (match-beginning 1)
- (match-end 1))
- (buffer-substring (match-beginning 2)
- (match-end 2)))
- crosses)
- (goto-char (match-end 0)))
- (gnus-agent-crosspost crosses (caar pos))))
- (goto-char (point-min))
- (if (not (re-search-forward "^Message-ID: *<\\([^>\n]+\\)>" nil t))
- (setq id "No-Message-ID-in-article")
- (setq id (buffer-substring (match-beginning 1) (match-end 1))))
- (let ((coding-system-for-write
- gnus-agent-file-coding-system))
- (write-region (point-min) (point-max)
- (concat dir (number-to-string (caar pos)))
- nil 'silent))
- (when (setq elem (assq (caar pos) gnus-agent-article-alist))
- (setcdr elem t))
- (gnus-agent-enter-history
- id (or crosses (list (cons group (caar pos)))) date)
- (widen)
- (pop pos)))
- (gnus-agent-save-alist group)))))
-
-(defun gnus-agent-crosspost (crosses article)
+ (gnus-agent-load-alist group)
+ (let* ((alist gnus-agent-article-alist)
+ (headers (if (< (length articles) 2) nil gnus-newsgroup-headers))
+ (selected-sets (list nil))
+ (current-set-size 0)
+ article
+ header-number)
+ ;; Check each article
+ (while (setq article (pop articles))
+ ;; Skip alist entries preceeding this article
+ (while (> article (or (caar alist) (1+ article)))
+ (setq alist (cdr alist)))
+
+ ;; Prune off articles that we have already fetched.
+ (unless (and (eq article (caar alist))
+ (cdar alist))
+ ;; Skip headers preceeding this article
+ (while (> article
+ (setq header-number
+ (let* ((header (car headers)))
+ (if header
+ (mail-header-number header)
+ (1+ article)))))
+ (setq headers (cdr headers)))
+
+ ;; Add this article to the current set
+ (setcar selected-sets (cons article (car selected-sets)))
+
+ ;; Update the set size, when the set is too large start a
+ ;; new one. I do this after adding the article as I want at
+ ;; least one article in each set.
+ (when (< gnus-agent-max-fetch-size
+ (setq current-set-size
+ (+ current-set-size
+ (if (= header-number article)
+ (let ((char-size (mail-header-chars
+ (car headers))))
+ (if (<= char-size 0)
+ ;; The char size was missing/invalid,
+ ;; assume a worst-case situation of
+ ;; 65 char/line. If the line count
+ ;; is missing, arbitrarily assume a
+ ;; size of 1000 characters.
+ (max (* 65 (mail-header-lines
+ (car headers)))
+ 1000)
+ char-size))
+ 0))))
+ (setcar selected-sets (nreverse (car selected-sets)))
+ (setq selected-sets (cons nil selected-sets)
+ current-set-size 0))))
+
+ (when (or (cdr selected-sets) (car selected-sets))
+ (let* ((fetched-articles (list nil))
+ (tail-fetched-articles fetched-articles)
+ (dir (gnus-agent-group-pathname group))
+ (date (time-to-days (current-time)))
+ (case-fold-search t)
+ pos crosses id)
+
+ (setcar selected-sets (nreverse (car selected-sets)))
+ (setq selected-sets (nreverse selected-sets))
+
+ (gnus-make-directory dir)
+ (gnus-message 7 "Fetching articles for %s..." group)
+
+ (unwind-protect
+ (while (setq articles (pop selected-sets))
+ ;; Fetch the articles from the backend.
+ (if (gnus-check-backend-function 'retrieve-articles group)
+ (setq pos (gnus-retrieve-articles articles group))
+ (with-temp-buffer
+ (let (article)
+ (while (setq article (pop articles))
+ (gnus-message 10 "Fetching article %s for %s..."
+ article group)
+ (when (or
+ (gnus-backlog-request-article group article
+ nntp-server-buffer)
+ (gnus-request-article article group))
+ (goto-char (point-max))
+ (push (cons article (point)) pos)
+ (insert-buffer-substring nntp-server-buffer)))
+ (copy-to-buffer
+ nntp-server-buffer (point-min) (point-max))
+ (setq pos (nreverse pos)))))
+ ;; Then save these articles into the Agent.
+ (save-excursion
+ (set-buffer nntp-server-buffer)
+ (while pos
+ (narrow-to-region (cdar pos) (or (cdadr pos) (point-max)))
+ (goto-char (point-min))
+ (unless (eobp) ;; Don't save empty articles.
+ (when (search-forward "\n\n" nil t)
+ (when (search-backward "\nXrefs: " nil t)
+ ;; Handle cross posting.
+ (goto-char (match-end 0)) ; move to end of header name
+ (skip-chars-forward "^ ") ; skip server name
+ (skip-chars-forward " ")
+ (setq crosses nil)
+ (while (looking-at "\\([^: \n]+\\):\\([0-9]+\\) *")
+ (push (cons (buffer-substring (match-beginning 1)
+ (match-end 1))
+ (string-to-int
+ (buffer-substring (match-beginning 2)
+ (match-end 2))))
+ crosses)
+ (goto-char (match-end 0)))
+ (gnus-agent-crosspost crosses (caar pos) date)))
+ (goto-char (point-min))
+ (if (not (re-search-forward
+ "^Message-ID: *<\\([^>\n]+\\)>" nil t))
+ (setq id "No-Message-ID-in-article")
+ (setq id (buffer-substring
+ (match-beginning 1) (match-end 1))))
+ (let ((coding-system-for-write
+ gnus-agent-file-coding-system))
+ (write-region (point-min) (point-max)
+ (concat dir (number-to-string (caar pos)))
+ nil 'silent))
+
+ (gnus-agent-append-to-list
+ tail-fetched-articles (caar pos)))
+ (widen)
+ (setq pos (cdr pos)))))
+
+ (gnus-agent-save-alist group (cdr fetched-articles) date)
+ (gnus-agent-update-files-total-fetched-for group (cdr fetched-articles))
+
+ (gnus-message 7 ""))
+ (cdr fetched-articles))))))
+
+(defun gnus-agent-crosspost (crosses article &optional date)
+ (setq date (or date t))
+
(let (gnus-agent-article-alist group alist beg end)
(save-excursion
(set-buffer gnus-agent-overview-buffer)
(unless (setq alist (assoc group gnus-agent-group-alist))
(push (setq alist (list group (gnus-agent-load-alist (caar crosses))))
gnus-agent-group-alist))
- (setcdr alist (cons (cons (cdar crosses) t) (cdr alist)))
+ (setcdr alist (cons (cons (cdar crosses) date) (cdr alist)))
(save-excursion
(set-buffer (gnus-get-buffer-create (format " *Gnus agent overview %s*"
group)))
(gnus-agent-article-name ".overview" group))))
(nnheader-find-nov-line (string-to-number (cdar crosses)))
(insert (string-to-number (cdar crosses)))
- (insert-buffer-substring gnus-agent-overview-buffer beg end))
- (pop crosses))))
+ (insert-buffer-substring gnus-agent-overview-buffer beg end)
+ (gnus-agent-check-overview-buffer))
+ (setq crosses (cdr crosses)))))
+
+(defun gnus-agent-backup-overview-buffer ()
+ (when gnus-newsgroup-name
+ (let ((root (gnus-agent-article-name ".overview" gnus-newsgroup-name))
+ (cnt 0)
+ name)
+ (while (file-exists-p
+ (setq name (concat root "~"
+ (int-to-string (setq cnt (1+ cnt))) "~"))))
+ (write-region (point-min) (point-max) name nil 'no-msg)
+ (gnus-message 1 "Created backup copy of overview in %s." name)))
+ t)
+
+(defun gnus-agent-check-overview-buffer (&optional buffer)
+ "Check the overview file given for sanity.
+In particular, checks that the file is sorted by article number
+and that there are no duplicates."
+ (let ((prev-num -1)
+ (backed-up nil))
+ (save-excursion
+ (when buffer
+ (set-buffer buffer))
+ (save-restriction
+ (widen)
+ (goto-char (point-min))
+
+ (while (< (point) (point-max))
+ (let ((p (point))
+ (cur (condition-case nil
+ (read (current-buffer))
+ (error nil))))
+ (cond
+ ((or (not (integerp cur))
+ (not (eq (char-after) ?\t)))
+ (or backed-up
+ (setq backed-up (gnus-agent-backup-overview-buffer)))
+ (gnus-message 1
+ "Overview buffer contains garbage '%s'."
+ (buffer-substring
+ p (point-at-eol))))
+ ((= cur prev-num)
+ (or backed-up
+ (setq backed-up (gnus-agent-backup-overview-buffer)))
+ (gnus-message 1
+ "Duplicate overview line for %d" cur)
+ (delete-region (point) (progn (forward-line 1) (point))))
+ ((< cur prev-num)
+ (or backed-up
+ (setq backed-up (gnus-agent-backup-overview-buffer)))
+ (gnus-message 1 "Overview buffer not sorted!")
+ (sort-numeric-fields 1 (point-min) (point-max))
+ (goto-char (point-min))
+ (setq prev-num -1))
+ (t
+ (setq prev-num cur)))
+ (forward-line 1)))))))
(defun gnus-agent-flush-cache ()
(save-excursion
(gnus-agent-article-name ".overview"
(caar gnus-agent-buffer-alist))
nil 'silent))
- (pop gnus-agent-buffer-alist))
+ (setq gnus-agent-buffer-alist (cdr gnus-agent-buffer-alist)))
(while gnus-agent-group-alist
- (with-temp-file (caar gnus-agent-group-alist)
+ (with-temp-file (gnus-agent-article-name
+ ".agentview" (caar gnus-agent-group-alist))
(princ (cdar gnus-agent-group-alist))
+ (insert "\n")
+ (princ 1 (current-buffer))
(insert "\n"))
- (pop gnus-agent-group-alist))))
+ (setq gnus-agent-group-alist (cdr gnus-agent-group-alist)))))
+
+(defun gnus-agent-find-parameter (group symbol)
+ "Search for GROUPs SYMBOL in the group's parameters, the group's
+topic parameters, the group's category, or the customizable
+variables. Returns the first non-nil value found."
+ (or (gnus-group-find-parameter group symbol t)
+ (gnus-group-parameter-value (cdr (gnus-group-category group)) symbol t)
+ (symbol-value
+ (cdr
+ (assq symbol
+ '((agent-short-article . gnus-agent-short-article)
+ (agent-long-article . gnus-agent-long-article)
+ (agent-low-score . gnus-agent-low-score)
+ (agent-high-score . gnus-agent-high-score)
+ (agent-days-until-old . gnus-agent-expire-days)
+ (agent-enable-expiration
+ . gnus-agent-enable-expiration)
+ (agent-predicate . gnus-agent-predicate)))))))
(defun gnus-agent-fetch-headers (group &optional force)
- (let ((articles (gnus-list-of-unread-articles group))
- (gnus-decode-encoded-word-function 'identity)
- (file (gnus-agent-article-name ".overview" group)))
- ;; Add article with marks to list of article headers we want to fetch.
- (dolist (arts (gnus-info-marks (gnus-get-info group)))
- (setq articles (gnus-union (gnus-uncompress-sequence (cdr arts))
- articles)))
- (setq articles (sort articles '<))
- ;; Remove known articles.
- (when (gnus-agent-load-alist group)
- (setq articles (gnus-sorted-intersection
- articles
- (gnus-uncompress-range
- (cons (1+ (caar (last gnus-agent-article-alist)))
- (cdr (gnus-active group)))))))
- ;; Fetch them.
- (gnus-make-directory (nnheader-translate-file-chars
- (file-name-directory file) t))
- (when articles
- (gnus-message 7 "Fetching headers for %s..." group)
+ "Fetch interesting headers into the agent. The group's overview
+file will be updated to include the headers while a list of available
+article numbers will be returned."
+ (let* ((fetch-all (and gnus-agent-consider-all-articles
+ ;; Do not fetch all headers if the predicate
+ ;; implies that we only consider unread articles.
+ (not (gnus-predicate-implies-unread
+ (gnus-agent-find-parameter group
+ 'agent-predicate)))))
+ (articles (if fetch-all
+ (gnus-uncompress-range (gnus-active group))
+ (gnus-list-of-unread-articles group)))
+ (gnus-decode-encoded-word-function 'identity)
+ (file (gnus-agent-article-name ".overview" group)))
+
+ (unless fetch-all
+ ;; Add articles with marks to the list of article headers we want to
+ ;; fetch. Don't fetch articles solely on the basis of a recent or seen
+ ;; mark, but do fetch recent or seen articles if they have other, more
+ ;; interesting marks. (We have to fetch articles with boring marks
+ ;; because otherwise the agent will remove their marks.)
+ (dolist (arts (gnus-info-marks (gnus-get-info group)))
+ (unless (memq (car arts) '(seen recent killed cache))
+ (setq articles (gnus-range-add articles (cdr arts)))))
+ (setq articles (sort (gnus-uncompress-sequence articles) '<)))
+
+ ;; At this point, I have the list of articles to consider for
+ ;; fetching. This is the list that I'll return to my caller. Some
+ ;; of these articles may have already been fetched. That's OK as
+ ;; the fetch article code will filter those out. Internally, I'll
+ ;; filter this list to just those articles whose headers need to
+ ;; be fetched.
+ (let ((articles articles))
+ ;; Remove known articles.
+ (when (and (or gnus-agent-cache
+ (not gnus-plugged))
+ (gnus-agent-load-alist group))
+ ;; Remove articles marked as downloaded.
+ (if fetch-all
+ ;; I want to fetch all headers in the active range.
+ ;; Therefore, exclude only those headers that are in the
+ ;; article alist.
+ ;; NOTE: This is probably NOT what I want to do after
+ ;; agent expiration in this group.
+ (setq articles (gnus-agent-uncached-articles articles group))
+
+ ;; I want to only fetch those headers that have never been
+ ;; fetched. Therefore, exclude all headers that are, or
+ ;; WERE, in the article alist.
+ (let ((low (1+ (caar (last gnus-agent-article-alist))))
+ (high (cdr (gnus-active group))))
+ ;; Low can be greater than High when the same group is
+ ;; fetched twice in the same session {The first fetch will
+ ;; fill the article alist such that (last
+ ;; gnus-agent-article-alist) equals (cdr (gnus-active
+ ;; group))}. The addition of one(the 1+ above) then
+ ;; forces Low to be greater than High. When this happens,
+ ;; gnus-list-range-intersection returns nil which
+ ;; indicates that no headers need to be fetched. -- Kevin
+ (setq articles (gnus-list-range-intersection
+ articles (list (cons low high)))))))
+
+ (gnus-message
+ 10 "gnus-agent-fetch-headers: undownloaded articles are '%s'"
+ (gnus-compress-sequence articles t))
+
(save-excursion
- (set-buffer nntp-server-buffer)
- (unless (eq 'nov (gnus-retrieve-headers articles group))
- (nnvirtual-convert-headers))
- ;; Save these headers for later processing.
- (copy-to-buffer gnus-agent-overview-buffer (point-min) (point-max))
- (when (file-exists-p file)
- (gnus-agent-braid-nov group articles file))
- (let ((coding-system-for-write
- gnus-agent-file-coding-system))
- (write-region (point-min) (point-max) file nil 'silent))
- (gnus-agent-save-alist group articles nil)
- (gnus-agent-enter-history
- "last-header-fetched-for-session"
- (list (cons group (nth (- (length articles) 1) articles)))
- (time-to-days (current-time)))
- articles))))
+ (set-buffer nntp-server-buffer)
+
+ (if articles
+ (progn
+ (gnus-message 7 "Fetching headers for %s..." group)
+
+ ;; Fetch them.
+ (gnus-make-directory (nnheader-translate-file-chars
+ (file-name-directory file) t))
+
+ (unless (eq 'nov (gnus-retrieve-headers articles group))
+ (nnvirtual-convert-headers))
+ (gnus-agent-check-overview-buffer)
+ ;; Move these headers to the overview buffer so that
+ ;; gnus-agent-braid-nov can merge them with the contents
+ ;; of FILE.
+ (copy-to-buffer
+ gnus-agent-overview-buffer (point-min) (point-max))
+ (when (file-exists-p file)
+ (gnus-agent-braid-nov group articles file))
+ (let ((coding-system-for-write
+ gnus-agent-file-coding-system))
+ (gnus-agent-check-overview-buffer)
+ (write-region (point-min) (point-max) file nil 'silent))
+ (gnus-agent-update-view-total-fetched-for group t)
+ (gnus-agent-save-alist group articles nil)
+ articles)
+ (ignore-errors
+ (erase-buffer)
+ (nnheader-insert-file-contents file)))))
+ articles))
(defsubst gnus-agent-copy-nov-line (article)
- (let (b e)
+ (let (art b e)
(set-buffer gnus-agent-overview-buffer)
- (setq b (point))
- (if (eq article (read (current-buffer)))
- (setq e (progn (forward-line 1) (point)))
- (progn
- (beginning-of-line)
- (setq e b)))
- (set-buffer nntp-server-buffer)
- (insert-buffer-substring gnus-agent-overview-buffer b e)))
+ (while (and (not (eobp))
+ (< (setq art (read (current-buffer))) article))
+ (forward-line 1))
+ (beginning-of-line)
+ (if (or (eobp)
+ (not (eq article art)))
+ (set-buffer nntp-server-buffer)
+ (setq b (point))
+ (setq e (progn (forward-line 1) (point)))
+ (set-buffer nntp-server-buffer)
+ (insert-buffer-substring gnus-agent-overview-buffer b e))))
(defun gnus-agent-braid-nov (group articles file)
- (set-buffer gnus-agent-overview-buffer)
- (goto-char (point-min))
- (set-buffer nntp-server-buffer)
- (erase-buffer)
- (nnheader-insert-file-contents file)
- (goto-char (point-max))
- (if (or (= (point-min) (point-max))
- (progn
- (forward-line -1)
- (< (read (current-buffer)) (car articles))))
- ;; We have only headers that are after the older headers,
- ;; so we just append them.
- (progn
- (goto-char (point-max))
- (insert-buffer-substring gnus-agent-overview-buffer))
- ;; We do it the hard way.
- (nnheader-find-nov-line (car articles))
- (gnus-agent-copy-nov-line (car articles))
- (pop articles)
- (while (and articles
- (not (eobp)))
- (while (and (not (eobp))
- (< (read (current-buffer)) (car articles)))
- (forward-line 1))
- (beginning-of-line)
- (unless (eobp)
- (gnus-agent-copy-nov-line (car articles))
- (setq articles (cdr articles))))
+ "Merge agent overview data with given file.
+Takes headers for ARTICLES from `gnus-agent-overview-buffer' and the given
+FILE and places the combined headers into `nntp-server-buffer'."
+ (let (start last)
+ (set-buffer gnus-agent-overview-buffer)
+ (goto-char (point-min))
+ (set-buffer nntp-server-buffer)
+ (erase-buffer)
+ (nnheader-insert-file-contents file)
+ (goto-char (point-max))
+ (forward-line -1)
+ (unless (looking-at "[0-9]+\t")
+ ;; Remove corrupted lines
+ (gnus-message
+ 1 "Overview %s is corrupted. Removing corrupted lines..." file)
+ (goto-char (point-min))
+ (while (not (eobp))
+ (if (looking-at "[0-9]+\t")
+ (forward-line 1)
+ (delete-region (point) (progn (forward-line 1) (point)))))
+ (forward-line -1))
+ (unless (or (= (point-min) (point-max))
+ (< (setq last (read (current-buffer))) (car articles)))
+ ;; We do it the hard way.
+ (when (nnheader-find-nov-line (car articles))
+ ;; Replacing existing NOV entry
+ (delete-region (point) (progn (forward-line 1) (point))))
+ (gnus-agent-copy-nov-line (pop articles))
+
+ (ignore-errors
+ (while articles
+ (while (let ((art (read (current-buffer))))
+ (cond ((< art (car articles))
+ (forward-line 1)
+ t)
+ ((= art (car articles))
+ (beginning-of-line)
+ (delete-region
+ (point) (progn (forward-line 1) (point)))
+ nil)
+ (t
+ (beginning-of-line)
+ nil))))
+
+ (gnus-agent-copy-nov-line (pop articles)))))
+
+ ;; Copy the rest lines
+ (set-buffer nntp-server-buffer)
+ (goto-char (point-max))
(when articles
- (let (b e)
+ (when last
(set-buffer gnus-agent-overview-buffer)
- (setq b (point)
- e (point-max))
- (set-buffer nntp-server-buffer)
- (insert-buffer-substring gnus-agent-overview-buffer b e)))))
+ (ignore-errors
+ (while (<= (read (current-buffer)) last)
+ (forward-line 1)))
+ (beginning-of-line)
+ (setq start (point))
+ (set-buffer nntp-server-buffer))
+ (insert-buffer-substring gnus-agent-overview-buffer start))))
-(defun gnus-agent-load-alist (group &optional dir)
- "Load the article-state alist for GROUP."
- (setq gnus-agent-article-alist
- (gnus-agent-read-file
- (if dir
- (expand-file-name ".agentview" dir)
- (gnus-agent-article-name ".agentview" group)))))
+;; Keeps the compiler from warning about the free variable in
+;; gnus-agent-read-agentview.
+(eval-when-compile
+ (defvar gnus-agent-read-agentview))
-(defun gnus-agent-save-alist (group &optional articles state dir)
+(defun gnus-agent-load-alist (group)
+ "Load the article-state alist for GROUP."
+ ;; Bind free variable that's used in `gnus-agent-read-agentview'.
+ (let ((gnus-agent-read-agentview group))
+ (setq gnus-agent-article-alist
+ (gnus-cache-file-contents
+ (gnus-agent-article-name ".agentview" group)
+ 'gnus-agent-file-loading-cache
+ 'gnus-agent-read-agentview))))
+
+;; Save format may be either 1 or 2. Two is the new, compressed
+;; format that is still being tested. Format 1 is uncompressed but
+;; known to be reliable.
+(defconst gnus-agent-article-alist-save-format 2)
+
+(defun gnus-agent-read-agentview (file)
+ "Load FILE and do a `read' there."
+ (with-temp-buffer
+ (condition-case nil
+ (progn
+ (nnheader-insert-file-contents file)
+ (goto-char (point-min))
+ (let ((alist (read (current-buffer)))
+ (version (condition-case nil (read (current-buffer))
+ (end-of-file 0)))
+ changed-version)
+
+ (cond
+ ((< version 2)
+ (error "gnus-agent-read-agentview no longer supports version %d. Stop gnus, manually evaluate gnus-agent-convert-to-compressed-agentview, then restart gnus." version))
+ ((= version 0)
+ (let ((inhibit-quit t)
+ entry)
+ (gnus-agent-open-history)
+ (set-buffer (gnus-agent-history-buffer))
+ (goto-char (point-min))
+ (while (not (eobp))
+ (if (and (looking-at
+ "[^\t\n]+\t\\([0-9]+\\)\t\\([^ \n]+\\) \\([0-9]+\\)")
+ (string= (match-string 2)
+ gnus-agent-read-agentview)
+ (setq entry (assoc (string-to-number (match-string 3)) alist)))
+ (setcdr entry (string-to-number (match-string 1))))
+ (forward-line 1))
+ (gnus-agent-close-history)
+ (setq changed-version t)))
+ ((= version 1)
+ (setq changed-version (not (= 1 gnus-agent-article-alist-save-format))))
+ ((= version 2)
+ (let (uncomp)
+ (mapcar
+ (lambda (comp-list)
+ (let ((state (car comp-list))
+ (sequence (inline
+ (gnus-uncompress-range
+ (cdr comp-list)))))
+ (mapcar (lambda (article-id)
+ (setq uncomp (cons (cons article-id state) uncomp)))
+ sequence)))
+ alist)
+ (setq alist (sort uncomp 'car-less-than-car)))))
+ (when changed-version
+ (let ((gnus-agent-article-alist alist))
+ (gnus-agent-save-alist gnus-agent-read-agentview)))
+ alist))
+ (file-error nil))))
+
+(defun gnus-agent-save-alist (group &optional articles state)
"Save the article-state alist for GROUP."
- (let ((file-name-coding-system nnmail-pathname-coding-system))
- (with-temp-file (if dir
- (expand-file-name ".agentview" dir)
- (gnus-agent-article-name ".agentview" group))
- (princ (setq gnus-agent-article-alist
- (nconc gnus-agent-article-alist
- (mapcar (lambda (article) (cons article state))
- articles)))
- (current-buffer))
- (insert "\n"))))
+ (let* ((file-name-coding-system nnmail-pathname-coding-system)
+ (prev (cons nil gnus-agent-article-alist))
+ (all prev)
+ print-level print-length item article)
+ (while (setq article (pop articles))
+ (while (and (cdr prev)
+ (< (caadr prev) article))
+ (setq prev (cdr prev)))
+ (cond
+ ((not (cdr prev))
+ (setcdr prev (list (cons article state))))
+ ((> (caadr prev) article)
+ (setcdr prev (cons (cons article state) (cdr prev))))
+ ((= (caadr prev) article)
+ (setcdr (cadr prev) state)))
+ (setq prev (cdr prev)))
+ (setq gnus-agent-article-alist (cdr all))
+
+ (gnus-agent-set-local group
+ (caar gnus-agent-article-alist)
+ (caar (last gnus-agent-article-alist)))
+
+ (gnus-make-directory (gnus-agent-article-name "" group))
+ (with-temp-file (gnus-agent-article-name ".agentview" group)
+ (cond ((eq gnus-agent-article-alist-save-format 1)
+ (princ gnus-agent-article-alist (current-buffer)))
+ ((eq gnus-agent-article-alist-save-format 2)
+ (let ((compressed nil))
+ (mapcar (lambda (pair)
+ (let* ((article-id (car pair))
+ (day-of-download (cdr pair))
+ (comp-list (assq day-of-download compressed)))
+ (if comp-list
+ (setcdr comp-list
+ (cons article-id (cdr comp-list)))
+ (setq compressed
+ (cons (list day-of-download article-id)
+ compressed)))
+ nil)) gnus-agent-article-alist)
+ (mapcar (lambda (comp-list)
+ (setcdr comp-list
+ (gnus-compress-sequence
+ (nreverse (cdr comp-list)))))
+ compressed)
+ (princ compressed (current-buffer)))))
+ (insert "\n")
+ (princ gnus-agent-article-alist-save-format (current-buffer))
+ (insert "\n"))
+
+ (gnus-agent-update-view-total-fetched-for group nil)))
+
+(defvar gnus-agent-article-local nil)
+(defvar gnus-agent-file-loading-local nil)
+
+(defun gnus-agent-load-local (&optional method)
+ "Load the METHOD'S local file. The local file contains min/max
+article counts for each of the method's subscribed groups."
+ (let ((gnus-command-method (or method gnus-command-method)))
+ (setq gnus-agent-article-local
+ (gnus-cache-file-contents
+ (gnus-agent-lib-file "local")
+ 'gnus-agent-file-loading-local
+ 'gnus-agent-read-and-cache-local))))
+
+(defun gnus-agent-read-and-cache-local (file)
+ "Load and read FILE then bind its contents to
+gnus-agent-article-local. If that variable had `dirty' (also known as
+modified) original contents, they are first saved to their own file."
+
+ (if (and gnus-agent-article-local
+ (symbol-value (intern "+dirty" gnus-agent-article-local)))
+ (gnus-agent-save-local))
+ (gnus-agent-read-local file))
+
+(defun gnus-agent-read-local (file)
+ "Load FILE and do a `read' there."
+ (let ((my-obarray (gnus-make-hashtable (count-lines (point-min)
+ (point-max))))
+ (line 1))
+ (with-temp-buffer
+ (condition-case nil
+ (let ((nnheader-file-coding-system gnus-agent-file-coding-system))
+ (nnheader-insert-file-contents file))
+ (file-error))
+
+ (goto-char (point-min))
+ ;; Skip any comments at the beginning of the file (the only place where they may appear)
+ (while (= (following-char) ?\;)
+ (forward-line 1)
+ (setq line (1+ line)))
+
+ (while (not (eobp))
+ (condition-case err
+ (let (group
+ min
+ max
+ (cur (current-buffer)))
+ (setq group (read cur)
+ min (read cur)
+ max (read cur))
+
+ (when (stringp group)
+ (setq group (intern group my-obarray)))
+
+ ;; NOTE: The '+ 0' ensure that min and max are both numerics.
+ (set group (cons (+ 0 min) (+ 0 max))))
+ (error
+ (gnus-message 3 "Warning - invalid agent local: %s on line %d: "
+ file line (error-message-string err))))
+ (forward-line 1)
+ (setq line (1+ line))))
+
+ (set (intern "+dirty" my-obarray) nil)
+ (set (intern "+method" my-obarray) gnus-command-method)
+ my-obarray))
+
+(defun gnus-agent-save-local (&optional force)
+ "Save gnus-agent-article-local under it method's agent.lib directory."
+ (let ((my-obarray gnus-agent-article-local))
+ (when (and my-obarray
+ (or force (symbol-value (intern "+dirty" my-obarray))))
+ (let* ((gnus-command-method (symbol-value (intern "+method" my-obarray)))
+ ;; NOTE: gnus-command-method is used within gnus-agent-lib-file.
+ (dest (gnus-agent-lib-file "local")))
+ (gnus-make-directory (gnus-agent-lib-file ""))
+
+ (let ((buffer-file-coding-system gnus-agent-file-coding-system))
+ (with-temp-file dest
+ (let ((gnus-command-method (symbol-value (intern "+method" my-obarray)))
+ (file-name-coding-system nnmail-pathname-coding-system)
+ print-level print-length item article
+ (standard-output (current-buffer)))
+ (mapatoms (lambda (symbol)
+ (cond ((not (boundp symbol))
+ nil)
+ ((member (symbol-name symbol) '("+dirty" "+method"))
+ nil)
+ (t
+ (prin1 symbol)
+ (let ((range (symbol-value symbol)))
+ (princ " ")
+ (princ (car range))
+ (princ " ")
+ (princ (cdr range))
+ (princ "\n")))))
+ my-obarray))))))))
+
+(defun gnus-agent-get-local (group &optional gmane method)
+ (let* ((gmane (or gmane (gnus-group-real-name group)))
+ (gnus-command-method (or method (gnus-find-method-for-group group)))
+ (local (gnus-agent-load-local))
+ (symb (intern gmane local))
+ (minmax (and (boundp symb) (symbol-value symb))))
+ (unless minmax
+ ;; Bind these so that gnus-agent-load-alist doesn't change the
+ ;; current alist (i.e. gnus-agent-article-alist)
+ (let* ((gnus-agent-article-alist gnus-agent-article-alist)
+ (gnus-agent-file-loading-cache gnus-agent-file-loading-cache)
+ (alist (gnus-agent-load-alist group)))
+ (when alist
+ (setq minmax
+ (cons (caar alist)
+ (caar (last alist))))
+ (gnus-agent-set-local group (car minmax) (cdr minmax)
+ gmane gnus-command-method local))))
+ minmax))
+
+(defun gnus-agent-set-local (group min max &optional gmane method local)
+ (let* ((gmane (or gmane (gnus-group-real-name group)))
+ (gnus-command-method (or method (gnus-find-method-for-group group)))
+ (local (or local (gnus-agent-load-local)))
+ (symb (intern gmane local))
+ (minmax (and (boundp symb) (symbol-value symb))))
+
+ (if (cond ((and minmax
+ (or (not (eq min (car minmax)))
+ (not (eq max (cdr minmax)))))
+ (setcar minmax min)
+ (setcdr minmax max)
+ t)
+ (minmax
+ nil)
+ ((and min max)
+ (set symb (cons min max))
+ t)
+ (t
+ (unintern symb local)))
+ (set (intern "+dirty" local) t))))
(defun gnus-agent-article-name (article group)
- (expand-file-name (if (stringp article) article (string-to-number article))
+ (expand-file-name article
(file-name-as-directory
- (expand-file-name (gnus-agent-group-path group)
- (gnus-agent-directory)))))
+ (gnus-agent-group-pathname group))))
(defun gnus-agent-batch-confirmation (msg)
"Show error message and return t."
(error "No servers are covered by the Gnus agent"))
(unless gnus-plugged
(error "Can't fetch articles while Gnus is unplugged"))
- (let ((methods gnus-agent-covered-methods)
+ (let ((methods (gnus-agent-covered-methods))
groups group gnus-command-method)
(save-excursion
(while methods
- (condition-case err
- (progn
- (setq gnus-command-method (car methods))
- (when (or (gnus-server-opened gnus-command-method)
- (gnus-open-server gnus-command-method))
- (setq groups (gnus-groups-from-server (car methods)))
- (gnus-agent-with-fetch
- (while (setq group (pop groups))
- (when (<= (gnus-group-level group) gnus-agent-handle-level)
- (gnus-agent-fetch-group-1 group gnus-command-method))))))
- (error
- (unless (funcall gnus-agent-confirmation-function
- (format "Error (%s). Continue? " err))
- (error "Cannot fetch articles into the Gnus agent.")))
- (quit
- (unless (funcall gnus-agent-confirmation-function
- (format "Quit fetching session (%s). Continue? "
- err))
- (signal 'quit "Cannot fetch articles into the Gnus agent."))))
- (pop methods))
+ (setq gnus-command-method (car methods))
+ (when (and (or (gnus-server-opened gnus-command-method)
+ (gnus-open-server gnus-command-method))
+ (gnus-online gnus-command-method))
+ (setq groups (gnus-groups-from-server (car methods)))
+ (gnus-agent-with-fetch
+ (while (setq group (pop groups))
+ (when (<= (gnus-group-level group)
+ gnus-agent-handle-level)
+ (if (or debug-on-error debug-on-quit)
+ (gnus-agent-fetch-group-1
+ group gnus-command-method)
+ (condition-case err
+ (gnus-agent-fetch-group-1
+ group gnus-command-method)
+ (error
+ (unless (funcall gnus-agent-confirmation-function
+ (format "Error %s while fetching session. Should gnus continue? "
+ (error-message-string err)))
+ (error "Cannot fetch articles into the Gnus agent")))
+ (quit
+ (gnus-agent-regenerate-group group)
+ (unless (funcall gnus-agent-confirmation-function
+ (format
+ "%s while fetching session. Should gnus continue? "
+ (error-message-string err)))
+ (signal 'quit
+ "Cannot fetch articles into the Gnus agent")))))))))
+ (setq methods (cdr methods)))
+ (gnus-run-hooks 'gnus-agent-fetched-hook)
(gnus-message 6 "Finished fetching articles into the Gnus agent"))))
(defun gnus-agent-fetch-group-1 (group method)
"Fetch GROUP."
(let ((gnus-command-method method)
(gnus-newsgroup-name group)
- gnus-newsgroup-dependencies gnus-newsgroup-headers
- gnus-newsgroup-scored gnus-headers gnus-score
- gnus-use-cache articles arts
- category predicate info marks score-param
+ (gnus-newsgroup-dependencies gnus-newsgroup-dependencies)
+ (gnus-newsgroup-headers gnus-newsgroup-headers)
+ (gnus-newsgroup-scored gnus-newsgroup-scored)
+ (gnus-use-cache gnus-use-cache)
(gnus-summary-expunge-below gnus-summary-expunge-below)
(gnus-summary-mark-below gnus-summary-mark-below)
(gnus-orphan-score gnus-orphan-score)
;; Maybe some other gnus-summary local variables should also
;; be put here.
+
+ gnus-headers
+ gnus-score
+ articles arts
+ category predicate info marks score-param
)
(unless (gnus-check-group group)
(error "Can't open server for %s" group))
+
;; Fetch headers.
- (when (and (or (gnus-active group) (gnus-activate-group group))
- (setq articles (gnus-agent-fetch-headers group))
- (let ((nntp-server-buffer gnus-agent-overview-buffer))
- ;; Parse them and see which articles we want to fetch.
- (setq gnus-newsgroup-dependencies
- (make-vector (length articles) 0))
- (setq gnus-newsgroup-headers
- (gnus-get-newsgroup-headers-xover articles nil nil
- group))
- ;; `gnus-agent-overview-buffer' may be killed for
- ;; timeout reason. If so, recreate it.
- (gnus-agent-create-buffer)))
- (setq category (gnus-group-category group))
- (setq predicate
- (gnus-get-predicate
- (or (gnus-group-find-parameter group 'agent-predicate t)
- (cadr category))))
- (if (memq (caaddr predicate) '(gnus-agent-true gnus-agent-false))
- ;; Simple implementation
- (setq arts
- (and (eq (caaddr predicate) 'gnus-agent-true) articles))
- (setq arts nil)
- (setq score-param
- (or (gnus-group-get-parameter group 'agent-score t)
- (caddr category)))
- ;; Translate score-param into real one
- (cond
- ((not score-param))
- ((eq score-param 'file)
- (setq score-param (gnus-all-score-files group)))
- ((stringp (car score-param)))
- (t
- (setq score-param (list (list score-param)))))
- (when score-param
- (gnus-score-headers score-param))
- (while (setq gnus-headers (pop gnus-newsgroup-headers))
- (setq gnus-score
- (or (cdr (assq (mail-header-number gnus-headers)
- gnus-newsgroup-scored))
- gnus-summary-default-score))
- (when (funcall predicate)
- (push (mail-header-number gnus-headers)
- arts))))
- ;; Fetch the articles.
- (when arts
- (gnus-agent-fetch-articles group arts)))
- ;; Perhaps we have some additional articles to fetch.
- (setq arts (assq 'download (gnus-info-marks
- (setq info (gnus-get-info group)))))
- (when (cdr arts)
- (gnus-agent-fetch-articles
- group (gnus-uncompress-range (cdr arts)))
- (setq marks (delq arts (gnus-info-marks info)))
- (gnus-info-set-marks info marks)
- (gnus-dribble-enter
- (concat "(gnus-group-set-info '"
- (gnus-prin1-to-string info)
- ")")))))
+ (when (or gnus-newsgroup-active
+ (gnus-active group)
+ (gnus-activate-group group))
+ (let ((marked-articles gnus-newsgroup-downloadable))
+ ;; Identify the articles marked for download
+ (unless gnus-newsgroup-active
+ ;; The variable gnus-newsgroup-active was selected as I need
+ ;; a gnus-summary local variable that is NOT bound to any
+ ;; value (its global value should default to nil).
+ (dolist (mark gnus-agent-download-marks)
+ (let ((arts (cdr (assq mark (gnus-info-marks
+ (setq info (gnus-get-info group)))))))
+ (when arts
+ (setq marked-articles (nconc (gnus-uncompress-range arts)
+ marked-articles))
+ ))))
+ (setq marked-articles (sort marked-articles '<))
+
+ ;; Fetch any new articles from the server
+ (setq articles (gnus-agent-fetch-headers group))
+
+ ;; Merge new articles with marked
+ (setq articles (sort (append marked-articles articles) '<))
+
+ (when articles
+ ;; Parse them and see which articles we want to fetch.
+ (setq gnus-newsgroup-dependencies
+ (or gnus-newsgroup-dependencies
+ (make-vector (length articles) 0)))
+ (setq gnus-newsgroup-headers
+ (or gnus-newsgroup-headers
+ (gnus-get-newsgroup-headers-xover articles nil nil
+ group)))
+ ;; `gnus-agent-overview-buffer' may be killed for
+ ;; timeout reason. If so, recreate it.
+ (gnus-agent-create-buffer)
+
+ ;; Figure out how to select articles in this group
+ (setq category (gnus-group-category group))
+
+ (setq predicate
+ (gnus-get-predicate
+ (gnus-agent-find-parameter group 'agent-predicate)))
+
+ ;; If the selection predicate requires scoring, score each header
+ (unless (memq predicate '(gnus-agent-true gnus-agent-false))
+ (let ((score-param
+ (gnus-agent-find-parameter group 'agent-score-file)))
+ ;; Translate score-param into real one
+ (cond
+ ((not score-param))
+ ((eq score-param 'file)
+ (setq score-param (gnus-all-score-files group)))
+ ((stringp (car score-param)))
+ (t
+ (setq score-param (list (list score-param)))))
+ (when score-param
+ (gnus-score-headers score-param))))
+
+ (unless (and (eq predicate 'gnus-agent-false)
+ (not marked-articles))
+ (let ((arts (list nil)))
+ (let ((arts-tail arts)
+ (alist (gnus-agent-load-alist group))
+ (marked-articles marked-articles)
+ (gnus-newsgroup-headers gnus-newsgroup-headers))
+ (while (setq gnus-headers (pop gnus-newsgroup-headers))
+ (let ((num (mail-header-number gnus-headers)))
+ ;; Determine if this article is already in the cache
+ (while (and alist
+ (> num (caar alist)))
+ (setq alist (cdr alist)))
+
+ (unless (and (eq num (caar alist))
+ (cdar alist))
+
+ ;; Determine if this article was marked for download.
+ (while (and marked-articles
+ (> num (car marked-articles)))
+ (setq marked-articles
+ (cdr marked-articles)))
+
+ ;; When this article is marked, or selected by the
+ ;; predicate, add it to the download list
+ (when (or (eq num (car marked-articles))
+ (let ((gnus-score
+ (or (cdr
+ (assq num gnus-newsgroup-scored))
+ gnus-summary-default-score))
+ (gnus-agent-long-article
+ (gnus-agent-find-parameter
+ group 'agent-long-article))
+ (gnus-agent-short-article
+ (gnus-agent-find-parameter
+ group 'agent-short-article))
+ (gnus-agent-low-score
+ (gnus-agent-find-parameter
+ group 'agent-low-score))
+ (gnus-agent-high-score
+ (gnus-agent-find-parameter
+ group 'agent-high-score))
+ (gnus-agent-expire-days
+ (gnus-agent-find-parameter
+ group 'agent-days-until-old)))
+ (funcall predicate)))
+ (gnus-agent-append-to-list arts-tail num))))))
+
+ (let (fetched-articles)
+ ;; Fetch all selected articles
+ (setq gnus-newsgroup-undownloaded
+ (gnus-sorted-ndifference
+ gnus-newsgroup-undownloaded
+ (setq fetched-articles
+ (if (cdr arts)
+ (gnus-agent-fetch-articles group (cdr arts))
+ nil))))
+
+ (let ((unfetched-articles
+ (gnus-sorted-ndifference (cdr arts) fetched-articles)))
+ (if gnus-newsgroup-active
+ ;; Update the summary buffer
+ (progn
+ (dolist (article marked-articles)
+ (gnus-summary-set-agent-mark article t))
+ (dolist (article fetched-articles)
+ (if gnus-agent-mark-unread-after-downloaded
+ (gnus-summary-mark-article
+ article gnus-unread-mark))
+ (when (gnus-summary-goto-subject article nil t)
+ (gnus-summary-update-download-mark article)))
+ (dolist (article unfetched-articles)
+ (gnus-summary-mark-article
+ article gnus-canceled-mark)))
+
+ ;; Update the group buffer.
+
+ ;; When some, or all, of the marked articles came
+ ;; from the download mark. Remove that mark. I
+ ;; didn't do this earlier as I only want to remove
+ ;; the marks after the fetch is completed.
+
+ (dolist (mark gnus-agent-download-marks)
+ (when (eq mark 'download)
+ (let ((marked-arts
+ (assq mark (gnus-info-marks
+ (setq info (gnus-get-info group))))))
+ (when (cdr marked-arts)
+ (setq marks
+ (delq marked-arts (gnus-info-marks info)))
+ (gnus-info-set-marks info marks)))))
+ (let ((read (gnus-info-read
+ (or info (setq info (gnus-get-info group))))))
+ (gnus-info-set-read
+ info (gnus-add-to-range read unfetched-articles)))
+
+ (gnus-group-update-group group t)
+ (sit-for 0)
+
+ (gnus-dribble-enter
+ (concat "(gnus-group-set-info '"
+ (gnus-prin1-to-string info)
+ ")"))))))))))))
;;;
;;; Agent Category Mode
"Hook run in `gnus-category-mode' buffers.")
(defvar gnus-category-line-format " %(%20c%): %g\n"
- "Format of category lines.")
+ "Format of category lines.
+
+Valid specifiers include:
+%c Topic name (string)
+%g The number of groups in the topic (integer)
+
+General format specifiers can also be used. See Info node
+`(gnus)Formatting Variables'.")
(defvar gnus-category-mode-line-format "Gnus: %%b"
"The format specification for the category mode line.")
+(defvar gnus-agent-predicate 'false
+ "The selection predicate used when no other source is available.")
+
(defvar gnus-agent-short-article 100
"Articles that have fewer lines than this are short.")
"k" gnus-category-kill
"c" gnus-category-copy
"a" gnus-category-add
+ "e" gnus-agent-customize-category
"p" gnus-category-edit-predicate
"g" gnus-category-edit-groups
"s" gnus-category-edit-score
["Add" gnus-category-add t]
["Kill" gnus-category-kill t]
["Copy" gnus-category-copy t]
+ ["Edit cate