changeset 38:8e972daaeab9 noffle

[svn] Applied patch from Jim Hague: - Forget cached group info when group database closed. - Added list of 'forbidden' newsgroup specs. - Fixed problem with article numbering if the overview file empties. - Changed %i to %d in sscanfs (%i interprets leading zeros as octal numbers) - New groups now always start numbering at article 1. - Record newsgroup posting status. Enforce it at posting time. Added --modify - Added group deletion. - Added wildmat code taken from INN
author enz
date Fri, 05 May 2000 08:23:15 +0100
parents 792eb10e936d
children 2c9ea1ffcc88
files CHANGELOG.html INSTALL.html Makefile README.html client.c config.c config.h content.c database.c database.h group.c group.h itemlist.c noffle.1 noffle.c noffle.conf.5 noffle.conf.example pseudo.c server.c util.c util.h wildmat.c wildmat.h
diffstat 23 files changed, 1154 insertions(+), 279 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGELOG.html	Thu May 04 09:16:09 2000 +0100
+++ b/CHANGELOG.html	Fri May 05 08:23:15 2000 +0100
@@ -12,12 +12,59 @@
 
 <hr>
 
-<h2>Current developer version</h2>
+<h2>Version 1.opre6pre</h2>
 
 <ul>
 <li>
-Noffle now sends a "MODE READER" command after connecting to the remote
-server. INN needs this before it will permit POST.
+Forget cached group info when group database closed.
+<li>
+Added list of 'forbidden' newsgroup specs., as defined in draft IETF
+Newsgroup Format (C.Lindsey), tracked to replace RFC1036. This defines
+newsgroup names that should only be used for server-local groups and
+server pseudo-groups (e.g. INN's to.*, cancel, cancel.*, junk). These
+are now intercepted when querying server groups and ignored.  Group names
+omitted are any single component names, any 'control.*', 'to' or
+'to.*',and any with a component 'all' or 'ctl'.
+Note these restrictions do not apply to local group names.
+<li>
+Fixed problem with article numbering if the overview file empties,
+e.g. due to all articles in a very low volume group expiring. This
+would cause article numbers to be set back to 1 when a new article
+arrives.
+<li>
+Changed %i to %d in sscanfs everywhere. INN often (as it is entitled to
+do) has leading zeros on numbers. %i interprets these as octal
+numbers. Also changed %i to %d in printfs, for no good reason.
+<li>
+New groups now always start numbering at article 1. Previously article
+numbering would start with the first held remote article number, in an
+attempt to avoid newsreaders noticing if noffle is deleted and
+reinstalled. Given Noffle may well not collect the first held article
+anyway - it only will if the default number of articles to retrieve on
+a first connect is big enough - and the fact that Noffle's pseudo
+articles make it impossible to keep local article numbers in lock-step
+with the server, there is the chance this scheme would just cause
+readers to miss new articles.
+<li>
+Record newsgroup posting status. Enforce it at posting time.
+Added --modify to change newsgroup descriptions for all groups and
+posting status for local groups.
+<li>
+Added group deletion.
+<li>
+Added message cancellation - from command line or by control message.
+Note command line only cancels locally - it can't be used to cancel a
+message that has already gone offsite. A control messages cancels
+locally if possible; it is only propaged offsite if the target is in a
+non-local group and has itself already gone offsite.
+<li>
+Added wildmat code taken from INN - ensure Noffle wildcarding is
+exactly to spec.
+<li>
+Added group-specific expire times.
+<li>
+Noffle now sends a "MODE READER" command after connecting to the
+remote server. INN needs this before it will permit POST.
 <li>
 Applied patch from Jim Hague: support for local groups / new command
 line options --create and --cancel.
--- a/INSTALL.html	Thu May 04 09:16:09 2000 +0100
+++ b/INSTALL.html	Fri May 05 08:23:15 2000 +0100
@@ -131,10 +131,11 @@
 Add a line for running noffle to the crontab of news (by running
 'crontab -u news -e' as root):
 <pre>
-         0 19 * * 1 /usr/local/bin/noffle --expire 14
+         0 19 * * 1 /usr/local/bin/noffle --expire
 </pre>
 (if you want to run 'noffle' on Monday (1st day of week) at
-19.00 and delete all articles not accessed within the last 14 days).
+19.00 and delete all articles not accessed recently. The default
+expiry period is 14 days, but this can be changed in /etc/noffle.conf.
 <p>
 
 </ul>
--- a/Makefile	Thu May 04 09:16:09 2000 +0100
+++ b/Makefile	Fri May 05 08:23:15 2000 +0100
@@ -2,7 +2,7 @@
 #
 # Makefile for Noffle news server
 #
-# $Id: Makefile 37 2000-04-30 14:22:12Z enz $
+# $Id: Makefile 44 2000-05-05 07:23:15Z enz $
 #
 ###############################################################################
 
@@ -23,12 +23,12 @@
 FILESH = client.h common.h config.h content.h control.h database.h \
     dynamicstring.h fetch.h fetchlist.h group.h itemlist.h lock.h log.h \
     online.h outgoing.h over.h post.h protocol.h pseudo.h request.h \
-    server.h util.h
+    server.h util.h wildmat.h
 
 FILESC = fetch.c client.c config.c content.c control.c database.c \
     dynamicstring.c fetchlist.c group.c itemlist.c lock.c log.c noffle.c \
     online.c outgoing.c over.c post.c protocol.c pseudo.c request.c \
-    server.c util.c
+    server.c util.c wildmat.c
 
 OBJS = $(patsubst %.c,%.o,$(FILESC))
 
@@ -65,17 +65,17 @@
 	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 README.html $(RPM_BUILD_ROOT)$(DOCDIR)
+	install -m 0644 -o 0 -g 0 NOTES.html $(RPM_BUILD_ROOT)$(DOCDIR)
+	install -m 0644 -o 0 -g 0 INSTALL.html $(RPM_BUILD_ROOT)$(DOCDIR)
+	install -m 0644 -o 0 -g 0 CHANGELOG.html $(RPM_BUILD_ROOT)$(DOCDIR)
+	install -m 0644 -o 0 -g 0 FAQ.html $(RPM_BUILD_ROOT)$(DOCDIR)
+	install -m 0644 -o 0 -g 0 COPYING.html $(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.
+	@echo Read INSTALL.html for further instructions.
 
 tags:
 	ctags -e $(FILESC) $(FILESH)
--- a/README.html	Thu May 04 09:16:09 2000 +0100
+++ b/README.html	Fri May 05 08:23:15 2000 +0100
@@ -27,6 +27,8 @@
 <a href="#documentation">Documentation</a>
 <li>
 <a href="#links">Links</a>
+<li>
+<a href="#acknowledgements">Acknowledgements</a>
 </ul>
 
 <p>
@@ -179,6 +181,12 @@
 </ul>
 
 
+<h2><a name="acknowledgements">Acknowledgements</a></h2>
+
+The <i>wildmat</i> newsgroup pattern matching software used by NOFFLE
+was developed by Rich Salz, and is as distributed with INN
+v2.2.
+
 <p>
 <hr>
 <small><i>
--- a/client.c	Thu May 04 09:16:09 2000 +0100
+++ b/client.c	Fri May 05 08:23:15 2000 +0100
@@ -1,7 +1,7 @@
 /*
   client.c
 
-  $Id: client.c 38 2000-04-30 19:07:54Z enz $
+  $Id: client.c 44 2000-05-05 07:23:15Z enz $
 */
 
 #include "client.h"
@@ -24,6 +24,33 @@
 #include "pseudo.h"
 #include "request.h"
 #include "util.h"
+#include "wildmat.h"
+
+/*
+  Some newsgroups names are reserved for server-specific or server
+  pseudo groups. We don't want to fetch them. For example, INN
+  keeps all its control messages in a 'control' hierarchy, and
+  used the "to." heirarchy for dark and mysterious purposes I think
+  are to do with newsfeeds. The recommended restrictions are documented
+  in C.Lindsay, "News Article Format", <draft-ietf-usefor-article-03.txt>.
+*/
+
+struct ForbiddenGroupName
+{
+    const char *pattern;
+    Bool match;
+} forbiddenGroupNames[] =
+{
+    { "*.*", FALSE },			/* Single component */
+    { "control.*", TRUE },		/* control.* groups */
+    { "to.*", TRUE },			/* control.* groups */
+    { "*.all", TRUE },			/* 'all' as a component */
+    { "*.all.*", TRUE },
+    { "all.*", TRUE },
+    { "*.ctl", TRUE },			/* 'ctl' as a component */
+    { "*.ctl.*", TRUE },
+    { "ctl.*", TRUE }
+};
 
 struct
 {
@@ -290,6 +317,8 @@
             if ( ! ( client.out = fdopen( sock, "w" ) )
                  || ! ( client.in  = fdopen( dup( sock ), "r" ) ) )
             {
+		if ( client.out != NULL )
+		    fclose( client.out );
                 close( sock );
                 break;
             }
@@ -312,11 +341,33 @@
                 Log_err( "Bad server stat %d", stat ); 
             }
             shutdown( fileno( client.out ), 0 );
+	    fclose( client.in );
+	    fclose( client.out );
+	    close( sock );
         }
     }
     return FALSE;
 }
 
