# HG changeset patch
# User enz
# Date 957511395 -3600
# Node ID 8e972daaeab95ddebc9aad405acb0c7fcf00b893
# Parent  792eb10e936df54eff27be699bc20ad0798c34bb
[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

diff -r 792eb10e936d -r 8e972daaeab9 CHANGELOG.html
--- 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.
diff -r 792eb10e936d -r 8e972daaeab9 INSTALL.html
--- 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>
diff -r 792eb10e936d -r 8e972daaeab9 Makefile
--- 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)
diff -r 792eb10e936d -r 8e972daaeab9 README.html
--- 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>
diff -r 792eb10e936d -r 8e972daaeab9 client.c
--- 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 );
diff -r 792eb10e936d -r 8e972daaeab9 config.c
--- 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 );
     }
diff -r 792eb10e936d -r 8e972daaeab9 config.h
--- 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
diff -r 792eb10e936d -r 8e972daaeab9 content.c
--- 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
diff -r 792eb10e936d -r 8e972daaeab9 database.c
--- 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
diff -r 792eb10e936d -r 8e972daaeab9 database.h
--- 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
diff -r 792eb10e936d -r 8e972daaeab9 group.c
--- 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 ) )
diff -r 792eb10e936d -r 8e972daaeab9 group.h
--- 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. */
diff -r 792eb10e936d -r 8e972daaeab9 itemlist.c
--- 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 );
diff -r 792eb10e936d -r 8e972daaeab9 noffle.1
--- 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,
diff -r 792eb10e936d -r 8e972daaeab9 noffle.c
--- 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" );
diff -r 792eb10e936d -r 8e972daaeab9 noffle.conf.5
--- 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.
+
+
diff -r 792eb10e936d -r 8e972daaeab9 noffle.conf.example
--- 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
diff -r 792eb10e936d -r 8e972daaeab9 pseudo.c
--- 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 );
diff -r 792eb10e936d -r 8e972daaeab9 server.c
--- 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:
diff -r 792eb10e936d -r 8e972daaeab9 util.c
--- 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 );
-}
diff -r 792eb10e936d -r 8e972daaeab9 util.h
--- 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
diff -r 792eb10e936d -r 8e972daaeab9 wildmat.c
--- /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) */
diff -r 792eb10e936d -r 8e972daaeab9 wildmat.h
--- /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