/*  $Revision: 1.10 $
**
**  Article-related routines.
*/
#include "nnrpd.h"


typedef enum _SENDTYPE {
    STarticle,
    SThead,
    STbody,
    STstat
} SENDTYPE;

typedef struct _SENDDATA {
    SENDTYPE	Type;
    int		ReplyCode;
    STRING	Item;
    STRING	BossQuery;
} SENDDATA;


STATIC char		ARTnotingroup[] = NNTP_NOTINGROUP;
STATIC char		ARTnoartingroup[] = NNTP_NOARTINGRP;
STATIC char		ARTnocurrart[] = NNTP_NOCURRART;
STATIC QIOSTATE		*ARTqp;
STATIC SENDDATA		SENDbody = {
    STbody,	NNTP_BODY_FOLLOWS_VAL,		"body",		NULL
};
STATIC SENDDATA		SENDarticle = {
    STarticle,	NNTP_ARTICLE_FOLLOWS_VAL,	"article",	NULL
};
STATIC SENDDATA		SENDstat = {
    STstat,	NNTP_NOTHING_FOLLOWS_VAL,	"status",	"stat"
};
STATIC SENDDATA		SENDhead = {
    SThead,	NNTP_HEAD_FOLLOWS_VAL,		"head",		"head"
};


/*
**  If we have an article open, close it.
*/
void
ARTclose()
{
    if (ARTqp) {
	QIOclose(ARTqp);
	ARTqp = NULL;
    }
}


/*
**  Get the Message-ID from a file.
*/
STATIC void
ARTgetmsgid(qp, id)
    register QIOSTATE	*qp;
    char		*id;
{
    register char	*p;
    register char	*q;

    for (*id = '\0'; (p = QIOread(qp)) != NULL && *p != '\0'; ) {
	if (*p != 'M' && *p != 'm')
	    continue;
	if ((q = strchr(p, ' ')) == NULL)
	    continue;
	*q++ = '\0';
	if (caseEQ(p, "Message-ID:")) {
	    (void)strcpy(id, q);
	    break;
	}
    }
    (void)QIOrewind(qp);
}


/*
**  If the article name is valid, open it and stuff in the ID.
*/
STATIC BOOL
ARTopen(name, id)
    char	*name;
    char	*id;
{
    static int	save_artnum;
    static char	save_artid[BIG_BUFFER];
    struct stat	Sb;

    /* Re-use article if it's the same one. */
    if (ARTqp != NULL) {
	if (save_artnum == atoi(name) && QIOrewind(ARTqp) != -1) {
	    if (id)
		(void)strcpy(id, save_artid);
	    return TRUE;
	}
	QIOclose(ARTqp);
    }

    /* Open it, make sure it's a regular file. */
    if ((ARTqp = QIOopen(name, QIO_BUFFER)) == NULL)
	return FALSE;
    if (fstat(QIOfileno(ARTqp), &Sb) < 0 || !S_ISREG(Sb.st_mode)) {
	QIOclose(ARTqp);
	ARTqp = NULL;
	return FALSE;
    }
    CloseOnExec(QIOfileno(ARTqp));

    save_artnum = atoi(name);
    ARTgetmsgid(ARTqp, save_artid);
    (void)strcpy(id, save_artid);
    return TRUE;
}


/*
**  Open the article for a given Message-ID.
*/
STATIC QIOSTATE *
ARTopenbyid(msg_id)
    char	*msg_id;
{
    STRING	p;
    QIOSTATE	*qp;

    if ((p = HISgetent(msg_id, FALSE)) == NULL)
	return NULL;
    if ((qp = QIOopen(p, QIO_BUFFER)) == NULL)
	return NULL;
    CloseOnExec(QIOfileno(qp));
    return qp;
}


/*
**  Send a (part of) a file to stdout, doing newline and dot conversion.
*/
STATIC void
ARTsend(qp, what)
    register QIOSTATE	*qp;
    SENDTYPE		what;
{
    register char	*p;

    ARTcount++;
    GRParticles++;

    /* Get the headers. */
    for ( ; ; ) {
	p = QIOread(qp);
	if (p == NULL) {
	    if (QIOtoolong(qp))
		continue;
	    break;
	}
	if (*p == '\0')
	    break;
	if (what == STbody)
	    continue;
	Printf("%s%s\r\n", *p == '.' ? "." : "", p);
    }

    if (what == SThead) {
	Printf(".\r\n");
	return;
    }

    if (what == STarticle)
	Printf("\r\n");
    for ( ; ; ) {
	p = QIOread(qp);
	if (p == NULL) {
	    if (QIOtoolong(qp))
		continue;
	    break;
	}
	Printf("%s%s\r\n", *p == '.' ? "." : "", p);
    }
    Printf(".\r\n");
}


