view src/client.c @ 43:2842f50feb55 noffle

[svn] * client.c, client.h, common.h, config.c, config.h, content.c, content.h, control.c, control.h, database.c, database.h, dynamicstring.c, dynamicstring.h, fetch.c, fetch.h, fetchlist.c, fetchlist.h, group.c, group.h, itemlist.c, itemlist.h, lock.c, lock.h, log.c, log.h, noffle.c, online.c, online.h, outgoing.c, outgoing.h, over.c, over.h, post.c, post.h, protocol.c, protocol.h, pseudo.c, pseudo.h, request.c, request.h, server.c, server.h, util.c, util.h, wildmat.c, wildmat.h: Moved files to the subdirectory src/ * Makefile.am, acconfig.h, configure.in, docs/Makefile.am, src/Makefile.am, Makefile.in, aclocal.m4, config.h.in, configure, install-sh, missing, mkinstalldirs, stamp-h.in, docs/Makefile.in, src/Makefile.in: Added files. They are used by aclocal, autoheader, autoconf and automake. * src/config.c, src/config.h: Renamed to configfile.c and configfile.h, because configure will generate a config.h file itself. * src/client.c, src/content.c, src/database.c, src/fetch.c, src/fetchlist.c, src/group.c, src/lock.c, src/noffle.c, src/online.c, src/outgoing.c, src/over.c, src/pseudo.c, src/request.c, src/server.c, src/util.c: Changed '#include "config.h"' to '#include "configfile.h"'. * src/client.c, src/content.c, src/database.c, src/fetch.c, src/fetchlist.c, src/group.c, src/lock.c, src/online.c, src/outgoing.c, src/post.c, src/protocol.c, src/request.c, src/server.c: Files now #include <config.h>. Added missing <stdio.h>. This removes the warnings about snprintf() not being declared. * Makefile: Removed. This is now generated by configure.
author uh1763
date Fri, 05 May 2000 22:45:56 +0100
parents
children 21d3102dbc37
line wrap: on
line source

/*
  client.c

  $Id: client.c 49 2000-05-05 21:45:56Z uh1763 $
*/

#if HAVE_CONFIG_H
#include <config.h>
#endif

#include "client.h"

#include <stdio.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <netdb.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdarg.h>
#include <sys/socket.h>
#include <unistd.h>
#include "configfile.h"
#include "content.h"
#include "dynamicstring.h"
#include "group.h"
#include "log.h"
#include "over.h"
#include "protocol.h"
#include "pseudo.h"
#include "request.h"
#include "util.h"
#include "wildmat.h"

/*
  Some newsgroups names are reserved for server-specific or server
  pseudo groups. We don't want to fetch them. For example, INN
  keeps all its control messages in a 'control' hierarchy, and
  used the "to." heirarchy for dark and mysterious purposes I think
  are to do with newsfeeds. The recommended restrictions are documented
  in C.Lindsay, "News Article Format", <draft-ietf-usefor-article-03.txt>.
*/

struct ForbiddenGroupName
{
    const char *pattern;
    Bool match;
} forbiddenGroupNames[] =
{
    { "*.*", FALSE },			/* Single component */
    { "control.*", TRUE },		/* control.* groups */
    { "to.*", TRUE },			/* control.* groups */
    { "*.all", TRUE },			/* 'all' as a component */
    { "*.all.*", TRUE },
    { "all.*", TRUE },
    { "*.ctl", TRUE },			/* 'ctl' as a component */
    { "*.ctl.*", TRUE },
    { "ctl.*", TRUE }
};

struct
{
    FILE* in;     /* Receiving socket from server */
    FILE* out;    /* Sending socket to server */
    Str lastCmd;  /* Last command line */
    Str lastStat; /* Response from server to last command */
    Str grp;      /* Selected group */
    int rmtFirst;  /* First article of current group at server */
    int rmtLast;   /* Last article of current group at server */
    Bool auth;    /* Authetication already done? */
    Str serv;     /* Remote server name */
} client = { NULL, NULL, "", "", "", 1, 0, FALSE, "" };

