1 ;;; gnus-agent.el --- unplugged support for Gnus
2 ;; Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002, 2003
3 ;; Free Software Foundation, Inc.
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6 ;; This file is part of GNU Emacs.
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)
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.
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.
36 (if (featurep 'xemacs)
42 (autoload 'gnus-server-update-server "gnus-srvr")
43 (autoload 'gnus-agent-customize-category "gnus-cus")
46 (defcustom gnus-agent-directory (nnheader-concat gnus-directory "agent/")
47 "Where the Gnus agent will store its files."
51 (defcustom gnus-agent-plugged-hook nil
52 "Hook run when plugging into the network."
56 (defcustom gnus-agent-unplugged-hook nil
57 "Hook run when unplugging from the network."
61 (defcustom gnus-agent-fetched-hook nil
62 "Hook run when finished fetching articles."
66 (defcustom gnus-agent-handle-level gnus-level-subscribed
67 "Groups on levels higher than this variable will be ignored by the Agent."
71 (defcustom gnus-agent-expire-days 7
72 "Read articles older than this will be expired.
73 If you wish to disable Agent expiring, see `gnus-agent-enable-expiration'."
75 :type '(number :tag "days"))
77 (defcustom gnus-agent-expire-all nil
78 "If non-nil, also expire unread, ticked and dormant articles.
79 If nil, only read articles will be expired."
83 (defcustom gnus-agent-group-mode-hook nil
84 "Hook run in Agent group minor modes."
88 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
89 (when (featurep 'xemacs)
90 (add-hook 'gnus-agent-group-mode-hook 'gnus-xmas-agent-group-menu-add))
92 (defcustom gnus-agent-summary-mode-hook nil
93 "Hook run in Agent summary minor modes."
97 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
98 (when (featurep 'xemacs)
99 (add-hook 'gnus-agent-summary-mode-hook 'gnus-xmas-agent-summary-menu-add))
101 (defcustom gnus-agent-server-mode-hook nil
102 "Hook run in Agent summary minor modes."
106 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
107 (when (featurep 'xemacs)
108 (add-hook 'gnus-agent-server-mode-hook 'gnus-xmas-agent-server-menu-add))
110 (defcustom gnus-agent-confirmation-function 'y-or-n-p
111 "Function to confirm when error happens."
116 (defcustom gnus-agent-synchronize-flags 'ask
117 "Indicate if flags are synchronized when you plug in.
118 If this is `ask' the hook will query the user."
120 :type '(choice (const :tag "Always" t)
121 (const :tag "Never" nil)
122 (const :tag "Ask" ask))
125 (defcustom gnus-agent-go-online 'ask
126 "Indicate if offline servers go online when you plug in.
127 If this is `ask' the hook will query the user."
129 :type '(choice (const :tag "Always" t)
130 (const :tag "Never" nil)
131 (const :tag "Ask" ask))
134 (defcustom gnus-agent-mark-unread-after-downloaded t
135 "Indicate whether to mark articles unread after downloaded."
140 (defcustom gnus-agent-download-marks '(download)
141 "Marks for downloading."
143 :type '(repeat (symbol :tag "Mark"))
146 (defcustom gnus-agent-consider-all-articles nil
147 "When non-`nil', the agent will let the agent predicate decide
148 whether articles need to be downloaded or not, for all articles. When
149 `nil', the default, the agent will only let the predicate decide
150 whether unread articles are downloaded or not. If you enable this,
151 groups with large active ranges may open slower and you may also want
152 to look into the agent expiry settings to block the expiration of
153 read articles as they would just be downloaded again."
158 (defcustom gnus-agent-max-fetch-size 10000000 ;; 10 Mb
159 "Chunk size for `gnus-agent-fetch-session'.
160 The function will split its article fetches into chunks smaller than
165 (defcustom gnus-agent-enable-expiration 'ENABLE
166 "The default expiration state for each group.
167 When set to ENABLE, the default, `gnus-agent-expire' will expire old
168 contents from a group's local storage. This value may be overridden
169 to disable expiration in specific categories, topics, and groups. Of
170 course, you could change gnus-agent-enable-expiration to DISABLE then
171 enable expiration per categories, topics, and groups."
173 :type '(radio (const :format "Enable " ENABLE)
174 (const :format "Disable " DISABLE)))
176 (defcustom gnus-agent-expire-unagentized-dirs t
177 "*Whether expiration should expire in unagentized directories.
178 Have gnus-agent-expire scan the directories under
179 \(gnus-agent-directory) for groups that are no longer agentized.
180 When found, offer to remove them."
184 (defcustom gnus-agent-auto-agentize-methods '(nntp nnimap)
185 "Initially, all servers from these methods are agentized.
186 The user may remove or add servers using the Server buffer.
187 See Info node `(gnus)Server Buffer'."
188 :type '(repeat symbol)
191 ;;; Internal variables
193 (defvar gnus-agent-history-buffers nil)
194 (defvar gnus-agent-buffer-alist nil)
195 (defvar gnus-agent-article-alist nil
196 "An assoc list identifying the articles whose headers have been fetched.
197 If successfully fetched, these headers will be stored in the group's overview
198 file. The key of each assoc pair is the article ID, the value of each assoc
199 pair is a flag indicating whether the identified article has been downloaded
200 \(gnus-agent-fetch-articles sets the value to the day of the download).
202 1) The last element of this list can not be expired as some
203 routines (for example, get-agent-fetch-headers) use the last
204 value to track which articles have had their headers retrieved.
205 2) The function `gnus-agent-regenerate' may destructively modify the value.")
206 (defvar gnus-agent-group-alist nil)
207 (defvar gnus-category-alist nil)
208 (defvar gnus-agent-current-history nil)
209 (defvar gnus-agent-overview-buffer nil)
210 (defvar gnus-category-predicate-cache nil)
211 (defvar gnus-category-group-cache nil)
212 (defvar gnus-agent-spam-hashtb nil)
213 (defvar gnus-agent-file-name nil)
214 (defvar gnus-agent-send-mail-function nil)
215 (defvar gnus-agent-file-coding-system 'raw-text)
216 (defvar gnus-agent-file-loading-cache nil)
217 (defvar gnus-agent-file-header-cache nil)
220 (defvar gnus-headers)
227 (defun gnus-open-agent ()
229 (gnus-agent-read-servers)
231 (gnus-agent-create-buffer)
232 (add-hook 'gnus-group-mode-hook 'gnus-agent-mode)
233 (add-hook 'gnus-summary-mode-hook 'gnus-agent-mode)
234 (add-hook 'gnus-server-mode-hook 'gnus-agent-mode))
236 (defun gnus-agent-create-buffer ()
237 (if (gnus-buffer-live-p gnus-agent-overview-buffer)
239 (setq gnus-agent-overview-buffer
240 (gnus-get-buffer-create " *Gnus agent overview*"))
241 (with-current-buffer gnus-agent-overview-buffer
242 (mm-enable-multibyte))
245 (gnus-add-shutdown 'gnus-close-agent 'gnus)
247 (defun gnus-close-agent ()
248 (setq gnus-category-predicate-cache nil
249 gnus-category-group-cache nil
250 gnus-agent-spam-hashtb nil)
251 (gnus-kill-buffer gnus-agent-overview-buffer))
254 ;;; Utility functions
257 (defun gnus-agent-read-file (file)
258 "Load FILE and do a `read' there."
261 (nnheader-insert-file-contents file)
262 (goto-char (point-min))
263 (read (current-buffer)))))
265 (defsubst gnus-agent-method ()
266 (concat (symbol-name (car gnus-command-method)) "/"
267 (if (equal (cadr gnus-command-method) "")
269 (cadr gnus-command-method))))
271 (defsubst gnus-agent-directory ()
272 "The name of the Gnus agent directory."
273 (nnheader-concat gnus-agent-directory
274 (nnheader-translate-file-chars (gnus-agent-method)) "/"))
276 (defun gnus-agent-lib-file (file)
277 "The full name of the Gnus agent library FILE."
278 (expand-file-name file
279 (file-name-as-directory
280 (expand-file-name "agent.lib" (gnus-agent-directory)))))
282 (defun gnus-agent-cat-set-property (category property value)
284 (setcdr (or (assq property category)
285 (let ((cell (cons property nil)))
286 (setcdr category (cons cell (cdr category)))
288 (let ((category category))
289 (while (cond ((eq property (caadr category))
290 (setcdr category (cddr category))
293 (setq category (cdr category)))))))
297 (defmacro gnus-agent-cat-defaccessor (name prop-name)
298 "Define accessor and setter methods for manipulating a list of the form
299 \(NAME (PROPERTY1 VALUE1) ... (PROPERTY_N VALUE_N)).
300 Given the call (gnus-agent-cat-defaccessor func PROPERTY1), the list may be
301 manipulated as follows:
302 (func LIST): Returns VALUE1
303 (setf (func LIST) NEW_VALUE1): Replaces VALUE1 with NEW_VALUE1."
304 `(progn (defmacro ,name (category)
305 (list (quote cdr) (list (quote assq)
306 (quote (quote ,prop-name)) category)))
308 (define-setf-method ,name (category)
309 (let* ((--category--temp-- (make-symbol "--category--"))
310 (--value--temp-- (make-symbol "--value--")))
311 (list (list --category--temp--) ; temporary-variables
312 (list category) ; value-forms
313 (list --value--temp--) ; store-variables
314 (let* ((category --category--temp--) ; store-form
315 (value --value--temp--))
316 (list (quote gnus-agent-cat-set-property)
318 (quote (quote ,prop-name))
320 (list (quote ,name) --category--temp--) ; access-form
324 (defmacro gnus-agent-cat-name (category)
327 (gnus-agent-cat-defaccessor
328 gnus-agent-cat-days-until-old agent-days-until-old)
329 (gnus-agent-cat-defaccessor
330 gnus-agent-cat-enable-expiration agent-enable-expiration)
331 (gnus-agent-cat-defaccessor
332 gnus-agent-cat-groups agent-groups)
333 (gnus-agent-cat-defaccessor
334 gnus-agent-cat-high-score agent-high-score)
335 (gnus-agent-cat-defaccessor
336 gnus-agent-cat-length-when-long agent-length-when-long)
337 (gnus-agent-cat-defaccessor
338 gnus-agent-cat-length-when-short agent-length-when-short)
339 (gnus-agent-cat-defaccessor
340 gnus-agent-cat-low-score agent-low-score)
341 (gnus-agent-cat-defaccessor
342 gnus-agent-cat-predicate agent-predicate)
343 (gnus-agent-cat-defaccessor
344 gnus-agent-cat-score-file agent-score-file)
345 (gnus-agent-cat-defaccessor
346 gnus-agent-cat-disable-undownloaded-faces agent-disable-undownloaded-faces)
349 (defsetf gnus-agent-cat-groups (category) (groups)
350 (list 'gnus-agent-set-cat-groups category groups)))
352 (defun gnus-agent-set-cat-groups (category groups)
353 (unless (eq groups 'ignore)
355 (old-g (gnus-agent-cat-groups category)))
356 (cond ((eq new-g old-g)
357 ;; gnus-agent-add-group is fiddling with the group
358 ;; list. Still, Im done.
361 ((eq new-g (cdr old-g))
362 ;; gnus-agent-add-group is fiddling with the group list
363 (setcdr (or (assq 'agent-groups category)
364 (let ((cell (cons 'agent-groups nil)))
365 (setcdr category (cons cell (cdr category)))
368 (let ((groups groups))
370 (let* ((group (pop groups))
371 (old-category (gnus-group-category group)))
372 (if (eq category old-category)
374 (setf (gnus-agent-cat-groups old-category)
375 (delete group (gnus-agent-cat-groups
377 ;; Purge cache as preceeding loop invalidated it.
378 (setq gnus-category-group-cache nil))
380 (setcdr (or (assq 'agent-groups category)
381 (let ((cell (cons 'agent-groups nil)))
382 (setcdr category (cons cell (cdr category)))
385 (defsubst gnus-agent-cat-make (name &optional default-agent-predicate)
386 (list name `(agent-predicate . ,(or default-agent-predicate 'false))))
388 ;;; Fetching setup functions.
390 (defun gnus-agent-start-fetch ()
391 "Initialize data structures for efficient fetching."
392 (gnus-agent-create-buffer))
394 (defun gnus-agent-stop-fetch ()
395 "Save all data structures and clean up."
396 (setq gnus-agent-spam-hashtb nil)
398 (set-buffer nntp-server-buffer)
401 (defmacro gnus-agent-with-fetch (&rest forms)
404 (let ((gnus-agent-fetching t))
405 (gnus-agent-start-fetch)
407 (gnus-agent-stop-fetch)))
409 (put 'gnus-agent-with-fetch 'lisp-indent-function 0)
410 (put 'gnus-agent-with-fetch 'edebug-form-spec '(body))
412 (defmacro gnus-agent-append-to-list (tail value)
413 `(setq ,tail (setcdr ,tail (cons ,value nil))))
415 (defmacro gnus-agent-message (level &rest args)
416 `(if (<= ,level gnus-verbose)
423 (defvar gnus-agent-mode-hook nil
424 "Hook run when installing agent mode.")
426 (defvar gnus-agent-mode nil)
427 (defvar gnus-agent-mode-status '(gnus-agent-mode " Plugged"))
429 (defun gnus-agent-mode ()
430 "Minor mode for providing a agent support in Gnus buffers."
431 (let* ((buffer (progn (string-match "^gnus-\\(.*\\)-mode$"
432 (symbol-name major-mode))
433 (match-string 1 (symbol-name major-mode))))
434 (mode (intern (format "gnus-agent-%s-mode" buffer))))
435 (set (make-local-variable 'gnus-agent-mode) t)
437 (set (make-local-variable mode) t)
439 (when (gnus-visual-p 'agent-menu 'menu)
440 (funcall (intern (format "gnus-agent-%s-make-menu-bar" buffer))))
441 (unless (assq 'gnus-agent-mode minor-mode-alist)
442 (push gnus-agent-mode-status minor-mode-alist))
443 (unless (assq mode minor-mode-map-alist)
444 (push (cons mode (symbol-value (intern (format "gnus-agent-%s-mode-map"
446 minor-mode-map-alist))
447 (when (eq major-mode 'gnus-group-mode)
448 (let ((init-plugged gnus-plugged)
449 (gnus-agent-go-online nil))
450 ;; g-a-t-p does nothing when gnus-plugged isn't changed.
451 ;; Therefore, make certain that the current value does not
452 ;; match the desired initial value.
453 (setq gnus-plugged :unknown)
454 (gnus-agent-toggle-plugged init-plugged)))
455 (gnus-run-hooks 'gnus-agent-mode-hook
456 (intern (format "gnus-agent-%s-mode-hook" buffer)))))
458 (defvar gnus-agent-group-mode-map (make-sparse-keymap))
459 (gnus-define-keys gnus-agent-group-mode-map
460 "Ju" gnus-agent-fetch-groups
461 "Jc" gnus-enter-category-buffer
462 "Jj" gnus-agent-toggle-plugged
463 "Js" gnus-agent-fetch-session
464 "JY" gnus-agent-synchronize-flags
465 "JS" gnus-group-send-queue
466 "Ja" gnus-agent-add-group
467 "Jr" gnus-agent-remove-group
468 "Jo" gnus-agent-toggle-group-plugged)
470 (defun gnus-agent-group-make-menu-bar ()
471 (unless (boundp 'gnus-agent-group-menu)
473 gnus-agent-group-menu gnus-agent-group-mode-map ""
475 ["Toggle plugged" gnus-agent-toggle-plugged t]
476 ["Toggle group plugged" gnus-agent-toggle-group-plugged t]
477 ["List categories" gnus-enter-category-buffer t]
478 ["Add (current) group to category" gnus-agent-add-group t]
479 ["Remove (current) group from category" gnus-agent-remove-group t]
480 ["Send queue" gnus-group-send-queue gnus-plugged]
482 ["All" gnus-agent-fetch-session gnus-plugged]
483 ["Group" gnus-agent-fetch-group gnus-plugged])
484 ["Synchronize flags" gnus-agent-synchronize-flags t]
487 (defvar gnus-agent-summary-mode-map (make-sparse-keymap))
488 (gnus-define-keys gnus-agent-summary-mode-map
489 "Jj" gnus-agent-toggle-plugged
490 "Ju" gnus-agent-summary-fetch-group
491 "JS" gnus-agent-fetch-group
492 "Js" gnus-agent-summary-fetch-series
493 "J#" gnus-agent-mark-article
494 "J\M-#" gnus-agent-unmark-article
495 "@" gnus-agent-toggle-mark
496 "Jc" gnus-agent-catchup)
498 (defun gnus-agent-summary-make-menu-bar ()
499 (unless (boundp 'gnus-agent-summary-menu)
501 gnus-agent-summary-menu gnus-agent-summary-mode-map ""
503 ["Toggle plugged" gnus-agent-toggle-plugged t]
504 ["Mark as downloadable" gnus-agent-mark-article t]
505 ["Unmark as downloadable" gnus-agent-unmark-article t]
506 ["Toggle mark" gnus-agent-toggle-mark t]
507 ["Fetch downloadable" gnus-agent-summary-fetch-group t]
508 ["Catchup undownloaded" gnus-agent-catchup t]))))
510 (defvar gnus-agent-server-mode-map (make-sparse-keymap))
511 (gnus-define-keys gnus-agent-server-mode-map
512 "Jj" gnus-agent-toggle-plugged
513 "Ja" gnus-agent-add-server
514 "Jr" gnus-agent-remove-server)
516 (defun gnus-agent-server-make-menu-bar ()
517 (unless (boundp 'gnus-agent-server-menu)
519 gnus-agent-server-menu gnus-agent-server-mode-map ""
521 ["Toggle plugged" gnus-agent-toggle-plugged t]
522 ["Add" gnus-agent-add-server t]
523 ["Remove" gnus-agent-remove-server t]))))
525 (defun gnus-agent-make-mode-line-string (string mouse-button mouse-func)
526 (if (and (fboundp 'propertize)
527 (fboundp 'make-mode-line-mouse-map))
528 (propertize string 'local-map
529 (make-mode-line-mouse-map mouse-button mouse-func))
532 (defun gnus-agent-toggle-plugged (set-to)
533 "Toggle whether Gnus is unplugged or not."
534 (interactive (list (not gnus-plugged)))
535 (cond ((eq set-to gnus-plugged)
538 (setq gnus-plugged set-to)
539 (gnus-run-hooks 'gnus-agent-plugged-hook)
540 (setcar (cdr gnus-agent-mode-status)
541 (gnus-agent-make-mode-line-string " Plugged"
543 'gnus-agent-toggle-plugged))
544 (gnus-agent-go-online gnus-agent-go-online)
545 (gnus-agent-possibly-synchronize-flags))
547 (gnus-agent-close-connections)
548 (setq gnus-plugged set-to)
549 (gnus-run-hooks 'gnus-agent-unplugged-hook)
550 (setcar (cdr gnus-agent-mode-status)
551 (gnus-agent-make-mode-line-string " Unplugged"
553 'gnus-agent-toggle-plugged))))
554 (set-buffer-modified-p t))
556 (defmacro gnus-agent-while-plugged (&rest body)
557 `(let ((original-gnus-plugged gnus-plugged))
559 (progn (gnus-agent-toggle-plugged t)
561 (gnus-agent-toggle-plugged original-gnus-plugged))))
563 (put 'gnus-agent-while-plugged 'lisp-indent-function 0)
564 (put 'gnus-agent-while-plugged 'edebug-form-spec '(body))
566 (defun gnus-agent-close-connections ()
567 "Close all methods covered by the Gnus agent."
568 (let ((methods (gnus-agent-covered-methods)))
570 (gnus-close-server (pop methods)))))
573 (defun gnus-unplugged ()
574 "Start Gnus unplugged."
576 (setq gnus-plugged nil)
580 (defun gnus-plugged ()
581 "Start Gnus plugged."
583 (setq gnus-plugged t)
587 (defun gnus-slave-unplugged (&optional arg)
588 "Read news as a slave unplugged."
590 (setq gnus-plugged nil)
591 (gnus arg nil 'slave))
594 (defun gnus-agentize ()
595 "Allow Gnus to be an offline newsreader.
597 The gnus-agentize function is now called internally by gnus when
598 gnus-agent is set. If you wish to avoid calling gnus-agentize,
599 customize gnus-agent to nil.
601 This will modify the `gnus-setup-news-hook', and
602 `message-send-mail-real-function' variables, and install the Gnus agent
603 minor mode in all Gnus buffers."
606 (add-hook 'gnus-setup-news-hook 'gnus-agent-queue-setup)
607 (unless gnus-agent-send-mail-function
608 (setq gnus-agent-send-mail-function
609 (or message-send-mail-real-function
610 message-send-mail-function)
611 message-send-mail-real-function 'gnus-agent-send-mail))
613 ;; If the servers file doesn't exist, auto-agentize some servers and
614 ;; save the servers file so this auto-agentizing isn't invoked
616 (unless (file-exists-p (nnheader-concat gnus-agent-directory "lib/servers"))
617 (gnus-message 3 "First time agent user, agentizing remote groups...")
619 (lambda (server-or-method)
620 (let ((method (gnus-server-to-method server-or-method)))
621 (when (memq (car method)
622 gnus-agent-auto-agentize-methods)
623 (push (gnus-method-to-server method)
624 gnus-agent-covered-methods)
625 (setq gnus-agent-method-p-cache nil))))
626 (cons gnus-select-method gnus-secondary-select-methods))
627 (gnus-agent-write-servers)))
629 (defun gnus-agent-queue-setup (&optional group-name)
630 "Make sure the queue group exists.
631 Optional arg GROUP-NAME allows to specify another group."
632 (unless (gnus-gethash (format "nndraft:%s" (or group-name "queue"))
634 (gnus-request-create-group (or group-name "queue") '(nndraft ""))
635 (let ((gnus-level-default-subscribed 1))
636 (gnus-subscribe-group (format "nndraft:%s" (or group-name "queue"))
638 (gnus-group-set-parameter
639 (format "nndraft:%s" (or group-name "queue"))
640 'gnus-dummy '((gnus-draft-mode)))))
642 (defun gnus-agent-send-mail ()
644 (funcall gnus-agent-send-mail-function)
645 (goto-char (point-min))
647 (concat "^" (regexp-quote mail-header-separator) "\n"))
649 (gnus-agent-insert-meta-information 'mail)
650 (gnus-request-accept-article "nndraft:queue" nil t t)))
652 (defun gnus-agent-insert-meta-information (type &optional method)
653 "Insert meta-information into the message that says how it's to be posted.
654 TYPE can be either `mail' or `news'. If the latter, then METHOD can
657 (message-remove-header gnus-agent-meta-information-header)
658 (goto-char (point-min))
659 (insert gnus-agent-meta-information-header ": "
660 (symbol-name type) " " (format "%S" method)
663 (while (search-backward "\n" nil t)
664 (replace-match "\\n" t t))))
666 (defun gnus-agent-restore-gcc ()
667 "Restore GCC field from saved header."
669 (goto-char (point-min))
670 (while (re-search-forward (concat gnus-agent-gcc-header ":") nil t)
671 (replace-match "Gcc:" 'fixedcase))))
673 (defun gnus-agent-any-covered-gcc ()
675 (message-narrow-to-headers)
676 (let* ((gcc (mail-fetch-field "gcc" nil t))
678 (mapcar 'gnus-inews-group-method
679 (message-unquote-tokens
680 (message-tokenize-header
683 (while (and (not covered) methods)
684 (setq covered (gnus-agent-method-p (car methods))
685 methods (cdr methods)))
689 (defun gnus-agent-possibly-save-gcc ()
690 "Save GCC if Gnus is unplugged."