+static Bool
+isForbiddenGroupName( const char *name )
+{
+    int i;
+
+    for ( i = 0;
+	  i < sizeof( forbiddenGroupNames ) /
+	      sizeof( struct ForbiddenGroupName );
+	  i++ )
+    {
+	/* Negate result of Wld_match to ensure it is 1 or 0. */
+	if ( forbiddenGroupNames[i].match !=
+	     ( ! Wld_match( name, forbiddenGroupNames[i].pattern ) ) )
+	    return TRUE;
+    }
+
+    return FALSE;
+}
+
 static void
 processGrps( void )
 {
@@ -327,24 +378,24 @@
     
     while ( getTxtLn( line, &err ) && ! err )
     {
-        if ( sscanf( line, "%s %i %i %c",
+        if ( sscanf( line, "%s %d %d %c",
                      grp, &last, &first, &postAllow ) != 4 )
         {
             Log_err( "Unknown reply to LIST or NEWGROUPS: %s", line );
             continue;
         }
+	if ( isForbiddenGroupName( grp ) )
+	{
+	    Log_inf( "Group %s forbidden", grp );
+	    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 );
+	    Grp_setPostAllow( grp, postAllow );
         }
         else
         {
@@ -354,6 +405,7 @@
                          grp, Grp_serv( grp ), client.serv );
                 Grp_setServ( grp, client.serv );
                 Grp_setRmtNext( grp, first );
+		Grp_setPostAllow( grp, postAllow );
             }
             else
                 Log_dbg( "Group %s is already fetched from %s",
@@ -515,7 +567,7 @@
     Str t;
     
     p = readField( t, line );
-    if ( sscanf( t, "%i", numb ) != 1 )
+    if ( sscanf( t, "%d", numb ) != 1 )
         return FALSE;
     p = readField( subj, p );
     p = readField( from, p );
@@ -550,7 +602,7 @@
     if ( strlen( s ) == 0 )
         return NULL;
     pColon = strstr( s, ":" );
-    if ( ! pColon || sscanf( pColon + 1, "%i", numb ) != 1 )
+    if ( ! pColon || sscanf( pColon + 1, "%d", numb ) != 1 )
     {
         Log_err( "Corrupt Xref at position '%s'", pXref );
         return NULL;
@@ -623,14 +675,14 @@
             {
                 Log_dbg( "Changing first server for '%s' from '%s' to '%s'",
                          msgId, Grp_serv( g ), client.serv );
-                snprintf( t, MAXCHAR, "%s:%i %s",
+                snprintf( t, MAXCHAR, "%s:%d %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",
+                snprintf( t, MAXCHAR, "%s %s:%d",
                           xref, client.grp, Ov_numb( ov ) );
                 Db_setXref( msgId, t );
             }
@@ -787,7 +839,7 @@
         return FALSE;
     if ( getStat() != STAT_GRP_SELECTED )
         return FALSE;
-    if ( sscanf( client.lastStat, "%u %i %i %i",
+    if ( sscanf( client.lastStat, "%u %d %d %d",
                  &stat, &estimatedNumb, &first, &last ) != 4 )
     {
         Log_err( "Bad server response to GROUP: %s", client.lastStat );
--- a/config.c	Thu May 04 09:16:09 2000 +0100
+++ b/config.c	Fri May 05 08:23:15 2000 +0100
@@ -6,7 +6,7 @@
     SPOOLDIR
     VERSION
 
-  $Id: config.c 7 2000-01-06 09:30:49Z enz $
+  $Id: config.c 44 2000-05-05 07:23:15Z enz $
 */
 
 #include "config.h"
@@ -23,6 +23,13 @@
 }
 ServEntry;
 
+typedef struct
+{
+    Str pattern;
+    int days;
+}
+ExpireEntry;
+
 struct
 {
     /* Compile time options */
@@ -39,10 +46,15 @@
     Bool replaceMsgId;
     Str autoSubscribeMode;
     Str mailTo;
+    int defaultExpire;
     int numServ;
     int maxServ;
     ServEntry *serv;
     int servIdx; /* for server enumeration */
+    int numExpire;
+    int maxExpire;
+    ExpireEntry *expire;
+    int expireIdx;
 } config =
 {
     SPOOLDIR, /* spoolDir */
@@ -57,10 +69,15 @@
     TRUE,     /* replaceMsgId */
     "over",   /* autoSubscribeMode */
     "",       /* mailTo */
+    14,       /* defaultExpire */
     0,        /* numServ */
     0,        /* maxServ */
     NULL,     /* serv */
-    0         /* servIdx */
+    0,	      /* servIdx */
+    0,        /* numExpire */
+    0,        /* maxExpire */
+    NULL,     /* expire */
+    0         /* expireIdx */
 };
 
 const char * Cfg_spoolDir( void ) { return config.spoolDir; }
@@ -77,6 +94,7 @@
 const char * Cfg_autoSubscribeMode( void ) {
     return config.autoSubscribeMode; }
 const char * Cfg_mailTo( void ) { return config.mailTo; }
+int Cfg_expire( void ) { return config.defaultExpire; }
 
 void
 Cfg_beginServEnum( void )
@@ -149,6 +167,21 @@
     }
 }
 
+void
+Cfg_beginExpireEnum( void )
+{
+    config.expireIdx = 0;
+}
+
+int
+Cfg_nextExpire( Str pattern )
+{
+    if ( config.expireIdx >= config.numExpire )
+        return -1;
+    strcpy( pattern, config.expire[ config.expireIdx ].pattern );
+    return config.expire[ config.expireIdx++ ].days;
+}
+
 static void
 logSyntaxErr( const char *line )
 {
@@ -244,6 +277,49 @@
     config.serv[ config.numServ++ ] = entry;
 }
 
+static void
+getExpire( const char *line )
+{
+    Str dummy;
+    ExpireEntry entry;
+    int days;
+
+    /*
+      The line is either "expire <num>" or "expire <pat> <num>".
+      The former updates the overall default.
+     */
+    if ( sscanf( line, "%s %s %d", dummy, entry.pattern, &days ) != 3 )
+    {
+	logSyntaxErr( line );
+	return;
+    }
+    else
+    {
+	if ( days < 0 )
+	{
+	    Log_err( "Expire days error in '%s': must be integer > 0",
+		     line, days );
+	    return;
+	}
+
+	Utl_toLower( entry.pattern );
+	entry.days = days;
+
+	if ( config.maxExpire < config.numExpire + 1 )
+	{
+	    if ( ! ( config.expire = realloc( config.expire,
+					      ( config.maxExpire + 5 )
+					      * sizeof( ExpireEntry ) ) ) )
+	    {
+		Log_err( "Could not realloc exipre list" );
+		exit( EXIT_FAILURE );
+	    }
+	    config.maxExpire += 5;
+	}
+	config.expire[ config.numExpire++ ] = entry;
+    }
+}
+
 void
 Cfg_read( void )
 {
@@ -259,10 +335,11 @@
     }
     while ( fgets( line, MAXCHAR, f ) )
     {
-        Utl_cpyStr( lowerLine, line );
+        p = Utl_stripWhiteSpace( line );
+	Utl_stripComment( p );
+        Utl_cpyStr( lowerLine, p );
         Utl_toLower( lowerLine );
-        p = Utl_stripWhiteSpace( lowerLine );
-        if ( *p == '#' || *p == '\0' )
+        if ( *p == '\0' )
             continue;
         if ( sscanf( p, "%s", name ) != 1 )
             Log_err( "Syntax error in %s: %s", file, line );
@@ -274,6 +351,8 @@
             getInt( &config.threadFollowTime, 0, INT_MAX, p );
         else if ( strcmp( "connect-timeout", name ) == 0 )
             getInt( &config.connectTimeout, 0, INT_MAX, p );
+        else if ( strcmp( "default-expire", name ) == 0 )
+            getInt( &config.defaultExpire, 0, INT_MAX, p );
         else if ( strcmp( "auto-subscribe", name ) == 0 )
             getBool( &config.autoSubscribe, p );
         else if ( strcmp( "auto-unsubscribe", name ) == 0 )
@@ -303,6 +382,8 @@
             getServ( line );
         else if ( strcmp( "mail-to", name ) == 0 )
             getStr( config.mailTo, p );
+        else if ( strcmp( "expire", name ) == 0 )
+            getExpire( p );
         else
             Log_err( "Unknown config option: %s", name );
     }
--- a/config.h	Thu May 04 09:16:09 2000 +0100
+++ b/config.h	Fri May 05 08:23:15 2000 +0100
@@ -3,7 +3,7 @@
 
   Common declarations and handling of the configuration file.
 
-  $Id: config.h 3 2000-01-04 11:35:42Z enz $
+  $Id: config.h 44 2000-05-05 07:23:15Z enz $
 */
 
 #ifndef CONFIG_H
@@ -38,6 +38,16 @@
 Bool Cfg_servIsPreferential( const char *name1, const char *name2 );
 void Cfg_authInfo( const char *name, Str user, Str pass );
 
+/* Begin iteration through expire entries. */
+void Cfg_beginExpireEnum( void );
+
+/* Put next expire pattern in "pattern" and return its days count.
+   Return -1 if no more expire patterns. */
+int Cfg_nextExpire( Str pattern );
+
+/* Return default expire days. */
+int Cfg_expire( void );
+
 void Cfg_read( void );
 
 #endif
--- a/content.c	Thu May 04 09:16:09 2000 +0100
+++ b/content.c	Fri May 05 08:23:15 2000 +0100
@@ -1,7 +1,7 @@
 /*
   content.c
 
-  $Id: content.c 6 2000-01-04 13:49:50Z enz $
+  $Id: content.c 44 2000-05-05 07:23:15Z enz $
 */
 
 #include <dirent.h>
@@ -11,6 +11,7 @@
 #include <unistd.h>
 #include "common.h"
 #include "config.h"
+#include "group.h"
 #include "log.h"
 #include "over.h"
 #include "pseudo.h"
@@ -20,8 +21,9 @@
 {
     DIR *dir;           /* Directory for browsing through all
                            groups */
-    int first;
-    int last;
+    int vecFirst;	/* First article number in vector */
+    int first;		/* First live article number */
+    int last;		/* Last article number */
     unsigned int size;  /* Number of overviews. */
     unsigned int max;   /* Size of elem. */
     Over **elem;        /* Ptr to array with ptrs to overviews.
@@ -29,7 +31,7 @@
                            in group. */
     Str name;
     Str file;
-} cont = { NULL, 1, 0, 0, 0, NULL, "", "" };
+} cont = { NULL, 1, 1, 0, 0, 0, NULL, "", "" };
 
 void
 Cont_app( Over *ov )
@@ -45,19 +47,18 @@
         }
         cont.max += 500;
     }
-    if ( cont.first == 0 )
-        cont.first = 1;
+    ASSERT( cont.vecFirst > 0 );
     if ( ov )
-        Ov_setNumb( ov, cont.first + cont.size );
+        Ov_setNumb( ov, cont.vecFirst + cont.size );
     cont.elem[ cont.size++ ] = ov;
-    cont.last = cont.first + cont.size - 1;
+    cont.last = cont.vecFirst + cont.size - 1;
 }
 
 Bool
 Cont_validNumb( int n )
 {
     return ( n != 0 && n >= cont.first && n <= cont.last
-             && cont.elem[ n - cont.first ] );
+             && cont.elem[ n - cont.vecFirst ] );
 }
 
 void
@@ -67,7 +68,7 @@
 
     if ( ! Cont_validNumb( n ) )
         return;
-    ov = &cont.elem[ n - cont.first ];
+    ov = &cont.elem[ n - cont.vecFirst ];
     free( *ov );
     *ov = NULL;
 }
