Importing Pterodactyl Gnus v0.97.
[elisp/gnus.git-] / lisp / gnus-agent.el
1 ;;; gnus-agent.el --- unplugged support for Gnus
2 ;; Copyright (C) 1997,98,99 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
5 ;; This file is part of GNU Emacs.
6
7 ;; GNU Emacs is free software; you can redistribute it and/or modify
8 ;; it under the terms of the GNU General Public License as published by
9 ;; the Free Software Foundation; either version 2, or (at your option)
10 ;; any later version.
11
12 ;; GNU Emacs is distributed in the hope that it will be useful,
13 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 ;; GNU General Public License for more details.
16
17 ;; You should have received a copy of the GNU General Public License
18 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
19 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
20 ;; Boston, MA 02111-1307, USA.
21
22 ;;; Commentary:
23
24 ;;; Code:
25
26 (require 'gnus)
27 (require 'gnus-cache)
28 (require 'nnvirtual)
29 (require 'gnus-sum)
30 (eval-when-compile
31   (require 'cl)
32   (require 'gnus-score))
33
34 (defcustom gnus-agent-directory (nnheader-concat gnus-directory "agent/")
35   "Where the Gnus agent will store its files."
36   :group 'gnus-agent
37   :type 'directory)
38
39 (defcustom gnus-agent-plugged-hook nil
40   "Hook run when plugging into the network."
41   :group 'gnus-agent
42   :type 'hook)
43
44 (defcustom gnus-agent-unplugged-hook nil
45   "Hook run when unplugging from the network."
46   :group 'gnus-agent
47   :type 'hook)
48
49 (defcustom gnus-agent-handle-level gnus-level-subscribed
50   "Groups on levels higher than this variable will be ignored by the Agent."
51   :group 'gnus-agent
52   :type 'integer)
53
54 (defcustom gnus-agent-expire-days 7
55   "Read articles older than this will be expired."
56   :group 'gnus-agent
57   :type 'integer)
58
59 (defcustom gnus-agent-expire-all nil
60   "If non-nil, also expire unread, ticked and dormant articles.
61 If nil, only read articles will be expired."
62   :group 'gnus-agent
63   :type 'boolean)
64
65 (defcustom gnus-agent-group-mode-hook nil
66   "Hook run in Agent group minor modes."
67   :group 'gnus-agent
68   :type 'hook)
69
70 (defcustom gnus-agent-summary-mode-hook nil
71   "Hook run in Agent summary minor modes."
72   :group 'gnus-agent
73   :type 'hook)
74
75 (defcustom gnus-agent-server-mode-hook nil
76   "Hook run in Agent summary minor modes."
77   :group 'gnus-agent
78   :type 'hook)
79
80 ;;; Internal variables
81
82 (defvar gnus-agent-history-buffers nil)
83 (defvar gnus-agent-buffer-alist nil)
84 (defvar gnus-agent-article-alist nil)
85 (defvar gnus-agent-group-alist nil)
86 (defvar gnus-agent-covered-methods nil)
87 (defvar gnus-category-alist nil)
88 (defvar gnus-agent-current-history nil)
89 (defvar gnus-agent-overview-buffer nil)
90 (defvar gnus-category-predicate-cache nil)
91 (defvar gnus-category-group-cache nil)
92 (defvar gnus-agent-spam-hashtb nil)
93 (defvar gnus-agent-file-name nil)
94 (defvar gnus-agent-send-mail-function nil)
95 (defvar gnus-agent-file-coding-system 'raw-text)
96
97 (defconst gnus-agent-scoreable-headers
98   '("subject" "from" "date" "message-id" "references" "chars" "lines" "xref")
99   "Headers that are considered when scoring articles for download via the Agent.")
100
101 ;; Dynamic variables
102 (defvar gnus-headers)
103 (defvar gnus-score)
104
105 ;;;
106 ;;; Setup
107 ;;;
108
109 (defun gnus-open-agent ()
110   (setq gnus-agent t)
111   (gnus-agent-read-servers)
112   (gnus-category-read)
113   (gnus-agent-create-buffer)
114   (add-hook 'gnus-group-mode-hook 'gnus-agent-mode)
115   (add-hook 'gnus-summary-mode-hook 'gnus-agent-mode)
116   (add-hook 'gnus-server-mode-hook 'gnus-agent-mode))
117
118 (defun gnus-agent-create-buffer ()
119   (if (gnus-buffer-live-p gnus-agent-overview-buffer)
120       t
121     (setq gnus-agent-overview-buffer
122           (gnus-get-buffer-create " *Gnus agent overview*"))
123     (with-current-buffer gnus-agent-overview-buffer
124       (mm-enable-multibyte))
125     nil))
126
127 (gnus-add-shutdown 'gnus-close-agent 'gnus)
128
129 (defun gnus-close-agent ()
130   (setq gnus-agent-covered-methods nil
131         gnus-category-predicate-cache nil
132         gnus-category-group-cache nil
133         gnus-agent-spam-hashtb nil)
134   (gnus-kill-buffer gnus-agent-overview-buffer))
135
136 ;;;
137 ;;; Utility functions
138 ;;;
139
140 (defun gnus-agent-read-file (file)
141   "Load FILE and do a `read' there."
142   (with-temp-buffer
143     (ignore-errors
144       (nnheader-insert-file-contents file)
145       (goto-char (point-min))
146       (read (current-buffer)))))
147
148 (defsubst gnus-agent-method ()
149   (concat (symbol-name (car gnus-command-method)) "/"
150           (if (equal (cadr gnus-command-method) "")
151               "unnamed"
152             (cadr gnus-command-method))))
153
154 (defsubst gnus-agent-directory ()
155   "Path of the Gnus agent directory."
156   (nnheader-concat gnus-agent-directory
157                    (nnheader-translate-file-chars (gnus-agent-method)) "/"))
158
159 (defun gnus-agent-lib-file (file)
160   "The full path of the Gnus agent library FILE."
161   (concat (gnus-agent-directory) "agent.lib/" file))
162
163 ;;; Fetching setup functions.
164
165 (defun gnus-agent-start-fetch ()
166   "Initialize data structures for efficient fetching."
167   (gnus-agent-open-history)
168   (setq gnus-agent-current-history (gnus-agent-history-buffer))
169   (gnus-agent-create-buffer))
170
171 (defun gnus-agent-stop-fetch ()
172   "Save all data structures and clean up."
173   (gnus-agent-save-history)
174   (gnus-agent-close-history)
175   (setq gnus-agent-spam-hashtb nil)
176   (save-excursion
177     (set-buffer nntp-server-buffer)
178     (widen)))
179
180 (defmacro gnus-agent-with-fetch (&rest forms)
181   "Do FORMS safely."
182   `(unwind-protect
183        (progn
184          (gnus-agent-start-fetch)
185          ,@forms)
186      (gnus-agent-stop-fetch)))
187
188 (put 'gnus-agent-with-fetch 'lisp-indent-function 0)
189 (put 'gnus-agent-with-fetch 'edebug-form-spec '(body))
190
191 ;;;
192 ;;; Mode infestation
193 ;;;
194
195 (defvar gnus-agent-mode-hook nil
196   "Hook run when installing agent mode.")
197
198 (defvar gnus-agent-mode nil)
199 (defvar gnus-agent-mode-status '(gnus-agent-mode " Plugged"))
200
201 (defun gnus-agent-mode ()
202   "Minor mode for providing a agent support in Gnus buffers."
203   (let* ((buffer (progn (string-match "^gnus-\\(.*\\)-mode$"
204                                       (symbol-name major-mode))
205                         (match-string 1 (symbol-name major-mode))))
206          (mode (intern (format "gnus-agent-%s-mode" buffer))))
207     (set (make-local-variable 'gnus-agent-mode) t)
208     (set mode nil)
209     (set (make-local-variable mode) t)
210     ;; Set up the menu.
211     (when (gnus-visual-p 'agent-menu 'menu)
212       (funcall (intern (format "gnus-agent-%s-make-menu-bar" buffer))))
213     (unless (assq 'gnus-agent-mode minor-mode-alist)
214       (push gnus-agent-mode-status minor-mode-alist))
215     (unless (assq mode minor-mode-map-alist)
216       (push (cons mode (symbol-value (intern (format "gnus-agent-%s-mode-map"
217                                                      buffer))))
218             minor-mode-map-alist))
219     (when (eq major-mode 'gnus-group-mode)
220       (gnus-agent-toggle-plugged gnus-plugged))
221     (gnus-run-hooks 'gnus-agent-mode-hook
222                     (intern (format "gnus-agent-%s-mode-hook" buffer)))))
223
224 (defvar gnus-agent-group-mode-map (make-sparse-keymap))
225 (gnus-define-keys gnus-agent-group-mode-map
226   "Ju" gnus-agent-fetch-groups
227   "Jc" gnus-enter-category-buffer
228   "Jj" gnus-agent-toggle-plugged
229   "Js" gnus-agent-fetch-session
230   "JS" gnus-group-send-drafts
231   "Ja" gnus-agent-add-group
232   "Jr" gnus-agent-remove-group)
233
234 (defun gnus-agent-group-make-menu-bar ()
235   (unless (boundp 'gnus-agent-group-menu)
236     (easy-menu-define
237      gnus-agent-group-menu gnus-agent-group-mode-map ""
238      '("Agent"
239        ["Toggle plugged" gnus-agent-toggle-plugged t]
240        ["List categories" gnus-enter-category-buffer t]
241        ["Send drafts" gnus-group-send-drafts gnus-plugged]
242        ("Fetch"
243         ["All" gnus-agent-fetch-session gnus-plugged]
244         ["Group" gnus-agent-fetch-group gnus-plugged])))))
245
246 (defvar gnus-agent-summary-mode-map (make-sparse-keymap))
247 (gnus-define-keys gnus-agent-summary-mode-map
248   "Jj" gnus-agent-toggle-plugged
249   "J#" gnus-agent-mark-article
250   "J\M-#" gnus-agent-unmark-article
251   "@" gnus-agent-toggle-mark
252   "Jc" gnus-agent-catchup)
253
254 (defun gnus-agent-summary-make-menu-bar ()
255   (unless (boundp 'gnus-agent-summary-menu)
256     (easy-menu-define
257      gnus-agent-summary-menu gnus-agent-summary-mode-map ""
258      '("Agent"
259        ["Toggle plugged" gnus-agent-toggle-plugged t]
260        ["Mark as downloadable" gnus-agent-mark-article t]
261        ["Unmark as downloadable" gnus-agent-unmark-article t]
262        ["Toggle mark" gnus-agent-toggle-mark t]
263        ["Catchup undownloaded" gnus-agent-catchup t]))))
264
265 (defvar gnus-agent-server-mode-map (make-sparse-keymap))
266 (gnus-define-keys gnus-agent-server-mode-map
267   "Jj" gnus-agent-toggle-plugged
268   "Ja" gnus-agent-add-server
269   "Jr" gnus-agent-remove-server)
270
271 (defun gnus-agent-server-make-menu-bar ()
272   (unless (boundp 'gnus-agent-server-menu)
273     (easy-menu-define
274      gnus-agent-server-menu gnus-agent-server-mode-map ""
275      '("Agent"
276        ["Toggle plugged" gnus-agent-toggle-plugged t]
277        ["Add" gnus-agent-add-server t]
278        ["Remove" gnus-agent-remove-server t]))))
279
280 (defun gnus-agent-toggle-plugged (plugged)
281   "Toggle whether Gnus is unplugged or not."
282   (interactive (list (not gnus-plugged)))
283   (if plugged
284       (progn
285         (setq gnus-plugged plugged)
286         (gnus-run-hooks 'gnus-agent-plugged-hook)
287         (setcar (cdr gnus-agent-mode-status) " Plugged"))
288     (gnus-agent-close-connections)
289     (setq gnus-plugged plugged)
290     (gnus-run-hooks 'gnus-agent-unplugged-hook)
291     (setcar (cdr gnus-agent-mode-status) " Unplugged"))
292   (set-buffer-modified-p t))
293
294 (defun gnus-agent-close-connections ()
295   "Close all methods covered by the Gnus agent."
296   (let ((methods gnus-agent-covered-methods))
297     (while methods
298       (gnus-close-server (pop methods)))))
299
300 ;;;###autoload
301 (defun gnus-unplugged ()
302   "Start Gnus unplugged."
303   (interactive)
304   (setq gnus-plugged nil)
305   (gnus))
306
307 ;;;###autoload
308 (defun gnus-plugged ()
309   "Start Gnus plugged."
310   (interactive)
311   (setq gnus-plugged t)
312   (gnus))
313
314 ;;;###autoload
315 (defun gnus-agentize ()
316   "Allow Gnus to be an offline newsreader.
317 The normal usage of this command is to put the following as the
318 last form in your `.gnus.el' file:
319
320 \(gnus-agentize)
321
322 This will modify the `gnus-before-startup-hook', `gnus-post-method',
323 and `message-send-mail-function' variables, and install the Gnus
324 agent minor mode in all Gnus buffers."
325   (interactive)
326   (gnus-open-agent)
327   (add-hook 'gnus-setup-news-hook 'gnus-agent-queue-setup)
328   (unless gnus-agent-send-mail-function
329     (setq gnus-agent-send-mail-function message-send-mail-function
330           message-send-mail-function 'gnus-agent-send-mail))
331   (unless gnus-agent-covered-methods
332     (setq gnus-agent-covered-methods (list gnus-select-method))))
333
334 (defun gnus-agent-queue-setup ()
335   "Make sure the queue group exists."
336   (unless (gnus-gethash "nndraft:queue" gnus-newsrc-hashtb)
337     (gnus-request-create-group "queue" '(nndraft ""))
338     (let ((gnus-level-default-subscribed 1))
339       (gnus-subscribe-group "nndraft:queue" nil '(nndraft "")))
340     (gnus-group-set-parameter
341      "nndraft:queue" 'gnus-dummy '((gnus-draft-mode)))))
342
343 (defun gnus-agent-send-mail ()
344   (if gnus-plugged
345       (funcall gnus-agent-send-mail-function)
346     (goto-char (point-min))
347     (re-search-forward
348      (concat "^" (regexp-quote mail-header-separator) "\n"))
349     (replace-match "\n")
350     (gnus-agent-insert-meta-information 'mail)
351     (gnus-request-accept-article "nndraft:queue" nil t t)))
352
353 (defun gnus-agent-insert-meta-information (type &optional method)
354   "Insert meta-information into the message that says how it's to be posted.
355 TYPE can be either `mail' or `news'.  If the latter METHOD can
356 be a select method."
357   (save-excursion
358     (message-remove-header gnus-agent-meta-information-header)
359     (goto-char (point-min))
360     (insert gnus-agent-meta-information-header ": "
361             (symbol-name type) " " (format "%S" method)
362             "\n")
363     (forward-char -1)
364     (while (search-backward "\n" nil t)
365       (replace-match "\\n" t t))))
366
367 ;;;
368 ;;; Group mode commands
369 ;;;
370
371 (defun gnus-agent-fetch-groups (n)
372   "Put all new articles in the current groups into the Agent."
373   (interactive "P")
374   (unless gnus-plugged
375     (error "Groups can't be fetched when Gnus is unplugged"))
376   (gnus-group-iterate n 'gnus-agent-fetch-group))
377
378 (defun gnus-agent-fetch-group (group)
379   "Put all new articles in GROUP into the Agent."
380   (interactive (list (gnus-group-group-name)))
381   (unless gnus-plugged
382     (error "Groups can't be fetched when Gnus is unplugged"))
383   (unless group
384     (error "No group on the current line"))
385   (let ((gnus-command-method (gnus-find-method-for-group group)))
386     (gnus-agent-with-fetch
387       (gnus-agent-fetch-group-1 group gnus-command-method)
388       (gnus-message 5 "Fetching %s...done" group))))
389
390 (defun gnus-agent-add-group (category arg)
391   "Add the current group to an agent category."
392   (interactive
393    (list
394     (intern
395      (completing-read
396       "Add to category: "
397       (mapcar (lambda (cat) (list (symbol-name (car cat))))
398               gnus-category-alist)
399       nil t))
400     current-prefix-arg))
401   (let ((cat (assq category gnus-category-alist))
402         c groups)
403     (gnus-group-iterate arg
404       (lambda (group)
405         (when (cadddr (setq c (gnus-group-category group)))
406           (setf (cadddr c) (delete group (cadddr c))))
407         (push group groups)))
408     (setf (cadddr cat) (nconc (cadddr cat) groups))
409     (gnus-category-write)))
410
411 (defun gnus-agent-remove-group (arg)
412   "Remove the current group from its agent category, if any."
413   (interactive "P")
414   (let (c)
415     (gnus-group-iterate arg
416       (lambda (group)
417         (when (cadddr (setq c (gnus-group-category group)))
418           (setf (cadddr c) (delete group (cadddr c))))))
419     (gnus-category-write)))
420
421 ;;;
422 ;;; Server mode commands
423 ;;;
424
425 (defun gnus-agent-add-server (server)
426   "Enroll SERVER in the agent program."
427   (interactive (list (gnus-server-server-name)))
428   (unless server
429     (error "No server on the current line"))
430   (let ((method (gnus-server-get-method nil (gnus-server-server-name))))
431     (when (member method gnus-agent-covered-methods)
432       (error "Server already in the agent program"))
433     (push method gnus-agent-covered-methods)
434     (gnus-agent-write-servers)
435     (message "Entered %s into the Agent" server)))
436
437 (defun gnus-agent-remove-server (server)
438   "Remove SERVER from the agent program."
439   (interactive (list (gnus-server-server-name)))
440   (unless server
441     (error "No server on the current line"))
442   (let ((method (gnus-server-get-method nil (gnus-server-server-name))))
443     (unless (member method gnus-agent-covered-methods)
444       (error "Server not in the agent program"))
445     (setq gnus-agent-covered-methods
446           (delete method gnus-agent-covered-methods))
447     (gnus-agent-write-servers)
448     (message "Removed %s from the agent" server)))
449
450 (defun gnus-agent-read-servers ()
451   "Read the alist of covered servers."
452   (setq gnus-agent-covered-methods
453         (gnus-agent-read-file
454          (nnheader-concat gnus-agent-directory "lib/servers"))))
455
456 (defun gnus-agent-write-servers ()
457   "Write the alist of covered servers."
458   (gnus-make-directory (nnheader-concat gnus-agent-directory "lib"))
459   (with-temp-file (nnheader-concat gnus-agent-directory "lib/servers")
460     (prin1 gnus-agent-covered-methods (current-buffer))))
461
462 ;;;
463 ;;; Summary commands
464 ;;;
465
466 (defun gnus-agent-mark-article (n &optional unmark)
467   "Mark the next N articles as downloadable.
468 If N is negative, mark backward instead.  If UNMARK is non-nil, remove
469 the mark instead.  The difference between N and the actual number of
470 articles marked is returned."
471   (interactive "p")
472   (let ((backward (< n 0))
473         (n (abs n)))
474     (while (and
475             (> n 0)
476             (progn
477               (gnus-summary-set-agent-mark
478                (gnus-summary-article-number) unmark)
479               (zerop (gnus-summary-next-subject (if backward -1 1) nil t))))
480       (setq n (1- n)))
481     (when (/= 0 n)
482       (gnus-message 7 "No more articles"))
483     (gnus-summary-recenter)
484     (gnus-summary-position-point)
485     n))
486
487 (defun gnus-agent-unmark-article (n)
488   "Remove the downloadable mark from the next N articles.
489 If N is negative, unmark backward instead.  The difference between N and
490 the actual number of articles unmarked is returned."
491   (interactive "p")
492   (gnus-agent-mark-article n t))
493
494 (defun gnus-agent-toggle-mark (n)
495   "Toggle the downloadable mark from the next N articles.
496 If N is negative, toggle backward instead.  The difference between N and
497 the actual number of articles toggled is returned."
498   (interactive "p")
499   (gnus-agent-mark-article n 'toggle))
500
501 (defun gnus-summary-set-agent-mark (article &optional unmark)
502   "Mark ARTICLE as downloadable."
503   (let ((unmark (if (and (not (null unmark)) (not (eq t unmark)))
504                     (memq article gnus-newsgroup-downloadable)
505                   unmark)))
506     (if unmark
507         (progn
508           (setq gnus-newsgroup-downloadable
509                 (delq article gnus-newsgroup-downloadable))
510           (push article gnus-newsgroup-undownloaded))
511       (setq gnus-newsgroup-undownloaded
512             (delq article gnus-newsgroup-undownloaded))
513       (push article gnus-newsgroup-downloadable))
514     (gnus-summary-update-mark
515      (if unmark gnus-undownloaded-mark gnus-downloadable-mark)
516      'unread)))
517
518 (defun gnus-agent-get-undownloaded-list ()
519   "Mark all unfetched articles as read."
520   (let ((gnus-command-method (gnus-find-method-for-group gnus-newsgroup-name)))
521     (when (and (not gnus-plugged)
522                (gnus-agent-method-p gnus-command-method))
523       (gnus-agent-load-alist gnus-newsgroup-name)
524       ;; First mark all undownloaded articles as undownloaded.
525       (let ((articles (append gnus-newsgroup-unreads
526                               gnus-newsgroup-marked
527                               gnus-newsgroup-dormant))
528             article)
529         (while (setq article (pop articles))
530           (unless (or (cdr (assq article gnus-agent-article-alist))
531                       (memq article gnus-newsgroup-downloadable)
532                       (memq article gnus-newsgroup-cached))
533             (push article gnus-newsgroup-undownloaded))))
534       ;; Then mark downloaded downloadable as not-downloadable,
535       ;; if you get my drift.
536       (let ((articles gnus-newsgroup-downloadable)
537             article)
538         (while (setq article (pop articles))
539           (when (cdr (assq article gnus-agent-article-alist))
540             (setq gnus-newsgroup-downloadable
541                   (delq article gnus-newsgroup-downloadable))))))))
542
543 (defun gnus-agent-catchup ()
544   "Mark all undownloaded articles as read."
545   (interactive)
546   (save-excursion
547     (while gnus-newsgroup-undownloaded
548       (gnus-summary-mark-article
549        (pop gnus-newsgroup-undownloaded) gnus-catchup-mark)))
550   (gnus-summary-position-point))
551
552 ;;;
553 ;;; Internal functions
554 ;;;
555
556 (defun gnus-agent-save-active (method)
557   (gnus-agent-save-active-1 method 'gnus-active-to-gnus-format))
558
559 (defun gnus-agent-save-active-1 (method function)
560   (when (gnus-agent-method-p method)
561     (let* ((gnus-command-method method)
562            (new (gnus-make-hashtable (count-lines (point-min) (point-max))))
563            (file (gnus-agent-lib-file "active")))
564       (funcall function nil new)
565       (gnus-agent-write-active file new)
566       (erase-buffer)
567       (insert-file-contents-literally file))))
568
569 (defun gnus-agent-write-active (file new)
570   (let ((orig (gnus-make-hashtable (count-lines (point-min) (point-max))))
571         (file (gnus-agent-lib-file "active"))
572         elem osym)
573     (when (file-exists-p file)
574       (with-temp-buffer
575         (insert-file-contents file)
576         (gnus-active-to-gnus-format nil orig))
577       (mapatoms
578        (lambda (sym)
579          (when (and sym (boundp sym))
580            (if (and (boundp (setq osym (intern (symbol-name sym) orig)))
581                     (setq elem (symbol-value osym)))
582                (setcdr elem (cdr (symbol-value sym)))
583              (set (intern (symbol-name sym) orig) (symbol-value sym)))))
584        new))
585     (gnus-make-directory (file-name-directory file))
586     (let ((coding-system-for-write gnus-agent-file-coding-system))
587       (gnus-write-active-file file orig))))
588
589 (defun gnus-agent-save-groups (method)
590   (gnus-agent-save-active-1 method 'gnus-groups-to-gnus-format))
591
592 (defun gnus-agent-save-group-info (method group active)
593   (when (gnus-agent-method-p method)
594     (let* ((gnus-command-method method)
595            (file (gnus-agent-lib-file "active")))
596       (gnus-make-directory (file-name-directory file))
597       (with-temp-file file
598         (when (file-exists-p file)
599           (nnheader-insert-file-contents file))
600         (goto-char (point-min))
601         (when (re-search-forward
602                (concat "^" (regexp-quote group) " ") nil t)
603           (gnus-delete-line))
604         (insert group " " (number-to-string (cdr active)) " "
605                 (number-to-string (car active)) " y\n")))))
606
607 (defun gnus-agent-group-path (group)
608   "Translate GROUP into a path."
609   (if nnmail-use-long-file-names
610       (gnus-group-real-name group)
611     (nnheader-replace-chars-in-string
612      (nnheader-translate-file-chars (gnus-group-real-name group))
613      ?. ?/)))
614
615 \f
616
617 (defun gnus-agent-method-p (method)
618   "Say whether METHOD is covered by the agent."
619   (member method gnus-agent-covered-methods))
620
621 (defun gnus-agent-get-function (method)
622   (if (and (not gnus-plugged)
623            (gnus-agent-method-p method))
624       (progn
625         (require 'nnagent)
626         'nnagent)
627     (car method)))
628
629 ;;; History functions
630
631 (defun gnus-agent-history-buffer ()
632   (cdr (assoc (gnus-agent-method) gnus-agent-history-buffers)))
633
634 (defun gnus-agent-open-history ()
635   (save-excursion
636     (push (cons (gnus-agent-method)
637                 (set-buffer (gnus-get-buffer-create
638                              (format " *Gnus agent %s history*"
639                                      (gnus-agent-method)))))
640           gnus-agent-history-buffers)
641     (erase-buffer)
642     (insert "\n")
643     (let ((file (gnus-agent-lib-file "history")))
644       (when (file-exists-p file)
645         (insert-file file))
646       (set (make-local-variable 'gnus-agent-file-name) file))))
647
648 (defun gnus-agent-save-history ()
649   (save-excursion
650     (set-buffer gnus-agent-current-history)
651     (gnus-make-directory (file-name-directory gnus-agent-file-name))
652     (let ((coding-system-for-write gnus-agent-file-coding-system))
653       (write-region (1+ (point-min)) (point-max)
654                     gnus-agent-file-name nil 'silent))))
655
656 (defun gnus-agent-close-history ()
657   (when (gnus-buffer-live-p gnus-agent-current-history)
658     (kill-buffer gnus-agent-current-history)
659     (setq gnus-agent-history-buffers
660           (delq (assoc (gnus-agent-method) gnus-agent-history-buffers)
661                 gnus-agent-history-buffers))))
662
663 (defun gnus-agent-enter-history (id group-arts date)
664   (save-excursion
665     (set-buffer gnus-agent-current-history)
666     (goto-char (point-max))
667     (insert id "\t" (number-to-string date) "\t")
668     (while group-arts
669       (insert (caar group-arts) " " (number-to-string (cdr (pop group-arts)))
670               " "))
671     (insert "\n")))
672
673 (defun gnus-agent-article-in-history-p (id)
674   (save-excursion
675     (set-buffer (gnus-agent-history-buffer))
676     (goto-char (point-min))
677     (search-forward (concat "\n" id "\t") nil t)))
678
679 (defun gnus-agent-history-path (id)
680   (save-excursion
681     (set-buffer (gnus-agent-history-buffer))
682     (goto-char (point-min))
683     (when (search-forward (concat "\n" id "\t") nil t)
684       (let ((method (gnus-agent-method)))
685         (let (paths group)
686           (while (not (numberp (setq group (read (current-buffer)))))
687             (push (concat method "/" group) paths))
688           (nreverse paths))))))
689
690 ;;;
691 ;;; Fetching
692 ;;;
693
694 (defun gnus-agent-fetch-articles (group articles)
695   "Fetch ARTICLES from GROUP and put them into the Agent."
696   (when articles
697     ;; Prune off articles that we have already fetched.
698     (while (and articles
699                 (cdr (assq (car articles) gnus-agent-article-alist)))
700      (pop articles))
701     (let ((arts articles))
702       (while (cdr arts)
703         (if (cdr (assq (cadr arts) gnus-agent-article-alist))
704             (setcdr arts (cddr arts))
705           (setq arts (cdr arts)))))
706     (when articles
707       (let ((dir (concat
708                   (gnus-agent-directory)
709                   (gnus-agent-group-path group) "/"))
710             (date (time-to-days (current-time)))
711             (case-fold-search t)
712             pos crosses id elem)
713         (gnus-make-directory dir)
714         (gnus-message 7 "Fetching articles for %s..." group)
715         ;; Fetch the articles from the backend.
716         (if (gnus-check-backend-function 'retrieve-articles group)
717             (setq pos (gnus-retrieve-articles articles group))
718           (with-temp-buffer
719             (let (article)
720               (while (setq article (pop articles))
721                 (when (gnus-request-article article group)
722                   (goto-char (point-max))
723                   (push (cons article (point)) pos)
724                   (insert-buffer-substring nntp-server-buffer)))
725               (copy-to-buffer nntp-server-buffer (point-min) (point-max))
726               (setq pos (nreverse pos)))))
727         ;; Then save these articles into the Agent.
728         (save-excursion
729           (set-buffer nntp-server-buffer)
730           (while pos
731             (narrow-to-region (cdar pos) (or (cdadr pos) (point-max)))
732             (goto-char (point-min))
733             (when (search-forward "\n\n" nil t)
734               (when (search-backward "\nXrefs: " nil t)
735                 ;; Handle crossposting.
736                 (skip-chars-forward "^ ")
737                 (skip-chars-forward " ")
738                 (setq crosses nil)
739                 (while (looking-at "\\([^: \n]+\\):\\([0-9]+\\) +")
740                   (push (cons (buffer-substring (match-beginning 1)
741                                                 (match-end 1))
742                               (buffer-substring (match-beginning 2)
743                                                 (match-end 2)))
744                         crosses)
745                   (goto-char (match-end 0)))
746                 (gnus-agent-crosspost crosses (caar pos))))
747             (goto-char (point-min))
748             (if (not (re-search-forward "^Message-ID: *<\\([^>\n]+\\)>" nil t))
749                 (setq id "No-Message-ID-in-article")
750               (setq id (buffer-substring (match-beginning 1) (match-end 1))))
751             (let ((coding-system-for-write
752                    gnus-agent-file-coding-system))
753               (write-region (point-min) (point-max)
754                             (concat dir (number-to-string (caar pos)))
755                             nil 'silent))
756             (when (setq elem (assq (caar pos) gnus-agent-article-alist))
757               (setcdr elem t))
758             (gnus-agent-enter-history
759              id (or crosses (list (cons group (caar pos)))) date)
760             (widen)
761             (pop pos)))
762         (gnus-agent-save-alist group)))))
763
764 (defun gnus-agent-crosspost (crosses article)
765   (let (gnus-agent-article-alist group alist beg end)
766     (save-excursion
767       (set-buffer gnus-agent-overview-buffer)
768       (when (nnheader-find-nov-line article)
769         (forward-word 1)
770         (setq beg (point))
771         (setq end (progn (forward-line 1) (point)))))
772     (while crosses
773       (setq group (caar crosses))
774       (unless (setq alist (assoc group gnus-agent-group-alist))
775         (push (setq alist (list group (gnus-agent-load-alist (caar crosses))))
776               gnus-agent-group-alist))
777       (setcdr alist (cons (cons (cdar crosses) t) (cdr alist)))
778       (save-excursion
779         (set-buffer (gnus-get-buffer-create (format " *Gnus agent overview %s*"
780                                                group)))
781         (when (= (point-max) (point-min))
782           (push (cons group (current-buffer)) gnus-agent-buffer-alist)
783           (ignore-errors
784             (nnheader-insert-file-contents
785              (gnus-agent-article-name ".overview" group))))
786         (nnheader-find-nov-line (string-to-number (cdar crosses)))
787         (insert (string-to-number (cdar crosses)))
788         (insert-buffer-substring gnus-agent-overview-buffer beg end))
789       (pop crosses))))
790
791 (defun gnus-agent-flush-cache ()
792   (save-excursion
793     (while gnus-agent-buffer-alist
794       (set-buffer (cdar gnus-agent-buffer-alist))
795       (let ((coding-system-for-write
796              gnus-agent-file-coding-system))
797         (write-region (point-min) (point-max)
798                       (gnus-agent-article-name ".overview"
799                                                (caar gnus-agent-buffer-alist))
800                       nil 'silent))
801       (pop gnus-agent-buffer-alist))
802     (while gnus-agent-group-alist
803       (with-temp-file (caar gnus-agent-group-alist)
804         (princ (cdar gnus-agent-group-alist))
805         (insert "\n"))
806       (pop gnus-agent-group-alist))))
807
808 (defun gnus-agent-fetch-headers (group &optional force)
809   (let ((articles (gnus-list-of-unread-articles group))
810         (gnus-decode-encoded-word-function 'identity)
811         (file (gnus-agent-article-name ".overview" group)))
812     ;; add article with marks to list of article headers we want to fetch
813     (dolist (arts (gnus-info-marks (gnus-get-info group)))
814       (setq articles (union (gnus-uncompress-sequence (cdr arts))
815                            articles)))
816     (setq articles (sort articles '<))
817     ;; remove known articles
818     (when (gnus-agent-load-alist group)
819       (setq articles (gnus-sorted-intersection
820                      articles
821                      (gnus-uncompress-range
822                       (cons (1+ (caar (last gnus-agent-article-alist)))
823                             (cdr (gnus-active group)))))))
824     ;; Fetch them.
825     (gnus-make-directory (nnheader-translate-file-chars
826                           (file-name-directory file)))
827     (when articles
828       (gnus-message 7 "Fetching headers for %s..." group)
829       (save-excursion
830         (set-buffer nntp-server-buffer)
831         (unless (eq 'nov (gnus-retrieve-headers articles group))
832           (nnvirtual-convert-headers))
833         ;; Save these headers for later processing.
834         (copy-to-buffer gnus-agent-overview-buffer (point-min) (point-max))
835         (when (file-exists-p file)
836           (gnus-agent-braid-nov group articles file))
837         (let ((coding-system-for-write
838                gnus-agent-file-coding-system))
839           (write-region (point-min) (point-max) file nil 'silent))
840         (gnus-agent-save-alist group articles nil)
841         (gnus-agent-enter-history
842          "last-header-fetched-for-session"
843          (list (cons group (nth (- (length  articles) 1) articles)))
844          (time-to-days (current-time)))
845         articles))))
846
847 (defsubst gnus-agent-copy-nov-line (article)
848   (let (b e)
849     (set-buffer gnus-agent-overview-buffer)
850     (setq b (point))
851     (if (eq article (read (current-buffer)))
852         (setq e (progn (forward-line 1) (point)))
853       (progn
854         (beginning-of-line)
855         (setq e b)))
856     (set-buffer nntp-server-buffer)
857     (insert-buffer-substring gnus-agent-overview-buffer b e)))
858
859 (defun gnus-agent-braid-nov (group articles file)
860   (set-buffer gnus-agent-overview-buffer)
861   (goto-char (point-min))
862   (set-buffer nntp-server-buffer)
863   (erase-buffer)
864   (nnheader-insert-file-contents file)
865   (goto-char (point-max))
866   (if (or (= (point-min) (point-max))
867           (progn
868             (forward-line -1)
869             (< (read (current-buffer)) (car articles))))
870       ;; We have only headers that are after the older headers,
871       ;; so we just append them.
872       (progn
873         (goto-char (point-max))
874         (insert-buffer-substring gnus-agent-overview-buffer))
875     ;; We do it the hard way.
876     (nnheader-find-nov-line (car articles))
877     (gnus-agent-copy-nov-line (car articles))
878     (pop articles)
879     (while (and articles
880                 (not (eobp)))
881       (while (and (not (eobp))
882                   (< (read (current-buffer)) (car articles)))
883         (forward-line 1))
884       (beginning-of-line)
885       (unless (eobp)
886         (gnus-agent-copy-nov-line (car articles))
887         (setq articles (cdr articles))))
888     (when articles
889       (let (b e)
890         (set-buffer gnus-agent-overview-buffer)
891         (setq b (point)
892               e (point-max))
893         (set-buffer nntp-server-buffer)
894         (insert-buffer-substring gnus-agent-overview-buffer b e)))))
895
896 (defun gnus-agent-load-alist (group &optional dir)
897   "Load the article-state alist for GROUP."
898   (setq gnus-agent-article-alist
899         (gnus-agent-read-file
900          (if dir
901              (concat dir ".agentview")
902            (gnus-agent-article-name ".agentview" group)))))
903
904 (defun gnus-agent-save-alist (group &optional articles state dir)
905   "Save the article-state alist for GROUP."
906   (with-temp-file (if dir
907                       (concat dir ".agentview")
908                     (gnus-agent-article-name ".agentview" group))
909     (princ (setq gnus-agent-article-alist
910                  (nconc gnus-agent-article-alist
911                         (mapcar (lambda (article) (cons article state))
912                                 articles)))
913            (current-buffer))
914     (insert "\n")))
915
916 (defun gnus-agent-article-name (article group)
917   (concat (gnus-agent-directory) (gnus-agent-group-path group) "/"
918           (if (stringp article) article (string-to-number article))))
919
920 ;;;###autoload
921 (defun gnus-agent-batch-fetch ()
922   "Start Gnus and fetch session."
923   (interactive)
924   (gnus)
925   (gnus-agent-fetch-session)
926   (gnus-group-exit))
927
928 (defun gnus-agent-fetch-session ()
929   "Fetch all articles and headers that are eligible for fetching."
930   (interactive)
931   (unless gnus-agent-covered-methods
932     (error "No servers are covered by the Gnus agent"))
933   (unless gnus-plugged
934     (error "Can't fetch articles while Gnus is unplugged"))
935   (let ((methods gnus-agent-covered-methods)
936         groups group gnus-command-method)
937     (save-excursion
938       (while methods
939         (setq gnus-command-method (car methods))
940         (when (or (gnus-server-opened gnus-command-method)
941                   (gnus-open-server gnus-command-method))
942           (setq groups (gnus-groups-from-server (car methods)))
943           (gnus-agent-with-fetch
944             (while (setq group (pop groups))
945               (when (<= (gnus-group-level group) gnus-agent-handle-level)
946                 (gnus-agent-fetch-group-1 group gnus-command-method)))))
947         (pop methods))
948       (gnus-message 6 "Finished fetching articles into the Gnus agent"))))
949
950 (defun gnus-agent-fetch-group-1 (group method)
951   "Fetch GROUP."
952   (let ((gnus-command-method method)
953         (gnus-newsgroup-name group)
954         gnus-newsgroup-dependencies gnus-newsgroup-headers
955         gnus-newsgroup-scored gnus-headers gnus-score
956         gnus-use-cache articles arts
957         category predicate info marks score-param)
958     ;; Fetch headers.
959     (when (and (or (gnus-active group) (gnus-activate-group group))
960                (setq articles (gnus-agent-fetch-headers group))
961                (progn
962                  ;; Parse them and see which articles we want to fetch.
963                  (setq gnus-newsgroup-dependencies
964                        (make-vector (length articles) 0))
965                  ;; No need to call `gnus-get-newsgroup-headers-xover' with
966                  ;; the entire .overview for group as we still have the just
967                  ;; downloaded headers in `gnus-agent-overview-buffer'.
968                  (let ((nntp-server-buffer gnus-agent-overview-buffer))
969                    (setq gnus-newsgroup-headers
970                          (gnus-get-newsgroup-headers-xover articles nil nil 
971                                                            group)))
972                  ;; `gnus-agent-overview-buffer' may be killed for
973                  ;; timeout reason. If so, recreate it.
974                  (gnus-agent-create-buffer)))
975       (setq category (gnus-group-category group))
976       (setq predicate
977             (gnus-get-predicate
978             (or (gnus-group-find-parameter group 'agent-predicate t)
979                  (cadr category))))
980       ;; Do we want to download everything, or nothing?
981       (if (or (eq (caaddr predicate) 'gnus-agent-true)
982               (eq (caaddr predicate) 'gnus-agent-false))
983           ;; Yes.
984           (setq arts (symbol-value
985                       (cadr (assoc (caaddr predicate)
986                                    '((gnus-agent-true articles)
987                                      (gnus-agent-false nil))))))
988         ;; No, we need to decide what we want.
989         (setq score-param
990               (let ((score-method
991                      (or
992                       (gnus-group-find-parameter group 'agent-score t)
993                       (caddr category))))
994                 (when score-method
995                   (require 'gnus-score)
996                   (if (eq score-method 'file)
997                       (let ((entries
998                              (gnus-score-load-files
999                               (gnus-all-score-files group)))
1000                             list score-file)
1001                         (while (setq list (car entries))
1002                           (push (car list) score-file)
1003                           (setq list (cdr list))
1004                           (while list
1005                             (when (member (caar list)
1006                                           gnus-agent-scoreable-headers)
1007                               (push (car list) score-file))
1008                             (setq list (cdr list)))
1009                           (setq score-param
1010                                 (append score-param (list (nreverse score-file)))
1011                                 score-file nil entries (cdr entries)))
1012                         (list score-param))
1013                     (if (stringp (car score-method))
1014                         score-method
1015                       (list (list score-method)))))))
1016         (when score-param
1017           (gnus-score-headers score-param))
1018         (setq arts nil)
1019         (while (setq gnus-headers (pop gnus-newsgroup-headers))
1020           (setq gnus-score
1021                 (or (cdr (assq (mail-header-number gnus-headers)
1022                                gnus-newsgroup-scored))
1023                     gnus-summary-default-score))
1024           (when (funcall predicate)
1025             (push (mail-header-number gnus-headers)
1026                   arts))))
1027       ;; Fetch the articles.
1028       (when arts
1029         (gnus-agent-fetch-articles group arts)))
1030     ;; Perhaps we have some additional articles to fetch.
1031     (setq arts (assq 'download (gnus-info-marks
1032                                 (setq info (gnus-get-info group)))))
1033     (when (cdr arts)
1034       (gnus-agent-fetch-articles
1035        group (gnus-uncompress-range (cdr arts)))
1036       (setq marks (delq arts (gnus-info-marks info)))
1037       (gnus-info-set-marks info marks)
1038       (gnus-dribble-enter
1039        (concat "(gnus-group-set-info '"
1040                (gnus-prin1-to-string info)
1041                ")")))))
1042
1043 ;;;
1044 ;;; Agent Category Mode
1045 ;;;
1046
1047 (defvar gnus-category-mode-hook nil
1048   "Hook run in `gnus-category-mode' buffers.")
1049
1050 (defvar gnus-category-line-format "     %(%20c%): %g\n"
1051   "Format of category lines.")
1052
1053 (defvar gnus-category-mode-line-format "Gnus: %%b"
1054   "The format specification for the category mode line.")
1055
1056 (defvar gnus-agent-short-article 100
1057   "Articles that have fewer lines than this are short.")
1058
1059 (defvar gnus-agent-long-article 200
1060   "Articles that have more lines than this are long.")
1061
1062 (defvar gnus-agent-low-score 0
1063   "Articles that have a score lower than this have a low score.")
1064
1065 (defvar gnus-agent-high-score 0
1066   "Articles that have a score higher than this have a high score.")
1067
1068
1069 ;;; Internal variables.
1070
1071 (defvar gnus-category-buffer "*Agent Category*")
1072
1073 (defvar gnus-category-line-format-alist
1074   `((?c gnus-tmp-name ?s)
1075     (?g gnus-tmp-groups ?d)))
1076
1077 (defvar gnus-category-mode-line-format-alist
1078   `((?u user-defined ?s)))
1079
1080 (defvar gnus-category-line-format-spec nil)
1081 (defvar gnus-category-mode-line-format-spec nil)
1082
1083 (defvar gnus-category-mode-map nil)
1084 (put 'gnus-category-mode 'mode-class 'special)
1085
1086 (unless gnus-category-mode-map
1087   (setq gnus-category-mode-map (make-sparse-keymap))
1088   (suppress-keymap gnus-category-mode-map)
1089
1090   (gnus-define-keys gnus-category-mode-map
1091     "q" gnus-category-exit
1092     "k" gnus-category-kill
1093     "c" gnus-category-copy
1094     "a" gnus-category-add
1095     "p" gnus-category-edit-predicate
1096     "g" gnus-category-edit-groups
1097     "s" gnus-category-edit-score
1098     "l" gnus-category-list
1099
1100     "\C-c\C-i" gnus-info-find-node
1101     "\C-c\C-b" gnus-bug))
1102
1103 (defvar gnus-category-menu-hook nil
1104   "*Hook run after the creation of the menu.")
1105
1106 (defun gnus-category-make-menu-bar ()
1107   (gnus-turn-off-edit-menu 'category)
1108   (unless (boundp 'gnus-category-menu)
1109     (easy-menu-define
1110      gnus-category-menu gnus-category-mode-map ""
1111      '("Categories"
1112        ["Add" gnus-category-add t]
1113        ["Kill" gnus-category-kill t]
1114        ["Copy" gnus-category-copy t]
1115        ["Edit predicate" gnus-category-edit-predicate t]
1116        ["Edit score" gnus-category-edit-score t]
1117        ["Edit groups" gnus-category-edit-groups t]
1118        ["Exit" gnus-category-exit t]))
1119
1120     (gnus-run-hooks 'gnus-category-menu-hook)))
1121
1122 (defun gnus-category-mode ()
1123   "Major mode for listing and editing agent categories.
1124
1125 All normal editing commands are switched off.
1126 \\<gnus-category-mode-map>
1127 For more in-depth information on this mode, read the manual
1128 (`\\[gnus-info-find-node]').
1129
1130 The following commands are available:
1131
1132 \\{gnus-category-mode-map}"
1133   (interactive)
1134   (when (gnus-visual-p 'category-menu 'menu)
1135     (gnus-category-make-menu-bar))
1136   (kill-all-local-variables)
1137   (gnus-simplify-mode-line)
1138   (setq major-mode 'gnus-category-mode)
1139   (setq mode-name "Category")
1140   (gnus-set-default-directory)
1141   (setq mode-line-process nil)
1142   (use-local-map gnus-category-mode-map)
1143   (buffer-disable-undo)
1144   (setq truncate-lines t)
1145   (setq buffer-read-only t)
1146   (gnus-run-hooks 'gnus-category-mode-hook))
1147
1148 (defalias 'gnus-category-position-point 'gnus-goto-colon)
1149
1150 (defun gnus-category-insert-line (category)
1151   (let* ((gnus-tmp-name (car category))
1152          (gnus-tmp-groups (length (cadddr category))))
1153     (beginning-of-line)
1154     (gnus-add-text-properties
1155      (point)
1156      (prog1 (1+ (point))
1157        ;; Insert the text.
1158        (eval gnus-category-line-format-spec))
1159      (list 'gnus-category gnus-tmp-name))))
1160
1161 (defun gnus-enter-category-buffer ()
1162   "Go to the Category buffer."
1163   (interactive)
1164   (gnus-category-setup-buffer)
1165   (gnus-configure-windows 'category)
1166   (gnus-category-prepare))
1167
1168 (defun gnus-category-setup-buffer ()
1169   (unless (get-buffer gnus-category-buffer)
1170     (save-excursion
1171       (set-buffer (gnus-get-buffer-create gnus-category-buffer))
1172       (gnus-category-mode))))
1173
1174 (defun gnus-category-prepare ()
1175   (gnus-set-format 'category-mode)
1176   (gnus-set-format 'category t)
1177   (let ((alist gnus-category-alist)
1178         (buffer-read-only nil))
1179     (erase-buffer)
1180     (while alist
1181       (gnus-category-insert-line (pop alist)))
1182     (goto-char (point-min))
1183     (gnus-category-position-point)))
1184
1185 (defun gnus-category-name ()
1186   (or (get-text-property (gnus-point-at-bol) 'gnus-category)
1187       (error "No category on the current line")))
1188
1189 (defun gnus-category-read ()
1190   "Read the category alist."
1191   (setq gnus-category-alist
1192         (or (gnus-agent-read-file
1193              (nnheader-concat gnus-agent-directory "lib/categories"))
1194             (list (list 'default 'short nil nil)))))
1195
1196 (defun gnus-category-write ()
1197   "Write the category alist."
1198   (setq gnus-category-predicate-cache nil
1199         gnus-category-group-cache nil)
1200   (gnus-make-directory (nnheader-concat gnus-agent-directory "lib"))
1201   (with-temp-file (nnheader-concat gnus-agent-directory "lib/categories")
1202     (prin1 gnus-category-alist (current-buffer))))
1203
1204 (defun gnus-category-edit-predicate (category)
1205   "Edit the predicate for CATEGORY."
1206   (interactive (list (gnus-category-name)))
1207   (let ((info (assq category gnus-category-alist)))
1208     (gnus-edit-form
1209      (cadr info) (format "Editing the predicate for category %s" category)
1210      `(lambda (predicate)
1211         (setf (cadr (assq ',category gnus-category-alist)) predicate)
1212         (gnus-category-write)
1213         (gnus-category-list)))))
1214
1215 (defun gnus-category-edit-score (category)
1216   "Edit the score expression for CATEGORY."
1217   (interactive (list (gnus-category-name)))
1218   (let ((info (assq category gnus-category-alist)))
1219     (gnus-edit-form
1220      (caddr info)
1221      (format "Editing the score expression for category %s" category)
1222      `(lambda (groups)
1223         (setf (caddr (assq ',category gnus-category-alist)) groups)
1224         (gnus-category-write)
1225         (gnus-category-list)))))
1226
1227 (defun gnus-category-edit-groups (category)
1228   "Edit the group list for CATEGORY."
1229   (interactive (list (gnus-category-name)))
1230   (let ((info (assq category gnus-category-alist)))
1231     (gnus-edit-form
1232      (cadddr info) (format "Editing the group list for category %s" category)
1233      `(lambda (groups)
1234         (setf (cadddr (assq ',category gnus-category-alist)) groups)
1235         (gnus-category-write)
1236         (gnus-category-list)))))
1237
1238 (defun gnus-category-kill (category)
1239   "Kill the current category."
1240   (interactive (list (gnus-category-name)))
1241   (let ((info (assq category gnus-category-alist))
1242         (buffer-read-only nil))
1243     (gnus-delete-line)
1244     (gnus-category-write)
1245     (setq gnus-category-alist (delq info gnus-category-alist))))
1246
1247 (defun gnus-category-copy (category to)
1248   "Copy the current category."
1249   (interactive (list (gnus-category-name) (intern (read-string "New name: "))))
1250   (let ((info (assq category gnus-category-alist)))
1251     (push (list to (gnus-copy-sequence (cadr info))
1252                 (gnus-copy-sequence (caddr info)) nil)
1253           gnus-category-alist)
1254     (gnus-category-write)
1255     (gnus-category-list)))
1256
1257 (defun gnus-category-add (category)
1258   "Create a new category."
1259   (interactive "SCategory name: ")
1260   (when (assq category gnus-category-alist)
1261     (error "Category %s already exists" category))
1262   (push (list category 'false nil nil)
1263         gnus-category-alist)
1264   (gnus-category-write)
1265   (gnus-category-list))
1266
1267 (defun gnus-category-list ()
1268   "List all categories."
1269   (interactive)
1270   (gnus-category-prepare))
1271
1272 (defun gnus-category-exit ()
1273   "Return to the group buffer."
1274   (interactive)
1275   (kill-buffer (current-buffer))
1276   (gnus-configure-windows 'group t))
1277
1278 ;; To avoid having 8-bit characters in the source file.
1279 (defvar gnus-category-not (list '! 'not (intern (format "%c" 172))))
1280
1281 (defvar gnus-category-predicate-alist
1282   '((spam . gnus-agent-spam-p)
1283     (short . gnus-agent-short-p)
1284     (long . gnus-agent-long-p)
1285     (low . gnus-agent-low-scored-p)
1286     (high . gnus-agent-high-scored-p)
1287     (true . gnus-agent-true)
1288     (false . gnus-agent-false))
1289   "Mapping from short score predicate symbols to predicate functions.")
1290
1291 (defun gnus-agent-spam-p ()
1292   "Say whether an article is spam or not."
1293   (unless gnus-agent-spam-hashtb
1294     (setq gnus-agent-spam-hashtb (gnus-make-hashtable 1000)))
1295   (if (not (equal (mail-header-references gnus-headers) ""))
1296       nil
1297     (let ((string (gnus-simplify-subject (mail-header-subject gnus-headers))))
1298       (prog1
1299           (gnus-gethash string gnus-agent-spam-hashtb)
1300         (gnus-sethash string t gnus-agent-spam-hashtb)))))
1301
1302 (defun gnus-agent-short-p ()
1303   "Say whether an article is short or not."
1304   (< (mail-header-lines gnus-headers) gnus-agent-short-article))
1305
1306 (defun gnus-agent-long-p ()
1307   "Say whether an article is long or not."
1308   (> (mail-header-lines gnus-headers) gnus-agent-long-article))
1309
1310 (defun gnus-agent-low-scored-p ()
1311   "Say whether an article has a low score or not."
1312   (< gnus-score gnus-agent-low-score))
1313
1314 (defun gnus-agent-high-scored-p ()
1315   "Say whether an article has a high score or not."
1316   (> gnus-score gnus-agent-high-score))
1317
1318 (defun gnus-category-make-function (cat)
1319   "Make a function from category CAT."
1320   `(lambda () ,(gnus-category-make-function-1 cat)))
1321
1322 (defun gnus-agent-true ()
1323   "Return t."
1324   t)
1325
1326 (defun gnus-agent-false ()
1327   "Return nil."
1328   nil)
1329
1330 (defun gnus-category-make-function-1 (cat)
1331   "Make a function from category CAT."
1332   (cond
1333    ;; Functions are just returned as is.
1334    ((or (symbolp cat)
1335         (gnus-functionp cat))
1336     `(,(or (cdr (assq cat gnus-category-predicate-alist))
1337            cat)))
1338    ;; More complex category.
1339    ((consp cat)
1340     `(,(cond
1341         ((memq (car cat) '(& and))
1342          'and)
1343         ((memq (car cat) '(| or))
1344          'or)
1345         ((memq (car cat) gnus-category-not)
1346          'not))
1347       ,@(mapcar 'gnus-category-make-function-1 (cdr cat))))
1348    (t
1349     (error "Unknown category type: %s" cat))))
1350
1351 (defun gnus-get-predicate (predicate)
1352   "Return the predicate for CATEGORY."
1353   (or (cdr (assoc predicate gnus-category-predicate-cache))
1354       (cdar (push (cons predicate
1355                         (gnus-category-make-function predicate))
1356                   gnus-category-predicate-cache))))
1357
1358 (defun gnus-group-category (group)
1359   "Return the category GROUP belongs to."
1360   (unless gnus-category-group-cache
1361     (setq gnus-category-group-cache (gnus-make-hashtable 1000))
1362     (let ((cs gnus-category-alist)
1363           groups cat)
1364       (while (setq cat (pop cs))
1365         (setq groups (cadddr cat))
1366         (while groups
1367           (gnus-sethash (pop groups) cat gnus-category-group-cache)))))
1368   (or (gnus-gethash group gnus-category-group-cache)
1369       (assq 'default gnus-category-alist)))
1370
1371 (defun gnus-agent-expire ()
1372   "Expire all old articles."
1373   (interactive)
1374   (let ((methods gnus-agent-covered-methods)
1375         (day (- (time-to-days (current-time)) gnus-agent-expire-days))
1376         gnus-command-method sym group articles
1377         history overview file histories elem art nov-file low info
1378         unreads marked article orig lowest highest)
1379     (save-excursion
1380       (setq overview (gnus-get-buffer-create " *expire overview*"))
1381       (while (setq gnus-command-method (pop methods))
1382         (when (file-exists-p (gnus-agent-lib-file "active"))
1383           (with-temp-buffer
1384             (insert-file-contents (gnus-agent-lib-file "active"))
1385             (gnus-active-to-gnus-format 
1386              gnus-command-method
1387              (setq orig (gnus-make-hashtable
1388                          (count-lines (point-min) (point-max))))))
1389           (let ((expiry-hashtb (gnus-make-hashtable 1023)))
1390             (gnus-agent-open-history)
1391             (set-buffer
1392              (setq gnus-agent-current-history
1393                    (setq history (gnus-agent-history-buffer))))
1394             (goto-char (point-min))
1395             (when (> (buffer-size) 1)
1396               (goto-char (point-min))
1397               (while (not (eobp))
1398                 (skip-chars-forward "^\t")
1399                 (if (> (read (current-buffer)) day)
1400                     ;; New article; we don't expire it.
1401                     (forward-line 1)
1402                   ;; Old article.  Schedule it for possible nuking.
1403                   (while (not (eolp))
1404                     (setq sym (let ((obarray expiry-hashtb))
1405                                 (read (current-buffer))))
1406                     (if (boundp sym)
1407                         (set sym (cons (cons (read (current-buffer)) (point))
1408                                        (symbol-value sym)))
1409                       (set sym (list (cons (read (current-buffer)) (point)))))
1410                     (skip-chars-forward " "))
1411                   (forward-line 1)))
1412               ;; We now have all articles that can possibly be expired.
1413               (mapatoms
1414                (lambda (sym)
1415                  (setq group (symbol-name sym)
1416                        articles (sort (symbol-value sym) 'car-less-than-car)
1417                        low (car (gnus-active group))
1418                        info (gnus-get-info group)
1419                        unreads (ignore-errors
1420                                  (gnus-list-of-unread-articles group))
1421                        marked (nconc
1422                                (gnus-uncompress-range
1423                                 (cdr (assq 'tick (gnus-info-marks info))))
1424                                (gnus-uncompress-range
1425                                 (cdr (assq 'dormant
1426                                            (gnus-info-marks info)))))
1427                        nov-file (gnus-agent-article-name ".overview" group)
1428                        lowest nil
1429                        highest nil)
1430                  (gnus-agent-load-alist group)
1431                  (gnus-message 5 "Expiring articles in %s" group)
1432                  (set-buffer overview)
1433                  (erase-buffer)
1434                  (when (file-exists-p nov-file)
1435                (nnheader-insert-file-contents nov-file))
1436                  (goto-char (point-min))
1437                  (setq article 0)
1438                  (while (setq elem (pop articles))
1439                    (setq article (car elem))
1440                    (when (or (null low)
1441                              (< article low)
1442                              gnus-agent-expire-all
1443                              (and (not (memq article unreads))
1444                                   (not (memq article marked))))
1445                      ;; Find and nuke the NOV line.
1446                      (while (and (not (eobp))
1447                                  (or (not (numberp
1448                                            (setq art (read (current-buffer)))))
1449                                      (< art article)))
1450                        (if (file-exists-p
1451                             (gnus-agent-article-name
1452                              (number-to-string art) group))
1453                            (progn
1454                              (unless lowest
1455                                (setq lowest art))
1456                              (setq highest art)
1457                              (forward-line 1))
1458                          ;; Remove old NOV lines that have no articles.
1459                          (gnus-delete-line)))
1460                      (if (or (eobp)
1461                              (/= art article))
1462                          (beginning-of-line)
1463                        (gnus-delete-line))
1464                      ;; Nuke the article.
1465                      (when (file-exists-p
1466                             (setq file (gnus-agent-article-name
1467                                         (number-to-string article)
1468                                         group)))
1469                        (delete-file file))
1470                      ;; Schedule the history line for nuking.
1471                      (push (cdr elem) histories)))
1472                  (gnus-make-directory (file-name-directory nov-file))
1473                  (let ((coding-system-for-write
1474                         gnus-agent-file-coding-system))
1475                    (write-region (point-min) (point-max) nov-file nil 'silent))
1476                  ;; Delete the unwanted entries in the alist.
1477                  (setq gnus-agent-article-alist
1478                        (sort gnus-agent-article-alist 'car-less-than-car))
1479                  (let* ((alist gnus-agent-article-alist)
1480                         (prev (cons nil alist))
1481                         (first prev)
1482                         expired)
1483                    (while (and alist
1484                                (<= (caar alist) article))
1485                      (if (or (not (cdar alist))
1486                              (not (file-exists-p
1487                                    (gnus-agent-article-name
1488                                     (number-to-string
1489                                      (caar alist))
1490                                     group))))
1491                          (progn
1492                            (push (caar alist) expired)
1493                            (setcdr prev (setq alist (cdr alist))))
1494                        (setq prev alist
1495                              alist (cdr alist))))
1496                    (setq gnus-agent-article-alist (cdr first))
1497                    (gnus-agent-save-alist group)
1498                    ;; Mark all articles up to the first article
1499                    ;; in `gnus-article-alist' as read.
1500                    (when (and info (caar gnus-agent-article-alist))
1501                      (setcar (nthcdr 2 info)
1502                              (gnus-range-add
1503                               (nth 2 info)
1504                               (cons 1 (- (caar gnus-agent-article-alist) 1)))))
1505                    ;; Maybe everything has been expired from `gnus-article-alist'
1506                    ;; and so the above marking as read could not be conducted,
1507                    ;; or there are expired article within the range of the alist.
1508                    (when (and info
1509                               expired
1510                               (or (not (caar gnus-agent-article-alist))
1511                                   (> (car expired)
1512                                      (caar gnus-agent-article-alist))))
1513                      (setcar (nthcdr 2 info)
1514                              (gnus-add-to-range
1515                               (nth 2 info)
1516                               (nreverse expired))))
1517                    (gnus-dribble-enter
1518                     (concat "(gnus-group-set-info '"
1519                             (gnus-prin1-to-string info)
1520                             ")")))
1521                  (when lowest
1522                    (if (gnus-gethash group orig)
1523                        (setcar (gnus-gethash group orig) lowest)
1524                      (gnus-sethash group (cons lowest highest) orig))))
1525                expiry-hashtb)
1526               (set-buffer history)
1527               (setq histories (nreverse (sort histories '<)))
1528               (while histories
1529                 (goto-char (pop histories))
1530                 (gnus-delete-line))
1531               (gnus-agent-save-history)
1532               (gnus-agent-close-history)
1533               (gnus-write-active-file
1534                (gnus-agent-lib-file "active") orig))
1535             (gnus-message 4 "Expiry...done")))))))
1536
1537 ;;;###autoload
1538 (defun gnus-agent-batch ()
1539   (interactive)
1540   (let ((init-file-user "")
1541         (gnus-always-read-dribble-file t))
1542     (gnus))
1543   (gnus-group-send-drafts)
1544   (gnus-agent-fetch-session))
1545
1546 (provide 'gnus-agent)
1547
1548 ;;; gnus-agent.el ends here