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