4.4BSD/usr/src/contrib/news/inn/backends/innxmit.c

Compare this file to the similar file:
Show the results in this format:

/*  $Revision: 1.14 $
**
**  Transmit articles to remote site.
*/
#include "configdata.h"
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <setjmp.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#if	defined(DO_NEED_TIME)
#include <time.h>
#endif	/* defined(DO_NEED_TIME) */
#include <sys/time.h>
#include <sys/uio.h>
#include "nntp.h"
#include "paths.h"
#include "logging.h"
#include "libinn.h"
#include "clibrary.h"
#include "qio.h"
#include "dbz.h"
#include "macros.h"


/*
**  Supported encoding schemes.
*/
typedef enum _MIMEXFERTYPE {
    MTnotmime,
    MTquotedprintable,
    MTbase64
} MIMEXFERTYPE;


#define OUTPUT_BUFFER_SIZE	(16 * 1024)


/*
** Syslog formats - collected together so they remain consistent
*/
STATIC char	STAT1[] =
	"%s stats offered %lu accepted %lu refused %lu rejected %lu";
STATIC char	STAT2[] = "%s times user %.3f system %.3f elapsed %.3f";
STATIC char	GOT_RESENDIT[] = "%s requeued %s %s";
STATIC char	CANT_CONNECT[] = "%s connect failed %s";
STATIC char	CANT_AUTHENTICATE[] = "%s authenticate failed %s";
STATIC char	IHAVE_FAIL[] = "%s ihave failed %s";


/*
**  Global variables.
*/
STATIC BOOL		AlwaysRewrite;
STATIC BOOL		Debug;
STATIC BOOL		DoRequeue = TRUE;
STATIC BOOL		Purging;
STATIC BOOL		Slavish;
STATIC BOOL		STATprint;
STATIC BOOL		Mime;
STATIC MIMEXFERTYPE	MimeArticle = MTnotmime;
STATIC char		*BATCHname;
STATIC char		*BATCHtemp;
STATIC char		*REMhost;
STATIC double		STATbegin;
STATIC double		STATend;
STATIC FILE		*BATCHfp;
STATIC int		FromServer;
STATIC int		ToServer;
STATIC QIOSTATE		*BATCHqp;
STATIC SIGVAR		GotAlarm;
STATIC SIGVAR		GotInterrupt;
STATIC SIGVAR		JMPyes;
STATIC jmp_buf		JMPwhere;
STATIC char		*REMbuffer;
STATIC char		*REMbuffptr;
STATIC char		*REMbuffend;
STATIC unsigned long	STATaccepted;
STATIC unsigned long	STAToffered;
STATIC unsigned long	STATrefused;
STATIC unsigned long	STATrejected;


/*
**  Find the history file entry for the Message-ID and return a file
**  positioned at the third field.
*/
STATIC FILE *
HistorySeek(MessageID)
    char		*MessageID;
{
    static char		History[] = _PATH_HISTORY;
    static FILE		*F;
    register char	*p;
    register char	*q;
    register int	i;
    datum		key;
    datum		val;
    OFFSET_T		offset;

    /* Open the history file. */
    if (F == NULL) {
	if (dbminit(History) < 0) {
	    (void)fprintf(stderr, "Can't set up \"%s\" database, %s\n",
		    History, strerror(errno));
	    exit(1);
	}
	if ((F = fopen(History, "r")) == NULL) {
	    (void)fprintf(stderr, "Can't open \"%s\" for reading, %s\n",
		    History, strerror(errno));
	    exit(1);
	}
    }

    /* Do the lookup. */
    key.dsize = strlen(MessageID) + 1;
    key.dptr = MessageID;
    val = dbzfetch(key);
    if (val.dptr == NULL || val.dsize != sizeof offset)
	return NULL;

    /* Get the seek offset, and seek. */
    for (p = val.dptr, q = (char *)&offset, i = sizeof offset; --i >= 0; )
	*q++ = *p++;
    if (fseek(F, offset, SEEK_SET) == -1)
	return NULL;
    return F;
}


/*
**  Return TRUE if the history file has the article expired.
*/
STATIC BOOL
Expired(MessageID)
    char		*MessageID;
{
    register int	c;
    register int	i;
    register FILE	*F;

    if ((F = HistorySeek(MessageID)) == NULL)
	/* Assume the worst. */
	return TRUE;

    /* Move to the filename fields. */
    for (i = 2; (c = getc(F)) != EOF && c != '\n'; )
	if (c == HIS_FIELDSEP && --i == 0)
	    break;
    if (c != HIS_FIELDSEP)
	return TRUE;

    /* See if we get any filename before the end of the line. */
    while ((c = getc(F)) != EOF && c != '\n')
	if (!ISWHITE(c))
	    /* Found non-whitespace; assume it's a filename. */
	    return FALSE;
    return TRUE;
}


