4cad4ae19356dc19ee882cff952d0d567b1aa332
[gnus] / lisp / gnus-agent.el
1 ;;; gnus-agent.el --- unplugged support for Gnus
2 ;; Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002
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-fetched-hook nil
58   "Hook run after finishing fetching articles."
59   :group 'gnus-agent
60   :type 'hook)
61
62 (defcustom gnus-agent-handle-level gnus-level-subscribed
63   "Groups on levels higher than this variable will be ignored by the Agent."
64   :group 'gnus-agent
65   :type 'integer)
66
67 (defcustom gnus-agent-expire-days 7
68   "Read articles older than this will be expired.
69 This can also be a list of regexp/day pairs.  The regexps will
70 be matched against group names."
71   :group 'gnus-agent
72   :type 'integer)
73
74 (defcustom gnus-agent-expire-all nil
75   "If non-nil, also expire unread, ticked and dormant articles.
76 If nil, only read articles will be expired."
77   :group 'gnus-agent
78   :type 'boolean)
79
80 (defcustom gnus-agent-group-mode-hook nil
81   "Hook run in Agent group minor modes."
82   :group 'gnus-agent
83   :type 'hook)
84
85 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
86 (when (featurep 'xemacs)
87   (add-hook 'gnus-agent-group-mode-hook 'gnus-xmas-agent-group-menu-add))
88
89 (defcustom gnus-agent-summary-mode-hook nil
90   "Hook run in Agent summary minor modes."
91   :group 'gnus-agent
92   :type 'hook)
93
94 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
95 (when (featurep 'xemacs)
96   (add-hook 'gnus-agent-summary-mode-hook 'gnus-xmas-agent-summary-menu-add))
97
98 (defcustom gnus-agent-server-mode-hook nil
99   "Hook run in Agent summary minor modes."
100   :group 'gnus-agent
101   :type 'hook)
102
103 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
104 (when (featurep 'xemacs)
105   (add-hook 'gnus-agent-server-mode-hook 'gnus-xmas-agent-server-menu-add))
106
107 (defcustom gnus-agent-confirmation-function 'y-or-n-p
108   "Function to confirm when error happens."
109   :version "21.1"
110   :group 'gnus-agent
111   :type 'function)
112
113 (defcustom gnus-agent-synchronize-flags 'ask
114   "Indicate if flags are synchronized when you plug in.
115 If this is `ask' the hook will query the user."
116   :version "21.1"
117   :type '(choice (const :tag "Always" t)
118                  (const :tag "Never" nil)
119                  (const :tag "Ask" ask))
120   :group 'gnus-agent)
121
122 (defcustom gnus-agent-go-online 'ask
123   "Indicate if offline servers go online when you plug in.
124 If this is `ask' the hook will query the user."
125   :version "21.1"
126   :type '(choice (const :tag "Always" t)
127                  (const :tag "Never" nil)
128                  (const :tag "Ask" ask))
129   :group 'gnus-agent)
130
131 (defcustom gnus-agent-mark-unread-after-downloaded t
132   "Indicate whether to mark articles unread after downloaded."
133   :version "21.1"
134   :type 'boolean
135   :group 'gnus-agent)
136
137 (defcustom gnus-agent-download-marks '(download)
138   "Marks for downloading."
139   :version "21.1"
140   :type '(repeat (symbol :tag "Mark"))
141   :group 'gnus-agent)
142
143 ;;; Internal variables
144
145 (defvar gnus-agent-history-buffers nil)
146 (defvar gnus-agent-buffer-alist nil)
147 (defvar gnus-agent-article-alist nil)
148 (defvar gnus-agent-group-alist nil)
149 (defvar gnus-category-alist nil)
150 (defvar gnus-agent-current-history nil)
151 (defvar gnus-agent-overview-buffer nil)
152 (defvar gnus-category-predicate-cache nil)
153 (defvar gnus-category-group-cache nil)
154 (defvar gnus-agent-spam-hashtb nil)
155 (defvar gnus-agent-file-name nil)
156 (defvar gnus-agent-send-mail-function nil)
157 (defvar gnus-agent-file-coding-system 'raw-text)
158 (defvar gnus-agent-file-loading-cache nil)
159
160 (defvar gnus-agent-auto-agentize-methods '(nntp nnimap)
161   "Initially, all servers from these methods are agentized.
162 The user may remove or add servers using the Server buffer.  See Info
163 node `(gnus)Server Buffer'.")
164
165 ;; Dynamic variables
166 (defvar gnus-headers)
167 (defvar gnus-score)
168
169 ;;;
170 ;;; Setup
171 ;;;
172
173 (defun gnus-open-agent ()
174   (setq gnus-agent t)
175   (gnus-agent-read-servers)
176   (gnus-category-read)
177   (gnus-agent-create-buffer)
178   (add-hook 'gnus-group-mode-hook 'gnus-agent-mode)
179   (add-hook 'gnus-summary-mode-hook 'gnus-agent-mode)
180   (add-hook 'gnus-server-mode-hook 'gnus-agent-mode))
181
182 (defun gnus-agent-create-buffer ()
183   (if (gnus-buffer-live-p gnus-agent-overview-buffer)
184       t
185     (setq gnus-agent-overview-buffer
186           (gnus-get-buffer-create " *Gnus agent overview*"))
187     (with-current-buffer gnus-agent-overview-buffer
188       (mm-enable-multibyte))
189     nil))
190
191 (gnus-add-shutdown 'gnus-close-agent 'gnus)
192
193 (defun gnus-close-agent ()
194   (setq gnus-agent-covered-methods nil
195         gnus-category-predicate-cache nil
196         gnus-category-group-cache nil
197         gnus-agent-spam-hashtb nil)
198   (gnus-kill-buffer gnus-agent-overview-buffer))
199
200 ;;;
201 ;;; Utility functions
202 ;;;
203
204 (defun gnus-agent-read-file (file)
205   "Load FILE and do a `read' there."
206   (with-temp-buffer
207     (ignore-errors
208       (nnheader-insert-file-contents file)
209       (goto-char (point-min))
210       (read (current-buffer)))))
211
212 (defsubst gnus-agent-method ()
213   (concat (symbol-name (car gnus-command-method)) "/"
214           (if (equal (cadr gnus-command-method) "")
215               "unnamed"
216             (cadr gnus-command-method))))
217
218 (defsubst gnus-agent-directory ()
219   "Path of the Gnus agent directory."
220   (nnheader-concat gnus-agent-directory
221                    (nnheader-translate-file-chars (gnus-agent-method)) "/"))
222
223 (defun gnus-agent-lib-file (file)
224   "The full path of the Gnus agent library FILE."
225   (expand-file-name file
226                     (file-name-as-directory
227                      (expand-file-name "agent.lib" (gnus-agent-directory)))))
228
229 ;;; Fetching setup functions.
230
231 (defun gnus-agent-start-fetch ()
232   "Initialize data structures for efficient fetching."
233   (gnus-agent-open-history)
234   (setq gnus-agent-current-history (gnus-agent-history-buffer))
235   (gnus-agent-create-buffer))
236
237 (defun gnus-agent-stop-fetch ()
238   "Save all data structures and clean up."
239   (gnus-agent-save-history)
240   (gnus-agent-close-history)
241   (setq gnus-agent-spam-hashtb nil)
242   (save-excursion
243     (set-buffer nntp-server-buffer)
244     (widen)))
245
246 (defmacro gnus-agent-with-fetch (&rest forms)
247   "Do FORMS safely."
248   `(unwind-protect
249        (let ((gnus-agent-fetching t))
250          (gnus-agent-start-fetch)
251          ,@forms)
252      (gnus-agent-stop-fetch)))
253
254 (put 'gnus-agent-with-fetch 'lisp-indent-function 0)
255 (put 'gnus-agent-with-fetch 'edebug-form-spec '(body))
256
257 ;;;
258 ;;; Mode infestation
259 ;;;
260
261 (defvar gnus-agent-mode-hook nil
262   "Hook run when installing agent mode.")
263
264 (defvar gnus-agent-mode nil)
265 (defvar gnus-agent-mode-status '(gnus-agent-mode " Plugged"))
266
267 (defun gnus-agent-mode ()
268   "Minor mode for providing a agent support in Gnus buffers."
269   (let* ((buffer (progn (string-match "^gnus-\\(.*\\)-mode$"
270                                       (symbol-name major-mode))
271                         (match-string 1 (symbol-name major-mode))))
272          (mode (intern (format "gnus-agent-%s-mode" buffer))))
273     (set (make-local-variable 'gnus-agent-mode) t)
274     (set mode nil)
275     (set (make-local-variable mode) t)
276     ;; Set up the menu.
277     (when (gnus-visual-p 'agent-menu 'menu)
278       (funcall (intern (format "gnus-agent-%s-make-menu-bar" buffer))))
279     (unless (assq 'gnus-agent-mode minor-mode-alist)
280       (push gnus-agent-mode-status minor-mode-alist))
281     (unless (assq mode minor-mode-map-alist)
282       (push (cons mode (symbol-value (intern (format "gnus-agent-%s-mode-map"
283                                                      buffer))))
284             minor-mode-map-alist))
285     (when (eq major-mode 'gnus-group-mode)
286       (gnus-agent-toggle-plugged gnus-plugged))
287     (gnus-run-hooks 'gnus-agent-mode-hook
288                     (intern (format "gnus-agent-%s-mode-hook" buffer)))))
289
290 (defvar gnus-agent-group-mode-map (make-sparse-keymap))
291 (gnus-define-keys gnus-agent-group-mode-map
292   "Ju" gnus-agent-fetch-groups
293   "Jc" gnus-enter-category-buffer
294   "Jj" gnus-agent-toggle-plugged
295   "Js" gnus-agent-fetch-session
296   "JY" gnus-agent-synchronize-flags
297   "JS" gnus-group-send-queue
298   "Ja" gnus-agent-add-group
299   "Jr" gnus-agent-remove-group
300   "Jo" gnus-agent-toggle-group-plugged)
301
302 (defun gnus-agent-group-make-menu-bar ()
303   (unless (boundp 'gnus-agent-group-menu)
304     (easy-menu-define
305      gnus-agent-group-menu gnus-agent-group-mode-map ""
306      '("Agent"
307        ["Toggle plugged" gnus-agent-toggle-plugged t]
308        ["Toggle group plugged" gnus-agent-toggle-group-plugged t]
309        ["List categories" gnus-enter-category-buffer t]
310        ["Send queue" gnus-group-send-queue gnus-plugged]
311        ("Fetch"
312         ["All" gnus-agent-fetch-session gnus-plugged]
313         ["Group" gnus-agent-fetch-group gnus-plugged])))))
314
315 (defvar gnus-agent-summary-mode-map (make-sparse-keymap))
316 (gnus-define-keys gnus-agent-summary-mode-map
317   "Jj" gnus-agent-toggle-plugged
318   "Ju" gnus-agent-summary-fetch-group
319   "J#" gnus-agent-mark-article
320   "J\M-#" gnus-agent-unmark-article
321   "@" gnus-agent-toggle-mark
322   "Jc" gnus-agent-catchup)
323
324 (defun gnus-agent-summary-make-menu-bar ()
325   (unless (boundp 'gnus-agent-summary-menu)
326     (easy-menu-define
327      gnus-agent-summary-menu gnus-agent-summary-mode-map ""
328      '("Agent"
329        ["Toggle plugged" gnus-agent-toggle-plugged t]
330        ["Mark as downloadable" gnus-agent-mark-article t]
331        ["Unmark as downloadable" gnus-agent-unmark-article t]
332        ["Toggle mark" gnus-agent-toggle-mark t]
333        ["Fetch downloadable" gnus-agent-summary-fetch-group t]
334        ["Catchup undownloaded" gnus-agent-catchup t]))))
335
336 (defvar gnus-agent-server-mode-map (make-sparse-keymap))
337 (gnus-define-keys gnus-agent-server-mode-map
338   "Jj" gnus-agent-toggle-plugged
339   "Ja" gnus-agent-add-server
340   "Jr" gnus-agent-remove-server)
341
342 (defun gnus-agent-server-make-menu-bar ()
343   (unless (boundp 'gnus-agent-server-menu)
344     (easy-menu-define
345      gnus-agent-server-menu gnus-agent-server-mode-map ""
346      '("Agent"
347        ["Toggle plugged" gnus-agent-toggle-plugged t]
348        ["Add" gnus-agent-add-server t]
349        ["Remove" gnus-agent-remove-server t]))))
350
351 (defun gnus-agent-make-mode-line-string (string mouse-button mouse-func)
352   (if (and (fboundp 'propertize)
353            (fboundp 'make-mode-line-mouse-map))
354       (propertize string 'local-map
355                   (make-mode-line-mouse-map mouse-button mouse-func))
356     string))
357
358 (defun gnus-agent-toggle-plugged (plugged)
359   "Toggle whether Gnus is unplugged or not."
360   (interactive (list (not gnus-plugged)))
361   (if plugged
362       (progn
363         (setq gnus-plugged plugged)
364         (gnus-run-hooks 'gnus-agent-plugged-hook)
365         (setcar (cdr gnus-agent-mode-status)
366                 (gnus-agent-make-mode-line-string " Plugged"
367                                                   'mouse-2
368                                                   'gnus-agent-toggle-plugged))
369         (gnus-agent-go-online gnus-agent-go-online)
370         (gnus-agent-possibly-synchronize-flags))
371     (gnus-agent-close-connections)
372     (setq gnus-plugged plugged)
373     (gnus-run-hooks 'gnus-agent-unplugged-hook)
374     (setcar (cdr gnus-agent-mode-status)
375             (gnus-agent-make-mode-line-string " Unplugged"
376                                               'mouse-2
377                                               'gnus-agent-toggle-plugged)))
378   (set-buffer-modified-p t))
379
380 (defun gnus-agent-close-connections ()
381   "Close all methods covered by the Gnus agent."
382   (let ((methods gnus-agent-covered-methods))
383     (while methods
384       (gnus-close-server (pop methods)))))
385
386 ;;;###autoload
387 (defun gnus-unplugged ()
388   "Start Gnus unplugged."
389   (interactive)
390   (setq gnus-plugged nil)
391   (gnus))
392
393 ;;;###autoload
394 (defun gnus-plugged ()
395   "Start Gnus plugged."
396   (interactive)
397   (setq gnus-plugged t)
398   (gnus))
399
400 ;;;###autoload
401 (defun gnus-slave-unplugged (&optional arg)
402   "Read news as a slave unplugged."
403   (interactive "P")
404   (setq gnus-plugged nil)
405   (gnus arg nil 'slave))
406
407 ;;;###autoload
408 (defun gnus-agentize ()
409   "Allow Gnus to be an offline newsreader.
410 The normal usage of this command is to put the following as the
411 last form in your `.gnus.el' file:
412
413 \(gnus-agentize)
414
415 This will modify the `gnus-setup-news-hook', and
416 `message-send-mail-real-function' variables, and install the Gnus agent
417 minor mode in all Gnus buffers."
418   (interactive)
419   (gnus-open-agent)
420   (add-hook 'gnus-setup-news-hook 'gnus-agent-queue-setup)
421   (unless gnus-agent-send-mail-function
422     (setq gnus-agent-send-mail-function (or
423                                          message-send-mail-real-function
424                                          message-send-mail-function)
425           message-send-mail-real-function 'gnus-agent-send-mail))
426   (unless gnus-agent-covered-methods
427     (mapcar
428      (lambda (server)
429        (if (memq (car (gnus-server-to-method server)) 
430                  gnus-agent-auto-agentize-methods)
431            (setq gnus-agent-covered-methods 
432                  (cons (gnus-server-to-method server)
433                        gnus-agent-covered-methods ))))
434      (append (list gnus-select-method) gnus-secondary-select-methods))))
435
436 (defun gnus-agent-queue-setup ()
437   "Make sure the queue group exists."
438   (unless (gnus-gethash "nndraft:queue" gnus-newsrc-hashtb)
439     (gnus-request-create-group "queue" '(nndraft ""))
440     (let ((gnus-level-default-subscribed 1))
441       (gnus-subscribe-group "nndraft:queue" nil '(nndraft "")))
442     (gnus-group-set-parameter
443      "nndraft:queue" 'gnus-dummy '((gnus-draft-mode)))))
444
445 (defun gnus-agent-send-mail ()
446   (if gnus-plugged
447       (funcall gnus-agent-send-mail-function)
448     (goto-char (point-min))
449     (re-search-forward
450      (concat "^" (regexp-quote mail-header-separator) "\n"))
451     (replace-match "\n")
452     (gnus-agent-insert-meta-information 'mail)
453     (gnus-request-accept-article "nndraft:queue" nil t t)))
454
455 (defun gnus-agent-insert-meta-information (type &optional method)
456   "Insert meta-information into the message that says how it's to be posted.
457 TYPE can be either `mail' or `news'.  If the latter, then METHOD can
458 be a select method."
459   (save-excursion
460     (message-remove-header gnus-agent-meta-information-header)
461     (goto-char (point-min))
462     (insert gnus-agent-meta-information-header ": "
463             (symbol-name type) " " (format "%S" method)
464             "\n")
465     (forward-char -1)
466     (while (search-backward "\n" nil t)
467       (replace-match "\\n" t t))))
468
469 (defun gnus-agent-restore-gcc ()
470   "Restore GCC field from saved header."
471   (save-excursion
472     (goto-char (point-min))
473     (while (re-search-forward (concat gnus-agent-gcc-header ":") nil t)
474       (replace-match "Gcc:" 'fixedcase))))
475
476 (defun gnus-agent-any-covered-gcc ()
477   (save-restriction
478     (message-narrow-to-headers)
479     (let* ((gcc (mail-fetch-field "gcc" nil t))
480            (methods (and gcc
481                          (mapcar 'gnus-inews-group-method
482                                  (message-unquote-tokens
483                                   (message-tokenize-header
484                                    gcc " ,")))))
485            covered)
486       (while (and (not covered) methods)
487         (setq covered (gnus-agent-method-p (car methods))
488               methods (cdr methods)))
489       covered)))
490
491 ;;;###autoload
492 (defun gnus-agent-possibly-save-gcc ()
493   "Save GCC if Gnus is unplugged."
494   (when (and (not gnus-plugged) (gnus-agent-any-covered-gcc))
495     (save-excursion
496       (goto-char (point-min))
497       (let ((case-fold-search t))
498         (while (re-search-forward "^gcc:" nil t)
499           (replace-match (concat gnus-agent-gcc-header ":") 'fixedcase))))))
500
501 (defun gnus-agent-possibly-do-gcc ()
502   "Do GCC if Gnus is plugged."
503   (when (or gnus-plugged (not (gnus-agent-any-covered-gcc)))
504     (gnus-inews-do-gcc)))
505
506 ;;;
507 ;;; Group mode commands
508 ;;;
509
510 (defun gnus-agent-fetch-groups (n)
511   "Put all new articles in the current groups into the Agent."
512   (interactive "P")
513   (unless gnus-plugged
514     (error "Groups can't be fetched when Gnus is unplugged"))
515   (gnus-group-iterate n 'gnus-agent-fetch-group))
516
517 (defun gnus-agent-fetch-group (group)
518   "Put all new articles in GROUP into the Agent."
519   (interactive (list (gnus-group-group-name)))
520   (let ((state gnus-plugged))
521     (unwind-protect
522         (progn
523           (unless group
524             (error "No group on the current line"))
525           (unless state
526             (gnus-agent-toggle-plugged gnus-plugged))
527           (let ((gnus-command-method (gnus-find-method-for-group group)))
528             (gnus-agent-with-fetch
529               (gnus-agent-fetch-group-1 group gnus-command-method)
530               (gnus-message 5 "Fetching %s...done" group))))
531       (when (and (not state)
532                  gnus-plugged)
533         (gnus-agent-toggle-plugged gnus-plugged)))))
534
535 (defun gnus-agent-add-group (category arg)
536   "Add the current group to an agent category."
537   (interactive
538    (list
539     (intern
540      (completing-read
541       "Add to category: "
542       (mapcar (lambda (cat) (list (symbol-name (car cat))))
543               gnus-category-alist)
544       nil t))
545     current-prefix-arg))
546   (let ((cat (assq category gnus-category-alist))
547         c groups)
548     (gnus-group-iterate arg
549       (lambda (group)
550         (when (cadddr (setq c (gnus-group-category group)))
551           (setf (cadddr c) (delete group (cadddr c))))
552         (push group groups)))
553     (setf (cadddr cat) (nconc (cadddr cat) groups))
554     (gnus-category-write)))
555
556 (defun gnus-agent-remove-group (arg)
557   "Remove the current group from its agent category, if any."
558   (interactive "P")
559   (let (c)
560     (gnus-group-iterate arg
561       (lambda (group)
562         (when (cadddr (setq c (gnus-group-category group)))
563           (setf (cadddr c) (delete group (cadddr c))))))
564     (gnus-category-write)))
565
566 (defun gnus-agent-synchronize-flags ()
567   "Synchronize unplugged flags with servers."
568   (interactive)
569   (save-excursion
570     (dolist (gnus-command-method gnus-agent-covered-methods)
571       (when (file-exists-p (gnus-agent-lib-file "flags"))
572         (gnus-agent-synchronize-flags-server gnus-command-method)))))
573
574 (defun gnus-agent-possibly-synchronize-flags ()
575   "Synchronize flags according to `gnus-agent-synchronize-flags'."
576   (interactive)
577   (save-excursion
578     (dolist (gnus-command-method gnus-agent-covered-methods)
579       (when (file-exists-p (gnus-agent-lib-file "flags"))
580         (gnus-agent-possibly-synchronize-flags-server gnus-command-method)))))
581
582 (defun gnus-agent-synchronize-flags-server (method)
583   "Synchronize flags set when unplugged for server."
584   (let ((gnus-command-method method))
585     (when (file-exists-p (gnus-agent-lib-file "flags"))
586       (set-buffer (get-buffer-create " *Gnus Agent flag synchronize*"))
587       (erase-buffer)
588       (nnheader-insert-file-contents (gnus-agent-lib-file "flags"))
589       (if (null (gnus-check-server gnus-command-method))
590           (message "Couldn't open server %s" (nth 1 gnus-command-method))
591         (while (not (eobp))
592           (if (null (eval (read (current-buffer))))
593               (progn (forward-line)
594                      (kill-line -1))
595             (write-file (gnus-agent-lib-file "flags"))
596             (error "Couldn't set flags from file %s"
597                    (gnus-agent-lib-file "flags"))))
598         (delete-file (gnus-agent-lib-file "flags")))
599       (kill-buffer nil))))
600
601 (defun gnus-agent-possibly-synchronize-flags-server (method)
602   "Synchronize flags for server according to `gnus-agent-synchronize-flags'."
603   (when (or (and gnus-agent-synchronize-flags
604                  (not (eq gnus-agent-synchronize-flags 'ask)))
605             (and (eq gnus-agent-synchronize-flags 'ask)
606                  (gnus-y-or-n-p (format "Synchronize flags on server `%s'? "
607                                         (cadr method)))))
608     (gnus-agent-synchronize-flags-server method)))
609
610 ;;;
611 ;;; Server mode commands
612 ;;;
613
614 (defun gnus-agent-add-server (server)
615   "Enroll SERVER in the agent program."
616   (interactive (list (gnus-server-server-name)))
617   (unless server
618     (error "No server on the current line"))
619   (let ((method (gnus-server-get-method nil (gnus-server-server-name))))
620     (when (gnus-agent-method-p method)
621       (error "Server already in the agent program"))
622     (push method gnus-agent-covered-methods)
623     (gnus-server-update-server server)
624     (gnus-agent-write-servers)
625     (message "Entered %s into the Agent" server)))
626
627 (defun gnus-agent-remove-server (server)
628   "Remove SERVER from the agent program."
629   (interactive (list (gnus-server-server-name)))
630   (unless server
631     (error "No server on the current line"))
632   (let ((method (gnus-server-get-method nil (gnus-server-server-name))))
633     (unless (gnus-agent-method-p method)
634       (error "Server not in the agent program"))
635     (setq gnus-agent-covered-methods
636           (delete method gnus-agent-covered-methods))
637     (gnus-server-update-server server)
638     (gnus-agent-write-servers)
639     (message "Removed %s from the agent" server)))
640
641 (defun gnus-agent-read-servers ()
642   "Read the alist of covered servers."
643   (mapcar (lambda (m)
644             (let ((server (gnus-server-get-method
645                            nil
646                            (or m "native"))))
647               (if server
648                   (push server gnus-agent-covered-methods)
649                 (message "Ignoring disappeared server `%s'" m)
650                 (sit-for 1))))
651           (gnus-agent-read-file
652            (nnheader-concat gnus-agent-directory "lib/servers"))))
653
654 (defun gnus-agent-write-servers ()
655   "Write the alist of covered servers."
656   (gnus-make-directory (nnheader-concat gnus-agent-directory "lib"))
657   (let ((coding-system-for-write nnheader-file-coding-system)
658         (file-name-coding-system nnmail-pathname-coding-system))
659     (with-temp-file (nnheader-concat gnus-agent-directory "lib/servers")
660       (prin1 (mapcar 'gnus-method-simplify gnus-agent-covered-methods)
661              (current-buffer)))))
662
663 ;;;
664 ;;; Summary commands
665 ;;;
666
667 (defun gnus-agent-mark-article (n &optional unmark)
668   "Mark the next N articles as downloadable.
669 If N is negative, mark backward instead.  If UNMARK is non-nil, remove
670 the mark instead.  The difference between N and the actual number of
671 articles marked is returned."
672   (interactive "p")
673   (let ((backward (< n 0))
674         (n (abs n)))
675     (while (and
676             (> n 0)
677             (progn
678               (gnus-summary-set-agent-mark
679                (gnus-summary-article-number) unmark)
680               (zerop (gnus-summary-next-subject (if backward -1 1) nil t))))
681       (setq n (1- n)))
682     (when (/= 0 n)
683       (gnus-message 7 "No more articles"))
684     (gnus-summary-recenter)
685     (gnus-summary-position-point)
686     n))
687
688 (defun gnus-agent-unmark-article (n)
689   "Remove the downloadable mark from the next N articles.
690 If N is negative, unmark backward instead.  The difference between N and
691 the actual number of articles unmarked is returned."
692   (interactive "p")
693   (gnus-agent-mark-article n t))
694
695 (defun gnus-agent-toggle-mark (n)
696   "Toggle the downloadable mark from the next N articles.
697 If N is negative, toggle backward instead.  The difference between N and
698 the actual number of articles toggled is returned."
699   (interactive "p")
700   (gnus-agent-mark-article n 'toggle))
701
702 (defun gnus-summary-set-agent-mark (article &optional unmark)
703   "Mark ARTICLE as downloadable."
704   (let ((unmark (if (and (not (null unmark)) (not (eq t unmark)))
705                     (memq article gnus-newsgroup-downloadable)
706                   unmark)))
707     (if unmark
708         (progn
709           (setq gnus-newsgroup-downloadable
710                 (delq article gnus-newsgroup-downloadable))
711           (push article gnus-newsgroup-undownloaded))
712       (setq gnus-newsgroup-undownloaded
713             (delq article gnus-newsgroup-undownloaded))
714       (setq gnus-newsgroup-downloadable
715             (gnus-add-to-sorted-list gnus-newsgroup-downloadable article)))
716     (gnus-summary-update-mark
717      (if unmark gnus-undownloaded-mark gnus-downloadable-mark)
718      'unread)))
719
720 (defun gnus-agent-get-undownloaded-list ()
721   "Mark all unfetched articles as read."
722   (let ((gnus-command-method (gnus-find-method-for-group gnus-newsgroup-name)))
723     (when (and (not (gnus-online gnus-command-method))
724                (gnus-agent-method-p gnus-command-method))
725       (gnus-agent-load-alist gnus-newsgroup-name)
726       ;; First mark all undownloaded articles as undownloaded.
727       (let ((articles (mapcar (lambda (header) (mail-header-number header))
728                               gnus-newsgroup-headers))
729             (agent-articles gnus-agent-article-alist)
730             candidates article)
731         (while (setq article (pop articles))
732           (while (and agent-articles
733                       (< (caar agent-articles) article))
734             (setq agent-articles (cdr agent-articles)))
735           (when (or (not (cdar agent-articles))
736                     (not (= (caar agent-articles) article)))
737             (push article candidates)))
738         (dolist (article candidates)
739           (unless (or (memq article gnus-newsgroup-downloadable)
740                       (memq article gnus-newsgroup-cached))
741             (push article gnus-newsgroup-undownloaded))))
742       ;; Then mark downloaded downloadable as not-downloadable,
743       ;; if you get my drift.
744       (dolist (article gnus-newsgroup-downloadable)
745         (when (cdr (assq article gnus-agent-article-alist))
746           (setq gnus-newsgroup-downloadable
747                 (delq article gnus-newsgroup-downloadable)))))))
748
749 (defun gnus-agent-catchup ()
750   "Mark all undownloaded articles as read."
751   (interactive)
752   (save-excursion
753     (while gnus-newsgroup-undownloaded
754       (gnus-summary-mark-article
755        (pop gnus-newsgroup-undownloaded) gnus-catchup-mark)))
756   (gnus-summary-position-point))
757
758 (defun gnus-agent-summary-fetch-group (&optional all)
759   "Fetch the downloadable articles in the group.
760 Optional arg ALL, if non-nil, means to fetch all articles."
761   (interactive "P")
762   (let ((articles
763          (if all gnus-newsgroup-articles
764            gnus-newsgroup-downloadable))
765         (gnus-command-method (gnus-find-method-for-group gnus-newsgroup-name))
766         (state gnus-plugged))
767     (unwind-protect
768         (progn
769           (unless state
770             (gnus-agent-toggle-plugged t))
771           (unless articles
772             (error "No articles to download"))
773           (gnus-agent-with-fetch
774             (gnus-agent-fetch-articles gnus-newsgroup-name articles))
775           (save-excursion
776             (dolist (article articles)
777               (setq gnus-newsgroup-downloadable
778                     (delq article gnus-newsgroup-downloadable))
779               (if gnus-agent-mark-unread-after-downloaded
780                   (gnus-summary-mark-article article gnus-unread-mark)))))
781       (when (and (not state)
782                  gnus-plugged)
783         (gnus-agent-toggle-plugged nil)))))
784
785 (defun gnus-agent-fetch-selected-article ()
786   "Fetch the current article as it is selected.
787 This can be added to `gnus-select-article-hook' or
788 `gnus-mark-article-hook'."
789   (let ((gnus-command-method gnus-current-select-method))
790     (when (gnus-agent-method-p gnus-command-method)
791       (gnus-agent-fetch-articles
792        gnus-newsgroup-name
793        (list gnus-current-article)))))
794
795 ;;;
796 ;;; Internal functions
797 ;;;
798
799 (defun gnus-agent-save-active (method)
800   (gnus-agent-save-active-1 method 'gnus-active-to-gnus-format))
801
802 (defun gnus-agent-save-active-1 (method function)
803   (when (gnus-agent-method-p method)
804     (let* ((gnus-command-method method)
805            (new (gnus-make-hashtable (count-lines (point-min) (point-max))))
806            (file (gnus-agent-lib-file "active")))
807       (funcall function nil new)
808       (gnus-agent-write-active file new)
809       (erase-buffer)
810       (nnheader-insert-file-contents file))))
811
812 (defun gnus-agent-write-active (file new)
813   (let ((orig (gnus-make-hashtable (count-lines (point-min) (point-max))))
814         (file (gnus-agent-lib-file "active"))
815         elem osym)
816     (when (file-exists-p file)
817       (with-temp-buffer
818         (nnheader-insert-file-contents file)
819         (gnus-active-to-gnus-format nil orig))
820       (mapatoms
821        (lambda (sym)
822          (when (and sym (boundp sym))
823            (if (and (boundp (setq osym (intern (symbol-name sym) orig)))
824                     (setq elem (symbol-value osym)))
825                (progn
826                  (if (and (integerp (car (symbol-value sym)))
827                           (> (car elem) (car (symbol-value sym))))
828                      (setcar elem (car (symbol-value sym))))
829                  (if (integerp (cdr (symbol-value sym)))
830                      (setcdr elem (cdr (symbol-value sym)))))
831              (set (intern (symbol-name sym) orig) (symbol-value sym)))))
832        new))
833     (gnus-make-directory (file-name-directory file))
834     (let ((nnmail-active-file-coding-system gnus-agent-file-coding-system))
835       ;; The hashtable contains real names of groups,  no more prefix
836       ;; removing, so set `full' to `t'.
837       (gnus-write-active-file file orig t))))
838
839 (defun gnus-agent-save-groups (method)
840   (gnus-agent-save-active-1 method 'gnus-groups-to-gnus-format))
841
842 (defun gnus-agent-save-group-info (method group active)
843   (when (gnus-agent-method-p method)
844     (let* ((gnus-command-method method)
845            (coding-system-for-write nnheader-file-coding-system)
846            (file-name-coding-system nnmail-pathname-coding-system)
847            (file (gnus-agent-lib-file "active"))
848            oactive-min)
849       (gnus-make-directory (file-name-directory file))
850       (with-temp-file file
851         ;; Emacs got problem to match non-ASCII group in multibyte buffer.
852         (mm-disable-multibyte)
853         (when (file-exists-p file)
854           (nnheader-insert-file-contents file))
855         (goto-char (point-min))
856         (when (re-search-forward
857                (concat "^" (regexp-quote group) " ") nil t)
858           (save-excursion
859             (read (current-buffer))                      ;; max
860             (setq oactive-min (read (current-buffer))))  ;; min
861           (gnus-delete-line))
862         (insert (format "%S %d %d y\n" (intern group)
863                         (cdr active)
864                         (or oactive-min (car active))))
865         (goto-char (point-max))
866         (while (search-backward "\\." nil t)
867           (delete-char 1))))))
868
869 (defun gnus-agent-group-path (group)
870   "Translate GROUP into a path."
871   (if nnmail-use-long-file-names
872       (gnus-group-real-name group)
873     (nnheader-translate-file-chars
874      (nnheader-replace-chars-in-string
875       (nnheader-replace-duplicate-chars-in-string
876        (nnheader-replace-chars-in-string
877         (gnus-group-real-name group)
878         ?/ ?_)
879        ?. ?_)
880       ?. ?/))))
881
882 \f
883
884 (defun gnus-agent-get-function (method)
885   (if (gnus-online method)
886       (car method)
887     (require 'nnagent)
888     'nnagent))
889
890 ;;; History functions
891
892 (defun gnus-agent-history-buffer ()
893   (cdr (assoc (gnus-agent-method) gnus-agent-history-buffers)))
894
895 (defun gnus-agent-open-history ()
896   (save-excursion
897     (push (cons (gnus-agent-method)
898                 (set-buffer (gnus-get-buffer-create
899                              (format " *Gnus agent %s history*"
900                                      (gnus-agent-method)))))
901           gnus-agent-history-buffers)
902     (mm-disable-multibyte) ;; everything is binary
903     (erase-buffer)
904     (insert "\n")
905     (let ((file (gnus-agent-lib-file "history")))
906       (when (file-exists-p file)
907         (nnheader-insert-file-contents file))
908       (set (make-local-variable 'gnus-agent-file-name) file))))
909
910 (defun gnus-agent-save-history ()
911   (save-excursion
912     (set-buffer gnus-agent-current-history)
913     (gnus-make-directory (file-name-directory gnus-agent-file-name))
914     (let ((coding-system-for-write gnus-agent-file-coding-system))
915       (write-region (1+ (point-min)) (point-max)
916                     gnus-agent-file-name nil 'silent))))
917
918 (defun gnus-agent-close-history ()
919   (when (gnus-buffer-live-p gnus-agent-current-history)
920     (kill-buffer gnus-agent-current-history)
921     (setq gnus-agent-history-buffers
922           (delq (assoc (gnus-agent-method) gnus-agent-history-buffers)
923                 gnus-agent-history-buffers))))
924
925 (defun gnus-agent-enter-history (id group-arts date)
926   (save-excursion
927     (set-buffer gnus-agent-current-history)
928     (goto-char (point-max))
929     (let ((p (point)))
930       (insert id "\t" (number-to-string date) "\t")
931       (while group-arts
932         (insert (format "%S" (intern (caar group-arts)))
933                 " " (number-to-string (cdr (pop group-arts)))
934                 " "))
935       (insert "\n")
936       (while (search-backward "\\." p t)
937         (delete-char 1)))))
938
939 (defun gnus-agent-article-in-history-p (id)
940   (save-excursion
941     (set-buffer (gnus-agent-history-buffer))
942     (goto-char (point-min))
943     (search-forward (concat "\n" id "\t") nil t)))
944
945 (defun gnus-agent-history-path (id)
946   (save-excursion
947     (set-buffer (gnus-agent-history-buffer))
948     (goto-char (point-min))
949     (when (search-forward (concat "\n" id "\t") nil t)
950       (let ((method (gnus-agent-method)))
951         (let (paths group)
952           (while (not (numberp (setq group (read (current-buffer)))))
953             (push (concat method "/" group) paths))
954           (nreverse paths))))))
955
956 ;;;
957 ;;; Fetching
958 ;;;
959
960 (defun gnus-agent-fetch-articles (group articles)
961   "Fetch ARTICLES from GROUP and put them into the Agent."
962   (when articles
963     ;; Prune off articles that we have already fetched.
964     (while (and articles
965                 (cdr (assq (car articles) gnus-agent-article-alist)))
966       (pop articles))
967     (let ((arts articles))
968       (while (cdr arts)
969         (if (cdr (assq (cadr arts) gnus-agent-article-alist))
970             (setcdr arts (cddr arts))
971           (setq arts (cdr arts)))))
972     (when articles
973       (let ((dir (concat
974                   (gnus-agent-directory)
975                   (gnus-agent-group-path group) "/"))
976             (date (time-to-days (current-time)))
977             (case-fold-search t)
978             pos crosses id elem)
979         (gnus-make-directory dir)
980         (gnus-message 7 "Fetching articles for %s..." group)
981         ;; Fetch the articles from the backend.
982         (if (gnus-check-backend-function 'retrieve-articles group)
983             (setq pos (gnus-retrieve-articles articles group))
984           (with-temp-buffer
985             (let (article)
986               (while (setq article (pop articles))
987                 (when (or
988                        (gnus-backlog-request-article group article
989                                                      nntp-server-buffer)
990                        (gnus-request-article article group))
991                   (goto-char (point-max))
992                   (push (cons article (point)) pos)
993                   (insert-buffer-substring nntp-server-buffer)))
994               (copy-to-buffer nntp-server-buffer (point-min) (point-max))
995               (setq pos (nreverse pos)))))
996         ;; Then save these articles into the Agent.
997         (save-excursion
998           (set-buffer nntp-server-buffer)
999           (while pos
1000             (narrow-to-region (cdar pos) (or (cdadr pos) (point-max)))
1001             (goto-char (point-min))
1002             (unless (eobp)  ;; Don't save empty articles.
1003               (when (search-forward "\n\n" nil t)
1004                 (when (search-backward "\nXrefs: " nil t)
1005                   ;; Handle cross posting.
1006                   (skip-chars-forward "^ ")
1007                   (skip-chars-forward " ")
1008                   (setq crosses nil)
1009                   (while (looking-at "\\([^: \n]+\\):\\([0-9]+\\) +")
1010                     (push (cons (buffer-substring (match-beginning 1)
1011                                                   (match-end 1))
1012                                 (buffer-substring (match-beginning 2)
1013                                                   (match-end 2)))
1014                           crosses)
1015                     (goto-char (match-end 0)))
1016                   (gnus-agent-crosspost crosses (caar pos))))
1017               (goto-char (point-min))
1018               (if (not (re-search-forward
1019                         "^Message-ID: *<\\([^>\n]+\\)>" nil t))
1020                   (setq id "No-Message-ID-in-article")
1021                 (setq id (buffer-substring (match-beginning 1) (match-end 1))))
1022               (let ((coding-system-for-write
1023                      gnus-agent-file-coding-system))
1024                 (write-region (point-min) (point-max)
1025                               (concat dir (number-to-string (caar pos)))
1026                               nil 'silent))
1027               (when (setq elem (assq (caar pos) gnus-agent-article-alist))
1028                 (setcdr elem t))
1029               (gnus-agent-enter-history
1030                id (or crosses (list (cons group (caar pos)))) date))
1031             (widen)
1032             (pop pos)))
1033         (gnus-agent-save-alist group)))))
1034
1035 (defun gnus-agent-crosspost (crosses article)
1036   (let (gnus-agent-article-alist group alist beg end)
1037     (save-excursion
1038       (set-buffer gnus-agent-overview-buffer)
1039       (when (nnheader-find-nov-line article)
1040         (forward-word 1)
1041         (setq beg (point))
1042         (setq end (progn (forward-line 1) (point)))))
1043     (while crosses
1044       (setq group (caar crosses))
1045       (unless (setq alist (assoc group gnus-agent-group-alist))
1046         (push (setq alist (list group (gnus-agent-load-alist (caar crosses))))
1047               gnus-agent-group-alist))
1048       (setcdr alist (cons (cons (cdar crosses) t) (cdr alist)))
1049       (save-excursion
1050         (set-buffer (gnus-get-buffer-create (format " *Gnus agent overview %s*"
1051                                                     group)))
1052         (when (= (point-max) (point-min))
1053           (push (cons group (current-buffer)) gnus-agent-buffer-alist)
1054           (ignore-errors
1055             (nnheader-insert-file-contents
1056              (gnus-agent-article-name ".overview" group))))
1057         (nnheader-find-nov-line (string-to-number (cdar crosses)))
1058         (insert (string-to-number (cdar crosses)))
1059         (insert-buffer-substring gnus-agent-overview-buffer beg end))
1060       (pop crosses))))
1061
1062 (defun gnus-agent-flush-cache ()
1063   (save-excursion
1064     (while gnus-agent-buffer-alist
1065       (set-buffer (cdar gnus-agent-buffer-alist))
1066       (let ((coding-system-for-write
1067              gnus-agent-file-coding-system))
1068         (write-region (point-min) (point-max)
1069                       (gnus-agent-article-name ".overview"
1070                                                (caar gnus-agent-buffer-alist))
1071                       nil 'silent))
1072       (pop gnus-agent-buffer-alist))
1073     (while gnus-agent-group-alist
1074       (with-temp-file (caar gnus-agent-group-alist)
1075         (princ (cdar gnus-agent-group-alist))
1076         (insert "\n"))
1077       (pop gnus-agent-group-alist))))
1078
1079 (defun gnus-agent-fetch-headers (group &optional force)
1080   (let ((articles (gnus-list-of-unread-articles group))
1081         (gnus-decode-encoded-word-function 'identity)
1082         (file (gnus-agent-article-name ".overview" group))
1083         gnus-agent-cache)
1084     ;; Add article with marks to list of article headers we want to fetch.
1085     (dolist (arts (gnus-info-marks (gnus-get-info group)))
1086       (unless (memq (car arts) '(seen recent))
1087         (setq articles (gnus-range-add articles (cdr arts)))))
1088     (setq articles (sort (gnus-uncompress-sequence articles) '<))
1089     ;; Remove known articles.
1090     (when (gnus-agent-load-alist group)
1091       (setq articles (gnus-list-range-intersection
1092                       articles
1093                       (list
1094                        (cons (1+ (caar (last gnus-agent-article-alist)))
1095                              (cdr (gnus-active group)))))))
1096     ;; Fetch them.
1097     (gnus-make-directory (nnheader-translate-file-chars
1098                           (file-name-directory file) t))
1099     (when articles
1100       (gnus-message 7 "Fetching headers for %s..." group)
1101       (save-excursion
1102         (set-buffer nntp-server-buffer)
1103         (unless (eq 'nov (gnus-retrieve-headers articles group))
1104           (nnvirtual-convert-headers))
1105         ;; Save these headers for later processing.
1106         (copy-to-buffer gnus-agent-overview-buffer (point-min) (point-max))
1107         (when (file-exists-p file)
1108           (gnus-agent-braid-nov group articles file))
1109         (let ((coding-system-for-write
1110                gnus-agent-file-coding-system))
1111           (write-region (point-min) (point-max) file nil 'silent))
1112         (gnus-agent-save-alist group articles nil)
1113         (gnus-agent-enter-history
1114          "last-header-fetched-for-session"
1115          (list (cons group (nth (- (length  articles) 1) articles)))
1116          (time-to-days (current-time)))
1117         articles))))
1118
1119 (defsubst gnus-agent-copy-nov-line (article)
1120   (let (art b e)
1121     (set-buffer gnus-agent-overview-buffer)
1122     (while (and (not (eobp))
1123                 (< (setq art (read (current-buffer))) article))
1124       (forward-line 1))
1125     (beginning-of-line)
1126     (if (or (eobp)
1127             (not (eq article art)))
1128         (set-buffer nntp-server-buffer)
1129       (setq b (point))
1130       (setq e (progn (forward-line 1) (point)))
1131       (set-buffer nntp-server-buffer)
1132       (insert-buffer-substring gnus-agent-overview-buffer b e))))
1133
1134 (defun gnus-agent-braid-nov (group articles file)
1135   (let (start last)
1136     (set-buffer gnus-agent-overview-buffer)
1137     (goto-char (point-min))
1138     (set-buffer nntp-server-buffer)
1139     (erase-buffer)
1140     (nnheader-insert-file-contents file)
1141     (goto-char (point-max))
1142     (unless (or (= (point-min) (point-max))
1143                 (progn
1144                   (forward-line -1)
1145                   (< (setq last (read (current-buffer))) (car articles))))
1146       ;; We do it the hard way.
1147       (nnheader-find-nov-line (car articles))
1148       (gnus-agent-copy-nov-line (pop articles))
1149       (while (and articles
1150                   (not (eobp)))
1151         (while (and (not (eobp))
1152                     (< (read (current-buffer)) (car articles)))
1153           (forward-line 1))
1154         (beginning-of-line)
1155         (unless (eobp)
1156           (gnus-agent-copy-nov-line (pop articles)))))
1157     ;; Copy the rest lines
1158     (set-buffer nntp-server-buffer)
1159     (goto-char (point-max))
1160     (when articles
1161       (when last
1162         (set-buffer gnus-agent-overview-buffer)
1163         (while (and (not (eobp))
1164                     (<= (read (current-buffer)) last))
1165           (forward-line 1))
1166         (beginning-of-line)
1167         (setq start (point))
1168         (set-buffer nntp-server-buffer))
1169       (insert-buffer-substring gnus-agent-overview-buffer start))))
1170
1171 (defun gnus-agent-load-alist (group &optional dir)
1172   "Load the article-state alist for GROUP."
1173   (let ((file ))
1174   (setq gnus-agent-article-alist
1175         (gnus-cache-file-contents
1176          (if dir
1177                   (expand-file-name ".agentview" dir)
1178                 (gnus-agent-article-name ".agentview" group))
1179          'gnus-agent-file-loading-cache
1180          'gnus-agent-read-file))))
1181
1182 (defun gnus-agent-save-alist (group &optional articles state dir)
1183   "Save the article-state alist for GROUP."
1184   (let* ((file-name-coding-system nnmail-pathname-coding-system)
1185          (prev (cons nil gnus-agent-article-alist))
1186          (all prev)
1187          print-level print-length item article)
1188     (while (setq article (pop articles))
1189       (while (and (cdr prev)
1190                  (< (caadr prev) article))
1191         (setq prev (cdr prev)))
1192       (cond
1193        ((not (cdr prev))
1194         (setcdr prev (list (cons article state))))
1195        ((> (caadr prev) article)
1196         (setcdr prev (cons (cons article state) (cdr prev))))
1197        ((= (caadr prev) article)
1198         (setcdr (cadr prev) state)))
1199       (setq prev (cdr prev)))
1200     (setq gnus-agent-article-alist (cdr all))
1201     (with-temp-file (if dir
1202                         (expand-file-name ".agentview" dir)
1203                       (gnus-agent-article-name ".agentview" group))
1204       (princ gnus-agent-article-alist (current-buffer))
1205       (insert "\n"))))
1206
1207 (defun gnus-agent-article-name (article group)
1208   (expand-file-name (if (stringp article) article (string-to-number article))
1209                     (file-name-as-directory
1210                      (expand-file-name (gnus-agent-group-path group)
1211                                        (gnus-agent-directory)))))
1212
1213 (defun gnus-agent-batch-confirmation (msg)
1214   "Show error message and return t."
1215   (gnus-message 1 msg)
1216   t)
1217
1218 ;;;###autoload
1219 (defun gnus-agent-batch-fetch ()
1220   "Start Gnus and fetch session."
1221   (interactive)
1222   (gnus)
1223   (let ((gnus-agent-confirmation-function 'gnus-agent-batch-confirmation))
1224     (gnus-agent-fetch-session))
1225   (gnus-group-exit))
1226
1227 (defun gnus-agent-fetch-session ()
1228   "Fetch all articles and headers that are eligible for fetching."
1229   (interactive)
1230   (unless gnus-agent-covered-methods
1231     (error "No servers are covered by the Gnus agent"))
1232   (unless gnus-plugged
1233     (error "Can't fetch articles while Gnus is unplugged"))
1234   (let ((methods gnus-agent-covered-methods)
1235         groups group gnus-command-method)
1236     (save-excursion
1237       (while methods
1238         (condition-case err
1239             (progn
1240               (setq gnus-command-method (car methods))
1241               (when (and (or (gnus-server-opened gnus-command-method)
1242                              (gnus-open-server gnus-command-method))
1243                          (gnus-online gnus-command-method))
1244                 (setq groups (gnus-groups-from-server (car methods)))
1245                 (gnus-agent-with-fetch
1246                   (while (setq group (pop groups))
1247                     (when (<= (gnus-group-level group) gnus-agent-handle-level)
1248                       (gnus-agent-fetch-group-1 group gnus-command-method))))))
1249           (error
1250            (unless (funcall gnus-agent-confirmation-function
1251                             (format "Error (%s).  Continue? " (cadr err)))
1252              (error "Cannot fetch articles into the Gnus agent")))
1253           (quit
1254            (unless (funcall gnus-agent-confirmation-function
1255                             (format "Quit fetching session (%s).  Continue? "
1256                                     (cadr err)))
1257              (signal 'quit "Cannot fetch articles into the Gnus agent"))))
1258         (pop methods))
1259       (run-hooks 'gnus-agent-fetch-hook)
1260       (gnus-message 6 "Finished fetching articles into the Gnus agent"))))
1261
1262 (defun gnus-agent-fetch-group-1 (group method)
1263   "Fetch GROUP."
1264   (let ((gnus-command-method method)
1265         (gnus-newsgroup-name group)
1266         gnus-newsgroup-dependencies gnus-newsgroup-headers
1267         gnus-newsgroup-scored gnus-headers gnus-score
1268         gnus-use-cache articles arts
1269         category predicate info marks score-param
1270         (gnus-summary-expunge-below gnus-summary-expunge-below)
1271         (gnus-summary-mark-below gnus-summary-mark-below)
1272         (gnus-orphan-score gnus-orphan-score)
1273         ;; Maybe some other gnus-summary local variables should also
1274         ;; be put here.
1275         )
1276     (unless (gnus-check-group group)
1277       (error "Can't open server for %s" group))
1278     ;; Fetch headers.
1279     (when (and (or (gnus-active group)
1280                    (gnus-activate-group group))
1281                (setq articles (gnus-agent-fetch-headers group))
1282                (let ((nntp-server-buffer gnus-agent-overview-buffer))
1283                  ;; Parse them and see which articles we want to fetch.
1284                  (setq gnus-newsgroup-dependencies
1285                        (make-vector (length articles) 0))
1286                  (setq gnus-newsgroup-headers
1287                        (gnus-get-newsgroup-headers-xover articles nil nil
1288                                                          group))
1289                  ;; `gnus-agent-overview-buffer' may be killed for
1290                  ;; timeout reason.  If so, recreate it.
1291                  (gnus-agent-create-buffer)))
1292       (setq category (gnus-group-category group))
1293       (setq predicate
1294             (gnus-get-predicate
1295              (or (gnus-group-find-parameter group 'agent-predicate t)
1296                  (cadr category))))
1297       (if (memq predicate '(gnus-agent-true gnus-agent-false))
1298           ;; Simple implementation
1299           (setq arts (and (eq predicate 'gnus-agent-true) articles))
1300         (setq arts nil)
1301         (setq score-param
1302               (or (gnus-group-get-parameter group 'agent-score t)
1303                   (caddr category)))
1304         ;; Translate score-param into real one
1305         (cond
1306          ((not score-param))
1307          ((eq score-param 'file)
1308           (setq score-param (gnus-all-score-files group)))
1309          ((stringp (car score-param)))
1310          (t
1311           (setq score-param (list (list score-param)))))
1312         (when score-param
1313           (gnus-score-headers score-param))
1314         (while (setq gnus-headers (pop gnus-newsgroup-headers))
1315           (setq gnus-score
1316                 (or (cdr (assq (mail-header-number gnus-headers)
1317                                gnus-newsgroup-scored))
1318                     gnus-summary-default-score))
1319           (when (funcall predicate)
1320             (push (mail-header-number gnus-headers)
1321                   arts))))
1322       ;; Fetch the articles.
1323       (when arts
1324         (gnus-agent-fetch-articles group arts)))
1325     ;; Perhaps we have some additional articles to fetch.
1326     (dolist (mark gnus-agent-download-marks)
1327       (setq arts (assq mark (gnus-info-marks
1328                              (setq info (gnus-get-info group)))))
1329       (when (cdr arts)
1330         (gnus-message 8 "Agent is downloading marked articles...")
1331         (gnus-agent-fetch-articles
1332          group (gnus-uncompress-range (cdr arts)))
1333         (when (eq mark 'download)
1334           (setq marks (delq arts (gnus-info-marks info)))
1335           (gnus-info-set-marks info marks)
1336           (gnus-dribble-enter
1337            (concat "(gnus-group-set-info '"
1338                    (gnus-prin1-to-string info)
1339                    ")")))))))
1340
1341 ;;;
1342 ;;; Agent Category Mode
1343 ;;;
1344
1345 (defvar gnus-category-mode-hook nil
1346   "Hook run in `gnus-category-mode' buffers.")
1347
1348 (defvar gnus-category-line-format "     %(%20c%): %g\n"
1349   "Format of category lines.
1350
1351 Valid specifiers include:
1352 %c  Topic name (string)
1353 %g  The number of groups in the topic (integer)
1354
1355 General format specifiers can also be used.  See Info node
1356 `(gnus)Formatting Variables'.")
1357
1358 (defvar gnus-category-mode-line-format "Gnus: %%b"
1359   "The format specification for the category mode line.")
1360
1361 (defvar gnus-agent-short-article 100
1362   "Articles that have fewer lines than this are short.")
1363
1364 (defvar gnus-agent-long-article 200
1365   "Articles that have more lines than this are long.")
1366
1367 (defvar gnus-agent-low-score 0
1368   "Articles that have a score lower than this have a low score.")
1369
1370 (defvar gnus-agent-high-score 0
1371   "Articles that have a score higher than this have a high score.")
1372
1373
1374 ;;; Internal variables.
1375
1376 (defvar gnus-category-buffer "*Agent Category*")
1377
1378 (defvar gnus-category-line-format-alist
1379   `((?c gnus-tmp-name ?s)
1380     (?g gnus-tmp-groups ?d)))
1381
1382 (defvar gnus-category-mode-line-format-alist
1383   `((?u user-defined ?s)))
1384
1385 (defvar gnus-category-line-format-spec nil)
1386 (defvar gnus-category-mode-line-format-spec nil)
1387
1388 (defvar gnus-category-mode-map nil)
1389 (put 'gnus-category-mode 'mode-class 'special)
1390
1391 (unless gnus-category-mode-map
1392   (setq gnus-category-mode-map (make-sparse-keymap))
1393   (suppress-keymap gnus-category-mode-map)
1394
1395   (gnus-define-keys gnus-category-mode-map
1396     "q" gnus-category-exit
1397     "k" gnus-category-kill
1398     "c" gnus-category-copy
1399     "a" gnus-category-add
1400     "p" gnus-category-edit-predicate
1401     "g" gnus-category-edit-groups
1402     "s" gnus-category-edit-score
1403     "l" gnus-category-list
1404
1405     "\C-c\C-i" gnus-info-find-node
1406     "\C-c\C-b" gnus-bug))
1407
1408 (defvar gnus-category-menu-hook nil
1409   "*Hook run after the creation of the menu.")
1410
1411 (defun gnus-category-make-menu-bar ()
1412   (gnus-turn-off-edit-menu 'category)
1413   (unless (boundp 'gnus-category-menu)
1414     (easy-menu-define
1415      gnus-category-menu gnus-category-mode-map ""
1416      '("Categories"
1417        ["Add" gnus-category-add t]
1418        ["Kill" gnus-category-kill t]
1419        ["Copy" gnus-category-copy t]
1420        ["Edit predicate" gnus-category-edit-predicate t]
1421        ["Edit score" gnus-category-edit-score t]
1422        ["Edit groups" gnus-category-edit-groups t]
1423        ["Exit" gnus-category-exit t]))
1424
1425     (gnus-run-hooks 'gnus-category-menu-hook)))
1426
1427 (defun gnus-category-mode ()
1428   "Major mode for listing and editing agent categories.
1429
1430 All normal editing commands are switched off.
1431 \\<gnus-category-mode-map>
1432 For more in-depth information on this mode, read the manual
1433 (`\\[gnus-info-find-node]').
1434
1435 The following commands are available:
1436
1437 \\{gnus-category-mode-map}"
1438   (interactive)
1439   (when (gnus-visual-p 'category-menu 'menu)
1440     (gnus-category-make-menu-bar))
1441   (kill-all-local-variables)
1442   (gnus-simplify-mode-line)
1443   (setq major-mode 'gnus-category-mode)
1444   (setq mode-name "Category")
1445   (gnus-set-default-directory)
1446   (setq mode-line-process nil)
1447   (use-local-map gnus-category-mode-map)
1448   (buffer-disable-undo)
1449   (setq truncate-lines t)
1450   (setq buffer-read-only t)
1451   (gnus-run-hooks 'gnus-category-mode-hook))
1452
1453 (defalias 'gnus-category-position-point 'gnus-goto-colon)
1454
1455 (defun gnus-category-insert-line (category)
1456   (let* ((gnus-tmp-name (format "%s" (car category)))
1457          (gnus-tmp-groups (length (cadddr category))))
1458     (beginning-of-line)
1459     (gnus-add-text-properties
1460      (point)
1461      (prog1 (1+ (point))
1462        ;; Insert the text.
1463        (eval gnus-category-line-format-spec))
1464      (list 'gnus-category gnus-tmp-name))))
1465
1466 (defun gnus-enter-category-buffer ()
1467   "Go to the Category buffer."
1468   (interactive)
1469   (gnus-category-setup-buffer)
1470   (gnus-configure-windows 'category)
1471   (gnus-category-prepare))
1472
1473 (defun gnus-category-setup-buffer ()
1474   (unless (get-buffer gnus-category-buffer)
1475     (save-excursion
1476       (set-buffer (gnus-get-buffer-create gnus-category-buffer))
1477       (gnus-category-mode))))
1478
1479 (defun gnus-category-prepare ()
1480   (gnus-set-format 'category-mode)
1481   (gnus-set-format 'category t)
1482   (let ((alist gnus-category-alist)
1483         (buffer-read-only nil))
1484     (erase-buffer)
1485     (while alist
1486       (gnus-category-insert-line (pop alist)))
1487     (goto-char (point-min))
1488     (gnus-category-position-point)))
1489
1490 (defun gnus-category-name ()
1491   (or (intern (get-text-property (gnus-point-at-bol) 'gnus-category))
1492       (error "No category on the current line")))
1493
1494 (defun gnus-category-read ()
1495   "Read the category alist."
1496   (setq gnus-category-alist
1497         (or (gnus-agent-read-file
1498              (nnheader-concat gnus-agent-directory "lib/categories"))
1499             (list (list 'default 'short nil nil)))))
1500
1501 (defun gnus-category-write ()
1502   "Write the category alist."
1503   (setq gnus-category-predicate-cache nil
1504         gnus-category-group-cache nil)
1505   (gnus-make-directory (nnheader-concat gnus-agent-directory "lib"))
1506   (with-temp-file (nnheader-concat gnus-agent-directory "lib/categories")
1507     (prin1 gnus-category-alist (current-buffer))))
1508
1509 (defun gnus-category-edit-predicate (category)
1510   "Edit the predicate for CATEGORY."
1511   (interactive (list (gnus-category-name)))
1512   (let ((info (assq category gnus-category-alist)))
1513     (gnus-edit-form
1514      (cadr info) (format "Editing the predicate for category %s" category)
1515      `(lambda (predicate)
1516         (setcar (cdr (assq ',category gnus-category-alist)) predicate)
1517         (gnus-category-write)
1518         (gnus-category-list)))))
1519
1520 (defun gnus-category-edit-score (category)
1521   "Edit the score expression for CATEGORY."
1522   (interactive (list (gnus-category-name)))
1523   (let ((info (assq category gnus-category-alist)))
1524     (gnus-edit-form
1525      (caddr info)
1526      (format "Editing the score expression for category %s" category)
1527      `(lambda (groups)
1528         (setcar (cddr (assq ',category gnus-category-alist)) groups)
1529         (gnus-category-write)
1530         (gnus-category-list)))))
1531
1532 (defun gnus-category-edit-groups (category)
1533   "Edit the group list for CATEGORY."
1534   (interactive (list (gnus-category-name)))
1535   (let ((info (assq category gnus-category-alist)))
1536     (gnus-edit-form
1537      (cadddr info) (format "Editing the group list for category %s" category)
1538      `(lambda (groups)
1539         (setcar (nthcdr 3 (assq ',category gnus-category-alist)) groups)
1540         (gnus-category-write)
1541         (gnus-category-list)))))
1542
1543 (defun gnus-category-kill (category)
1544   "Kill the current category."
1545   (interactive (list (gnus-category-name)))
1546   (let ((info (assq category gnus-category-alist))
1547         (buffer-read-only nil))
1548     (gnus-delete-line)
1549     (setq gnus-category-alist (delq info gnus-category-alist))
1550     (gnus-category-write)))
1551
1552 (defun gnus-category-copy (category to)
1553   "Copy the current category."
1554   (interactive (list (gnus-category-name) (intern (read-string "New name: "))))
1555   (let ((info (assq category gnus-category-alist)))
1556     (push (list to (gnus-copy-sequence (cadr info))
1557                 (gnus-copy-sequence (caddr info)) nil)
1558           gnus-category-alist)
1559     (gnus-category-write)
1560     (gnus-category-list)))
1561
1562 (defun gnus-category-add (category)
1563   "Create a new category."
1564   (interactive "SCategory name: ")
1565   (when (assq category gnus-category-alist)
1566     (error "Category %s already exists" category))
1567   (push (list category 'false nil nil)
1568         gnus-category-alist)
1569   (gnus-category-write)
1570   (gnus-category-list))
1571
1572 (defun gnus-category-list ()
1573   "List all categories."
1574   (interactive)
1575   (gnus-category-prepare))
1576
1577 (defun gnus-category-exit ()
1578   "Return to the group buffer."
1579   (interactive)
1580   (kill-buffer (current-buffer))
1581   (gnus-configure-windows 'group t))
1582
1583 ;; To avoid having 8-bit characters in the source file.
1584 (defvar gnus-category-not (list '! 'not (intern (format "%c" 172))))
1585
1586 (defvar gnus-category-predicate-alist
1587   '((spam . gnus-agent-spam-p)
1588     (short . gnus-agent-short-p)
1589     (long . gnus-agent-long-p)
1590     (low . gnus-agent-low-scored-p)
1591     (high . gnus-agent-high-scored-p)
1592     (true . gnus-agent-true)
1593     (false . gnus-agent-false))
1594   "Mapping from short score predicate symbols to predicate functions.")
1595
1596 (defun gnus-agent-spam-p ()
1597   "Say whether an article is spam or not."
1598   (unless gnus-agent-spam-hashtb
1599     (setq gnus-agent-spam-hashtb (gnus-make-hashtable 1000)))
1600   (if (not (equal (mail-header-references gnus-headers) ""))
1601       nil
1602     (let ((string (gnus-simplify-subject (mail-header-subject gnus-headers))))
1603       (prog1
1604           (gnus-gethash string gnus-agent-spam-hashtb)
1605         (gnus-sethash string t gnus-agent-spam-hashtb)))))
1606
1607 (defun gnus-agent-short-p ()
1608   "Say whether an article is short or not."
1609   (< (mail-header-lines gnus-headers) gnus-agent-short-article))
1610
1611 (defun gnus-agent-long-p ()
1612   "Say whether an article is long or not."
1613   (> (mail-header-lines gnus-headers) gnus-agent-long-article))
1614
1615 (defun gnus-agent-low-scored-p ()
1616   "Say whether an article has a low score or not."
1617   (< gnus-score gnus-agent-low-score))
1618
1619 (defun gnus-agent-high-scored-p ()
1620   "Say whether an article has a high score or not."
1621   (> gnus-score gnus-agent-high-score))
1622
1623 (defun gnus-category-make-function (cat)
1624   "Make a function from category CAT."
1625   (let ((func (gnus-category-make-function-1 cat)))
1626     (if (and (= (length func) 1)
1627              (symbolp (car func)))
1628         (car func)
1629       (gnus-byte-compile `(lambda () ,func)))))
1630
1631 (defun gnus-agent-true ()
1632   "Return t."
1633   t)
1634
1635 (defun gnus-agent-false ()
1636   "Return nil."
1637   nil)
1638
1639 (defun gnus-category-make-function-1 (cat)
1640   "Make a function from category CAT."
1641   (cond
1642    ;; Functions are just returned as is.
1643    ((or (symbolp cat)
1644         (gnus-functionp cat))
1645     `(,(or (cdr (assq cat gnus-category-predicate-alist))
1646            cat)))
1647    ;; More complex category.
1648    ((consp cat)
1649     `(,(cond
1650         ((memq (car cat) '(& and))
1651          'and)
1652         ((memq (car cat) '(| or))
1653          'or)
1654         ((memq (car cat) gnus-category-not)
1655          'not))
1656       ,@(mapcar 'gnus-category-make-function-1 (cdr cat))))
1657    (t
1658     (error "Unknown category type: %s" cat))))
1659
1660 (defun gnus-get-predicate (predicate)
1661   "Return the predicate for CATEGORY."
1662   (or (cdr (assoc predicate gnus-category-predicate-cache))
1663       (let ((func (gnus-category-make-function predicate)))
1664         (setq gnus-category-predicate-cache
1665               (nconc gnus-category-predicate-cache
1666                      (list (cons predicate func))))
1667         func)))
1668
1669 (defun gnus-group-category (group)
1670   "Return the category GROUP belongs to."
1671   (unless gnus-category-group-cache
1672     (setq gnus-category-group-cache (gnus-make-hashtable 1000))
1673     (let ((cs gnus-category-alist)
1674           groups cat)
1675       (while (setq cat (pop cs))
1676         (setq groups (cadddr cat))
1677         (while groups
1678           (gnus-sethash (pop groups) cat gnus-category-group-cache)))))
1679   (or (gnus-gethash group gnus-category-group-cache)
1680       (assq 'default gnus-category-alist)))
1681
1682 (defun gnus-agent-expire (&optional articles group force)
1683   "Expire all old articles.
1684 If you want to force expiring of certain articles, this function can
1685 take ARTICLES, GROUP and FORCE parameters as well.  Setting ARTICLES
1686 and GROUP without FORCE is not supported."
1687   (interactive)
1688   (let ((methods (if group
1689                      (list (gnus-find-method-for-group group))
1690                    gnus-agent-covered-methods))
1691         (day (if (numberp gnus-agent-expire-days)
1692                  (- (time-to-days (current-time)) gnus-agent-expire-days)
1693                nil))
1694         (current-day (time-to-days (current-time)))
1695         gnus-command-method sym arts pos
1696         history overview file histories elem art nov-file low info
1697         unreads marked article orig lowest highest found days)
1698     (save-excursion
1699       (setq overview (gnus-get-buffer-create " *expire overview*"))
1700       (while (setq gnus-command-method (pop methods))
1701         (when (file-exists-p (gnus-agent-lib-file "active"))
1702           (with-temp-buffer
1703             (nnheader-insert-file-contents (gnus-agent-lib-file "active"))
1704             (gnus-active-to-gnus-format
1705              gnus-command-method
1706              (setq orig (gnus-make-hashtable
1707                          (count-lines (point-min) (point-max))))))
1708           (let ((expiry-hashtb (gnus-make-hashtable 1023)))
1709             (gnus-agent-open-history)
1710             (set-buffer
1711              (setq gnus-agent-current-history
1712                    (setq history (gnus-agent-history-buffer))))
1713             (goto-char (point-min))
1714             (if (and articles group force) ;; point usless without art+group
1715                 (while (setq article (pop articles))
1716                   ;; try to find history entries for articles
1717                   (goto-char (point-min))
1718                   (if (re-search-forward 
1719                        (concat "^[^\t]*\t[^\t]*\t\(.* ?\)"
1720                                (format "%S" (gnus-group-prefixed-name
1721                                              group gnus-command-method))
1722                                " "
1723                                (number-to-string article)
1724                                " $")
1725                        nil t)
1726                       (setq pos (point))
1727                     (setq pos nil))
1728                   (setq sym (let ((obarray expiry-hashtb) s)
1729                               (intern group)))
1730                   (if (boundp sym)
1731                       (set sym (cons (cons article pos)
1732                                      (symbol-value sym)))
1733                     (set sym (list (cons article pos)))))
1734               ;; go through history file to find eligble articles
1735               (when (> (buffer-size) 1)
1736                 (goto-char (point-min))
1737                 (while (not (eobp))
1738                   (skip-chars-forward "^\t")
1739                   (if (let ((fetch-date (read (current-buffer))))
1740                         (if (numberp fetch-date)
1741                             ;; We now have the arrival day, so we see
1742                             ;; whether it's old enough to be expired.
1743                             (if (numberp day)
1744                                 (> fetch-date day)
1745                               (skip-chars-forward "\t")
1746                               (setq found nil
1747                                     days gnus-agent-expire-days)
1748                               (while (and (not found)
1749                                           days)
1750                                 (when (looking-at (caar days))
1751                                   (setq found (cadar days)))
1752                                 (pop days))
1753                               (> fetch-date (- current-day found)))
1754                           ;; History file is corrupted.
1755                           (gnus-message
1756                            5
1757                            (format "File %s is corrupted!"
1758                                    (gnus-agent-lib-file "history")))
1759                           (sit-for 1)
1760                           ;; Ignore it
1761                           t))
1762                       ;; New article; we don't expire it.
1763                       (forward-line 1)
1764                     ;; Old article.  Schedule it for possible nuking.
1765                     (while (not (eolp))
1766                       (setq sym (let ((obarray expiry-hashtb) s)
1767                                   (setq s (read (current-buffer)))
1768                                   (if (stringp s) (intern s) s)))
1769                       (if (boundp sym)
1770                           (set sym (cons (cons (read (current-buffer)) (point))
1771                                          (symbol-value sym)))
1772                         (set sym (list (cons (read (current-buffer))
1773                                              (point)))))
1774                       (skip-chars-forward " "))
1775                     (forward-line 1)))))
1776             ;; We now have all articles that can possibly be expired.
1777             (mapatoms
1778              (lambda (sym)
1779                (setq group (symbol-name sym)
1780                      arts (sort (symbol-value sym) 'car-less-than-car)
1781                      low (car (gnus-active group))
1782                      info (gnus-get-info group)
1783                      unreads (ignore-errors
1784                                (gnus-list-of-unread-articles group))
1785                      marked (nconc
1786                              (gnus-uncompress-range
1787                               (cdr (assq 'tick (gnus-info-marks info))))
1788                              (gnus-uncompress-range
1789                               (cdr (assq 'dormant
1790                                          (gnus-info-marks info)))))
1791                      nov-file (gnus-agent-article-name ".overview" group)
1792                      lowest nil
1793                      highest nil)
1794                (gnus-agent-load-alist group)
1795                (gnus-message 5 "Expiring articles in %s" group)
1796                (set-buffer overview)
1797                (erase-buffer)
1798                (when (file-exists-p nov-file)
1799                  (nnheader-insert-file-contents nov-file))
1800                (goto-char (point-min))
1801                (setq article 0)
1802                (while (setq elem (pop arts))
1803                  (setq article (car elem))
1804                  (when (or (null low)
1805                            (< article low)
1806                            gnus-agent-expire-all
1807                            (and (not (memq article unreads))
1808                                 (not (memq article marked)))
1809                            force)
1810                    ;; Find and nuke the NOV line.
1811                    (while (and (not (eobp))
1812                                (or (not (numberp
1813                                          (setq art (read (current-buffer)))))
1814                                    (< art article)))
1815                      (if (and (numberp art)
1816                               (file-exists-p
1817                                (gnus-agent-article-name
1818                                 (number-to-string art) group)))
1819                          (progn
1820                            (unless lowest
1821                              (setq lowest art))
1822                            (setq highest art)
1823                            (forward-line 1))
1824                        ;; Remove old NOV lines that have no articles.
1825                        (gnus-delete-line)))
1826                    (if (or (eobp)
1827                            (/= art article))
1828                        (beginning-of-line)
1829                      (gnus-delete-line))
1830                    ;; Nuke the article.
1831                    (when (file-exists-p
1832                           (setq file (gnus-agent-article-name
1833                                       (number-to-string article)
1834                                       group)))
1835                      (delete-file file))
1836                    ;; Schedule the history line for nuking.
1837                    (if (cdr elem)
1838                        (push (cdr elem) histories))))
1839                (gnus-make-directory (file-name-directory nov-file))
1840                (let ((coding-system-for-write
1841                       gnus-agent-file-coding-system))
1842                  (write-region (point-min) (point-max) nov-file nil 'silent))
1843                ;; Delete the unwanted entries in the alist.
1844                (setq gnus-agent-article-alist
1845                      (sort gnus-agent-article-alist 'car-less-than-car))
1846                (let* ((alist gnus-agent-article-alist)
1847                       (prev (cons nil alist))
1848                       (first prev)
1849                       expired)
1850                  (while (and alist
1851                              (<= (caar alist) article))
1852                    (if (or (not (cdar alist))
1853                            (not (file-exists-p
1854                                  (gnus-agent-article-name
1855                                   (number-to-string
1856                                    (caar alist))
1857                                   group))))
1858                        (progn
1859                          (push (caar alist) expired)
1860                          (setcdr prev (setq alist (cdr alist))))
1861                      (setq prev alist
1862                            alist (cdr alist))))
1863                  (setq gnus-agent-article-alist (cdr first))
1864                  (gnus-agent-save-alist group)
1865                  ;; Mark all articles up to the first article
1866                  ;; in `gnus-agent-article-alist' as read.
1867                  (when (and info (caar gnus-agent-article-alist))
1868                    (setcar (nthcdr 2 info)
1869                            (gnus-range-add
1870                             (nth 2 info)
1871                             (cons 1 (- (caar gnus-agent-article-alist) 1)))))
1872                  ;; Maybe everything has been expired from
1873                  ;; `gnus-agent-article-alist' and so the above marking as
1874                  ;; read could not be conducted, or there are
1875                  ;; expired article within the range of the alist.
1876                  (when (and info
1877                             expired
1878                             (or (not (caar gnus-agent-article-alist))
1879                                 (> (car expired)
1880                                    (caar gnus-agent-article-alist))))
1881                    (setcar (nthcdr 2 info)
1882                            (gnus-add-to-range
1883                             (nth 2 info)
1884                             (nreverse expired))))
1885                  (gnus-dribble-enter
1886                   (concat "(gnus-group-set-info '"
1887                           (gnus-prin1-to-string info)
1888                           ")")))
1889                (when lowest
1890                  (if (gnus-gethash group orig)
1891                      (setcar (gnus-gethash group orig) lowest)
1892                    (gnus-sethash group (cons lowest highest) orig))))
1893              expiry-hashtb)
1894             (set-buffer history)
1895             (setq histories (nreverse (sort histories '<)))
1896             (while histories
1897               (goto-char (pop histories))
1898               (gnus-delete-line))
1899             (gnus-agent-save-history)
1900             (gnus-agent-close-history)
1901             (gnus-write-active-file
1902              (gnus-agent-lib-file "active") orig))
1903           (gnus-message 4 "Expiry...done"))))))
1904
1905 ;;;###autoload
1906 (defun gnus-agent-batch ()
1907   "Start Gnus, send queue and fetch session."
1908   (interactive)
1909   (let ((init-file-user "")
1910         (gnus-always-read-dribble-file t))
1911     (gnus))
1912   (let ((gnus-agent-confirmation-function 'gnus-agent-batch-confirmation))
1913     (gnus-group-send-queue)
1914     (gnus-agent-fetch-session)))
1915
1916 (defun gnus-agent-retrieve-headers (articles group &optional fetch-old)
1917   (save-excursion
1918     (gnus-agent-create-buffer)
1919     (let ((gnus-decode-encoded-word-function 'identity)
1920           (file (gnus-agent-article-name ".overview" group))
1921           cached-articles uncached-articles)
1922       (gnus-make-directory (nnheader-translate-file-chars
1923                             (file-name-directory file) t))
1924       (when (file-exists-p file)
1925         (with-current-buffer gnus-agent-overview-buffer
1926           (erase-buffer)
1927           (let ((nnheader-file-coding-system
1928                  gnus-agent-file-coding-system))
1929             (nnheader-insert-nov-file file (car articles)))
1930           (nnheader-find-nov-line (car articles))
1931           (while (not (eobp))
1932             (when (looking-at "[0-9]")
1933               (push (read (current-buffer)) cached-articles))
1934             (forward-line 1))
1935           (setq cached-articles (nreverse cached-articles))))
1936       (if (setq uncached-articles
1937                 (gnus-sorted-difference articles cached-articles))
1938           (progn
1939             (set-buffer nntp-server-buffer)
1940             (erase-buffer)
1941             (let (gnus-agent-cache)
1942               (unless (eq 'nov
1943                           (gnus-retrieve-headers
1944                            uncached-articles group fetch-old))
1945                 (nnvirtual-convert-headers)))
1946             (set-buffer gnus-agent-overview-buffer)
1947             (erase-buffer)
1948             (set-buffer nntp-server-buffer)
1949             (copy-to-buffer gnus-agent-overview-buffer (point-min) (point-max))
1950             (when (and uncached-articles (file-exists-p file))
1951               (gnus-agent-braid-nov group uncached-articles file))
1952             (set-buffer nntp-server-buffer)
1953             (let ((coding-system-for-write
1954                    gnus-agent-file-coding-system))
1955               (write-region (point-min) (point-max) file nil 'silent))
1956             (gnus-agent-load-alist group)
1957             (gnus-agent-save-alist group uncached-articles nil)
1958             (gnus-agent-open-history)
1959             (setq gnus-agent-current-history (gnus-agent-history-buffer))
1960             (gnus-agent-enter-history
1961              "last-header-fetched-for-session"
1962              (list (cons group (nth (- (length  articles) 1) articles)))
1963              (time-to-days (current-time)))
1964             (gnus-agent-save-history))
1965         (set-buffer nntp-server-buffer)
1966         (erase-buffer)
1967         (insert-buffer-substring gnus-agent-overview-buffer)))
1968     (if (and fetch-old
1969              (not (numberp fetch-old)))
1970         t                               ; Don't remove anything.
1971       (nnheader-nov-delete-outside-range
1972        (if fetch-old (max 1 (- (car articles) fetch-old))
1973          (car articles))
1974        (car (last articles)))
1975       t)
1976     'nov))
1977
1978 (defun gnus-agent-request-article (article group)
1979   "Retrieve ARTICLE in GROUP from the agent cache."
1980   (let* ((gnus-command-method (gnus-find-method-for-group group))
1981          (file (concat
1982                   (gnus-agent-directory)
1983                   (gnus-agent-group-path group) "/"
1984                   (number-to-string article)))
1985          (buffer-read-only nil))
1986     (when (and (file-exists-p file)
1987                (> (nth 7 (file-attributes file)) 0))
1988       (erase-buffer)
1989       (gnus-kill-all-overlays)
1990       (let ((coding-system-for-read gnus-cache-coding-system))
1991         (insert-file-contents file))
1992       t)))
1993
1994 (defun gnus-agent-regenerate-group (group &optional clean)
1995   "Regenerate GROUP."
1996   (let ((dir (concat (gnus-agent-directory)
1997                      (gnus-agent-group-path group) "/"))
1998         (file (gnus-agent-article-name ".overview" group))
1999         n point arts alist header new-alist changed)
2000     (when (file-exists-p dir)
2001       (setq arts
2002             (sort (mapcar (lambda (name) (string-to-int name))
2003                           (directory-files dir nil "^[0-9]+$" t))
2004                   '<)))
2005     (gnus-make-directory (nnheader-translate-file-chars
2006                           (file-name-directory file) t))
2007     (mm-with-unibyte-buffer
2008       (if (file-exists-p file)
2009           (let ((nnheader-file-coding-system
2010                  gnus-agent-file-coding-system))
2011             (nnheader-insert-file-contents file)))
2012       (goto-char (point-min))
2013       (while (not (eobp))
2014         (while (not (or (eobp) (looking-at "[0-9]")))
2015           (setq point (point))
2016           (forward-line 1)
2017           (delete-region point (point)))
2018         (unless (eobp)
2019           (setq n (read (current-buffer)))
2020           (when (and arts (> n (car arts)))
2021             (beginning-of-line)
2022             (while (and arts (> n (car arts)))
2023               (message "Regenerating NOV %s %d..." group (car arts))
2024               (mm-with-unibyte-buffer
2025                 (nnheader-insert-file-contents
2026                  (concat dir (number-to-string (car arts))))
2027                 (goto-char (point-min))
2028                 (if (search-forward "\n\n" nil t)
2029                     (delete-region (point) (point-max))
2030                   (goto-char (point-max)))
2031                 (setq header (nnheader-parse-head t)))
2032               (mail-header-set-number header (car arts))
2033               (nnheader-insert-nov header)
2034               (setq changed t)
2035               (push (cons (car arts) t) alist)
2036               (pop arts)))
2037           (if (and arts (= n (car arts)))
2038               (progn
2039                 (push (cons n t) alist)
2040                 (pop arts))
2041             (push (cons n nil) alist))
2042           (forward-line 1)))
2043       (if changed
2044           (let ((coding-system-for-write gnus-agent-file-coding-system))
2045             (write-region (point-min) (point-max) file nil 'silent))))
2046     (setq gnus-agent-article-alist nil)
2047     (unless clean
2048       (gnus-agent-load-alist group))
2049     (setq alist (sort alist 'car-less-than-car))
2050     (setq gnus-agent-article-alist (sort gnus-agent-article-alist
2051                                          'car-less-than-car))
2052     (while (and alist gnus-agent-article-alist)
2053       (cond
2054        ((< (caar alist) (caar gnus-agent-article-alist))
2055         (push (pop alist) new-alist))
2056        ((> (caar alist) (caar gnus-agent-article-alist))
2057         (push (list (car (pop gnus-agent-article-alist))) new-alist))
2058        (t
2059         (pop gnus-agent-article-alist)
2060         (while (and gnus-agent-article-alist
2061                     (= (caar alist) (caar gnus-agent-article-alist)))
2062           (pop gnus-agent-article-alist))
2063         (push (pop alist) new-alist))))
2064     (while alist
2065       (push (pop alist) new-alist))
2066     (while gnus-agent-article-alist
2067       (push (list (car (pop gnus-agent-article-alist))) new-alist))
2068     (setq gnus-agent-article-alist (nreverse new-alist))
2069     (gnus-agent-save-alist group)))
2070
2071 (defun gnus-agent-regenerate-history (group article)
2072   (let ((file (concat (gnus-agent-directory)
2073                       (gnus-agent-group-path group) "/"
2074                       (number-to-string article))) id)
2075     (mm-with-unibyte-buffer
2076       (nnheader-insert-file-contents file)
2077       (message-narrow-to-head)
2078       (goto-char (point-min))
2079       (if (not (re-search-forward "^Message-ID: *<\\([^>\n]+\\)>" nil t))
2080           (setq id "No-Message-ID-in-article")
2081         (setq id (buffer-substring (match-beginning 1) (match-end 1))))
2082       (gnus-agent-enter-history
2083        id (list (cons group article))
2084        (time-to-days (nth 5 (file-attributes file)))))))
2085
2086 ;;;###autoload
2087 (defun gnus-agent-regenerate (&optional clean)
2088   "Regenerate all agent covered files.
2089 If CLEAN, don't read existing active and agentview files."
2090   (interactive "P")
2091   (message "Regenerating Gnus agent files...")
2092   (dolist (gnus-command-method gnus-agent-covered-methods)
2093     (let ((active-file (gnus-agent-lib-file "active"))
2094           history-hashtb active-hashtb active-changed
2095           history-changed point)
2096       (gnus-make-directory (file-name-directory active-file))
2097       (if clean
2098           (setq active-hashtb (gnus-make-hashtable 1000))
2099         (mm-with-unibyte-buffer
2100           (if (file-exists-p active-file)
2101               (let ((nnheader-file-coding-system
2102                      gnus-agent-file-coding-system))
2103                 (nnheader-insert-file-contents active-file))
2104             (setq active-changed t))
2105           (gnus-active-to-gnus-format
2106            nil (setq active-hashtb
2107                      (gnus-make-hashtable
2108                       (count-lines (point-min) (point-max)))))))
2109       (gnus-agent-open-history)
2110       (setq history-hashtb (gnus-make-hashtable 1000))
2111       (with-current-buffer
2112           (setq gnus-agent-current-history (gnus-agent-history-buffer))
2113         (goto-char (point-min))
2114         (forward-line 1)
2115         (while (not (eobp))
2116           (if (looking-at
2117                "\\([^\t\n]+\\)\t[0-9]+\t\\([^ \n]+\\) \\([0-9]+\\)")
2118               (progn
2119                 (unless (string= (match-string 1)
2120                                  "last-header-fetched-for-session")
2121                   (gnus-sethash (match-string 2)
2122                                 (cons (string-to-number (match-string 3))
2123                                       (gnus-gethash-safe (match-string 2)
2124                                                          history-hashtb))
2125                                 history-hashtb))
2126                 (forward-line 1))
2127             (setq point (point))
2128             (forward-line 1)
2129             (delete-region point (point))
2130             (setq history-changed t))))
2131       (dolist (group (gnus-groups-from-server gnus-command-method))
2132         (gnus-agent-regenerate-group group clean)
2133         (let ((min (or (caar gnus-agent-article-alist) 1))
2134               (max (or (caar (last gnus-agent-article-alist)) 0))
2135               (active (gnus-gethash-safe (gnus-group-real-name group)
2136                                          active-hashtb)))
2137           (if (not active)
2138               (progn
2139                 (setq active (cons min max)
2140                       active-changed t)
2141                 (gnus-sethash group active active-hashtb))
2142             (when (> (car active) min)
2143               (setcar active min)
2144               (setq active-changed t))
2145             (when (< (cdr active) max)
2146               (setcdr active max)
2147               (setq active-changed t))))
2148         (let ((arts (sort (gnus-gethash-safe group history-hashtb) '<))
2149               n)
2150           (gnus-sethash group arts history-hashtb)
2151           (while (and arts gnus-agent-article-alist)
2152             (cond
2153              ((> (car arts) (caar gnus-agent-article-alist))
2154               (when (cdar gnus-agent-article-alist)
2155                 (gnus-agent-regenerate-history
2156                  group (caar gnus-agent-article-alist))
2157                 (setq history-changed t))
2158               (setq n (car (pop gnus-agent-article-alist)))
2159               (while (and gnus-agent-article-alist
2160                           (= n (caar gnus-agent-article-alist)))
2161                 (pop gnus-agent-article-alist)))
2162              ((< (car arts) (caar gnus-agent-article-alist))
2163               (setq n (pop arts))
2164               (while (and arts (= n (car arts)))
2165                 (pop arts)))
2166              (t
2167               (setq n (car (pop gnus-agent-article-alist)))
2168               (while (and gnus-agent-article-alist
2169                           (= n (caar gnus-agent-article-alist)))
2170                 (pop gnus-agent-article-alist))
2171               (setq n (pop arts))
2172               (while (and arts (= n (car arts)))
2173                 (pop arts)))))
2174           (while gnus-agent-article-alist
2175             (when (cdar gnus-agent-article-alist)
2176               (gnus-agent-regenerate-history
2177                group (caar gnus-agent-article-alist))
2178               (setq history-changed t))
2179             (pop gnus-agent-article-alist))))
2180       (when history-changed
2181         (message "Regenerate the history file of %s:%s"
2182                  (car gnus-command-method)
2183                  (cadr gnus-command-method))
2184         (gnus-agent-save-history))
2185       (gnus-agent-close-history)
2186       (when active-changed
2187         (message "Regenerate %s" active-file)
2188         (let ((nnmail-active-file-coding-system gnus-agent-file-coding-system))
2189           (gnus-write-active-file active-file active-hashtb)))))
2190   (message "Regenerating Gnus agent files...done"))
2191
2192 (defun gnus-agent-go-online (&optional force)
2193   "Switch servers into online status."
2194   (interactive (list t))
2195   (dolist (server gnus-opened-servers)
2196     (when (eq (nth 1 server) 'offline)
2197       (if (if (eq force 'ask)
2198               (gnus-y-or-n-p
2199                (format "Switch %s:%s into online status? "
2200                        (caar server) (cadar server)))
2201             force)
2202           (setcar (nthcdr 1 server) 'close)))))
2203
2204 (defun gnus-agent-toggle-group-plugged (group)
2205   "Toggle the status of the server of the current group."
2206   (interactive (list (gnus-group-group-name)))
2207   (let* ((method (gnus-find-method-for-group group))
2208          (status (cadr (assoc method gnus-opened-servers))))
2209     (if (eq status 'offline)
2210         (gnus-server-set-status method 'closed)
2211       (gnus-close-server method)
2212       (gnus-server-set-status method 'offline))
2213     (message "Turn %s:%s from %s to %s." (car method) (cadr method)
2214              (if (eq status 'offline) 'offline 'online)
2215              (if (eq status 'offline) 'online 'offline))))
2216
2217 (provide 'gnus-agent)
2218
2219 ;;; gnus-agent.el ends here