view src/noffle.c @ 249:0340b9c17edc noffle

[svn] *** empty log message ***
author mirkol
date Tue, 14 May 2002 15:25:45 +0100
parents f8a91e2b4060
children 93d5d8b098da
line wrap: on
line source

/*
  noffle.c

  Main program. Implements specified actions, but running as server, which
  is done by Server_run(), declared in server.h.
  
  Locking policy: lock access to databases while noffle is running, but
  not as server. If noffle runs as server, locking is performed while
  executing NNTP commands, but temporarily released if no new command is
  received for some seconds (to allow multiple clients connect at the same
  time).

  $Id: noffle.c 381 2002-05-14 14:25:45Z mirkol $
*/

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

#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <syslog.h>
#include <unistd.h>
#include "client.h"
#include "common.h"
#include "content.h"
#include "control.h"
#include "configfile.h"
#include "database.h"
#include "fetch.h"
#include "fetchlist.h"
#include "filter.h"
#include "group.h"
#include "itemlist.h"
#include "log.h"
#include "online.h"
#include "outgoing.h"
#include "over.h"
#include "post.h"
#include "pseudo.h"
#include "util.h"
#include "server.h"
#include "request.h"
#include "lock.h"
#include "portable.h"
#include "wildmat.h"

struct Noffle
{
    Bool queryGrps;
    Bool queryDsc;
    Bool queryTimes;
    Bool lockAtStartup;
    char *serverPattern;
} noffle = { FALSE, FALSE, FALSE, TRUE, NULL };

static void
doArt( const char *msgId )
{
    const char *id;

    if ( strcmp( msgId, "all" ) == 0 )
    {
        if ( ! Db_first( &id ) )
            fprintf( stderr, "Database empty.\n" );
        else
            do
            {
                printf( "From %s %s\n"
                        "%s\n"
                        "%s\n",
                        Db_from( id ), Db_date( id ),
                        Db_header( id ),
                        Db_body( id ) );
            }
            while ( Db_next( &id ) );
    }
    else
    {
        if ( ! Db_contains( msgId ) )
            fprintf( stderr, "Not in database.\n" );
        else
            printf( "%s\n%s", Db_header( msgId ), Db_body( msgId ) );
    }
}

static void
doCancel( const char *msgId )
{
    switch( Ctrl_cancel( msgId ) )
    {
    case CANCEL_NO_SUCH_MSG:
	printf( "No such message '%s'.\n", msgId );
	break;

    case CANCEL_OK:
	printf( "Message '%s' cancelled.\n", msgId );
	break;

    case CANCEL_NEEDS_MSG:
	printf( "Message '%s' cancelled in local database only.\n", msgId );
	break;
    }
}

/* List articles requested from one particular server */
static void
listRequested1( const char* serv )
{
  Str msgid;

  if ( ! Req_first( serv, msgid ) )
      return;
  do
      printf( "%s %s\n", msgid, serv );
  while ( Req_next( msgid ) );
}

/* List requested articles. List for all servers if serv = "all" or serv =
   NULL. */
static void
doRequested( const char *arg )
{
    Str serv;
    
    if ( ! arg || ! strcmp( arg, "all" ) )
    {
        Cfg_beginServEnum();   
        while ( Cfg_nextServ( serv ) )
            listRequested1( serv );
    }   
    else
        listRequested1( arg );
}


static void
doDb( void )
{
    const char *msgId;

    if ( ! Db_first( &msgId ) )
        fprintf( stderr, "Database empty.\n" );
    else
        do
            printf( "%s\n", msgId );
        while ( Db_next( &msgId ) );
}

