diff server.c @ 0:04124a4423d4 noffle

[svn] Initial revision
author enz
date Tue, 04 Jan 2000 11:35:42 +0000
parents
children 43631b72021f
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server.c	Tue Jan 04 11:35:42 2000 +0000
@@ -0,0 +1,1368 @@
+/*
+  server.c
+
+  $Id: server.c 3 2000-01-04 11:35:42Z enz $
+*/
+
+#include "server.h"
+#include <ctype.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+#include "client.h"
+#include "common.h"
+#include "config.h"
+#include "content.h"
+#include "database.h"
+#include "dynamicstring.h"
+#include "fetch.h"
+#include "fetchlist.h"
+#include "group.h"
+#include "lock.h"
+#include "log.h"
+#include "online.h"
+#include "outgoing.h"
+#include "protocol.h"
+#include "pseudo.h"
+#include "request.h"
+#include "util.h"
+
+struct
+{
+    Bool running;
+    int artPtr;
+    Str grp; /* selected group, "" if none */
+} serv = { FALSE, 0, "" };
+
+typedef struct Cmd
+{
+    const char *name;
+    const char *syntax;
+    /* Returns false, if quit cmd */
+    Bool (*cmdProc)( char *arg, const struct Cmd *cmd );
+}
+Cmd;
+
+static Bool doArt( char *arg, const Cmd *cmd );
+static Bool doBody( char *arg, const Cmd *cmd );
+static Bool doGrp( char *arg, const Cmd *cmd );
+static Bool doHead( char *arg, const Cmd *cmd );
+static Bool doHelp( char *arg, const Cmd *cmd );
+static Bool doIhave( char *arg, const Cmd *cmd );
+static Bool doLast( char *arg, const Cmd *cmd );
+static Bool doList( char *arg, const Cmd *cmd );
+static Bool doListgrp( char *arg, const Cmd *cmd );
+static Bool doMode( char *arg, const Cmd *cmd );
+static Bool doNewgrps( char *arg, const Cmd *cmd );
+static Bool doNext( char *arg, const Cmd *cmd );
+static Bool doPost( char *arg, const Cmd *cmd );
+static Bool doSlave( char *arg, const Cmd *cmd );
+static Bool doStat( char *arg, const Cmd *cmd );
+static Bool doQuit( char *arg, const Cmd *cmd );
+static Bool doXhdr( char *arg, const Cmd *cmd );
+static Bool doXpat( char *arg, const Cmd *cmd );
+static Bool doXOver( char *arg, const Cmd *cmd );
+static Bool notImplemented( char *arg, const Cmd *cmd );
+static void putStat( unsigned int stat, const char *fmt, ... );
+
+Cmd commands[] =
+{
+    { "article", "ARTICLE [msg-id|n]", &doArt },
+    { "body", "BODY [msg-id|n]", &doBody },
+    { "head", "HEAD [msg-id|n]", &doHead },
+    { "group", "GROUP grp", &doGrp },
+    { "help", "HELP", &doHelp },
+    { "ihave", "IHAVE (ignored)", &doIhave },
+    { "last", "LAST", &doLast },
+    { "list", "LIST [ACTIVE [pat]]|ACTIVE.TIMES [pat]|"
+      "EXTENSIONS|NEWSGROUPS [pat]|OVERVIEW.FMT", &doList },
+    { "listgroup", "LISTGROUP grp", &doListgrp },
+    { "mode", "MODE (ignored)", &doMode },
+    { "newgroups", "NEWGROUPS [xx]yymmdd hhmmss [GMT]", &doNewgrps },
+    { "newnews", "NEWNEWS (not implemented)", &notImplemented },
+    { "next", "NEXT", &doNext },
+    { "post", "POST", &doPost },
+    { "quit", "QUIT", &doQuit },
+    { "slave", "SLAVE (ignored)", &doSlave },
+    { "stat", "STAT [msg-id|n]", &doStat },
+    { "xhdr", "XHDR over-field [m[-[n]]]", &doXhdr },
+    { "xpat", "XPAT over-field m[-[n]] pat", &doXpat },
+    { "xover", "XOVER [m[-[n]]]", &doXOver }
+};
+
+/*
+  Notice interest in reading this group.
+  Automatically subscribe if option set in config file.
+*/
+static void
+noteInterest( void )
+{
+    FetchMode mode;
+
+    Grp_setLastAccess( serv.grp, time( NULL ) );
+    if ( Cfg_autoSubscribe() && ! Online_true() )
+    {
+        Fetchlist_read();
+        if ( ! Fetchlist_contains( serv.grp ) )
+        {
+            if ( strcmp( Cfg_autoSubscribeMode(), "full" ) == 0 )
+                mode = FULL;
+            else if ( strcmp( Cfg_autoSubscribeMode(), "thread" ) == 0 )
+                mode = THREAD;
+            else
+                mode = OVER;
+            Fetchlist_add( serv.grp, mode );
+            Fetchlist_write();
+            Pseudo_autoSubscribed();
+        }
+    }
+}
+
+static void
+putStat( unsigned int stat, const char *fmt, ... )
+{
+    Str s, line;
+    va_list ap;
+
+    ASSERT( stat <= 999 );
+    va_start( ap, fmt );
+    vsnprintf( s, MAXCHAR, fmt, ap );
+    va_end( ap );
+    snprintf( line, MAXCHAR, "%u %s", stat, s );
+    Log_dbg( "[S] %s", line );
+    printf( "%s\r\n", line );
+}
+
+static void
+putTxtLn( const char *fmt, ... )
+{
+    Str line;
+    va_list ap;
+
+    va_start( ap, fmt );
+    vsnprintf( line, MAXCHAR, fmt, ap );
+    va_end( ap );
+    Prt_putTxtLn( line, stdout );
+}
+
+static void
+putTxtBuf( const char *buf )
+{
+    if ( buf )
+        Prt_putTxtBuf( buf, stdout );
+}
+
+static void
+putEndOfTxt( void )
+{
+    Prt_putEndOfTxt( stdout );
+}
+
+static void
+putSyntax( const Cmd *cmd )
+{
+    putStat( STAT_SYNTAX_ERR, "Syntax error. Usage: %s", cmd->syntax );
+}
+
+static Bool
+getLn( Str line )
+{
+    return Prt_getLn( line, stdin );
+}
+
+static Bool
+getTxtLn( Str line, Bool *err )
+{
+    return Prt_getTxtLn( line, err, stdin );
+}
+
+static Bool
+notImplemented( char *arg, const Cmd *cmd )
+{
+    putStat( STAT_NO_PERMISSION, "Command not implemented" );
+    return TRUE;
+}
+
+static void
+checkNewArts( const char *grp )
+{
+    if ( ! Online_true()
+         || strcmp( grp, serv.grp ) == 0
+         || time( NULL ) - Grp_lastAccess( serv.grp ) < 1800 )
+        return;
+    if ( Fetch_init( Grp_serv( grp ) ) )
+    {
+        Fetch_getNewArts( grp, OVER );
+        Fetch_close();
+    }
+}
+
+static void
+postArts()
+{
+    Str serv;
+
+    Cfg_beginServEnum();
+    while ( Cfg_nextServ( serv ) )
+        if ( Fetch_init( serv ) )
+        {
+            Fetch_postArts();
+            Fetch_close();
+        }
+}
+
+static void
+readCont( const char *name )
+{
+    Fetchlist_read();
+    Cont_read( name );
+    if ( ! Fetchlist_contains( name ) && ! Online_true() )
+    { 
+        Pseudo_appGeneralInfo();
+        Grp_setFirstLast( name, Cont_first(), Cont_last() );
+    }
+}
+
+static void
+changeToGrp( const char *grp )
+{
+    checkNewArts( grp );
+    Utl_cpyStr( serv.grp, grp );
+    readCont( grp );
+    serv.artPtr = Cont_first();
+}
+
+static Bool
+doGrp( char *arg, const Cmd *cmd )
+{
+    int first, last, numb;
+
+    if ( arg[ 0 ] == '\0' )
+        putSyntax( cmd );
+    else if ( ! Grp_exists( arg ) )
+        putStat( STAT_NO_SUCH_GRP, "No such group" );
+    else
+    {
+        changeToGrp( arg );
+        first = Cont_first();
+        last = Cont_last();
+        numb = last - first + 1;
+        if ( first > last )
+            first = last = numb = 0;
+        putStat( STAT_GRP_SELECTED, "%lu %lu %lu %s selected",
+                 numb, first, last, arg );
+    }
+    return TRUE;
+}
+
+static Bool
+testGrpSelected( void )
+{
+    if ( *serv.grp == '\0' )
+    {
+        putStat( STAT_NO_GRP_SELECTED, "No group selected" );
+        return FALSE;
+    }
+    return TRUE;
+}
+
+static void
+findServ( const char *msgId, Str result )
+{
+    const char *p, *pColon, *serv;
+    Str s, grp;
+
+    Utl_cpyStr( result, "(unknown)" );
+    if ( Db_contains( msgId ) )
+    {
+        Utl_cpyStr( s, Db_xref( msgId ) );
+        p = strtok( s, " \t" );
+        if ( p )
+            do
+            {
+                pColon = strstr( p, ":" );
+                if ( pColon )
+                {
+                    Utl_cpyStrN( grp, p, pColon - p );
+                    serv = Grp_serv( grp );
+                    if ( Cfg_servIsPreferential( serv, result ) )
+                        Utl_cpyStr( result, serv );
+                }
+            }
+            while ( ( p = strtok( NULL, " \t" ) ) );
+    }
+}
+
+static Bool
+retrieveArt( const char *msgId )
+{
+    Str serv;
+
+    findServ( msgId, serv );    
+    if ( strcmp( serv, "(unknown)" ) == 0 )
+        return FALSE;        
+    if ( ! Client_connect( serv ) )
+        return FALSE;
+    Client_retrieveArt( msgId );
+    Client_disconnect();
+    return TRUE;
+}
+
+static Bool
+checkNumb( int numb )
+{
+    if ( ! testGrpSelected() )
+        return FALSE;
+    if ( ! Cont_validNumb( numb ) )
+    {
+        putStat( STAT_NO_SUCH_NUMB, "No such article" );
+        return FALSE;
+    }
+    return TRUE;
+}
+
+/*
+  Parse arguments for ARTICLE, BODY, HEAD, STAT commands.
+  Return message-ID and article number (0 if unknown).
+*/
+static Bool
+whichId( const char **msgId, int *numb, char *arg )
+{
+    const Over *ov;
+    int n;
+
+    if ( sscanf( arg, "%i", &n ) == 1 )
+    {
+        if ( ! checkNumb( n ) )
+            return FALSE;
+        serv.artPtr = n;
+        ov = Cont_get( n );
+        *msgId = Ov_msgId( ov );
+        *numb = n;
+    }
+    else if ( strcmp( arg, "" ) == 0 )
+    {
+        if ( ! checkNumb( serv.artPtr ) )
+            return FALSE;
+        ov = Cont_get( serv.artPtr );
+        *msgId = Ov_msgId( ov );
+        *numb =  serv.artPtr;
+    }
+    else
+    {
+        *msgId = arg;
+        *numb = 0;
+    }
+    if ( ! Pseudo_isGeneralInfo( *msgId ) && ! Db_contains( *msgId ) )
+    {
+        putStat( STAT_NO_SUCH_NUMB, "No such article" );
+        return FALSE;
+    }
+    return TRUE;
+}
+
+void
+touchArticle( const char *msgId )
+{
+    int stat = Db_stat( msgId );
+    stat |= DB_INTERESTING;
+    Db_setStat( msgId, stat );
+    Db_updateLastAccess( msgId );
+}
+
+static void
+touchReferences( const char *msgId )
+{
+    Str s;
+    int len;
+    char *p;
+    const char *ref = Db_ref( msgId );
+
+    while ( TRUE )
+    {
+        p = s;
+        while ( *ref != '<' )
+            if ( *(ref++) == '\0' )
+                return;
+        len = 0;
+        while ( *ref != '>' )
+        {
+            if ( *ref == '\0' || ++len >= MAXCHAR - 1 )
+                return;
+            *(p++) = *(ref++);
+        }
+        *(p++) = '>';
+        *p = '\0';
+        if ( Db_contains( s ) )
+            touchArticle( s );
+    }
+}
+
+static void
+doBodyInDb( const char *msgId )
+{
+    int stat;
+    Str serv;
+
+    touchArticle( msgId );
+    touchReferences( msgId );
+    stat = Db_stat( msgId );
+    if ( Online_true() && ( stat & DB_NOT_DOWNLOADED ) )
+    {
+        retrieveArt( msgId );
+        stat = Db_stat( msgId );
+    }
+    if ( stat & DB_RETRIEVING_FAILED )
+    {
+        Db_setStat( msgId, stat & ~DB_RETRIEVING_FAILED );
+        putTxtBuf( Db_body( msgId ) );
+    }
+    else if ( stat & DB_NOT_DOWNLOADED )
+    {
+        findServ( msgId, serv );
+        if ( Req_contains( serv, msgId ) )
+            putTxtBuf( Pseudo_alreadyMarkedBody() );
+        else if ( strcmp( serv, "(unknown)" ) != 0 && Req_add( serv, msgId ) )
+            putTxtBuf( Pseudo_markedBody() );
+        else
+            putTxtBuf( Pseudo_markingFailedBody() );
+    }
+    else
+        putTxtBuf( Db_body( msgId ) );
+}
+
+static Bool
+doBody( char *arg, const Cmd *cmd )
+{
+    const char *msgId;
+    int numb;
+    
+    if ( ! whichId( &msgId, &numb, arg ) )
+        return TRUE;
+    putStat( STAT_BODY_FOLLOWS, "%ld %s Body", numb, msgId );
+    if ( Pseudo_isGeneralInfo( msgId ) )
+        putTxtBuf( Pseudo_generalInfoBody() );
+    else
+        doBodyInDb( msgId );
+    putEndOfTxt();
+    noteInterest();
+    return TRUE;
+}
+
+static void
+doHeadInDb( const char *msgId )
+{
+    putTxtBuf( Db_header( msgId ) );
+}
+
+static Bool
+doHead( char *arg, const Cmd *cmd )
+{
+    const char *msgId;
+    int numb;
+    
+    if ( ! whichId( &msgId, &numb, arg ) )
+        return TRUE;
+    putStat( STAT_HEAD_FOLLOWS, "%ld %s Head", numb, msgId );
+    if ( Pseudo_isGeneralInfo( msgId ) )
+        putTxtBuf( Pseudo_generalInfoHead() );
+    else
+        doHeadInDb( msgId );
+    putEndOfTxt();
+    return TRUE;
+}
+
+static void
+doArtInDb( const char *msgId )
+{
+    doHeadInDb( msgId );
+    putTxtLn( "" );
+    doBodyInDb( msgId );
+}
+
+static Bool
+doArt( char *arg, const Cmd *cmd )
+{
+    const char *msgId;
+    int numb;
+    
+    if ( ! whichId( &msgId, &numb, arg ) )
+        return TRUE;
+    putStat( STAT_ART_FOLLOWS, "%ld %s Article", numb, msgId );
+    if ( Pseudo_isGeneralInfo( msgId ) )
+    {
+        putTxtBuf( Pseudo_generalInfoHead() );
+        putTxtLn( "" );
+        putTxtBuf( Pseudo_generalInfoBody() );
+    }
+    else
+        doArtInDb( msgId );
+    putEndOfTxt();
+    noteInterest();
+    return TRUE;
+}
+
+static Bool
+doHelp( char *arg, const Cmd *cmd )
+{
+    unsigned int i;
+
+    putStat( STAT_HELP_FOLLOWS, "Help" );
+    putTxtBuf( "\nCommands:\n\n" );
+    for ( i = 0; i < sizeof( commands ) / sizeof( commands[ 0 ] ); ++i )
+        putTxtLn( "%s", commands[ i ].syntax );
+    putEndOfTxt();
+    return TRUE;
+}
+
+static Bool
+doIhave( char *arg, const Cmd *cmd )
+{
+    putStat( STAT_ART_REJECTED, "Command not used" );
+    return TRUE;
+}
+
+static Bool
+doLast( char *arg, const Cmd *cmd )
+{
+    int n;
+
+    if ( testGrpSelected() )
+    {
+        n = serv.artPtr;
+        if ( ! Cont_validNumb( n ) )
+            putStat( STAT_NO_ART_SELECTED, "No article selected" );
+        else
+        {
+            while ( ! Cont_validNumb( --n ) && n >= Cont_first() );
+            if ( ! Cont_validNumb( n ) )
+                putStat( STAT_NO_PREV_ART, "No previous article" );
+            else
+            {
+                putStat( STAT_ART_RETRIEVED, "%ld %s selected",
+                         n, Ov_msgId( Cont_get( n ) ) );
+                serv.artPtr = n;
+            }
+        }
+    }
+    return TRUE;
+}
+
+static void
+printGroups( const char *pat, void (*printProc)( Str, const char* ) )
+{
+    Str line;
+    const char *g;
+    FILE *f;
+    sig_t lastHandler;
+    int ret;
+
+    putStat( STAT_GRPS_FOLLOW, "Groups" );
+    fflush( stdout );
+    Log_dbg( "[S FLUSH]" );
+    if ( Grp_exists( pat ) )
+    {
+        (*printProc)( line, pat );
+        if ( ! Prt_putTxtLn( line, stdout ) )
+            Log_err( "Writing to stdout failed." );
+    }                    
+    else
+    {
+        lastHandler = signal( SIGPIPE, SIG_IGN );
+        f = popen( "sort", "w" );
+        if ( f == NULL )
+        {
+            Log_err( "Cannot open pipe to 'sort'" );
+            if ( Grp_firstGrp( &g ) )
+                do
+                    if ( Utl_matchPattern( g, pat ) )
+                    {
+                        (*printProc)( line, g );
+                        if ( ! Prt_putTxtLn( line, stdout ) )
+                            Log_err( "Writing to stdout failed." );
+                    }
+                while ( Grp_nextGrp( &g ) );
+        }
+        else
+        {
+            if ( Grp_firstGrp( &g ) )
+                do
+                    if ( Utl_matchPattern( g, pat ) )
+                    {
+                        (*printProc)( line, g );
+                        if ( ! Prt_putTxtLn( line, f ) )
+                        {
+                            Log_err( "Writing to 'sort' pipe failed." );
+                            break;
+                        }                    
+                    }
+                while ( Grp_nextGrp( &g ) );
+            ret = pclose( f );
+            if ( ret != EXIT_SUCCESS )
+                Log_err( "sort command returned %i", ret );
+            fflush( stdout );
+            Log_dbg( "[S FLUSH]" );
+            signal( SIGPIPE, lastHandler );
+        }
+    }
+    putEndOfTxt();
+}
+
+static void
+printActiveTimes( Str result, const char *grp )
+{
+    snprintf( result, MAXCHAR, "%s %ld", grp, Grp_created( grp ) );
+}
+
+static void
+doListActiveTimes( const char *pat )
+{
+    printGroups( pat, &printActiveTimes );
+}
+
+static void
+printActive( Str result, const char *grp )
+{
+    snprintf( result, MAXCHAR, "%s %i %i y",
+              grp, Grp_last( grp ), Grp_first( grp ) );
+}
+
+static void
+doListActive( const char *pat )
+{
+    printGroups( pat, &printActive );
+}
+
+static void
+printNewsgrp( Str result, const char *grp )
+{
+    snprintf( result, MAXCHAR, "%s %s", grp, Grp_dsc( grp ) );
+}
+
+static void
+doListNewsgrps( const char *pat )
+{
+    printGroups( pat, &printNewsgrp );
+}
+
+static void
+putGrp( const char *name )
+{
+    putTxtLn( "%s %lu %lu y", name, Grp_last( name ), Grp_first( name ) );
+}
+
+static void
+doListOverFmt( void )
+{
+    putStat( STAT_GRPS_FOLLOW, "Overview format" );
+    putTxtBuf( "Subject:\n"
+               "From:\n"
+               "Date:\n"
+               "Message-ID:\n"
+               "References:\n"
+               "Bytes:\n"
+               "Lines:\n" );
+    putEndOfTxt();
+}
+
+static void
+doListExtensions( void )
+{
+    putStat( STAT_CMD_OK, "Extensions" );
+    putTxtBuf( " LISTGROUP\n"
+               " XOVER\n" );
+    putEndOfTxt();    
+}
+
+static Bool
+doList( char *line, const Cmd *cmd )
+{
+    Str s, arg;
+    const char *pat;
+
+    if ( sscanf( line, "%s", s ) != 1 )
+        doListActive( "*" );
+    else
+    {
+        Utl_toLower( s );
+        strcpy( arg, Utl_restOfLn( line, 1 ) );
+        pat = Utl_stripWhiteSpace( arg );
+        if ( pat[ 0 ] == '\0' )
+            pat = "*";
+        if ( strcmp( "active", s ) == 0 )
+            doListActive( pat );
+        else if ( strcmp( "overview.fmt", s ) == 0 )
+            doListOverFmt();
+        else if ( strcmp( "newsgroups", s ) == 0 )
+            doListNewsgrps( pat );
+        else if ( strcmp( "active.times", s ) == 0 )
+            doListActiveTimes( pat );
+        else if ( strcmp( "extensions", s ) == 0 )
+            doListExtensions();
+        else
+            putSyntax( cmd );
+    }
+    return TRUE;
+}
+
+static Bool
+doListgrp( char *arg, const Cmd *cmd )
+{
+    const Over *ov;
+    int first, last, i;
+
+    if ( ! Grp_exists( arg ) )
+        putStat( STAT_NO_SUCH_GRP, "No such group" );
+    else
+    {
+        changeToGrp( arg );
+        first = Cont_first();
+        last = Cont_last();
+        putStat( STAT_GRP_SELECTED, "Article list" );
+        for ( i = first; i <= last; ++i )
+            if ( ( ov = Cont_get( i ) ) )
+                putTxtLn( "%lu", i );
+        putEndOfTxt();
+    }
+    return TRUE;
+}
+
+static Bool
+doMode( char *arg, const Cmd *cmd )
+{
+    putStat( STAT_READY_POST_ALLOW, "Ok" );
+    return TRUE;
+}
+
+static unsigned long
+getTimeInSeconds( unsigned int year, unsigned int mon, unsigned int day,
+                  unsigned int hour, unsigned int min, unsigned int sec )
+{
+    struct tm t = { 0 };
+
+    t.tm_year = year - 1900;
+    t.tm_mon = mon - 1;
+    t.tm_mday = day;
+    t.tm_hour = hour;
+    t.tm_min = min;
+    t.tm_sec = sec;
+    return mktime( &t );
+}
+
+
+static Bool
+doNewgrps( char *arg, const Cmd *cmd )
+{
+    time_t t, now, lastUpdate;
+    unsigned int year, mon, day, hour, min, sec, cent, len;
+    const char *g;
+    Str date, timeofday, file;
+
+    if ( sscanf( arg, "%s %s", date, timeofday ) != 2 ) 
+    {
+        putSyntax( cmd );
+        return TRUE;
+    }
+    len = strlen( date );
+    switch ( len )
+    {
+    case 6:
+        if ( sscanf( date, "%2u%2u%2u", &year, &mon, &day ) != 3 )
+        {
+            putSyntax( cmd );
+            return TRUE;
+        }
+        now = time( NULL );
+        cent = 1900;
+        while ( now > getTimeInSeconds( cent + 100, 1, 1, 0, 0, 0 ) )
+            cent += 100;
+        year += cent;
+        break;
+    case 8:
+        if ( sscanf( date, "%4u%2u%2u", &year, &mon, &day ) != 3 )
+        {
+            putSyntax( cmd );
+            return TRUE;
+        }
+        break;
+    default:
+        putSyntax( cmd );
+        return TRUE;
+    }
+    if ( sscanf( timeofday, "%2u%2u%2u", &hour, &min, &sec ) != 3 )
+    {
+        putSyntax( cmd );
+        return TRUE;
+    }
+    if ( year < 1970 || mon == 0 || mon > 12 || day == 0 || day > 31
+         || hour > 23 || min > 59 || sec > 60 )
+    {
+        putSyntax( cmd );
+        return TRUE;
+    }
+    snprintf( file, MAXCHAR, "%s/groupinfo.lastupdate", Cfg_spoolDir() );
+    t = getTimeInSeconds( year, mon, day, hour, min, sec );
+    putStat( STAT_NEW_GRP_FOLLOW, "New groups since %s", arg );
+
+    if ( ! Utl_getStamp( &lastUpdate, file ) || t <= lastUpdate )
+    {
+        if ( Grp_firstGrp( &g ) )
+            do
+                if ( Grp_created( g ) > t )
+                    putGrp( g );
+            while ( Grp_nextGrp( &g ) );
+    }
+    putEndOfTxt();
+    return TRUE;
+}
+
+static Bool
+doNext( char *arg, const Cmd *cmd )
+{
+    int n;
+
+    if ( testGrpSelected() )
+    {
+        n = serv.artPtr;
+        if ( ! Cont_validNumb( n ) )
+            putStat( STAT_NO_ART_SELECTED, "No article selected" );
+        else
+        {
+            while ( ! Cont_validNumb( ++n ) && n <= Cont_last() );
+            if ( ! Cont_validNumb( n ) )
+                putStat( STAT_NO_NEXT_ART, "No next article" );
+            else
+            {
+                putStat( STAT_ART_RETRIEVED, "%ld %s selected",
+                         n, Ov_msgId( Cont_get( n ) ) );
+                serv.artPtr = n;
+            }
+        }
+    }
+    return TRUE;
+}
+
+/*
+  Get first group of the Newsgroups field content, which is
+  a comma separated list of groups.
+*/
+static void
+getFirstGrp( char *grpResult, const char *list )
+{
+    Str t;
+    const char *src = list;
+    char *dest = t;
+    while( TRUE )
+    {
+        if ( *src == ',' )
+            *dest = ' ';
+        else
+            *dest = *src;
+        if ( *src == '\0' )
+            break;
+        ++src;
+        ++dest;
+    }
+    *grpResult = '\0';
+    sscanf( t, "%s", grpResult );
+}
+
+static Bool
+doPost( char *arg, const Cmd *cmd )
+{
+    Bool err, replyToFound, inHeader;
+    DynStr *s;
+    Str line, field, val, msgId, from, grp;
+    const char* p;
+
+    /*
+      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)
+    */
+    putStat( STAT_SEND_ART, "Continue (end with period)" );
+    fflush( stdout );
+    Log_dbg( "[S FLUSH]" );
+    s = new_DynStr( 10000 );
+    msgId[ 0 ] = '\0';
+    from[ 0 ] = '\0';
+    grp[ 0 ] = '\0';
+    replyToFound = 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_removeMsgId() )
+                {
+                    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 );
+                }
+                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 )
+                {
+                    getFirstGrp( grp, val );
+                    Utl_toLower( grp );
+                    DynStr_appLn( s, p );
+                }
+                else if ( strcmp( field, "reply-to" ) == 0 )
+                {
+                    replyToFound = 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
+                Log_err( "Ignoring invalid header line '%s'", p );
+        }
+        else
+            DynStr_appLn( s, line );
+    }
+    if ( inHeader )
+        Log_err( "Posted message has no body" );
+    if ( ! err )
+    {
+        if ( grp[ 0 ] == '\0' )
+        {
+            Log_err( "Posted message has no 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;
+        }
+    }
+    if ( err )
+        putStat( STAT_POST_FAILED, "Posting failed" );
+    else
+    {
+        putStat( STAT_POST_OK, "Message queued for posting" );
+        if ( Online_true() )
+            postArts();
+    }
+    del_DynStr( s );
+    return TRUE;
+}
+
+static void
+parseRange( const char *s, int *first, int *last, int *numb )
+{
+    int r, i;
+    char* p;
+    Str t;
+
+    Utl_cpyStr( t, s );
+    p = Utl_stripWhiteSpace( t );
+    r = sscanf( p, "%i-%i", first, last );
+    if ( r < 1 )
+    {
+        *first = serv.artPtr;
+        *last = serv.artPtr;
+    }
+    else if ( r == 1 )
+    {
+        if ( p[ strlen( p ) - 1 ] == '-' )
+            *last = Cont_last();
+        else
+            *last = *first;
+    }    
+    if ( *first < Cont_first() )
+        *first = Cont_first();
+    if ( *last > Cont_last() )
+        *last = Cont_last();
+    if ( *first > Cont_last() ||  *last < Cont_first() )
+        *last = *first - 1;
+    *numb = 0;
+    for ( i = *first; i <= *last; ++i )
+        if ( Cont_validNumb( i ) )
+            ++(*numb);
+}
+
+static Bool
+doXhdr( char *arg, const Cmd *cmd )
+{
+    int first, last, i, n, numb;
+    enum { SUBJ, FROM, DATE, MSG_ID, REF, BYTES, LINES } what;
+    const char *p;
+    const Over *ov;
+    Str whatStr;
+
+    if ( ! testGrpSelected() )
+        return TRUE;
+    if ( sscanf( arg, "%s", whatStr ) != 1 )
+    {
+        putSyntax( cmd );
+        return TRUE;
+    }
+    Utl_toLower( whatStr );
+    if ( strcmp( whatStr, "subject" ) == 0 )
+        what = SUBJ;
+    else if ( strcmp( whatStr, "from" ) == 0 )
+        what = FROM;
+    else if ( strcmp( whatStr, "date" ) == 0 )
+        what = DATE;
+    else if ( strcmp( whatStr, "message-id" ) == 0 )
+        what = MSG_ID;
+    else if ( strcmp( whatStr, "references" ) == 0 )
+        what = REF;
+    else if ( strcmp( whatStr, "bytes" ) == 0 )
+        what = BYTES;
+    else if ( strcmp( whatStr, "lines" ) == 0 )
+        what = LINES;
+    else
+    {
+        putStat( STAT_HEAD_FOLLOWS, "Unknown header (empty list follows)" );
+        putEndOfTxt();
+        return TRUE;
+    }
+    p = Utl_restOfLn( arg, 1 );
+    parseRange( p, &first, &last, &numb );
+    if ( numb == 0 )
+        putStat( STAT_NO_ART_SELECTED, "No articles selected" );
+    else
+    {
+        putStat( STAT_HEAD_FOLLOWS, "%s header %lu-%lu",
+                 whatStr, first, last ) ;
+        for ( i = first; i <= last; ++i )
+            if ( ( ov = Cont_get( i ) ) )
+            {
+                n = Ov_numb( ov );
+                switch ( what )
+                {
+                case SUBJ:
+                    putTxtLn( "%lu %s", n, Ov_subj( ov ) );
+                    break;
+                case FROM:
+                    putTxtLn( "%lu %s", n, Ov_from( ov ) );
+                    break;
+                case DATE:
+                    putTxtLn( "%lu %s", n, Ov_date( ov ) );
+                    break;
+                case MSG_ID:
+                    putTxtLn( "%lu %s", n, Ov_msgId( ov ) );
+                    break;
+                case REF:
+                    putTxtLn( "%lu %s", n, Ov_ref( ov ) );
+                    break;
+                case BYTES:
+                    putTxtLn( "%lu %d", n, Ov_bytes( ov ) );
+                    break;
+                case LINES:
+                    putTxtLn( "%lu %d", n, Ov_lines( ov ) );
+                    break;
+                default:
+                    ASSERT( FALSE );
+                }
+            }
+        putEndOfTxt();
+    }
+    return TRUE;
+}
+
+static Bool
+doXpat( char *arg, const Cmd *cmd )
+{
+    int first, last, i, n;
+    enum { SUBJ, FROM, DATE, MSG_ID, REF } what;
+    const Over *ov;
+    Str whatStr, pat;
+
+    if ( ! testGrpSelected() )
+        return TRUE;
+    if ( sscanf( arg, "%s %i-%i %s", whatStr, &first, &last, pat ) != 4 )
+    {
+        if ( sscanf( arg, "%s %i- %s", whatStr, &first, pat ) == 3 )
+            last = Cont_last();
+        else if ( sscanf( arg, "%s %i %s", whatStr, &first, pat ) == 3 )
+            last = first;
+        else
+        {
+            putSyntax( cmd );
+            return TRUE;
+        }
+    }
+    Utl_toLower( whatStr );
+    if ( strcmp( whatStr, "subject" ) == 0 )
+        what = SUBJ;
+    else if ( strcmp( whatStr, "from" ) == 0 )
+        what = FROM;
+    else if ( strcmp( whatStr, "date" ) == 0 )
+        what = DATE;
+    else if ( strcmp( whatStr, "message-id" ) == 0 )
+        what = MSG_ID;
+    else if ( strcmp( whatStr, "references" ) == 0 )
+        what = REF;
+    else
+    {
+        putStat( STAT_HEAD_FOLLOWS, "invalid header (empty list follows)" );
+        putEndOfTxt();
+        return TRUE;
+    }
+    putStat( STAT_HEAD_FOLLOWS, "header" ) ;
+    for ( i = first; i <= last; ++i )
+        if ( ( ov = Cont_get( i ) ) )
+        {
+            n = Ov_numb( ov );
+            switch ( what )
+            {
+            case SUBJ:
+                if ( Utl_matchPattern( Ov_subj( ov ), pat ) )
+                     putTxtLn( "%lu %s", n, Ov_subj( ov ) );
+                break;
+            case FROM:
+                if ( Utl_matchPattern( Ov_from( ov ), pat ) )
+                    putTxtLn( "%lu %s", n, Ov_from( ov ) );
+                break;
+            case DATE:
+                if ( Utl_matchPattern( Ov_date( ov ), pat ) )
+                    putTxtLn( "%lu %s", n, Ov_date( ov ) );
+                break;
+            case MSG_ID:
+                if ( Utl_matchPattern( Ov_msgId( ov ), pat ) )
+                    putTxtLn( "%lu %s", n, Ov_msgId( ov ) );
+                break;
+            case REF:
+                if ( Utl_matchPattern( Ov_ref( ov ), pat ) )
+                    putTxtLn( "%lu %s", n, Ov_ref( ov ) );
+                break;
+            default:
+                ASSERT( FALSE );
+            }
+        }
+    putEndOfTxt();
+    return TRUE;
+}
+
+static Bool
+doSlave( char *arg, const Cmd *cmd )
+{
+    putStat( STAT_CMD_OK, "Ok" );
+    return TRUE;
+}
+
+static Bool
+doStat( char *arg, const Cmd *cmd )
+{
+    const char *msgId;
+    int numb;
+    
+    if ( ! whichId( &msgId, &numb, arg ) )
+        return TRUE;
+    if ( numb > 0 )
+        putStat( STAT_ART_RETRIEVED, "%ld %s selected",
+                 numb, msgId );
+    else
+        putStat( STAT_ART_RETRIEVED, "0 %s selected", msgId );
+    return TRUE;
+}
+
+static Bool
+doQuit( char *arg, const Cmd *cmd )
+{
+    putStat( STAT_GOODBYE, "Goodbye" );
+    return FALSE;
+}
+
+static Bool
+doXOver( char *arg, const Cmd *cmd )
+{
+    int first, last, i, n;
+    const Over *ov;
+
+    if ( ! testGrpSelected() )
+        return TRUE;
+    parseRange( arg, &first, &last, &n );
+    if ( n == 0 )
+        putStat( STAT_NO_ART_SELECTED, "No articles selected" );
+    else
+    {
+        putStat( STAT_OVERS_FOLLOW, "Overview %ld-%ld", first, last );
+        for ( i = first; i <= last; ++i )
+            if ( ( ov = Cont_get( i ) ) )
+                putTxtLn( "%lu\t%s\t%s\t%s\t%s\t%s\t%d\t%d\t",
+                          Ov_numb( ov ), Ov_subj( ov ), Ov_from( ov ),
+                          Ov_date( ov ), Ov_msgId( ov ), Ov_ref( ov ),
+                          Ov_bytes( ov ), Ov_lines( ov ) );
+        putEndOfTxt();
+    }
+    return TRUE;
+}
+
+static void
+putFatal( const char *fmt, ... )
+{
+    va_list ap;
+    Str s;
+
+    va_start( ap, fmt );
+    vsnprintf( s, MAXCHAR, fmt, ap );
+    va_end( ap );
+    Log_err( s );
+    putStat( STAT_PROGRAM_FAULT, "%s", s );
+    fflush( stdout );
+    Log_dbg( "[S FLUSH]" );
+}
+
+/* Parse line, execute command and return FALSE, if it was the quit command. */
+static Bool
+parseAndExecute( Str line )
+{
+    unsigned int i, n;
+    Cmd *c;
+    Str s, arg;
+    Bool ret;
+
+    if ( sscanf( line, "%s", s ) == 1 )
+    {
+        Utl_toLower( s );
+        strcpy( arg, Utl_restOfLn( line, 1 ) );
+        n = sizeof( commands ) / sizeof( commands[ 0 ] );
+        for ( i = 0, c = commands; i < n; ++i, ++c )
+            if ( strcmp( c->name, s ) == 0 )
+            {
+                ret = c->cmdProc( Utl_stripWhiteSpace( arg ), c );
+                fflush( stdout );
+                Log_dbg( "[S FLUSH]" );
+                return ret;
+            }
+    }
+    putStat( STAT_NO_SUCH_CMD, "Command not recognized" );
+    fflush( stdout );
+    Log_dbg( "[S FLUSH]" );
+    return TRUE;
+}
+
+static void
+putWelcome( void )
+{
+    putStat( STAT_READY_POST_ALLOW, "NNTP server NOFFLE %s",
+             Cfg_version() );
+    fflush( stdout );
+    Log_dbg( "[S FLUSH]" );
+}
+
+static Bool
+initServ( void )
+{
+    ASSERT( ! serv.running );
+    if ( ! Lock_openDatabases() )
+      return FALSE;
+    serv.running = TRUE;
+    return TRUE;
+}
+
+static void
+closeServ( void )
+{
+    ASSERT( serv.running );
+    serv.running = FALSE;
+    Lock_closeDatabases();
+}
+
+void
+Serv_run( void )
+{
+    Bool done;
+    int r;
+    Str line;
+    struct timeval timeOut;
+    fd_set readSet;
+
+    putWelcome();
+    done = FALSE;
+    while ( ! done )
+    {
+        FD_ZERO( &readSet );
+        FD_SET( STDIN_FILENO, &readSet );
+        /* Never hold lock more than 5 seconds (empirically good value,
+           avoids to close/open databases, if clients sends several
+           commands, but releases the lock often enough, for allowing
+           multiple persons to read news at the same time) */
+        timeOut.tv_sec = 5;
+        timeOut.tv_usec = 0;
+        r = select( STDIN_FILENO + 1, &readSet, NULL, NULL, &timeOut );
+        if ( r < 0 )
+            done = TRUE;
+        else if ( r == 0 )
+        {
+            if ( serv.running )
+                closeServ();
+        }
+        else /* ( r > 0 ) */
+        {
+            if ( ! serv.running )
+            {
+                if ( ! initServ() )
+                {
+                    putFatal( "Cannot init server" );
+                    done = TRUE;
+                }
+            }
+            if ( ! getLn( line ) )
+            {
+                Log_inf( "Client disconnected. Terminating." );
+                done = TRUE;
+            }
+            else if ( ! parseAndExecute( line ) )
+                done = TRUE;
+        }
+    }
+    if ( serv.running )
+        closeServ();
+}