Mercurial > noffle
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/client.c Fri May 05 22:45:56 2000 +0100 @@ -0,0 +1,888 @@ +/* + 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; +}