/*
 * @(#)smtprelay.c--Relay smtp protocol with intervention in the DATA phase
 * @(#)STS/KBS 14feb2001
 *
 * usage: [-xn] filter_path real_server_path ... server args ...
 */

#include "smtprelay.h"

char	*iam;		/* Global process name (av[0]) */
char	msgbuf[4096];	/* General message buffer */
			/* Leave this an explicit array--We use sizeof here
			 * and there */

char	msg_354[] = "354 Enter Mail, end with '.'";
char	msg_451[] = "451 Local processing error--Try again later";
char	msg_552_template[] = "552 Message too big (>%d bytes)--Don't try again";
char	msg_552[512];
char	msg_noop[] = "NOOP";

/*
 * Funtion defs
 */
int	RelayProtocol(int,int,int,int,char *);
char	*FilterMessage(char *,char *,int *,char *,char *);
int	GetArticle(NEWS *, NEWS *, NEWS *, char *, int, int *);
void	PingServer(NEWS *, NEWS *);
void	usage(void);

/*
 * Mail buffer for incomming messages.
 * Do it all in memory, no tmp files
 *
 * Put this as the last data def... maybe the system will allocate
 * memory in the order we declare it and this will create the least
 * fragmentation and the large buffer, if not used, will only be
 * unused virtual memory
 */
int	mailbytes;
char	mailbuf[MAXMESSAGE];

/*
 * System constant--Ticks per second
 */
int	ticksPerSecond;

int
main(
    int		ac,
    char	*av[]
)
{
    int		serverrfd;		/* Server read fd */
    int		serverwfd;		/* Server write fd */
    int		pipe1[2],pipe2[2];	/* Tmp pipe fd's */
    int		serverpid;		/* Server pid */
    /* getopt stuff */
    int		oc;			/* current option char */
    int		on;			/* current string option number */
    extern	char *optarg;		/* stuff for getopt */
    extern	int optind, optopt;	/* stuff for getopt */

    iam = av[0];
    logId = "smtpr";

    setbuf(stderr,NULL);
    debugLevel = DEFAULTDEBUGLEVEL;

    on = 0;
    while(( oc = getopt(ac,av,"x:")) != -1 )
    {
	switch(oc)
	{
	 case 'x':
	    debugLevel = atoi(optarg);
	    break;
	 case ':':
	 case '?':
	 default:
	    usage();
	}
    }

    /* 
     * Adjust for remaining arguments
     */
    ac += optind;
    av += optind;

    if (ac < 2)
    {
	sprintf(msgbuf,"usage: %s filter-path smtp-server-path ...args...",iam);
	LogMsg(msgbuf);
	return(1);
    }

    /*
     * Dynamic message contents
     */
    sprintf(
	msg_552,
	msg_552_template,
	MAXMESSAGE
    );

    /*
     * System clock ticks
     */
    ticksPerSecond = (int)sysconf(_SC_CLK_TCK);
    dprintf(DEBUG_ACTIVE,(debugout,
	"Ticks per second = %d\n",
	ticksPerSecond
    ));

    /*
     * Ignore sigpipe's--We want an error if a pipe reader dies
     */
    signal(SIGPIPE,SIG_IGN);

    /*
     * Create two pipes for bi-directional communication with the
     * server process
     *
     * pipe 1
     *   0r	Server stdin
     *   1w	Relay write to server
     * pipe 2
     *   0r	Relay read from server
     *   1w	Server stdout
     */
    if( pipe(pipe1) != 0) {
	LogError("Can't create pipe 1");
    }
    if( pipe(pipe2) != 0) {
	LogError("Can't create pipe 2");
    }

    /*
     * Fork and exec the server
     *
     * Fork()
     * Diddle the pipes to setup stdin/out for the server
     * Exec the server.
     */
    switch( serverpid = fork() ) {
     case -1:	/* error */

	LogError("Couldn't fork");
	return(2);

     default:	/* parent */

	serverrfd = pipe2[0]; close(pipe2[1]);
	serverwfd = pipe1[1]; close(pipe1[0]);
	break;

     case 0:	/* child */

	/*
	 * Setup stdin/out from pipes
	 */
	close(0); dup(pipe1[0]); close(pipe1[0]); close(pipe1[1]);
	close(1); dup(pipe2[1]); close(pipe2[0]); close(pipe2[1]);
	/*
	 * Exec the server (av[1])
	 * Shift the arguments and use the new av[0] as the path
	 * to execute and pass on av[] as the arg list
	 */
	av += 1;
	execv(av[0],av);
	LogError(av[0]);
	return(2);
    }
    /* We only fall out of here from a successful parent */

    return( RelayProtocol(0,1,serverrfd,serverwfd,av[0]) );
}