/*
**  Flush and reset the site's output buffer.  Return FALSE on error.
*/
STATIC BOOL
REMflush()
{
    int		i;

    i = xwrite(ToServer, REMbuffer, (int)(REMbuffptr - REMbuffer));
    REMbuffptr = REMbuffer;
    return i < 0 ? FALSE : TRUE;
}


/*
**  Send a line to the server, adding the dot escape and \r\n.
*/
STATIC BOOL
REMwrite(p, i)
    register char	*p;
    register int	i;
{
    static char		HDR[] = "Content-Transfer-Encoding:";
    static char		COD[] =
		"Content-Transfer-Encoding: quoted-printable\r\n";
    register char	*dest;
    int			size;

    /* Buffer too full? */
    if (REMbuffend - REMbuffptr < i + 3) {
	if (!REMflush())
	    return FALSE;
	if (REMbuffend - REMbuffer < i + 3) {
	    /* Line too long -- grow buffer. */
	    size = i * 2;
	    RENEW(REMbuffer, char, size);
	    REMbuffend = &REMbuffer[size];
	}
    }

    if (MimeArticle != MTnotmime)
        if ((*p == 'C' && EQn(p, HDR, STRLEN(HDR)))
         || ((*p == 'C' || *p == 'c') && caseEQn(p, HDR, STRLEN(HDR)))) {
	    (void)memcpy((POINTER)REMbuffptr, (POINTER)COD, STRLEN(COD));
	    REMbuffptr += STRLEN(COD);
	    return TRUE;
        }

    /* Dot escape, text of the line, line terminator. */
    if (*p == '.')
	*REMbuffptr++ = '.';
    if (i > MEMCPY_THRESHOLD) {
	(void)memcpy((POINTER)REMbuffptr, (POINTER)p, (SIZE_T)i);
	REMbuffptr += i;
    }
    else {
	for (dest = REMbuffptr, i++; --i > 0; )
	    *dest++ = *p++;
	REMbuffptr = dest;
    }
    *REMbuffptr++ = '\r';
    *REMbuffptr++ = '\n';

    return TRUE;
}


/*
**  Send a line to the server, adding the dot escape and \r\n.
*/
STATIC BOOL
REMwriteQuoted(p, i)
    register char	*p;
    register int	i;
{
    static char		HEXDIGITS[] = "0123456789ABCDEF";
    register char	*dest;
    register int	size;
    register int	count;
    register int	prev;

    /* Buffer too full? */
    if (REMbuffend - REMbuffptr < i + 3) {
	if (!REMflush())
	    return FALSE;
	if (REMbuffend - REMbuffer < i + 3) {
	    /* Line too long -- grow buffer. */
	    size = i * 2;
	    RENEW(REMbuffer, char, size);
	    REMbuffend = &REMbuffer[size];
	}
    }

    for (count = 0, prev = 255, dest = REMbuffptr, i++; --i > 0; ) {
	if ((*p < 32 && *p != '\t')
	 || *p == '='
	 || *p >= 127
	 || (count == 0 && *p =='.')) {
	    *dest++ = '=';
	    *dest++ = HEXDIGITS[*p >> 4];
	    *dest++ = HEXDIGITS[*p & 0x0F];
	    p++;
	    count += 3;
	    prev = 'A';
	}
	else {
	    prev = *dest++ = *p++;
	    count++;
	}
        if (count > 72) {
	    *dest++ = '=';
	    *dest++ = '\r';
	    *dest++ = '\n';
	    count = 0;
	    prev = '\n';
        }
    }
    if (prev == ' ' || prev == '\t')
	*dest++ = '=';

    REMbuffptr = dest;
    *REMbuffptr++ = '\r';
    *REMbuffptr++ = '\n';

    return TRUE;
}