@@ -83,6 +84,14 @@
     cont.size = 0;
 }
 
+static void
+setupEmpty( const char *name )
+{
+    cont.last = Grp_last( name );
+    cont.first = cont.vecFirst = cont.last + 1;
+    ASSERT( cont.first > 0 );
+}
+
 /* Extend content list to size "cnt" and append NULL entries. */
 static void
 extendCont( int cnt )
@@ -108,6 +117,7 @@
     Str line;
 
     /* Delete old overviews and make room for new ones. */
+    cont.vecFirst = 0;
     cont.first = 0;
     cont.last = 0;
     Utl_cpyStr( cont.name, name );
@@ -120,6 +130,7 @@
     if ( ! f )
     {
         Log_dbg( "No group overview file: %s", cont.file );
+	setupEmpty( name );
         return;
     }
     Log_dbg( "Reading %s", cont.file );
@@ -137,25 +148,31 @@
             continue;
         }
         if ( cont.first == 0 )
-            cont.first = numb;
+            cont.first = cont.vecFirst = numb;
         cont.last = numb;
         extendCont( numb - cont.first + 1 );
         cont.elem[ numb - cont.first ] = ov;
     }
     fclose( f );
+
+    if ( cont.first == 0 )
+	setupEmpty( name );		/* Corrupt overview file recovery */
 }
 
 void
 Cont_write( void )
 {
     Bool anythingWritten;
-    int i, first;
+    int i;
     FILE *f;
     const Over *ov;
 
-    first = cont.first;
-    while ( ! Cont_validNumb( first ) && first <= cont.last )
-        ++first;
+
+    /* Move the first article no. to the first active article */
+    while ( ! Cont_validNumb( cont.first ) && cont.first <= cont.last )
+        ++cont.first;
+
+    /* Save the overview */
     if ( ! ( f = fopen( cont.file, "w" ) ) )
     {
         Log_err( "Could not open %s for writing", cont.file );
@@ -180,8 +197,16 @@
         }
     }
     fclose( f );
+
+    /*
+      If empty, remove the overview file and set set first to one
+      beyond last to flag said emptiness.
+     */
     if ( ! anythingWritten )
-        unlink( cont.file );
+    {
+	unlink( cont.file );
+	cont.first = cont.last + 1;
+    }
 }
 
 const Over *
@@ -189,7 +214,7 @@
 {
     if ( ! Cont_validNumb( numb ) )
         return NULL;
-    return cont.elem[ numb - cont.first ];
+    return cont.elem[ numb - cont.vecFirst ];
 }
 
 int
--- a/database.c	Thu May 04 09:16:09 2000 +0100
+++ b/database.c	Fri May 05 08:23:15 2000 +0100
@@ -1,7 +1,7 @@
 /*
   database.c
 
-  $Id: database.c 39 2000-05-01 09:22:42Z enz $
+  $Id: database.c 44 2000-05-05 07:23:15Z 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
@@ -18,9 +18,11 @@
 #include <sys/types.h>
 #include <sys/stat.h>
 #include "config.h"
+#include "itemlist.h"
 #include "log.h"
 #include "protocol.h"
 #include "util.h"
+#include "wildmat.h"
 
 static struct Db
 {
@@ -561,11 +563,49 @@
     return ( cursor.dptr != NULL );
 }
 
-Bool
-Db_expire( unsigned int days )
+static int
+calcExpireDays( const char *msgId )
 {
-    double limit;
-    int cntDel, cntLeft, flags;
+    const char *xref;
+    ItemList *refs;
+    const char *ref;
+    int res;
+
+    xref = Db_xref( msgId );
+    if ( xref[ 0 ] == '\0' )
+	return -1;
+
+    res = -1;
+    refs = new_Itl( xref, " :" );
+    for ( ref = Itl_first( refs ); ref != NULL; ref = Itl_next( refs ) )
+    {
+	Str pattern;
+	int days;
+	
+	Cfg_beginExpireEnum();
+	while ( ( days = Cfg_nextExpire( pattern ) ) != -1 )
+	    if ( Wld_match( ref, pattern )
+		 && ( ( days > res && res != 0 ) ||
+		      days == 0 ) )
+	    {
+		res = days;
+		Log_dbg ( "Custom expiry %d for %s in group %s",
+			  days, msgId, ref );
+		break;
+	    }
+	
+	Itl_next( refs );	/* Throw away group number */
+    }
+
+    if ( res == -1 )
+	res = Cfg_expire();
+    return res;
+}
+
+Bool
+Db_expire( void )
+{
+    int cntDel, cntLeft, flags, expDays;
     time_t nowTime, lastAccess;
     const char *msgId;
     Str name, tmpName;
@@ -583,22 +623,35 @@
         Db_close();
         return FALSE;
     }
-    Log_inf( "Expiring articles that have not been accessed for %u days",
-             days );
-    limit = days * 24. * 3600.;
+    Log_inf( "Expiring articles" );
     cntDel = 0;
     cntLeft = 0;
     nowTime = time( NULL );
     if ( Db_first( &msgId ) )
         do
         {
+	    expDays = calcExpireDays( msgId );
             lastAccess = Db_lastAccess( msgId );
-            if ( lastAccess == -1 )
+	    if ( expDays == -1 )
+		Log_err( "Internal error: Failed expiry calculation on %s",
+			 msgId );
+	    else if ( lastAccess == -1 )
                 Log_err( "Internal error: Getting lastAccess of %s failed",
                          msgId );
-            else if ( difftime( nowTime, lastAccess ) > limit )
+            else if ( expDays > 0
+		      && difftime( nowTime, lastAccess ) >
+		          ( (double) expDays * 24 * 3600 ) )
             {
-                Log_dbg( "Expiring %s", msgId );
+#ifdef DEBUG
+		Str last, now;
+
+		Utl_cpyStr( last, ctime( &lastAccess ) );
+		last[ strlen( last ) - 1 ] = '\0';
+		Utl_cpyStr( now, ctime( &nowTime ) );
+		last[ strlen( now ) - 1 ] = '\0';
+                Log_dbg( "Expiring %s: last access %s, time now %s",
+			 msgId, last, now );
+#endif
                 ++cntDel;
             }
             else
--- a/database.h	Thu May 04 09:16:09 2000 +0100
+++ b/database.h	Fri May 05 08:23:15 2000 +0100
@@ -3,7 +3,7 @@
 
   Article database.
 
-  $Id: database.h 39 2000-05-01 09:22:42Z enz $
+  $Id: database.h 44 2000-05-05 07:23:15Z enz $
 */
 
 #ifndef DB_H
@@ -89,8 +89,11 @@
 Bool
 Db_next( const char** msgId );
 
-/* Expire all articles that have not been accessed for <days> */
+/*
+  Expire all articles that have not been accessed for a number of
+  days determined by their group membership and noffle configuration.
+ */
 Bool
-Db_expire( unsigned int days );
+Db_expire( void );
 
 #endif
--- a/group.c	Thu May 04 09:16:09 2000 +0100
+++ b/group.c	Fri May 05 08:23:15 2000 +0100
@@ -7,7 +7,7 @@
   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 33 2000-04-29 14:49:13Z enz $
+  $Id: group.c 44 2000-05-05 07:23:15Z enz $
 */
 
 #include "group.h"
@@ -22,22 +22,31 @@
 /* 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;
+    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;
+    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 */
+    char postAllow;	/* Posting status */
+    GDBM_FILE dbf;
+} grp = { "(no grp)", { 0, 0, 0, 0, 0 }, "", "", ' ', NULL };
 
-} grp = { "(no grp)", { 0, 0, 0, 0, 0 }, "", "", NULL };
+/*
+  Note: postAllow should really go in Entry. But changing Entry would
+  make backwards group file format capability tricky, so it goes
+  where it is, and we test the length of the retrieved record to
+  determine if it exists.
+
+  Someday if we really change the record format this should be tidied up.
+ */
 
 static const char *
 errMsg( void )
@@ -72,6 +81,7 @@
     Log_dbg( "Closing groupinfo" );
     gdbm_close( grp.dbf );
     grp.dbf = NULL;
+    Utl_cpyStr( grp.name, "" );
 }
 
 /* Load group info from gdbm-database into global struct grp */
@@ -94,6 +104,11 @@
     Utl_cpyStr( grp.serv, p );
     p += strlen( p ) + 1;
     Utl_cpyStr( grp.dsc, p );
+    p += strlen( p) + 1;
+    if ( p - val.dptr < val.dsize )
+	grp.postAllow = p[ 0 ];
+    else
+	grp.postAllow = 'y';
     Utl_cpyStr( grp.name, name );
     free( val.dptr );
     return TRUE;
