changeset 26:526a4c34ee2e noffle

[svn] Applied patch from Jim Hague: support for local groups / new command line options --create and --cancel.
author enz
date Sat, 29 Apr 2000 15:45:56 +0100
parents ab6cf19be6d3
children 2b79c0df1e69
files CHANGELOG.html Makefile README.html control.c control.h database.c database.h group.c group.h itemlist.c itemlist.h noffle.1 noffle.c outgoing.c outgoing.h post.c post.h pseudo.c server.c util.c util.h
diffstat 21 files changed, 1007 insertions(+), 151 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGELOG.html	Sat Apr 29 14:37:59 2000 +0100
+++ b/CHANGELOG.html	Sat Apr 29 15:45:56 2000 +0100
@@ -12,6 +12,14 @@
 
 <hr>
 
+<h2>Current developer version</h2>
+
+<ul>
+<li>
+Applied patch from Jim Hague: support for local groups / new command
+line options --create and --cancel.
+</ul>
+
 <h2>Version 1.0pre5</h2>
 
 <ul>
--- a/Makefile	Sat Apr 29 14:37:59 2000 +0100
+++ b/Makefile	Sat Apr 29 15:45:56 2000 +0100
@@ -2,7 +2,7 @@
 #
 # Makefile for Noffle news server
 #
-# $Id: Makefile 20 2000-04-18 06:11:50Z enz $
+# $Id: Makefile 32 2000-04-29 14:45:56Z enz $
 #
 ###############################################################################
 
@@ -18,15 +18,17 @@
 CFLAGS = -Wall -O -g
 #CFLAGS = -Wall -g -DDEBUG
 
-VERSION = 1.0pre5
+VERSION = 1.0pre5develop
 
-FILESH = client.h common.h config.h content.h database.h dynamicstring.h  \
-    fetch.h fetchlist.h group.h lock.h log.h online.h outgoing.h over.h \
-    protocol.h pseudo.h request.h server.h util.h
+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
 
-FILESC = fetch.c client.c config.c content.c database.c dynamicstring.c \
-    fetchlist.c group.c lock.c log.c noffle.c online.c outgoing.c over.c \
-    protocol.c pseudo.c request.c server.c util.c
+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
 
 OBJS = $(patsubst %.c,%.o,$(FILESC))
 
--- a/README.html	Sat Apr 29 14:37:59 2000 +0100
+++ b/README.html	Sat Apr 29 15:45:56 2000 +0100
@@ -1,7 +1,8 @@
 <html>
-
 <head>
 <title>NOFFLE</title>
+<META NAME="description" CONTENT="NOFFLE is a news server for Linux that is optimized for low speed dialup connections to the Internet and few users. It can be used to add offline reading capability to news readers.">
+<META NAME="keywords" CONTENT="noffle, news server, news reader, offline, modem, dialup, linux">
 </head>
 
 <body bgcolor=white>
@@ -15,7 +16,24 @@
 <hr>
 <p>
 
-<h2>Features</h2>
+<ul>
+<li>
+<a href="#features">Features</a>
+<li>
+<a href="#compatibility">Compatibility with News Clients</a>
+<li>
+<a href="#getting">Getting NOFFLE</a>
+<li>
+<a href="#documentation">Documentation</a>
+<li>
+<a href="#links">Links</a>
+</ul>
+
+<p>
+<hr>
+<p>
+
+<h2><a name="features">Features</a></h2>
 
 NOFFLE is a
 <a href=http://search.yahoo.com/bin/search?p=usenet>Usenet</a>
@@ -23,15 +41,10 @@
 to the Internet.
 It acts as a server to news clients running on the
 local host, but gets its news feed by acting as a client to a remote server.
-<p>
-It allows reading news offline with many news reader programs,
-even if they do not support offline reading by themselves.
-<p>
 NOFFLE is written for the
