view 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 source

/*
  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();
}