@@ -111,13 +126,15 @@
     ASSERT( grp.dbf );
     lenServ = strlen( grp.serv );
     lenDsc = strlen( grp.dsc );
-    bufLen = sizeof( grp.entry ) + lenServ + lenDsc + 2;
+    bufLen = sizeof( grp.entry ) + lenServ + lenDsc + 2 + sizeof( char );
     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 );
+    p += lenDsc + 1;
+    p[ 0 ] = grp.postAllow;
     key.dptr = (void *)grp.name;
     key.dsize = strlen( grp.name ) + 1;
     val.dptr = buf;
@@ -152,14 +169,26 @@
     Utl_cpyStr( grp.name, name );
     Utl_cpyStr( grp.serv, "(unknown)" );
     grp.dsc[ 0 ] = '\0';
-    grp.entry.first = 0;
+    grp.entry.first = 1;
     grp.entry.last = 0;
     grp.entry.rmtNext = 0;
     grp.entry.created = 0;
     grp.entry.lastAccess = 0;
+    grp.postAllow = 'y';
     saveGrp();
 }
 
+void
+Grp_delete( const char *name )
+{
+    datum key;
+
+    ASSERT( grp.dbf );
+    key.dptr = (void*)name;
+    key.dsize = strlen( name ) + 1;
+    gdbm_delete( grp.dbf, key );
+}
+
 const char *
 Grp_dsc( const char *name )
 {
@@ -175,7 +204,7 @@
 
     if ( ! loadGrp( name ) )
         return "[unknown grp]";
-    if ( Grp_local( name ) || Cfg_servListContains( grp.serv ) )
+    if ( Cfg_servListContains( grp.serv ) )
         Utl_cpyStr( serv, grp.serv );
     else
         snprintf( serv, MAXCHAR, "[%s]", grp.serv );
@@ -222,6 +251,15 @@
     return grp.entry.created;
 }
 
+char
+Grp_postAllow( const char *name )
+{
+    if ( ! loadGrp( name ) )
+        return 0;
+    return grp.postAllow;
+}
+
+
 /* Replace group's description (only if value != ""). */
 void
 Grp_setDsc( const char *name, const char *value )
@@ -280,6 +318,16 @@
 }
 
 void
+Grp_setPostAllow( const char *name, char postAllow )
+{
+    if ( loadGrp( name ) )
+    {
+        grp.postAllow = postAllow;
+        saveGrp();
+    }
+}
+
+void
 Grp_setFirstLast( const char *name, int first, int last )
 {
     if ( loadGrp( name ) )
--- a/group.h	Thu May 04 09:16:09 2000 +0100
+++ b/group.h	Fri May 05 08:23:15 2000 +0100
@@ -3,7 +3,7 @@
 
   Groups database
 
-  $Id: group.h 32 2000-04-29 14:45:56Z enz $
+  $Id: group.h 44 2000-05-05 07:23:15Z enz $
 */
 
 #ifndef GRP_H
@@ -32,6 +32,10 @@
 void
 Grp_create( const char *name );
 
+/* delete a group and its articles from the database. */
+void
+Grp_delete( const char *name );
+
 /* Get group description */
 const char *
 Grp_dsc( const char *name );
@@ -65,6 +69,9 @@
 time_t
 Grp_created( const char *name );
 
+char
+Grp_postAllow( const char *name );
+
 /* Replace group's description (only if value != ""). */
 void
 Grp_setDsc( const char *name, const char *value );
@@ -87,6 +94,9 @@
 void
 Grp_setFirstLast( const char *name, int first, int last );
 
+void
+Grp_setPostAllow( const char *name, char postAllow );
+
 /* 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. */
--- a/itemlist.c	Thu May 04 09:16:09 2000 +0100
+++ b/itemlist.c	Fri May 05 08:23:15 2000 +0100
@@ -1,7 +1,7 @@
 /*
   itemlist.c
 
-  $Id: itemlist.c 32 2000-04-29 14:45:56Z enz $
+  $Id: itemlist.c 44 2000-05-05 07:23:15Z enz $
 */
 
 #include "itemlist.h"
@@ -34,7 +34,7 @@
 	exit( EXIT_FAILURE );
     }
     