/*
**  Print transfer statistics, clean up, and exit.
*/
STATIC NORETURN
ExitWithStats(x)
    int			x;
{
    static char		QUIT[] = "quit";
    TIMEINFO		Now;
    double		usertime;
    double		systime;

    if (!Purging) {
	(void)REMwrite(QUIT, STRLEN(QUIT));
	(void)REMflush();
    }
    (void)GetTimeInfo(&Now);
    STATend = TIMEINFOasDOUBLE(Now);
    if (GetResourceUsage(&usertime, &systime) < 0) {
	usertime = 0;
	systime = 0;
    }

    if (STATprint) {
	(void)printf(STAT1,
	    REMhost, STAToffered, STATaccepted, STATrefused, STATrejected);
	(void)printf("\n");
	(void)printf(STAT2, REMhost, usertime, systime, STATend - STATbegin);
	(void)printf("\n");
    }

    syslog(L_NOTICE, STAT1,
	REMhost, STAToffered, STATaccepted, STATrefused, STATrejected);
    syslog(L_NOTICE, STAT2, REMhost, usertime, systime, STATend - STATbegin);

    if (BATCHfp != NULL && unlink(BATCHtemp) < 0 && errno != ENOENT)
	(void)fprintf(stderr, "Can't remove \"%s\", %s\n",
		BATCHtemp, strerror(errno));
    exit(x);
    /* NOTREACHED */
}


/*
**  Close the batchfile and the temporary file, and rename the temporary
**  to be the batchfile.
*/
STATIC void
CloseAndRename()
{
    /* Close the files, rename the temporary. */
    QIOclose(BATCHqp);
    if (ferror(BATCHfp)
     || fflush(BATCHfp) == EOF
     || fclose(BATCHfp) == EOF) {
	(void)unlink(BATCHtemp);
	(void)fprintf(stderr, "Can't close \"%s\", %s\n",
		BATCHtemp, strerror(errno));
	ExitWithStats(1);
    }
    if (rename(BATCHtemp, BATCHname) < 0) {
	(void)fprintf(stderr, "Can't rename \"%s\", %s\n",
		BATCHtemp, strerror(errno));
	ExitWithStats(1);
    }
}


/*
**  Requeue an article, opening the temp file if we have to.  If we get
**  a file write error, exit so that the original input is left alone.
*/
STATIC void
Requeue(Article, MessageID)
    char	*Article;
    char	*MessageID;
{
    /* Temp file already open? */
    if (BATCHfp == NULL) {
	(void)mktemp(BATCHtemp);
	if ((BATCHfp = fopen(BATCHtemp, "w")) == NULL) {
	    (void)fprintf(stderr, "Can't open \"%s\", %s\n",
		    BATCHtemp, strerror(errno));
	    ExitWithStats(1);
	}
    }

    /* Called only to get the file open? */
    if (Article == NULL)
	return;

    if (MessageID != NULL)
	(void)fprintf(BATCHfp, "%s %s\n", Article, MessageID);
    else
	(void)fprintf(BATCHfp, "%s\n", Article);
    if (fflush(BATCHfp) == EOF || ferror(BATCHfp)) {
	(void)fprintf(stderr, "Can't requeue \"%s\", %s\n",
		Article, strerror(errno));
	ExitWithStats(1);
    }
}


/*
**  Requeue an article then copy the rest of the batch file out.
*/
STATIC void
RequeueRestAndExit(Article, MessageID)
    char		*Article;
    char		*MessageID;
{
    register char	*p;

    if (!AlwaysRewrite
     && STATaccepted == 0 && STATrejected == 0 && STATrefused == 0) {
	(void)fprintf(stderr, "Nothing sent -- leaving batchfile alone.\n");
	ExitWithStats(1);
    }

    (void)fprintf(stderr, "Rewriting batch file and exiting.\n");
    Requeue(Article, MessageID);

    for ( ; ; ) {
	if ((p = QIOread(BATCHqp)) == NULL) {
	    if (QIOerror(BATCHqp)) {
		(void)fprintf(stderr, "Can't read \"%s\", %s\n",
			BATCHname, strerror(errno));
		ExitWithStats(1);
	    }
	    if (QIOtoolong(BATCHqp)) {
		(void)fprintf(stderr, "Skipping long line in \"%s\".\n",
			BATCHname);
		(void)QIOread(BATCHqp);
		continue;
	    }

	    /* Normal EOF. */
	    break;
	}

	if (fprintf(BATCHfp, "%s\n", p) == EOF
	 || ferror(BATCHfp)) {
	    (void)fprintf(stderr, "Can't requeue \"%s\", %s\n",
		    p, strerror(errno));
	    ExitWithStats(1);
	}
    }

    CloseAndRename();
    ExitWithStats(1);
}


/*
**  Clean up the NNTP escapes from a line.
*/
STATIC char *
REMclean(buff)
    char	*buff;
{
    char	*p;

    if ((p = strchr(buff, '\r')) != NULL)
	*p = '\0';
    if ((p = strchr(buff, '\n')) != NULL)
	*p = '\0';

    /* The dot-escape is only in text, not command responses. */
    return buff;
}