static void
logBreakDown( void )
{
    Log_err( "Connection to remote server lost "
             "(article numbers could be inconsistent)" );
}

static Bool
getLn( Str line )
{
    Bool r;

    r = Prt_getLn( line, client.in );
    if ( ! r )
        logBreakDown();
    return r; 
}

static Bool
getTxtLn( Str line, Bool *err )
{
    Bool r;

    r = Prt_getTxtLn( line, err, client.in );
    if ( *err )
        logBreakDown();
    return r; 
}

static void
putTxtBuf( const char *buf )
{
    Prt_putTxtBuf( buf, client.out );
    fflush( client.out );
    Log_dbg( "[S FLUSH]" );
}

static void
putEndOfTxt( void )
{
    Prt_putEndOfTxt( client.out );
    fflush( client.out );
    Log_dbg( "[S FLUSH]" );
}

static Bool
putCmd( const char *fmt, ... )
{
    Bool err;
    unsigned int n;
    Str line;
    va_list ap;

    va_start( ap, fmt );
    vsnprintf( line, MAXCHAR, fmt, ap );
    va_end( ap );
    strcpy( client.lastCmd, line );
    Log_dbg( "[S] %s", line );
    n = fprintf( client.out, "%s\r\n", line );
    fflush( client.out );
    Log_dbg( "[S FLUSH]" );
    err = ( n != strlen( line ) + 2 );
    if ( err )
        logBreakDown();;
    return ! err;
}

static Bool
putCmdNoFlush( const char *fmt, ... )
{
    Bool err;
    unsigned int n;
    Str line;
    va_list ap;

    va_start( ap, fmt );
    vsnprintf( line, MAXCHAR, fmt, ap );
    va_end( ap );
    strcpy( client.lastCmd, line );
    Log_dbg( "[S] %s", line );
    n = fprintf( client.out, "%s\r\n", line );
    err = ( n != strlen( line ) + 2 );
    if ( err )
        logBreakDown();;
    return ! err;
}

static int getStat( void );

static Bool
performAuth( void )
{
    int stat;
    Str user, pass;
    
    Cfg_authInfo( client.serv, user, pass );
    if ( strcmp( user, "" ) == 0 )
    {
        Log_err( "No username for authentication set" );
        return FALSE;
    }    
    putCmd( "AUTHINFO USER %s", user );
    stat = getStat();
    if ( stat == STAT_AUTH_ACCEPTED )
        return TRUE;
    else if ( stat != STAT_MORE_AUTH_REQUIRED )
    {
        Log_err( "Username rejected. Server stat: %s", client.lastStat );
        return FALSE;
    }    
    if ( strcmp( pass, "" ) == 0 )
    {
        Log_err( "No password for authentication set" );
        return FALSE;
    }
    putCmd( "AUTHINFO PASS %s", pass );
    stat = getStat();
    if ( stat != STAT_AUTH_ACCEPTED )
    {
        Log_err( "Password rejected. Server status: %s", client.lastStat );
        return FALSE;
    }    
    return TRUE;    
}

static int
getStat( void )
{
    int result;
    Str lastCmd;

    if ( ! getLn( client.lastStat ) )
        result = STAT_PROGRAM_FAULT;
    else if ( sscanf( client.lastStat, "%d", &result ) != 1 )
    {
        Log_err( "Invalid server status: %s", client.lastStat );
        result = STAT_PROGRAM_FAULT;
    }
    if ( result == STAT_AUTH_REQUIRED && ! client.auth )
    {
        client.auth = TRUE;
        strcpy( lastCmd, client.lastCmd );
        if ( performAuth() )
        {
            putCmd( lastCmd );
            return getStat();
        }
    }
    return result;
}

static void
connectAlarm( int sig )
{
    return;
}