-    res->list = (char *) malloc( strlen(list) + 2 );
+    res->list = (char *) malloc ( strlen(list) + 2 );
     if ( res->list == NULL )
     {
 	Log_err( "Malloc of ItemList.list failed." );
@@ -90,7 +90,7 @@
 
 /* Get first item. */
 const char *
-Itl_first( ItemList *self )
+Itl_first( ItemList *self)
 {
     self->next = self->list;
     return Itl_next( self );
--- a/noffle.1	Thu May 04 09:16:09 2000 +0100
+++ b/noffle.1	Fri May 05 08:23:15 2000 +0100
@@ -1,5 +1,5 @@
 .TH noffle 1
-.\" $Id: noffle.1 32 2000-04-29 14:45:56Z enz $
+.\" $Id: noffle.1 44 2000-05-05 07:23:15Z enz $
 .SH NAME
 noffle \- Usenet package optimized for dialup connections.
 
@@ -18,7 +18,10 @@
 \-d | \-\-database
 .br
 .B noffle
-\-e | \-\-expire <days>
+\-D | \-\-delete <newsgroup name>
+.br
+.B noffle
+\-e | \-\-expire
 .br
 .B noffle
 \-f | \-\-fetch
@@ -33,6 +36,12 @@
 \-l | \-\-list
 .br
 .B noffle
+\-m | \-\-modify desc <newsgroup name> <group description>
+.br
+.B noffle
+\-m | \-\-modify post <local newsgroup name> (y|n)
+.br
+.B noffle
 \-n | \-\-online
 .br
 .B noffle
@@ -104,8 +113,8 @@
 .TP
 .B \-a, \-\-article <message id>|all
 Write article <message id> to standard output. Message Id must contain
-the leading '<' and trailing '>' (quote the argument with single quotes to
-avoid shell interpretation of characters like '<' and '>' and '$').
+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. 
 
@@ -127,10 +136,21 @@
 Write the complete content of the article database to standard output.
 
 .TP
-.B \-e, \-\-expire <days>
-Delete all articles older than <days> days from the database.
+.B \-D, \-\-delete <newsgroup name>
+Delete the newsgroup with the given name. All articles that only
+belong to the group are deleted as well.
+
+.TP
+.B \-e, \-\-expire
+Delete all articles that have not been accessed recently from the
+database.
 Should be run regularily from
 .BR crond (8).
+.TP
+The default expire period is 14 days. This can be changed and
+custom expiry periods set for individual newsgroups or sets of
+newsgroups in
+.B /etc/noffle.conf.
 
 .TP
 .B \-f, \-\-fetch
@@ -154,7 +174,7 @@
 .br
 Format (fields separated by tabs):
 .br
-<name> <server> <first> <last> <remote next> <created> <last access> <desc>
+<name> <server> <first> <last> <remote next> <post allowed> <created> <last access> <desc>
 
 .TP
 .B \-h, \-\-help
@@ -167,6 +187,16 @@
 Format: <groupname> <server> full|thread|over
 
 .TP
+.B \-m | \-\-modify desc <newsgroup name> <group description>
+Modify the description of the named newsgroup.
+
+.TP
+.B \-m | \-\-modify post <local newsgroup name> <permission>
+Modify the posting permission on a local newsgroup. <permission> must
+be either 'y' (yes, posting allowed) or 'n' (no, posting not allowed).
+Attempts to post to a newsgroup with posting disabled will be rejected.
+
+.TP
 .B \-n, \-\-online
 Put
 .B NOFFLE
@@ -249,93 +279,17 @@
 
 .SH FILES
 
-There exists a spool directory (default
-.I /var/spool/noffle),
-and a config file (default
-.I /etc/noffle.conf).
-
-.PP
+.B NOFFLE
+takes its configuration from a configuration file, by default
+.I /etc/noffle.conf.
+For a description of this file, see
+.BR noffle.conf (5).
+.
 
-.TP
-.B <config file>
-Configuration file. Comment lines begin with
-.I #.
-Definition lines may contain:
-.br
-.B server <hostname>[:<port>] [<user> <pass>]
-Name of the remote server. If no port is given, port 119 is used.
-Username and password for servers that need authentication
-(Original AUTHINFO). The password may not contain white-spaces.
-If there are multiple server entries in the config file, all of them are
-used for getting groups. In this case the first server should be
-the one of your main provider. Note that you must always run
-"noffle --query groups" after making changes to the server entries.
-.br
-.B max-fetch <n>
-Never get more than <n> articles. If there are more, the oldest ones
-are discarded.
-.br
-Default: 300
-.br
-.B mail-to <address>
-Receiver of failed postings. If empty then failed postings are returned
-to the sender (taking the address from the article's Sender, X-Sender or
-From field, in this order).
-.br
-Default: <empty string>
-.br
-.B auto-unsubscribe yes|no
-Automatically remove groups from fetch list if they have not been
-accessed for a number of days.
-.br
-Default: no
-.br
-.B auto-unsubscribe-days <n>
-Number of days used for auto-unsubscribe option.
-.br
-Default: 30
-.br
-.B thread-follow-time <n>
-Automatically mark articles for download in thread mode, if they
-are referencing an article that has been opened by a reader within the last
-<n> days.
-.br
-Default: 7
-.br
-.B connect-timeout <n>
-Timeout for connecting to remote server in seconds.
-.br
-Default: 30
-.br
-.B auto-subscribe yes|no
-Automatically put groups on fetch list if someone reads them.
-<mode> can be full, over, thread (depending on the fetch mode) or
-off (do not subscribe automatically). Condition for putting a group
-on the list is that an article is opened. For this reason there is
-always a pseudo article visible in groups that are not on the fetch list.
-.br
-Default: no
-.br
-.B auto-subscribe-mode full|thread|over
-Mode for auto-subscribe option.
-.br
-Default: over
-.br
-.B remove-messageid yes|no
-Remove Message-ID from posted articles. Some remote servers can generate
-Message-IDs.
-.br
-Default: no
-.br
-.B replace-messageid yes|no
-Replace Message-ID of posted articles by a Message-ID generated by
-NOFFLE. Some news readers generate Message-IDs that are not accepted by
-some servers. For generating Message-IDs, the domain name of your system should
-be a valid domain name. If you are in a local domain, set it to your
-provider's domain name.
-.br
-Default: yes
-.br
+.B NOFFLE
+keeps all its data files in a spool directory.
+.I /var/spool/noffle
+is the default location.
 
 .TP
 .B <spool dir>/fetchlist
@@ -373,9 +327,10 @@
 
 .SH SEE ALSO
 
-.BR crond (8)
+.BR noffle.conf (5),
+.BR crond (8),
 .BR inetd (8),
-.BR pppd (8),
+.BR pppd (8)
 .br
 .B RFC 977,
 .B RFC 1036,
--- a/noffle.c	Thu May 04 09:16:09 2000 +0100
+++ b/noffle.c	Fri May 05 08:23:15 2000 +0100
@@ -10,7 +10,7 @@
   received for some seconds (to allow multiple clients connect at the same
   time).
 
-  $Id: noffle.c 40 2000-05-01 09:23:31Z enz $
+  $Id: noffle.c 44 2000-05-05 07:23:15Z enz $
 */
 
 #include <ctype.h>
@@ -224,7 +224,7 @@
                     else
                         ++cntLeft;
                 }
-            if ( !Grp_local( grp )
+            if ( ! Grp_local( grp )
 		 && autoUnsubscribe
                  && difftime( now, Grp_lastAccess( grp ) ) > maxAge )
             {
@@ -246,10 +246,10 @@
 }
 
 static void
-doExpire( unsigned int days )
+doExpire( void )
 {
     Db_close();
-    Db_expire( days );
+    Db_expire();
     if ( ! Db_open() )
         return;
     expireContents();
@@ -258,15 +258,70 @@
 static void
 doCreateLocalGroup( const char * name )
 {
+    Str grp;
+
+    Utl_cpyStr( grp, name );
+    Utl_toLower( grp );
+    name = Utl_stripWhiteSpace( grp );
+    
     if ( Grp_exists( name ) )
         fprintf( stderr, "'%s' already exists.\n", name );
     else
     {
         Log_inf( "Creating new local group '%s'", name );
         Grp_create( name );
-        Grp_setFirstLast( name, 1, 0 );
         Grp_setLocal( name );
-        printf( "New local group '%s' created.\n", name );
+	printf( "New local group '%s' created.\n", name );
+    }
+}
+
+static void
+doDeleteLocalGroup( const char * name )
+{
+    Str grp;
+
+    Utl_cpyStr( grp, name );
+    Utl_toLower( grp );
+    name = Utl_stripWhiteSpace( grp );
+    
+    if ( ! Grp_exists( name ) )
+        fprintf( stderr, "'%s' does not exist.\n", name );
+    else
+    {
+	int i;
+	
+        Log_inf( "Deleting group '%s'", name );
+
+	/*
+	  Delete all articles that are only in the group. Check the
+	  article Xref for more than one group.
+	 */
+	Cont_read( name );
+	for ( i = Cont_first(); i <= Cont_last(); i++ )
+	{
+	    const Over *over;
+	    Bool toDelete;
+	    Str msgId;
+
+	    over = Cont_get( i );
+	    toDelete = TRUE;
+	    if ( over != NULL )
+	    {
+		ItemList * xref;
+
+		Utl_cpyStr( msgId, Ov_msgId( over ) );
+		xref = new_Itl( Db_xref( msgId ), " " );
+		if ( Itl_count( xref ) > 1 )
+		    toDelete = FALSE;
+		del_Itl( xref );
+	    }
+	    Cont_delete( i );
+	    if ( toDelete )
+		Db_delete( msgId );
+	}
+	Cont_write();
+	Grp_delete( name );
+	printf( "Group '%s' deleted.\n", name );
     }
 }
 
@@ -298,6 +353,65 @@
         }
 }
 
+/* A modify command. argc/argv start AFTER '-m'. */
+static Bool
+doModify( const char *cmd, int argc, char **argv )
+{
+    const char *grp;
+
+    if ( argc < 2 )
+    {
+	fprintf( stderr, "Insufficient arguments to -m\n" );
+	return FALSE;
+	
+    }
+    else if ( strcmp( cmd, "desc" ) != 0
+	      && strcmp( cmd, "post" ) != 0 )
+    {
+	fprintf( stderr, "Unknown argument -m %s\n", optarg );
+	return FALSE;
+    }
+    
+    grp = argv[ 0 ];
+    argv++;
+    argc--;
+    
+    if ( strcmp( cmd, "desc" ) == 0 )
+    {
+	Str desc;
+
+	Utl_cpyStr( desc, *( argv++ ) );
+	while ( --argc > 0 )
+	{
+	    Utl_catStr( desc, " " );
+	    Utl_catStr( desc, *( argv++ ) );
+	}
+
+	Grp_setDsc( grp, desc );
+    }
+    else
+    {
+	char c;
+
+	if ( ! Grp_local( grp ) )
+	{
+	    fprintf( stderr, "%s is not a local group\n", grp );
+	    return FALSE;
+	}
+	
+	c = **argv;
+	if ( c == 'y' || c == 'm' || c == 'n' )
+	    Grp_setPostAllow( grp, c );
+	else
+	{
+	    fprintf( stderr, "Access must be 'y', 'n' or 'm'" );
+	    return FALSE;
+	}
+    }
+
+    return TRUE;
+}
+
 static void
 doGrps( void )
 {
@@ -316,9 +430,9 @@
                       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",
+            printf( "%s\t%s\t%i\t%i\t%i\t%c\t%s\t%s\t%s\n",
                     g, Grp_serv( g ), Grp_first( g ), Grp_last( g ),
-                    Grp_rmtNext( g ), dateCreated,
+                    Grp_rmtNext( g ), Grp_postAllow( g ), dateCreated,
                     dateLastAccess, Grp_dsc( g ) );
         }
         while ( Grp_nextGrp( &g ) );
@@ -364,27 +478,30 @@
     static const char *msg =
       "Usage: noffle <option>\n"
       "Option is one of the following:\n"
-      " -a | --article <msg id>|all   Show article(s) in database\n"
-      " -c | --cancel <msg id>        Remove article from database\n"
-      " -C | --create <grp>           Create a local group\n"
-      " -d | --database               Show content of article database\n"
-      " -e | --expire <n>             Expire articles older than <n> days\n"
-      " -f | --fetch                  Get newsfeed from server/post articles\n"
-      " -g | --groups                 Show all groups available at server\n"
-      " -h | --help                   Show this text\n"
-      " -l | --list                   List groups on fetch list\n"
-      " -n | --online                 Switch to online mode\n"
-      " -o | --offline                Switch to offline mode\n"
-      " -q | --query groups           Get group list from server\n"
-      " -q | --query desc             Get group descriptions from server\n"
-      " -q | --query times            Get group creation times from server\n"
-      " -r | --server                 Run as server on stdin/stdout\n"
-      " -R | --requested              List articles marked for download\n"
-      " -s | --subscribe-over <grp>   Add group to fetch list (overview)\n"
-      " -S | --subscribe-full <grp>   Add group to fetch list (full)\n"
-      " -t | --subscribe-thread <grp> Add group to fetch list (thread)\n"
-      " -u | --unsubscribe <grp>      Remove group from fetch list\n"
-      " -v | --version                Print version\n";
+      " -a | --article <msg id>|all      Show article(s) in database\n"
+      " -c | --cancel <msg id>           Remove article from database\n"
+      " -C | --create <grp>              Create a local group\n"
+      " -d | --database                  Show content of article database\n"
+      " -D | --delete <grp>              Delete a group\n"
+      " -e | --expire                    Expire articles\n"
+      " -f | --fetch                     Get newsfeed from server/post articles\n"
+      " -g | --groups                    Show all groups available at server\n"
+      " -h | --help                      Show this text\n"
+      " -l | --list                      List groups on fetch list\n"
+      " -m | --modify desc <grp> <desc>  Modify a group description\n"
+      " -m | --modify post <grp> (y|n)   Modify posting status of a local group\n"
+      " -n | --online                    Switch to online mode\n"
+      " -o | --offline                   Switch to offline mode\n"
+      " -q | --query groups              Get group list from server\n"
+      " -q | --query desc                Get group descriptions from server\n"
+      " -q | --query times               Get group creation times from server\n"
+      " -r | --server                    Run as server on stdin/stdout\n"
+      " -R | --requested                 List articles marked for download\n"
+      " -s | --subscribe-over <grp>      Add group to fetch list (overview)\n"
+      " -S | --subscribe-full <grp>      Add group to fetch list (full)\n"
+      " -t | --subscribe-thread <grp>    Add group to fetch list (thread)\n"
+      " -u | --unsubscribe <grp>         Remove group from fetch list\n"
+      " -v | --version                   Print version\n";
     fprintf( stderr, "%s", msg );
 }
 
@@ -442,7 +559,7 @@
 static void
 bugReport( int sig )
 {
-    Log_err( "Received SIGSEGV. Please submit a bug report." );
+    Log_err( "Received SIGSEGV. Please submit a bug report" );
     signal( SIGSEGV, SIG_DFL );
     raise( sig );
 }
@@ -487,11 +604,13 @@
         { "cancel",           required_argument, NULL, 'c' },
         { "create",           required_argument, NULL, 'C' },
         { "database",         no_argument,       NULL, 'd' },