/*
**  Find an article number in the article array.
**  Should take advantage of the fact that this is sorted.
*/
STATIC int
ARTfind(name)
    char		*name;
{
    register int	*ip;
    register int	*end;
    register int	i;

    for (i = atoi(name), ip = ARTnumbers, end = &ip[ARTsize]; ip < end; ip++)
	if (*ip == i)
	    return ip - ARTnumbers;

    return -1;
}


/*
**  Ask the innd server for the article.  Only called from CMDfetch,
**  and only if history file is buffered.  Common case:  "oops, cancel
**  that article I just posted."
*/
STATIC QIOSTATE *
ARTfromboss(what, id)
    SENDDATA		*what;
    char		*id;
{
    FILE		*F;
    FILE		*FromServer;
    FILE		*ToServer;
    QIOSTATE		*qp;
    char		buff[NNTP_STRLEN + 2];
    char		name[SMBUF];
    char		*p;

    /* If we can, open the connection. */
    if (what->BossQuery == NULL)
	return NULL;
    if (NNTPlocalopen(&FromServer, &ToServer, (char *)NULL) < 0)
	return NULL;

    /* Send the query to the server. */
    qp = NULL;
    (void)fprintf(ToServer, "%s %s\r\n", what->BossQuery, id);
    (void)fflush(ToServer);
    if (ferror(ToServer))
	goto QuitClose;

    /* Get the reply; article exist? */
    if (fgets(buff, sizeof buff, FromServer) == NULL
     || atoi(buff) == NNTP_DONTHAVEIT_VAL)
	goto QuitClose;

    /* Yes.  Be quick if just doing a stat. */
    if (what == &SENDstat) {
	qp = QIOopen("/dev/null", 0);
	goto QuitClose;
    }

    /* Open a temp file to hold the article. */
    p = getenv("TMPDIR");
    (void)sprintf(name, "%s/artXXXXXX", p ? p : _PATH_TMP);
    if ((F = fopen(name, "w")) == NULL)
	goto QuitClose;

    /* Write the data into a temp file. */
    while (fgets(buff, sizeof buff, FromServer) != NULL) {
	if ((p = strchr(buff, '\r')) != NULL)
	    *p = '\0';
	if ((p = strchr(buff, '\n')) != NULL)
	    *p = '\0';
	if (EQ(buff, "."))
	    break;
    }
    if (ferror(FromServer)) {
	(void)fclose(F);
	goto QuitClose;
    }
    if (fclose(F) == EOF)
	goto QuitClose;

    /* Open the file, remove it while open, and fall through to return
     * the anonymous temp file. */
    qp = QIOopen(name, QIO_BUFFER);
    (void)unlink(name);

    /* Send quit, read server's reply, close up and return. */
QuitClose:
    (void)fprintf(ToServer, "quit\r\n");
    (void)fclose(ToServer);
    (void)fgets(buff, sizeof buff, FromServer);
    (void)fclose(FromServer);
    return qp;
}


/*
**  Fetch part or all of an article and send it to the client.
*/
FUNCTYPE
CMDfetch(ac, av)
    int			ac;
    char		*av[];
{
    char		buff[SMBUF];
    char		idbuff[BIG_BUFFER];
    SENDDATA		*what;
    register QIOSTATE	*qp;
    register BOOL	ok;

    /* Find what to send; get permissions. */
    ok = PERMcanread;
    switch (*av[0]) {
    default:
	what = &SENDbody;
	break;
    case 'a': case 'A':
	what = &SENDarticle;
	break;
    case 's': case 'S':
	what = &SENDstat;
	break;
    case 'h': case 'H':
	what = &SENDhead;
	/* Poster might do a "head" command to verify the article. */
	ok = PERMcanread || PERMcanpost;
	break;
    }

    if (!ok) {
	Reply("%s\r\n", NOACCESS);
	return;
    }

    /* Requesting by Message-ID? */
    if (ac == 2 && av[1][0] == '<') {
	if ((qp = ARTopenbyid(av[1])) == NULL
	 && (qp == ARTfromboss(what, av[1])) == NULL) {
	    Reply("%d No such article\r\n", NNTP_DONTHAVEIT_VAL);
	    return;
	}
	if (!PERMartok(qp)) {
	    QIOclose(qp);
	    Reply("%s\r\n", NOACCESS);
	    return;
	}
	Reply("%d 0 %s %s\r\n", what->ReplyCode, what->Item, av[1]);
	if (what->Type != STstat)
	    ARTsend(qp, what->Type);
	QIOclose(qp);
	return;
    }

    /* Trying to read. */
    if (GRPcount == 0) {
	Reply("%s\r\n", ARTnotingroup);
	return;
    }

    /* Default is to get current article, or specified article. */
    if (ac == 1) {
	if (ARTindex < 0 || ARTindex >= ARTsize) {
	    Reply("%s\r\n", ARTnocurrart);
	    return;
	}
	(void)sprintf(buff, "%d", ARTnumbers[ARTindex]);
    }
    else {
	if (strspn(av[1], "0123456789") != strlen(av[1])) {
	    Reply("%s\r\n", ARTnoartingroup);
	    return;
	}
	(void)strcpy(buff, av[1]);
    }

    /* Move forward until we can find one. */
    while (!ARTopen(buff, idbuff)) {
	if (ac > 1 || ++ARTindex >= ARTsize) {
	    Reply("%s\r\n", ARTnoartingroup);
	    return;
	}
	(void)sprintf(buff, "%d", ARTnumbers[ARTindex]);
    }

    Reply("%d %s %s %s\r\n", what->ReplyCode, buff, idbuff, what->Item);
    if (what->Type != STstat)
	ARTsend(ARTqp, what->Type);
    if (ac > 1)
	ARTindex = ARTfind(buff);
}


