ftp.nice.ch/pub/next/unix/network/news/nn.6.4.16.s.tar.gz#/nn/nntp.c

This is nntp.c in view mode; [Download] [Up]

/*
 * nntp module for nn.
 *
 * The original taken from the nntp 1.5 clientlib.c
 * Modified heavily for nn.
 *
 * Rene' Seindal (seindal@diku.dk) Thu Dec  1 18:41:23 1988
 *
 * I have modified Rene's code quite a lot for 6.4 -- I hope he
 * can still recognize a bit here and a byte there; in any case,
 * any mistakes are mine :-)  ++Kim
 */


#include "config.h"

/*
 * 	nn maintains a cache of recently used articles to improve efficiency.
 * 	To change the size of the cache, define NNTPCACHE in config.h to be
 *	the new size of this cache.
 */

#ifndef NNTPCACHE
#define NNTPCACHE	10
#endif

#ifdef NNTP
#include <stdio.h>
#include "nntp.h"
#include <sys/socket.h>
#ifndef EXCELAN
#include <netdb.h>
#endif
#include <errno.h>

/* This is necessary due to the definitions in m-XXX.h */
#if !defined(NETWORK_DATABASE) || defined(NETWORK_BYTE_ORDER)
#include <netinet/in.h>
#endif

#ifdef EXCELAN
#ifndef IPPORT_NNTP
#define	IPPORT_NNTP	119
#endif
#endif

import char *db_directory, *tmp_directory, *news_active;

export char nntp_server[256];	/* name of nntp server */
export int nntp_failed = 0;	/* bool: t iff connection is broken in
				   nntp_get_article() or nntp_get_active() */

export int nntp_cache_size = NNTPCACHE;
export char *nntp_cache_dir = NULL;

export int nntp_local_server = 0;
export int nntp_debug = 0;

import int silent, no_update;

import int sys_nerr;
import char *sys_errlist[];
extern int user_error();
extern int sys_error();
extern int sys_warning();

#define syserr() (errno >= 0 && errno < sys_nerr ? \
		  sys_errlist[errno] : "Unknown error.")

import char *mktemp();

static FILE *nntp_in = NULL;		/* fp for reading from server */
static FILE *nntp_out = NULL;		/* fp for writing to server */
static int is_connected = 0;		/* bool: t iff we are connected */
static group_header *group_hd;		/* ptr to servers current group */
static int group_is_set = 0;			/* bool: t iff group_hd is set */
static int try_again = 0;		/* bool: t if timeout forces retry */
static int can_post = 0;		/* bool: t iff NNTP server accepts postings */

#define ERR_TIMEOUT	503		/* Response code for timeout */


#ifdef NO_BZERO
static bzero(p, l)
register char *p;
register int l;
{  
    while (l-- > 0)
	*p++ = 0;
}
#endif

#ifdef NO_RENAME
static rename(old, new)
char *old, *new;
{
    if (unlink(new) < 0 && errno != ENOENT) return -1;
    if (link(old, new) < 0) return -1;
    return unlink(old);
}
#endif

/*
 * debug_msg: print a debug message.
 *
 *	The master appends prefix and str to a log file, and clients
 *	prints it as a message.
 *
 *	This is controlled via the nntp-debug variable in nn, and
 *	the option -D2 (or -D3 if the normal -D option should also
 *	be turned on).  Debug output from the master is written in
 *	$TMP/nnmaster.log.
 */

static debug_msg(prefix, str)
char *prefix, *str;
{
    static FILE *f = NULL;
    
    if (who_am_i == I_AM_MASTER) {
	if (f == NULL) {
	    f = open_file(relative(tmp_directory, "nnmaster.log"), OPEN_CREATE);
	    if (f == NULL) {
		nntp_debug = 0;
		return;
	    }
	}	    
	fprintf(f, "%s %s\n", prefix, str);
	fflush(f);
	return;
    }

    msg("NNTP%s %s", prefix, str);
    user_delay(1);
}

/*
 * io_error: signal an I/O error in talking to the server.
 *
 * 	An nn client terminates a session with the user.  The master
 *	simply closes the connection.  The flag nntp_failed is set, for
 *	use by the master to terminate collection.
 *
 *	BUG: if the nntp server is forcibly killed, errno can contain a
 *	bogus value, resulting in strange error messages.  It is
 *	probably better just to write out the numerical value of errno.
 */

