lisp/ChangeLog: Fix last commit
[gnus] / lisp / gnus-agent.el
1 ;;; gnus-agent.el --- unplugged support for Gnus
2
3 ;; Copyright (C) 1997-2015 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 3 of the License, or
11 ;; (at your option) 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.  If not, see <http://www.gnu.org/licenses/>.
20
21 ;;; Commentary:
22
23 ;;; Code:
24
25 (require 'gnus)
26 (require 'gnus-cache)
27 (require 'nnmail)
28 (require 'nnvirtual)
29 (require 'gnus-sum)
30 (require 'gnus-score)
31 (require 'gnus-srvr)
32 (require 'gnus-util)
33 (eval-when-compile
34   (if (featurep 'xemacs)
35       (require 'itimer)
36     (require 'timer))
37   (require 'cl))
38
39 (autoload 'gnus-server-update-server "gnus-srvr")
40 (autoload 'gnus-agent-customize-category "gnus-cus")
41
42 (defcustom gnus-agent-directory (nnheader-concat gnus-directory "agent/")
43   "Where the Gnus agent will store its files."
44   :group 'gnus-agent
45   :type 'directory)
46
47 (defcustom gnus-agent-plugged-hook nil
48   "Hook run when plugging into the network."
49   :group 'gnus-agent
50   :type 'hook)
51
52 (defcustom gnus-agent-unplugged-hook nil
53   "Hook run when unplugging from the network."
54   :group 'gnus-agent
55   :type 'hook)
56
57 (defcustom gnus-agent-fetched-hook nil
58   "Hook run when finished fetching articles."
59   :version "22.1"
60   :group 'gnus-agent
61   :type 'hook)
62
63 (defcustom gnus-agent-handle-level gnus-level-subscribed
64   "Groups on levels higher than this variable will be ignored by the Agent."
65   :group 'gnus-agent
66   :type 'integer)
67
68 (defcustom gnus-agent-expire-days 7
69   "Read articles older than this will be expired.
70 If you wish to disable Agent expiring, see `gnus-agent-enable-expiration'."
71   :group 'gnus-agent
72   :type '(number :tag "days"))
73
74 (defcustom gnus-agent-expire-all nil
75   "If non-nil, also expire unread, ticked and dormant articles.
76 If nil, only read articles will be expired."
77   :group 'gnus-agent
78   :type 'boolean)
79
80 (defcustom gnus-agent-group-mode-hook nil
81   "Hook run in Agent group minor modes."
82   :group 'gnus-agent
83   :type 'hook)
84
85 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
86 (when (featurep 'xemacs)
87   (add-hook 'gnus-agent-group-mode-hook 'gnus-xmas-agent-group-menu-add))
88
89 (defcustom gnus-agent-summary-mode-hook nil
90   "Hook run in Agent summary minor modes."
91   :group 'gnus-agent
92   :type 'hook)
93
94 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
95 (when (featurep 'xemacs)
96   (add-hook 'gnus-agent-summary-mode-hook 'gnus-xmas-agent-summary-menu-add))
97
98 (defcustom gnus-agent-server-mode-hook nil
99   "Hook run in Agent summary minor modes."
100   :group 'gnus-agent
101   :type 'hook)
102
103 ;; Extracted from gnus-xmas-redefine in order to preserve user settings
104 (when (featurep 'xemacs)
105   (add-hook 'gnus-agent-server-mode-hook 'gnus-xmas-agent-server-menu-add))
106
107 (defcustom gnus-agent-confirmation-function 'y-or-n-p
108   "Function to confirm when error happens."
109   :version "21.1"
110   :group 'gnus-agent
111   :type 'function)
112
113 (defcustom gnus-agent-synchronize-flags nil
114   "Indicate if flags are synchronized when you plug in.
115 If this is `ask' the hook will query the user."
116   ;; If the default switches to something else than nil, then the function
117   ;; should be fixed not be exceedingly slow.  See 2005-09-20 ChangeLog entry.
118   :version "21.1"
119   :type '(choice (const :tag "Always" t)
120                  (const :tag "Never" nil)
121                  (const :tag "Ask" ask))
122   :group 'gnus-agent)
123
124 (defcustom gnus-agent-go-online 'ask
125   "Indicate if offline servers go online when you plug in.
126 If this is `ask' the hook will query the user."
127   :version "21.3"
128   :type '(choice (const :tag "Always" t)
129                  (const :tag "Never" nil)
130                  (const :tag "Ask" ask))
131   :group 'gnus-agent)
132
133 (defcustom gnus-agent-mark-unread-after-downloaded t
134   "Indicate whether to mark articles unread after downloaded."
135   :version "21.1"
136   :type 'boolean
137   :group 'gnus-agent)
138
139 (defcustom gnus-agent-download-marks '(download)
140   "Marks for downloading."
141   :version "21.1"
142   :type '(repeat (symbol :tag "Mark"))
143   :group 'gnus-agent)
144
145 (defcustom gnus-agent-consider-all-articles nil
146   "When non-nil, the agent will let the agent predicate decide
147 whether articles need to be downloaded or not, for all articles.  When
148 nil, the default, the agent will only let the predicate decide
149 whether unread articles are downloaded or not.  If you enable this,
150 groups with large active ranges may open slower and you may also want
151 to look into the agent expiry settings to block the expiration of
152 read articles as they would just be downloaded again."
153   :version "22.1"
154   :type 'boolean
155   :group 'gnus-agent)
156
157 (defcustom gnus-agent-max-fetch-size 10000000 ;; 10 Mb
158   "Chunk size for `gnus-agent-fetch-session'.
159 The function will split its article fetches into chunks smaller than
160 this limit."
161   :version "22.1"
162   :group 'gnus-agent
163   :type 'integer)
164
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."
172   :version "22.1"
173   :group 'gnus-agent
174   :type '(radio (const :format "Enable " ENABLE)
175                 (const :format "Disable " DISABLE)))
176
177 (defcustom gnus-agent-expire-unagentized-dirs t
178   "*Whether expiration should expire in unagentized directories.
179 Have gnus-agent-expire scan the directories under
180 \(gnus-agent-directory) for groups that are no longer agentized.
181 When found, offer to remove them."
182   :version "22.1"
183   :type 'boolean
184   :group 'gnus-agent)
185
186 (defcustom gnus-agent-auto-agentize-methods nil
187   "Initially, all servers from these methods are agentized.
188 The user may remove or add servers using the Server buffer.
189 See Info nodes `(gnus)Server Buffer', `(gnus)Agent Variables'."
190   :version "22.1"
191   :type '(repeat symbol)
192   :group 'gnus-agent)
193
194 (defcustom gnus-agent-queue-mail t
195   "Whether and when outgoing mail should be queued by the agent.
196 When `always', always queue outgoing mail.  When nil, never
197 queue.  Otherwise, queue if and only if unplugged."
198   :version "22.1"
199   :group 'gnus-agent
200   :type '(radio (const :format "Always" always)
201                 (const :format "Never" nil)
202                 (const :format "When unplugged" t)))
203
204 (defcustom gnus-agent-prompt-send-queue nil
205   "If non-nil, `gnus-group-send-queue' will prompt if called when unplugged."
206   :version "22.1"
207   :group 'gnus-agent
208   :type 'boolean)
209
210 (defcustom gnus-agent-article-alist-save-format 1
211   "Indicates whether to use compression(2), versus no
212 compression(1), when writing agentview files.  The compressed
213 files do save space but load times are 6-7 times higher.  A group
214 must be opened then closed for the agentview to be updated using
215 the new format."
216   ;; Wouldn't symbols instead numbers be nicer?  --rsteib
217   :version "22.1"
218   :group 'gnus-agent
219   :type '(radio (const :format "Compressed" 2)
220                 (const :format "Uncompressed" 1)))
221
222 ;;; Internal variables
223
224 (defvar gnus-agent-history-buffers nil)
225 (defvar gnus-agent-buffer-alist nil)
226 (defvar gnus-agent-article-alist nil
227   "An assoc list identifying the articles whose headers have been fetched.
228 If successfully fetched, these headers will be stored in the group's overview
229 file.  The key of each assoc pair is the article ID, the value of each assoc
230 pair is a flag indicating whether the identified article has been downloaded
231 \(gnus-agent-fetch-articles sets the value to the day of the download).
232 NOTES:
233 1) The last element of this list can not be expired as some
234    routines (for example, get-agent-fetch-headers) use the last
235    value to track which articles have had their headers retrieved.
236 2) The function `gnus-agent-regenerate' may destructively modify the value.")
237 (defvar gnus-agent-group-alist nil)
238 (defvar gnus-category-alist nil)
239 (defvar gnus-agent-current-history nil)
240 (defvar gnus-agent-overview-buffer nil)
241 (defvar gnus-category-predicate-cache nil)
242 (defvar gnus-category-group-cache nil)
243 (defvar gnus-agent-spam-hashtb nil)
244 (defvar gnus-agent-file-name nil)
245 (defvar gnus-agent-file-coding-system 'raw-text)
246 (defvar gnus-agent-file-loading-cache nil)
247 (defvar gnus-agent-total-fetched-hashtb nil)
248 (defvar gnus-agent-inhibit-update-total-fetched-for nil)
249 (defvar gnus-agent-need-update-total-fetched-for nil)
250
251 ;; Dynamic variables
252 (defvar gnus-headers)
253 (defvar gnus-score)
254
255 ;; Added to support XEmacs
256 (eval-and-compile
257   (unless (fboundp 'directory-files-and-attributes)
258     (defun directory-files-and-attributes (directory
259                                            &optional full match nosort)
260       (let (result)
261         (dolist (file (directory-files directory full match nosort))
262           (push (cons file (file-attributes file)) result))
263         (nreverse result)))))
264
265 ;;;
266 ;;; Setup
267 ;;;
268
269 (defun gnus-open-agent ()
270   (setq gnus-agent t)
271   (gnus-agent-read-servers)
272   (gnus-category-read)
273   (gnus-agent-create-buffer)
274   (add-hook 'gnus-group-mode-hook 'gnus-agent-mode)
275   (add-hook 'gnus-summary-mode-hook 'gnus-agent-mode)
276   (add-hook 'gnus-server-mode-hook 'gnus-agent-mode))
277
278 (defun gnus-agent-create-buffer ()
279   (if (gnus-buffer-live-p gnus-agent-overview-buffer)
280       t
281     (setq gnus-agent-overview-buffer
282           (gnus-get-buffer-create " *Gnus agent overview*"))
283     (with-current-buffer gnus-agent-overview-buffer
284       (mm-enable-multibyte))
285     nil))
286
287 (gnus-add-shutdown 'gnus-close-agent 'gnus)
288
289 (defun gnus-close-agent ()
290   (setq gnus-category-predicate-cache nil
291         gnus-category-group-cache nil
292         gnus-agent-spam-hashtb nil)
293   (gnus-kill-buffer gnus-agent-overview-buffer))
294
295 ;;;
296 ;;; Utility functions
297 ;;;
298
299 (defmacro gnus-agent-with-refreshed-group (group &rest body)
300   "Performs the body then updates the group's line in the group
301 buffer.  Automatically blocks multiple updates due to recursion."
302 `(prog1 (let ((gnus-agent-inhibit-update-total-fetched-for t)) ,@body)
303      (when (and gnus-agent-need-update-total-fetched-for
304                 (not gnus-agent-inhibit-update-total-fetched-for))
305         (with-current-buffer gnus-group-buffer
306           (setq gnus-agent-need-update-total-fetched-for nil)
307           (gnus-group-update-group ,group t)))))
308
309 (defun gnus-agent-read-file (file)
310   "Load FILE and do a `read' there."
311   (with-temp-buffer
312     (ignore-errors
313       (nnheader-insert-file-contents file)
314       (goto-char (point-min))
315       (read (current-buffer)))))
316
317 (defsubst gnus-agent-method ()
318   (concat (symbol-name (car gnus-command-method)) "/"
319           (if (equal (cadr gnus-command-method) "")
320               "unnamed"
321             (cadr gnus-command-method))))
322
323 (defsubst gnus-agent-directory ()
324   "The name of the Gnus agent directory."
325   (nnheader-concat gnus-agent-directory
326                    (nnheader-translate-file-chars (gnus-agent-method)) "/"))
327
328 (defun gnus-agent-lib-file (file)
329   "The full name of the Gnus agent library FILE."
330   (expand-file-name file
331                     (file-name-as-directory
332                      (expand-file-name "agent.lib" (gnus-agent-directory)))))
333
334 (defun gnus-agent-cat-set-property (category property value)
335   (if value
336       (setcdr (or (assq property category)
337               (let ((cell (cons property nil)))
338                     (setcdr category (cons cell (cdr category)))
339                     cell)) value)
340     (let ((category category))
341       (while (cond ((eq property (caadr category))
342                     (setcdr category (cddr category))
343                     nil)
344                    (t
345                     (setq category (cdr category)))))))
346   category)
347
348 (eval-when-compile
349   (defmacro gnus-agent-cat-defaccessor (name prop-name)
350     "Define accessor and setter methods for manipulating a list of the form
351 \(NAME (PROPERTY1 VALUE1) ... (PROPERTY_N VALUE_N)).
352 Given the call (gnus-agent-cat-defaccessor func PROPERTY1), the list may be
353 manipulated as follows:
354   (func LIST): Returns VALUE1
355   (setf (func LIST) NEW_VALUE1): Replaces VALUE1 with NEW_VALUE1."
356     `(progn (defmacro ,name (category)
357               (list 'cdr (list 'assq '',prop-name category)))
358
359             (defsetf ,name (category) (value)
360               (list 'gnus-agent-cat-set-property
361                     category '',prop-name value))))
362   )
363
364 (defmacro gnus-agent-cat-name (category)
365   `(car ,category))
366
367 (gnus-agent-cat-defaccessor
368  gnus-agent-cat-days-until-old             agent-days-until-old)
369 (gnus-agent-cat-defaccessor
370  gnus-agent-cat-enable-expiration          agent-enable-expiration)
371 (gnus-agent-cat-defaccessor
372  gnus-agent-cat-groups                     agent-groups)
373 (gnus-agent-cat-defaccessor
374  gnus-agent-cat-high-score                 agent-high-score)
375 (gnus-agent-cat-defaccessor
376  gnus-agent-cat-length-when-long           agent-long-article)
377 (gnus-agent-cat-defaccessor
378  gnus-agent-cat-length-when-short          agent-short-article)
379 (gnus-agent-cat-defaccessor
380  gnus-agent-cat-low-score                  agent-low-score)
381 (gnus-agent-cat-defaccessor
382  gnus-agent-cat-predicate                  agent-predicate)
383 (gnus-agent-cat-defaccessor
384  gnus-agent-cat-score-file                 agent-score)
385 (gnus-agent-cat-defaccessor
386  gnus-agent-cat-enable-undownloaded-faces  agent-enable-undownloaded-faces)
387
388
389 ;; This form may expand to code that uses CL functions at run-time,
390 ;; but that's OK since those functions will only ever be called from
391 ;; something like `setf', so only when CL is loaded anyway.
392 (defsetf gnus-agent-cat-groups gnus-agent-set-cat-groups)
393
394 (defun gnus-agent-set-cat-groups (category groups)
395   (unless (eq groups 'ignore)
396     (let ((new-g groups)
397           (old-g (gnus-agent-cat-groups category)))
398       (cond ((eq new-g old-g)
399              ;; gnus-agent-add-group is fiddling with the group
400              ;; list. Still, Im done.
401              nil
402              )
403             ((eq new-g (cdr old-g))
404              ;; gnus-agent-add-group is fiddling with the group list
405              (setcdr (or (assq 'agent-groups category)
406                          (let ((cell (cons 'agent-groups nil)))
407                            (setcdr category (cons cell (cdr category)))
408                            cell)) new-g))
409             (t
410              (let ((groups groups))
411                (while groups
412                  (let* ((group        (pop groups))
413                         (old-category (gnus-group-category group)))
414                    (if (eq category old-category)
415                        nil
416                      (setf (gnus-agent-cat-groups old-category)
417                            (delete group (gnus-agent-cat-groups
418                                           old-category))))))
419                ;; Purge cache as preceding loop invalidated it.
420                (setq gnus-category-group-cache nil))
421
422              (setcdr (or (assq 'agent-groups category)
423                          (let ((cell (cons 'agent-groups nil)))
424                            (setcdr category (cons cell (cdr category)))
425                            cell)) groups))))))
426
427 (defsubst gnus-agent-cat-make (name &optional default-agent-predicate)
428   (list name `(agent-predicate . ,(or default-agent-predicate 'false))))
429
430 (defun gnus-agent-read-group ()
431   "Read a group name in the minibuffer, with completion."
432   (let ((def (or (gnus-group-group-name) gnus-newsgroup-name)))
433     (when def
434       (setq def (gnus-group-decoded-name def)))
435     (gnus-group-completing-read nil nil t nil nil def)))
436
437 ;;; Fetching setup functions.
438
439 (defun gnus-agent-start-fetch ()
440   "Initialize data structures for efficient fetching."
441   (gnus-agent-create-buffer))
442
443 (defun gnus-agent-stop-fetch ()
444   "Save all data structures and clean up."
445   (setq gnus-agent-spam-hashtb nil)
446   (with-current-buffer nntp-server-buffer
447     (widen)))
448
449 (defmacro gnus-agent-with-fetch (&rest forms)
450   "Do FORMS safely."
451   `(unwind-protect
452        (let ((gnus-agent-fetching t))
453          (gnus-agent-start-fetch)
454          ,@forms)
455      (gnus-agent-stop-fetch)))
456
457 (put 'gnus-agent-with-fetch 'lisp-indent-function 0)
458 (put 'gnus-agent-with-fetch 'edebug-form-spec '(body))
459
460 (defmacro gnus-agent-append-to-list (tail value)
461   `(setq ,tail (setcdr ,tail (cons ,value nil))))
462
463 (defmacro gnus-agent-message (level &rest args)
464   `(if (<= ,level gnus-verbose)
465        (message ,@args)))
466
467 ;;;
468 ;;; Mode infestation
469 ;;;
470
471 (defvar gnus-agent-mode-hook nil
472   "Hook run when installing agent mode.")
473
474 (defvar gnus-agent-mode nil)
475 (defvar gnus-agent-mode-status '(gnus-agent-mode " Plugged"))
476
477 (defun gnus-agent-mode ()
478   "Minor mode for providing a agent support in Gnus buffers."
479   (let* ((buffer (progn (string-match "^gnus-\\(.*\\)-mode$"
480                                       (symbol-name major-mode))
481                         (match-string 1 (symbol-name major-mode))))
482          (mode (intern (format "gnus-agent-%s-mode" buffer))))
483     (set (make-local-variable 'gnus-agent-mode) t)
484     (set mode nil)
485     (set (make-local-variable mode) t)
486     ;; Set up the menu.
487     (when (gnus-visual-p 'agent-menu 'menu)
488       (funcall (intern (format "gnus-agent-%s-make-menu-bar" buffer))))
489     (unless (assq mode minor-mode-alist)
490       (push (cons mode (cdr gnus-agent-mode-status)) minor-mode-alist))
491     (unless (assq mode minor-mode-map-alist)
492       (push (cons mode (symbol-value (intern (format "gnus-agent-%s-mode-map"
493                                                      buffer))))
494             minor-mode-map-alist))
495     (when (derived-mode-p 'gnus-group-mode)
496       (let ((init-plugged gnus-plugged)
497             (gnus-agent-go-online nil))
498         ;; g-a-t-p does nothing when gnus-plugged isn't changed.
499         ;; Therefore, make certain that the current value does not
500         ;; match the desired initial value.
501         (setq gnus-plugged :unknown)
502         (gnus-agent-toggle-plugged init-plugged)))
503     (gnus-run-hooks 'gnus-agent-mode-hook
504                     (intern (format "gnus-agent-%s-mode-hook" buffer)))))
505
506 (defvar gnus-agent-group-mode-map (make-sparse-keymap))
507 (gnus-define-keys gnus-agent-group-mode-map
508   "Ju" gnus-agent-fetch-groups
509   "Jc" gnus-enter-category-buffer
510   "Jj" gnus-agent-toggle-plugged
511   "Js" gnus-agent-fetch-session
512   "JY" gnus-agent-synchronize-flags
513   "JS" gnus-group-send-queue
514   "Ja" gnus-agent-add-group
515   "Jr" gnus-agent-remove-group
516   "Jo" gnus-agent-toggle-group-plugged)
517
518 (defun gnus-agent-group-make-menu-bar ()
519   (unless (boundp 'gnus-agent-group-menu)
520     (easy-menu-define
521      gnus-agent-group-menu gnus-agent-group-mode-map ""
522      '("Agent"
523        ["Toggle plugged" gnus-agent-toggle-plugged t]
524        ["Toggle group plugged" gnus-agent-toggle-group-plugged t]
525        ["List categories" gnus-enter-category-buffer t]
526        ["Add (current) group to category" gnus-agent-add-group t]
527        ["Remove (current) group from category" gnus-agent-remove-group t]
528        ["Send queue" gnus-group-send-queue gnus-plugged]
529        ("Fetch"
530         ["All" gnus-agent-fetch-session gnus-plugged]
531         ["Group" gnus-agent-fetch-group gnus-plugged])
532        ["Synchronize flags" gnus-agent-synchronize-flags t]
533        ))))
534
535 (defvar gnus-agent-summary-mode-map (make-sparse-keymap))
536 (gnus-define-keys gnus-agent-summary-mode-map
537   "Jj" gnus-agent-toggle-plugged
538   "Ju" gnus-agent-summary-fetch-group
539   "JS" gnus-agent-fetch-group
540   "Js" gnus-agent-summary-fetch-series
541   "J#" gnus-agent-mark-article
542   "J\M-#" gnus-agent-unmark-article
543   "@" gnus-agent-toggle-mark
544   "Jc" gnus-agent-catchup)
545
546 (defun gnus-agent-summary-make-menu-bar ()
547   (unless (boundp 'gnus-agent-summary-menu)
548     (easy-menu-define
549      gnus-agent-summary-menu gnus-agent-summary-mode-map ""
550      '("Agent"
551        ["Toggle plugged" gnus-agent-toggle-plugged t]
552        ["Mark as downloadable" gnus-agent-mark-article t]
553        ["Unmark as downloadable" gnus-agent-unmark-article t]
554        ["Toggle mark" gnus-agent-toggle-mark t]
555        ["Fetch downloadable" gnus-agent-summary-fetch-group t]
556        ["Catchup undownloaded" gnus-agent-catchup t]))))
557
558 (defvar gnus-agent-server-mode-map (make-sparse-keymap))
559 (gnus-define-keys gnus-agent-server-mode-map
560   "Jj" gnus-agent-toggle-plugged
561   "Ja" gnus-agent-add-server
562   "Jr" gnus-agent-remove-server)
563
564 (defun gnus-agent-server-make-menu-bar ()
565   (unless (boundp 'gnus-agent-server-menu)
566     (easy-menu-define
567      gnus-agent-server-menu gnus-agent-server-mode-map ""
568      '("Agent"
569        ["Toggle plugged" gnus-agent-toggle-plugged t]
570        ["Add" gnus-agent-add-server t]
571        ["Remove" gnus-agent-remove-server t]))))
572
573 (defun gnus-agent-make-mode-line-string (string mouse-button mouse-func)
574   (if (and (fboundp 'propertize)
575            (fboundp 'make-mode-line-mouse-map))
576       (propertize string 'local-map
577                   (make-mode-line-mouse-map mouse-button mouse-func)
578                   'mouse-face
579                   (if (and (featurep 'xemacs)
580                            ;; XEmacs's `facep' only checks for a face
581                            ;; object, not for a face name, so it's useless
582                            ;; to check with `facep'.
583                            (find-face 'modeline))
584                       'modeline
585                     'mode-line-highlight))
586     string))
587
588 (defun gnus-agent-toggle-plugged (set-to)
589   "Toggle whether Gnus is unplugged or not."
590   (interactive (list (not gnus-plugged)))
591   (cond ((eq set-to gnus-plugged)
592          nil)
593         (set-to
594          (setq gnus-plugged set-to)
595          (gnus-run-hooks 'gnus-agent-plugged-hook)
596          (setcar (cdr gnus-agent-mode-status)
597                  (gnus-agent-make-mode-line-string " Plugged"
598                                                    'mouse-2
599                                                    'gnus-agent-toggle-plugged))
600          (gnus-agent-go-online gnus-agent-go-online))
601         (t
602          (gnus-agent-close-connections)
603          (setq gnus-plugged set-to)
604          (gnus-run-hooks 'gnus-agent-unplugged-hook)
605          (setcar (cdr gnus-agent-mode-status)
606                  (gnus-agent-make-mode-line-string " Unplugged"
607                                                    'mouse-2
608                                                    'gnus-agent-toggle-plugged))))
609   (set-buffer-modified-p t))
610
611 (defmacro gnus-agent-while-plugged (&rest body)
612   `(let ((original-gnus-plugged gnus-plugged))
613     (unwind-protect
614         (progn (gnus-agent-toggle-plugged t)
615                ,@body)
616       (gnus-agent-toggle-plugged original-gnus-plugged))))
617
618 (put 'gnus-agent-while-plugged 'lisp-indent-function 0)
619 (put 'gnus-agent-while-plugged 'edebug-form-spec '(body))
620
621 (defun gnus-agent-close-connections ()
622   "Close all methods covered by the Gnus agent."
623   (let ((methods (gnus-agent-covered-methods)))
624     (while methods
625       (gnus-close-server (pop methods)))))
626
627 ;;;###autoload
628 (defun gnus-unplugged ()
629   "Start Gnus unplugged."
630   (interactive)
631   (setq gnus-plugged nil)
632   (gnus))
633
634 ;;;###autoload
635 (defun gnus-plugged ()
636   "Start Gnus plugged."
637   (interactive)
638   (setq gnus-plugged t)
639   (gnus))
640
641 ;;;###autoload
642 (defun gnus-slave-unplugged (&optional arg)
643   "Read news as a slave unplugged."
644   (interactive "P")
645   (setq gnus-plugged nil)
646   (gnus arg nil 'slave))
647
648 ;;;###autoload
649 (defun gnus-agentize ()
650   "Allow Gnus to be an offline newsreader.
651
652 The gnus-agentize function is now called internally by gnus when
653 gnus-agent is set.  If you wish to avoid calling gnus-agentize,
654 customize gnus-agent to nil.
655
656 This will modify the `gnus-setup-news-hook', and
657 `message-send-mail-real-function' variables, and install the Gnus agent
658 minor mode in all Gnus buffers."
659   (interactive)
660   (gnus-open-agent)
661   (setq message-send-mail-real-function 'gnus-agent-send-mail)
662
663   ;; If the servers file doesn't exist, auto-agentize some servers and
664   ;; save the servers file so this auto-agentizing isn't invoked
665   ;; again.
666   (when (and (not (file-exists-p (nnheader-concat
667                                   gnus-agent-directory "lib/servers")))
668              gnus-agent-auto-agentize-methods)
669     (gnus-message 3 "First time agent user, agentizing remote groups...")
670     (mapc
671      (lambda (server-or-method)
672        (let ((method (gnus-server-to-method server-or-method)))
673          (when (memq (car method)
674                      gnus-agent-auto-agentize-methods)
675            (push (gnus-method-to-server method)
676                  gnus-agent-covered-methods)
677            (setq gnus-agent-method-p-cache nil))))
678      (cons gnus-select-method gnus-secondary-select-methods))
679     (gnus-agent-write-servers)))
680
681 (defun gnus-agent-queue-setup (&optional group-name)
682   "Make sure the queue group exists.
683 Optional arg GROUP-NAME allows to specify another group."
684   (unless (gnus-gethash (format "nndraft:%s" (or group-name "queue"))
685                         gnus-newsrc-hashtb)
686     (gnus-request-create-group (or group-name "queue") '(nndraft ""))
687     (let ((gnus-level-default-subscribed 1))
688       (gnus-subscribe-group (format "nndraft:%s" (or group-name "queue"))
689                             nil '(nndraft "")))
690     (gnus-group-set-parameter
691      (format "nndraft:%s" (or group-name "queue"))
692      'gnus-dummy '((gnus-draft-mode)))))
693
694 (defun gnus-agent-send-mail ()
695   (if (or (not gnus-agent-queue-mail)
696           (and gnus-plugged (not (eq gnus-agent-queue-mail 'always))))
697       (message-multi-smtp-send-mail)
698     (goto-char (point-min))
699     (re-search-forward
700      (concat "^" (regexp-quote mail-header-separator) "\n"))
701     (replace-match "\n")
702     (gnus-agent-insert-meta-information 'mail)
703     (gnus-request-accept-article "nndraft:queue" nil t t)
704     (gnus-group-refresh-group "nndraft:queue")))
705
706 (defun gnus-agent-insert-meta-information (type &optional method)
707   "Insert meta-information into the message that says how it's to be posted.
708 TYPE can be either `mail' or `news'.  If the latter, then METHOD can
709 be a select method."
710   (save-excursion
711     (message-remove-header gnus-agent-meta-information-header)
712     (goto-char (point-min))
713     (insert gnus-agent-meta-information-header ": "
714             (symbol-name type) " " (format "%S" method)
715             "\n")
716     (forward-char -1)
717     (while (search-backward "\n" nil t)
718       (replace-match "\\n" t t))))
719
720 (defun gnus-agent-restore-gcc ()
721   "Restore GCC field from saved header."
722   (save-excursion
723     (goto-char (point-min))
724     (while (re-search-forward
725             (concat "^" (regexp-quote gnus-agent-gcc-header) ":") nil t)
726       (replace-match "Gcc:" 'fixedcase))))
727
728 (defun gnus-agent-any-covered-gcc ()
729   (save-restriction
730     (message-narrow-to-headers)
731     (let* ((gcc (mail-fetch-field "gcc" nil t))
732            (methods (and gcc
733                          (mapcar 'gnus-inews-group-method
734                                  (message-unquote-tokens
735                                   (message-tokenize-header
736                                    gcc " ,")))))
737            covered)
738       (while (and (not covered) methods)
739         (setq covered (gnus-agent-method-p (car methods))
740               methods (cdr methods)))
741       covered)))
742
743 ;;;###autoload
744 (defun gnus-agent-possibly-save-gcc ()
745   "Save GCC if Gnus is unplugged."
746   (when (and (not gnus-plugged) (gnus-agent-any-covered-gcc))
747     (save-excursion
748       (goto-char (point-min))
749       (let ((case-fold-search t))
750         (while (re-search-forward "^gcc:" nil t)
751           (replace-match (concat gnus-agent-gcc-header ":") 'fixedcase))))))
752
753 (defun gnus-agent-possibly-do-gcc ()
754   "Do GCC if Gnus is plugged."
755   (when (or gnus-plugged (not (gnus-agent-any-covered-gcc)))
756     (gnus-inews-do-gcc)))
757
758 ;;;
759 ;;; Group mode commands
760 ;;;
761
762 (defun gnus-agent-fetch-groups (n)
763   "Put all new articles in the current groups into the Agent."
764   (interactive "P")
765   (unless gnus-plugged
766     (error "Groups can't be fetched when Gnus is unplugged"))
767   (gnus-group-iterate n 'gnus-agent-fetch-group))
768
769 (defun gnus-agent-fetch-group (&optional group)
770   "Put all new articles in GROUP into the Agent."
771   (interactive (list (gnus-group-group-name)))
772   (setq group (or group gnus-newsgroup-name))
773   (unless group
774     (error "No group on the current line"))
775   (if (not (gnus-agent-group-covered-p group))
776       (message "%s isn't covered by the agent" group)
777     (gnus-agent-while-plugged
778       (let ((gnus-command-method (gnus-find-method-for-group group)))
779         (gnus-agent-with-fetch
780           (gnus-agent-fetch-group-1 group gnus-command-method)
781           (gnus-message 5 "Fetching %s...done" group))))))
782
783 (defun gnus-agent-add-group (category arg)
784   "Add the current group to an agent category."
785   (interactive
786    (list
787     (intern
788      (gnus-completing-read
789       "Add to category"
790       (mapcar (lambda (cat) (symbol-name (car cat)))
791               gnus-category-alist)
792       t))
793     current-prefix-arg))
794   (let ((cat (assq category gnus-category-alist))
795         c groups)
796     (gnus-group-iterate arg
797       (lambda (group)
798         (when (gnus-agent-cat-groups (setq c (gnus-group-category group)))
799           (setf (gnus-agent-cat-groups c)
800                 (delete group (gnus-agent-cat-groups c))))
801         (push group groups)))
802     (setf (gnus-agent-cat-groups cat)
803           (nconc (gnus-agent-cat-groups cat) groups))
804     (gnus-category-write)))
805
806 (defun gnus-agent-remove-group (arg)
807   "Remove the current group from its agent category, if any."
808   (interactive "P")
809   (let (c)
810     (gnus-group-iterate arg
811       (lambda (group)
812         (when (gnus-agent-cat-groups (setq c (gnus-group-category group)))
813           (setf (gnus-agent-cat-groups c)
814                 (delete group (gnus-agent-cat-groups c))))))
815     (gnus-category-write)))
816
817 (defun gnus-agent-synchronize-flags ()
818   "Synchronize unplugged flags with servers."
819   (interactive)
820   (save-excursion
821     (dolist (gnus-command-method (gnus-agent-covered-methods))
822       (when (file-exists-p (gnus-agent-lib-file "flags"))
823         (gnus-agent-synchronize-flags-server gnus-command-method)))))
824
825 (defun gnus-agent-possibly-synchronize-flags ()
826   "Synchronize flags according to `gnus-agent-synchronize-flags'."
827   (interactive)
828   (save-excursion
829     (dolist (gnus-command-method (gnus-agent-covered-methods))
830       (when (eq (gnus-server-status gnus-command-method) 'ok)
831         (gnus-agent-possibly-synchronize-flags-server gnus-command-method)))))
832
833 (defun gnus-agent-synchronize-flags-server (method)
834   "Synchronize flags set when unplugged for server."
835   (let ((gnus-command-method method)
836         (gnus-agent nil))
837     (when (file-exists-p (gnus-agent-lib-file "flags"))
838       (set-buffer (get-buffer-create " *Gnus Agent flag synchronize*"))
839       (erase-buffer)
840       (nnheader-insert-file-contents (gnus-agent-lib-file "flags"))
841       (cond ((null gnus-plugged)
842              (gnus-message
843               1 "You must be plugged to synchronize flags with server %s"
844               (nth 1 gnus-command-method)))
845             ((null (gnus-check-server gnus-command-method))
846              (gnus-message
847               1 "Couldn't open server %s" (nth 1 gnus-command-method)))
848             (t
849              (condition-case err
850                  (while t
851                    (let ((bgn (point)))
852                      (eval (read (current-buffer)))
853                      (delete-region bgn (point))))
854                (end-of-file
855                 (delete-file (gnus-agent-lib-file "flags")))
856                (error
857                 (let ((file (gnus-agent-lib-file "flags")))
858                   (write-region (point-min) (point-max)
859                                 (gnus-agent-lib-file "flags") nil 'silent)
860                   (error "Couldn't set flags from file %s due to %s"
861                          file (error-message-string err)))))))
862       (kill-buffer nil))))
863
864 (defun gnus-agent-possibly-synchronize-flags-server (method)
865   "Synchronize flags for server according to `gnus-agent-synchronize-flags'."
866   (when (and (file-exists-p (gnus-agent-lib-file "flags"))
867              (or (and gnus-agent-synchronize-flags
868                       (not (eq gnus-agent-synchronize-flags 'ask)))
869                  (and (eq gnus-agent-synchronize-flags 'ask)
870                       (gnus-y-or-n-p
871                        (format "Synchronize flags on server `%s'? "
872                                (cadr method))))))
873     (gnus-agent-synchronize-flags-server method)))
874
875 ;;;###autoload
876 (defun gnus-agent-rename-group (old-group new-group)
877   "Rename fully-qualified OLD-GROUP as NEW-GROUP.
878 Always updates the agent, even when disabled, as the old agent
879 files would corrupt gnus when the agent was next enabled.
880 Depends upon the caller to determine whether group renaming is
881 supported."
882   (let* ((old-command-method (gnus-find-method-for-group old-group))
883          (old-path           (directory-file-name
884                               (let ((gnus-command-method old-command-method))
885                                 (gnus-agent-group-pathname old-group))))
886          (new-command-method (gnus-find-method-for-group new-group))
887          (new-path           (directory-file-name
888                               (let ((gnus-command-method new-command-method))
889                                 (gnus-agent-group-pathname new-group))))
890          (file-name-coding-system nnmail-pathname-coding-system))
891     (gnus-rename-file old-path new-path t)
892
893     (let* ((old-real-group (gnus-group-real-name old-group))
894            (new-real-group (gnus-group-real-name new-group))
895            (old-active (gnus-agent-get-group-info old-command-method old-real-group)))
896       (gnus-agent-save-group-info old-command-method old-real-group nil)
897       (gnus-agent-save-group-info new-command-method new-real-group old-active)
898
899       (let ((old-local (gnus-agent-get-local old-group
900                                              old-real-group old-command-method)))
901         (gnus-agent-set-local old-group
902                               nil nil
903                               old-real-group old-command-method)
904         (gnus-agent-set-local new-group
905                               (car old-local) (cdr old-local)
906                               new-real-group new-command-method)))))
907
908 ;;;###autoload
909 (defun gnus-agent-delete-group (group)
910   "Delete fully-qualified GROUP.
911 Always updates the agent, even when disabled, as the old agent
912 files would corrupt gnus when the agent was next enabled.
913 Depends upon the caller to determine whether group deletion is
914 supported."
915   (let* ((command-method (gnus-find-method-for-group group))
916          (path           (directory-file-name
917                           (let ((gnus-command-method command-method))
918                             (gnus-agent-group-pathname group))))
919          (file-name-coding-system nnmail-pathname-coding-system))
920     (gnus-delete-directory path)
921
922     (let* ((real-group (gnus-group-real-name group)))
923       (gnus-agent-save-group-info command-method real-group nil)
924       ;; FIXME: Does gnus-agent-get-local have any useful side-effect?
925       (gnus-agent-get-local group real-group command-method)
926       (gnus-agent-set-local group
927                             nil nil
928                             real-group command-method))))
929
930 ;;;
931 ;;; Server mode commands
932 ;;;
933
934 (defun gnus-agent-add-server ()
935   "Enroll SERVER in the agent program."
936   (interactive)
937   (let* ((server       (gnus-server-server-name))
938          (named-server (gnus-server-named-server))
939          (method       (and server
940                             (gnus-server-get-method nil server))))
941     (unless server
942       (error "No server on the current line"))
943
944     (when (gnus-agent-method-p method)
945       (error "Server already in the agent program"))
946
947     (push named-server gnus-agent-covered-methods)
948
949     (setq gnus-agent-method-p-cache nil)
950     (gnus-server-update-server server)
951     (gnus-agent-write-servers)
952     (gnus-message 1 "Entered %s into the Agent" server)))
953
954 (defun gnus-agent-remove-server ()
955   "Remove SERVER from the agent program."
956   (interactive)
957   (let* ((server       (gnus-server-server-name))
958          (named-server (gnus-server-named-server)))
959     (unless server
960       (error "No server on the current line"))
961
962     (unless (member named-server gnus-agent-covered-methods)
963       (error "Server not in the agent program"))
964
965     (setq gnus-agent-covered-methods
966           (delete named-server gnus-agent-covered-methods)
967           gnus-agent-method-p-cache nil)
968
969     (gnus-server-update-server server)
970     (gnus-agent-write-servers)
971     (gnus-message 1 "Removed %s from the agent" server)))
972
973 (defun gnus-agent-read-servers ()
974   "Read the alist of covered servers."
975   (setq gnus-agent-covered-methods
976         (gnus-agent-read-file
977          (nnheader-concat gnus-agent-directory "lib/servers"))
978         gnus-agent-method-p-cache nil)
979
980   ;; I am called so early in start-up that I can not validate server
981   ;; names.  When that is the case, I skip the validation.  That is
982   ;; alright as the gnus startup code calls the validate methods
983   ;; directly.
984   (if gnus-server-alist
985       (gnus-agent-read-servers-validate)))
986
987 (defun gnus-agent-read-servers-validate ()
988   (mapcar (lambda (server-or-method)
989             (let* ((server (if (stringp server-or-method)
990                                server-or-method
991                              (gnus-method-to-server server-or-method)))
992                    (method (gnus-server-to-method server)))
993               (if method
994                   (unless (member server gnus-agent-covered-methods)
995                     (push server gnus-agent-covered-methods)
996                     (setq gnus-agent-method-p-cache nil))
997                 (gnus-message 8 "Ignoring disappeared server `%s'" server))))
998           (prog1 gnus-agent-covered-methods
999             (setq gnus-agent-covered-methods nil))))
1000
1001 (defun gnus-agent-read-servers-validate-native (native-method)
1002   (setq gnus-agent-covered-methods
1003         (mapcar (lambda (method)
1004                   (if (or (not method)
1005                           (equal method native-method))
1006                       "native"
1007                     method)) gnus-agent-covered-methods)))
1008
1009 (defun gnus-agent-write-servers ()
1010   "Write the alist of covered servers."
1011   (gnus-make-directory (nnheader-concat gnus-agent-directory "lib"))
1012   (let ((coding-system-for-write nnheader-file-coding-system)
1013         (file-name-coding-system nnmail-pathname-coding-system))
1014     (with-temp-file (nnheader-concat gnus-agent-directory "lib/servers")
1015       (prin1 gnus-agent-covered-methods
1016              (current-buffer)))))
1017
1018 ;;;
1019 ;;; Summary commands
1020 ;;;
1021
1022 (defun gnus-agent-mark-article (n &optional unmark)
1023   "Mark the next N articles as downloadable.
1024 If N is negative, mark backward instead.  If UNMARK is non-nil, remove
1025 the mark instead.  The difference between N and the actual number of
1026 articles marked is returned."
1027   (interactive "p")
1028   (let ((backward (< n 0))
1029         (n (abs n)))
1030     (while (and
1031             (> n 0)
1032             (progn
1033               (gnus-summary-set-agent-mark
1034                (gnus-summary-article-number) unmark)
1035               (zerop (gnus-summary-next-subject (if backward -1 1) nil t))))
1036       (setq n (1- n)))
1037     (when (/= 0 n)
1038       (gnus-message 7 "No more articles"))
1039     (gnus-summary-recenter)
1040     (gnus-summary-position-point)
1041     n))
1042
1043 (defun gnus-agent-unmark-article (n)
1044   "Remove the downloadable mark from the next N articles.
1045 If N is negative, unmark backward instead.  The difference between N and
1046 the actual number of articles unmarked is returned."
1047   (interactive "p")
1048   (gnus-agent-mark-article n t))
1049
1050 (defun gnus-agent-toggle-mark (n)
1051   "Toggle the downloadable mark from the next N articles.
1052 If N is negative, toggle backward instead.  The difference between N and
1053 the actual number of articles toggled is returned."
1054   (interactive "p")
1055   (gnus-agent-mark-article n 'toggle))
1056
1057 (defun gnus-summary-set-agent-mark (article &optional unmark)
1058   "Mark ARTICLE as downloadable.  If UNMARK is nil, article is marked.
1059 When UNMARK is t, the article is unmarked.  For any other value, the
1060 article's mark is toggled."
1061   (let ((unmark (cond ((eq nil unmark)
1062                        nil)
1063                       ((eq t unmark)
1064                        t)
1065                       (t
1066                        (memq article gnus-newsgroup-downloadable)))))
1067     (when (gnus-summary-goto-subject article nil t)
1068       (gnus-summary-update-mark
1069        (if unmark
1070            (progn
1071              (setq gnus-newsgroup-downloadable
1072                    (delq article gnus-newsgroup-downloadable))
1073              (gnus-article-mark article))
1074          (setq gnus-newsgroup-downloadable
1075                (gnus-add-to-sorted-list gnus-newsgroup-downloadable article))
1076          gnus-downloadable-mark)
1077        'unread))))
1078
1079 ;;;###autoload
1080 (defun gnus-agent-get-undownloaded-list ()
1081   "Construct list of articles that have not been downloaded."
1082   (let ((gnus-command-method (gnus-find-method-for-group gnus-newsgroup-name)))
1083     (when (set (make-local-variable 'gnus-newsgroup-agentized)
1084                (gnus-agent-method-p gnus-command-method))
1085       (let* ((alist (gnus-agent-load-alist gnus-newsgroup-name))
1086              (headers (sort (mapcar (lambda (h)
1087                                       (mail-header-number h))
1088                                     gnus-newsgroup-headers) '<))
1089              (cached (and gnus-use-cache gnus-newsgroup-cached))
1090              (undownloaded (list nil))
1091              (tail-undownloaded undownloaded)
1092              (unfetched (list nil))
1093              (tail-unfetched unfetched))
1094         (while (and alist headers)
1095           (let ((a (caar alist))
1096                 (h (car headers)))
1097             (cond ((< a h)
1098                    ;; Ignore IDs in the alist that are not being
1099                    ;; displayed in the summary.
1100                    (setq alist (cdr alist)))
1101                   ((> a h)
1102                    ;; Headers that are not in the alist should be
1103                    ;; fictitious (see nnagent-retrieve-headers); they
1104                    ;; imply that this article isn't in the agent.
1105                    (gnus-agent-append-to-list tail-undownloaded h)
1106                    (gnus-agent-append-to-list tail-unfetched    h)
1107                    (setq headers (cdr headers)))
1108                   ((cdar alist)
1109                    (setq alist (cdr alist))
1110                    (setq headers (cdr headers))
1111                    nil                  ; ignore already downloaded
1112                    )
1113                   (t
1114                    (setq alist (cdr alist))
1115                    (setq headers (cdr headers))
1116
1117                    ;; This article isn't in the agent.  Check to see
1118                    ;; if it is in the cache.  If it is, it's been
1119                    ;; downloaded.
1120                    (while (and cached (< (car cached) a))
1121                      (setq cached (cdr cached)))
1122                    (unless (equal a (car cached))
1123                      (gnus-agent-append-to-list tail-undownloaded a))))))
1124
1125         (while headers
1126           (let ((num (pop headers)))
1127             (gnus-agent-append-to-list tail-undownloaded num)
1128             (gnus-agent-append-to-list tail-unfetched    num)))
1129
1130         (setq gnus-newsgroup-undownloaded (cdr undownloaded)
1131               gnus-newsgroup-unfetched    (cdr unfetched))))))
1132
1133 (defun gnus-agent-catchup ()
1134   "Mark as read all unhandled articles.
1135 An article is unhandled if it is neither cached, nor downloaded, nor
1136 downloadable."
1137   (interactive)
1138   (save-excursion
1139     (let ((articles gnus-newsgroup-undownloaded))
1140       (when (or gnus-newsgroup-downloadable
1141                 gnus-newsgroup-cached)
1142         (setq articles (gnus-sorted-ndifference
1143                         (gnus-sorted-ndifference
1144                          (gnus-copy-sequence articles)
1145                          gnus-newsgroup-downloadable)
1146                         gnus-newsgroup-cached)))
1147
1148       (while articles
1149         (gnus-summary-mark-article
1150          (pop articles) gnus-catchup-mark)))
1151     (gnus-summary-position-point)))
1152
1153 (defun gnus-agent-summary-fetch-series ()
1154   "Fetch the process-marked articles into the Agent."
1155   (interactive)
1156   (when gnus-newsgroup-processable
1157     (setq gnus-newsgroup-downloadable
1158           (let* ((dl gnus-newsgroup-downloadable)
1159                  (processable (sort (gnus-copy-sequence gnus-newsgroup-processable) '<))
1160                  (gnus-newsgroup-downloadable processable))
1161             (gnus-agent-summary-fetch-group)
1162
1163             ;; For each article that I processed that is no longer
1164             ;; undownloaded, remove its processable mark.
1165
1166             (mapc #'gnus-summary-remove-process-mark
1167                   (gnus-sorted-ndifference gnus-newsgroup-processable gnus-newsgroup-undownloaded))
1168
1169             ;; The preceding call to (gnus-agent-summary-fetch-group)
1170             ;; updated the temporary gnus-newsgroup-downloadable to
1171             ;; remove each article successfully fetched.  Now, I
1172             ;; update the real gnus-newsgroup-downloadable to only
1173             ;; include undownloaded articles.
1174             (gnus-sorted-ndifference dl (gnus-sorted-ndifference processable gnus-newsgroup-undownloaded))))))
1175
1176 (defun gnus-agent-summary-fetch-group (&optional all)
1177   "Fetch the downloadable articles in the group.
1178 Optional arg ALL, if non-nil, means to fetch all articles."
1179   (interactive "P")
1180   (let ((articles
1181          (if all gnus-newsgroup-articles
1182            gnus-newsgroup-downloadable))
1183         (gnus-command-method (gnus-find-method-for-group gnus-newsgroup-name))
1184         fetched-articles)
1185     (gnus-agent-while-plugged
1186       (unless articles
1187         (error "No articles to download"))
1188       (gnus-agent-with-fetch
1189         (setq gnus-newsgroup-undownloaded
1190               (gnus-sorted-ndifference
1191                gnus-newsgroup-undownloaded
1192                (setq fetched-articles
1193                      (gnus-agent-fetch-articles
1194                       gnus-newsgroup-name articles)))))
1195       (save-excursion
1196         (dolist (article articles)
1197           (let ((was-marked-downloadable
1198                  (memq article gnus-newsgroup-downloadable)))
1199             (cond (gnus-agent-mark-unread-after-downloaded
1200                    (setq gnus-newsgroup-downloadable
1201                          (delq article gnus-newsgroup-downloadable))
1202                    (when (and (not (member article gnus-newsgroup-dormant))
1203                               (not (member article gnus-newsgroup-marked)))
1204                      (gnus-summary-mark-article article gnus-unread-mark)))
1205                   (was-marked-downloadable
1206                    (gnus-summary-set-agent-mark article t)))
1207             (when (gnus-summary-goto-subject article nil t)
1208               (gnus-summary-update-download-mark article))))))
1209     fetched-articles))
1210
1211 (defun gnus-agent-fetch-selected-article ()
1212   "Fetch the current article as it is selected.
1213 This can be added to `gnus-select-article-hook' or
1214 `gnus-mark-article-hook'."
1215   (let ((gnus-command-method gnus-current-select-method))
1216     (when (and gnus-plugged (gnus-agent-method-p gnus-command-method))
1217       (when (gnus-agent-fetch-articles
1218              gnus-newsgroup-name
1219              (list gnus-current-article))
1220         (setq gnus-newsgroup-undownloaded
1221               (delq gnus-current-article gnus-newsgroup-undownloaded))
1222         (gnus-summary-update-download-mark gnus-current-article)))))
1223
1224 ;;;
1225 ;;; Internal functions
1226 ;;;
1227
1228 (defun gnus-agent-synchronize-group-flags (group actions server)
1229 "Update a plugged group by performing the indicated actions."
1230   (let* ((gnus-command-method (gnus-server-to-method server))
1231          (info
1232           ;; This initializer is required as gnus-request-set-mark
1233           ;; calls gnus-group-real-name to strip off the host name
1234           ;; before calling the backend.  Now that the backend is
1235           ;; trying to call gnus-request-set-mark, I have to
1236           ;; reconstruct the original group name.
1237           (or (gnus-get-info group)
1238               (gnus-get-info
1239                (setq group (gnus-group-full-name
1240                             group gnus-command-method))))))
1241     (gnus-request-set-mark group actions)
1242
1243     (when info
1244       (dolist (action actions)
1245         (let ((range (nth 0 action))
1246               (what  (nth 1 action))
1247               (marks (nth 2 action)))
1248           (dolist (mark marks)
1249             (cond ((eq mark 'read)
1250                    (gnus-info-set-read
1251                     info
1252                     (funcall (if (eq what 'add)
1253                                  'gnus-range-add
1254                                'gnus-remove-from-range)
1255                              (gnus-info-read info)
1256                              range))
1257                    (gnus-get-unread-articles-in-group
1258                     info
1259                     (gnus-active (gnus-info-group info))))
1260                   ((memq mark '(tick))
1261                    (let ((info-marks (assoc mark (gnus-info-marks info))))
1262                      (unless info-marks
1263                        (gnus-info-set-marks info (cons (setq info-marks (list mark)) (gnus-info-marks info))))
1264                      (setcdr info-marks (funcall (if (eq what 'add)
1265                                   'gnus-range-add
1266                                 'gnus-remove-from-range)
1267                               (cdr info-marks)
1268                               range))))))))
1269
1270       ;;Marks can be synchronized at any time by simply toggling from
1271       ;;unplugged to plugged.  If that is what is happening right now, make
1272       ;;sure that the group buffer is up to date.
1273           (when (gnus-buffer-live-p gnus-group-buffer)
1274             (gnus-group-update-group group t)))
1275     nil))
1276
1277 (defun gnus-agent-save-active (method &optional groups-p)
1278   "Sync the agent's active file with the current buffer.
1279 Pass non-nil for GROUPS-P if the buffer starts out in groups format.
1280 Regardless, both the file and the buffer end up in active format
1281 if METHOD is agentized; otherwise the function is a no-op."
1282   (when (gnus-agent-method-p method)
1283     (let* ((gnus-command-method method)
1284            (new (gnus-make-hashtable (count-lines (point-min) (point-max))))
1285            (file (gnus-agent-lib-file "active")))
1286       (if groups-p
1287           (gnus-groups-to-gnus-format nil new)
1288         (gnus-active-to-gnus-format nil new))
1289       (gnus-agent-write-active file new)
1290       (erase-buffer)
1291       (let ((nnheader-file-coding-system gnus-agent-file-coding-system))
1292         (nnheader-insert-file-contents file)))))
1293
1294 (defun gnus-agent-write-active (file new)
1295     (gnus-make-directory (file-name-directory file))
1296     (let ((nnmail-active-file-coding-system gnus-agent-file-coding-system))
1297       ;; The hashtable contains real names of groups.  However, do NOT
1298       ;; add the foreign server prefix as gnus-active-to-gnus-format
1299       ;; will add it while reading the file.
1300       (gnus-write-active-file file new nil)))
1301
1302 ;;;###autoload
1303 (defun gnus-agent-possibly-alter-active (group active &optional info)
1304   "Possibly expand a group's active range to include articles
1305 downloaded into the agent."
1306   (let* ((gnus-command-method (or gnus-command-method
1307                                   (gnus-find-method-for-group group))))
1308     (when (gnus-agent-method-p gnus-command-method)
1309       (let* ((local (gnus-agent-get-local group))
1310              (active-min (or (car active) 0))
1311              (active-max (or (cdr active) 0))
1312              (agent-min (or (car local) active-min))
1313              (agent-max (or (cdr local) active-max)))
1314
1315         (when (< agent-min active-min)
1316           (setcar active agent-min))
1317
1318         (when (> agent-max active-max)
1319           (setcdr active agent-max))
1320
1321         (when (and info (< agent-max (- active-min 100)))
1322           ;; I'm expanding the active range by such a large amount
1323           ;; that there is a gap of more than 100 articles between the
1324           ;; last article known to the agent and the first article
1325           ;; currently available on the server.  This gap contains
1326           ;; articles that have been lost, mark them as read so that
1327           ;; gnus doesn't waste resources trying to fetch them.
1328
1329           ;; NOTE: I don't do this for smaller gaps (< 100) as I don't
1330           ;; want to modify the local file everytime someone restarts
1331           ;; gnus.  The small gap will cause a tiny performance hit
1332           ;; when gnus tries, and fails, to retrieve the articles.
1333           ;; Still that should be smaller than opening a buffer,
1334           ;; printing this list to the buffer, and then writing it to a
1335           ;; file.
1336
1337           (let ((read (gnus-info-read info)))
1338             (gnus-info-set-read
1339              info
1340              (gnus-range-add
1341               read
1342               (list (cons (1+ agent-max)
1343                           (1- active-min))))))
1344
1345           ;; Lie about the agent's local range for this group to
1346           ;; disable the set read each time this server is opened.
1347           ;; NOTE: Opening this group will restore the valid local
1348           ;; range but it will also expand the local range to
1349           ;; encompass the new active range.
1350           (gnus-agent-set-local group agent-min (1- active-min)))))))
1351
1352 (defun gnus-agent-save-group-info (method group active)
1353   "Update a single group's active range in the agent's copy of the server's active file."
1354   (when (gnus-agent-method-p method)
1355     (let* ((gnus-command-method (or method gnus-command-method))
1356            (coding-system-for-write nnheader-file-coding-system)
1357            (file-name-coding-system nnmail-pathname-coding-system)
1358            (file (gnus-agent-lib-file "active"))
1359            oactive-min oactive-max)
1360       (gnus-make-directory (file-name-directory file))
1361       (with-temp-file file
1362         ;; Emacs got problem to match non-ASCII group in multibyte buffer.
1363         (mm-disable-multibyte)
1364         (when (file-exists-p file)
1365           (nnheader-insert-file-contents file)
1366
1367           (goto-char (point-min))
1368           (when (re-search-forward
1369                  (concat "^" (regexp-quote group) " ") nil t)
1370             (save-excursion
1371               (setq oactive-max (read (current-buffer)) ;; max
1372                     oactive-min (read (current-buffer)))) ;; min
1373             (gnus-delete-line)))
1374         (when active
1375           (insert (format "%S %d %d y\n" (intern group)
1376                           (max (or oactive-max (cdr active)) (cdr active))
1377                           (min (or oactive-min (car active)) (car active))))
1378           (goto-char (point-max))
1379           (while (search-backward "\\." nil t)
1380             (delete-char 1)))))))