changeset 88:1fcdced0246e noffle

[svn] Move posting code to post.c, add command line posting
author bears
date Thu, 18 May 2000 13:17:23 +0100
parents bf8c97460fd7
children f17eb481c126
files docs/noffle.1 docs/noffle.conf.5 src/client.c src/configfile.c src/configfile.h src/noffle.c src/post.c src/post.h src/server.c
diffstat 9 files changed, 874 insertions(+), 425 deletions(-) [+]
line wrap: on
line diff
--- a/docs/noffle.1	Thu May 18 13:11:05 2000 +0100
+++ b/docs/noffle.1	Thu May 18 13:17:23 2000 +0100
@@ -1,5 +1,5 @@
 .TH noffle 1
-.\" $Id: noffle.1 51 2000-05-05 23:49:38Z uh1763 $
+.\" $Id: noffle.1 100 2000-05-18 12:17:23Z bears $
 .SH NAME
 noffle \- Usenet package optimized for dialup connections.
 
@@ -48,6 +48,9 @@
 \-o | \-\-offline
 .br
 .B noffle
+\-p | \-\-post all|local
+.br
+.B noffle
 \-q | \-\-query groups|desc|times
 .br
 .B noffle
@@ -219,6 +222,16 @@
 .BR pppd (8).
 
 .TP
+.B \-p, \-\-post all|local
+Read a news article from standard input and post it.
+.B all
+permits posting to any group, local and external, but will
+observe the group posting permission flags.
+.B local
+will only post to local groups, but ignores the group posting
+permission flag.
+
+.TP
 .B \-q, \-\-query groups|desc|times
 Query information about all groups from the remote server and merge it to
 the
--- a/docs/noffle.conf.5	Thu May 18 13:11:05 2000 +0100
+++ b/docs/noffle.conf.5	Thu May 18 13:17:23 2000 +0100
@@ -1,5 +1,5 @@
 .TH noffle.conf 5
-.\" $Id: noffle.conf.5 96 2000-05-17 10:51:22Z enz $
+.\" $Id: noffle.conf.5 100 2000-05-18 12:17:23Z bears $
 
 .SH NAME
 noffle.conf \- Configuration file for NOFFLE news server
@@ -38,6 +38,38 @@
 after making changes to the server entries.
 
 .TP
+.B getgroups <group pattern> [, <group pattern> ...]
+Only retrieve from the most recently specified server newsgroups that match
+the specified patterns. The patterns can contain \fIwildcards\fP, and
+there can be multiple
+.B getgroups
+lines. For further details on
+.B getgroups
+processing in tandem with \fBomitgroups\fP, see the section on
+.B omitgroups
+following.
+.br
+Default: All groups
+
+.TP
+.B omitgroups <group pattern> [, <group pattern> ...]
+Don't retrieve from the most recently specified server newsgroups that match
+the specified patterns. The patterns can contain \fIwildcards\fP, and
+there can be multiple
+.B omitgroups
+lines. When processing a new newsgroup name, it is checked first to see
+if it appears on the
+.B getgroups
+list (if any). If not, the group is rejected.
+Then the group is checked to see if it appears on the
+.B omitgroups
+list. If it does not, the group is accepted as a group
+.B NOFFLE
+will list and collect as required.
+.br
+Default: No groups
+
+.TP
 .B max-fetch <n>
 Never get more than <n> articles. If there are more, the oldest ones
 are discarded.
@@ -108,9 +140,14 @@
 Default: yes
 
 .TP
-.B remove-messageid yes|no
-This option is deprecated and ignored.
-It is only there for compatibility with old config-files.
+.B post-locally yes|no
+Place articles posted to external servers in the local aticle database
+immediately. Some servers may rewrite Message-IDs, which will cause
+duplicate postings of this option is enabled. Also, if for some reason
+the post to the remote server fails, the article still exist in the local
+database, which may be a source of some confusion.
+.br
+Default: no
 
 .TP
 .B replace-messageid yes|no