/*
**  Go to the next or last (really previous) article in the group.
*/
FUNCTYPE
CMDnextlast(ac, av)
    int		ac;
    char	*av[];
{
    char	buff[SPOOLNAMEBUFF];
    char	idbuff[SMBUF];
    int		save;
    BOOL	next;
    int		delta;
    int		errcode;
    STRING	message;

    if (!PERMcanread) {
	Reply("%s\r\n", NOACCESS);
	return;
    }
    if (GRPcount == 0) {
	Reply("%s\r\n", ARTnotingroup);
	return;
    }
    if (ARTindex < 0 || ARTindex >= ARTsize) {
	Reply("%s\r\n", ARTnocurrart);
	return;
    }

    next = (av[0][0] == 'n' || av[0][0] == 'N');
    if (next) {
	delta = 1;
	errcode = NNTP_NONEXT_VAL;
	message = "next";
    }
    else {
	delta = -1;
	errcode = NNTP_NOPREV_VAL;
	message = "previous";
    }

    save = ARTindex;
    ARTindex += delta;
    if (ARTindex < 0 || ARTindex >= ARTsize) {
	Reply("%d No %s to retrieve.\r\n", errcode, message);
	ARTindex = save;
	return;
    }

    (void)sprintf(buff, "%d", ARTnumbers[ARTindex]);
    while (!ARTopen(buff, idbuff)) {
	ARTindex += delta;
	if (ARTindex < 0 || ARTindex >= ARTsize) {
	    Reply("%d No %s article to retrieve.\r\n", errcode, message);
	    ARTindex = save;
	    return;
	}
	(void)sprintf(buff, "%d", ARTnumbers[ARTindex]);
    }

    Reply("%d %s %s Article retrieved; request text separately.\r\n",
	   NNTP_NOTHING_FOLLOWS_VAL, buff, idbuff);

    if (ac > 1)
	ARTindex = ARTfind(buff);
}


/*
**  XHDR, a common extension.  Retrieve specified header from a
**  Message-ID or article range.
*/
STATIC void
PrintHeader(qp, header, name, IsLines)
    register QIOSTATE	*qp;
    register char	*header;
    register char	*name;
    BOOL		IsLines;
{
    register char	*p;
    register char	*q;
    struct stat		Sb;

    for ( ; ; ) {
	p = QIOread(qp);
	if (p == NULL) {
	    if (QIOtoolong(qp))
		continue;
	    break;
	}
	if (*p == '\0')
	    break;
	if ((q = strchr(p, ':')) == NULL)
	    continue;
	*q = '\0';
	if (caseEQ(header, p)) {
	    Printf("%s %s\r\n", name, q + 2);
	    return;
	}
    }
    if (IsLines && fstat(QIOfileno(qp), &Sb) >= 0) {
	/* Lines estimation taken from Tor Lillqvist <tml@tik.vtt.fi>'s
	 * posting <TML.92Jul10031233@hemuli.tik.vtt.fi> in
	 * news.sysadmin. */
 	Printf("%s %d\r\n",
	    name,
	    (int)(6.4e-8 * Sb.st_size * Sb.st_size + 0.023 * Sb.st_size - 12));
	return;
    }
    Printf("%s (none)\r\n", name);
}