static sig_t
installSignalHandler( int sig, sig_t handler )
{
    struct sigaction act, oldAct;

    act.sa_handler = handler;
    sigemptyset( &act.sa_mask );
    act.sa_flags = 0;
    if ( sig == SIGALRM )
        act.sa_flags |= SA_INTERRUPT;
    else
        act.sa_flags |= SA_RESTART;
    if ( sigaction( sig, &act, &oldAct ) < 0 )
        return SIG_ERR;
    return oldAct.sa_handler;
}

static Bool
connectWithTimeout( int sock, const struct sockaddr *servAddr,
                    socklen_t addrLen )
{
    sig_t oldHandler;
    int r, to;

    oldHandler = installSignalHandler( SIGALRM, connectAlarm );
    if ( oldHandler == SIG_ERR )
    {
        Log_err( "client.c:connectWithTimeout: signal failed." );
        return FALSE;
    }
    to = Cfg_connectTimeout();
    if ( alarm( to ) != 0 )
        Log_err( "client.c:connectWithTimeout: Alarm was already set." );
    r = connect( sock, servAddr, addrLen );
    alarm( 0 );
    installSignalHandler( SIGALRM, oldHandler );
    return ( r >= 0 );
}

Bool
Client_connect( const char *serv )
{
    unsigned short int port;
    int sock, i;
    unsigned int stat;
    struct hostent *hp;
    char *pStart, *pColon;
    Str host, s;
    struct sockaddr_in sIn;

    client.auth = FALSE;
    Utl_cpyStr( s, serv );
    pStart = Utl_stripWhiteSpace( s );
    pColon = strstr( pStart, ":" );
    if ( pColon == NULL )
    {
        strcpy( host, pStart );
        port = 119;
    }
    else
    {
        *pColon = '\0';
        strcpy( host, pStart );
        if ( sscanf( pColon + 1, "%hi", &port ) != 1 )
        {
            Log_err( "Syntax error in server name: '%s'", serv );
            return FALSE;;
        }
        if ( port <= 0 || port > 65535 )
        {
            Log_err( "Invalid port number %hi. Must be in [1, 65535]", port );
            return FALSE;;
        }
    }
    memset( (void *)&sIn, 0, sizeof( sIn ) );
    hp = gethostbyname( host );
    if ( hp )
    {
        for ( i = 0; (hp->h_addr_list)[ i ]; ++i )
        {
            sIn.sin_family = hp->h_addrtype;
            sIn.sin_port = htons( port );
            sIn.sin_addr = *( (struct in_addr *)hp->h_addr_list[ i ] );
            sock = socket( AF_INET, SOCK_STREAM, 0 );
            if ( sock < 0 )
                break;
            if ( ! connectWithTimeout( sock, (struct sockaddr *)&sIn,
                                       sizeof( sIn ) ) )
            {
                close( sock );
                break;
            }
            if ( ! ( client.out = fdopen( sock, "w" ) )
                 || ! ( client.in  = fdopen( dup( sock ), "r" ) ) )
            {
		if ( client.out != NULL )
		    fclose( client.out );
                close( sock );
                break;
            }
            stat = getStat();
	    if ( stat == STAT_READY_POST_ALLOW ||
		 stat == STAT_READY_NO_POST_ALLOW )
	    {
		/* INN needs a MODE READER before it will permit POST. */
		putCmd( "MODE READER" );
		stat = getStat();
	    }
            switch( stat ) {
            case STAT_READY_POST_ALLOW:
            case STAT_READY_NO_POST_ALLOW: 
                Log_inf( "Connected to %s:%d",
                         inet_ntoa( sIn.sin_addr ), port );
                Utl_cpyStr( client.serv, serv );
                return TRUE;
            default:
                Log_err( "Bad server stat %d", stat ); 
            }
            shutdown( fileno( client.out ), 0 );
	    fclose( client.in );
	    fclose( client.out );
	    close( sock );
        }
    }
    return FALSE;
}