static io_error()
{
    if (who_am_i != I_AM_MASTER) {
	user_error("Lost connection to NNTP server %s: %s", nntp_server, syserr());
        /* NOTREACHED */
    }
    nntp_failed = 1;
    if (is_connected) {
	log_entry('N', "Lost connection to server %s: %s", nntp_server, syserr());
	nntp_close_server();
    }
}

/*
 * find_server: Find out which host to use as NNTP server.
 *
 * 	This is done by consulting the file NNTP_SERVER (defined in
 * 	config.h).  Set nntp_server[] to the host's name.
 */

static void find_server()
{
    char *cp, *name, *getenv();
    char buf[BUFSIZ];
    FILE *fp;

    /*
     * This feature cannot normally be enabled, because the database and
     * the users rc file contains references to articles by number, and
     * these numbers are not unique across NNTP servers.
     */
#ifdef DEBUG
    if ((cp = getenv("NNTPSERVER")) != NULL) {
	strncpy(nntp_server, cp, sizeof nntp_server);
	return;
    }
#endif /* DEBUG */

    name = NNTP_SERVER;
    if (*name != '/')
	name = relative(lib_directory, name);

    if ((fp = open_file(name, OPEN_READ)) != NULL) {
	while (fgets(buf, sizeof buf, fp) != 0) {
	    if (*buf == '#' || *buf == '\n')
		continue;
	    if ((cp = strchr(buf, '\n')) != 0)
		*cp = '\0';
	    strncpy(nntp_server, buf, sizeof nntp_server);
	    fclose(fp);
	    return;
	}
	fclose(fp);
    }

    if (who_am_i != I_AM_MASTER)
	printf("\nCannot find name of NNTP server.\nCheck %s\n", name);

    sys_error("Failed to find name of NNTP server!");
}

/*
 * get_server_line: get a line from the server.
 *
 * 	Expects to be connected to the server.
 * 	The line can be any kind of line, i.e., either response or text.
 */

static get_server_line(string, size)
    char *string;
    int size;
{
    register char *cp, *nl;

    if (fgets(string, size, nntp_in) == NULL) {
	io_error();
	return -1;
    }
    for (cp = string, nl = NULL; *cp != NUL; cp++) {
	if (*cp == CR) {
	    nl = cp;
	    break;
	}
	if (nl == NULL && *cp == NL)
	    nl = cp;
    }
    if (nl != NULL) *nl = NUL;

    return 0;
}

/*
 * get_server: get a response line from the server.
 *
 * 	Expects to be connected to the server.
 * 	Returns the numerical value of the reponse, or -1 in case of errors.
 */

static get_server(string, size)
    char *string;
    int size;
{
    if (get_server_line(string, size) < 0)
	return -1;

    if (nntp_debug) debug_msg("<<<", string);

    return isdigit(*string) ? atoi(string) : 0;
}

/*
 * get_socket:  get a connection to the nntp server.
 *
 * Errors can happen when YP services or DNS are temporarily down or
 * hung, so we log errors and return failure rather than exitting if we
 * are the master.  The effects of retrying every 15 minutes (or whatever
 * the -r interval is) are not that bad.  Dave Olson, SGI
 */