-        { "expire",           required_argument, NULL, 'e' },
+        { "delete",           required_argument, NULL, 'D' },
+        { "expire",           no_argument,       NULL, 'e' },
         { "fetch",            no_argument,       NULL, 'f' },
         { "groups",           no_argument,       NULL, 'g' },
         { "help",             no_argument,       NULL, 'h' },
         { "list",             no_argument,       NULL, 'l' },
+        { "modify",           required_argument, NULL, 'm' },
         { "offline",          no_argument,       NULL, 'o' },
         { "online",           no_argument,       NULL, 'n' },
         { "query",            required_argument, NULL, 'q' },
@@ -512,7 +631,7 @@
     signal( SIGINT, logSignal );
     signal( SIGTERM, logSignal );
     signal( SIGPIPE, logSignal );
-    c = getopt_long( argc, argv, "a:c:C:de:fghlonq:rRs:S:t:u:v",
+    c = getopt_long( argc, argv, "a:c:C:dD:efghlm:onq:rRs:S:t:u:v",
                      longOptions, NULL );
     if ( ! initNoffle( c != 'r' ) )
         return EXIT_FAILURE;
@@ -552,19 +671,18 @@
     case 'd':
         doDb();
         break;
-    case 'e':
+    case 'D':
+        if ( ! optarg )
         {
-            unsigned int days;
-
-            if ( ! optarg || sscanf( optarg, "%u", &days ) != 1 )
-            {
-                fprintf( stderr, "Bad argument: -e %s\n", optarg );
-                result = EXIT_FAILURE;
-            }
-            else
-                doExpire( days );
+            fprintf( stderr, "Option -D needs argument.\n" );
+            result = EXIT_FAILURE;
         }
+        else
+            doDeleteLocalGroup( optarg );
         break;
+    case 'e':
+	doExpire();
+	break;
     case 'f':
         doFetch();
         break;
@@ -578,6 +696,16 @@
     case 'l':
         doList();
         break;
+    case 'm':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -m needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+	    if ( ! doModify( optarg, argc - optind, &argv[ optind ] ) )
+		result = EXIT_FAILURE;
+        break;
     case 'n':
         if ( Online_true() )
             fprintf( stderr, "NOFFLE is already online\n" );
--- a/noffle.conf.5	Thu May 04 09:16:09 2000 +0100
+++ b/noffle.conf.5	Fri May 05 08:23:15 2000 +0100
@@ -1,14 +1,200 @@
-
 .TH noffle.conf 5
-.\" $Id: noffle.conf.5 3 2000-01-04 11:35:42Z enz $
+.\" $Id: noffle.conf.5 44 2000-05-05 07:23:15Z enz $
 .SH NAME
 noffle.conf \- Configuration file for NOFFLE news server
 
 .SH DESCRIPTION
 
-noffle.conf is the configuration file of the NOFFLE
-news server.
+The
+.B NOFFLE
+news server - see
+.BR noffle (1)
+- takes its configuration from a configuration file.
+By default this file is \fI/etc/noffle.conf\fP.
+
+.PP
+.B noffle.conf
+is a normal text file containing
+.B NOFFLE
+settings, one per line.
+
+.PP
+Leading whitespace on a line is ignored, as is any comment
+text. Comment text begins with a '#' character and continues to the
+end of the line. Blank lines are permitted.
+
+.SH SETTINGS
+
+.TP
+.B server <hostname>[:<port>] [<user> <pass>]
+Name of the remote server. If no port given, port 119 is used.
+Username and password for servers that need authentication
+(Original AUTHINFO). The password may not contain white-spaces.
+If there are multiple server entries in the config file, all of them are
+used for getting groups. In this case the first server should be
+the one of your main provider. Note that you must always
+run 'noffle --query groups'
+after making changes to the server entries.
+
+.TP
+.B max-fetch <n>
+Never get more than <n> articles. If there are more, the oldest ones
+are discarded.
+.br
+Default: 300
+
+.TP
+.B mail-to <address>
+Receiver of failed postings. If empty then failed postings are returned
+to the sender (taking the address from the article's Sender, X-Sender or
+From field, in this order).
+.br
+Default: <empty string>
+
+.TP
+.B auto-unsubscribe yes|no
+Automatically remove groups from fetch list if they have not been
+accessed for a number days.
+.br
+Default: no
+
+.TP
+.B auto-unsubscribe-days <n>
+Number of days used for auto-unsubscribe option.
+.br
+Default: 30
+
+.TP
+.B thread-follow-time <n>
+Automatically mark articles for download in thread mode, if they
+are referencing an article that has been opened by a reader within the last
+<n> days.
+.br
+Default: 7
+
+.TP
+.B connect-timeout <n>
+Timeout for connecting to remote server in seconds.
+.br
+Default: 30
+
+.TP
+.B auto-subscribe yes|no
+Automatically put groups on fetch list if someone reads them.
+<mode> can be full, over, thread (depending on the fetch mode) or
+off (do not subscribe automatically). Condition for putting a group
+on the list is that an article is opened. For this reason there is
+always a pseudo article visible in groups that are not on the fetch list.
+.br
+Default: no
+
+.TP
+.B auto-subscribe-mode full|thread|over
+Mode for auto-subscribe option.
+.br
+Default: over
+
+.TP
+.B remove-messageid yes|no
+Remove Message-ID from posted articles. Some remote servers can generate
+Message-IDs.
+.br
+Default: no
 
-See
-.BR noffle (3)
-for information about NOFFLE and the format of the configuration file.
+.TP
+.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
+
+.TP
+.B default-expire <n>
+The default expiry period, in days. An expiry period of 0 means "never".
+.br
+Default: 14
+
+.TP
+.B expire <group pattern> <n>
+The expiry period for a newsgroup or set of newsgroups, in days. The
+expiry pattern can contain \fIwildcards\fP, and there can be multiple
+.B expire
+lines. When checking the expiry period for a group, the expiry
+patterns are checked in the order in which they appear in
+.I /etc/noffle.conf
+until the first match occurs. If no pattern matches the group name, the
+.B default expiry period
+is used. An expiry period of 0 means "never".
+.br
+Default: no
+
+.SH "GROUP NAME WILDCARDS"
+
+.B NOFFLE
+uses a wildcard format that closely matches filename-style wildcards.
+\fIalt.binaries.*\fP, for example, matches all newsgroups under the
+.I alt.binaries
+hierarchy. A full description of the fomat (known as
+.B wildmat
+patterns) is as follows.
+
+.TP
+.BI \e x
+Turns off the special meaning of
+.I x
+and matches it directly; this is used mostly before a question mark or
+asterisk, and is not special inside square brackets.
+.TP
+.B ?
+Matches any single character.
+.TP
+.B *
+Matches any sequence of zero or more characters.
+.TP
+.BI [ x...y ]
+Matches any single character specified by the set
+.IR x...y .
+A minus sign may be used to indicate a range of characters.
+That is,
+.I [0\-5abc]
+is a shorthand for
+.IR [012345abc] .
+More than one range may appear inside a character set;
+.I [0-9a-zA-Z._]
+matches almost all of the legal characters for a host name.
+The close bracket,
+.IR ] ,
+may be used if it is the first character in the set.
+The minus sign,
+.IR \- ,
+may be used if it is either the first or last character in the set.
+.TP
+.BI [^ x...y ]
+This matches any character
+.I not
+in the set
+.IR x...y ,
+which is interpreted as described above.
+For example,
+.I [^]\-]
+matches any character other than a close bracket or minus sign.
+
+
+.SH SEE ALSO
+
+.BR noffle (1)
+
+.SH AUTHORS
+
+Markus Enzenberger <markus.enzenberger@t-online.de>
+.br
+Volker Wysk <volker.wysk@student.uni-tuebingen.de>
+.br
+Jim Hague <jim.hague@acm.org>
+.br
+1998-2000.
+
+
--- a/noffle.conf.example	Thu May 04 09:16:09 2000 +0100
+++ b/noffle.conf.example	Fri May 05 08:23:15 2000 +0100
@@ -52,3 +52,18 @@
 
 remove-messageid no
 replace-messageid yes
+
+# Set the default expire period in days
+default-expire 14
+
+# Expire all alt.* groups after 2 days, except for alt.oxford.*
+# expire after 4 days and alt.oxford.talk never expire.
+#expire alt.oxford.talk 0
+#expire alt.oxford.* 4
+#expire alt.* 2
+
+# Appearing here, this is equivalent to 'default-expire 20' above. If it
+# appeared before the other expire lines, all groups would be
+# expired at 20 days, as it would be the first custom match
+# for every group.
+#expire * 20
--- a/pseudo.c	Thu May 04 09:16:09 2000 +0100
+++ b/pseudo.c	Fri May 05 08:23:15 2000 +0100
@@ -1,7 +1,7 @@
 /*
   pseudo.c
   
-  $Id: pseudo.c 32 2000-04-29 14:45:56Z enz $
+  $Id: pseudo.c 44 2000-05-05 07:23:15Z enz $
 */
 
 #include "pseudo.h"
@@ -14,6 +14,7 @@
 #include "group.h"
 #include "log.h"
 #include "protocol.h"
+#include "util.h"
 
 Over *
 genOv( const char *rawSubj, const char *rawBody, const char *suffix )
@@ -24,7 +25,7 @@
 
     snprintf( subj, MAXCHAR, "[ %s ]", rawSubj );
     time( &t );
-    strftime( date, MAXCHAR, "%d %b %Y %H:%M:%S %Z", localtime( &t ) );
+    Utl_rfc822Date( t, date );
     Prt_genMsgId( msgId, "", suffix );
     bytes = lines = 0;
     while ( *rawBody )
@@ -245,8 +246,7 @@
                     "consistent Probably the remote news server\n"
                     "was changed or has reset its article counter\n"
                     "for this group. As a consequence there could\n"
-                    "be some articles be duplicated in this group\n"
-                    "\n" );
+                    "be some articles be duplicated in this group\n" );
         snprintf( s, MAXCHAR, "Group: %s", grp );
         DynStr_appLn( info, s );
         snprintf( s, MAXCHAR, "Remote first article number: %i", first );