static Bool
isForbiddenGroupName( const char *name )
{
    int i;

    for ( i = 0;
	  i < sizeof( forbiddenGroupNames ) /
	      sizeof( struct ForbiddenGroupName );
	  i++ )
    {
	/* Negate result of Wld_match to ensure it is 1 or 0. */
	if ( forbiddenGroupNames[i].match !=
	     ( ! Wld_match( name, forbiddenGroupNames[i].pattern ) ) )
	    return TRUE;
    }

    return FALSE;
}

static void
processGrps( void )
{
    char postAllow;
    Bool err;
    int first, last;
    Str grp, line, file;
    
    while ( getTxtLn( line, &err ) && ! err )
    {
        if ( sscanf( line, "%s %d %d %c",
                     grp, &last, &first, &postAllow ) != 4 )
        {
            Log_err( "Unknown reply to LIST or NEWGROUPS: %s", line );
            continue;
        }
	if ( isForbiddenGroupName( grp ) )
	{
	    Log_inf( "Group %s forbidden", grp );
	    continue;
	}
        if ( ! Grp_exists( grp ) )
        {
            Log_inf( "Registering new group '%s'", grp );
            Grp_create( grp );
            Grp_setRmtNext( grp, first );
            Grp_setServ( grp, client.serv );
	    Grp_setPostAllow( grp, postAllow );
        }
        else
        {
            if ( Cfg_servIsPreferential( client.serv, Grp_serv( grp ) ) )
            {
                Log_inf( "Changing server for '%s': '%s'->'%s'",
                         grp, Grp_serv( grp ), client.serv );
                Grp_setServ( grp, client.serv );
                Grp_setRmtNext( grp, first );
		Grp_setPostAllow( grp, postAllow );
            }
            else
                Log_dbg( "Group %s is already fetched from %s",
                           grp, Grp_serv( grp ) );
            
        }
    }
    if ( ! err )
    {
        snprintf( file, MAXCHAR, "%s/groupinfo.lastupdate", Cfg_spoolDir() );
        Utl_stamp( file );
    }
}

void
Client_disconnect( void )
{
    if ( putCmd( "QUIT" ) )
        getStat();
    fclose( client.in );
    fclose( client.out );
    client.in = client.out = NULL;
}

Bool
Client_getGrps( void )
{
    if ( ! putCmd( "LIST ACTIVE" ) )
        return FALSE;
    if ( getStat() != STAT_GRPS_FOLLOW )
    {
        Log_err( "LIST ACTIVE command failed: %s", client.lastStat );
        return FALSE;
    }
    processGrps();
    return TRUE;
}

Bool
Client_getDsc( void )
{
    Bool err;
    Str name, line, dsc;

    Log_inf( "Querying group descriptions" );
    if ( ! putCmd( "LIST NEWSGROUPS" ) )
        return FALSE;
    if ( getStat() != STAT_GRPS_FOLLOW )
    {
        Log_err( "LIST NEWSGROUPS failed: %s", client.lastStat );
        return FALSE;
    }
    while ( getTxtLn( line, &err ) && ! err )
    {
        if ( sscanf( line, "%s", name ) != 1 )
        {
            Log_err( "Unknown reply to LIST NEWSGROUPS: %s", line );
            continue;
        }
        strcpy( dsc, Utl_restOfLn( line, 1 ) );
        if ( Grp_exists( name ) )
        {
            Log_dbg( "Description of %s: %s", name, dsc );
            Grp_setDsc( name, dsc );
        }
    }
    return TRUE;
}

Bool
Client_getCreationTimes( void )
{
    Bool err;
    Str name, line;
    time_t t;

    Log_inf( "Querying group creation times" );
    if ( ! putCmd( "LIST ACTIVE.TIMES" ) )
        return FALSE;
    if ( getStat() != STAT_GRPS_FOLLOW )
    {
        Log_err( "LIST ACTIVE.TIMES failes: %s", client.lastStat );
        return FALSE;
    }
    while ( getTxtLn( line, &err ) && ! err )
    {
        if ( sscanf( line, "%s %ld", name, &t ) != 2 )
        {
            Log_err( "Unknown reply to LIST ACTIVE.TIMES: %s", line );
            continue;
        }
        if ( Grp_exists( name ) )
        {
            Log_inf( "Creation time of %s: %ld", name, t );
            Grp_setCreated( name, t );
        }
    }
    return TRUE;
}

