/*  $Id: inews.c,v 1.54 2003/04/03 05:02:02 rra Exp $
**
**  Send an article (prepared by someone on the local site) to the
**  master news server.
*/

#include "config.h"
#include "clibrary.h"
#include "portable/time.h"
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <sys/stat.h>

#include "inn/innconf.h"
#include "inn/messages.h"
#include "libinn.h"
#include "nntp.h"
#include "paths.h"

/* Signature handling.  The separator will be appended before the signature,
   and at most SIG_MAXLINES will be appended. */
#define SIG_MAXLINES		4
#define SIG_SEPARATOR		"-- \n"

#define FLUSH_ERROR(F)		(fflush((F)) == EOF || ferror((F)))
#define LPAREN			'('	/* For vi :-) */
#define HEADER_DELTA		20
#define GECOSTERM(c)		\
	    ((c) == ',' || (c) == ';' || (c) == ':' || (c) == LPAREN)
#define HEADER_STRLEN		998

typedef enum _HEADERTYPE {
    HTobs,
    HTreq,
    HTstd
} HEADERTYPE;

typedef struct _HEADER {
    const char *Name;
    bool	CanSet;
    HEADERTYPE	Type;
    int		Size;
    char	*Value;
} HEADER;

static bool	Dump;
static bool	Revoked;
static bool	Spooling;
static char	**OtherHeaders;
static char	SIGSEP[] = SIG_SEPARATOR;
static FILE	*FromServer;
static FILE	*ToServer;
static int	OtherCount;
static int	OtherSize;
static const char *Exclusions = "";
static const char * const BadDistribs[] = {
    BAD_DISTRIBS
};

static HEADER	Table[] = {
    /* 	Name			Canset	Type	*/
    {	"Path",			true,	HTstd,  0, NULL },
#define _path		 0
    {	"From",			true,	HTstd,  0, NULL },
#define _from		 1
    {	"Newsgroups",		true,	HTreq,  0, NULL },
#define _newsgroups	 2
    {	"Subject",		true,	HTreq,  0, NULL },
#define _subject	 3
    {	"Control",		true,	HTstd,  0, NULL },
#define _control	 4
    {	"Supersedes",		true,	HTstd,  0, NULL },
#define _supersedes	 5
    {	"Followup-To",		true,	HTstd,  0, NULL },
#define _followupto	 6
    {	"Date",			true,	HTstd,  0, NULL },
#define _date		 7
    {	"Organization",		true,	HTstd,  0, NULL },
#define _organization	 8
    {	"Lines",		true,	HTstd,  0, NULL },
#define _lines		 9
    {	"Sender",		true,	HTstd,  0, NULL },
#define _sender		10
    {	"Approved",		true,	HTstd,  0, NULL },
#define _approved	11
    {	"Distribution",		true,	HTstd,  0, NULL },
#define _distribution	12
    {	"Expires",		true,	HTstd,  0, NULL },
#define _expires	13
    {	"Message-ID",		true,	HTstd,  0, NULL },
#define _messageid	14
    {	"References",		true,	HTstd,  0, NULL },
#define _references	15
    {	"Reply-To",		true,	HTstd,  0, NULL },
#define _replyto	16
    {	"Also-Control",		true,	HTstd,  0, NULL },
#define _alsocontrol	17
    {	"Xref",			false,	HTstd,  0, NULL },
    {	"Summary",		true,	HTstd,  0, NULL },
    {	"Keywords",		true,	HTstd,  0, NULL },
    {	"Date-Received",	false,	HTobs,  0, NULL },
    {	"Received",		false,	HTobs,  0, NULL },
    {	"Posted",		false,	HTobs,  0, NULL },
    {	"Posting-Version",	false,	HTobs,  0, NULL },
    {	"Relay-Version",	false,	HTobs,  0, NULL },
};

#define HDR(_x)	(Table[(_x)].Value)



