view src/client.c @ 58:b4e6f7f96135 noffle

[svn] Add some intermediate variables for easier debugging in needsMark(). It seems that thread mode is sometimes not working. Changed some variable types and used some casts to avoid compiler warnings about signedness. In general, int should be used for parameters for allowing a signedness assertion in the function.
author enz
date Fri, 12 May 2000 17:52:07 +0100
parents 84e32c84666b
children adf0af5152f7
line wrap: on
line source

/*
  client.c

  $Id: client.c 64 2000-05-12 16:52:07Z enz $
*/

#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"
#include "portable.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 )
{
    size_t 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 );
            /* Start local numbering with remote first number to avoid
               new numbering at the readers if noffle is re-installed */
            if ( first != 0 )
                Grp_setFirstLast( grp, first, first - 1 );
            else
                Grp_setFirstLast( grp, 1, 0 );
            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, interesting;
    char *p;
    Str msgId;
    int stat, len;
    time_t lastAccess, nowTime;
    double threadFollowTime, limit, age;

    nowTime = time( NULL );
    threadFollowTime = (double)Cfg_threadFollowTime();
    limit = 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 );
            interesting = ( stat & DB_INTERESTING );
            age = difftime( nowTime, lastAccess );
            if ( interesting && age <= 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( (int)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;
}