*** empty log message ***
[gnus] / lisp / gnus-agent.el
index 4029a56..12efc52 100644 (file)
@@ -1,9 +1,7 @@
 ;;; gnus-agent.el --- unplugged support for Gnus
-;; Copyright (C) 1997 Free Software Foundation, Inc.
-
-;; Author: Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
-;; Keywords: news
+;; Copyright (C) 1997,98 Free Software Foundation, Inc.
 
+;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
 ;; This file is part of GNU Emacs.
 
 ;; GNU Emacs is free software; you can redistribute it and/or modify
   :group 'gnus-agent
   :type 'hook)
 
+(defcustom gnus-agent-handle-level gnus-level-subscribed
+  "Groups on levels higher than this variable will be ignored by the Agent."
+  :group 'gnus-agent
+  :type 'integer)
+
+(defcustom gnus-agent-expire-days 7
+  "Read articles older than this will be expired."
+  :group 'gnus-agent
+  :type 'integer)
+
+(defcustom gnus-agent-expire-all nil
+  "If non-nil, also expire unread, ticked and dormant articles.
+If nil, only read articles will be expired."
+  :group 'gnus-agent
+  :type 'boolean)
+
+(defcustom gnus-agent-group-mode-hook nil
+  "Hook run in Agent group minor modes."
+  :group 'gnus-agent
+  :type 'hook)
+
+(defcustom gnus-agent-summary-mode-hook nil
+  "Hook run in Agent summary minor modes."
+  :group 'gnus-agent
+  :type 'hook)
+
+(defcustom gnus-agent-server-mode-hook nil
+  "Hook run in Agent summary minor modes."
+  :group 'gnus-agent
+  :type 'hook)
+
 ;;; Internal variables
 
+(defvar gnus-agent-meta-information-header "X-Gnus-Agent-Meta-Information")
+
 (defvar gnus-agent-history-buffers nil)
 (defvar gnus-agent-buffer-alist nil)
 (defvar gnus-agent-article-alist nil)
@@ -61,9 +92,7 @@
 (defvar gnus-agent-spam-hashtb nil)
 (defvar gnus-agent-file-name nil)
 (defvar gnus-agent-send-mail-function nil)
-
-(defvar gnus-plugged t
-  "Whether Gnus is plugged or not.")
+(defvar gnus-agent-article-file-coding-system 'no-conversion)
 
 ;; Dynamic variables
 (defvar gnus-headers)
 
 (defsubst gnus-agent-directory ()
   "Path of the Gnus agent directory."
-  (nnheader-concat gnus-agent-directory (gnus-agent-method) "/"))
+  (nnheader-concat gnus-agent-directory
+                  (nnheader-translate-file-chars (gnus-agent-method)) "/"))
 
 (defun gnus-agent-lib-file (file)
   "The full path of the Gnus agent library FILE."
   (concat (gnus-agent-directory) "agent.lib/" file))
 
+;;; Fetching setup functions.
+
+(defun gnus-agent-start-fetch ()
+  "Initialize data structures for efficient fetching."
+  (gnus-agent-open-history)
+  (setq gnus-agent-current-history (gnus-agent-history-buffer)))
+
+(defun gnus-agent-stop-fetch ()
+  "Save all data structures and clean up."
+  (gnus-agent-save-history)
+  (gnus-agent-close-history)
+  (setq gnus-agent-spam-hashtb nil)
+  (save-excursion
+    (set-buffer nntp-server-buffer)
+    (widen)))
+
+(defmacro gnus-agent-with-fetch (&rest forms)
+  "Do FORMS safely."
+  `(unwind-protect
+       (progn
+        (gnus-agent-start-fetch)
+        ,@forms)
+     (gnus-agent-stop-fetch)))
+
+(put 'gnus-agent-with-fetch 'lisp-indent-function 0)
+(put 'gnus-agent-with-fetch 'edebug-form-spec '(body))
+
 ;;;
 ;;; Mode infestation
 ;;;
                                                     buffer))))
            minor-mode-map-alist))
     (gnus-agent-toggle-plugged gnus-plugged)
-    (run-hooks 'gnus-agent-mode-hook)))
+    (gnus-run-hooks 'gnus-agent-mode-hook
+                   (intern (format "gnus-agent-%s-mode-hook" buffer)))))
 
 (defvar gnus-agent-group-mode-map (make-sparse-keymap))
 (gnus-define-keys gnus-agent-group-mode-map
-  "Ju" gnus-agent-fetch-group
+  "Ju" gnus-agent-fetch-groups
   "Jc" gnus-enter-category-buffer
   "Jj" gnus-agent-toggle-plugged
   "Js" gnus-agent-fetch-session
   (interactive (list (not gnus-plugged)))
   (if plugged
       (progn
-       (run-hooks 'gnus-agent-plugged-hook)
+       (setq gnus-plugged plugged)
+       (gnus-run-hooks 'gnus-agent-plugged-hook)
        (setcar (cdr gnus-agent-mode-status) " Plugged"))
     (gnus-agent-close-connections)
-    (run-hooks 'gnus-agent-unplugged-hook)
+    (setq gnus-plugged plugged)
+    (gnus-run-hooks 'gnus-agent-unplugged-hook)
     (setcar (cdr gnus-agent-mode-status) " Unplugged"))
-  (setq gnus-plugged plugged)
   (set-buffer-modified-p t))
 
 (defun gnus-agent-close-connections ()
   (setq gnus-plugged nil)
   (gnus))
 
+;;;###autoload
+(defun gnus-plugged ()
+  "Start Gnus plugged."
+  (interactive)
+  (setq gnus-plugged t)
+  (gnus))
+
 ;;;###autoload
 (defun gnus-agentize ()
   "Allow Gnus to be an offline newsreader.
@@ -242,7 +308,7 @@ This will modify the `gnus-before-startup-hook', `gnus-post-method',
 and `message-send-mail-function' variables, and install the Gnus
 agent minor mode in all Gnus buffers."
   (interactive)
-  (add-hook 'gnus-before-startup-hook 'gnus-open-agent)
+  (gnus-open-agent)
   (add-hook 'gnus-setup-news-hook 'gnus-agent-queue-setup)
   (unless gnus-agent-send-mail-function 
     (setq gnus-agent-send-mail-function message-send-mail-function
@@ -266,20 +332,41 @@ agent minor mode in all Gnus buffers."
     (re-search-forward
      (concat "^" (regexp-quote mail-header-separator) "\n"))
     (replace-match "\n")
+    (gnus-agent-insert-meta-information 'mail)
     (gnus-request-accept-article "nndraft:queue")))
 
+(defun gnus-agent-insert-meta-information (type &optional method)
+  "Insert meta-information into the message that says how it's to be posted.
+TYPE can be either `mail' or `news'.  If the latter METHOD can
+be a select method."
+  (save-excursion
+    (message-remove-header gnus-agent-meta-information-header)
+    (goto-char (point-min))
+    (insert gnus-agent-meta-information-header ": "
+           (symbol-name type) " " (format "%S" method)
+           "\n")
+    (forward-char -1)
+    (while (search-backward "\n" nil t)
+      (replace-match "\\n" t t))))
+
 ;;;
 ;;; Group mode commands
 ;;;
 
+(defun gnus-agent-fetch-groups (n)
+  "Put all new articles in the current groups into the Agent."
+  (interactive "P")
+  (gnus-group-iterate n 'gnus-agent-fetch-group))
+
 (defun gnus-agent-fetch-group (group)
-  "Put all new articles in GROUP into the agent."
+  "Put all new articles in GROUP into the Agent."
   (interactive (list (gnus-group-group-name)))
   (unless group
     (error "No group on the current line"))
   (let ((gnus-command-method (gnus-find-method-for-group group)))
     (gnus-agent-with-fetch
-      (gnus-agent-fetch-group-1 group gnus-command-method))))
+      (gnus-agent-fetch-group-1 group gnus-command-method)
+      (gnus-message 5 "Fetching %s...done" group))))
 
 (defun gnus-agent-add-group (category arg)
   "Add the current group to an agent category."
@@ -316,7 +403,7 @@ agent minor mode in all Gnus buffers."
       (error "Server already in the agent program"))
     (push method gnus-agent-covered-methods)
     (gnus-agent-write-servers)
-    (message "Entered %s into the agent" server)))
+    (message "Entered %s into the Agent" server)))
 
 (defun gnus-agent-remove-server (server)
   "Remove SERVER from the agent program."
@@ -352,7 +439,6 @@ If N is negative, mark backward instead.  If UNMARK is non-nil, remove
 the mark instead.  The difference between N and the actual number of
 articles marked is returned."
   (interactive "p")
-  (gnus-set-global-variables)
   (let ((backward (< n 0))
        (n (abs n)))
     (while (and
@@ -373,7 +459,6 @@ articles marked is returned."
 If N is negative, unmark backward instead.  The difference between N and
 the actual number of articles unmarked is returned."
   (interactive "p")
-  (gnus-set-global-variables)
   (gnus-agent-mark-article n t))
 
 (defun gnus-agent-toggle-mark (n)
@@ -381,7 +466,6 @@ the actual number of articles unmarked is returned."
 If N is negative, toggle backward instead.  The difference between N and
 the actual number of articles toggled is returned."
   (interactive "p")
-  (gnus-set-global-variables)
   (gnus-agent-mark-article n 'toggle))
 
 (defun gnus-summary-set-agent-mark (article &optional unmark)
@@ -389,9 +473,13 @@ the actual number of articles toggled is returned."
   (let ((unmark (if (and (not (null unmark)) (not (eq t unmark)))
                    (memq article gnus-newsgroup-downloadable)
                  unmark)))
-    (setq gnus-newsgroup-downloadable
-         (delq article gnus-newsgroup-downloadable))
-    (unless unmark
+    (if unmark
+       (progn
+         (setq gnus-newsgroup-downloadable
+               (delq article gnus-newsgroup-downloadable))
+         (push article gnus-newsgroup-undownloaded))
+      (setq gnus-newsgroup-undownloaded
+           (delq article gnus-newsgroup-undownloaded))
       (push article gnus-newsgroup-downloadable))
     (gnus-summary-update-mark
      (if unmark gnus-undownloaded-mark gnus-downloadable-mark)
@@ -428,7 +516,8 @@ the actual number of articles toggled is returned."
     (let* ((gnus-command-method method)
           (file (gnus-agent-lib-file "active")))
       (gnus-make-directory (file-name-directory file))
-      (write-region (point-min) (point-max) file nil 'silent)
+      (let ((coding-system-for-write gnus-agent-article-file-coding-system))
+       (write-region (point-min) (point-max) file nil 'silent))
       (when (file-exists-p (gnus-agent-lib-file "groups"))
        (delete-file (gnus-agent-lib-file "groups"))))))
 
@@ -440,9 +529,27 @@ the actual number of articles toggled is returned."
     (when (file-exists-p (gnus-agent-lib-file "active"))
       (delete-file (gnus-agent-lib-file "active"))))
 
+(defun gnus-agent-save-group-info (method group active)
+  (when (gnus-agent-method-p method)
+    (let* ((gnus-command-method method)
+          (file (gnus-agent-lib-file "active")))
+      (gnus-make-directory (file-name-directory file))
+      (nnheader-temp-write file
+       (when (file-exists-p file)
+         (insert-file-contents file))
+       (goto-char (point-min))
+       (when (re-search-forward (concat "^" (regexp-quote group) " ") nil t)
+         (gnus-delete-line))
+       (insert group " " (number-to-string (cdr active)) " "
+               (number-to-string (car active)) "\n")))))
+
 (defun gnus-agent-group-path (group)
   "Translate GROUP into a path."
-  (nnheader-replace-chars-in-string group ?. ?/))
+  (if nnmail-use-long-file-names
+      (gnus-group-real-name group)
+    (nnheader-replace-chars-in-string
+     (nnheader-translate-file-chars (gnus-group-real-name group))
+     ?. ?/)))
 
 \f
 
@@ -497,7 +604,7 @@ the actual number of articles toggled is returned."
     (goto-char (point-max))
     (insert id "\t" (number-to-string date) "\t")
     (while group-arts
-      (insert (caar group-arts) "/" (number-to-string (cdr (pop group-arts)))
+      (insert (caar group-arts) " " (number-to-string (cdr (pop group-arts)))
              " "))
     (insert "\n")))
 
@@ -522,33 +629,8 @@ the actual number of articles toggled is returned."
 ;;; Fetching
 ;;;
 
-(defun gnus-agent-start-fetch ()
-  "Initialize data structures for efficient fetching."
-  (gnus-agent-open-history)
-  (setq gnus-agent-current-history (gnus-agent-history-buffer)))
-
-(defun gnus-agent-stop-fetch ()
-  "Save all data structures and clean up."
-  (gnus-agent-save-history)
-  (gnus-agent-close-history)
-  (setq gnus-agent-spam-hashtb nil)
-  (save-excursion
-    (set-buffer nntp-server-buffer)
-    (widen)))
-
-(defmacro gnus-agent-with-fetch (&rest forms)
-  "Do FORMS safely."
-  `(unwind-protect
-       (progn
-        (gnus-agent-start-fetch)
-        ,@forms)
-     (gnus-agent-stop-fetch)))
-
-(put 'gnus-agent-with-fetch 'lisp-indent-function 0)
-(put 'gnus-agent-with-fetch 'edebug-form-spec '(body))
-
 (defun gnus-agent-fetch-articles (group articles)
-  "Fetch ARTICLES from GROUP and put them into the agent."
+  "Fetch ARTICLES from GROUP and put them into the Agent."
   (when articles
     ;; Prune off articles that we have already fetched.
     (while (and articles
@@ -581,7 +663,7 @@ the actual number of articles toggled is returned."
                  (insert-buffer-substring nntp-server-buffer)))
              (copy-to-buffer nntp-server-buffer (point-min) (point-max))
              (setq pos (nreverse pos)))))
-       ;; Then save these articles into the agent.
+       ;; Then save these articles into the Agent.
        (save-excursion
          (set-buffer nntp-server-buffer)
          (while pos
@@ -605,9 +687,11 @@ the actual number of articles toggled is returned."
            (if (not (re-search-forward "^Message-ID: *<\\([^>\n]+\\)>" nil t))
                (setq id "No-Message-ID-in-article")
              (setq id (buffer-substring (match-beginning 1) (match-end 1))))
-           (write-region (point-min) (point-max)
-                         (concat dir (number-to-string (caar pos)))
-                         nil 'silent)
+           (let ((coding-system-for-write
+                  gnus-agent-article-file-coding-system))
+             (write-region (point-min) (point-max)
+                           (concat dir (number-to-string (caar pos)))
+                           nil 'silent))
            (when (setq elem (assq (caar pos) gnus-agent-article-alist))
              (setcdr elem t))
            (gnus-agent-enter-history
@@ -683,10 +767,14 @@ the actual number of articles toggled is returned."
          (when (file-exists-p
                 (setq file (gnus-agent-article-name ".overview" group)))
            (gnus-agent-braid-nov group articles file))
-         (gnus-make-directory (file-name-directory file))
+         (gnus-make-directory (nnheader-translate-file-chars
+                               (file-name-directory file)))
          (write-region (point-min) (point-max) file nil 'silent)
-         (gnus-agent-save-alist group articles nil))
-       t))))
+         (gnus-agent-save-alist group articles nil)
+         (gnus-agent-enter-history "last-header-fetched-for-session"
+                                   (list (cons group (nth (- (length  articles) 1) articles)))
+                                   (gnus-time-to-day (current-time)))
+       t)))))
 
 (defsubst gnus-agent-copy-nov-line (article)
   (let (b e)
@@ -760,6 +848,14 @@ the actual number of articles toggled is returned."
   (concat (gnus-agent-directory) (gnus-agent-group-path group) "/"
          (if (stringp article) article (string-to-number article))))
 
+;;;###autoload
+(defun gnus-agent-batch-fetch ()
+  "Start Gnus and fetch session."
+  (interactive)
+  (gnus)
+  (gnus-agent-fetch-session)
+  (gnus-group-exit))
+
 (defun gnus-agent-fetch-session ()
   "Fetch all articles and headers that are eligible for fetching."
   (interactive)
@@ -771,11 +867,15 @@ the actual number of articles toggled is returned."
        groups group gnus-command-method)
     (save-excursion
       (while methods
-       (setq gnus-command-method (car methods)
-             groups (gnus-groups-from-server (pop methods)))
-       (gnus-agent-with-fetch
-         (while (setq group (pop groups))
-           (gnus-agent-fetch-group-1 group gnus-command-method))))
+       (setq gnus-command-method (car methods))
+       (when (or (gnus-server-opened gnus-command-method)
+                 (gnus-open-server gnus-command-method))
+         (setq groups (gnus-groups-from-server (car methods)))
+         (gnus-agent-with-fetch
+           (while (setq group (pop groups))
+             (when (<= (gnus-group-level group) gnus-agent-handle-level)
+               (gnus-agent-fetch-group-1 group gnus-command-method)))))
+       (pop methods))
       (gnus-message 6 "Finished fetching articles into the Gnus agent"))))
 
 (defun gnus-agent-fetch-group-1 (group method)
@@ -786,7 +886,8 @@ the actual number of articles toggled is returned."
        gnus-use-cache articles score arts
        category predicate info marks score-param)
     ;; Fetch headers.
-    (when (and (setq articles (gnus-list-of-unread-articles group))
+    (when (and (or (gnus-active group) (gnus-activate-group group))
+              (setq articles (gnus-list-of-unread-articles group))
               (gnus-agent-fetch-headers group articles))
       ;; Parse them and see which articles we want to fetch.
       (setq gnus-newsgroup-dependencies
@@ -901,7 +1002,7 @@ the actual number of articles toggled is returned."
        ["Edit groups" gnus-category-edit-groups t]
        ["Exit" gnus-category-exit t]))
 
-    (run-hooks 'gnus-category-menu-hook)))
+    (gnus-run-hooks 'gnus-category-menu-hook)))
 
 (defun gnus-category-mode ()
   "Major mode for listing and editing agent categories.
@@ -927,7 +1028,7 @@ The following commands are available:
   (buffer-disable-undo (current-buffer))
   (setq truncate-lines t)
   (setq buffer-read-only t)
-  (run-hooks 'gnus-category-mode-hook))
+  (gnus-run-hooks 'gnus-category-mode-hook))
 
 (defalias 'gnus-category-position-point 'gnus-goto-colon)
 
@@ -976,7 +1077,7 @@ The following commands are available:
   (setq gnus-category-alist
        (or (gnus-agent-read-file
             (nnheader-concat gnus-agent-directory "lib/categories"))
-           (list (list 'default 'true nil nil)))))
+           (list (list 'default 'short nil nil)))))
     
 (defun gnus-category-write ()
   "Write the category alist."
@@ -1097,7 +1198,7 @@ The following commands are available:
 
 (defun gnus-agent-high-scored-p ()
   "Say whether an article has a high score or not."
-  (> gnus-score gnus-agent-low-score))
+  (> gnus-score gnus-agent-high-score))
 
 (defun gnus-category-make-function (cat)
   "Make a function from category CAT."
@@ -1156,88 +1257,136 @@ The following commands are available:
   "Expire all old articles."
   (interactive)
   (let ((methods gnus-agent-covered-methods)
-       (alist (cdr gnus-newsrc-alist))
-       gnus-command-method ofiles info method file group)
-    (while (setq gnus-command-method (pop methods))
-      (setq ofiles (nconc ofiles (gnus-agent-expire-directory
-                                 (gnus-agent-directory)))))
-    (while (setq info (pop alist))
-      (when (and (gnus-agent-method-p
-                 (setq gnus-command-method
-                       (gnus-find-method-for-group
-                        (setq group (gnus-info-group info)))))
-                (member
-                 (setq file
-                       (concat
-                        (gnus-agent-directory)
-                        (gnus-agent-group-path group) "/.overview"))
-                 ofiles))
-       (setq ofiles (delete file ofiles))
-       (gnus-agent-expire-group file group)))
-    (while ofiles
-      (gnus-agent-expire-group (pop ofiles)))))
-
-(defun gnus-agent-expire-directory (dir)
-  "Expire all groups in DIR recursively."
-  (when (file-directory-p dir)
-    (let ((files (directory-files dir t))
-         file ofiles)
-      (while (setq file (pop files))
-       (cond
-        ((member (file-name-nondirectory file) '("." ".."))
-         ;; Do nothing.
-         )
-        ((file-directory-p file)
-         ;; Recurse.
-         (setq ofiles (nconc ofiles (gnus-agent-expire-directory file))))
-        ((string-match "\\.overview$" file)
-         ;; Expire group.
-         (push file ofiles))))
-      ofiles)))
-
-(defun gnus-agent-expire-group (overview &optional group)
-  "Expire articles in OVERVIEW."
-  (gnus-message 5 "Expiring %s..." overview)
-  (let ((odate (- (gnus-time-to-day (current-time)) 4))
-       (dir (file-name-directory overview))
-       (info (when group (gnus-get-info group)))
-       headers article file point unreads)
-    (gnus-agent-load-alist nil dir)
-    (when info
-      (setq unreads
-           (nconc
-            (gnus-list-of-unread-articles group)
-            (gnus-uncompress-range
-             (cdr (assq 'tick (gnus-info-marks info))))
-            (gnus-uncompress-range
-             (cdr (assq 'dormant (gnus-info-marks info)))))))
-    (nnheader-temp-write overview
-      (insert-file-contents overview)
-      (goto-char (point-min))
-      (while (not (eobp))
-       (setq point (point))
-       (condition-case ()
-           (setq headers (inline (nnheader-parse-nov)))
-         (error
-          (goto-char point)
-          (gnus-delete-line)
-          (setq headers nil)))
-       (when headers
-         (unless (memq (setq article (mail-header-number headers)) unreads)
-           (if (not (< (inline
-                         (gnus-time-to-day
-                          (inline (nnmail-date-to-time
-                                   (mail-header-date headers)))))
-                       odate))
-               (forward-line 1)              
-             (gnus-delete-line)
-             (setq gnus-agent-article-alist
-                   (delq (assq article gnus-agent-article-alist)
-                         gnus-agent-article-alist))
-             (when (file-exists-p
-                    (setq file (concat dir (number-to-string article))))
-               (delete-file file))))))
-      (gnus-agent-save-alist nil nil nil dir))))
+       (day (- (gnus-time-to-day (current-time)) gnus-agent-expire-days))
+       gnus-command-method sym group articles
+       history overview file histories elem art nov-file low info
+       unreads marked article)
+    (save-excursion
+      (setq overview (get-buffer-create " *expire overview*"))
+      (while (setq gnus-command-method (pop methods))
+       (let ((expiry-hashtb (gnus-make-hashtable 1023)))
+       (gnus-agent-open-history)
+       (set-buffer
+        (setq gnus-agent-current-history
+              (setq history (gnus-agent-history-buffer))))
+       (goto-char (point-min))
+       (when (> (buffer-size) 1)
+         (goto-char (point-min))
+         (while (not (eobp))
+           (skip-chars-forward "^\t")
+           (if (> (read (current-buffer)) day)
+               ;; New article; we don't expire it.
+               (forward-line 1)
+             ;; Old article.  Schedule it for possible nuking.
+             (while (not (eolp))
+               (setq sym (let ((obarray expiry-hashtb))
+                           (read (current-buffer))))
+               (if (boundp sym)
+                   (set sym (cons (cons (read (current-buffer)) (point))
+                                  (symbol-value sym)))
+                 (set sym (list (cons (read (current-buffer)) (point)))))
+               (skip-chars-forward " "))
+             (forward-line 1)))
+         ;; We now have all articles that can possibly be expired.
+         (mapatoms
+          (lambda (sym)
+            (setq group (symbol-name sym)
+                  articles (sort (symbol-value sym) 'car-less-than-car)
+                  low (car (gnus-active group))
+                  info (gnus-get-info group)
+                  unreads (ignore-errors (gnus-list-of-unread-articles group))
+                  marked (nconc (gnus-uncompress-range
+                                 (cdr (assq 'tick (gnus-info-marks info))))
+                                (gnus-uncompress-range
+                                 (cdr (assq 'dormant
+                                            (gnus-info-marks info)))))
+                  nov-file (gnus-agent-article-name ".overview" group))
+            (gnus-agent-load-alist group)
+            (gnus-message 5 "Expiring articles in %s" group)
+            (set-buffer overview)
+            (erase-buffer)
+            (when (file-exists-p nov-file)
+              (insert-file-contents nov-file))
+            (goto-char (point-min))
+            (setq article 0)
+            (while (setq elem (pop articles))
+              (setq article (car elem))
+              (when (or (null low)
+                        (< article low)
+                        gnus-agent-expire-all
+                        (and (not (memq article unreads))
+                             (not (memq article marked))))
+                ;; Find and nuke the NOV line.
+                (while (and (not (eobp))
+                            (or (not (numberp
+                                      (setq art (read (current-buffer)))))
+                                (< art article)))
+                  (if (file-exists-p
+                       (gnus-agent-article-name
+                        (number-to-string art) group))
+                      (forward-line 1)
+                    ;; Remove old NOV lines that have no articles.
+                    (gnus-delete-line)))
+                (if (or (eobp)
+                        (/= art article))
+                    (beginning-of-line)
+                  (gnus-delete-line))
+                ;; Nuke the article.
+                (when (file-exists-p (setq file (gnus-agent-article-name
+                                                 (number-to-string article)
+                                                 group)))
+                  (delete-file file))
+                ;; Schedule the history line for nuking.
+                (push (cdr elem) histories)))
+            (write-region (point-min) (point-max) nov-file nil 'silent)
+            ;; Delete the unwanted entries in the alist.
+            (setq gnus-agent-article-alist
+                  (sort gnus-agent-article-alist 'car-less-than-car))
+            (let* ((alist gnus-agent-article-alist)
+                   (prev (cons nil alist))
+                   (first prev))
+              (while (and alist
+                          (<= (caar alist) article))
+                (if (or (not (cdar alist))
+                        (not (file-exists-p
+                              (gnus-agent-article-name
+                               (number-to-string
+                                (caar alist))
+                               group))))
+                    (setcdr prev (setq alist (cdr alist)))
+                  (setq prev alist
+                        alist (cdr alist))))
+              (setq gnus-agent-article-alist (cdr first))
+              ;;; Mark all articles up to the first article
+              ;;; in `gnus-article-alist' as read.
+              (when (caar gnus-agent-article-alist)
+                (setcar (nthcdr 2 info)
+                        (gnus-range-add
+                         (nth 2 info)
+                         (cons 1 (- (caar gnus-agent-article-alist) 1)))))
+              (gnus-dribble-enter
+               (concat "(gnus-group-set-info '"
+                       (gnus-prin1-to-string info)
+                       ")"))
+              (gnus-agent-save-alist group)))
+          expiry-hashtb)
+         (set-buffer history)
+         (setq histories (nreverse (sort histories '<)))
+         (while histories
+           (goto-char (pop histories))
+           (gnus-delete-line))
+         (gnus-agent-save-history)
+         (gnus-agent-close-history))
+       (gnus-message 4 "Expiry...done"))))))
+
+;;;###autoload
+(defun gnus-agent-batch ()
+  (interactive)
+  (let ((init-file-user "")
+       (gnus-always-read-dribble-file t))
+    (gnus))
+  (gnus-group-send-drafts)
+  (gnus-agent-fetch-session))
 
 (provide 'gnus-agent)