--- a/src/client.c	Thu May 18 13:11:05 2000 +0100
+++ b/src/client.c	Thu May 18 13:17:23 2000 +0100
@@ -1,7 +1,7 @@
 /*
   client.c
 
-  $Id: client.c 80 2000-05-13 15:36:35Z bears $
+  $Id: client.c 100 2000-05-18 12:17:23Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -359,6 +359,47 @@
 }
 
 static Bool
+isGetGroup( const char *name )
+{
+    GroupEnum *ge;
+    Bool emptyList;
+    const char *pattern;
+
+    emptyList = TRUE;
+    ge = new_GetGrEn( client.serv );
+    while ( ( pattern = GrEn_next( ge ) ) != NULL )
+    {
+	emptyList = FALSE;
+	if ( Wld_match( name, pattern ) )
+	{
+	    del_GrEn( ge );
+	    return TRUE;
+	}
+    }
+    
+    del_GrEn( ge );
+    return emptyList;
+}
+
+static Bool
+isOmitGroup( const char *name )
+{
+    GroupEnum *ge;
+    const char *pattern;
+
+    ge = new_OmitGrEn( client.serv );
+    while ( ( pattern = GrEn_next( ge ) ) != NULL )
+	if ( Wld_match( name, pattern ) )
+	{
+	    del_GrEn( ge );
+	    return TRUE;
+	}
+    
+    del_GrEn( ge );
+    return FALSE;   
+}
+
+static Bool
 isForbiddenGroupName( const char *name )
 {
     size_t i;
@@ -378,7 +419,7 @@
 }
 
 static void
-processGrps( void )
+processGrps( Bool noServerPattern )
 {
     char postAllow;
     Bool err;
@@ -398,6 +439,10 @@
 	    Log_inf( "Group %s forbidden", grp );
 	    continue;
 	}
+	if ( noServerPattern && ! isGetGroup( grp ) )
+	    continue;
+	if ( isOmitGroup( grp ) )
+	    continue;
         if ( ! Grp_exists( grp ) )
         {
             Log_inf( "Registering new group '%s'", grp );
@@ -445,32 +490,94 @@
     client.in = client.out = NULL;
 }
 
-Bool
-Client_getGrps( void )
+static Bool
+doGetGrps( const char *pattern, Bool *noServerPattern )
 {
-    if ( ! putCmd( "LIST ACTIVE" ) )
+    Str cmd;
+    int stat;
+
+    Utl_cpyStr( cmd, "LIST ACTIVE" );
+    if ( pattern[ 0 ] != '\0' )
+    {
+	Utl_catStr( cmd, " " );
+	Utl_catStr( cmd, pattern );
+    }
+
+    *noServerPattern = FALSE;
+    if ( ! putCmd( cmd ) )
         return FALSE;
-    if ( getStat() != STAT_GRPS_FOLLOW )
+    stat = getStat();
+    if ( pattern[ 0 ] != '\0' && stat != STAT_GRPS_FOLLOW )
     {
-        Log_err( "LIST ACTIVE command failed: %s", client.lastStat );
-        return FALSE;
+	*noServerPattern = TRUE;
+	if ( ! putCmd( "LIST ACTIVE" ) )
+	    return FALSE;
+	stat = getStat();
+    }    
+    if ( stat != STAT_GRPS_FOLLOW )    
+    {
+	Log_err( "%s failed: %s", cmd, client.lastStat );
+	return FALSE;
     }
-    processGrps();
+    processGrps( *noServerPattern );
     return TRUE;
 }
 
 Bool
-Client_getDsc( void )
+Client_getGrps( void )
+{
+    GroupEnum *ge;
+    const char *pattern;
+    Bool doneOne, noServerPattern, res;
+
+    Log_inf( "Getting groups" );
+
+    doneOne = FALSE;
+    res = TRUE;
+    ge = new_GetGrEn( client.serv );
+    while ( res && ( pattern = GrEn_next( ge ) ) != NULL )
+    {
+	res = doGetGrps( pattern, &noServerPattern );
+	doneOne = TRUE;
+	if ( noServerPattern )
+	    break;
+    }
+
+    if ( ! doneOne )
+	res = doGetGrps( "", &noServerPattern );
+
+    del_GrEn( ge );
+    return res;
+}
+
+static Bool
+doGetDsc( const char *pattern, Bool *noServerPattern )
 {
     Bool err;
-    Str name, line, dsc;
+    Str name, line, dsc, cmd;
+    int stat;
+
+    Utl_cpyStr( cmd, "LIST NEWSGROUPS" );
+    if ( pattern[ 0 ] != '\0' )
+    {
+	Utl_catStr( cmd, " " );
+	Utl_catStr( cmd, pattern );
+    }
 
-    Log_inf( "Querying group descriptions" );
-    if ( ! putCmd( "LIST NEWSGROUPS" ) )
+    *noServerPattern = FALSE;
+    if ( ! putCmd( cmd ) )
         return FALSE;
-    if ( getStat() != STAT_GRPS_FOLLOW )
+    stat = getStat();
+    if ( pattern[ 0 ] != '\0' && stat != STAT_GRPS_FOLLOW )
     {
-        Log_err( "LIST NEWSGROUPS failed: %s", client.lastStat );
+	*noServerPattern = TRUE;
+	if ( !putCmd( "LIST NEWSGROUPS" ) )
+	     return FALSE;
+	stat = getStat();
+    }
+    if ( stat != STAT_GRPS_FOLLOW )
+    {
+        Log_err( "%s failed: %s", cmd, client.lastStat );
         return FALSE;
     }
     while ( getTxtLn( line, &err ) && ! err )
@@ -480,6 +587,8 @@
             Log_err( "Unknown reply to LIST NEWSGROUPS: %s", line );
             continue;
         }
+	if ( *noServerPattern && ! isGetGroup( name ) )
+	    continue;
         strcpy( dsc, Utl_restOfLn( line, 1 ) );
         if ( Grp_exists( name ) )
         {
@@ -491,18 +600,61 @@
 }
 
 Bool
-Client_getCreationTimes( void )
+Client_getDsc( void )
+{
+    GroupEnum *ge;
+    const char *pattern;
+    Bool doneOne, noServerPattern, res;
+
+    Log_inf( "Querying group descriptions" );
+
+    doneOne = FALSE;
+    res = TRUE;
+    ge = new_GetGrEn( client.serv );
+    while ( res && ( pattern = GrEn_next( ge ) ) != NULL )
+    {
+	res = doGetDsc( pattern, &noServerPattern );
+	doneOne = TRUE;
+	if ( noServerPattern )
+	    break;
+    }
+
+    if ( ! doneOne )
+	res = doGetDsc( "", &noServerPattern );
+
+    del_GrEn( ge );
+    return res;
+}
+
+static Bool
+doGetCreationTimes( const char *pattern, Bool *noServerPattern )
 {
     Bool err;
-    Str name, line;
+    Str name, line, cmd;
     time_t t;
+    int stat;
+
+    Utl_cpyStr( cmd, "LIST ACTIVE.TIMES" );
+    if ( pattern[ 0 ] != '\0' )
+    {
+	Utl_catStr( cmd, " " );
+	Utl_catStr( cmd, pattern );
+    }
 
-    Log_inf( "Querying group creation times" );
-    if ( ! putCmd( "LIST ACTIVE.TIMES" ) )
+    *noServerPattern = FALSE;
+    if ( ! putCmd( cmd ) )
         return FALSE;
-    if ( getStat() != STAT_GRPS_FOLLOW )
+    stat = getStat();
+    if ( pattern[ 0 ] != '\0' && stat != STAT_GRPS_FOLLOW )
     {
-        Log_err( "LIST ACTIVE.TIMES failes: %s", client.lastStat );
+	*noServerPattern= TRUE;
+	if ( ! putCmd( "LIST ACTIVE.TIMES" ) )
+	    return FALSE;
+	stat = getStat();
+    }    
+    if ( stat != STAT_GRPS_FOLLOW )
+    {
+        Log_err( "%s failed: %s", cmd, client.lastStat );
         return FALSE;
     }
     while ( getTxtLn( line, &err ) && ! err )
@@ -512,6 +664,8 @@
             Log_err( "Unknown reply to LIST ACTIVE.TIMES: %s", line );
             continue;
         }
+	if ( *noServerPattern && ! isGetGroup( name ) )
+	    continue;
         if ( Grp_exists( name ) )
         {
             Log_inf( "Creation time of %s: %ld", name, t );
@@ -522,6 +676,33 @@
 }
 
 Bool
+Client_getCreationTimes( void )
+{
+    GroupEnum *ge;
+    const char *pattern;
+    Bool doneOne, noServerPattern, res;
+
+    Log_inf( "Querying group creation times" );
+
+    doneOne = FALSE;
+    res = TRUE;
+    ge = new_GetGrEn( client.serv );
+    while ( res && ( pattern = GrEn_next( ge ) ) != NULL )
+    {
+	res = doGetCreationTimes( pattern, &noServerPattern );
+	doneOne = TRUE;
+	if ( noServerPattern )
+	    break;
+    }
+
+    if ( ! doneOne )
+	res = doGetCreationTimes( "", &noServerPattern );
+
+    del_GrEn( ge );
+    return res;
+}
+
+Bool
 Client_getNewgrps( const time_t *lastTime )
 {
     Str s;
@@ -535,14 +716,14 @@
       (directly using %y in fmt string causes a Y2K compiler warning)
     */
     p = s + 2;
