diff src/noffle.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 32ba1198c6fa
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/noffle.c	Fri May 05 22:45:56 2000 +0100
@@ -0,0 +1,798 @@
+/*
+  noffle.c
+
+  Main program. Implements specified actions, but running as server, which
+  is done by Serv_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 49 2000-05-05 21:45:56Z uh1763 $
+*/
+
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+#include <signal.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 "group.h"
+#include "itemlist.h"
+#include "log.h"
+#include "online.h"
+#include "outgoing.h"
+#include "over.h"
+#include "pseudo.h"
+#include "util.h"
+#include "server.h"
+#include "request.h"
+#include "lock.h"
+
+struct Noffle
+{
+    Bool queryGrps;
+    Bool queryDsc;
+    Bool queryTimes;
+    Bool interactive;
+} noffle = { FALSE, FALSE, FALSE, TRUE };
+
+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. */
+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;
+
+    Cfg_beginServEnum();
+    while ( Cfg_nextServ( serv ) )
+        if ( Fetch_init( serv ) )
+        {
+            Fetch_postArts();
+
+            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. */
+            Fetch_updateGrps();         
+
+            /* get requested articles */
+            Fetch_getReq_();
+
+            Fetch_close();
+        }
+}
+
+static void
+doQuery( void )
+{
+    Str serv;
+
+    Cfg_beginServEnum();
+    while ( Cfg_nextServ( serv ) )
+        if ( Fetch_init( serv ) )
+        {
+            if ( noffle.queryGrps )
+                Client_getGrps();
+            if ( noffle.queryDsc )
+                Client_getDsc();
+            if ( noffle.queryTimes )
+                Client_getCreationTimes();
+            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 now = time( NULL ), 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" );
+    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;
+                }
+            if ( ! Grp_local( grp )
+		 && autoUnsubscribe
+                 && difftime( now, Grp_lastAccess( grp ) ) > maxAge )
+            {
+                Log_ntc( "Auto-unsubscribing from %s after %d "
+                         "days without access",
+                         grp, autoUnsubscribeDays );
+                Pseudo_autoUnsubscribed( grp, autoUnsubscribeDays );
+                Fetchlist_read();
+                Fetchlist_remove( grp );
+                Fetchlist_write();
+            }
+            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 ) );
+}
+
+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 );
+    else
+    {
+        Log_inf( "Creating new local group '%s'", name );
+        Grp_create( name );
+        Grp_setLocal( name );
+	printf( "New local group '%s' created.\n", name );
+    }
+}
+
+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 );
+    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 );
+	}
+	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_serv( 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", optarg );
+	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'" );
+	    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_serv( 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" );
+    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() )
+        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                     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"
+      " -q | --query groups              Get group list from server\n"
+      " -q | --query desc                Get group descriptions from server\n"
+      " -q | --query times               Get group creation times 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()
+{
+    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( "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( "Changed to directory '%s'", Cfg_spoolDir() );
+}
+
+static Bool
+initNoffle( Bool interactive )
+{
+    Log_init( "noffle", interactive, LOG_NEWS );
+    Cfg_read();
+    Log_dbg( "NOFFLE version %s", Cfg_version() );
+    noffle.interactive = interactive;
+    if ( interactive )
+      if ( ! Lock_openDatabases() )
+        return FALSE;
+    if ( ! interactive )
+        enableCorefiles();
+    return TRUE;
+}
+
+static void
+closeNoffle( void )
+{
+    if ( noffle.interactive )
+      Lock_closeDatabases();
+}
+
+static void
+bugReport( int sig )
+{
+    Log_err( "Received SIGSEGV. Please submit a bug report" );
+    signal( SIGSEGV, SIG_DFL );
+    raise( sig );
+}
+
+static void
+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 );
+    raise( sig );
+}
+
+int main ( int argc, char **argv )
+{
+    int c, result;
+    struct option longOptions[] =
+    {
+        { "article",          required_argument, NULL, 'a' },
+        { "cancel",           required_argument, NULL, 'c' },
+        { "create",           required_argument, NULL, 'C' },
+        { "database",         no_argument,       NULL, 'd' },
+        { "delete",           required_argument, NULL, 'D' },
+        { "expire",           no_argument,       NULL, 'e' },
+        { "fetch",            no_argument,       NULL, 'f' },
+        { "groups",           no_argument,       NULL, 'g' },
+        { "help",             no_argument,       NULL, 'h' },
+        { "list",             no_argument,       NULL, 'l' },
+        { "modify",           required_argument, NULL, 'm' },
+        { "offline",          no_argument,       NULL, 'o' },
+        { "online",           no_argument,       NULL, 'n' },
+        { "query",            required_argument, NULL, 'q' },
+        { "server",           no_argument,       NULL, 'r' },
+        { "requested",        no_argument,       NULL, 'R' },
+        { "subscribe-over",   required_argument, NULL, 's' },
+        { "subscribe-full",   required_argument, NULL, 'S' },
+        { "subscribe-thread", required_argument, NULL, 't' },
+        { "unsubscribe",      required_argument, NULL, 'u' },
+        { "version",          no_argument,       NULL, 'v' },
+        { NULL, 0, NULL, 0 }
+    };
+    
+    signal( SIGSEGV, bugReport );
+    signal( SIGABRT, logSignal );
+    signal( SIGFPE, logSignal );
+    signal( SIGILL, logSignal );
+    signal( SIGINT, logSignal );
+    signal( SIGTERM, logSignal );
+    signal( SIGPIPE, logSignal );
+    c = getopt_long( argc, argv, "a:c:C:dD:efghlm:onq:rRs:S:t:u:v",
+                     longOptions, NULL );
+    if ( ! initNoffle( c != 'r' ) )
+        return EXIT_FAILURE;
+    result = EXIT_SUCCESS;
+    switch ( c )
+    {
+    case 0:
+        /* Options that set a flag. */
+        break;
+    case 'a':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -a needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            doArt( optarg );
+        break;
+    case 'c':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -c needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            doCancel( optarg );
+        break;
+    case 'C':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -C needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            doCreateLocalGroup( optarg );
+        break;
+    case 'd':
+        doDb();
+        break;
+    case 'D':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -D needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            doDeleteLocalGroup( optarg );
+        break;
+    case 'e':
+	doExpire();
+	break;
+    case 'f':
+        doFetch();
+        break;
+    case 'g':
+        doGrps();
+        break;
+    case -1:
+    case 'h':
+        printUsage();
+        break;
+    case 'l':
+        doList();
+        break;
+    case 'm':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -m needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+	    if ( ! doModify( optarg, argc - optind, &argv[ optind ] ) )
+		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 'q':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -q needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+        {
+            if ( strcmp( optarg, "groups" ) == 0 )
+                noffle.queryGrps = TRUE;
+            else if ( strcmp( optarg, "desc" ) == 0 )
+                noffle.queryDsc = TRUE;
+            else if ( strcmp( optarg, "times" ) == 0 )
+                noffle.queryTimes = TRUE;
+            else
+            {
+                fprintf( stderr, "Unknown argument -q %s\n", optarg );
+                result = EXIT_FAILURE;
+            }
+            doQuery();
+        }
+        break;
+    case 'r':
+        Log_inf( "Starting as server" );
+        Serv_run();
+        break;
+    case 'R':
+        doRequested( optarg );
+        break;
+    case 's':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -s needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            result = doSubscribe( optarg, OVER );
+        break;
+    case 'S':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -S needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            doSubscribe( optarg, FULL );
+        break;
+    case 't':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -t needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            result = doSubscribe( optarg, THREAD );
+        break;
+    case 'u':
+        if ( ! optarg )
+        {
+            fprintf( stderr, "Option -u needs argument.\n" );
+            result = EXIT_FAILURE;
+        }
+        else
+            doUnsubscribe( optarg );
+        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;
+}