*** empty log message ***
[gnus] / lisp / gnus-agent.el
1 ;;; gnus-agent.el --- unplugged support for Gnus
2 ;; Copyright (C) 1997 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
5 ;; Keywords: news
6
7 ;; This file is part of GNU Emacs.
8
9 ;; GNU Emacs is free software; you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation; either version 2, or (at your option)
12 ;; any later version.
13
14 ;; GNU Emacs is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 ;; GNU General Public License for more details.
18
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
21 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
22 ;; Boston, MA 02111-1307, USA.
23
24 ;;; Commentary:
25
26 ;; (add-hook 'gnus-before-startup-hook 'gnus-open-agent)
27
28 ;;; Code:
29
30 (require 'gnus)
31 (require 'gnus-cache)
32 (require 'nnvirtual)
33 (require 'gnus-sum)
34
35 (defcustom gnus-agent-directory (nnheader-concat gnus-directory "agent/")
36   "Where the Gnus agent will store its files."
37   :group 'gnus-agent
38   :type 'directory)
39
40 (defcustom gnus-agent-plugged-hook nil
41   "Hook run when plugging into the network."
42   :group 'gnus-agent
43   :type 'hook)
44
45 (defcustom gnus-agent-unplugged-hook nil
46   "Hook run when unplugging from the network."
47   :group 'gnus-agent
48   :type 'hook)
49
50 ;;; Internal variables
51
52 (defvar gnus-agent-history-buffers nil)
53 (defvar gnus-agent-buffer-alist nil)
54 (defvar gnus-agent-article-alist nil)
55 (defvar gnus-agent-group-alist nil)
56 (defvar gnus-agent-covered-methods nil)
57 (defvar gnus-category-alist nil)
58 (defvar gnus-agent-current-history nil)
59 (defvar gnus-agent-overview-buffer nil)
60 (defvar gnus-category-predicate-cache nil)
61 (defvar gnus-category-group-cache nil)
62 (defvar gnus-agent-spam-hashtb nil)
63 (defvar gnus-agent-file-name nil)
64
65 (defvar gnus-plugged t
66   "Whether Gnus is plugged or not.")
67
68 ;; Dynamic variables
69 (defvar gnus-headers)
70 (defvar gnus-score)
71
72 ;;;
73 ;;; Setup
74 ;;;
75
76 (defun gnus-open-agent ()
77   (setq gnus-agent t)
78   (gnus-agent-read-servers)
79   (gnus-category-read)
80   (setq gnus-agent-overview-buffer
81         (get-buffer-create " *Gnus agent overview*"))
82   (add-hook 'gnus-group-mode-hook 'gnus-agent-mode)
83   (add-hook 'gnus-summary-mode-hook 'gnus-agent-mode)
84   (add-hook 'gnus-server-mode-hook 'gnus-agent-mode))
85
86 (gnus-add-shutdown 'gnus-close-agent 'gnus)
87
88 (defun gnus-close-agent ()
89   (setq gnus-agent-covered-methods nil
90         gnus-category-predicate-cache nil
91         gnus-category-group-cache nil
92         gnus-agent-spam-hashtb nil)
93   (gnus-kill-buffer gnus-agent-overview-buffer))
94
95 ;;;
96 ;;; Utility functions
97 ;;;
98
99 (defun gnus-agent-read-file (file)
100   "Load FILE and do a `read' there."
101   (nnheader-temp-write nil
102     (ignore-errors
103       (insert-file-contents file)
104       (goto-char (point-min))
105       (read (current-buffer)))))
106
107 (defsubst gnus-agent-method ()
108   (concat (symbol-name (car gnus-command-method)) "/"
109           (if (equal (cadr gnus-command-method) "")
110               "unnamed"
111             (cadr gnus-command-method))))
112
113 (defsubst gnus-agent-directory ()
114   "Path of the Gnus agent directory."
115   (nnheader-concat gnus-agent-directory (gnus-agent-method) "/"))
116
117 (defun gnus-agent-lib-file (file)
118   "The full path of the Gnus agent library FILE."
119   (concat (gnus-agent-directory) "lib/" file))
120
121 ;;;
122 ;;; Mode infestation
123 ;;;
124
125 (defvar gnus-agent-mode-hook nil
126   "Hook run when installing agent mode.")
127
128 (defvar gnus-agent-mode nil)
129 (defvar gnus-agent-mode-status '(gnus-agent-mode " Plugged"))
130
131 (defun gnus-agent-mode ()
132   "Minor mode for providing a agent support in Gnus buffers."
133   (let* ((buffer (progn (string-match "^gnus-\\(.*\\)-mode$"
134                                       (symbol-name major-mode))
135                         (match-string 1 (symbol-name major-mode))))
136          (mode (intern (format "gnus-agent-%s-mode" buffer))))
137     (set (make-local-variable 'gnus-agent-mode) t)
138     (set mode nil)
139     (set (make-local-variable mode) t)
140     ;; Set up the menu.
141     (when (gnus-visual-p 'agent-menu 'menu)
142       (funcall (intern (format "gnus-agent-%s-make-menu-bar" buffer))))
143     (unless (assq 'gnus-agent-mode minor-mode-alist)
144       (push gnus-agent-mode-status minor-mode-alist))
145     (unless (assq mode minor-mode-map-alist)
146       (push (cons mode (symbol-value (intern (format "gnus-agent-%s-mode-map"
147                                                      buffer))))
148             minor-mode-map-alist))
149     (gnus-agent-toggle-plugged gnus-plugged)
150     (run-hooks 'gnus-agent-mode-hook)))
151
152 (defvar gnus-agent-group-mode-map (make-sparse-keymap))
153 (gnus-define-keys gnus-agent-group-mode-map
154   "Ju" gnus-agent-fetch-group
155   "Jc" gnus-enter-category-buffer
156   "Jj" gnus-agent-toggle-plugged
157   "Js" gnus-agent-fetch-session
158   "Ja" gnus-agent-add-group)
159
160 (defun gnus-agent-group-make-menu-bar ()
161   (unless (boundp 'gnus-agent-group-menu)
162     (easy-menu-define
163      gnus-agent-group-menu gnus-agent-group-mode-map ""
164      '("Agent"
165        ["Toggle plugged" gnus-agent-toggle-plugged t]
166        ["List categories" gnus-enter-category-buffer t]
167        ("Fetch"
168         ["Group" gnus-agent-fetch-group t])))))
169
170 (defvar gnus-agent-summary-mode-map (make-sparse-keymap))
171 (gnus-define-keys gnus-agent-summary-mode-map
172   "Jj" gnus-agent-toggle-plugged
173   "J#" gnus-agent-mark-article
174   "J\M-#" gnus-agent-unmark-article
175   "@" gnus-agent-toggle-mark
176   "Jc" gnus-agent-catchup)
177
178 (defun gnus-agent-summary-make-menu-bar ()
179   (unless (boundp 'gnus-agent-summary-menu)
180     (easy-menu-define
181      gnus-agent-summary-menu gnus-agent-summary-mode-map ""
182      '("Agent"
183        ["Toggle plugged" gnus-agent-toggle-plugged t]
184        ["Mark as downloadable" gnus-agent-mark-article t]
185        ["Unmark as downloadable" gnus-agent-unmark-article t]
186        ["Toggle mark" gnus-agent-toggle-mark t]
187        ["Catchup undownloaded" gnus-agent-catchup t]))))
188
189 (defvar gnus-agent-server-mode-map (make-sparse-keymap))
190 (gnus-define-keys gnus-agent-server-mode-map
191   "Jj" gnus-agent-toggle-plugged
192   "Ja" gnus-agent-add-server
193   "Jr" gnus-agent-remove-server)
194
195 (defun gnus-agent-server-make-menu-bar ()
196   (unless (boundp 'gnus-agent-server-menu)
197     (easy-menu-define
198      gnus-agent-server-menu gnus-agent-server-mode-map ""
199      '("Agent"
200        ["Toggle plugged" gnus-agent-toggle-plugged t]
201        ["Add" gnus-agent-add-server t]
202        ["Remove" gnus-agent-remove-server t]))))
203
204 (defun gnus-agent-toggle-plugged (plugged)
205   "Toggle whether Gnus is unplugged or not."
206   (interactive (list (not gnus-plugged)))
207   (if plugged
208       (progn
209         (run-hooks 'gnus-agent-plugged-hook)
210         (setcar (cdr gnus-agent-mode-status) " Plugged"))
211     (gnus-agent-close-connections)
212     (run-hooks 'gnus-agent-unplugged-hook)
213     (setcar (cdr gnus-agent-mode-status) " Unplugged"))
214   (setq gnus-plugged plugged)
215   (set-buffer-modified-p t))
216
217 (defun gnus-agent-close-connections ()
218   "Close all methods covered by the Gnus agent."
219   (let ((methods gnus-agent-covered-methods))
220     (while methods
221       (gnus-close-server (pop methods)))))
222
223 ;;;###autoload
224 (defun gnus-unplugged ()
225   "Start Gnus unplugged."
226   (interactive)
227   (setq gnus-plugged nil)
228   (gnus))
229
230 ;;;
231 ;;; Group mode commands
232 ;;;
233
234 (defun gnus-agent-fetch-group (group)
235   "Put all new articles in GROUP into the agent."
236   (interactive (list (gnus-group-group-name)))
237   (unless group
238     (error "No group on the current line"))
239   (let ((articles (gnus-list-of-unread-articles group))
240         (gnus-command-method (gnus-find-method-for-group group)))
241     (gnus-agent-with-fetch
242       (gnus-agent-fetch-articles group articles))))
243
244 (defun gnus-agent-add-group (category arg)
245   "Add the current group to an agent category."
246   (interactive
247    (list
248     (intern
249      (completing-read
250       "Add to category: "
251       (mapcar (lambda (cat) (list (symbol-name (car cat))))
252               gnus-category-alist)
253       nil t))
254     current-prefix-arg))
255   (let ((cat (assq category gnus-category-alist))
256         c groups)
257     (gnus-group-iterate arg
258       (lambda (group)
259         (when (cadddr (setq c (gnus-group-category group)))
260           (setf (cadddr c) (delete group (cadddr c))))
261         (push group groups)))
262     (setf (cadddr cat) (nconc (cadddr cat) groups))
263     (gnus-category-write)))
264
265 ;;;
266 ;;; Server mode commands
267 ;;;
268
269 (defun gnus-agent-add-server (server)
270   "Enroll SERVER in the agent program."
271   (interactive (list (gnus-server-server-name)))
272   (unless server
273     (error "No server on the current line"))
274   (let ((method (gnus-server-get-method nil (gnus-server-server-name))))
275     (when (member method gnus-agent-covered-methods)
276       (error "Server already in the agent program"))
277     (push method gnus-agent-covered-methods)
278     (gnus-agent-write-servers)
279     (message "Entered %s into the agent" server)))
280
281 (defun gnus-agent-remove-server (server)
282   "Remove SERVER from the agent program."
283   (interactive (list (gnus-server-server-name)))
284   (unless server
285     (error "No server on the current line"))
286   (let ((method (gnus-server-get-method nil (gnus-server-server-name))))
287     (unless (member method gnus-agent-covered-methods)
288       (error "Server not in the agent program"))
289     (setq gnus-agent-covered-methods
290           (delete method gnus-agent-covered-methods))
291     (gnus-agent-write-servers)
292     (message "Removed %s from the agent" server)))
293
294 (defun gnus-agent-read-servers ()
295   "Read the alist of covered servers."
296   (setq gnus-agent-covered-methods
297         (gnus-agent-read-file
298          (nnheader-concat gnus-agent-directory "lib/servers"))))
299
300 (defun gnus-agent-write-servers ()
301   "Write the alist of covered servers."
302   (nnheader-temp-write (nnheader-concat gnus-agent-directory "lib/servers")
303     (prin1 gnus-agent-covered-methods (current-buffer))))
304
305 ;;;
306 ;;; Summary commands
307 ;;;
308
309 (defun gnus-agent-mark-article (n &optional unmark)
310   "Mark the next N articles as downloadable.
311 If N is negative, mark backward instead.  If UNMARK is non-nil, remove
312 the mark instead.  The difference between N and the actual number of
313 articles marked is returned."
314   (interactive "p")
315   (gnus-set-global-variables)
316   (let ((backward (< n 0))
317         (n (abs n)))
318     (while (and
319             (> n 0)
320             (progn
321               (gnus-summary-set-agent-mark
322                (gnus-summary-article-number) unmark)
323               (zerop (gnus-summary-next-subject (if backward -1 1) nil t))))
324       (setq n (1- n)))
325     (when (/= 0 n)
326       (gnus-message 7 "No more articles"))
327     (gnus-summary-recenter)
328     (gnus-summary-position-point)
329     n))
330
331 (defun gnus-agent-unmark-article (n)
332   "Remove the downloadable mark from the next N articles.
333 If N is negative, unmark backward instead.  The difference between N and
334 the actual number of articles unmarked is returned."
335   (interactive "p")
336   (gnus-set-global-variables)
337   (gnus-agent-mark-article n t))
338
339 (defun gnus-agent-toggle-mark (n)
340   "Toggle the downloadable mark from the next N articles.
341 If N is negative, toggle backward instead.  The difference between N and
342 the actual number of articles toggled is returned."
343   (interactive "p")
344   (gnus-set-global-variables)
345   (gnus-agent-mark-article n 'toggle))
346
347 (defun gnus-summary-set-agent-mark (article &optional unmark)
348   "Mark ARTICLE as downloadable."
349   (let ((unmark (if (and (not (null unmark)) (not (eq t unmark)))
350                     (memq article gnus-newsgroup-downloadable)
351                   unmark)))
352     (setq gnus-newsgroup-downloadable
353           (delq article gnus-newsgroup-downloadable))
354     (unless unmark
355       (push article gnus-newsgroup-downloadable))
356     (gnus-summary-update-mark
357      (if unmark gnus-undownloaded-mark gnus-downloadable-mark)
358      'unread)))
359
360 (defun gnus-agent-get-undownloaded-list ()
361   "Mark all unfetched articles as read."
362   (let ((gnus-command-method (gnus-find-method-for-group gnus-newsgroup-name)))
363     (when (and (not gnus-plugged)
364                (gnus-agent-method-p gnus-command-method))
365       (gnus-agent-load-alist gnus-newsgroup-name)
366       (let ((articles gnus-newsgroup-unreads)
367             article)
368         (while (setq article (pop articles))
369           (unless (or (cdr (assq article gnus-agent-article-alist))
370                   (memq article gnus-newsgroup-downloadable))
371             (push article gnus-newsgroup-undownloaded)))))))
372
373 (defun gnus-agent-catchup ()
374   "Mark all undownloaded articles as read."
375   (interactive)
376   (save-excursion
377     (while gnus-newsgroup-undownloaded
378       (gnus-summary-mark-article
379        (pop gnus-newsgroup-undownloaded) gnus-catchup-mark)))
380   (gnus-summary-position-point))
381
382 ;;;
383 ;;; Internal functions
384 ;;;
385
386 (defun gnus-agent-save-active (method)
387   (when (gnus-agent-method-p method)
388     (let* ((gnus-command-method method)
389            (file (gnus-agent-lib-file "active")))
390       (gnus-make-directory (file-name-directory file))
391       (write-region (point-min) (point-max) file nil 'silent)
392       (when (file-exists-p (gnus-agent-lib-file "groups"))
393         (delete-file (gnus-agent-lib-file "groups"))))))
394
395 (defun gnus-agent-save-groups (method)
396   (let* ((gnus-command-method method)
397          (file (gnus-agent-lib-file "groups")))
398     (gnus-make-directory (file-name-directory file))
399     (write-region (point-min) (point-max) file nil 'silent))
400     (when (file-exists-p (gnus-agent-lib-file "active"))
401       (delete-file (gnus-agent-lib-file "active"))))
402
403 (defun gnus-agent-group-path (group)
404   "Translate GROUP into a path."
405   (nnheader-replace-chars-in-string group ?. ?/))
406
407 \f
408
409 (defun gnus-agent-method-p (method)
410   "Say whether METHOD is covered by the agent."
411   (member method gnus-agent-covered-methods))
412
413 (defun gnus-agent-get-function (method)
414   (if (and (not gnus-plugged)
415            (gnus-agent-method-p method))
416       (progn
417         (require 'nnagent)
418         'nnagent)
419     (car method)))
420
421 ;;; History functions
422
423 (defun gnus-agent-history-buffer ()
424   (cdr (assoc (gnus-agent-method) gnus-agent-history-buffers)))
425
426 (defun gnus-agent-open-history ()
427   (save-excursion
428     (push (cons (gnus-agent-method)
429                 (set-buffer (get-buffer-create
430                              (format " *Gnus agent %s history*"
431                                      (gnus-agent-method)))))
432           gnus-agent-history-buffers)
433     (erase-buffer)
434     (insert "\n")
435     (let ((file (gnus-agent-lib-file "history")))
436       (when (file-exists-p file)
437         (insert-file file))
438       (set (make-local-variable 'gnus-agent-file-name) file))))
439
440 (defun gnus-agent-save-history ()
441   (save-excursion
442     (set-buffer gnus-agent-current-history)
443     (gnus-make-directory (file-name-directory gnus-agent-file-name))
444     (write-region (1+ (point-min)) (point-max)
445                   gnus-agent-file-name nil 'silent)))
446
447 (defun gnus-agent-close-history ()
448   (when (gnus-buffer-live-p gnus-agent-current-history)
449     (kill-buffer gnus-agent-current-history)
450     (setq gnus-agent-history-buffers
451           (delq (assoc (gnus-agent-method) gnus-agent-history-buffers)
452                 gnus-agent-history-buffers))))
453
454 (defun gnus-agent-enter-history (id group-arts date)
455   (save-excursion
456     (set-buffer gnus-agent-current-history)
457     (goto-char (point-max))
458     (insert id "\t" (number-to-string date) "\t")
459     (while group-arts
460       (insert (caar group-arts) "/" (number-to-string (cdr (pop group-arts)))
461               " "))
462     (insert "\n")))
463
464 (defun gnus-agent-article-in-history-p (id)
465   (save-excursion
466     (set-buffer (gnus-agent-history-buffer))
467     (goto-char (point-min))
468     (search-forward (concat "\n" id "\t") nil t)))
469
470 (defun gnus-agent-history-path (id)
471   (save-excursion
472     (set-buffer (gnus-agent-history-buffer))
473     (goto-char (point-min))
474     (when (search-forward (concat "\n" id "\t") nil t)
475       (let ((method (gnus-agent-method)))
476         (let (paths group)
477           (while (not (numberp (setq group (read (current-buffer)))))
478             (push (concat method "/" group) paths))
479           (nreverse paths))))))
480
481 ;;;
482 ;;; Fetching
483 ;;;
484
485 (defun gnus-agent-start-fetch ()
486   "Initialize data structures for efficient fetching."
487   (gnus-agent-open-history)
488   (setq gnus-agent-current-history (gnus-agent-history-buffer)))
489
490 (defun gnus-agent-stop-fetch ()
491   "Save all data structures and clean up."
492   (gnus-agent-save-history)
493   (gnus-agent-close-history)
494   (setq gnus-agent-spam-hashtb nil)
495   (save-excursion
496     (set-buffer nntp-server-buffer)
497     (widen)))
498
499 (defmacro gnus-agent-with-fetch (&rest forms)
500   "Do FORMS safely."
501   `(unwind-protect
502        (progn
503          (gnus-agent-start-fetch)
504          ,@forms)
505      (gnus-agent-stop-fetch)))
506
507 (put 'gnus-agent-with-fetch 'lisp-indent-function 0)
508 (put 'gnus-agent-with-fetch 'edebug-form-spec '(body))
509
510 (defun gnus-agent-fetch-articles (group articles)
511   "Fetch ARTICLES from GROUP and put them into the agent."
512   (when articles
513     ;; Prune off articles that we have already fetched.
514     (while (and articles
515                 (cdr (assq (car articles) gnus-agent-article-alist)))
516       (pop articles))
517     (let ((arts articles))
518       (while (cdr arts)
519         (if (cdr (assq (cadr arts) gnus-agent-article-alist))
520             (setcdr arts (cddr arts))
521           (setq arts (cdr arts)))))
522     (when articles
523       (let ((dir (concat
524                   (gnus-agent-directory)
525                   (gnus-agent-group-path group) "/"))
526             (date (gnus-time-to-day (current-time)))
527             (case-fold-search t)
528             pos alists crosses id elem)
529         (gnus-make-directory dir)
530         (gnus-message 7 "Fetching articles for %s..." group)
531         ;; Fetch the articles from the backend.
532         (if (gnus-check-backend-function 'retrieve-articles group)
533             (setq pos (gnus-retrieve-articles articles group))
534           (nnheader-temp-write nil
535             (let ((buf (current-buffer))
536                   article)
537               (while (setq article (pop articles))
538                 (when (gnus-request-article article group)
539                   (goto-char (point-max))
540                   (push (cons article (point)) pos)
541                   (insert-buffer-substring nntp-server-buffer)))
542               (copy-to-buffer nntp-server-buffer (point-min) (point-max))
543               (setq pos (nreverse pos)))))
544         ;; Then save these articles into the agent.
545         (save-excursion
546           (set-buffer nntp-server-buffer)
547           (while pos
548             (narrow-to-region (cdar pos) (or (cdadr pos) (point-max)))
549             (goto-char (point-min))
550             (when (search-forward "\n\n" nil t)
551               (when (search-backward "\nXrefs: " nil t)
552                 ;; Handle crossposting.
553                 (skip-chars-forward "^ ")
554                 (skip-chars-forward " ")
555                 (setq crosses nil)
556                 (while (looking-at "\\([^: \n]+\\):\\([0-9]+\\) +")
557                   (push (cons (buffer-substring (match-beginning 1)
558                                                 (match-end 1))
559                               (buffer-substring (match-beginning 2)
560                                                 (match-end 2)))
561                         crosses)
562                   (goto-char (match-end 0)))
563                 (gnus-agent-crosspost crosses (caar pos))))
564             (goto-char (point-min))
565             (if (not (re-search-forward "^Message-ID: *<\\([^>\n]+\\)>" nil t))
566                 (setq id "No-Message-ID-in-article")
567               (setq id (buffer-substring (match-beginning 1) (match-end 1))))
568             (write-region (point-min) (point-max)
569                           (concat dir (number-to-string (caar pos)))
570                           nil 'silent)
571             (when (setq elem (assq (caar pos) gnus-agent-article-alist))
572               (setcdr elem t))
573             (gnus-agent-enter-history
574              id (or crosses (list (cons group (caar pos)))) date)
575             (widen)
576             (pop pos)))
577         (gnus-agent-save-alist group)))))
578
579 (defun gnus-agent-crosspost (crosses article)
580   (let (gnus-agent-article-alist group alist beg end)
581     (save-excursion
582       (set-buffer gnus-agent-overview-buffer)
583       (when (nnheader-find-nov-line article)
584         (forward-word 1)
585         (setq beg (point))
586         (setq end (progn (forward-line 1) (point)))))
587     (while crosses
588       (setq group (caar crosses))
589       (unless (setq alist (assoc group gnus-agent-group-alist))
590         (push (setq alist (list group (gnus-agent-load-alist (caar crosses))))
591               gnus-agent-group-alist))
592       (setcdr alist (cons (cons (cdar crosses) t) (cdr alist)))
593       (save-excursion
594         (set-buffer (get-buffer-create (format " *Gnus agent overview %s*"
595                                                group)))
596         (when (= (point-max) (point-min))
597           (push (cons group (current-buffer)) gnus-agent-buffer-alist)
598           (ignore-errors
599             (insert-file-contents
600              (gnus-agent-article-name ".overview" group))))
601         (nnheader-find-nov-line (string-to-number (cdar crosses)))
602         (insert (string-to-number (cdar crosses)))
603         (insert-buffer-substring gnus-agent-overview-buffer beg end))
604       (pop crosses))))
605
606 (defun gnus-agent-flush-cache ()
607   (save-excursion
608     (while gnus-agent-buffer-alist
609       (set-buffer (cdar gnus-agent-buffer-alist))
610       (write-region (point-min) (point-max)
611                     (gnus-agent-article-name ".overview"
612                                              (caar gnus-agent-buffer-alist))
613                      nil 'silent)
614       (pop gnus-agent-buffer-alist))
615     (while gnus-agent-group-alist
616       (nnheader-temp-write (caar gnus-agent-group-alist)
617         (princ (cdar gnus-agent-group-alist))
618         (insert "\n"))
619       (pop gnus-agent-group-alist))))
620
621 (defun gnus-agent-fetch-headers (group articles &optional force)
622   (gnus-agent-load-alist group)
623   ;; Find out what headers we need to retrieve.
624   (when articles
625     (while (and articles
626                 (assq (car articles) gnus-agent-article-alist))
627       (pop articles))
628     (let ((arts articles))
629       (while (cdr arts)
630         (if (assq (cadr arts) gnus-agent-article-alist)
631             (setcdr arts (cddr arts))
632           (setq arts (cdr arts)))))
633     ;; Fetch them.
634     (when articles
635       (gnus-message 7 "Fetching headers for %s..." group)
636       (save-excursion
637         (set-buffer nntp-server-buffer)
638         (unless (eq 'nov (gnus-retrieve-headers articles group))
639           (nnvirtual-convert-headers))
640         ;; Save these headers for later processing.
641         (copy-to-buffer gnus-agent-overview-buffer (point-min) (point-max))
642         (let (file)
643           (when (file-exists-p
644                  (setq file (gnus-agent-article-name ".overview" group)))
645             (gnus-agent-braid-nov group articles file))
646           (gnus-make-directory (file-name-directory file))
647           (write-region (point-min) (point-max) file nil 'silent)
648           (gnus-agent-save-alist group articles nil))
649         t))))
650
651 (defsubst gnus-agent-copy-nov-line (article)
652   (let (b e)
653     (set-buffer gnus-agent-overview-buffer)
654     (setq b (point))
655     (if (eq article (read (current-buffer)))
656         (setq e (progn (forward-line 1) (point)))
657       (setq e b))
658     (set-buffer nntp-server-buffer)
659     (insert-buffer-substring gnus-agent-overview-buffer b e)))
660
661 (defun gnus-agent-braid-nov (group articles file)
662   (let (beg end)
663     (set-buffer gnus-agent-overview-buffer)
664     (goto-char (point-min))
665     (set-buffer nntp-server-buffer)
666     (erase-buffer)
667     (insert-file-contents file)
668     (goto-char (point-min))
669     (if (or (= (point-min) (point-max))
670             (progn
671               (forward-line -1)
672               (< (read (current-buffer)) (car articles))))
673         ;; We have only headers that are after the older headers,
674         ;; so we just append them.
675         (progn
676           (goto-char (point-max))
677           (insert-buffer-substring gnus-agent-overview-buffer))
678       ;; We do it the hard way.
679       (nnheader-find-nov-line (car articles))
680       (gnus-agent-copy-nov-line (car articles))
681       (pop articles)
682       (while (and articles
683                   (not (eobp)))
684         (while (and (not (eobp))
685                     (< (read (current-buffer)) (car articles)))
686           (forward-line 1))
687         (beginning-of-line)
688         (unless (eobp)
689           (gnus-agent-copy-nov-line (car articles))
690           (setq articles (cdr articles))))
691       (when articles
692         (let (b e)
693           (set-buffer gnus-agent-overview-buffer)
694           (setq b (point)
695                 e (point-max))
696           (set-buffer nntp-server-buffer)
697           (insert-buffer-substring gnus-agent-overview-buffer b e))))))
698
699 (defun gnus-agent-load-alist (group &optional dir)
700   "Load the article-state alist for GROUP."
701   (setq gnus-agent-article-alist
702         (gnus-agent-read-file
703          (if dir
704              (concat dir ".agentview")
705            (gnus-agent-article-name ".agentview" group)))))
706
707 (defun gnus-agent-save-alist (group &optional articles state dir)
708   "Load the article-state alist for GROUP."
709   (nnheader-temp-write (if dir
710                            (concat dir ".agentview")
711                          (gnus-agent-article-name ".agentview" group))
712     (princ (setq gnus-agent-article-alist
713                  (nconc gnus-agent-article-alist
714                         (mapcar (lambda (article) (cons article state))
715                                 articles)))
716            (current-buffer))
717     (insert "\n")))
718
719 (defun gnus-agent-article-name (article group)
720   (concat (gnus-agent-directory) (gnus-agent-group-path group) "/"
721           (if (stringp article) article (string-to-number article))))
722
723 (defun gnus-agent-fetch-session ()
724   "Fetch all articles and headers that are eligible for fetching."
725   (interactive)
726   (unless gnus-agent-covered-methods
727     (error "No servers are covered by the Gnus agent"))
728   (unless gnus-plugged
729     (error "Can't fetch articles while Gnus is unplugged"))
730   (let ((methods gnus-agent-covered-methods)
731         gnus-newsgroup-dependencies gnus-newsgroup-headers
732         gnus-newsgroup-scored
733         gnus-headers gnus-score
734         gnus-use-cache
735         gnus-command-method groups group articles score arts
736         category predicate info marks score-param)
737     (save-excursion
738       (while methods
739         (setq gnus-command-method (car methods)
740               groups (gnus-groups-from-server (pop methods)))
741         (gnus-agent-with-fetch
742           (while (setq group (pop groups))
743             ;; Fetch headers.
744             (when (and (setq articles (gnus-list-of-unread-articles group))
745                        (gnus-agent-fetch-headers group articles))
746               ;; Parse them and see which articles we want to fetch.
747               (setq gnus-newsgroup-dependencies
748                     (make-vector (length articles) 0))
749               (setq gnus-newsgroup-headers
750                     (gnus-get-newsgroup-headers-xover articles nil nil group))
751               (setq category (gnus-group-category group))
752               (setq predicate
753                     (gnus-get-predicate 
754                      (or (gnus-group-get-parameter group 'agent-predicate)
755                          (cadr category))))
756               (setq score-param
757                     (or (gnus-group-get-parameter group 'agent-score)
758                         (caddr category)))
759               (when score-param
760                 (gnus-score-headers (list (list score-param))))
761               (setq arts nil)
762               (while (setq gnus-headers (pop gnus-newsgroup-headers))
763                 (setq gnus-score
764                       (or (cdr (assq (mail-header-number gnus-headers)
765                                      gnus-newsgroup-scored))
766                           gnus-summary-default-score))
767                 (when (funcall predicate)
768                   (push (mail-header-number gnus-headers)
769                         arts)))
770               ;; Fetch the articles.
771               (when arts
772                 (gnus-agent-fetch-articles group arts)))
773             ;; Perhaps we have some additional articles to fetch.
774             (setq arts (assq 'download (gnus-info-marks
775                                         (setq info (gnus-get-info group)))))
776             (when (cdr arts)
777               (gnus-agent-fetch-articles
778                group (gnus-uncompress-range (cdr arts)))
779               (setq marks (delq arts (gnus-info-marks info)))
780               (gnus-info-set-marks info marks)))))
781       (gnus-message 6 "Finished fetching articles into the Gnus agent"))))
782
783 ;;;
784 ;;; Agent Category Mode
785 ;;;
786
787 (defvar gnus-category-mode-hook nil
788   "Hook run in `gnus-category-mode' buffers.")
789
790 (defvar gnus-category-line-format "     %(%20c%): %g\n"
791   "Format of category lines.")
792
793 (defvar gnus-category-mode-line-format "Gnus: %%b"
794   "The format specification for the category mode line.")
795
796 ;;; Internal variables.
797
798 (defvar gnus-category-buffer "*Agent Category*")
799
800 (defvar gnus-category-line-format-alist
801   `((?c name ?s)
802     (?g groups ?d)))
803
804 (defvar gnus-category-mode-line-format-alist
805   `((?u user-defined ?s)))
806
807 (defvar gnus-category-line-format-spec nil)
808 (defvar gnus-category-mode-line-format-spec nil)
809
810 (defvar gnus-category-mode-map nil)
811 (put 'gnus-category-mode 'mode-class 'special)
812
813 (unless gnus-category-mode-map
814   (setq gnus-category-mode-map (make-sparse-keymap))
815   (suppress-keymap gnus-category-mode-map)
816
817   (gnus-define-keys gnus-category-mode-map
818     "q" gnus-category-exit
819     "k" gnus-category-kill
820     "c" gnus-category-copy
821     "a" gnus-category-add
822     "p" gnus-category-edit-predicate
823     "g" gnus-category-edit-groups
824     "s" gnus-category-edit-score
825     "l" gnus-category-list
826
827     "\C-c\C-i" gnus-info-find-node
828     "\C-c\C-b" gnus-bug))
829
830 (defvar gnus-category-menu-hook nil
831   "*Hook run after the creation of the menu.")
832
833 (defun gnus-category-make-menu-bar ()
834   (gnus-turn-off-edit-menu 'category)
835   (unless (boundp 'gnus-category-menu)
836     (easy-menu-define
837      gnus-category-menu gnus-category-mode-map ""
838      '("Categories"
839        ["Add" gnus-category-add t]
840        ["Kill" gnus-category-kill t]
841        ["Copy" gnus-category-copy t]
842        ["Edit predicate" gnus-category-edit-predicate t]
843        ["Edit score" gnus-category-edit-score t]
844        ["Edit groups" gnus-category-edit-groups t]
845        ["Exit" gnus-category-exit t]))
846
847     (run-hooks 'gnus-category-menu-hook)))
848
849 (defun gnus-category-mode ()
850   "Major mode for listing and editing agent categories.
851
852 All normal editing commands are switched off.
853 \\<gnus-category-mode-map>
854 For more in-depth information on this mode, read the manual
855 (`\\[gnus-info-find-node]').
856
857 The following commands are available:
858
859 \\{gnus-category-mode-map}"
860   (interactive)
861   (when (gnus-visual-p 'category-menu 'menu)
862     (gnus-category-make-menu-bar))
863   (kill-all-local-variables)
864   (gnus-simplify-mode-line)
865   (setq major-mode 'gnus-category-mode)
866   (setq mode-name "Category")
867   (gnus-set-default-directory)
868   (setq mode-line-process nil)
869   (use-local-map gnus-category-mode-map)
870   (buffer-disable-undo (current-buffer))
871   (setq truncate-lines t)
872   (setq buffer-read-only t)
873   (run-hooks 'gnus-category-mode-hook))
874
875 (defalias 'gnus-category-position-point 'gnus-goto-colon)
876
877 (defun gnus-category-insert-line (category)
878   (let* ((name (car category))
879          (groups (length (cadddr category))))
880     (beginning-of-line)
881     (gnus-add-text-properties
882      (point)
883      (prog1 (1+ (point))
884        ;; Insert the text.
885        (eval gnus-category-line-format-spec))
886      (list 'gnus-category name))))
887
888 (defun gnus-enter-category-buffer ()
889   "Go to the Category buffer."
890   (interactive)
891   (gnus-category-setup-buffer)
892   (gnus-configure-windows 'category)
893   (gnus-category-prepare))
894
895 (defun gnus-category-setup-buffer ()
896   (unless (get-buffer gnus-category-buffer)
897     (save-excursion
898       (set-buffer (get-buffer-create gnus-category-buffer))
899       (gnus-add-current-to-buffer-list)
900       (gnus-category-mode))))
901
902 (defun gnus-category-prepare ()
903   (gnus-set-format 'category-mode)
904   (gnus-set-format 'category t)
905   (let ((alist gnus-category-alist)
906         (buffer-read-only nil))
907     (erase-buffer)
908     (while alist
909       (gnus-category-insert-line (pop alist)))
910     (goto-char (point-min))
911     (gnus-category-position-point)))
912
913 (defun gnus-category-name ()
914   (or (get-text-property (gnus-point-at-bol) 'gnus-category)
915       (error "No category on the current line")))
916
917 (defun gnus-category-read ()
918   "Read the category alist."
919   (setq gnus-category-alist
920         (or (gnus-agent-read-file
921              (nnheader-concat gnus-agent-directory "lib/categories"))
922             (list (list 'default 'true nil nil)))))
923     
924 (defun gnus-category-write ()
925   "Write the category alist."
926   (setq gnus-category-predicate-cache nil
927         gnus-category-group-cache nil)
928   (nnheader-temp-write (nnheader-concat gnus-agent-directory "lib/categories")
929     (prin1 gnus-category-alist (current-buffer))))
930
931 (defun gnus-category-edit-predicate (category)
932   "Edit the predicate for CATEGORY."
933   (interactive (list (gnus-category-name)))
934   (let ((info (assq category gnus-category-alist)))
935     (gnus-edit-form
936      (cadr info) (format "Editing the predicate for category %s" category)
937      `(lambda (predicate)
938         (setf (cadr (assq ',category gnus-category-alist)) predicate)
939         (gnus-category-write)
940         (gnus-category-list)))))
941   
942 (defun gnus-category-edit-score (category)
943   "Edit the score expression for CATEGORY."
944   (interactive (list (gnus-category-name)))
945   (let ((info (assq category gnus-category-alist)))
946     (gnus-edit-form
947      (caddr info)
948      (format "Editing the score expression for category %s" category)
949      `(lambda (groups)
950         (setf (caddr (assq ',category gnus-category-alist)) groups)
951         (gnus-category-write)
952         (gnus-category-list)))))
953
954 (defun gnus-category-edit-groups (category)
955   "Edit the group list for CATEGORY."
956   (interactive (list (gnus-category-name)))
957   (let ((info (assq category gnus-category-alist)))
958     (gnus-edit-form
959      (cadddr info) (format "Editing the group list for category %s" category)
960      `(lambda (groups)
961         (setf (cadddr (assq ',category gnus-category-alist)) groups)
962         (gnus-category-write)
963         (gnus-category-list)))))
964
965 (defun gnus-category-kill (category)
966   "Kill the current category."
967   (interactive (list (gnus-category-name)))
968   (let ((info (assq category gnus-category-alist))
969         (buffer-read-only nil))
970     (gnus-delete-line)
971     (gnus-category-write)
972     (setq gnus-category-alist (delq info gnus-category-alist))))
973
974 (defun gnus-category-copy (category to)
975   "Copy the current category."
976   (interactive (list (gnus-category-name) (intern (read-string "New name: "))))
977   (let ((info (assq category gnus-category-alist)))
978     (push (list to (gnus-copy-sequence (cadr info))
979                 (gnus-copy-sequence (caddr info)) nil)
980           gnus-category-alist)
981     (gnus-category-write)
982     (gnus-category-list)))
983
984 (defun gnus-category-add (category)
985   "Create a new category."
986   (interactive "SCategory name: ")
987   (when (assq category gnus-category-alist)
988     (error "Category %s already exists" category))
989   (push (list category 'true nil nil)
990         gnus-category-alist)
991   (gnus-category-write)
992   (gnus-category-list))
993
994 (defun gnus-category-list ()
995   "List all categories."
996   (interactive)
997   (gnus-category-prepare))
998
999 (defun gnus-category-exit ()
1000   "Return to the group buffer."
1001   (interactive)
1002   (kill-buffer (current-buffer))
1003   (gnus-configure-windows 'group t))
1004
1005 ;; To avoid having 8-bit characters in the source file.
1006 (defvar gnus-category-not (list '! 'not (intern (format "%c" 172))))
1007
1008 (defvar gnus-category-predicate-alist
1009   '((spam . gnus-agent-spam-p)
1010     (short . gnus-agent-short-p)
1011     (long . gnus-agent-long-p)
1012     (low . gnus-agent-low-scored-p)
1013     (high . gnus-agent-high-scored-p)
1014     (true . gnus-agent-true)
1015     (false . gnus-agent-false))
1016   "Mapping from short score predicate symbols to predicate functions.")
1017
1018 (defun gnus-agent-spam-p ()
1019   "Say whether an article is spam or not."
1020   (unless gnus-agent-spam-hashtb
1021     (setq gnus-agent-spam-hashtb (gnus-make-hashtable 1000)))
1022   (if (not (equal (mail-header-references gnus-headers) ""))
1023       nil
1024     (let ((string (gnus-simplify-subject (mail-header-subject gnus-headers))))
1025       (prog1
1026           (gnus-gethash string gnus-agent-spam-hashtb)
1027         (gnus-sethash string t gnus-agent-spam-hashtb)))))
1028
1029 (defun gnus-agent-short-p ()
1030   "Say whether an article is short (less than 100 lines) or not."
1031   (< (mail-header-lines gnus-headers) 100))
1032
1033 (defun gnus-agent-long-p ()
1034   "Say whether an article is long (more than 200 lines) or not."
1035   (> (mail-header-lines gnus-headers) 200))
1036
1037 (defun gnus-agent-low-scored-p ()
1038   "Say whether an article has a low score or not."
1039   (< gnus-score gnus-summary-default-score))
1040
1041 (defun gnus-agent-high-scored-p ()
1042   "Say whether an article has a high score or not."
1043   (> gnus-score gnus-summary-default-score))
1044
1045 (defun gnus-category-make-function (cat)
1046   "Make a function from category CAT."
1047   `(lambda () ,(gnus-category-make-function-1 cat)))
1048
1049 (defun gnus-agent-true ()
1050   "Return t."
1051   t)
1052
1053 (defun gnus-agent-false ()
1054   "Return nil."
1055   nil)
1056   
1057 (defun gnus-category-make-function-1 (cat)
1058   "Make a function from category CAT."
1059   (cond
1060    ;; Functions are just returned as is.
1061    ((or (symbolp cat)
1062         (gnus-functionp cat))
1063     `(,(or (cdr (assq cat gnus-category-predicate-alist))
1064            cat)))
1065    ;; More complex category.
1066    ((consp cat)
1067     `(,(cond
1068         ((memq (car cat) '(& and))
1069          'and)
1070         ((memq (car cat) '(| or))
1071          'or)
1072         ((memq (car cat) gnus-category-not)
1073          'not))
1074       ,@(mapcar 'gnus-category-make-function-1 (cdr cat))))
1075    (t
1076     (error "Unknown category type: %s" cat))))
1077
1078 (defun gnus-get-predicate (predicate)
1079   "Return the predicate for CATEGORY."
1080   (or (cdr (assoc predicate gnus-category-predicate-cache))
1081       (cdar (push (cons predicate
1082                         (gnus-category-make-function predicate))
1083                   gnus-category-predicate-cache))))
1084
1085 (defun gnus-group-category (group)
1086   "Return the category GROUP belongs to."
1087   (unless gnus-category-group-cache
1088     (setq gnus-category-group-cache (gnus-make-hashtable 1000))
1089     (let ((cs gnus-category-alist)
1090           groups cat)
1091       (while (setq cat (pop cs))
1092         (setq groups (cadddr cat))
1093         (while groups
1094           (gnus-sethash (pop groups) cat gnus-category-group-cache)))))
1095   (or (gnus-gethash group gnus-category-group-cache)
1096       (assq 'default gnus-category-alist)))
1097
1098 (defun gnus-agent-expire ()
1099   "Expire all old articles."
1100   (interactive)
1101   (let ((methods gnus-agent-covered-methods)
1102         (alist (cdr gnus-newsrc-alist))
1103         gnus-command-method ofiles info method file group)
1104     (while (setq gnus-command-method (pop methods))
1105       (setq ofiles (nconc ofiles (gnus-agent-expire-directory
1106                                   (gnus-agent-directory)))))
1107     (while (setq info (pop alist))
1108       (when (and (gnus-agent-method-p
1109                   (setq gnus-command-method
1110                         (gnus-find-method-for-group
1111                          (setq group (gnus-info-group info)))))
1112                  (member
1113                   (setq file
1114                         (concat
1115                          (gnus-agent-directory)
1116                          (gnus-agent-group-path group) "/.overview"))
1117                   ofiles))
1118         (setq ofiles (delete file ofiles))
1119         (gnus-agent-expire-group file group)))
1120     (while ofiles
1121       (gnus-agent-expire-group (pop ofiles)))))
1122
1123 (defun gnus-agent-expire-directory (dir)
1124   "Expire all groups in DIR recursively."
1125   (when (file-directory-p dir)
1126     (let ((files (directory-files dir t))
1127           file ofiles)
1128       (while (setq file (pop files))
1129         (cond
1130          ((member (file-name-nondirectory file) '("." ".."))
1131           ;; Do nothing.
1132           )
1133          ((file-directory-p file)
1134           ;; Recurse.
1135           (setq ofiles (nconc ofiles (gnus-agent-expire-directory file))))
1136          ((string-match "\\.overview$" file)
1137           ;; Expire group.
1138           (push file ofiles))))
1139       ofiles)))
1140
1141 (defun gnus-agent-expire-group (overview &optional group)
1142   "Expire articles in OVERVIEW."
1143   (gnus-message 5 "Expiring %s..." overview)
1144   (let ((odate (- (gnus-time-to-day (current-time)) 4))
1145         (dir (file-name-directory overview))
1146         (info (when group (gnus-get-info group)))
1147         headers article file point unreads)
1148     (gnus-agent-load-alist nil dir)
1149     (when info
1150       (setq unreads
1151             (nconc
1152              (gnus-list-of-unread-articles group)
1153              (gnus-uncompress-range
1154               (cdr (assq 'tick (gnus-info-marks info))))
1155              (gnus-uncompress-range
1156               (cdr (assq 'dormant (gnus-info-marks info)))))))
1157     (nnheader-temp-write overview
1158       (insert-file-contents overview)
1159       (goto-char (point-min))
1160       (while (not (eobp))
1161         (setq point (point))
1162         (condition-case ()
1163             (setq headers (inline (nnheader-parse-nov)))
1164           (error
1165            (goto-char point)
1166            (gnus-delete-line)
1167            (setq headers nil)))
1168         (when headers
1169           (unless (memq (setq article (mail-header-number headers)) unreads)
1170             (if (not (< (inline
1171                           (gnus-time-to-day
1172                            (inline (nnmail-date-to-time
1173                                     (mail-header-date headers)))))
1174                         odate))
1175                 (forward-line 1)              
1176               (gnus-delete-line)
1177               (setq gnus-agent-article-alist
1178                     (delq (assq article gnus-agent-article-alist)
1179                           gnus-agent-article-alist))
1180               (when (file-exists-p
1181                      (setq file (concat dir (number-to-string article))))
1182                 (delete-file file))))))
1183       (gnus-agent-save-alist nil nil nil dir))))
1184    
1185 (provide 'gnus-agent)
1186
1187 ;;; gnus-agent.el ends here