static get_socket()
{
    int s;
    struct sockaddr_in sin;
#ifndef EXCELAN
    struct servent *getservbyname(), *sp;
    struct hostent *gethostbyname(), *hp;

#ifdef h_addr
    int     x = 0;
    register char **cp;
#endif

    if ((sp = getservbyname("nntp", "tcp")) ==  NULL)
	return sys_warning("nntp/tcp: Unknown service.\n");

    s = who_am_i == I_AM_MASTER ? 10 : 2;
    while ((hp = gethostbyname(nntp_server)) == NULL) {
	if (--s < 0) goto host_err;
	sleep(10);
    }

    bzero((char *) &sin, sizeof(sin));
    sin.sin_family = hp->h_addrtype;
    sin.sin_port = sp->s_port;

#else /* EXCELAN */
    char *machine;

    bzero((char*) &sin, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(0);
#endif  /* EXCELAN */

#ifdef h_addr
    /* get a socket and initiate connection -- use multiple addresses */

    s = x = -1;
    for (cp = hp->h_addr_list; cp && *cp; cp++) {
	s = socket(hp->h_addrtype, SOCK_STREAM, 0);
	if (s < 0) goto sock_err;
	bcopy(*cp, (char *)&sin.sin_addr, hp->h_length);

	x = connect(s, (struct sockaddr *)&sin, sizeof (sin));
	if (x == 0)
	    break;
	if (who_am_i != I_AM_MASTER)
	    msg("Connecting to %s failed: %s", nntp_server, syserr());
	(void) close(s);
	s = -1;
    }
    if (x < 0)
	sys_warning("Giving up on NNTP server %s!", nntp_server);
#else					/* no name server */
#ifdef EXCELAN
    if ((s = socket(SOCK_STREAM, NULL, &sin, SO_KEEPALIVE)) < 0)
	goto sock_err;
    
    sin.sin_port = htons(IPPORT_NNTP);
    machine = nntp_server;
    if ((sin.sin_addr.s_addr = rhost(&machine)) == -1) {
	(void) close(s);
	goto host_err;
    }

    /* And then connect */
    if (connect(s, &sin) < 0)
	goto conn_err;
#else /* not EXCELAN */
    if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	goto sock_err;
    
    /* And then connect */
    bcopy(hp->h_addr, (char *) &sin.sin_addr, hp->h_length);
    if (connect(s, (struct sockaddr *) &sin, sizeof(sin)) < 0)
	goto conn_err;
#endif  /* EXCELAN */
#endif
    return s;

 host_err:
    sys_warning("NNTP server %s unknown.\n", nntp_server);
    return -1;

 sock_err:
    sys_warning("Can't get NNTP socket: %s", syserr());
    return -1;

 conn_err:
    (void) close(s);
    if (who_am_i == I_AM_MASTER)
	sys_warning("Connecting to %s failed: %s", nntp_server, syserr());
    return -1;
}

/*
 * connect_server: initialise a connection to the nntp server.
 *
 * 	It expects nntp_server[] to be set previously, by a call to
 * 	nntp_check.  It is called from nntp_get_article() and
 *	nntp_get_active() if there is no established connection.
 */

static connect_server()
{
    int sockt_rd, sockt_wr;
    int response;
    char line[NNTP_STRLEN];
    
    if (who_am_i != I_AM_MASTER && !silent)
	msg("Connecting to NNTP server %s ... ", nntp_server);

    nntp_failed = 1;
    is_connected = 0;

    sockt_rd = get_socket();
    if (sockt_rd < 0)
	return -1;

    if ((nntp_in = fdopen(sockt_rd, "r")) == NULL) {
	close(sockt_rd);
        return -1;
    }
    sockt_wr = dup(sockt_rd);
    if ((nntp_out = fdopen(sockt_wr, "w")) == NULL) {
	close(sockt_wr);
	fclose(nntp_in);
        nntp_in = NULL;               /* from above */
        return -1;
    }

    /* Now get the server's signon message */
    response = get_server(line, sizeof(line));

    if (who_am_i == I_AM_MASTER) {
	if (response != OK_CANPOST && response != OK_NOPOST) {
	    log_entry('N', "Failed to connect to NNTP server");
	    log_entry('N', "Response: %s", line);
	    fclose(nntp_out);
	    fclose(nntp_in);
	    return -1;
	}
    } else {
	switch (response) {
	case OK_CANPOST:
	    can_post = 1;
	    break;
	case OK_NOPOST:
	    can_post = 0;
	    break;
	default:
	    user_error(line);
	    /* NOTREACHED */
	}
    }
    if (who_am_i != I_AM_MASTER && !silent)
	msg("Connecting to NNTP server %s ... ok (%s)",
	    nntp_server, can_post ? "posting is allowed" : "no posting");

    is_connected = 1;
    group_is_set = 0;
    nntp_failed = 0;
    try_again = 0;
    return 0;
}


/*
 * put_server:  send a line to the nntp server.
 *
 * 	Expects to be connected to the server.
 */

static put_server(string)
    char *string;
{
    if (nntp_debug) debug_msg(">>>", string);

    fprintf(nntp_out, "%s\r\n", string);
    if (fflush(nntp_out) == EOF) {
	io_error();
	return -1;
    }
    return 0;
}

/*
 * ask_server:  ask the server a question and return the answer.
 *
 *	Expects to be connected to the server.
 *	Returns the numerical value of the reponse, or -1 in case of
 *	errors.
 *	Contains some code to handle server timeouts intelligently.
 */

/* LIST XXX return fatal ERR_FAULT code if requested list does not exist */
/* This is only fatal for LIST ACTIVE -- else change to ERR_NOGROUPS */
static int fix_list_response = 0;

/*VARARGS*/
static ask_server(va_alist)
va_dcl
{
    char buf[NNTP_STRLEN];
    char *fmt;
    int response;
    int fix_err;
    use_vararg;

    fix_err = fix_list_response;
    fix_list_response = 0;

    start_vararg;
    fmt = va_arg1(char *);
    vsprintf(buf, fmt, va_args2toN);
    end_vararg;

    if (put_server(buf) < 0)
	return -1;
    response = get_server(buf, sizeof(buf));

    /*
     * Handle the response from the server.  Responses are handled as
     * followes:
     *
     * 100-199	Informational.  Passed back. (should they be ignored?).
     * 200-299	Ok messages.  Passed back.
     * 300-399	Ok and proceed.  Can not happen in nn.
     * 400-499	Errors (no article, etc).  Passed up and handled there.
     * 500-599	Fatal NNTP errors.  Handled below.
     */
    if (response == ERR_GOODBYE || response > ERR_COMMAND) {
	if (fix_err && response == ERR_FAULT) return ERR_NOGROUP;

	nntp_failed = 1;
	nntp_close_server();

	if (response != ERR_TIMEOUT) {	/* if not timeout, complain */
	    sys_error("NNTP %s response: %d", buf, response);
	    /* NOTREACHED */
	}
	try_again = 1;
	group_is_set = 0;
    }
    return response;
}

/*
 * copy_text: copy text response into file.
 *
 * 	Copies a text response into an open file.
 *	Return -1 on error, 0 otherwise.  It is treated as an error, if
 *	the returned response it not what was expected.
 */

static int last_copy_blank;

static copy_text(fp)
register FILE *fp;
{
    char buf[NNTP_STRLEN];
    register char *cp;
    register int nlines;

    nlines = 0;
    last_copy_blank = 0;
    while (get_server_line(buf, sizeof buf) >= 0) {
	cp = buf;
	if (*cp == '.')
	    if (*++cp == '\0') {
		if (nlines <= 0) break;
		if (nntp_debug) {
		    sprintf(buf, "%d lines", nlines);
		    debug_msg("COPY", buf);
		}
		return 0;
	    }
	fputs(cp, fp);
	last_copy_blank = (*cp == NUL);
	putc('\n', fp);
	nlines++;
    }
    fclose(fp);
    if (nntp_debug) debug_msg("COPY", "EMPTY");
    return -1;
}


static do_set_group()
{
    int n;

    switch (n = ask_server("GROUP %s", group_hd->group_name)) {
     case OK_GROUP:
	group_is_set = 1;
	return 1;

     case ERR_NOGROUP:
	log_entry('N', "NNTP: group %s not found", group_hd->group_name);
	return -1;

     default:
	if (try_again) return 0;	/* Handle nntp server timeouts */
	break;
    }
    if (!nntp_failed) {
	log_entry('N', "GROUP %s response: %d", group_hd->group_name, n);
	nntp_failed = 1;
    }
    return -1;
}

/*
 * The following functions implements a simple lru cache of recently
 * accessed articles.  It is a simple way to improve effeciency.  Files
 * must be kept by name, because the rest of the code expects to be able
 * to open an article multiple times, and get separate file pointers.
 */

struct cache {
    char		*file_name;	/* file name */
    article_number	art;		/* article stored in file */
    group_header	*grp;		/* from this group */
    unsigned		time;		/* time last accessed */
} cache[NNTPCACHE];

static unsigned time_counter = 1;		/* virtual time */

/*
 * search_cache: search the cache for an (article, group) pair.
 *
 * 	Returns a pointer to the slot where it is, null otherwise
 */

static struct cache *search_cache(art, gh)
    article_number art;
    group_header *gh;
{
    struct cache *cptr = cache;
    int i;

    if (who_am_i == I_AM_MASTER) return NULL;

    if (nntp_cache_size > NNTPCACHE) nntp_cache_size = NNTPCACHE;

    for (i = 0; i < nntp_cache_size; i++, cptr++)
	if (cptr->art == art && cptr->grp == gh) {
	    cptr->time = time_counter++;
	    return cptr;
	}
    return NULL;
}

/*
 * new_cache_slot: get a free cache slot.
 *
 * 	Returns a pointer to the allocated slot.
 * 	Frees the old filename, and allocates a new, unused filename.
 *	Cache files can also stored in a common directory defined in
 *	~/.nn or CACHE_DIRECTORY if defined in config.h.
 */

static struct cache *new_cache_slot()
{
    register struct cache *cptr = cache;
    int i, lru;
    unsigned min_time = time_counter;
    char name[FILENAME];

    if (nntp_cache_dir == NULL) {
#ifdef CACHE_DIRECTORY
	nntp_cache_dir = CACHE_DIRECTORY;
#else
	if (who_am_i == I_AM_MASTER)
	    nntp_cache_dir = db_directory;
	else
	    nntp_cache_dir = nn_directory;
#endif
    }

    if (who_am_i == I_AM_MASTER) {
	cptr = &cache[0];
	if (cptr->file_name == NULL)
	    cptr->file_name = mk_file_name(nntp_cache_dir, "master_cache");
	return cptr;
    }

    for (i = 0; i < nntp_cache_size; i++, cptr++)
	if (min_time > cptr->time) {
	    min_time = cptr->time;
	    lru = i;
	}
    cptr = &cache[lru];

    if (cptr->file_name == NULL) {
	sprintf(name, "%s/nn-%d.%02d~", nntp_cache_dir, process_id, lru);
	cptr->file_name = copy_str(name);
    } else
	unlink(cptr->file_name);

    cptr->time = time_counter++;
    return cptr;
}

/*
 * clean_cache: clean up the cache.
 *
 * 	Removes all allocated files.
 */

static void clean_cache()
{
    struct cache *cptr = cache;
    int i;

    for (i = 0; i < nntp_cache_size; i++, cptr++)
	if (cptr->file_name)
	    unlink(cptr->file_name);
}

/*
 * nntp_check: Find out whether we need to use NNTP.
 *
 * 	This is done by comparing the NNTP servers name with whatever
 * 	gethostname() returns.
 *	use_nntp and news_active are initialised as a side effect.
 */

nntp_check()
{
    char host[128];

    if (nntp_local_server) return;

    find_server();
    gethostname(host, sizeof host);
    use_nntp = strcmp(host, nntp_server) != 0; /* too simplistic ??? */

    if (use_nntp) {
	freeobj(news_active);
	news_active = mk_file_name(db_directory, "ACTIVE");
    }
}

/*
 * nntp_no_post: Check to see whether posting is allowed.
 */

nntp_no_post()
{
    if (!is_connected && connect_server() < 0)
	return 1;			/* If we cannot connect, neither can inews */
    if (can_post == 0) {
	msg("NNTP server does not allow postings from this host.  Sorry!");
	return 1;
    }
    return 0;
}


/*
 * nntp_set_group: set the server's current group.
 *
 * 	Actual communication is delayed until an article is accessed, to
 * 	avoid unnecessary traffic.
 */

nntp_set_group(gh)
    group_header *gh;
{
    group_hd = gh;
    group_is_set = 0;
    return 0;
}

/*
 * nntp_get_active:  get a copy of the active file.
 *
 * 	If we are the master get a copy of the file from the nntp server.
 * 	nnadmin just uses the one we already got.  In this way the master
 *	can maintain a remote copy of the servers active file.
 *	We try to be a little smart, if not inefficient, about the
 *	modification times on the local active file.
 *	Even when the master is running on the nntp server, a separate
 *	copy of the active file will be made for access via NFS.
 */

nntp_get_active()
{
    FILE *old, *new;
    char bufo[NNTP_STRLEN], bufn[NNTP_STRLEN];
    char *new_name;
    int same, n;

    if (who_am_i != I_AM_MASTER)
	return access(news_active, 4);

 again:
    if (!is_connected && connect_server() < 0)
	return -1;

    new_name = mktemp(relative(db_directory, ".actXXXXXX"));

    switch (n = ask_server("LIST")) {
     case OK_GROUPS:
	new = open_file(new_name, OPEN_CREATE_RW|MUST_EXIST);
	if (copy_text(new) == 0) {
	    if (fflush(new) != EOF) break;
	    fclose(new);
	}
	unlink(new_name);
	if (!nntp_failed) {
	    log_entry('N', "LIST empty");
	    nntp_failed = 1;
	}
	return -1;
     default:
	if (try_again) goto again; /* Handle nntp server timeouts */
	log_entry('N', "LIST response: %d", n);
	return -1;
    }

    rewind(new);
    same = 0;
    if ((old = open_file(news_active, OPEN_READ)) != NULL) {
	do {
	    fgets(bufo, sizeof bufo, old);
	    fgets(bufn, sizeof bufn, new);
	} while (!feof(old) && !feof(new) && strcmp(bufo, bufn) == 0);
	same = feof(old) && feof(new);
	fclose(old);
    }
    fclose(new);

    if (same)
	unlink(new_name);
    else
	if (rename(new_name, news_active) != 0)
	    sys_error("Cannot rename %s to %s", new_name, news_active);

    return 0;
}

/*
 * nntp_get_newsgroups:  get a copy of the newsgroups file.
 *
 *	Use the "LIST NEWSGROUPS" command to get the newsgroup descriptions.
 *	Based on code from: olson%anchor.esd@sgi.com (Dave Olson)
 */

FILE *nntp_get_newsgroups()
{
    char *new_name;
    FILE *new = NULL;
    int n;

    new_name = mktemp(relative(tmp_directory, "nngrXXXXXX"));
    new = open_file(new_name, OPEN_CREATE_RW|OPEN_UNLINK);
    if (new == NULL) return NULL;

 again:
    if (!is_connected && connect_server() < 0) goto err;

    fix_list_response = 1;
    switch (n = ask_server("LIST NEWSGROUPS")) {
     case ERR_NOGROUP:		/* really ERR_FAULT */
	goto err;

     case OK_GROUPS:
	if (copy_text(new) == 0) {
	    if (fflush(new) != EOF) break;
	    fclose(new);
	}
	if (!nntp_failed) {
	    log_entry('N', "LIST NEWSGROUPS empty");
	    nntp_failed = 1;
	}
	return NULL;

     default:
	if (try_again) goto again; /* Handle nntp server timeouts */
	log_entry('N', "LIST NEWSGROUPS response: %d", n);
	goto err;
    }
    rewind(new);
    return new;

 err:
    fclose(new);
    return NULL;
}

/*
 * nntp_get_article_list: get list of all article numbers in group
 *
 * 	Sends XHDR command to the server, and parses the following
 *	text response to get a list of article numbers which is saved
 *	in a list and returned.
 *	Return NULL on error.  It is treated as an error, if
 *	the returned response it not what was expected.
 */

static article_number *article_list = NULL;
static long art_list_length = 0;

static sort_art_list(f1, f2)
register article_number *f1, *f2;
{
    return (*f1 < *f2) ? -1 : (*f1 == *f2) ? 0 : 1;
}

article_number *nntp_get_article_list(gh)
group_header *gh;
{
    char buf[NNTP_STRLEN];
    register article_number *art;
    register char *cp;
    register long count = 0;	/* No. of completions plus one */
    int n;
    static int try_listgroup = 1;

 again:
    if (!is_connected && connect_server() < 0)
	return NULL;

    /* it is really an extreme waste of time to use XHDR since all we	*/
    /* are interested in is the article numbers (as we do locally).	*/
    /* If somebody hacks up an nntp server that understands LISTGROUP	*/
    /* they will get much less load on the nntp server			*/
    /* It should simply return the existing article numbers is the group*/
    /* -- they don't even have to be sorted (only XHDR needs that)	*/

    if (try_listgroup) {
	switch (n = ask_server("LISTGROUP %s", group_hd->group_name)) {
	 case OK_GROUP:
	    break;
	 default:
	    if (try_again) goto again; /* Handle nntp server timeouts */
	    log_entry('N', "LISTGROUP response: %d", n);
	    return NULL;
	 case ERR_COMMAND:
	    try_listgroup = 0;
	    goto again;	/* error may have closed down server connection */
	}
    }
    if (!try_listgroup) {
	if (group_is_set == 0)
	    switch (do_set_group()) {
	     case -1:
		return NULL;
	     case 0:
		goto again;
	     case 1:
		break;
	    }

	switch (n = ask_server("XHDR message-id %ld-%ld",
		(long)gh->first_db_article, (long)gh->last_db_article)) {
	 case OK_HEAD:
	    break;
	 default:
	    if (try_again) goto again; /* Handle nntp server timeouts */
	    log_entry('N', "XHDR response: %d", n);
	    return NULL;
	 case ERR_COMMAND:
	    nntp_failed = 2;
	    return NULL;
	}
    }

    count = 0;
    art = article_list;
    
    while (get_server_line(buf, sizeof buf) >= 0) {
	cp = buf;
	if (*cp == '.' && *++cp == '\0') break;

	if (count == art_list_length) {
	    art_list_length += 250;
	    article_list = resizeobj(article_list, article_number, art_list_length + 1);
	    art = article_list + count;
	}
	*art++ = atol(cp);
	count++;
    }

    if (article_list != NULL) {
	*art = 0;
	if (try_listgroup && count > 1)
	    quicksort(article_list, count, article_number, sort_art_list);
    }
    return article_list;
}

/*
 * nntp_get_article: get an article from the server.
 *
 * 	Returns a FILE pointer.
 *	If necessary the server's current group is set.
 *	The article (header and body) are copied into a file, so they
 *	are seekable (nn likes that).
 */

static char *mode_cmd[] = {
    "ARTICLE",
    "HEAD",
    "BODY"
};

FILE *nntp_get_article(article, mode)
article_number article;
int mode;	/* 0 => whole article, 1 => head only, 2 => body only */
{
    FILE *tmp;
    static struct cache *cptr;
    int n;
    
 again:
    if (!is_connected && connect_server() < 0) {
	return NULL;
    }

    /*
     * Set the server group to the current group
     */
    if (group_is_set == 0)
	switch (do_set_group()) {
	 case -1:
	    return NULL;
	 case 0:
	    goto again;
	 case 1:
	    break;
	}

    /*
     * Search the cache for the requested article, and allocate a new
     * slot if necessary (if appending body, we already got it).
     */

    if (mode != 2) {
	cptr = search_cache(article, group_hd);
	if (cptr != NULL) goto out;
	cptr = new_cache_slot();
    }
    
    /*
     * Copy the article.
     */
    switch (n = ask_server("%s %ld", mode_cmd[mode], (long)article)) {
     case OK_ARTICLE:
     case OK_HEAD:
	tmp = open_file(cptr->file_name, OPEN_CREATE|MUST_EXIST);
	if (copy_text(tmp) < 0)
	    return NULL;

	if (mode == 1 && !last_copy_blank)
	    fputc(NL, tmp); /* add blank line after header */

	if (fclose(tmp) == EOF) goto err;
	cptr->art = article;
	cptr->grp = group_hd;
	goto out;

     case OK_BODY:
	tmp = open_file(cptr->file_name, OPEN_APPEND|MUST_EXIST);
	fseek(tmp, (off_t)0, 2);
	if (copy_text(tmp) < 0)
	    return NULL;
	if (fclose(tmp) == EOF) goto err;
	goto out;
	
     case ERR_NOARTIG:
	return NULL;

     default:
	if (try_again) goto again; /* Handle nntp server timeouts */
	log_entry('N', "ARTICLE %ld response: %d", (long)article, n);
	nntp_failed = 1;
	return NULL;
    }

 out:
    return open_file(cptr->file_name, OPEN_READ|MUST_EXIST);

 err:
    sys_error('N', "Cannot write temporary file %s", cptr->file_name);
}

/*
 *	Return local file name holding article
 */

char *nntp_get_filename(art, gh)
article_number art;
group_header *gh;
{
    struct cache *cptr;

    cptr = search_cache(art, gh);

    return cptr == NULL ? NULL : cptr->file_name;
}

/*
 * nntp_close_server: close the connection to the server.
 */

nntp_close_server()
{
    if (!is_connected)
	return;

    if (!nntp_failed) {			/* avoid infinite recursion */
	int n;

	n = ask_server("QUIT");
	if (n != OK_GOODBYE)
	    ;				/* WHAT NOW ??? */
    }

    (void) fclose(nntp_out);
    (void) fclose(nntp_in);

    is_connected = 0;
}

/*
 * nntp_cleanup:  clean up after an nntp session.
 *
 *	Called from nn_exit().
 */

nntp_cleanup()
{
    if (is_connected)
	nntp_close_server();
    clean_cache();
}
#endif /* NNTP */

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.