changeset 0:04124a4423d4 noffle

[svn] Initial revision
author enz
date Tue, 04 Jan 2000 11:35:42 +0000
parents
children 912123d43a87
files AUTHORS.html CHANGELOG.html COPYING.html FAQ.html INSTALL.html LSM.TXT Makefile NOTES.html README.html TODO.TXT client.c client.h common.h config.c config.h content.c content.h database.c database.h dynamicstring.c dynamicstring.h fetch.c fetch.h fetchlist.c fetchlist.h group.c group.h lock.c lock.h log.c log.h make-check make-distribution noffle.1 noffle.c noffle.conf.5 noffle.conf.example online.c online.h outgoing.c outgoing.h over.c over.h protocol.c protocol.h pseudo.c pseudo.h request.c request.h server.c server.h util.c util.h
diffstat 53 files changed, 9101 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AUTHORS.html	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,23 @@
+<html>
+
+<head>
+<title>NOFFLE Authors</title>
+</head>
+
+<body bgcolor=white>
+
+<center>
+<h1>NOFFLE Authors</h1>
+</center>
+
+<hr>
+
+<ul>
+<li>
+Markus Enzenberger
+<li>
+Volker Wysk
+</ul>
+
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CHANGELOG.html	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,164 @@
+<html>
+
+<head>
+<title>NOFFLE Changelog</title>
+</head>
+
+<body bgcolor=white>
+
+<center>
+<h1>NOFFLE Changelog</h1>
+</center>
+
+<hr>
+
+<h2>Since 1.0pre4</h2>
+
+<ul>
+<li>
+Core files are always enabled when running as server and debugging
+symbols are always in the executable.
+<li>
+Use GDBM_FAST flag for hash files.
+<li>
+Minor changes and improvements
+</ul>
+
+<h2>Version 1.0pre4</h2>
+
+<ul>
+<li>
+Fixed a bug that broke cross-posting of articles
+<li>
+Long overview header lines are now split into multiple lines in response
+to HEAD or ARTICLE commands.
+<li>
+Fixed a bug that caused a crash sometimes when updating the requested
+article list after releasing/regetting the global lock
+<li>
+Server is now allowed to generate core files on crash (in spool directory)
+if compiled with -DDEBUG option
+<li>
+Opening an article additionally marks all references as interesting,
+so more articles are fetched in thread mode, if one article of a thread
+was opened.
+<li>
+New config option "connect-timeout"
+<li>
+Minor improvements and bug-fixes
+</ul>
+
+<h2>Version 1.0pre3</h2>
+
+<ul>
+<li>
+Added XPAT command. Not full syntax, but enough for making slrn's thread
+reconstruction work 
+<li>
+Storing of requested message-ids completely rewritten (thanks to
+Volker Wysk for the patch). Much more efficient now. Bug removed
+that broke requesting articles with message-IDs containing a slash.
+Added --requested option.
+<li>
+When fetching requested articles, do not send more than 20 ARTICLE commands
+at once, before parsing the server response.
+<li>
+Minor bug fixes and improvements.
+</ul>
+
+<h2>Version 1.0pre2</h2>
+
+<ul>
+<li>
+Added RPM_BUILD_ROOT variable to Makefile (useful for creating RPM source
+packages)
+<li>
+Removed terrible bug that truncated article body after releasing and re-getting
+global lock
+</ul>
+
+<h2>Version 1.0pre1</h2>
+
+<ul>
+<li>
+<em>needs complete re-installing, some formats have changed</em>
+<li>
+Support for multiple remote servers
+<li>
+Faster download when fetching news, because articles are prepared
+in database while parsing response to XOVER and all ARTICLE commands
+are sent at once
+<li>
+Bug removed that made authetication only work with lower-case passwords
+<li>
+Other small bug fixes and improvements
+</ul>
+
+<h2>Version 0.19</h2>
+
+<ul>
+<li>
+Fix broken full mode
+<li>
+Fix cutting of articles after line beginning with '.'
+<li>
+Other bug fixes
+<li>
+LIST commands can have pattern argument now
+<li>
+initial-fetch option removed (same as max-fetch now)
+</ul>
+
+<h2>Version 0.18</h2>
+
+<ul>
+<li>
+<em>needs complete re-installing, most file format have changed</em>
+<li>
+Group database uses gdbm, databases moved to /var/spool/noffle/data
+<li>
+Most config options changed their names, some do not longer exists
+<li>
+New fetch mode "thread" added
+<li>
+Different --fetch invocations replaced by single option
+<li>
+Meaning of "--database" option changed, "--article" option added
+<li>
+Failed postings are now returned to sender by "mail" command
+<li>
+Expire uses last access time
+<li>
+Auto-subscribe option only subscribes groups now, if an article
+body is opened (no longer if group is selected).
+<li>
+Improve posting at German T-Online provider: rename X-Sender header,
+Reply-To header is added, if missing (T-Online overwrites From headers),
+allow to remove Message-ID as a config option.
+<li>
+Doc files are now copied to $(PREFIX)/doc/noffle
+<li>
+Y2K compliance of NEWGROUPS command
+<li>
+Various bug fixes (thanks to all users helping with bug reports)
+<li>
+Various changes for tuning and improvement
+</ul>
+
+<h2>Version 0.17</h2>
+
+<ul>
+<li>
+Bug removed that caused NOFFLE to exceed the allowed maximum number
+of open files on longer sessions.
+</ul>
+
+<h2>Version 0.16</h2>
+
+<ul>
+<li>
+Noffle generates Message-ID if a message received for posting has none.
+</ul>
+
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/COPYING.html	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,29 @@
+<html>
+
+<head>
+<title>NOFFLE Copying</title>
+</head>
+
+<body bgcolor=white>
+<p>
+
+<center>
+<h1>NOFFLE Copying</h1>
+</center>
+
+<p>
+<hr>
+<p>
+
+
+This program is available under the GNU General Public License.
+<p>
+The full terms and conditions for copying, distribution and modification
+can be found at:
+<blockquote>
+<a href="http://www.fsf.org/copyleft/gpl.html">http://www.fsf.org/copyleft/gpl.html</a>
+</blockquote>
+
+
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/FAQ.html	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,59 @@
+<html>
+
+<head>
+<title>NOFFLE FAQ</title>
+</head>
+
+<body bgcolor=white>
+<p>
+
+<center>
+<h1>NOFFLE FAQ</h1>
+</center>
+
+<p>
+<hr>
+<p>
+
+<b>Q:</b> What is the difference between NOFFLE and leafnode?
+<p>
+<b>A:</b> NOFFLE uses less resources (disk space and bandwidth). Downloading
+groups in overview mode is several times faster, because it uses the XOVER
+command instead of HEAD. In addition, there is the quasi-transparent mode,
+when online, which allows to browse through groups and cache everything
+without subscribing.
+
+<p>
+<hr>
+<p>
+
+<b>Q:</b> I subscribe to groups, but get a "Retreiving failed"
+message for every requested article.
+<p>
+<b>A:</b> Some news server do not allow retrieving articles by message-ID.
+You cannot use NOFFLE together with these servers presently.
+
+<p>
+<hr>
+<p>
+
+<b>Q:</b> I changed the server in the config files, but the
+new groups do not appear.
+<p>
+<b>A:</b> You should run <code>noffle --query groups</code> again. If you
+want all old group information deleted, you should remove the file
+data/groupinfo.gdbm in the spool directory before.
+
+<p>
+<hr>
+<p>
+
+<b>Q:</b> The Emacs news reader GNUS hangs while getting active list
+from server.
+<p>
+<b>A:</b> This is a known phenomena and I believe that it is a bug with
+GNUS, because the log files show correct handling of client commands by
+noffle.
+                                                                                
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/INSTALL.html	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,156 @@
+<html>
+
+<head>
+<title>NOFFLE Installation</title>
+</head>
+
+<body bgcolor=white>
+<p>
+
+<center>
+<h1>NOFFLE Installation</h1>
+</center>
+
+<p>
+<hr>
+<p>
+
+For compiling NOFFLE there are the following requirements:
+<p>
+
+<ul>
+
+<li>
+The gdbm library must be installed on your system (standard with
+most distributions).
+<p>
+Please use the same compiler for compiling NOFFLE that was
+used for compiling the gdbm library!
+<p>
+The reason for this warning is that there is an incompatibility between
+egcs and gcc that causes programs to crash on some distributions,
+depending on the optimisation level.
+<p>
+
+<li>
+The program "mail" must be available, because failed postings are
+returned to the sender by calling it (with option -s and by piping
+message text into it).
+<p>
+
+<li>
+The program "sort" must be available (standard with most distributions).
+<p>
+
+</ul>
+
+<p>
+For installing NOFFLE on your system, the following steps are necessary:
+<p>
+
+<ul>
+
+<li>
+Edit the Makefile. Change SPOOLDIR and PREFIX, if you do not
+like the defaults.
+<p>
+
+<li>
+Type 'make'.
+<p>
+
+<li>
+Log in as root and type 'make install'.
+<p>
+
+<li>
+Copy '&lt;PREFIX&gt;/doc/noffle/noffle.conf.example' to '/etc/noffle.conf' and
+edit it. Write in the name of the remote news server.
+<br>
+Change the owner to 'news':
+<pre>
+         chown news.news /etc/noffle.conf
+</pre>
+Make it unreadable by others, if it contains a username and a password:
+<pre>
+         chmod o-r /etc/noffle.conf
+</pre>
+Now you can leave the root account.
+<p>
+
+<li>
+Go online and run
+<pre>
+         noffle --query groups # required
+         noffle --query desc   # optional group descriptions
+</pre>
+<p>
+to retrieve newsgroup information.
+<br>
+This may take a while depending on the number of active newsgroups
+at the remote news server.
+<p>   
+Subscribe to some groups by running
+<pre>
+         noffle --subscribe-over <groupname>
+</pre>
+or
+<pre>
+         noffle --subscribe-thread <groupname>
+</pre>
+or
+<pre>
+         noffle --subscribe-full <groupname>
+</pre>
+Then run
+<pre>
+         noffle --fetch
+</pre>
+for testing the retrieving of overviews/articles of the groups subscribed.
+<p>
+
+<li>
+Add a line for 'noffle' to '/etc/inetd.conf':
+<pre>
+         nntp stream tcp nowait news /usr/sbin/tcpd /usr/local/bin/noffle -r
+</pre>
+(Change the path of noffle if necessary)
+<p>
+
+<li>
+Add the following lines to your 'ip-up' script:
+<pre>
+         /usr/local/bin/noffle --fetch
+         /usr/local/bin/noffle --online
+</pre>
+<p>
+Add the following line to your 'ip-down' script:
+<pre>
+         /usr/local/bin/noffle --offline
+</pre>
+Add a line for running noffle to the crontab of news (by running
+'crontab -u news -e' as root):
+<pre>
+         0 19 * * 1 /usr/local/bin/noffle --expire 14
+</pre>
+(if you want to run 'noffle' on Monday (1st day of week) at
+19.00 and delete all articles not accessed within the last 14 days).
+<p>
+
+</ul>
+
+Now you are ready, configure the client readers to use "localhost" port 119
+as news server and/or set the environment variable NNTPSERVER to
+"localhost" and/or create the file /etc/nntpserver containing "localhost".
+<p>
+If something goes wrong, have a look at '/var/log/news' for error and
+logging messages.
+<p>
+It can be helpful to recompile NOFFLE with the
+-DDEBUG option to increase the level of logged details. Additionally,
+the -DDEBUG option will create a core file in the spool directory if NOFFLE
+should crash. This will allow those of you familiar with a debugger to send
+me a detailed bug report :-)
+
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LSM.TXT	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,16 @@
+Begin3
+Title:          NOFFLE - news server 
+Version:	
+Entered-date:   28AUG98
+Description:    NOFFLE is a news server optimized for low speed dialup
+		connections to the Internet and few users.
+		It allows reading news offline with many news reader
+                programs, even if they do not support offline reading
+                by themselves.
+Keywords:       news server, news reader, offline, modem, dialup
+Author:		Markus Enzenberger <markus.enzenberger@t-online.de>
+Maintained-by:  Markus Enzenberger <markus.enzenberger@t-online.de>
+Primary-site:   http://home.t-online.de/home/markus.enzenberger/noffle.html
+Platforms:      UNIX, Linux
+Copying-policy: GPL
+End
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,81 @@
+###############################################################################
+#
+# Makefile for Noffle news server
+#
+# $Id: Makefile 3 2000-01-04 11:35:42Z enz $
+#
+###############################################################################
+
+SPOOLDIR = /var/spool/noffle
+PREFIX = /usr/local
+
+CONFIGFILE = /etc/noffle.conf
+BINDIR = $(PREFIX)/bin
+MANDIR = $(PREFIX)/man
+DOCDIR = $(PREFIX)/doc/noffle
+
+CC = gcc
+CFLAGS = -Wall -O -g
+#CFLAGS = -Wall -g -DDEBUG
+
+VERSION = 19991217
+
+FILESH = client.h common.h config.h content.h database.h dynamicstring.h  \
+    fetch.h fetchlist.h group.h lock.h log.h online.h outgoing.h over.h \
+    protocol.h pseudo.h request.h server.h util.h
+
+FILESC = fetch.c client.c config.c content.c database.c dynamicstring.c \
+    fetchlist.c group.c lock.c log.c noffle.c online.c outgoing.c over.c \
+    protocol.c pseudo.c request.c server.c util.c
+
+OBJS = $(patsubst %.c,%.o,$(FILESC))
+
+all: noffle
+
+noffle: $(OBJS)
+	$(CC) $(CFLAGS) -o noffle $(OBJS) -lgdbm
+
+config.o: config.c config.h Makefile
+	$(CC) $(CFLAGS) -c -DSPOOLDIR=\"$(SPOOLDIR)\" \
+        -DVERSION=\"$(VERSION)\" -DCONFIGFILE=\"$(CONFIGFILE)\" config.c
+
+log.o: log.c log.h Makefile
+
+depend:
+	gcc -MM -MG -E $(FILESC) >depend
+
+clean:
+	rm -f depend $(OBJS) noffle
+
+install: noffle
+	install -o 0 -g 0 -d $(RPM_BUILD_ROOT)$(BINDIR)
+	install -m 4755 -o news -g news noffle $(RPM_BUILD_ROOT)$(BINDIR)
+	install -o 0 -g 0 -d $(RPM_BUILD_ROOT)$(MANDIR)/man1
+	install -m 0644 -o 0 -g 0 noffle.1 \
+            $(RPM_BUILD_ROOT)$(MANDIR)/man1/noffle.1
+	install -o 0 -g 0 -d $(RPM_BUILD_ROOT)$(MANDIR)/man5
+	install -m 0644 -o 0 -g 0 noffle.conf.5 \
+            $(RPM_BUILD_ROOT)$(MANDIR)/man5/noffle.conf.5
+	install -m 2755 -o news -g news -d $(RPM_BUILD_ROOT)$(SPOOLDIR)
+	install -o news -g news -d $(RPM_BUILD_ROOT)$(SPOOLDIR)/data
+	install -o news -g news -d $(RPM_BUILD_ROOT)$(SPOOLDIR)/lock
+	install -o news -g news -d $(RPM_BUILD_ROOT)$(SPOOLDIR)/requested
+	install -o news -g news -d $(RPM_BUILD_ROOT)$(SPOOLDIR)/outgoing
+	install -o news -g news -d $(RPM_BUILD_ROOT)$(SPOOLDIR)/overview
+	install -o 0 -g 0 -d $(RPM_BUILD_ROOT)$(DOCDIR)
+	install -m 0644 -o 0 -g 0 README.txt $(RPM_BUILD_ROOT)$(DOCDIR)
+	install -m 0644 -o 0 -g 0 NOTES.txt $(RPM_BUILD_ROOT)$(DOCDIR)
+	install -m 0644 -o 0 -g 0 INSTALL.txt $(RPM_BUILD_ROOT)$(DOCDIR)
+	install -m 0644 -o 0 -g 0 CHANGELOG.txt $(RPM_BUILD_ROOT)$(DOCDIR)
+	install -m 0644 -o 0 -g 0 FAQ.txt $(RPM_BUILD_ROOT)$(DOCDIR)
+	install -m 0644 -o 0 -g 0 COPYING.txt $(RPM_BUILD_ROOT)$(DOCDIR)
+	install -m 0644 -o 0 -g 0 noffle.conf.example \
+            $(RPM_BUILD_ROOT)$(DOCDIR)
+	chown -R news.news $(RPM_BUILD_ROOT)$(SPOOLDIR)
+	@echo
+	@echo Read INSTALL.txt for further instructions.
+
+tags:
+	etags $(FILESC) $(FILESH)
+
+-include depend
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/NOTES.html	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,198 @@
+<html>
+
+<head>
+<title>NOFFLE Compatibility Notes</title>
+</head>
+
+<body bgcolor=white>
+
+<center>
+<h1>NOFFLE Compatibility Notes</h1>
+</center>
+
+<p>
+<hr>
+<p>
+
+Subscribing to groups in "full" mode should work with any news reader.
+Caching of articles is unnecessary, since NOFFLE already caches them
+and should be switched off.
+<p>
+Subscribing to groups in "overview" or "thread" mode requires more
+from the news reader program:
+<p>
+<ul>
+<li>
+It must not cache articles at all (or allow to switch the cache off),
+because the article bodies change from the pseudo article
+"marked for download" to the real body.
+<li>
+The reader should rarely open article bodies automatically,
+because it will mark them unwantedly for download.
+</ul>
+
+Please send me reports on your experiences. If a reader does not work at
+all, compile NOFFLE with the -DDEBUG option in CFLAGS. Then you will
+see every NNTP command and status line in /var/log/news. Most interesting
+is the last line, before the reader (or NOFFLE) hangs :-)
+<p>
+Here comes a list with news readers that have been tested with NOFFLE,
+especially with regards to subscribing to groups in "overview" or "thread"
+mode.
+<p>
+
+<h2>kexpress 0.2.0</h2>
+
+I found no way to disable caching, apart from writing a
+wrapper script, which removes all files from the cache after
+terminating kexpress:
+<p>
+<pre>
+      #!/bin/bash
+      # kexpress wrapper, save as /usr/local/bin/kexpress
+
+      /opt/kde/bin/kexpress $@
+      rm $HOME/.kde/share/apps/kexpress/data/*
+</pre>
+<p>
+
+<h2>krn 0.4.0</h2>
+
+Set "Options/NNTP Options/Connect on Startup,Connect without asking"
+and "Options/Expire options/Article bodies/Read=0,UnRead=0"
+Sometimes the article bodies remain in the cache, the following
+wrapper script helps:
+<p>
+<pre>
+      #!/bin/bash
+      # krn wrapper, save as /usr/local/bin/krn
+
+      /opt/kde/bin/krn $@
+      rm $HOME/.kde/share/apps/krn/cache/*
+</pre>
+<p>
+Articles can be marked as read/unread without opening with the
+middle mouse button.
+This version of krn is still unstable.
+
+<h2>netscape 3.04</h2>
+
+No cache problems, netscape caches the article overviews, but not
+the bodies.
+It is best to use "Options/Show only Unread Messages" and to keep
+requested articles in unread state until their bodies
+are downloaded.
+For avoiding unwanted opening of articles one should first
+"Message/Mark Newsgroup read", then open the wanted articles
+one by one and mark them as unread again ("Message/Mark as Unread")
+immediately after opening.
+
+<h2>netscape communicator 4.0.5</h2>
+
+Same as with netscape 3.04, but automatically opens
+the first article of a listed group and
+marks it for download thereby. If this bothers you,
+choose "View/Hide message".
+This version of netscape still seems to be unstable for reading
+news.
+
+<h2>netscape communicator 4.5</h2>
+
+As with 4.0.5 "View/Show/Message" can be used to switch off
+automatic message display (and marking for download).
+
+<h2>pine 3.96, 4.05</h2>
+
+Ok.
+
+<h2>slrn 0.9.5.2</h2>
+
+Ok. You can change some keybindings, by saving the following
+script to ~/.slrn.sl and adding "interpret .slrn.sl" at the end
+of your ~/.slrnrc
+<p>
+<pre>
+      % SLRN script for better interplay with NOFFLE news server.
+      % Redefines some keys for opening articles without modifying flags.
+      define my_article_linedn()
+      {
+          variable flags = get_header_flags();
+          call ( "article_linedn" );
+          set_header_flags( flags );
+      }
+      define my_scroll_dn()
+      {
+          variable flags = get_header_flags();
+          call ( "scroll_dn" );
+          set_header_flags( flags );
+      }
+      define my_hide_article()
+      {
+          variable flags = get_header_flags();
+          call ( "hide_article" );
+          set_header_flags( flags );
+      }
+      definekey( "my_article_linedn", "\r", "article" );
+      definekey( "my_scroll_dn", " ", "article" );
+      definekey( "my_hide_article", "h", "article" );
+</pre>
+<p>
+
+<h2>tin pre</h2>
+
+Call with "tin -r" or "rtin". 'K' marks articles/thread as
+read without opening them. '-' marks them as unread.
+
+<h2>Emacs Gnus</h2>
+
+With newer versions of NOFFLE, Gnus freezes up when retrieving active
+groups. Since NOFFLE's log files in DEBUG mode show nothing unusual,
+I believe that this is a bug in Gnus. 
+<p>
+Here is a proposal for changing some key-bindings.
+<p>
+<pre>
+      ;; Customising Gnus for use with the NOFFLE news server
+      ;; 
+      ;; <return> tick and open article
+      ;;          for reading/marking for download
+      ;; <space>  scroll article text circular
+      ;;          for avoiding automatic opening of next article
+      ;; <d>      mark article as read and go to next line
+      (defun my-gnus-summary-tick-and-open(n)
+        "Tick and open article, so that NOFFLE marks it for download" 
+        (interactive "p")
+        (gnus-summary-scroll-up n)
+        (gnus-summary-mark-article nil gnus-ticked-mark t)
+        )
+      (defun my-gnus-summary-next-page(n)
+        "Next page of article, but do not open next article automatically"
+        (interactive "p")
+        (gnus-summary-next-page 10 t) ;; Call with argument `circular'.
+        )
+      (defun my-gnus-summary-mark-read-next-line(n)
+        "Mark article as read and go to next line"
+        (interactive "p")
+        (gnus-summary-mark-article-as-read gnus-read-mark)
+        (next-line n)
+        )
+      (defun my-gnus-summary-mode-hook ()
+        (define-key gnus-summary-mode-map "\r"
+          'my-gnus-summary-tick-and-open)
+        (define-key gnus-summary-mode-map " "
+          'my-gnus-summary-next-page)
+        (define-key gnus-summary-mode-map "d"
+          'my-gnus-summary-mark-read-next-line)
+        )
+      (add-hook 'gnus-summary-mode-hook 'my-gnus-summary-mode-hook)
+</pre>
+
+<p>
+<hr>
+<small><i>
+Last modified 4/99,
+<a href="mailto:markus.enzenberger@t-online.de">Markus Enzenberger</a>
+</i></small>
+
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.html	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,153 @@
+<html>
+
+<head>
+<title>NOFFLE</title>
+</head>
+
+<body bgcolor=white>
+<p>
+
+<center>
+<h1>The NOFFLE News Server</h1>
+</center>
+
+<p>
+<hr>
+<p>
+
+<h2>Features</h2>
+
+NOFFLE is a
+<a href=http://search.yahoo.com/bin/search?p=usenet>Usenet</a>
+news server optimized for few users and low speed dial-up connections
+to the Internet.
+It acts as a server to news clients running on the
+local host, but gets its news feed by acting as a client to a remote server.
+<p>
+It allows reading news offline with many news reader programs,
+even if they do not support offline reading by themselves.
+<p>
+NOFFLE is written for the
+<a href="http://www.linux.org/">Linux</a>
+operating system and freely available under GPL
+(see <a href="http://www.fsf.org/copyleft/gpl.html">
+http://www.fsf.org/copyleft/gpl.html</a>).
+<p>
+While Online:
+<ul>
+<li>
+Any newsgroup can be read, selected articles are fetched
+immediately from the remote server.
+</ul>
+<p>
+While Offline:
+<ul>
+<li>
+Allows reading news offline with many news clients,
+even if they do not support offline reading by themselves.
+<p>
+<li>
+Groups can be retrieved in different modes:
+<ul>
+<li>
+In overview mode, opened articles that have not been completely downloaded
+yet are marked for download. NOFFLE generates a pseudo article telling
+the human about this.
+<li>
+In full mode, the complete articles are fetched.
+<li>
+Thread mode is like overview mode, but automatically downloads all
+article bodies that are in the same thread as articles that already have
+been read.
+</ul>
+<p>
+<li>
+The news feed is invoked automatically next online time by calling
+NOFFLE in the ip-up script.
+<p>
+<li>
+Groups can be put on the fetch list via the 'noffle'
+command or automatically when someone tries to read them. Groups can be
+removed from the fetch list manually or automatically, when nobody accesses
+them for some time.
+</i>
+</ul>
+
+<h2>Compatibility with News Clients</h2>
+
+Subscribing to groups in full mode should work with any news reader.
+Caching of articles is unnecessary, since NOFFLE already caches them
+and should be switched off.
+<p>
+Subscribing to groups in overview mode or thread mode puts some
+requirements on the news reader program. See
+<a href="NOTES.html">NOTES.txt</a> for compatibility notes.
+<p>
+
+<h2>Getting NOFFLE</h2>
+
+NOFFLE can be downloaded from the NOFFLE homepage at
+<a href="http://home.t-online.de/home/markus.enzenberger/noffle.html">
+http://home.t-online.de/home/markus.enzenberger/noffle.html</a>:
+<blockquote>
+<tt>
+<a href="http://home.t-online.de/home/markus.enzenberger/noffle-0.19.tar.gz">
+noffle-0.19.tar.gz</a>
+</tt>
+(current release, 25 Apr 1999)
+<br>
+<tt>
+<a href=http://home.t-online.de/home/markus.enzenberger/noffle-0.17.tar.gz>
+noffle-0.17.tar.gz</a>
+</tt>
+(last release, 25 Jan 1999)
+</blockquote>
+Read
+<a href="http://home.t-online.de/home/markus.enzenberger/noffle_INSTALL.html">
+INSTALL.txt</a>
+from the package for compiling and installing NOFFLE on your system.
+<p>
+RPM packages have been created by Mario Moder
+(<a href="mailto:moderm@gmx.net">moderm@gmx.net</a>). They are available
+at 
+<blockquote>
+<a href="ftp://ftp.fbam.de/pub/linux/">ftp://ftp.fbam.de/pub/linux/</a>
+</blockquote>
+<p>
+The current version is still beta. Please send bug reports,
+comments and patches to me
+(<a href="mailto:markus.enzenberger@t-online.de">markus.enzenberger@t-online.de</a>).
+<p>
+If you want to receive announcements about NOFFLE, I will put you on
+my announcement list. Just send me a mail.
+
+<h2>Links</h2>
+
+<ul>
+<li>
+NNTP information
+<br>
+(<a href="http://www.tin.org/docs.html">http://www.tin.org/docs.html</a>)
+<li>
+Leafnode - news server similar to NOFFLE
+<br>
+(<a href="http://wpxx02.toxi.uni-wuerzburg.de/~krasel/leafnode.html">http://wpxx02.toxi.uni-wuerzburg.de/~krasel/leafnode.html</a>)
+<li>
+SLRN - powerful news reader for Windows and Unix
+<br>
+(<a href="http://space.mit.edu/~davis/slrn.html">http://space.mit.edu/~davis/slrn.html</a>)
+<li>
+GNUS - Emacs news reader
+<br>
+(<a href="http://www.gnus.org/">http://www.gnus.org/</a>
+</ul>
+
+<p>
+<hr>
+<small><i>
+Last modified 5/99,
+<a href="mailto:markus.enzenberger@t-online.de">Markus Enzenberger</a>
+</i></small>
+
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/TODO.TXT	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,79 @@
+
+=============================================================================
+Urgent
+=============================================================================
+
+Got some complains about readers sorting threads wrongly. Caused by noffle?
+
+Debug gnus.
+
+Has Client_connect resource leaks if it fails?
+
+=============================================================================
+Later
+=============================================================================
+
+Move some text from noffle.1 to noffle.conf.5
+
+Read timeout when running as server and automatically close if client
+does not send data for a longer time.
+
+Implement simple filter using popen or fifos.
+
+Make compatible to latest NNTP draft.
+
+Improve speed of online mode:
+  * Never update overview more than once per hour (or configurable time)
+  * Keep connection to server open for a while
+
+Check all in
+  http://mars.superlink.net/user/tal/writings/news-software-authors.html
+  (Use NOV library? Use inews for validating posted articles? ... )
+
+Use numbers when retrieving articles. Retrieving by message-id
+is disabled at some servers.
+
+Expire should clean up empty request/outgoing directories, so they will not
+exists forever after a server change.
+
+understand supersedes header (useful for reading news.answers group)
+
+Do not log program abortion due to SIGINT, if no inconsistency can occur,
+(e.g. when calling 'noffle -d' to a pipe and next program terminates or
+pressing ^C). 
+
+Improve www page and documentation.
+
+Keeping the content list for several lock/unlock times could lead to
+inconsistent results, because content list is maybe modified by
+pseudo articles. Check this!
+
+Optimize NEWGROUPS (extra list?)
+
+Add noffle query option for checking all groups, if they are still
+available at the remote server(s) and delete them otherwise.
+
+=============================================================================
+Some day far away
+=============================================================================
+
+Get and execute cancel messages (read control.cancel, but use xpat to get
+only cancels for groups in fetchlist). Seems to be expensive (20000 headers
+a day, takes the remote server to search through)
+
+=============================================================================
+Always
+=============================================================================
+
+Regularely look into news.software.readers to see what people are using,
+and test them with Noffle compiled in debug mode.
+
+Readers used by users or suitable for use with noffle.
+
+	tin
+	krn	
+	pine
+	netscape (4.5)
+	staroffice
+	slrn
+        gnus (?)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,822 @@
+/*
+  client.c
+
+  $Id: client.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "client.h"
+
+#include <arpa/inet.h>
+#include <ctype.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <sys/socket.h>
+#include <unistd.h>
+#include "config.h"
+#include "content.h"
+#include "dynamicstring.h"
+#include "group.h"
+#include "log.h"
+#include "over.h"
+#include "protocol.h"
+#include "pseudo.h"
+#include "request.h"
+#include "util.h"
+
+struct
+{
+    FILE* in;     /* Receiving socket from server */
+    FILE* out;    /* Sending socket to server */
+    Str lastCmd;  /* Last command line */
+    Str lastStat; /* Response from server to last command */
+    Str grp;      /* Selected group */
+    int rmtFirst;  /* First article of current group at server */
+    int rmtLast;   /* Last article of current group at server */
+    Bool auth;    /* Authetication already done? */
+    Str serv;     /* Remote server name */
+} client = { NULL, NULL, "", "", "", 1, 0, FALSE, "" };
+
+static void
+logBreakDown( void )
+{
+    Log_err( "Connection to remote server lost "
+             "(article numbers could be inconsistent)" );
+}
+
+static Bool
+getLn( Str line )
+{
+    Bool r;
+
+    r = Prt_getLn( line, client.in );
+    if ( ! r )
+        logBreakDown();
+    return r; 
+}
+
+static Bool
+getTxtLn( Str line, Bool *err )
+{
+    Bool r;
+
+    r = Prt_getTxtLn( line, err, client.in );
+    if ( *err )
+        logBreakDown();
+    return r; 
+}
+
+static void
+putTxtBuf( const char *buf )
+{
+    Prt_putTxtBuf( buf, client.out );
+    fflush( client.out );
+    Log_dbg( "[S FLUSH]" );
+}
+
+static void
+putEndOfTxt( void )
+{
+    Prt_putEndOfTxt( client.out );
+    fflush( client.out );
+    Log_dbg( "[S FLUSH]" );
+}
+
+static Bool
+putCmd( const char *fmt, ... )
+{
+    Bool err;
+    unsigned int n;
+    Str line;
+    va_list ap;
+
+    va_start( ap, fmt );
+    vsnprintf( line, MAXCHAR, fmt, ap );
+    va_end( ap );
+    strcpy( client.lastCmd, line );
+    Log_dbg( "[S] %s", line );
+    n = fprintf( client.out, "%s\r\n", line );
+    fflush( client.out );
+    Log_dbg( "[S FLUSH]" );
+    err = ( n != strlen( line ) + 2 );
+    if ( err )
+        logBreakDown();;
+    return ! err;
+}
+
+static Bool
+putCmdNoFlush( const char *fmt, ... )
+{
+    Bool err;
+    unsigned int n;
+    Str line;
+    va_list ap;
+
+    va_start( ap, fmt );
+    vsnprintf( line, MAXCHAR, fmt, ap );
+    va_end( ap );
+    strcpy( client.lastCmd, line );
+    Log_dbg( "[S] %s", line );
+    n = fprintf( client.out, "%s\r\n", line );
+    err = ( n != strlen( line ) + 2 );
+    if ( err )
+        logBreakDown();;
+    return ! err;
+}
+
+static int getStat( void );
+
+static Bool
+performAuth( void )
+{
+    int stat;
+    Str user, pass;
+    
+    Cfg_authInfo( client.serv, user, pass );
+    if ( strcmp( user, "" ) == 0 )
+    {
+        Log_err( "No username for authentication set" );
+        return FALSE;
+    }    
+    putCmd( "AUTHINFO USER %s", user );
+    stat = getStat();
+    if ( stat == STAT_AUTH_ACCEPTED )
+        return TRUE;
+    else if ( stat != STAT_MORE_AUTH_REQUIRED )
+    {
+        Log_err( "Username rejected. Server stat: %s", client.lastStat );
+        return FALSE;
+    }    
+    if ( strcmp( pass, "" ) == 0 )
+    {
+        Log_err( "No password for authentication set" );
+        return FALSE;
+    }
+    putCmd( "AUTHINFO PASS %s", pass );
+    stat = getStat();
+    if ( stat != STAT_AUTH_ACCEPTED )
+    {
+        Log_err( "Password rejected. Server status: %s", client.lastStat );
+        return FALSE;
+    }    
+    return TRUE;    
+}
+
+static int
+getStat( void )
+{
+    int result;
+    Str lastCmd;
+
+    if ( ! getLn( client.lastStat ) )
+        result = STAT_PROGRAM_FAULT;
+    else if ( sscanf( client.lastStat, "%d", &result ) != 1 )
+    {
+        Log_err( "Invalid server status: %s", client.lastStat );
+        result = STAT_PROGRAM_FAULT;
+    }
+    if ( result == STAT_AUTH_REQUIRED && ! client.auth )
+    {
+        client.auth = TRUE;
+        strcpy( lastCmd, client.lastCmd );
+        if ( performAuth() )
+        {
+            putCmd( lastCmd );
+            return getStat();
+        }
+    }
+    return result;
+}
+
+static void
+connectAlarm( int sig )
+{
+    return;
+}
+
+static sig_t
+installSignalHandler( int sig, sig_t handler )
+{
+    struct sigaction act, oldAct;
+
+    act.sa_handler = handler;
+    sigemptyset( &act.sa_mask );
+    act.sa_flags = 0;
+    if ( sig == SIGALRM )
+        act.sa_flags |= SA_INTERRUPT;
+    else
+        act.sa_flags |= SA_RESTART;
+    if ( sigaction( sig, &act, &oldAct ) < 0 )
+        return SIG_ERR;
+    return oldAct.sa_handler;
+}
+
+static Bool
+connectWithTimeout( int sock, const struct sockaddr *servAddr,
+                    socklen_t addrLen )
+{
+    sig_t oldHandler;
+    int r, to;
+
+    oldHandler = installSignalHandler( SIGALRM, connectAlarm );
+    if ( oldHandler == SIG_ERR )
+    {
+        Log_err( "client.c:connectWithTimeout: signal failed." );
+        return FALSE;
+    }
+    to = Cfg_connectTimeout();
+    if ( alarm( to ) != 0 )
+        Log_err( "client.c:connectWithTimeout: Alarm was already set." );
+    r = connect( sock, servAddr, addrLen );
+    alarm( 0 );
+    installSignalHandler( SIGALRM, oldHandler );
+    return ( r >= 0 );
+}
+
+Bool
+Client_connect( const char *serv )
+{
+    unsigned short int port;
+    int sock, i;
+    unsigned int stat;
+    struct hostent *hp;
+    char *pStart, *pColon;
+    Str host, s;
+    struct sockaddr_in sIn;
+
+    Utl_cpyStr( s, serv );
+    pStart = Utl_stripWhiteSpace( s );
+    pColon = strstr( pStart, ":" );
+    if ( pColon == NULL )
+    {
+        strcpy( host, pStart );
+        port = 119;
+    }
+    else
+    {
+        *pColon = '\0';
+        strcpy( host, pStart );
+        if ( sscanf( pColon + 1, "%hi", &port ) != 1 )
+        {
+            Log_err( "Syntax error in server name: '%s'", serv );
+            return FALSE;;
+        }
+        if ( port <= 0 || port > 65535 )
+        {
+            Log_err( "Invalid port number %hi. Must be in [1, 65535]", port );
+            return FALSE;;
+        }
+    }
+    memset( (void *)&sIn, 0, sizeof( sIn ) );
+    hp = gethostbyname( host );
+    if ( hp )
+    {
+        for ( i = 0; (hp->h_addr_list)[ i ]; ++i )
+        {
+            sIn.sin_family = hp->h_addrtype;
+            sIn.sin_port = htons( port );
+            sIn.sin_addr = *( (struct in_addr *)hp->h_addr_list[ i ] );
+            sock = socket( AF_INET, SOCK_STREAM, 0 );
+            if ( sock < 0 )
+                break;
+            if ( ! connectWithTimeout( sock, (struct sockaddr *)&sIn,
+                                       sizeof( sIn ) ) )
+            {
+                close( sock );
+                break;
+            }
+            if ( ! ( client.out = fdopen( sock, "w" ) )
+                 || ! ( client.in  = fdopen( dup( sock ), "r" ) ) )
+            {
+                close( sock );
+                break;
+            }
+            stat = getStat();
+            switch( stat ) {
+            case STAT_READY_POST_ALLOW:
+            case STAT_READY_NO_POST_ALLOW: 
+                Log_inf( "Connected to %s:%d",
+                         inet_ntoa( sIn.sin_addr ), port );
+                Utl_cpyStr( client.serv, serv );
+                return TRUE;
+            default:
+                Log_err( "Bad server stat %d", stat ); 
+            }
+            shutdown( fileno( client.out ), 0 );
+        }
+    }
+    return FALSE;
+}
+
+static void
+processGrps( void )
+{
+    char postAllow;
+    Bool err;
+    int first, last;
+    Str grp, line, file;
+    
+    while ( getTxtLn( line, &err ) && ! err )
+    {
+        if ( sscanf( line, "%s %i %i %c",
+                     grp, &last, &first, &postAllow ) != 4 )
+        {
+            Log_err( "Unknown reply to LIST or NEWGROUPS: %s", line );
+            continue;
+        }
+        if ( ! Grp_exists( grp ) )
+        {
+            Log_inf( "Registering new group '%s'", grp );
+            Grp_create( grp );
+            /* Start local numbering with remote first number to avoid
+               new numbering at the readers if noffle is re-installed */
+            if ( first != 0 )
+                Grp_setFirstLast( grp, first, first - 1 );
+            else
+                Grp_setFirstLast( grp, 1, 0 );
+            Grp_setRmtNext( grp, first );
+            Grp_setServ( grp, client.serv );
+        }
+        else
+        {
+            if ( Cfg_servIsPreferential( client.serv, Grp_serv( grp ) ) )
+            {
+                Log_inf( "Changing server for '%s': '%s'->'%s'",
+                         grp, Grp_serv( grp ), client.serv );
+                Grp_setServ( grp, client.serv );
+                Grp_setRmtNext( grp, first );
+            }
+            else
+                Log_dbg( "Group %s is already fetched from %s",
+                           grp, Grp_serv( grp ) );
+            
+        }
+    }
+    if ( ! err )
+    {
+        snprintf( file, MAXCHAR, "%s/groupinfo.lastupdate", Cfg_spoolDir() );
+        Utl_stamp( file );
+    }
+}
+
+void
+Client_disconnect( void )
+{
+    if ( putCmd( "QUIT" ) )
+        getStat();
+    fclose( client.in );
+    fclose( client.out );
+    client.in = client.out = NULL;
+}
+
+Bool
+Client_getGrps( void )
+{
+    if ( ! putCmd( "LIST ACTIVE" ) )
+        return FALSE;
+    if ( getStat() != STAT_GRPS_FOLLOW )
+    {
+        Log_err( "LIST ACTIVE command failed: %s", client.lastStat );
+        return FALSE;
+    }
+    processGrps();
+    return TRUE;
+}
+
+Bool
+Client_getDsc( void )
+{
+    Bool err;
+    Str name, line, dsc;
+
+    Log_inf( "Querying group descriptions" );
+    if ( ! putCmd( "LIST NEWSGROUPS" ) )
+        return FALSE;
+    if ( getStat() != STAT_GRPS_FOLLOW )
+    {
+        Log_err( "LIST NEWSGROUPS failed: %s", client.lastStat );
+        return FALSE;
+    }
+    while ( getTxtLn( line, &err ) && ! err )
+    {
+        if ( sscanf( line, "%s", name ) != 1 )
+        {
+            Log_err( "Unknown reply to LIST NEWSGROUPS: %s", line );
+            continue;
+        }
+        strcpy( dsc, Utl_restOfLn( line, 1 ) );
+        if ( Grp_exists( name ) )
+        {
+            Log_dbg( "Description of %s: %s", name, dsc );
+            Grp_setDsc( name, dsc );
+        }
+    }
+    return TRUE;
+}
+
+Bool
+Client_getCreationTimes( void )
+{
+    Bool err;
+    Str name, line;
+    time_t t;
+
+    Log_inf( "Querying group creation times" );
+    if ( ! putCmd( "LIST ACTIVE.TIMES" ) )
+        return FALSE;
+    if ( getStat() != STAT_GRPS_FOLLOW )
+    {
+        Log_err( "LIST ACTIVE.TIMES failes: %s", client.lastStat );
+        return FALSE;
+    }
+    while ( getTxtLn( line, &err ) && ! err )
+    {
+        if ( sscanf( line, "%s %ld", name, &t ) != 2 )
+        {
+            Log_err( "Unknown reply to LIST ACTIVE.TIMES: %s", line );
+            continue;
+        }
+        if ( Grp_exists( name ) )
+        {
+            Log_inf( "Creation time of %s: %ld", name, t );
+            Grp_setCreated( name, t );
+        }
+    }
+    return TRUE;
+}
+
+Bool
+Client_getNewgrps( const time_t *lastTime )
+{
+    Str s;
+    const char *p;
+
+    ASSERT( *lastTime > 0 );
+    strftime( s, MAXCHAR, "%Y%m%d %H%M00", gmtime( lastTime ) );
+    /*
+      Do not use century for working with old server software until 2000.
+      According to newest IETF draft, this is still valid after 2000.
+      (directly using %y in fmt string causes a Y2K compiler warning)
+    */
+    p = s + 2;
+    if ( ! putCmd( "NEWGROUPS %s", p ) )
+        return FALSE;
+    if ( getStat() != STAT_NEW_GRP_FOLLOW )
+    {
+        Log_err( "NEWGROUPS command failed: %s", client.lastStat );
+        return FALSE;
+    }
+    processGrps();
+    return TRUE;
+}
+
+static const char *
+readField( Str result, const char *p )
+{
+    size_t len;
+    char *r;
+
+    if ( ! p )
+        return NULL;
+    r = result;
+    *r = '\0';
+    len = 0;
+    while ( *p != '\t' && *p != '\n' )
+    {
+        if ( ! *p )
+            return p;
+        *(r++) = *(p++);
+        ++len;
+        if ( len >= MAXCHAR - 1 )
+        {
+            *r = '\0';
+            Log_err( "Field in overview too long: %s", r );
+            return ++p;
+        }
+    }
+    *r = '\0';
+    return ++p;
+}
+
+static Bool
+parseOvLn( Str line, int *numb, Str subj, Str from,
+           Str date, Str msgId, Str ref, size_t *bytes, size_t *lines )
+{
+    const char *p;
+    Str t;
+    
+    p = readField( t, line );
+    if ( sscanf( t, "%i", numb ) != 1 )
+        return FALSE;
+    p = readField( subj, p );
+    p = readField( from, p );
+    p = readField( date, p );
+    p = readField( msgId, p );
+    p = readField( ref, p );
+    p = readField( t, p );
+    *bytes = 0;
+    *lines = 0;
+    if ( sscanf( t, "%d", bytes ) != 1 )
+        return TRUE;
+    p = readField( t, p );
+    if ( sscanf( t, "%d", lines ) != 1 )
+        return TRUE;
+    return TRUE;
+}
+
+static const char*
+nextXref( const char *pXref, Str grp, int *numb )
+{
+    Str s;
+    const char *pColon, *src;
+    char *dst;
+
+    src = pXref;
+    while ( *src && isspace( *src ) )
+        ++src;
+    dst = s;
+    while ( *src && ! isspace( *src ) )
+        *(dst++) = *(src++);
+    *dst = '\0';
+    if ( strlen( s ) == 0 )
+        return NULL;
+    pColon = strstr( s, ":" );
+    if ( ! pColon || sscanf( pColon + 1, "%i", numb ) != 1 )
+    {
+        Log_err( "Corrupt Xref at position '%s'", pXref );
+        return NULL;
+    }
+    Utl_cpyStrN( grp, s, pColon - s );
+    Log_dbg( "client.c: nextXref: grp '%s' numb %lu", grp, numb );
+    return src;
+}
+
+static Bool
+needsMark( const char *ref )
+{
+    Bool done = FALSE;
+    char *p;
+    Str msgId;
+    int stat, len;
+    time_t lastAccess, nowTime;
+    double limit;
+
+    nowTime = time( NULL );
+    limit = Cfg_threadFollowTime() * 24. * 3600.;
+    while ( ! done )
+    {
+        p = msgId;
+        while ( *ref != '<' )
+            if ( *(ref++) == '\0' )
+                return FALSE;
+        len = 0;
+        while ( *ref != '>' )
+        {
+            if ( *ref == '\0' || ++len >= MAXCHAR - 1 )
+                return FALSE;
+            *(p++) = *(ref++);
+        }
+        *(p++) = '>';
+        *p = '\0';
+        if ( Db_contains( msgId ) )
+        {
+            stat = Db_stat( msgId );
+            lastAccess = Db_lastAccess( msgId );
+            if ( ( stat & DB_INTERESTING )
+                 && difftime( nowTime, lastAccess ) <= limit )
+                return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+static void
+prepareEntry( Over *ov )
+{
+    Str g, t;
+    const char *msgId, *p, *xref;
+    int n;
+
+    msgId = Ov_msgId( ov );
+    if ( Pseudo_isGeneralInfo( msgId ) )
+        Log_dbg( "Skipping general info '%s'", msgId );
+    else if ( Db_contains( msgId ) )
+    {
+        xref = Db_xref( msgId );
+        Log_dbg( "Entry '%s' already in db with Xref '%s'", msgId, xref );
+        p = nextXref( xref, g, &n );
+        if ( p == NULL )
+            Log_err( "Overview with no group in Xref '%s'", msgId );
+        else
+        {
+            if ( Cfg_servIsPreferential( client.serv, Grp_serv( g ) ) )
+            {
+                Log_dbg( "Changing first server for '%s' from '%s' to '%s'",
+                         msgId, Grp_serv( g ), client.serv );
+                snprintf( t, MAXCHAR, "%s:%i %s",
+                          client.grp, Ov_numb( ov ), xref );
+                Db_setXref( msgId, t );
+            }
+            else
+            {
+                Log_dbg( "Adding '%s' to Xref of '%s'", g, msgId );
+                snprintf( t, MAXCHAR, "%s %s:%i",
+                          xref, client.grp, Ov_numb( ov ) );
+                Db_setXref( msgId, t );
+            }
+        }
+    }
+    else
+    {
+        Log_dbg( "Preparing '%s' in database", msgId );
+        Db_prepareEntry( ov, client.grp, Ov_numb( ov ) );
+    }
+}
+
+Bool
+Client_getOver( int rmtFirst, int rmtLast, FetchMode mode )
+{
+    Bool err;
+    size_t bytes, lines;
+    int rmtNumb, oldLast, cntMarked;
+    Over *ov;
+    Str line, subj, from, date, msgId, ref;
+
+    ASSERT( strcmp( client.grp, "" ) != 0 );
+    if ( ! putCmd( "XOVER %lu-%lu", rmtFirst, rmtLast ) )
+        return FALSE;
+    if ( getStat() != STAT_OVERS_FOLLOW )
+    {
+        Log_err( "XOVER command failed: %s", client.lastStat );
+        return FALSE;
+    }
+    Log_dbg( "Requesting overview for remote %lu-%lu", rmtFirst, rmtLast );
+    oldLast = Cont_last();
+    cntMarked = 0;
+    while ( getTxtLn( line, &err ) && ! err )
+    {
+        if ( ! parseOvLn( line, &rmtNumb, subj, from, date, msgId, ref,
+                          &bytes, &lines ) )
+            Log_err( "Bad overview line: %s", line );
+        else
+        {
+            ov = new_Over( subj, from, date, msgId, ref, bytes, lines );
+            Cont_app( ov );
+            prepareEntry( ov );
+            if ( mode == FULL || ( mode == THREAD && needsMark( ref ) ) )
+            {
+                Req_add( client.serv, msgId );
+                ++cntMarked;
+            }
+        }
+        Grp_setRmtNext( client.grp, rmtNumb + 1 );
+    }
+    if ( oldLast != Cont_last() )
+        Log_inf( "Added %s %lu-%lu", client.grp, oldLast + 1, Cont_last() );
+    Log_inf( "%u articles marked for download in %s", cntMarked, client.grp  );
+    return err;
+}
+
+static void
+retrievingFailed( const char* msgId, const char *reason )
+{
+    int stat;
+
+    Log_err( "Retrieving of %s failed: %s", msgId, reason );
+    stat = Db_stat( msgId );
+    Pseudo_retrievingFailed( msgId, reason );
+    Db_setStat( msgId, stat | DB_RETRIEVING_FAILED );
+}
+
+static Bool
+retrieveAndStoreArt( const char *msgId )
+{
+    Bool err;
+    DynStr *s = NULL;
+    Str line;
+
+    Log_inf( "Retrieving %s", msgId );
+    s = new_DynStr( 5000 );
+    while ( getTxtLn( line, &err ) && ! err )
+        DynStr_appLn( s, line );
+    if ( ! err )
+        Db_storeArt( msgId, DynStr_str( s ) );
+    else
+        retrievingFailed( msgId, "Connection broke down" );
+    del_DynStr( s );
+    return ! err;
+}
+
+void
+Client_retrieveArt( const char *msgId )
+{
+    if ( ! Db_contains( msgId ) )
+    {
+        Log_err( "Article '%s' not prepared in database. Skipping.", msgId );
+        return;
+    }
+    if ( ! ( Db_stat( msgId ) & DB_NOT_DOWNLOADED ) )
+    {
+        Log_inf( "Article '%s' already retrieved. Skipping.", msgId );
+        return;
+    }
+    if ( ! putCmd( "ARTICLE %s", msgId ) )
+        retrievingFailed( msgId, "Connection broke down" );
+    else if ( getStat() != STAT_ART_FOLLOWS )
+        retrievingFailed( msgId, client.lastStat );
+    else
+        retrieveAndStoreArt( msgId );
+}
+
+void
+Client_retrieveArtList( const char *list )
+{
+    Str msgId;
+    DynStr *s;
+    const char *p;
+    
+    Log_inf( "Retrieving article list" );
+    s = new_DynStr( strlen( list ) );
+    p = list;
+    while ( ( p = Utl_getLn( msgId, p ) ) )
+        if ( ! Db_contains( msgId ) )
+            Log_err( "Skipping retrieving of %s (not prepared in database)",
+                     msgId );
+        else if ( ! ( Db_stat( msgId ) & DB_NOT_DOWNLOADED ) )
+            Log_inf( "Skipping %s (already retrieved)", msgId );
+        else if ( ! putCmdNoFlush( "ARTICLE %s", msgId ) )
+        {
+            retrievingFailed( msgId, "Connection broke down" );
+            del_DynStr( s );
+            return;
+        }
+        else
+            DynStr_appLn( s, msgId );
+    fflush( client.out );
+    Log_dbg( "[S FLUSH]" );
+    p = DynStr_str( s );
+    while ( ( p = Utl_getLn( msgId, p ) ) )
+    {
+        if ( getStat() != STAT_ART_FOLLOWS )
+            retrievingFailed( msgId, client.lastStat );
+        else if ( ! retrieveAndStoreArt( msgId ) )
+            break;
+    }
+    del_DynStr( s );
+}
+
+Bool
+Client_changeToGrp( const char* name )
+{
+    unsigned int stat;
+    int estimatedNumb, first, last;
+
+    if ( ! Grp_exists( name ) )
+        return FALSE;
+    if ( ! putCmd( "GROUP %s", name ) )
+        return FALSE;
+    if ( getStat() != STAT_GRP_SELECTED )
+        return FALSE;
+    if ( sscanf( client.lastStat, "%u %i %i %i",
+                 &stat, &estimatedNumb, &first, &last ) != 4 )
+    {
+        Log_err( "Bad server response to GROUP: %s", client.lastStat );
+        return FALSE;
+    }
+    Utl_cpyStr( client.grp, name );
+    client.rmtFirst = first;
+    client.rmtLast = last;
+    return TRUE;
+}
+
+void
+Client_rmtFirstLast( int *first, int *last )
+{
+    *first = client.rmtFirst;
+    *last = client.rmtLast;
+}
+
+Bool
+Client_postArt( const char *msgId, const char *artTxt,
+                    Str errStr )
+{
+    if ( ! putCmd( "POST" ) )
+        return FALSE;
+    if ( getStat() != STAT_SEND_ART )
+    {
+        Log_err( "Posting of %s not allowed: %s", msgId, client.lastStat );
+        strcpy( errStr, client.lastStat );
+        return FALSE;
+    }
+    putTxtBuf( artTxt );
+    putEndOfTxt();
+    if ( getStat() != STAT_POST_OK )
+    {
+        Log_err( "Posting of %s failed: %s", msgId, client.lastStat );
+        strcpy( errStr, client.lastStat );
+        return FALSE;
+    }
+    Log_inf( "Posted %s (Status: %s)", msgId, client.lastStat );
+    return TRUE;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,74 @@
+/*
+  client.h
+
+  Noffle acting as client to other NNTP-servers
+
+  $Id: client.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef CLIENT_H
+#define CLIENT_H
+
+#include <time.h>
+#include "common.h"
+#include "database.h"
+#include "fetchlist.h"
+
+/* Format of server name: <host>[:<port>] */
+Bool
+Client_connect( const char *serv );
+
+void
+Client_disconnect( void );
+
+Bool
+Client_getGrps( void );
+
+Bool
+Client_getDsc( void );
+
+Bool
+Client_getCreationTimes( void );
+
+Bool
+Client_getNewgrps( const time_t *lastTime );
+
+/*
+  Change to group <name> at server if it is also in current local grouplist.
+  Returns TRUE at success.
+*/
+Bool
+Client_changeToGrp( const Str name );
+
+/*
+  Get overviews <rmtFirst> - <rmtLast> from server and append it
+  to the current content. For articles that are to be fetched due to FULL
+  or THREAD mode, store IDs in request database.
+*/
+Bool
+Client_getOver( int rmtFirst, int rmtLast, FetchMode mode );
+
+/*
+  Retrieve full article text and store it into database.
+*/
+void
+Client_retrieveArt( const char *msgId );
+
+/*
+  Same, but for a list of msgId's (new line after each msgId).
+  All ARTICLE commands are sent and then all answers read.
+*/
+void
+Client_retrieveArtList( const char *list );
+
+/*
+  Store IDs of first and last article of group selected by
+  Client_changeToGroup at remote server. 
+*/
+void
+Client_rmtFirstLast( int *first, int *last );
+
+Bool
+Client_postArt( const char *msgId, const char *artTxt, Str errStr );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,33 @@
+/*
+  common.h
+
+  Common declarations.
+
+  $Id: common.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef COMMON_H
+#define COMMON_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define FALSE 0
+#define TRUE !0
+#define MAXCHAR 2048
+
+#ifdef DEBUG
+#include <assert.h>
+#define ASSERT( x ) \
+    if ( ! ( x ) ) \
+        Log_err( "ASSERTION FAILED: %s line %i", __FILE__, __LINE__ ); \
+    assert( x )
+#else
+#define ASSERT( x )
+#endif
+
+typedef int Bool;
+typedef char Str[ MAXCHAR ];
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,315 @@
+/*
+  config.c
+
+  The following macros must be set, when compiling this file:
+    CONFIGFILE
+    SPOOLDIR
+    VERSION
+
+  $Id: config.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "config.h"
+
+#include <limits.h>
+#include "log.h"
+#include "util.h"
+
+typedef struct
+{
+    Str name;
+    Str user;
+    Str pass;
+}
+ServEntry;
+
+struct
+{
+    /* Compile time options */
+    const char *spoolDir;
+    const char *version;
+    /* Options from the config file */
+    int maxFetch;
+    int autoUnsubscribeDays;
+    int threadFollowTime;
+    int connectTimeout;
+    Bool autoSubscribe;
+    Bool autoUnsubscribe;
+    Bool removeMsgId;
+    Bool replaceMsgId;
+    Str autoSubscribeMode;
+    Str mailTo;
+    int numServ;
+    int maxServ;
+    ServEntry *serv;
+    int servIdx; /* for server enumeration */
+} config =
+{
+    SPOOLDIR,
+    VERSION,
+    300,
+    30,
+    7,
+    30,
+    FALSE,
+    FALSE,
+    FALSE,
+    TRUE,
+    "over",
+    "",
+    0,
+    0,
+    NULL,
+    0
+};
+
+const char * Cfg_spoolDir( void ) { return config.spoolDir; }
+const char * Cfg_version( void ) { return config.version; }
+
+int Cfg_maxFetch( void ) { return config.maxFetch; }
+int Cfg_autoUnsubscribeDays( void ) { return config.autoUnsubscribeDays; }
+int Cfg_threadFollowTime( void ) { return config.threadFollowTime; }
+int Cfg_connectTimeout( void ) { return config.connectTimeout; }
+Bool Cfg_autoUnsubscribe( void ) { return config.autoUnsubscribe; }
+Bool Cfg_autoSubscribe( void )  { return config.autoSubscribe; }
+Bool Cfg_removeMsgId( void ) { return config.removeMsgId; }
+Bool Cfg_replaceMsgId( void ) { return config.replaceMsgId; }
+const char * Cfg_autoSubscribeMode( void ) {
+    return config.autoSubscribeMode; }
+const char * Cfg_mailTo( void ) { return config.mailTo; }
+
+void
+Cfg_beginServEnum( void )
+{
+    config.servIdx = 0;
+}
+
+Bool
+Cfg_nextServ( Str name )
+{
+    if ( config.servIdx >= config.numServ )
+        return FALSE;
+    strcpy( name, config.serv[ config.servIdx ].name );
+    ++config.servIdx;
+    return TRUE;
+}
+
+static Bool
+searchServ( const char *name, int *idx )
+{
+    int i;
+
+    for ( i = 0; i < config.numServ; ++i )
+        if ( strcmp( name, config.serv[ i ].name ) == 0 )
+        {
+            *idx = i;
+            return TRUE;
+        }
+    return FALSE;
+}
+
+Bool
+Cfg_servListContains( const char *name )
+{
+    int idx;
+
+    return searchServ( name, &idx );
+}
+
+Bool
+Cfg_servIsPreferential( const char *name1, const char *name2 )
+{
+    Bool exists1, exists2;
+    int idx1, idx2;
+
+    exists1 = searchServ( name1, &idx1 );
+    exists2 = searchServ( name2, &idx2 );
+    if ( exists1 && exists2 )
+        return ( idx1 < idx2 );
+    if ( exists1 && ! exists2 )
+        return TRUE;
+    /* ( ! exists1 && exists2 ) || ( ! exists1 && ! exists2 ) */
+    return FALSE;
+}
+
+void
+Cfg_authInfo( const char *name, Str user, Str pass )
+{
+    int idx;
+
+    if ( searchServ( name, &idx ) )
+    {
+        strcpy( user, config.serv[ idx ].user );
+        strcpy( pass, config.serv[ idx ].pass );
+    }
+    else
+    {
+        user[ 0 ] = '\0';
+        pass[ 0 ] = '\0';
+    }
+}
+
+static void
+logSyntaxErr( const char *line )
+{
+    Log_err( "Syntax error in config file: %s", line );
+}
+
+static void
+getBool( Bool *variable, const char *line )
+{
+    Str value, name, lowerLn;
+
+    strcpy( lowerLn, line );
+    Utl_toLower( lowerLn );
+    if ( sscanf( lowerLn, "%s %s", name, value ) != 2 )
+    {
+        logSyntaxErr( line );
+        return;
+    }
+    
+    if ( strcmp( value, "yes" ) == 0 )
+        *variable = TRUE;
+    else if ( strcmp( value, "no" ) == 0 )
+        *variable = FALSE;
+    else
+        Log_err( "Error in config file %s must be yes or no", name );
+}
+
+static void
+getInt( int *variable, int min, int max, const char *line )
+{
+    int value;
+    Str name;
+
+    if ( sscanf( line, "%s %d", name, &value ) != 2 )
+    {
+        logSyntaxErr( line );
+        return;
+    }
+    if ( value < min || value > max )
+    {
+        Log_err( "Range error in config file %s [%d,%d]", name, min, max );
+        return;
+    }
+    *variable = value;
+}
+
+static void
+getStr( char *variable, const char *line )
+{
+    Str dummy;
+
+    if ( sscanf( line, "%s %s", dummy, variable ) != 2 )
+    {
+        logSyntaxErr( line );
+        return;
+    }
+}
+
+static void
+getServ( const char *line )
+{
+    Str dummy;
+    int r, len;
+    ServEntry entry;
+
+    entry.user[ 0 ] = '\0';
+    entry.pass[ 0 ] = '\0';
+    r = sscanf( line, "%s %s %s %s",
+                dummy, entry.name, entry.user, entry.pass );
+    if ( r < 2 )
+    {
+        logSyntaxErr( line );
+        return;
+    }
+    len = strlen( entry.name );
+    /* To make server name more definit, it is made lowercase and
+       port is removed, if it is the default port */
+    if ( len > 4 && strcmp( entry.name + len - 4, ":119" ) == 0 )
+        entry.name[ len - 4 ] = '\0';
+    Utl_toLower( entry.name );
+
+    if ( config.maxServ < config.numServ + 1 )
+    {
+        if ( ! ( config.serv = realloc( config.serv,
+                                        ( config.maxServ + 5 )
+                                        * sizeof( ServEntry ) ) ) )
+        {
+            Log_err( "Could not realloc server list" );
+            exit( EXIT_FAILURE );
+        }
+        config.maxServ += 5;
+    }
+    config.serv[ config.numServ++ ] = entry;
+}
+
+void
+Cfg_read( void )
+{
+    char *p;
+    FILE *f;
+    Str file, line, lowerLine, name, s;
+
+    snprintf( file, MAXCHAR, CONFIGFILE );
+    if ( ! ( f = fopen( file, "r" ) ) )
+    {
+        Log_err( "Cannot read %s", file );
+        return;
+    }
+    while ( fgets( line, MAXCHAR, f ) )
+    {
+        Utl_cpyStr( lowerLine, line );
+        Utl_toLower( lowerLine );
+        p = Utl_stripWhiteSpace( lowerLine );
+        if ( *p == '#' || *p == '\0' )
+            continue;
+        if ( sscanf( p, "%s", name ) != 1 )
+            Log_err( "Syntax error in %s: %s", file, line );
+        else if ( strcmp( "max-fetch", name ) == 0 )
+            getInt( &config.maxFetch, 0, INT_MAX, p );
+        else if ( strcmp( "auto-unsubscribe-days", name ) == 0 )
+            getInt( &config.autoUnsubscribe, -1, INT_MAX, p );
+        else if ( strcmp( "thread-follow-time", name ) == 0 )
+            getInt( &config.threadFollowTime, 0, INT_MAX, p );
+        else if ( strcmp( "connect-timeout", name ) == 0 )
+            getInt( &config.connectTimeout, 0, INT_MAX, p );
+        else if ( strcmp( "auto-subscribe", name ) == 0 )
+            getBool( &config.autoSubscribe, p );
+        else if ( strcmp( "auto-unsubscribe", name ) == 0 )
+            getBool( &config.autoUnsubscribe, p );
+        else if ( strcmp( "remove-messageid", name ) == 0 )
+            getBool( &config.removeMsgId, p );
+        else if ( strcmp( "replace-messageid", name ) == 0 )
+            getBool( &config.replaceMsgId, p );
+        else if ( strcmp( "auto-subscribe-mode", name ) == 0 )
+        {
+            getStr( s, p );
+            Utl_toLower( s );
+            if ( strcmp( s, "full" ) != 0
+                 && strcmp( s, "thread" ) != 0
+                 && strcmp( s, "over" ) != 0
+                 && strcmp( s, "off" ) != 0 )
+            {
+                Log_err( "Syntax error in config file: %s", line );
+                return;
+            }
+            else
+                strcpy( config.autoSubscribeMode, s );
+        }
+        else if ( strcmp( "server", name ) == 0 )
+            /* Server needs line not p,
+               because password may contain uppercase */
+            getServ( line );
+        else if ( strcmp( "mail-to", name ) == 0 )
+            getStr( config.mailTo, p );
+        else
+            Log_err( "Unknown config option: %s", name );
+    }
+    fclose( f );
+    if ( ! config.numServ )
+    {
+        Log_err( "Config file contains no server" );
+        exit( EXIT_FAILURE );
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,43 @@
+/*
+  config.h
+
+  Common declarations and handling of the configuration file.
+
+  $Id: config.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#include "common.h"
+
+const char * Cfg_spoolDir( void );
+const char * Cfg_version( void );
+
+int Cfg_maxFetch( void );
+int Cfg_autoUnsubscribeDays( void );
+int Cfg_threadFollowTime( void );
+int Cfg_connectTimeout( void );
+Bool Cfg_autoUnsubscribe( void );
+Bool Cfg_autoSubscribe( void );
+Bool Cfg_removeMsgId( void );
+Bool Cfg_replaceMsgId( void );
+const char * Cfg_autoSubscribeMode( void ); /* Can be: full, thread, over */
+const char * Cfg_mailTo( void );
+
+/* Begin iteration through the server names */
+void Cfg_beginServEnum( void );
+
+/* Save next server name in "name". Return TRUE if name has been was saved.
+   Return FALSE if there are no more server names. */
+Bool Cfg_nextServ( Str name );
+
+Bool Cfg_servListContains( const char *name );
+/* Prefer server earlier in config file. Known servers are always preferential
+   to unknown servers. */
+Bool Cfg_servIsPreferential( const char *name1, const char *name2 );
+void Cfg_authInfo( const char *name, Str user, Str pass );
+
+void Cfg_read( void );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/content.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,238 @@
+/*
+  content.c
+
+  $Id: content.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include "common.h"
+#include "config.h"
+#include "log.h"
+#include "over.h"
+#include "pseudo.h"
+#include "util.h"
+
+struct
+{
+    DIR *dir;           /* Directory for browsing through all
+                           groups */
+    int first;
+    int last;
+    unsigned int size;  /* Number of overviews. */
+    unsigned int max;   /* Size of elem. */
+    Over **elem;        /* Ptr to array with ptrs to overviews.
+                           NULL entries for non-existing article numbers
+                           in group. */
+    Str name;
+    Str file;
+} cont = { NULL, 1, 0, 0, 0, NULL, "", "" };
+
+void
+Cont_app( Over *ov )
+{
+    if ( cont.max < cont.size + 1 )
+    {
+        if ( ! ( cont.elem = realloc( cont.elem,
+                                      ( cont.max + 500 )
+                                      * sizeof( cont.elem[ 0 ] ) ) ) )
+        {
+            Log_err( "Could not realloc overview list" );
+            exit( EXIT_FAILURE );
+        }
+        cont.max += 500;
+    }
+    if ( cont.first == 0 )
+        cont.first = 1;
+    if ( ov )
+        Ov_setNumb( ov, cont.first + cont.size );
+    cont.elem[ cont.size++ ] = ov;
+    cont.last = cont.first + cont.size - 1;
+}
+
+Bool
+Cont_validNumb( int n )
+{
+    return ( n != 0 && n >= cont.first && n <= cont.last
+             && cont.elem[ n - cont.first ] );
+}
+
+void
+Cont_delete( int n )
+{
+    Over **ov;
+
+    if ( ! Cont_validNumb( n ) )
+        return;
+    ov = &cont.elem[ n - cont.first ];
+    free( *ov );
+    *ov = NULL;
+}
+
+/* Remove all overviews from content. */
+static void
+clearCont()
+{
+    int i;
+
+    for ( i = 0; i < cont.size; ++i )
+        del_Over( cont.elem[ i ] );
+    cont.size = 0;
+}
+
+/* Extend content list to size "cnt" and append NULL entries. */
+static void
+extendCont( int cnt )
+{
+    int i, n;
+    
+    if ( cont.size < cnt )
+    {
+        n = cnt - cont.size;
+        for ( i = 0; i < n; ++i )
+            Cont_app( NULL );
+    }
+}
+
+/* Discard all cached overviews, and read in the overviews of a new group
+   from its overviews file. */
+void
+Cont_read( const char *name )
+{
+    FILE *f;
+    Over *ov;
+    int cnt, numb;
+    Str line;
+
+    /* Delete old overviews and make room for new ones. */
+    cont.first = 0;
+    cont.last = 0;
+    Utl_cpyStr( cont.name, name );
+    clearCont();
+
+    /* read overviews from overview file and store them in the overviews
+       list */
+    snprintf( cont.file, MAXCHAR, "%s/overview/%s", Cfg_spoolDir(), name ); 
+    if ( cnt == 0 )
+        return;
+    f = fopen( cont.file, "r" );
+    if ( ! f )
+    {
+        Log_dbg( "No group overview file: %s", cont.file );
+        return;
+    }
+    Log_dbg( "Reading %s", cont.file );
+    while ( fgets( line, MAXCHAR, f ) )
+    {
+        if ( ! ( ov = Ov_read( line ) ) )
+        {
+            Log_err( "Overview corrupted in %s: %s", name, line );
+            continue;
+        }
+        numb = Ov_numb( ov );
+        if ( numb < cont.first )
+        {
+            Log_err( "Wrong ordering in %s: %s", name, line );
+            continue;
+        }
+        if ( cont.first == 0 )
+            cont.first = numb;
+        cont.last = numb;
+        extendCont( numb - cont.first + 1 );
+        cont.elem[ numb - cont.first ] = ov;
+    }
+    fclose( f );
+}
+
+void
+Cont_write( void )
+{
+    Bool anythingWritten;
+    int i, first;
+    FILE *f;
+    const Over *ov;
+
+    first = cont.first;
+    while ( ! Cont_validNumb( first ) && first <= cont.last )
+        ++first;
+    if ( ! ( f = fopen( cont.file, "w" ) ) )
+    {
+        Log_err( "Could not open %s for writing", cont.file );
+        return;
+    }
+    Log_dbg( "Writing %s (%lu)", cont.file, cont.size );
+    anythingWritten = FALSE;
+    for ( i = 0; i < cont.size; ++i )
+    {
+        if ( ( ov = cont.elem[ i ] ) )
+        {
+            if ( ! Pseudo_isGeneralInfo( Ov_msgId( ov ) ) )
+            {
+                if ( ! Ov_write( ov, f ) )
+                {
+                    Log_err( "Writing of overview line failed" );
+                    break;
+                }
+                else
+                    anythingWritten = TRUE;
+            }
+        }
+    }
+    fclose( f );
+    if ( ! anythingWritten )
+        unlink( cont.file );
+}
+
+const Over *
+Cont_get( int numb )
+{
+    if ( ! Cont_validNumb( numb ) )
+        return NULL;
+    return cont.elem[ numb - cont.first ];
+}
+
+int
+Cont_first( void ) { return cont.first; }
+
+int
+Cont_last( void ) { return cont.last; }
+
+const char *
+Cont_grp( void ) { return cont.name; }
+
+Bool
+Cont_nextGrp( Str result )
+{
+    struct dirent *d;
+    
+    ASSERT( cont.dir );
+    if ( ! ( d = readdir( cont.dir ) ) )
+    {
+        cont.dir = NULL;
+        return FALSE;
+    }
+    if ( ! d->d_name )
+        return FALSE;
+    Utl_cpyStr( result, d->d_name );
+    result[ MAXCHAR - 1 ] = '\0';
+    return TRUE;
+}
+
+Bool
+Cont_firstGrp( Str result )
+{
+    Str name;
+
+    snprintf( name, MAXCHAR, "%s/overview", Cfg_spoolDir() );
+    if ( ! ( cont.dir = opendir( name ) ) )
+    {
+        Log_err( "Cannot open %s", name );
+        return FALSE;
+    }
+    Cont_nextGrp( result ); /* "."  */
+    Cont_nextGrp( result ); /* ".." */
+    return Cont_nextGrp( result );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/content.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,65 @@
+/*
+  content.h
+
+  Contents of a newsgroup
+  - list of article overviews for selected group.
+
+  The overviews of all articles of a group are stored in an overview file,
+  filename SPOOLDIR/overview/GROUPNAME. One entire overview file is read
+  and cached in memory, at a time.
+
+  $Id: content.h 3 2000-01-04 11:35:42Z enz $ 
+*/
+
+#ifndef CONT_H
+#define CONT_H
+
+#include "over.h"
+
+/*
+  Try to read overviews from overview file for group <grp>.
+  Fill with fake articles, if something goes wrong.
+*/
+void
+Cont_read( const char *grp );
+
+/*
+  Append overview to current list and increment the current
+  group's last article counter. Ownership of the ptr is transfered
+  to content
+*/
+void
+Cont_app( Over *ov );
+
+/* Write content */
+void
+Cont_write( void );
+
+Bool
+Cont_validNumb( int numb );
+
+const Over *
+Cont_get( int numb );
+
+void
+Cont_delete( int numb );
+
+int
+Cont_first( void );
+
+int
+Cont_last( void );
+
+const char *
+Cont_grp( void );
+
+Bool
+Cont_nextGrp( Str result );
+
+Bool
+Cont_firstGrp( Str result );
+
+void
+Cont_expire( void );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/database.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,609 @@
+/*
+  database.c
+
+  $Id: database.c 3 2000-01-04 11:35:42Z enz $
+
+  Uses GNU gdbm library. Using Berkeley db (included in libc6) was
+  cumbersome. It is based on Berkeley db 1.85, which has severe bugs
+  (e.g. it is not recommended to delete or overwrite entries with
+  overflow pages).
+*/
+
+#include "database.h"
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <gdbm.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "config.h"
+#include "log.h"
+#include "protocol.h"
+#include "util.h"
+
+static struct Db
+{
+    GDBM_FILE dbf;
+
+    /* Start string for Xref header line: "Xref: <host>" */
+    Str xrefHost;
+
+    /* Msg Id of presently loaded article, empty if none loaded */
+    Str msgId;
+
+    /* Status of loaded article */
+    int stat; /* Flags */
+    time_t lastAccess;
+
+    /* Overview of loaded article */
+    Str subj; 
+    Str from;
+    Str date;
+    Str ref;
+    Str xref;
+    size_t bytes;
+    size_t lines;
+
+    /* Article text (except for overview header lines) */
+    DynStr *txt;
+
+} db = { NULL, "(unknown)", "", 0, 0, "", "", "", "", "", 0, 0, NULL };
+
+static const char *
+errMsg( void )
+{
+    if ( errno != 0 )
+        return strerror( errno );
+    return gdbm_strerror( gdbm_errno );
+}
+
+Bool
+Db_open( void )
+{
+    Str name, host;
+    int flags;
+
+    ASSERT( db.dbf == NULL );
+    snprintf( name, MAXCHAR, "%s/data/articles.gdbm", Cfg_spoolDir() );
+    flags = GDBM_WRCREAT | GDBM_FAST;
+
+    if ( ! ( db.dbf = gdbm_open( name, 512, flags, 0644, NULL ) ) )
+    {
+        Log_err( "Error opening %s for r/w (%s)", name, errMsg() );
+        return FALSE;
+    }
+    Log_dbg( "%s opened for r/w", name );
+
+    if ( db.txt == NULL )
+        db.txt = new_DynStr( 5000 );
+
+    gethostname( host, MAXCHAR );
+    snprintf( db.xrefHost, MAXCHAR, "Xref: %s", host );
+
+    return TRUE;
+}
+
+void
+Db_close( void )
+{
+    ASSERT( db.dbf );
+    Log_dbg( "Closing database" );
+    gdbm_close( db.dbf );
+    db.dbf = NULL;
+    del_DynStr( db.txt );
+    db.txt = NULL;
+    Utl_cpyStr( db.msgId, "" );
+}
+
+static Bool
+loadArt( const char *msgId )
+{
+    static void *dptr = NULL;
+    
+    datum key, val;
+    Str t = "";
+    const char *p;
+    
+    ASSERT( db.dbf );
+
+    if ( strcmp( msgId, db.msgId ) == 0 )
+        return TRUE;
+
+    key.dptr = (void *)msgId;
+    key.dsize = strlen( msgId ) + 1;
+    if ( dptr != NULL )
+    {
+        free( dptr );
+        dptr = NULL;
+    }
+    val = gdbm_fetch( db.dbf, key );
+    dptr = val.dptr;
+    if ( dptr == NULL )
+    {
+        Log_dbg( "database.c loadArt: gdbm_fetch found no entry" );
+        return FALSE;
+    }
+    
+    Utl_cpyStr( db.msgId, msgId );
+    p = Utl_getLn( t, (char *)dptr );
+    if ( ! p || sscanf( t, "%x", &db.stat ) != 1 )
+    {
+        Log_err( "Entry in database '%s' is corrupt (status)", msgId );
+        return FALSE;
+    }
+    p = Utl_getLn( t, p );
+    if ( ! p || sscanf( t, "%lu", &db.lastAccess ) != 1 )
+    {
+        Log_err( "Entry in database '%s' is corrupt (lastAccess)", msgId );
+        return FALSE;
+    }
+    p = Utl_getLn( db.subj, p );
+    p = Utl_getLn( db.from, p );
+    p = Utl_getLn( db.date, p );
+    p = Utl_getLn( db.ref, p );
+    p = Utl_getLn( db.xref, p );
+    if ( ! p )
+    {
+        Log_err( "Entry in database '%s' is corrupt (overview)", msgId );
+        return FALSE;
+    }
+    p = Utl_getLn( t, p );
+    if ( ! p || sscanf( t, "%u", &db.bytes ) != 1 )
+    {
+        Log_err( "Entry in database '%s' is corrupt (bytes)", msgId );
+        return FALSE;
+    }
+    p = Utl_getLn( t, p );
+    if ( ! p || sscanf( t, "%u", &db.lines ) != 1 )
+    {
+        Log_err( "Entry in database '%s' is corrupt (lines)", msgId );
+        return FALSE;
+    }
+    DynStr_clear( db.txt );
+    DynStr_app( db.txt, p );
+    return TRUE;
+}
+
+static Bool
+saveArt( void )
+{
+    DynStr *s;
+    Str t = "";
+    datum key, val;
+
+    if ( strcmp( db.msgId, "" ) == 0 )
+        return FALSE;
+    s = new_DynStr( 5000 );
+    snprintf( t, MAXCHAR, "%x", db.stat );
+    DynStr_appLn( s, t );
+    snprintf( t, MAXCHAR, "%lu", db.lastAccess );
+    DynStr_appLn( s, t );
+    DynStr_appLn( s, db.subj );
+    DynStr_appLn( s, db.from );
+    DynStr_appLn( s, db.date );
+    DynStr_appLn( s, db.ref );
+    DynStr_appLn( s, db.xref );
+    snprintf( t, MAXCHAR, "%u", db.bytes );
+    DynStr_appLn( s, t );
+    snprintf( t, MAXCHAR, "%u", db.lines );
+    DynStr_appLn( s, t );
+    DynStr_appDynStr( s, db.txt );
+
+    key.dptr = (void *)db.msgId;
+    key.dsize = strlen( db.msgId ) + 1;
+    val.dptr = (void *)DynStr_str( s );
+    val.dsize = DynStr_len( s ) + 1;
+    if ( gdbm_store( db.dbf, key, val, GDBM_REPLACE ) != 0 )
+    {
+        Log_err( "Could not store %s in database (%s)", errMsg() );
+        return FALSE;
+    }
+
+    del_DynStr( s );
+    return TRUE;
+}
+
+Bool
+Db_prepareEntry( const Over *ov, const char *grp, int numb )
+{
+    const char *msgId;
+
+    ASSERT( db.dbf );
+    ASSERT( ov );
+    ASSERT( grp );
+
+    msgId = Ov_msgId( ov );
+    Log_dbg( "Preparing entry %s", msgId );
+    if ( Db_contains( msgId ) )
+        Log_err( "Preparing article twice: %s", msgId );
+
+    db.stat = DB_NOT_DOWNLOADED;
+    db.lastAccess = time( NULL );
+
+    Utl_cpyStr( db.msgId, msgId );
+    Utl_cpyStr( db.subj, Ov_subj( ov ) );
+    Utl_cpyStr( db.from, Ov_from( ov ) );
+    Utl_cpyStr( db.date, Ov_date( ov ) );
+    Utl_cpyStr( db.ref, Ov_ref( ov ) );
+    snprintf( db.xref, MAXCHAR, "%s:%i", grp, numb );
+    db.bytes = Ov_bytes( ov );
+    db.lines = Ov_lines( ov );
+
+    DynStr_clear( db.txt );
+
+    return saveArt();
+}
+
+Bool
+Db_storeArt( const char *msgId, const char *artTxt )
+{
+    Str line, lineEx, field, value;
+    const char *startPos;
+
+    ASSERT( db.dbf );
+
+    Log_dbg( "Store article %s", msgId );
+    if ( ! loadArt( msgId ) )
+    {
+        Log_err( "Cannot find info about '%s' in database", msgId );
+        return FALSE;
+    }
+    if ( ! ( db.stat & DB_NOT_DOWNLOADED ) )
+    {
+        Log_err( "Trying to store alrady retrieved article '%s'", msgId );
+        return FALSE;
+    }
+    db.stat &= ~DB_NOT_DOWNLOADED;
+    db.stat &= ~DB_RETRIEVING_FAILED;
+    db.lastAccess = time( NULL );
+
+    DynStr_clear( db.txt );
+
+    /* Read header */
+    startPos = artTxt;
+    while ( TRUE )
+    {
+        artTxt = Utl_getLn( lineEx, artTxt );
+        if ( lineEx[ 0 ] == '\0' )
+        {
+            DynStr_appLn( db.txt, lineEx );
+            break;
+        }
+        /* Get other lines if field is split over multiple lines */
+        while ( ( artTxt = Utl_getLn( line, artTxt ) ) )
+            if ( isspace( line[ 0 ] ) )
+            {
+                strncat( lineEx, "\n", MAXCHAR );                
+                strncat( lineEx, line, MAXCHAR );
+            }
+            else
+            {
+                artTxt = Utl_ungetLn( startPos, artTxt );
+                break;
+            }
+        /* Remove fields already in overview and handle x-noffle
+           headers correctly in case of cascading NOFFLEs */
+        if ( Prt_getField( field, value, lineEx ) )
+        {
+            if ( strcmp( field, "x-noffle-status" ) == 0 )
+            {
+                if ( strstr( value, "NOT_DOWNLOADED" ) != 0 )
+                    db.stat |= DB_NOT_DOWNLOADED;
+            }
+            else if ( strcmp( field, "message-id" ) != 0
+                      && strcmp( field, "xref" ) != 0
+                      && strcmp( field, "references" ) != 0
+                      && strcmp( field, "subject" ) != 0
+                      && strcmp( field, "from" ) != 0
+                      && strcmp( field, "date" ) != 0
+                      && strcmp( field, "bytes" ) != 0
+                      && strcmp( field, "lines" ) != 0
+                      && strcmp( field, "x-noffle-lastaccess" ) != 0 )
+                DynStr_appLn( db.txt, lineEx );
+        }
+    }
+
+    /* Read body */
+    while ( ( artTxt = Utl_getLn( line, artTxt ) ) )
+        if ( ! ( db.stat & DB_NOT_DOWNLOADED ) )
+            DynStr_appLn( db.txt, line );
+    
+    return saveArt();
+}
+
+void
+Db_setStat( const char *msgId, int stat )
+{
+    if ( loadArt( msgId ) )
+    {
+        db.stat = stat;
+        saveArt();
+    }
+}
+
+void
+Db_updateLastAccess( const char *msgId )
+{
+    if ( loadArt( msgId ) )
+    {
+        db.lastAccess = time( NULL );
+        saveArt();
+    }
+}
+
+void
+Db_setXref( const char *msgId, const char *xref )
+{
+    if ( loadArt( msgId ) )
+    {
+        Utl_cpyStr( db.xref, xref );
+        saveArt();
+    }
+}
+
+/* Search best position for breaking a line */
+static const char *
+searchBreakPos( const char *line, int wantedLength )
+{
+    const char *lastSpace = NULL;
+    Bool lastWasSpace = FALSE;
+    int len = 0;
+
+    while ( *line != '\0' )
+    {
+        if ( isspace( *line ) )
+        {
+            if ( len > wantedLength && lastSpace != NULL )
+                return lastSpace;
+            if ( ! lastWasSpace )
+                lastSpace = line;
+            lastWasSpace = TRUE;
+        }
+        else
+            lastWasSpace = FALSE;
+        ++len;
+        ++line;
+    }
+    if ( len > wantedLength && lastSpace != NULL )
+        return lastSpace;
+    return line;
+}
+
+/* Append header line by breaking long line into multiple lines */
+static void
+appendLongHeader( DynStr *target, const char *field, const char *value )
+{
+    const int wantedLength = 78;
+    const char *breakPos, *old;
+    int len;
+
+    len = strlen( field );
+    DynStr_appN( target, field, len );
+    DynStr_appN( target, " ", 1 );
+    old = value;
+    while ( isspace( *old ) )
+        ++old;
+    breakPos = searchBreakPos( old, wantedLength - len - 1 );
+    DynStr_appN( target, old, breakPos - old );
+    if ( *breakPos == '\0' )
+    {
+        DynStr_appN( target, "\n", 1 );
+        return;
+    }
+    DynStr_appN( target, "\n ", 2 );
+    while ( TRUE )
+    {
+        old = breakPos;
+        while ( isspace( *old ) )
+            ++old;
+        breakPos = searchBreakPos( old, wantedLength - 1 );
+        DynStr_appN( target, old, breakPos - old );
+        if ( *breakPos == '\0' )
+        {
+            DynStr_appN( target, "\n", 1 );
+            return;
+        }
+        DynStr_appN( target, "\n ", 2 );
+    }
+}
+
+const char *
+Db_header( const char *msgId )
+{
+    static DynStr *s = NULL;
+
+    Str date, t;
+    int stat;
+    const char *p;
+
+    if ( s == NULL )
+        s = new_DynStr( 5000 );
+    else
+        DynStr_clear( s );
+    ASSERT( db.dbf );
+    if ( ! loadArt( msgId ) )
+        return NULL;
+    strftime( date, MAXCHAR, "%Y-%m-%d %H:%M:%S",
+              localtime( &db.lastAccess ) );
+    stat = db.stat;
+    snprintf( t, MAXCHAR,
+              "Message-ID: %s\n"
+              "X-NOFFLE-Status:%s%s%s\n"
+              "X-NOFFLE-LastAccess: %s\n",
+              msgId,
+              stat & DB_INTERESTING ? " INTERESTING" : "",
+              stat & DB_NOT_DOWNLOADED ? " NOT_DOWNLOADED" : "",
+              stat & DB_RETRIEVING_FAILED ? " RETRIEVING_FAILED" : "",
+              date );
+    DynStr_app( s, t );
+    appendLongHeader( s, "Subject:", db.subj );
+    appendLongHeader( s, "From:", db.from );
+    appendLongHeader( s, "Date:", db.date );
+    appendLongHeader( s, "References:", db.ref );
+    DynStr_app( s, "Bytes: " );
+    snprintf( t, MAXCHAR, "%u", db.bytes );
+    DynStr_appLn( s, t );
+    DynStr_app( s, "Lines: " );
+    snprintf( t, MAXCHAR, "%u", db.lines );
+    DynStr_appLn( s, t );
+    appendLongHeader( s, db.xrefHost, db.xref );
+    p = strstr( DynStr_str( db.txt ), "\n\n" );
+    if ( ! p )
+        DynStr_appDynStr( s, db.txt );
+    else
+        DynStr_appN( s, DynStr_str( db.txt ), p - DynStr_str( db.txt ) + 1 );
+    return DynStr_str( s );
+}
+
+const char *
+Db_body( const char *msgId )
+{
+    const char *p;
+
+    if ( ! loadArt( msgId ) )
+        return "";
+    p = strstr( DynStr_str( db.txt ), "\n\n" );
+    if ( ! p )
+        return "";
+    return ( p + 2 );
+}
+
+int
+Db_stat( const char *msgId )
+{
+    if ( ! loadArt( msgId ) )
+        return 0;
+    return db.stat;
+}
+
+time_t
+Db_lastAccess( const char *msgId )
+{
+    if ( ! loadArt( msgId ) )
+        return -1;
+    return db.lastAccess;
+}
+
+const char *
+Db_ref( const char *msgId )
+{
+    if ( ! loadArt( msgId ) )
+        return "";
+    return db.ref;
+}
+
+const char *
+Db_xref( const char *msgId )
+{
+    if ( ! loadArt( msgId ) )
+        return "";
+    return db.xref;
+}
+
+Bool
+Db_contains( const char *msgId )
+{
+    datum key;
+
+    ASSERT( db.dbf );
+    if ( strcmp( msgId, db.msgId ) == 0 )
+        return TRUE;
+    key.dptr = (void*)msgId;
+    key.dsize = strlen( msgId ) + 1;
+    return gdbm_exists( db.dbf, key );
+}
+
+static datum cursor = { NULL, 0 };
+
+Bool
+Db_first( const char** msgId )
+{
+    ASSERT( db.dbf );
+    if ( cursor.dptr != NULL )
+    {
+        free( cursor.dptr );
+        cursor.dptr = NULL;
+    }
+    cursor = gdbm_firstkey( db.dbf );
+    *msgId = cursor.dptr;
+    return ( cursor.dptr != NULL );
+}
+
+Bool
+Db_next( const char** msgId )
+{
+    void *oldDptr = cursor.dptr;
+
+    ASSERT( db.dbf );
+    if ( cursor.dptr == NULL )
+        return FALSE;
+    cursor = gdbm_nextkey( db.dbf, cursor );
+    free( oldDptr );
+    *msgId = cursor.dptr;
+    return ( cursor.dptr != NULL );
+}
+
+Bool
+Db_expire( unsigned int days )
+{
+    double limit;
+    int cntDel, cntLeft, flags;
+    time_t nowTime, lastAccess;
+    const char *msgId;
+    Str name, tmpName;
+    GDBM_FILE tmpDbf;
+    datum key, val;
+
+    if ( ! Db_open() )
+        return FALSE;
+    snprintf( name, MAXCHAR, "%s/data/articles.gdbm", Cfg_spoolDir() );
+    snprintf( tmpName, MAXCHAR, "%s/data/articles.gdbm.new", Cfg_spoolDir() );
+    flags = GDBM_NEWDB | GDBM_FAST;
+    if ( ! ( tmpDbf = gdbm_open( tmpName, 512, flags, 0644, NULL ) ) )
+    {
+        Log_err( "Error opening %s for read/write (%s)", errMsg() );
+        Db_close();
+        return FALSE;
+    }
+    Log_inf( "Expiring articles that have not been accessed for %u days",
+             days );
+    limit = days * 24. * 3600.;
+    cntDel = 0;
+    cntLeft = 0;
+    nowTime = time( NULL );
+    if ( Db_first( &msgId ) )
+        do
+        {
+            lastAccess = Db_lastAccess( msgId );
+            if ( lastAccess == -1 )
+                Log_err( "Internal error: Getting lastAccess of %s failed",
+                         msgId );
+            else if ( difftime( nowTime, lastAccess ) > limit )
+            {
+                Log_dbg( "Expiring %s", msgId );
+                ++cntDel;
+            }
+            else
+            {
+                ++cntLeft;
+                key.dptr = (void *)msgId;
+                key.dsize = strlen( msgId ) + 1;
+
+                val = gdbm_fetch( db.dbf, key );
+                if ( val.dptr != NULL )
+                {
+                    if ( gdbm_store( tmpDbf, key, val, GDBM_INSERT ) != 0 )
+                        Log_err( "Could not store %s in new database (%s)",
+                                 errMsg() );
+                    free( val.dptr );
+                }
+            }
+        }
+        while ( Db_next( &msgId ) );
+    Log_inf( "%lu articles deleted, %lu left", cntDel, cntLeft );
+    gdbm_close( tmpDbf );
+    Db_close();
+    rename( tmpName, name );
+    return TRUE;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/database.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,79 @@
+/*
+  database.h
+
+  Article database.
+
+  $Id: database.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef DB_H
+#define DB_H
+
+#include <time.h>
+#include "common.h"
+#include "dynamicstring.h"
+#include "over.h"
+
+/* Article status flags: */
+#define DB_INTERESTING       0x01  /* Was article ever tried to read? */
+#define DB_NOT_DOWNLOADED    0x02  /* Not fully downloaded */
+#define DB_RETRIEVING_FAILED 0x04  /* Retrieving of article failed */
+
+/* Open database for r/w. Locking must be done by the caller! */
+Bool
+Db_open( void );
+
+void
+Db_close( void );
+
+Bool
+Db_prepareEntry( const Over *ov, const char *grp, int numb );
+
+Bool
+Db_storeArt( const char *msgId, const char *artTxt );
+
+void
+Db_setStat( const char *msgId, int stat );
+
+void
+Db_updateLastAccess( const char *msgId );
+
+/* Xref header line without hostname */
+void
+Db_setXref( const char *msgId, const char *xref );
+
+const char *
+Db_header( const char *msgId );
+
+const char *
+Db_body( const char *msgId );
+
+int
+Db_stat( const char *msgId );
+
+/* Get last modification time of entry. Returns -1, if msgId non-existing. */
+time_t
+Db_lastAccess( const char *msgId );
+
+/* Value of references header line */
+const char *
+Db_ref( const char *msgId );
+
+/* Xref header line without hostname */
+const char *
+Db_xref( const char *msgId );
+
+Bool
+Db_contains( const char *msgId );
+
+Bool
+Db_first( const char** msgId );
+
+Bool
+Db_next( const char** msgId );
+
+/* Expire all articles that have not been accessed for <days> */
+Bool
+Db_expire( unsigned int days );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dynamicstring.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,120 @@
+/*
+  dynamicstring.c
+
+  $Id: dynamicstring.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "dynamicstring.h"
+
+#include <sys/types.h>
+#include "log.h"
+
+struct DynStr
+{
+    size_t len; /* Current length (without trailing '\0') */
+    size_t max; /* Max length that fits into buffer (incl. trailing '\0') */
+    char *str;
+};
+
+static void
+reallocStr( DynStr *self, size_t max )
+{
+    if ( max <= self->max )
+        return;
+    if ( ! ( self->str = (char *)realloc( self->str, max ) ) )
+    {
+        Log_err( "Realloc of DynStr failed" );
+        exit( EXIT_FAILURE );
+    } 
+    if ( self->max == 0 ) /* First allocation? */
+        *(self->str) = '\0';
+    self->max = max;
+}
+
+DynStr *
+new_DynStr( size_t reserve )
+{
+    DynStr *s;
+    
+    if ( ! ( s = (DynStr *) malloc( sizeof( DynStr ) ) ) )
+    {
+        Log_err( "Allocation of DynStr failed" );
+        exit( EXIT_FAILURE );
+    }
+    s->len = 0;
+    s->max = 0;
+    s->str = NULL;
+    if ( reserve > 0 )
+        reallocStr( s, reserve + 1 );
+    return s;
+}
+
+void
+del_DynStr( DynStr *self )
+{
+    if ( ! self )
+        return;
+    free( self->str );
+    self->str = NULL;
+    free( self );
+}
+
+size_t
+DynStr_len( const DynStr *self )
+{
+    return self->len;
+}
+
+const char *
+DynStr_str( const DynStr *self )
+{
+    return self->str;
+}
+
+void
+DynStr_app( DynStr *self, const char *s )
+{
+    size_t len;
+
+    len = strlen( s );
+    if ( self->len + len + 1 > self->max )
+        reallocStr( self, self->len * 2 + len + 1 );
+    strcpy( self->str + self->len, s );
+    self->len += len;
+}
+
+void
+DynStr_appDynStr( DynStr *self, const DynStr *s )
+{
+    if ( self->len + s->len + 1 > self->max )
+        reallocStr( self, self->len * 2 + s->len + 1 );
+    memcpy( self->str + self->len, s->str, s->len + 1 );
+    self->len += s->len;
+}
+
+void
+DynStr_appLn( DynStr *self, const char *s )
+{
+    DynStr_app( self, s );
+    DynStr_app( self, "\n" );
+}
+
+void
+DynStr_appN( DynStr *self, const char *s, size_t n )
+{
+    size_t len = self->len;
+
+    if ( len + n + 1 > self->max )
+        reallocStr( self, len * 2 + n + 1 );
+    strncat( self->str + len, s, n );
+    self->len = len + strlen( self->str + len );
+}
+
+void
+DynStr_clear( DynStr *self )
+{
+    self->len = 0;
+    if ( self->max > 0 )
+        *(self->str) = '\0';
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dynamicstring.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,54 @@
+/*
+  dynamicstring.h
+
+  String utilities
+
+  $Id: dynamicstring.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef DYNAMICSTRING_H
+#define DYNAMICSTRING_H
+
+#include <sys/types.h>
+
+/* A dynamically growing string */
+struct DynStr;
+typedef struct DynStr DynStr;
+
+/* Create new DynStr with given capacity */
+DynStr *
+new_DynStr( size_t reserve );
+
+/* Delete DynStr */
+void
+del_DynStr( DynStr *self );
+
+/* Return DynStr's length */
+size_t
+DynStr_len( const DynStr *self );
+
+/* Return DynStr's content ptr */
+const char *
+DynStr_str( const DynStr *self );
+
+/* append C-string to DynStr */
+void
+DynStr_app( DynStr *self, const char *s );
+
+/* append a DynStr to DynStr */
+void
+DynStr_appDynStr( DynStr *self, const DynStr *s );
+
+/* Append C-string + newline to DynStr */
+void
+DynStr_appLn( DynStr *self, const char *s );
+
+/* Append a maximum of n characters from C-string s to DynStr self */
+void
+DynStr_appN( DynStr *self, const char *s, size_t n );
+
+/* Truncate content of DynString to zero length */
+void
+DynStr_clear( DynStr *self );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fetch.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,247 @@
+/*
+  fetch.c
+
+  $Id: fetch.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "fetch.h"
+#include <errno.h>
+#include <time.h>
+#include <signal.h>
+#include "client.h"
+#include "config.h"
+#include "content.h"
+#include "dynamicstring.h"
+#include "fetchlist.h"
+#include "request.h"
+#include "group.h"
+#include "log.h"
+#include "outgoing.h"
+#include "protocol.h"
+#include "pseudo.h"
+#include "util.h"
+
+struct Fetch
+{
+    Bool ready;
+    Str serv;
+} fetch = { FALSE, "" };
+
+static Bool
+connectToServ( const char *name )
+{
+    Log_inf( "Fetch from '%s'", name );
+    if ( ! Client_connect( name ) )
+    {
+        Log_err( "Could not connect to %s", name );
+        return FALSE;
+    }
+    return TRUE;
+}
+
+void
+Fetch_getNewGrps( void )
+{
+    time_t t;
+    Str file;
+
+    ASSERT( fetch.ready );
+    snprintf( file, MAXCHAR, "%s/groupinfo.lastupdate", Cfg_spoolDir() );
+    if ( ! Utl_getStamp( &t, file ) )
+    {
+        Log_err( "Cannot read %s. Please run noffle --query groups", file );
+        return;
+    }
+    Log_inf( "Updating groupinfo" );
+    Client_getNewgrps( &t );
+    Utl_stamp( file );
+}
+
+void
+Fetch_getNewArts( const char *name, FetchMode mode )
+{
+    int next, first, last, oldLast;
+
+    if ( ! Client_changeToGrp( name ) )
+    {
+        Log_err( "Could not change to group %s", name );
+        return;
+    }
+    Cont_read( name );
+    Client_rmtFirstLast( &first, &last );
+    next = Grp_rmtNext( name );
+    oldLast = Cont_last();
+    if ( next == last + 1 )
+    {
+        Log_inf( "No new articles in %s", name );
+        Cont_write();
+        Grp_setFirstLast( name, Cont_first(), Cont_last() );
+        return;
+    }
+    if ( first == 0 && last == 0 )
+    {
+        Log_inf( "No articles in %s", name );
+        Cont_write();
+        Grp_setFirstLast( name, Cont_first(), Cont_last() );
+        return;
+    }
+    if ( next > last + 1 )
+    {
+        Log_err( "Article number inconsistent (%s rmt=%lu-%lu, next=%lu)",
+                 name, first, last, next );
+        Pseudo_cntInconsistent( name, first, last, next );
+    }
+    else if ( next < first )
+    {
+        Log_inf( "Missing articles (%s first=%lu next=%lu)",
+                 name, first, next );
+        Pseudo_missArts( name, first, next );
+    }
+    else
+        first = next;
+    if ( last - first > Cfg_maxFetch() )
+    {
+        Log_ntc( "Cutting number of overviews to %lu", Cfg_maxFetch() );
+        first = last - Cfg_maxFetch() + 1;
+    }
+    Log_inf( "Getting remote overviews %lu-%lu for group %s",
+             first, last, name );
+    Client_getOver( first, last, mode );
+    Cont_write();
+    Grp_setFirstLast( name, Cont_first(), Cont_last() );
+}
+
+void
+Fetch_updateGrps( void )
+{
+    FetchMode mode;
+    int i, size;
+    const char* name;
+
+    ASSERT( fetch.ready );
+    Fetchlist_read();
+    size = Fetchlist_size();
+    for ( i = 0; i < size; ++i )
+    {
+        Fetchlist_element( &name, &mode, i );
+        if ( strcmp( Grp_serv( name ), fetch.serv ) == 0 )
+            Fetch_getNewArts( name, mode );
+    }
+}
+
+void
+Fetch_getReq_( void )
+{
+    Str msgId;
+    DynStr *list;
+    const char *p;
+    int count = 0;
+
+    ASSERT( fetch.ready );
+    Log_dbg( "Retrieving articles marked for download" );
+    list = new_DynStr( 10000 );
+    if ( Req_first( fetch.serv, msgId ) )
+        do
+        {
+            DynStr_appLn( list, msgId );
+            if ( ++count % 20 == 0 ) /* Send max. 20 ARTICLE cmds at once */
+            {
+                p = DynStr_str( list );
+                Client_retrieveArtList( p );
+                while ( ( p = Utl_getLn( msgId, p ) ) )
+                    Req_remove( fetch.serv, msgId );
+                DynStr_clear( list );
+            }
+        }
+        while ( Req_next( msgId ) );
+    p = DynStr_str( list );
+    Client_retrieveArtList( p );
+    while ( ( p = Utl_getLn( msgId, p ) ) )
+        Req_remove( fetch.serv, msgId );
+    del_DynStr( list );
+}
+
+void
+Fetch_postArts( void )
+{
+    DynStr *s;
+    Str msgId, cmd, errStr, sender;
+    int ret;
+    const char *txt;
+    FILE *f;
+    sig_t lastHandler;
+
+    s = new_DynStr( 10000 );
+    if ( Out_first( fetch.serv, msgId, s ) )
+    {
+        Log_inf( "Posting articles" );
+        do
+        {
+            txt = DynStr_str( s );
+            Out_remove( fetch.serv, msgId );
+            if ( ! Client_postArt( msgId, txt, errStr ) )
+            {
+                Utl_cpyStr( sender, Cfg_mailTo() );
+                if ( strcmp( sender, "" ) == 0
+                     && ! Prt_searchHeader( txt, "SENDER", sender )
+                     && ! Prt_searchHeader( txt, "X-NOFFLE-X-SENDER",
+                                            sender ) /* see server.c */
+                     && ! Prt_searchHeader( txt, "FROM", sender ) )
+                    Log_err( "Article %s has no From/Sender/X-Sender field",
+                             msgId );
+                else
+                {
+                    Log_ntc( "Return article to '%s' by mail", sender );
+                    snprintf( cmd, MAXCHAR,
+                              "mail -s '[ NOFFLE: Posting failed ]' '%s'",
+                              sender );
+                    lastHandler = signal( SIGPIPE, SIG_IGN );
+                    f = popen( cmd, "w" );
+                    if ( f == NULL )
+                        Log_err( "Invocation of '%s' failed (%s)", cmd,
+                                 strerror( errno ) );
+                    else
+                    {
+                        fprintf( f,
+                                 "\t[ NOFFLE: POSTING OF ARTICLE FAILED ]\n"
+                                 "\n"
+                                 "\t[ The posting of your article failed. ]\n"
+                                 "\t[ Reason of failure at remote server: ]\n"
+                                 "\n"
+                                 "\t[ %s ]\n"
+                                 "\n"
+                                 "\t[ Full article text has been appended. ]\n"
+                                 "\n"
+                                 "%s"
+                                 ".\n",
+                                 errStr, txt );
+                        ret = pclose( f );
+                        if ( ret != EXIT_SUCCESS )
+                            Log_err( "'%s' exit value %d", cmd, ret );
+                        signal( SIGPIPE, lastHandler );
+                    }
+                }
+            }
+        }
+        while ( Out_next( msgId, s ) );
+    }
+    del_DynStr( s );
+}
+
+Bool
+Fetch_init( const char *serv )
+{
+    if ( ! connectToServ( serv ) )
+        return FALSE;
+    Utl_cpyStr( fetch.serv, serv );
+    fetch.ready = TRUE;
+    return TRUE;
+}
+
+void
+Fetch_close()
+{
+    Client_disconnect();
+    fetch.ready = FALSE;
+    Log_inf( "Fetch from '%s' finished", fetch.serv );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fetch.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,38 @@
+/*
+  fetch.h
+
+  Do the daily business by using client.c
+
+  $Id: fetch.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef FETCH_H
+#define FETCH_H
+
+#include "common.h"
+#include "database.h"
+#include "fetchlist.h"
+
+Bool
+Fetch_init( const char *serv );
+
+void
+Fetch_close( void );
+
+void
+Fetch_getNewGrps( void );
+
+void
+Fetch_updateGrps( void );
+
+void
+Fetch_getReq_( void );
+
+void
+Fetch_postArts( void );
+
+/* Get new articles in group "grp", using fetch mode "mode". */
+void
+Fetch_getNewArts( const char *grp, FetchMode mode );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fetchlist.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,205 @@
+/*
+  fetchlist.c
+
+  $Id: fetchlist.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "fetchlist.h"
+#include "config.h"
+#include "log.h"
+#include "util.h"
+
+struct Elem
+{
+    Str name;
+    FetchMode mode;
+};
+
+static struct Fetchlist
+{
+    struct Elem *elem;
+    int size;
+    int max;
+} fetchlist = { NULL, 0, 0 };
+
+static const char *
+getFile( void )
+{
+    static Str file;
+    snprintf( file, MAXCHAR, "%s/fetchlist", Cfg_spoolDir() );
+    return file;
+}
+
+static void
+clearList( void )
+{
+    fetchlist.size = 0;
+}
+
+static int
+compareElem( const void *elem1, const void *elem2 )
+{
+    struct Elem* e1 = (struct Elem*)elem1; 
+    struct Elem* e2 = (struct Elem*)elem2;
+    return strcmp( e1->name, e2->name );
+}
+
+static struct Elem *
+searchElem( const char *name )
+{
+    int i;
+    
+    for ( i = 0; i < fetchlist.size; ++i )
+        if ( strcmp( name, fetchlist.elem[ i ].name ) == 0 )
+            return &fetchlist.elem[ i ];
+    return NULL;
+}
+
+static void
+appGrp( const char *name, FetchMode mode )
+{
+    struct Elem elem;
+
+    if ( fetchlist.max < fetchlist.size + 1 )
+    {
+        if ( ! ( fetchlist.elem
+                 = realloc( fetchlist.elem,
+                            ( fetchlist.max + 50 )
+                            * sizeof( fetchlist.elem[ 0 ] ) ) ) )
+        {
+            Log_err( "Could not realloc fetchlist" );
+            exit( EXIT_FAILURE );
+        }
+        fetchlist.max += 50;
+    }
+    strcpy( elem.name, name );
+    elem.mode = mode;
+    fetchlist.elem[ fetchlist.size++ ] = elem;
+}
+
+void
+Fetchlist_read( void )
+{
+    FILE *f;
+    const char *file = getFile();
+    char *p;
+    FetchMode mode = OVER;
+    Bool valid;
+    int ret;
+    Str line, grp, modeStr;
+
+    Log_dbg( "Reading %s", file );
+    clearList();
+    if ( ! ( f = fopen( file, "r" ) ) )
+    {
+        Log_inf( "No file %s", file );
+        return;
+    }
+    while ( fgets( line, MAXCHAR, f ) )
+    {
+        p = Utl_stripWhiteSpace( line );
+        if ( *p == '#' || *p == '\0' )
+            continue;
+        ret = sscanf( p, "%s %s", grp, modeStr );
+        valid = TRUE;
+        if ( ret < 1 || ret > 2 )
+            valid = FALSE;
+        else if ( ret >= 2 )
+        {
+            if ( strcmp( modeStr, "full" ) == 0 )
+                mode = FULL;
+            else if ( strcmp( modeStr, "thread" ) == 0 )
+                mode = THREAD;
+            else if ( strcmp( modeStr, "over" ) == 0 )
+                mode = OVER;
+            else
+                valid = FALSE;
+        }
+        if ( ! valid )
+        {
+            Log_err( "Invalid entry in %s: %s", file, line );
+            continue;
+        }
+        appGrp( grp, mode );
+    }
+    fclose( f );
+}
+
+Bool
+Fetchlist_write( void )
+{
+    int i;
+    FILE *f;
+    const char *file = getFile();
+    const char *modeStr = "";
+
+    qsort( fetchlist.elem, fetchlist.size, sizeof( fetchlist.elem[ 0 ] ),
+           compareElem );
+    if ( ! ( f = fopen( file, "w" ) ) )
+    {
+        Log_err( "Could not open %s for writing", file );
+        return FALSE;
+    }
+    for ( i = 0; i < fetchlist.size; ++i )
+    {
+        switch ( fetchlist.elem[ i ].mode )
+        {
+        case FULL:
+            modeStr = "full"; break;
+        case THREAD:
+            modeStr = "thread"; break;
+        case OVER:
+            modeStr = "over"; break;
+        }
+        fprintf( f, "%s %s\n", fetchlist.elem[ i ].name, modeStr );
+    }
+    fclose( f );
+    return TRUE;
+}
+
+int
+Fetchlist_size( void )
+{
+    return fetchlist.size;
+}
+
+Bool
+Fetchlist_contains( const char *name )
+{
+    return ( searchElem( name ) != NULL );
+}
+
+Bool
+Fetchlist_element( const char **name, FetchMode *mode, int index )
+{
+    if ( index < 0 || index >= fetchlist.size )
+        return FALSE;
+    *name = fetchlist.elem[ index ].name;
+    *mode = fetchlist.elem[ index ].mode;
+    return TRUE;
+}
+
+Bool
+Fetchlist_add( const char *name, FetchMode mode )
+{
+    struct Elem *elem = searchElem( name );
+    if ( elem == NULL )
+    {
+        appGrp( name, mode );
+        return TRUE;
+    }
+    strcpy( elem->name, name );
+    elem->mode = mode;
+    return FALSE;
+}
+
+Bool
+Fetchlist_remove( const char *name )
+{
+    struct Elem *elem = searchElem( name );
+    if ( elem == NULL )
+        return FALSE;
+    *elem = fetchlist.elem[ fetchlist.size - 1 ];
+    --fetchlist.size;
+    return TRUE;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fetchlist.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,42 @@
+/*
+  fetchlist.h
+
+  List of groups that are to be fetched presently.
+
+  $Id: fetchlist.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef FETCHLIST_H
+#define FETCHLIST_H
+
+#include "common.h"
+
+typedef enum { FULL, THREAD, OVER } FetchMode;
+
+void
+Fetchlist_read( void );
+
+/* Invalidates any indices (list is sorted by name before saving) */
+Bool
+Fetchlist_write( void );
+
+int
+Fetchlist_size( void );
+
+Bool
+Fetchlist_contains( const char *name );
+
+/* Get element number index. */
+Bool
+Fetchlist_element( const char **name, FetchMode *mode, int index );
+
+/* Add entry. Invalidates any indices. Returns TRUE if new entry, FALSE if
+   entry was overwritten. */
+Bool
+Fetchlist_add( const char *name, FetchMode mode );
+
+/* Remove entry. Invalidates any indices. Returns FALSE if not found. */
+Bool
+Fetchlist_remove( const char *name );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/group.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,307 @@
+/*
+  group.c
+
+  The group database resides in groupinfo.gdbm and stores all we know about
+  the groups we know of. One database record is cached in the global struct
+  grp. Group information is transfered between the grp and the database by
+  loadGrp() and saveGrp(). This is done transparently. Access to the groups
+  database is done by group name, by the functions defined in group.h.        
+
+  $Id: group.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "group.h"
+#include <gdbm.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include "config.h"
+#include "log.h"
+#include "util.h"
+
+/* currently only used within grp */
+typedef struct
+{
+  int first;             /* number of first article within group */
+  int last;              /* number of last article within group */
+  int rmtNext;
+  time_t created;
+  time_t lastAccess;
+} Entry;
+
+struct
+{
+  Str name;             /* name of the group */
+  Entry entry;          /* more information about this group */
+  Str serv;             /* server the group resides on */
+  Str dsc;              /* description of the group */
+  GDBM_FILE dbf;
+
+} grp = { "(no grp)", { 0, 0, 0, 0, 0 }, "", "", NULL };
+
+static const char *
+errMsg( void )
+{
+    if ( errno != 0 )
+        return strerror( errno );
+    return gdbm_strerror( gdbm_errno );
+}
+
+Bool
+Grp_open( void )
+{
+    Str name;
+    int flags;
+
+    ASSERT( grp.dbf == NULL );
+    snprintf( name, MAXCHAR, "%s/data/groupinfo.gdbm", Cfg_spoolDir() );
+    flags = GDBM_WRCREAT | GDBM_FAST;
+    if ( ! ( grp.dbf = gdbm_open( name, 512, flags, 0644, NULL ) ) )
+    {
+        Log_err( "Error opening %s for r/w (%s)", errMsg() );
+        return FALSE;
+    }
+    Log_dbg( "%s opened for r/w", name );
+    return TRUE;
+}
+
+void
+Grp_close( void )
+{
+    ASSERT( grp.dbf );
+    Log_dbg( "Closing groupinfo" );
+    gdbm_close( grp.dbf );
+    grp.dbf = NULL;
+}
+
+/* Load group info from gdbm-database into global struct grp */
+static Bool
+loadGrp( const char *name )
+{
+    const char *p;
+    datum key, val;
+
+    ASSERT( grp.dbf );
+    if ( strcmp( grp.name, name ) == 0 )
+         return TRUE;
+    key.dptr = (void *)name;
+    key.dsize = strlen( name ) + 1;
+    val = gdbm_fetch( grp.dbf, key );
+    if ( val.dptr == NULL )
+        return FALSE;
+    grp.entry = *( (Entry *)val.dptr );
+    p = val.dptr + sizeof( grp.entry );
+    Utl_cpyStr( grp.serv, p );
+    p += strlen( p ) + 1;
+    Utl_cpyStr( grp.dsc, p );
+    Utl_cpyStr( grp.name, name );
+    free( val.dptr );
+    return TRUE;
+}
+
+/* Save group info from global struct grp into gdbm-database */
+static void
+saveGrp( void )
+{
+    size_t lenServ, lenDsc, bufLen;
+    datum key, val;
+    void *buf;
+    char *p;
+
+    ASSERT( grp.dbf );
+    lenServ = strlen( grp.serv );
+    lenDsc = strlen( grp.dsc );
+    bufLen = sizeof( grp.entry ) + lenServ + lenDsc + 2;
+    buf = malloc( bufLen );
+    memcpy( buf, (void *)&grp.entry, sizeof( grp.entry ) );
+    p = (char *)buf + sizeof( grp.entry );
+    strcpy( p, grp.serv );
+    p += lenServ + 1;
+    strcpy( p, grp.dsc );
+    key.dptr = (void *)grp.name;
+    key.dsize = strlen( grp.name ) + 1;
+    val.dptr = buf;
+    val.dsize = bufLen;
+    if ( gdbm_store( grp.dbf, key, val, GDBM_REPLACE ) != 0 )
+        Log_err( "Could not save group %s: %s", errMsg() );
+    free( buf );
+}
+
+Bool
+Grp_exists( const char *name )
+{
+    datum key;
+
+    ASSERT( grp.dbf );
+    key.dptr = (void*)name;
+    key.dsize = strlen( name ) + 1;
+    return gdbm_exists( grp.dbf, key );
+}
+
+void
+Grp_create( const char *name )
+{
+    Utl_cpyStr( grp.name, name );
+    Utl_cpyStr( grp.serv, "(unknown)" );
+    grp.dsc[ 0 ] = '\0';
+    grp.entry.first = 0;
+    grp.entry.last = 0;
+    grp.entry.rmtNext = 0;
+    grp.entry.created = 0;
+    grp.entry.lastAccess = 0;
+    saveGrp();
+}
+
+const char *
+Grp_dsc( const char *name )
+{
+    if ( ! loadGrp( name ) )
+        return NULL;
+    return grp.dsc;
+}
+
+const char *
+Grp_serv( const char *name )
+{
+    static Str serv = "";
+
+    if ( ! loadGrp( name ) )
+        return "[unknown grp]";
+    if ( Cfg_servListContains( grp.serv ) )
+        Utl_cpyStr( serv, grp.serv );
+    else
+        snprintf( serv, MAXCHAR, "[%s]", grp.serv );
+    return serv;
+}
+
+int
+Grp_first( const char *name )
+{
+    if ( ! loadGrp( name ) )
+        return 0;
+    return grp.entry.first;
+}
+
+int
+Grp_last( const char *name )
+{
+    if ( ! loadGrp( name ) )
+        return 0;
+    return grp.entry.last;
+}
+
+int
+Grp_lastAccess( const char *name )
+{
+    if ( ! loadGrp( name ) )
+        return 0;
+    return grp.entry.lastAccess;
+}
+
+int
+Grp_rmtNext( const char *name )
+{
+    if ( ! loadGrp( name ) )
+        return 0;
+    return grp.entry.rmtNext;
+}
+
+time_t
+Grp_created( const char *name )
+{
+    if ( ! loadGrp( name ) )
+        return 0;
+    return grp.entry.created;
+}
+
+/* Replace group's description (only if value != ""). */
+void
+Grp_setDsc( const char *name, const char *value )
+{
+    if ( loadGrp( name ) )
+    {
+        Utl_cpyStr( grp.dsc, value );
+        saveGrp();
+    }
+}
+
+void
+Grp_setServ( const char *name, const char *value )
+{
+    if ( loadGrp( name ) )
+    {
+        Utl_cpyStr( grp.serv, value );
+        saveGrp();
+    }
+}
+
+void
+Grp_setCreated( const char *name, time_t value )
+{
+    if ( loadGrp( name ) )
+    {
+        grp.entry.created = value;
+        saveGrp();
+    }
+}
+
+void
+Grp_setRmtNext( const char *name, int value )
+{
+    if ( loadGrp( name ) )
+    {
+        grp.entry.rmtNext = value;
+        saveGrp();
+    }
+}
+
+void
+Grp_setLastAccess( const char *name, int value )
+{
+    if ( loadGrp( name ) )
+    {
+        grp.entry.lastAccess = value;
+        saveGrp();
+    }
+}
+
+void
+Grp_setFirstLast( const char *name, int first, int last )
+{
+    if ( loadGrp( name ) )
+    {
+        grp.entry.first = first;
+        grp.entry.last = last;
+        saveGrp();
+    }
+}
+
+static datum cursor = { NULL, 0 };
+
+Bool
+Grp_firstGrp( const char **name )
+{
+    ASSERT( grp.dbf );
+    if ( cursor.dptr != NULL )
+    {
+        free( cursor.dptr );
+        cursor.dptr = NULL;
+    }
+    cursor = gdbm_firstkey( grp.dbf );
+    *name = cursor.dptr;
+    return ( cursor.dptr != NULL );
+}
+
+Bool
+Grp_nextGrp( const char **name )
+{
+    void *oldDptr = cursor.dptr;
+
+    ASSERT( grp.dbf );
+    if ( cursor.dptr == NULL )
+        return FALSE;
+    cursor = gdbm_nextkey( grp.dbf, cursor );
+    free( oldDptr );
+    *name = cursor.dptr;
+    return ( cursor.dptr != NULL );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/group.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,95 @@
+/*
+  group.h
+
+  Groups database
+
+  $Id: group.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef GRP_H
+#define GRP_H
+
+#include <time.h>
+#include "common.h"
+
+/* open group database */
+Bool
+Grp_open( void );
+
+/* close group database */
+void
+Grp_close( void );
+
+/* does group exist? */
+Bool
+Grp_exists( const char *name );
+
+/* create new group and save it in database */
+void
+Grp_create( const char *name );
+
+/* Get group description */
+const char *
+Grp_dsc( const char *name );
+
+/* Get server the group resides on */
+const char *
+Grp_serv( const char *name );
+
+/*
+  Get article number of the first article in the group
+  This number is a hint only, it is independent of the
+  real articles in content.c
+*/
+int
+Grp_first( const char *name );
+
+/*
+  Get article number of the last article in the group
+  This number is a hint only, it is independent of the
+  real articles in content.c
+*/
+int
+Grp_last( const char *name );
+
+int
+Grp_lastAccess( const char *name );
+
+int
+Grp_rmtNext( const char *name );
+
+time_t
+Grp_created( const char *name );
+
+/* Replace group's description (only if value != ""). */
+void
+Grp_setDsc( const char *name, const char *value );
+
+void
+Grp_setServ( const char *name, const char *value );
+
+void
+Grp_setCreated( const char *name, time_t value );
+
+void
+Grp_setRmtNext( const char *name, int value );
+
+void
+Grp_setLastAccess( const char *name, int value );
+
+void
+Grp_setFirstLast( const char *name, int first, int last );
+
+/* Begin iterating trough the names of all groups. Store name of first
+   group (or NULL if there aren't any) in name. Returns whether there are
+   any groups. */
+Bool
+Grp_firstGrp( const char **name );
+
+/* Continue iterating trough the names of all groups. Store name of next
+   group (or NULL if there aren't any more) in name. Returns TRUE on
+   success, FALSE when there are no more groups. */
+Bool
+Grp_nextGrp( const char **name );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lock.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,126 @@
+/*
+  lock.c
+
+  $Id: lock.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "lock.h"
+#include <errno.h>
+#include <ctype.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <time.h>
+#include <unistd.h>
+#include "config.h"
+#include "log.h"
+#include "database.h"
+#include "group.h"
+#include "request.h"
+
+struct Lock
+{
+    int lockFd;
+    Str lockFile;
+} lock = { -1, "" };
+
+
+#ifdef DEBUG
+static Bool
+testLock( void )
+{
+    return ( lock.lockFd != -1 );    
+}
+#endif
+
+static Bool
+waitLock( void )
+{
+    int fd;
+    struct flock l;
+
+    ASSERT( ! testLock() );
+    Log_dbg( "Waiting for lock ..." );
+    snprintf( lock.lockFile, MAXCHAR, "%s/lock/global", Cfg_spoolDir() );
+    if ( ( fd = open( lock.lockFile, O_WRONLY | O_CREAT, 0644 ) ) < 0 )
+    {
+        Log_err( "Cannot open %s (%s)", lock.lockFile, strerror( errno ) );
+        return FALSE;
+    }
+    l.l_type = F_WRLCK;
+    l.l_start = 0;
+    l.l_whence = SEEK_SET;
+    l.l_len = 0;
+    if ( fcntl( fd, F_SETLKW, &l ) < 0 )
+    {
+        Log_err( "Cannot lock %s: %s", lock.lockFile, strerror( errno ) );
+        return FALSE;
+    }
+    lock.lockFd = fd;
+    Log_dbg( "Lock successful" );
+    return TRUE;
+}
+
+static void
+releaseLock( void )
+{
+    struct flock l;
+
+    ASSERT( testLock() );    
+    l.l_type = F_UNLCK;
+    l.l_start = 0;
+    l.l_whence = SEEK_SET;
+    l.l_len = 0;
+    if ( fcntl( lock.lockFd, F_SETLK, &l ) < 0 )
+        Log_err( "Cannot release %s: %s", lock.lockFile,
+                 strerror( errno ) );
+    close( lock.lockFd );
+    lock.lockFd = -1;
+    Log_dbg( "Releasing lock" );
+}
+
+
+/* Open all databases and set global lock. */
+Bool
+Lock_openDatabases( void )
+{
+  if ( ! waitLock() )
+    {
+      Log_err( "Could not get write lock" );
+      return FALSE;
+    }
+  if ( ! Db_open() )
+    {
+      Log_err( "Could not open database" );
+      releaseLock();
+      return FALSE;
+    }
+  if ( ! Grp_open() )
+    {
+      Log_err( "Could not open groupinfo" );
+      Db_close();
+      releaseLock();
+      return FALSE;
+    }
+  if ( ! Req_open() )
+    {
+      Log_err( "Could not initialize request database" );
+      Grp_close();
+      Db_close();
+      releaseLock();
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+
+/* Close all databases and release global lock. */
+void
+Lock_closeDatabases( void )
+{
+  Grp_close();
+  Db_close();
+  Req_close();
+  releaseLock();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lock.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,24 @@
+/*
+  lock.h
+
+  Opening/Closing of the various databases: article overview database,
+  articla database, groups database, outgoing articles database, requests
+  database. Handles global lock.
+
+  $Id: lock.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef LOCK_H
+#define LOCK_H
+
+#include "common.h"
+
+/* Open all databases and set global lock. */
+Bool
+Lock_openDatabases( void );
+
+/* Close all databases and release global lock. */
+void
+Lock_closeDatabases( void );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/log.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,75 @@
+/*
+  log.c
+
+  $Id: log.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include <syslog.h>
+#include <stdarg.h>
+#include "common.h"
+
+#define MAXLENGTH 240
+
+struct
+{
+    Bool interactive;
+} log = { FALSE };
+
+void
+Log_init( Str name, Bool interactive, int facility )
+{
+    int option = LOG_PID | LOG_CONS;
+
+    log.interactive = interactive;
+    openlog( name, option, facility );
+}
+
+#define DO_LOG( LEVEL )               \
+    va_list ap;                       \
+    Str t;                            \
+                                      \
+    va_start( ap, fmt );              \
+    vsnprintf( t, MAXCHAR, fmt, ap ); \
+    if ( MAXLENGTH < MAXCHAR )        \
+        t[ MAXLENGTH ] = '\0';        \
+    syslog( LEVEL, "%s", t );         \
+    if ( log.interactive )            \
+        fprintf( stderr, "%s\n", t );   \
+    va_end( ap );
+
+void
+Log_inf( const char *fmt, ... )
+{
+    DO_LOG( LOG_INFO );
+}
+
+void
+Log_err( const char *fmt, ... )
+{
+    DO_LOG( LOG_ERR );
+}
+
+/* Ensure the condition "cond" is true; otherwise log an error and return 1 */
+int 
+Log_check(int cond, const char *fmt, ... )
+{
+  if (!cond) {
+    DO_LOG( LOG_ERR );
+    return 1;
+  }
+  return 0;
+}
+
+void
+Log_ntc( const char *fmt, ... )
+{
+    DO_LOG( LOG_NOTICE );
+}
+
+void
+Log_dbg( const char *fmt, ... )
+{
+#ifdef DEBUG
+    DO_LOG( LOG_DEBUG );
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/log.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,43 @@
+/*
+  log.h
+
+  Print log messages to syslog, stdout/stderr.
+
+  $Id: log.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef LOG_H
+#define LOG_H
+
+#include "common.h"
+
+/*
+  Initialise logging (required before using any log functions).
+  name: program name for syslog
+  interactive: print messages also to stderr/stdout
+  facility: like syslog
+*/
+void
+Log_init( Str name, Bool interactive, int facility );
+
+/* Log level info */
+void
+Log_inf( const char *fmt, ... );
+
+/* Log level error */
+void
+Log_err( const char *fmt, ... );
+
+/* Check for cond being true. Otherwise log an error, and return 1. */
+int 
+Log_check(int cond, const char *fmt, ... );
+
+/* Log level notice */
+void
+Log_ntc( const char *fmt, ... );
+
+/* Log only if DEBUG is defined. */
+void
+Log_dbg( const char *fmt, ... );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/make-check	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+if grep '^CFLAGS.*DEBUG' Makefile ; then
+	echo "Debugging options are on in Makefile"
+	exit -1
+fi
+
+if grep // *.[ch] ; then
+	echo "Failed: Source contains C++ style comments"
+	exit -1
+fi
+
+if grep strncpy *.[ch] ; then
+	echo "strncpy may result in unterminated strings."
+	echo "Use Util_copyString"
+	exit -1
+fi
+
+if grep XXX *.[ch] ; then
+	echo "Source contains XXX marker (personnally used)"
+	exit -1
+fi
+
+if grep -i "since version" CHANGELOG.html ; then
+	echo "Warning: CHANGELOG.html should mention new version"
+	echo "Continue anyway? (y/n)"
+	read a
+	if test "$a" != "j" -a "$a" != "J"; then exit -1; fi
+fi
+
+exit 0
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/make-distribution	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,47 @@
+#!/usr/bin/bash
+
+if ! make-check ; then
+    echo "make-check failed"
+    exit -1
+fi
+
+FILESH="client.h common.h config.h content.h database.h dynamicstring.h  \
+    fetch.h fetchlist.h group.h lock.h \
+    log.h online.h outgoing.h over.h protocol.h pseudo.h \
+    request.h server.h util.h"
+
+FILESC="fetch.c client.c config.c content.c database.c \
+    dynamicstring.c fetchlist.c \
+    group.c lock.c log.c noffle.c online.c outgoing.c over.c \
+    protocol.c pseudo.c request.c server.c util.c"
+
+FILESDOC="README NOTES CHANGELOG COPYING INSTALL FAQ"
+
+FILES="$FILESH $FILESC noffle.conf.example Makefile noffle.1 noffle.conf.5"
+
+echo
+echo !!! WARNING !!!
+echo
+echo "You are creating a distribution now."
+echo "Are the compiler settings in the Makefile for distribution?"
+echo "Files will be tagged in CVS (with version, but '.' replaced by '_')"
+echo "Input the version (CTRL-C to abort):"
+read VERSION
+TAG=`echo "dist_$VERSION" | tr "." "_"`
+rm -rf noffle-$VERSION
+DIR=noffle-$VERSION
+mkdir $DIR \
+&& cp $FILES $DIR \
+&& ( for a in $FILESDOC; do echo Creating $a.txt; lynx -dump -nolist \
+     $a.html >$DIR/$a.txt || exit -1; done ) \
+&& sed 's/^VERSION *= *[^ ]*/VERSION = '$VERSION'/' <Makefile >$DIR/Makefile \
+&& cp $DIR/Makefile Makefile \
+&& tar cf noffle-$VERSION.tar $DIR \
+&& gzip -9 noffle-$VERSION.tar \
+&& cvs tag -d dist_test \
+&& cvs ci -m "Makefile for version $VERSION" Makefile \
+&& cvs tag $TAG
+echo Do not forget to change VERSION in Makefile to experimental if
+echo you plan to modify the sources from now on.
+echo
+echo Please try to compile this version to ensure that no file is missing.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/noffle.1	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,363 @@
+
+.TH noffle 1
+.\" $Id: noffle.1 3 2000-01-04 11:35:42Z enz $
+.SH NAME
+noffle \- Usenet package optimized for dialup connections.
+
+.SH SYNOPSIS
+
+.B noffle
+\-a | \-\-article <message id>|all
+.br
+.B noffle
+\-d | \-\-database
+.br
+.B noffle
+\-e | \-\-expire <days>
+.br
+.B noffle
+\-f | \-\-fetch
+.br
+.B noffle
+\-g | \-\-groups
+.br
+.B noffle
+\-h | \-\-help
+.br
+.B noffle
+\-l | \-\-list
+.br
+.B noffle
+\-n | \-\-online
+.br
+.B noffle
+\-o | \-\-offline
+.br
+.B noffle
+\-q | \-\-query groups|desc|times
+.br
+.B noffle
+\-R | \-\-requested
+.br
+.B noffle
+\-r | \-\-server
+.br
+.B noffle
+\-s | \-\-subscribe-over <group>
+.br
+.B noffle
+\-S | \-\-subscribe-full <group>
+.br
+.B noffle
+\-t | \-\-subscribe-thread <group>
+.br
+.B noffle
+\-u | \-\-unsubscribe <group>
+
+.SH DESCRIPTION
+
+.B NOFFLE
+is an Usenet package optimized for low speed dialup Internet connections
+and few users.
+The
+.B noffle
+program is used for running and steering of the proxy news server,
+for retrieving new articles from the remote server and for
+expiring old articles from the database.
+.B NOFFLE
+can fetch newsgroups in one of the following modes:
+.TP
+.B full
+fetch full articles,
+.TP
+.B over
+fetch only article overviews by default. Opening an article marks it
+for download next time online,
+.TP
+.B thread
+like
+.B over,
+but download articles full if an article of the same thread already has
+been downloaded.
+
+.SH OPTIONS
+
+.TP
+.B \-a, \-\-article <message id>|all
+Write article <message id> to standard output. Message Id must contain
+the leading '<' and trailing '>' (quote the argument to avoid shell
+interpretation of '<' and '>').
+.br
+If "all" is given as message Id, all articles are shown. 
+
+.TP
+.B \-d, \-\-database
+Write the complete content of the article database to standard output.
+
+.TP
+.B \-e, \-\-expire <days>
+Delete all articles older than <days> days from the database.
+Should be run regularily from
+.BR crond (8).
+
+.TP
+.B \-f, \-\-fetch
+Get new newsfeed from the remote server.
+Updates the list of the existing newsgroups,
+fetches new articles overviews or full articles from subscribed
+groups (see
+.B fetchlist
+),
+delivers all posted articles to the remote server,
+and retrieves all articles marked for download.
+.B noffle --fetch
+should be run in the
+.B ip-up
+script of
+.BR pppd (8).
+
+.TP
+.B \-g, \-\-groups
+List all groups available at remote server.
+.br
+Format (fields separated by tabs):
+.br
+<name> <server> <first> <last> <remote next> <created> <last access> <desc>
+
+.TP
+.B \-h, \-\-help
+Print a list of all options.
+
+.TP
+.B \-l, \-\-list
+List all groups that are presently to be fetched and their fetch mode.
+.br
+Format: <groupname> <server> full|thread|over
+
+.TP
+.B \-n, \-\-online
+Put
+.B NOFFLE
+to online status. Requested articles or overviews of selected
+groups are immediately fetched from the remote server.
+Should be run in the
+.B ip-up
+script of
+.BR pppd (8).
+
+.TP
+.B \-o, \-\-offline
+Put
+.B NOFFLE
+to offline status. Requested articles not already in the
+database are marked for download.
+Should be run in the
+.B ip-down
+script of
+.BR pppd (8).
+
+.TP
+.B \-q, \-\-query groups|desc|times
+Query information about all groups from the remote server and merge it to
+the
+.B groupinfo
+file. This must be run after installing 
+.B noffle
+or sometimes after a change of the remote news server or corruption
+of the file. It can take some time on slow connections.
+.B groups
+retrieves the list of the existing newsgroups
+(resets all local article counters),
+.B desc
+retrieves all newsgroup descriptions,
+.B times
+retrieves the creation times of the newsgroups.
+
+.TP
+.B \-r, \-\-server
+Run as NNTP server on standard input and standard output. This is
+intended to be called by
+.BR inetd (8)
+and should be registered in
+.B /etc/inetd.conf.
+Error and logging messages are put to the
+.BR syslogd (8)
+daemon which is usually configured to write them to
+.B /var/log/news.
+A list of the NNTP commands that are understood by
+.B noffle
+can be retrieved by running the server and typing
+.B HELP.
+
+.TP
+.B \-R, \-\-requested
+List articles that are marked for download.
+
+Format: <message-id> <server>
+
+.TP
+.B \-s, \-\-subscribe-over <group>
+Add group with name <group> to list of groups that are presently to be fetched
+(in over mode).
+
+.TP
+.B \-S, \-\-subscribe-full <group>
+Add group with name <group> to list of groups that are presently to be fetched
+(in full mode).
+
+.TP
+.B \-t, \-\-subscribe-thread <group>
+Add group with name <group> to list of groups that are presently to be fetched
+(in thread mode).
+
+.TP
+.B \-u, \-\-unsubscribe <group>
+Remove group with name <group> from list of groups that are presently to
+be fetched.
+
+.SH FILES
+
+There exists a spool directory (default
+.I /var/spool/news),
+and a config file (default
+.I /etc/noffle.conf).
+
+.PP
+
+.TP
+.B <config file>
+Configuration file. Comment lines begin with
+.I #.
+Definition lines may contain:
+.br
+.B server <hostname>[:<port>] [<user> <pass>]
+Name of the remote server. If no port given, port 119 is used.
+Username and password for servers that need authentication
+(Original AUTHINFO). The password may not contain white-spaces.
+If there are multiple server entries in the config file, all of them are
+used for getting groups. In this case the first server should be
+the one of your main provider. Note that you must always run
+"noffle --query groups" after making changes to the server entries.
+.br
+.B max-fetch <n>
+Never get more than <n> articles. If there are more, the oldest ones
+are discarded.
+.br
+Default: 300
+.br
+.B mail-to <address>
+Receiver of failed postings. If empty then failed postings are returned
+to the sender (taking the address from the article's Sender, X-Sender or
+From field, in this order).
+.br
+Default: <empty string>
+.br
+.B auto-unsubscribe yes|no
+Automatically remove groups from fetch list if they have not been
+accessed for a number days.
+.br
+Default: no
+.br
+.B auto-unsubscribe-days <n>
+Number of days used for auto-unsubscribe option.
+.br
+Default: 30
+.br
+.B thread-follow-time <n>
+Automatically mark articles for download in thread mode, if they
+are referencing an article that has been opened by a reader within the last
+<n> days.
+.br
+.B connect-timeout <n>
+Timeout for connecting to remote server in seconds.
+.br
+Default: 30
+.br
+.B auto-subscribe yes|no
+Automatically put groups on fetch list if someone reads them.
+<mode> can be full, over, thread (depending on the fetch mode) or
+off (do not subscribe automatically). Condition for putting a group
+on the list is that an article is opened. For this reason there is
+always a pseudo article visible in groups that are not on the fetch list.
+.br
+Default: no
+.br
+.B auto-subscribe-mode full|thread|over
+Mode for auto-subscribe option.
+.br
+Default: over
+.br
+.B remove-messageid yes|no
+Remove Message-ID from posted articles. Some remote servers can generate
+Message-IDs.
+.br
+Default: no
+.br
+.B replace-messageid yes|no
+Replace Message-ID of posted articles by a Message-ID generated by
+NOFFLE. Some news readers generate Message-IDs that are not accepted by
+some servers. For generating Message-IDs, the domain name of your system should
+be a valid domain name. If you are in a local domain, set it to your
+provider's domain name.
+.br
+Default: yes
+.br
+
+.TP
+.B <spool dir>/fetchlist
+List of newsgroups that are presently to be fetched.
+.br
+
+.TP
+.B <spool dir>/data/groupinfo.gdbm
+Database with groups in
+.BR gdbm(3)
+format.
+
+.TP
+.B <spool dir>/data/articles.gdbm
+Database with articles in
+.BR gdbm(3)
+format.
+
+.TP
+.B <spool dir>/lock/
+Lock files and files indicating online/offline status.
+
+.TP
+.B <spool dir>/outgoing/
+Posted articles to be delivered to the remote server.
+
+.TP
+.B <spool dir>/overview/
+Text file per group with article overviews.
+
+.TP
+.B <spool dir>/requested/
+Message IDs of articles marked for download.
+
+
+.SH SEE ALSO
+
+.BR crond (8)
+.BR inetd (8),
+.BR pppd (8),
+.br
+.B RFC 977,
+.B RFC 1036,
+.br
+.B IETF drafts on common NNTP extensions:
+.br
+.B http://www.karlsruhe.org/
+.br
+.B NOFFLE home page:
+.br
+.B http://home.t-online.de/home/markus.enzenberger/noffle.html
+
+.SH AUTHORS
+
+Markus Enzenberger <markus.enzenberger@t-online.de>
+.br
+Volker Wysk <volker.wysk@student.uni-tuebingen.de>
+
+1998-1999.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/noffle.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,607 @@
+/*
+  noffle.c
+
+  Main program. Implements specified actions, but running as server, which
+  is done by Serv_run(), declared in server.h.
+  
+  Locking policy: lock access to databases while noffle is running, but
+  not as server. If noffle runs as server, locking is performed while
+  executing NNTP commands, but temporarily released if no new command is
+  received for some seconds (to allow multiple clients connect at the same
+  time).
+
+  $Id: noffle.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include <errno.h>
+#include <getopt.h>
+#include <signal.h>
+#include <sys/resource.h>
+#include <syslog.h>
+#include <unistd.h>
+#include "client.h"
+#include "common.h"
+#include "content.h"
+#include "config.h"
+#include "database.h"
+#include "fetch.h"
+#include "fetchlist.h"
+#include "group.h"
+#include "log.h"
+#include "online.h"
+#include "over.h"
+#include "pseudo.h"
+#include "util.h"
+#include "server.h"
+#include "request.h"
+#include "lock.h"
+
+struct Noffle
+{
+    Bool queryGrps;
+    Bool queryDsc;
+    Bool queryTimes;
+    Bool interactive;
+} noffle = { FALSE, FALSE, FALSE, TRUE };
+
+static void
+doArt( const char *msgId )
+{
+    const char *id;
+
+    if ( strcmp( msgId, "all" ) == 0 )
+    {
+        if ( ! Db_first( &id ) )
+            fprintf( stderr, "Database empty.\n" );
+        else
+            do
+            {
+                printf( "%s\n%s"
+                        "========================================"
+                        "======================================\n",
+                        Db_header( id ), Db_body( id ) );
+            }
+            while ( Db_next( &id ) );
+    }
+    else
+    {
+        if ( ! Db_contains( msgId ) )
+            fprintf( stderr, "Not in database.\n" );
+        else
+            printf( "%s\n%s", Db_header( msgId ), Db_body( msgId ) );
+    }
+}
+
+/* List articles requested from one particular server */
+static void
+listRequested1( const char* serv )
+{
+  Str msgid;
+
+  if ( ! Req_first( serv, msgid ) )
+      return;
+  do
+      printf( "%s %s\n", msgid, serv );
+  while ( Req_next( msgid ) );
+}
+
+/* List requested articles. List for all servers if serv = "all" or serv =
+   NULL. */
+void
+doRequested( const char *arg )
+{
+    Str serv;
+    
+    if ( ! arg || ! strcmp( arg, "all" ) )
+    {
+        Cfg_beginServEnum();   
+        while ( Cfg_nextServ( serv ) )
+            listRequested1( serv );
+    }   
+    else
+        listRequested1( arg );
+}
+
+
+static void
+doDb( void )
+{
+    const char *msgId;
+
+    if ( ! Db_first( &msgId ) )
+        fprintf( stderr, "Database empty.\n" );
+    else
+        do
+            printf( "%s\n", msgId );
+        while ( Db_next( &msgId ) );
+}
+
+static void
+doFetch( void )
+{
+    Str serv;
+
+    Cfg_beginServEnum();
+    while ( Cfg_nextServ( serv ) )
+        if ( Fetch_init( serv ) )
+        {
+            Fetch_postArts();
+
+            Fetch_getNewGrps();
+
+            /* Get overviews of new articles and store IDs of new articles
+               that are to be fetched becase of FULL or THREAD mode in the
+               request database. */
+            Fetch_updateGrps();         
+
+            /* get requested articles */
+            Fetch_getReq_();
+
+            Fetch_close();
+        }
+}
+
+static void
+doQuery( void )
+{
+    Str serv;
+
+    Cfg_beginServEnum();
+    while ( Cfg_nextServ( serv ) )
+        if ( Fetch_init( serv ) )
+        {
+            if ( noffle.queryGrps )
+                Client_getGrps();
+            if ( noffle.queryDsc )
+                Client_getDsc();
+            if ( noffle.queryTimes )
+                Client_getCreationTimes();
+            Fetch_close();
+        }
+}
+
+/* Expire all overviews not in database */
+static void
+expireContents( void )
+{
+    const Over *ov;
+    int i;
+    int cntDel, cntLeft;
+    Str grp;
+    Bool autoUnsubscribe;
+    int autoUnsubscribeDays;
+    time_t now = time( NULL ), maxAge = 0;
+    const char *msgId;
+
+    autoUnsubscribe = Cfg_autoUnsubscribe();
+    autoUnsubscribeDays = Cfg_autoUnsubscribeDays();
+    maxAge = Cfg_autoUnsubscribeDays() * 24 * 3600;
+    if ( ! Cont_firstGrp( grp ) )
+        return;
+    Log_inf( "Expiring overviews not in database" );
+    do
+    {
+        if ( ! Grp_exists( grp ) )
+            Log_err( "Overview file for unknown group %s exists", grp );
+        else
+        {
+            cntDel = cntLeft = 0;
+            Cont_read( grp );
+            for ( i = Cont_first(); i <= Cont_last(); ++i )
+                if ( ( ov = Cont_get( i ) ) )
+                {
+                    msgId = Ov_msgId( ov );
+                    if ( ! Db_contains( msgId ) )
+                    {
+                        Cont_delete( i );
+                        ++cntDel;
+                    }
+                    else
+                        ++cntLeft;
+                }
+            if ( autoUnsubscribe
+                 && difftime( now, Grp_lastAccess( grp ) ) > maxAge )
+            {
+                Log_ntc( "Auto-unsubscribing from %s after %d "
+                         "days without access",
+                         grp, autoUnsubscribeDays );
+                Pseudo_autoUnsubscribed( grp, autoUnsubscribeDays );
+                Fetchlist_read();
+                Fetchlist_remove( grp );
+                Fetchlist_write();
+            }
+            Cont_write();
+            Grp_setFirstLast( grp, Cont_first(), Cont_last() );
+            Log_inf( "%ld overviews deleted from group %s, %ld left (%ld-%ld)",
+                     cntDel, grp, cntLeft, Grp_first( grp ), Grp_last( grp ) );
+        }
+    }
+    while ( Cont_nextGrp( grp ) );
+}
+
+static void
+doExpire( unsigned int days )
+{
+    Db_close();
+    Db_expire( days );
+    if ( ! Db_open() )
+        return;
+    expireContents();
+}
+
+static void
+doList( void )
+{
+    FetchMode mode;
+    int i, size;
+    const char *name, *modeStr = "";
+
+    Fetchlist_read();
+    size = Fetchlist_size();
+    if ( size == 0 )
+        fprintf( stderr, "Fetch list is empty.\n" );
+    else
+        for ( i = 0; i < size; ++i )
+        {
+            Fetchlist_element( &name, &mode, i );
+            switch ( mode )
+            {
+            case FULL:
+                modeStr = "full"; break;
+            case THREAD:
+                modeStr = "thread"; break;
+            case OVER:
+                modeStr = "over"; break;
+            }
+            printf( "%s %s %s\n", name, Grp_serv( name ), modeStr );
+        }
+}
+
+static void
+doGrps( void )
+{
+    const char *g;
+    Str dateLastAccess, dateCreated;
+    time_t lastAccess, created;
+
+    if ( Grp_firstGrp( &g ) )
+        do
+        {
+            lastAccess = Grp_lastAccess( g );
+            created = Grp_created( g );
+            ASSERT( lastAccess >= 0 );
+            ASSERT( created >= 0 );
+            strftime( dateLastAccess, MAXCHAR, "%Y-%m-%d %H:%M:%S",
+                      localtime( &lastAccess ) );
+            strftime( dateCreated, MAXCHAR, "%Y-%m-%d %H:%M:%S",
+                      localtime( &created ) );
+            printf( "%s\t%s\t%i\t%i\t%i\t%s\t%s\t%s\n",
+                    g, Grp_serv( g ), Grp_first( g ), Grp_last( g ),
+                    Grp_rmtNext( g ), dateCreated,
+                    dateLastAccess, Grp_dsc( g ) );
+        }
+        while ( Grp_nextGrp( &g ) );
+}
+
+static Bool
+doSubscribe( const char *name, FetchMode mode )
+{
+    if ( ! Grp_exists( name ) )
+    {
+        fprintf( stderr, "%s is not available at rmt server.\n", name );
+        return FALSE;
+    }
+    Fetchlist_read();
+    if ( Fetchlist_add( name, mode ) )
+        printf( "Adding %s to fetch list in %s mode.\n",
+                name, mode == FULL ? "full" : mode == THREAD ?
+                "thread" : "overview" );
+    else
+        printf( "%s is already in fetch list. Mode is now: %s.\n",
+                name, mode == FULL ? "full" : mode == THREAD ?
+                "thread" : "overview" );
+    if ( ! Fetchlist_write() )
+        fprintf( stderr, "Could not save fetchlist.\n" );
+    return TRUE;
+}
+
+static void
+doUnsubscribe( const char *name )
+{
+    Fetchlist_read();
+    if ( ! Fetchlist_remove( name ) )
+        printf( "%s is not in fetch list.\n", name );
+    else
+        printf( "%s removed from fetch list.\n", name );
+    if ( ! Fetchlist_write() )
+        fprintf( stderr, "Could not save fetchlist.\n" );
+}
+
+static void
+printUsage( void )
+{
+    static const char *msg =
+      "Usage: noffle <option>\n"
+      "Option is one of the following:\n"
+      " -a | --article <msg id>|all   Show article(s) in database\n"
+      " -d | --database               Show content of article database\n"
+      " -e | --expire <n>             Expire articles older than <n> days\n"
+      " -f | --fetch                  Get newsfeed from server/post articles\n"
+      " -g | --groups                 Show all groups available at server\n"
+      " -h | --help                   Show this text\n"
+      " -l | --list                   List groups on fetch list\n"
+      " -n | --online                 Switch to online mode\n"
+      " -o | --offline                Switch to offline mode\n"
+      " -q | --query groups           Get group list from server\n"
+      " -q | --query desc             Get group descriptions from server\n"
+      " -q | --query times            Get group creation times from server\n"
+      " -r | --server                 Run as server on stdin/stdout\n"
+      " -R | --requested              List articles marked for download\n"
+      " -s | --subscribe-over <grp>   Add group to fetch list (overview)\n"
+      " -S | --subscribe-full <grp>   Add group to fetch list (full)\n"
+      " -t | --subscribe-thread <grp> Add group to fetch list (thread)\n"
+      " -u | --unsubscribe <grp>      Remove group from fetch list\n"
+      " -v | --version                Print version\n";
+    fprintf( stderr, "%s", msg );
+}
+
+/*
+  Allow core files: Change core limit and change working directory
+  to spool directory, where news has write permissions.
+*/
+static void
+enableCorefiles()
+{
+    struct rlimit lim;
+
+    if ( getrlimit( RLIMIT_CORE, &lim ) != 0 )
+    {
+        Log_err( "Cannot get system core limit: %s", strerror( errno ) );
+        return;
+    }
+    lim.rlim_cur = lim.rlim_max;
+    if ( setrlimit( RLIMIT_CORE, &lim ) != 0 )
+    {
+        Log_err( "Cannot set system core limit: %s", strerror( errno ) );
+        return;
+    }
+    Log_dbg( "Core limit set to %i", lim.rlim_max );
+    if ( chdir( Cfg_spoolDir() ) != 0 )
+    {
+         Log_err( "Cannot change to directory '%s'", Cfg_spoolDir() );
+         return;
+    }
+    Log_dbg( "Changed to directory '%s'", Cfg_spoolDir() );
+}
+
+static Bool
+initNoffle( Bool interactive )
+{
+    Log_init( "noffle", interactive, LOG_NEWS );
+    Cfg_read();
+    Log_dbg( "NOFFLE version %s", Cfg_version() );
+    noffle.interactive = interactive;
+    if ( interactive )
+      if ( ! Lock_openDatabases() )
+        return FALSE;
+    if ( ! interactive )
+        enableCorefiles();
+    return TRUE;
+}
+
+static void
+closeNoffle( void )
+{
+    if ( noffle.interactive )
+      Lock_closeDatabases();
+}
+
+static void
+bugReport( int sig )
+{
+    Log_err( "Received SIGSEGV. Please submit a bug report" );
+    signal( SIGSEGV, SIG_DFL );
+    raise( sig );
+}
+
+static void
+logSignal( int sig )
+{
+    const char *name;
+    Bool err = TRUE;
+
+    switch ( sig )
+    {
+    case SIGABRT:
+        name = "SIGABRT"; break;
+    case SIGFPE:
+        name = "SIGFPE"; break;
+    case SIGILL:
+        name = "SIGILL"; break;
+    case SIGINT:
+        name = "SIGINT"; break;
+    case SIGTERM:
+        name = "SIGTERM"; break;
+    case SIGPIPE:
+        name = "SIGPIPE"; err = FALSE; break;
+    default:
+        name = "?"; break;
+    }
+    if ( err )
+        Log_err( "Received signal %i (%s). Aborting.", sig, name );
+    else
+        Log_inf( "Received signal %i (%s). Aborting.", sig, name );
+    signal( sig, SIG_DFL );
+    raise( sig );
+}
+
+int main ( int argc, char **argv )
+{
+    int c, result;
+    struct option longOptions[] =
+    {
+        { "article",          required_argument, NULL, 'a' },
+        { "database",         no_argument,       NULL, 'd' },
+        { "expire",           required_argument, NULL, 'e' },
+        { "fetch",            no_argument,       NULL, 'f' },
+        { "groups",           no_argument,       NULL, 'g' },
+        { "help",             no_argument,       NULL, 'h' },
+        { "list",             no_argument,       NULL, 'l' },
+        { "offline",          no_argument,       NULL, 'o' },
+        { "online",           no_argument,       NULL, 'n' },
+        { "query",            required_argument, NULL, 'q' },
+        { "server",           no_argument,       NULL, 'r' },
+        { "requested",        no_argument,       NULL, 'R' },
+        { "subscribe-over",   required_argument, NULL, 's' },
+        { "subscribe-full",   required_argument, NULL, 'S' },
+        { "subscribe-thread", required_argument, NULL, 't' },
+        { "unsubscribe",      required_argument, NULL, 'u' },
+        { "version",          no_argument,       NULL, 'v' },
+        { NULL, 0, NULL, 0 }
+    };
+    
+    signal( SIGSEGV, bugReport );
+    signal( SIGABRT, logSignal );
+    signal( SIGFPE, logSignal );
+    signal( SIGILL, logSignal );
+    signal( SIGINT, logSignal );
+    signal( SIGTERM, logSignal );
+    signal( SIGPIPE, logSignal );
+    c = getopt_long( argc, argv, "a:de:fghlonq:rRs:S:t:u:v",
+                     longOptions, NULL );
+    if ( ! initNoffle( c != 'r' ) )
+        return EXIT_FAILURE;
+    result = EXIT_SUCCESS;
+    switch ( c )
+    {
+    case 0:
+        /* Options that set a flag. */
+        break;
+    case 'a':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -a needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            doArt( optarg );
+        break;
+    case 'd':
+        doDb();
+        break;
+    case 'e':
+        {
+            unsigned int days;
+
+            if ( ! optarg || sscanf( optarg, "%u", &days ) != 1 )
+            {
+                fprintf( stderr, "Bad argument: -e %s\n", optarg );
+                result = EXIT_FAILURE;
+            }
+            else
+                doExpire( days );
+        }
+        break;
+    case 'f':
+        doFetch();
+        break;
+    case 'g':
+        doGrps();
+        break;
+    case -1:
+    case 'h':
+        printUsage();
+        break;
+    case 'l':
+        doList();
+        break;
+    case 'n':
+        if ( Online_true() )
+            fprintf( stderr, "NOFFLE is already online\n" );
+        else
+            Online_set( TRUE );
+        break;
+    case 'o':
+        if ( ! Online_true() )
+            fprintf( stderr, "NOFFLE is already offline\n" );
+        else
+            Online_set( FALSE );
+        break;
+    case 'q':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -q needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+        {
+            if ( strcmp( optarg, "groups" ) == 0 )
+                noffle.queryGrps = TRUE;
+            else if ( strcmp( optarg, "desc" ) == 0 )
+                noffle.queryDsc = TRUE;
+            else if ( strcmp( optarg, "times" ) == 0 )
+                noffle.queryTimes = TRUE;
+            else
+            {
+                fprintf( stderr, "Unknown argument -q %s\n", optarg );
+                result = EXIT_FAILURE;
+            }
+            doQuery();
+        }
+        break;
+    case 'r':
+        Log_inf( "Starting as server" );
+        Serv_run();
+        break;
+    case 'R':
+        doRequested( optarg );
+        break;
+    case 's':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -s needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            result = doSubscribe( optarg, OVER );
+        break;
+    case 'S':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -S needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            doSubscribe( optarg, FULL );
+        break;
+    case 't':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -t needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            result = doSubscribe( optarg, THREAD );
+        break;
+    case 'u':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -u needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            doUnsubscribe( optarg );
+        break;
+    case '?':
+        /* Error message already printed by getopt_long */
+        result = EXIT_FAILURE;
+        break;
+    case 'v':
+        printf( "NNTP server NOFFLE, version %s.\n", Cfg_version() );
+        break;
+    default:
+        abort(); /* Never reached */
+    }
+    closeNoffle();
+    return result;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/noffle.conf.5	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,14 @@
+
+.TH noffle.conf 5
+.\" $Id: noffle.conf.5 3 2000-01-04 11:35:42Z enz $
+.SH NAME
+noffle.conf \- Configuration file for NOFFLE news server
+
+.SH DESCRIPTION
+
+noffle.conf is the configuration file of the NOFFLE
+news server.
+
+See
+.BR noffle (3)
+for information about NOFFLE and the format of the configuration file.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/noffle.conf.example	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,54 @@
+###############################################################################
+#
+# NOFFLE news server config file
+#
+###############################################################################
+
+# Remote news server. Format: <hostname>[:<port>] [<user> <pass>]
+# (<user> and <pass> only for servers with authentication,
+# the password may not contain white-spaces)
+
+server news
+
+
+# Mail addredd for failed postings
+
+#mail-to root
+
+
+# Never get more than <max-fetch> articles. Discard oldest, if there are more
+
+max-fetch 300
+
+
+# Automatically remove groups from fetch list if they have not been
+# accessed for <n> days.
+
+auto-unsubscribe no
+#auto-unsubscribe-days 30
+
+
+# Parameter for thread mode. Retrieve articles, if they are referencing an
+# article that has been read within the last <n> days
+
+thread-follow-time 7
+
+
+# Timeout for connecting to remote server in seconds.
+
+connect-timeout 30
+
+
+# Automatically put groups on fetchlist, if someone accesses them.
+# Mode can be: full, thread, over
+
+auto-subscribe no
+#auto-subscribe-mode over
+
+
+# Remove/replace Message-ID in posted articles. For generating Message-IDs,
+# the domain name of your system should be a valid domain name.
+# If you are in a local domain, set it to your provider's domain name.
+
+remove-messageid no
+replace-messageid yes
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/online.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,57 @@
+/*
+  online.c
+
+  $Id: online.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include <unistd.h>
+#include "common.h"
+#include "config.h"
+#include "log.h"
+
+static void
+fileOnline( Str s )
+{
+    snprintf( s, MAXCHAR, "%s/lock/online", Cfg_spoolDir() );
+}
+
+Bool
+Online_true( void )
+{
+    FILE *f;
+    Str file;
+
+    fileOnline( file );
+    if ( ! ( f = fopen( file, "r" ) ) )
+        return FALSE;
+    fclose( f );
+    return TRUE;
+}
+
+void
+Online_set( Bool value )
+{
+    FILE *f;
+    Str file;
+
+    fileOnline( file );
+    if ( value )
+    {
+        if ( ! ( f = fopen( file, "a" ) ) )
+        {
+            Log_err( "Could not create %s", file );
+            return;
+        }
+        fclose( f );
+        Log_inf( "NOFFLE is now online" );
+    }
+    else
+    {
+        if ( unlink( file ) != 0 )
+        {
+            Log_err( "Cannot remove %s", file );
+            return;
+        }
+        Log_inf( "NOFFLE is now offline" );
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/online.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,18 @@
+/*
+  online.h
+
+  Online/offline status.
+
+  $Id: online.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef ONLINE_H
+#define ONLINE_H
+
+Bool
+Online_true( void );
+
+void
+Online_set( Bool value );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/outgoing.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,121 @@
+/*
+  outgoing.c
+
+  $Id: outgoing.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "outgoing.h"
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+#include "config.h"
+#include "log.h"
+#include "util.h"
+
+struct Outgoing
+{
+    DIR *dir;
+    Str serv;
+} outgoing = { NULL, "" };
+
+static void
+fileOutgoing( Str file, const char *serv, const char *msgId )
+{
+    snprintf( file, MAXCHAR, "%s/outgoing/%s/%s",
+              Cfg_spoolDir(), serv, msgId );
+}
+
+static void
+createDir( const char *serv )
+{
+    Str dir;
+    int r;
+
+    snprintf( dir, MAXCHAR, "%s/outgoing/%s", Cfg_spoolDir(), serv );
+    r = mkdir( dir, 0755 );
+    if ( r != 0 )
+        Log_dbg( "mkdir: %s", strerror( errno ) );
+}
+
+Bool
+Out_add( const char *serv, const Str msgId, const DynStr *artTxt )
+{
+    Str file;
+    FILE *f;
+
+    fileOutgoing( file, serv, msgId );
+    if ( ! ( f = fopen( file, "w" ) ) )
+    {
+        createDir( serv );
+        if ( ! ( f = fopen( file, "w" ) ) )
+        {
+            Log_err( "Cannot open %s", file );
+            return FALSE;
+        }
+    }
+    fprintf( f, "%s", DynStr_str( artTxt ) );
+    fclose( f );
+    return TRUE;
+}
+
+Bool
+Out_first( const char *serv, Str msgId, DynStr *artTxt )
+{
+    Str file;
+    
+    snprintf( file, MAXCHAR, "%s/outgoing/%s", Cfg_spoolDir(), serv );
+    if ( ! ( outgoing.dir = opendir( file ) ) )
+    {
+        Log_dbg( "Cannot open %s", file );
+        return FALSE;
+    }
+    Utl_cpyStr( outgoing.serv, serv );
+    Out_next( NULL, NULL ); /* "."  */
+    Out_next( NULL, NULL ); /* ".." */
+    return Out_next( msgId, artTxt );
+}
+
+Bool
+Out_next( Str msgId, DynStr *artTxt )
+{
+    struct dirent *d;
+    FILE *f;
+    Str file, line;
+
+    ASSERT( outgoing.dir );
+    if ( ! ( d = readdir( outgoing.dir ) ) )
+    {
+        closedir( outgoing.dir );
+        outgoing.dir = NULL;
+        return FALSE;
+    }
+    if ( artTxt == NULL )
+        return ( d->d_name != NULL );
+    fileOutgoing( file, outgoing.serv, d->d_name );
+    if ( ! ( f = fopen( file, "r" ) ) )
+    {
+        Log_err( "Cannot open %s for read", file );
+        return FALSE;
+    }
+    DynStr_clear( artTxt );
+    while ( fgets( line, MAXCHAR, f ) )
+        DynStr_app( artTxt, line );
+    Utl_cpyStr( msgId, d->d_name );
+    fclose( f );
+    return TRUE;
+}
+
+void
+Out_remove( const char *serv, Str msgId )
+{
+    Str file;
+
+    fileOutgoing( file, serv, msgId );
+    if ( unlink( file ) != 0 )
+        Log_err( "Cannot remove %s", file );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/outgoing.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,30 @@
+/*
+  outgoing.h
+
+  Collection of posted articles.
+
+  $Id: outgoing.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef OUT_H
+#define OUT_H
+
+#include "common.h"
+#include "dynamicstring.h"
+
+Bool
+Out_add( const char *serv, const Str msgId, const DynStr *artTxt );
+
+/* Start enumeration. Return TRUE on success. */
+Bool
+Out_first( const char *serv, Str msgId, DynStr *artTxt );
+
+/* Continue enumeration. Return TRUE on success. */
+Bool
+Out_next( Str msgId, DynStr *s );
+
+/* Delete article from outgoing collection */
+void
+Out_remove( const char *serv, Str msgId );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/over.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,191 @@
+/*
+  over.c
+
+  $Id: over.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include <errno.h>
+#include <time.h>
+#include "config.h"
+#include "content.h"
+#include "database.h"
+#include "fetchlist.h"
+#include "log.h"
+#include "util.h"
+#include "protocol.h"
+#include "pseudo.h"
+
+struct Over
+{
+    int numb;            /* message number of the overviewed article */
+    char *subj;
+    char *from;
+    char *date;
+    char *msgId;
+    char *ref;
+    size_t bytes;
+    size_t lines;
+    time_t time;
+};
+
+Over *
+new_Over( const char *subj, const char *from,
+          const char *date, const char *msgId, char *ref,
+          size_t bytes, size_t lines )
+{
+    Over *ov;
+
+    if ( ! ( ov = (Over *)malloc( sizeof( Over ) ) ) )
+    {
+        Log_err( "Cannot allocate Over" );
+        exit( EXIT_FAILURE );
+    }
+    ov->numb = 0;
+    Utl_allocAndCpy( &ov->subj, subj );
+    Utl_allocAndCpy( &ov->from, from );
+    Utl_allocAndCpy( &ov->date, date );
+    Utl_allocAndCpy( &ov->msgId, msgId );
+    Utl_allocAndCpy( &ov->ref, ref );
+    ov->bytes = bytes;
+    ov->lines = lines;
+    return ov;
+}
+
+void
+del_Over( Over *self )
+{
+    if ( ! self )
+        return;
+    free( self->subj );
+    self->subj = NULL;
+    free( self->from );
+    self->from = NULL;
+    free( self->date );
+    self->date = NULL;
+    free( self->msgId );
+    self->msgId = NULL;
+    free( self->ref );
+    self->ref = NULL;
+    free( self );
+}
+
+int
+Ov_numb( const Over *self )
+{
+    return self->numb;
+}
+
+const char *
+Ov_subj( const Over *self )
+{
+    return self->subj;
+}
+
+const char *
+Ov_from( const Over *self )
+{
+    return self->from;
+}
+
+const char *
+Ov_date( const Over *self )
+{
+    return self->date;
+}
+
+const char *
+Ov_msgId( const Over *self )
+{
+    return self->msgId;
+}
+
+const char *
+Ov_ref( const Over *self )
+{
+    return self->ref;
+}
+
+size_t
+Ov_bytes( const Over *self )
+{
+    return self->bytes;
+}
+
+size_t
+Ov_lines( const Over *self )
+{
+    return self->lines;
+}
+
+void
+Ov_setNumb( Over *self, int numb )
+{
+    self->numb = numb;
+}
+
+Bool
+Ov_write( const Over *self, FILE *f )
+{
+    return ( fprintf( f, "%i\t%s\t%s\t%s\t%s\t%s\t%d\t%d\n",
+                      self->numb, self->subj,
+                      self->from, self->date, self->msgId,
+                      self->ref, self->bytes,
+                      self->lines ) > 0 );
+}
+
+static const char *
+readField( Str result, const char *p )
+{
+    size_t len;
+    char *r;
+
+    if ( ! p )
+        return NULL;
+    r = result;
+    *r = '\0';
+    len = 0;
+    while ( *p != '\t' && *p != '\n' )
+    {
+        if ( ! *p )
+            return p;
+        *(r++) = *(p++);
+        ++len;
+        if ( len >= MAXCHAR - 1 )
+        {
+            *r = '\0';
+            Log_err( "Field in overview too long: %s", r );
+            return ++p;
+        }
+    }
+    *r = '\0';
+    return ++p;
+}
+
+/* read Over-struct from line */
+Over *
+Ov_read( char *line )
+{
+    size_t bytes, lines;
+    const char *p;
+    Over *result;
+    int numb;
+    Str t, subj, from, date, msgId, ref;
+    
+    p = readField( t, line );
+    if ( sscanf( t, "%i", &numb ) != 1 )
+        return NULL;
+    p = readField( subj, p );
+    p = readField( from, p );
+    p = readField( date, p );
+    p = readField( msgId, p );
+    p = readField( ref, p );
+    p = readField( t, p );
+    if ( sscanf( t, "%d", &bytes ) != 1 )
+        return NULL;
+    p = readField( t, p );
+    if ( sscanf( t, "%d", &lines ) != 1 )
+        return NULL;
+    result = new_Over( subj, from, date, msgId, ref, bytes, lines );
+    Ov_setNumb( result, numb );
+    return result;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/over.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,70 @@
+/*
+  over.h
+
+  Processing of single article overviews. Handling of overview files is in
+  content.c. An article overview contains important article properties,
+  such as date, from, subject.
+
+  $Id: over.h 3 2000-01-04 11:35:42Z enz $ 
+*/
+
+#ifndef OVER_H
+#define OVER_H
+
+#include <time.h>
+#include "common.h"
+
+struct Over;
+typedef struct Over Over;
+
+/*
+  Usual fields from overview databases.
+  Xref without hostname.
+*/
+Over *
+new_Over( const char *subj, const char *from, const char *date,
+          const char *msgId, char *ref, size_t bytes, size_t lines );
+
+
+/* free memory */
+void
+del_Over( Over *self );
+
+/* read Over-struct from line */
+Over *
+Ov_read( char *line );
+
+/* write struct Over to f as a line */
+Bool
+Ov_write( const Over *self, FILE *f );
+
+/* Access particular fields in struct over */
+
+int
+Ov_numb( const Over *self );
+
+const char *
+Ov_subj( const Over *self );
+
+const char *
+Ov_from( const Over *self );
+
+const char *
+Ov_date( const Over *self );
+
+const char *
+Ov_msgId( const Over *self );
+
+const char *
+Ov_ref( const Over *self );
+
+size_t
+Ov_bytes( const Over *self );
+
+size_t
+Ov_lines( const Over *self );
+
+void
+Ov_setNumb( Over *self, int numb );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/protocol.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,278 @@
+/*
+  protocol.c
+
+  $Id: protocol.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include <ctype.h> 
+#include <netdb.h>
+#include <sys/types.h>
+#include <sys/utsname.h>
+#include "common.h"
+#include "dynamicstring.h"
+#include "log.h"
+#include "over.h"
+#include "util.h"
+
+Bool
+Prt_getLn( Str line, FILE *f )
+{
+    size_t len;
+
+    /*
+      We also accept lines ending with "\n" instead of "\r\n", some
+      clients wrongly send such lines.
+    */
+    if ( ! fgets( line, MAXCHAR, f ) )
+    {
+        Log_dbg( "Prt_getLine failed" );
+        return FALSE;
+    }
+    len = strlen( line );
+    if ( line[ len - 1 ] == '\n' )
+    {
+        line[ len - 1 ] = '\0';
+        if ( line[ len - 2 ] == '\r' )
+            line[ len - 2 ] = '\0';
+    }
+    Log_dbg( "[R] %s", line );
+    return TRUE;
+}
+
+Bool
+Prt_getTxtLn( Str line, Bool *err, FILE *f )
+{
+    Str buf;
+
+    if ( ! Prt_getLn( buf, f ) )
+    {
+        Log_err( "Cannot get text line" );
+        *err = TRUE;
+        return FALSE;
+    }
+    *err = FALSE;
+    if ( buf[ 0 ] == '.' )
+    {
+        if ( buf[ 1 ] == 0 )
+            return FALSE;
+        else
+            strcpy( line, buf + 1 );
+    }
+    else
+        strcpy( line, buf );
+    return TRUE;
+}
+
+Bool
+Prt_putTxtLn( const char* line, FILE *f )
+{
+    if ( line[ 0 ] == '.' )
+    {
+        Log_dbg( "[S] .%s", line );
+        return ( fprintf( f, ".%s\r\n", line ) == strlen( line ) + 3 );
+    }
+    else
+    {
+        Log_dbg( "[S] %s", line );
+        return ( fprintf( f, "%s\r\n", line ) == strlen( line ) + 2 );
+    }
+}
+
+Bool
+Prt_putEndOfTxt( FILE *f )
+{
+    Log_dbg( "[S] ." );
+    return ( fprintf( f, ".\r\n" ) == 3 );
+}
+
+/*
+  Write text buffer of lines each ending with '\n'.
+  Replace '\n' by "\r\n".
+*/
+Bool
+Prt_putTxtBuf( const char *buf, FILE *f )
+{
+    Str line;
+    const char *pBuf;
+    char *pLn;
+
+    pBuf = buf;
+    pLn = line;
+    while ( *pBuf != '\0' )
+    {
+        if ( *pBuf == '\n' )
+        {
+            *pLn = '\0';
+            if ( ! Prt_putTxtLn( line, f ) )
+                return FALSE;
+            pLn = line;
+            ++pBuf;
+        }
+        else if ( pLn - line >= MAXCHAR - 1 )
+        {
+            /* Put it out raw to prevent String overflow */
+            Log_err( "Writing VERY long line" );
+            *pLn = '\0';
+            if ( fprintf( f, "%s", line ) != strlen( line ) )
+                return FALSE;
+            pLn = line;
+        }   
+        else
+            *(pLn++) = *(pBuf++);
+    }
+    return TRUE;
+}
+
+Bool
+Prt_getField( Str resultField, Str resultValue, const char* line )
+{
+    char *dst;
+    const char *p;
+    Str lineLower, t;
+    
+    Utl_cpyStr( lineLower, line );
+    Utl_toLower( lineLower );
+    p = Utl_stripWhiteSpace( lineLower );
+    dst = resultField;
+    while ( ! isspace( *p ) && *p != ':' && *p != '\0' )
+        *(dst++) = *(p++);
+    *dst = '\0';
+    while ( isspace( *p ) )
+        ++p;    
+    if ( *p == ':' )
+    {
+        ++p;
+        strcpy( t, line + ( p - lineLower ) );
+        p = Utl_stripWhiteSpace( t );
+        strcpy( resultValue, p );
+        return TRUE;
+    }
+    return FALSE;
+}
+
+Bool
+Prt_searchHeader( const char *artTxt, const char *which, Str result )
+{
+    const char *src, *p;
+    char *dst;
+    Str line, whichLower, field;
+    int len;
+    
+    Utl_cpyStr( whichLower, which );
+    Utl_toLower( whichLower );
+    src = artTxt;
+    while ( TRUE )
+    {
+        dst = line;
+        len = 0;
+        while ( *src != '\n' && len < MAXCHAR )
+        {
+            if ( *src == '\0' )
+                return FALSE;
+            *(dst++) = *(src++);
+            ++len;
+        }
+        if ( *src == '\n' )
+            ++src;
+        *dst = '\0';
+        p = Utl_stripWhiteSpace( line );
+        if ( *p == '\0' )
+            break;
+        if ( Prt_getField( field, result, line )
+             && strcmp( field, whichLower ) == 0 )
+            return TRUE;
+    }
+    return FALSE;
+}
+
+static Bool
+getFQDN( Str result )
+{
+    struct hostent *myHostEnt;
+    struct utsname myName;
+    
+    if ( uname( &myName ) >= 0
+         && ( myHostEnt = gethostbyname( myName.nodename ) ) )
+    {
+        Utl_cpyStr( result, myHostEnt->h_name );
+        return TRUE;
+    }
+    return FALSE;
+}
+
+static void
+getDomain( Str domain, const char *from )
+{
+    const char *addTopLevel, *p1, *p2, *p, *domainStart;
+    Str myDomain;
+
+    if ( getFQDN( myDomain ) )
+    {
+        p = strstr( myDomain, "." );
+        if ( p != NULL )
+            domainStart = p + 1;
+        else
+            domainStart = myDomain;
+    }
+    else /* Take domain of From field */
+    {
+        myDomain[ 0 ] = '\0';
+        p1 = strstr( from, "@" );
+        if ( p1 != NULL )
+        {
+            p2 = strstr( p1, ">" );
+            if ( p2 != NULL )
+                Utl_cpyStrN( myDomain, p1 + 1, p2 - p1 - 1 );
+        }
+        if ( myDomain[ 0 ] == '\0' )
+            Utl_cpyStr( myDomain, "unknown" );
+        domainStart = myDomain;
+    }
+    /*
+      If domain contains no dot (and is probably invalid anyway),
+      we add ".local", because some servers insist on domainnames with dot
+      in message ID.
+    */
+    addTopLevel = strstr( domainStart, "." ) == NULL ? ".local" : "";
+    snprintf( domain, MAXCHAR, "%s%s", myDomain, addTopLevel );    
+}
+
+Bool
+Prt_isValidMsgId( const char *msgId )
+{
+    Str head, domain;
+    int len, headLen;
+    const char *p;
+
+    len = strlen( msgId );
+    p = strstr( msgId, "@" );
+    if ( msgId[ 0 ] != '<' || msgId[ len - 1 ] != '>' || p == NULL )
+        return FALSE;
+    strcpy( domain, p + 1 );
+    domain[ strlen( domain ) - 1 ] = '\0';
+    headLen = p - msgId - 1;
+    Utl_cpyStrN( head, msgId + 1, headLen );
+    head[ headLen ] = '\0';
+    /*
+      To do: check for special characters in head and domain (non-printable
+      or '@', '<', '>'). Maybe compare domain with a config option 
+      and replace it by the config option, if not equal.
+     */
+    if ( strstr( domain, "." ) == NULL )
+        return FALSE;
+    return TRUE;
+}
+
+void
+Prt_genMsgId( Str msgId, const char *from, const char *suffix )
+{
+    Str domain, date;
+    time_t t;
+
+    getDomain( domain, from );
+    time( &t );
+    strftime( date, MAXCHAR, "%Y%m%d%H%M%S", gmtime( &t ) );
+    srand( time( NULL ) );
+    snprintf( msgId, MAXCHAR, "<%s.%X.%s@%s>", date, rand(), suffix, domain );
+    ASSERT( Prt_isValidMsgId( msgId ) );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/protocol.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,105 @@
+/*
+  protocol.h
+
+  Functions related with the NNTP protocol which are useful for both
+  the server and the client.
+
+  $Id: protocol.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef PRT_H
+#define PRT_H
+
+#include "dynamicstring.h"
+#include "over.h"
+
+#define STAT_HELP_FOLLOWS        100
+#define STAT_DEBUG_FOLLOWS       199
+
+#define STAT_READY_POST_ALLOW    200
+#define STAT_READY_NO_POST_ALLOW 201
+#define STAT_CMD_OK              202
+#define STAT_GOODBYE             205
+#define STAT_GRP_SELECTED        211
+#define STAT_GRPS_FOLLOW         215
+#define STAT_ART_FOLLOWS         220
+#define STAT_HEAD_FOLLOWS        221
+#define STAT_BODY_FOLLOWS        222
+#define STAT_ART_RETRIEVED       223
+#define STAT_OVERS_FOLLOW        224
+#define STAT_NEW_GRP_FOLLOW      231
+#define STAT_POST_OK             240
+#define STAT_AUTH_ACCEPTED       281
+
+#define STAT_SEND_ART            340
+#define STAT_MORE_AUTH_REQUIRED  381
+
+#define STAT_NO_SUCH_GRP         411
+#define STAT_NO_GRP_SELECTED     412
+#define STAT_NO_ART_SELECTED     420
+#define STAT_NO_NEXT_ART         421
+#define STAT_NO_PREV_ART         422
+#define STAT_NO_SUCH_NUMB        423
+#define STAT_NO_SUCH_ID          430
+#define STAT_ART_REJECTED        437
+#define STAT_POST_FAILED         441
+#define STAT_AUTH_REQUIRED       480
+#define STAT_AUTH_REJECTED       482
+
+#define STAT_NO_SUCH_CMD         500
+#define STAT_SYNTAX_ERR          501
+#define STAT_NO_PERMISSION       502
+#define STAT_PROGRAM_FAULT       503
+
+/* 
+   Read next line from f into Str, up to "\n" or "\r\n". Don't save "\n"
+   or "\r\n" in line. Terminate with '\0'. 
+*/
+Bool
+Prt_getLn( Str line, FILE *f );
+
+/*
+  Read a text line from server. Returns TRUE if line != ".", removes
+  leading '.' otherwise.
+*/
+Bool
+Prt_getTxtLn( Str line, Bool *err, FILE *f );
+
+/*
+  Write text line to f. Escape "." at the beginning with another ".".
+  Terminate with "\r\n".
+*/
+Bool
+Prt_putTxtLn( const char* line, FILE *f );
+
+/*
+  Write text buffer of lines each ending with '\n'.
+  Replace '\n' by "\r\n".
+*/
+Bool
+Prt_putTxtBuf( const char *buf, FILE *f );
+
+/* 
+   Write text-ending "."-line to f
+*/
+Bool
+Prt_putEndOfTxt( FILE *f );
+
+/*
+  Splits line in field and value. Field is converted to lower-case. 
+*/
+Bool
+Prt_getField( Str resultField, Str resultValue, const char* line );
+
+/* Search header. Works only with single line headers (ignores continuation
+   lines */
+Bool
+Prt_searchHeader( const char *artTxt, const char* which, Str result );
+
+Bool
+Prt_isValidMsgId( const char *msgId );
+
+void
+Prt_genMsgId( Str msgId, const char *from, const char *suffix );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pseudo.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,333 @@
+/*
+  pseudo.c
+  
+  $Id: pseudo.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "pseudo.h"
+
+#include <time.h>
+#include "common.h"
+#include "config.h"
+#include "content.h"
+#include "database.h"
+#include "group.h"
+#include "log.h"
+#include "protocol.h"
+
+Over *
+genOv( const char *rawSubj, const char *rawBody, const char *suffix )
+{
+    size_t bytes, lines;
+    time_t t;
+    Str subj, date, msgId;
+
+    snprintf( subj, MAXCHAR, "[ %s ]", rawSubj );
+    time( &t );
+    strftime( date, MAXCHAR, "%d %b %Y %H:%M:%S %Z", localtime( &t ) );
+    Prt_genMsgId( msgId, "", suffix );
+    bytes = lines = 0;
+    while ( *rawBody )
+    {
+        ++bytes;
+        if ( *rawBody == '\n' )
+            ++lines;
+        ++rawBody;
+    }
+    return new_Over( subj, "news (\"[ NOFFLE ]\")" , date, msgId, "",
+                     bytes, lines );
+}
+
+void
+Pseudo_appGeneralInfo()
+{
+    Cont_app( genOv( "General info", Pseudo_generalInfoBody(),
+                     "NOFFLE-GENERAL-INFO" ) );
+}
+
+Bool
+Pseudo_isGeneralInfo( const char *msgId )
+{
+    return ( strstr( msgId, "NOFFLE-GENERAL-INFO" ) != NULL );
+}
+
+const char *
+Pseudo_generalInfoHead()
+{
+    static Str s;
+
+    Over *ov;
+
+    ov = genOv( "General info", Pseudo_generalInfoBody(),
+                "NOFFLE-GENERAL-INFO" );
+    if ( ov )
+    {
+        snprintf( s, MAXCHAR,
+                  "Message-ID: %s\n"
+                  "Subject: %s\n"
+                  "From: %s\n"
+                  "Date: %s\n"
+                  "Bytes: %u\n"
+                  "Lines: %u\n",
+                  Ov_msgId( ov ),
+                  Ov_subj( ov ),
+                  Ov_from( ov ),
+                  Ov_date( ov ),
+                  Ov_bytes( ov ),
+                  Ov_lines( ov ) );
+        del_Over( ov );
+        return s;
+    }
+    return NULL;
+}
+
+const char *
+Pseudo_generalInfoBody( void )
+{
+    if ( Cfg_autoSubscribe() )
+        return
+            "\n"
+            "\t[ NOFFLE INFO: General information ]\n"
+            "\n"
+            "\t[ This server is running NOFFLE, which is a NNTP server ]\n"
+            "\t[ optimized for low speed dial-up Internet connections. ]\n"
+            "\n"
+            "\t[ By reading this or any other article of this group, ]\n"
+            "\t[ NOFFLE has put it on its fetch list and will retrieve ]\n"
+            "\t[ articles next time it is online. ]\n"
+            "\n"
+            "\t[ If you have more questions about NOFFLE please talk ]\n"
+            "\t[ to your newsmaster or read the manual page for ]\n"
+            "\t[ \"noffle\". ]\n";
+    else
+        return
+            "\n"
+            "\t[ NOFFLE INFO: General information ]\n"
+            "\n"
+            "\t[ This server is running NOFFLE, which is a NNTP server ]\n"
+            "\t[ optimized for low speed dial-up Internet connections. ]\n"
+            "\n"
+            "\t[ This group is presently not on the fetch list. You can ]\n"
+            "\t[ put groups on the fetxh list by running the \"noffle\" ]\n"
+            "\t[ command on the computer where this server is running. ]\n"
+            "\n"
+            "\t[ If you have more questions about NOFFLE please talk ]\n"
+            "\t[ to your newsmaster or read the manual page for ]\n"
+            "\t[ \"noffle\". ]\n";
+}
+
+const char *
+Pseudo_markedBody( void )
+{
+    return
+        "\n"
+        "\t[ NOFFLE INFO: Marked for download ]\n"
+        "\n"
+        "\t[ The body of this article has been marked for download. ]\n";
+}
+
+const char *
+Pseudo_alreadyMarkedBody( void )
+{
+    return
+        "\n"
+        "\t[ NOFFLE INFO: Already marked for download ]\n"
+        "\n"
+        "\t[ The body of this article has already been marked ]\n"
+        "\t[ for download. ]\n";
+}
+
+const char *
+Pseudo_markingFailedBody( void )
+{
+    return
+        "\n"
+        "\t[ NOFFLE ERROR: Marking for download failed ]\n"
+        "\n"
+        "\t[ Sorry, I could not mark this article for download. ]\n"
+        "\t[ Either the database is corrupted, or I was unable to ]\n"
+        "\t[ get write access to the request directory. ]\n"
+        "\t[ Please contact your newsmaster to remove this problem. ]\n";
+}
+
+void
+genPseudo( const char *rawSubj, const char* rawBody )
+{
+    Over *ov;
+    DynStr *body = 0, *artTxt = 0;
+
+    body = new_DynStr( 10000 );
+    artTxt = new_DynStr( 10000 );
+    DynStr_app( body, "\n\t[ NOFFLE INFO: " );
+    DynStr_app( body, rawSubj );
+    DynStr_app( body, " ]\n\n" );
+    DynStr_app( body, "\t[ " );
+    while( *rawBody )
+    {
+        if ( *rawBody == '\n' )
+        {
+            DynStr_app( body, " ]\n" );
+            if ( *( rawBody + 1 ) == '\n' )
+            {
+                DynStr_app( body, "\n\t[ " );
+                ++rawBody;
+            }
+            else if ( *( rawBody + 1 ) != '\0' )
+                DynStr_app( body, "\t[ " );
+        }
+        else
+            DynStr_appN( body, rawBody, 1 );
+        ++rawBody;
+    }
+    DynStr_appLn( body, "" );    
+    DynStr_appLn( artTxt,
+                  "Comments: Pseudo article generated by news server NOFFLE" );
+    DynStr_appLn( artTxt, "" );
+    DynStr_appDynStr( artTxt, body );
+    ov = genOv( rawSubj, DynStr_str( body ), "PSEUDO" );
+    if ( body && artTxt && ov )
+    {
+        Cont_app( ov );
+        if ( Db_prepareEntry( ov, Cont_grp(), Cont_last() ) )
+            Db_storeArt( Ov_msgId( ov ), DynStr_str( artTxt ) );
+        Cont_write();
+        Grp_setFirstLast( Cont_grp(), Cont_first(), Cont_last() );
+    }
+    del_DynStr( body );
+    del_DynStr( artTxt );
+}
+
+void
+Pseudo_retrievingFailed( const char *msgId, const char *reason )
+{
+    DynStr *artTxt = 0;
+
+    if ( ! Db_contains( msgId ) )
+    {
+        Log_err( "Article %s has no entry in database %s", msgId );
+        return;
+    }
+    artTxt = new_DynStr( 10000 );
+    DynStr_appLn( artTxt,
+                  "Comments: Pseudo body generated by news server NOFFLE" );
+    DynStr_appLn( artTxt, "" );
+    DynStr_app( artTxt,
+                "\n"
+                "\t[ NOFFLE ERROR: Retrieving failed ]\n"
+                "\n"
+                "\t[ This article could not be retrieved. Maybe ]\n"
+                "\t[ it has already expired at the remote server ]\n"
+                "\t[ or it has been cancelled by its sender. See ]\n"
+                "\t[ the appended status line of the remote ]\n"
+                "\t[ server for more information. ]\n"
+                "\n"
+                "\t[ This message will disappear the next time ]\n"
+                "\t[ someone tries to read this article, so that ]\n"
+                "\t[ it can be marked for download again. ]\n" );
+    DynStr_app( artTxt, "\n\t[ Remote server status: " );
+    DynStr_app( artTxt, reason );
+    DynStr_app( artTxt, " ]\n" );
+    Db_storeArt( msgId, DynStr_str( artTxt ) );
+    del_DynStr( artTxt );
+}
+
+void
+Pseudo_cntInconsistent( const char *grp, int first, int last, int next )
+{
+    DynStr *info;
+    Str s;
+
+    info = new_DynStr( 10000 );
+    if ( info )
+    {
+        DynStr_app( info,
+                    "This group's article counter is not \n"
+                    "consistent Probably the remote news server\n"
+                    "was changed or has reset its article counter\n"
+                    "for this group. As a consequence there could\n"
+                    "be some articles be duplicated in this group\n"
+                    "\n" );
+        snprintf( s, MAXCHAR, "Group: %s", grp );
+        DynStr_appLn( info, s );
+        snprintf( s, MAXCHAR, "Remote first article number: %i", first );
+        DynStr_appLn( info, s );
+        snprintf( s, MAXCHAR, "Remote last article number: %i", last );
+        DynStr_appLn( info, s );
+        snprintf( s, MAXCHAR, "Remote next article number: %i", next );
+        DynStr_appLn( info, s );
+        genPseudo( "Article counter inconsistent", DynStr_str( info ) );
+    }
+    del_DynStr( info );
+}
+
+void
+Pseudo_missArts( const char *grp, int first, int next )
+{
+    DynStr *info;
+    Str s;
+
+    info = new_DynStr( 5000 );
+    if ( info )
+    {
+        DynStr_app( info,
+                    "Some articles could not be retrieved from\n"
+                    "the remote server, because it had already\n"
+                    "deleted them.\n"
+                    "If this group is on the fetch list, then\n"
+                    "contact your newsmaster to ensure that\n"
+                    "\"noffle\" is fetching news more frequently.\n"
+                    "\n" );
+        snprintf( s, MAXCHAR, "Group: %s", grp );
+        DynStr_appLn( info, s );
+        snprintf( s, MAXCHAR, "Remote next article number: %i", next );
+        DynStr_appLn( info, s );
+        snprintf( s, MAXCHAR, "Remote first article number: %i", first );
+        DynStr_appLn( info, s );
+        genPseudo( "Missing articles", DynStr_str( info ) );
+        del_DynStr( info );
+    }
+}
+
+void
+Pseudo_autoUnsubscribed( const char *grp, int days )
+{
+    DynStr *info;
+    Str s;
+
+    info = new_DynStr( 10000 );
+    if ( info )
+    {
+        DynStr_app( info,
+                    "NOFFLE has automatically unsubscribed this\n"
+                    "group since it has not been accessed for\n"
+                    "some time.\n"
+                    "Re-subscribing is done either automatically\n"
+                    "by NOFFLE (if configured) or by manually\n"
+                    "running the 'noffle --subscribe' command\n"
+                    "\n" );
+        snprintf( s, MAXCHAR, "Group: %s", grp );
+        DynStr_appLn( info, s );
+        snprintf( s, MAXCHAR, "Days without access: %i", days );
+        DynStr_appLn( info, s );
+        genPseudo( "Auto unsubscribed", DynStr_str( info ) );
+    }
+    del_DynStr( info );
+}
+
+void
+Pseudo_autoSubscribed()
+{
+    DynStr *info;
+
+    info = new_DynStr( 10000 );
+    if ( info )
+    {
+        DynStr_app( info,
+                    "NOFFLE has now automatically subscribed to\n"
+                    "this group. It will fetch articles next time\n"
+                    "it is online.\n"
+                    "\n" );
+        genPseudo( "Auto subscribed", DynStr_str( info ) );
+    }
+    del_DynStr( info );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pseudo.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,64 @@
+/*
+  pseudo.h
+
+  Handling of pseudo articles.
+
+  $Id: pseudo.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef PSEUDO_H
+#define PSEUDO_H
+
+#include "over.h"
+
+/*
+  General info is a special pseudo message for groups not on fetchlist.
+  It is never stored in database, but generated every time a content is read.
+  However the group counter is always increased. This ensures that there
+  is always at least 1 article visible (even if the user deletes it) for
+  using the auto-subscribe option.
+*/
+Bool
+Pseudo_isGeneralInfo( const char *msgId );
+
+void
+Pseudo_appGeneralInfo( void );
+
+const char *
+Pseudo_generalInfoHead( void );
+
+const char *
+Pseudo_generalInfoBody( void );
+
+
+const char *
+Pseudo_markedBody( void );
+
+const char *
+Pseudo_alreadyMarkedBody( void );
+
+const char *
+Pseudo_markingFailedBody( void );
+
+void
+Pseudo_retrievingFailed( const char *msgId, const char *reason );
+
+
+/*
+  Other pseudo articles are stored in database and can contain dynamically
+  generated information about the failure.
+ */
+
+void
+Pseudo_cntInconsistent( const char *grp, int first, int last, int next );
+
+void
+Pseudo_missArts( const char *grp, int first, int next );
+
+void
+Pseudo_autoUnsubscribed( const char *grp, int days );
+
+void
+Pseudo_autoSubscribed( void );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/request.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,405 @@
+/*
+  request.c
+
+  Collection of articles that are marked for download.
+
+  $Id: request.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "request.h"
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <assert.h>
+#include "config.h"
+#include "log.h"
+#include "util.h"
+
+
+/* This struct keeps record of the message IDs that are to be fetched from
+   one particular host. Several of these are chained together via the
+   "next" pointer, if we have several servers.
+*/
+
+struct Reqserv;
+typedef struct Reqserv Reqserv;
+
+struct Reqserv {
+  char*    serv;                /* Server the messages are to be requested
+                                   from */
+  char**   reql;                /* List of message IDs of requested
+                                   messages. Some entries (that have been
+                                   deleted) may be NULL */
+  int      reql_length;         /* Number of string pointers in reql,
+                                   including NULL entries */
+  int      reql_capacity;       /* maximum number of string pointers reql
+                                   can hold */
+  Bool     dirty;               /* whether the request list needs to be
+                                   rewritten to disk */
+  Reqserv* next;                /* next Reqserv in list */
+  time_t   mtime;               /* last modification time of request file */ 
+};
+
+/* List of servers */
+static Reqserv* reqserv = 0;
+
+/* sanity check */
+static Bool is_open = FALSE;
+
+/* for Req_first/Req_next */
+static char** iterator = 0;
+static char** iterator_end = 0;
+
+
+/* local functions */
+static Reqserv* newReqserv      (const char* serv);
+static Bool     getReqserv      (const char* serv, Reqserv** rsz);
+static void     fileRequest     (Str file, const char *serv);
+static char**   searchMsgId     (const Reqserv * rs, const char *msgId);
+static void     storeMsgId      (Reqserv* rs, const char* msgId);
+static Bool     readRequestfile (const char* serv, Reqserv** rsz);
+static time_t   get_mtime       (const char* serv);
+
+/* read modification time of request file */
+static time_t get_mtime(const char* serv)
+{
+  Str filename;
+  struct stat stat1;
+
+  fileRequest(filename, serv);
+  stat(filename, &stat1);
+  return stat1.st_mtime;
+}
+
+
+/* create new Reqserv and queue it */
+static Reqserv* newReqserv(const char* serv)
+{
+  Reqserv* rs = (Reqserv*) malloc(sizeof(Reqserv));
+  rs->serv = strcpy(malloc(strlen(serv)+1), serv);
+  rs->reql = 0;
+  rs->reql_length = 0;
+  rs->reql_capacity = 0;
+  rs->next = reqserv;
+  rs->dirty = FALSE;
+  rs->mtime = 0;
+  reqserv = rs;
+  return rs;
+}
+
+
+/* get Reqserv for given server, and save it in "rsz". Load from file as
+   necessary. Return TRUE on success. Otherwise log errors and return
+   FALSE. (details in errno)
+*/
+static Bool getReqserv(const char* serv, Reqserv** rsz)
+{
+  Reqserv* rs;
+  for (rs = reqserv; rs; rs = rs->next)
+    if (!strcmp(serv, rs->serv)) {
+      *rsz = rs;
+      return TRUE;
+    }
+  return readRequestfile(serv, rsz);
+}
+
+
+/* Delete Reqserv from cache, if not up-to-date */
+static void
+cleanupReqserv( void )
+{
+  Reqserv *rs, *prev, *next;
+
+  rs = reqserv;
+  prev = NULL;
+  while ( rs != NULL )
+  {      
+      ASSERT( ! rs->dirty );
+      next = rs->next;
+      if ( get_mtime( rs->serv ) != rs->mtime )
+      {
+          if ( prev != NULL )
+              prev->next = next;
+          else
+              reqserv = next;
+          free( rs->serv );
+          rs->serv = NULL;
+          free( rs->reql );
+          rs->reql = NULL;
+          free( rs );
+      }
+      prev = rs;
+      rs = next;
+  }
+}
+
+/* Save name of file storing requests from server "serv" in "file" */
+static void fileRequest( Str file, const char *serv)
+{
+  snprintf( file, MAXCHAR, "%s/requested/%s", Cfg_spoolDir(), serv);
+}
+
+
+/* Search for msgid in Reqserv. Return pointer to list entry. Return 0 if
+   list does not contain msgid. */
+static char** searchMsgId(const Reqserv * rs, const char *msgId )
+{
+  char** rz;
+  ASSERT(rs != 0);
+
+  if (!rs->reql)
+    return 0;
+
+  for (rz = rs->reql; rz < rs->reql + rs->reql_length; rz++)
+    if (*rz && !strcmp(*rz, msgId))
+      return rz;
+
+  return 0;
+}
+
+
+Bool
+Req_contains(const char *serv, const char *msgId)
+{
+  Reqserv* rs;
+  ASSERT( is_open );
+  if (getReqserv(serv, &rs) == FALSE) 
+    return FALSE;
+  return searchMsgId(rs, msgId) ? TRUE : FALSE;
+}
+
+
+static void storeMsgId(Reqserv* rs, const char* msgId)
+{
+  char* msgid;
+
+  if (searchMsgId(rs, msgId))
+    /* already recorded */
+    return;
+
+  msgid = strcpy(malloc(strlen(msgId)+1), msgId);
+
+  if (rs->reql_length >= rs->reql_capacity) {
+    int c1 = rs->reql_capacity*2 + 10;
+    rs->reql = (char**) realloc(rs->reql, c1*sizeof(char*));
+    rs->reql_capacity = c1;
+  }
+
+  *(rs->reql + rs->reql_length++) = msgid;
+  rs->dirty = TRUE;
+}
+
+
+/* Add request for message "msgIg" from server "serv". Return TRUE iff
+   successful. 
+*/
+Bool Req_add(const char *serv, const char *msgId)
+{
+    Reqserv* rs;
+    ASSERT( is_open );
+    Log_dbg( "Marking %s on %s for download", msgId, serv );
+
+    if (getReqserv(serv, &rs) == FALSE) 
+      return FALSE;
+    storeMsgId(rs, msgId);
+    return TRUE;
+}
+
+static Bool
+readLn( Str line, FILE* f )
+{
+    size_t len;
+
+    if ( ! fgets( line, MAXCHAR, f ) )
+        return FALSE;
+    len = strlen( line );
+    if ( line[ len - 1 ] == '\n' )
+        line[ len - 1 ] = '\0';
+    return TRUE;
+}
+
+/* Read request file into new, non-queued Reqserv. Save new Reqserv in
+   "rsz" and return TRUE on success. Returns FALSE on failure, see errno.
+   If the file doesn't exist, an empty Reqserv is returned.
+*/
+static Bool readRequestfile(const char* serv, Reqserv** rsz)
+{
+  Str           filename;
+  Str           line;
+  FILE*         file;
+  Reqserv*      rs;
+
+  fileRequest(filename, serv);
+  Log_dbg("reading request file %s", filename);
+
+  file = fopen(filename, "r");
+  if (!file && (errno == ENOENT)) {
+    *rsz = newReqserv(serv);
+    (*rsz)->mtime = get_mtime(serv);
+    return TRUE;
+  }
+  if (Log_check(file != 0,
+            "could not open %s for reading: %s", 
+            filename, strerror(errno)))
+    return FALSE;
+
+  rs = *rsz = newReqserv(serv);
+
+  while( readLn(line, file) == TRUE) {
+    char* line1 = Utl_stripWhiteSpace(line);
+    if (*line1)
+      storeMsgId(rs, line1);
+  }
+
+  rs->dirty = FALSE;
+
+  if (Log_check(fclose(file) != EOF, 
+            "could not close %s properly: %s\n", 
+            filename, strerror(errno)))
+    return FALSE;
+
+  return TRUE;
+}
+
+
+/* Write out request file for given Reqserv. Return TRUE on success. If an
+   I/O error occurs, it is logged, and FALSE is returned.
+*/
+static Bool writeRequestfile(Reqserv* rs)
+{
+  Str    filename;
+  FILE*  file;
+  char** z;
+
+  fileRequest(filename, rs->serv);
+  Log_dbg("writing request file %s", filename);
+
+  if (Log_check((file = fopen(filename, "w")) != 0,
+            "could not open %s for writing: %s", 
+            filename, strerror(errno)))
+    return FALSE;
+
+  if (rs->reql)
+    for (z = rs->reql; z < rs->reql+rs->reql_length; z++)
+      if (*z) {
+        if (Log_check(   fputs(*z, file) != EOF
+                      && fputs("\n", file) != EOF,
+                  "write error: %s", strerror(errno)))
+          return FALSE;
+      }
+  
+  if (Log_check(fclose(file) != EOF, 
+                "could not close %s properly: %s\n", 
+                filename, strerror(errno)))
+    return FALSE;
+            
+  rs->dirty = FALSE;
+  rs->mtime = get_mtime(rs->serv);
+
+  return TRUE;
+}
+
+
+void
+Req_remove( const char *serv, const char *msgId )
+{
+    Reqserv* rs;
+    char** z;
+    
+    ASSERT( is_open );
+    Log_dbg("Req_remove(\"%s\", \"%s\")", serv, msgId);
+    
+    if (getReqserv(serv, &rs) == FALSE) 
+        return;
+    
+    z = searchMsgId(rs, msgId);
+    if ( ! z )
+        return;
+    
+    free(*z);
+    *z = 0;
+    rs->dirty = TRUE;
+}
+
+
+Bool
+Req_first( const char *serv, Str msgId )
+{
+  Reqserv* rs;
+
+  ASSERT( is_open );
+  ASSERT( !iterator && !iterator_end );
+
+  if (getReqserv(serv, &rs) == FALSE)
+    return FALSE;
+
+  if (!rs->reql) 
+    return FALSE;
+
+  iterator = rs->reql - 1;
+  iterator_end = rs->reql + rs->reql_length;
+
+  return Req_next(msgId);
+}
+
+
+Bool
+Req_next( Str msgId )
+{
+  ASSERT( is_open );
+  ASSERT(iterator && iterator_end);
+
+  if (iterator >= iterator_end)
+      return FALSE;
+  iterator++;
+
+  while (iterator < iterator_end) {
+    if (!*iterator)
+      iterator++;
+    else {
+      Utl_cpyStr(msgId, *iterator);
+      return TRUE;
+    }
+  }
+
+  iterator = iterator_end = 0;
+  return FALSE;
+}
+
+
+/* Get exclusive access to all request files. Maybe we already have had it,
+   and the cache is outdated. So we delete request files, which have
+   changed recently, from cache. These files will be reread on demand.
+*/
+Bool
+Req_open(void)
+{
+  Log_dbg("opening request database");
+  ASSERT(is_open == FALSE);
+  cleanupReqserv();
+  is_open = TRUE;
+  return TRUE;
+}
+
+
+/* Do not occupy the request files any longer. Write any changes to disk.
+   Return TRUE on success, FALSE if an IO error occurs. */
+void Req_close(void) 
+{
+  Bool ret = TRUE;
+  Reqserv* rs;
+  Log_dbg("closing request database, writing changes to disk");
+  ASSERT(is_open == TRUE);
+
+  for (rs = reqserv; rs; rs = rs->next) {
+    if (rs->dirty == TRUE) {
+      if (!writeRequestfile(rs))
+        ret = FALSE;
+    }
+  }
+
+  is_open = FALSE;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/request.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,51 @@
+/*
+  request.h
+
+  Collection of requested articles.
+
+  $Id: request.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef REQ_H
+#define REQ_H
+
+#include "common.h"
+
+/* Is request for message msgId from server serv already recorded? This
+   function has no error detection facility. On error, FALSE is returned.
+   Nevertheless, errors are logged. */
+Bool
+Req_contains( const char *serv, const char *msgId );
+
+/* Add request for message "msgId" from server "serv". Return TRUE if
+   successful. */ 
+Bool
+Req_add( const char *serv, const char *msgId );
+
+/* Remove request for message msgIg from server serv. This function does
+   not return any errors. Nevertheless, they are logged. */
+void
+Req_remove( const char *serv, const char *msgId );
+
+/* Begin iteration through all messages requested from one server. Return
+   TRUE if there are any requests. Save first message ID in msgId. On
+   error, it is logged, and FALSE is returned.
+*/
+Bool
+Req_first( const char *serv, Str msgId );
+
+/* Continue iteration. Return TRUE on success, FALSE when there are no more
+   requests. Save message ID in msgId. On error, it is logged, and FALSE is
+   returned. */
+Bool
+Req_next( Str msgId );
+
+/* Get exclusive access to the request files. Refresh cache as necessary. */
+Bool 
+Req_open(void);
+
+/* Write changes to disk */
+void
+Req_close(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,1368 @@
+/*
+  server.c
+
+  $Id: server.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "server.h"
+#include <ctype.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+#include "client.h"
+#include "common.h"
+#include "config.h"
+#include "content.h"
+#include "database.h"
+#include "dynamicstring.h"
+#include "fetch.h"
+#include "fetchlist.h"
+#include "group.h"
+#include "lock.h"
+#include "log.h"
+#include "online.h"
+#include "outgoing.h"
+#include "protocol.h"
+#include "pseudo.h"
+#include "request.h"
+#include "util.h"
+
+struct
+{
+    Bool running;
+    int artPtr;
+    Str grp; /* selected group, "" if none */
+} serv = { FALSE, 0, "" };
+
+typedef struct Cmd
+{
+    const char *name;
+    const char *syntax;
+    /* Returns false, if quit cmd */
+    Bool (*cmdProc)( char *arg, const struct Cmd *cmd );
+}
+Cmd;
+
+static Bool doArt( char *arg, const Cmd *cmd );
+static Bool doBody( char *arg, const Cmd *cmd );
+static Bool doGrp( char *arg, const Cmd *cmd );
+static Bool doHead( char *arg, const Cmd *cmd );
+static Bool doHelp( char *arg, const Cmd *cmd );
+static Bool doIhave( char *arg, const Cmd *cmd );
+static Bool doLast( char *arg, const Cmd *cmd );
+static Bool doList( char *arg, const Cmd *cmd );
+static Bool doListgrp( char *arg, const Cmd *cmd );
+static Bool doMode( char *arg, const Cmd *cmd );
+static Bool doNewgrps( char *arg, const Cmd *cmd );
+static Bool doNext( char *arg, const Cmd *cmd );
+static Bool doPost( char *arg, const Cmd *cmd );
+static Bool doSlave( char *arg, const Cmd *cmd );
+static Bool doStat( char *arg, const Cmd *cmd );
+static Bool doQuit( char *arg, const Cmd *cmd );
+static Bool doXhdr( char *arg, const Cmd *cmd );
+static Bool doXpat( char *arg, const Cmd *cmd );
+static Bool doXOver( char *arg, const Cmd *cmd );
+static Bool notImplemented( char *arg, const Cmd *cmd );
+static void putStat( unsigned int stat, const char *fmt, ... );
+
+Cmd commands[] =
+{
+    { "article", "ARTICLE [msg-id|n]", &doArt },
+    { "body", "BODY [msg-id|n]", &doBody },
+    { "head", "HEAD [msg-id|n]", &doHead },
+    { "group", "GROUP grp", &doGrp },
+    { "help", "HELP", &doHelp },
+    { "ihave", "IHAVE (ignored)", &doIhave },
+    { "last", "LAST", &doLast },
+    { "list", "LIST [ACTIVE [pat]]|ACTIVE.TIMES [pat]|"
+      "EXTENSIONS|NEWSGROUPS [pat]|OVERVIEW.FMT", &doList },
+    { "listgroup", "LISTGROUP grp", &doListgrp },
+    { "mode", "MODE (ignored)", &doMode },
+    { "newgroups", "NEWGROUPS [xx]yymmdd hhmmss [GMT]", &doNewgrps },
+    { "newnews", "NEWNEWS (not implemented)", &notImplemented },
+    { "next", "NEXT", &doNext },
+    { "post", "POST", &doPost },
+    { "quit", "QUIT", &doQuit },
+    { "slave", "SLAVE (ignored)", &doSlave },
+    { "stat", "STAT [msg-id|n]", &doStat },
+    { "xhdr", "XHDR over-field [m[-[n]]]", &doXhdr },
+    { "xpat", "XPAT over-field m[-[n]] pat", &doXpat },
+    { "xover", "XOVER [m[-[n]]]", &doXOver }
+};
+
+/*
+  Notice interest in reading this group.
+  Automatically subscribe if option set in config file.
+*/
+static void
+noteInterest( void )
+{
+    FetchMode mode;
+
+    Grp_setLastAccess( serv.grp, time( NULL ) );
+    if ( Cfg_autoSubscribe() && ! Online_true() )
+    {
+        Fetchlist_read();
+        if ( ! Fetchlist_contains( serv.grp ) )
+        {
+            if ( strcmp( Cfg_autoSubscribeMode(), "full" ) == 0 )
+                mode = FULL;
+            else if ( strcmp( Cfg_autoSubscribeMode(), "thread" ) == 0 )
+                mode = THREAD;
+            else
+                mode = OVER;
+            Fetchlist_add( serv.grp, mode );
+            Fetchlist_write();
+            Pseudo_autoSubscribed();
+        }
+    }
+}
+
+static void
+putStat( unsigned int stat, const char *fmt, ... )
+{
+    Str s, line;
+    va_list ap;
+
+    ASSERT( stat <= 999 );
+    va_start( ap, fmt );
+    vsnprintf( s, MAXCHAR, fmt, ap );
+    va_end( ap );
+    snprintf( line, MAXCHAR, "%u %s", stat, s );
+    Log_dbg( "[S] %s", line );
+    printf( "%s\r\n", line );
+}
+
+static void
+putTxtLn( const char *fmt, ... )
+{
+    Str line;
+    va_list ap;
+
+    va_start( ap, fmt );
+    vsnprintf( line, MAXCHAR, fmt, ap );
+    va_end( ap );
+    Prt_putTxtLn( line, stdout );
+}
+
+static void
+putTxtBuf( const char *buf )
+{
+    if ( buf )
+        Prt_putTxtBuf( buf, stdout );
+}
+
+static void
+putEndOfTxt( void )
+{
+    Prt_putEndOfTxt( stdout );
+}
+
+static void
+putSyntax( const Cmd *cmd )
+{
+    putStat( STAT_SYNTAX_ERR, "Syntax error. Usage: %s", cmd->syntax );
+}
+
+static Bool
+getLn( Str line )
+{
+    return Prt_getLn( line, stdin );
+}
+
+static Bool
+getTxtLn( Str line, Bool *err )
+{
+    return Prt_getTxtLn( line, err, stdin );
+}
+
+static Bool
+notImplemented( char *arg, const Cmd *cmd )
+{
+    putStat( STAT_NO_PERMISSION, "Command not implemented" );
+    return TRUE;
+}
+
+static void
+checkNewArts( const char *grp )
+{
+    if ( ! Online_true()
+         || strcmp( grp, serv.grp ) == 0
+         || time( NULL ) - Grp_lastAccess( serv.grp ) < 1800 )
+        return;
+    if ( Fetch_init( Grp_serv( grp ) ) )
+    {
+        Fetch_getNewArts( grp, OVER );
+        Fetch_close();
+    }
+}
+
+static void
+postArts()
+{
+    Str serv;
+
+    Cfg_beginServEnum();
+    while ( Cfg_nextServ( serv ) )
+        if ( Fetch_init( serv ) )
+        {
+            Fetch_postArts();
+            Fetch_close();
+        }
+}
+
+static void
+readCont( const char *name )
+{
+    Fetchlist_read();
+    Cont_read( name );
+    if ( ! Fetchlist_contains( name ) && ! Online_true() )
+    { 
+        Pseudo_appGeneralInfo();
+        Grp_setFirstLast( name, Cont_first(), Cont_last() );
+    }
+}
+
+static void
+changeToGrp( const char *grp )
+{
+    checkNewArts( grp );
+    Utl_cpyStr( serv.grp, grp );
+    readCont( grp );
+    serv.artPtr = Cont_first();
+}
+
+static Bool
+doGrp( char *arg, const Cmd *cmd )
+{
+    int first, last, numb;
+
+    if ( arg[ 0 ] == '\0' )
+        putSyntax( cmd );
+    else if ( ! Grp_exists( arg ) )
+        putStat( STAT_NO_SUCH_GRP, "No such group" );
+    else
+    {
+        changeToGrp( arg );
+        first = Cont_first();
+        last = Cont_last();
+        numb = last - first + 1;
+        if ( first > last )
+            first = last = numb = 0;
+        putStat( STAT_GRP_SELECTED, "%lu %lu %lu %s selected",
+                 numb, first, last, arg );
+    }
+    return TRUE;
+}
+
+static Bool
+testGrpSelected( void )
+{
+    if ( *serv.grp == '\0' )
+    {
+        putStat( STAT_NO_GRP_SELECTED, "No group selected" );
+        return FALSE;
+    }
+    return TRUE;
+}
+
+static void
+findServ( const char *msgId, Str result )
+{
+    const char *p, *pColon, *serv;
+    Str s, grp;
+
+    Utl_cpyStr( result, "(unknown)" );
+    if ( Db_contains( msgId ) )
+    {
+        Utl_cpyStr( s, Db_xref( msgId ) );
+        p = strtok( s, " \t" );
+        if ( p )
+            do
+            {
+                pColon = strstr( p, ":" );
+                if ( pColon )
+                {
+                    Utl_cpyStrN( grp, p, pColon - p );
+                    serv = Grp_serv( grp );
+                    if ( Cfg_servIsPreferential( serv, result ) )
+                        Utl_cpyStr( result, serv );
+                }
+            }
+            while ( ( p = strtok( NULL, " \t" ) ) );
+    }
+}
+
+static Bool
+retrieveArt( const char *msgId )
+{
+    Str serv;
+
+    findServ( msgId, serv );    
+    if ( strcmp( serv, "(unknown)" ) == 0 )
+        return FALSE;        
+    if ( ! Client_connect( serv ) )
+        return FALSE;
+    Client_retrieveArt( msgId );
+    Client_disconnect();
+    return TRUE;
+}
+
+static Bool
+checkNumb( int numb )
+{
+    if ( ! testGrpSelected() )
+        return FALSE;
+    if ( ! Cont_validNumb( numb ) )
+    {
+        putStat( STAT_NO_SUCH_NUMB, "No such article" );
+        return FALSE;
+    }
+    return TRUE;
+}
+
+/*
+  Parse arguments for ARTICLE, BODY, HEAD, STAT commands.
+  Return message-ID and article number (0 if unknown).
+*/
+static Bool
+whichId( const char **msgId, int *numb, char *arg )
+{
+    const Over *ov;
+    int n;
+
+    if ( sscanf( arg, "%i", &n ) == 1 )
+    {
+        if ( ! checkNumb( n ) )
+            return FALSE;
+        serv.artPtr = n;
+        ov = Cont_get( n );
+        *msgId = Ov_msgId( ov );
+        *numb = n;
+    }
+    else if ( strcmp( arg, "" ) == 0 )
+    {
+        if ( ! checkNumb( serv.artPtr ) )
+            return FALSE;
+        ov = Cont_get( serv.artPtr );
+        *msgId = Ov_msgId( ov );
+        *numb =  serv.artPtr;
+    }
+    else
+    {
+        *msgId = arg;
+        *numb = 0;
+    }
+    if ( ! Pseudo_isGeneralInfo( *msgId ) && ! Db_contains( *msgId ) )
+    {
+        putStat( STAT_NO_SUCH_NUMB, "No such article" );
+        return FALSE;
+    }
+    return TRUE;
+}
+
+void
+touchArticle( const char *msgId )
+{
+    int stat = Db_stat( msgId );
+    stat |= DB_INTERESTING;
+    Db_setStat( msgId, stat );
+    Db_updateLastAccess( msgId );
+}
+
+static void
+touchReferences( const char *msgId )
+{
+    Str s;
+    int len;
+    char *p;
+    const char *ref = Db_ref( msgId );
+
+    while ( TRUE )
+    {
+        p = s;
+        while ( *ref != '<' )
+            if ( *(ref++) == '\0' )
+                return;
+        len = 0;
+        while ( *ref != '>' )
+        {
+            if ( *ref == '\0' || ++len >= MAXCHAR - 1 )
+                return;
+            *(p++) = *(ref++);
+        }
+        *(p++) = '>';
+        *p = '\0';
+        if ( Db_contains( s ) )
+            touchArticle( s );
+    }
+}
+
+static void
+doBodyInDb( const char *msgId )
+{
+    int stat;
+    Str serv;
+
+    touchArticle( msgId );
+    touchReferences( msgId );
+    stat = Db_stat( msgId );
+    if ( Online_true() && ( stat & DB_NOT_DOWNLOADED ) )
+    {
+        retrieveArt( msgId );
+        stat = Db_stat( msgId );
+    }
+    if ( stat & DB_RETRIEVING_FAILED )
+    {
+        Db_setStat( msgId, stat & ~DB_RETRIEVING_FAILED );
+        putTxtBuf( Db_body( msgId ) );
+    }
+    else if ( stat & DB_NOT_DOWNLOADED )
+    {
+        findServ( msgId, serv );
+        if ( Req_contains( serv, msgId ) )
+            putTxtBuf( Pseudo_alreadyMarkedBody() );
+        else if ( strcmp( serv, "(unknown)" ) != 0 && Req_add( serv, msgId ) )
+            putTxtBuf( Pseudo_markedBody() );
+        else
+            putTxtBuf( Pseudo_markingFailedBody() );
+    }
+    else
+        putTxtBuf( Db_body( msgId ) );
+}
+
+static Bool
+doBody( char *arg, const Cmd *cmd )
+{
+    const char *msgId;
+    int numb;
+    
+    if ( ! whichId( &msgId, &numb, arg ) )
+        return TRUE;
+    putStat( STAT_BODY_FOLLOWS, "%ld %s Body", numb, msgId );
+    if ( Pseudo_isGeneralInfo( msgId ) )
+        putTxtBuf( Pseudo_generalInfoBody() );
+    else
+        doBodyInDb( msgId );
+    putEndOfTxt();
+    noteInterest();
+    return TRUE;
+}
+
+static void
+doHeadInDb( const char *msgId )
+{
+    putTxtBuf( Db_header( msgId ) );
+}
+
+static Bool
+doHead( char *arg, const Cmd *cmd )
+{
+    const char *msgId;
+    int numb;
+    
+    if ( ! whichId( &msgId, &numb, arg ) )
+        return TRUE;
+    putStat( STAT_HEAD_FOLLOWS, "%ld %s Head", numb, msgId );
+    if ( Pseudo_isGeneralInfo( msgId ) )
+        putTxtBuf( Pseudo_generalInfoHead() );
+    else
+        doHeadInDb( msgId );
+    putEndOfTxt();
+    return TRUE;
+}
+
+static void
+doArtInDb( const char *msgId )
+{
+    doHeadInDb( msgId );
+    putTxtLn( "" );
+    doBodyInDb( msgId );
+}
+
+static Bool
+doArt( char *arg, const Cmd *cmd )
+{
+    const char *msgId;
+    int numb;
+    
+    if ( ! whichId( &msgId, &numb, arg ) )
+        return TRUE;
+    putStat( STAT_ART_FOLLOWS, "%ld %s Article", numb, msgId );
+    if ( Pseudo_isGeneralInfo( msgId ) )
+    {
+        putTxtBuf( Pseudo_generalInfoHead() );
+        putTxtLn( "" );
+        putTxtBuf( Pseudo_generalInfoBody() );
+    }
+    else
+        doArtInDb( msgId );
+    putEndOfTxt();
+    noteInterest();
+    return TRUE;
+}
+
+static Bool
+doHelp( char *arg, const Cmd *cmd )
+{
+    unsigned int i;
+
+    putStat( STAT_HELP_FOLLOWS, "Help" );
+    putTxtBuf( "\nCommands:\n\n" );
+    for ( i = 0; i < sizeof( commands ) / sizeof( commands[ 0 ] ); ++i )
+        putTxtLn( "%s", commands[ i ].syntax );
+    putEndOfTxt();
+    return TRUE;
+}
+
+static Bool
+doIhave( char *arg, const Cmd *cmd )
+{
+    putStat( STAT_ART_REJECTED, "Command not used" );
+    return TRUE;
+}
+
+static Bool
+doLast( char *arg, const Cmd *cmd )
+{
+    int n;
+
+    if ( testGrpSelected() )
+    {
+        n = serv.artPtr;
+        if ( ! Cont_validNumb( n ) )
+            putStat( STAT_NO_ART_SELECTED, "No article selected" );
+        else
+        {
+            while ( ! Cont_validNumb( --n ) && n >= Cont_first() );
+            if ( ! Cont_validNumb( n ) )
+                putStat( STAT_NO_PREV_ART, "No previous article" );
+            else
+            {
+                putStat( STAT_ART_RETRIEVED, "%ld %s selected",
+                         n, Ov_msgId( Cont_get( n ) ) );
+                serv.artPtr = n;
+            }
+        }
+    }
+    return TRUE;
+}
+
+static void
+printGroups( const char *pat, void (*printProc)( Str, const char* ) )
+{
+    Str line;
+    const char *g;
+    FILE *f;
+    sig_t lastHandler;
+    int ret;
+
+    putStat( STAT_GRPS_FOLLOW, "Groups" );
+    fflush( stdout );
+    Log_dbg( "[S FLUSH]" );
+    if ( Grp_exists( pat ) )
+    {
+        (*printProc)( line, pat );
+        if ( ! Prt_putTxtLn( line, stdout ) )
+            Log_err( "Writing to stdout failed." );
+    }                    
+    else
+    {
+        lastHandler = signal( SIGPIPE, SIG_IGN );
+        f = popen( "sort", "w" );
+        if ( f == NULL )
+        {
+            Log_err( "Cannot open pipe to 'sort'" );
+            if ( Grp_firstGrp( &g ) )
+                do
+                    if ( Utl_matchPattern( g, pat ) )
+                    {
+                        (*printProc)( line, g );
+                        if ( ! Prt_putTxtLn( line, stdout ) )
+                            Log_err( "Writing to stdout failed." );
+                    }
+                while ( Grp_nextGrp( &g ) );
+        }
+        else
+        {
+            if ( Grp_firstGrp( &g ) )
+                do
+                    if ( Utl_matchPattern( g, pat ) )
+                    {
+                        (*printProc)( line, g );
+                        if ( ! Prt_putTxtLn( line, f ) )
+                        {
+                            Log_err( "Writing to 'sort' pipe failed." );
+                            break;
+                        }                    
+                    }
+                while ( Grp_nextGrp( &g ) );
+            ret = pclose( f );
+            if ( ret != EXIT_SUCCESS )
+                Log_err( "sort command returned %i", ret );
+            fflush( stdout );
+            Log_dbg( "[S FLUSH]" );
+            signal( SIGPIPE, lastHandler );
+        }
+    }
+    putEndOfTxt();
+}
+
+static void
+printActiveTimes( Str result, const char *grp )
+{
+    snprintf( result, MAXCHAR, "%s %ld", grp, Grp_created( grp ) );
+}
+
+static void
+doListActiveTimes( const char *pat )
+{
+    printGroups( pat, &printActiveTimes );
+}
+
+static void
+printActive( Str result, const char *grp )
+{
+    snprintf( result, MAXCHAR, "%s %i %i y",
+              grp, Grp_last( grp ), Grp_first( grp ) );
+}
+
+static void
+doListActive( const char *pat )
+{
+    printGroups( pat, &printActive );
+}
+
+static void
+printNewsgrp( Str result, const char *grp )
+{
+    snprintf( result, MAXCHAR, "%s %s", grp, Grp_dsc( grp ) );
+}
+
+static void
+doListNewsgrps( const char *pat )
+{
+    printGroups( pat, &printNewsgrp );
+}
+
+static void
+putGrp( const char *name )
+{
+    putTxtLn( "%s %lu %lu y", name, Grp_last( name ), Grp_first( name ) );
+}
+
+static void
+doListOverFmt( void )
+{
+    putStat( STAT_GRPS_FOLLOW, "Overview format" );
+    putTxtBuf( "Subject:\n"
+               "From:\n"
+               "Date:\n"
+               "Message-ID:\n"
+               "References:\n"
+               "Bytes:\n"
+               "Lines:\n" );
+    putEndOfTxt();
+}
+
+static void
+doListExtensions( void )
+{
+    putStat( STAT_CMD_OK, "Extensions" );
+    putTxtBuf( " LISTGROUP\n"
+               " XOVER\n" );
+    putEndOfTxt();    
+}
+
+static Bool
+doList( char *line, const Cmd *cmd )
+{
+    Str s, arg;
+    const char *pat;
+
+    if ( sscanf( line, "%s", s ) != 1 )
+        doListActive( "*" );
+    else
+    {
+        Utl_toLower( s );
+        strcpy( arg, Utl_restOfLn( line, 1 ) );
+        pat = Utl_stripWhiteSpace( arg );
+        if ( pat[ 0 ] == '\0' )
+            pat = "*";
+        if ( strcmp( "active", s ) == 0 )
+            doListActive( pat );
+        else if ( strcmp( "overview.fmt", s ) == 0 )
+            doListOverFmt();
+        else if ( strcmp( "newsgroups", s ) == 0 )
+            doListNewsgrps( pat );
+        else if ( strcmp( "active.times", s ) == 0 )
+            doListActiveTimes( pat );
+        else if ( strcmp( "extensions", s ) == 0 )
+            doListExtensions();
+        else
+            putSyntax( cmd );
+    }
+    return TRUE;
+}
+
+static Bool
+doListgrp( char *arg, const Cmd *cmd )
+{
+    const Over *ov;
+    int first, last, i;
+
+    if ( ! Grp_exists( arg ) )
+        putStat( STAT_NO_SUCH_GRP, "No such group" );
+    else
+    {
+        changeToGrp( arg );
+        first = Cont_first();
+        last = Cont_last();
+        putStat( STAT_GRP_SELECTED, "Article list" );
+        for ( i = first; i <= last; ++i )
+            if ( ( ov = Cont_get( i ) ) )
+                putTxtLn( "%lu", i );
+        putEndOfTxt();
+    }
+    return TRUE;
+}
+
+static Bool
+doMode( char *arg, const Cmd *cmd )
+{
+    putStat( STAT_READY_POST_ALLOW, "Ok" );
+    return TRUE;
+}
+
+static unsigned long
+getTimeInSeconds( unsigned int year, unsigned int mon, unsigned int day,
+                  unsigned int hour, unsigned int min, unsigned int sec )
+{
+    struct tm t = { 0 };
+
+    t.tm_year = year - 1900;
+    t.tm_mon = mon - 1;
+    t.tm_mday = day;
+    t.tm_hour = hour;
+    t.tm_min = min;
+    t.tm_sec = sec;
+    return mktime( &t );
+}
+
+
+static Bool
+doNewgrps( char *arg, const Cmd *cmd )
+{
+    time_t t, now, lastUpdate;
+    unsigned int year, mon, day, hour, min, sec, cent, len;
+    const char *g;
+    Str date, timeofday, file;
+
+    if ( sscanf( arg, "%s %s", date, timeofday ) != 2 ) 
+    {
+        putSyntax( cmd );
+        return TRUE;
+    }
+    len = strlen( date );
+    switch ( len )
+    {
+    case 6:
+        if ( sscanf( date, "%2u%2u%2u", &year, &mon, &day ) != 3 )
+        {
+            putSyntax( cmd );
+            return TRUE;
+        }
+        now = time( NULL );
+        cent = 1900;
+        while ( now > getTimeInSeconds( cent + 100, 1, 1, 0, 0, 0 ) )
+            cent += 100;
+        year += cent;
+        break;
+    case 8:
+        if ( sscanf( date, "%4u%2u%2u", &year, &mon, &day ) != 3 )
+        {
+            putSyntax( cmd );
+            return TRUE;
+        }
+        break;
+    default:
+        putSyntax( cmd );
+        return TRUE;
+    }
+    if ( sscanf( timeofday, "%2u%2u%2u", &hour, &min, &sec ) != 3 )
+    {
+        putSyntax( cmd );
+        return TRUE;
+    }
+    if ( year < 1970 || mon == 0 || mon > 12 || day == 0 || day > 31
+         || hour > 23 || min > 59 || sec > 60 )
+    {
+        putSyntax( cmd );
+        return TRUE;
+    }
+    snprintf( file, MAXCHAR, "%s/groupinfo.lastupdate", Cfg_spoolDir() );
+    t = getTimeInSeconds( year, mon, day, hour, min, sec );
+    putStat( STAT_NEW_GRP_FOLLOW, "New groups since %s", arg );
+
+    if ( ! Utl_getStamp( &lastUpdate, file ) || t <= lastUpdate )
+    {
+        if ( Grp_firstGrp( &g ) )
+            do
+                if ( Grp_created( g ) > t )
+                    putGrp( g );
+            while ( Grp_nextGrp( &g ) );
+    }
+    putEndOfTxt();
+    return TRUE;
+}
+
+static Bool
+doNext( char *arg, const Cmd *cmd )
+{
+    int n;
+
+    if ( testGrpSelected() )
+    {
+        n = serv.artPtr;
+        if ( ! Cont_validNumb( n ) )
+            putStat( STAT_NO_ART_SELECTED, "No article selected" );
+        else
+        {
+            while ( ! Cont_validNumb( ++n ) && n <= Cont_last() );
+            if ( ! Cont_validNumb( n ) )
+                putStat( STAT_NO_NEXT_ART, "No next article" );
+            else
+            {
+                putStat( STAT_ART_RETRIEVED, "%ld %s selected",
+                         n, Ov_msgId( Cont_get( n ) ) );
+                serv.artPtr = n;
+            }
+        }
+    }
+    return TRUE;
+}
+
+/*
+  Get first group of the Newsgroups field content, which is
+  a comma separated list of groups.
+*/
+static void
+getFirstGrp( char *grpResult, const char *list )
+{
+    Str t;
+    const char *src = list;
+    char *dest = t;
+    while( TRUE )
+    {
+        if ( *src == ',' )
+            *dest = ' ';
+        else
+            *dest = *src;
+        if ( *src == '\0' )
+            break;
+        ++src;
+        ++dest;
+    }
+    *grpResult = '\0';
+    sscanf( t, "%s", grpResult );
+}
+
+static Bool
+doPost( char *arg, const Cmd *cmd )
+{
+    Bool err, replyToFound, inHeader;
+    DynStr *s;
+    Str line, field, val, msgId, from, grp;
+    const char* p;
+
+    /*
+      Get article and make following changes to the header:
+      - add/replace/cut Message-ID depending on config options
+      - add Reply-To with content of From, if missing
+      (some providers overwrite From field)
+      - rename X-Sender header to X-NOFFLE-X-Sender
+      (some providers want to insert their own X-Sender)
+    */
+    putStat( STAT_SEND_ART, "Continue (end with period)" );
+    fflush( stdout );
+    Log_dbg( "[S FLUSH]" );
+    s = new_DynStr( 10000 );
+    msgId[ 0 ] = '\0';
+    from[ 0 ] = '\0';
+    grp[ 0 ] = '\0';
+    replyToFound = FALSE;
+    inHeader = TRUE;
+    while ( getTxtLn( line, &err ) )
+    {
+        if ( inHeader )
+        {
+            p = Utl_stripWhiteSpace( line );
+            if ( *p == '\0' )
+            {
+                inHeader = FALSE;
+                if ( from[ 0 ] == '\0' )
+                    Log_err( "Posted message has no From field" );
+                if ( ! Cfg_removeMsgId() )
+                {
+                    if ( Cfg_replaceMsgId() )
+                    {
+                        Prt_genMsgId( msgId, from, "NOFFLE" );
+                        Log_dbg( "Replacing Message-ID with '%s'", msgId );
+                    }
+                    else if ( msgId[ 0 ] == '\0' )
+                    {
+                        Prt_genMsgId( msgId, from, "NOFFLE" );
+                        Log_inf( "Adding missing Message-ID '%s'", msgId );
+                    }
+                    else if ( ! Prt_isValidMsgId( msgId ) )
+                    {
+                        Log_ntc( "Replacing invalid Message-ID with '%s'",
+                                 msgId );
+                        Prt_genMsgId( msgId, from, "NOFFLE" );
+                    }
+                    DynStr_app( s, "Message-ID: " );
+                    DynStr_appLn( s, msgId );
+                }
+                if ( ! replyToFound && from[ 0 ] != '\0' )
+                {
+                    Log_dbg( "Adding Reply-To field to posted message." );
+                    DynStr_app( s, "Reply-To: " );
+                    DynStr_appLn( s, from );
+                }
+                DynStr_appLn( s, p );
+            }
+            else if ( Prt_getField( field, val, p ) )
+            {
+                if ( strcmp( field, "message-id" ) == 0 )
+                    strcpy( msgId, val );
+                else if ( strcmp( field, "from" ) == 0 )
+                {
+                    strcpy( from, val );
+                    DynStr_appLn( s, p );
+                }
+                else if ( strcmp( field, "newsgroups" ) == 0 )
+                {
+                    getFirstGrp( grp, val );
+                    Utl_toLower( grp );
+                    DynStr_appLn( s, p );
+                }
+                else if ( strcmp( field, "reply-to" ) == 0 )
+                {
+                    replyToFound = TRUE;
+                    DynStr_appLn( s, p );
+                }
+                else if ( strcmp( field, "x-sender" ) == 0 )
+                {
+                    DynStr_app( s, "X-NOFFLE-X-Sender: " );
+                    DynStr_appLn( s, val );
+                }
+                else
+                    DynStr_appLn( s, p );
+            }
+            else
+                Log_err( "Ignoring invalid header line '%s'", p );
+        }
+        else
+            DynStr_appLn( s, line );
+    }
+    if ( inHeader )
+        Log_err( "Posted message has no body" );
+    if ( ! err )
+    {
+        if ( grp[ 0 ] == '\0' )
+        {
+            Log_err( "Posted message has no Newsgroups header field" );
+            err = TRUE;
+        }
+        else if ( ! Grp_exists( grp ) )
+        {    
+            Log_err( "Unknown group in Newsgroups header field" );
+            err = TRUE;
+        }
+        else if ( ! Out_add( Grp_serv( grp ), msgId, s ) )
+        {
+            Log_err( "Cannot add posted article to outgoing directory" );
+            err = TRUE;
+        }
+    }
+    if ( err )
+        putStat( STAT_POST_FAILED, "Posting failed" );
+    else
+    {
+        putStat( STAT_POST_OK, "Message queued for posting" );
+        if ( Online_true() )
+            postArts();
+    }
+    del_DynStr( s );
+    return TRUE;
+}
+
+static void
+parseRange( const char *s, int *first, int *last, int *numb )
+{
+    int r, i;
+    char* p;
+    Str t;
+
+    Utl_cpyStr( t, s );
+    p = Utl_stripWhiteSpace( t );
+    r = sscanf( p, "%i-%i", first, last );
+    if ( r < 1 )
+    {
+        *first = serv.artPtr;
+        *last = serv.artPtr;
+    }
+    else if ( r == 1 )
+    {
+        if ( p[ strlen( p ) - 1 ] == '-' )
+            *last = Cont_last();
+        else
+            *last = *first;
+    }    
+    if ( *first < Cont_first() )
+        *first = Cont_first();
+    if ( *last > Cont_last() )
+        *last = Cont_last();
+    if ( *first > Cont_last() ||  *last < Cont_first() )
+        *last = *first - 1;
+    *numb = 0;
+    for ( i = *first; i <= *last; ++i )
+        if ( Cont_validNumb( i ) )
+            ++(*numb);
+}
+
+static Bool
+doXhdr( char *arg, const Cmd *cmd )
+{
+    int first, last, i, n, numb;
+    enum { SUBJ, FROM, DATE, MSG_ID, REF, BYTES, LINES } what;
+    const char *p;
+    const Over *ov;
+    Str whatStr;
+
+    if ( ! testGrpSelected() )
+        return TRUE;
+    if ( sscanf( arg, "%s", whatStr ) != 1 )
+    {
+        putSyntax( cmd );
+        return TRUE;
+    }
+    Utl_toLower( whatStr );
+    if ( strcmp( whatStr, "subject" ) == 0 )
+        what = SUBJ;
+    else if ( strcmp( whatStr, "from" ) == 0 )
+        what = FROM;
+    else if ( strcmp( whatStr, "date" ) == 0 )
+        what = DATE;
+    else if ( strcmp( whatStr, "message-id" ) == 0 )
+        what = MSG_ID;
+    else if ( strcmp( whatStr, "references" ) == 0 )
+        what = REF;
+    else if ( strcmp( whatStr, "bytes" ) == 0 )
+        what = BYTES;
+    else if ( strcmp( whatStr, "lines" ) == 0 )
+        what = LINES;
+    else
+    {
+        putStat( STAT_HEAD_FOLLOWS, "Unknown header (empty list follows)" );
+        putEndOfTxt();
+        return TRUE;
+    }
+    p = Utl_restOfLn( arg, 1 );
+    parseRange( p, &first, &last, &numb );
+    if ( numb == 0 )
+        putStat( STAT_NO_ART_SELECTED, "No articles selected" );
+    else
+    {
+        putStat( STAT_HEAD_FOLLOWS, "%s header %lu-%lu",
+                 whatStr, first, last ) ;
+        for ( i = first; i <= last; ++i )
+            if ( ( ov = Cont_get( i ) ) )
+            {
+                n = Ov_numb( ov );
+                switch ( what )
+                {
+                case SUBJ:
+                    putTxtLn( "%lu %s", n, Ov_subj( ov ) );
+                    break;
+                case FROM:
+                    putTxtLn( "%lu %s", n, Ov_from( ov ) );
+                    break;
+                case DATE:
+                    putTxtLn( "%lu %s", n, Ov_date( ov ) );
+                    break;
+                case MSG_ID:
+                    putTxtLn( "%lu %s", n, Ov_msgId( ov ) );
+                    break;
+                case REF:
+                    putTxtLn( "%lu %s", n, Ov_ref( ov ) );
+                    break;
+                case BYTES:
+                    putTxtLn( "%lu %d", n, Ov_bytes( ov ) );
+                    break;
+                case LINES:
+                    putTxtLn( "%lu %d", n, Ov_lines( ov ) );
+                    break;
+                default:
+                    ASSERT( FALSE );
+                }
+            }
+        putEndOfTxt();
+    }
+    return TRUE;
+}
+
+static Bool
+doXpat( char *arg, const Cmd *cmd )
+{
+    int first, last, i, n;
+    enum { SUBJ, FROM, DATE, MSG_ID, REF } what;
+    const Over *ov;
+    Str whatStr, pat;
+
+    if ( ! testGrpSelected() )
+        return TRUE;
+    if ( sscanf( arg, "%s %i-%i %s", whatStr, &first, &last, pat ) != 4 )
+    {
+        if ( sscanf( arg, "%s %i- %s", whatStr, &first, pat ) == 3 )
+            last = Cont_last();
+        else if ( sscanf( arg, "%s %i %s", whatStr, &first, pat ) == 3 )
+            last = first;
+        else
+        {
+            putSyntax( cmd );
+            return TRUE;
+        }
+    }
+    Utl_toLower( whatStr );
+    if ( strcmp( whatStr, "subject" ) == 0 )
+        what = SUBJ;
+    else if ( strcmp( whatStr, "from" ) == 0 )
+        what = FROM;
+    else if ( strcmp( whatStr, "date" ) == 0 )
+        what = DATE;
+    else if ( strcmp( whatStr, "message-id" ) == 0 )
+        what = MSG_ID;
+    else if ( strcmp( whatStr, "references" ) == 0 )
+        what = REF;
+    else
+    {
+        putStat( STAT_HEAD_FOLLOWS, "invalid header (empty list follows)" );
+        putEndOfTxt();
+        return TRUE;
+    }
+    putStat( STAT_HEAD_FOLLOWS, "header" ) ;
+    for ( i = first; i <= last; ++i )
+        if ( ( ov = Cont_get( i ) ) )
+        {
+            n = Ov_numb( ov );
+            switch ( what )
+            {
+            case SUBJ:
+                if ( Utl_matchPattern( Ov_subj( ov ), pat ) )
+                     putTxtLn( "%lu %s", n, Ov_subj( ov ) );
+                break;
+            case FROM:
+                if ( Utl_matchPattern( Ov_from( ov ), pat ) )
+                    putTxtLn( "%lu %s", n, Ov_from( ov ) );
+                break;
+            case DATE:
+                if ( Utl_matchPattern( Ov_date( ov ), pat ) )
+                    putTxtLn( "%lu %s", n, Ov_date( ov ) );
+                break;
+            case MSG_ID:
+                if ( Utl_matchPattern( Ov_msgId( ov ), pat ) )
+                    putTxtLn( "%lu %s", n, Ov_msgId( ov ) );
+                break;
+            case REF:
+                if ( Utl_matchPattern( Ov_ref( ov ), pat ) )
+                    putTxtLn( "%lu %s", n, Ov_ref( ov ) );
+                break;
+            default:
+                ASSERT( FALSE );
+            }
+        }
+    putEndOfTxt();
+    return TRUE;
+}
+
+static Bool
+doSlave( char *arg, const Cmd *cmd )
+{
+    putStat( STAT_CMD_OK, "Ok" );
+    return TRUE;
+}
+
+static Bool
+doStat( char *arg, const Cmd *cmd )
+{
+    const char *msgId;
+    int numb;
+    
+    if ( ! whichId( &msgId, &numb, arg ) )
+        return TRUE;
+    if ( numb > 0 )
+        putStat( STAT_ART_RETRIEVED, "%ld %s selected",
+                 numb, msgId );
+    else
+        putStat( STAT_ART_RETRIEVED, "0 %s selected", msgId );
+    return TRUE;
+}
+
+static Bool
+doQuit( char *arg, const Cmd *cmd )
+{
+    putStat( STAT_GOODBYE, "Goodbye" );
+    return FALSE;
+}
+
+static Bool
+doXOver( char *arg, const Cmd *cmd )
+{
+    int first, last, i, n;
+    const Over *ov;
+
+    if ( ! testGrpSelected() )
+        return TRUE;
+    parseRange( arg, &first, &last, &n );
+    if ( n == 0 )
+        putStat( STAT_NO_ART_SELECTED, "No articles selected" );
+    else
+    {
+        putStat( STAT_OVERS_FOLLOW, "Overview %ld-%ld", first, last );
+        for ( i = first; i <= last; ++i )
+            if ( ( ov = Cont_get( i ) ) )
+                putTxtLn( "%lu\t%s\t%s\t%s\t%s\t%s\t%d\t%d\t",
+                          Ov_numb( ov ), Ov_subj( ov ), Ov_from( ov ),
+                          Ov_date( ov ), Ov_msgId( ov ), Ov_ref( ov ),
+                          Ov_bytes( ov ), Ov_lines( ov ) );
+        putEndOfTxt();
+    }
+    return TRUE;
+}
+
+static void
+putFatal( const char *fmt, ... )
+{
+    va_list ap;
+    Str s;
+
+    va_start( ap, fmt );
+    vsnprintf( s, MAXCHAR, fmt, ap );
+    va_end( ap );
+    Log_err( s );
+    putStat( STAT_PROGRAM_FAULT, "%s", s );
+    fflush( stdout );
+    Log_dbg( "[S FLUSH]" );
+}
+
+/* Parse line, execute command and return FALSE, if it was the quit command. */
+static Bool
+parseAndExecute( Str line )
+{
+    unsigned int i, n;
+    Cmd *c;
+    Str s, arg;
+    Bool ret;
+
+    if ( sscanf( line, "%s", s ) == 1 )
+    {
+        Utl_toLower( s );
+        strcpy( arg, Utl_restOfLn( line, 1 ) );
+        n = sizeof( commands ) / sizeof( commands[ 0 ] );
+        for ( i = 0, c = commands; i < n; ++i, ++c )
+            if ( strcmp( c->name, s ) == 0 )
+            {
+                ret = c->cmdProc( Utl_stripWhiteSpace( arg ), c );
+                fflush( stdout );
+                Log_dbg( "[S FLUSH]" );
+                return ret;
+            }
+    }
+    putStat( STAT_NO_SUCH_CMD, "Command not recognized" );
+    fflush( stdout );
+    Log_dbg( "[S FLUSH]" );
+    return TRUE;
+}
+
+static void
+putWelcome( void )
+{
+    putStat( STAT_READY_POST_ALLOW, "NNTP server NOFFLE %s",
+             Cfg_version() );
+    fflush( stdout );
+    Log_dbg( "[S FLUSH]" );
+}
+
+static Bool
+initServ( void )
+{
+    ASSERT( ! serv.running );
+    if ( ! Lock_openDatabases() )
+      return FALSE;
+    serv.running = TRUE;
+    return TRUE;
+}
+
+static void
+closeServ( void )
+{
+    ASSERT( serv.running );
+    serv.running = FALSE;
+    Lock_closeDatabases();
+}
+
+void
+Serv_run( void )
+{
+    Bool done;
+    int r;
+    Str line;
+    struct timeval timeOut;
+    fd_set readSet;
+
+    putWelcome();
+    done = FALSE;
+    while ( ! done )
+    {
+        FD_ZERO( &readSet );
+        FD_SET( STDIN_FILENO, &readSet );
+        /* Never hold lock more than 5 seconds (empirically good value,
+           avoids to close/open databases, if clients sends several
+           commands, but releases the lock often enough, for allowing
+           multiple persons to read news at the same time) */
+        timeOut.tv_sec = 5;
+        timeOut.tv_usec = 0;
+        r = select( STDIN_FILENO + 1, &readSet, NULL, NULL, &timeOut );
+        if ( r < 0 )
+            done = TRUE;
+        else if ( r == 0 )
+        {
+            if ( serv.running )
+                closeServ();
+        }
+        else /* ( r > 0 ) */
+        {
+            if ( ! serv.running )
+            {
+                if ( ! initServ() )
+                {
+                    putFatal( "Cannot init server" );
+                    done = TRUE;
+                }
+            }
+            if ( ! getLn( line ) )
+            {
+                Log_inf( "Client disconnected. Terminating." );
+                done = TRUE;
+            }
+            else if ( ! parseAndExecute( line ) )
+                done = TRUE;
+        }
+    }
+    if ( serv.running )
+        closeServ();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,14 @@
+/*
+  server.h
+
+  Be NNTP server on stdin/stdout.
+
+  $Id: server.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef SERV_H
+#define SERV_H
+
+void Serv_run( void );
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,198 @@
+/*
+  util.c
+
+  $Id: util.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "util.h"
+#include <errno.h>
+#include <ctype.h>
+#include <fnmatch.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <time.h>
+#include <unistd.h>
+#include "config.h"
+#include "log.h"
+
+static const char *
+nextWhiteSpace( const char *p )
+{
+    while ( *p && ! isspace( *p ) )
+        ++p;
+    return p;
+}
+
+static const char *
+nextNonWhiteSpace( const char *p )
+{
+    while ( *p && isspace( *p ) )
+        ++p;
+    return p;
+}
+
+const char *
+Utl_restOfLn( const char *line, unsigned int token )
+{
+    unsigned int i;
+    const char *p;
+
+    p = line;
+    for ( i = 0; i < token; ++i )
+    {
+        p = nextNonWhiteSpace( p );
+        p = nextWhiteSpace( p );
+    }
+    p = nextNonWhiteSpace( p );
+    return p;
+}
+
+const char *
+Utl_getLn( Str result, const char *pos )
+{
+    int len = 0;
+    const char *p = pos;
+
+    if ( ! p )
+        return NULL;
+    while ( *p != '\n' )
+    {
+        if ( *p == '\0' )
+        {
+            if ( len > 0 )
+                Log_err( "Line not terminated by newline: '%s'", pos );
+            return NULL;
+        }
+        *(result++) = *(p++);
+        ++len;
+        if ( len >= MAXCHAR - 1 )
+        {
+            *result = '\0';
+            Log_err( "Utl_getLn: line too long: %s", result );
+            return ++p;
+        }
+    }
+    *result = '\0';
+    return ++p;
+
+}
+
+const char *
+Utl_ungetLn( const char *str, const char *p )
+{
+    if ( str == p )
+        return FALSE;
+    --p;
+    if ( *p != '\n' )
+    {
+        Log_dbg( "Utl_ungetLn: not at beginning of line" );
+        return NULL;
+    }
+    --p;
+    while ( TRUE )
+    {
+        if ( p == str )
+            return p;
+        if ( *p == '\n' )
+            return p + 1;
+        --p;
+    }
+}
+
+void
+Utl_toLower( Str line )
+{
+    char *p;
+
+    p = line;
+    while ( *p )
+    {
+        *p = tolower( *p );
+        ++p;
+    }
+}
+
+char *
+Utl_stripWhiteSpace( char *line )
+{
+    char *p;
+
+    while ( isspace( *line ) )
+        ++line;
+    p = line + strlen( line ) - 1;
+    while ( isspace( *p ) )
+    {
+        *p = '\0';
+        --p;
+    }
+    return line;
+}
+
+void
+Utl_cpyStr( Str dst, const char *src )
+{
+    dst[ 0 ] = '\0';
+    strncat( dst, src, MAXCHAR );
+}
+
+void
+Utl_cpyStrN( Str dst, const char *src, size_t n )
+{
+    dst[ 0 ] = '\0';
+    strncat( dst, src, n );
+}
+
+void
+Utl_stamp( Str file )
+{
+    FILE *f;
+    time_t t;
+
+    time( &t );
+    if ( ! ( f = fopen( file, "w" ) ) )
+    {
+        Log_err( "Could not open %s for writing (%s)",
+                 file, strerror( errno ) );
+        return;
+    }
+    fprintf( f, "%lu\n", t );
+    fclose( f );
+}
+
+Bool
+Utl_getStamp( time_t *result, Str file )
+{
+    FILE *f;
+
+    if ( ! ( f = fopen( file, "r" ) ) )
+        return FALSE;
+    if ( fscanf( f, "%lu", result ) != 1 )
+    {
+        Log_err( "File %s corrupted", file );
+        fclose( f );
+        return FALSE;
+    }
+    fclose( f );
+    return TRUE;
+}
+
+void
+Utl_allocAndCpy( char **dst, const char *src )
+{
+    int len = strlen( src );
+    if ( ! ( *dst = (char *)malloc( len + 1 ) ) )
+    {
+        Log_err( "Cannot allocate string with length %lu", strlen( src ) );
+        exit( EXIT_FAILURE );
+    }
+    memcpy( *dst, src, len + 1 );
+}
+
+Bool
+Utl_matchPattern( const char *text, const char *pattern )
+{
+    if ( pattern[ 0 ] == '*' && pattern[ 1 ] == '\0' )
+        return TRUE;
+    return ( fnmatch( pattern, text, 0 ) == 0 );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util.h	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,69 @@
+/*
+  util.h
+
+  Miscellaneous helper functions.
+
+  $Id: util.h 3 2000-01-04 11:35:42Z enz $
+*/
+
+#ifndef UTL_H
+#define UTL_H
+
+#include <time.h>
+#include "common.h"
+
+/*
+  Find first non-whitespace character after <token> tokens in string <line>.
+  Return pointer to end of string, if parsing failed.
+*/
+const char *
+Utl_restOfLn( const char *line, unsigned int token );
+
+void
+Utl_toLower( Str line );
+
+/*
+  Read a line from string.
+  Return NULL if pos == NULL or no more line to read
+*/
+const char *
+Utl_getLn( Str result, const char *p );
+
+/*
+  Go back to last line.
+*/
+const char *
+Utl_ungetLn( const char *str, const char *p );
+
+/*
+  Strip white spaces from left and right side.
+  Return pointer to new start. This is within line.
+*/
+char *
+Utl_stripWhiteSpace( char *line );
+
+/* Write timestamp into <file>. */
+void
+Utl_stamp( Str file );
+
+/* Get time stamp from <file> */
+Bool
+Utl_getStamp( time_t *result, Str file );
+
+void
+Utl_cpyStr( Str dst, const char *src );
+
+void
+Utl_cpyStrN( Str dst, const char *src, size_t n );
+
+/* String allocation and copying. */
+void
+Utl_allocAndCpy( char **dst, const char *src );
+
+/*
+  Do shell-style pattern matching for ?, \, [], and * characters.
+*/
+Bool
+Utl_matchPattern( const char *text, const char *pattern );
+
+#endif