Initial revision
authorChris Hanson <org/chris-hanson/cph>
Wed, 3 May 1995 07:51:01 +0000 (07:51 +0000)
committerChris Hanson <org/chris-hanson/cph>
Wed, 3 May 1995 07:51:01 +0000 (07:51 +0000)
v7/src/edwin/nntp.scm [new file with mode: 0644]
v7/src/edwin/snr.scm [new file with mode: 0644]

diff --git a/v7/src/edwin/nntp.scm b/v7/src/edwin/nntp.scm
new file mode 100644 (file)
index 0000000..b6ea7ca
--- /dev/null
@@ -0,0 +1,944 @@
+;;; -*-Scheme-*-
+;;;
+;;;    $Id: nntp.scm,v 1.1 1995/05/03 07:51:01 cph Exp $
+;;;
+;;;    Copyright (c) 1995 Massachusetts Institute of Technology
+;;;
+;;;    This material was developed by the Scheme project at the
+;;;    Massachusetts Institute of Technology, Department of
+;;;    Electrical Engineering and Computer Science.  Permission to
+;;;    copy this software, to redistribute it, and to use it for any
+;;;    purpose is granted, subject to the following restrictions and
+;;;    understandings.
+;;;
+;;;    1. Any copy made of this software must include this copyright
+;;;    notice in full.
+;;;
+;;;    2. Users of this software agree to make their best efforts (a)
+;;;    to return to the MIT Scheme project any improvements or
+;;;    extensions that they make, so that these may be included in
+;;;    future releases; and (b) to inform MIT of noteworthy uses of
+;;;    this software.
+;;;
+;;;    3. All materials developed as a consequence of the use of this
+;;;    software shall duly acknowledge such use, in accordance with
+;;;    the usual standards of acknowledging credit in academic
+;;;    research.
+;;;
+;;;    4. MIT has made no warrantee or representation that the
+;;;    operation of this software will be error-free, and MIT is
+;;;    under no obligation to provide any services, by way of
+;;;    maintenance, update, or otherwise.
+;;;
+;;;    5. In conjunction with products arising from the use of this
+;;;    material, there shall be no use of the name of the
+;;;    Massachusetts Institute of Technology nor of any adaptation
+;;;    thereof in any advertising, promotional, or sales literature
+;;;    without prior written consent from MIT in each case.
+;;;
+;;; NOTE: Parts of this program (Edwin) were created by translation
+;;; from corresponding parts of GNU Emacs.  Users should be aware that
+;;; the GNU GENERAL PUBLIC LICENSE may apply to these parts.  A copy
+;;; of that license should have been included along with this file.
+;;;
+
+;;;; NNTP Interface
+
+;;; This program provides a high-level interface to an NNTP server.
+;;; It implements a database abstraction which gives the impression
+;;; that the news database is in memory and can be manipulated
+;;; directly.  This abstraction largely hides the underlying server
+;;; communication on which it is built.
+
+;;; The abstraction provides models for the server, each of the groups
+;;; it contains, and the headers in each group.  It also provides a
+;;; method for combining headers into conversation threads.
+
+(declare (usual-integrations))
+\f
+;;;; NNTP Connection
+
+(define-structure (nntp-connection
+                  (conc-name nntp-connection:)
+                  (constructor make-nntp-connection (server)))
+  (server #f read-only #t)
+  (process #f)
+  banner
+  (group-table (make-string-hash-table) read-only #t)
+  (%active-groups #f)
+  (reader-hook #f))
+
+(define (nntp-connection:port connection)
+  (subprocess-i/o-port (nntp-connection:process connection)))
+
+(define (open-nntp-connection server)
+  (let ((connection (make-nntp-connection server)))
+    (nntp-connection:reopen connection)
+    connection))
+
+(define (nntp-connection:reopen connection)
+  (let ((process
+        (let ((program (os/find-program "tcp" #f)))
+          (start-pipe-subprocess program
+                                 (vector (file-namestring program)
+                                         (nntp-connection:server connection)
+                                         "nntp")
+                                 #f))))
+    (set-nntp-connection:process! connection process)
+    (set-nntp-connection:banner!
+     connection
+     ;; Set up the line translation for the process, because the
+     ;; network line translation is CR-LF regardless of the operating
+     ;; system this program runs on.
+     (input-port/read-line (subprocess-i/o-port process "\r\n")))))
+
+(define (nntp-connection:closed? connection)
+  (let ((port (nntp-connection:port connection)))
+    (or (not port)
+       (input-port/eof? port))))
+
+(define (nntp-connection:close connection)
+  (if (not (nntp-connection:closed? connection))
+      (begin
+       (nntp-write-line connection "quit")
+       (nntp-drain-output connection)))
+  (nntp-flush-input connection)
+  (subprocess-delete (nntp-connection:process connection)))
+
+(define (nntp-connection:active-groups connection)
+  (or (nntp-connection:%active-groups connection)
+      (let ((lines
+            (let ((msg "Reading list of news groups... "))
+              (message msg)
+              (let ((lines (list->vector (nntp-list-command connection))))
+                (message msg "done")
+                lines))))
+       (let ((msg "Parsing list of news groups... "))
+         (message msg)
+         (let ((end (vector-length lines)))
+           (do ((index 0 (fix:+ index 1)))
+               ((fix:= index end))
+             (let ((tokens (string-tokenize (vector-ref lines index))))
+               (let ((group (make-news-group connection (car tokens))))
+                 (set-news-group:server-probe!
+                  group
+                  (let ((last (token->number (cadr tokens)))
+                        (first (token->number (caddr tokens))))
+                    (vector (- (+ last 1) first)
+                            first
+                            last)))
+                 (vector-set! lines index group)))))
+         (message msg "done")
+         (sort! lines news-group:<)
+         (set-nntp-connection:%active-groups! connection lines)
+         lines))))
+
+(define (nntp-connection:discard-active-groups-cache! connection)
+  (set-nntp-connection:%active-groups! connection #f))
+\f
+;;;; NNTP Commands
+
+(define (nntp-group-command connection group-name)
+  (prepare-nntp-connection connection)
+  (nntp-write-command connection "group" group-name)
+  (nntp-drain-output connection)
+  (let ((response (nntp-read-line connection)))
+    (case (nntp-response-number response)
+      ((211)
+       (let ((tokens (string-tokenize response)))
+        (vector (token->number (cadr tokens))
+                (token->number (caddr tokens))
+                (token->number (cadddr tokens)))))
+      ((411)
+       'NO-SUCH-GROUP)
+      (else
+       (nntp-error response)))))
+
+;; This says how many pending HEAD requests may be sent before it's
+;; necessary to starting reading the replies, to avoid deadlock.
+(define nntp-maximum-request 400)
+
+(define (nntp-head-request connection key)
+  (nntp-write-command connection "head" key))
+
+(define (nntp-head-reply connection)
+  (let ((response (nntp-read-line connection)))
+    (case (nntp-response-number response)
+      ((221)
+       (let ((tokens (string-tokenize response)))
+        (let ((article-number (cadr tokens))
+              (message-id (caddr tokens)))
+          (if (and (valid-article-number? article-number)
+                   (valid-message-id? message-id))
+              (vector article-number
+                      message-id
+                      (nntp-read-text-lines connection))
+              'NO-SUCH-ARTICLE))))
+      ((423 430)
+       'NO-SUCH-ARTICLE)
+      (else
+       (nntp-error response)))))
+
+(define (nntp-body-command connection key port)
+  (prepare-nntp-connection connection)
+  (nntp-write-command connection "body" key)
+  (nntp-drain-output connection)
+  (let ((response (nntp-read-line connection)))
+    (case (nntp-response-number response)
+      ((222)
+       (nntp-read-text-1 connection port)
+       #t)
+      ((423 430)
+       #f)
+      (else
+       (nntp-error response)))))
+
+(define (nntp-list-command connection)
+  (prepare-nntp-connection connection)
+  (nntp-write-command connection "list")
+  (nntp-drain-output connection)
+  (let ((response (nntp-read-line connection)))
+    (if (fix:= 215 (nntp-response-number response))
+       (nntp-read-text-lines connection)
+       (nntp-error response))))
+
+(define (nntp-error response)
+  (error "NNTP error:" response))
+\f
+(define (prepare-nntp-connection connection)
+  (nntp-flush-input connection)
+  (if (nntp-connection:closed? connection)
+      (nntp-connection:reopen connection)))
+
+(define (nntp-flush-input connection)
+  (let ((port (nntp-connection:port connection)))
+    (if port
+       (do ()
+           ((not (input-port/char-ready? port 100)))
+         (input-port/discard-line port)))))
+
+(define (nntp-write-command connection string . strings)
+  (let ((port (nntp-connection:port connection)))
+    (output-port/write-string port string)
+    (do ((strings strings (cdr strings)))
+       ((null? strings))
+      (output-port/write-char port #\space)
+      (output-port/write-string port (car strings)))
+    (output-port/write-char port #\newline)))
+
+(define (nntp-write-line connection string)
+  (let ((port (nntp-connection:port connection)))
+    (output-port/write-string port string)
+    (output-port/write-char port #\newline)))
+
+(define (nntp-drain-output connection)
+  (output-port/flush-output (nntp-connection:port connection)))
+
+(define (nntp-read-line connection)
+  (input-port/read-line (nntp-connection:port connection)))
+
+(define (nntp-response-number line)
+  (if (fix:< (string-length line) 3)
+      (error "Malformed NNTP response:" line))
+  (substring->nonnegative-integer line 0 3))
+
+(define (nntp-read-text connection)
+  (with-string-output-port
+    (lambda (port)
+      (nntp-read-text-1 connection port))))
+
+(define (nntp-read-text-1 connection port)
+  (let loop ()
+    (let ((line (nntp-read-line connection)))
+      (let ((length (string-length line)))
+       (cond ((fix:= 0 length)
+              (output-port/write-char port #\newline)
+              (loop))
+             ((char=? #\. (string-ref line 0))
+              (if (not (fix:= 1 length))
+                  (begin
+                    (output-port/write-substring port line 1 length)
+                    (output-port/write-char port #\newline)
+                    (loop))))
+             (else
+              (output-port/write-substring port line 0 length)
+              (output-port/write-char port #\newline)
+              (loop)))))))
+
+(define (nntp-read-text-lines connection)
+  (let loop ((lines '()))
+    (let ((line (nntp-read-line connection)))
+      (let ((length (string-length line)))
+       (cond ((or (fix:= 0 length)
+                  (not (char=? #\. (string-ref line 0))))
+              (loop (cons line lines)))
+             ((fix:= 1 length)
+              (reverse! lines))
+             (else
+              (loop (cons (string-tail line 1) lines))))))))
+\f
+;;;; News-Group Data Structure
+
+(define-structure (news-group
+                  (conc-name news-group:)
+                  (constructor %make-news-group (connection name)))
+  (connection #f read-only #t)
+  (name #f read-only #t)
+  (header-table (make-eqv-hash-table) read-only #t)
+  (server-probe #f)
+  (reader-hook #f))
+
+(define (make-news-group connection name)
+  (let ((table (nntp-connection:group-table connection)))
+    (or (hash-table/get table name #f)
+       (let ((group (%make-news-group connection name)))
+         (hash-table/put! table name group)
+         group))))
+
+(define-integrable (news-group:server group)
+  (nntp-connection:server (news-group:connection group)))
+
+(define (news-group:< x y)
+  (string-ci<? (news-group:name x) (news-group:name y)))
+
+(define (find-news-group connection name)
+  (hash-table/get (nntp-connection:group-table connection) name #f))
+
+(define (find-active-news-group connection name)
+  (let ((table (nntp-connection:group-table connection)))
+    (let ((group (hash-table/get table name #f)))
+      (if group
+         (and (news-group:active? group) group)
+         (and (not (nntp-connection:%active-groups connection))
+              (let ((probe (nntp-group-command connection name)))
+                (and (not (eq? 'NO-SUCH-GROUP probe))
+                     (let ((group (%make-news-group connection name)))
+                       (set-news-group:server-probe! group probe)
+                       (hash-table/put! table name group)
+                       group))))))))
+
+(define (news-group:active? group)
+  (news-group:maybe-update-probe! group)
+  (not (eq? 'NO-SUCH-GROUP (news-group:server-probe group))))
+
+(define-integrable (news-group:estimated-n-articles group)
+  (vector-ref (news-group:guarantee-server-probe group) 0))
+
+(define-integrable (news-group:first-article group)
+  (vector-ref (news-group:guarantee-server-probe group) 1))
+
+(define-integrable (news-group:last-article group)
+  (vector-ref (news-group:guarantee-server-probe group) 2))
+
+(define (news-group:guarantee-server-probe group)
+  (news-group:maybe-update-probe! group)
+  (let ((probe (news-group:server-probe group)))
+    (if (eq? 'NO-SUCH-GROUP probe)
+       (error "Unknown news group:" (news-group:name group)))
+    probe))
+
+(define (news-group:maybe-update-probe! group)
+  (if (not (news-group:server-probe group))
+      (news-group:update-probe! group)))
+
+(define (news-group:update-probe! group)
+  (set-news-group:server-probe!
+   group
+   (nntp-group-command (news-group:connection group)
+                      (news-group:name group))))
+\f
+;;;; Header Cache
+
+(define (news-group:header group number)
+  (let ((table (news-group:header-table group)))
+    (let ((header (hash-table/get table number #f)))
+      (and (not (eq? 'NONE header))
+          (or header
+              (let ((header (parse-header group (read-header group number))))
+                (hash-table/put! table number (or header 'NONE))
+                header))))))
+
+(define (news-group:headers group numbers)
+  (call-with-values (lambda () (cached-headers group numbers))
+    (lambda (headers numbers)
+      (if (null? numbers)
+         headers
+         (let ((table (news-group:header-table group)))
+           (let loop
+               ((headers headers)
+                (numbers numbers)
+                (responses (read-headers group numbers)))
+             (if (null? responses)
+                 headers
+                 (loop (let ((header (parse-header group (car responses))))
+                         (hash-table/put! table
+                                          (car numbers)
+                                          (or header 'NONE))
+                         (if header (cons header headers) headers))
+                       (cdr numbers)
+                       (cdr responses)))))))))
+
+(define (cached-headers group numbers)
+  (let ((table (news-group:header-table group)))
+    (let loop ((numbers numbers) (headers '()) (numbers* '()))
+      (if (null? numbers)
+         (values headers (reverse! numbers*))
+         (let ((header (hash-table/get table (car numbers) #f)))
+           (loop (cdr numbers)
+                 (if (or (not header) (eq? 'NONE header))
+                     headers
+                     (cons header headers))
+                 (if (not header)
+                     (cons (car numbers) numbers*)
+                     numbers*)))))))
+
+(define (news-group:cached-header group number)
+  (hash-table/get (news-group:header-table group) number #f))
+
+(define (news-group:discard-cached-header! group number)
+  (hash-table/remove! (news-group:header-table group) number))
+
+(define (news-group:cached-header-numbers group)
+  (hash-table/key-list (news-group:header-table group)))
+
+(define (news-group:cached-headers group)
+  (hash-table/datum-list (news-group:header-table group)))
+\f
+;;;; Read Headers
+
+(define (read-header group number)
+  (news-group:update-probe! group)
+  (let ((connection (news-group:connection group))
+       (msg "Reading news header... "))
+    (message msg)
+    (prepare-nntp-connection connection)
+    (nntp-head-request connection (number->string number))
+    (let ((response (nntp-head-reply connection)))
+      (message msg "done")
+      response)))
+
+(define (read-headers group numbers)
+  (news-group:update-probe! group)
+  (let ((n-to-read (length numbers))
+       (connection (news-group:connection group))
+       (msg "Reading news headers... ")
+       (n-received 0))
+    (let ((msg? (fix:>= n-to-read 100)))
+
+      (define (send-requests numbers n)
+       (do ((numbers numbers (cdr numbers))
+            (n n (fix:- n 1)))
+           ((fix:= n 0) numbers)
+         (nntp-head-request connection (number->string (car numbers)))))
+
+      (define (receive-replies numbers n responses)
+       (nntp-drain-output connection)
+       (do ((numbers numbers (cdr numbers))
+            (n n (fix:- n 1))
+            (responses responses
+                       (cons (nntp-head-reply connection) responses)))
+           ((fix:= n 0) responses)
+         (if (and msg?
+                  (begin
+                    (set! n-received (fix:+ n-received 1))
+                    (fix:= 0 (fix:remainder n-received 20))))
+             (message msg (integer-round (* n-received 100) n-to-read) "%"))))
+
+      (message msg)
+      (prepare-nntp-connection connection)
+      (let loop ((numbers numbers) (n-left n-to-read) (responses '()))
+       (if (null? numbers)
+           (begin
+             (message msg "done")
+             (reverse! responses))
+           (let ((n (min n-left nntp-maximum-request)))
+             (let ((numbers* (send-requests numbers n)))
+               (loop numbers*
+                     (fix:- n-left n)
+                     (receive-replies numbers n responses)))))))))
+\f
+;;;; Parse Headers
+
+(define (parse-header group response)
+  (and (vector? response)
+       (let ((lines (vector-ref response 2)))
+        (make-news-header group
+                          (token->number (vector-ref response 0))
+                          (vector-ref response 1)
+                          (lines->header-text lines)
+                          (parse-header-lines lines)))))
+
+(define (lines->header-text lines)
+  (let ((length
+        (do ((lines lines (cdr lines))
+             (nchars 0 (fix:+ (fix:+ nchars 1) (string-length (car lines)))))
+            ((null? lines) nchars))))
+    (let ((text (make-string length)))
+      (let loop ((lines lines) (index 0))
+       (if (not (null? lines))
+           (loop (cdr lines)
+                 (let* ((line (car lines))
+                        (end (string-length line)))
+                   (do ((i1 0 (fix:+ i1 1))
+                        (i2 index (fix:+ i2 1)))
+                       ((fix:= i1 end)
+                        (string-set! text i2 #\newline)
+                        (fix:+ i2 1))
+                     (string-set! text i2 (string-ref line i1)))))))
+      text)))
+
+(define (parse-header-lines lines)
+  (cond ((null? lines)
+        '())
+       ((header-line? (car lines))
+        (let ((unfold
+               (lambda (rest)
+                 (let ((colon (string-find-next-char (car lines) #\:))
+                       (end (string-length (car lines))))
+                   (cons (substring-trim (car lines) 0 colon)
+                         (let ((value
+                                (substring-trim (car lines)
+                                                (fix:+ colon 1)
+                                                end)))
+                           (if (null? rest)
+                               value
+                               (apply string-append
+                                      (cons value
+                                            (append-map
+                                             (lambda (string)
+                                               (list " "
+                                                     (string-trim string)))
+                                             (reverse! rest)))))))))))
+          (let loop ((lines (cdr lines)) (rest '()))
+            (cond ((null? lines)
+                   (list (unfold rest)))
+                  ((header-continuation-line? (car lines))
+                   (loop (cdr lines) (cons (car lines) rest)))
+                  (else
+                   (cons (unfold rest) (parse-header-lines lines)))))))
+       (else
+        (parse-header-lines (cdr lines)))))
+
+(define (header-line? line)
+  (and (not (string-null? line))
+       (not (or (char=? #\space (string-ref line 0))
+               (char=? #\tab (string-ref line 0))))
+       (string-find-next-char line #\:)))
+
+(define (header-continuation-line? line)
+  (and (not (string-null? line))
+       (or (char=? #\space (string-ref line 0))
+          (char=? #\tab (string-ref line 0)))
+       (string-find-next-char-in-set line char-set:not-whitespace)))
+\f
+;;;; News-Header Data Structure
+
+(define-structure (news-header
+                  (conc-name news-header:)
+                  (constructor make-news-header
+                               (group number message-id text alist)))
+  (group #f read-only #t)
+  number
+  (message-id #f read-only #t)
+  (text #f read-only #t)
+  (alist #f read-only #t)
+  (followup-to #f)
+  (followups '())
+  (thread #f)
+  (reader-hook #f))
+
+(define (dummy-news-header message-id)
+  (make-news-header #f #f message-id #f '()))
+
+(define (news-header:dummy? header)
+  (not (news-header:text header)))
+
+(define (news-header:field-value header name)
+  (let ((field
+        (list-search-positive (news-header:alist header)
+          (lambda (field)
+            (string-ci=? (car field) name)))))
+    (if field
+       (cdr field)
+       "")))
+
+(define (news-header:< x y)
+  (< (news-header:number x) (news-header:number y)))
+
+(define (news-header:read-body header port)
+  (nntp-body-command (news-group:connection (news-header:group header))
+                    (news-header:message-id header)
+                    port))
+
+(define (news-header:xref header)
+  (parse-xref-tokens
+   (string-tokenize (news-header:field-value header "xref"))))
+
+(define (parse-xref-tokens tokens)
+  (if (null? tokens)
+      tokens
+      (let ((colon (string-find-next-char (car tokens) #\:))
+           (rest (parse-xref-tokens (cdr tokens))))
+       (if colon
+           (cons (cons (string-head (car tokens) colon)
+                       (string-tail (car tokens) (fix:+ colon 1)))
+                 rest)
+           rest))))
+
+(define (valid-article-number? string)
+  (let ((end (string-length string)))
+    (and (let loop ((index 0))
+          (and (not (fix:= index end))
+               (or (not (char=? #\0 (string-ref string index)))
+                   (loop (fix:+ index 1)))))
+        (let loop ((index 0))
+          (or (fix:= index end)
+              (and (fix:<= (char->integer #\0)
+                           (char->integer (string-ref string index)))
+                   (fix:<= (char->integer (string-ref string index))
+                           (char->integer #\9)))
+              (loop (fix:+ index 1)))))))
+
+(define (valid-message-id? string)
+  (let ((end (string-length string)))
+    (and (fix:> end 2)
+        (char=? #\< (string-ref string 0))
+        (let loop ((index 1))
+          (and (not (fix:= index end))
+               (if (char=? #\> (string-ref string index))
+                   (fix:= (fix:+ index 1) end)
+                   (and (not (char=? #\space (string-ref string index)))
+                        (not (char=? #\< (string-ref string index)))
+                        (loop (fix:+ index 1)))))))))
+\f
+;;;; Conversation Threads
+
+;;; This is by far the hairiest part of this implementation.  Headers
+;;; are first organized into trees based on their "references" fields.
+;;; The tree structure is reflected in their FOLLOWUP-TO and FOLLOWUPS
+;;; fields.  These trees are then gathered into threads by means of
+;;; subject matching.  Each resulting thread consists of a list of
+;;; these trees, represented by the tree roots.  The list is sorted by
+;;; the header order of the roots.
+
+(define-structure (news-thread
+                  (conc-name news-thread:)
+                  (constructor make-news-thread (root-headers)))
+  (root-headers #f read-only #t)
+  (reader-hook #f))
+
+(define (news-thread:< x y)
+  (news-header:< (car (news-thread:root-headers x))
+                (car (news-thread:root-headers y))))
+
+(define (news-thread:for-each-header thread procedure)
+  (for-each (letrec ((loop
+                     (lambda (header)
+                       (procedure header)
+                       (for-each loop (news-header:followups header)))))
+             loop)
+           (news-thread:root-headers thread)))
+
+(define (organize-headers-into-threads headers)
+  (build-followup-trees! headers)
+  (sort (map make-threads-equivalent!
+            (let ((threads (associate-threads-with-trees headers)))
+              (for-each (lambda (thread)
+                          (for-each guarantee-header-number
+                                    (news-thread:root-headers thread)))
+                        threads)
+              (build-equivalence-classes
+               threads
+               (find-subject-associations threads))))
+       news-thread:<))
+\f
+(define (build-followup-trees! headers)
+  (let ((references (make-eq-hash-table))
+       (dummy-headers '()))
+    (let ((get-refs (lambda (h) (hash-table/get references h '())))
+         (set-refs (lambda (h r) (hash-table/put! references h r))))
+      (let ((id-table (make-string-hash-table)))
+       (for-each (lambda (header)
+                   (set-news-header:followup-to! header #f)
+                   (set-news-header:followups! header '())
+                   (set-news-header:thread! header #f)
+                   (set-refs header (news-header:references header))
+                   (hash-table/put! id-table
+                                    (news-header:message-id header)
+                                    header))
+                 headers)
+       (for-each (lambda (header)
+                   (do ((refs (get-refs header) (cdr refs)))
+                       ((null? refs))
+                     (set-car! refs
+                               (let ((id (car refs)))
+                                 (or (hash-table/get id-table id #f)
+                                     (let ((header (dummy-news-header id)))
+                                       (hash-table/put! id-table id header)
+                                       (set! dummy-headers
+                                             (cons header dummy-headers))
+                                       header))))))
+                 headers))
+      (for-each (lambda (header)
+                 (do ((refs (get-refs header) (cdr refs)))
+                     ((null? refs))
+                   (if (news-header:dummy? (car refs))
+                       (let ((drefs (get-refs (car refs))))
+                         (if (not (eq? 'BROKEN drefs))
+                             (let loop ((x (cdr refs)) (y drefs))
+                               (cond ((null? x)
+                                      unspecific)
+                                     ((null? y)
+                                      (set-refs (car refs) (cdr refs)))
+                                     ((eq? (car x) (car y))
+                                      (loop (cdr x) (cdr y)))
+                                     (else
+                                      (set-refs (car refs) 'BROKEN)))))))))
+               headers)
+      (for-each (lambda (dummy-header)
+                 (if (eq? 'BROKEN (get-refs dummy-header))
+                     (set-refs dummy-header '())))
+               dummy-headers)
+      (let ((set-followups
+            (lambda (header)
+              (let ((refs (get-refs header)))
+                (if (not (null? refs))
+                    (let ((header* (car refs)))
+                      (set-news-header:followup-to! header header*)
+                      (set-news-header:followups!
+                       header*
+                       (cons header (news-header:followups header*)))))))))
+       (for-each set-followups headers)
+       (for-each set-followups dummy-headers)))))
+
+(define (news-header:references header)
+  ;; Check the references header field to guarantee that it's
+  ;; well-formed, and discard it entirely if it isn't.  This paranoia
+  ;; is reasonable since I've already seen bad references during the
+  ;; first few days of testing.
+  (let ((tokens
+        (reverse-string-tokenize
+         (news-header:field-value header "references"))))
+    (if (let loop ((tokens tokens))
+         (or (null? tokens)
+             (and (valid-message-id? (car tokens))
+                  (not (member (car tokens) (cdr tokens)))
+                  (loop (cdr tokens)))))
+       tokens
+       '())))
+\f
+(define (associate-threads-with-trees headers)
+  (let ((threads '()))
+    (for-each (lambda (header)
+               (if (not (news-header:thread header))
+                   (let ((root
+                          (let loop ((header header))
+                            (if (news-header:followup-to header)
+                                (loop (news-header:followup-to header))
+                                header))))
+                     (let ((thread (make-news-thread (list root))))
+                       (set! threads (cons thread threads))
+                       (news-thread:for-each-header thread
+                         (lambda (header)
+                           (set-news-header:thread! header thread)))))))
+             headers)
+    threads))
+
+(define (guarantee-header-number header)
+  (let ((followups (news-header:followups header)))
+    (for-each guarantee-header-number followups)
+    (set-news-header:followups! header (sort followups news-header:<)))
+  (if (not (news-header:number header))
+      (let ((followups (news-header:followups header)))
+       (if (null? followups)
+           (error "Dummy header has no followups:" header))
+       (set-news-header:number! header
+                                (news-header:number (car followups))))))
+\f
+(define (find-subject-associations threads)
+  (let ((subject-alist '()))
+    (for-each (lambda (thread)
+               (news-thread:for-each-header thread
+                 (lambda (header)
+                   (let ((subject
+                          (canonicalize-subject
+                           (news-header:field-value header "subject"))))
+                     (if (not (string-null? subject))
+                         (let ((entry (assoc-subject subject subject-alist)))
+                           (cond ((not entry)
+                                  (set! subject-alist
+                                        (cons (list subject thread)
+                                              subject-alist))
+                                  unspecific)
+                                 ((not (memq thread (cdr entry)))
+                                  (set-cdr! entry
+                                            (cons thread (cdr entry)))))))))))
+             threads)
+    subject-alist))
+
+(define (canonicalize-subject subject)
+  ;; This is optimized by assuming that the subject lines have no
+  ;; leading or trailing white space.  The news-header parser makes
+  ;; that guarantee.
+  (let ((end (string-length subject)))
+    (substring subject
+              (let loop ((start 0))
+                (if (substring-prefix-ci? "re:" 0 3 subject start end)
+                    (loop (substring-skip-leading-space subject
+                                                        (fix:+ start 3)
+                                                        end))
+                    start))
+              end)))
+
+(define (assoc-subject subject alist)
+  (let loop ((alist alist))
+    (and (not (null? alist))
+        (let ((comparison (compare-subjects subject (caar alist))))
+          (if comparison
+              (begin
+                (if (eq? 'LEFT-PREFIX comparison)
+                    (set-car! (car alist) subject))
+                (car alist))
+              (loop (cdr alist)))))))
+
+(define (compare-subjects x y)
+  (let ((xe (string-length x))
+       (ye (string-length y)))
+    (let ((i (substring-match-forward-ci x 0 xe y 0 ye)))
+      (if (fix:= i xe)
+         (if (fix:= i ye) 'EQUAL 'LEFT-PREFIX)
+         (if (fix:= i ye) 'RIGHT-PREFIX #f)))))
+\f
+(define (build-equivalence-classes threads subject-alist)
+  (let ((equivalences (make-eq-hash-table)))
+    (for-each (lambda (thread)
+               (hash-table/put! equivalences
+                                thread
+                                (let ((t (list thread)))
+                                  (set-cdr! t (list t))
+                                  t)))
+             threads)
+    (let ((equivalence!
+          (lambda (x y)
+            (let ((x (hash-table/get equivalences x #f))
+                  (y (hash-table/get equivalences y #f)))
+              (if (not (eq? (cdr x) (cdr y)))
+                  (let ((k
+                         (lambda (x y)
+                           (for-each (lambda (y) (set-cdr! y x)) y)
+                           (set-cdr! (last-pair x) y))))
+                    (if (news-thread:< (car x) (car y))
+                        (k (cdr x) (cdr y))
+                        (k (cdr y) (cdr x)))))))))
+      (for-each (lambda (entry)
+                 (let ((thread (cadr entry)))
+                   (for-each (lambda (thread*) (equivalence! thread thread*))
+                             (cddr entry))))
+               subject-alist))
+    (map (lambda (class) (map car class))
+        (eliminate-duplicates
+         (map cdr (hash-table/datum-list equivalences))))))
+
+(define (eliminate-duplicates items)
+  (let loop ((items items) (result '()))
+    (if (null? items)
+       result
+       (loop (cdr items)
+             (if (memq (car items) result)
+                 result
+                 (cons (car items) result))))))
+
+(define (make-threads-equivalent! threads)
+  (let ((threads (sort threads news-thread:<)))
+    (let ((thread (car threads))
+         (threads (cdr threads)))
+      (for-each (lambda (thread*)
+                 (news-thread:for-each-header thread*
+                   (lambda (header)
+                     (set-news-header:thread! header thread))))
+               threads)
+      (set-cdr! (news-thread:root-headers thread)
+               (map (lambda (thread)
+                      (car (news-thread:root-headers thread)))
+                    threads))
+      thread)))
+\f
+;;;; Miscellaneous
+
+(define (input-port/read-line port)
+  (let ((line (input-port/read-string port char-set:newline)))
+    ;; Discard delimiter, if any -- this is a no-op at EOF.
+    (input-port/discard-char port)
+    line))
+
+(define (input-port/discard-line port)
+  (input-port/discard-chars port char-set:newline)
+  (input-port/discard-char port))
+
+(define char-set:newline (char-set #\newline))
+
+(define (input-port/eof? port)
+  ((or (port/operation port 'EOF?) (error "Port missing EOF? operation:" port))
+   port))
+
+(define (string-tokenize string)
+  (substring-tokenize string 0 (string-length string)))
+
+(define (substring-tokenize string start end)
+  (reverse! (reverse-substring-tokenize string start end)))
+
+(define (reverse-string-tokenize string)
+  (reverse-substring-tokenize string 0 (string-length string)))
+
+(define (reverse-substring-tokenize string start end)
+  (let loop ((start start) (tokens '()))
+    (if (fix:= start end)
+       tokens
+       (let ((delimiter
+              (or (substring-find-next-char-in-set
+                   string start end char-set:whitespace)
+                  end)))
+         (loop (or (substring-find-next-char-in-set
+                    string delimiter end char-set:not-whitespace)
+                   end)
+               (cons (substring string start delimiter) tokens))))))
+
+(define (token->number token)
+  (substring->nonnegative-integer token 0 (string-length token)))
+
+(define (substring->nonnegative-integer line start end)
+  (let ((get-digit
+        (lambda (index)
+          (let ((n
+                 (fix:- (char->integer (string-ref line index))
+                        (char->integer #\0))))
+            (if (or (fix:< n 0) (fix:> n 9))
+                (error:bad-range-argument line #f))
+            n))))
+    (let loop ((index start) (n 0))
+      (if (fix:= index end)
+         n
+         (loop (fix:+ index 1)
+               (+ (* n 10) (get-digit index)))))))
+
+(define (substring-skip-leading-space string start end)
+  (let loop ((index start))
+    (if (and (fix:< index end)
+            (or (char=? #\space (string-ref string index))
+                (char=? #\tab (string-ref string index))))
+       (loop (fix:+ index 1))
+       index)))
+
+(define (substring-skip-trailing-space string start end)
+  (let loop ((index end))
+    (if (fix:< start index)
+       (let ((index* (fix:- index 1)))
+         (if (or (char=? #\space (string-ref string index*))
+                 (char=? #\tab (string-ref string index*)))
+             (loop index*)
+             index))
+       index)))
+
+(define (substring-trim string start end)
+  (let ((start (substring-skip-leading-space string start end)))
+    (substring string start (substring-skip-trailing-space string start end))))
\ No newline at end of file
diff --git a/v7/src/edwin/snr.scm b/v7/src/edwin/snr.scm
new file mode 100644 (file)
index 0000000..4915d27
--- /dev/null
@@ -0,0 +1,1754 @@
+;;; -*-Scheme-*-
+;;;
+;;;    $Id: snr.scm,v 1.1 1995/05/03 07:50:49 cph Exp $
+;;;
+;;;    Copyright (c) 1995 Massachusetts Institute of Technology
+;;;
+;;;    This material was developed by the Scheme project at the
+;;;    Massachusetts Institute of Technology, Department of
+;;;    Electrical Engineering and Computer Science.  Permission to
+;;;    copy this software, to redistribute it, and to use it for any
+;;;    purpose is granted, subject to the following restrictions and
+;;;    understandings.
+;;;
+;;;    1. Any copy made of this software must include this copyright
+;;;    notice in full.
+;;;
+;;;    2. Users of this software agree to make their best efforts (a)
+;;;    to return to the MIT Scheme project any improvements or
+;;;    extensions that they make, so that these may be included in
+;;;    future releases; and (b) to inform MIT of noteworthy uses of
+;;;    this software.
+;;;
+;;;    3. All materials developed as a consequence of the use of this
+;;;    software shall duly acknowledge such use, in accordance with
+;;;    the usual standards of acknowledging credit in academic
+;;;    research.
+;;;
+;;;    4. MIT has made no warrantee or representation that the
+;;;    operation of this software will be error-free, and MIT is
+;;;    under no obligation to provide any services, by way of
+;;;    maintenance, update, or otherwise.
+;;;
+;;;    5. In conjunction with products arising from the use of this
+;;;    material, there shall be no use of the name of the
+;;;    Massachusetts Institute of Technology nor of any adaptation
+;;;    thereof in any advertising, promotional, or sales literature
+;;;    without prior written consent from MIT in each case.
+;;;
+;;; NOTE: Parts of this program (Edwin) were created by translation
+;;; from corresponding parts of GNU Emacs.  Users should be aware that
+;;; the GNU GENERAL PUBLIC LICENSE may apply to these parts.  A copy
+;;; of that license should have been included along with this file.
+;;;
+
+;;;; Scheme News Reader
+
+(declare (usual-integrations))
+
+(load-option 'ORDERED-VECTOR)
+\f
+(define-variable news-server
+  "Host name of the default news server.
+This is the name used by \\[rnews].  If it is an empty string,
+\\[rnews] will prompt for a host name and save it back into
+news-server."
+  ""
+  string?)
+
+(define-variable news-server-show-domain
+  "Switch controlling appearance of server name in news buffers.
+The buffers used by the News reader contain the server name.  If this
+switch is false (the default), the server's domain is suppressed
+before forming the buffer name.  Otherwise, the full server name is
+used."
+  #f
+  boolean?)
+
+(define-variable show-unsubscribed-news-groups
+  "Switch controlling whether unsubscribed news groups appear in news buffers.
+If false (the default), only currently subscribed groups are shown.
+If true, previously subscribed buffers are also shown."
+  #f
+  boolean?)
+
+(define-command rnews
+  "Start a News reader.
+Normally uses the server specified by the variable news-server,
+but with a prefix arg prompts for the server name.
+Only one News reader may be open per server; if a previous News reader
+is open the that server, its buffer is selected."
+  "P"
+  (lambda (prompt?)
+    (select-buffer
+     (let ((server
+           (let ((server (ref-variable news-server #f)))
+             (if (or prompt? (string-null? server))
+                 (prompt-for-news-server "News server")
+                 server))))
+       (or (find-news-server-buffer server)
+          (make-news-server-buffer server))))))
+
+(define (prompt-for-news-server prompt)
+  (let ((default (ref-variable news-server #f)))
+    (let ((server
+          (prompt-for-string prompt
+                             (and (not (string-null? default))
+                                  default))))
+      (if (string-null? default)
+         (set-variable! news-server server #f))
+      server)))
+
+(define-command news-kill-current-buffer
+  "Kill the current buffer."
+  ()
+  (lambda ()
+    (let ((buffer (current-buffer)))
+      (let ((parent (buffer-tree:parent buffer #f)))
+       (if parent
+           (select-buffer parent)))
+      (kill-buffer buffer))))
+
+(define-command news-next-line
+  "Move down one or more lines and put point at start of line."
+  "P"
+  (lambda (argument)
+    (set-current-point! (line-start (current-point) 0))
+    ((ref-command next-line) argument)))
+
+(define-command news-previous-line
+  "Move up one or more lines and put point at start of line."
+  "p"
+  (lambda (argument)
+    (set-current-point! (line-start (current-point) 0))
+    ((ref-command previous-line) argument)))
+\f
+;;;; News-Server Buffer
+
+(define (find-news-server-buffer server)
+  (let ((buffer
+        (list-search-positive (buffer-list)
+          (lambda (buffer)
+            (and (news-server-buffer? buffer)
+                 (string-ci=? (news-server-buffer:server buffer) server))))))
+    (and buffer
+        (begin
+          (news-server-buffer:guarantee-connection buffer)
+          buffer))))
+
+(define (make-news-server-buffer server)
+  (let ((buffer (new-buffer (news-server-buffer-name server))))
+    (set-buffer-major-mode! buffer (ref-mode-object news-server))
+    (disable-group-undo! (buffer-group buffer))
+    (add-kill-buffer-hook buffer news-server-buffer:kill)
+    (news-server-buffer:open-connection buffer server)
+    (let ((groups
+          (sort (get-ini-file-groups (news-server-buffer:connection buffer))
+                news-group:<)))
+      (for-each news-group:update-ranges! groups)
+      (initialize-buffer-news-groups buffer groups))
+    (set-buffer-point! buffer (buffer-start buffer))
+    (buffer-not-modified! buffer)
+    (set-buffer-read-only! buffer)
+    buffer))
+
+(define (news-server-buffer-name server)
+  (string-append (if (ref-variable news-server-show-domain #f)
+                    server
+                    (let ((dot (string-find-next-char server #\.)))
+                      (if dot
+                          (string-head server dot)
+                          server)))
+                ":news"))
+
+(define (news-server-buffer:kill buffer)
+  (news-server-buffer:save-groups buffer)
+  (news-server-buffer:close-connection buffer)
+  (for-each kill-buffer (buffer-tree:children buffer)))
+
+(define (news-server-buffer:save-groups buffer)
+  (put-ini-file-groups (news-server-buffer:connection buffer)
+                      (buffer-news-groups buffer)))
+\f
+(define (news-server-buffer? buffer)
+  (buffer-get buffer 'NNTP-CONNECTION #f))
+
+(define (news-server-buffer:open-connection buffer server)
+  (let ((msg (string-append "Opening connection to " server "... ")))
+    (message msg)
+    (buffer-put! buffer 'NNTP-CONNECTION (open-nntp-connection server))
+    (message msg "done")))
+
+(define (news-server-buffer:guarantee-connection buffer)
+  (let ((connection (news-server-buffer:connection buffer)))
+    (if (nntp-connection:closed? connection)
+       (let ((msg
+              (string-append "Reopening connection to "
+                             (nntp-connection:server connection)
+                             "... ")))
+         (message msg)
+         (nntp-connection:reopen connection)
+         (message msg "done")))))
+
+(define (news-server-buffer:close-connection buffer)
+  (let ((connection (buffer-get buffer 'NNTP-CONNECTION)))
+    (if connection
+       (let ((msg
+              (string-append "Closing connection to "
+                             (nntp-connection:server connection)
+                             "... ")))
+         (message msg)
+         (nntp-connection:close connection)
+         (message msg "done")))))
+
+(define (news-server-buffer:connection buffer)
+  (let ((connection (buffer-get buffer 'NNTP-CONNECTION #f)))
+    (if (not connection)
+       (error "Buffer has no NNTP connection:" (buffer-name buffer)))
+    connection))
+
+(define (news-server-buffer:server buffer)
+  (nntp-connection:server (news-server-buffer:connection buffer)))
+\f
+(define (initialize-buffer-news-groups buffer groups)
+  (let ((mark (mark-left-inserting-copy (buffer-start buffer))))
+    (for-each (lambda (group)
+               (if (show-buffer-news-group? buffer group)
+                   (begin
+                     (insert-news-group-line group mark)
+                     (insert-newline mark))))
+             groups)
+    (mark-temporary! mark))
+  (let loop ((start (buffer-start buffer)) (groups groups))
+    (if (not (null? groups))
+       (if (show-buffer-news-group? buffer (car groups))
+           (let ((end (line-start start 1 'ERROR))
+                 (group (car groups)))
+             (region-put! start end 'NEWS-GROUP group)
+             (loop end (cdr groups)))
+           (loop start (cdr groups)))))
+  (buffer-put! buffer 'NEWS-GROUPS groups))
+
+(define (buffer-news-groups buffer)
+  (buffer-get buffer 'NEWS-GROUPS '()))
+
+(define (buffer-news-group-mark buffer group error?)
+  (or (find-buffer-line buffer group 'NEWS-GROUP news-group:<)
+      (and error?
+          (error "Buffer has no line for this group:" group buffer))))
+
+(define (add-buffer-news-group buffer group)
+  (let loop ((groups (buffer-news-groups buffer)) (prev #f))
+    (cond ((or (null? groups)
+              (news-group:< group (car groups)))
+          (let ((groups (cons group groups)))
+            (if prev
+                (set-cdr! prev groups)
+                (buffer-put! buffer 'NEWS-GROUPS groups))))
+         ((not (eq? group (car groups)))
+          (loop (cdr groups) groups)))))
+\f
+(define (maybe-update-buffer-news-group buffer group)
+  (if (memq group (buffer-news-groups buffer))
+      (update-buffer-news-group buffer group)))
+
+(define (update-buffer-news-group buffer group)
+  (with-buffer-open buffer
+    (lambda ()
+      (let ((mark (buffer-news-group-mark buffer group #f)))
+       (if mark
+           (if (show-buffer-news-group? buffer group)
+               (let ((start (mark-right-inserting-copy mark))
+                     (end (mark-left-inserting-copy mark))
+                     (point (buffer-point buffer)))
+                 (let ((le (line-end end 0)))
+                   (let ((column
+                          (and (mark<= end point)
+                               (mark<= point le)
+                               (mark-column point))))
+                     (delete-string end le)
+                     (insert-news-group-line group end)
+                     (if column
+                         (set-buffer-point! buffer
+                                            (move-to-column start column)))))
+                 (region-put! start (mark1+ end) 'NEWS-GROUP group)
+                 (mark-temporary! end)
+                 (mark-temporary! start))
+               (delete-string mark (line-start mark 1 'ERROR)))
+           (if (show-buffer-news-group? buffer group)
+               (let ((mark
+                      (let ((tail (memq group (buffer-news-groups buffer))))
+                        (if (null? (cdr tail))
+                            (buffer-end buffer)
+                            (buffer-news-group-mark buffer (cadr tail) #t)))))
+                 (let ((start (mark-right-inserting-copy mark))
+                       (end (mark-left-inserting-copy mark)))
+                   (insert-news-group-line group end)
+                   (insert-newline end)
+                   (region-put! start end 'NEWS-GROUP group)
+                   (mark-temporary! end)
+                   (mark-temporary! start)))))))))
+
+(define (show-buffer-news-group? buffer group)
+  (and (news-group:active? group)
+       (or (news-group:subscribed? group)
+          (ref-variable show-unsubscribed-news-groups buffer))))
+
+(define (insert-news-group-line group mark)
+  (insert-string (if (news-group:subscribed? group) "  " "U ") mark)
+  (insert-string-pad-left
+   (number->string (news-group:number-of-articles group))
+   5 #\space mark)
+  (insert-string ": " mark)
+  (insert-string (news-group:name group) mark))
+\f
+;;;; News-Server Mode
+
+(define-major-mode news-server read-only "News Server"
+  "Major mode for browsing a News server.
+Each line shows one of the News groups on the server.  The number near
+the left of the line is an estimate of the number of unread messages
+available in that group.  A `U' character appearing in the left column
+indicates that the group is Unsubscribed.
+
+This mode's commands include:
+
+\\[all-news-groups]    select a buffer showing all of the server's News groups
+\\[select-news-group]  browse articles in the News group indicated by point
+\\[subscribe-news-group]       subscribe to the News group indicated by point
+\\[unsubscribe-news-group]     unsubscribe from the News group indicated by point")
+
+(define-key 'news-server #\space 'select-news-group)
+(define-key 'news-server #\a 'all-news-groups)
+(define-key 'news-server #\g 'refresh-news-groups)
+(define-key 'news-server #\q 'news-kill-current-buffer)
+(define-key 'news-server #\r 'refresh-news-group)
+(define-key 'news-server #\s 'subscribe-news-group)
+(define-key 'news-server #\S 'subscribe-news-group-by-name)
+(define-key 'news-server #\u 'unsubscribe-news-group)
+(define-key 'news-server #\c-n 'news-next-line)
+(define-key 'news-server #\c-p 'news-previous-line)
+(define-key 'news-server '(#\c-x #\c-s) 'save-news-server-data)
+
+(define-command select-news-group
+  "Browse the News group indicated by point.
+Selects a buffer showing the subject lines of the articles in the News group."
+  ()
+  (lambda ()
+    (let ((buffer
+          (let ((server-buffer (current-news-server-buffer))
+                (group (current-news-group)))
+            (or (find-news-group-buffer server-buffer group)
+                (make-news-group-buffer server-buffer group)))))
+      (select-buffer buffer)
+      (news-group-buffer:update-server-buffer buffer))))
+
+(define-command refresh-news-groups
+  "Update the unread-message estimates for all of the News groups shown.
+This will take a long time if done in the all-groups buffer."
+  ()
+  (lambda ()
+    (let ((buffer (current-buffer)))
+      (for-each
+       (lambda (group)
+        (if (show-buffer-news-group? buffer group)
+            (begin
+              (news-group:update-ranges! group)
+              (update-buffer-news-group buffer group))))
+       (buffer-news-groups buffer)))))
+
+(define-command refresh-news-group
+  "Update the unread-message estimate for the News group indicated by point.
+With prefix argument, updates the next several News groups."
+  "P"
+  (lambda (argument)
+    (news-group-command argument
+      (let ((buffer (current-buffer)))
+       (lambda (group)
+         (news-group:update-ranges! group)
+         (update-buffer-news-group buffer group))))))
+\f
+(define-command subscribe-news-group
+  "Subscribe to the News group indicated by point.
+Normally useful only in the all-groups buffer, since the server buffer
+doesn't show unsubscribed groups.
+With prefix argument, subscribes to the next several News groups."
+  "P"
+  (lambda (argument)
+    (news-group-command argument
+                       (make-news-group-subscriber (current-buffer)))))
+
+(define-command subscribe-news-group-by-name
+  "Subscribe to a News group by name.
+Prompts for the News-group name, with completion."
+  ()
+  (lambda ()
+    ((make-news-group-subscriber (current-buffer))
+     (prompt-for-active-news-group "Subscribe to news group"
+                                  #f
+                                  (current-news-server-buffer)))))
+
+(define (make-news-group-subscriber buffer)
+  (let ((server-buffer (news-server-buffer buffer)))
+    (let ((all-groups (find-all-news-groups-buffer server-buffer)))
+      (lambda (group)
+       (set-news-group:subscribed?! group #t)
+       (add-buffer-news-group server-buffer group)
+       (update-buffer-news-group buffer group)
+       (cond ((not (eq? buffer server-buffer))
+              (update-buffer-news-group server-buffer group))
+             ((and all-groups (not (eq? buffer all-groups)))
+              (update-buffer-news-group all-groups group)))))))
+
+(define-command unsubscribe-news-group
+  "Unsubscribe from the News group indicated by point.
+With prefix argument, unsubscribes from the next several News groups."
+  "P"
+  (lambda (argument)
+    (news-group-command argument
+      (let ((buffer (current-buffer)))
+       (let ((server-buffer (news-server-buffer buffer)))
+         (let ((all-groups (find-all-news-groups-buffer server-buffer)))
+           (lambda (group)
+             (set-news-group:subscribed?! group #f)
+             (update-buffer-news-group buffer group)
+             (cond ((not (eq? buffer server-buffer))
+                    (maybe-update-buffer-news-group server-buffer group))
+                   ((and all-groups (not (eq? buffer all-groups)))
+                    (update-buffer-news-group all-groups group))))))))))
+
+(define-command all-news-groups
+  "Select a buffer showing all of the News groups on this server.
+This buffer shows subscribed and unsubscribed groups, and is useful
+for choosing new groups to subscribe to.
+
+Making this buffer for the first time can be slow."
+  ()
+  (lambda ()
+    (select-buffer
+     (let ((server-buffer (current-news-server-buffer)))
+       (or (find-all-news-groups-buffer server-buffer)
+          (make-all-news-groups-buffer server-buffer))))))
+
+(define-command save-news-server-data
+  "Update the \"snr.ini\" file with current data."
+  ()
+  (lambda ()
+    (news-server-buffer:save-groups (current-news-server-buffer))))
+\f
+(define (current-news-server-buffer)
+  (news-server-buffer (current-buffer)))
+
+(define (news-server-buffer buffer)
+  (if (news-server-buffer? buffer)
+      buffer
+      (buffer-tree:parent buffer #t)))
+
+(define (current-news-group)
+  (current-property-item 'NEWS-GROUP "news-group"))
+
+(define (news-group-command argument procedure)
+  (repeating-command argument procedure 'NEWS-GROUP "news-group"))
+
+(define (prompt-for-active-news-group prompt default server-buffer)
+  (let ((connection (news-server-buffer:connection server-buffer)))
+    (let ((groups (lambda () (nntp-connection:active-groups connection)))
+         (string->group
+          (lambda (string) (find-active-news-group connection string))))
+      (string->group
+       (let ((convert
+             (lambda (vector) (map news-group:name (vector->list vector)))))
+        (prompt-for-completed-string prompt default 'VISIBLE-DEFAULT
+          (lambda (string if-unique if-not-unique if-not-found)
+            (ordered-vector-minimum-match (groups) string news-group:name
+                                          string-order (prefix-matcher string)
+              (lambda (group)
+                (if-unique (news-group:name group)))
+              (lambda (group gcm all-matches)
+                (if-not-unique (string-head (news-group:name group) gcm)
+                               (lambda () (convert (all-matches)))))
+              if-not-found))
+          (lambda (string)
+            (convert
+             (ordered-vector-matches (groups) string news-group:name
+                                     string-order (prefix-matcher string))))
+          string->group
+          #t))))))
+
+(define (string-order x y)
+  (string-compare x y
+                 (lambda () 'EQUAL)
+                 (lambda () 'LESS)
+                 (lambda () 'GREATER)))
+
+(define (prefix-matcher prefix)
+  (let ((plen (string-length prefix)))
+    (lambda (x y)
+      (let ((n (string-match-forward x y)))
+       (and (fix:>= n plen)
+            n)))))
+\f
+;;;; All-Groups Buffer
+
+(define (find-all-news-groups-buffer server-buffer)
+  (buffer-tree:child server-buffer 'ALL-NEWS-GROUPS #f))
+
+(define (make-all-news-groups-buffer server-buffer)
+  (let ((buffer
+        (new-buffer
+         (string-append "all-groups:"
+                        (news-server-buffer-name
+                         (news-server-buffer:server server-buffer))))))
+    (set-buffer-major-mode! buffer (ref-mode-object news-server))
+    (local-set-variable! show-unsubscribed-news-groups #t buffer)
+    (disable-group-undo! (buffer-group buffer))
+    (let ((groups
+          (nntp-connection:active-groups
+           (news-server-buffer:connection server-buffer))))
+      (let ((msg "Building all-groups buffer... "))
+       (message msg)
+       (initialize-buffer-news-groups buffer (vector->list groups))
+       (message msg "done")))
+    (buffer-tree:attach-child! server-buffer 'ALL-NEWS-GROUPS buffer)
+    (set-buffer-point! buffer (buffer-start buffer))
+    (buffer-not-modified! buffer)
+    (set-buffer-read-only! buffer)
+    buffer))
+\f
+;;;; News-Group Buffer
+
+(define (find-news-group-buffer server-buffer group)
+  (buffer-tree:child server-buffer group #f))
+
+(define (make-news-group-buffer server-buffer group)
+  (let ((buffer (new-buffer (news-group-buffer-name group))))
+    (set-buffer-major-mode! buffer (ref-mode-object news-group))
+    (disable-group-undo! (buffer-group buffer))
+    (buffer-put! buffer 'NEWS-GROUP group)
+    (buffer-tree:attach-child! server-buffer group buffer)
+    (add-kill-buffer-hook buffer news-group-buffer:kill)
+    (add-select-buffer-hook buffer news-group-buffer:select)
+    (initialize-news-group-buffer buffer #f)
+    (set-buffer-read-only! buffer)
+    buffer))
+
+(define (news-group-buffer-name group)
+  (string-append (news-group:name group)
+                ":"
+                (news-server-buffer-name (news-group:server group))))
+
+(define (news-group-buffer:kill buffer)
+  (news-group-buffer:update-server-buffer buffer)
+  (let ((group (get-buffer-property buffer 'NEWS-GROUP)))
+    (for-each
+     (lambda (header)
+       (if (not (news-header:article-unseen? header))
+          (news-group:discard-cached-header! group
+                                             (news-header:number header))))
+     (news-group:cached-headers group))))
+
+(define (news-group-buffer:select group-buffer window)
+  (news-group-buffer:delete-context-window group-buffer window))
+
+(define (initialize-news-group-buffer buffer all?)
+  (fill-news-group-buffer
+   buffer
+   (let ((group (get-buffer-property buffer 'NEWS-GROUP)))
+     (news-group:headers
+      group
+      (ranges->list
+       (complement-ranges (if all? '() (news-group:ranges-seen group))
+                         (news-group:first-article group)
+                         (news-group:last-article group))))))
+  (set-buffer-point! buffer (buffer-start buffer))
+  (buffer-not-modified! buffer))
+\f
+(define (fill-news-group-buffer buffer headers)
+  (let ((mark (mark-left-inserting-copy (buffer-end buffer)))
+       (subject #f)
+       (line 0)
+       (lines '()))
+    (for-each
+     (lambda (thread)
+       (let insert-headers
+          ((headers (news-thread:root-headers thread))
+           (indentation 0))
+        (for-each
+         (lambda (header)
+           (if (news-header:dummy? header)
+               (insert-headers (news-header:followups header) indentation)
+               (let* ((subject*
+                       (canonicalize-subject
+                        (news-header:field-value header "subject"))))
+                 (let ((comparison
+                        (and subject (compare-subjects subject subject*))))
+                   (insert-news-header-line header
+                                            indentation
+                                            (not comparison)
+                                            mark)
+                   (if (or (not comparison)
+                           (eq? 'RIGHT-PREFIX comparison))
+                       (set! subject subject*)))
+                 (set-news-header:line-number! header line)
+                 (set! line (fix:+ line 1))
+                 (set! lines (cons header lines))
+                 (insert-headers (news-header:followups header)
+                                 (+ indentation 4)))))
+         headers)))
+     (organize-headers-into-threads headers))
+    (mark-temporary! mark)
+    (buffer-put! buffer 'NEWS-HEADERS (list->vector (reverse! lines)))))
+
+(define (insert-news-header-line header indentation subject? mark)
+  (let ((start (mark-right-inserting-copy mark)))
+    (insert-char (news-header:status header) mark)
+    (insert-char #\space mark)
+    (insert-string-pad-left (news-header:field-value header "lines")
+                           4 #\space mark)
+    (insert-char #\: mark)
+    (insert-chars #\space indentation mark)
+    (if subject?
+       (begin
+         (insert-char #\space mark)
+         (insert-string (news-header:field-value header "subject") mark)))
+    (insert-string " (" mark)
+    (insert-string (let ((from (news-header:field-value header "from")))
+                    (or (rfc822-first-address from)
+                        from))
+                  mark)
+    (insert-char #\) mark)
+    (insert-newline mark)
+    (region-put! start mark 'NEWS-HEADER header)
+    (mark-temporary! start)))
+
+(define (buffer-news-headers buffer)
+  (buffer-get buffer 'NEWS-HEADERS '#()))
+
+(define (buffer-news-header-mark buffer header error?)
+  (or (find-buffer-line buffer header 'NEWS-HEADER
+                       (lambda (x y)
+                         (fix:< (news-header:line-number x)
+                                (news-header:line-number y))))
+      (and error?
+          (error "Buffer has no line for this header:" header buffer))))
+\f
+(define (update-buffer-news-header-status buffer header)
+  (with-buffer-open buffer
+    (lambda ()
+      (let ((mark
+            (mark-right-inserting-copy
+             (buffer-news-header-mark buffer header #t))))
+       (let ((preserve-point? (mark= (buffer-point buffer) mark)))
+         (delete-right-char mark)
+         (insert-char (news-header:status header) mark)
+         ;; Grumble: must rewrite 'NEWS-HEADER property because
+         ;; inserted characters have no properties.
+         (region-put! mark (mark1+ mark) 'NEWS-HEADER header)
+         (if preserve-point? (set-buffer-point! buffer mark))
+         (mark-temporary! mark))))))
+
+(define (news-group-buffer:update-server-buffer buffer)
+  (let ((group (get-buffer-property buffer 'NEWS-GROUP)))
+    (news-group:update-ranges! group)
+    (let ((server-buffer (buffer-tree:parent buffer #f)))
+      (if server-buffer
+         (update-buffer-news-group server-buffer group)))))
+
+(define (show-news-article-context article-window context-lines)
+  (let ((context-window
+        (window-split-vertically! article-window
+                                  (- (window-y-size article-window)
+                                     context-lines)))
+       (group-buffer (buffer-tree:parent (window-buffer article-window) #t)))
+    (buffer-put! group-buffer 'CONTEXT-WINDOW (weak-cons context-window #f))
+    (select-buffer-in-window group-buffer context-window #t)
+    (center-news-article-context context-window)))
+
+(define (news-group-buffer:delete-context-window group-buffer window)
+  (let ((context-window (news-group-buffer:context-window group-buffer #t)))
+    (if (and context-window (not (eq? window context-window)))
+       (begin
+         (window-delete! context-window window)
+         (buffer-remove! group-buffer 'CONTEXT-WINDOW)))))
+
+(define (news-group-buffer:set-point! group-buffer mark)
+  (set-buffer-point! group-buffer mark)
+  (let ((context-window (news-group-buffer:context-window group-buffer #t)))
+    (if context-window
+       (begin
+         (set-window-point! context-window mark)
+         (center-news-article-context context-window)))))
+
+(define (center-news-article-context context-window)
+  (window-scroll-y-absolute! context-window
+                            (integer-floor (window-y-size context-window) 2)))
+
+(define (news-group-buffer:context-window buffer require-buffer?)
+  (let ((pair (buffer-get buffer 'CONTEXT-WINDOW #f)))
+    (and pair
+        (let ((window
+               (let ((window (weak-car pair)))
+                 (and window
+                      (window-visible? window)
+                      (or (not require-buffer?)
+                          (eq? buffer (window-buffer window)))
+                      window))))
+          (if (not window)
+              (buffer-remove! buffer 'CONTEXT-WINDOW))
+          window))))
+\f
+;;;; News-Group Mode
+
+(define-major-mode news-group read-only "News Group"
+  "Major mode for browsing subjects of articles in a News group.
+Each line shows one of the articles in the group.  The number near the
+left is an estimate of the number of lines in the article.  A `D' in
+the left column indicates that the article has either been read or
+marked as such.  The right-hand side of the line shows the subject
+line from the article, followed by the author's name in parenthesis.
+
+Articles are grouped into conversational `threads' where possible.  In
+such threads, the subjects of followup articles are suppressed, and
+the parenthesized author's name appears indented.  The indentation
+shows structure of the conversation, with followups being indented a
+bit more than the articles they follow-up to.
+
+This mode's commands include:
+
+\\[select-news-article]        select a buffer containing the article indicated by point
+\\[delete-news-article]        mark the article indicated by point as read
+\\[delete-news-thread] mark the whole thread as read
+\\[undelete-news-article]      unmark the article indicated by point
+\\[undelete-news-thread]       unmark the whole thread
+\\[expunge-news-group] remove from the buffer all marked lines"
+  (lambda (buffer)
+    (local-set-variable! truncate-lines #t buffer)))
+
+(define-key 'news-group #\space 'select-news-article)
+(define-key 'news-group #\c 'catch-up-news-group)
+(define-key 'news-group #\d 'delete-news-article)
+(define-key 'news-group #\D 'delete-news-thread)
+(define-key 'news-group #\g 'revert-news-group)
+(define-key 'news-group #\q 'news-kill-current-buffer)
+(define-key 'news-group #\u 'undelete-news-article)
+(define-key 'news-group #\U 'undelete-news-thread)
+(define-key 'news-group #\x 'expunge-news-group)
+(define-key 'news-group #\c-n 'news-next-line)
+(define-key 'news-group #\c-p 'news-previous-line)
+(define-key 'news-server '(#\c-x #\c-s) 'save-news-server-data)
+
+(define-command select-news-article
+  "Select a buffer containing the News article indicated by point."
+  ()
+  (lambda ()
+     (select-news-article (current-buffer) (current-news-header))))
+
+(define (select-news-article group-buffer header)
+  (select-buffer
+   (or (find-news-article-buffer group-buffer header)
+       (make-news-article-buffer group-buffer header)
+       (editor-error "Article no longer available from server."))))
+\f
+(define-command delete-news-article
+  "Mark as `read' the News article indicated by point.
+With prefix argument, marks the next several articles."
+  "P"
+  (lambda (argument)
+    (news-header-command argument (header-deletion-procedure))))
+
+(define-command delete-news-thread
+  "Mark as `read' the conversation thread indicated by point.
+This marks the article indicated by point and any other articles in
+the same thread as that article."
+  ()
+  (lambda ()
+    (let ((root (news-header:thread (current-news-header))))
+      (news-thread:for-each-header root (header-deletion-procedure))
+      (set-current-point!
+       (find-next-header-line (line-start (current-point) 0)
+        (lambda (header)
+          (not (eq? root (news-header:thread header)))))))))
+
+(define (header-deletion-procedure)
+  (let ((buffer (current-buffer)))
+    (lambda (header)
+      (if (not (news-header:dummy? header))
+         (begin
+           (news-header:article-seen! header)
+           (update-buffer-news-header-status buffer header))))))
+
+(define-command undelete-news-article
+  "Unmark the News article indicated by point.
+With prefix argument, unmarks the next several articles."
+  "P"
+  (lambda (argument)
+    (news-header-command argument (header-undeletion-procedure))))
+
+(define-command undelete-news-thread
+  "Unmark the conversation thread indicated by point.
+This unmarks the article indicated by point and any other articles in
+the same thread as that article."
+  ()
+  (lambda ()
+    (let ((root (news-header:thread (current-news-header))))
+      (news-thread:for-each-header root (header-undeletion-procedure))
+      (set-current-point!
+       (find-next-header-line (line-start (current-point) 0)
+        (lambda (header)
+          (not (eq? root (news-header:thread header)))))))))
+
+(define (header-undeletion-procedure)
+  (let ((buffer (current-buffer)))
+    (lambda (header)
+      (if (not (news-header:dummy? header))
+         (begin
+           (news-header:article-unseen! header)
+           (update-buffer-news-header-status buffer header))))))
+
+(define-command expunge-news-group
+  "Remove all marked lines from the current buffer."
+  ()
+  (lambda ()
+    (let ((buffer (current-buffer)))
+      (with-buffer-open buffer
+       (lambda ()
+         (let loop ((mark (buffer-absolute-start buffer)))
+           (if (not (group-end? mark))
+               (if (news-header:article-unseen? (get-news-header mark))
+                   (loop (line-start mark 1 'ERROR))
+                   (let ((mark (mark-right-inserting-copy mark)))
+                     (delete-string mark (line-start mark 1 'ERROR))
+                     (mark-temporary! mark)
+                     (loop mark))))))))))
+\f
+(define-command catch-up-news-group
+  "Mark all of the articles as read, and return to the News server buffer.
+This kills the current buffer."
+  ()
+  (lambda ()
+    (if (prompt-for-confirmation? "Delete all articles not marked as read")
+       (begin
+         (let ((buffer (current-buffer)))
+           (for-each-vector-element (buffer-news-headers buffer)
+                                    news-header:article-seen!))
+         ((ref-command news-kill-current-buffer))))))
+
+(define-command revert-news-group
+  "Refresh the article list from the News server.
+This gets any new article headers from the News server, adding their
+lines to the current buffer.  With a prefix argument, this shows all
+of the articles in the News group, including those that were
+previously marked as `read'."
+  "P"
+  (lambda (argument)
+    (let ((buffer (current-buffer)))
+      (with-buffer-open buffer
+       (lambda ()
+         (region-delete! (buffer-region buffer))
+         (initialize-news-group-buffer buffer argument))))))
+
+(define (find-next-header-line ls predicate)
+  (if (group-end? ls)
+      ls
+      (let loop ((ls (line-start ls 1 'ERROR)))
+       (if (or (let ((header (region-get ls 'NEWS-HEADER #f)))
+                 (or (not header)
+                     (predicate header)))
+               (group-end? ls))
+           ls
+           (loop (line-start ls 1 'ERROR))))))
+
+(define (find-previous-header-line ls predicate)
+  (if (group-start? ls)
+      ls
+      (let loop ((ls (line-start ls -1 'ERROR)))
+       (if (or (let ((header (region-get ls 'NEWS-HEADER #f)))
+                 (or (not header)
+                     (predicate header)))
+               (group-start? ls))
+           ls
+           (loop (line-start ls -1 'ERROR))))))
+
+(define (next-unseen-header-line ls)
+  (find-next-header-line ls news-header:article-unseen?))
+
+(define (previous-unseen-header-line ls)
+  (find-previous-header-line ls news-header:article-unseen?))
+
+(define (current-news-header)
+  (current-property-item 'NEWS-HEADER "article header"))
+
+(define (news-header-command argument procedure)
+  (repeating-command argument procedure 'NEWS-HEADER "article header"))
+
+(define (get-news-header mark)
+  (property-item mark 'NEWS-HEADER "news header"))
+\f
+;;;; News-Article Buffer
+
+(define (find-news-article-buffer group-buffer header)
+  (buffer-tree:child group-buffer header #f))
+
+(define (make-news-article-buffer group-buffer header)
+  (let ((buffer (new-buffer (news-article-buffer-name header))))
+    (set-buffer-major-mode! buffer (ref-mode-object news-article))
+    (disable-group-undo! (buffer-group group-buffer))
+    (if (let ((msg "Reading article... "))
+         (message msg)
+         (let ((value
+                (call-with-output-mark (buffer-end buffer)
+                  (lambda (port)
+                    (news-header:read-body header port)))))
+           (message msg "done")
+           value))
+       (begin
+         (insert-news-header header buffer #t)
+         (enable-group-undo! (buffer-group group-buffer))
+         (buffer-put! buffer 'NEWS-HEADER header)
+         (buffer-tree:attach-child! group-buffer header buffer)
+         (set-buffer-point! buffer (buffer-start buffer))
+         (buffer-not-modified! buffer)
+         (set-buffer-read-only! buffer)
+         (news-header:article-seen! header)
+         (update-buffer-news-header-status group-buffer header)
+         buffer)
+       (begin
+         (kill-buffer buffer)
+         (news-header:article-seen! header)
+         (update-buffer-news-header-status group-buffer header)
+         #f))))
+
+(define (insert-news-header header buffer truncate?)
+  (with-buffer-open buffer
+    (lambda ()
+      (let ((hend (mark-left-inserting-copy (buffer-start buffer))))
+       (insert-string (news-header:text header) hend)
+       (insert-newline hend)
+       (if truncate? (delete-ignored-headers (buffer-start buffer) hend))
+       (mark-temporary! hend))))
+  (buffer-put! buffer 'NEWS-ARTICLE-HEADER-TRUNCATED? truncate?))
+
+(define (delete-ignored-headers hstart hend)
+  (let ((regexp (ref-variable rmail-ignored-headers hstart)))
+    (if regexp
+       (let ((point (mark-right-inserting-copy hstart))
+             (group (mark-group hstart))
+             (p1 (re-compile-pattern regexp #t))
+             (p2 (re-compile-pattern "\n[^ \t]" #f)))
+         (do ()
+             ((not (re-search-buffer-forward p1 #t #f
+                                             group
+                                             (mark-index point)
+                                             (mark-index hend))))
+           (move-mark-to! point (line-start (re-match-start 0) 0))
+           (delete-string
+            point
+            (make-mark group
+                       (fix:- (re-search-buffer-forward p2 #f #f
+                                                        group
+                                                        (mark-index point)
+                                                        (mark-index hend))
+                              1))))
+         (mark-temporary! point)))))
+
+(define (delete-news-header buffer)
+  (with-buffer-open buffer
+    (lambda ()
+      (let ((mark (search-forward "\n\n" (buffer-start buffer))))
+       (if (not mark)
+           (error "Can't find end of news article header:" buffer))
+       (delete-string (buffer-start buffer) mark)))))
+
+(define (news-article-buffer-name header)
+  (string-append (number->string (news-header:number header))
+                ":"
+                (news-group-buffer-name (news-header:group header))))
+\f
+;;;; News-Article Mode
+
+(define-major-mode news-article read-only "News Article"
+  "Major mode for reading a News article.
+This mode's commands include:
+
+\\[next-news-article]  read the next unread article
+\\[previous-news-article]      read the previous unread article
+\\[toggle-news-article-header] show/don't show all of the articles header lines
+\\[toggle-news-article-context]        show/don't show window of the News group buffer
+\\[reply-to-news-article]      reply by email to this article
+\\[output-news-article]        output this article to a mail file
+\\[output-news-article-to-rmail-file]  output this article to an RMAIL file")
+
+(define-key 'news-article #\space '(news-article . #\c-v))
+(define-key 'news-article #\rubout '(news-article . #\m-v))
+(define-key 'news-article #\c 'toggle-news-article-context)
+(define-key 'news-article #\n 'next-news-article)
+(define-key 'news-article #\o 'output-news-article-to-rmail-file)
+(define-key 'news-article #\p 'previous-news-article)
+(define-key 'news-article #\q 'news-kill-current-buffer)
+(define-key 'news-article #\r 'reply-to-news-article)
+(define-key 'news-article #\t 'toggle-news-article-header)
+(define-key 'news-article #\c-o 'output-news-article)
+\f
+(define-command next-news-article
+  "Select a buffer containing the next unread article in the News group.
+If there is no such article, returns to the News group buffer.
+Kills the current buffer in either case."
+  ()
+  (lambda ()
+    (news-article-motion-command
+     (lambda (group-buffer header)
+       (let ((headers (buffer-news-headers group-buffer)))
+        (let ((index (vector-find-next-element headers header))
+              (length (vector-length headers)))
+          (if (not index)
+              (error "News header missing from headers list:" header))
+          (let loop ((index (fix:+ index 1)))
+            (if (fix:= index length)
+                "No next message in group."
+                (let ((header (vector-ref headers index)))
+                  (if (buffer-news-header-mark group-buffer header #f)
+                      header
+                      (loop (fix:+ index 1))))))))))))
+
+(define-command previous-news-article
+  "Select a buffer containing the previous unread article in the News group.
+If there is no such article, returns to the News group buffer.
+Kills the current buffer in either case."
+  ()
+  (lambda ()
+    (news-article-motion-command
+     (lambda (group-buffer header)
+       (let ((headers (buffer-news-headers group-buffer)))
+        (let ((index (vector-find-next-element headers header)))
+          (if (not index)
+              (error "News header missing from headers list:" header))
+          (let loop ((index (fix:- index 1)))
+            (if (fix:< index 0)
+                "No previous message in group."
+                (let ((header (vector-ref headers index)))
+                  (if (buffer-news-header-mark group-buffer header #f)
+                      header
+                      (loop (fix:- index 1))))))))))))
+
+(define (news-article-motion-command procedure)
+  (let ((buffer (current-buffer)))
+    (let ((group-buffer (buffer-tree:parent buffer #t)))
+      (let ((header (procedure group-buffer (buffer-news-header buffer))))
+       (if (news-header? header)
+           (begin
+             (select-news-article group-buffer header)
+             (news-group-buffer:set-point!
+              group-buffer
+              (buffer-news-header-mark group-buffer header #t)))
+           (begin
+             (select-buffer group-buffer)
+             (message header)))))
+    (kill-buffer buffer)))
+\f
+(define-command toggle-news-article-header
+  "Show original article header if pruned header currently shown, or vice versa.
+Normally, the header lines specified in the variable rmail-ignored-headers
+are not shown; this command shows them, or hides them if they are shown."
+  ()
+  (lambda ()
+    (let ((buffer (current-buffer)))
+      (let ((header (buffer-news-header buffer)))
+       (delete-news-header buffer)
+       (insert-news-header
+        header
+        buffer
+        (not (buffer-get buffer 'NEWS-ARTICLE-HEADER-TRUNCATED? #f))))
+      (set-current-point! (buffer-start buffer)))))
+
+(define-command toggle-news-article-context
+  "Show context window into News group buffer, or hide it if currently shown.
+This is a small window showing a few lines around the subject line of the
+current article.  The number of lines is specified by the variable
+news-article-context-lines, but a prefix argument overrides this."
+  "P"
+  (lambda (argument)
+    (let ((article-window (current-window))
+         (context-lines
+          (if argument
+              (min 1 (command-argument-value argument))
+              (ref-variable news-article-context-lines))))
+      (let ((article-buffer (window-buffer article-window)))
+       (let ((group-buffer (buffer-tree:parent article-buffer #t)))
+         (let ((context-window
+                (news-group-buffer:context-window group-buffer #f)))
+           (let ((set-height
+                  (lambda ()
+                    (let ((delta
+                           (- context-lines (window-y-size context-window))))
+                      (if (not (= delta 0))
+                          (window-grow-vertically! context-window delta)))
+                    (center-news-article-context context-window))))
+             (cond ((not context-window)
+                    (show-news-article-context article-window context-lines))
+                   ((not (eq? group-buffer (window-buffer context-window)))
+                    (select-buffer-in-window group-buffer context-window #f)
+                    (set-height))
+                   (argument
+                    (set-height))
+                   (else
+                    (window-delete! context-window article-window)
+                    (buffer-remove! group-buffer 'CONTEXT-WINDOW))))))))))
+
+(define-variable news-article-context-lines
+  "The number of lines to show in a News group context window."
+  5
+  (lambda (object) (and (exact-integer? object) (> object 0))))
+\f
+(define-command output-news-article-to-rmail-file
+  "Append the current article to an Rmail file named FILE-NAME.
+If the file does not exist, ask if it should be created.
+If file is being visited, the article is appended to the
+buffer visiting that file."
+  (lambda ()
+    (list (prompt-for-rmail-output-filename
+          "Output article to Rmail file"
+          (ref-variable rmail-last-rmail-file))))
+  (lambda (pathname)
+    (set-variable! rmail-last-rmail-file (->namestring pathname))
+    (let ((buffer (get-article-output-buffer (current-buffer))))
+      (rfc822-region->babyl (buffer-region buffer))
+      (rmail-output-to-rmail-file (buffer-region buffer) pathname)
+      (kill-buffer buffer))))
+
+(define-command output-news-article
+  "Append this article to Unix mail file named FILE-NAME."
+  (lambda ()
+    (list (prompt-for-rmail-output-filename "Output article to Unix mail file"
+                                           (ref-variable rmail-last-file))))
+  (lambda (pathname)
+    (set-variable! rmail-last-file (->namestring pathname))
+    (let ((buffer (get-article-output-buffer (current-buffer))))
+      (rmail-output-to-unix-mail-file (buffer-region buffer) pathname)
+      (kill-buffer buffer))))
+
+(define (get-article-output-buffer buffer)
+  (let ((buffer* (temporary-buffer " news conversion")))
+    (insert-region (buffer-absolute-start buffer)
+                  (buffer-absolute-end buffer)
+                  (buffer-start buffer*))
+    (delete-news-header buffer*)
+    (insert-news-header (buffer-news-header buffer) buffer* #f)
+    buffer*))
+
+(define-command reply-to-news-article
+  "Mail a reply to the author of the current news article.
+While composing the reply, use \\[mail-yank-original] to yank the
+original message into it."
+  ()
+  (lambda ()
+    (let ((reply-buffer (current-buffer)))
+      (make-mail-buffer
+       (let ((buffer (temporary-buffer " news conversion")))
+        (insert-news-header (buffer-news-header reply-buffer) buffer #f)
+        (let ((headers
+               (rfc822-region-reply-headers (buffer-region buffer) #t)))
+          (kill-buffer buffer)
+          headers))
+       reply-buffer
+       select-buffer-other-window))))
+
+(define (buffer-news-header buffer)
+  (let ((header (buffer-get buffer 'NEWS-HEADER #f)))
+    (if (not header)
+       (error "Not in a news-article buffer."))
+    header))
+\f
+;;;; INI File
+
+(define (get-ini-file-groups connection)
+  (let ((buffer (ini-file-buffer))
+       (server (nntp-connection:server connection)))
+    (let ((mark (ini-file-buffer:server-groups-entry buffer server)))
+      (if mark
+         (map (lambda (entry)
+                (make-news-group-1 connection
+                                   (car entry)
+                                   (cadr entry)
+                                   (cddr entry)))
+              (ini-file-buffer:read-server-groups-entry mark server))
+         '()))))
+
+(define (put-ini-file-groups connection groups)
+  (let ((buffer (ini-file-buffer))
+       (server (nntp-connection:server connection)))
+    (let ((mark (ini-file-buffer:server-groups-entry buffer server))
+         (insert-groups
+          (lambda (mark)
+            (call-with-output-mark mark
+              (lambda (port)
+                (write-string "(server-groups " port)
+                (write server port)
+                (for-each (lambda (group)
+                            (write-string "\n\t" port)
+                            (write (cons* (news-group:name group)
+                                          (news-group:subscribed? group)
+                                          (news-group:ranges-seen group))
+                                   port))
+                          groups)
+                (write-string "\n\t)" port))))))
+      (cond (mark
+            (let ((mark (mark-right-inserting-copy mark)))
+              (set-buffer-major-mode! buffer (ref-mode-object scheme))
+              (delete-string mark (forward-sexp mark 1 'ERROR))
+              (if (not (null? groups))
+                  (insert-groups mark))
+              (mark-temporary! mark))
+            (save-buffer buffer #f))
+           ((not (null? groups))
+            (let ((mark (mark-left-inserting-copy (buffer-end buffer))))
+              (if (mark= (buffer-start buffer) mark)
+                  (insert-string ";;; -*-Scheme-*- News Reader INI file"
+                                 mark))
+              (guarantee-newlines 2 mark)
+              (insert-groups mark)
+              (mark-temporary! mark))
+            (save-buffer buffer #f))))))
+\f
+(define (ini-file-buffer)
+  (find-file-noselect (merge-pathnames "snr.ini" (current-home-directory)) #t))
+
+(define (ini-file-buffer:server-groups-entry buffer server)
+  (let ((end (buffer-end buffer)))
+    (let loop ((start (buffer-start buffer)))
+      (and (re-search-forward "^(server-groups[ \t]+\"\\([^\"]+\\)\""
+                             start end #t)
+          (if (string-ci=? (extract-string (re-match-start 1)
+                                           (re-match-end 1))
+                           server)
+              (re-match-start 0)
+              (loop (re-match-end 0)))))))
+
+(define (ini-file-buffer:read-server-groups-entry mark server)
+  (bind-condition-handler (list condition-type:error)
+      (lambda (condition)
+       condition
+       (editor-error "Entry for "
+                     server
+                     " in "
+                     (->namestring (buffer-pathname (mark-buffer mark)))
+                     " is damaged."))
+    (lambda ()
+      (let ((entry (with-input-from-mark mark read)))
+       (if (not (ini-file-buffer:valid-server-groups-entry? entry))
+           (error "Invalid server entry:" entry))
+       (cddr entry)))))
+
+(define (ini-file-buffer:valid-server-groups-entry? entry)
+  (and (list? entry)
+       (>= (length entry) 2)
+       (eq? 'SERVER-GROUPS (car entry))
+       (string? (cadr entry))
+       (for-all? (cddr entry)
+        (lambda (entry)
+          (and (list? entry)
+               (>= (length entry) 2)
+               (string? (car entry))
+               (boolean? (cadr entry))
+               (for-all? (cddr entry) range?))))))
+\f
+;;;; .newsrc File
+
+(define (get-newsrc-file-groups connection)
+  (parse-newsrc-buffer connection (newsrc-file-buffer connection)))
+
+(define (parse-newsrc-buffer connection buffer)
+  (let loop ((start (buffer-start buffer)) (groups '()))
+    (let ((end (line-end start 0)))
+      (let ((groups
+            (let ((mark (re-match-forward "^[^:! \t\n]+[:!]" start end)))
+              (if mark
+                  (cons (make-news-group-1
+                         connection
+                         (extract-string start (mark-1+ mark))
+                         (char=? #\: (extract-left-char mark))
+                         (parse-newsrc-group-ranges mark end))
+                        groups)
+                  groups))))
+       (if (group-end? end)
+           (reverse! groups)
+           (loop (mark1+ end) groups))))))
+
+(define (parse-newsrc-group-ranges mark end)
+  (let loop ((mark mark) (ranges '()))
+    (if (re-match-forward "[, \t]*\\([0-9-]+\\)" mark end)
+       (let ((s (re-match-start 1))
+             (e (re-match-end 1)))
+         (loop e
+               (cond ((re-match-forward "[0-9]+" s e)
+                      (cons (let ((n (extract-nonnegative-integer s e)))
+                              (make-range n n))
+                            ranges))
+                     ((re-match-forward "\\([0-9]+\\)-\\([0-9]+\\)" s e)
+                      (let ((n
+                             (extract-nonnegative-integer (re-match-start 1)
+                                                          (re-match-end 1)))
+                            (m
+                             (extract-nonnegative-integer (re-match-start 2)
+                                                          (re-match-end 2))))
+                        (if (< n m)
+                            (cons (make-range n m) ranges)
+                            ranges)))
+                     (else
+                      ranges))))
+       (reverse! ranges))))
+
+(define (extract-nonnegative-integer start end)
+  (let loop ((mark start) (n 0))
+    (if (mark= mark end)
+       n
+       (loop (mark1+ mark)
+             (+ (* n 10)
+                (fix:- (char->integer (extract-right-char mark))
+                       (char->integer #\0)))))))
+\f
+(define (update-newsrc-group group)
+  (let ((buffer (newsrc-file-buffer (news-group:connection group))))
+    (let ((mark
+          (re-search-forward
+           (string-append "^"
+                          (re-quote-string (news-group:name group))
+                          "[:!]")
+           (buffer-start buffer)))
+         (finish
+          (lambda (mark)
+            (insert-char (if (news-group:subscribed? group) #\: #\!) mark)
+            (insert-char #\space mark)
+            (for-each
+             (lambda (range)
+               (let ((f (range-first range))
+                     (l (range-last range)))
+                 (if (= f l)
+                     (insert-string (number->string f) mark)
+                     (begin
+                       (insert-string (number->string f) mark)
+                       (insert-char #\- mark)
+                       (insert-string (number->string l) mark)))))
+             (news-group:ranges-seen group))
+            (mark-temporary! mark))))
+      (if mark
+         (let ((mark (mark-left-inserting-copy (mark-1+ mark))))
+           (delete-string mark (line-end mark 0))
+           (finish mark))
+         (let ((mark (mark-left-inserting-copy (buffer-end buffer))))
+           (guarantee-newline mark)
+           (insert-string (news-group:name group) mark)
+           (finish mark))))))
+
+(define (newsrc-file-buffer connection)
+  (find-file-noselect (os/newsrc-file-name (nntp-connection:server connection))
+                     #f))
+
+(define (os/newsrc-file-name server)
+  (let ((homedir (user-homedir-pathname)))
+    (if (os2/fs-long-filenames? homedir)
+       (let ((specific
+              (merge-pathnames (string-append ".newsrc-" server)
+                               homedir)))
+         (if (file-exists? specific)
+             specific
+             (merge-pathnames ".newsrc" homedir)))
+       (merge-pathnames "newsrc.ini" homedir))))
+\f
+;;;; Miscellaneous
+
+(define (repeating-command argument procedure key adjective)
+  (let ((procedure
+        (lambda (group)
+          (procedure group)
+          (set-current-point! (line-start (current-point) 1 'ERROR)))))
+    (if argument
+       (for-each procedure
+                 (next-n-property-items (command-argument-value argument)
+                                        key))
+       (procedure (current-property-item key adjective)))))
+
+(define (current-property-item key adjective)
+  (let ((item (region-get (current-point) key #f)))
+    (if (not item)
+       (editor-error "Point isn't on a"
+                     (if (memv (string-ref adjective 0)
+                               '(#\a #\e #\i #\o #\u))
+                         "n"
+                         "")
+                     " "
+                     adjective
+                     " line."))
+    item))
+
+(define (next-n-property-items n key)
+  (let loop ((start (line-start (current-point) 0)) (n n))
+    (if (<= n 0)
+       '()
+       (cons (let ((item (region-get start key #f)))
+               (if (not item)
+                   (error "Missing property item:" key))
+               item)
+             (let ((next (line-start start 1 #f)))
+               (if next
+                   (loop next (- n 1))
+                   '()))))))
+
+(define (property-item mark key adjective)
+  (let ((item (region-get mark key #f)))
+    (if (not item)
+       (editor-error "Missing " adjective " property."))
+    item))
+
+(define (find-buffer-line buffer item key <)
+  (let ((group (mark-group (buffer-absolute-start buffer))))
+    (let loop
+       ((low (mark-index (buffer-absolute-start buffer)))
+        (high (mark-index (buffer-absolute-end buffer))))
+      (and (fix:< low high)
+          (let ((index (fix:quotient (fix:+ low high) 2)))
+            (let ((item* (get-text-property group index key #f)))
+              (if (not item*)
+                  (error "Missing text property:" key (buffer-name buffer)))
+              (cond ((eq? item item*)
+                     (line-start (make-mark group index) 0))
+                    ((< item item*)
+                     (loop low index))
+                    (else
+                     (loop (fix:+ index 1) high)))))))))
+
+(define (get-buffer-property buffer key)
+  (let ((item (buffer-get buffer key #f)))
+    (if (not item)
+       (error "Missing buffer property:" key (buffer-name buffer)))
+    item))
+\f
+;;;; Buffer Trees
+
+(define (buffer-tree:parent buffer error?)
+  (or (let ((node (buffer-tree:node buffer #f)))
+       (and node
+            (car node)))
+      (and error?
+          (error "Missing parent buffer:" (buffer-name buffer)))))
+
+(define (buffer-tree:child buffer key error?)
+  (or (let ((node (buffer-tree:node buffer #f)))
+       (and node
+            (let ((entry (assq key (cdr node))))
+              (and entry
+                   (cdr entry)))))
+      (and error?
+          (error "Missing child buffer:" key (buffer-name buffer)))))
+
+(define (buffer-tree:children buffer)
+  (let ((node (buffer-tree:node buffer #f)))
+    (if node
+       (map cdr (cdr node))
+       '())))
+
+(define (buffer-tree:attach-child! parent key child)
+  (let ((node (buffer-tree:node parent #t)))
+    (let ((entry (assq key (cdr node))))
+      (if entry
+         (set-cdr! entry child)
+         (set-cdr! node (cons (cons key child) (cdr node))))))
+  (set-car! (buffer-tree:node child #t) parent))
+
+(define (buffer-tree:node buffer intern?)
+  (or (buffer-get buffer 'BUFFER-TREE #f)
+      (and intern?
+          (let ((node (cons #f '())))
+            (buffer-put! buffer 'BUFFER-TREE node)
+            (add-kill-buffer-hook buffer buffer-tree:kill)
+            node))))
+
+(define (buffer-tree:kill buffer)
+  (let ((node (buffer-tree:node buffer #f)))
+    (if node
+       (begin
+         (let ((parent (car node)))
+           (if parent
+               (let ((node (buffer-tree:node parent #f)))
+                 (and node
+                      (set-cdr! node
+                                ((list-deletor!
+                                  (lambda (entry)
+                                    (eq? buffer (cdr entry))))
+                                 (cdr node)))))))
+         (for-each (lambda (child)
+                     (let ((node (buffer-tree:node child #f)))
+                       (if node
+                           (set-car! node #f))))
+                   (map cdr (cdr node)))))))
+\f
+;;;; News-Group Extensions
+
+(define-structure (news-group-extra
+                  (conc-name news-group-extra:)
+                  (constructor make-news-group-extra ()))
+  (subscribed? #f)
+  (ranges-seen '()))
+
+(define (get-news-group-extra group)
+  (or (news-group:reader-hook group)
+      (let ((extra (make-news-group-extra)))
+       (set-news-group:reader-hook! group extra)
+       extra)))
+
+(define-integrable (news-group:subscribed? group)
+  (news-group-extra:subscribed? (get-news-group-extra group)))
+
+(define-integrable (set-news-group:subscribed?! group value)
+  (set-news-group-extra:subscribed?! (get-news-group-extra group) value))
+
+(define-integrable (news-group:ranges-seen group)
+  (news-group-extra:ranges-seen (get-news-group-extra group)))
+
+(define-integrable (set-news-group:ranges-seen! group value)
+  (set-news-group-extra:ranges-seen! (get-news-group-extra group) value))
+
+(define (make-news-group-1 connection name subscribed? ranges-seen)
+  (let ((group (make-news-group connection name)))
+    (set-news-group:subscribed?! group subscribed?)
+    (set-news-group:ranges-seen! group (canonicalize-ranges ranges-seen))
+    group))
+
+(define (news-group:update-ranges! group)
+  (news-group:update-probe! group)
+  (if (news-group:active? group)
+      (set-news-group:ranges-seen!
+       group
+       (clip-ranges! (news-group:ranges-seen group)
+                    (news-group:first-article group)
+                    (news-group:last-article group)))))
+
+(define (news-group:number-of-articles group)
+  (let ((n-seen (count-ranges (news-group:ranges-seen group))))
+    (if (= n-seen 0)
+       (news-group:estimated-n-articles group)
+       (- (- (+ (news-group:last-article group) 1)
+             (news-group:first-article group))
+          n-seen))))
+
+(define (news-group:article-seen! group header)
+  (let ((do-it
+        (lambda (group number)
+          (set-news-group:ranges-seen!
+           group
+           (add-to-ranges! (news-group:ranges-seen group) number)))))
+    (do-it group (news-header:number header))
+    (for-each (let ((connection (news-group:connection group)))
+               (lambda (xref)
+                 (let ((group (find-news-group connection (car xref))))
+                   (if (and group (news-group:subscribed? group))
+                       (do-it group (token->number (cdr xref)))))))
+             (news-header:xref header))))
+
+(define (news-group:article-unseen! group header)
+  (set-news-group:ranges-seen!
+   group
+   (remove-from-ranges! (news-group:ranges-seen group)
+                       (news-header:number header))))
+\f
+;;;; Article Ranges
+
+(define (range? object)
+  (or (article-number? object)
+      (and (pair? object)
+          (article-number? (car object))
+          (article-number? (cdr object))
+          (<= (car object) (cdr object)))))
+
+(define (article-number? object)
+  (and (exact-integer? object)
+       (> object 0)))
+
+(define (make-range f l) (if (= f l) f (cons f l)))
+(define (range-first r)  (if (pair? r) (car r) r))
+(define (range-last r)   (if (pair? r) (cdr r) r))
+(define (range-length r) (if (pair? r) (+ (- (cdr r) (car r)) 1) 1))
+
+(define (count-ranges ranges)
+  (let loop ((ranges ranges) (count 0))
+    (if (null? ranges)
+       count
+       (loop (cdr ranges) (+ count (range-length (car ranges)))))))
+
+(define (canonicalize-ranges ranges)
+  (if (null? ranges)
+      ranges
+      (let ((ranges
+            (sort ranges (lambda (x y) (< (range-first x) (range-first y))))))
+       (let loop ((ranges ranges))
+         (if (not (null? (cdr ranges)))
+             (let ((x (car ranges))
+                   (y (cadr ranges)))
+               (if (<= (range-first y) (+ (range-last x) 1))
+                   (begin
+                     (set-car! ranges
+                               (make-range (range-first x)
+                                           (max (range-last x)
+                                                (range-last y))))
+                     (set-cdr! ranges (cddr ranges))
+                     (loop ranges))
+                   (loop (cdr ranges))))))
+       ranges)))
+
+(define (clip-ranges! ranges first last)
+  (let ((holder
+        (cons 'HOLDER
+              (let clip-first ((ranges ranges))
+                (cond ((or (null? ranges)
+                           (<= first (range-first (car ranges))))
+                       ranges)
+                      ((< (range-last (car ranges)) first)
+                       (clip-first (cdr ranges)))
+                      (else
+                       (set-car! ranges
+                                 (make-range first (range-last (car ranges))))
+                       ranges))))))
+    (let clip-last ((ranges (cdr holder)) (prev holder))
+      (cond ((null? ranges)
+            unspecific)
+           ((< (range-last (car ranges)) last)
+            (clip-last (cdr ranges) ranges))
+           ((> (range-first (car ranges)) last)
+            (set-cdr! prev '()))
+           (else
+            (if (> (range-last (car ranges)) last)
+                (set-car! ranges
+                          (make-range (range-first (car ranges))
+                                      last)))
+            (set-cdr! ranges '()))))
+    (cdr holder)))
+\f
+(define (complement-ranges ranges first last)
+  (if (null? ranges)
+      (list (make-range first last))
+      (let loop
+         ((e (range-last (car ranges)))
+          (ranges (cdr ranges))
+          (result
+           (let ((s (range-first (car ranges))))
+             (if (< first s)
+                 (list (make-range first (- s 1)))
+                 '()))))
+       (if (null? ranges)
+           (reverse! (if (< e last)
+                         (cons (make-range (+ e 1) last) result)
+                         result))
+           (loop (range-last (car ranges))
+                 (cdr ranges)
+                 (cons (make-range (+ e 1) (- (range-first (car ranges)) 1))
+                       result))))))
+
+(define (add-to-ranges! ranges number)
+  (let ((holder (cons 'HOLDER ranges)))
+    (let loop ((ranges ranges) (prev holder))
+      (if (null? ranges)
+         (set-cdr! prev (list (make-range number number)))
+         (let ((f (range-first (car ranges)))
+               (l (range-last (car ranges))))
+           (cond ((> number (+ l 1))
+                  (loop (cdr ranges) ranges))
+                 ((< number (- f 1))
+                  (set-cdr! prev (cons (make-range number number) ranges)))
+                 (else
+                  (let ((f (min f number))
+                        (l (max l number)))
+                    (if (and (not (null? (cdr ranges)))
+                             (= (+ l 1) (range-first (cadr ranges))))
+                        (begin
+                          (set-car! ranges
+                                    (make-range f (range-last (cadr ranges))))
+                          (set-cdr! ranges (cddr ranges)))
+                        (set-car! ranges (make-range f l)))))))))
+    (cdr holder)))
+\f
+(define (remove-from-ranges! ranges number)
+  (let ((holder (cons 'HOLDER ranges)))
+    (let loop ((ranges ranges) (prev holder))
+      (if (not (null? ranges))
+         (let ((f (range-first (car ranges)))
+               (l (range-last (car ranges))))
+           (cond ((> number l)
+                  (loop (cdr ranges) ranges))
+                 ((>= number f)
+                  (if (= number f)
+                      (if (= number l)
+                          (set-cdr! prev (cdr ranges))
+                          (set-car! ranges (make-range (+ f 1) l)))
+                      (if (= number l)
+                          (set-car! ranges (make-range f (- l 1)))
+                          (begin
+                            (set-car! ranges (make-range (+ number 1) l))
+                            (set-cdr! prev
+                                      (cons (make-range f (- number 1))
+                                            ranges))))))))))
+    (cdr holder)))
+
+(define (member-of-ranges? ranges number)
+  (let loop ((ranges ranges))
+    (and (not (null? ranges))
+        (or (<= (range-first (car ranges)) number (range-last (car ranges)))
+            (loop (cdr ranges))))))
+
+(define (ranges->list ranges)
+  (let loop ((ranges ranges) (result '()))
+    (if (null? ranges)
+       (reverse! result)
+       (loop (cdr ranges)
+             (let ((e (range-last (car ranges))))
+               (let loop ((n (range-first (car ranges))) (result result))
+                 (let ((result (cons n result)))
+                   (if (= n e)
+                       result
+                       (loop (+ n 1) result)))))))))
+
+(define (for-each-range-element procedure ranges)
+  (for-each (lambda (range)
+             (let ((e (+ (range-last range) 1)))
+               (do ((n (range-first range) (+ n 1)))
+                   ((= n e) unspecific)
+                 (procedure n))))
+           ranges))
+\f
+;;;; News-Header Extensions
+
+(define-structure (news-header-extra
+                  (conc-name news-header-extra:)
+                  (constructor make-news-header-extra ()))
+  (status #\space)
+  (line-number '()))
+
+(define (get-news-header-extra header)
+  (or (news-header:reader-hook header)
+      (let ((extra (make-news-header-extra)))
+       (set-news-header:reader-hook! header extra)
+       extra)))
+
+(define news-header-extra-table
+  (make-eq-hash-table))
+
+(define-integrable (news-header:status header)
+  (news-header-extra:status (get-news-header-extra header)))
+
+(define-integrable (set-news-header:status! header value)
+  (set-news-header-extra:status! (get-news-header-extra header) value))
+
+(define-integrable (news-header:line-number header)
+  (news-header-extra:line-number (get-news-header-extra header)))
+
+(define-integrable (set-news-header:line-number! header value)
+  (set-news-header-extra:line-number! (get-news-header-extra header) value))
+
+(define (news-header:article-seen! header)
+  (set-news-header:status! header #\D)
+  (news-group:article-seen! (news-header:group header) header))
+
+(define (news-header:article-unseen! header)
+  (set-news-header:status! header #\space)
+  (news-group:article-unseen! (news-header:group header) header))
+
+(define (news-header:article-unseen? header)
+  (char=? #\space (news-header:status header)))
\ No newline at end of file