(rfc2047-encode-region): Don't error out on invalid
[gnus] / lisp / gnus-agent.el
1 ;;; gnus-agent.el --- unplugged support for Gnus
2 ;; Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002, 2003
3 ;;        Free Software Foundation, Inc.
4
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
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 'nnmail)
30 (require 'nnvirtual)
31 (require 'gnus-sum)
32 (require 'gnus-score)
33 (require 'gnus-srvr)
34 (require 'gnus-util)
35 (eval-when-compile
36   (if (featurep 'xemacs)
37       (require 'itimer)
38     (require 'timer))
39   (require 'cl))
40
41 (eval-and-compile
42   (autoload 'gnus-server-update-server "gnus-srvr")
43   (autoload 'gnus-agent-customize-category "gnus-cus")
44 )
45
46 (defcustom gnus-agent-directory (nnheader-concat gnus-directory "agent/")
47   "Where the Gnus agent will store its files."
48   :group 'gnus-agent
49   :type 'directory)
50
51 (defcustom gnus-agent-plugged-hook nil
52   "Hook run when plugging into the network."
53   :group 'gnus-agent
54   :type 'hook)
55
56 (defcustom gnus-agent-unplugged-hook nil
57   "Hook run when unplugging from the network."
58   :group 'gnus-agent
59   :type 'hook)
60
61 (defcustom gnus-agent-fetched-hook nil
62   "Hook run when finished fetching articles."
63   :group 'gnus-agent
64   :type 'hook)
65
66 (defcustom gnus-agent-handle-level gnus-level-subscribed
67   "Groups on levels higher than this variable will be ignored by the Agent."
68   :group 'gnus-agent
69   :type 'integer)
70
71 (defcustom gnus-agent-expire-days 7
72   "Read articles older than this will be expired.
73 This can also be a list of regexp/day pairs.  The regexps will be
74 matched against group names."
75   :group 'gnus-agent
76   :type '(choice (number :tag "days")
77                  (sexp :tag "List" nil)))
78
79 (defcustom gnus-agent-expire-all nil
80   "If non-nil, also expire unread, ticked and dormant articles.
81 If nil, only read articles will be expired."
82   :group 'gnus-agent
83   :type 'boolean)
84
85 (defcustom gnus-agent-group-mode-hook nil
86   "Hook run in Agent group minor modes."
87   :group 'gnus-agent
88   :type 'hook)
89
90 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
91 (when (featurep 'xemacs)
92   (add-hook 'gnus-agent-group-mode-hook 'gnus-xmas-agent-group-menu-add))
93
94 (defcustom gnus-agent-summary-mode-hook nil
95   "Hook run in Agent summary minor modes."
96   :group 'gnus-agent
97   :type 'hook)
98
99 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
100 (when (featurep 'xemacs)
101   (add-hook 'gnus-agent-summary-mode-hook 'gnus-xmas-agent-summary-menu-add))
102
103 (defcustom gnus-agent-server-mode-hook nil
104   "Hook run in Agent summary minor modes."
105   :group 'gnus-agent
106   :type 'hook)
107
108 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
109 (when (featurep 'xemacs)
110   (add-hook 'gnus-agent-server-mode-hook 'gnus-xmas-agent-server-menu-add))
111
112 (defcustom gnus-agent-confirmation-function 'y-or-n-p
113   "Function to confirm when error happens."
114   :version "21.1"
115   :group 'gnus-agent
116   :type 'function)
117
118 (defcustom gnus-agent-synchronize-flags 'ask
119   "Indicate if flags are synchronized when you plug in.
120 If this is `ask' the hook will query the user."
121   :version "21.1"
122   :type '(choice (const :tag "Always" t)
123                  (const :tag "Never" nil)
124                  (const :tag "Ask" ask))
125   :group 'gnus-agent)
126
127 (defcustom gnus-agent-go-online 'ask
128   "Indicate if offline servers go online when you plug in.
129 If this is `ask' the hook will query the user."
130   :version "21.1"
131   :type '(choice (const :tag "Always" t)
132                  (const :tag "Never" nil)
133                  (const :tag "Ask" ask))
134   :group 'gnus-agent)
135
136 (defcustom gnus-agent-mark-unread-after-downloaded t
137   "Indicate whether to mark articles unread after downloaded."
138   :version "21.1"
139   :type 'boolean
140   :group 'gnus-agent)
141
142 (defcustom gnus-agent-download-marks '(download)
143   "Marks for downloading."
144   :version "21.1"
145   :type '(repeat (symbol :tag "Mark"))
146   :group 'gnus-agent)
147
148 (defcustom gnus-agent-consider-all-articles nil
149   "If non-nil, consider also the read articles for downloading."
150   :version "21.4"
151   :type 'boolean
152   :group 'gnus-agent)
153
154 (defcustom gnus-agent-max-fetch-size 10000000 ;; 10 Mb
155   "Chunk size for `gnus-agent-fetch-session'.
156 The function will split its article fetches into chunks smaller than
157 this limit."
158   :group 'gnus-agent
159   :type 'integer)
160
161 (defcustom gnus-agent-enable-expiration 'ENABLE
162   "The default expiration state for each group.
163 When set to ENABLE, the default, `gnus-agent-expire' will expire old
164 contents from a group's local storage.  This value may be overridden
165 to disable expiration in specific categories, topics, and groups.  Of
166 course, you could change gnus-agent-enable-expiration to DISABLE then
167 enable expiration per categories, topics, and groups."
168   :group 'gnus-agent
169   :type '(radio (const :format "Enable " ENABLE)
170                 (const :format "Disable " DISABLE)))
171
172 (defcustom gnus-agent-expire-unagentized-dirs t
173 "Have gnus-agent-expire scan the directories under
174 \(gnus-agent-directory) for groups that are no longer agentized.  When
175 found, offer to remove them.")
176
177 ;;; Internal variables
178
179 (defvar gnus-agent-history-buffers nil)
180 (defvar gnus-agent-buffer-alist nil)
181 (defvar gnus-agent-article-alist nil
182   "An assoc list identifying the articles whose headers have been fetched.  
183 If successfully fetched, these headers will be stored in the group's overview
184 file.  The key of each assoc pair is the article ID, the value of each assoc
185 pair is a flag indicating whether the identified article has been downloaded
186 \(gnus-agent-fetch-articles sets the value to the day of the download).
187 NOTES:
188 1) The last element of this list can not be expired as some 
189    routines (for example, get-agent-fetch-headers) use the last
190    value to track which articles have had their headers retrieved.
191 2) The function `gnus-agent-regenerate' may destructively modify the value.")
192 (defvar gnus-agent-group-alist nil)
193 (defvar gnus-category-alist nil)
194 (defvar gnus-agent-current-history nil)
195 (defvar gnus-agent-overview-buffer nil)
196 (defvar gnus-category-predicate-cache nil)
197 (defvar gnus-category-group-cache nil)
198 (defvar gnus-agent-spam-hashtb nil)
199 (defvar gnus-agent-file-name nil)
200 (defvar gnus-agent-send-mail-function nil)
201 (defvar gnus-agent-file-coding-system 'raw-text)
202 (defvar gnus-agent-file-loading-cache nil)
203 (defvar gnus-agent-file-header-cache nil)
204
205 (defvar gnus-agent-auto-agentize-methods '(nntp nnimap)
206   "Initially, all servers from these methods are agentized.
207 The user may remove or add servers using the Server buffer.  See Info
208 node `(gnus)Server Buffer'.")
209
210 ;; Dynamic variables
211 (defvar gnus-headers)
212 (defvar gnus-score)
213
214 ;;;
215 ;;; Setup
216 ;;;
217
218 (defun gnus-open-agent ()
219   (setq gnus-agent t)
220   (gnus-agent-read-servers)
221   (gnus-category-read)
222   (gnus-agent-create-buffer)
223   (add-hook 'gnus-group-mode-hook 'gnus-agent-mode)
224   (add-hook 'gnus-summary-mode-hook 'gnus-agent-mode)
225   (add-hook 'gnus-server-mode-hook 'gnus-agent-mode))
226
227 (defun gnus-agent-create-buffer ()
228   (if (gnus-buffer-live-p gnus-agent-overview-buffer)
229       t
230     (setq gnus-agent-overview-buffer
231           (gnus-get-buffer-create " *Gnus agent overview*"))
232     (with-current-buffer gnus-agent-overview-buffer
233       (mm-enable-multibyte))
234     nil))
235
236 (gnus-add-shutdown 'gnus-close-agent 'gnus)
237
238 (defun gnus-close-agent ()
239   (setq gnus-category-predicate-cache nil
240         gnus-category-group-cache nil
241         gnus-agent-spam-hashtb nil)
242   (gnus-kill-buffer gnus-agent-overview-buffer))
243
244 ;;;
245 ;;; Utility functions
246 ;;;
247
248 (defun gnus-agent-read-file (file)
249   "Load FILE and do a `read' there."
250   (with-temp-buffer
251     (ignore-errors
252       (nnheader-insert-file-contents file)
253       (goto-char (point-min))
254       (read (current-buffer)))))
255
256 (defsubst gnus-agent-method ()
257   (concat (symbol-name (car gnus-command-method)) "/"
258           (if (equal (cadr gnus-command-method) "")
259               "unnamed"
260             (cadr gnus-command-method))))
261
262 (defsubst gnus-agent-directory ()
263   "The name of the Gnus agent directory."
264   (nnheader-concat gnus-agent-directory
265                    (nnheader-translate-file-chars (gnus-agent-method)) "/"))
266
267 (defun gnus-agent-lib-file (file)
268   "The full name of the Gnus agent library FILE."
269   (expand-file-name file
270                     (file-name-as-directory
271                      (expand-file-name "agent.lib" (gnus-agent-directory)))))
272
273 (defun gnus-agent-cat-set-property (category property value)
274   (if value
275       (setcdr (or (assq property category)
276               (let ((cell (cons property nil)))
277                     (setcdr category (cons cell (cdr category)))
278                     cell)) value)
279     (let ((category category))
280       (while (cond ((eq property (caadr category))
281                     (setcdr category (cddr category))
282                     nil)
283                    (t
284                     (setq category (cdr category)))))))
285   category)
286
287 (eval-when-compile
288   (defmacro gnus-agent-cat-defaccessor (name prop-name)
289     "Define accessor and setter methods for manipulating a list of the form
290 \(NAME (PROPERTY1 VALUE1) ... (PROPERTY_N VALUE_N)).
291 Given the call (gnus-agent-cat-defaccessor func PROPERTY1), the list may be
292 manipulated as follows:
293   (func LIST): Returns VALUE1
294   (setf (func LIST) NEW_VALUE1): Replaces VALUE1 with NEW_VALUE1."
295     `(progn (defmacro ,name (category)
296               (list (quote cdr) (list (quote assq)
297                                       (quote (quote ,prop-name)) category)))
298
299             (define-setf-method ,name (category)
300               (let* ((--category--temp-- (make-symbol "--category--"))
301                      (--value--temp-- (make-symbol "--value--")))
302                 (list (list --category--temp--) ; temporary-variables
303                       (list category)   ; value-forms
304                       (list --value--temp--) ; store-variables
305                       (let* ((category --category--temp--) ; store-form
306                              (value --value--temp--))
307                         (list (quote gnus-agent-cat-set-property)
308                               category
309                               (quote (quote ,prop-name))
310                               value))
311                       (list (quote ,name) --category--temp--) ; access-form
312                       )))))
313   )
314
315 (defmacro gnus-agent-cat-name (category)
316   `(car ,category))
317
318 (gnus-agent-cat-defaccessor
319  gnus-agent-cat-days-until-old             agent-days-until-old)
320 (gnus-agent-cat-defaccessor
321  gnus-agent-cat-enable-expiration          agent-enable-expiration)
322 (gnus-agent-cat-defaccessor
323  gnus-agent-cat-groups                     agent-groups)
324 (gnus-agent-cat-defaccessor
325  gnus-agent-cat-high-score                 agent-high-score)
326 (gnus-agent-cat-defaccessor
327  gnus-agent-cat-length-when-long           agent-length-when-long)
328 (gnus-agent-cat-defaccessor
329  gnus-agent-cat-length-when-short          agent-length-when-short)
330 (gnus-agent-cat-defaccessor
331  gnus-agent-cat-low-score                  agent-low-score)
332 (gnus-agent-cat-defaccessor
333  gnus-agent-cat-predicate                  agent-predicate)
334 (gnus-agent-cat-defaccessor
335  gnus-agent-cat-score-file                 agent-score-file)
336 (gnus-agent-cat-defaccessor
337  gnus-agent-cat-disable-undownloaded-faces agent-disable-undownloaded-faces)
338
339 (eval-when-compile
340   (defsetf gnus-agent-cat-groups (category) (groups)
341     (list 'gnus-agent-set-cat-groups category groups)))
342
343 (defun gnus-agent-set-cat-groups (category groups)
344   (unless (eq groups 'ignore)
345     (let ((new-g groups)
346           (old-g (gnus-agent-cat-groups category)))
347       (cond ((eq new-g old-g)
348              ;; gnus-agent-add-group is fiddling with the group
349              ;; list. Still, Im done.
350              nil
351              )
352             ((eq new-g (cdr old-g))
353              ;; gnus-agent-add-group is fiddling with the group list
354              (setcdr (or (assq 'agent-groups category)
355                          (let ((cell (cons 'agent-groups nil)))
356                            (setcdr category (cons cell (cdr category)))
357                            cell)) new-g))
358             (t
359              (let ((groups groups))
360                (while groups
361                  (let* ((group        (pop groups))
362                         (old-category (gnus-group-category group)))
363                    (if (eq category old-category)
364                        nil
365                      (setf (gnus-agent-cat-groups old-category)
366                            (delete group (gnus-agent-cat-groups
367                                           old-category))))))
368                ;; Purge cache as preceeding loop invalidated it.
369                (setq gnus-category-group-cache nil))
370
371              (setcdr (or (assq 'agent-groups category)
372                          (let ((cell (cons 'agent-groups nil)))
373                            (setcdr category (cons cell (cdr category)))
374                            cell)) groups))))))
375
376 (defsubst gnus-agent-cat-make (name &optional default-agent-predicate)
377   (list name `(agent-predicate . ,(or default-agent-predicate 'false))))
378
379 ;;; Fetching setup functions.
380
381 (defun gnus-agent-start-fetch ()
382   "Initialize data structures for efficient fetching."
383   (gnus-agent-create-buffer))
384
385 (defun gnus-agent-stop-fetch ()
386   "Save all data structures and clean up."
387   (setq gnus-agent-spam-hashtb nil)
388   (save-excursion
389     (set-buffer nntp-server-buffer)
390     (widen)))
391
392 (defmacro gnus-agent-with-fetch (&rest forms)
393   "Do FORMS safely."
394   `(unwind-protect
395        (let ((gnus-agent-fetching t))
396          (gnus-agent-start-fetch)
397          ,@forms)
398      (gnus-agent-stop-fetch)))
399
400 (put 'gnus-agent-with-fetch 'lisp-indent-function 0)
401 (put 'gnus-agent-with-fetch 'edebug-form-spec '(body))
402
403 (defmacro gnus-agent-append-to-list (tail value)
404   `(setq ,tail (setcdr ,tail (cons ,value nil))))
405
406 (defmacro gnus-agent-message (level &rest args)
407   `(if (<= ,level gnus-verbose)
408        (message ,@args)))
409
410 ;;;
411 ;;; Mode infestation
412 ;;;
413
414 (defvar gnus-agent-mode-hook nil
415   "Hook run when installing agent mode.")
416
417 (defvar gnus-agent-mode nil)
418 (defvar gnus-agent-mode-status '(gnus-agent-mode " Plugged"))
419
420 (defun gnus-agent-mode ()
421   "Minor mode for providing a agent support in Gnus buffers."
422   (let* ((buffer (progn (string-match "^gnus-\\(.*\\)-mode$"
423                                       (symbol-name major-mode))
424                         (match-string 1 (symbol-name major-mode))))
425          (mode (intern (format "gnus-agent-%s-mode" buffer))))
426     (set (make-local-variable 'gnus-agent-mode) t)
427     (set mode nil)
428     (set (make-local-variable mode) t)
429     ;; Set up the menu.
430     (when (gnus-visual-p 'agent-menu 'menu)
431       (funcall (intern (format "gnus-agent-%s-make-menu-bar" buffer))))
432     (unless (assq 'gnus-agent-mode minor-mode-alist)
433       (push gnus-agent-mode-status minor-mode-alist))
434     (unless (assq mode minor-mode-map-alist)
435       (push (cons mode (symbol-value (intern (format "gnus-agent-%s-mode-map"
436                                                      buffer))))
437             minor-mode-map-alist))
438     (when (eq major-mode 'gnus-group-mode)
439       (let ((init-plugged gnus-plugged)
440             (gnus-agent-go-online nil))
441         ;; g-a-t-p does nothing when gnus-plugged isn't changed.
442         ;; Therefore, make certain that the current value does not
443         ;; match the desired initial value.
444         (setq gnus-plugged :unknown)
445         (gnus-agent-toggle-plugged init-plugged)))
446     (gnus-run-hooks 'gnus-agent-mode-hook
447                     (intern (format "gnus-agent-%s-mode-hook" buffer)))))
448
449 (defvar gnus-agent-group-mode-map (make-sparse-keymap))
450 (gnus-define-keys gnus-agent-group-mode-map
451   "Ju" gnus-agent-fetch-groups
452   "Jc" gnus-enter-category-buffer
453   "Jj" gnus-agent-toggle-plugged
454   "Js" gnus-agent-fetch-session
455   "JY" gnus-agent-synchronize-flags
456   "JS" gnus-group-send-queue
457   "Ja" gnus-agent-add-group
458   "Jr" gnus-agent-remove-group
459   "Jo" gnus-agent-toggle-group-plugged)
460
461 (defun gnus-agent-group-make-menu-bar ()
462   (unless (boundp 'gnus-agent-group-menu)
463     (easy-menu-define
464      gnus-agent-group-menu gnus-agent-group-mode-map ""
465      '("Agent"
466        ["Toggle plugged" gnus-agent-toggle-plugged t]
467        ["Toggle group plugged" gnus-agent-toggle-group-plugged t]
468        ["List categories" gnus-enter-category-buffer t]
469        ["Add (current) group to category" gnus-agent-add-group t]
470        ["Remove (current) group from category" gnus-agent-remove-group t]
471        ["Send queue" gnus-group-send-queue gnus-plugged]
472        ("Fetch"
473         ["All" gnus-agent-fetch-session gnus-plugged]
474         ["Group" gnus-agent-fetch-group gnus-plugged])
475        ["Synchronize flags" gnus-agent-synchronize-flags t]
476        ))))
477
478 (defvar gnus-agent-summary-mode-map (make-sparse-keymap))
479 (gnus-define-keys gnus-agent-summary-mode-map
480   "Jj" gnus-agent-toggle-plugged
481   "Ju" gnus-agent-summary-fetch-group
482   "JS" gnus-agent-fetch-group
483   "Js" gnus-agent-summary-fetch-series
484   "J#" gnus-agent-mark-article
485   "J\M-#" gnus-agent-unmark-article
486   "@" gnus-agent-toggle-mark
487   "Jc" gnus-agent-catchup)
488
489 (defun gnus-agent-summary-make-menu-bar ()
490   (unless (boundp 'gnus-agent-summary-menu)
491     (easy-menu-define
492      gnus-agent-summary-menu gnus-agent-summary-mode-map ""
493      '("Agent"
494        ["Toggle plugged" gnus-agent-toggle-plugged t]
495        ["Mark as downloadable" gnus-agent-mark-article t]
496        ["Unmark as downloadable" gnus-agent-unmark-article t]
497        ["Toggle mark" gnus-agent-toggle-mark t]
498        ["Fetch downloadable" gnus-agent-summary-fetch-group t]
499        ["Catchup undownloaded" gnus-agent-catchup t]))))
500
501 (defvar gnus-agent-server-mode-map (make-sparse-keymap))
502 (gnus-define-keys gnus-agent-server-mode-map
503   "Jj" gnus-agent-toggle-plugged
504   "Ja" gnus-agent-add-server
505   "Jr" gnus-agent-remove-server)
506
507 (defun gnus-agent-server-make-menu-bar ()
508   (unless (boundp 'gnus-agent-server-menu)
509     (easy-menu-define
510      gnus-agent-server-menu gnus-agent-server-mode-map ""
511      '("Agent"
512        ["Toggle plugged" gnus-agent-toggle-plugged t]
513        ["Add" gnus-agent-add-server t]
514        ["Remove" gnus-agent-remove-server t]))))
515
516 (defun gnus-agent-make-mode-line-string (string mouse-button mouse-func)
517   (if (and (fboundp 'propertize)
518            (fboundp 'make-mode-line-mouse-map))
519       (propertize string 'local-map
520                   (make-mode-line-mouse-map mouse-button mouse-func))
521     string))
522
523 (defun gnus-agent-toggle-plugged (set-to)
524   "Toggle whether Gnus is unplugged or not."
525   (interactive (list (not gnus-plugged)))
526   (cond ((eq set-to gnus-plugged)
527          nil)
528         (set-to
529          (setq gnus-plugged set-to)
530          (gnus-run-hooks 'gnus-agent-plugged-hook)
531          (setcar (cdr gnus-agent-mode-status)
532                  (gnus-agent-make-mode-line-string " Plugged"
533                                                    'mouse-2
534                                                    'gnus-agent-toggle-plugged))
535          (gnus-agent-go-online gnus-agent-go-online)
536          (gnus-agent-possibly-synchronize-flags))
537         (t
538          (gnus-agent-close-connections)
539          (setq gnus-plugged set-to)
540          (gnus-run-hooks 'gnus-agent-unplugged-hook)
541          (setcar (cdr gnus-agent-mode-status)
542                  (gnus-agent-make-mode-line-string " Unplugged"
543                                                    'mouse-2
544                                                    'gnus-agent-toggle-plugged))))
545   (set-buffer-modified-p t))
546
547 (defmacro gnus-agent-while-plugged (&rest body)
548   `(let ((original-gnus-plugged gnus-plugged))
549     (unwind-protect
550         (progn (gnus-agent-toggle-plugged t)
551                ,@body)
552       (gnus-agent-toggle-plugged original-gnus-plugged))))
553
554 (put 'gnus-agent-while-plugged 'lisp-indent-function 0)
555 (put 'gnus-agent-while-plugged 'edebug-form-spec '(body))
556
557 (defun gnus-agent-close-connections ()
558   "Close all methods covered by the Gnus agent."
559   (let ((methods gnus-agent-covered-methods))
560     (while methods
561       (gnus-close-server (pop methods)))))
562
563 ;;;###autoload
564 (defun gnus-unplugged ()
565   "Start Gnus unplugged."
566   (interactive)
567   (setq gnus-plugged nil)
568   (gnus))
569
570 ;;;###autoload
571 (defun gnus-plugged ()
572   "Start Gnus plugged."
573   (interactive)
574   (setq gnus-plugged t)
575   (gnus))
576
577 ;;;###autoload
578 (defun gnus-slave-unplugged (&optional arg)
579   "Read news as a slave unplugged."
580   (interactive "P")
581   (setq gnus-plugged nil)
582   (gnus arg nil 'slave))
583
584 ;;;###autoload
585 (defun gnus-agentize ()
586   "Allow Gnus to be an offline newsreader.
587
588 The gnus-agentize function is now called internally by gnus when
589 gnus-agent is set.  If you wish to avoid calling gnus-agentize,
590 customize gnus-agent to nil.
591
592 This will modify the `gnus-setup-news-hook', and
593 `message-send-mail-real-function' variables, and install the Gnus agent
594 minor mode in all Gnus buffers."
595   (interactive)
596   (gnus-open-agent)
597   (add-hook 'gnus-setup-news-hook 'gnus-agent-queue-setup)
598   (unless gnus-agent-send-mail-function
599     (setq gnus-agent-send-mail-function
600           (or message-send-mail-real-function
601                                          message-send-mail-function)
602           message-send-mail-real-function 'gnus-agent-send-mail))
603
604   (unless gnus-agent-covered-methods
605     (mapcar
606      (lambda (server)
607        (if (memq (car (gnus-server-to-method server)) 
608                  gnus-agent-auto-agentize-methods)
609            (setq gnus-agent-covered-methods 
610                  (cons (gnus-server-to-method server)
611                        gnus-agent-covered-methods ))))
612      (append (list gnus-select-method) gnus-secondary-select-methods))))
613
614 (defun gnus-agent-queue-setup (&optional group-name)
615   "Make sure the queue group exists.
616 Optional arg GROUP-NAME allows to specify another group."
617   (unless (gnus-gethash (format "nndraft:%s" (or group-name "queue"))
618                         gnus-newsrc-hashtb)
619     (gnus-request-create-group (or group-name "queue") '(nndraft ""))
620     (let ((gnus-level-default-subscribed 1))
621       (gnus-subscribe-group (format "nndraft:%s" (or group-name "queue"))
622                             nil '(nndraft "")))
623     (gnus-group-set-parameter
624      (format "nndraft:%s" (or group-name "queue"))
625      'gnus-dummy '((gnus-draft-mode)))))
626
627 (defun gnus-agent-send-mail ()
628   (if gnus-plugged
629       (funcall gnus-agent-send-mail-function)
630     (goto-char (point-min))
631     (re-search-forward
632      (concat "^" (regexp-quote mail-header-separator) "\n"))
633     (replace-match "\n")
634     (gnus-agent-insert-meta-information 'mail)
635     (gnus-request-accept-article "nndraft:queue" nil t t)))
636
637 (defun gnus-agent-insert-meta-information (type &optional method)
638   "Insert meta-information into the message that says how it's to be posted.
639 TYPE can be either `mail' or `news'.  If the latter, then METHOD can
640 be a select method."
641   (save-excursion
642     (message-remove-header gnus-agent-meta-information-header)
643     (goto-char (point-min))
644     (insert gnus-agent-meta-information-header ": "
645             (symbol-name type) " " (format "%S" method)
646             "\n")
647     (forward-char -1)
648     (while (search-backward "\n" nil t)
649       (replace-match "\\n" t t))))
650
651 (defun gnus-agent-restore-gcc ()
652   "Restore GCC field from saved header."
653   (save-excursion
654     (goto-char (point-min))
655     (while (re-search-forward (concat gnus-agent-gcc-header ":") nil t)
656       (replace-match "Gcc:" 'fixedcase))))
657
658 (defun gnus-agent-any-covered-gcc ()
659   (save-restriction
660     (message-narrow-to-headers)
661     (let* ((gcc (mail-fetch-field "gcc" nil t))
662            (methods (and gcc
663                          (mapcar 'gnus-inews-group-method
664                                  (message-unquote-tokens
665                                   (message-tokenize-header
666                                    gcc " ,")))))
667            covered)
668       (while (and (not covered) methods)
669         (setq covered (gnus-agent-method-p (car methods))
670               methods (cdr methods)))
671       covered)))
672
673 ;;;###autoload
674 (defun gnus-agent-possibly-save-gcc ()
675   "Save GCC if Gnus is unplugged."
676   (when (and (not gnus-plugged) (gnus-agent-any-covered-gcc))
677     (save-excursion
678       (goto-char (point-min))
679       (let ((case-fold-search t))
680         (while (re-search-forward "^gcc:" nil t)
681           (replace-match (concat gnus-agent-gcc-header ":") 'fixedcase))))))
682
683 (defun gnus-agent-possibly-do-gcc ()
684   "Do GCC if Gnus is plugged."
685   (when (or gnus-plugged (not (gnus-agent-any-covered-gcc)))
686     (gnus-inews-do-gcc)))
687
688 ;;;
689 ;;; Group mode commands
690 ;;;
691
692 (defun gnus-agent-fetch-groups (n)
693   "Put all new articles in the current groups into the Agent."
694   (interactive "P")
695   (unless gnus-plugged
696     (error "Groups can't be fetched when Gnus is unplugged"))
697   (gnus-group-iterate n 'gnus-agent-fetch-group))
698
699 (defun gnus-agent-fetch-group (&optional group)
700   "Put all new articles in GROUP into the Agent."
701   (interactive (list (gnus-group-group-name)))
702   (setq group (or group gnus-newsgroup-name))
703   (unless group
704     (error "No group on the current line"))
705
706   (gnus-agent-while-plugged
707     (let ((gnus-command-method (gnus-find-method-for-group group)))
708       (gnus-agent-with-fetch
709         (gnus-agent-fetch-group-1 group gnus-command-method)
710         (gnus-message 5 "Fetching %s...done" group)))))
711
712 (defun gnus-agent-add-group (category arg)
713   "Add the current group to an agent category."
714   (interactive
715    (list
716     (intern
717      (completing-read
718       "Add to category: "
719       (mapcar (lambda (cat) (list (symbol-name (car cat))))
720               gnus-category-alist)
721       nil t))
722     current-prefix-arg))
723   (let ((cat (assq category gnus-category-alist))
724         c groups)
725     (gnus-group-iterate arg
726       (lambda (group)
727         (when (gnus-agent-cat-groups (setq c (gnus-group-category group)))
728           (setf (gnus-agent-cat-groups c)
729                 (delete group (gnus-agent-cat-groups c))))
730         (push group groups)))
731     (setf (gnus-agent-cat-groups cat)
732           (nconc (gnus-agent-cat-groups cat) groups))
733     (gnus-category-write)))
734
735 (defun gnus-agent-remove-group (arg)
736   "Remove the current group from its agent category, if any."
737   (interactive "P")
738   (let (c)
739     (gnus-group-iterate arg
740       (lambda (group)
741         (when (gnus-agent-cat-groups (setq c (gnus-group-category group)))
742           (setf (gnus-agent-cat-groups c)
743                 (delete group (gnus-agent-cat-groups c))))))
744     (gnus-category-write)))
745
746 (defun gnus-agent-synchronize-flags ()
747   "Synchronize unplugged flags with servers."
748   (interactive)
749   (save-excursion
750     (dolist (gnus-command-method gnus-agent-covered-methods)
751       (when (file-exists-p (gnus-agent-lib-file "flags"))
752         (gnus-agent-synchronize-flags-server gnus-command-method)))))
753
754 (defun gnus-agent-possibly-synchronize-flags ()
755   "Synchronize flags according to `gnus-agent-synchronize-flags'."
756   (interactive)
757   (save-excursion
758     (dolist (gnus-command-method gnus-agent-covered-methods)
759       (when (file-exists-p (gnus-agent-lib-file "flags"))
760         (gnus-agent-possibly-synchronize-flags-server gnus-command-method)))))
761
762 (defun gnus-agent-synchronize-flags-server (method)
763   "Synchronize flags set when unplugged for server."
764   (let ((gnus-command-method method))
765     (when (file-exists-p (gnus-agent-lib-file "flags"))
766       (set-buffer (get-buffer-create " *Gnus Agent flag synchronize*"))
767       (erase-buffer)
768       (nnheader-insert-file-contents (gnus-agent-lib-file "flags"))
769       (if (null (gnus-check-server gnus-command-method))
770           (gnus-message 1 "Couldn't open server %s" (nth 1 gnus-command-method))
771         (while (not (eobp))
772           (if (null (eval (read (current-buffer))))
773               (gnus-delete-line)
774             (write-file (gnus-agent-lib-file "flags"))
775             (error "Couldn't set flags from file %s"
776                    (gnus-agent-lib-file "flags"))))
777         (delete-file (gnus-agent-lib-file "flags")))
778       (kill-buffer nil))))
779
780 (defun gnus-agent-possibly-synchronize-flags-server (method)
781   "Synchronize flags for server according to `gnus-agent-synchronize-flags'."
782   (when (or (and gnus-agent-synchronize-flags
783                  (not (eq gnus-agent-synchronize-flags 'ask)))
784             (and (eq gnus-agent-synchronize-flags 'ask)
785                  (gnus-y-or-n-p (format "Synchronize flags on server `%s'? "
786                                         (cadr method)))))
787     (gnus-agent-synchronize-flags-server method)))
788
789 ;;;
790 ;;; Server mode commands
791 ;;;
792
793 (defun gnus-agent-add-server (server)
794   "Enroll SERVER in the agent program."
795   (interactive (list (gnus-server-server-name)))
796   (unless server
797     (error "No server on the current line"))
798   (let ((method (gnus-server-get-method nil (gnus-server-server-name))))
799     (when (gnus-agent-method-p method)
800       (error "Server already in the agent program"))
801     (push method gnus-agent-covered-methods)
802     (gnus-server-update-server server)
803     (gnus-agent-write-servers)
804     (gnus-message 1 "Entered %s into the Agent" server)))
805
806 (defun gnus-agent-remove-server (server)
807   "Remove SERVER from the agent program."
808   (interactive (list (gnus-server-server-name)))
809   (unless server
810     (error "No server on the current line"))
811   (let ((method (gnus-server-get-method nil (gnus-server-server-name))))
812     (unless (gnus-agent-method-p method)
813       (error "Server not in the agent program"))
814     (setq gnus-agent-covered-methods
815           (delete method gnus-agent-covered-methods))
816     (gnus-server-update-server server)
817     (gnus-agent-write-servers)
818     (gnus-message 1 "Removed %s from the agent" server)))
819
820 (defun gnus-agent-read-servers ()
821   "Read the alist of covered servers."
822   (mapcar (lambda (m)
823             (let ((method (gnus-server-get-method
824                            nil
825                            (or m "native"))))
826               (if method
827                   (unless (member method gnus-agent-covered-methods)
828                     (push method gnus-agent-covered-methods))
829                 (gnus-message 1 "Ignoring disappeared server `%s'" m)
830                 (sit-for 1))))
831           (gnus-agent-read-file
832            (nnheader-concat gnus-agent-directory "lib/servers"))))
833
834 (defun gnus-agent-write-servers ()
835   "Write the alist of covered servers."
836   (gnus-make-directory (nnheader-concat gnus-agent-directory "lib"))
837   (let ((coding-system-for-write nnheader-file-coding-system)
838         (file-name-coding-system nnmail-pathname-coding-system))
839     (with-temp-file (nnheader-concat gnus-agent-directory "lib/servers")
840       (prin1 (mapcar 'gnus-method-simplify gnus-agent-covered-methods)
841              (current-buffer)))))
842
843 ;;;
844 ;;; Summary commands
845 ;;;
846
847 (defun gnus-agent-mark-article (n &optional unmark)
848   "Mark the next N articles as downloadable.
849 If N is negative, mark backward instead.  If UNMARK is non-nil, remove
850 the mark instead.  The difference between N and the actual number of
851 articles marked is returned."
852   (interactive "p")
853   (let ((backward (< n 0))
854         (n (abs n)))
855     (while (and
856             (> n 0)
857             (progn
858               (gnus-summary-set-agent-mark
859                (gnus-summary-article-number) unmark)
860               (zerop (gnus-summary-next-subject (if backward -1 1) nil t))))
861       (setq n (1- n)))
862     (when (/= 0 n)
863       (gnus-message 7 "No more articles"))
864     (gnus-summary-recenter)
865     (gnus-summary-position-point)
866     n))
867
868 (defun gnus-agent-unmark-article (n)
869   "Remove the downloadable mark from the next N articles.
870 If N is negative, unmark backward instead.  The difference between N and
871 the actual number of articles unmarked is returned."
872   (interactive "p")
873   (gnus-agent-mark-article n t))
874
875 (defun gnus-agent-toggle-mark (n)
876   "Toggle the downloadable mark from the next N articles.
877 If N is negative, toggle backward instead.  The difference between N and
878 the actual number of articles toggled is returned."
879   (interactive "p")
880   (gnus-agent-mark-article n 'toggle))
881
882 (defun gnus-summary-set-agent-mark (article &optional unmark)
883   "Mark ARTICLE as downloadable.  If UNMARK is nil, article is marked.
884 When UNMARK is t, the article is unmarked.  For any other value, the
885 article's mark is toggled."
886   (let ((unmark (cond ((eq nil unmark)
887                        nil)
888                       ((eq t unmark)
889                        t)
890                       (t
891                        (memq article gnus-newsgroup-downloadable)))))
892     (when (gnus-summary-goto-subject article nil t)
893       (gnus-summary-update-mark
894        (if unmark
895            (progn
896              (setq gnus-newsgroup-downloadable
897                    (delq article gnus-newsgroup-downloadable))
898              (gnus-article-mark article))
899          (progn
900            (setq gnus-newsgroup-downloadable
901                  (gnus-add-to-sorted-list gnus-newsgroup-downloadable article))
902            gnus-downloadable-mark)
903          )
904        'unread))))
905
906 (defun gnus-agent-get-undownloaded-list ()
907   "Construct list of articles that have not been downloaded."
908   (let ((gnus-command-method (gnus-find-method-for-group gnus-newsgroup-name)))
909     (when (set (make-local-variable 'gnus-newsgroup-agentized)
910                (gnus-agent-method-p gnus-command-method))
911       (let* ((alist (gnus-agent-load-alist gnus-newsgroup-name))
912              (headers (sort (mapcar (lambda (h)
913                                       (mail-header-number h))
914                                     gnus-newsgroup-headers) '<))
915              (cached (and gnus-use-cache gnus-newsgroup-cached))
916              (undownloaded (list nil))
917              (tail-undownloaded undownloaded)
918              (unfetched (list nil))
919              (tail-unfetched unfetched))
920         (while (and alist headers)
921           (let ((a (caar alist))
922                 (h (car headers)))
923             (cond ((< a h)
924                    ;; Ignore IDs in the alist that are not being
925                    ;; displayed in the summary.
926                    (setq alist (cdr alist)))
927                   ((> a h)
928                    ;; Headers that are not in the alist should be
929                    ;; fictious (see nnagent-retrieve-headers); they
930                    ;; imply that this article isn't in the agent.
931                    (gnus-agent-append-to-list tail-undownloaded h)
932                    (gnus-agent-append-to-list tail-unfetched    h)
933                    (setq headers (cdr headers))) 
934                   ((cdar alist)
935                    (setq alist (cdr alist))
936                    (setq headers (cdr headers))
937                    nil                  ; ignore already downloaded
938                    )
939                   (t
940                    (setq alist (cdr alist))
941                    (setq headers (cdr headers))
942                    
943                    ;; This article isn't in the agent.  Check to see
944                    ;; if it is in the cache.  If it is, it's been
945                    ;; downloaded.
946                    (while (and cached (< (car cached) a))
947                      (setq cached (cdr cached)))
948                    (unless (equal a (car cached))
949                      (gnus-agent-append-to-list tail-undownloaded a))))))
950
951         (while headers
952           (let ((num (pop headers)))
953             (gnus-agent-append-to-list tail-undownloaded num)
954             (gnus-agent-append-to-list tail-unfetched    num)))
955
956         (setq gnus-newsgroup-undownloaded (cdr undownloaded)
957               gnus-newsgroup-unfetched    (cdr unfetched))))))
958
959 (defun gnus-agent-catchup ()
960   "Mark as read all unhandled articles.
961 An article is unhandled if it is neither cached, nor downloaded, nor
962 downloadable."
963   (interactive)
964   (save-excursion
965     (let ((articles gnus-newsgroup-undownloaded))
966       (when (or gnus-newsgroup-downloadable
967                 gnus-newsgroup-cached)
968         (setq articles (gnus-sorted-ndifference
969                         (gnus-sorted-ndifference
970                          (gnus-copy-sequence articles)
971                          gnus-newsgroup-downloadable)
972                         gnus-newsgroup-cached)))
973
974       (while articles
975         (gnus-summary-mark-article
976          (pop articles) gnus-catchup-mark)))
977     (gnus-summary-position-point)))
978
979 (defun gnus-agent-summary-fetch-series ()
980   (interactive)
981   (when gnus-newsgroup-processable
982     (setq gnus-newsgroup-downloadable
983           (let* ((dl gnus-newsgroup-downloadable)
984                  (gnus-newsgroup-downloadable
985                   (sort (gnus-copy-sequence gnus-newsgroup-processable) '<))
986                  (fetched-articles (gnus-agent-summary-fetch-group)))
987             ;; The preceeding call to (gnus-agent-summary-fetch-group)
988             ;; updated gnus-newsgroup-downloadable to remove each
989             ;; article successfully fetched.
990
991             ;; For each article that I processed, remove its
992             ;; processable mark IF the article is no longer
993             ;; downloadable (i.e. it's already downloaded)
994             (dolist (article gnus-newsgroup-processable)
995               (unless (memq article gnus-newsgroup-downloadable)
996                 (gnus-summary-remove-process-mark article)))
997             (gnus-sorted-ndifference dl fetched-articles)))))
998
999 (defun gnus-agent-summary-fetch-group (&optional all)
1000   "Fetch the downloadable articles in the group.
1001 Optional arg ALL, if non-nil, means to fetch all articles."
1002   (interactive "P")
1003   (let ((articles
1004          (if all gnus-newsgroup-articles
1005            gnus-newsgroup-downloadable))
1006         (gnus-command-method (gnus-find-method-for-group gnus-newsgroup-name))
1007         fetched-articles)
1008     (gnus-agent-while-plugged
1009       (unless articles
1010         (error "No articles to download"))
1011       (gnus-agent-with-fetch
1012         (setq gnus-newsgroup-undownloaded
1013               (gnus-sorted-ndifference
1014                gnus-newsgroup-undownloaded
1015                (setq fetched-articles
1016                      (gnus-agent-fetch-articles
1017                       gnus-newsgroup-name articles)))))
1018       (save-excursion
1019         (dolist (article articles)
1020           (let ((was-marked-downloadable 
1021                  (memq article gnus-newsgroup-downloadable)))
1022             (cond (gnus-agent-mark-unread-after-downloaded
1023                    (setq gnus-newsgroup-downloadable
1024                          (delq article gnus-newsgroup-downloadable))
1025
1026                    ;; The downloadable mark is implemented as a
1027                    ;; type of read mark.  Therefore, marking the
1028                    ;; article as unread is sufficient to clear
1029                    ;; its downloadable flag.  
1030                    (gnus-summary-mark-article article gnus-unread-mark))
1031                   (was-marked-downloadable
1032                    (gnus-summary-set-agent-mark article t)))
1033             (when (gnus-summary-goto-subject article nil t)
1034               (gnus-summary-update-download-mark article))))))
1035     fetched-articles))
1036
1037 (defun gnus-agent-fetch-selected-article ()
1038   "Fetch the current article as it is selected.
1039 This can be added to `gnus-select-article-hook' or
1040 `gnus-mark-article-hook'."
1041   (let ((gnus-command-method gnus-current-select-method))
1042     (when (and gnus-plugged (gnus-agent-method-p gnus-command-method))
1043       (when (gnus-agent-fetch-articles
1044              gnus-newsgroup-name
1045              (list gnus-current-article))
1046         (setq gnus-newsgroup-undownloaded
1047               (delq gnus-current-article gnus-newsgroup-undownloaded))
1048         (gnus-summary-update-download-mark gnus-current-article)))))
1049
1050 ;;;
1051 ;;; Internal functions
1052 ;;;
1053
1054 ;;; NOTES:
1055 ;;; The agent's active range is defined as follows:
1056 ;;;  If the agent has no record of the group, use the actual active
1057 ;;;    range.
1058 ;;;  If the agent has a record, set the agent's active range to
1059 ;;;    include the max limit of the actual active range.
1060 ;;;  When expiring, update the min limit to match the smallest of the
1061 ;;;    min article not expired or the min actual active range.
1062
1063 (defun gnus-agent-save-active (method)
1064   (gnus-agent-save-active-1 method 'gnus-active-to-gnus-format))
1065
1066 (defun gnus-agent-save-active-1 (method function)
1067   (when (gnus-agent-method-p method)
1068     (let* ((gnus-command-method method)
1069            (new (gnus-make-hashtable (count-lines (point-min) (point-max))))
1070            (file (gnus-agent-lib-file "active")))
1071       (funcall function nil new)
1072       (gnus-agent-write-active file new)
1073       (erase-buffer)
1074       (nnheader-insert-file-contents file))))
1075
1076 (defun gnus-agent-write-active (file new &optional literal-replacement)
1077   (let ((old new))
1078     (when (and (not literal-replacement)
1079                (file-exists-p file))
1080       (setq old (gnus-make-hashtable (count-lines (point-min) (point-max))))
1081       (with-temp-buffer
1082         (nnheader-insert-file-contents file)
1083         (gnus-active-to-gnus-format nil old))
1084       ;; Iterate over the current active groups, the current active
1085       ;; range may expand, but NOT CONTRACT, the agent's active range.
1086       (mapatoms
1087        (lambda (nsym)
1088          (let ((new-active (and nsym (boundp nsym) (symbol-value nsym))))
1089            (when new-active
1090              (let* ((osym       (intern (symbol-name nsym) old))
1091                     (old-active (and (boundp osym) (symbol-value osym))))
1092                (if old-active
1093                    (let ((new-min (car new-active))
1094                          (old-min (car old-active))
1095                          (new-max (cdr new-active))
1096                          (old-max (cdr old-active)))
1097                      (if (and (integerp new-min)
1098                               (< new-min old-min))
1099                          (setcar old-active new-min))
1100                      (if (and (integerp new-max)
1101                               (> new-max old-max))
1102                          (setcdr old-active new-max)))
1103                  (set osym new-active))))))
1104        new))
1105     (gnus-make-directory (file-name-directory file))
1106     (let ((nnmail-active-file-coding-system gnus-agent-file-coding-system))
1107       ;; The hashtable contains real names of groups.  However, do NOT
1108       ;; add the foreign server prefix as gnus-active-to-gnus-format
1109       ;; will add it while reading the file.
1110       (gnus-write-active-file file old nil))))
1111
1112 (defun gnus-agent-save-groups (method)
1113   (gnus-agent-save-active-1 method 'gnus-groups-to-gnus-format))
1114
1115 (defun gnus-agent-save-group-info (method group active)
1116   (when (gnus-agent-method-p method)
1117     (let* ((gnus-command-method method)
1118            (coding-system-for-write nnheader-file-coding-system)
1119            (file-name-coding-system nnmail-pathname-coding-system)
1120            (file (gnus-agent-lib-file "active"))
1121            oactive-min oactive-max)
1122       (gnus-make-directory (file-name-directory file))
1123       (with-temp-file file
1124         ;; Emacs got problem to match non-ASCII group in multibyte buffer.
1125         (mm-disable-multibyte)
1126         (when (file-exists-p file)
1127           (nnheader-insert-file-contents file)
1128
1129           (goto-char (point-min))
1130           (when (re-search-forward
1131                  (concat "^" (regexp-quote group) " ") nil t)
1132             (save-excursion
1133               (setq oactive-max (read (current-buffer)) ;; max
1134                     oactive-min (read (current-buffer)))) ;; min
1135             (gnus-delete-line)))
1136         (insert (format "%S %d %d y\n" (intern group)
1137                         (max (or oactive-max (cdr active)) (cdr active))
1138                         (min (or oactive-min (car active)) (car active))))
1139         (goto-char (point-max))
1140         (while (search-backward "\\." nil t)
1141           (delete-char 1))))))
1142
1143 (defun gnus-agent-group-path (group)
1144   "Translate GROUP into a file name."
1145
1146   ;; NOTE: This is what nnmail-group-pathname does as of Apr 2003.
1147   ;; The two methods must be kept synchronized, which is why
1148   ;; gnus-agent-group-pathname was added.
1149
1150   (setq group
1151         (nnheader-translate-file-chars
1152          (nnheader-replace-duplicate-chars-in-string
1153           (nnheader-replace-chars-in-string 
1154            (gnus-group-real-name group)
1155            ?/ ?_)
1156           ?. ?_)))
1157   (if (or nnmail-use-long-file-names
1158           (file-directory-p (expand-file-name group (gnus-agent-directory))))
1159       group
1160     (mm-encode-coding-string
1161      (nnheader-replace-chars-in-string group ?. ?/)
1162      nnmail-pathname-coding-system)))
1163
1164 (defun gnus-agent-group-pathname (group)
1165   "Translate GROUP into a file name."
1166   ;; nnagent uses nnmail-group-pathname to read articles while
1167   ;; unplugged.  The agent must, therefore, use the same directory
1168   ;; while plugged.