# HG changeset patch
# User enz
# Date 957019556 -3600
# Node ID 526a4c34ee2e7a32c3411a2de3553abd393c9ce1
# Parent  ab6cf19be6d3b9ebe3ccde4ab579875bae96f883
[svn] Applied patch from Jim Hague: support for local groups / new command
line options --create and --cancel.

diff -r ab6cf19be6d3 -r 526a4c34ee2e CHANGELOG.html
--- 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>
diff -r ab6cf19be6d3 -r 526a4c34ee2e Makefile
--- 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))
 
diff -r ab6cf19be6d3 -r 526a4c34ee2e README.html
--- 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>
 
diff -r ab6cf19be6d3 -r 526a4c34ee2e control.c
--- /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;
+}
diff -r ab6cf19be6d3 -r 526a4c34ee2e control.h
--- /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
diff -r ab6cf19be6d3 -r 526a4c34ee2e database.c
--- 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
diff -r ab6cf19be6d3 -r 526a4c34ee2e database.h
--- 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 );
 
diff -r ab6cf19be6d3 -r 526a4c34ee2e group.c
--- 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 ) )
diff -r ab6cf19be6d3 -r 526a4c34ee2e group.h
--- 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
diff -r ab6cf19be6d3 -r 526a4c34ee2e itemlist.c
--- /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;
+}
diff -r ab6cf19be6d3 -r 526a4c34ee2e itemlist.h
--- /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
diff -r ab6cf19be6d3 -r 526a4c34ee2e noffle.1
--- 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.
 
diff -r ab6cf19be6d3 -r 526a4c34ee2e noffle.c
--- 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;
diff -r ab6cf19be6d3 -r 526a4c34ee2e outgoing.c
--- 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;
+}
+
+
+
+
+
diff -r ab6cf19be6d3 -r 526a4c34ee2e outgoing.h
--- 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
diff -r ab6cf19be6d3 -r 526a4c34ee2e post.c
--- /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;
+}
+
diff -r ab6cf19be6d3 -r 526a4c34ee2e post.h
--- /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
diff -r ab6cf19be6d3 -r 526a4c34ee2e pseudo.c
--- 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"
diff -r ab6cf19be6d3 -r 526a4c34ee2e server.c
--- 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;
 }
diff -r ab6cf19be6d3 -r 526a4c34ee2e util.c
--- 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;
diff -r ab6cf19be6d3 -r 526a4c34ee2e util.h
--- 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 );