Bool
Client_getNewgrps( const time_t *lastTime )
{
    Str s;
    const char *p;

    ASSERT( *lastTime > 0 );
    strftime( s, MAXCHAR, "%Y%m%d %H%M00", gmtime( lastTime ) );
    /*
      Do not use century for working with old server software until 2000.
      According to newest IETF draft, this is still valid after 2000.
      (directly using %y in fmt string causes a Y2K compiler warning)
    */
    p = s + 2;
    if ( ! putCmd( "NEWGROUPS %s", p ) )
        return FALSE;
    if ( getStat() != STAT_NEW_GRP_FOLLOW )
    {
        Log_err( "NEWGROUPS command failed: %s", client.lastStat );
        return FALSE;
    }
    processGrps();
    return TRUE;
}

static const char *
readField( Str result, const char *p )
{
    size_t len;
    char *r;

    if ( ! p )
        return NULL;
    r = result;
    *r = '\0';
    len = 0;
    while ( *p != '\t' && *p != '\n' )
    {
        if ( ! *p )
            return p;
        *(r++) = *(p++);
        ++len;
        if ( len >= MAXCHAR - 1 )
        {
            *r = '\0';
            Log_err( "Field in overview too long: %s", r );
            return ++p;
        }
    }
    *r = '\0';
    return ++p;
}

static Bool
parseOvLn( Str line, int *numb, Str subj, Str from,
           Str date, Str msgId, Str ref, size_t *bytes, size_t *lines )
{
    const char *p;
    Str t;
    
    p = readField( t, line );
    if ( sscanf( t, "%d", numb ) != 1 )
        return FALSE;
    p = readField( subj, p );
    p = readField( from, p );
    p = readField( date, p );
    p = readField( msgId, p );
    p = readField( ref, p );
    p = readField( t, p );
    *bytes = 0;
    *lines = 0;
    if ( sscanf( t, "%d", bytes ) != 1 )
        return TRUE;
    p = readField( t, p );
    if ( sscanf( t, "%d", lines ) != 1 )
        return TRUE;
    return TRUE;
}

static const char*
nextXref( const char *pXref, Str grp, int *numb )
{
    Str s;
    const char *pColon, *src;
    char *dst;

    src = pXref;
    while ( *src && isspace( *src ) )
        ++src;
    dst = s;
    while ( *src && ! isspace( *src ) )
        *(dst++) = *(src++);
    *dst = '\0';
    if ( strlen( s ) == 0 )
        return NULL;
    pColon = strstr( s, ":" );
    if ( ! pColon || sscanf( pColon + 1, "%d", numb ) != 1 )
    {
        Log_err( "Corrupt Xref at position '%s'", pXref );
        return NULL;
    }
    Utl_cpyStrN( grp, s, pColon - s );
    Log_dbg( "client.c: nextXref: grp '%s' numb %lu", grp, numb );
    return src;
}

static Bool
needsMark( const char *ref )
{
    Bool done = FALSE;
    char *p;
    Str msgId;
    int stat, len;
    time_t lastAccess, nowTime;
    double limit;

    nowTime = time( NULL );
    limit = Cfg_threadFollowTime() * 24. * 3600.;
    while ( ! done )
    {
        p = msgId;
        while ( *ref != '<' )
            if ( *(ref++) == '\0' )
                return FALSE;
        len = 0;
        while ( *ref != '>' )
        {
            if ( *ref == '\0' || ++len >= MAXCHAR - 1 )
                return FALSE;
            *(p++) = *(ref++);
        }
        *(p++) = '>';
        *p = '\0';
        if ( Db_contains( msgId ) )
        {
            stat = Db_stat( msgId );
            lastAccess = Db_lastAccess( msgId );
            if ( ( stat & DB_INTERESTING )
                 && difftime( nowTime, lastAccess ) <= limit )
                return TRUE;
        }
    }
    return FALSE;
}

