* uudecode.el (uudecode-use-external): Add :version.
[gnus] / lisp / gnus-agent.el
1 ;;; gnus-agent.el --- unplugged support for Gnus
2 ;; Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004
3 ;;        Free Software Foundation, Inc.
4
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6 ;; This file is part of GNU Emacs.
7
8 ;; GNU Emacs is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation; either version 2, or (at your option)
11 ;; any later version.
12
13 ;; GNU Emacs is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 ;; GNU General Public License for more details.
17
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
20 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
21 ;; Boston, MA 02111-1307, USA.
22
23 ;;; Commentary:
24
25 ;;; Code:
26
27 (require 'gnus)
28 (require 'gnus-cache)
29 (require 'nnmail)
30 (require 'nnvirtual)
31 (require 'gnus-sum)
32 (require 'gnus-score)
33 (require 'gnus-srvr)
34 (require 'gnus-util)
35 (eval-when-compile
36   (if (featurep 'xemacs)
37       (require 'itimer)
38     (require 'timer))
39   (require 'cl))
40
41 (eval-and-compile
42   (autoload 'gnus-server-update-server "gnus-srvr")
43   (autoload 'gnus-agent-customize-category "gnus-cus")
44 )
45
46 (defcustom gnus-agent-directory (nnheader-concat gnus-directory "agent/")
47   "Where the Gnus agent will store its files."
48   :group 'gnus-agent
49   :type 'directory)
50
51 (defcustom gnus-agent-plugged-hook nil
52   "Hook run when plugging into the network."
53   :group 'gnus-agent
54   :type 'hook)
55
56 (defcustom gnus-agent-unplugged-hook nil
57   "Hook run when unplugging from the network."
58   :group 'gnus-agent
59   :type 'hook)
60
61 (defcustom gnus-agent-fetched-hook nil
62   "Hook run when finished fetching articles."
63   :version "21.4"
64   :group 'gnus-agent
65   :type 'hook)
66
67 (defcustom gnus-agent-handle-level gnus-level-subscribed
68   "Groups on levels higher than this variable will be ignored by the Agent."
69   :group 'gnus-agent
70   :type 'integer)
71
72 (defcustom gnus-agent-expire-days 7
73   "Read articles older than this will be expired.
74 If you wish to disable Agent expiring, see `gnus-agent-enable-expiration'."
75   :group 'gnus-agent
76   :type '(number :tag "days"))
77
78 (defcustom gnus-agent-expire-all nil
79   "If non-nil, also expire unread, ticked and dormant articles.
80 If nil, only read articles will be expired."
81   :group 'gnus-agent
82   :type 'boolean)
83
84 (defcustom gnus-agent-group-mode-hook nil
85   "Hook run in Agent group minor modes."
86   :group 'gnus-agent
87   :type 'hook)
88
89 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
90 (when (featurep 'xemacs)
91   (add-hook 'gnus-agent-group-mode-hook 'gnus-xmas-agent-group-menu-add))
92
93 (defcustom gnus-agent-summary-mode-hook nil
94   "Hook run in Agent summary minor modes."
95   :group 'gnus-agent
96   :type 'hook)
97
98 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
99 (when (featurep 'xemacs)
100   (add-hook 'gnus-agent-summary-mode-hook 'gnus-xmas-agent-summary-menu-add))
101
102 (defcustom gnus-agent-server-mode-hook nil
103   "Hook run in Agent summary minor modes."
104   :group 'gnus-agent
105   :type 'hook)
106
107 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
108 (when (featurep 'xemacs)
109   (add-hook 'gnus-agent-server-mode-hook 'gnus-xmas-agent-server-menu-add))
110
111 (defcustom gnus-agent-confirmation-function 'y-or-n-p
112   "Function to confirm when error happens."
113   :version "21.1"
114   :group 'gnus-agent
115   :type 'function)
116
117 (defcustom gnus-agent-synchronize-flags 'ask
118   "Indicate if flags are synchronized when you plug in.
119 If this is `ask' the hook will query the user."
120   :version "21.1"
121   :type '(choice (const :tag "Always" t)
122                  (const :tag "Never" nil)
123                  (const :tag "Ask" ask))
124   :group 'gnus-agent)
125
126 (defcustom gnus-agent-go-online 'ask
127   "Indicate if offline servers go online when you plug in.
128 If this is `ask' the hook will query the user."
129   :version "21.3"
130   :type '(choice (const :tag "Always" t)
131                  (const :tag "Never" nil)
132                  (const :tag "Ask" ask))
133   :group 'gnus-agent)
134
135 (defcustom gnus-agent-mark-unread-after-downloaded t
136   "Indicate whether to mark articles unread after downloaded."
137   :version "21.1"
138   :type 'boolean
139   :group 'gnus-agent)
140
141 (defcustom gnus-agent-download-marks '(download)
142   "Marks for downloading."
143   :version "21.1"
144   :type '(repeat (symbol :tag "Mark"))
145   :group 'gnus-agent)
146
147 (defcustom gnus-agent-consider-all-articles nil
148   "When non-nil, the agent will let the agent predicate decide
149 whether articles need to be downloaded or not, for all articles.  When
150 nil, the default, the agent will only let the predicate decide
151 whether unread articles are downloaded or not.  If you enable this,
152 groups with large active ranges may open slower and you may also want
153 to look into the agent expiry settings to block the expiration of
154 read articles as they would just be downloaded again."
155   :version "21.4"
156   :type 'boolean
157   :group 'gnus-agent)
158
159 (defcustom gnus-agent-max-fetch-size 10000000 ;; 10 Mb
160   "Chunk size for `gnus-agent-fetch-session'.
161 The function will split its article fetches into chunks smaller than
162 this limit."
163   :group 'gnus-agent
164   :type 'integer)
165
166 (defcustom gnus-agent-enable-expiration 'ENABLE
167   "The default expiration state for each group.
168 When set to ENABLE, the default, `gnus-agent-expire' will expire old
169 contents from a group's local storage.  This value may be overridden
170 to disable expiration in specific categories, topics, and groups.  Of
171 course, you could change gnus-agent-enable-expiration to DISABLE then
172 enable expiration per categories, topics, and groups."
173   :group 'gnus-agent
174   :type '(radio (const :format "Enable " ENABLE)
175                 (const :format "Disable " DISABLE)))
176
177 (defcustom gnus-agent-expire-unagentized-dirs t
178   "*Whether expiration should expire in unagentized directories.
179 Have gnus-agent-expire scan the directories under
180 \(gnus-agent-directory) for groups that are no longer agentized.
181 When found, offer to remove them."
182   :version "21.4"
183   :type 'boolean
184   :group 'gnus-agent)
185
186 (defcustom gnus-agent-auto-agentize-methods '(nntp nnimap)
187   "Initially, all servers from these methods are agentized.
188 The user may remove or add servers using the Server buffer.
189 See Info node `(gnus)Server Buffer'."
190   :version "21.4"
191   :type '(repeat symbol)
192   :group 'gnus-agent)
193
194 (defcustom gnus-agent-queue-mail t
195   "Whether and when outgoing mail should be queued by the agent.
196 When `always', always queue outgoing mail.  When nil, never
197 queue.  Otherwise, queue if and only if unplugged."
198   :group 'gnus-agent
199   :type '(radio (const :format "Always" always)
200                 (const :format "Never" nil)
201                 (const :format "When plugged" t)))
202
203 (defcustom gnus-agent-prompt-send-queue nil
204   "If non-nil, `gnus-group-send-queue' will prompt if called when
205 unplugged."
206   :group 'gnus-agent
207   :type 'boolean)
208
209 ;;; Internal variables
210
211 (defvar gnus-agent-history-buffers nil)
212 (defvar gnus-agent-buffer-alist nil)
213 (defvar gnus-agent-article-alist nil
214   "An assoc list identifying the articles whose headers have been fetched.  
215 If successfully fetched, these headers will be stored in the group's overview
216 file.  The key of each assoc pair is the article ID, the value of each assoc
217 pair is a flag indicating whether the identified article has been downloaded
218 \(gnus-agent-fetch-articles sets the value to the day of the download).
219 NOTES:
220 1) The last element of this list can not be expired as some 
221    routines (for example, get-agent-fetch-headers) use the last
222    value to track which articles have had their headers retrieved.
223 2) The function `gnus-agent-regenerate' may destructively modify the value.")
224 (defvar gnus-agent-group-alist nil)
225 (defvar gnus-category-alist nil)
226 (defvar gnus-agent-current-history nil)
227 (defvar gnus-agent-overview-buffer nil)
228 (defvar gnus-category-predicate-cache nil)
229 (defvar gnus-category-group-cache nil)
230 (defvar gnus-agent-spam-hashtb nil)
231 (defvar gnus-agent-file-name nil)
232 (defvar gnus-agent-send-mail-function nil)
233 (defvar gnus-agent-file-coding-system 'raw-text)
234 (defvar gnus-agent-file-loading-cache nil)
235 (defvar gnus-agent-total-fetched-hashtb nil)
236 (defvar gnus-agent-inhibit-update-total-fetched-for nil)
237 (defvar gnus-agent-need-update-total-fetched-for nil)
238
239 ;; Dynamic variables
240 (defvar gnus-headers)
241 (defvar gnus-score)
242
243 ;;;
244 ;;; Setup
245 ;;;
246
247 (defun gnus-open-agent ()
248   (setq gnus-agent t)
249   (gnus-agent-read-servers)
250   (gnus-category-read)
251   (gnus-agent-create-buffer)
252   (add-hook 'gnus-group-mode-hook 'gnus-agent-mode)
253   (add-hook 'gnus-summary-mode-hook 'gnus-agent-mode)
254   (add-hook 'gnus-server-mode-hook 'gnus-agent-mode))
255
256 (defun gnus-agent-create-buffer ()
257   (if (gnus-buffer-live-p gnus-agent-overview-buffer)
258       t
259     (setq gnus-agent-overview-buffer
260           (gnus-get-buffer-create " *Gnus agent overview*"))
261     (with-current-buffer gnus-agent-overview-buffer
262       (mm-enable-multibyte))
263     nil))
264
265 (gnus-add-shutdown 'gnus-close-agent 'gnus)
266
267 (defun gnus-close-agent ()
268   (setq gnus-category-predicate-cache nil
269         gnus-category-group-cache nil
270         gnus-agent-spam-hashtb nil)
271   (gnus-kill-buffer gnus-agent-overview-buffer))
272
273 ;;;
274 ;;; Utility functions
275 ;;;
276
277 (defmacro gnus-agent-with-refreshed-group (group &rest body)
278   "Performs the body then updates the group's line in the group
279 buffer.  Automatically blocks multiple updates due to recursion."
280 `(prog1 (let ((gnus-agent-inhibit-update-total-fetched-for t)) ,@body)
281      (when (and gnus-agent-need-update-total-fetched-for
282                 (not gnus-agent-inhibit-update-total-fetched-for))
283         (save-excursion
284           (set-buffer gnus-group-buffer)
285           (setq gnus-agent-need-update-total-fetched-for nil)
286           (gnus-group-update-group ,group t)))))
287
288 (defun gnus-agent-read-file (file)
289   "Load FILE and do a `read' there."
290   (with-temp-buffer
291     (ignore-errors
292       (nnheader-insert-file-contents file)
293       (goto-char (point-min))
294       (read (current-buffer)))))
295
296 (defsubst gnus-agent-method ()
297   (concat (symbol-name (car gnus-command-method)) "/"
298           (if (equal (cadr gnus-command-method) "")
299               "unnamed"
300             (cadr gnus-command-method))))
301
302 (defsubst gnus-agent-directory ()
303   "The name of the Gnus agent directory."
304   (nnheader-concat gnus-agent-directory
305                    (nnheader-translate-file-chars (gnus-agent-method)) "/"))
306
307 (defun gnus-agent-lib-file (file)
308   "The full name of the Gnus agent library FILE."
309   (expand-file-name file
310                     (file-name-as-directory
311                      (expand-file-name "agent.lib" (gnus-agent-directory)))))
312
313 (defun gnus-agent-cat-set-property (category property value)
314   (if value
315       (setcdr (or (assq property category)
316               (let ((cell (cons property nil)))
317                     (setcdr category (cons cell (cdr category)))
318                     cell)) value)
319     (let ((category category))
320       (while (cond ((eq property (caadr category))
321                     (setcdr category (cddr category))
322                     nil)
323                    (t
324                     (setq category (cdr category)))))))
325   category)
326
327 (eval-when-compile
328   (defmacro gnus-agent-cat-defaccessor (name prop-name)
329     "Define accessor and setter methods for manipulating a list of the form
330 \(NAME (PROPERTY1 VALUE1) ... (PROPERTY_N VALUE_N)).
331 Given the call (gnus-agent-cat-defaccessor func PROPERTY1), the list may be
332 manipulated as follows:
333   (func LIST): Returns VALUE1
334   (setf (func LIST) NEW_VALUE1): Replaces VALUE1 with NEW_VALUE1."
335     `(progn (defmacro ,name (category)
336               (list (quote cdr) (list (quote assq)
337                                       (quote (quote ,prop-name)) category)))
338
339             (define-setf-method ,name (category)
340               (let* ((--category--temp-- (make-symbol "--category--"))
341                      (--value--temp-- (make-symbol "--value--")))
342                 (list (list --category--temp--) ; temporary-variables
343                       (list category)   ; value-forms
344                       (list --value--temp--) ; store-variables
345                       (let* ((category --category--temp--) ; store-form
346                              (value --value--temp--))
347                         (list (quote gnus-agent-cat-set-property)
348                               category
349                               (quote (quote ,prop-name))
350                               value))
351                       (list (quote ,name) --category--temp--) ; access-form
352                       )))))
353   )
354
355 (defmacro gnus-agent-cat-name (category)
356   `(car ,category))
357
358 (gnus-agent-cat-defaccessor
359  gnus-agent-cat-days-until-old             agent-days-until-old)
360 (gnus-agent-cat-defaccessor
361  gnus-agent-cat-enable-expiration          agent-enable-expiration)
362 (gnus-agent-cat-defaccessor
363  gnus-agent-cat-groups                     agent-groups)
364 (gnus-agent-cat-defaccessor
365  gnus-agent-cat-high-score                 agent-high-score)
366 (gnus-agent-cat-defaccessor
367  gnus-agent-cat-length-when-long           agent-length-when-long)
368 (gnus-agent-cat-defaccessor
369  gnus-agent-cat-length-when-short          agent-length-when-short)
370 (gnus-agent-cat-defaccessor
371  gnus-agent-cat-low-score                  agent-low-score)
372 (gnus-agent-cat-defaccessor
373  gnus-agent-cat-predicate                  agent-predicate)
374 (gnus-agent-cat-defaccessor
375  gnus-agent-cat-score-file                 agent-score-file)
376 (gnus-agent-cat-defaccessor
377  gnus-agent-cat-enable-undownloaded-faces agent-enable-undownloaded-faces)
378
379
380 ;; This form is equivalent to defsetf except that it calls make-symbol
381 ;; whereas defsetf calls gensym (Using gensym creates a run-time
382 ;; dependency on the CL library).
383
384 (eval-and-compile
385   (define-setf-method gnus-agent-cat-groups (category)
386     (let* ((--category--temp-- (make-symbol "--category--"))
387            (--groups--temp-- (make-symbol "--groups--")))
388       (list (list --category--temp--)
389             (list category)
390             (list --groups--temp--)
391             (let* ((category --category--temp--)
392                    (groups --groups--temp--))
393               (list (quote gnus-agent-set-cat-groups) category groups))
394             (list (quote gnus-agent-cat-groups) --category--temp--))))
395   )
396
397 (defun gnus-agent-set-cat-groups (category groups)
398   (unless (eq groups 'ignore)
399     (let ((new-g groups)
400           (old-g (gnus-agent-cat-groups category)))
401       (cond ((eq new-g old-g)
402              ;; gnus-agent-add-group is fiddling with the group
403              ;; list. Still, Im done.
404              nil
405              )
406             ((eq new-g (cdr old-g))
407              ;; gnus-agent-add-group is fiddling with the group list
408              (setcdr (or (assq 'agent-groups category)
409                          (let ((cell (cons 'agent-groups nil)))
410                            (setcdr category (cons cell (cdr category)))
411                            cell)) new-g))
412             (t
413              (let ((groups groups))
414                (while groups
415                  (let* ((group        (pop groups))
416                         (old-category (gnus-group-category group)))
417                    (if (eq category old-category)
418                        nil
419                      (setf (gnus-agent-cat-groups old-category)
420                            (delete group (gnus-agent-cat-groups
421                                           old-category))))))
422                ;; Purge cache as preceeding loop invalidated it.
423                (setq gnus-category-group-cache nil))
424
425              (setcdr (or (assq 'agent-groups category)
426                          (let ((cell (cons 'agent-groups nil)))
427                            (setcdr category (cons cell (cdr category)))
428                            cell)) groups))))))
429
430 (defsubst gnus-agent-cat-make (name &optional default-agent-predicate)
431   (list name `(agent-predicate . ,(or default-agent-predicate 'false))))
432
433 ;;; Fetching setup functions.
434
435 (defun gnus-agent-start-fetch ()
436   "Initialize data structures for efficient fetching."
437   (gnus-agent-create-buffer))
438
439 (defun gnus-agent-stop-fetch ()
440   "Save all data structures and clean up."
441   (setq gnus-agent-spam-hashtb nil)
442   (save-excursion
443     (set-buffer nntp-server-buffer)
444     (widen)))
445
446 (defmacro gnus-agent-with-fetch (&rest forms)
447   "Do FORMS safely."
448   `(unwind-protect
449        (let ((gnus-agent-fetching t))
450          (gnus-agent-start-fetch)
451          ,@forms)
452      (gnus-agent-stop-fetch)))
453
454 (put 'gnus-agent-with-fetch 'lisp-indent-function 0)
455 (put 'gnus-agent-with-fetch 'edebug-form-spec '(body))
456
457 (defmacro gnus-agent-append-to-list (tail value)
458   `(setq ,tail (setcdr ,tail (cons ,value nil))))
459
460 (defmacro gnus-agent-message (level &rest args)
461   `(if (<= ,level gnus-verbose)
462        (message ,@args)))
463
464 ;;;
465 ;;; Mode infestation
466 ;;;
467
468 (defvar gnus-agent-mode-hook nil
469   "Hook run when installing agent mode.")
470
471 (defvar gnus-agent-mode nil)
472 (defvar gnus-agent-mode-status '(gnus-agent-mode " Plugged"))
473
474 (defun gnus-agent-mode ()
475   "Minor mode for providing a agent support in Gnus buffers."
476   (let* ((buffer (progn (string-match "^gnus-\\(.*\\)-mode$"
477                                       (symbol-name major-mode))
478                         (match-string 1 (symbol-name major-mode))))
479          (mode (intern (format "gnus-agent-%s-mode" buffer))))
480     (set (make-local-variable 'gnus-agent-mode) t)
481     (set mode nil)
482     (set (make-local-variable mode) t)
483     ;; Set up the menu.
484     (when (gnus-visual-p 'agent-menu 'menu)
485       (funcall (intern (format "gnus-agent-%s-make-menu-bar" buffer))))
486     (unless (assq 'gnus-agent-mode minor-mode-alist)
487       (push gnus-agent-mode-status minor-mode-alist))
488     (unless (assq mode minor-mode-map-alist)
489       (push (cons mode (symbol-value (intern (format "gnus-agent-%s-mode-map"
490                                                      buffer))))
491             minor-mode-map-alist))
492     (when (eq major-mode 'gnus-group-mode)
493       (let ((init-plugged gnus-plugged)
494             (gnus-agent-go-online nil))
495         ;; g-a-t-p does nothing when gnus-plugged isn't changed.
496         ;; Therefore, make certain that the current value does not
497         ;; match the desired initial value.
498         (setq gnus-plugged :unknown)
499         (gnus-agent-toggle-plugged init-plugged)))
500     (gnus-run-hooks 'gnus-agent-mode-hook
501                     (intern (format "gnus-agent-%s-mode-hook" buffer)))))
502
503 (defvar gnus-agent-group-mode-map (make-sparse-keymap))
504 (gnus-define-keys gnus-agent-group-mode-map
505   "Ju" gnus-agent-fetch-groups
506   "Jc" gnus-enter-category-buffer
507   "Jj" gnus-agent-toggle-plugged
508   "Js" gnus-agent-fetch-session
509   "JY" gnus-agent-synchronize-flags
510   "JS" gnus-group-send-queue
511   "Ja" gnus-agent-add-group
512   "Jr" gnus-agent-remove-group
513   "Jo" gnus-agent-toggle-group-plugged)
514
515 (defun gnus-agent-group-make-menu-bar ()
516   (unless (boundp 'gnus-agent-group-menu)
517     (easy-menu-define
518      gnus-agent-group-menu gnus-agent-group-mode-map ""
519      '("Agent"
520        ["Toggle plugged" gnus-agent-toggle-plugged t]
521        ["Toggle group plugged" gnus-agent-toggle-group-plugged t]
522        ["List categories" gnus-enter-category-buffer t]
523        ["Add (current) group to category" gnus-agent-add-group t]
524        ["Remove (current) group from category" gnus-agent-remove-group t]
525        ["Send queue" gnus-group-send-queue gnus-plugged]
526        ("Fetch"
527         ["All" gnus-agent-fetch-session gnus-plugged]
528         ["Group" gnus-agent-fetch-group gnus-plugged])
529        ["Synchronize flags" gnus-agent-synchronize-flags t]
530        ))))
531
532 (defvar gnus-agent-summary-mode-map (make-sparse-keymap))
533 (gnus-define-keys gnus-agent-summary-mode-map
534   "Jj" gnus-agent-toggle-plugged
535   "Ju" gnus-agent-summary-fetch-group
536   "JS" gnus-agent-fetch-group
537   "Js" gnus-agent-summary-fetch-series
538   "J#" gnus-agent-mark-article
539   "J\M-#" gnus-agent-unmark-article
540   "@" gnus-agent-toggle-mark
541   "Jc" gnus-agent-catchup)
542
543 (defun gnus-agent-summary-make-menu-bar ()
544   (unless (boundp 'gnus-agent-summary-menu)
545     (easy-menu-define
546      gnus-agent-summary-menu gnus-agent-summary-mode-map ""
547      '("Agent"
548        ["Toggle plugged" gnus-agent-toggle-plugged t]
549        ["Mark as downloadable" gnus-agent-mark-article t]
550        ["Unmark as downloadable" gnus-agent-unmark-article t]
551        ["Toggle mark" gnus-agent-toggle-mark t]
552        ["Fetch downloadable" gnus-agent-summary-fetch-group t]
553        ["Catchup undownloaded" gnus-agent-catchup t]))))
554
555 (defvar gnus-agent-server-mode-map (make-sparse-keymap))
556 (gnus-define-keys gnus-agent-server-mode-map
557   "Jj" gnus-agent-toggle-plugged
558   "Ja" gnus-agent-add-server
559   "Jr" gnus-agent-remove-server)
560
561 (defun gnus-agent-server-make-menu-bar ()
562   (unless (boundp 'gnus-agent-server-menu)
563     (easy-menu-define
564      gnus-agent-server-menu gnus-agent-server-mode-map ""
565      '("Agent"
566        ["Toggle plugged" gnus-agent-toggle-plugged t]
567        ["Add" gnus-agent-add-server t]
568        ["Remove" gnus-agent-remove-server t]))))
569
570 (defun gnus-agent-make-mode-line-string (string mouse-button mouse-func)
571   (if (and (fboundp 'propertize)
572            (fboundp 'make-mode-line-mouse-map))
573       (propertize string 'local-map
574                   (make-mode-line-mouse-map mouse-button mouse-func))
575     string))
576
577 (defun gnus-agent-toggle-plugged (set-to)
578   "Toggle whether Gnus is unplugged or not."
579   (interactive (list (not gnus-plugged)))
580   (cond ((eq set-to gnus-plugged)
581          nil)
582         (set-to
583          (setq gnus-plugged set-to)
584          (gnus-run-hooks 'gnus-agent-plugged-hook)
585          (setcar (cdr gnus-agent-mode-status)
586                  (gnus-agent-make-mode-line-string " Plugged"
587                                                    'mouse-2
588                                                    'gnus-agent-toggle-plugged))
589          (gnus-agent-go-online gnus-agent-go-online)
590          (gnus-agent-possibly-synchronize-flags))
591         (t
592          (gnus-agent-close-connections)
593          (setq gnus-plugged set-to)
594          (gnus-run-hooks 'gnus-agent-unplugged-hook)
595          (setcar (cdr gnus-agent-mode-status)
596                  (gnus-agent-make-mode-line-string " Unplugged"
597                                                    'mouse-2
598                                                    'gnus-agent-toggle-plugged))))
599   (set-buffer-modified-p t))
600
601 (defmacro gnus-agent-while-plugged (&rest body)
602   `(let ((original-gnus-plugged gnus-plugged))
603     (unwind-protect
604         (progn (gnus-agent-toggle-plugged t)
605                ,@body)
606       (gnus-agent-toggle-plugged original-gnus-plugged))))
607
608 (put 'gnus-agent-while-plugged 'lisp-indent-function 0)
609 (put 'gnus-agent-while-plugged 'edebug-form-spec '(body))
610
611 (defun gnus-agent-close-connections ()
612   "Close all methods covered by the Gnus agent."
613   (let ((methods (gnus-agent-covered-methods)))
614     (while methods
615       (gnus-close-server (pop methods)))))
616
617 ;;;###autoload
618 (defun gnus-unplugged ()
619   "Start Gnus unplugged."
620   (interactive)
621   (setq gnus-plugged nil)
622   (gnus))
623
624 ;;;###autoload
625 (defun gnus-plugged ()
626   "Start Gnus plugged."
627   (interactive)
628   (setq gnus-plugged t)
629   (gnus))
630
631 ;;;###autoload
632 (defun gnus-slave-unplugged (&optional arg)
633   "Read news as a slave unplugged."
634   (interactive "P")
635   (setq gnus-plugged nil)
636   (gnus arg nil 'slave))
637
638 ;;;###autoload
639 (defun gnus-agentize ()
640   "Allow Gnus to be an offline newsreader.
641
642 The gnus-agentize function is now called internally by gnus when
643 gnus-agent is set.  If you wish to avoid calling gnus-agentize,
644 customize gnus-agent to nil.
645
646 This will modify the `gnus-setup-news-hook', and
647 `message-send-mail-real-function' variables, and install the Gnus agent
648 minor mode in all Gnus buffers."
649   (interactive)
650   (gnus-open-agent)
651   (add-hook 'gnus-setup-news-hook 'gnus-agent-queue-setup)
652   (unless gnus-agent-send-mail-function
653     (setq gnus-agent-send-mail-function
654           (or message-send-mail-real-function
655               (function (lambda () (funcall message-send-mail-function))))
656           message-send-mail-real-function 'gnus-agent-send-mail))
657
658   ;; If the servers file doesn't exist, auto-agentize some servers and
659   ;; save the servers file so this auto-agentizing isn't invoked
660   ;; again.
661   (unless (file-exists-p (nnheader-concat gnus-agent-directory "lib/servers"))
662     (gnus-message 3 "First time agent user, agentizing remote groups...")
663     (mapc
664      (lambda (server-or-method)
665        (let ((method (gnus-server-to-method server-or-method)))
666          (when (memq (car method)
667                      gnus-agent-auto-agentize-methods)
668            (push (gnus-method-to-server method)
669                  gnus-agent-covered-methods)
670            (setq gnus-agent-method-p-cache nil))))
671      (cons gnus-select-method gnus-secondary-select-methods))
672     (gnus-agent-write-servers)))
673
674 (defun gnus-agent-queue-setup (&optional group-name)
675   "Make sure the queue group exists.
676 Optional arg GROUP-NAME allows to specify another group."
677   (unless (gnus-gethash (format "nndraft:%s" (or group-name "queue"))
678                         gnus-newsrc-hashtb)
679     (gnus-request-create-group (or group-name "queue") '(nndraft ""))
680     (let ((gnus-level-default-subscribed 1))
681       (gnus-subscribe-group (format "nndraft:%s" (or group-name "queue"))
682                             nil '(nndraft "")))
683     (gnus-group-set-parameter
684      (format "nndraft:%s" (or group-name "queue"))
685      'gnus-dummy '((gnus-draft-mode)))))
686
687 (defun gnus-agent-send-mail ()
688   (if (or (not gnus-agent-queue-mail)
689           (and gnus-plugged (not (eq gnus-agent-queue-mail 'always))))
690       (funcall gnus-agent-send-mail-function)
691     (goto-char (point-min))
692     (re-search-forward
693      (concat "^" (regexp-quote mail-header-separator) "\n"))
694     (replace-match "\n")
695     (gnus-agent-insert-meta-information 'mail)
696     (gnus-request-accept-article "nndraft:queue" nil t t)))
697
698 (defun gnus-agent-insert-meta-information (type &optional method)
699   "Insert meta-information into the message that says how it's to be posted.
700 TYPE can be either `mail' or `news'.  If the latter, then METHOD can
701 be a select method."
702   (save-excursion
703     (message-remove-header gnus-agent-meta-information-header)
704     (goto-char (point-min))
705     (insert gnus-agent-meta-information-header ": "
706             (symbol-name type) " " (format "%S" method)
707             "\n")
708     (forward-char -1)
709     (while (search-backward "\n" nil t)
710       (replace-match "\\n" t t))))
711
712 (defun gnus-agent-restore-gcc ()
713   "Restore GCC field from saved header."
714   (save-excursion
715     (goto-char (point-min))
716     (while (re-search-forward
717             (concat "^" (regexp-quote gnus-agent-gcc-header) ":") nil t)
718       (replace-match "Gcc:" 'fixedcase))))
719
720 (defun gnus-agent-any-covered-gcc ()
721   (save-restriction
722     (message-narrow-to-headers)
723     (let* ((gcc (mail-fetch-field "gcc" nil t))
724            (methods (and gcc
725                          (mapcar 'gnus-inews-group-method
726                                  (message-unquote-tokens
727                                   (message-tokenize-header
728                                    gcc " ,")))))
729            covered)
730       (while (and (not covered) methods)
731         (setq covered (gnus-agent-method-p (car methods))
732               methods (cdr methods)))
733       covered)))
734
735 ;;;###autoload
736 (defun gnus-agent-possibly-save-gcc ()
737   "Save GCC if Gnus is unplugged."
738   (when (and (not gnus-plugged) (gnus-agent-any-covered-gcc))
739     (save-excursion
740       (goto-char (point-min))
741       (let ((case-fold-search t))
742         (while (re-search-forward "^gcc:" nil t)
743           (replace-match (concat gnus-agent-gcc-header ":") 'fixedcase))))))
744
745 (defun gnus-agent-possibly-do-gcc ()
746   "Do GCC if Gnus is plugged."
747   (when (or gnus-plugged (not (gnus-agent-any-covered-gcc)))
748     (gnus-inews-do-gcc)))
749
750 ;;;
751 ;;; Group mode commands
752 ;;;
753
754 (defun gnus-agent-fetch-groups (n)
755   "Put all new articles in the current groups into the Agent."
756   (interactive "P")
757   (unless gnus-plugged
758     (error "Groups can't be fetched when Gnus is unplugged"))
759   (gnus-group-iterate n 'gnus-agent-fetch-group))
760
761 (defun gnus-agent-fetch-group (&optional group)
762   "Put all new articles in GROUP into the Agent."
763   (interactive (list (gnus-group-group-name)))
764   (setq group (or group gnus-newsgroup-name))
765   (unless group
766     (error "No group on the current line"))
767
768   (gnus-agent-while-plugged
769     (let ((gnus-command-method (gnus-find-method-for-group group)))
770       (gnus-agent-with-fetch
771         (gnus-agent-fetch-group-1 group gnus-command-method)
772         (gnus-message 5 "Fetching %s...done" group)))))
773
774 (defun gnus-agent-add-group (category arg)
775   "Add the current group to an agent category."
776   (interactive
777    (list
778     (intern
779      (completing-read
780       "Add to category: "
781       (mapcar (lambda (cat) (list (symbol-name (car cat))))
782               gnus-category-alist)
783       nil t))
784     current-prefix-arg))
785   (let ((cat (assq category gnus-category-alist))
786         c groups)
787     (gnus-group-iterate arg
788       (lambda (group)
789         (when (gnus-agent-cat-groups (setq c (gnus-group-category group)))
790           (setf (gnus-agent-cat-groups c)
791                 (delete group (gnus-agent-cat-groups c))))
792         (push group groups)))
793     (setf (gnus-agent-cat-groups cat)
794           (nconc (gnus-agent-cat-groups cat) groups))
795     (gnus-category-write)))
796
797 (defun gnus-agent-remove-group (arg)
798   "Remove the current group from its agent category, if any."
799   (interactive "P")
800   (let (c)
801     (gnus-group-iterate arg
802       (lambda (group)
803         (when (gnus-agent-cat-groups (setq c (gnus-group-category group)))
804           (setf (gnus-agent-cat-groups c)
805                 (delete group (gnus-agent-cat-groups c))))))
806     (gnus-category-write)))
807
808 (defun gnus-agent-synchronize-flags ()
809   "Synchronize unplugged flags with servers."
810   (interactive)
811   (save-excursion
812     (dolist (gnus-command-method (gnus-agent-covered-methods))
813       (when (file-exists-p (gnus-agent-lib-file "flags"))
814         (gnus-agent-synchronize-flags-server gnus-command-method)))))
815
816 (defun gnus-agent-possibly-synchronize-flags ()
817   "Synchronize flags according to `gnus-agent-synchronize-flags'."
818   (interactive)
819   (save-excursion
820     (dolist (gnus-command-method (gnus-agent-covered-methods))
821       (when (file-exists-p (gnus-agent-lib-file "flags"))
822         (gnus-agent-possibly-synchronize-flags-server gnus-command-method)))))
823
824 (defun gnus-agent-synchronize-flags-server (method)
825   "Synchronize flags set when unplugged for server."
826   (let ((gnus-command-method method))
827     (when (file-exists-p (gnus-agent-lib-file "flags"))
828       (set-buffer (get-buffer-create " *Gnus Agent flag synchronize*"))
829       (erase-buffer)
830       (nnheader-insert-file-contents (gnus-agent-lib-file "flags"))
831       (if (null (gnus-check-server gnus-command-method))
832           (gnus-message 1 "Couldn't open server %s" (nth 1 gnus-command-method))
833         (while (not (eobp))
834           (if (null (eval (read (current-buffer))))
835               (gnus-delete-line)
836             (write-file (gnus-agent-lib-file "flags"))
837             (error "Couldn't set flags from file %s"
838                    (gnus-agent-lib-file "flags"))))
839         (delete-file (gnus-agent-lib-file "flags")))
840       (kill-buffer nil))))
841
842 (defun gnus-agent-possibly-synchronize-flags-server (method)
843   "Synchronize flags for server according to `gnus-agent-synchronize-flags'."
844   (when (or (and gnus-agent-synchronize-flags
845                  (not (eq gnus-agent-synchronize-flags 'ask)))
846             (and (eq gnus-agent-synchronize-flags 'ask)
847                  (gnus-y-or-n-p (format "Synchronize flags on server `%s'? "
848                                         (cadr method)))))
849     (gnus-agent-synchronize-flags-server method)))
850
851 ;;;###autoload
852 (defun gnus-agent-rename-group (old-group new-group)
853   "Rename fully-qualified OLD-GROUP as NEW-GROUP.  Always updates the agent, even when
854 disabled, as the old agent files would corrupt gnus when the agent was
855 next enabled. Depends upon the caller to determine whether group renaming is supported."
856   (let* ((old-command-method (gnus-find-method-for-group old-group))
857          (old-path           (directory-file-name
858                               (let (gnus-command-method old-command-method)
859                                 (gnus-agent-group-pathname old-group))))
860          (new-command-method (gnus-find-method-for-group new-group))
861          (new-path           (directory-file-name
862                               (let (gnus-command-method new-command-method)
863                                 (gnus-agent-group-pathname new-group)))))
864     (gnus-rename-file old-path new-path t)
865
866     (let* ((old-real-group (gnus-group-real-name old-group))
867            (new-real-group (gnus-group-real-name new-group))
868            (old-active (gnus-agent-get-group-info old-command-method old-real-group)))
869       (gnus-agent-save-group-info old-command-method old-real-group nil)
870       (gnus-agent-save-group-info new-command-method new-real-group old-active)
871
872       (let ((old-local (gnus-agent-get-local old-group 
873                                              old-real-group old-command-method)))
874         (gnus-agent-set-local old-group
875                               nil nil
876                               old-real-group old-command-method)
877         (gnus-agent-set-local new-group
878                               (car old-local) (cdr old-local)
879                               new-real-group new-command-method)))))
880
881 ;;;###autoload
882 (defun gnus-agent-delete-group (group)
883   "Delete fully-qualified GROUP.  Always updates the agent, even when
884 disabled, as the old agent files would corrupt gnus when the agent was
885 next enabled. Depends upon the caller to determine whether group deletion is supported."
886   (let* ((command-method (gnus-find-method-for-group group))
887          (path           (directory-file-name
888                           (let (gnus-command-method command-method)
889                             (gnus-agent-group-pathname group)))))
890     (gnus-delete-file path)
891
892     (let* ((real-group (gnus-group-real-name group)))
893       (gnus-agent-save-group-info command-method real-group nil)
894
895       (let ((local (gnus-agent-get-local group 
896                                          real-group command-method)))
897         (gnus-agent-set-local group
898                               nil nil
899                               real-group command-method)))))
900
901 ;;;
902 ;;; Server mode commands
903 ;;;
904
905 (defun gnus-agent-add-server ()
906   "Enroll SERVER in the agent program."
907   (interactive)
908   (let* ((server       (gnus-server-server-name))
909          (named-server (gnus-server-named-server))
910          (method       (and server
911                             (gnus-server-get-method nil server))))
912     (unless server
913       (error "No server on the current line"))
914
915     (when (gnus-agent-method-p method)
916       (error "Server already in the agent program"))
917
918     (push named-server gnus-agent-covered-methods)
919
920     (setq gnus-agent-method-p-cache nil)
921     (gnus-server-update-server server)
922     (gnus-agent-write-servers)
923     (gnus-message 1 "Entered %s into the Agent" server)))
924
925 (defun gnus-agent-remove-server ()
926   "Remove SERVER from the agent program."
927   (interactive)
928   (let* ((server       (gnus-server-server-name))
929          (named-server (gnus-server-named-server)))
930     (unless server
931       (error "No server on the current line"))
932
933     (unless (member named-server gnus-agent-covered-methods)
934       (error "Server not in the agent program"))
935
936     (setq gnus-agent-covered-methods 
937           (delete named-server gnus-agent-covered-methods)
938           gnus-agent-method-p-cache nil)
939
940     (gnus-server-update-server server)
941     (gnus-agent-write-servers)
942     (gnus-message 1 "Removed %s from the agent" server)))
943
944 (defun gnus-agent-read-servers ()
945   "Read the alist of covered servers."
946   (setq gnus-agent-covered-methods 
947         (gnus-agent-read-file
948          (nnheader-concat gnus-agent-directory "lib/servers"))
949         gnus-agent-method-p-cache nil)
950
951   ;; I am called so early in start-up that I can not validate server
952   ;; names.  When that is the case, I skip the validation.  That is
953   ;; alright as the gnus startup code calls the validate methods
954   ;; directly.
955   (if gnus-server-alist
956       (gnus-agent-read-servers-validate)))
957
958 (defun gnus-agent-read-servers-validate ()
959   (mapcar (lambda (server-or-method)
960             (let* ((server (if (stringp server-or-method)
961                                server-or-method
962                              (gnus-method-to-server server-or-method)))
963                    (method (gnus-server-to-method server)))
964               (if method
965                   (unless (member server gnus-agent-covered-methods)
966                     (push server gnus-agent-covered-methods)
967                     (setq gnus-agent-method-p-cache nil))
968                 (gnus-message 1 "Ignoring disappeared server `%s'" server))))
969           (prog1 gnus-agent-covered-methods
970             (setq gnus-agent-covered-methods nil))))
971
972 (defun gnus-agent-read-servers-validate-native (native-method)
973   (setq gnus-agent-covered-methods
974         (mapcar (lambda (method)
975                   (if (or (not method)
976                           (equal method native-method))
977                       "native"
978                     method)) gnus-agent-covered-methods)))
979
980 (defun gnus-agent-write-servers ()
981   "Write the alist of covered servers."
982   (gnus-make-directory (nnheader-concat gnus-agent-directory "lib"))
983   (let ((coding-system-for-write nnheader-file-coding-system)
984         (file-name-coding-system nnmail-pathname-coding-system))
985     (with-temp-file (nnheader-concat gnus-agent-directory "lib/servers")
986       (prin1 gnus-agent-covered-methods
987              (current-buffer)))))
988
989 ;;;
990 ;;; Summary commands
991 ;;;
992
993 (defun gnus-agent-mark-article (n &optional unmark)
994   "Mark the next N articles as downloadable.
995 If N is negative, mark backward instead.  If UNMARK is non-nil, remove
996 the mark instead.  The difference between N and the actual number of
997 articles marked is returned."
998   (interactive "p")
999   (let ((backward (< n 0))
1000         (n (abs n)))
1001     (while (and
1002             (> n 0)
1003             (progn
1004               (gnus-summary-set-agent-mark
1005                (gnus-summary-article-number) unmark)
1006               (zerop (gnus-summary-next-subject (if backward -1 1) nil t))))
1007       (setq n (1- n)))
1008     (when (/= 0 n)
1009       (gnus-message 7 "No more articles"))
1010     (gnus-summary-recenter)
1011     (gnus-summary-position-point)
1012     n))
1013
1014 (defun gnus-agent-unmark-article (n)
1015   "Remove the downloadable mark from the next N articles.
1016 If N is negative, unmark backward instead.  The difference between N and
1017 the actual number of articles unmarked is returned."
1018   (interactive "p")
1019   (gnus-agent-mark-article n t))
1020
1021 (defun gnus-agent-toggle-mark (n)
1022   "Toggle the downloadable mark from the next N articles.
1023 If N is negative, toggle backward instead.  The difference between N and
1024 the actual number of articles toggled is returned."
1025   (interactive "p")
1026   (gnus-agent-mark-article n 'toggle))
1027
1028 (defun gnus-summary-set-agent-mark (article &optional unmark)
1029   "Mark ARTICLE as downloadable.  If UNMARK is nil, article is marked.
1030 When UNMARK is t, the article is unmarked.  For any other value, the
1031 article's mark is toggled."
1032   (let ((unmark (cond ((eq nil unmark)
1033                        nil)
1034                       ((eq t unmark)
1035                        t)
1036                       (t
1037                        (memq article gnus-newsgroup-downloadable)))))
1038     (when (gnus-summary-goto-subject article nil t)
1039       (gnus-summary-update-mark
1040        (if unmark
1041            (progn
1042              (setq gnus-newsgroup-downloadable
1043                    (delq article gnus-newsgroup-downloadable))
1044              (gnus-article-mark article))
1045          (setq gnus-newsgroup-downloadable
1046                (gnus-add-to-sorted-list gnus-newsgroup-downloadable article))
1047          gnus-downloadable-mark)
1048        'unread))))
1049
1050 ;;;###autoload
1051 (defun gnus-agent-get-undownloaded-list ()
1052   "Construct list of articles that have not been downloaded."
1053   (let ((gnus-command-method (gnus-find-method-for-group gnus-newsgroup-name)))
1054     (when (set (make-local-variable 'gnus-newsgroup-agentized)
1055                (gnus-agent-method-p gnus-command-method))
1056       (let* ((alist (gnus-agent-load-alist gnus-newsgroup-name))
1057              (headers (sort (mapcar (lambda (h)
1058                                       (mail-header-number h))
1059                                     gnus-newsgroup-headers) '<))
1060              (cached (and gnus-use-cache gnus-newsgroup-cached))
1061              (undownloaded (list nil))
1062              (tail-undownloaded undownloaded)
1063              (unfetched (list nil))
1064              (tail-unfetched unfetched))
1065         (while (and alist headers)
1066           (let ((a (caar alist))
1067                 (h (car headers)))
1068             (cond ((< a h)
1069                    ;; Ignore IDs in the alist that are not being
1070                    ;; displayed in the summary.
1071                    (setq alist (cdr alist)))
1072                   ((> a h)
1073                    ;; Headers that are not in the alist should be
1074                    ;; fictious (see nnagent-retrieve-headers); they
1075                    ;; imply that this article isn't in the agent.
1076                    (gnus-agent-append-to-list tail-undownloaded h)
1077                    (gnus-agent-append-to-list tail-unfetched    h)
1078                    (setq headers (cdr headers))) 
1079                   ((cdar alist)
1080                    (setq alist (cdr alist))
1081                    (setq headers (cdr headers))
1082                    nil                  ; ignore already downloaded
1083                    )
1084                   (t
1085                    (setq alist (cdr alist))
1086                    (setq headers (cdr headers))
1087                    
1088                    ;; This article isn't in the agent.  Check to see
1089                    ;; if it is in the cache.  If it is, it's been
1090                    ;; downloaded.
1091                    (while (and cached (< (car cached) a))
1092                      (setq cached (cdr cached)))
1093                    (unless (equal a (car cached))
1094                      (gnus-agent-append-to-list tail-undownloaded a))))))
1095
1096         (while headers
1097           (let ((num (pop headers)))
1098             (gnus-agent-append-to-list tail-undownloaded num)
1099             (gnus-agent-append-to-list tail-unfetched    num)))
1100
1101         (setq gnus-newsgroup-undownloaded (cdr undownloaded)
1102               gnus-newsgroup-unfetched    (cdr unfetched))))))
1103
1104 (defun gnus-agent-catchup ()
1105   "Mark as read all unhandled articles.
1106 An article is unhandled if it is neither cached, nor downloaded, nor
1107 downloadable."
1108   (interactive)
1109   (save-excursion
1110     (let ((articles gnus-newsgroup-undownloaded))
1111       (when (or gnus-newsgroup-downloadable
1112                 gnus-newsgroup-cached)
1113         (setq articles (gnus-sorted-ndifference
1114                         (gnus-sorted-ndifference
1115                          (gnus-copy-sequence articles)
1116                          gnus-newsgroup-downloadable)
1117                         gnus-newsgroup-cached)))
1118
1119       (while articles
1120         (gnus-summary-mark-article
1121          (pop articles) gnus-catchup-mark)))
1122     (gnus-summary-position-point)))
1123
1124 (defun gnus-agent-summary-fetch-series ()
1125   (interactive)
1126   (when gnus-newsgroup-processable
1127     (setq gnus-newsgroup-downloadable
1128           (let* ((dl gnus-newsgroup-downloadable)
1129                  (gnus-newsgroup-downloadable
1130                   (sort (gnus-copy-sequence gnus-newsgroup-processable) '<))
1131                  (fetched-articles (gnus-agent-summary-fetch-group)))
1132             ;; The preceeding call to (gnus-agent-summary-fetch-group)
1133             ;; updated gnus-newsgroup-downloadable to remove each
1134             ;; article successfully fetched.
1135
1136             ;; For each article that I processed, remove its
1137             ;; processable mark IF the article is no longer
1138             ;; downloadable (i.e. it's already downloaded)
1139             (dolist (article gnus-newsgroup-processable)
1140               (unless (memq article gnus-newsgroup-downloadable)
1141                 (gnus-summary-remove-process-mark article)))
1142             (gnus-sorted-ndifference dl fetched-articles)))))
1143
1144 (defun gnus-agent-summary-fetch-group (&optional all)
1145   "Fetch the downloadable articles in the group.
1146 Optional arg ALL, if non-nil, means to fetch all articles."
1147   (interactive "P")
1148   (let ((articles
1149          (if all gnus-newsgroup-articles
1150            gnus-newsgroup-downloadable))
1151         (gnus-command-method (gnus-find-method-for-group gnus-newsgroup-name))
1152         fetched-articles)
1153     (gnus-agent-while-plugged
1154       (unless articles
1155         (error "No articles to download"))
1156       (gnus-agent-with-fetch
1157         (setq gnus-newsgroup-undownloaded
1158               (gnus-sorted-ndifference
1159                gnus-newsgroup-undownloaded
1160                (setq fetched-articles
1161                      (gnus-agent-fetch-articles
1162                       gnus-newsgroup-name articles)))))
1163       (save-excursion
1164         (dolist (article articles)
1165           (let ((was-marked-downloadable 
1166                  (memq article gnus-newsgroup-downloadable)))
1167             (cond (gnus-agent-mark-unread-after-downloaded
1168                    (setq gnus-newsgroup-downloadable
1169                          (delq article gnus-newsgroup-downloadable))
1170
1171                    (gnus-summary-mark-article article gnus-unread-mark))
1172                   (was-marked-downloadable
1173                    (gnus-summary-set-agent-mark article t)))
1174             (when (gnus-summary-goto-subject article nil t)
1175               (gnus-summary-update-download-mark article))))))
1176     fetched-articles))
1177
1178 (defun gnus-agent-fetch-selected-article ()
1179   "Fetch the current article as it is selected.
1180 This can be added to `gnus-select-article-hook' or
1181 `gnus-mark-article-hook'."
1182   (let ((gnus-command-method gnus-current-select-method))
1183     (when (and gnus-plugged (gnus-agent-method-p gnus-command-method))
1184       (when (gnus-agent-fetch-articles
1185              gnus-newsgroup-name
1186              (list gnus-current-article))
1187         (setq gnus-newsgroup-undownloaded
1188               (delq gnus-current-article gnus-newsgroup-undownloaded))
1189         (gnus-summary-update-download-mark gnus-current-article)))))
1190
1191 ;;;
1192 ;;; Internal functions
1193 ;;;
1194
1195 (defun gnus-agent-save-active (method)
1196   (when (gnus-agent-method-p method)
1197     (let* ((gnus-command-method method)
1198            (new (gnus-make-hashtable (count-lines (point-min) (point-max))))
1199            (file (gnus-agent-lib-file "active")))
1200       (gnus-active-to-gnus-format nil new)
1201       (gnus-agent-write-active file new)
1202       (erase-buffer)
1203       (nnheader-insert-file-contents file))))
1204
1205 (defun gnus-agent-write-active (file new)
1206     (gnus-make-directory (file-name-directory file))
1207     (let ((nnmail-active-file-coding-system gnus-agent-file-coding-system))
1208       ;; The hashtable contains real names of groups.  However, do NOT
1209       ;; add the foreign server prefix as gnus-active-to-gnus-format
1210       ;; will add it while reading the file.
1211       (gnus-write-active-file file new nil)))
1212
1213 ;;;###autoload
1214 (defun gnus-agent-possibly-alter-active (group active &optional info)
1215   "Possibly expand a group's active range to include articles
1216 downloaded into the agent."
1217   (let* ((gnus-command-method (or gnus-command-method
1218                                   (gnus-find-method-for-group group))))
1219     (when (gnus-agent-method-p gnus-command-method)
1220       (let* ((local (gnus-agent-get-local group))
1221              (active-min (or (car active) 0))
1222              (active-max (or (cdr active) 0))
1223              (agent-min (or (car local) active-min))
1224              (agent-max (or (cdr local) active-max)))
1225
1226         (when (< agent-min active-min)
1227           (setcar active agent-min))
1228
1229         (when (> agent-max active-max)
1230           (setcdr active agent-max))
1231
1232         (when (and info (< agent-max (- active-min 100)))
1233           ;; I'm expanding the active range by such a large amount
1234           ;; that there is a gap of more than 100 articles between the
1235           ;; last article known to the agent and the first article
1236           ;; currently available on the server.  This gap contains
1237           ;; articles that have been lost, mark them as read so that
1238           ;; gnus doesn't waste resources trying to fetch them.
1239
1240           ;; NOTE: I don't do this for smaller gaps (< 100) as I don't
1241           ;; want to modify the local file everytime someone restarts
1242           ;; gnus.  The small gap will cause a tiny performance hit
1243           ;; when gnus tries, and fails, to retrieve the articles.
1244           ;; Still that should be smaller than opening a buffer,
1245           ;; printing this list to the buffer, and then writing it to a
1246           ;; file.
1247
1248           (let ((read (gnus-info-read info)))
1249             (gnus-info-set-read 
1250              info 
1251              (gnus-range-add 
1252               read 
1253               (list (cons (1+ agent-max) 
1254                           (1- active-min))))))
1255
1256           ;; Lie about the agent's local range for this group to
1257           ;; disable the set read each time this server is opened.
1258           ;; NOTE: Opening this group will restore the valid local
1259           ;; range but it will also expand the local range to
1260           ;; incompass the new active range.
1261           (gnus-agent-set-local group agent-min (1- active-min)))))))
1262
1263 (defun gnus-agent-save-group-info (method group active)
1264   "Update a single group's active range in the agent's copy of the server's active file."
1265   (when (gnus-agent-method-p method)
1266     (let* ((gnus-command-method (or method gnus-command-method))
1267            (coding-system-for-write nnheader-file-coding-system)
1268            (file-name-coding-system nnmail-pathname-coding-system)
1269            (file (gnus-agent-lib-file "active"))
1270            oactive-min oactive-max)
1271       (gnus-make-directory (file-name-directory file))
1272       (with-temp-file file
1273         ;; Emacs got problem to match non-ASCII group in multibyte buffer.
1274         (mm-disable-multibyte)
1275         (when (file-exists-p file)
1276           (nnheader-insert-file-contents file)
1277
1278           (goto-char (point-min))
1279           (when (re-search-forward
1280                  (concat "^" (regexp-quote group) " ") nil t)
1281             (save-excursion
1282               (setq oactive-max (read (current-buffer)) ;; max
1283                     oactive-min (read (current-buffer)))) ;; min
1284             (gnus-delete-line)))
1285         (when active
1286           (insert (format "%S %d %d y\n" (intern group)
1287                           (max (or oactive-max (cdr active)) (cdr active))
1288                           (min (or oactive-min (car active)) (car active))))
1289           (goto-char (point-max))
1290           (while (search-backward "\\." nil t)
1291             (delete-char 1)))))))
1292
1293 (defun gnus-agent-get-group-info (method group)
1294   "Get a single group's active range in the agent's copy of the server's active file."
1295   (when (gnus-agent-method-p method)
1296     (let* ((gnus-command-method (or method gnus-command-method))
1297            (coding-system-for-write nnheader-file-coding-system)
1298            (file-name-coding-system nnmail-pathname-coding-system)
1299            (file (gnus-agent-lib-file "active"))
1300            oactive-min oactive-max)
1301       (gnus-make-directory (file-name-directory file))
1302       (with-temp-buffer
1303         ;; Emacs got problem to match non-ASCII group in multibyte buffer.
1304         (mm-disable-multibyte)
1305         (when (file-exists-p file)
1306           (nnheader-insert-file-contents file)
1307
1308           (goto-char (point-min))
1309           (when (re-search-forward
1310                  (concat "^" (regexp-quote group) " ") nil t)
1311             (save-excursion
1312               (setq oactive-max (read (current-buffer)) ;; max
1313                     oactive-min (read (current-buffer))) ;; min
1314               (cons oactive-min oactive-max))))))))
1315
1316 (defun gnus-agent-group-path (group)
1317   "Translate GROUP into a file name."
1318
1319   ;; NOTE: This is what nnmail-group-pathname does as of Apr 2003.
1320   ;; The two methods must be kept synchronized, which is why
1321   ;; gnus-agent-group-pathname was added.
1322
1323   (setq group
1324         (nnheader-translate-file-chars
1325          (nnheader-replace-duplicate-chars-in-string
1326           (nnheader-replace-chars-in-string 
1327            (gnus-group-real-name group)
1328            ?/ ?_)
1329           ?. ?_)))
1330   (if (or nnmail-use-long-file-names
1331           (file-directory-p (expand-file-name group (gnus-agent-directory))))
1332       group
1333     (mm-encode-coding-string
1334      (nnheader-replace-chars-in-string group ?. ?/)
1335      nnmail-pathname-coding-system)))
1336
1337 (defun gnus-agent-group-pathname (group)
1338   "Translate GROUP into a file name."
1339   ;; nnagent uses nnmail-group-pathname to read articles while
1340   ;; unplugged.  The agent must, therefore, use the same directory
1341   ;; while plugged.
1342   (let ((gnus-command-method (or gnus-command-method
1343                                  (gnus-find-method-for-group group))))
1344     (nnmail-group-pathname (gnus-group-real-name group) (gnus-agent-directory))))
1345
1346 (defun gnus-agent-get-function (method)
1347   (if (gnus-online method)
1348       (car method)
1349     (require 'nnagent)
1350     'nnagent))
1351
1352 (defun gnus-agent-covered-methods ()
1353   "Return the subset of methods that are covered by the agent."
1354   (delq nil (mapcar #'gnus-server-to-method gnus-agent-covered-methods)))
1355
1356 ;;; History functions
1357
1358 (defun gnus-agent-history-buffer ()
1359   (cdr (assoc (gnus-agent-method) gnus-agent-history-buffers)))
1360
1361 (defun gnus-agent-open-history ()
1362   (save-excursion
1363     (push (cons (gnus-agent-method)
1364                 (set-buffer (gnus-get-buffer-create
1365                              (format " *Gnus agent %s history*"
1366                                      (gnus-agent-method)))))
1367           gnus-agent-history-buffers)
1368     (mm-disable-multibyte) ;; everything is binary
1369     (erase-buffer)
1370     (insert "\n")
1371     (let ((file (gnus-agent-lib-file "history")))
1372       (when (file-exists-p file)
1373         (nnheader-insert-file-contents file))
1374       (set (make-local-variable 'gnus-agent-file-name) file))))
1375
1376 (defun gnus-agent-close-history ()
1377   (when (gnus-buffer-live-p gnus-agent-current-history)
1378     (kill-buffer gnus-agent-current-history)
1379     (setq gnus-agent-history-buffers
1380           (delq (assoc (gnus-agent-method) gnus-agent-history-buffers)
1381                 gnus-agent-history-buffers))))
1382
1383 ;;;
1384 ;;; Fetching
1385 ;;;
1386
1387 (defun gnus-agent-fetch-articles (group articles)
1388   "Fetch ARTICLES from GROUP and put them into the Agent."
1389   (when articles
1390     (gnus-agent-load-alist group)
1391     (let* ((alist   gnus-agent-article-alist)
1392            (headers (if (< (length articles) 2) nil gnus-newsgroup-headers))
1393            (selected-sets (list nil))
1394            (current-set-size 0)
1395            article
1396            header-number)
1397       ;; Check each article
1398       (while (setq article (pop articles))
1399         ;; Skip alist entries preceeding this article
1400         (while (> article (or (caar alist) (1+ article)))
1401           (setq alist (cdr alist)))
1402
1403         ;; Prune off articles that we have already fetched.
1404         (unless (and (eq article (caar alist))
1405                      (cdar alist))
1406           ;; Skip headers preceeding this article
1407           (while (> article 
1408                     (setq header-number
1409                           (let* ((header (car headers)))
1410                             (if header
1411                                 (mail-header-number header)
1412                               (1+ article)))))
1413             (setq headers (cdr headers)))
1414
1415           ;; Add this article to the current set
1416           (setcar selected-sets (cons article (car selected-sets)))
1417
1418           ;; Update the set size, when the set is too large start a
1419           ;; new one.  I do this after adding the article as I want at
1420           ;; least one article in each set.
1421           (when (< gnus-agent-max-fetch-size
1422                    (setq current-set-size
1423                          (+ current-set-size
1424                             (if (= header-number article)
1425                                 (let ((char-size (mail-header-chars
1426                                                   (car headers))))
1427                                   (if (<= char-size 0)
1428                                       ;; The char size was missing/invalid,
1429                                       ;; assume a worst-case situation of
1430                                       ;; 65 char/line.  If the line count
1431                                       ;; is missing, arbitrarily assume a
1432                                       ;; size of 1000 characters.
1433                                     (max (* 65 (mail-header-lines
1434                                                 (car headers)))
1435                                          1000)
1436                                     char-size))
1437                               0))))
1438             (setcar selected-sets (nreverse (car selected-sets)))
1439             (setq selected-sets (cons nil selected-sets)
1440                   current-set-size 0))))
1441
1442       (when (or (cdr selected-sets) (car selected-sets))
1443         (let* ((fetched-articles (list nil))
1444                (tail-fetched-articles fetched-articles)
1445                (dir (gnus-agent-group-pathname group))
1446                (date (time-to-days (current-time)))
1447                (case-fold-search t)
1448                pos crosses id)
1449
1450           (setcar selected-sets (nreverse (car selected-sets)))
1451           (setq selected-sets (nreverse selected-sets))
1452
1453           (gnus-make-directory dir)
1454           (gnus-message 7 "Fetching articles for %s..." group)
1455
1456           (unwind-protect
1457               (while (setq articles (pop selected-sets))
1458                 ;; Fetch the articles from the backend.
1459                 (if (gnus-check-backend-function 'retrieve-articles group)
1460                     (setq pos (gnus-retrieve-articles articles group))
1461                   (with-temp-buffer
1462                     (let (article)
1463                       (while (setq article (pop articles))
1464                         (gnus-message 10 "Fetching article %s for %s..."
1465                                       article group)
1466                         (when (or
1467                                (gnus-backlog-request-article group article
1468                                                              nntp-server-buffer)
1469                                (gnus-request-article article group))
1470                           (goto-char (point-max))
1471                           (push (cons article (point)) pos)
1472                           (insert-buffer-substring nntp-server-buffer)))
1473                       (copy-to-buffer
1474                        nntp-server-buffer (point-min) (point-max))
1475                       (setq pos (nreverse pos)))))
1476                 ;; Then save these articles into the Agent.
1477                 (save-excursion
1478                   (set-buffer nntp-server-buffer)
1479                   (while pos
1480                     (narrow-to-region (cdar pos) (or (cdadr pos) (point-max)))
1481                     (goto-char (point-min))
1482                     (unless (eobp) ;; Don't save empty articles.
1483                       (when (search-forward "\n\n" nil t)
1484                         (when (search-backward "\nXrefs: " nil t)
1485                           ;; Handle cross posting.
1486                           (goto-char (match-end 0)) ; move to end of header name
1487                           (skip-chars-forward "^ ") ; skip server name
1488                           (skip-chars-forward " ")
1489                           (setq crosses nil)
1490                           (while (looking-at "\\([^: \n]+\\):\\([0-9]+\\) *")
1491                             (push (cons (buffer-substring (match-beginning 1)
1492                                                           (match-end 1))
1493                                         (string-to-int
1494                                          (buffer-substring (match-beginning 2)
1495                                                            (match-end 2))))
1496                                   crosses)
1497                             (goto-char (match-end 0)))
1498                           (gnus-agent-crosspost crosses (caar pos) date)))
1499                       (goto-char (point-min))
1500                       (if (not (re-search-forward
1501                                 "^Message-ID: *<\\([^>\n]+\\)>" nil t))
1502                           (setq id "No-Message-ID-in-article")
1503                         (setq id (buffer-substring
1504                                   (match-beginning 1) (match-end 1))))
1505                       (let ((coding-system-for-write
1506                              gnus-agent-file-coding-system))
1507                         (write-region (point-min) (point-max)
1508                                       (concat dir (number-to-string (caar pos)))
1509                                       nil 'silent))
1510
1511                       (gnus-agent-append-to-list
1512                        tail-fetched-articles (caar pos)))
1513                     (widen)
1514                     (setq pos (cdr pos)))))
1515
1516             (gnus-agent-save-alist group (cdr fetched-articles) date)
1517             (gnus-agent-update-files-total-fetched-for group (cdr fetched-articles))
1518
1519             (gnus-message 7 ""))
1520           (cdr fetched-articles))))))
1521
1522 (defun gnus-agent-unfetch-articles (group articles)
1523   "Delete ARTICLES that were fetched from GROUP into the agent."
1524   (when articles
1525     (gnus-agent-with-refreshed-group 
1526      group
1527      (gnus-agent-load-alist group)
1528      (let* ((alist (cons nil gnus-agent-article-alist))
1529             (articles (sort articles #'<))
1530             (next-possibility alist)
1531             (delete-this (pop articles)))
1532        (while (and (cdr next-possibility) delete-this)
1533          (let ((have-this (caar (cdr next-possibility))))
1534            (cond ((< delete-this have-this)
1535                   (setq delete-this (pop articles)))
1536                  ((= delete-this have-this)
1537                   (let ((timestamp (cdar (cdr next-possibility))))
1538                     (when timestamp
1539                       (let* ((file-name (concat (gnus-agent-group-pathname group)
1540                                                 (number-to-string have-this)))
1541                              (size-file (float (or (and gnus-agent-total-fetched-hashtb
1542                                                         (nth 7 (file-attributes file-name)))
1543                                                    0))))
1544                         (delete-file file-name)
1545                         (gnus-agent-update-files-total-fetched-for group (- size-file)))))
1546
1547                   (setcdr next-possibility (cddr next-possibility)))
1548                  (t
1549                   (setq next-possibility (cdr next-possibility))))))
1550        (setq gnus-agent-article-alist (cdr alist))
1551        (gnus-agent-save-alist group)))))
1552
1553 (defun gnus-agent-crosspost (crosses article &optional date)
1554   (setq date (or date t))
1555
1556   (let (gnus-agent-article-alist group alist beg end)
1557     (save-excursion
1558       (set-buffer gnus-agent-overview-buffer)
1559       (when (nnheader-find-nov-line article)
1560         (forward-word 1)
1561         (setq beg (point))
1562         (setq end (progn (forward-line 1) (point)))))
1563     (while crosses
1564       (setq group (caar crosses))
1565       (unless (setq alist (assoc group gnus-agent-group-alist))
1566         (push (setq alist (list group (gnus-agent-load-alist (caar crosses))))
1567               gnus-agent-group-alist))
1568       (setcdr alist (cons (cons (cdar crosses) date) (cdr alist)))
1569       (save-excursion
1570         (set-buffer (gnus-get-buffer-create (format " *Gnus agent overview %s*"
1571                                                     group)))
1572         (when (= (point-max) (point-min))
1573           (push (cons group (current-buffer)) gnus-agent-buffer-alist)
1574           (ignore-errors
1575             (nnheader-insert-file-contents
1576              (gnus-agent-article-name ".overview" group))))
1577         (nnheader-find-nov-line (string-to-number (cdar crosses)))
1578         (insert (string-to-number (cdar crosses)))
1579         (insert-buffer-substring gnus-agent-overview-buffer beg end)
1580         (gnus-agent-check-overview-buffer))
1581       (setq crosses (cdr crosses)))))
1582
1583 (defun gnus-agent-backup-overview-buffer ()
1584   (when gnus-newsgroup-name
1585     (let ((root (gnus-agent-article-name ".overview" gnus-newsgroup-name))
1586           (cnt 0)
1587           name)
1588       (while (file-exists-p
1589               (setq name (concat root "~"
1590                                  (int-to-string (setq cnt (1+ cnt))) "~"))))
1591       (write-region (point-min) (point-max) name nil 'no-msg)
1592       (gnus-message 1 "Created backup copy of overview in %s." name)))
1593   t)
1594
1595 (defun gnus-agent-check-overview-buffer (&optional buffer)
1596   "Check the overview file given for sanity.
1597 In particular, checks that the file is sorted by article number
1598 and that there are no duplicates."
1599   (let ((prev-num -1)
1600         (backed-up nil))
1601     (save-excursion
1602       (when buffer
1603         (set-buffer buffer))
1604       (save-restriction
1605         (widen)
1606         (goto-char (point-min))
1607
1608         (while (< (point) (point-max))
1609           (let ((p (point))
1610                 (cur (condition-case nil
1611                          (read (current-buffer))
1612                        (error nil))))
1613             (cond
1614              ((or (not (integerp cur))
1615                   (not (eq (char-after) ?\t)))
1616               (or backed-up
1617                   (setq backed-up (gnus-agent-backup-overview-buffer)))
1618               (gnus-message 1
1619                             "Overview buffer contains garbage '%s'."
1620                             (buffer-substring
1621                              p (point-at-eol))))
1622              ((= cur prev-num)
1623               (or backed-up
1624                   (setq backed-up (gnus-agent-backup-overview-buffer)))
1625               (gnus-message 1
1626                             "Duplicate overview line for %d" cur)
1627               (delete-region (point) (progn (forward-line 1) (point))))
1628              ((< cur prev-num)
1629               (or backed-up
1630                   (setq backed-up (gnus-agent-backup-overview-buffer)))
1631               (gnus-message 1 "Overview buffer not sorted!")
1632               (sort-numeric-fields 1 (point-min) (point-max))
1633               (goto-char (point-min))
1634               (setq prev-num -1))
1635              (t
1636               (setq prev-num cur)))
1637             (forward-line 1)))))))
1638
1639 (defun gnus-agent-flush-cache ()
1640   (save-excursion
1641     (while gnus-agent-buffer-alist
1642       (set-buffer (cdar gnus-agent-buffer-alist))
1643       (let ((coding-system-for-write
1644              gnus-agent-file-coding-system))
1645         (write-region (point-min) (point-max)
1646                       (gnus-agent-article-name ".overview"
1647                                                (caar gnus-agent-buffer-alist))
1648                       nil 'silent))
1649       (setq gnus-agent-buffer-alist (cdr gnus-agent-buffer-alist)))
1650     (while gnus-agent-group-alist
1651       (with-temp-file (gnus-agent-article-name
1652                        ".agentview" (caar gnus-agent-group-alist))
1653         (princ (cdar gnus-agent-group-alist))
1654         (insert "\n")
1655         (princ 1 (current-buffer))
1656         (insert "\n"))
1657       (setq gnus-agent-group-alist (cdr gnus-agent-group-alist)))))
1658
1659 ;;;###autoload
1660 (defun gnus-agent-find-parameter (group symbol)
1661   "Search for GROUPs SYMBOL in the group's parameters, the group's
1662 topic parameters, the group's category, or the customizable
1663 variables.  Returns the first non-nil value found."
1664   (or (gnus-group-find-parameter group symbol t)
1665       (gnus-group-parameter-value (cdr (gnus-group-category group)) symbol t)
1666       (symbol-value
1667        (cdr
1668         (assq symbol
1669               '((agent-short-article . gnus-agent-short-article)
1670                 (agent-long-article . gnus-agent-long-article)
1671                 (agent-low-score . gnus-agent-low-score)
1672                 (agent-high-score . gnus-agent-high-score)
1673                 (agent-days-until-old . gnus-agent-expire-days)
1674                 (agent-enable-expiration
1675                  . gnus-agent-enable-expiration)
1676                 (agent-predicate . gnus-agent-predicate)))))))
1677
1678 (defun gnus-agent-fetch-headers (group &optional force)
1679   "Fetch interesting headers into the agent.  The group's overview
1680 file will be updated to include the headers while a list of available
1681 article numbers will be returned."
1682   (let* ((fetch-all (and gnus-agent-consider-all-articles
1683                          ;; Do not fetch all headers if the predicate
1684                          ;; implies that we only consider unread articles.
1685                          (not (gnus-predicate-implies-unread
1686                                (gnus-agent-find-parameter group
1687                                                           'agent-predicate)))))
1688          (articles (if fetch-all
1689                        (gnus-uncompress-range (gnus-active group))
1690                      (gnus-list-of-unread-articles group)))
1691          (gnus-decode-encoded-word-function 'identity)
1692          (file (gnus-agent-article-name ".overview" group)))
1693
1694     (unless fetch-all
1695       ;; Add articles with marks to the list of article headers we want to
1696       ;; fetch.  Don't fetch articles solely on the basis of a recent or seen
1697       ;; mark, but do fetch recent or seen articles if they have other, more
1698       ;; interesting marks.  (We have to fetch articles with boring marks
1699       ;; because otherwise the agent will remove their marks.)
1700       (dolist (arts (gnus-info-marks (gnus-get-info group)))
1701         (unless (memq (car arts) '(seen recent killed cache))
1702           (setq articles (gnus-range-add articles (cdr arts)))))
1703       (setq articles (sort (gnus-uncompress-sequence articles) '<)))
1704
1705     ;; At this point, I have the list of articles to consider for
1706     ;; fetching.  This is the list that I'll return to my caller. Some
1707     ;; of these articles may have already been fetched.  That's OK as
1708     ;; the fetch article code will filter those out.  Internally, I'll
1709     ;; filter this list to just those articles whose headers need to
1710     ;; be fetched.
1711     (let ((articles articles))
1712       ;; Remove known articles.
1713       (when (and (or gnus-agent-cache
1714                      (not gnus-plugged))
1715                  (gnus-agent-load-alist group))
1716         ;; Remove articles marked as downloaded.
1717         (if fetch-all
1718             ;; I want to fetch all headers in the active range.
1719             ;; Therefore, exclude only those headers that are in the
1720             ;; article alist.
1721             ;; NOTE: This is probably NOT what I want to do after
1722             ;; agent expiration in this group.
1723             (setq articles (gnus-agent-uncached-articles articles group))
1724
1725           ;; I want to only fetch those headers that have never been
1726           ;; fetched.  Therefore, exclude all headers that are, or
1727           ;; WERE, in the article alist.
1728           (let ((low (1+ (caar (last gnus-agent-article-alist))))
1729                 (high (cdr (gnus-active group))))
1730             ;; Low can be greater than High when the same group is
1731             ;; fetched twice in the same session {The first fetch will
1732             ;; fill the article alist such that (last
1733             ;; gnus-agent-article-alist) equals (cdr (gnus-active
1734             ;; group))}.  The addition of one(the 1+ above) then
1735             ;; forces Low to be greater than High.  When this happens,
1736             ;; gnus-list-range-intersection returns nil which
1737             ;; indicates that no headers need to be fetched. -- Kevin
1738             (setq articles (gnus-list-range-intersection
1739                             articles (list (cons low high)))))))
1740
1741       (gnus-message
1742        10 "gnus-agent-fetch-headers: undownloaded articles are '%s'"
1743        (gnus-compress-sequence articles t))
1744
1745       (save-excursion
1746         (set-buffer nntp-server-buffer)
1747
1748         (if articles
1749             (progn
1750               (gnus-message 7 "Fetching headers for %s..." group)
1751
1752               ;; Fetch them.
1753               (gnus-make-directory (nnheader-translate-file-chars
1754                                     (file-name-directory file) t))
1755
1756               (unless (eq 'nov (gnus-retrieve-headers articles group))
1757                 (nnvirtual-convert-headers))
1758               (gnus-agent-check-overview-buffer)
1759               ;; Move these headers to the overview buffer so that
1760               ;; gnus-agent-braid-nov can merge them with the contents
1761               ;; of FILE.
1762               (copy-to-buffer
1763                gnus-agent-overview-buffer (point-min) (point-max))
1764               ;; NOTE: Call g-a-brand-nov even when the file does not
1765               ;; exist.  As a minimum, it will validate the article
1766               ;; numbers already in the buffer.
1767               (gnus-agent-braid-nov group articles file)
1768               (let ((coding-system-for-write
1769                      gnus-agent-file-coding-system))
1770                 (gnus-agent-check-overview-buffer)
1771                 (write-region (point-min) (point-max) file nil 'silent))
1772               (gnus-agent-update-view-total-fetched-for group t)
1773               (gnus-agent-save-alist group articles nil)
1774               articles)
1775           (ignore-errors
1776             (erase-buffer)
1777             (nnheader-insert-file-contents file)))))
1778     articles))
1779
1780 (defsubst gnus-agent-read-article-number ()
1781   "Reads the article number at point.  Returns nil when a valid article number can not be read."
1782
1783   ;; It is unfortunite but the read function quietly overflows
1784   ;; integer.  As a result, I have to use string operations to test
1785   ;; for overflow BEFORE calling read.
1786   (when (looking-at "[0-9]+\t")
1787     (let ((len (- (match-end 0) (match-beginning 0))))
1788       (cond ((< len 9)
1789              (read (current-buffer)))
1790             ((= len 9)
1791              ;; Many 9 digit base-10 numbers can be represented in a 27-bit int
1792              ;; Back convert from int to string to ensure that this is one of them.
1793              (let* ((str1 (buffer-substring (match-beginning 0) (1- (match-end 0))))
1794                     (num (read (current-buffer)))
1795                     (str2 (int-to-string num)))
1796                (when (equal str1 str2)
1797                  num)))))))
1798
1799 (defsubst gnus-agent-copy-nov-line (article)
1800   "Copy the indicated ARTICLE from the overview buffer to the nntp server buffer."
1801   (let (art b e)
1802     (set-buffer gnus-agent-overview-buffer)
1803     (while (and (not (eobp))
1804                 (or (not (setq art (gnus-agent-read-article-number)))
1805                     (< art article)))
1806       (forward-line 1))
1807     (beginning-of-line)
1808     (if (or (eobp)
1809             (not (eq article art)))
1810         (set-buffer nntp-server-buffer)
1811       (setq b (point))
1812       (setq e (progn (forward-line 1) (point)))
1813       (set-buffer nntp-server-buffer)
1814       (insert-buffer-substring gnus-agent-overview-buffer b e))))
1815
1816 (defun gnus-agent-braid-nov (group articles file)
1817   "Merge agent overview data with given file.
1818 Takes unvalidated headers for ARTICLES from
1819 `gnus-agent-overview-buffer' and validated headers from the given
1820 FILE and places the combined valid headers into
1821 `nntp-server-buffer'.  This function can be used, when file
1822 doesn't exist, to valid the overview buffer."
1823   (let (start last)
1824     (set-buffer gnus-agent-overview-buffer)
1825     (goto-char (point-min))
1826     (set-buffer nntp-server-buffer)
1827     (erase-buffer)
1828     (when (file-exists-p file)
1829       (nnheader-insert-file-contents file))
1830     (goto-char (point-max))
1831     (forward-line -1)
1832
1833     (unless (or (= (point-min) (point-max))
1834                 (< (setq last (read (current-buffer))) (car articles)))
1835       ;; Old and new overlap -- We do it the hard way.
1836       (when (nnheader-find-nov-line (car articles))
1837         ;; Replacing existing NOV entry
1838         (delete-region (point) (progn (forward-line 1) (point))))
1839       (gnus-agent-copy-nov-line (pop articles))
1840
1841       (ignore-errors
1842        (while articles
1843          (while (let ((art (read (current-buffer))))
1844                   (cond ((< art (car articles))
1845                          (forward-line 1)
1846                          t)
1847                         ((= art (car articles))
1848                          (beginning-of-line)
1849                          (delete-region
1850                           (point) (progn (forward-line 1) (point)))
1851                          nil)
1852                         (t
1853                          (beginning-of-line)
1854                          nil))))
1855
1856          (gnus-agent-copy-nov-line (pop articles)))))
1857
1858     (goto-char (point-max))
1859
1860     ;; Append the remaining lines
1861     (when articles
1862       (when last
1863         (set-buffer gnus-agent-overview-buffer)
1864         (setq start (point))
1865         (set-buffer nntp-server-buffer))
1866
1867       (let ((p (point)))
1868         (insert-buffer-substring gnus-agent-overview-buffer start)
1869         (goto-char p))
1870
1871       (setq last (or last -134217728))
1872       (let (sort art)
1873         (while (not (eobp))
1874           (setq art (gnus-agent-read-article-number))
1875           (cond ((not art)
1876                  ;; Bad art num - delete this line
1877                  (beginning-of-line)
1878                  (delete-region (point) (progn (forward-line 1) (point))))
1879                 ((< art last)
1880                  ;; Art num out of order - enable sort
1881                  (setq sort t)
1882                  (forward-line 1))
1883                 (t
1884                  ;; Good art num
1885                  (setq last art)
1886                  (forward-line 1))))
1887         (when sort
1888           (sort-numeric-fields 1 (point-min) (point-max)))))))
1889
1890 ;; Keeps the compiler from warning about the free variable in
1891 ;; gnus-agent-read-agentview.
1892 (eval-when-compile
1893   (defvar gnus-agent-read-agentview))
1894
1895 (defun gnus-agent-load-alist (group)
1896   "Load the article-state alist for GROUP."
1897   ;; Bind free variable that's used in `gnus-agent-read-agentview'.
1898   (let ((gnus-agent-read-agentview group))
1899     (setq gnus-agent-article-alist
1900           (gnus-cache-file-contents
1901            (gnus-agent-article-name ".agentview" group)
1902            'gnus-agent-file-loading-cache
1903            'gnus-agent-read-agentview))))
1904
1905 ;; Save format may be either 1 or 2.  Two is the new, compressed
1906 ;; format that is still being tested.  Format 1 is uncompressed but
1907 ;; known to be reliable.
1908 (defconst gnus-agent-article-alist-save-format 2)
1909
1910 (defun gnus-agent-read-agentview (file)
1911   "Load FILE and do a `read' there."
1912   (with-temp-buffer
1913     (condition-case nil
1914       (progn
1915         (nnheader-insert-file-contents file)
1916         (goto-char (point-min))
1917         (let ((alist (read (current-buffer)))
1918               (version (condition-case nil (read (current-buffer))
1919                          (end-of-file 0)))
1920               changed-version)
1921
1922           (cond
1923            ((< version 2)
1924             (error "gnus-agent-read-agentview no longer supports version %d.  Stop gnus, manually evaluate gnus-agent-convert-to-compressed-agentview, then restart gnus." version))
1925            ((= version 0)
1926             (let ((inhibit-quit t)
1927                   entry)
1928               (gnus-agent-open-history)
1929               (set-buffer (gnus-agent-history-buffer))
1930               (goto-char (point-min))
1931               (while (not (eobp))
1932                 (if (and (looking-at
1933                           "[^\t\n]+\t\\([0-9]+\\)\t\\([^ \n]+\\) \\([0-9]+\\)")
1934                          (string= (match-string 2)
1935                                   gnus-agent-read-agentview)
1936                          (setq entry (assoc (string-to-number (match-string 3)) alist)))
1937                     (setcdr entry (string-to-number (match-string 1))))
1938                 (forward-line 1))
1939               (gnus-agent-close-history)
1940               (setq changed-version t)))
1941            ((= version 1)
1942             (setq changed-version (not (= 1 gnus-agent-article-alist-save-format))))
1943            ((= version 2)
1944             (let (uncomp)
1945               (mapcar
1946                (lambda (comp-list)
1947                  (let ((state (car comp-list))
1948                        (sequence (inline
1949                                    (gnus-uncompress-range
1950                                     (cdr comp-list)))))
1951                    (mapcar (lambda (article-id)
1952                              (setq uncomp (cons (cons article-id state) uncomp)))
1953                            sequence)))
1954                alist)
1955               (setq alist (sort uncomp 'car-less-than-car)))))
1956           (when changed-version
1957             (let ((gnus-agent-article-alist alist))
1958               (gnus-agent-save-alist gnus-agent-read-agentview)))
1959           alist))
1960       (file-error nil))))
1961
1962 (defun gnus-agent-save-alist (group &optional articles state)
1963   "Save the article-state alist for GROUP."
1964   (let* ((file-name-coding-system nnmail-pathname-coding-system)
1965          (prev (cons nil gnus-agent-article-alist))
1966          (all prev)
1967          print-level print-length item article)
1968     (while (setq article (pop articles))
1969       (while (and (cdr prev)
1970                   (< (caadr prev) article))
1971         (setq prev (cdr prev)))
1972       (cond
1973        ((not (cdr prev))
1974         (setcdr prev (list (cons article state))))
1975        ((> (caadr prev) article)
1976         (setcdr prev (cons (cons article state) (cdr prev))))
1977        ((= (caadr prev) article)
1978         (setcdr (cadr prev) state)))
1979       (setq prev (cdr prev)))
1980     (setq gnus-agent-article-alist (cdr all))
1981
1982     (gnus-agent-set-local group 
1983                           (caar gnus-agent-article-alist) 
1984                           (caar (last gnus-agent-article-alist)))
1985
1986     (gnus-make-directory (gnus-agent-article-name "" group))
1987     (with-temp-file (gnus-agent-article-name ".agentview" group)
1988       (cond ((eq gnus-agent-article-alist-save-format 1)
1989              (princ gnus-agent-article-alist (current-buffer)))
1990             ((eq gnus-agent-article-alist-save-format 2)
1991              (let ((compressed nil))
1992                (mapcar (lambda (pair)
1993                          (let* ((article-id (car pair))
1994                                 (day-of-download (cdr pair))
1995                                 (comp-list (assq day-of-download compressed)))
1996                            (if comp-list
1997                                (setcdr comp-list
1998                                        (cons article-id (cdr comp-list)))
1999                              (setq compressed
2000                                    (cons (list day-of-download article-id)
2001                                          compressed)))
2002                            nil)) gnus-agent-article-alist)
2003                (mapcar (lambda (comp-list)
2004                          (setcdr comp-list
2005                                  (gnus-compress-sequence
2006                                   (nreverse (cdr comp-list)))))
2007                        compressed)
2008                (princ compressed (current-buffer)))))
2009       (insert "\n")
2010       (princ gnus-agent-article-alist-save-format (current-buffer))
2011       (insert "\n"))
2012
2013     (gnus-agent-update-view-total-fetched-for group nil)))
2014
2015 (defvar gnus-agent-article-local nil)
2016 (defvar gnus-agent-file-loading-local nil)
2017
2018 (defun gnus-agent-load-local (&optional method)
2019   "Load the METHOD'S local file.  The local file contains min/max
2020 article counts for each of the method's subscribed groups."
2021   (let ((gnus-command-method (or method gnus-command-method)))
2022     (setq gnus-agent-article-local
2023           (gnus-cache-file-contents
2024            (gnus-agent-lib-file "local")
2025            'gnus-agent-file-loading-local
2026            'gnus-agent-read-and-cache-local))))
2027
2028 (defun gnus-agent-read-and-cache-local (file)
2029   "Load and read FILE then bind its contents to
2030 gnus-agent-article-local.  If that variable had `dirty' (also known as
2031 modified) original contents, they are first saved to their own file."
2032
2033   (if (and gnus-agent-article-local
2034            (symbol-value (intern "+dirty" gnus-agent-article-local)))
2035       (gnus-agent-save-local))
2036   (gnus-agent-read-local file))
2037
2038 (defun gnus-agent-read-local (file)
2039   "Load FILE and do a `read' there."
2040   (let ((my-obarray (gnus-make-hashtable (count-lines (point-min) 
2041                                                       (point-max))))
2042         (line 1))
2043     (with-temp-buffer
2044       (condition-case nil
2045           (let ((nnheader-file-coding-system gnus-agent-file-coding-system))
2046             (nnheader-insert-file-contents file))
2047         (file-error))
2048
2049       (goto-char (point-min))
2050       ;; Skip any comments at the beginning of the file (the only place where they may appear)
2051       (while (= (following-char) ?\;)
2052         (forward-line 1)
2053         (setq line (1+ line)))
2054
2055       (while (not (eobp))
2056         (condition-case err
2057             (let (group 
2058                   min
2059                   max
2060                   (cur (current-buffer)))
2061               (setq group (read cur)
2062                     min (read cur)
2063                     max (read cur))
2064
2065               (when (stringp group)
2066                 (setq group (intern group my-obarray)))
2067
2068               ;; NOTE: The '+ 0' ensure that min and max are both numerics.
2069               (set group (cons (+ 0 min) (+ 0 max))))
2070           (error
2071            (gnus-message 3 "Warning - invalid agent local: %s on line %d: "
2072                          file line (error-message-string err))))
2073         (forward-line 1)
2074         (setq line (1+ line))))
2075       
2076     (set (intern "+dirty" my-obarray) nil)
2077     (set (intern "+method" my-obarray) gnus-command-method)
2078     my-obarray))
2079
2080 (defun gnus-agent-save-local (&optional force)
2081   "Save gnus-agent-article-local under it method's agent.lib directory."
2082   (let ((my-obarray gnus-agent-article-local))
2083     (when (and my-obarray
2084                (or force (symbol-value (intern "+dirty" my-obarray))))
2085       (let* ((gnus-command-method (symbol-value (intern "+method" my-obarray)))
2086              ;; NOTE: gnus-command-method is used within gnus-agent-lib-file.
2087              (dest (gnus-agent-lib-file "local")))
2088         (gnus-make-directory (gnus-agent-lib-file ""))
2089
2090         (let ((buffer-file-coding-system gnus-agent-file-coding-system))
2091           (with-temp-file dest
2092             (let ((gnus-command-method (symbol-value (intern "+method" my-obarray)))
2093                   (file-name-coding-system nnmail-pathname-coding-system)
2094                   print-level print-length item article
2095                   (standard-output (current-buffer)))
2096               (mapatoms (lambda (symbol)
2097                           (cond ((not (boundp symbol))
2098                                  nil)
2099                                 ((member (symbol-name symbol) '("+dirty" "+method"))
2100                                  nil)
2101                                 (t
2102                                  (prin1 symbol)
2103                                  (let ((range (symbol-value symbol)))
2104                                    (princ " ")
2105                                    (princ (car range))
2106                                    (princ " ")
2107                                    (princ (cdr range))
2108                                    (princ "\n"))))) 
2109                         my-obarray))))))))
2110
2111 (defun gnus-agent-get-local (group &optional gmane method)
2112   (let* ((gmane (or gmane (gnus-group-real-name group)))
2113          (gnus-command-method (or method (gnus-find-method-for-group group)))
2114          (local (gnus-agent-load-local))
2115          (symb (intern gmane local))
2116          (minmax (and (boundp symb) (symbol-value symb))))
2117     (unless minmax
2118       ;; Bind these so that gnus-agent-load-alist doesn't change the
2119       ;; current alist (i.e. gnus-agent-article-alist)
2120       (let* ((gnus-agent-article-alist gnus-agent-article-alist)
2121              (gnus-agent-file-loading-cache gnus-agent-file-loading-cache)
2122              (alist (gnus-agent-load-alist group)))
2123         (when alist
2124           (setq minmax
2125                 (cons (caar alist)
2126                       (caar (last alist))))
2127           (gnus-agent-set-local group (car minmax) (cdr minmax) 
2128                                 gmane gnus-command-method local))))
2129     minmax))
2130
2131 (defun gnus-agent-set-local (group min max &optional gmane method local)
2132   (let* ((gmane (or gmane (gnus-group-real-name group)))
2133          (gnus-command-method (or method (gnus-find-method-for-group group)))
2134          (local (or local (gnus-agent-load-local)))
2135          (symb (intern gmane local))
2136          (minmax (and (boundp symb) (symbol-value symb))))
2137     
2138     (if (cond ((and minmax
2139                     (or (not (eq min (car minmax)))
2140                         (not (eq max (cdr minmax)))))
2141                (setcar minmax min)
2142                (setcdr minmax max)
2143                t)
2144               (minmax
2145                nil)
2146               ((and min max)
2147                (set symb (cons min max))
2148                t)
2149               (t
2150                (unintern symb local)))
2151         (set (intern "+dirty" local) t))))
2152
2153 (defun gnus-agent-article-name (article group)
2154   (expand-file-name article
2155                     (file-name-as-directory
2156                      (gnus-agent-group-pathname group))))
2157
2158 (defun gnus-agent-batch-confirmation (msg)
2159   "Show error message and return t."
2160   (gnus-message 1 msg)
2161   t)
2162
2163 ;;;###autoload
2164 (defun gnus-agent-batch-fetch ()
2165   "Start Gnus and fetch session."
2166   (interactive)
2167   (gnus)
2168   (let ((gnus-agent-confirmation-function 'gnus-agent-batch-confirmation))
2169     (gnus-agent-fetch-session))
2170   (gnus-group-exit))
2171
2172 (defun gnus-agent-fetch-session ()
2173   "Fetch all articles and headers that are eligible for fetching."
2174   (interactive)
2175   (unless gnus-agent-covered-methods
2176     (error "No servers are covered by the Gnus agent"))
2177   (unless gnus-plugged
2178     (error "Can't fetch articles while Gnus is unplugged"))
2179   (let ((methods (gnus-agent-covered-methods))
2180         groups group gnus-command-method)
2181     (save-excursion
2182       (while methods
2183         (setq gnus-command-method (car methods))
2184         (when (and (or (gnus-server-opened gnus-command-method)
2185                        (gnus-open-server gnus-command-method))
2186                    (gnus-online gnus-command-method))
2187           (setq groups (gnus-groups-from-server (car methods)))
2188           (gnus-agent-with-fetch
2189             (while (setq group (pop groups))
2190               (when (<= (gnus-group-level group)
2191                         gnus-agent-handle-level)
2192                 (if (or debug-on-error debug-on-quit)
2193                     (gnus-agent-fetch-group-1
2194                      group gnus-command-method)
2195                   (condition-case err
2196                       (gnus-agent-fetch-group-1
2197                        group gnus-command-method)
2198                     (error
2199                      (unless (funcall gnus-agent-confirmation-function
2200                                       (format "Error %s while fetching session.  Should gnus continue? "
2201                                               (error-message-string err)))
2202                        (error "Cannot fetch articles into the Gnus agent")))
2203                     (quit
2204                      (gnus-agent-regenerate-group group)
2205                      (unless (funcall gnus-agent-confirmation-function
2206                                       (format
2207                                        "%s while fetching session.  Should gnus continue? "
2208                                        (error-message-string err)))
2209                        (signal 'quit
2210                                "Cannot fetch articles into the Gnus agent")))))))))
2211         (setq methods (cdr methods)))
2212       (gnus-run-hooks 'gnus-agent-fetched-hook)
2213       (gnus-message 6 "Finished fetching articles into the Gnus agent"))))
2214
2215 (defun gnus-agent-fetch-group-1 (group method)
2216   "Fetch GROUP."
2217   (let ((gnus-command-method method)
2218         (gnus-newsgroup-name group)
2219         (gnus-newsgroup-dependencies gnus-newsgroup-dependencies)
2220         (gnus-newsgroup-headers gnus-newsgroup-headers)
2221         (gnus-newsgroup-scored gnus-newsgroup-scored)
2222         (gnus-use-cache gnus-use-cache)
2223         (gnus-summary-expunge-below gnus-summary-expunge-below)
2224         (gnus-summary-mark-below gnus-summary-mark-below)
2225         (gnus-orphan-score gnus-orphan-score)
2226         ;; Maybe some other gnus-summary local variables should also
2227         ;; be put here.
2228
2229         gnus-headers
2230         gnus-score
2231         articles arts
2232         category predicate info marks score-param
2233         )
2234     (unless (gnus-check-group group)
2235       (error "Can't open server for %s" group))
2236
2237     ;; Fetch headers.
2238     (when (or gnus-newsgroup-active
2239               (gnus-active group)
2240               (gnus-activate-group group))
2241       (let ((marked-articles gnus-newsgroup-downloadable))
2242         ;; Identify the articles marked for download
2243         (unless gnus-newsgroup-active
2244           ;; The variable gnus-newsgroup-active was selected as I need
2245           ;; a gnus-summary local variable that is NOT bound to any
2246           ;; value (its global value should default to nil).
2247           (dolist (mark gnus-agent-download-marks)
2248             (let ((arts (cdr (assq mark (gnus-info-marks
2249                                          (setq info (gnus-get-info group)))))))
2250               (when arts
2251                 (setq marked-articles (nconc (gnus-uncompress-range arts)
2252                                              marked-articles))
2253                 ))))
2254         (setq marked-articles (sort marked-articles '<))
2255
2256         ;; Fetch any new articles from the server
2257         (setq articles (gnus-agent-fetch-headers group))
2258
2259         ;; Merge new articles with marked
2260         (setq articles (sort (append marked-articles articles) '<))
2261
2262         (when articles
2263           ;; Parse them and see which articles we want to fetch.
2264           (setq gnus-newsgroup-dependencies
2265                 (or gnus-newsgroup-dependencies
2266                     (make-vector (length articles) 0)))
2267           (setq gnus-newsgroup-headers
2268                 (or gnus-newsgroup-headers
2269                     (gnus-get-newsgroup-headers-xover articles nil nil
2270                                                       group)))
2271           ;; `gnus-agent-overview-buffer' may be killed for
2272           ;; timeout reason.  If so, recreate it.
2273           (gnus-agent-create-buffer)
2274
2275           ;; Figure out how to select articles in this group
2276           (setq category (gnus-group-category group))
2277
2278           (setq predicate
2279                 (gnus-get-predicate
2280                  (gnus-agent-find-parameter group 'agent-predicate)))
2281
2282           ;; If the selection predicate requires scoring, score each header
2283           (unless (memq predicate '(gnus-agent-true gnus-agent-false))
2284             (let ((score-param
2285                    (gnus-agent-find-parameter group 'agent-score-file)))
2286               ;; Translate score-param into real one
2287               (cond
2288                ((not score-param))
2289                ((eq score-param 'file)
2290                 (setq score-param (gnus-all-score-files group)))
2291                ((stringp (car score-param)))
2292                (t
2293                 (setq score-param (list (list score-param)))))
2294               (when score-param
2295                 (gnus-score-headers score-param))))
2296
2297           (unless (and (eq predicate 'gnus-agent-false)
2298                        (not marked-articles))
2299             (let ((arts (list nil)))
2300               (let ((arts-tail arts)
2301                     (alist (gnus-agent-load-alist group))
2302                     (marked-articles marked-articles)
2303                     (gnus-newsgroup-headers gnus-newsgroup-headers))
2304                 (while (setq gnus-headers (pop gnus-newsgroup-headers))
2305                   (let ((num (mail-header-number gnus-headers)))
2306                     ;; Determine if this article is already in the cache
2307                     (while (and alist
2308                                 (> num (caar alist)))
2309                       (setq alist (cdr alist)))
2310
2311                     (unless (and (eq num (caar alist))
2312                                  (cdar alist))
2313
2314                       ;; Determine if this article was marked for download.
2315                       (while (and marked-articles
2316                                   (> num (car marked-articles)))
2317                         (setq marked-articles
2318                               (cdr marked-articles)))
2319
2320                       ;; When this article is marked, or selected by the
2321                       ;; predicate, add it to the download list
2322                       (when (or (eq num (car marked-articles))
2323                                 (let ((gnus-score
2324                                        (or (cdr
2325                                             (assq num gnus-newsgroup-scored))
2326                                            gnus-summary-default-score))
2327                                       (gnus-agent-long-article
2328                                        (gnus-agent-find-parameter
2329                                         group 'agent-long-article))
2330                                       (gnus-agent-short-article
2331                                        (gnus-agent-find-parameter
2332                                         group 'agent-short-article))
2333                                       (gnus-agent-low-score
2334                                        (gnus-agent-find-parameter
2335                                         group 'agent-low-score))
2336                                       (gnus-agent-high-score
2337                                        (gnus-agent-find-parameter
2338                                         group 'agent-high-score))
2339                                       (gnus-agent-expire-days
2340                                        (gnus-agent-find-parameter
2341                                         group 'agent-days-until-old)))
2342                                   (funcall predicate)))
2343                         (gnus-agent-append-to-list arts-tail num))))))
2344
2345               (let (fetched-articles)
2346                 ;; Fetch all selected articles
2347                 (setq gnus-newsgroup-undownloaded
2348                       (gnus-sorted-ndifference
2349                        gnus-newsgroup-undownloaded
2350                        (setq fetched-articles
2351                              (if (cdr arts)
2352                                  (gnus-agent-fetch-articles group (cdr arts))
2353                                nil))))
2354
2355                 (let ((unfetched-articles
2356                        (gnus-sorted-ndifference (cdr arts) fetched-articles)))
2357                   (if gnus-newsgroup-active
2358                       ;; Update the summary buffer
2359                       (progn
2360                         (dolist (article marked-articles)
2361                           (gnus-summary-set-agent-mark article t))
2362                         (dolist (article fetched-articles)
2363                           (if gnus-agent-mark-unread-after-downloaded
2364                               (gnus-summary-mark-article
2365                                article gnus-unread-mark))
2366                           (when (gnus-summary-goto-subject article nil t)
2367                             (gnus-summary-update-download-mark article)))
2368                         (dolist (article unfetched-articles)
2369                           (gnus-summary-mark-article
2370                            article gnus-canceled-mark)))
2371
2372                     ;; Update the group buffer.
2373
2374                     ;; When some, or all, of the marked articles came
2375                     ;; from the download mark.  Remove that mark.  I
2376                     ;; didn't do this earlier as I only want to remove
2377                     ;; the marks after the fetch is completed.
2378
2379                     (dolist (mark gnus-agent-download-marks)
2380                       (when (eq mark 'download)
2381                         (let ((marked-arts
2382                                (assq mark (gnus-info-marks
2383                                            (setq info (gnus-get-info group))))))
2384                           (when (cdr marked-arts)
2385                             (setq marks
2386                                   (delq marked-arts (gnus-info-marks info)))
2387                             (gnus-info-set-marks info marks)))))
2388                     (let ((read (gnus-info-read
2389                                  (or info (setq info (gnus-get-info group))))))
2390                       (gnus-info-set-read
2391                        info (gnus-add-to-range read unfetched-articles)))
2392
2393                     (gnus-group-update-group group t)
2394                     (sit-for 0)
2395
2396                     (gnus-dribble-enter
2397                      (concat "(gnus-group-set-info '"
2398                              (gnus-prin1-to-string info)
2399                              ")"))))))))))))
2400
2401 ;;;
2402 ;;; Agent Category Mode
2403 ;;;
2404
2405 (defvar gnus-category-mode-hook nil
2406   "Hook run in `gnus-category-mode' buffers.")
2407
2408 (defvar gnus-category-line-format "     %(%20c%): %g\n"
2409   "Format of category lines.
2410
2411 Valid specifiers include:
2412 %c  Topic name (string)
2413 %g  The number of groups in the topic (integer)
2414
2415 General format specifiers can also be used.  See Info node
2416 `(gnus)Formatting Variables'.")
2417
2418 (defvar gnus-category-mode-line-format "Gnus: %%b"
2419   "The format specification for the category mode line.")
2420
2421 (defvar gnus-agent-predicate 'false
2422   "The selection predicate used when no other source is available.")
2423
2424 (defvar gnus-agent-short-article 100
2425   "Articles that have fewer lines than this are short.")
2426
2427 (defvar gnus-agent-long-article 200
2428   "Articles that have more lines than this are long.")
2429
2430 (defvar gnus-agent-low-score 0
2431   "Articles that have a score lower than this have a low score.")
2432
2433 (defvar gnus-agent-high-score 0
2434   "Articles that have a score higher than this have a high score.")
2435
2436
2437 ;;; Internal variables.
2438
2439 (defvar gnus-category-buffer "*Agent Category*")
2440
2441 (defvar gnus-category-line-format-alist
2442   `((?c gnus-tmp-name ?s)
2443     (?g gnus-tmp-groups ?d)))
2444
2445 (defvar gnus-category-mode-line-format-alist
2446   `((?u user-defined ?s)))
2447
2448 (defvar gnus-category-line-format-spec nil)
2449 (defvar gnus-category-mode-line-format-spec nil)
2450
2451 (defvar gnus-category-mode-map nil)
2452 (put 'gnus-category-mode 'mode-class 'special)
2453
2454 (unless gnus-category-mode-map
2455   (setq gnus-category-mode-map (make-sparse-keymap))
2456   (suppress-keymap gnus-category-mode-map)
2457
2458   (gnus-define-keys gnus-category-mode-map
2459     "q" gnus-category-exit
2460     "k" gnus-category-kill
2461     "c" gnus-category-copy
2462     "a" gnus-category-add
2463     "e" gnus-agent-customize-category
2464     "p" gnus-category-edit-predicate
2465     "g" gnus-category-edit-groups
2466     "s" gnus-category-edit-score
2467     "l" gnus-category-list
2468
2469     "\C-c\C-i" gnus-info-find-node
2470     "\C-c\C-b" gnus-bug))
2471
2472 (defvar gnus-category-menu-hook nil
2473   "*Hook run after the creation of the menu.")
2474
2475 (defun gnus-category-make-menu-bar ()
2476   (gnus-turn-off-edit-menu 'category)
2477   (unless (boundp 'gnus-category-menu)
2478     (easy-menu-define
2479      gnus-category-menu gnus-category-mode-map ""
2480      '("Categories"
2481        ["Add" gnus-category-add t]
2482        ["Kill" gnus-category-kill t]
2483        ["Copy" gnus-category-copy t]
2484        ["Edit category" gnus-agent-customize-category t]
2485        ["Edit predicate" gnus-category-edit-predicate t]
2486        ["Edit score" gnus-category-edit-score t]
2487        ["Edit groups" gnus-category-edit-groups t]
2488        ["Exit" gnus-category-exit t]))
2489
2490     (gnus-run-hooks 'gnus-category-menu-hook)))
2491
2492 (defun gnus-category-mode ()
2493   "Major mode for listing and editing agent categories.
2494
2495 All normal editing commands are switched off.
2496 \\<gnus-category-mode-map>
2497 For more in-depth information on this mode, read the manual
2498 \(`\\[gnus-info-find-node]').
2499
2500 The following commands are available:
2501
2502 \\{gnus-category-mode-map}"
2503   (interactive)
2504   (when (gnus-visual-p 'category-menu 'menu)
2505     (gnus-category-make-menu-bar))
2506   (kill-all-local-variables)
2507   (gnus-simplify-mode-line)
2508   (setq major-mode 'gnus-category-mode)
2509   (setq mode-name "Category")
2510   (gnus-set-default-directory)
2511   (setq mode-line-process nil)
2512   (use-local-map gnus-category-mode-map)
2513   (buffer-disable-undo)
2514   (setq truncate-lines t)
2515   (setq buffer-read-only t)
2516   (gnus-run-hooks 'gnus-category-mode-hook))
2517
2518 (defalias 'gnus-category-position-point 'gnus-goto-colon)
2519
2520 (defun gnus-category-insert-line (category)
2521   (let* ((gnus-tmp-name (format "%s" (car category)))
2522          (gnus-tmp-groups (length (gnus-agent-cat-groups category))))
2523     (beginning-of-line)
2524     (gnus-add-text-properties
2525      (point)
2526      (prog1 (1+ (point))
2527        ;; Insert the text.
2528        (eval gnus-category-line-format-spec))
2529      (list 'gnus-category gnus-tmp-name))))
2530
2531 (defun gnus-enter-category-buffer ()
2532   "Go to the Category buffer."
2533   (interactive)
2534   (gnus-category-setup-buffer)
2535   (gnus-configure-windows 'category)
2536   (gnus-category-prepare))
2537
2538 (defun gnus-category-setup-buffer ()
2539   (unless (get-buffer gnus-category-buffer)
2540     (save-excursion
2541       (set-buffer (gnus-get-buffer-create gnus-category-buffer))
2542       (gnus-category-mode))))
2543
2544 (defun gnus-category-prepare ()
2545   (gnus-set-format 'category-mode)
2546   (gnus-set-format 'category t)
2547   (let ((alist gnus-category-alist)
2548         (buffer-read-only nil))
2549     (erase-buffer)
2550     (while alist
2551       (gnus-category-insert-line (pop alist)))
2552     (goto-char (point-min))
2553     (gnus-category-position-point)))
2554
2555 (defun gnus-category-name ()
2556   (or (intern (get-text-property (point-at-bol) 'gnus-category))
2557       (error "No category on the current line")))
2558
2559 (defun gnus-category-read ()
2560   "Read the category alist."
2561   (setq gnus-category-alist
2562         (or
2563          (with-temp-buffer
2564            (ignore-errors
2565             (nnheader-insert-file-contents (nnheader-concat gnus-agent-directory "lib/categories"))
2566             (goto-char (point-min))
2567             ;; This code isn't temp, it will be needed so long as
2568             ;; anyone may be migrating from an older version.
2569
2570             ;; Once we're certain that people will not revert to an
2571             ;; earlier version, we can take out the old-list code in
2572             ;; gnus-category-write.
2573             (let* ((old-list (read (current-buffer)))
2574                    (new-list (ignore-errors (read (current-buffer)))))
2575               (if new-list
2576                   new-list
2577                 ;; Convert from a positional list to an alist.
2578                 (mapcar
2579                  (lambda (c)
2580                    (setcdr c
2581                            (delq nil
2582                                  (gnus-mapcar
2583                                   (lambda (valu symb)
2584                                     (if valu
2585                                         (cons symb valu)))
2586                                   (cdr c)
2587                                   '(agent-predicate agent-score-file agent-groups))))
2588                    c)
2589                  old-list)))))
2590          (list (gnus-agent-cat-make 'default 'short)))))
2591
2592 (defun gnus-category-write ()
2593   "Write the category alist."
2594   (setq gnus-category-predicate-cache nil
2595         gnus-category-group-cache nil)
2596   (gnus-make-directory (nnheader-concat gnus-agent-directory "lib"))
2597   (with-temp-file (nnheader-concat gnus-agent-directory "lib/categories")
2598     ;; This prin1 is temporary.  It exists so that people can revert
2599     ;; to an earlier version of gnus-agent.
2600     (prin1 (mapcar (lambda (c)
2601               (list (car c)
2602                     (cdr (assoc 'agent-predicate c))
2603                     (cdr (assoc 'agent-score-file c))
2604                     (cdr (assoc 'agent-groups c))))
2605                    gnus-category-alist)
2606            (current-buffer))
2607     (newline)
2608     (prin1 gnus-category-alist (current-buffer))))
2609
2610 (defun gnus-category-edit-predicate (category)
2611   "Edit the predicate for CATEGORY."
2612   (interactive (list (gnus-category-name)))
2613   (let ((info (assq category gnus-category-alist)))
2614     (gnus-edit-form
2615      (gnus-agent-cat-predicate info)
2616      (format "Editing the select predicate for category %s" category)
2617      `(lambda (predicate)
2618         ;; Avoid run-time execution of setf form
2619         ;; (setf (gnus-agent-cat-predicate (assq ',category gnus-category-alist))
2620         ;;       predicate)
2621         ;; use its expansion instead:
2622         (gnus-agent-cat-set-property (assq ',category gnus-category-alist)
2623                                      'agent-predicate predicate)
2624
2625         (gnus-category-write)
2626         (gnus-category-list)))))
2627
2628 (defun gnus-category-edit-score (category)
2629   "Edit the score expression for CATEGORY."
2630   (interactive (list (gnus-category-name)))
2631   (let ((info (assq category gnus-category-alist)))
2632     (gnus-edit-form
2633      (gnus-agent-cat-score-file info)
2634      (format "Editing the score expression for category %s" category)
2635      `(lambda (score-file)
2636         ;; Avoid run-time execution of setf form
2637         ;; (setf (gnus-agent-cat-score-file (assq ',category gnus-category-alist))
2638         ;;       score-file)
2639         ;; use its expansion instead:
2640         (gnus-agent-cat-set-property (assq ',category gnus-category-alist)
2641                                      'agent-score-file score-file)
2642
2643         (gnus-category-write)
2644         (gnus-category-list)))))
2645
2646 (defun gnus-category-edit-groups (category)
2647   "Edit the group list for CATEGORY."
2648   (interactive (list (gnus-category-name)))
2649   (let ((info (assq category gnus-category-alist)))
2650     (gnus-edit-form
2651      (gnus-agent-cat-groups info)
2652      (format "Editing the group list for category %s" category)
2653      `(lambda (groups)
2654         ;; Avoid run-time execution of setf form
2655         ;; (setf (gnus-agent-cat-groups (assq ',category gnus-category-alist))
2656         ;;       groups)
2657         ;; use its expansion instead:
2658         (gnus-agent-set-cat-groups (assq ',category gnus-category-alist)
2659                                    groups)
2660
2661         (gnus-category-write)
2662         (gnus-category-list)))))
2663
2664 (defun gnus-category-kill (category)
2665   "Kill the current category."
2666   (interactive (list (gnus-category-name)))
2667   (let ((info (assq category gnus-category-alist))
2668         (buffer-read-only nil))
2669     (gnus-delete-line)
2670     (setq gnus-category-alist (delq info gnus-category-alist))
2671     (gnus-category-write)))
2672
2673 (defun gnus-category-copy (category to)
2674   "Copy the current category."
2675   (interactive (list (gnus-category-name) (intern (read-string "New name: "))))
2676   (let ((info (assq category gnus-category-alist)))
2677     (push (let ((newcat (gnus-copy-sequence info)))
2678             (setf (gnus-agent-cat-name newcat) to)
2679             (setf (gnus-agent-cat-groups newcat) nil)
2680             newcat)
2681           gnus-category-alist)
2682     (gnus-category-write)
2683     (gnus-category-list)))
2684
2685 (defun gnus-category-add (category)
2686   "Create a new category."
2687   (interactive "SCategory name: ")
2688   (when (assq category gnus-category-alist)
2689     (error "Category %s already exists" category))
2690   (push (gnus-agent-cat-make category)
2691         gnus-category-alist)
2692   (gnus-category-write)
2693   (gnus-category-list))
2694
2695 (defun gnus-category-list ()
2696   "List all categories."
2697   (interactive)
2698   (gnus-category-prepare))
2699
2700 (defun gnus-category-exit ()
2701   "Return to the group buffer."
2702   (interactive)
2703   (kill-buffer (current-buffer))
2704   (gnus-configure-windows 'group t))
2705
2706 ;; To avoid having 8-bit characters in the source file.
2707 (defvar gnus-category-not (list '! 'not (intern (format "%c" 172))))
2708
2709 (defvar gnus-category-predicate-alist
2710   '((spam . gnus-agent-spam-p)
2711     (short . gnus-agent-short-p)
2712     (long . gnus-agent-long-p)
2713     (low . gnus-agent-low-scored-p)
2714     (high . gnus-agent-high-scored-p)
2715     (read . gnus-agent-read-p)
2716     (true . gnus-agent-true)
2717     (false . gnus-agent-false))
2718   "Mapping from short score predicate symbols to predicate functions.")
2719
2720 (defun gnus-agent-spam-p ()
2721   "Say whether an article is spam or not."
2722   (unless gnus-agent-spam-hashtb
2723     (setq gnus-agent-spam-hashtb (gnus-make-hashtable 1000)))
2724   (if (not (equal (mail-header-references gnus-headers) ""))
2725       nil
2726     (let ((string (gnus-simplify-subject (mail-header-subject gnus-headers))))
2727       (prog1
2728           (gnus-gethash string gnus-agent-spam-hashtb)
2729         (gnus-sethash string t gnus-agent-spam-hashtb)))))
2730
2731 (defun gnus-agent-short-p ()
2732   "Say whether an article is short or not."
2733   (< (mail-header-lines gnus-headers) gnus-agent-short-article))
2734
2735 (defun gnus-agent-long-p ()
2736   "Say whether an article is long or not."
2737   (> (mail-header-lines gnus-headers) gnus-agent-long-article))
2738
2739 (defun gnus-agent-low-scored-p ()
2740   "Say whether an article has a low score or not."
2741   (< gnus-score gnus-agent-low-score))
2742
2743 (defun gnus-agent-high-scored-p ()
2744   "Say whether an article has a high score or not."
2745   (> gnus-score gnus-agent-high-score))
2746
2747 (defun gnus-agent-read-p ()
2748   "Say whether an article is read or not."
2749   (gnus-member-of-range (mail-header-number gnus-headers)
2750                         (gnus-info-read (gnus-get-info gnus-newsgroup-name))))
2751
2752 (defun gnus-category-make-function (predicate)
2753   "Make a function from PREDICATE."
2754   (let ((func (gnus-category-make-function-1 predicate)))
2755     (if (and (= (length func) 1)
2756              (symbolp (car func)))
2757         (car func)
2758       (gnus-byte-compile `(lambda () ,func)))))
2759
2760 (defun gnus-agent-true ()
2761   "Return t."
2762   t)
2763
2764 (defun gnus-agent-false ()
2765   "Return nil."
2766   nil)
2767
2768 (defun gnus-category-make-function-1 (predicate)
2769   "Make a function from PREDICATE."
2770   (cond
2771    ;; Functions are just returned as is.
2772    ((or (symbolp predicate)
2773         (functionp predicate))
2774     `(,(or (cdr (assq predicate gnus-category-predicate-alist))
2775            predicate)))
2776    ;; More complex predicate.
2777    ((consp predicate)
2778     `(,(cond
2779         ((memq (car predicate) '(& and))
2780          'and)
2781         ((memq (car predicate) '(| or))
2782          'or)
2783         ((memq (car predicate) gnus-category-not)
2784          'not))
2785       ,@(mapcar 'gnus-category-make-function-1 (cdr predicate))))
2786    (t
2787     (error "Unknown predicate type: %s" predicate))))
2788
2789 (defun gnus-get-predicate (predicate)
2790   "Return the function implementing PREDICATE."
2791   (or (cdr (assoc predicate gnus-category-predicate-cache))
2792       (let ((func (gnus-category-make-function predicate)))
2793         (setq gnus-category-predicate-cache
2794               (nconc gnus-category-predicate-cache
2795                      (list (cons predicate func))))
2796         func)))
2797
2798 (defun gnus-predicate-implies-unread (predicate)
2799   "Say whether PREDICATE implies unread articles only.
2800 It is okay to miss some cases, but there must be no false positives.
2801 That is, if this predicate returns true, then indeed the predicate must
2802 return only unread articles."
2803   (eq t (gnus-function-implies-unread-1 
2804          (gnus-category-make-function-1 predicate))))
2805
2806 (defun gnus-function-implies-unread-1 (function)
2807   "Recursively evaluate a predicate function to determine whether it can select
2808 any read articles.  Returns t if the function is known to never
2809 return read articles, nil when it is known to always return read
2810 articles, and t_nil when the function may return both read and unread
2811 articles."
2812   (let ((func (car function))
2813         (args (mapcar 'gnus-function-implies-unread-1 (cdr function))))
2814     (cond ((eq func 'and)
2815            (cond ((memq t args) ; if any argument returns only unread articles
2816                   ;; then that argument constrains the result to only unread articles.
2817                   t)
2818                  ((memq 't_nil args) ; if any argument is indeterminate
2819                   ;; then the result is indeterminate
2820                   't_nil)))
2821           ((eq func 'or)
2822            (cond ((memq nil args) ; if any argument returns read articles
2823                   ;; then that argument ensures that the results includes read articles.
2824                   nil)
2825                  ((memq 't_nil args) ; if any argument is indeterminate
2826                   ;; then that argument ensures that the results are indeterminate
2827                   't_nil)
2828                  (t ; if all arguments return only unread articles
2829                   ;; then the result returns only unread articles
2830                   t)))
2831           ((eq func 'not)
2832            (cond ((eq (car args) 't_nil) ; if the argument is indeterminate
2833                   ; then the result is indeterminate
2834                   (car args))
2835                  (t ; otherwise
2836                   ; toggle the result to be the opposite of the argument
2837                   (not (car args)))))
2838           ((eq func 'gnus-agent-read-p)
2839            nil) ; The read predicate NEVER returns unread articles
2840           ((eq func 'gnus-agent-false)
2841            t) ; The false predicate returns t as the empty set excludes all read articles
2842           ((eq func 'gnus-agent-true)
2843            nil) ; The true predicate ALWAYS returns read articles
2844           ((catch 'found-match
2845              (let ((alist gnus-category-predicate-alist))
2846                (while alist
2847                  (if (eq func (cdar alist))
2848                      (throw 'found-match t)
2849                    (setq alist (cdr alist))))))
2850            't_nil) ; All other predicates return read and unread articles
2851           (t
2852            (error "Unknown predicate function: %s" function)))))
2853
2854 (defun gnus-group-category (group)
2855   "Return the category GROUP belongs to."
2856   (unless gnus-category-group-cache
2857     (setq gnus-category-group-cache (gnus-make-hashtable 1000))
2858     (let ((cs gnus-category-alist)
2859           groups cat)
2860       (while (setq cat (pop cs))
2861         (setq groups (gnus-agent-cat-groups cat))
2862         (while groups
2863           (gnus-sethash (pop groups) cat gnus-category-group-cache)))))
2864   (or (gnus-gethash group gnus-category-group-cache)
2865       (assq 'default gnus-category-alist)))
2866
2867 (defun gnus-agent-expire-group (group &optional articles force)
2868   "Expire all old articles in GROUP.
2869 If you want to force expiring of certain articles, this function can
2870 take ARTICLES, and FORCE parameters as well.
2871
2872 The articles on which the expiration process runs are selected as follows:
2873   if ARTICLES is null, all read and unmarked articles.
2874   if ARTICLES is t, all articles.
2875   if ARTICLES is a list, just those articles.
2876 FORCE is equivalent to setting the expiration predicates to true."
2877   (interactive
2878    (list (let ((def (or (gnus-group-group-name)
2879                         gnus-newsgroup-name)))
2880            (let ((select (read-string (if def
2881                                           (concat "Group Name ("
2882                                                   def "): ")
2883                                         "Group Name: "))))
2884              (if (and (equal "" select)
2885                       def)
2886                  def
2887                select)))))
2888
2889   (if (not group)
2890       (gnus-agent-expire articles group force)
2891     (let ( ;; Bind gnus-agent-expire-stats to enable tracking of
2892           ;; expiration statistics of this single group
2893           (gnus-agent-expire-stats (list 0 0 0.0)))
2894       (if (or (not (eq articles t))
2895               (yes-or-no-p
2896                (concat "Are you sure that you want to "
2897                        "expire all articles in " group ".")))
2898           (let ((gnus-command-method (gnus-find-method-for-group group))
2899                 (overview (gnus-get-buffer-create " *expire overview*"))
2900                 orig)
2901             (unwind-protect
2902                 (let ((active-file (gnus-agent-lib-file "active")))
2903                   (when (file-exists-p active-file)
2904                     (with-temp-buffer
2905                       (nnheader-insert-file-contents active-file)
2906                       (gnus-active-to-gnus-format
2907                        gnus-command-method
2908                        (setq orig (gnus-make-hashtable
2909                                    (count-lines (point-min) (point-max))))))
2910                     (save-excursion
2911                       (gnus-agent-expire-group-1
2912                        group overview (gnus-gethash-safe group orig)
2913                        articles force))))
2914               (kill-buffer overview))))
2915       (gnus-message 4 (gnus-agent-expire-done-message)))))
2916
2917 (defun gnus-agent-expire-group-1 (group overview active articles force)
2918   ;; Internal function - requires caller to have set
2919   ;; gnus-command-method, initialized overview buffer, and to have
2920   ;; provided a non-nil active
2921
2922   (let ((dir (gnus-agent-group-pathname group)))
2923     (gnus-agent-with-refreshed-group 
2924      group
2925      (when (boundp 'gnus-agent-expire-current-dirs)
2926        (set 'gnus-agent-expire-current-dirs 
2927             (cons dir 
2928                   (symbol-value 'gnus-agent-expire-current-dirs))))
2929
2930      (if (and (not force)
2931               (eq 'DISABLE (gnus-agent-find-parameter group 
2932                                                       'agent-enable-expiration)))
2933          (gnus-message 5 "Expiry skipping over %s" group)
2934        (gnus-message 5 "Expiring articles in %s" group)
2935        (gnus-agent-load-alist group)
2936        (let* ((bytes-freed 0)
2937               (size-files-deleted 0.0)
2938               (files-deleted 0)
2939               (nov-entries-deleted 0)
2940               (info (gnus-get-info group))
2941               (alist gnus-agent-article-alist)
2942               (day (- (time-to-days (current-time))
2943                       (gnus-agent-find-parameter group 'agent-days-until-old)))
2944               (specials (if (and alist
2945                                  (not force))
2946                             ;; This could be a bit of a problem.  I need to
2947                             ;; keep the last article to avoid refetching
2948                             ;; headers when using nntp in the backend.  At
2949                             ;; the same time, if someone uses a backend
2950                             ;; that supports article moving then I may have
2951                             ;; to remove the last article to complete the
2952                             ;; move.  Right now, I'm going to assume that
2953                             ;; FORCE overrides specials.
2954                             (list (caar (last alist)))))
2955               (unreads ;; Articles that are excluded from the
2956                ;; expiration process
2957                (cond (gnus-agent-expire-all
2958                       ;; All articles are marked read by global decree
2959                       nil)
2960                      ((eq articles t)
2961                       ;; All articles are marked read by function
2962                       ;; parameter
2963                       nil)
2964                      ((not articles)
2965                       ;; Unread articles are marked protected from
2966                       ;; expiration Don't call
2967                       ;; gnus-list-of-unread-articles as it returns
2968                       ;; articles that have not been fetched into the
2969                       ;; agent.
2970                       (ignore-errors
2971                         (gnus-agent-unread-articles group)))
2972                      (t
2973                       ;; All articles EXCEPT those named by the caller
2974                       ;; are protected from expiration
2975                       (gnus-sorted-difference
2976                        (gnus-uncompress-range
2977                         (cons (caar alist)
2978                               (caar (last alist))))
2979                        (sort articles '<)))))
2980               (marked ;; More articles that are excluded from the
2981                ;; expiration process
2982                (cond (gnus-agent-expire-all
2983                       ;; All articles are unmarked by global decree
2984                       nil)
2985                      ((eq articles t)
2986                       ;; All articles are unmarked by function
2987                       ;; parameter
2988                       nil)
2989                      (articles
2990                       ;; All articles may as well be unmarked as the
2991                       ;; unreads list already names the articles we are
2992                       ;; going to keep
2993                       nil)
2994                      (t
2995                       ;; Ticked and/or dormant articles are excluded
2996                       ;; from expiration
2997                       (nconc
2998                        (gnus-uncompress-range
2999                         (cdr (assq 'tick (gnus-info-marks info))))
3000                        (gnus-uncompress-range
3001                         (cdr (assq 'dormant
3002                                    (gnus-info-marks info))))))))
3003               (nov-file (concat dir ".overview"))
3004               (cnt 0)
3005               (completed -1)
3006               dlist
3007               type)
3008
3009          ;; The normal article alist contains elements that look like
3010          ;; (article# .  fetch_date) I need to combine other
3011          ;; information with this list.  For example, a flag indicating
3012          ;; that a particular article MUST BE KEPT.  To do this, I'm
3013          ;; going to transform the elements to look like (article#
3014          ;; fetch_date keep_flag NOV_entry_marker) Later, I'll reverse
3015          ;; the process to generate the expired article alist.
3016
3017          ;; Convert the alist elements to (article# fetch_date nil
3018          ;; nil).
3019          (setq dlist (mapcar (lambda (e)
3020                                (list (car e) (cdr e) nil nil)) alist))
3021
3022          ;; Convert the keep lists to elements that look like (article#
3023          ;; nil keep_flag nil) then append it to the expanded dlist
3024          ;; These statements are sorted by ascending precidence of the
3025          ;; keep_flag.
3026          (setq dlist (nconc dlist
3027                             (mapcar (lambda (e)
3028                                       (list e nil 'unread  nil))
3029                                     unreads)))
3030          (setq dlist (nconc dlist
3031                             (mapcar (lambda (e)
3032                                       (list e nil 'marked  nil))
3033                                     marked)))
3034          (setq dlist (nconc dlist
3035                             (mapcar (lambda (e)
3036                                       (list e nil 'special nil))
3037                                     specials)))
3038
3039          (set-buffer overview)
3040          (erase-buffer)
3041          (buffer-disable-undo)
3042          (when (file-exists-p nov-file)
3043            (gnus-message 7 "gnus-agent-expire: Loading overview...")
3044            (nnheader-insert-file-contents nov-file)
3045            (goto-char (point-min))
3046
3047            (let (p)
3048              (while (< (setq p (point)) (point-max))
3049                (condition-case nil
3050                    ;; If I successfully read an integer (the plus zero
3051                    ;; ensures a numeric type), prepend a marker entry
3052                    ;; to the list
3053                    (push (list (+ 0 (read (current-buffer))) nil nil
3054                                (set-marker (make-marker) p))
3055                          dlist)
3056                  (error
3057                   (gnus-message 1 "gnus-agent-expire: read error \
3058 occurred when reading expression at %s in %s.  Skipping to next \
3059 line." (point) nov-file)))
3060                ;; Whether I succeeded, or failed, it doesn't matter.
3061                ;; Move to the next line then try again.
3062                (forward-line 1)))
3063
3064            (gnus-message
3065             7 "gnus-agent-expire: Loading overview... Done"))
3066          (set-buffer-modified-p nil)
3067
3068          ;; At this point, all of the information is in dlist.  The
3069          ;; only problem is that much of it is spread across multiple
3070          ;; entries.  Sort then MERGE!!
3071          (gnus-message 7 "gnus-agent-expire: Sorting entries... ")
3072          ;; If two entries have the same article-number then sort by
3073          ;; ascending keep_flag.
3074          (let ((special 0)
3075                (marked 1)
3076                (unread 2))
3077            (setq dlist
3078                  (sort dlist
3079                        (lambda (a b)
3080                          (cond ((< (nth 0 a) (nth 0 b))
3081                                 t)
3082                                ((> (nth 0 a) (nth 0 b))
3083                                 nil)
3084                                (t
3085                                 (let ((a (or (symbol-value (nth 2 a))
3086                                              3))
3087                                       (b (or (symbol-value (nth 2 b))
3088                                              3)))
3089                                   (<= a b))))))))
3090          (gnus-message 7 "gnus-agent-expire: Sorting entries... Done")
3091          (gnus-message 7 "gnus-agent-expire: Merging entries... ")
3092          (let ((dlist dlist))
3093            (while (cdr dlist)           ; I'm not at the end-of-list
3094              (if (eq (caar dlist) (caadr dlist))
3095                  (let ((first (cdr (car dlist)))
3096                        (secnd (cdr (cadr dlist))))
3097                    (setcar first (or (car first)
3098                                      (car secnd))) ; fetch_date
3099                    (setq first (cdr first)
3100                          secnd (cdr secnd))
3101                    (setcar first (or (car first)
3102                                      (car secnd))) ; Keep_flag
3103                    (setq first (cdr first)
3104                          secnd (cdr secnd))
3105                    (setcar first (or (car first)
3106                                      (car secnd))) ; NOV_entry_marker
3107
3108                    (setcdr dlist (cddr dlist)))
3109                (setq dlist (cdr dlist)))))
3110          (gnus-message 7 "gnus-agent-expire: Merging entries... Done")
3111
3112          (let* ((len (float (length dlist)))
3113                 (alist (list nil))
3114                 (tail-alist alist))
3115            (while dlist
3116              (let ((new-completed (truncate (* 100.0
3117                                                (/ (setq cnt (1+ cnt))
3118                                                   len))))
3119                    message-log-max)
3120                (when (> new-completed completed)
3121                  (setq completed new-completed)
3122                  (gnus-message 7 "%3d%% completed..."  completed)))
3123              (let* ((entry          (car dlist))
3124                     (article-number (nth 0 entry))
3125                     (fetch-date     (nth 1 entry))
3126                     (keep           (nth 2 entry))
3127                     (marker         (nth 3 entry)))
3128
3129                (cond
3130                 ;; Kept articles are unread, marked, or special.
3131                 (keep
3132                  (gnus-agent-message 10
3133                                      "gnus-agent-expire: %s:%d: Kept %s article%s."
3134                                      group article-number keep (if fetch-date " and file" ""))
3135                  (when fetch-date
3136                    (unless (file-exists-p
3137                             (concat dir (number-to-string
3138                                          article-number)))
3139                      (setf (nth 1 entry) nil)
3140                      (gnus-agent-message 3 "gnus-agent-expire cleared \
3141 download flag on %s:%d as the cached article file is missing."
3142                                          group (caar dlist)))
3143                    (unless marker
3144                      (gnus-message 1 "gnus-agent-expire detected a \
3145 missing NOV entry.  Run gnus-agent-regenerate-group to restore it.")))
3146                  (gnus-agent-append-to-list
3147                   tail-alist
3148                   (cons article-number fetch-date)))
3149
3150                 ;; The following articles are READ, UNMARKED, and
3151                 ;; ORDINARY.  See if they can be EXPIRED!!!
3152                 ((setq type
3153                        (cond
3154                         ((not (integerp fetch-date))
3155                          'read) ;; never fetched article (may expire
3156                         ;; right now)
3157                         ((not (file-exists-p
3158                                (concat dir (number-to-string
3159                                             article-number))))
3160                          (setf (nth 1 entry) nil)
3161                          'externally-expired) ;; Can't find the cached
3162                         ;; article.  Handle case
3163                         ;; as though this article
3164                         ;; was never fetched.
3165
3166                         ;; We now have the arrival day, so we see
3167                         ;; whether it's old enough to be expired.
3168                         ((< fetch-date day)
3169                          'expired)
3170                         (force
3171                          'forced)))
3172
3173                  ;; I found some reason to expire this entry.
3174
3175                  (let ((actions nil))
3176                    (when (memq type '(forced expired))
3177                      (ignore-errors     ; Just being paranoid.
3178                        (let* ((file-name (nnheader-concat dir (number-to-string
3179                                                                article-number)))
3180                               (size (float (nth 7 (file-attributes file-name)))))
3181                          (incf bytes-freed size)
3182                          (incf size-files-deleted size)
3183                          (incf files-deleted)
3184                          (delete-file file-name))
3185                        (push "expired cached article" actions))
3186                      (setf (nth 1 entry) nil)
3187                      )
3188
3189                    (when marker
3190                      (push "NOV entry removed" actions)
3191                      (goto-char marker)
3192
3193                      (incf nov-entries-deleted)
3194
3195                      (let ((from (point-at-bol))
3196                            (to (progn (forward-line 1) (point))))
3197                        (incf bytes-freed (- to from))
3198                        (delete-region from to)))
3199
3200                    ;; If considering all articles is set, I can only
3201                    ;; expire article IDs that are no longer in the
3202                    ;; active range (That is, articles that preceed the
3203                    ;; first article in the new alist).
3204                    (if (and gnus-agent-consider-all-articles
3205                             (>= article-number (car active)))
3206                        ;; I have to keep this ID in the alist
3207                        (gnus-agent-append-to-list
3208                         tail-alist (cons article-number fetch-date))
3209                      (push (format "Removed %s article number from \
3210 article alist" type) actions))
3211
3212                    (when actions
3213                      (gnus-agent-message 8 "gnus-agent-expire: %s:%d: %s"
3214                                          group article-number
3215                                          (mapconcat 'identity actions ", ")))))
3216                 (t
3217                  (gnus-agent-message
3218                   10 "gnus-agent-expire: %s:%d: Article kept as \
3219 expiration tests failed." group article-number)
3220                  (gnus-agent-append-to-list
3221                   tail-alist (cons article-number fetch-date)))
3222                 )
3223
3224                ;; Clean up markers as I want to recycle this buffer
3225                ;; over several groups.
3226                (when marker
3227                  (set-marker marker nil))
3228
3229                (setq dlist (cdr dlist))))
3230
3231            (setq alist (cdr alist))
3232
3233            (let ((inhibit-quit t))
3234              (unless (equal alist gnus-agent-article-alist)
3235                (setq gnus-agent-article-alist alist)
3236                (gnus-agent-save-alist group))
3237
3238              (when (buffer-modified-p)
3239                (let ((coding-system-for-write
3240                       gnus-agent-file-coding-system))
3241                  (gnus-make-directory dir)
3242                  (write-region (point-min) (point-max) nov-file nil
3243                                'silent)
3244                  ;; clear the modified flag as that I'm not confused by
3245                  ;; its status on the next pass through this routine.
3246                  (set-buffer-modified-p nil)
3247                  (gnus-agent-update-view-total-fetched-for group t)))
3248
3249              (when (eq articles t)
3250                (gnus-summary-update-info))))
3251
3252          (when (boundp 'gnus-agent-expire-stats)
3253            (let ((stats (symbol-value 'gnus-agent-expire-stats)))
3254              (incf (nth 2 stats) bytes-freed)
3255              (incf (nth 1 stats) files-deleted)
3256              (incf (nth 0 stats) nov-entries-deleted)))
3257
3258          (gnus-agent-update-files-total-fetched-for group (- size-files-deleted)))))))
3259
3260 (defun gnus-agent-expire (&optional articles group force)
3261   "Expire all old articles.
3262 If you want to force expiring of certain articles, this function can
3263 take ARTICLES, GROUP and FORCE parameters as well.
3264
3265 The articles on which the expiration process runs are selected as follows:
3266   if ARTICLES is null, all read and unmarked articles.
3267   if ARTICLES is t, all articles.
3268   if ARTICLES is a list, just those articles.
3269 Setting GROUP will limit expiration to that group.
3270 FORCE is equivalent to setting the expiration predicates to true."
3271   (interactive)
3272   
3273   (if group
3274       (gnus-agent-expire-group group articles force)
3275     (if (or (not (eq articles t))
3276             (yes-or-no-p "Are you sure that you want to expire all \
3277 articles in every agentized group."))
3278         (let ((methods (gnus-agent-covered-methods))
3279               ;; Bind gnus-agent-expire-current-dirs to enable tracking
3280               ;; of agent directories.
3281               (gnus-agent-expire-current-dirs nil)
3282               ;; Bind gnus-agent-expire-stats to enable tracking of
3283               ;; expiration statistics across all groups
3284               (gnus-agent-expire-stats (list 0 0 0.0))
3285               gnus-command-method overview orig)
3286           (setq overview (gnus-get-buffer-create " *expire overview*"))
3287           (unwind-protect
3288               (while (setq gnus-command-method (pop methods))
3289                 (let ((active-file (gnus-agent-lib-file "active")))
3290                   (when (file-exists-p active-file)
3291                     (with-temp-buffer
3292                       (nnheader-insert-file-contents active-file)
3293                       (gnus-active-to-gnus-format
3294                        gnus-command-method
3295                        (setq orig (gnus-make-hashtable
3296                                    (count-lines (point-min) (point-max))))))
3297                     (dolist (expiring-group (gnus-groups-from-server
3298                                              gnus-command-method))
3299                       (let* ((active
3300                               (gnus-gethash-safe expiring-group orig)))
3301                                         
3302                         (when active
3303                           (save-excursion
3304                             (gnus-agent-expire-group-1
3305                              expiring-group overview active articles force))))))))
3306             (kill-buffer overview))
3307           (gnus-agent-expire-unagentized-dirs)
3308           (gnus-message 4 (gnus-agent-expire-done-message))))))
3309
3310 (defun gnus-agent-expire-done-message ()
3311   (if (and (> gnus-verbose 4)
3312            (boundp 'gnus-agent-expire-stats))
3313       (let* ((stats (symbol-value 'gnus-agent-expire-stats))
3314              (size (nth 2 stats))
3315             (units '(B KB MB GB)))
3316         (while (and (> size 1024.0)
3317                     (cdr units))
3318           (setq size (/ size 1024.0)
3319                 units (cdr units)))
3320
3321         (format "Expiry recovered %d NOV entries, deleted %d files,\
3322  and freed %f %s." 
3323                 (nth 0 stats) 
3324                 (nth 1 stats) 
3325                 size (car units)))
3326     "Expiry...done"))
3327
3328 (defun gnus-agent-expire-unagentized-dirs ()
3329   (when (and gnus-agent-expire-unagentized-dirs
3330              (boundp 'gnus-agent-expire-current-dirs))
3331     (let* ((keep (gnus-make-hashtable))
3332            ;; Formally bind gnus-agent-expire-current-dirs so that the
3333            ;; compiler will not complain about free references.
3334            (gnus-agent-expire-current-dirs
3335             (symbol-value 'gnus-agent-expire-current-dirs))
3336            dir)
3337
3338       (gnus-sethash gnus-agent-directory t keep)
3339       (while gnus-agent-expire-current-dirs
3340         (setq dir (pop gnus-agent-expire-current-dirs))
3341         (when (and (stringp dir)
3342                    (file-directory-p dir))
3343           (while (not (gnus-gethash dir keep))
3344             (gnus-sethash dir t keep)
3345             (setq dir (file-name-directory (directory-file-name dir))))))
3346
3347       (let* (to-remove
3348              checker
3349              (checker
3350               (function
3351                (lambda (d)
3352                  "Given a directory, check it and its subdirectories for 
3353               membership in the keep hash.  If it isn't found, add 
3354               it to to-remove." 
3355                  (let ((files (directory-files d))
3356                        file)
3357                    (while (setq file (pop files))
3358                      (cond ((equal file ".") ; Ignore self
3359                             nil)
3360                            ((equal file "..") ; Ignore parent
3361                             nil)
3362                            ((equal file ".overview") 
3363                             ;; Directory must contain .overview to be
3364                             ;; agent's cache of a group.
3365                             (let ((d (file-name-as-directory d))
3366                                   r)
3367                               ;; Search ancestor's for last directory NOT
3368                               ;; found in keep hash.
3369                               (while (not (gnus-gethash
3370                                            (setq d (file-name-directory d)) keep))
3371                                 (setq r d
3372                                       d (directory-file-name d)))
3373                               ;; if ANY ancestor was NOT in keep hash and
3374                               ;; it it's already in to-remove, add it to
3375                               ;; to-remove.                          
3376                               (if (and r
3377                                        (not (member r to-remove)))
3378                                   (push r to-remove))))
3379                            ((file-directory-p (setq file (nnheader-concat d file)))
3380                             (funcall checker file)))))))))
3381         (funcall checker (expand-file-name gnus-agent-directory))
3382
3383         (when (and to-remove
3384                    (or gnus-expert-user
3385                        (gnus-y-or-n-p
3386                         "gnus-agent-expire has identified local directories that are\
3387  not currently required by any agentized group.  Do you wish to consider\
3388  deleting them?")))
3389           (while to-remove
3390             (let ((dir (pop to-remove)))
3391               (if (gnus-y-or-n-p (format "Delete %s? " dir))
3392                   (let* (delete-recursive
3393                          (delete-recursive
3394                           (function
3395                            (lambda (f-or-d)
3396                              (ignore-errors
3397                                (if (file-directory-p f-or-d)
3398                                    (condition-case nil
3399                                        (delete-directory f-or-d)
3400                                      (file-error
3401                                       (mapcar (lambda (f)
3402                                                 (or (member f '("." ".."))
3403                                                     (funcall delete-recursive
3404                                                              (nnheader-concat
3405                                                               f-or-d f))))
3406                                               (directory-files f-or-d))
3407                                       (delete-directory f-or-d)))
3408                                  (delete-file f-or-d)))))))
3409                     (funcall delete-recursive dir))))))))))
3410
3411 ;;;###autoload
3412 (defun gnus-agent-batch ()
3413   "Start Gnus, send queue and fetch session."
3414   (interactive)
3415   (let ((init-file-user "")
3416         (gnus-always-read-dribble-file t))
3417     (gnus))
3418   (let ((gnus-agent-confirmation-function 'gnus-agent-batch-confirmation))
3419     (gnus-group-send-queue)
3420     (gnus-agent-fetch-session)))
3421
3422 (defun gnus-agent-unread-articles (group)
3423   (let* ((read (gnus-info-read (gnus-get-info group)))
3424          (known (gnus-agent-load-alist group))
3425          (unread (list nil))
3426          (tail-unread unread))
3427     (while (and known read)
3428       (let ((candidate (car (pop known))))
3429         (while (let* ((range (car read))
3430                       (min   (if (numberp range) range (car range)))
3431                       (max   (if (numberp range) range (cdr range))))
3432                  (cond ((or (not min)
3433                             (< candidate min))
3434                         (gnus-agent-append-to-list tail-unread candidate)
3435                         nil)
3436                        ((> candidate max)
3437                         (setq read (cdr read))
3438                         ;; return t so that I always loop one more
3439                         ;; time.  If I just iterated off the end of
3440                         ;; read, min will become nil and the current
3441                         ;; candidate will be added to the unread list.
3442                         t))))))
3443     (while known
3444       (gnus-agent-append-to-list tail-unread (car (pop known))))
3445     (cdr unread)))
3446
3447 (defun gnus-agent-uncached-articles (articles group &optional cached-header)
3448   "Restrict ARTICLES to numbers already fetched.
3449 Returns a sublist of ARTICLES that excludes those article ids in GROUP
3450 that have already been fetched.
3451 If CACHED-HEADER is nil, articles are only excluded if the article itself
3452 has been fetched."
3453
3454   ;; Logically equivalent to: (gnus-sorted-difference articles (mapcar
3455   ;; 'car gnus-agent-article-alist))
3456
3457   ;; Functionally, I don't need to construct a temp list using mapcar.
3458
3459   (if (and (or gnus-agent-cache (not gnus-plugged))
3460            (gnus-agent-load-alist group))
3461     (let* ((ref gnus-agent-article-alist)
3462            (arts articles)
3463            (uncached (list nil))
3464            (tail-uncached uncached))
3465       (while (and ref arts)
3466         (let ((v1 (car arts))
3467               (v2 (caar ref)))
3468           (cond ((< v1 v2) ; v1 does not appear in the reference list
3469                  (gnus-agent-append-to-list tail-uncached v1)
3470                  (setq arts (cdr arts)))
3471                 ((= v1 v2)
3472                  (unless (or cached-header (cdar ref)) ; v1 is already cached
3473                    (gnus-agent-append-to-list tail-uncached v1))
3474                  (setq arts (cdr arts))
3475                  (setq ref (cdr ref)))
3476                 (t ; reference article (v2) preceeds the list being filtered
3477                  (setq ref (cdr ref))))))
3478       (while arts
3479         (gnus-agent-append-to-list tail-uncached (pop arts)))
3480       (cdr uncached))
3481     ;; if gnus-agent-load-alist fails, no articles are cached.
3482     articles))
3483
3484 (defun gnus-agent-retrieve-headers (articles group &optional fetch-old)
3485   (save-excursion
3486     (gnus-agent-create-buffer)
3487     (let ((gnus-decode-encoded-word-function 'identity)
3488           (file (gnus-agent-article-name ".overview" group))
3489           cached-articles uncached-articles)
3490       (gnus-make-directory (nnheader-translate-file-chars
3491                             (file-name-directory file) t))
3492
3493       ;; Populate temp buffer with known headers
3494       (when (file-exists-p file)
3495         (with-current-buffer gnus-agent-overview-buffer
3496           (erase-buffer)
3497           (let ((nnheader-file-coding-system
3498                  gnus-agent-file-coding-system))
3499             (nnheader-insert-nov-file file (car articles)))))
3500
3501       (if (setq uncached-articles (gnus-agent-uncached-articles articles group
3502                                                                 t))
3503           (progn
3504             ;; Populate nntp-server-buffer with uncached headers
3505             (set-buffer nntp-server-buffer)
3506             (erase-buffer)
3507             (cond ((not (eq 'nov (let (gnus-agent) ; Turn off agent
3508                                    (gnus-retrieve-headers
3509                                     uncached-articles group fetch-old))))
3510                    (nnvirtual-convert-headers))
3511                   ((eq 'nntp (car gnus-current-select-method))
3512                    ;; The author of gnus-get-newsgroup-headers-xover
3513                    ;; reports that the XOVER command is commonly
3514                    ;; unreliable. The problem is that recently
3515                    ;; posted articles may not be entered into the
3516                    ;; NOV database in time to respond to my XOVER
3517                    ;; query.
3518                    ;;
3519                    ;; I'm going to use his assumption that the NOV
3520                    ;; database is updated in order of ascending
3521                    ;; article ID.  Therefore, a response containing
3522                    ;; article ID N implies that all articles from 1
3523                    ;; to N-1 are up-to-date.  Therefore, missing
3524                    ;; articles in that range have expired.
3525
3526                    (set-buffer nntp-server-buffer)
3527                    (let* ((fetched-articles (list nil))
3528                           (tail-fetched-articles fetched-articles)
3529                           (min (cond ((numberp fetch-old)
3530                                       (max 1 (- (car articles) fetch-old)))
3531                                      (fetch-old
3532                                       1)
3533                                      (t
3534                                       (car articles))))
3535                           (max (car (last articles))))
3536
3537                      ;; Get the list of articles that were fetched
3538                      (goto-char (point-min))
3539                      (let ((pm (point-max))
3540                            art)
3541                        (while (< (point) pm)
3542                          (when (setq art (gnus-agent-read-article-number))
3543                            (gnus-agent-append-to-list tail-fetched-articles art))
3544                          (forward-line 1)))
3545
3546                      ;; Clip this list to the headers that will
3547                      ;; actually be returned
3548                      (setq fetched-articles (gnus-list-range-intersection
3549                                              (cdr fetched-articles)
3550                                              (cons min max)))
3551
3552                      ;; Clip the uncached articles list to exclude
3553                      ;; IDs after the last FETCHED header.  The
3554                      ;; excluded IDs may be fetchable using HEAD.
3555                      (if (car tail-fetched-articles)
3556                          (setq uncached-articles
3557                                (gnus-list-range-intersection
3558                                 uncached-articles
3559                                 (cons (car uncached-articles)
3560                                       (car tail-fetched-articles)))))
3561
3562                      ;; Create the list of articles that were
3563                      ;; "successfully" fetched.  Success, in this
3564                      ;; case, means that the ID should not be
3565                      ;; fetched again.  In the case of an expired
3566                      ;; article, the header will not be fetched.
3567                      (setq uncached-articles
3568                            (gnus-sorted-nunion fetched-articles
3569                                                uncached-articles))
3570                      )))
3571
3572             ;; Erase the temp buffer
3573             (set-buffer gnus-agent-overview-buffer)
3574             (erase-buffer)
3575
3576             ;; Copy the nntp-server-buffer to the temp buffer
3577             (set-buffer nntp-server-buffer)
3578             (copy-to-buffer gnus-agent-overview-buffer (point-min) (point-max))
3579
3580             ;; Merge the temp buffer with the known headers (found on
3581             ;; disk in FILE) into the nntp-server-buffer
3582             (when uncached-articles
3583               (gnus-agent-braid-nov group uncached-articles file))
3584
3585             ;; Save the new set of known headers to FILE
3586             (set-buffer nntp-server-buffer)
3587             (let ((coding-system-for-write
3588                    gnus-agent-file-coding-system))
3589               (gnus-agent-check-overview-buffer)
3590               (write-region (point-min) (point-max) file nil 'silent))
3591
3592             (gnus-agent-update-view-total-fetched-for group t)
3593
3594             ;; Update the group's article alist to include the newly
3595             ;; fetched articles.
3596             (gnus-agent-load-alist group)
3597             (gnus-agent-save-alist group uncached-articles nil)
3598             )
3599
3600         ;; Copy the temp buffer to the nntp-server-buffer
3601         (set-buffer nntp-server-buffer)
3602         (erase-buffer)
3603         (insert-buffer-substring gnus-agent-overview-buffer)))
3604
3605     (if (and fetch-old
3606              (not (numberp fetch-old)))
3607         t                               ; Don't remove anything.
3608       (nnheader-nov-delete-outside-range
3609        (if fetch-old (max 1 (- (car articles) fetch-old))
3610          (car articles))
3611        (car (last articles)))
3612       t)
3613
3614     'nov))
3615
3616 (defun gnus-agent-request-article (article group)
3617   "Retrieve ARTICLE in GROUP from the agent cache."
3618   (when (and gnus-agent
3619              (or gnus-agent-cache
3620                  (not gnus-plugged))
3621              (numberp article))
3622     (let* ((gnus-command-method (gnus-find-method-for-group group))
3623            (file (gnus-agent-article-name (number-to-string article) group))
3624            (buffer-read-only nil))
3625       (when (and (file-exists-p file)
3626                  (> (nth 7 (file-attributes file)) 0))
3627         (erase-buffer)
3628         (gnus-kill-all-overlays)
3629         (let ((coding-system-for-read gnus-cache-coding-system))
3630           (insert-file-contents file))
3631         t))))
3632
3633 (defun gnus-agent-regenerate-group (group &optional reread)
3634   "Regenerate GROUP.
3635 If REREAD is t, all articles in the .overview are marked as unread.
3636 If REREAD is a list, the specified articles will be marked as unread.
3637 In addition, their NOV entries in .overview will be refreshed using
3638 the articles' current headers.
3639 If REREAD is not nil, downloaded articles are marked as unread."
3640   (interactive
3641    (list (let ((def (or (gnus-group-group-name)
3642                         gnus-newsgroup-name)))
3643            (let ((select (read-string (if def
3644                                           (concat "Group Name ("
3645                                                   def "): ")
3646                                         "Group Name: "))))
3647              (if (and (equal "" select)
3648                       def)
3649                  def
3650                select)))
3651          (catch 'mark
3652            (while (let (c
3653                         (cursor-in-echo-area t)
3654                         (echo-keystrokes 0))
3655                     (message "Mark as unread: (n)one / (a)ll / all (d)ownloaded articles? (n) ")
3656                     (setq c (read-char-exclusive))
3657
3658                     (cond ((or (eq c ?\r) (eq c ?n) (eq c ?N))
3659                            (throw 'mark nil))
3660                           ((or (eq c ?a) (eq c ?A))
3661                            (throw 'mark t))
3662                           ((or (eq c ?d) (eq c ?D))
3663                            (throw 'mark 'some)))
3664                     (gnus-message 3 "Ignoring unexpected input")
3665                     (sit-for 1)
3666                     t)))))
3667   (when group
3668     (gnus-message 5 "Regenerating in %s" group)
3669     (let* ((gnus-command-method (or gnus-command-method
3670                                     (gnus-find-method-for-group group)))
3671            (file (gnus-agent-article-name ".overview" group))
3672            (dir (file-name-directory file))
3673            point
3674            (downloaded (if (file-exists-p dir)
3675                            (sort (mapcar (lambda (name) (string-to-int name))
3676                                          (directory-files dir nil "^[0-9]+$" t))
3677                                  '>)
3678                          (progn (gnus-make-directory dir) nil)))
3679            dl nov-arts
3680            alist header
3681            regenerated)
3682
3683       (mm-with-unibyte-buffer
3684         (if (file-exists-p file)
3685             (let ((nnheader-file-coding-system
3686                    gnus-agent-file-coding-system))
3687               (nnheader-insert-file-contents file)))
3688         (set-buffer-modified-p nil)
3689
3690         ;; Load the article IDs found in the overview file.  As a
3691         ;; side-effect, validate the file contents.
3692         (let ((load t))
3693           (while load
3694             (setq load nil)
3695             (goto-char (point-min))
3696             (while (< (point) (point-max))
3697               (cond ((and (looking-at "[0-9]+\t")
3698                           (<= (- (match-end 0) (match-beginning 0)) 9))
3699                      (push (read (current-buffer)) nov-arts)
3700                      (forward-line 1)
3701                      (let ((l1 (car nov-arts))
3702                            (l2 (cadr nov-arts)))
3703                        (cond ((and (listp reread) (memq l1 reread))
3704                               (gnus-delete-line)
3705                               (setq nov-arts (cdr nov-arts))
3706                               (gnus-message 4 "gnus-agent-regenerate-group: NOV\
3707 entry of article %s deleted." l1))
3708                              ((not l2)
3709                               nil)
3710                              ((< l1 l2)
3711                               (gnus-message 3 "gnus-agent-regenerate-group: NOV\
3712  entries are NOT in ascending order.")
3713                               ;; Don't sort now as I haven't verified
3714                               ;; that every line begins with a number
3715                               (setq load t))
3716                              ((= l1 l2)
3717                               (forward-line -1)
3718                               (gnus-message 4 "gnus-agent-regenerate-group: NOV\
3719  entries contained duplicate of article %s.      Duplicate deleted." l1)
3720                               (gnus-delete-line)
3721                               (setq nov-arts (cdr nov-arts))))))
3722                     (t
3723                      (gnus-message 1 "gnus-agent-regenerate-group: NOV\
3724  entries contained line that did not begin with an article number.  Deleted\
3725  line.")
3726                      (gnus-delete-line))))
3727             (when load
3728               (gnus-message 5 "gnus-agent-regenerate-group: Sorting NOV\
3729  entries into ascending order.")
3730               (sort-numeric-fields 1 (point-min) (point-max))
3731               (setq nov-arts nil))))
3732         (gnus-agent-check-overview-buffer)
3733
3734         ;; Construct a new article alist whose nodes match every header
3735         ;; in the .overview file.  As a side-effect, missing headers are
3736         ;; reconstructed from the downloaded article file.
3737         (while (or downloaded nov-arts)
3738           (cond ((and downloaded
3739                       (or (not nov-arts)
3740                           (> (car downloaded) (car nov-arts))))
3741                  ;; This entry is missing from the overview file
3742                  (gnus-message 3 "Regenerating NOV %s %d..." group
3743                                (car downloaded))
3744                  (let ((file (concat dir (number-to-string (car downloaded)))))
3745                    (mm-with-unibyte-buffer
3746                      (nnheader-insert-file-contents file)
3747                      (nnheader-remove-body)
3748                      (setq header (nnheader-parse-naked-head)))
3749                    (mail-header-set-number header (car downloaded))
3750                    (if nov-arts
3751                        (let ((key (concat "^" (int-to-string (car nov-arts))
3752                                           "\t")))
3753                          (or (re-search-backward key nil t)
3754                              (re-search-forward key))
3755                          (forward-line 1))
3756                      (goto-char (point-min)))
3757                    (nnheader-insert-nov header))
3758                  (setq nov-arts (cons (car downloaded) nov-arts)))
3759                 ((eq (car downloaded) (car nov-arts))
3760                  ;; This entry in the overview has been downloaded
3761                  (push (cons (car downloaded)
3762                              (time-to-days
3763                               (nth 5 (file-attributes
3764                                       (concat dir (number-to-string
3765                                                    (car downloaded))))))) alist)
3766                  (setq downloaded (cdr downloaded))
3767                  (setq nov-arts (cdr nov-arts)))
3768                 (t
3769                  ;; This entry in the overview has not been downloaded
3770                  (push (cons (car nov-arts) nil) alist)
3771                  (setq nov-arts (cdr nov-arts)))))
3772
3773         ;; When gnus-agent-consider-all-articles is set,
3774         ;; gnus-agent-regenerate-group should NOT remove article IDs from
3775         ;; the alist.  Those IDs serve as markers to indicate that an
3776         ;; attempt has been made to fetch that article's header.
3777
3778         ;; When gnus-agent-consider-all-articles is NOT set,
3779         ;; gnus-agent-regenerate-group can remove the article ID of every
3780         ;; article (with the exception of the last ID in the list - it's
3781         ;; special) that no longer appears in the overview.  In this
3782         ;; situtation, the last article ID in the list implies that it,
3783         ;; and every article ID preceeding it, have been fetched from the
3784         ;; server.
3785
3786         (if gnus-agent-consider-all-articles
3787             ;; Restore all article IDs that were not found in the overview file.
3788             (let* ((n (cons nil alist))
3789                    (merged n)
3790                    (o (gnus-agent-load-alist group)))
3791               (while o
3792                 (let ((nID (caadr n))
3793                       (oID (caar o)))
3794                   (cond ((not nID)
3795                          (setq n (setcdr n (list (list oID))))
3796                          (setq o (cdr o)))
3797                         ((< oID nID)
3798                          (setcdr n (cons (list oID) (cdr n)))
3799                          (setq o (cdr o)))
3800                         ((= oID nID)
3801                          (setq o (cdr o))
3802                          (setq n (cdr n)))
3803                         (t
3804                          (setq n (cdr n))))))
3805               (setq alist (cdr merged)))
3806           ;; Restore the last article ID if it is not already in the new alist
3807           (let ((n (last alist))
3808                 (o (last (gnus-agent-load-alist group))))
3809             (cond ((not o)
3810                    nil)
3811                   ((not n)
3812                    (push (cons (caar o) nil) alist))
3813                   ((< (caar n) (caar o))
3814                    (setcdr n (list (car o)))))))
3815
3816         (let ((inhibit-quit t))
3817           (if (setq regenerated (buffer-modified-p))
3818               (let ((coding-system-for-write gnus-agent-file-coding-system))
3819                 (write-region (point-min) (point-max) file nil 'silent)))
3820
3821           (setq regenerated (or regenerated
3822                                 (and reread gnus-agent-article-alist)
3823                                 (not (equal alist gnus-agent-article-alist))))
3824
3825           (setq gnus-agent-article-alist alist)
3826
3827           (when regenerated
3828             (gnus-agent-save-alist group)
3829        
3830             ;; I have to alter the group's active range NOW as
3831             ;; gnus-make-ascending-articles-unread will use it to
3832             ;; recalculate the number of unread articles in the group
3833
3834             (let ((group (gnus-group-real-name group))
3835                   (group-active (or (gnus-active group)
3836                                     (gnus-activate-group group))))
3837               (gnus-agent-possibly-alter-active group group-active)))))
3838
3839       (when (and reread gnus-agent-article-alist)
3840         (gnus-make-ascending-articles-unread
3841          group
3842          (if (listp reread)
3843              reread
3844            (delq nil (mapcar (function (lambda (c)
3845                                          (cond ((eq reread t)
3846                                                 (car c))
3847                                                ((cdr c)
3848                                                 (car c)))))
3849                              gnus-agent-article-alist))))
3850
3851         (when regenerated
3852             (gnus-agent-update-files-total-fetched-for group nil)))
3853
3854       (gnus-message 5 "")
3855       regenerated)))
3856
3857 ;;;###autoload
3858 (defun gnus-agent-regenerate (&optional clean reread)
3859   "Regenerate all agent covered files.
3860 If CLEAN, obsolete (ignore)."
3861   (interactive "P")
3862   (let (regenerated)
3863     (gnus-message 4 "Regenerating Gnus agent files...")
3864     (dolist (gnus-command-method (gnus-agent-covered-methods))
3865         (dolist (group (gnus-groups-from-server gnus-command-method))
3866           (setq regenerated (or (gnus-agent-regenerate-group group reread)
3867                                 regenerated))))
3868     (gnus-message 4 "Regenerating Gnus agent files...done")
3869
3870     regenerated))
3871
3872 (defun gnus-agent-go-online (&optional force)
3873   "Switch servers into online status."
3874   (interactive (list t))
3875   (dolist (server gnus-opened-servers)
3876     (when (eq (nth 1 server) 'offline)
3877       (if (if (eq force 'ask)
3878               (gnus-y-or-n-p
3879                (format "Switch %s:%s into online status? "
3880                        (caar server) (cadar server)))
3881             force)
3882           (setcar (nthcdr 1 server) 'close)))))
3883
3884 (defun gnus-agent-toggle-group-plugged (group)
3885   "Toggle the status of the server of the current group."
3886   (interactive (list (gnus-group-group-name)))
3887   (let* ((method (gnus-find-method-for-group group))
3888          (status (cadr (assoc method gnus-opened-servers))))
3889     (if (eq status 'offline)
3890         (gnus-server-set-status method 'closed)
3891       (gnus-close-server method)
3892       (gnus-server-set-status method 'offline))
3893     (message "Turn %s:%s from %s to %s." (car method) (cadr method)
3894              (if (eq status 'offline) 'offline 'online)
3895              (if (eq status 'offline) 'online 'offline))))
3896
3897 (defun gnus-agent-group-covered-p (group)
3898   (gnus-agent-method-p (gnus-group-method group)))
3899
3900 ;; Added to support XEmacs
3901 (eval-and-compile
3902   (unless (fboundp 'directory-files-and-attributes)
3903     (defun directory-files-and-attributes (directory
3904                                            &optional full match nosort)
3905       (let (result)
3906         (dolist (file (directory-files directory full match nosort))
3907           (push (cons file (file-attributes file)) result))
3908         (nreverse result)))))
3909
3910 (defun gnus-agent-update-files-total-fetched-for 
3911   (group delta &optional method path)
3912   "Update, or set, the total disk space used by the articles that the
3913 agent has fetched."
3914   (when gnus-agent-total-fetched-hashtb
3915     (gnus-agent-with-refreshed-group
3916      group
3917      ;; if null, gnus-agent-group-pathname will calc method.
3918      (let* ((gnus-command-method method) 
3919             (path (or path (gnus-agent-group-pathname group)))
3920             (entry (or (gnus-gethash path gnus-agent-total-fetched-hashtb)
3921                        (gnus-sethash path (make-list 3 0) 
3922                                      gnus-agent-total-fetched-hashtb))))
3923        (when (listp delta)
3924          (if delta
3925              (let ((sum 0.0)
3926                    file)
3927                (while (setq file (pop delta))
3928                  (incf sum (float (or (nth 7 (file-attributes 
3929                                               (nnheader-concat 
3930                                                path 
3931                                                (if (numberp file)
3932                                                    (number-to-string file)
3933                                                  file)))) 0))))
3934                (setq delta sum))
3935            (let ((sum 0.0)
3936                  (info (directory-files-and-attributes path nil "^-?[0-9]+$" t))
3937                  file)
3938              (while (setq file (pop info))
3939                (incf sum (float (or (nth 8 file) 0))))
3940              (setq delta sum))))
3941
3942        (setq gnus-agent-need-update-total-fetched-for t)
3943        (incf (nth 2 entry) delta)))))
3944
3945 (defun gnus-agent-update-view-total-fetched-for 
3946   (group agent-over &optional method path)
3947   "Update, or set, the total disk space used by the .agentview and
3948 .overview files.  These files are calculated separately as they can be
3949 modified."
3950   (when gnus-agent-total-fetched-hashtb
3951     (gnus-agent-with-refreshed-group
3952      group
3953      ;; if null, gnus-agent-group-pathname will calc method.
3954      (let* ((gnus-command-method method) 
3955             (path (or path (gnus-agent-group-pathname group)))
3956             (entry (or (gnus-gethash path gnus-agent-total-fetched-hashtb)
3957                        (gnus-sethash path (make-list 3 0) 
3958                                      gnus-agent-total-fetched-hashtb)))
3959             (size (or (nth 7 (file-attributes 
3960                               (nnheader-concat
3961                                path (if agent-over 
3962                                         ".overview"
3963                                       ".agentview"))))
3964                       0)))
3965        (setq gnus-agent-need-update-total-fetched-for t)
3966        (setf (nth (if agent-over 1 0) entry) size)))))
3967
3968 (defun gnus-agent-total-fetched-for (group &optional method no-inhibit)
3969   "Get the total disk space used by the specified GROUP."
3970   (unless gnus-agent-total-fetched-hashtb
3971     (setq gnus-agent-total-fetched-hashtb (gnus-make-hashtable 1024)))
3972
3973   ;; if null, gnus-agent-group-pathname will calc method.
3974   (let* ((gnus-command-method method) 
3975          (path (gnus-agent-group-pathname group))
3976          (entry (gnus-gethash path gnus-agent-total-fetched-hashtb)))
3977     (if entry
3978         (apply '+ entry)
3979       (let ((gnus-agent-inhibit-update-total-fetched-for (not no-inhibit)))
3980         (+ 
3981          (gnus-agent-update-view-total-fetched-for  group nil method path)
3982          (gnus-agent-update-view-total-fetched-for  group t   method path)
3983          (gnus-agent-update-files-total-fetched-for group nil method path))))))
3984
3985 (provide 'gnus-agent)
3986
3987 ;;; arch-tag: b0ba4afc-5229-4cee-ad25-9956daa4e91e
3988 ;;; gnus-agent.el ends here