/*
**  Read a line of input, with timeout.  Also handle \r\n-->\n mapping
**  and the dot escape.  Return TRUE if okay, *or we got interrupted.*
*/
STATIC BOOL
REMread(start, size)
    char		*start;
    int			size;
{
    static int		count;
    static char		buffer[BUFSIZ];
    static char		*bp;
    register char	*p;
    register char	*q;
    register char	*end;
    struct timeval	t;
    FDSET		rmask;
    int			i;
    char		c;

    if (!REMflush())
	return FALSE;

    for (p = start, end = &start[size - 1]; ; ) {
	if (count == 0) {
	    /* Fill the buffer. */
    Again:
	    FD_ZERO(&rmask);
	    FD_SET(FromServer, &rmask);
	    t.tv_sec = 10 * 60;
	    t.tv_usec = 0;
	    i = select(FromServer + 1, &rmask, (FDSET *)NULL,
			(FDSET *)NULL, &t);
	    if (GotInterrupt)
		return TRUE;
	    if (i < 0) {
		if (errno == EINTR)
		    goto Again;
		return FALSE;
	    }
	    if (i == 0 || !FD_ISSET(FromServer, &rmask))
		return FALSE;
	    count = read(FromServer, buffer, sizeof buffer);
	    if (GotInterrupt)
		return TRUE;
	    if (count <= 0)
		return FALSE;
	    bp = buffer;
	}

	/* Process next character. */
	count--;
	c = *bp++;
	if (c == '\n')
	    break;
	if (p < end)
	    *p++ = c;
    }

    /* We know we got \n; if previous char was \r, turn it into \n. */
    if (p > start && p < end && p[-1] == '\r')
	p[-1] = '\n';
    *p = '\0';

    /* Handle the dot escape. */
    if (*p == '.') {
	if (p[1] == '\n' && p[2] == '\0')
	    /* EOF. */
	    return FALSE;
	for (q = &start[1]; (*p++ = *q++) != '\0'; )
	    continue;
    }
    return TRUE;
}


/*
**  Handle the interrupt.
*/
static void
Interrupted(Article, MessageID)
    char	*Article;
    char	*MessageID;
{
    (void)fprintf(stderr, "Interrupted\n");
    RequeueRestAndExit(Article, MessageID);
}


/*
**  Send a whole article to the server.
*/
STATIC BOOL
REMsendarticle(Article, MessageID, qp)
    char		*Article;
    char		*MessageID;
    register QIOSTATE	*qp;
{
    static char		TERM[] = ".\r\n";
    register char	*p;
    register BOOL	ok;
    register BOOL	InHeaders;
    char		buff[NNTP_STRLEN];

    for (InHeaders = TRUE; ; ) {
	if ((p = QIOread(qp)) == NULL) {
	    if (QIOerror(qp)) {
		(void)fprintf(stderr, "Can't read \"%s\", %s\n",
			Article, strerror(errno));
		return FALSE;
	    }
	    if (QIOtoolong(qp)) {
		(void)fprintf(stderr, "Line too long in \"%s\"\n", Article);
		(void)QIOread(BATCHqp);
		continue;
	    }

	    /* Normal EOF. */
	    break;
	}
	if (*p == '\0')
	    InHeaders = FALSE;

	if (InHeaders || MimeArticle == MTnotmime) {
	    if (!REMwrite(p, QIOlength(qp))) {
	        (void)fprintf(stderr, "Can't send \"%s\", %s\n",
		        Article, strerror(errno));
	        return FALSE;
	    }
	}
	else {
	    switch (MimeArticle) {
	    default:
	    case MTbase64:
		ok = FALSE;
		break;
	    case MTquotedprintable:
		ok = REMwriteQuoted(p, QIOlength(qp));
		break;
	    }
	    if (!ok) {
		(void)fprintf(stderr, "Can't send \"%s\", %s\n",
			Article, strerror(errno));
		return FALSE;
	    }
	}
	if (GotInterrupt)
	    Interrupted(Article, MessageID);
    }
    if (!REMflush()) {
	(void)fprintf(stderr, "Can't end \"%s\", %s\n",
		Article, strerror(errno));
	return FALSE;
    }
    if (Debug)
	(void)fprintf(stderr, "> [ article ]%s\n",
	     MimeArticle == MTnotmime ? "" : " (Mime: quoted-printable)");

    /* Write the terminator. */
    if (write(ToServer, TERM, STRLEN(TERM)) != STRLEN(TERM)) {
	(void)fprintf(stderr, "Can't end \"%s\", %s\n",
		Article, strerror(errno));
	return FALSE;
    }
    if (GotInterrupt)
	Interrupted(Article, MessageID);
    if (Debug)
	(void)fprintf(stderr, "> .\n");

    /* What did the remote site say? */
    if (!REMread(buff, (int)sizeof buff)) {
	(void)fprintf(stderr, "No reply after sending \"%s\", %s\n",
		Article, strerror(errno));
	return FALSE;
    }
    if (GotInterrupt)
	Interrupted(Article, MessageID);
    if (Debug)
	(void)fprintf(stderr, "< %s", buff);

    /* Parse the reply. */
    switch (atoi(buff)) {
    default:
	(void)fprintf(stderr, "Unknown reply after \"%s\" -- %s",
		Article, buff);
	if (DoRequeue)
	    Requeue(Article, MessageID);
	break;
    case NNTP_RESENDIT_VAL:
    case NNTP_GOODBYE_VAL:
	syslog(L_NOTICE, GOT_RESENDIT, REMhost, MessageID, REMclean(buff));
	Requeue(Article, MessageID);
	break;
    case NNTP_TOOKIT_VAL:
	STATaccepted++;
	break;
    case NNTP_REJECTIT_VAL:
	STATrejected++;
	break;
    }

    /* Article sent, or we requeued it. */
    return TRUE;
}