-    if ( ! putCmd( "NEWGROUPS %s", p ) )
+    if ( ! putCmd( "NEWGROUPS %s GMT", p ) )
         return FALSE;
     if ( getStat() != STAT_NEW_GRP_FOLLOW )
     {
         Log_err( "NEWGROUPS command failed: %s", client.lastStat );
         return FALSE;
     }
-    processGrps();
+    processGrps( TRUE );
     return TRUE;
 }
 
@@ -745,6 +926,8 @@
         if ( ! parseOvLn( line, &rmtNumb, subj, from, date, msgId, ref,
                           &bytes, &lines ) )
             Log_err( "Bad overview line: %s", line );
+	else if ( Cont_find( msgId ) >= 0 )
+	    Log_inf( "Already have '%s'", msgId );
         else
         {
             ov = new_Over( subj, from, date, msgId, ref, bytes, lines );
--- a/src/configfile.c	Thu May 18 13:11:05 2000 +0100
+++ b/src/configfile.c	Thu May 18 13:17:23 2000 +0100
@@ -6,7 +6,7 @@
     SPOOLDIR
     VERSION
 
-  $Id: configfile.c 88 2000-05-14 16:15:08Z bears $
+  $Id: configfile.c 100 2000-05-18 12:17:23Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -15,22 +15,40 @@
 
 #include "configfile.h"
 
+#include <ctype.h>
 #include <limits.h>
+#include "itemlist.h"
 #include "log.h"
 #include "util.h"
 #include "portable.h"
 
 typedef struct
 {
-    Str name;
-    Str user;
-    Str pass;
+    int numGroup;
+    int maxGroup;
+    char **groups;
+}
+GroupEntry;
+
+struct GroupEnum
+{
+    GroupEntry *groupEntry;
+    int groupIdx;
+};
+
+typedef struct
+{
+    char *name;
+    char *user;
+    char *pass;
+    GroupEntry getgroups;
+    GroupEntry omitgroups;
 }
 ServEntry;
 
 typedef struct
 {
-    Str pattern;
+    char *pattern;
     int days;
 }
 ExpireEntry;
@@ -48,8 +66,8 @@
     Bool autoSubscribe;
     Bool autoUnsubscribe;
     Bool infoAlways;
-    Bool removeMsgId;
     Bool replaceMsgId;
+    Bool postLocal;
     Str autoSubscribeMode;
     Str mailTo;
     int defaultExpire;
@@ -72,8 +90,8 @@
     FALSE,    /* autoSubscribe */
     FALSE,    /* autoUnsubscribe */
     TRUE,     /* infoAlways */
-    FALSE,    /* removeMsgId */
     TRUE,     /* replaceMsgId */
+    FALSE,    /* postLocal */
     "over",   /* autoSubscribeMode */
     "",       /* mailTo */
     14,       /* defaultExpire */
@@ -97,8 +115,8 @@
 Bool Cfg_autoUnsubscribe( void ) { return config.autoUnsubscribe; }
 Bool Cfg_autoSubscribe( void )  { return config.autoSubscribe; }
 Bool Cfg_infoAlways( void )  { return config.infoAlways; }
-Bool Cfg_removeMsgId( void ) { return config.removeMsgId; }
 Bool Cfg_replaceMsgId( void ) { return config.replaceMsgId; }
+Bool Cfg_postLocal( void ) { return config.postLocal; }
 const char * Cfg_autoSubscribeMode( void ) {
     return config.autoSubscribeMode; }
 const char * Cfg_mailTo( void ) { return config.mailTo; }
@@ -190,6 +208,67 @@
     return config.expire[ config.expireIdx++ ].days;
 }
 
+GroupEnum *
+new_GetGrEn( const char *name )
+{
+    GroupEnum *res;
+    int servIdx;
+
+    res = (GroupEnum *) malloc( sizeof( GroupEnum ) );
+    if ( res == NULL )
+    {
+	Log_err( "Malloc of GroupEnum failed." );
+	exit( EXIT_FAILURE );
+    }
+    if ( ! searchServ( name, &servIdx ) )
+	res->groupEntry = NULL;
+    else
+	res->groupEntry = &config.serv[ servIdx ].getgroups;
+    GrEn_first( res );
+    return res;
+}
+
+GroupEnum *
+new_OmitGrEn( const char *name )
+{
+    GroupEnum *res;
+    int servIdx;
+
+    res = (GroupEnum *) malloc( sizeof( GroupEnum ) );
+    if ( res == NULL )
+    {
+	Log_err( "Malloc of GroupEnum failed." );
+	exit( EXIT_FAILURE );
+    }
+    if ( ! searchServ( name, &servIdx ) )
+	res->groupEntry = NULL;
+    else
+	res->groupEntry = &config.serv[ servIdx ].omitgroups;
+    GrEn_first( res );
+    return res;
+}
+
+void
+del_GrEn( GroupEnum *ge )
+{
+    free(ge);
+}
+
+void
+GrEn_first( GroupEnum *ge )
+{
+    ge->groupIdx = 0;
+}
+
+const char *
+GrEn_next( GroupEnum *ge )
+{
+    if ( ge->groupEntry == NULL ||
+	 ge->groupIdx >= ge->groupEntry->numGroup )
+	return NULL;
+    return ge->groupEntry->groups[ ge->groupIdx++ ];
+}
+
 static void
 logSyntaxErr( const char *line )
 {
@@ -251,25 +330,28 @@
 static void
 getServ( const char *line )
 {
-    Str dummy;
+    Str dummy, name, user, pass;
     int r, len;
     ServEntry entry;
 
-    entry.user[ 0 ] = '\0';
-    entry.pass[ 0 ] = '\0';
+    memset( &entry, 0, sizeof( entry ) );
     r = sscanf( line, "%s %s %s %s",
-                dummy, entry.name, entry.user, entry.pass );
+                dummy, name, user, pass );
     if ( r < 2 )
     {
         logSyntaxErr( line );
         return;
     }
-    len = strlen( entry.name );
+    len = strlen( name );
     /* To make server name more definit, it is made lowercase and
        port is removed, if it is the default port */
-    if ( len > 4 && strcmp( entry.name + len - 4, ":119" ) == 0 )
-        entry.name[ len - 4 ] = '\0';
-    Utl_toLower( entry.name );
+    if ( len > 4 && strcmp( name + len - 4, ":119" ) == 0 )
+        name[ len - 4 ] = '\0';
+    Utl_toLower( name );
+
+    Utl_allocAndCpy( &entry.name, name );
+    Utl_allocAndCpy( &entry.user, user );
+    Utl_allocAndCpy( &entry.pass, pass );
 
     if ( config.maxServ < config.numServ + 1 )
     {
@@ -288,7 +370,7 @@
 static void
 getExpire( const char *line )
 {
-    Str dummy;
+    Str dummy, pattern;
     ExpireEntry entry;
     int days;
 
@@ -296,7 +378,7 @@
       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 )
+    if ( sscanf( line, "%s %s %d", dummy, pattern, &days ) != 3 )
     {
 	logSyntaxErr( line );
 	return;
@@ -310,7 +392,8 @@
 	    return;
 	}
 
-	Utl_toLower( entry.pattern );
+	Utl_toLower( pattern );
+	Utl_allocAndCpy( &entry.pattern, pattern );
 	entry.days = days;
 
 	if ( config.maxExpire < config.numExpire + 1 )
@@ -328,6 +411,58 @@
     }
 }
 