/*
**  Send the server a quit message, wait for a reply.
*/
static void
QuitServer(int x)
{
    char	buff[HEADER_STRLEN];
    char	*p;

    if (Spooling)
	exit(x);
    if (x)
        warn("article not posted");
    fprintf(ToServer, "quit\r\n");
    if (FLUSH_ERROR(ToServer))
        sysdie("cannot send quit to server");
    if (fgets(buff, sizeof buff, FromServer) == NULL)
        sysdie("warning: server did not reply to quit");
    if ((p = strchr(buff, '\r')) != NULL)
	*p = '\0';
    if ((p = strchr(buff, '\n')) != NULL)
	*p = '\0';
    if (atoi(buff) != NNTP_GOODBYE_ACK_VAL)
        die("server did not reply to quit properly: %s", buff);
    fclose(FromServer);
    fclose(ToServer);
    exit(x);
}


/*
**  Failure handler, called by die.  Calls QuitServer to cleanly shut down the
**  connection with the remote server before exiting.
*/
static int
fatal_cleanup(void)
{
    /* Don't recurse. */
    message_fatal_cleanup = NULL;

    /* QuitServer does all the work. */
    QuitServer(1);
    return 1;
}


/*
**  Flush a stdio FILE; exit if there are any errors.
*/
static void
SafeFlush(FILE *F)
{
    if (FLUSH_ERROR(F))
        sysdie("cannot send text to server");
}


/*
**  Trim trailing spaces, return pointer to first non-space char.
*/
static char *
TrimSpaces(char *p)
{
    char	*start;

    for (start = p; ISWHITE(*start); start++)
	continue;
    for (p = start + strlen(start); p > start && CTYPE(isspace, p[-1]); )
	*--p = '\0';
    return start;
}


/*
**  Mark the end of the header starting at p, and return a pointer
**  to the start of the next one.  Handles continuations.
*/
static char *
NextHeader(char *p)
{
    for ( ; ; p++) {
	if ((p = strchr(p, '\n')) == NULL)
            die("article is all headers");
	if (!ISWHITE(p[1])) {
	    *p = '\0';
	    return p + 1;
	}
    }
}


/*
**  Strip any headers off the article and dump them into the table.
*/
static char *
StripOffHeaders(char *article)
{
    char	*p;
    char	*q;
    HEADER	*hp;
    char	c;
    int	i;

    /* Set up the other headers list. */
    OtherSize = HEADER_DELTA;
    OtherHeaders = xmalloc(OtherSize * sizeof(char *));
    OtherCount = 0;

    /* Scan through buffer, a header at a time. */
    for (i = 0, p = article; ; i++) {

	if ((q = strchr(p, ':')) == NULL)
            die("no colon in header line \"%.30s...\"", p);
	if (q[1] == '\n' && !ISWHITE(q[2])) {
	    /* Empty header; ignore this one, get next line. */
	    p = NextHeader(p);
	    if (*p == '\n')
		break;
	}

	if (q[1] != '\0' && !ISWHITE(q[1])) {
	    if ((q = strchr(q, '\n')) != NULL)
		*q = '\0';
            die("no space after colon in \"%.30s...\"", p);
	}

	/* See if it's a known header. */
	c = CTYPE(islower, *p) ? toupper(*p) : *p;
	for (hp = Table; hp < ARRAY_END(Table); hp++)
	    if (c == hp->Name[0]
	     && p[hp->Size] == ':'
	     && ISWHITE(p[hp->Size + 1])
	     && strncasecmp(p, hp->Name, hp->Size) == 0) {
		if (hp->Type == HTobs)
                    die("obsolete header: %s", hp->Name);
		if (hp->Value)
                    die("duplicate header: %s", hp->Name);
		for (q = &p[hp->Size + 1]; ISWHITE(*q); q++)
		    continue;
		hp->Value = q;
		break;
	    }

	/* Too many headers? */
	if (++i > 5 * HEADER_DELTA)
            die("more than %d lines of header", i);

	/* No; add it to the set of other headers. */
	if (hp == ARRAY_END(Table)) {
	    if (OtherCount >= OtherSize - 1) {
		OtherSize += HEADER_DELTA;
                OtherHeaders = xrealloc(OtherHeaders, OtherSize * sizeof(char *));
	    }
	    OtherHeaders[OtherCount++] = p;
	}

	/* Get start of next header; if it's a blank line, we hit the end. */
	p = NextHeader(p);
	if (*p == '\n')
	    break;
    }

    return p + 1;
}