static void
doFetch( void )
{
    Str serv;

    if ( ! Lock_getFetchLock( LOCK_NOWAIT ) )
    {
	Log_err( "Another 'noffle --fetch' is in progress" );
	return;
    }
    
    Cfg_beginServEnum();
    while ( Cfg_nextServ( serv ) )
        if ( ! noffle.serverPattern 
                || Wld_match( serv, noffle.serverPattern ) )
            if ( Fetch_init( serv ) )
            {
	        Bool connOK;
	    
                connOK = Fetch_postArts();

                Flt_init( serv ); /* get filter data before processGrps() calls Utl_stamp(). */

                connOK = connOK && Fetch_getNewGrps();

            /* Get overviews of new articles and store IDs of new articles
               that are to be fetched becase of FULL or THREAD mode in the
               request database. */
                connOK = connOK && Fetch_updateGrps();         

            /* get requested articles */
                connOK = connOK && Fetch_getReq_();

                Fetch_close();
            }

    Lock_releaseFetchLock();
}

static Bool
doPost( FILE *f, unsigned flags )
{
    Str line;
    DynStr *s;
    Bool res;

    s = new_DynStr( 10000 );
    while ( fgets( line, MAXCHAR, f ) != NULL )
	DynStr_app( s, line );

    res = TRUE;
    if ( ! Post_open( DynStr_str( s ), flags ) )
    {
	fprintf( stderr, "Post failed: Malformed article.\n" );
	res = FALSE;
    }
    else if ( ! Post_post() )
    {
	fprintf( stderr, "Post failed: Can't post to group.\n" );
	res = FALSE;
    }
    Post_close();
    return res;
}

static void
doQuery( void )
{
    Str serv;

    Cfg_beginServEnum();
    while ( Cfg_nextServ( serv ) )
        if ( ! noffle.serverPattern 
                || Wld_match( serv, noffle.serverPattern ) )
            if ( Fetch_init( serv ) )
            {
	        int stat = STAT_OK;
	    
                if ( noffle.queryGrps )
                    stat = Client_getGrps();
                if ( stat == STAT_OK && noffle.queryDsc )
                    Client_getDsc();
                Fetch_close();
            }
}

/* Expire all overviews not in database */
static void
expireContents( void )
{
    const Over *ov;
    int i;
    int cntDel, cntLeft;
    Str grp;
    Bool autoUnsubscribe;
    int autoUnsubscribeDays;
    time_t maxAge = 0;
    const char *msgId;

    autoUnsubscribe = Cfg_autoUnsubscribe();
    autoUnsubscribeDays = Cfg_autoUnsubscribeDays();
    maxAge = Cfg_autoUnsubscribeDays() * 24 * 3600;
    if ( ! Cont_firstGrp( grp ) )
        return;
    Log_inf( "Expiring overviews not in database" );
    Fetchlist_read();
    do
    {
	if ( ! Grp_exists( grp ) )
            Log_err( "Overview file for unknown group %s exists", grp );
        else
        {
            cntDel = cntLeft = 0;
            Cont_read( grp );
            for ( i = Cont_first(); i <= Cont_last(); ++i )
                if ( ( ov = Cont_get( i ) ) )
                {
                    msgId = Ov_msgId( ov );
                    if ( ! Db_contains( msgId ) )
                    {
                        Cont_delete( i );
                        ++cntDel;
                    }
                    else
                        ++cntLeft;
                }

	    /*
	     * Auto unsubscribe where applicable if last article arrival
	     * time is maxAge newer than the last access time. This ensures
	     * the low traffic groups don't get expired simply because
	     * there's been nothing to read.
	     */
            if ( ! Grp_local( grp )
                 && Fetchlist_contains( grp, NULL )
                 && autoUnsubscribe
                 && difftime( Grp_lastPostTime(grp),
			      Grp_lastAccess( grp ) ) > maxAge )
            {
		Log_ntc( "Auto-unsubscribing from %s after %d "
			 "days without access",
			 grp, autoUnsubscribeDays );
		Pseudo_autoUnsubscribed( grp, autoUnsubscribeDays );
		Fetchlist_remove( grp );
		Grp_setRmtNext( grp, GRP_RMT_NEXT_NOT_SUBSCRIBED );
            }
            if ( Cont_write() )
                Grp_setFirstLast( grp, Cont_first(), Cont_last() );
            Log_inf( "%ld overviews deleted from group %s, %ld left (%ld-%ld)",
                     cntDel, grp, cntLeft, Grp_first( grp ), Grp_last( grp ) );
        }
    }
    while ( Cont_nextGrp( grp ) );
    Fetchlist_write();
}