@@ -275,8 +275,7 @@
                     "deleted them.\n"
                     "If this group is on the fetch list, then\n"
                     "contact your newsmaster to ensure that\n"
-                    "\"noffle\" is fetching news more frequently.\n"
-                    "\n" );
+                    "\"noffle\" is fetching news more frequently.\n" );
         snprintf( s, MAXCHAR, "Group: %s", grp );
         DynStr_appLn( info, s );
         snprintf( s, MAXCHAR, "Remote next article number: %i", next );
@@ -303,8 +302,7 @@
                     "some time.\n"
                     "Re-subscribing is done either automatically\n"
                     "by NOFFLE (if configured) or by manually\n"
-                    "running the 'noffle --subscribe' command\n"
-                    "\n" );
+                    "running the 'noffle --subscribe' command\n" );
         snprintf( s, MAXCHAR, "Group: %s", grp );
         DynStr_appLn( info, s );
         snprintf( s, MAXCHAR, "Days without access: %i", days );
@@ -325,8 +323,7 @@
         DynStr_app( info,
                     "NOFFLE has now automatically subscribed to\n"
                     "this group. It will fetch articles next time\n"
-                    "it is online.\n"
-                    "\n" );
+                    "it is online.\n" );
         genPseudo( "Auto subscribed", DynStr_str( info ) );
     }
     del_DynStr( info );
--- a/server.c	Thu May 04 09:16:09 2000 +0100
+++ b/server.c	Fri May 05 08:23:15 2000 +0100
@@ -1,7 +1,7 @@
 /*
   server.c
 
-  $Id: server.c 43 2000-05-04 08:16:09Z enz $
+  $Id: server.c 44 2000-05-05 07:23:15Z enz $
 */
 
 #include "server.h"
@@ -32,6 +32,7 @@
 #include "pseudo.h"
 #include "request.h"
 #include "util.h"
+#include "wildmat.h"
 
 struct
 {
@@ -254,9 +255,11 @@
         changeToGrp( arg );
         first = Cont_first();
         last = Cont_last();
-        numb = last - first + 1;
-        if ( first > last )
+	if ( ( first == 0 && last == 0 )
+	     || first > last )
             first = last = numb = 0;
+	else
+	    numb = last - first + 1;
         putStat( STAT_GRP_SELECTED, "%lu %lu %lu %s selected",
                  numb, first, last, arg );
     }
@@ -339,7 +342,7 @@
     const Over *ov;
     int n;
 