-<a href="http://www.linux.org/">Linux</a>
-operating system and freely available under GPL
-(see <a href="http://www.fsf.org/copyleft/gpl.html">
-http://www.fsf.org/copyleft/gpl.html</a>).
+<a href=http://www.linux.org/>Linux </a>
+operating system and freely available under
+<a href=http://www.fsf.org/copyleft/gpl.html>GPL</a>.
 <p>
 While Online:
 <ul>
@@ -45,107 +58,131 @@
 <li>
 Allows reading news offline with many news clients,
 even if they do not support offline reading by themselves.
-<p>
+</li>
 <li>
-Groups can be retrieved in different modes:
+Groups can be retrieved in overview, full or thread mode.
 <ul>
 <li>
 In overview mode, opened articles that have not been completely downloaded
-yet are marked for download. NOFFLE generates a pseudo article telling
+yet are marked for download. NOFFLE generates a pseudo article body telling
 the human about this.
+</li>
 <li>
-In full mode, the complete articles are fetched.
+In full mode, complete articles are fetched at once.
+</li>
 <li>
-Thread mode is like overview mode, but automatically downloads all
-article bodies that are in the same thread as articles that already have
-been read.
+Thread mode is like overview mode, but opening an article marks the
+whole thread for download (all later articles for some time
+that are referencing the original article).
+</li>
 </ul>
-<p>
+</li>
 <li>
 The news feed is invoked automatically next online time by calling
 NOFFLE in the ip-up script.
-<p>
+</li>
 <li>
 Groups can be put on the fetch list via the 'noffle'
-command or automatically when someone tries to read them. Groups can be
-removed from the fetch list manually or automatically, when nobody accesses
-them for some time.
-</i>
+command or
+automatically when someone tries to read them. Groups can be automatically
+removed from the fetch list, when nobody accesses them for some time.
+</li>
+<li>
+NOFFLE also offers limited support for local groups. Articles posted
+in local groups appear in the news database for those groups immediately,
+and are expired in the same way as other articles.
+</li>
 </ul>
 
-<h2>Compatibility with News Clients</h2>
+<h2><a name="compatibility">Compatibility with News Clients</a></h2>
 
 Subscribing to groups in full mode should work with any news reader.
 Caching of articles is unnecessary, since NOFFLE already caches them
 and should be switched off.
 <p>
-Subscribing to groups in overview mode or thread mode puts some
-requirements on the news reader program. See
-<a href="NOTES.html">NOTES.txt</a> for compatibility notes.
+Subscribing to groups in overview mode or thread mode requires the
+following from the news reader program:
+<p>
+<ul>
+<li>
+It must not cache articles at all (or allow to switch the cache off),
+because the article bodies change from the pseudo body
+"marked for download" to the real body.
+<p>
+<li>
+The reader should rarely open article bodies automatically,
+because it will mark them unwantedly for download.
+</ul>
+
 <p>
 
-<h2>Getting NOFFLE</h2>
+<h2><a name="getting">Getting NOFFLE</a></h2>
 
-NOFFLE can be downloaded from the NOFFLE homepage at
-<a href="http://home.t-online.de/home/markus.enzenberger/noffle.html">
-http://home.t-online.de/home/markus.enzenberger/noffle.html</a>:
+NOFFLE can be downloaded from this web site.
 <blockquote>
-<tt>
-<a href="http://home.t-online.de/home/markus.enzenberger/noffle-0.19.tar.gz">
-noffle-0.19.tar.gz</a>
-</tt>
-(current release, 25 Apr 1999)
+<tt><a href=./noffle-1.0pre5.tar.gz>noffle-1.0pre5.tar.gz</a></tt>
+(current release, 18 Apr 2000)
 <br>
-<tt>
-<a href=http://home.t-online.de/home/markus.enzenberger/noffle-0.17.tar.gz>
-noffle-0.17.tar.gz</a>
-</tt>
-(last release, 25 Jan 1999)
+<tt><a href=./noffle-1.0pre4.tar.gz>noffle-1.0pre4.tar.gz</a></tt>
+(current release, 13 Nov 1999)
 </blockquote>
-Read
-<a href="http://home.t-online.de/home/markus.enzenberger/noffle_INSTALL.html">
-INSTALL.txt</a>
-from the package for compiling and installing NOFFLE on your system.
-<p>
-RPM packages have been created by Mario Moder
-(<a href="mailto:moderm@gmx.net">moderm@gmx.net</a>). They are available
+RPM packages have been created by
+<a href="mailto:moderm@gmx.net">Mario Moder</a>. They are available
 at 
 <blockquote>
-<a href="ftp://ftp.fbam.de/pub/linux/">ftp://ftp.fbam.de/pub/linux/</a>
+<tt><a href="ftp://ftp.fbam.de/pub/linux/">ftp://ftp.fbam.de/pub/linux/</a></tt>
 </blockquote>
 <p>
+
+<img src="img_new.gif" width="32" height="16" alt="[NEW]">
+I moved Noffle to
+<a href="http://sourceforge.net/">SourceForge</a>
+recently. You can download files from the
+<a href="http://sourceforge.net/project/?group_id=1044">Noffle project page</a>.
+There is also a mailing list and a discussion forum.
+
+<h2><a name="documentation">Documentation</a></h2>
+
+Read the files README and INSTALL from the package
+for compiling and installing NOFFLE on your system.
+<p>
+Some German documentation
+(<img src="img_german.gif" width="16" height="14" alt="[Deutsch]">
+<a href="http://home.t-online.de/home/klaus.moedinger/noffle_install_de.html">"NOFFLE Installation"</a>)
+is provided by
+<a href="mailto:klaus.moedinger@t-online.de">Klaus M&ouml;dinger</a>.
+<p>
 The current version is still beta. Please send bug reports,
-comments and patches to me
-(<a href="mailto:markus.enzenberger@t-online.de">markus.enzenberger@t-online.de</a>).
+comments and patches to
+<a href="mailto:markus.enzenberger@t-online.de">
+markus.enzenberger@t-online.de</a>.
 <p>
-If you want to receive announcements about NOFFLE, I will put you on
-my announcement list. Just send me a mail.
 
-<h2>Links</h2>
+<h2><a name="links">Links</a></h2>
 
 <ul>
 <li>
-NNTP information
-<br>
-(<a href="http://www.tin.org/docs.html">http://www.tin.org/docs.html</a>)
+<a href="http://www.tin.org/docs.html">NNTP information</a>
 <li>
-Leafnode - news server similar to NOFFLE
-<br>
-(<a href="http://wpxx02.toxi.uni-wuerzburg.de/~krasel/leafnode.html">http://wpxx02.toxi.uni-wuerzburg.de/~krasel/leafnode.html</a>)
+<a href="http://www.privat.kkf.net/~mark.bulmahn/ncontr.html">ncontr</a>
+- graphical front-end for NOFFLE by
+<a href="mailto:mbu@privat.kkf.net">Mark Bulmahn</a>
 <li>
-SLRN - powerful news reader for Windows and Unix
-<br>
-(<a href="http://space.mit.edu/~davis/slrn.html">http://space.mit.edu/~davis/slrn.html</a>)
+<a href="http://www.leafnode.org/">
+Leafnode</a> - news server similar to NOFFLE
 <li>
-GNUS - Emacs news reader
-<br>
-(<a href="http://www.gnus.org/">http://www.gnus.org/</a>
+<a href="http://space.mit.edu/~davis/slrn.html">SLRN</a>
+- powerful news reader for Windows and Unix  
+<li>
+<a href="http://www.gedanken.demon.co.uk/wwwoffle/index.html">WWWOFFLE</a>
+- http proxy
 </ul>
 
+
 <p>
 <hr>
 <small><i>
-Last modified 5/99,
+Last modified 4/2000,
 <a href="mailto:markus.enzenberger@t-online.de">Markus Enzenberger</a>
 </i></small>
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/control.c	Sat Apr 29 15:45:56 2000 +0100
@@ -0,0 +1,74 @@
+/*
+  control.c
+
+  $Id: control.c 32 2000-04-29 14:45:56Z enz $
+*/
+
+#include "control.h"
+#include <stdio.h>
+#include "common.h"
+#include "content.h"
+#include "database.h"
+#include "group.h"
+#include "itemlist.h"
+#include "log.h"
+#include "outgoing.h"
+
+int
+Ctrl_cancel( const char *msgId )
+{
+    ItemList *refs;
+    const char *ref;
+    Str server;
+    Bool seen = FALSE;
+    int res = CANCEL_OK;
+
+    /* See if in outgoing and zap if so. */
+    if ( Out_find( msgId, server ) )
+    {
+	Out_remove( server, msgId );
+	Log_inf( "'%s' cancelled from outgoing queue for '%s'.\n",
+		 msgId, server );
+	seen = TRUE;
+    }
+
+    if ( ! Db_contains( msgId ) )
+    {
+	Log_inf( "Cancel: '%s' not in database.", msgId );
+	return seen ? CANCEL_OK : CANCEL_NO_SUCH_MSG;
+    }
+
+    /*
+      Retrieve the Xrefs, remove from each group and then
+      remove from the database.
+     */
+    refs = new_Itl( Db_xref( msgId ), " " );
+    for( ref = Itl_first( refs ); ref != NULL; ref = Itl_next( refs ) )
+    {
+	Str grp;
+	int no;
+
+	if ( sscanf( ref, "%s:%d", grp, &no ) != 2 )
+	    break;
+	
+	if ( Grp_exists( grp ) )
+	{
+	    Cont_read( grp );
+	    Cont_delete( no );
+	    Cont_write();
+
+	    if ( ! Grp_local( grp ) && ! seen )
+		res = CANCEL_NEEDS_MSG;
+
+	    Log_dbg( "Removed '%s' from group '%s'.", msgId, grp );
+	}
+	else
+	{
+	    Log_inf( "Group '%s' in Xref for '%s' not found.", grp, msgId );
+	}
+    }
+    del_Itl( refs );
+    Db_delete( msgId );
+    Log_inf( "Message '%s' cancelled.", msgId );
+    return res;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/control.h	Sat Apr 29 15:45:56 2000 +0100
@@ -0,0 +1,25 @@
+/*
+  control.h
+
+  Control actions needed by server and command line.
+
+  $Id: control.h 32 2000-04-29 14:45:56Z enz $
+*/
+
+#ifndef CONTROL_H
+#define CONTROL_H
+
+#define	CANCEL_OK		0
+#define	CANCEL_NO_SUCH_MSG	1
+#define	CANCEL_NEEDS_MSG	2
+
+/*
+   Cancel a message. Return CANCEL_OK if completely cancelled,
+   CANCEL_NO_SUCH_MSG if no message with that ID exists, and
+   CANCEL_NEEDS_MSG if a 'cancel' message should be propagated upstream
+   to cancel the message elsewhere.
+ */
+int
+Ctrl_cancel( const char *msgId );
+
+#endif
--- a/database.c	Sat Apr 29 14:37:59 2000 +0100
+++ b/database.c	Sat Apr 29 15:45:56 2000 +0100
@@ -1,7 +1,7 @@
 /*
   database.c
 
-  $Id: database.c 3 2000-01-04 11:35:42Z enz $
+  $Id: database.c 32 2000-04-29 14:45:56Z 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
@@ -251,7 +251,7 @@
     }
     if ( ! ( db.stat & DB_NOT_DOWNLOADED ) )
     {
-        Log_err( "Trying to store alrady retrieved article '%s'", msgId );
+        Log_err( "Trying to store already retrieved article '%s'", msgId );
         return FALSE;
     }
     db.stat &= ~DB_NOT_DOWNLOADED;
@@ -264,24 +264,12 @@
     startPos = artTxt;
     while ( TRUE )
     {
-        artTxt = Utl_getLn( lineEx, artTxt );
+        artTxt = Utl_getHeaderLn( lineEx, artTxt );
         if ( lineEx[ 0 ] == '\0' )
         {
             DynStr_appLn( db.txt, lineEx );
             break;
         }
-        /* Get other lines if field is split over multiple lines */
-        while ( ( artTxt = Utl_getLn( line, artTxt ) ) )
-            if ( isspace( line[ 0 ] ) )
-            {
-                strncat( lineEx, "\n", MAXCHAR );                
-                strncat( lineEx, line, MAXCHAR );
-            }
-            else
-            {
-                artTxt = Utl_ungetLn( startPos, artTxt );
-                break;
-            }
         /* Remove fields already in overview and handle x-noffle
            headers correctly in case of cascading NOFFLEs */
         if ( Prt_getField( field, value, lineEx ) )
@@ -514,6 +502,19 @@
     return gdbm_exists( db.dbf, key );
 }
 
+void
+Db_delete( const char *msgId )
+{
+    datum key;
+
+    ASSERT( db.dbf );
+    if ( strcmp( msgId, db.msgId ) == 0 )
+        db.msgId[ 0 ] = '\0';
+    key.dptr = (void*)msgId;
+    key.dsize = strlen( msgId ) + 1;
+    gdbm_delete( db.dbf, key );
+}
+
 static datum cursor = { NULL, 0 };
 
 Bool
--- a/database.h	Sat Apr 29 14:37:59 2000 +0100
+++ b/database.h	Sat Apr 29 15:45:56 2000 +0100
@@ -3,7 +3,7 @@
 
   Article database.
 
-  $Id: database.h 3 2000-01-04 11:35:42Z enz $
+  $Id: database.h 32 2000-04-29 14:45:56Z enz $
 */
 
 #ifndef DB_H
@@ -66,6 +66,10 @@
 Bool
 Db_contains( const char *msgId );
 
+/* Delete entry from database */
+void
+Db_delete( const char *msgId );
+
 Bool
 Db_first( const char** msgId );
 
--- a/group.c	Sat Apr 29 14:37:59 2000 +0100
+++ b/group.c	Sat Apr 29 15:45:56 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 3 2000-01-04 11:35:42Z enz $
+  $Id: group.c 32 2000-04-29 14:45:56Z enz $
 */
 
 #include "group.h"
@@ -138,6 +138,14 @@
     return gdbm_exists( grp.dbf, key );
 }
 
+Bool
+Grp_local( const char *name )
+{
+    if ( ! loadGrp( name ) )
+        return 0;
+    return ( strcmp( grp.serv, "(local)" ) == 0 );
+}
+
 void
 Grp_create( const char *name )
 {
@@ -226,6 +234,12 @@
 }
 
 void
+Grp_setLocal( const char *name )
+{
+    Grp_setServ( name, "(local)" );
+}
+
+void
 Grp_setServ( const char *name, const char *value )
 {
     if ( loadGrp( name ) )
--- a/group.h	Sat Apr 29 14:37:59 2000 +0100
+++ b/group.h	Sat Apr 29 15:45:56 2000 +0100
@@ -3,7 +3,7 @@
 
   Groups database
 
-  $Id: group.h 3 2000-01-04 11:35:42Z enz $
+  $Id: group.h 32 2000-04-29 14:45:56Z enz $
 */
 
 #ifndef GRP_H
@@ -24,6 +24,10 @@
 Bool
 Grp_exists( const char *name );
 
+/* is it a local group? */
+Bool
+Grp_local( const char *name );
+
 /* create new group and save it in database */
 void
 Grp_create( const char *name );
@@ -66,6 +70,9 @@
 Grp_setDsc( const char *name, const char *value );
 
 void
+Grp_setLocal( const char *name );
+
+void
 Grp_setServ( const char *name, const char *value );
 
 void
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/itemlist.c	Sat Apr 29 15:45:56 2000 +0100
@@ -0,0 +1,123 @@
+/*
+  itemlist.c
+
+  $Id: itemlist.c 32 2000-04-29 14:45:56Z enz $
+*/
+
+#include "itemlist.h"
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.h>
+#include "common.h"
+#include "log.h"
+
+struct ItemList
+{
+    char *list;
+    char *separators;
+    char *next;
+    size_t count;
+};
+
+/* Make a new item list. */
+ItemList *
+new_Itl( const char *list, const char *separators )
+{
+    ItemList * res;
+    char *p;
+    Bool inItem;
+
+    res = (ItemList *) malloc( sizeof( ItemList ) );
+    if ( res == NULL )
+    {
+	Log_err( "Malloc of ItemList failed." );
+	exit( EXIT_FAILURE );
+    }
+    
+    res->list = (char *) malloc( strlen(list) + 2 );
+    if ( res->list == NULL )
+    {
+	Log_err( "Malloc of ItemList.list failed." );
+	exit( EXIT_FAILURE );
+    }
+    strcpy( res->list, list );
+
+    if (  ( res->separators = strdup( separators ) ) == NULL )
+    {
+	Log_err( "Malloc of ItemList.separators failed." );
+	exit( EXIT_FAILURE );
+    }
+
+    res->count = 0;
+    res->next = res->list;
+
+    /* Separate items into strings and have final zero-length string. */
+    for( p = res->list, inItem = FALSE; *p != '\0'; p++ )
+    {
+	Bool isSep = ( strchr( separators, p[ 0 ] ) != NULL );
+	
+	if ( inItem )
+	{
+	    if ( isSep )
+	    {
+		p[ 0 ] = '\0';
+		inItem = FALSE;
+		res->count++;
+	    }
+	}
+	else
+	{
+	    if ( ! isSep )
+		inItem = TRUE;
+	}
+    }
+    if ( inItem )
+	res->count++;
+    p[ 1 ] = '\0';
+    return res;
+}
+
+/* Delete an item list. */
+void
+del_Itl( ItemList *self )
+{
+    if ( self == NULL )
+	return;
+    free( self->list );
+    free( self->separators );
+    free( self );
+}
+
+/* Get first item. */
+const char *
+Itl_first( ItemList *self )
+{
+    self->next = self->list;
+    return Itl_next( self );
+}
+
+/* Get next item or NULL. */
+const char *
+Itl_next( ItemList *self )
+{
+    const char *res = self->next;
+
+    if ( res[ 0 ] == '\0' )
+	return NULL;
+
+    while ( strchr( self->separators, res[ 0 ] ) != NULL )
+	res++;
+
+    if ( res[ 0 ] == '\0' && res[ 1 ] == '\0' )
+	return NULL;
+
+    self->next += strlen( res ) + 1;
+    return res;
+}
+
+/* Get count of items in list. */
+size_t
+Itl_count( const ItemList *self )
+{
+    return self->count;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/itemlist.h	Sat Apr 29 15:45:56 2000 +0100
@@ -0,0 +1,38 @@
+/*
+  itemlist.h
+
+  Copy a string wiht a list of separated items (as found in several
+  header lines) and provide a convenient way of accessing the
+  individual items.
+  
+  $Id: itemlist.h 32 2000-04-29 14:45:56Z enz $ */
+
+#ifndef ITEMLIST_H
+#define ITEMLIST_H
+
+#include <sys/types.h>
+
+struct ItemList;
+typedef struct ItemList ItemList;
+
+/* Make a new item list. */
+ItemList *
+new_Itl( const char *list, const char *separators );
+
+/* Delete an item list. */
+void
+del_Itl( ItemList *self );
+
+/* Get first item. */
+const char *
+Itl_first( ItemList *self);
+
+/* Get next item or NULL. */
+const char *
+Itl_next( ItemList *self );
+
+/* Get count of items in list. */
+size_t
+Itl_count( const ItemList *self );
+
+#endif
--- a/noffle.1	Sat Apr 29 14:37:59 2000 +0100
+++ b/noffle.1	Sat Apr 29 15:45:56 2000 +0100
@@ -1,6 +1,5 @@
-
 .TH noffle 1
-.\" $Id: noffle.1 29 2000-04-29 12:59:10Z enz $
+.\" $Id: noffle.1 32 2000-04-29 14:45:56Z enz $
 .SH NAME
 noffle \- Usenet package optimized for dialup connections.
 
@@ -10,6 +9,12 @@
 \-a | \-\-article <message id>|all
 .br
 .B noffle
+\-c | \-\-cancel <message id>
+.br
+.B noffle
+\-C | \-\-create <local newsgroup name>
+.br
+.B noffle
 \-d | \-\-database
 .br
 .B noffle
@@ -80,6 +85,20 @@
 but download articles full if an article of the same thread already has
 been downloaded.
 
+.PP
+.B NOFFLE
+also offers limited support for local news groups. Articles
+posted to these appear in full in the database for the local group(s)
+immediately. They are expired in the usual way.
+.PP
+If an article is cross-posted to a local group and a remote group, it
+appears in the local group immediately and in the remote group after
+the next fetch from the remove server.
+.PP
+Note that
+.B NOFFLE
+cannot exchange the contents of local groups with other news servers.
+
 .SH OPTIONS
 
 .TP
@@ -91,6 +110,19 @@
 If "all" is given as message Id, all articles are shown. 
 
 .TP
+.B \-c, \-\-cancel <message id>
+Cancel the article from the database and remove it from the queue of
+outbound messages if it has not already been sent. Message Id must
+contain the leading '<' and trailing '>' (quote the argument to avoid
+shell interpretation of '<' and '>').
+
+.TP
+.B \-C, \-\-create <local newsgroup name>
+Create a new local newsgroup with the given name. The name should
+conform to the usual newsgroup naming rules to avoid confusing
+newsreaders.
+
+.TP
 .B \-d, \-\-database
 Write the complete content of the article database to standard output.
 
--- a/noffle.c	Sat Apr 29 14:37:59 2000 +0100
+++ b/noffle.c	Sat Apr 29 15:45:56 2000 +0100
@@ -10,9 +10,10 @@
   received for some seconds (to allow multiple clients connect at the same
   time).
 
-  $Id: noffle.c 15 2000-04-11 06:36:57Z enz $
+  $Id: noffle.c 32 2000-04-29 14:45:56Z enz $
 */
 
+#include <ctype.h>
 #include <errno.h>
 #include <getopt.h>
 #include <signal.h>
@@ -22,13 +23,16 @@
 #include "client.h"
 #include "common.h"
 #include "content.h"
+#include "control.h"
 #include "config.h"
 #include "database.h"
 #include "fetch.h"
 #include "fetchlist.h"
 #include "group.h"
+#include "itemlist.h"
 #include "log.h"
 #include "online.h"
+#include "outgoing.h"
 #include "over.h"
 #include "pseudo.h"
 #include "util.h"
@@ -72,6 +76,25 @@
     }
 }
 
+static void
+doCancel( const char *msgId )
+{
+    switch( Ctrl_cancel( msgId ) )
+    {
+    case CANCEL_NO_SUCH_MSG:
+	printf( "No such message '%s'.\n", msgId );
+	break;
+
+    case CANCEL_OK:
+	printf( "Message '%s' cancelled.\n", msgId );
+	break;
+
+    case CANCEL_NEEDS_MSG:
+	printf( "Message '%s' cancelled in local database only.\n", msgId );
+	break;
+    }
+}
+
 /* List articles requested from one particular server */
 static void
 listRequested1( const char* serv )
@@ -199,7 +222,8 @@
                     else
                         ++cntLeft;
                 }
-            if ( autoUnsubscribe
+            if ( !Grp_local( grp )
+		 && autoUnsubscribe
                  && difftime( now, Grp_lastAccess( grp ) ) > maxAge )
             {
                 Log_ntc( "Auto-unsubscribing from %s after %d "
@@ -230,6 +254,21 @@
 }
 
 static void
+doCreateLocalGroup( const char * name )
+{
+    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 );
+    }
+}
+
+static void
 doList( void )
 {
     FetchMode mode;
@@ -324,6 +363,8 @@
       "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"
@@ -399,7 +440,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 );
 }
@@ -441,6 +482,8 @@
     struct option longOptions[] =
     {
         { "article",          required_argument, NULL, 'a' },
+        { "cancel",           required_argument, NULL, 'c' },
+        { "create",           required_argument, NULL, 'C' },
         { "database",         no_argument,       NULL, 'd' },
         { "expire",           required_argument, NULL, 'e' },
         { "fetch",            no_argument,       NULL, 'f' },
@@ -467,7 +510,7 @@
     signal( SIGINT, logSignal );
     signal( SIGTERM, logSignal );
     signal( SIGPIPE, logSignal );
-    c = getopt_long( argc, argv, "a:de:fghlonq:rRs:S:t:u:v",
+    c = getopt_long( argc, argv, "a:c:C:de:fghlonq:rRs:S:t:u:v",
                      longOptions, NULL );
     if ( ! initNoffle( c != 'r' ) )
         return EXIT_FAILURE;
@@ -486,6 +529,24 @@
         else
             doArt( optarg );
         break;
+    case 'c':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -c needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            doCancel( optarg );
+        break;
+    case 'C':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -C needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            doCreateLocalGroup( optarg );
+        break;
     case 'd':
         doDb();
         break;
--- a/outgoing.c	Sat Apr 29 14:37:59 2000 +0100
+++ b/outgoing.c	Sat Apr 29 15:45:56 2000 +0100
@@ -1,7 +1,7 @@
 /*
   outgoing.c
 
-  $Id: outgoing.c 3 2000-01-04 11:35:42Z enz $
+  $Id: outgoing.c 32 2000-04-29 14:45:56Z enz $
 */
 
 #include "outgoing.h"
@@ -43,7 +43,7 @@
 }
 
 Bool
-Out_add( const char *serv, const Str msgId, const DynStr *artTxt )
+Out_add( const char *serv, const char *msgId, const DynStr *artTxt )
 {
     Str file;
     FILE *f;
@@ -111,7 +111,7 @@
 }
 
 void
-Out_remove( const char *serv, Str msgId )
+Out_remove( const char *serv, const char *msgId )
 {
     Str file;
 
@@ -119,3 +119,45 @@
     if ( unlink( file ) != 0 )
         Log_err( "Cannot remove %s", file );
 }
+
+Bool
+Out_find( const char *msgId, Str server )
+{
+    Str servdir;
+    DIR *d;
+    struct dirent *entry;
+    Bool res;
+    
+    
+    snprintf( servdir, MAXCHAR, "%s/outgoing", Cfg_spoolDir() );
+    if ( ! ( d = opendir( servdir ) ) )
+    {
+        Log_dbg( "Cannot open %s", servdir );
+        return FALSE;
+    }
+
+    readdir( d );	/* '.' */
+    readdir( d );	/* '..' */
+
+    res = FALSE;
+    while ( ! res && ( entry = readdir( d ) ) != NULL )
+    {
+	Str file;
+	struct stat s;
+
+	fileOutgoing( file, entry->d_name, msgId );
+	if ( stat( file, &s ) == 0 )
+	{
+	    res = TRUE;
+	    Utl_cpyStr( server, entry->d_name );
+	}
+    }
+
+    closedir( d );
+    return res;
+}
+
+
+
+
+
--- a/outgoing.h	Sat Apr 29 14:37:59 2000 +0100
+++ b/outgoing.h	Sat Apr 29 15:45:56 2000 +0100
@@ -3,7 +3,7 @@
 
   Collection of posted articles.
 
-  $Id: outgoing.h 3 2000-01-04 11:35:42Z enz $
+  $Id: outgoing.h 32 2000-04-29 14:45:56Z enz $
 */
 
 #ifndef OUT_H
@@ -13,7 +13,7 @@
 #include "dynamicstring.h"
 
 Bool
-Out_add( const char *serv, const Str msgId, const DynStr *artTxt );
+Out_add( const char *serv, const char *msgId, const DynStr *artTxt );
 
 /* Start enumeration. Return TRUE on success. */
 Bool
@@ -25,6 +25,10 @@
 
 /* Delete article from outgoing collection */
 void
-Out_remove( const char *serv, Str msgId );
+Out_remove( const char *serv, const char *msgId );
+
+/* Find server for outgoing message. */
+Bool
+Out_find( const char *msgId, Str server );
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/post.c	Sat Apr 29 15:45:56 2000 +0100
@@ -0,0 +1,150 @@
+/*
+  post.c
+
+  $Id: post.c 32 2000-04-29 14:45:56Z enz $
+*/
+
+#include "post.h"
+#include <string.h>
+#include "common.h"
+#include "content.h"
+#include "database.h"
+#include "group.h"
+#include "log.h"
+#include "over.h"
+#include "protocol.h"
+#include "util.h"
+
+struct OverInfo
+{
+    Str subject;
+    Str from;
+    Str date;
+    Str msgId;
+    Str ref;
+    size_t bytes;
+    size_t lines;
+};
+
+struct Article
+{
+    const char * text;
+    Bool posted;
+    struct OverInfo over;
+};
+
+static struct Article article = { NULL, FALSE };
+
+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;
+}
+
+
+/* Add the article to a group. */
+Bool
+Post_add ( const char * grp )
+{
+    Over * over;
+    const char *msgId;
+    
+    over = new_Over( article.over.subject,
+		     article.over.from,
+		     article.over.date,
+		     article.over.msgId,
+		     article.over.ref,
+		     article.over.bytes,
+		     article.over.lines );
+    
+    msgId = article.over.msgId;
+    
+    Cont_read( grp );
+    Cont_app( over );
+    Log_dbg( "Added message '%s' to group '%s'.", msgId, grp );
+
+    if ( !article.posted )
+    {
+        Log_inf( "Added '%s' to database.", msgId );
+        if ( ! Db_prepareEntry( over, Cont_grp(), Cont_last() )
+	     || ! Db_storeArt ( msgId, article.text ) )
+	    return FALSE;
+	article.posted = TRUE;
+    }
+    else
+    {
+	Str t;
+	const char *xref;
+
+	xref = Db_xref( msgId );
+	Log_dbg( "Adding '%s' to Xref of '%s'", grp, msgId );
+	snprintf( t, MAXCHAR, "%s %s:%i", xref, grp, Ov_numb( over ) );
+	Db_setXref( msgId, t );
+    }
+    
+    Cont_write();
+    Grp_setFirstLast( Cont_grp(), Cont_first(), Cont_last() );
+    return TRUE;
+}
+   
+/* Done with article - tidy up. */
+void
+Post_close( void )
+{
+    article.text = NULL;
+    article.posted = FALSE;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/post.h	Sat Apr 29 15:45:56 2000 +0100
@@ -0,0 +1,28 @@
+/*
+  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).
+
+  $Id: post.h 32 2000-04-29 14:45:56Z enz $
+*/
+
+#ifndef POST_H
+#define POST_H
+
+#include "common.h"
+
+/* Register an article for posting. */
+Bool
+Post_open( const char * text );
+
+/* Add the article to a group. */
+Bool
+Post_add ( const char * grp );
+   
+/* Done with article - tidy up. */
+void
+Post_close( void );
+
+#endif
--- a/pseudo.c	Sat Apr 29 14:37:59 2000 +0100
+++ b/pseudo.c	Sat Apr 29 15:45:56 2000 +0100
@@ -1,7 +1,7 @@
 /*
   pseudo.c
   
-  $Id: pseudo.c 3 2000-01-04 11:35:42Z enz $
+  $Id: pseudo.c 32 2000-04-29 14:45:56Z enz $
 */
 
 #include "pseudo.h"
@@ -108,7 +108,7 @@
             "\t[ optimized for low speed dial-up Internet connections. ]\n"
             "\n"
             "\t[ This group is presently not on the fetch list. You can ]\n"
-            "\t[ put groups on the fetxh list by running the \"noffle\" ]\n"
+            "\t[ put groups on the fetch list by running the \"noffle\" ]\n"
             "\t[ command on the computer where this server is running. ]\n"
             "\n"
             "\t[ If you have more questions about NOFFLE please talk ]\n"
--- a/server.c	Sat Apr 29 14:37:59 2000 +0100
+++ b/server.c	Sat Apr 29 15:45:56 2000 +0100
@@ -1,7 +1,7 @@
 /*
   server.c
 
-  $Id: server.c 18 2000-04-15 10:09:20Z enz $
+  $Id: server.c 32 2000-04-29 14:45:56Z enz $
 */
 
 #include "server.h"
@@ -16,15 +16,18 @@
 #include "common.h"
 #include "config.h"
 #include "content.h"
+#include "control.h"
 #include "database.h"
 #include "dynamicstring.h"
 #include "fetch.h"
 #include "fetchlist.h"
 #include "group.h"
+#include "itemlist.h"
 #include "lock.h"
 #include "log.h"
 #include "online.h"
 #include "outgoing.h"
+#include "post.h"
 #include "protocol.h"
 #include "pseudo.h"
 #include "request.h"
@@ -103,7 +106,7 @@
     FetchMode mode;
 
     Grp_setLastAccess( serv.grp, time( NULL ) );
-    if ( Cfg_autoSubscribe() && ! Online_true() )
+    if ( ! Grp_local ( serv.grp ) && Cfg_autoSubscribe() && ! Online_true() )
     {
         Fetchlist_read();
         if ( ! Fetchlist_contains( serv.grp ) )
@@ -219,7 +222,9 @@
 {
     Fetchlist_read();
     Cont_read( name );
-    if ( ! Fetchlist_contains( name ) && ! Online_true() )
+    if ( ! Grp_local ( name )
+	 && ! Fetchlist_contains( name )
+	 && ! Online_true() )
     { 
         Pseudo_appGeneralInfo();
         Grp_setFirstLast( name, Cont_first(), Cont_last() );
@@ -302,7 +307,7 @@
     Str serv;
 
     findServ( msgId, serv );    
-    if ( strcmp( serv, "(unknown)" ) == 0 )
+    if ( strcmp( serv, "(unknown)" ) == 0  || strcmp( serv, "(local)" ) == 0 )
         return FALSE;        
     if ( ! Client_connect( serv ) )
         return FALSE;
@@ -425,7 +430,9 @@
         findServ( msgId, serv );
         if ( Req_contains( serv, msgId ) )
             putTxtBuf( Pseudo_alreadyMarkedBody() );
-        else if ( strcmp( serv, "(unknown)" ) != 0 && Req_add( serv, msgId ) )
+        else if ( strcmp( serv, "(unknown)" ) != 0 && 
+		  strcmp( serv, "(local)" ) != 0 && 
+		  Req_add( serv, msgId ) )
             putTxtBuf( Pseudo_markedBody() );
         else
             putTxtBuf( Pseudo_markingFailedBody() );
@@ -845,38 +852,133 @@
     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 );
+}
+
 /*
-  Get first group of the Newsgroups field content, which is
-  a comma separated list of groups.
-*/
-static void
-getFirstGrp( char *grpResult, const char *list )
+  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 )
 {
-    Str t;
-    const char *src = list;
-    char *dest = t;
-    while( TRUE )
+    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_serv( 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 ( *src == ',' )
-            *dest = ' ';
-        else
-            *dest = *src;
-        if ( *src == '\0' )
-            break;
-        ++src;
-        ++dest;
+	if ( Grp_exists( grp ) && Grp_local( grp ) )
+	    Log_inf( "Ignoring control '%s' for '%s'.", op, grp );
     }
-    *grpResult = '\0';
-    sscanf( t, "%s", grpResult );
+
+    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_serv( 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, inHeader;
+    Bool err, replyToFound, dateFound, inHeader;
     DynStr *s;
-    Str line, field, val, msgId, from, grp;
+    Str line, field, val, msgId, from;
     const char* p;
+    ItemList * newsgroups, *control;
 
     /*
       Get article and make following changes to the header:
@@ -895,8 +997,8 @@
     s = new_DynStr( 10000 );
     msgId[ 0 ] = '\0';
     from[ 0 ] = '\0';
-    grp[ 0 ] = '\0';
-    replyToFound = FALSE;
+    newsgroups = control = NULL;
+    replyToFound = dateFound = FALSE;
     inHeader = TRUE;
     while ( getTxtLn( line, &err ) )
     {
@@ -918,6 +1020,7 @@
                     else if ( msgId[ 0 ] == '\0' )
                     {
                         Prt_genMsgId( msgId, from, "NOFFLE" );
+
                         Log_inf( "Adding missing Message-ID '%s'", msgId );
                     }
                     else if ( ! Prt_isValidMsgId( msgId ) )
@@ -935,6 +1038,16 @@
                     DynStr_app( s, "Reply-To: " );
                     DynStr_appLn( s, from );
                 }
+		if ( ! dateFound )
+		{
+		    time_t t;
+
+		    time( &t );
+		    strftime( val, MAXCHAR, "%d %b %Y %H:%M:%S %Z",
+			      localtime( &t ) );
+		    DynStr_app( s, "Date: " );
+		    DynStr_appLn( s, val );
+		}
                 DynStr_appLn( s, p );
             }
             else if ( Prt_getField( field, val, p ) )
@@ -948,8 +1061,13 @@
                 }
                 else if ( strcmp( field, "newsgroups" ) == 0 )
                 {
-                    getFirstGrp( grp, val );
-                    Utl_toLower( grp );
+                    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 )
@@ -957,6 +1075,11 @@
                     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: " );
@@ -975,21 +1098,41 @@
         Log_err( "Posted message has no body" );
     if ( ! err )
     {
-        if ( grp[ 0 ] == '\0' )
+        if ( newsgroups == NULL || Itl_count( newsgroups ) == 0 )
         {
-            Log_err( "Posted message has no Newsgroups header field" );
+            Log_err( "Posted message has no valid Newsgroups header field" );
             err = TRUE;
         }
-        else if ( ! Grp_exists( grp ) )
-        {    
-            Log_err( "Unknown group in Newsgroups header field" );
-            err = TRUE;
-        }
-        else if ( ! Out_add( Grp_serv( grp ), msgId, s ) )
-        {
-            Log_err( "Cannot add posted article to outgoing directory" );
-            err = TRUE;
-        }
+        else
+	{
+	    const char *grp;
+	    Bool knownGrp = 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;
+		    break;
+		}
+	    }
+	    
+	    if ( ! knownGrp )
+	    {
+
+		Log_err( "No known group in Newsgroups header field" );
+		err = TRUE;
+	    }
+	    else
+	    {
+		err = ( control == NULL )
+		    ? postArticle( newsgroups, msgId, s )
+		    : handleControl( control, newsgroups, msgId, s );
+	    }	    
+	}
     }
     if ( err )
         putStat( STAT_POST_FAILED, "Posting failed" );
@@ -999,6 +1142,8 @@
         if ( Online_true() )
             postArts();
     }
+    del_Itl( newsgroups );
+    del_Itl( control );
     del_DynStr( s );
     return TRUE;
 }
--- a/util.c	Sat Apr 29 14:37:59 2000 +0100
+++ b/util.c	Sat Apr 29 15:45:56 2000 +0100
@@ -1,7 +1,7 @@
 /*
   util.c
 
-  $Id: util.c 3 2000-01-04 11:35:42Z enz $
+  $Id: util.c 32 2000-04-29 14:45:56Z enz $
 */
 
 #include "util.h"
@@ -100,6 +100,38 @@
     }
 }
 
+const char *
+Utl_getHeaderLn( Str result, const char *p )
+{
+    const char * res = Utl_getLn( result, p );
+
+    /* Look for followon line if this isn't a blank line. */
+    if ( res != NULL && !isspace( result[ 0 ] ) )
+	while ( res != NULL && res[ 0 ] != '\n' && isspace( res[ 0 ] ) )
+	{
+	    Str nextLine;
+	    const char *here;
+	    char *next;
+
+	    here = res;
+	    res = Utl_getLn( nextLine, res );
+	    next = Utl_stripWhiteSpace( nextLine );
+
+	    if ( next[ 0 ] != '\0' )
+	    {
+		Utl_catStr( result, " " );
+		Utl_catStr( result, next );
+	    }
+	    else
+	    {
+		res = here;
+		break;
+	    }
+	}
+
+    return res;
+}
+
 void
 Utl_toLower( Str line )
 {
@@ -139,11 +171,27 @@
 void
 Utl_cpyStrN( Str dst, const char *src, size_t n )
 {
+    if ( n > MAXCHAR )
+    	n = MAXCHAR;
     dst[ 0 ] = '\0';
     strncat( dst, src, n );
 }
 
 void
+Utl_catStr( Str dst, const char *src )
+{
+    strncat( dst, src, MAXCHAR - strlen( dst ) );
+}
+
+void
+Utl_catStrN( Str dst, const char *src, size_t n )
+{
+    if ( n > MAXCHAR - strlen( dst ) )
+    	n = MAXCHAR - strlen( dst );
+    strncat( dst, src, n );
+}
+
+void
 Utl_stamp( Str file )
 {
     FILE *f;
--- a/util.h	Sat Apr 29 14:37:59 2000 +0100
+++ b/util.h	Sat Apr 29 15:45:56 2000 +0100
@@ -3,7 +3,7 @@
 
   Miscellaneous helper functions.
 
-  $Id: util.h 3 2000-01-04 11:35:42Z enz $
+  $Id: util.h 32 2000-04-29 14:45:56Z enz $
 */
 
 #ifndef UTL_H
@@ -36,6 +36,13 @@
 Utl_ungetLn( const char *str, const char *p );
 
 /*
+  Read a header line from string. Reads continuation lines if
+  necessary. Return NULL if pos == NULL or no more line to read
+*/
+const char *
+Utl_getHeaderLn( Str result, const char *p );
+
+/*
   Strip white spaces from left and right side.
   Return pointer to new start. This is within line.
 */
@@ -56,6 +63,12 @@
 void
 Utl_cpyStrN( Str dst, const char *src, size_t n );
 
+void
+Utl_catStr( Str dst, const char *src );
+
+void
+Utl_catStrN( Str dst, const char *src, size_t n );
+
 /* String allocation and copying. */
 void
 Utl_allocAndCpy( char **dst, const char *src );