static void
doExpire( void )
{
    Db_close();
    Db_expire();
    if ( ! Db_open() )
        return;
    expireContents();
}

static void
doCreateLocalGroup( const char * name )
{
    Str grp;

    Utl_cpyStr( grp, name );
    Utl_toLower( grp );
    name = Utl_stripWhiteSpace( grp );
    
    if ( Grp_exists( name ) )
    {
        fprintf( stderr, "'%s' already exists.\n", name );
        return;
    }
    if ( ! Grp_isValidGroupName( name ) )
        fprintf( stderr, "'%s' invalid group name.\n", name );
    else
    {
        Log_inf( "Creating new local group '%s'", name );
        Grp_create( name );
        Grp_setLocal( name );
	printf( "New local group '%s' created.\n", name );
	
	snprintf( grp, MAXCHAR, "%s/groupinfo.lastupdate", Cfg_spoolDir() );
	Utl_stamp( grp );
    }
}

static void
doDeleteLocalGroup( const char * name )
{
    Str grp;

    Utl_cpyStr( grp, name );
    Utl_toLower( grp );
    name = Utl_stripWhiteSpace( grp );
    
    if ( ! Grp_exists( name ) )
    {
        fprintf( stderr, "'%s' does not exist.\n", name );
        return;
    }
    if ( ! Grp_isValidGroupName( name ) )
    {
        fprintf( stderr, "'%s' invalid group name. Skipping deletion of overviews.\n", name );
        Log_inf( "Deleting invalid group '%s' without deleting overviews.", name );
        Grp_delete( name );
        printf( "Group '%s' deleted.\n", name );
    }
    else
    {
	int i;
	
        Log_inf( "Deleting group '%s'", name );

	/*
	  Delete all articles that are only in the group. Check the
	  article Xref for more than one group.
	 */
	Cont_read( name );
	for ( i = Cont_first(); i <= Cont_last(); i++ )
	{
	    const Over *over;
	    Bool toDelete;
	    Str msgId;

	    over = Cont_get( i );
	    toDelete = TRUE;
	    if ( over != NULL )
	    {
		ItemList * xref;

		Utl_cpyStr( msgId, Ov_msgId( over ) );
		xref = new_Itl( Db_xref( msgId ), " " );
		if ( Itl_count( xref ) > 1 )
		    toDelete = FALSE;
		del_Itl( xref );
	    }
	    Cont_delete( i );
	    if ( toDelete )
		Db_delete( msgId );
	}
        if ( Cont_write() )
        {
            Grp_delete( name );
            printf( "Group '%s' deleted.\n", name );
        }
    }
}

static void
doList( void )
{
    FetchMode mode;
    int i, size;
    const char *name, *modeStr = "";

    Fetchlist_read();
    size = Fetchlist_size();
    if ( size == 0 )
        fprintf( stderr, "Fetch list is empty.\n" );
    else
        for ( i = 0; i < size; ++i )
        {
            Fetchlist_element( &name, &mode, i );
            switch ( mode )
            {
            case FULL:
                modeStr = "full"; break;
            case THREAD:
                modeStr = "thread"; break;
            case OVER:
                modeStr = "over"; break;
            }
            printf( "%s %s %s\n", name, Grp_server( name ), modeStr );
        }
}