static void
prepareEntry( Over *ov )
{
    Str g, t;
    const char *msgId, *p, *xref;
    int n;

    msgId = Ov_msgId( ov );
    if ( Pseudo_isGeneralInfo( msgId ) )
        Log_dbg( "Skipping general info '%s'", msgId );
    else if ( Db_contains( msgId ) )
    {
        xref = Db_xref( msgId );
        Log_dbg( "Entry '%s' already in db with Xref '%s'", msgId, xref );
        p = nextXref( xref, g, &n );
        if ( p == NULL )
            Log_err( "Overview with no group in Xref '%s'", msgId );
        else
        {
            /* TODO: This code block seems unnessesary. Can we remove it? */
            if ( Cfg_servIsPreferential( client.serv, Grp_serv( g ) ) )
            {
                Log_dbg( "Changing first server for '%s' from '%s' to '%s'",
                         msgId, Grp_serv( g ), client.serv );
                snprintf( t, MAXCHAR, "%s:%d %s",
                          client.grp, Ov_numb( ov ), xref );
                Db_setXref( msgId, t );
            }
            else
            {
                Log_dbg( "Adding '%s' to Xref of '%s'", g, msgId );
                snprintf( t, MAXCHAR, "%s %s:%d",
                          xref, client.grp, Ov_numb( ov ) );
                Db_setXref( msgId, t );
            }
        }
    }
    else
    {
        Log_dbg( "Preparing '%s' in database", msgId );
        Db_prepareEntry( ov, client.grp, Ov_numb( ov ) );
    }
}

Bool
Client_getOver( int rmtFirst, int rmtLast, FetchMode mode )
{
    Bool err;
    size_t bytes, lines;
    int rmtNumb, oldLast, cntMarked;
    Over *ov;
    Str line, subj, from, date, msgId, ref;

    ASSERT( strcmp( client.grp, "" ) != 0 );
    if ( ! putCmd( "XOVER %lu-%lu", rmtFirst, rmtLast ) )
        return FALSE;
    if ( getStat() != STAT_OVERS_FOLLOW )
    {
        Log_err( "XOVER command failed: %s", client.lastStat );
        return FALSE;
    }
    Log_dbg( "Requesting overview for remote %lu-%lu", rmtFirst, rmtLast );
    oldLast = Cont_last();
    cntMarked = 0;
    while ( getTxtLn( line, &err ) && ! err )
    {
        if ( ! parseOvLn( line, &rmtNumb, subj, from, date, msgId, ref,
                          &bytes, &lines ) )
            Log_err( "Bad overview line: %s", line );
        else
        {
            ov = new_Over( subj, from, date, msgId, ref, bytes, lines );
            Cont_app( ov );
            prepareEntry( ov );
            if ( mode == FULL || ( mode == THREAD && needsMark( ref ) ) )
            {
                Req_add( client.serv, msgId );
                ++cntMarked;
            }
        }
        Grp_setRmtNext( client.grp, rmtNumb + 1 );
    }
    if ( oldLast != Cont_last() )
        Log_inf( "Added %s %lu-%lu", client.grp, oldLast + 1, Cont_last() );
    Log_inf( "%u articles marked for download in %s", cntMarked, client.grp  );
    return err;
}

static void
retrievingFailed( const char* msgId, const char *reason )
{
    int stat;

    Log_err( "Retrieving of %s failed: %s", msgId, reason );
    stat = Db_stat( msgId );
    Pseudo_retrievingFailed( msgId, reason );
    Db_setStat( msgId, stat | DB_RETRIEVING_FAILED );
}

