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