+static void
+getGroups( char *line, Bool isGet )
+{
+    const char *name;
+    ItemList *patterns;
+    const char *pattern;
+
+    if ( config.numServ == 0 )
+    {
+	Log_err( "No current server in %s", line );
+	return;
+    }
+    
+    name = line;
+    /* Skip over name and terminate it */
+    while ( line[ 0 ] != '\0' && ! isspace( line[ 0 ] ) )
+	line++;
+    if ( line[ 0 ] == '\0' )
+    {
+	logSyntaxErr( name );
+	return;
+    }
+    line[ 0 ] = '\0';
+    line++;
+    
+    patterns = new_Itl( line, " ," );
+    for( pattern = Itl_first( patterns );
+	 pattern != NULL;
+	 pattern = Itl_next( patterns ) )
+    {
+	GroupEntry *g;
+
+	if ( isGet )
+	    g = &config.serv[ config.numServ - 1 ].getgroups;
+	else
+	    g = &config.serv[ config.numServ - 1 ].omitgroups;
+	if ( g->maxGroup < g->numGroup + 1 )
+	{
+	    if ( ! ( g->groups = realloc( g->groups,
+					  ( g->maxGroup + 5 )
+					  * sizeof( char * ) ) ) )
+	    {
+		Log_err( "Could not realloc group list" );
+		exit( EXIT_FAILURE );
+	    }
+	    g->maxGroup += 5;
+	}
+	Utl_allocAndCpy( &g->groups[ g->numGroup++ ], pattern );
+    }
+    del_Itl( patterns) ;
+}
+
 void
 Cfg_read( void )
 {
@@ -367,10 +502,10 @@
             getBool( &config.autoUnsubscribe, p );
         else if ( strcmp( "info-always-unread", name ) == 0 )
             getBool( &config.infoAlways, p );
-        else if ( strcmp( "remove-messageid", name ) == 0 )
-            getBool( &config.removeMsgId, p );
         else if ( strcmp( "replace-messageid", name ) == 0 )
             getBool( &config.replaceMsgId, p );
+        else if ( strcmp( "post-locally", name ) == 0 )
+            getBool( &config.postLocal, p );
         else if ( strcmp( "auto-subscribe-mode", name ) == 0 )
         {
             getStr( s, p );
@@ -394,6 +529,10 @@
             getStr( config.mailTo, p );
         else if ( strcmp( "expire", name ) == 0 )
             getExpire( p );
+        else if ( strcmp( "getgroups", name ) == 0 )
+            getGroups( p, TRUE );
+        else if ( strcmp( "omitgroups", name ) == 0 )
+            getGroups( p, FALSE );
         else
             Log_err( "Unknown config option: %s", name );
     }
--- a/src/configfile.h	Thu May 18 13:11:05 2000 +0100
+++ b/src/configfile.h	Thu May 18 13:17:23 2000 +0100
@@ -3,7 +3,7 @@
 
   Common declarations and handling of the configuration file.
 
-  $Id: configfile.h 96 2000-05-17 10:51:22Z enz $
+  $Id: configfile.h 100 2000-05-18 12:17:23Z bears $
 */
 
 #ifndef CONFIGFILE_H
@@ -15,6 +15,9 @@
 
 #include "common.h"
 
+struct GroupEnum;
+typedef struct GroupEnum GroupEnum;
+
 const char * Cfg_spoolDir( void );
 const char * Cfg_version( void );
 
@@ -26,10 +29,8 @@
 Bool Cfg_autoSubscribe( void );
 Bool Cfg_infoAlways( void );
 
-/* Ignored. Should be removed in development version. */
-Bool Cfg_removeMsgId( void );
-
 Bool Cfg_replaceMsgId( void );
+Bool Cfg_postLocal( void );
 const char * Cfg_autoSubscribeMode( void ); /* Can be: full, thread, over */
 const char * Cfg_mailTo( void );
 
@@ -58,4 +59,24 @@
 
 void Cfg_read( void );
 
+/* Get a new iterator for get group patterns for the given server */
+GroupEnum *
+new_GetGrEn( const char *name );
+
+/* Get a new iterator for omit group patterns for the given server */
+GroupEnum *
+new_OmitGrEn( const char *name );
+
+/* Free up a group enumerator */
+void
+del_GrEn( GroupEnum * ge );
+
+/* Rewind to first group */
+void
+GrEn_first( GroupEnum * ge );
+
+/* Get next group pattern or NULL if no more */
+const char *
+GrEn_next( GroupEnum *ge );
+
 #endif
--- a/src/noffle.c	Thu May 18 13:11:05 2000 +0100
+++ b/src/noffle.c	Thu May 18 13:17:23 2000 +0100
@@ -10,7 +10,7 @@
   received for some seconds (to allow multiple clients connect at the same
   time).
 
-  $Id: noffle.c 67 2000-05-12 17:19:38Z enz $
+  $Id: noffle.c 100 2000-05-18 12:17:23Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -38,6 +38,7 @@
 #include "online.h"
 #include "outgoing.h"
 #include "over.h"
+#include "post.h"
 #include "pseudo.h"
 #include "util.h"
 #include "server.h"
@@ -172,6 +173,33 @@
         }
 }
 
