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