FUNCTYPE
CMDxhdr(ac, av)
    int			ac;
    char		*av[];
{
    register char	*p;
    register QIOSTATE	*qp;
    register int	artnum;
    register int	high;
    register int	i;
    register int	low;
    BOOL		IsLines;
    char		buff[SPOOLNAMEBUFF];

    if (!PERMcanread) {
	Reply("%s\r\n", NOACCESS);
	return;
    }
    IsLines = caseEQ(av[1], "lines");

    /* Message-ID specified? */
    if (ac == 3 && av[2][0] == '<') {
	qp = ARTopenbyid(av[2]);
	if (qp == NULL) {
	    Reply("%d No such article\r\n", NNTP_DONTHAVEIT_VAL);
	    return;
	}
	Reply("%d 0 %s header of article %s.\r\n",
	       NNTP_HEAD_FOLLOWS_VAL, av[1], av[2]);
	PrintHeader(qp, av[1], av[2], IsLines);
	QIOclose(qp);
	Printf(".\r\n");
	return;
    }

    /* Range specified -- must be in a group. */
    if (GRPcount == 0) {
	Reply("%s\r\n", ARTnotingroup);
	return;
    }

    if (ac == 2) {
	if (ARTindex < 0 || ARTindex >= ARTsize) {
	    Reply("%s\r\n", ARTnocurrart);
	    return;
	}
	/* No argument, do only current article. */
	high = low = ARTnumbers[ARTindex];
	i = ARTindex;
    }
    else {
	if ((p = strchr(av[2], '-')) == NULL)
	    low = high = atoi(av[2]);
	else {
	    *p++ = '\0';
	    low = atoi(av[2]);
	    high = atoi(p);
	    if (high < low)
		high = ARTnumbers[ARTsize - 1];
	}
	i = 0;
    }

    Reply("%d %s fields follow\r\n", NNTP_HEAD_FOLLOWS_VAL, av[1]);
    for (; i < ARTsize; i++) {
	if ((artnum = ARTnumbers[i]) < low)
	    continue;
	if (artnum > high)
	    break;

	(void)sprintf(buff, "%d", artnum);
	if ((qp = QIOopen(buff, QIO_BUFFER)) != NULL) {
	    PrintHeader(qp, av[1], buff, IsLines);
	    QIOclose(qp);
	}
    }

    Printf(".\r\n");
}


/*
**  XPAT, an uncommon extension.  Print only headers that match the pattern.
*/
STATIC void
PrintMatchHeader(qp, header, pattern, name)
    register QIOSTATE	*qp;
    register char	*header;
    register char	*pattern;
    register char	*name;
{
    register char	*p;
    register char	*q;

    for ( ; ; ) {
	p = QIOread(qp);
	if (p == NULL) {
	    if (QIOtoolong(qp))
		continue;
	    break;
	}
	if (*p == '\0')
	    break;
	if ((q = strchr(p, ':')) == NULL)
	    continue;
	*q = '\0';
	if (caseEQ(header, p)) {
	    q += 2;
	    if (wildmat(q, pattern))
		Printf("%s %s\r\n", name, q);
	    break;
	}
    }
}


/* ARGSUSED */
FUNCTYPE
CMDxpat(ac, av)
    int			ac;
    char		*av[];
{
    register char	*p;
    register QIOSTATE	*qp;
    char		*header;
    char		*pattern;
    register int	artnum;
    register int	high;
    register int	i;
    register int	low;
    char		buff[SPOOLNAMEBUFF];

    if (!PERMcanread) {
	Printf("%s\r\n", NOACCESS);
	return;
    }

    header = av[1];

    /* Message-ID specified? */
    if (av[2][0] == '<') {
	p = av[2];
	qp = ARTopenbyid(p);
	if (qp == NULL) {
	    Printf("%d No such article.\r\n", NNTP_DONTHAVEIT_VAL);
	    return;
	}
	Printf("%d %s matches follow.\r\n", NNTP_HEAD_FOLLOWS_VAL, header);
	pattern = Glom(&av[3]);
	PrintMatchHeader(qp, header, pattern, p);
	QIOclose(qp);
	Printf(".\r\n");
	DISPOSE(pattern);
	return;
    }

    /* Range specified -- must be in a group. */
    if (GRPcount == 0) {
	Printf("%s\r\n", ARTnotingroup);
	return;
    }

    if ((p = strchr(av[2], '-')) == NULL)
	low = high = atoi(av[2]);
    else {
	*p++ = '\0';
	low = atoi(av[2]);
	high = atoi(p);
	if (high < low)
	    high = ARTnumbers[ARTsize - 1];
    }

    Printf("%d %s matches follow.\r\n", NNTP_HEAD_FOLLOWS_VAL, header);
    pattern = Glom(&av[3]);
    for (i = 0; i < ARTsize; i++) {
	if ((artnum = ARTnumbers[i]) < low)
	    continue;
	if (artnum > high)
	    break;

	(void)sprintf(buff, "%d", artnum);
	if ((qp = QIOopen(buff, QIO_BUFFER)) != NULL) {
	    PrintMatchHeader(qp, header, pattern, buff);
	    QIOclose(qp);
	}
    }

    Printf(".\r\n");
    DISPOSE(pattern);
}
