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