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