static Bool
retrieveAndStoreArt( const char *msgId )
{
    Bool err;
    DynStr *s = NULL;
    Str line;

    Log_inf( "Retrieving %s", msgId );
    s = new_DynStr( 5000 );
    while ( getTxtLn( line, &err ) && ! err )
        DynStr_appLn( s, line );
    if ( ! err )
        Db_storeArt( msgId, DynStr_str( s ) );
    else
        retrievingFailed( msgId, "Connection broke down" );
    del_DynStr( s );
    return ! err;
}

void
Client_retrieveArt( const char *msgId )
{
    if ( ! Db_contains( msgId ) )
    {
        Log_err( "Article '%s' not prepared in database. Skipping.", msgId );
        return;
    }
    if ( ! ( Db_stat( msgId ) & DB_NOT_DOWNLOADED ) )
    {
        Log_inf( "Article '%s' already retrieved. Skipping.", msgId );
        return;
    }
    if ( ! putCmd( "ARTICLE %s", msgId ) )
        retrievingFailed( msgId, "Connection broke down" );
    else if ( getStat() != STAT_ART_FOLLOWS )
        retrievingFailed( msgId, client.lastStat );
    else
        retrieveAndStoreArt( msgId );
}

void
Client_retrieveArtList( const char *list )
{
    Str msgId;
    DynStr *s;
    const char *p;
    
    Log_inf( "Retrieving article list" );
    s = new_DynStr( strlen( list ) );
    p = list;
    while ( ( p = Utl_getLn( msgId, p ) ) )
        if ( ! Db_contains( msgId ) )
            Log_err( "Skipping retrieving of %s (not prepared in database)",
                     msgId );
        else if ( ! ( Db_stat( msgId ) & DB_NOT_DOWNLOADED ) )
            Log_inf( "Skipping %s (already retrieved)", msgId );
        else if ( ! putCmdNoFlush( "ARTICLE %s", msgId ) )
        {
            retrievingFailed( msgId, "Connection broke down" );
            del_DynStr( s );
            return;
        }
        else
            DynStr_appLn( s, msgId );
    fflush( client.out );
    Log_dbg( "[S FLUSH]" );
    p = DynStr_str( s );
    while ( ( p = Utl_getLn( msgId, p ) ) )
    {
        if ( getStat() != STAT_ART_FOLLOWS )
            retrievingFailed( msgId, client.lastStat );
        else if ( ! retrieveAndStoreArt( msgId ) )
            break;
    }
    del_DynStr( s );
}

Bool
Client_changeToGrp( const char* name )
{
    unsigned int stat;
    int estimatedNumb, first, last;

    if ( ! Grp_exists( name ) )
        return FALSE;
    if ( ! putCmd( "GROUP %s", name ) )
        return FALSE;
    if ( getStat() != STAT_GRP_SELECTED )
        return FALSE;
    if ( sscanf( client.lastStat, "%u %d %d %d",
                 &stat, &estimatedNumb, &first, &last ) != 4 )
    {
        Log_err( "Bad server response to GROUP: %s", client.lastStat );
        return FALSE;
    }
    Utl_cpyStr( client.grp, name );
    client.rmtFirst = first;
    client.rmtLast = last;
    return TRUE;
}

void
Client_rmtFirstLast( int *first, int *last )
{
    *first = client.rmtFirst;
    *last = client.rmtLast;
}

Bool
Client_postArt( const char *msgId, const char *artTxt,
                    Str errStr )
{
    if ( ! putCmd( "POST" ) )
        return FALSE;
    if ( getStat() != STAT_SEND_ART )
    {
        Log_err( "Posting of %s not allowed: %s", msgId, client.lastStat );
        strcpy( errStr, client.lastStat );
        return FALSE;
    }
    putTxtBuf( artTxt );
    putEndOfTxt();
    if ( getStat() != STAT_POST_OK )
    {
        Log_err( "Posting of %s failed: %s", msgId, client.lastStat );
        strcpy( errStr, client.lastStat );
        return FALSE;
    }
    Log_inf( "Posted %s (Status: %s)", msgId, client.lastStat );
    return TRUE;
}