/*
**  Get the Message-ID header from an open article.
*/
STATIC char *
GetMessageID(qp)
    register QIOSTATE	*qp;
{
    static char		HDR[] = "Message-ID:";
    static char		buff[DBZMAXKEY + 1];
    register char	*p;

    while ((p = QIOread(qp)) != NULL)
	if ((*p == 'M' && EQn(p, HDR, STRLEN(HDR)))
	 || ((*p == 'M' || *p == 'm') && caseEQn(p, HDR, STRLEN(HDR)))) {
	    /* Found the header -- skip whitespace. */
	    for (p += STRLEN(HDR); ISWHITE(*p); p++)
		continue;
	    if (*p == '\0' || (int)strlen(p) > DBZMAXKEY)
		/* Header is empty or too long. */
		break;
	    (void)strcpy(buff, p);
	    return buff;
	}
    return NULL;
}


/*
**  Get the MIME Content headers from an open article.
*/
STATIC void
GetMimeHeaders(qp, Encodingp, Typep)
    register QIOSTATE	*qp;
    register char	**Encodingp;
    register char	**Typep;
{
    static char		ENC_HDR[] = "Content-Transfer-Encoding:";
    static char		TYPE_HDR[] = "Content-Type:";
    static char		Encoding[SMBUF + 1];
    static char		ContentType[SMBUF + 1];
    register char	*p;

    for (*Encodingp = *Typep = NULL; (p = QIOread(qp)) != NULL && *p; ) {
	if (*p != 'C' && *p != 'c')
	    continue;
	if (caseEQn(p, ENC_HDR, STRLEN(ENC_HDR))) {
	    for (p += STRLEN(ENC_HDR); ISWHITE(*p); p++)
		continue;
	    if (*p == '\0' || (int)strlen(p) > sizeof Encoding)
		/* Header is empty or too long. */
		continue;
	    (void)strcpy(Encoding, p);
	    *Encodingp = Encoding;
	    if (*Typep)
		break;
	}
	else if (caseEQn(p, TYPE_HDR, STRLEN(TYPE_HDR))) {
	    for (p += STRLEN(TYPE_HDR); ISWHITE(*p); p++)
		continue;
	    if (*p == '\0' || (int)strlen(p) > sizeof ContentType)
		/* Header is empty or too long. */
		break;
	    (void)strcpy(ContentType, p);
	    /* Strip off any subtype part. */
	    for (p = ContentType; *p; p++)
		if (*p == '/' || *p == ';') {
		    *p = '\0';
		    break;
		}
	    *Typep = ContentType;
	    if (*Encodingp)
		break;
	}
    }
}



/*
**  Mark that we got interrupted.
*/
STATIC SIGHANDLER
CATCHinterrupt(s)
    int		s;
{
    GotInterrupt = TRUE;
    /* Let two interrupts kill us. */
    (void)signal(s, SIG_DFL);
}


/*
**  Mark that the alarm went off.
*/
/* ARGSUSED0 */
STATIC SIGHANDLER
CATCHalarm(s)
    int		s;
{
    GotAlarm = TRUE;
    if (JMPyes)
	longjmp(JMPwhere, 1);
}