/*
**  See if the user is allowed to cancel the indicated message.  Assumes
**  that the Sender or From line has already been filled in.
*/
static void
CheckCancel(char *msgid, bool JustReturn)
{
    char		localfrom[SMBUF];
    char	*p;
    char		buff[BUFSIZ];
    char		remotefrom[SMBUF];

    /* Ask the server for the article. */
    fprintf(ToServer, "head %s\r\n", msgid);
    SafeFlush(ToServer);
    if (fgets(buff, sizeof buff, FromServer) == NULL
     || atoi(buff) != NNTP_HEAD_FOLLOWS_VAL) {
	if (JustReturn)
	    return;
        die("server has no such article");
    }

    /* Read the headers, looking for the From or Sender. */
    remotefrom[0] = '\0';
    while (fgets(buff, sizeof buff, FromServer) != NULL) {
	if ((p = strchr(buff, '\r')) != NULL)
	    *p = '\0';
	if ((p = strchr(buff, '\n')) != NULL)
	    *p = '\0';
	if (buff[0] == '.' && buff[1] == '\0')
	    break;
        if (strncmp(buff, "Sender:", 7) == 0)
            strlcpy(remotefrom, TrimSpaces(&buff[7]), SMBUF);
        else if (remotefrom[0] == '\0' && strncmp(buff, "From:", 5) == 0)
            strlcpy(remotefrom, TrimSpaces(&buff[5]), SMBUF);
    }
    if (remotefrom[0] == '\0') {
	if (JustReturn)
	    return;
        die("article is garbled");
    }
    HeaderCleanFrom(remotefrom);

    /* Get the local user. */
    strlcpy(localfrom, HDR(_sender) ? HDR(_sender) : HDR(_from), SMBUF);
    HeaderCleanFrom(localfrom);

    /* Is the right person cancelling? */
    if (strcasecmp(localfrom, remotefrom) != 0)
        die("article was posted by \"%s\" and you are \"%s\"", remotefrom,
            localfrom);
}


/*
**  See if the user is the news administrator.
*/
static bool
AnAdministrator(char *name, gid_t group)
{
    struct passwd	*pwp;
    struct group	*grp;
    char		**mem;
    char		*p;

    if (Revoked)
	return false;

    /* Find out who we are. */
    if ((pwp = getpwnam(NEWSUSER)) == NULL)
	/* Silent falure; clients might not have the group. */
	return false;
    if (getuid() == pwp->pw_uid)
	return true;

    /* See if the we're in the right group. */
    if ((grp = getgrnam(NEWSGRP)) == NULL || (mem = grp->gr_mem) == NULL)
	/* Silent falure; clients might not have the group. */
	return false;
    if (group == grp->gr_gid)
	return true;
    while ((p = *mem++) != NULL)
	if (strcmp(name, p) == 0)
	    return true;
    return false;
}


/*
**  Check the control message, and see if it's legit.
*/
static void
CheckControl(char *ctrl, struct passwd *pwp)
{
    char	*p;
    char	*q;
    char		save;
    char		name[SMBUF];

    /* Snip off the first word. */
    for (p = ctrl; ISWHITE(*p); p++)
	continue;
    for (ctrl = p; *p && !ISWHITE(*p); p++)
	continue;
    if (p == ctrl)
        die("emtpy control message");
    save = *p;
    *p = '\0';

    if (strcmp(ctrl, "cancel") == 0) {
	for (q = p + 1; ISWHITE(*q); q++)
	    continue;
	if (*q == '\0')
            die("message ID missing in cancel");
	if (!Spooling)
	    CheckCancel(q, false);
    }
    else if (strcmp(ctrl, "checkgroups") == 0
	  || strcmp(ctrl, "ihave")       == 0
	  || strcmp(ctrl, "sendme")      == 0
	  || strcmp(ctrl, "newgroup")    == 0
	  || strcmp(ctrl, "rmgroup")     == 0
	  || strcmp(ctrl, "sendsys")     == 0
	  || strcmp(ctrl, "senduuname")  == 0
	  || strcmp(ctrl, "version")     == 0) {
	strlcpy(name, pwp->pw_name, SMBUF);
	if (!AnAdministrator(name, pwp->pw_gid))
            die("ask your news administrator to do the %s for you", ctrl);
    }
    else {
        die("%s is not a valid control message", ctrl);
    }
    *p = save;
}