int
RelayProtocol(
    int		netrfd,			/* Network read fd (0/stdin) */
    int		netwfd,			/* Network write fd (1/stdout) */
    int		serverrfd,		/* Server read fd */
    int		serverwfd,		/* Server write fd */
    char	*filter			/* Filter path */
)
{
    NEWS	*NI,*NO;		/* Network i/o */
    NEWS	*SI,*SO;		/* Server i/o */
    int		fd;			/* Current active fd (from select) */
    int		minfd, maxfd;		/* Minimum/Maximum select fd */
    fd_set	bmask;			/* Base selection mask */
    fd_set	rmask;			/* select mask */
    struct	timeval	tval;		/* select timeout */
    int		selres;			/* select result */
    char	netbuf[513];		/* network buffer */
    char	*np;			/* pointer in netbuf */
    int		rsize;			/* read size */
    int		wsize;			/* write size */
    int		state_data;		/* data state (0=no, 1=msg) */
    int		state_reset;		/* reset sate (0=no, 1=ign svr rsp) */
    int		ret;			/* misc return values */
    char	*filtermessage;		/* Return message from filter routine */
    char	*mailfrom;		/* 'MAIL FROM: ...' value */
    char	*rcptto;		/* 'RCPT TO: names */
    int		recycle;		/* flag for buffered data */

    dprintf(DEBUG_FUN,(debugout,
	"RelayProtocol(netrfd=%d, netwfd=%d, serverrfd=%d, serverwfd=%d)\n",
	netrfd,
	netwfd,
	serverrfd,
	serverwfd
    ));

    /*
     * Allocate and initialize network and server stream structures
     */
    if( (NI = (NEWS *)calloc(1,sizeof(NEWS))) == NULL)
    {
	LogMsg("Out of memory");
	return(2);
    }
    else
    {
	NI->fd = netrfd;
	NI->nsbp = NI->nsb;
	NI->nsbs = 0;
    }
    if( (NO = (NEWS *)calloc(1,sizeof(NEWS))) == NULL)
    {
	LogMsg("Out of memory");
	return(2);
    }
    else
    {
	NO->fd = netwfd;
	NO->nsbp = NO->nsb;
	NO->nsbs = 0;
    }
    if( (SI = (NEWS *)calloc(1,sizeof(NEWS))) == NULL)
    {
	LogMsg("Out of memory");
	return(2);
    }
    else
    {
	SI->fd = serverrfd;
	SI->nsbp = SI->nsb;
	SI->nsbs = 0;
    }
    if( (SO = (NEWS *)calloc(1,sizeof(NEWS))) == NULL)
    {
	LogMsg("Out of memory");
	return(2);
    }
    else
    {
	SO->fd = serverwfd;
	SO->nsbp = SO->nsb;
	SO->nsbs = 0;
    }

    /*
     * Setup selection mask and minimum/maximum fd for select
     */
    FD_ZERO(&bmask);
    FD_SET(NI->fd, &bmask);
    FD_SET(SI->fd, &bmask);
    minfd = (NI->fd < SI->fd ? NI->fd : SI->fd);
    maxfd = (NI->fd > SI->fd ? NI->fd : SI->fd);

    /*
     * Other inits
     */
    state_data = 0;		/* Not in a 'data' state initially */
    rcptto = mailfrom = NULL;

    while(1)
    {
	/*
	 * Setup timeout
	 */
	memset(&tval,'\0',sizeof(tval));
	tval.tv_sec = SELECT_TIMEOUT;
	tval.tv_usec = 0;

	/*
	 * Install the base read mask and wait for something to happen
	 */
	memcpy(&rmask, &bmask, sizeof(bmask));
	selres = select(maxfd+1,&rmask,0,0,&tval);

	/*
	 * Errors from select are fatal
	 */
	if (selres == -1) {
	    LogError("select()");
	    return(2);
	}

	/*
	 * Timeouts are fatal
	 */
	if (selres == 0) {
	    sprintf(
		msgbuf,
		"TIMEOUT: Connections inactive %d seconds",
		SELECT_TIMEOUT
	    );
	    LogMsg(msgbuf);
	    return(2);
	}

	/*
	 * Ok, we got something.
	 * Process any active descriptors.
	 *
	 * Recycle flag is set internally if one of the fd's has
	 * buffered data and needs to be reprocessed.
	 * This happens because the NewsGets() routine reads the
	 * network into a relativly large internal buffer.  If
	 * either the server or the connecting host sends more than
	 * one line, NewsGets() will return the first line and likely
	 * will have read some or all of the additional data into
	 * it's buffer.  When we detect buffered data, we will set
	 * the recycle flag and do an FD_ISSET so we will reprocess
	 * the additional data.
	 */
	recycle = 1;
	while (recycle)
	{
	    recycle = 0;
	    for(fd = minfd; fd <= maxfd; fd++)
	    {
		if (!FD_ISSET(fd, &rmask)) continue;

		dprintf(DEBUG_ACTIVE,(debugout,
		    "Select on fd %d\n",
		    fd
		));

		if (fd == NI->fd)
		{
		    /*
		     * DATA FROM NETWORK
		     */
		    if(
			NewsGets(
			    NI,
			    netbuf,
			    sizeof(netbuf),
			    &rsize,
			    NEWSGETS_LINE
			) != 0
		    )
		    {
			/*
			 * Blow out on any anomolies
			 */
			return(2);
		    }

		    dprintf(DEBUG_DETAIL,(debugout,
			"Read %d bytes from %d [%s]\n",
			rsize,
			fd,
			netbuf
		    ));

		    /*
		     * Buffered data--Setup for recycle
		     */
		    if (SI->nsbs > 0)
		    {
			FD_SET(fd,&rmask);
			recycle++;
		    }

		    /*
		     * Data from the network... Send back to the server
		     */

		    /*
		     * Check for "data" phase and intercept
		     */
		    if (strcasecmp(netbuf,"data") == 0)
		    {
			dlogmsg(DEBUG_ACTIVE,"Acking 'DATA' command");
			
			/*
			 * Ack 'data' locally--Fatal if error
			 */
			if(NewsPuts(NO,msg_354,sizeof(msg_354)-1,NEWSPUTS_CRLF))
			{
			    return(2);
			}
			if(
			    ( ret =
				GetArticle(
				    NI,
				    SI,
				    SO,
				    mailbuf,
				    sizeof(mailbuf),
				    &mailbytes
				)
			    ) != 1
			)
			{
			    if(ret == 0)
			    {
				/*
				 * If we overran the buffer, read
				 * the reset of the article and throw
				 * it away then reject the message
				 */
				while(
				    ! (
					ret =
					    GetArticle(
						NI,
						SI,
						SO,
						mailbuf,
						sizeof(mailbuf),
						&mailbytes
					    )
				    )
				);
				/*
				 * Nak the message with a too large
				 */
				if(
				    NewsPuts(
					NO,
					msg_552,
					strlen(msg_552),
					NEWSPUTS_CRLF
				    )
				)
				{
				    return(2);
				}
				/*
				 * Send a reset to the server
				 */
				sprintf(netbuf,"RSET");
				rsize = strlen(netbuf);
				state_reset = 1;
			    }
			    else
			    {
				return(2);		/* Other errors */
			    }
			}
			else
			{
			    /*
			     * Article read ok.
			     */
			    state_data = 1;
			    sprintf(
				msgbuf,
				"Received message (%d bytes)",
				mailbytes
			    );
			    dlogmsg(DEBUG_ACTIVE,msgbuf);
			    /*
			     * Ping the server before we start the filter
			     * just so everybody stays awake
			     */
			    PingServer(SI,SO);
			    /*
			     * Note! the original 'data' command is still in
			     * netbuf so we will fall out and let that get
			     * sent on to the server
			     */
			    filtermessage =
				FilterMessage(
				    filter,
				    mailbuf,
				    &mailbytes,
				    mailfrom,
				    rcptto
				);

			    dprintf(DEBUG_ACTIVE,(debugout,
				"Filter: %s\n",
				filtermessage ? filtermessage : "OK"
			    ));

			    if (filtermessage)
			    {
				if(
				    NewsPuts(
					NO,
					filtermessage,
					strlen(filtermessage),
					NEWSPUTS_CRLF
				    )
				)
				{
				    return(2);
				}
				/*
				 * Send a reset to the server
				 */
				sprintf(netbuf,"RSET");
				rsize = strlen(netbuf);
				state_reset = 1;
			    }
			}
		    }

		    /*
		     * Check for 'mail from:' and save the value
		     */
		    if (strncasecmp(netbuf,"mail from:",10) == 0)
		    {
			if (mailfrom)
			{
			    free(mailfrom);
			}
			if (rcptto)
			{
			    /*
			     * Also reset rcptto when we get a mailfrom
			     */
			    free(rcptto);
			    rcptto = NULL;
			}
			mailfrom = strdup(netbuf+10);
			dprintf(DEBUG_ACTIVE,(debugout,
			    "MAIL FROM: [%s]\n",
			    mailfrom
			));
		    }

		    /*
		     * Check for 'rcpt to:' and save the value
		     *
		     * If there is an existing value, append the new one
		     * (multiple addresses)
		     */
		    else if (strncasecmp(netbuf,"rcpt to:",8) == 0)
		    {
			if (rcptto)
			{
			    realloc(rcptto,strlen(rcptto)+strlen(netbuf)-8+3);
			    sprintf(rcptto+strlen(rcptto),", %s",netbuf+8);
			}
			else
			{
			    rcptto = strdup(netbuf+8);
			}
			dprintf(DEBUG_ACTIVE,(debugout,
			    "RCPT TO: [%s] -> [%s]\n",
			    netbuf+8,
			    rcptto
			));
		    }

		    if(NewsPuts(SO,netbuf,rsize,NEWSPUTS_CRLF)) {
			return(2);
		    }
		}

		else if (fd == SI->fd)
		{
		    /*
		     * DATA FROM SERVER
		     */
		    if(
			NewsGets(
			    SI,
			    netbuf,
			    sizeof(netbuf),
			    &rsize,
			    NEWSGETS_LINE
			) != 0
		    )
		    {
			/*
			 * Blow out on any anomolies
			 */
			return(2);
		    }

		    dprintf(DEBUG_DETAIL,(debugout,
			"Read %d bytes from %d [%s]\n",
			rsize,
			fd,
			netbuf
		    ));

		    /*
		     * Buffered data--Setup for recycle
		     */
		    if (SI->nsbs > 0)
		    {
			FD_SET(fd,&rmask);
			recycle++;
		    }

		    /*
		     * If we are in a server reset state, ignore the
		     * server response.
		     *
		     * We are here if we failed the message internally
		     * (virus check, size, etc...) and we had issued
		     * a reset (RSET) to the server.  The network isn't
		     * expecting a response at this point so we just throw
		     * it away.
		     */
		    if (state_reset == 1)
		    {
			state_reset = 0;
			continue;
		    }

		    /*
		     * If we are in a 'data' state this should be the
		     * server response to the 'data' comamnd.
		     * If it is an "ok" response (354) then send the
		     * spooled data on to the server.
		     *
		     * Clear the data state.  Whatever comes back from
		     * the server will be passed back to the network
		     * as the final message response.
		     */
		    if (state_data == 1)
		    {
			dlogmsg(DEBUG_ACTIVE,"Data state 1: forwarding message");

			state_data = 0;
			if (strncmp(netbuf,"354",3) == 0)
			{
			    /*
			     * Send the message on to the server.
			     * The server's response will go back to the 
			     * network in the normal loop.
			     */
			    if(NewsPuts(SO,mailbuf,mailbytes,NEWSPUTS_NONE)) {
				return(2);
			    }

			    /*
			     * Don't send anything back now.  Just go back
			     * into the normal loop and wait for a server
			     * response
			     */
			    continue;
			}
			/*
			 * If we got a bad return code from the DATA command
			 * (from the server), let it go back to the network
			 *
			 * The network is really waiting for a response to the
			 * message (not the 'DATA' command, which we already
			 * acked) but the codes are close enough that it should
			 * fly.
			 */
		    }
		    
		    /*
		     * Data from the server... Send back to network
		     */
		    if(NewsPuts(NO,netbuf,rsize,NEWSPUTS_CRLF)) {
			return(2);
		    }
		}
		else
		{
		    /*
		     * Unexpected descriptor--Fatal
		     */
		    sprintf(
			msgbuf,
			"Unexpected i/o on socket %d",
			fd
		    );
		    LogMsg(msgbuf);
		    return(2);
		}
	    }
	}
    }
    /* NOTREACHED */
}