/* A modify command. argc/argv start AFTER '-m'. */
static Bool
doModify( const char *cmd, int argc, char **argv )
{
    const char *grp;

    if ( argc < 2 )
    {
	fprintf( stderr, "Insufficient arguments to -m\n" );
	return FALSE;
	
    }
    else if ( strcmp( cmd, "desc" ) != 0
	      && strcmp( cmd, "post" ) != 0 )
    {
	fprintf( stderr, "Unknown argument -m %s\n", cmd );
	return FALSE;
    }
    
    grp = argv[ 0 ];
    argv++;
    argc--;
    
    if ( strcmp( cmd, "desc" ) == 0 )
    {
	Str desc;

	Utl_cpyStr( desc, *( argv++ ) );
	while ( --argc > 0 )
	{
	    Utl_catStr( desc, " " );
	    Utl_catStr( desc, *( argv++ ) );
	}

	Grp_setDsc( grp, desc );
    }
    else
    {
	char c;

	if ( ! Grp_local( grp ) )
	{
	    fprintf( stderr, "%s is not a local group\n", grp );
	    return FALSE;
	}
	
	c = **argv;
	if ( c == 'y' || c == 'm' || c == 'n' )
	    Grp_setPostAllow( grp, c );
	else
	{
	    fprintf( stderr, "Access must be 'y', 'n' or 'm'\n" );
	    return FALSE;
	}
    }

    return TRUE;
}

static void
doGrps( void )
{
    const char *g;
    Str dateLastAccess, dateCreated;
    time_t lastAccess, created;

    if ( Grp_firstGrp( &g ) )
        do
        {
            lastAccess = Grp_lastAccess( g );
            created = Grp_created( g );
            ASSERT( lastAccess >= 0 );
            ASSERT( created >= 0 );
            strftime( dateLastAccess, MAXCHAR, "%Y-%m-%d %H:%M:%S",
                      localtime( &lastAccess ) );
            strftime( dateCreated, MAXCHAR, "%Y-%m-%d %H:%M:%S",
                      localtime( &created ) );
            printf( "%s\t%s\t%i\t%i\t%i\t%c\t%s\t%s\t%s\n",
                    g, Grp_server( g ), Grp_first( g ), Grp_last( g ),
                    Grp_rmtNext( g ), Grp_postAllow( g ), dateCreated,
                    dateLastAccess, Grp_dsc( g ) );
        }
        while ( Grp_nextGrp( &g ) );
}

static Bool
doSubscribe( const char *name, FetchMode mode )
{
    if ( ! Grp_exists( name ) )
    {
        fprintf( stderr, "%s is not available at remote servers.\n", name );
        return FALSE;
    }
    Fetchlist_read();
    if ( Fetchlist_add( name, mode ) )
        printf( "Adding %s to fetch list in %s mode.\n",
                name, mode == FULL ? "full" : mode == THREAD ?
                "thread" : "overview" );
    else
        printf( "%s is already in fetch list. Mode is now: %s.\n",
                name, mode == FULL ? "full" : mode == THREAD ?
                "thread" : "overview" );
    if ( ! Fetchlist_write() )
        fprintf( stderr, "Could not save fetchlist.\n" );
    Grp_setLastAccess( name );
    return TRUE;
}

static void
doUnsubscribe( const char *name )
{
    Fetchlist_read();
    if ( ! Fetchlist_remove( name ) )
        printf( "%s is not in fetch list.\n", name );
    else
        printf( "%s removed from fetch list.\n", name );
    if ( Fetchlist_write() )
	Grp_setRmtNext( name, GRP_RMT_NEXT_NOT_SUBSCRIBED );
    else
        fprintf( stderr, "Could not save fetchlist.\n" );
}

static void
printUsage( void )
{
    static const char *msg =
      "Usage: noffle <option>\n"
      "Option is one of the following:\n"
      " -a | --article <msg id>|all      Show article(s) in database\n"
      " -c | --cancel <msg id>           Remove article from database\n"
      " -C | --create <grp>              Create a local group\n"
      " -d | --database                  Show content of article database\n"
      " -D | --delete <grp>              Delete a group\n"
      " -e | --expire                    Expire articles\n"
      " -f | --fetch [server]            Get newsfeed from server/post articles\n"
      " -g | --groups                    Show all groups available at server\n"
      " -h | --help                      Show this text\n"
      " -l | --list                      List groups on fetch list\n"
      " -m | --modify desc <grp> <desc>  Modify a group description\n"
      " -m | --modify post <grp> (y|n)   Modify posting status of a local group\n"
      " -n | --online                    Switch to online mode\n"
      " -o | --offline                   Switch to offline mode\n"
      " -p | --post                      Post article on stdin\n"
      " -q | --query groups [server]     Get group list from server\n"
      " -q | --query desc [server]       Get group descriptions from server\n"
      " -r | --server                    Run as server on stdin/stdout\n"
      " -R | --requested                 List articles marked for download\n"
      " -s | --subscribe-over <grp>      Add group to fetch list (overview)\n"
      " -S | --subscribe-full <grp>      Add group to fetch list (full)\n"
      " -t | --subscribe-thread <grp>    Add group to fetch list (thread)\n"
      " -u | --unsubscribe <grp>         Remove group from fetch list\n"
      " -v | --version                   Print version\n";
    fprintf( stderr, "%s", msg );
}