+static Bool
+doPost( Bool localOnly )
+{
+    Str line;
+    DynStr *s;
+    Bool res;
+    
+
+    s = new_DynStr( 10000 );
+    while ( fgets( line, MAXCHAR, stdin ) != NULL )
+	DynStr_app( s, line );
+
+    res = TRUE;
+    if ( ! Post_open( DynStr_str( s ) ) )
+    {
+	fprintf( stderr, "Post failed: Malformed article.\n" );
+	res = FALSE;
+    }
+    else if ( ! Post_post( localOnly ) )
+    {
+	fprintf( stderr, "Post failed: Can't post to group.\n" );
+	res = FALSE;
+    }
+    Post_close();
+    return res;
+}
+
 static void
 doQuery( void )
 {
@@ -498,6 +526,8 @@
       " -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"
+      " -p | --post all                  Post article on stdin to all groups\n"
+      " -p | --post local                Post article on stdin to local groups\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"
@@ -619,6 +649,7 @@
         { "modify",           required_argument, NULL, 'm' },
         { "offline",          no_argument,       NULL, 'o' },
         { "online",           no_argument,       NULL, 'n' },
+        { "post",             required_argument, NULL, 'p' },
         { "query",            required_argument, NULL, 'q' },
         { "server",           no_argument,       NULL, 'r' },
         { "requested",        no_argument,       NULL, 'R' },
@@ -637,7 +668,7 @@
     signal( SIGINT, logSignal );
     signal( SIGTERM, logSignal );
     signal( SIGPIPE, logSignal );
-    c = getopt_long( argc, argv, "a:c:C:dD:efghlm:onq:rRs:S:t:u:v",
+    c = getopt_long( argc, argv, "a:c:C:dD:efghlm:onp:q:rRs:S:t:u:v",
                      longOptions, NULL );
     if ( ! initNoffle( c != 'r' ) )
         return EXIT_FAILURE;
@@ -724,6 +755,28 @@
         else
             Online_set( FALSE );
         break;
+    case 'p':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -p needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+        {
+	    Bool local;
+	    
+	    local = FALSE;
+            if ( strcmp( optarg, "local" ) == 0 )
+                local = TRUE;
+            else if ( strcmp( optarg, "all" ) != 0 )
+            {
+                fprintf( stderr, "Unknown argument -p %s\n", optarg );
+                result = EXIT_FAILURE;
+            }
+            if ( ! doPost( local ) )
+		result = EXIT_FAILURE;
+        }
+        break;
     case 'q':
         if ( ! optarg )
         {
--- a/src/post.c	Thu May 18 13:11:05 2000 +0100
+++ b/src/post.c	Thu May 18 13:17:23 2000 +0100
@@ -1,7 +1,7 @@
 /*
   post.c
 
-  $Id: post.c 70 2000-05-12 17:35:00Z enz $
+  $Id: post.c 100 2000-05-18 12:17:23Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -12,10 +12,14 @@
 #include "post.h"
 #include <string.h>
 #include "common.h"
+#include "configfile.h"
 #include "content.h"
+#include "control.h"
 #include "database.h"
 #include "group.h"
+#include "itemlist.h"
 #include "log.h"
+#include "outgoing.h"
 #include "over.h"
 #include "protocol.h"
 #include "util.h"
@@ -34,80 +38,24 @@
 
 struct Article
 {
-    const char * text;
-    Bool posted;
+    DynStr *text;	   /* Processed article text */
+    ItemList *newsgroups;  /* Newsgroups for dispatch */
+    ItemList *control;	   /* Control message? NULL if not */
+    Bool posted;	   /*  Has it been put in the article database? */
+    const char *server;	   /* Server for external post */
     struct OverInfo over;
 };
 
-static struct Article article = { NULL, FALSE, { "", "", "", "", "", 0, 0 } };
-
-static void
-getOverInfo( struct OverInfo * o )
-{
-    const char *p = article.text;
-    Str line, field, value;
-    
-    o->bytes = strlen( p );
-
-    while( p != NULL )
-    {
-        p = Utl_getHeaderLn( line, p );
-        if ( line[ 0 ] == '\0' )
-	    break;
-
-	/* Look for headers we need to stash. */
-        if ( Prt_getField( field, value, line ) )
-        {
-	    if ( strcmp( field, "subject" ) == 0 )
-		Utl_cpyStr( o->subject, value );
-	    else if ( strcmp ( field, "from" ) == 0 )
-		Utl_cpyStr( o->from, value );
-	    else if ( strcmp ( field, "date" ) == 0 )
-		Utl_cpyStr( o->date, value );
-	    else if ( strcmp ( field, "references" ) == 0 )
-		Utl_cpyStr( o->ref, value );
-	    else if ( strcmp ( field, "message-id" ) == 0 )
-		Utl_cpyStr( o->msgId, value );
-	}
-    }
-
-    /* Move to start of body and count lines. */
-    for ( p++, o->lines = 0; *p != '\0'; p++ )
-	if ( *p == '\n' )
-	    o->lines++;
-}
-
-/* Register an article for posting. */
-Bool
-Post_open( const char * text )
-{
-    if ( article.text != NULL )
-    {
-	Log_err( "Busy article in Post_open." );
-	return FALSE;
-    }
-
-    memset( &article.over, 0, sizeof( article.over ) );
-    article.text = text;
-    getOverInfo( &article.over );
-
-    if ( Db_contains( article.over.msgId ) )
-    {
-	Log_err( "Duplicate article %s.", article.over.msgId );
-	return FALSE;
-    }
-
-    return TRUE;
-}
-
+static struct Article article = { NULL, NULL, NULL, FALSE, NULL,
+				  { "", "", "", "", "", 0, 0 } };
 
 /* Add the article to a group. */
-Bool
-Post_add ( const char * grp )
+static Bool
+addToGroup( const char * grp )
 {
     Over * over;
     const char *msgId;
-    
+
     over = new_Over( article.over.subject,
 		     article.over.from,
 		     article.over.date,
@@ -126,7 +74,7 @@
     {
         Log_inf( "Added '%s' to database.", msgId );
         if ( ! Db_prepareEntry( over, Cont_grp(), Cont_last() )
-	     || ! Db_storeArt ( msgId, article.text ) )
+	     || ! Db_storeArt ( msgId, DynStr_str( article.text ) ) )
 	    return FALSE;
 	article.posted = TRUE;
     }
@@ -145,12 +93,347 @@
     Grp_setFirstLast( Cont_grp(), Cont_first(), Cont_last() );
     return TRUE;
 }
+
+static Bool
+checkPostableNewsgroup( Bool localOnly )
+{
+    const char *grp;
+    Bool knownGrp = FALSE;
+    Bool postAllowedGrp = FALSE;
+    Bool local;
+
+    /*
+       Check at least one group is known. Look for external group and
+       set server.
+     */
+    article.server = NULL;
+    for( grp = Itl_first( article.newsgroups );
+	 grp != NULL;
+	 grp = Itl_next( article.newsgroups ) )
+    {
+	if ( Grp_exists( grp ) )
+	{
+	    local = Grp_local( grp );
+	    if ( localOnly && ! local )
+		continue;
+	    knownGrp = TRUE;
+	    switch( Grp_postAllow( grp ) )
+	    {
+	    case 'n':
+		if ( localOnly )
+		    postAllowedGrp = TRUE;
+		break;
+	    case 'y':
+		postAllowedGrp = TRUE;
+		break;
+	    default:
+		if ( localOnly )
+		    postAllowedGrp = TRUE;
+		else
+		    /* Can't post to moderated local groups. */
+		    postAllowedGrp = ! local;
+		break;
+	    }
+	    if ( postAllowedGrp && ! local && article.server == NULL )
+		article.server = Grp_server( grp );
+	    if ( postAllowedGrp && article.server != NULL )
+		break;
+	}
+    }
+	    
+    if ( ! knownGrp )
+    {
+	Log_err( "No known group in Newsgroups header field" );
+	return FALSE;
+    }
+    else if ( ! postAllowedGrp )
+    {
+	Log_err( "No group permits posting" );
+	return FALSE;
+    }
+
+    return TRUE;
+}
+
+/* Get article text, check for validity & build overview. */
+static Bool
+getArticleText( const char *p )
+{
+    DynStr * s;
+    Str line, field, value;
+    Bool replyToFound;
+
+    s = new_DynStr( 10000 );
+    article.text = s;
+
+    memset( &article.over, 0, sizeof( article.over ) );
+    replyToFound = FALSE;
+    
+    /* Grab header lines first, getting overview info as we go. */
+    while ( ( p = Utl_getHeaderLn( line, p ) ) != NULL
+	    && line[ 0 ] != '\0'
+	    && Prt_getField( field, value, line ) )
+    {
+	/* Look for headers we need to stash. */
+	if ( strcmp( field, "subject" ) == 0 )
+	{
+	    Utl_cpyStr( article.over.subject, value );
+	    DynStr_appLn( s, line );
+	}
+	else if ( strcmp ( field, "from" ) == 0 )
+	{
+	    Utl_cpyStr( article.over.from, value );
+	    DynStr_appLn( s, line );
+	}
+	else if ( strcmp ( field, "date" ) == 0 )
+	{
+	    Utl_cpyStr( article.over.date, value );
+	    DynStr_appLn( s, line );
+	}
+	else if ( strcmp ( field, "references" ) == 0 )
+	{
+	    Utl_cpyStr( article.over.ref, value );
+	    DynStr_appLn( s, line );
+	}
+	else if ( strcmp ( field, "message-id" ) == 0 )
+	    Utl_cpyStr( article.over.msgId, value );
+	else if ( strcmp ( field, "newsgroups" ) == 0 )
+	{
+	    article.newsgroups = new_Itl( value, " ,\n\t" );
+	    DynStr_appLn( s, line );
+	}
+	else if ( strcmp ( field, "control" ) == 0 )
+	{
+	    article.control = new_Itl( value, " " );
+	    DynStr_appLn( s, line );
+	}
+	else if ( strcmp ( field, "reply-to" ) == 0 )
+	{
+	    replyToFound = TRUE;
+	    DynStr_appLn( s, line );
+	}
+	else if ( strcmp ( field, "x-sender" ) == 0 )
+	{
+	    DynStr_app( s, "X-NOFFLE-X-Sender: " );
+	    DynStr_appLn( s, value );
+	}
+	else if ( strcmp ( field, "xref" ) == 0 )
+	    Log_inf( "Xref header in post ignored" );
+	else
+	    DynStr_appLn( s, line );
+    }
+
+    /* Now sort header-related issues */
+    if ( article.over.from[ 0 ] == '\0' )
+    {
+	Log_err( "Posted message has no From field" );
+	return FALSE;
+    }
+    if ( article.over.subject[ 0 ] == '\0' )
+    {
+	Log_err( "Posted message has no Subject field" );
+	return FALSE;
+    }
+    if ( article.newsgroups == NULL || Itl_count( article.newsgroups) == 0 )
+    {
+	Log_err( "Posted message has no valid Newsgroups field" );
+	return FALSE;
+    }
+    if ( Cfg_replaceMsgId() )
+    {
+	Prt_genMsgId( article.over.msgId, article.over.from, "NOFFLE" );
+	Log_dbg( "Replacing Message-ID with '%s'", article.over.msgId );
+    }
+    else if ( article.over.msgId[ 0 ] == '\0' )
+    {
+	Prt_genMsgId( article.over.msgId, article.over.from, "NOFFLE" );
+	Log_inf( "Adding missing Message-ID '%s'", article.over.msgId );
+    }
+    else if ( ! Prt_isValidMsgId( article.over.msgId ) )
+    {
+	Log_ntc( "Replacing invalid Message-ID '%s'", article.over.msgId );
+	Prt_genMsgId( article.over.msgId, article.over.from, "NOFFLE" );
+    }
+    DynStr_app( s, "Message-ID: " );
+    DynStr_appLn( s, article.over.msgId );
+    if ( ! replyToFound )
+    {
+	Log_dbg( "Adding Reply-To field to posted message." );
+	DynStr_app( s, "Reply-To: " );
+	DynStr_appLn( s, article.over.from );
+    }
+    if ( article.over.date[ 0 ] == '\0' )
+    {
+	time_t t;
+
+	time( &t );
+	Utl_rfc822Date( t, article.over.date );
+	DynStr_app( s, "Date: " );
+	DynStr_appLn( s, article.over.date );
+    }
+    if ( p == NULL || p[ 0 ] == '\0' )
+    {
+	Log_err( "Posted  message has no body" );
+	return FALSE;
+    }
+
+    /* Add the empty line separating header and body */
+    DynStr_appLn( s, "" );
+
+    /* Now pop on the rest of the body and count the lines & bytes */
+    DynStr_app( s, p );
+    for ( p++, article.over.lines = 0; *p != '\0'; p++ )
+	if ( *p == '\n' )
+	    article.over.lines++;
+    article.over.bytes = DynStr_len( s );
+
+    return TRUE;
+}
+
+/* Add article to outgoing if needs be */
+static Bool
+postExternal( void )
+{
+    if ( article.server == NULL )
+	return TRUE;
+
+    if ( ! Out_add( article.server, article.over.msgId, article.text ) )
+    {
+	Log_err( "Cannot add posted article to outgoing directory" );
+	return FALSE;
+    }
+
+    return TRUE;
+}
+
+/* Cancel and return TRUE if need to send cancel message on to server. */
+static Bool
+controlCancel( const char *cancelId )
+{
+    return ( Ctrl_cancel( cancelId ) == CANCEL_NEEDS_MSG );
+}
+
+/*
+  It's a control message. Currently we only know about 'cancel'
+  messages; others are passed on for outside groups, and logged
+  as ignored for local groups.
+ */
+static Bool
+handleControl( void )
+{
+    const char *grp;
+    const char *op;
+
+    op = Itl_first( article.control );
+    if ( op == NULL )
+    {
+	Log_err( "Malformed control line." );
+	return TRUE;
+    }
+    else if ( strcasecmp( op, "cancel" ) == 0 )
+    {
+	if ( ! controlCancel( Itl_next( article.control ) ) )
+	    return TRUE;	/* Handled entirely locally */
+    }
+    else
+    {
+	/* Log 'can't do' for internal groups. */
+	for( grp = Itl_first( article.newsgroups );
+	     grp != NULL;
+	     grp = Itl_next( article.newsgroups ) )
+	{
+	    if ( Grp_exists( grp ) && Grp_local( grp ) )
+		Log_inf( "Ignoring control '%s' for '%s'.", op, grp );
+	}
+    }
+
+    return postExternal();
+}
+
+static Bool
+postArticle( Bool localOnly )
+{
+    const char *grp;
+    Bool err;
+    Bool postLocal;
+    Bool local;
+
+    err = FALSE;
+    postLocal = Cfg_postLocal();
+
+    /* Run round & post locally */
+    for( grp = Itl_first( article.newsgroups );
+	 grp != NULL;
+	 grp = Itl_next( article.newsgroups ) )
+    {
+	local = Grp_local( grp );
+	if ( localOnly && ! local )
+	    continue;
+	if ( ( local || postLocal )
+	     && ( Grp_postAllow( grp ) == 'y' || localOnly ) )
+	    err = addToGroup( grp ) && err;
+    }
+
+    if ( localOnly )
+	return err;
+    else
+	return postExternal() && err;
+}
+
+/* Register an article for posting. */
+Bool
+Post_open( const char * text )
+{
+    if ( article.text != NULL )
+    {
+	Log_err( "Busy article in Post_open." );
+	return FALSE;
+    }
+
+    if ( ! getArticleText( text ) )
+	return FALSE;
+
+    if ( Db_contains( article.over.msgId ) )
+    {
+	Log_err( "Duplicate article %s.", article.over.msgId );
+	return FALSE;
+    }
+
+    return TRUE;
+}
+
+/* Process the posting */
+Bool
+Post_post( Bool localOnly )
+{
+    if ( ! checkPostableNewsgroup( localOnly ) )
+	return FALSE;
+    
+    return ( article.control == NULL )
+	? ! postArticle( localOnly )
+	: ! handleControl();
+}
    
 /* Done with article - tidy up. */
 void
 Post_close( void )
 {
-    article.text = NULL;
+    if ( article.text != NULL )
+    {
+	del_DynStr( article.text );
+	article.text = NULL;
+    }
+    if ( article.newsgroups != NULL )
+    {
+	del_Itl( article.newsgroups );
+	article.newsgroups = NULL;
+    }
+    if ( article.control != NULL )
+    {
+	del_Itl( article.control );
+	article.control = NULL;
+    }
     article.posted = FALSE;
+    article.server = NULL;
 }
 
--- a/src/post.h	Thu May 18 13:11:05 2000 +0100
+++ b/src/post.h	Thu May 18 13:17:23 2000 +0100
@@ -1,11 +1,11 @@
 /*
   post.h
 
-  Take a single article received in its entirety without an overview
-  (i.e. received via at the server via a POST), and add it to the database
-  and (possibly multiple) group(s).
+  Take the text of a single article, parse it and add/amend headers as
+  necessary, and add to the local database and/or queue for external
+  posting as appropriate.
 
-  $Id: post.h 51 2000-05-05 23:49:38Z uh1763 $
+  $Id: post.h 100 2000-05-18 12:17:23Z bears $
 */
 
 #ifndef POST_H
@@ -21,9 +21,12 @@
 Bool
 Post_open( const char * text );
 
-/* Add the article to a group. */
+/*
+  Post the article. If localOnly, article is only posted to local
+  groups, and is posted regardless of group posting status.
+ */
 Bool
-Post_add ( const char * grp );
+Post_post( Bool localOnly );
    
 /* Done with article - tidy up. */
 void
--- a/src/server.c	Thu May 18 13:11:05 2000 +0100
+++ b/src/server.c	Thu May 18 13:17:23 2000 +0100
@@ -1,7 +1,7 @@
 /*
   server.c
 
-  $Id: server.c 96 2000-05-17 10:51:22Z enz $
+  $Id: server.c 100 2000-05-18 12:17:23Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -31,7 +31,6 @@
 #include "common.h"
 #include "configfile.h"
 #include "content.h"
-#include "control.h"
 #include "database.h"
 #include "dynamicstring.h"
 #include "fetch.h"
@@ -41,7 +40,6 @@
 #include "lock.h"
 #include "log.h"
 #include "online.h"
-#include "outgoing.h"
 #include "over.h"
 #include "post.h"
 #include "protocol.h"
@@ -959,316 +957,35 @@
     return TRUE;
 }
 
-/* Cancel and return TRUE if need to send cancel message on to server. */
-static Bool
-controlCancel( const char *cancelId )
-{
-    return ( Ctrl_cancel( cancelId ) == CANCEL_NEEDS_MSG );
-}
-
-/*
-  It's a control message. Currently we only know about 'cancel'
-  messages; others are passed on for outside groups, and logged
-  as ignored for local groups.
- */
-static Bool
-handleControl( ItemList *control, ItemList *newsgroups,
-	       const char *msgId, const DynStr *art )
-{
-    const char *grp;
-    const char *op;
-    Bool err = FALSE;
-    Bool localDone = FALSE;
-
-    op = Itl_first( control );
-    if ( op == NULL )
-    {
-	Log_err( "Malformed control line." );
-	return TRUE;
-    }
-    else if ( strcasecmp( op, "cancel" ) == 0 )
-    {
-	if ( controlCancel( Itl_next( control ) ) )
-	    localDone = TRUE;
-	else
-	    return err;
-    }
-
-    /* Pass on for outside groups. */
-    for( grp = Itl_first( newsgroups );
-	 grp != NULL;
-	 grp = Itl_next( newsgroups ) )
-    {
-	if ( Grp_exists( grp ) && ! Grp_local( grp ) )
-	{
-	    if ( ! Out_add( Grp_server( grp ), msgId, art ) )
-	    {
-		Log_err( "Cannot add posted article to outgoing directory" );
-		err = TRUE;
-	    }
-	    break;
-	}
-    }
-
-    if ( localDone )
-	return err;
-
-    /* Log 'can't do' for internal groups. */
-    for( grp = Itl_first( newsgroups );
-	 grp != NULL;
-	 grp = Itl_next( newsgroups ) )
-    {
-	if ( Grp_exists( grp ) && Grp_local( grp ) )
-	    Log_inf( "Ignoring control '%s' for '%s'.", op, grp );
-    }
-
-    return err;
-}
-
-static Bool
-postArticle( ItemList *newsgroups, const char *msgId, const DynStr *art )
-{
-    const char *grp;
-    Bool err;
-    Bool oneLocal;
-
-    err = oneLocal = FALSE;
-
-    /* Run round first doing all local groups. */ 
-    for( grp = Itl_first( newsgroups );
-	 grp != NULL;
-	 grp = Itl_next( newsgroups ) )
-    {
-	if ( Grp_local( grp ) )
-	{
-	    if ( ! oneLocal )
-	    {
-		if ( ! Post_open( DynStr_str( art ) ) )
-		{
-		    err = TRUE;
-		    break;
-		}
-		else
-		    oneLocal = TRUE;
-	    }
-
-	    if ( ! Post_add( grp ) )
-		err = TRUE;
-	}
-    }
-    if ( oneLocal )
-	Post_close();
-
-    /* Now look for a valid external group. */
-    for( grp = Itl_first( newsgroups );
-	 grp != NULL;
-	 grp = Itl_next( newsgroups ) )
-    {
-	if ( Grp_exists( grp ) && ! Grp_local( grp ) )
-	{
-	    if ( ! Out_add( Grp_server( grp ), msgId, art ) )
-	    {
-		Log_err( "Cannot add posted article to outgoing directory" );
-		err = TRUE;
-	    }
-	    break;
-	}
-    }
-
-    return err;
-}
-
 static Bool
 doPost( char *arg, const Cmd *cmd )
 {
-    Bool err, replyToFound, dateFound, inHeader;
     DynStr *s;
-    Str line, field, val, msgId, from;
-    const char* p;
-    ItemList * newsgroups, *control;
+    Str line;
+    Bool err;
 
     UNUSED(arg);
     UNUSED(cmd);
     
-    /*
-      Get article and make following changes to the header:
-      - add/replace/cut Message-ID depending on config options
-      - add Reply-To with content of From, if missing
-      (some providers overwrite From field)
-      - rename X-Sender header to X-NOFFLE-X-Sender
-      (some providers want to insert their own X-Sender)
-
-      For doing this, it is not necessary to parse multiple-line
-      headers.
-    */
     putStat( STAT_SEND_ART, "Continue (end with period)" );
     fflush( stdout );
     Log_dbg( "[S FLUSH]" );
     s = new_DynStr( 10000 );
-    msgId[ 0 ] = '\0';
-    from[ 0 ] = '\0';
-    newsgroups = control = NULL;
-    replyToFound = dateFound = FALSE;
-    inHeader = TRUE;
-    while ( getTxtLn( line, &err ) )
-    {
-        if ( inHeader )
-        {
-            p = Utl_stripWhiteSpace( line );
-            if ( *p == '\0' )
-            {
-                inHeader = FALSE;
-                if ( from[ 0 ] == '\0' )
-                    Log_err( "Posted message has no From field" );
-                if ( Cfg_replaceMsgId() )
-                {
-                    Prt_genMsgId( msgId, from, "NOFFLE" );
-                    Log_dbg( "Replacing Message-ID with '%s'", msgId );
-                }
-                else if ( msgId[ 0 ] == '\0' )
-                {
-                    Prt_genMsgId( msgId, from, "NOFFLE" );
-                    
-                    Log_inf( "Adding missing Message-ID '%s'", msgId );
-                }
-                else if ( ! Prt_isValidMsgId( msgId ) )
-                {
-                    Log_ntc( "Replacing invalid Message-ID with '%s'",
-                             msgId );
-                    Prt_genMsgId( msgId, from, "NOFFLE" );
-                }
-                DynStr_app( s, "Message-ID: " );
-                DynStr_appLn( s, msgId );
-                if ( ! replyToFound && from[ 0 ] != '\0' )
-                {
-                    Log_dbg( "Adding Reply-To field to posted message." );
-                    DynStr_app( s, "Reply-To: " );
-                    DynStr_appLn( s, from );
-                }
-		if ( ! dateFound )
-		{
-		    time_t t;
+    err = FALSE;
+    while ( ! err && getTxtLn( line, &err ) )
+	DynStr_appLn( s, line );
 
-		    time( &t );
-		    Utl_rfc822Date( t, val );
-		    DynStr_app( s, "Date: " );
-		    DynStr_appLn( s, val );
-		}
-                DynStr_appLn( s, p );
-            }
-            else if ( Prt_getField( field, val, p ) )
-            {
-                if ( strcmp( field, "message-id" ) == 0 )
-                    strcpy( msgId, val );
-                else if ( strcmp( field, "from" ) == 0 )
-                {
-                    strcpy( from, val );
-                    DynStr_appLn( s, p );
-                }
-                else if ( strcmp( field, "newsgroups" ) == 0 )
-                {
-		    Utl_toLower( val );
-		    newsgroups = new_Itl ( val, " ," );
-                    DynStr_appLn( s, p );
-                }
-                else if ( strcmp( field, "control" ) == 0 )
-                {
-		    control = new_Itl ( val, " " );
-                    DynStr_appLn( s, p );
-                }
-                else if ( strcmp( field, "reply-to" ) == 0 )
-                {
-                    replyToFound = TRUE;
-                    DynStr_appLn( s, p );
-                }
-                else if ( strcmp( field, "date" ) == 0 )
-                {
-                    dateFound = TRUE;
-                    DynStr_appLn( s, p );
-                }
-                else if ( strcmp( field, "x-sender" ) == 0 )
-                {
-                    DynStr_app( s, "X-NOFFLE-X-Sender: " );
-                    DynStr_appLn( s, val );
-                }
-                else
-                    DynStr_appLn( s, p );
-            }
-            else
-                DynStr_appLn( s, line );
-        }
-        else
-            DynStr_appLn( s, line );
-    }
-    if ( inHeader )
-        Log_err( "Posted message has no body" );
-    if ( ! err )
-    {
-        if ( newsgroups == NULL || Itl_count( newsgroups ) == 0 )
-        {
-            Log_err( "Posted message has no valid Newsgroups header field" );
-            err = TRUE;
-        }
-        else
-	{
-	    const char *grp;
-	    Bool knownGrp = FALSE;
-	    Bool postAllowedGrp = FALSE;
-
-	    /* Check at least one group is known. */
-	    for( grp = Itl_first( newsgroups );
-		 grp != NULL;
-		 grp = Itl_next( newsgroups ) )
-	    {
-		if ( Grp_exists( grp ) )
-		{
-		    knownGrp = TRUE;
-		    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;
-		}
-	    }
-	    
-	    if ( ! knownGrp )
-	    {
-
-		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 )
-		    ? postArticle( newsgroups, msgId, s )
-		    : handleControl( control, newsgroups, msgId, s );
-	    }	    
-	}
-    }
-    if ( err )
-        putStat( STAT_POST_FAILED, "Posting failed" );
-    else
+    if ( ! err
+	 && Post_open( DynStr_str( s ) )
+	 && Post_post( FALSE ) )
     {
         putStat( STAT_POST_OK, "Message posted" );
         if ( Online_true() )
             postArts();
     }
-    del_Itl( newsgroups );
-    del_Itl( control );
+    else
+        putStat( STAT_POST_FAILED, "Posting failed" );
+    Post_close();
     del_DynStr( s );
     return TRUE;
 }