/*
**  Parse the GECOS field to get the user's full name.  This comes Sendmail's
**  buildfname routine.  Ignore leading stuff like "23-" "stuff]-" or
**  "stuff -" as well as trailing whitespace, or anything that comes after
**  a comma, semicolon, or in parentheses.  This seems to strip off most of
**  the UCB or ATT stuff people fill out the entries with.  Also, turn &
**  into the login name, with perhaps an initial capital.  (Everyone seems
**  to hate that, but everyone also supports it.)
*/
static char *
FormatUserName(struct passwd *pwp, char *node)
{
    char	outbuff[SMBUF];
    char        *buff;
    char	*out;
    char	*p;
    int         left;

#if	!defined(DONT_MUNGE_GETENV)
    memset(outbuff, 0, SMBUF);
    if ((p = getenv("NAME")) != NULL)
	strlcpy(outbuff, p, SMBUF);
    if (strlen(outbuff) == 0) {
#endif	/* !defined(DONT_MUNGE_GETENV) */


#ifndef DO_MUNGE_GECOS
    strlcpy(outbuff, pwp->pw_gecos, SMBUF);
#else
    /* Be very careful here.  If we're not, we can potentially overflow our
     * buffer.  Remember that on some Unix systems, the content of the GECOS
     * field is under (untrusted) user control and we could be setgid. */
    p = pwp->pw_gecos;
    left = SMBUF - 1;
    if (*p == '*')
	p++;
    for (out = outbuff; *p && !GECOSTERM(*p) && left; p++) {
	if (*p == '&') {
	    strncpy(out, pwp->pw_name, left);
	    if (CTYPE(islower, *out)
	     && (out == outbuff || !CTYPE(isalpha, out[-1])))
		*out = toupper(*out);
	    while (*out) {
		out++;
                left--;
            }
	}
	else if (*p == '-'
	      && p > pwp->pw_gecos
              && (CTYPE(isdigit, p[-1]) || CTYPE(isspace, p[-1])
                  || p[-1] == ']')) {
	    out = outbuff;
            left = SMBUF - 1;
        }
	else {
	    *out++ = *p;
            left--;
        }
    }
    *out = '\0';
#endif /* DO_MUNGE_GECOS */

#if	!defined(DONT_MUNGE_GETENV)
    }
#endif	/* !defined(DONT_MUNGE_GETENV) */

    out = TrimSpaces(outbuff);
    if (out[0])
        buff = concat(pwp->pw_name, "@", node, " (", out, ")", (char *) 0);
    else
        buff = concat(pwp->pw_name, "@", node, (char *) 0);
    return buff;
}


/*
**  Check the Distribution header, and exit on error.
*/
static void CheckDistribution(char *p)
{
    static char	SEPS[] = " \t,";
    const char  * const *dp;

    if ((p = strtok(p, SEPS)) == NULL)
        die("cannot parse Distribution header");
    do {
	for (dp = BadDistribs; *dp; dp++)
	    if (uwildmat(p, *dp))
                die("illegal distribution %s", p);
    } while ((p = strtok((char *)NULL, SEPS)) != NULL);
}