/*
  Allow core files: Change core limit and change working directory
  to spool directory, where news has write permissions.
*/
static void
enableCorefiles( void )
{
    struct rlimit lim;

    if ( getrlimit( RLIMIT_CORE, &lim ) != 0 )
    {
        Log_err( "Cannot get system core limit: %s", strerror( errno ) );
        return;
    }
    lim.rlim_cur = lim.rlim_max;
    if ( setrlimit( RLIMIT_CORE, &lim ) != 0 )
    {
        Log_err( "Cannot set system core limit: %s", strerror( errno ) );
        return;
    }
    Log_dbg( LOG_DBG_NOFFLE, "Core limit set to %i", lim.rlim_max );
    if ( chdir( Cfg_spoolDir() ) != 0 )
    {
         Log_err( "Cannot change to directory '%s'", Cfg_spoolDir() );
         return;
    }
    Log_dbg( LOG_DBG_NOFFLE, "Changed to directory '%s'", Cfg_spoolDir() );
}

static Bool
initNoffle( void )
{
    Log_init( "noffle", noffle.lockAtStartup, LOG_NEWS );
    Cfg_read();
    Log_dbg( LOG_DBG_NOFFLE, "NOFFLE version %s", Cfg_version() );
    if ( noffle.lockAtStartup )
        if ( ! Lock_openDatabases() )
            return FALSE;
    enableCorefiles();
    return TRUE;
}

static void
closeNoffle( void )
{
    if ( noffle.lockAtStartup )
      Lock_closeDatabases();
    Lock_syncDatabases();
}

static RETSIGTYPE
bugReport( int sig )
{
    Log_err( "Received SIGSEGV. Please submit a bug report" );
    signal( SIGSEGV, SIG_DFL );

    /* Attempt to save database state before passing on sig. */
    Lock_syncDatabases();
    
    raise( sig );
}

static RETSIGTYPE
logSignal( int sig )
{
    const char *name;
    Bool err = TRUE;

    switch ( sig )
    {
    case SIGABRT:
        name = "SIGABRT"; break;
    case SIGFPE:
        name = "SIGFPE"; break;
    case SIGILL:
        name = "SIGILL"; break;
    case SIGINT:
        name = "SIGINT"; break;
    case SIGTERM:
        name = "SIGTERM"; break;
    case SIGPIPE:
        name = "SIGPIPE"; err = FALSE; break;
    default:
        name = "?"; break;
    }
    if ( err )
        Log_err( "Received signal %i (%s). Aborting.", sig, name );
    else
        Log_inf( "Received signal %i (%s). Aborting.", sig, name );
    signal( sig, SIG_DFL );

    /* Attempt to save database state before passing on sig. */
    Lock_syncDatabases();
    
    raise( sig );
}

static void
printInewsUsage( void )
{
    static const char *msg =
	"Usage: inews [-D] [-O] [-S] [input]\n"
	" -D    Debug - send article to stdout and don't post\n"
	" -O    Don't add Organization header\n"
	" -S    Don't add .signature\n"
	" input File containing message, standard input if none specified.\n"
	"For compatability, -h, -A, -V, -W are ignored and -N is\n"
	"equivalent to -D.\n";
    fprintf( stderr, "%s", msg );
}

