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;
+}