/*
**  Process all the headers.  FYI, they're done in RFC-order.
*/
static void
ProcessHeaders(bool AddOrg, int linecount, struct passwd *pwp)
{
    static char		PATHFLUFF[] = PATHMASTER;
    HEADER              *hp;
    char                *p;
    TIMEINFO		Now;
    char		buff[SMBUF];
    char		from[SMBUF];

    /* Do some preliminary fix-ups. */
    for (hp = Table; hp < ARRAY_END(Table); hp++) {
	if (!hp->CanSet && hp->Value)
            die("cannot set system header %s", hp->Name);
	if (hp->Value) {
	    hp->Value = TrimSpaces(hp->Value);
	    if (*hp->Value == '\0')
		hp->Value = NULL;
	}
    }

    /* Set From or Sender. */
    if ((p = innconf->fromhost) == NULL)
        sysdie("cannot get hostname");
    if (HDR(_from) == NULL)
	HDR(_from) = FormatUserName(pwp, p);
    else {
      if (strlen(pwp->pw_name) + strlen(p) + 2 > sizeof(buff))
          die("username and host are too long");
      sprintf(buff, "%s@%s", pwp->pw_name, p);
      strlcpy(from, HDR(_from), SMBUF);
      HeaderCleanFrom(from);
      if (strcmp(from, buff) != 0)
        HDR(_sender) = xstrdup(buff);
    }

    if (HDR(_date) == NULL) {
	/* Set Date. */
	if (!makedate(-1, true, buff, sizeof(buff)))
	    die("cannot generate Date header");
	HDR(_date) = xstrdup(buff);
    }

    /* Newsgroups are checked later. */

    /* Set Subject; Control overrides the subject. */
    if (HDR(_control)) {
	CheckControl(HDR(_control), pwp);
    }
    else {
	p = HDR(_subject);
	if (p == NULL)
            die("required Subject header is missing or empty");
	else if (HDR(_alsocontrol))
	    CheckControl(HDR(_alsocontrol), pwp);
#if	0
	if (strncmp(p, "Re: ", 4) == 0 && HDR(_references) == NULL)
            die("article subject begins with \"Re: \" but has no references");
#endif	/* 0 */
    }

    /* Set Message-ID */
    if (HDR(_messageid) == NULL) {
	if ((p = GenerateMessageID(innconf->domain)) == NULL)
            die("cannot generate Message-ID header");
	HDR(_messageid) = xstrdup(p);
    }
    else if ((p = strchr(HDR(_messageid), '@')) == NULL
             || strchr(++p, '@') != NULL) {
        die("message ID must have exactly one @");
    }

    /* Set Path */
    if (HDR(_path) == NULL) {
#if	defined(DO_INEWS_PATH)
	if ((p = innconf->pathhost) != NULL) {
	    if (*p)
                HDR(_path) = concat(Exclusions, p, "!", PATHFLUFF, (char *) 0);
	    else
                HDR(_path) = concat(Exclusions, PATHFLUFF, (char *) 0);
	}
	else if (innconf->server != NULL) {
	    if ((p = GetFQDN(innconf->domain)) == NULL)
                sysdie("cannot get hostname");
	    HDR(_path) = concat(Exclusions, p, "!", PATHFLUFF, (char *) 0);
	}
	else {
	    HDR(_path) = concat(Exclusions, PATHFLUFF, (char *) 0);
	}
#else
	HDR(_path) = concat(Exclusions, PATHFLUFF, (char *) 0);
#endif	/* defined(DO_INEWS_PATH) */
    }

    /* Reply-To; left alone. */
    /* Sender; set above. */
    /* Followup-To; checked with Newsgroups. */

    /* Check Expires. */
    if (GetTimeInfo(&Now) < 0)
        sysdie("cannot get the time");
    if (HDR(_expires) && parsedate(HDR(_expires), &Now) == -1)
        die("cannot parse \"%s\" as an expiration date", HDR(_expires));

    /* References; left alone. */
    /* Control; checked above. */

    /* Distribution. */
    if ((p = HDR(_distribution)) != NULL) {
	p = xstrdup(p);
	CheckDistribution(p);
	free(p);
    }

    /* Set Organization. */
    if (AddOrg
     && HDR(_organization) == NULL
     && (p = innconf->organization) != NULL) {
	HDR(_organization) = xstrdup(p);
    }

    /* Keywords; left alone. */
    /* Summary; left alone. */
    /* Approved; left alone. */

    /* Set Lines */
    sprintf(buff, "%d", linecount);
    HDR(_lines) = xstrdup(buff);

    /* Check Supersedes. */
    if (HDR(_supersedes))
	CheckCancel(HDR(_supersedes), true);

    /* Now make sure everything is there. */
    for (hp = Table; hp < ARRAY_END(Table); hp++)
	if (hp->Type == HTreq && hp->Value == NULL)
            die("required header %s is missing or empty", hp->Name);
}


