# HG changeset patch
# User enz
# Date 946985742 0
# Node ID 04124a4423d4fc508728627a1e285a9ae19b804c
[svn] Initial revision
diff -r 000000000000 -r 04124a4423d4 AUTHORS.html
--- /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 @@
+
+
+
+NOFFLE Authors
+
+
+
+
+
+NOFFLE Authors
+
+
+
+
+
+
+Markus Enzenberger
+
+Volker Wysk
+
+
+
+
diff -r 000000000000 -r 04124a4423d4 CHANGELOG.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 @@
+
+
+
+NOFFLE Changelog
+
+
+
+
+
+NOFFLE Changelog
+
+
+
+
+Since 1.0pre4
+
+
+
+Core files are always enabled when running as server and debugging
+symbols are always in the executable.
+
+Use GDBM_FAST flag for hash files.
+
+Minor changes and improvements
+
+
+Version 1.0pre4
+
+
+
+Fixed a bug that broke cross-posting of articles
+
+Long overview header lines are now split into multiple lines in response
+to HEAD or ARTICLE commands.
+
+Fixed a bug that caused a crash sometimes when updating the requested
+article list after releasing/regetting the global lock
+
+Server is now allowed to generate core files on crash (in spool directory)
+if compiled with -DDEBUG option
+
+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.
+
+New config option "connect-timeout"
+
+Minor improvements and bug-fixes
+
+
+Version 1.0pre3
+
+
+
+Added XPAT command. Not full syntax, but enough for making slrn's thread
+reconstruction work
+
+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.
+
+When fetching requested articles, do not send more than 20 ARTICLE commands
+at once, before parsing the server response.
+
+Minor bug fixes and improvements.
+
+
+Version 1.0pre2
+
+
+
+Added RPM_BUILD_ROOT variable to Makefile (useful for creating RPM source
+packages)
+
+Removed terrible bug that truncated article body after releasing and re-getting
+global lock
+
+
+Version 1.0pre1
+
+
+
+needs complete re-installing, some formats have changed
+
+Support for multiple remote servers
+
+Faster download when fetching news, because articles are prepared
+in database while parsing response to XOVER and all ARTICLE commands
+are sent at once
+
+Bug removed that made authetication only work with lower-case passwords
+
+Other small bug fixes and improvements
+
+
+Version 0.19
+
+
+
+Fix broken full mode
+
+Fix cutting of articles after line beginning with '.'
+
+Other bug fixes
+
+LIST commands can have pattern argument now
+
+initial-fetch option removed (same as max-fetch now)
+
+
+Version 0.18
+
+
+
+needs complete re-installing, most file format have changed
+
+Group database uses gdbm, databases moved to /var/spool/noffle/data
+
+Most config options changed their names, some do not longer exists
+
+New fetch mode "thread" added
+
+Different --fetch invocations replaced by single option
+
+Meaning of "--database" option changed, "--article" option added
+
+Failed postings are now returned to sender by "mail" command
+
+Expire uses last access time
+
+Auto-subscribe option only subscribes groups now, if an article
+body is opened (no longer if group is selected).
+
+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.
+
+Doc files are now copied to $(PREFIX)/doc/noffle
+
+Y2K compliance of NEWGROUPS command
+
+Various bug fixes (thanks to all users helping with bug reports)
+
+Various changes for tuning and improvement
+
+
+Version 0.17
+
+
+
+Bug removed that caused NOFFLE to exceed the allowed maximum number
+of open files on longer sessions.
+
+
+Version 0.16
+
+
+
+Noffle generates Message-ID if a message received for posting has none.
+
+
+
+
diff -r 000000000000 -r 04124a4423d4 COPYING.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 @@
+
+
+
+NOFFLE Copying
+
+
+
+
+
+
+NOFFLE Copying
+
+
+
+
+
+
+
+This program is available under the GNU General Public License.
+
+The full terms and conditions for copying, distribution and modification
+can be found at:
+
+http://www.fsf.org/copyleft/gpl.html
+
+
+
+
+
diff -r 000000000000 -r 04124a4423d4 FAQ.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 @@
+
+
+
+NOFFLE FAQ
+
+
+
+
+
+
+NOFFLE FAQ
+
+
+
+
+
+
+Q: What is the difference between NOFFLE and leafnode?
+
+A: 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.
+
+
+
+
+
+Q: I subscribe to groups, but get a "Retreiving failed"
+message for every requested article.
+
+A: Some news server do not allow retrieving articles by message-ID.
+You cannot use NOFFLE together with these servers presently.
+
+
+
+
+
+Q: I changed the server in the config files, but the
+new groups do not appear.
+
+A: You should run noffle --query groups
again. If you
+want all old group information deleted, you should remove the file
+data/groupinfo.gdbm in the spool directory before.
+
+
+
+
+
+Q: The Emacs news reader GNUS hangs while getting active list
+from server.
+
+A: 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.
+
+
+
diff -r 000000000000 -r 04124a4423d4 INSTALL.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 @@
+
+
+
+NOFFLE Installation
+
+
+
+
+
+
+NOFFLE Installation
+
+
+
+
+
+
+For compiling NOFFLE there are the following requirements:
+
+
+
+
+
+The gdbm library must be installed on your system (standard with
+most distributions).
+
+Please use the same compiler for compiling NOFFLE that was
+used for compiling the gdbm library!
+
+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.
+
+
+
+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).
+
+
+
+The program "sort" must be available (standard with most distributions).
+
+
+
+
+
+For installing NOFFLE on your system, the following steps are necessary:
+
+
+
+
+
+Edit the Makefile. Change SPOOLDIR and PREFIX, if you do not
+like the defaults.
+
+
+
+Type 'make'.
+
+
+
+Log in as root and type 'make install'.
+
+
+
+Copy '<PREFIX>/doc/noffle/noffle.conf.example' to '/etc/noffle.conf' and
+edit it. Write in the name of the remote news server.
+
+Change the owner to 'news':
+
+ chown news.news /etc/noffle.conf
+
+Make it unreadable by others, if it contains a username and a password:
+
+ chmod o-r /etc/noffle.conf
+
+Now you can leave the root account.
+
+
+
+Go online and run
+
+ noffle --query groups # required
+ noffle --query desc # optional group descriptions
+
+
+to retrieve newsgroup information.
+
+This may take a while depending on the number of active newsgroups
+at the remote news server.
+
+Subscribe to some groups by running
+
+ noffle --subscribe-over
+
+or
+
+ noffle --subscribe-thread
+
+or
+
+ noffle --subscribe-full
+
+Then run
+
+ noffle --fetch
+
+for testing the retrieving of overviews/articles of the groups subscribed.
+
+
+
+Add a line for 'noffle' to '/etc/inetd.conf':
+
+ nntp stream tcp nowait news /usr/sbin/tcpd /usr/local/bin/noffle -r
+
+(Change the path of noffle if necessary)
+
+
+
+Add the following lines to your 'ip-up' script:
+
+ /usr/local/bin/noffle --fetch
+ /usr/local/bin/noffle --online
+
+
+Add the following line to your 'ip-down' script:
+
+ /usr/local/bin/noffle --offline
+
+Add a line for running noffle to the crontab of news (by running
+'crontab -u news -e' as root):
+
+ 0 19 * * 1 /usr/local/bin/noffle --expire 14
+
+(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).
+
+
+
+
+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".
+
+If something goes wrong, have a look at '/var/log/news' for error and
+logging messages.
+
+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 :-)
+
+
+
diff -r 000000000000 -r 04124a4423d4 LSM.TXT
--- /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
+Maintained-by: Markus Enzenberger
+Primary-site: http://home.t-online.de/home/markus.enzenberger/noffle.html
+Platforms: UNIX, Linux
+Copying-policy: GPL
+End
diff -r 000000000000 -r 04124a4423d4 Makefile
--- /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
diff -r 000000000000 -r 04124a4423d4 NOTES.html
--- /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 @@
+
+
+
+NOFFLE Compatibility Notes
+
+
+
+
+
+NOFFLE Compatibility Notes
+
+
+
+
+
+
+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.
+
+Subscribing to groups in "overview" or "thread" mode requires more
+from the news reader program:
+
+
+
+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.
+
+The reader should rarely open article bodies automatically,
+because it will mark them unwantedly for download.
+
+
+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 :-)
+
+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.
+
+
+
kexpress 0.2.0
+
+I found no way to disable caching, apart from writing a
+wrapper script, which removes all files from the cache after
+terminating kexpress:
+
+
+ #!/bin/bash
+ # kexpress wrapper, save as /usr/local/bin/kexpress
+
+ /opt/kde/bin/kexpress $@
+ rm $HOME/.kde/share/apps/kexpress/data/*
+
+
+
+
krn 0.4.0
+
+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:
+
+
+ #!/bin/bash
+ # krn wrapper, save as /usr/local/bin/krn
+
+ /opt/kde/bin/krn $@
+ rm $HOME/.kde/share/apps/krn/cache/*
+
+
+Articles can be marked as read/unread without opening with the
+middle mouse button.
+This version of krn is still unstable.
+
+
netscape 3.04
+
+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.
+
+netscape communicator 4.0.5
+
+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.
+
+netscape communicator 4.5
+
+As with 4.0.5 "View/Show/Message" can be used to switch off
+automatic message display (and marking for download).
+
+pine 3.96, 4.05
+
+Ok.
+
+slrn 0.9.5.2
+
+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
+
+
+ % 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" );
+
+
+
+
tin pre
+
+Call with "tin -r" or "rtin". 'K' marks articles/thread as
+read without opening them. '-' marks them as unread.
+
+Emacs Gnus
+
+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.
+
+Here is a proposal for changing some key-bindings.
+
+
+ ;; Customising Gnus for use with the NOFFLE news server
+ ;;
+ ;; tick and open article
+ ;; for reading/marking for download
+ ;; scroll article text circular
+ ;; for avoiding automatic opening of next article
+ ;; 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)
+
+
+
+
+
+Last modified 4/99,
+Markus Enzenberger
+
+
+
+
diff -r 000000000000 -r 04124a4423d4 README.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 @@
+
+
+
+NOFFLE
+
+
+
+
+
+
+The NOFFLE News Server
+
+
+
+
+
+
+
Features
+
+NOFFLE is a
+Usenet
+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.
+
+It allows reading news offline with many news reader programs,
+even if they do not support offline reading by themselves.
+
+NOFFLE is written for the
+Linux
+operating system and freely available under GPL
+(see
+http://www.fsf.org/copyleft/gpl.html ).
+
+While Online:
+
+
+Any newsgroup can be read, selected articles are fetched
+immediately from the remote server.
+
+
+While Offline:
+
+
+Allows reading news offline with many news clients,
+even if they do not support offline reading by themselves.
+
+
+Groups can be retrieved in different modes:
+
+
+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.
+
+In full mode, the complete articles are fetched.
+
+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.
+
+
+
+The news feed is invoked automatically next online time by calling
+NOFFLE in the ip-up script.
+
+
+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.
+
+
+
+Compatibility with News Clients
+
+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.
+
+Subscribing to groups in overview mode or thread mode puts some
+requirements on the news reader program. See
+NOTES.txt for compatibility notes.
+
+
+
Getting NOFFLE
+
+NOFFLE can be downloaded from the NOFFLE homepage at
+
+http://home.t-online.de/home/markus.enzenberger/noffle.html :
+
+
+
+noffle-0.19.tar.gz
+
+(current release, 25 Apr 1999)
+
+
+
+noffle-0.17.tar.gz
+
+(last release, 25 Jan 1999)
+
+Read
+
+INSTALL.txt
+from the package for compiling and installing NOFFLE on your system.
+
+RPM packages have been created by Mario Moder
+(moderm@gmx.net ). They are available
+at
+
+ftp://ftp.fbam.de/pub/linux/
+
+
+The current version is still beta. Please send bug reports,
+comments and patches to me
+(markus.enzenberger@t-online.de ).
+
+If you want to receive announcements about NOFFLE, I will put you on
+my announcement list. Just send me a mail.
+
+
Links
+
+
+
+
+
+
+Last modified 5/99,
+Markus Enzenberger
+
+
+
+
diff -r 000000000000 -r 04124a4423d4 TODO.TXT
--- /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 (?)
diff -r 000000000000 -r 04124a4423d4 client.c
--- /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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#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;
+}
diff -r 000000000000 -r 04124a4423d4 client.h
--- /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
+#include "common.h"
+#include "database.h"
+#include "fetchlist.h"
+
+/* Format of server name: [:] */
+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 at server if it is also in current local grouplist.
+ Returns TRUE at success.
+*/
+Bool
+Client_changeToGrp( const Str name );
+
+/*
+ Get overviews - 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
diff -r 000000000000 -r 04124a4423d4 common.h
--- /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
+#include
+#include
+
+#define FALSE 0
+#define TRUE !0
+#define MAXCHAR 2048
+
+#ifdef DEBUG
+#include
+#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
diff -r 000000000000 -r 04124a4423d4 config.c
--- /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
+#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 );
+ }
+}
diff -r 000000000000 -r 04124a4423d4 config.h
--- /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
diff -r 000000000000 -r 04124a4423d4 content.c
--- /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
+#include
+#include
+#include
+#include
+#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 );
+}
diff -r 000000000000 -r 04124a4423d4 content.h
--- /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 .
+ 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
diff -r 000000000000 -r 04124a4423d4 database.c
--- /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
+#include
+#include
+#include
+#include
+#include
+#include
+#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: " */
+ 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;
+}
diff -r 000000000000 -r 04124a4423d4 database.h
--- /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
+#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 */
+Bool
+Db_expire( unsigned int days );
+
+#endif
diff -r 000000000000 -r 04124a4423d4 dynamicstring.c
--- /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
+#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';
+}
+
diff -r 000000000000 -r 04124a4423d4 dynamicstring.h
--- /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
+
+/* 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
diff -r 000000000000 -r 04124a4423d4 fetch.c
--- /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
+#include
+#include
+#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 );
+}
diff -r 000000000000 -r 04124a4423d4 fetch.h
--- /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
diff -r 000000000000 -r 04124a4423d4 fetchlist.c
--- /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;
+}
diff -r 000000000000 -r 04124a4423d4 fetchlist.h
--- /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
diff -r 000000000000 -r 04124a4423d4 group.c
--- /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
+#include
+#include
+#include
+#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 );
+}
diff -r 000000000000 -r 04124a4423d4 group.h
--- /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
+#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
diff -r 000000000000 -r 04124a4423d4 lock.c
--- /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
+#include
+#include
+#include
+#include
+#include
+#include
+#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();
+}
diff -r 000000000000 -r 04124a4423d4 lock.h
--- /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
diff -r 000000000000 -r 04124a4423d4 log.c
--- /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
+#include
+#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
+}
diff -r 000000000000 -r 04124a4423d4 log.h
--- /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
diff -r 000000000000 -r 04124a4423d4 make-check
--- /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
+
diff -r 000000000000 -r 04124a4423d4 make-distribution
--- /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'/' $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.
diff -r 000000000000 -r 04124a4423d4 noffle.1
--- /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 |all
+.br
+.B noffle
+\-d | \-\-database
+.br
+.B noffle
+\-e | \-\-expire
+.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
+.br
+.B noffle
+\-S | \-\-subscribe-full
+.br
+.B noffle
+\-t | \-\-subscribe-thread
+.br
+.B noffle
+\-u | \-\-unsubscribe
+
+.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 |all
+Write article 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
+Delete all articles older than 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
+
+
+.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: 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:
+
+.TP
+.B \-s, \-\-subscribe-over
+Add group with name to list of groups that are presently to be fetched
+(in over mode).
+
+.TP
+.B \-S, \-\-subscribe-full
+Add group with name to list of groups that are presently to be fetched
+(in full mode).
+
+.TP
+.B \-t, \-\-subscribe-thread
+Add group with name to list of groups that are presently to be fetched
+(in thread mode).
+
+.TP
+.B \-u, \-\-unsubscribe
+Remove group with name 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
+Configuration file. Comment lines begin with
+.I #.
+Definition lines may contain:
+.br
+.B server [:] [ ]
+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
+Never get more than articles. If there are more, the oldest ones
+are discarded.
+.br
+Default: 300
+.br
+.B mail-to
+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:
+.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
+Number of days used for auto-unsubscribe option.
+.br
+Default: 30
+.br
+.B thread-follow-time
+Automatically mark articles for download in thread mode, if they
+are referencing an article that has been opened by a reader within the last
+ days.
+.br
+.B connect-timeout
+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.
+ 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 /fetchlist
+List of newsgroups that are presently to be fetched.
+.br
+
+.TP
+.B /data/groupinfo.gdbm
+Database with groups in
+.BR gdbm(3)
+format.
+
+.TP
+.B /data/articles.gdbm
+Database with articles in
+.BR gdbm(3)
+format.
+
+.TP
+.B /lock/
+Lock files and files indicating online/offline status.
+
+.TP
+.B /outgoing/
+Posted articles to be delivered to the remote server.
+
+.TP
+.B /overview/
+Text file per group with article overviews.
+
+.TP
+.B /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
+.br
+Volker Wysk
+
+1998-1999.
diff -r 000000000000 -r 04124a4423d4 noffle.c
--- /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
+#include
+#include
+#include
+#include
+#include
+#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 \n"
+ "Option is one of the following:\n"
+ " -a | --article |all Show article(s) in database\n"
+ " -d | --database Show content of article database\n"
+ " -e | --expire Expire articles older than 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 Add group to fetch list (overview)\n"
+ " -S | --subscribe-full Add group to fetch list (full)\n"
+ " -t | --subscribe-thread Add group to fetch list (thread)\n"
+ " -u | --unsubscribe 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;
+}
diff -r 000000000000 -r 04124a4423d4 noffle.conf.5
--- /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.
diff -r 000000000000 -r 04124a4423d4 noffle.conf.example
--- /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: [:] [ ]
+# ( and 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 articles. Discard oldest, if there are more
+
+max-fetch 300
+
+
+# Automatically remove groups from fetch list if they have not been
+# accessed for 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 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
diff -r 000000000000 -r 04124a4423d4 online.c
--- /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
+#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" );
+ }
+}
diff -r 000000000000 -r 04124a4423d4 online.h
--- /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
diff -r 000000000000 -r 04124a4423d4 outgoing.c
--- /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
+#include
+#include
+#include
+#include
+#include
+#include
+#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 );
+}
diff -r 000000000000 -r 04124a4423d4 outgoing.h
--- /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
diff -r 000000000000 -r 04124a4423d4 over.c
--- /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
+#include
+#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;
+}
diff -r 000000000000 -r 04124a4423d4 over.h
--- /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
+#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
diff -r 000000000000 -r 04124a4423d4 protocol.c
--- /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
+#include
+#include
+#include
+#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 ) );
+}
diff -r 000000000000 -r 04124a4423d4 protocol.h
--- /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
diff -r 000000000000 -r 04124a4423d4 pseudo.c
--- /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
+#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 );
+}
diff -r 000000000000 -r 04124a4423d4 pseudo.h
--- /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
diff -r 000000000000 -r 04124a4423d4 request.c
--- /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
+#include
+#include
+#include
+#include
+#include
+#include
+#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;
+}
diff -r 000000000000 -r 04124a4423d4 request.h
--- /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
diff -r 000000000000 -r 04124a4423d4 server.c
--- /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
+#include
+#include
+#include
+#include
+#include
+#include
+#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)", ¬Implemented },
+ { "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();
+}
diff -r 000000000000 -r 04124a4423d4 server.h
--- /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
diff -r 000000000000 -r 04124a4423d4 util.c
--- /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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#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 );
+}
diff -r 000000000000 -r 04124a4423d4 util.h
--- /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
+#include "common.h"
+
+/*
+ Find first non-whitespace character after tokens in string .
+ 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 . */
+void
+Utl_stamp( Str file );
+
+/* Get time stamp from */
+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