-    if ( sscanf( arg, "%i", &n ) == 1 )
+    if ( sscanf( arg, "%d", &n ) == 1 )
     {
         if ( ! checkNumb( n ) )
             return FALSE;
@@ -585,7 +588,7 @@
             Log_err( "Cannot open pipe to 'sort'" );
             if ( Grp_firstGrp( &g ) )
                 do
-                    if ( Utl_matchPattern( g, pat ) )
+                    if ( Wld_match( g, pat ) )
                     {
                         (*printProc)( line, g );
                         if ( ! Prt_putTxtLn( line, stdout ) )
@@ -597,7 +600,7 @@
         {
             if ( Grp_firstGrp( &g ) )
                 do
-                    if ( Utl_matchPattern( g, pat ) )
+                    if ( Wld_match( g, pat ) )
                     {
                         (*printProc)( line, g );
                         if ( ! Prt_putTxtLn( line, f ) )
@@ -609,7 +612,7 @@
                 while ( Grp_nextGrp( &g ) );
             ret = pclose( f );
             if ( ret != EXIT_SUCCESS )
-                Log_err( "sort command returned %i", ret );
+                Log_err( "sort command returned %d", ret );
             fflush( stdout );
             Log_dbg( "[S FLUSH]" );
             signal( SIGPIPE, lastHandler );
@@ -633,8 +636,8 @@
 static void
 printActive( Str result, const char *grp )
 {
-    snprintf( result, MAXCHAR, "%s %i %i y",
-              grp, Grp_last( grp ), Grp_first( grp ) );
+    snprintf( result, MAXCHAR, "%s %d %d %c",
+              grp, Grp_last( grp ), Grp_first( grp ), Grp_postAllow( grp ) );
 }
 
 static void
@@ -1043,8 +1046,7 @@
 		    time_t t;
 
 		    time( &t );
-		    strftime( val, MAXCHAR, "%d %b %Y %H:%M:%S %Z",
-			      localtime( &t ) );
+		    Utl_rfc822Date( t, val );
 		    DynStr_app( s, "Date: " );
 		    DynStr_appLn( s, val );
 		}
@@ -1061,13 +1063,13 @@
                 }
                 else if ( strcmp( field, "newsgroups" ) == 0 )
                 {
-                    Utl_toLower( val );
-                    newsgroups = new_Itl ( val, " ," );
+		    Utl_toLower( val );
+		    newsgroups = new_Itl ( val, " ," );
                     DynStr_appLn( s, p );
                 }
                 else if ( strcmp( field, "control" ) == 0 )
                 {
-                    control = new_Itl ( val, " " );
+		    control = new_Itl ( val, " " );
                     DynStr_appLn( s, p );
                 }
                 else if ( strcmp( field, "reply-to" ) == 0 )
@@ -1107,6 +1109,7 @@
 	{
 	    const char *grp;
 	    Bool knownGrp = FALSE;
+	    Bool postAllowedGrp = FALSE;
 
 	    /* Check at least one group is known. */
 	    for( grp = Itl_first( newsgroups );
@@ -1116,7 +1119,19 @@
 		if ( Grp_exists( grp ) )
 		{
 		    knownGrp = TRUE;
-		    break;
+		    switch( Grp_postAllow( grp ) )
+		    {
+		    case 'n':
+			break;
+		    case 'm':
+			/* Can't post to moderated local groups. */
+			postAllowedGrp = ! Grp_local( grp );
+			break;
+		    default:
+			postAllowedGrp = TRUE;
+		    }
+		    if ( postAllowedGrp )
+			break;
 		}
 	    }
 	    
@@ -1126,6 +1141,12 @@
 		Log_err( "No known group in Newsgroups header field" );
 		err = TRUE;
 	    }
+	    else if ( ! postAllowedGrp )
+	    {
+
+		Log_err( "No group permits posting" );
+		err = TRUE;
+	    }
 	    else
 	    {
 		err = ( control == NULL )
@@ -1157,7 +1178,7 @@
 
     Utl_cpyStr( t, s );
     p = Utl_stripWhiteSpace( t );
-    r = sscanf( p, "%i-%i", first, last );
+    r = sscanf( p, "%d-%d", first, last );
     if ( r < 1 )
     {
         *first = serv.artPtr;
@@ -1273,11 +1294,11 @@
 
     if ( ! testGrpSelected() )
         return TRUE;
-    if ( sscanf( arg, "%s %i-%i %s", whatStr, &first, &last, pat ) != 4 )
+    if ( sscanf( arg, "%s %d-%d %s", whatStr, &first, &last, pat ) != 4 )
     {
-        if ( sscanf( arg, "%s %i- %s", whatStr, &first, pat ) == 3 )
+        if ( sscanf( arg, "%s %d- %s", whatStr, &first, pat ) == 3 )
             last = Cont_last();
-        else if ( sscanf( arg, "%s %i %s", whatStr, &first, pat ) == 3 )
+        else if ( sscanf( arg, "%s %d %s", whatStr, &first, pat ) == 3 )
             last = first;
         else
         {
@@ -1310,23 +1331,23 @@
             switch ( what )
             {
             case SUBJ:
-                if ( Utl_matchPattern( Ov_subj( ov ), pat ) )
+                if ( Wld_match( Ov_subj( ov ), pat ) )
                      putTxtLn( "%lu %s", n, Ov_subj( ov ) );
                 break;
             case FROM:
-                if ( Utl_matchPattern( Ov_from( ov ), pat ) )
+                if ( Wld_match( Ov_from( ov ), pat ) )
                     putTxtLn( "%lu %s", n, Ov_from( ov ) );
                 break;
             case DATE:
-                if ( Utl_matchPattern( Ov_date( ov ), pat ) )
+                if ( Wld_match( Ov_date( ov ), pat ) )
                     putTxtLn( "%lu %s", n, Ov_date( ov ) );
                 break;
             case MSG_ID:
-                if ( Utl_matchPattern( Ov_msgId( ov ), pat ) )
+                if ( Wld_match( Ov_msgId( ov ), pat ) )
                     putTxtLn( "%lu %s", n, Ov_msgId( ov ) );
                 break;
             case REF:
-                if ( Utl_matchPattern( Ov_ref( ov ), pat ) )
+                if ( Wld_match( Ov_ref( ov ), pat ) )
                     putTxtLn( "%lu %s", n, Ov_ref( ov ) );
                 break;
             default:
--- a/util.c	Thu May 04 09:16:09 2000 +0100
+++ b/util.c	Fri May 05 08:23:15 2000 +0100
@@ -1,13 +1,12 @@
 /*
   util.c
 
-  $Id: util.c 32 2000-04-29 14:45:56Z enz $
+  $Id: util.c 44 2000-05-05 07:23:15Z enz $
 */
 
 #include "util.h"
 #include <errno.h>
 #include <ctype.h>
-#include <fnmatch.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
@@ -15,6 +14,7 @@
 #include <unistd.h>
 #include "config.h"
 #include "log.h"
+#include "wildmat.h"
 
 static const char *
 nextWhiteSpace( const char *p )
@@ -162,6 +162,17 @@
 }
 
 void
+Utl_stripComment( char *line )
+{
+    for ( ; *line != '\0'; line++ )
+	if ( *line =='#' )
+	{
+	    *line = '\0';
+	    break;
+	}
+}
+
+void
 Utl_cpyStr( Str dst, const char *src )
 {
     dst[ 0 ] = '\0';
@@ -226,6 +237,12 @@
 }
 
 void
+Utl_rfc822Date( time_t t, Str res )
+{
+    strftime( res, MAXCHAR,"%a, %d %b %Y %H:%M:%S %z", localtime( &t ) );
+}
+
+void
 Utl_allocAndCpy( char **dst, const char *src )
 {
     int len = strlen( src );
@@ -236,11 +253,3 @@
     }
     memcpy( *dst, src, len + 1 );
 }
-
-Bool
-Utl_matchPattern( const char *text, const char *pattern )
-{
-    if ( pattern[ 0 ] == '*' && pattern[ 1 ] == '\0' )
-        return TRUE;
-    return ( fnmatch( pattern, text, 0 ) == 0 );
-}
--- a/util.h	Thu May 04 09:16:09 2000 +0100
+++ b/util.h	Fri May 05 08:23:15 2000 +0100
@@ -3,7 +3,7 @@
 
   Miscellaneous helper functions.
 
-  $Id: util.h 32 2000-04-29 14:45:56Z enz $
+  $Id: util.h 44 2000-05-05 07:23:15Z enz $
 */
 
 #ifndef UTL_H
@@ -49,6 +49,10 @@
 char *
 Utl_stripWhiteSpace( char *line );
 
+/* Strip comment from a line. Comments start with '#'. */
+void
+Utl_stripComment( char *line );
+
 /* Write timestamp into <file>. */
 void
 Utl_stamp( Str file );
@@ -57,6 +61,10 @@
 Bool
 Utl_getStamp( time_t *result, Str file );
 
+/* Put RFC822-compliant date string into res. */
+void
+Utl_rfc822Date( time_t t, Str res );
+
 void
 Utl_cpyStr( Str dst, const char *src );
 
@@ -73,10 +81,4 @@
 void
 Utl_allocAndCpy( char **dst, const char *src );
 
-/*
-  Do shell-style pattern matching for ?, \, [], and * characters.
-*/
-Bool
-Utl_matchPattern( const char *text, const char *pattern );
-
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wildmat.c	Fri May 05 08:23:15 2000 +0100
@@ -0,0 +1,206 @@
+/*
+  wildmat.c
+
+  Taken from the INN 2.2 distribution and slightly altered to fit the
+  Noffle environment. Changes are:
+  o Rename wildmat() to Wld_match().
+  o Adjust includes.
+  
+  $Id: wildmat.c 44 2000-05-05 07:23:15Z enz $
+
+  The entire INN distribution is covered by the following copyright
+  notice. As this file originated in the INN distribution is it
+  subject to the conditions of this notice.
+  
+    Copyright 1991 Rich Salz.
+    All rights reserved.
+    $Revision: 44 $
+
+    Redistribution and use in any form are permitted provided that the
+    following restrictions are are met:
+        1.  Source distributions must retain this entire copyright notice
+            and comment.
+        2.  Binary distributions must include the acknowledgement ``This
+            product includes software developed by Rich Salz'' in the
+            documentation or other materials provided with the
+            distribution.  This must not be represented as an endorsement
+            or promotion without specific prior written permission.
+        3.  The origin of this software must not be misrepresented, either
+            by explicit claim or by omission.  Credits must appear in the
+            source and documentation.
+        4.  Altered versions must be plainly marked as such in the source
+            and documentation and must not be misrepresented as being the
+            original software.
+    THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED
+    WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
+    MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+
+*/
+
+/*  $Revision: 44 $
+**
+**  Do shell-style pattern matching for ?, \, [], and * characters.
+**  Might not be robust in face of malformed patterns; e.g., "foo[a-"
+**  could cause a segmentation violation.  It is 8bit clean.
+**
+**  Written by Rich $alz, mirror!rs, Wed Nov 26 19:03:17 EST 1986.
+**  Rich $alz is now <rsalz@osf.org>.
+**  April, 1991:  Replaced mutually-recursive calls with in-line code
+**  for the star character.
+**
+**  Special thanks to Lars Mathiesen <thorinn@diku.dk> for the ABORT code.
+**  This can greatly speed up failing wildcard patterns.  For example:
+**	pattern: -*-*-*-*-*-*-12-*-*-*-m-*-*-*
+**	text 1:	 -adobe-courier-bold-o-normal--12-120-75-75-m-70-iso8859-1
+**	text 2:	 -adobe-courier-bold-o-normal--12-120-75-75-X-70-iso8859-1
+**  Text 1 matches with 51 calls, while text 2 fails with 54 calls.  Without
+**  the ABORT code, it takes 22310 calls to fail.  Ugh.  The following
+**  explanation is from Lars:
+**  The precondition that must be fulfilled is that DoMatch will consume
+**  at least one character in text.  This is true if *p is neither '*' nor
+**  '\0'.)  The last return has ABORT instead of FALSE to avoid quadratic
+**  behaviour in cases like pattern "*a*b*c*d" with text "abcxxxxx".  With
+**  FALSE, each star-loop has to run to the end of the text; with ABORT
+**  only the last one does.
+**
+**  Once the control of one instance of DoMatch enters the star-loop, that
+**  instance will return either TRUE or ABORT, and any calling instance
+**  will therefore return immediately after (without calling recursively
+**  again).  In effect, only one star-loop is ever active.  It would be
+**  possible to modify the code to maintain this context explicitly,
+**  eliminating all recursive calls at the cost of some complication and
+**  loss of clarity (and the ABORT stuff seems to be unclear enough by
+**  itself).  I think it would be unwise to try to get this into a
+**  released version unless you have a good test data base to try it out
+**  on.
+*/
+#include <stdio.h>
+#include <sys/types.h>
+#include "common.h"
+#include "log.h"
+
+#define	ABORT		(-1)
+
+/* What character marks an inverted character class? */
+#define NEGATE_CLASS		'^'
+    /* Is "*" a common pattern? */
+#define OPTIMIZE_JUST_STAR
+    /* Do tar(1) matching rules, which ignore a trailing slash? */
+#undef MATCH_TAR_PATTERN
+
+
+/*
+**  Match text and p, return TRUE, FALSE, or ABORT.
+*/
+static int DoMatch(const char *text, const char *p)
+{
+    int	                last;
+    int	                matched;
+    int	                reverse;
+
+    for ( ; *p; text++, p++) {
+	if (*text == '\0' && *p != '*')
+	    return ABORT;
+	switch (*p) {
+	case '\\':
+	    /* Literal match with following character. */
+	    p++;
+	    /* FALLTHROUGH */
+	default:
+	    if (*text != *p)
+		return FALSE;
+	    continue;
+	case '?':
+	    /* Match anything. */
+	    continue;
+	case '*':
+	    while (*++p == '*')
+		/* Consecutive stars act just like one. */
+		continue;
+	    if (*p == '\0')
+		/* Trailing star matches everything. */
+		return TRUE;
+	    while (*text)
+		if ((matched = DoMatch(text++, p)) != FALSE)
+		    return matched;
+	    return ABORT;
+	case '[':
+	    reverse = p[1] == NEGATE_CLASS ? TRUE : FALSE;
+	    if (reverse)
+		/* Inverted character class. */
+		p++;
+	    matched = FALSE;
+	    if (p[1] == ']' || p[1] == '-')
+		if (*++p == *text)
+		    matched = TRUE;
+	    for (last = *p; *++p && *p != ']'; last = *p)
+		/* This next line requires a good C compiler. */
+		if (*p == '-' && p[1] != ']'
+		    ? *text <= *++p && *text >= last : *text == *p)
+		    matched = TRUE;
+	    if (matched == reverse)
+		return FALSE;
+	    continue;
+	}
+    }
+
+#ifdef	MATCH_TAR_PATTERN
+    if (*text == '/')
+	return TRUE;
+#endif	/* MATCH_TAR_ATTERN */
+    return *text == '\0';
+}
+
+
+/*
+**  User-level routine.  Returns TRUE or FALSE.
+*/
+Bool
+Wld_match(const char *text, const char *pattern)
+{
+#ifdef	OPTIMIZE_JUST_STAR
+    if (pattern[0] == '*' && pattern[1] == '\0')
+	return TRUE;
+#endif	/* OPTIMIZE_JUST_STAR */
+    return DoMatch(text, pattern) == TRUE;
+}
+
+
+
+#if	defined(WILDMAT_TEST)
+
+/* Yes, we use gets not fgets.  Sue me. */
+extern char	*gets();
+
+
+int
+main()
+{
+    char	 p[80];
+    char	 text[80];
+
+    printf("Wildmat tester.  Enter pattern, then strings to test.\n");
+    printf("A blank line gets prompts for a new pattern; a blank pattern\n");
+    printf("exits the program.\n");
+
+    for ( ; ; ) {
+	printf("\nEnter pattern:  ");
+	(void)fflush(stdout);
+	if (gets(p) == NULL || p[0] == '\0')
+	    break;
+	for ( ; ; ) {
+	    printf("Enter text:  ");
+	    (void)fflush(stdout);
+	    if (gets(text) == NULL)
+		exit(0);
+	    if (text[0] == '\0')
+		/* Blank line; go back and get a new pattern. */
+		break;
+	    printf("      %s\n", Wld_match(text, p) ? "YES" : "NO");
+	}
+    }
+
+    exit(0);
+    /* NOTREACHED */
+}
+#endif	/* defined(TEST) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wildmat.h	Fri May 05 08:23:15 2000 +0100
@@ -0,0 +1,18 @@
+/*
+  wildmat.h
+
+  Noffle header file for wildmat.
+
+  $Id: wildmat.h 44 2000-05-05 07:23:15Z enz $
+ */
+
+#ifndef WILDMAT_H
+#define WILDMAT_H
+
+/*
+  See if test is matched by pattern p. Return TRUE if so.
+ */
+Bool
+Wld_match(const char *text, const char *pattern);
+
+#endif