/*
**  Try to append $HOME/.signature to the article.  When in doubt, exit
**  out in order to avoid postings like "Sorry, I forgot my .signature
**  -- here's the article again."
*/
static char *
AppendSignature(bool UseMalloc, char *article, char *homedir, int *linesp)
{
    static char	NOSIG[] = "Can't add your .signature (%s), article not posted";
    int		i;
    int		length;
    size_t      artsize;
    char	*p;
    char	buff[BUFSIZ];
    FILE	*F;

    /* Open the file. */
    *linesp = 0;
    if (strlen(homedir) > sizeof(buff) - 14)
        die("home directory path too long");
    sprintf(buff, "%s/.signature", homedir);
    if ((F = fopen(buff, "r")) == NULL) {
	if (errno == ENOENT)
	    return article;
	fprintf(stderr, NOSIG, strerror(errno));
	QuitServer(1);
    }

    /* Read it in. */
    length = fread(buff, 1, sizeof buff - 2, F);
    i = feof(F);
    fclose(F);
    if (length == 0)
        die("signature file is empty");
    if (length < 0)
        sysdie("cannot read signature file");
    if (length == sizeof buff - 2 && !i)
        die("signature is too large");

    /* Make sure the buffer ends with \n\0. */
    if (buff[length - 1] != '\n')
	buff[length++] = '\n';
    buff[length] = '\0';

    /* Count the lines. */
    for (i = 0, p = buff; (p = strchr(p, '\n')) != NULL; p++)
	if (++i > SIG_MAXLINES)
            die("signature has too many lines");
    *linesp = 1 + i;

    /* Grow the article to have the signature. */
    i = strlen(article);
    artsize = i + sizeof(SIGSEP) - 1 + length + 1;
    if (UseMalloc) {
        p = xmalloc(artsize);
        strlcpy(p, article, artsize);
	article = p;
    }
    else
        article = xrealloc(article, artsize);
    strlcat(article, SIGSEP, artsize);
    strlcat(article, buff, artsize);
    return article;
}


/*
**  See if the user has more included text than new text.  Simple-minded, but
**  reasonably effective for catching neophyte's mistakes.  A line starting
**  with > is included text.  Decrement the count on lines starting with <
**  so that we don't reject diff(1) output.
*/
static void
CheckIncludedText(char *p, int lines)
{
    int	i;

    for (i = 0; ; p++) {
	switch (*p) {
	case '>':
	    i++;
	    break;
	case '|':
	    i++;
	    break;
	case ':':
	    i++;
	    break;
	case '<':
	    i--;
	    break;
	}
	if ((p = strchr(p, '\n')) == NULL)
	    break;
    }
    if ((i * 2 > lines) && (lines > 40))
        die("more included text than new text");
}



/*
**  Read stdin into a string and return it.  Can't use ReadInDescriptor
**  since that will fail if stdin is a tty.
*/
static char *
ReadStdin(void)
{
    int	size;
    char	*p;
    char		*article;
    char	*end;
    int	i;

    size = BUFSIZ;
    article = xmalloc(size);
    end = &article[size - 3];
    for (p = article; (i = getchar()) != EOF; *p++ = (char)i)
	if (p == end) {
            article = xrealloc(article, size + BUFSIZ);
	    p = &article[size - 3];
	    size += BUFSIZ;
	    end = &article[size - 3];
	}

    /* Force a \n terminator. */
    if (p > article && p[-1] != '\n')
	*p++ = '\n';
    *p = '\0';
    return article;
}



/*
**  Offer the article to the server, return its reply.
*/
static int
OfferArticle(char *buff, bool Authorized)
{
    fprintf(ToServer, "post\r\n");
    SafeFlush(ToServer);
    if (fgets(buff, HEADER_STRLEN, FromServer) == NULL)
        sysdie(Authorized ? "Can't offer article to server (authorized)"
                          : "Can't offer article to server");
    return atoi(buff);
}


/*
**  Spool article to temp file.
*/
static void
Spoolit(char *article, size_t Length, char *deadfile)
{
    HEADER *hp;
    FILE *F;
    int i;

    /* Try to write to the deadfile. */
    if (deadfile == NULL)
        return;
    F = xfopena(deadfile);
    if (F == NULL)
        sysdie("cannot create spool file");

    /* Write the headers and a blank line. */
    for (hp = Table; hp < ARRAY_END(Table); hp++)
	if (hp->Value)
	    fprintf(F, "%s: %s\n", hp->Name, hp->Value);
    for (i = 0; i < OtherCount; i++)
	fprintf(F, "%s\n", OtherHeaders[i]);
    fprintf(F, "\n");
    if (FLUSH_ERROR(F))
        sysdie("cannot write headers");

    /* Write the article and exit. */
    if (fwrite(article, 1, Length, F) != Length)
        sysdie("cannot write article");
    if (FLUSH_ERROR(F))
        sysdie("cannot write article");
    if (fclose(F) == EOF)
        sysdie("cannot close spool file");
}