/*
 * Filter the message
 *
 * Invokes the filter program (must be a full path name) with one arg
 *    mailfrom value
 *
 * The current message is sent to the filter on stdin.
 * The filter is expected to return the message on it's stdout.
 *
 * Return codes (from the filter or from this routine)
 *	90	message is ok
 *	91	message contains a virus
 *	99	message is bad (for some reason)
 *	100	filter failed
 *
 * Return from this routine
 *	message response
 *	   250	message is ok
 *	   451	local error--Try later
 *	   452	no storage--try later
 *	   552	mailbox exceeded storage limits--fatal
 *	   554	transaction failed--fatal
 *
 */
char *
FilterMessage(
    char	*filter,		/* Filter path name */
    char	*message,		/* Message buffer */
    int		*msglength,		/* Length of message */
    char	*mailfrom,		/* 'mail from: ' value */
    char	*rcptto			/* 'rcpt to:' value(s) */
)
{
    int		pipe1[2], pipe2[2];	/* Pipes to filter process */
    int		filterpid;		/* Filter pid */
    int		filterstat;		/* filter termination status */
    int		filterrfd, filterwfd;	/* Filter read/write descriptors */
    int		i;			/* misc var */
    int		wsize;			/* write size */
    int		rsize;			/* read size */
    char	*p;			/* misc pointer */
    char	*rval;			/* return value */

    static	char rbuf[512];		/* return message buffer */

    dprintf(DEBUG_FUN,(debugout,
	"FilterMessage(filter='%s', message=%p, msglength=%d, mailfrom=%s, rcptto=%s)\n",
	filter,
	message,
	*msglength,
	mailfrom ? mailfrom : "(null)",
	rcptto ? rcptto : "(null)"
    ));

    /*
     * Create two pipes for bi-directional communication with the
     * server process
     *
     * pipe 1
     *   0r	Filter stdin
     *   1w	Relay write to filter
     * pipe 2
     *   0r	Relay read from filter
     *   1w	Filter stdout
     */
    if( pipe(pipe1) != 0) {
	LogError("Can't create filter pipe 1");
	return(msg_451);
    }
    if( pipe(pipe2) != 0) {
	LogError("Can't create filter pipe 2");
	close(pipe1[0]);
	close(pipe1[1]);
	return(msg_451);
    }

    /*
     * Fork and exec the filter
     *
     * Fork()
     * Diddle the pipes to setup stdin/out for the server
     * Exec the server.
     */
    switch( filterpid = fork() ) {
     case -1:	/* error */

	LogError("Couldn't fork filter");
	rval = msg_451;
	goto done;

     default:	/* parent */

	filterrfd = pipe2[0]; close(pipe2[1]);
	filterwfd = pipe1[1]; close(pipe1[0]);
	break;

     case 0:	/* child */

	/*
	 * Setup stdin/out from pipes
	 */
	close(0); dup(pipe1[0]); close(pipe1[0]); close(pipe1[1]);
	close(1); dup(pipe2[1]); close(pipe2[0]); close(pipe2[1]);
	/*
	 * Exec the filter
	 */
	execl(filter,filter,mailfrom,rcptto,(char *)0);
	LogError(filter);
	exit(100);
    }
    /* Good parent if we fall out here */

    /*
     * Send message to filter
     */
    p = message;
    i = *msglength;
    while ( i > 0 )
    {
	if ( (wsize = write(filterwfd,p,i)) <= 0 )
	{
	    if (wsize == 0)
	    {
		LogMsg("Unexpected EOF on write to filter");
	    }
	    else
	    {
		LogError("Write to filter");
	    }
	    rval = msg_451;
	    goto done;
	}
	p += wsize;
	i -= wsize;
    }
    close(filterwfd); filterwfd = -1;
    dlogmsg(DEBUG_ACTIVE,"Message sent to filter");

    /*
     * Get the message back from the filter
     */
    p = message;
    i = MAXMESSAGE;
    while( i > 0 )
    {
	if ( (rsize = read(filterrfd,p,i)) <= 0 )
	{
	    if (rsize == 0)
	    {
		/*
		 * EOF--ok, break out of read loop
		 */
		break;
	    }
	    else
	    {
		LogError("Read from filter");
	    }
	    rval = msg_451;
	    goto done;
	}
	p += rsize;
	i -= rsize;
    }

    /*
     * Check if the filtered message is now too big
     *
     * Read a single char.  If not EOF we exceeded our message
     * maximum.  Read and discard the rest of the message and
     * return a too-big error.
     */
    i = p - message;		/* current size */
    rsize = read(filterrfd,msgbuf,1);
    if (rsize > 0) {
	/*
	 * Message is too big.  Read and discard the rest
	 */
	i += rsize;
	while((rsize = read(filterrfd,msgbuf,sizeof(msgbuf))) > 0) i += rsize;
/* UNTESTED */
	sprintf(
	    msgbuf,
	    "Message returned from filter is too big (%d > %d)",
	    i,
	    MAXMESSAGE
	);
	LogMsg(msgbuf);
	rval = msg_552;
	goto done;
    }
    *msglength = i;

    dprintf(DEBUG_ACTIVE,(debugout,
	"%d bytes received back from the filter\n",
	i
    ));

    /*
     * Wait for the filter to exit
     *
     * Ignore errors in wait (shouldn't happen)
     */
    if ( (i=waitpid(filterpid,&filterstat,0)) == filterpid )
    {
	if(WIFSIGNALED(filterstat))
	{
	    sprintf(
		msgbuf,
		"Filter crashed with signal %d",
		WTERMSIG(filterstat)
	    );
	    LogMsg(msgbuf);
	    sprintf(
		rbuf,
		"451 Local filter error (c%d)--Possibly temporary",
		WTERMSIG(filterstat)
	    );
	    rval = rbuf;
	    goto done;
	}
    }
    else
    {
	filterstat = 0;
    }

    rval = rbuf;
    switch(WEXITSTATUS(filterstat)) {
     case 90:	/* ok */
	
	rval = NULL;		/* No return message--good */
	break;

     case 91:	/* virus */

	sprintf(
	    rbuf,
	    "554 Message contains a VIRUS!!!--Don't try again"
	);
	break;

     case 99:	/* mail was bad */

	sprintf(
	    rbuf,
	    "554 Message was rejected for unspecified reasons--Don't try again"
	);
	break;

     case 100:	/* filter failure (assumed temporary) */
    
	sprintf(
	    rbuf,
	    "451 Local filter error (100)--Possibly temporary"
	);
	break;

     default:	/* other exit codes are fatal */
    
	sprintf(
	    rbuf,
	    "451 Local filter error (%d)--Possibly temporary",
	    WEXITSTATUS(filterstat)
	);
	break;

    }

done:
    close(pipe1[0]);
    close(pipe1[1]);
    close(pipe2[0]);
    close(pipe2[1]);
    return(rval);
}