static int
doInews( int argc, char **argv )
{
    int result;
    int flags;
    FILE *f;

    UNUSED( argc );
    
    noffle.lockAtStartup = TRUE;

    /* Process options */
    flags = POST_ADD_ORG | POST_ADD_SIG | POST_ADD_FROM;
    for ( ; argv[0] != NULL && argv[0][0] == '-' ; argv++ )
    {
	if ( argv[0][2] != '\0' )
	{
	    printInewsUsage();
	    return EXIT_FAILURE;
	}
	
	switch( argv[0][1] )
	{
	case 'h':
	case 'A':
	case 'V':
	case 'W':
	    break;
	case 'N':
	case 'D':
	    flags |= POST_DEBUG;
	    break;
	case 'O':
	    flags &= ~POST_ADD_ORG;
	    break;
	case 'S':
	    flags &= ~POST_ADD_SIG;
	    break;
	default:
	    printInewsUsage();
	    return EXIT_FAILURE;
	}
    }
    
    if ( argv[0] ==  NULL )
	f = stdin;
    else
    {
	f = fopen( argv[0], "r" );
	if ( f == NULL )
	{
	    Log_err( "Can't access %s (%s).", argv[0], strerror( errno ) );
	    return EXIT_FAILURE;
	}
    }

    if ( ! initNoffle() )
	return EXIT_FAILURE;
    result = EXIT_SUCCESS;

    if ( ! doPost( f, flags ) )
	result = EXIT_FAILURE;

    if ( f != stdin )
	fclose( f );

    closeNoffle();
    return result;
}

static int
getArgLetter(const char *arg)
{
    int res;
    struct option
    {
	const char *longOpt;
	const char *opt;
    } options[] =
    {
	{ "--article", 		"-a" },
	{ "--cancel", 		"-c" },
	{ "--create", 		"-C" },
	{ "--database",		"-d" },
	{ "--delete", 		"-D" },
	{ "--expire", 		"-e" },
	{ "--fetch", 		"-f" },
	{ "--groups", 		"-g" },
	{ "--help", 		"-h" },
	{ "--list", 		"-l" },
	{ "--modify", 		"-m" },
	{ "--offline", 		"-o" },
	{ "--online", 		"-n" },
	{ "--post", 		"-p" },
	{ "--query", 		"-q" },
	{ "--server",		"-r" },
	{ "--requested",	"-R" },
	{ "--subscribe-over",	"-s" },
	{ "--subscribe-full",	"-S" },
	{ "--subscribe-thread",	"-t" },
	{ "--unsubscribe",	"-u" },
	{ "--version",		"-v" },
	{ NULL,			NULL }
	
    };
    struct option *opt;

    res = -1;
    for ( opt = options; opt->opt != NULL; opt++ )
	if ( strcmp( arg, opt->longOpt ) == 0 ||
	     strcmp( arg, opt->opt ) == 0 )
	{
	    res = opt->opt[1];
	    break;
	}

    return res;
}