/*
**  Print usage message and exit.
*/
static void
Usage(void)
{
    fprintf(stderr, "Usage: inews [-D] [-h] [header_flags] [article]\n");
    /* Don't call QuitServer here -- connection isn't open yet. */
    exit(1);
}


int
main(int ac, char *av[])
{
    static char		NOCONNECT[] = "cannot connect to server";
    int                 i;
    char                *p;
    HEADER              *hp;
    int			j;
    int			port;
    int			Mode;
    int			SigLines;
    struct passwd	*pwp;
    char		*article;
    char		*deadfile;
    char		buff[HEADER_STRLEN];
    char		SpoolMessage[HEADER_STRLEN];
    bool		DoSignature;
    bool		AddOrg;
    size_t		Length;
    uid_t               uid;

    /* First thing, set up logging and our identity. */
    message_program_name = "inews";

    /* Find out who we are. */
    uid = geteuid();
    if (uid == (uid_t) -1)
        sysdie("cannot get your user ID");
    if ((pwp = getpwuid(uid)) == NULL)
        sysdie("cannot get your passwd entry");

    /* Set defaults. */
    Mode = '\0';
    Dump = false;
    DoSignature = true;
    AddOrg = true;
    port = 0;

    if (!innconf_read(NULL))
        exit(1);

    umask(NEWSUMASK);

    /* Parse JCL. */
    while ((i = getopt(ac, av, "DNAVWORShx:a:c:d:e:f:n:p:r:t:F:o:w:")) != EOF)
	switch (i) {
	default:
	    Usage();
	    /* NOTREACHED */
	case 'D':
	case 'N':
	    Dump = true;
	    break;
	case 'A':
	case 'V':
	case 'W':
	    /* Ignore C News options. */
	    break;
	case 'O':
	    AddOrg = false;
	    break;
	case 'R':
	    Revoked = true;
	    break;
	case 'S':
	    DoSignature = false;
	    break;
	case 'h':
	    Mode = i;
	    break;
	case 'x':
            Exclusions = concat(optarg, "!", (char *) 0);
	    break;
	 case 'p':
	    port = atoi(optarg);
	    break;
	/* Header lines that can be specified on the command line. */
	case 'a':	HDR(_approved) = optarg;		break;
	case 'c':	HDR(_control) = optarg;			break;
	case 'd':	HDR(_distribution) = optarg;		break;
	case 'e':	HDR(_expires) = optarg;			break;
	case 'f':	HDR(_from) = optarg;			break;
	case 'n':	HDR(_newsgroups) = optarg;		break;
	case 'r':	HDR(_replyto) = optarg;			break;
	case 't':	HDR(_subject) = optarg;			break;
	case 'F':	HDR(_references) = optarg;		break;
	case 'o':	HDR(_organization) = optarg;		break;
	case 'w':	HDR(_followupto) = optarg;		break;
	}
    ac -= optind;
    av += optind;

    /* Parse positional arguments; at most one, the input file. */
    switch (ac) {
    default:
	Usage();
	/* NOTREACHED */
    case 0:
	/* Read stdin. */
	article = ReadStdin();
	break;
    case 1:
	/* Read named file. */
	article = ReadInFile(av[0], (struct stat *)NULL);
	if (article == NULL)
            sysdie("cannot read input file");
	break;
    }

    if (port == 0) port = (innconf->port != 0) ? innconf->port : NNTP_PORT;

    /* Try to open a connection to the server. */
    if (NNTPremoteopen(port, &FromServer, &ToServer, buff) < 0) {
	Spooling = true;
	if ((p = strchr(buff, '\n')) != NULL)
	    *p = '\0';
	if ((p = strchr(buff, '\r')) != NULL)
	    *p = '\0';
	strcpy(SpoolMessage, buff[0] ? buff : NOCONNECT);
        deadfile = concatpath(pwp->pw_dir, "dead.article");
    }
    else {
        /* We now have an open server connection, so close it on failure. */
        message_fatal_cleanup = fatal_cleanup;

	/* See if we can post. */
	i = atoi(buff);

	/* Tell the server we're posting. */
	setbuf(FromServer, xmalloc(BUFSIZ));
	setbuf(ToServer, xmalloc(BUFSIZ));
	fprintf(ToServer, "mode reader\r\n");
	SafeFlush(ToServer);
	if (fgets(buff, HEADER_STRLEN, FromServer) == NULL)
            sysdie("cannot tell server we're reading");
	if ((j = atoi(buff)) != NNTP_BAD_COMMAND_VAL)
	    i = j;

	if (i != NNTP_POSTOK_VAL)
            die("you do not have permission to post");
	deadfile = NULL;
    }

    /* Basic processing. */
    for (hp = Table; hp < ARRAY_END(Table); hp++)
	hp->Size = strlen(hp->Name);
    if (Mode == 'h')
	article = StripOffHeaders(article);
    for (i = 0, p = article; (p = strchr(p, '\n')) != NULL; i++, p++)
	continue;
    if (innconf->checkincludedtext)
	CheckIncludedText(article, i);
    if (DoSignature)
	article = AppendSignature(Mode == 'h', article, pwp->pw_dir, &SigLines);
    else
	SigLines = 0;
    ProcessHeaders(AddOrg, i + SigLines, pwp);
    Length = strlen(article);
    if ((innconf->localmaxartsize > 0)
	    && (Length > (size_t)innconf->localmaxartsize))
        die("article is larger than local limit of %ld bytes",
            innconf->localmaxartsize);

    /* Do final checks. */
    if (i == 0 && HDR(_control) == NULL)
        die("article is empty");
    for (hp = Table; hp < ARRAY_END(Table); hp++)
	if (hp->Value && (int)strlen(hp->Value) + hp->Size > HEADER_STRLEN)
            die("%s header is too long", hp->Name);
    for (i = 0; i < OtherCount; i++)
	if ((int)strlen(OtherHeaders[i]) > HEADER_STRLEN)
            die("header too long (maximum length is %d): %.40s...",
                HEADER_STRLEN, OtherHeaders[i]);

    if (Dump) {
	/* Write the headers and a blank line. */
	for (hp = Table; hp < ARRAY_END(Table); hp++)
	    if (hp->Value)
		printf("%s: %s\n", hp->Name, hp->Value);
	for (i = 0; i < OtherCount; i++)
	    printf("%s\n", OtherHeaders[i]);
	printf("\n");
	if (FLUSH_ERROR(stdout))
            sysdie("cannot write headers");

	/* Write the article and exit. */
	if (fwrite(article, 1, Length, stdout) != Length)
            sysdie("cannot write article");
	SafeFlush(stdout);
	QuitServer(0);
    }

    if (Spooling) {
        warn("warning: %s", SpoolMessage);
        warn("article will be spooled");
	Spoolit(article, Length, deadfile);
	exit(0);
    }

    /* Article is prepared, offer it to the server. */
    i = OfferArticle(buff, false);
    if (i == NNTP_AUTH_NEEDED_VAL) {
	/* Posting not allowed, try to authorize. */
	if (NNTPsendpassword((char *)NULL, FromServer, ToServer) < 0)
            sysdie("authorization error");
	i = OfferArticle(buff, true);
    }
    if (i != NNTP_START_POST_VAL)
        die("server doesn't want the article: %s", buff);

    /* Write the headers, a blank line, then the article. */
    for (hp = Table; hp < ARRAY_END(Table); hp++)
	if (hp->Value)
	    fprintf(ToServer, "%s: %s\r\n", hp->Name, hp->Value);
    for (i = 0; i < OtherCount; i++)
	fprintf(ToServer, "%s\r\n", OtherHeaders[i]);
    fprintf(ToServer, "\r\n");
    if (NNTPsendarticle(article, ToServer, true) < 0)
        sysdie("cannot send article to server");
    SafeFlush(ToServer);

    if (fgets(buff, sizeof buff, FromServer) == NULL)
        sysdie("no reply from server after sending the article");
    if ((p = strchr(buff, '\r')) != NULL)
	*p = '\0';
    if ((p = strchr(buff, '\n')) != NULL)
	*p = '\0';
    if (atoi(buff) != NNTP_POSTEDOK_VAL)
        sysdie("cannot send article to server: %s", buff);

    /* Close up. */
    QuitServer(0);
    /* NOTREACHED */
    return 1;
}
