# 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

+
+ +
+ + + + + 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

+ + + +

Version 1.0pre4

+ + + +

Version 1.0pre3

+ + + +

Version 1.0pre2

+ + + +

Version 1.0pre1

+ + + +

Version 0.19

+ + + +

Version 0.18

+ + + +

Version 0.17

+ + + +

Version 0.16

+ + + + + 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: +

+ +

+ +

+For installing NOFFLE on your system, the following steps are necessary: +

+ +

+ +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: +

+

+ +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: +

+

+While Offline: +

+ +

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