/*
**  Print a usage message and exit.
*/
STATIC NORETURN
Usage()
{
    (void)fprintf(stderr,
	"Usage: innxmit [-a] [-d] [-M] [-p] [-r] [-S] [-t#] [-T#] host file\n");
    exit(1);
}


int
main(ac, av)
    int			ac;
    char		*av[];
{
    static char		SPOOL[] = _PATH_SPOOL;
    static char		BATCHDIR[] = _PATH_BATCHDIR;
    static char		SKIPPING[] = "Skipping \"%s\" --%s?\n";
    register int	i;
    register char	*p;
    register QIOSTATE	*qp;
    TIMEINFO		Now;
    FILE		*From;
    FILE		*To;
    char		buff[NNTP_STRLEN];
    char		*AltSpool;
    char		*Article;
    char		*ContentEncoding;
    char		*ContentType;
    char		*MessageID;
    char		*AltPath;
    SIGHANDLER		(*old)();
    unsigned int	ConnectTimeout;
    unsigned int	TotalTimeout;

    /* Set defaults. */
    ConnectTimeout = 0;
    TotalTimeout = 0;
    AltSpool = NULL;
    (void)umask(NEWSUMASK);

    /* Parse JCL. */
    while ((i = getopt(ac, av, "A:adMprSt:T:v")) != EOF)
	switch (i) {
	default:
	    Usage();
	    /* NOTREACHED */
	case 'A':
	    AltSpool = optarg;
	    AltPath = NEW(char, SPOOLNAMEBUFF + strlen(AltSpool));
	    break;
	case 'a':
	    AlwaysRewrite = TRUE;
	    break;
	case 'd':
	    Debug = TRUE;
	    break;
	case 'M':
	    Mime = TRUE;
	    break;
	case 'p':
	    AlwaysRewrite = TRUE;
	    Purging = TRUE;
	    break;
	case 'r':
	    DoRequeue = FALSE;
	    break;
	case 'S':
	    Slavish = TRUE;
	    break;
	case 't':
	    ConnectTimeout = atoi(optarg);
	    break;
	case 'T':
	    TotalTimeout = atoi(optarg);
	    break;
	case 'v':
	    STATprint = TRUE;
	    break;
	}
    ac -= optind;
    av += optind;

    /* Parse arguments; host and filename. */
    if (ac != 2)
	Usage();
    REMhost = av[0];
    BATCHname = av[1];

    if (chdir(SPOOL) < 0) {
	(void)fprintf(stderr, "Can't cd to \"%s\", %s\n",
		SPOOL, strerror(errno));
	exit(1);
    }

    (void)openlog("innxmit", L_OPENLOG_FLAGS | LOG_PID, LOG_INN_PROG);

    /* Open the batch file and lock others out. */
    if (BATCHname[0] != '/') {
	BATCHname = NEW(char, STRLEN(BATCHDIR) + 1 + strlen(av[1]) + 1);
	(void)sprintf(BATCHname, "%s/%s", BATCHDIR, av[1]);
    }
    if ((i = open(BATCHname, O_RDWR)) < 0
     || (BATCHqp = QIOfdopen(i, QIO_BUFFER)) == NULL) {
	(void)fprintf(stderr, "Can't open \"%s\", %s\n",
		BATCHname, strerror(errno));
	exit(1);
    }
    if (LockFile(QIOfileno(BATCHqp), TRUE) < 0) {
#if	defined(EWOULDBLOCK)
	if (errno == EWOULDBLOCK)
	    exit(0);
#endif	/* defined(EWOULDBLOCK) */
	(void)fprintf(stderr, "Can't lock \"%s\", %s\n",
		BATCHname, strerror(errno));
	exit(1);
    }

    /* Get a temporary name in the same directory as the batch file. */
    p = strrchr(BATCHname, '/');
    BATCHtemp = NEW(char, strlen(BATCHname) + STRLEN("/bchXXXXXX") + 1);
    *p = '\0';
    (void)sprintf(BATCHtemp, "%s/bchXXXXXX", BATCHname);
    *p = '/';

    /* Set up buffer used by REMwrite. */
    REMbuffer = NEW(char, OUTPUT_BUFFER_SIZE);
    REMbuffend = &REMbuffer[OUTPUT_BUFFER_SIZE];
    REMbuffptr = REMbuffer;

    /* Start timing. */
    if (GetTimeInfo(&Now) < 0) {
	(void)fprintf(stderr, "Can't get time, %s\n", strerror(errno));
	exit(1);
    }
    STATbegin = TIMEINFOasDOUBLE(Now);

    if (!Purging) {
	/* Open a connection to the remote server. */
	if (ConnectTimeout) {
	    GotAlarm = FALSE;
	    old = signal(SIGALRM, CATCHalarm);
	    (void)alarm(ConnectTimeout);
	    JMPyes = TRUE;
	    if (setjmp(JMPwhere)) {
		(void)fprintf(stderr, "Can't connect to %s, timed out\n",
			REMhost);
		exit(1);
	    }
	}
	if (NNTPconnect(REMhost, &From, &To, buff) < 0 || GotAlarm) {
	    i = errno;
	    (void)fprintf(stderr, "Can't connect to %s, %s\n",
		    REMhost, buff[0] ? REMclean(buff) : strerror(errno));
	    if (GotAlarm)
		syslog(L_NOTICE, CANT_CONNECT, REMhost, "timeout");
	    else
		syslog(L_NOTICE, CANT_CONNECT, REMhost,
		    buff[0] ? REMclean(buff) : strerror(i));
	    exit(1);
	}
	if (Debug)
	    (void)fprintf(stderr, "< %s\n", REMclean(buff));
	if (NNTPsendpassword(REMhost, From, To) < 0 || GotAlarm) {
	    i = errno;
	    (void)fprintf(stderr, "Can't authenticate with %s, %s\n",
		    REMhost, strerror(errno));
	    syslog(L_ERROR, CANT_AUTHENTICATE,
		REMhost, GotAlarm ? "timeout" : strerror(i));
	    /* Don't send quit; we want the remote to print a message. */
	    exit(1);
	}
	if (ConnectTimeout) {
	    (void)alarm(0);
	    (void)signal(SIGALRM, old);
	    JMPyes = FALSE;
	}

	/* We no longer need standard I/O. */
	FromServer = fileno(From);
	ToServer = fileno(To);
    }

    /* Set up signal handlers. */
    (void)signal(SIGHUP, CATCHinterrupt);
    (void)signal(SIGINT, CATCHinterrupt);
    (void)signal(SIGTERM, CATCHinterrupt);
    (void)signal(SIGPIPE, SIG_IGN);
    if (TotalTimeout) {
	(void)alarm(TotalTimeout);
	(void)signal(SIGALRM, CATCHalarm);
    }

    /* Main processing loop. */
    GotInterrupt = FALSE;
    GotAlarm = FALSE;
    for (Article = NULL, MessageID = NULL; ; ) {
	if (GotAlarm) {
	    (void)fprintf(stderr, "Timed out\n");
	    /* Don't resend the current article. */
	    RequeueRestAndExit((char *)NULL, (char *)NULL);
	}
	if (GotInterrupt)
	    Interrupted(Article, MessageID);

	if ((Article = QIOread(BATCHqp)) == NULL) {
	    if (QIOerror(BATCHqp)) {
		(void)fprintf(stderr, "Can't read \"%s\", %s\n",
			BATCHname, strerror(errno));
		ExitWithStats(1);
	    }
	    if (QIOtoolong(BATCHqp)) {
		(void)fprintf(stderr, "Skipping long line in \"%s\"\n",
			BATCHname);
		(void)QIOread(BATCHqp);
		continue;
	    }

	    /* Normal EOF -- we're done. */
	    QIOclose(BATCHqp);
	    break;
	}

	/* Split the line into possibly two fields. */
	if (Article[0] == '/'
	 && Article[STRLEN(SPOOL)] == '/'
	 && EQn(Article, SPOOL, STRLEN(SPOOL)))
	    Article += STRLEN(SPOOL) + 1;
	if ((MessageID = strchr(Article, ' ')) != NULL) {
	    *MessageID++ = '\0';
	    if (!Slavish) {
		if (*MessageID != '<'
		 || (p = strrchr(MessageID, '>')) == NULL
		 || *++p != '\0') {
		    (void)fprintf(stderr, "Ignoring line \"%s %s...\"\n",
			Article, MessageID);
		    continue;
		}
		*p = '\0';
	    }
	}

	if (*Article == '\0') {
	    (void)fprintf(stderr, "Empty filename for \"%s\" in \"%s\"\n",
		    MessageID, BATCHname);
	    /* We could do a history lookup. */
	    continue;
	}

	if (Purging && MessageID != NULL && !Expired(MessageID)) {
	    Requeue(Article, MessageID);
	    continue;
	}

	/* Open the article. */
	if ((qp = QIOopen(Article, QIO_BUFFER)) == NULL
	 && AltSpool
	 && *Article != '/') {
	    (void)sprintf(AltPath, "%s/%s", AltSpool, Article);
	    qp = QIOopen(AltPath, QIO_BUFFER);
	}

	if (qp == NULL) {
	    switch (errno) {
	    default:
		(void)fprintf(stderr, "Requeue \"%s\", %s\n",
			Article, strerror(errno));
		Requeue(Article, MessageID);
		break;
	    case ENOENT:
		/* Cancelled or expired.  We could look the file up
		 * in the history database and see if it does exist. */
		break;
	    case ENOTDIR:
		(void)fprintf(stderr, SKIPPING, Article, "mangled");
		break;
	    }
	    continue;
	}

	if (Purging) {
	    QIOclose(qp);
	    Requeue(Article, MessageID);
	    continue;
	}

	/* Get the Message-ID from the article if we need to. */
	if (MessageID == NULL) {
	    if ((MessageID = GetMessageID(qp)) == NULL) {
		(void)fprintf(stderr, SKIPPING, Article, "no Message-ID");
		QIOclose(qp);
		continue;
	    }
	    if (QIOrewind(qp) < 0) {
		(void)fprintf(stderr, "Can't rewind \"%s\", %s -- requeue\n",
			Article, strerror(errno));
		QIOclose(qp);
		Requeue(Article, (char *)NULL);
		continue;
	    }
	}
	if (Mime == TRUE) {
	    MimeArticle = MTnotmime;
	    GetMimeHeaders(qp, &ContentEncoding, &ContentType);
	    if (QIOrewind(qp) < 0) {
		(void)fprintf(stderr, "Can't rewind \"%s\", %s -- requeue\n",
			Article, strerror(errno));
		QIOclose(qp);
		Requeue(Article, (char *)NULL);
		continue;
	    }
	    if (ContentEncoding 
	     && (caseEQ(ContentEncoding, "binary")
	      || caseEQ(ContentEncoding, "8bit")))
		if (ContentType == NULL || caseEQ(ContentType, "text"))
		    MimeArticle = MTquotedprintable;
		else
		    /* Shouldbe MTbase64, but not implemented yet. */
		    MimeArticle = MTnotmime;
	}
	if (GotInterrupt)
	    Interrupted(Article, MessageID);

	/* Offer the article. */
	(void)sprintf(buff, "%s %s", Slavish ? "xreplic" : "ihave", MessageID);
	if (!REMwrite(buff, (int)strlen(buff))) {
	    (void)fprintf(stderr, "Can't offer article, %s\n",
		    strerror(errno));
	    QIOclose(qp);
	    RequeueRestAndExit(Article, MessageID);
	}
	STAToffered++;
	if (Debug)
	    (void)fprintf(stderr, "> %s\n", buff);
	if (GotInterrupt)
	    Interrupted(Article, MessageID);

	/* Does he want it? */
	if (!REMread(buff, (int)sizeof buff)) {
	    (void)fprintf(stderr, "No reply to ihave, %s\n", strerror(errno));
	    QIOclose(qp);
	    RequeueRestAndExit(Article, MessageID);
	}
	if (GotInterrupt)
	    Interrupted(Article, MessageID);
	if (Debug)
	    (void)fprintf(stderr, "< %s", buff);

	/* Parse the reply. */
	switch (atoi(buff)) {
	default:
	    (void)fprintf(stderr, "Unknown reply to \"%s\" -- %s",
		    Article, buff);
	    if (DoRequeue)
		Requeue(Article, MessageID);
	    break;
	case NNTP_RESENDIT_VAL:
	case NNTP_GOODBYE_VAL:
	    /* Most likely out of space -- no point in continuing. */
	    syslog(L_NOTICE, IHAVE_FAIL, REMhost, REMclean(buff));
	    RequeueRestAndExit(Article, MessageID);
	    /* NOTREACHED */
	case NNTP_SENDIT_VAL:
	    if (!REMsendarticle(Article, MessageID, qp))
		RequeueRestAndExit(Article, MessageID);
	    break;
	case NNTP_SYNTAX_VAL:
	case NNTP_HAVEIT_VAL:
	    STATrefused++;
	    break;
#if	defined(NNTP_SENDIT_LATER)
	case NNTP_SENDIT_LATER_VAL:
	    Requeue(Article, MessageID);
	    break;
#endif	/* defined(NNTP_SENDIT_LATER) */
	}

	QIOclose(qp);
    }

    if (BATCHfp != NULL)
	/* We requeued something, so close the temp file. */
	CloseAndRename();
    else if (unlink(BATCHname) < 0 && errno != ENOENT)
	(void)fprintf(stderr, "Can't remove \"%s\", %s\n",
		BATCHtemp, strerror(errno));
    ExitWithStats(0);
    /* NOTREACHED */
}