int main ( int argc, char **argv )
{
    int c, result;
    const char *cmdname, *p;

    signal( SIGSEGV, bugReport );
    signal( SIGABRT, logSignal );
    signal( SIGFPE, logSignal );
    signal( SIGILL, logSignal );
    signal( SIGINT, logSignal );
    signal( SIGTERM, logSignal );
    signal( SIGPIPE, logSignal );

    /* Find last component of command name. */
    cmdname = argv[0];
    p = strrchr( cmdname, '/' );
    if ( p != NULL )
	cmdname = ++p;

    argv++;
    argc--;

    /* Were we invoked as inews? */
    if ( strcmp( cmdname, "inews" ) == 0 )
	return doInews( argc, argv );

    c = -1;
    if ( *argv != NULL )
    {
	c = getArgLetter( *argv );
	argv++;
	argc--;
    }

    noffle.lockAtStartup = ! ( c == 'r' || c == 'h' );
    if ( ! initNoffle() )
        return EXIT_FAILURE;
    result = EXIT_SUCCESS;
    switch ( c )
    {
    case 'a':
        if ( *argv == NULL )
        {
            fprintf( stderr, "Option -a needs argument.\n" );
            result = EXIT_FAILURE;
        }
        else
            doArt( *argv );
        break;
    case 'c':
        if ( *argv == NULL )
        {
            fprintf( stderr, "Option -c needs argument.\n" );
            result = EXIT_FAILURE;
        }
        else
            doCancel( *argv );
        break;
    case 'C':
        if ( *argv == NULL  )
        {
            fprintf( stderr, "Option -C needs argument.\n" );
            result = EXIT_FAILURE;
        }
        else
            doCreateLocalGroup( *argv );
        break;
    case 'd':
        doDb();
        break;
    case 'D':
        if ( *argv == NULL )
        {
            fprintf( stderr, "Option -D needs argument.\n" );
            result = EXIT_FAILURE;
        }
        else
            doDeleteLocalGroup( *argv );
        break;
    case 'e':
	doExpire();
	break;
    case 'f':
        noffle.serverPattern = *argv;
        doFetch();
        break;
    case 'g':
        doGrps();
        break;
    case -1:
    case 'h':
        printUsage();
        break;
    case 'l':
        doList();
        break;
    case 'm':
        if ( *argv == NULL )
        {
            fprintf( stderr, "Option -m needs argument.\n" );
            result = EXIT_FAILURE;
        }
        else
	    if ( ! doModify( argv[0], --argc, &argv[1] ) )
		result = EXIT_FAILURE;
        break;
    case 'n':
        if ( Online_true() )
            fprintf( stderr, "NOFFLE is already online\n" );
        else
            Online_set( TRUE );
        break;
    case 'o':
        if ( ! Online_true() )
            fprintf( stderr, "NOFFLE is already offline\n" );
        else
            Online_set( FALSE );
        break;
    case 'p':
	if ( ! doPost( stdin, 0 ) )
	    result = EXIT_FAILURE;
        break;
    case 'q':
        if ( *argv == NULL )
        {
            fprintf( stderr, "Option -q needs argument.\n" );
            result = EXIT_FAILURE;
        }
        else
        {
            if ( strcmp( *argv, "groups" ) == 0 )
                noffle.queryGrps = TRUE;
            else if ( strcmp( *argv, "desc" ) == 0 )
                noffle.queryDsc = TRUE;
            else
            {
                fprintf( stderr, "Unknown argument -q %s\n", *argv );
                result = EXIT_FAILURE;
            }
            argv++;
            argc--;
            noffle.serverPattern = *argv;
            doQuery();
        }
        break;
    case 'r':
        Log_inf( "Starting as server" );
        Server_run();
        break;
    case 'R':
        doRequested( *argv );
        break;
    case 's':
        if ( *argv == NULL )
        {
            fprintf( stderr, "Option -s needs argument.\n" );
            result = EXIT_FAILURE;
        }
        else
            result = doSubscribe( *argv, OVER );
        break;
    case 'S':
        if ( *argv == NULL )
        {
            fprintf( stderr, "Option -S needs argument.\n" );
            result = EXIT_FAILURE;
        }
        else
            doSubscribe( *argv, FULL );
        break;
    case 't':
        if ( *argv == NULL )
        {
            fprintf( stderr, "Option -t needs argument.\n" );
            result = EXIT_FAILURE;
        }
        else
            result = doSubscribe( *argv, THREAD );
        break;
    case 'u':
        if ( *argv == NULL )
        {
            fprintf( stderr, "Option -u needs argument.\n" );
            result = EXIT_FAILURE;
        }
        else
            doUnsubscribe( *argv );
        break;
    case '?':
        /* Error message already printed by getopt_long */
        result = EXIT_FAILURE;
        break;
    case 'v':
        printf( "NNTP server NOFFLE, version %s.\n", Cfg_version() );
        break;
    default:
        abort(); /* Never reached */
    }
    closeNoffle();
    return result;
}