/*
 * Make a guess at the network throughput then do controlled size
 * reads from the netowork.  After each read, if multiple reads
 * are necessare, hit the server with a NOOP command as a keepalive
 * so it doesn't disconnect during long processing.
 *
 * Normal timeout for a response is five minutes (300 seconds).
 * Read sizes are computed to ping the server every 30 seconds.
 * This should give us plenty of leeway for varying network loads
 * and data compressability.
 */
int
GetArticle(
    NEWS	*NI,		/* Network input stream */
    NEWS	*SI,		/* Server input stream */
    NEWS	*SO,		/* Server output stream */
    char	*abuf,		/* Article buffer */
    int		abufsize,	/* Article buffer size */
    int		*asize		/* Pointer to storage for art sz--NULL ok*/
)
{
    int		ret;
    char	*ap;		/* Current offset in the article buffer */
    int		as;		/* Article size to read */
    int		rsize;		/* Amount read */
    int		tread;		/* Total read */
    int		ttr;		/* Tmp total read */
    struct	tms tms;	/* times() buffer--Not used */
    clock_t	stime;		/* Start time of the read */
    clock_t	etime;		/* End time of the read */
    int		cps;		/* Computed characters per second */

    dprintf(DEBUG_FUN,(debugout,
	"GetArticle(NI=%p, SI=%p, SO=%p, abuf=%p, abufsize=%d, *asize=%p)\n",
	NI,
	SI,
	SO,
	abuf,
	abufsize,
	asize
    ));

    tread = 0;
    ap = abuf;
    as = 30000;		/* Initial estimate 30 sec @ 1000 cps */
    do {
	stime = times(&tms);
	ttr = 0;
	/*
	 * Set the read size to the computed 60 second buffer
	 * or whatever is left.  Whichever is smaller.
	 */
	rsize = abufsize - (ap - abuf);
	if (rsize > as) rsize = as;
	/*
	 * Get the article (or part)
	 */
	ret =
	    NewsGets(
		NI,
		ap,
		rsize,
		&ttr,
		NEWSGETS_ARTICLE
	    );
	tread += ttr;
	ap += ttr;
	if (ret == 0)
	{
	    /*
	     * We reached our read limit without finding the end of the
	     * article.  If we really totally filled the article buffer
	     * then return--The article is too big.
	     * If we only filled a partial read, compute our throughput,
	     * ping the server, and start a new read based on our
	     * throughput.
	     */
	    if (tread >= abufsize)
	    {
		/*
		 * We filled the article buffer without reaching the
		 * end of the article.  Pass back NewsGets() return
		 * code
		 */
		goto done;
	    }
	    /*
	     * Compute the throughput and the next read size
	     * The read size is computed to finish in one minute
	     * so the net effect will be to ping the server once
	     * every 30 seconds while we are reading the message.
	     */
	    etime = times(&tms);
	    cps = ( ttr * ticksPerSecond) / (etime - stime);
	    dprintf(DEBUG_ACTIVE,(debugout,
		"%d bytes in %d ticks = %d cps\n",
		ttr,
		etime-stime,
		cps
	    ));
	    if (cps < 1000) cps = 1000;
	    as = cps * 30;
	    /*
	     * Ping the server to keep it alive
	     * Ignore any errors... Something else will blow up if the
	     * server dies.
	     */
	    PingServer(SI,SO);
	}
    } while (ret == 0);

    /*
     * Set the read and pass back the last return code
     */
done:
    if (asize) *asize = tread;
    return(ret);
}

void
PingServer(
    NEWS	*SI,		/* Server input stream */
    NEWS	*SO		/* Server output stream */
)
{
    char	netbuf[513];		/* network buffer */

    if(NewsPuts(SO,msg_noop,sizeof(msg_noop)-1,NEWSPUTS_CRLF) == 0)
    {
	if (
	    NewsGets(
		SI,
		netbuf,
		sizeof(netbuf),
		(int *)0,
		NEWSGETS_LINE
	    ) == 0
	)
	{
	    dlogmsg(DEBUG_ACTIVE,"Ping server: OK");
	}
	else
	{
	    dlogmsg(DEBUG_OP,"Ping server: FAIL ON SERVER READ");
	}
    }
    else
    {
	dlogmsg(DEBUG_OP,"Ping server: FAIL ON SERVER WRITE");
    }
}

void
usage(void)
{
    sprintf(
	msgbuf,
	"usage: %s [-xn] filter-path smtp-server-path ...args...",
	iam
    );
    LogMsg(msgbuf);
    exit(1);
    /* NOTREACHED */
}
