/* TODO: backoff algorithm */
/* TODO: moderator mail error check */
/* TODO: check if authenticated users are properly put in X-Trace */

/*
 * POST.C
 *
 *	Post an article.  The article is posted to one or several of the
 *	upstream servers depending on prioritization and the existence of
 *	the 'P'ost flag.  
 *
 *	The article is loaded into memory prior to posting.  If no servers
 *	are available, the article is written to a local posting queue and
 *	the post is retried later.
 *
 * (c)Copyright 1998, Matthew Dillon, All Rights Reserved.  Refer to
 *    the COPYRIGHT file in the base directory of this distribution
 *    for specific rights granted.
 */

#include "defs.h"

#ifdef	POST_FORCEMSGID
#ifndef	POST_CKPATHLOOP
#error	"Please go back and read the message in vendor.h about POST_FORCEMSGID"
#endif
#endif

#ifdef	POST_CFILTERHOOK
char *PostCFilter(char *base, int artLen, Connection *conn);
#endif

Prototype void NNPostBuffer(Connection *conn);
Prototype void NNPostCommand1(Connection *conn);

void NNPostResponse1(Connection *conn);
void NNPostArticle1(Connection *conn);
void NNPostResponse2(Connection *conn);
int MailIfModerated(char *groups, Connection *conn);
int groupAlready(int v, int *save, int saveCount);
void MailToModerator(const char *group, Connection *conn, const char *modMail);
int CheckGroupModeration(const char *group);
void GenerateDistribution(MBufHead *mbuf, char *newsGrps);
int BreakIfBadGroup(char *groups, Connection *conn);
int CheckValidRestrictedGroup(const char *group);

#ifdef	POST_HIDENNTPPOSTHOST

/* JG19990908 don't publicize NNPH's, but still present a NNPH field that
   can be used to identify posts from the same node (i.e. for things like
   UPS and DSRS).  Hash changes every 1hour. */

char *NNPHMangle(char *vserv, char *node, char *cryptpw)
{
	char buffer[256], md5buf[33];
	static char res[256];

	/* This string is arbitrary, as long as it is the same for
	   any given node, you're ok */
	snprintf(buffer, sizeof(buffer), "%s/%d/%s/%s", node, (int)time(NULL) / 3600, vserv, cryptpw);
	/* Safety: in the event we cant md5, then leave some useful
	   data in md5buf */
	snprintf(md5buf, sizeof(md5buf), "%s", node);
#ifdef	__APPLE_CC__
	MD5(buffer, strlen(buffer), md5buf);
#else
	MD5Data(buffer, strlen(buffer), md5buf);
#endif
	/* Clip the string.  8 digits is enough */
	md5buf[8] = '\0';
	snprintf(res, sizeof(res), "%s.%s", md5buf, vserv);
	return(res);
}
#endif

#ifdef	POST_CRYPTXTRACE

/* JG19990908 encrypt (for loose def'n of encrypt, heh) X-Trace, presenting
   a uuencode-style line that can be decoded by abuse */

char *XTRAMangle(char *xtrace, char *cryptpw)
{
	static char res[256];
	char *rptr, *xptr;
	int count;
	unsigned char plain[8], enc[8];
	des_cblock key;
	des_key_schedule sched;

	des_string_to_key(cryptpw, &key);
	des_set_key(&key, sched);

	rptr = res;

	/* Overflow protect; 192 chars expands to 256, overflowing res */
	if (strlen(xtrace) > 191) {
		xtrace[191] = '\0';
	}

	/* Step thru 'xtrace' 8 chars at a time, encrypt as we go */
	xptr = xtrace;
	count = 0;
	bzero(plain, sizeof(plain));
	while (*xptr) {
	    plain[count++ % 8] = *xptr++;
	    if (! *xptr || (count % 8) == 0) {
		/* Encrypt block of (up to) 8 and pump out coded ASCII */
		des_ecb_encrypt((des_cblock *)plain,(des_cblock *)enc, sched, 1);
		bzero(plain, sizeof(plain));
		*rptr++ = '0' + (enc[0] & 0x3f);
		*rptr++ = '0' + (enc[0] >> 6) + ((enc[1] & 0x0f) << 2);
		*rptr++ = '0' + (enc[1] >> 4) + ((enc[2] & 0x03) << 4);
		*rptr++ = '0' + (enc[2] >> 2);
		*rptr++ = '0' + (enc[3] & 0x3f);
		*rptr++ = '0' + (enc[3] >> 6) + ((enc[4] & 0x0f) << 2);
		*rptr++ = '0' + (enc[4] >> 4) + ((enc[5] & 0x03) << 4);
		*rptr++ = '0' + (enc[5] >> 2);
		*rptr++ = '0' + (enc[6] & 0x3f);
		*rptr++ = '0' + (enc[6] >> 6) + ((enc[7] & 0x0f) << 2);
		*rptr++ = '0' + (enc[7] >> 4) + ((time(NULL) & 0x03) << 4);
	    }
	}
	*rptr = '\0';
	return(res);
}

char *XTRAUnmangle(char *str, char *cryptpw)
{
	static char res[256];
	char *rptr, *sptr;
	int count, i;
	unsigned char plain[8], enc[8];
	des_cblock key;
	des_key_schedule sched;

	des_string_to_key(cryptpw, &key);
	des_set_key(&key, sched);

	if (! strncmp(str, "DXC=", 4)) {
		str += 4;
	}

	*res = '\0';
	if (strlen(str) % 11) {
		snprintf(res, sizeof(res), "(X-Trace damaged; may not decode OK) ");
	}

	rptr = res + strlen(res);
	
	sptr = str;
	count = 0;
	bzero(enc, sizeof(enc));
	while (*sptr) {
	    switch (count % 11) {
		case 0:
		    enc[0] = (*sptr - '0');
		    break;
		case 1:
		    enc[0] += ((*sptr - '0') & 0x03) << 6;
		    enc[1] = (*sptr - '0') >> 2;
		    break;
		case 2:
		    enc[1] += ((*sptr - '0') & 0x0f) << 4;
		    enc[2] = (*sptr - '0') >> 4;
		    break;
		case 3:
		    enc[2] += (*sptr - '0') << 2;
		    break;
		case 4:
		    enc[3] = (*sptr - '0');
		    break;
		case 5:
		    enc[3] += ((*sptr - '0') & 0x03) << 6;
		    enc[4] = (*sptr - '0') >> 2;
		    break;
		case 6:
		    enc[4] += ((*sptr - '0') & 0x0f) << 4;
		    enc[5] = (*sptr - '0') >> 4;
		    break;
		case 7:
		    enc[5] += (*sptr - '0') << 2;
		    break;
		case 8:
		    enc[6] = (*sptr - '0');
		    break;
		case 9:
		    enc[6] += ((*sptr - '0') & 0x03) << 6;
		    enc[7] = (*sptr - '0') >> 2;
		    break;
		case 10:
		    enc[7] += ((*sptr - '0') & 0x0f) << 4;
		    break;
	    }
	    count++;
	    sptr++;
	    if (! *sptr || (count % 11) == 0) {
		/* Decrypt block of (up to) 8 and pump out plain ASCII */
		des_ecb_encrypt((des_cblock *)enc,(des_cblock *)plain, sched, 0);
		bzero(enc, sizeof(enc));
		for (i = 0; i < 8; i++) {
			*rptr++ = plain[i];
		}
	    }
	}
	*rptr = '\0';
	return(res);
}
#endif

/*
 * NNPostBuffer() - called when client connection post buffer contains the
 *		    entire article.  We need to check the article headers
 *		    and post it as appropriate.  If we are unable to post it
 *		    immediately, we queue it.
 *
 * STEPS:
 *	(1) remove headers otherwise generated internally (for security)
 *	(2) add internally generated headers
 *	
 * Internally generated headers:
 *	Path:			(removed, generated)
 *	Date:			(removed, generated)
 *	Lines:			(removed, generated)
 *	Message-ID:		(checked, generated)
 *	NNTP-Posting-Date:	(removed, generated - optionally)
 *	NNTP-Posting-Host:	(removed, generated)
 *	X-Trace:		(removed, generated)
 *	Xref:			(removed, RIP)
 *	Distribution:		(checked, generated)
 *
 * Errors:
 *	441 Article has no body -- just headers
 *	441 Required "XXX" header is missing (Subject, From, Newsgroups)
 *	441 435 Bad Message-ID
 */

void
NNPostBuffer(Connection *conn)
{
    int artLen;
    int haveMsgId = 0;
    int haveSubject = 0;
    int haveFrom = 0;
    int haveDate = 0;
    int haveApproved = 0;
    int haveNewsgroups = 0;
    int haveOrganization = 0;
    int haveDist = 0;
    const char *errMsg = NULL;
    char *newsGrps = NULL;
    char *base = MBNormalize(&conn->co_ArtBuf, &artLen);
#ifdef	POST_CRYPTXTRACE
    char xtrace[256];
#endif
    MBufHead	tmbuf;

    MBInit(&tmbuf, -1, &conn->co_MemPool, &conn->co_BufPool);

    if (!errMsg && conn->co_Flags & COF_POSTTOOBIG) {
	conn->co_Flags &= ~COF_POSTTOOBIG;
	errMsg = "441 Article too big for this server\r\n";
    }

#ifdef	POST_CFILTERHOOK
    if (! errMsg) {
	errMsg = PostCFilter(base, artLen, conn);
    }
#endif

    /*
     * Output Path: line
     */

    MBPrintf(&tmbuf, "Path: %s%snot-for-mail\r\n",
		conn->co_Auth.dr_VServerDef->vs_PostPath, 
		(*conn->co_Auth.dr_VServerDef->vs_PostPath ? "!" : ""));

    /*
     * Scan Headers, check for required headers
     */

    while (artLen > 1 && !(base[0] == '\n') && !(base[0] == '\r' && base[1] == '\n')) {
	int l;
	int keep = 1;

	/*
	 * extract header.  Headers can be multi-line so deal with that.
	 */

	for (l = 0; l < artLen; ++l) {
	    if (base[l] == '\r' && 
		base[l+1] == '\n' &&
		(l + 2 >= artLen || (base[l+2] != ' ' && base[l+2] != '\t'))
	    ) {
		l += 2;
		break;
	    }
	}

	/*
	 * check against specials
	 */
	if (strncasecmp(base, "Subject:", 8) == 0) {
	    haveSubject = 1;
	} else if (strncasecmp(base, "From:", 5) == 0) {
#ifdef	POST_CKADDRESS
	    char *ptr = zallocStrTrim(&conn->co_MemPool, base+5, l - 5);
	    if (ptr && *ptr) {
		HeaderCleanFrom(ptr);
		/*
		 * If somebody realm authenticated, then they are
		 * unconditionally allowed to use that address to post
		 */
		if (! (strchr(ptr, '@') && *conn->co_Auth.dr_AuthUser && ! strcasecmp(ptr, conn->co_Auth.dr_AuthUser))) {
			/*
			 * Otherwise we sanity check the address
			 */
			if (ckaddress(ptr)) {
			    errMsg = "441 From: address not in Internet syntax\r\n";
			}
		}
	    }
	    zfreeStr(&conn->co_MemPool, &ptr);
#endif
	    haveFrom = 1;
	} else if (strncasecmp(base, "Approved:", 9) == 0) {
	    haveApproved = 1;
	} else if (strncasecmp(base, "Newsgroups:", 11) == 0) {
	    haveNewsgroups = 1;
	    newsGrps = zallocStrTrim(&conn->co_MemPool, base + 11, l - 11);
	} else if (strncasecmp(base, "Organization:", 13) == 0) {
	    haveOrganization = 1;
	} else if (strncasecmp(base, "Date:", 5) == 0) {
	    haveDate = 1;
	} else if (strncasecmp(base, "Distribution:", 13) == 0) {
	    /*
	     * Only keep the header and set haveDist if it has something in 
	     * it, otherwise we (potentially) override it.
	     */
	    {
		char *ptr = zallocStrTrim(&conn->co_MemPool, base+13, l - 13);
		if (ptr && *ptr)
		    haveDist = 1;
		zfreeStr(&conn->co_MemPool, &ptr);
	    }
	    if (haveDist == 0)
		keep = 0;
	} else if (strncasecmp(base, "Path:", 5) == 0) {
	    /* JG19990907 path element count and reject */
	    char *ptr = zallocStrTrim(&conn->co_MemPool, base+5, l - 5);
	    if (conn->co_Auth.dr_ReaderDef->rd_PathComponents && ptr && *ptr) {
		char *p = ptr;
		int count = 0;
		while ((p = strchr(p + 1, '!')))
			count++;
		if (count > conn->co_Auth.dr_ReaderDef->rd_PathComponents)
			errMsg = "441 Can't inject non-local news article\r\n";

#ifdef POST_CKPATHLOOP
		if (strstr(ptr, POST_CKPATHLOOP))
			errMsg = "441 Can't reinject news article\r\n";
#endif
	    }
	    zfreeStr(&conn->co_MemPool, &ptr);
	    keep = 0;	/* delete */
	} else if (strncasecmp(base, "Date:", 5) == 0) {
	    keep = 0;	/* delete */
	} else if (strncasecmp(base, "Lines:", 6) == 0) {
	    keep = 0;	/* delete */
	} else if (strncasecmp(base, "Message-ID:", 11) == 0) {
#ifdef	POST_FORCEMSGID
	    keep = 0;   /* delete */
#else
	    int save = base[l-2];
	    const char *msgmsgid;

	    base[l-2] = 0;
	    if (strcmp((msgmsgid = MsgId(base + 11, NULL)), "<>") == 0 ||
		strchr(base, '@') == NULL
	    ) {
		keep = 0;	/* delete */
		errMsg = "441 435 Bad Message-ID\r\n";
		if (conn->co_IHaveMsgId)
		    zfreeStr(&conn->co_MemPool, &conn->co_IHaveMsgId);
	    } else {
		keep = 1;
		haveMsgId = 1;
		conn->co_IHaveMsgId = zallocStr(&conn->co_MemPool, msgmsgid);
	    }
	    base[l-2] = save;
#endif
#ifdef	POST_BOFHCLEANUP
	/* JG19990304 other cleanups */
	} else if (strncasecmp(base, "Date-Received:", 14) == 0) {
		errMsg = "441 Obsolete \"Date-Received\" header rejected\r\n";
	} else if (strncasecmp(base, "Received:", 9) == 0) {
		errMsg = "441 Obsolete \"Received\" header rejected\r\n";
	} else if (strncasecmp(base, "Posted:", 7) == 0) {
		errMsg = "441 Obsolete \"Posted\" header rejected\r\n";
	} else if (strncasecmp(base, "Posting-Version:", 16) == 0) {
		errMsg = "441 Obsolete \"Posting-Version\" header rejected\r\n";
	} else if (strncasecmp(base, "Relay-Version:", 14) == 0) {
		errMsg = "441 Obsolete \"Relay-Version\" header rejected\r\n";
	} else if (strncasecmp(base, "To:", 3) == 0) {
		errMsg = "441 Obsolete \"To\" header rejected\r\n";
	} else if (strncasecmp(base, "Cc:", 3) == 0) {
		errMsg = "441 Obsolete \"Cc\" header rejected\r\n";
#endif
	} else if (strncasecmp(base, "X-Complaints-To:", 16) == 0) {
		errMsg = "441 Illegal \"X-Complaints-To\" header rejected\r\n";
	} else if (strncasecmp(base, "NNTP-Posting-Date:", 18) == 0) {
		errMsg = "441 Illegal \"NNTP-Posting-Date\" header rejected\r\n";
	} else if (strncasecmp(base, "NNTP-Posting-Host:", 18) == 0) {
		errMsg = "441 Illegal \"NNTP-Posting-Host\" header rejected\r\n";
	} else if (strncasecmp(base, "X-Trace:", 8) == 0) {
		errMsg = "441 Illegal \"X-Trace\" header rejected\r\n";
	} else if (strncasecmp(base, "Xref:", 5) == 0) {
	    keep = 0;	/* delete */
	} else if (strncasecmp(base, "Control:", 8) == 0) {
		if (strncasecmp(base, "Control: cancel", 15)) {
			if (! (conn->co_Auth.dr_Flags & DF_CONTROLPOST)) {
				errMsg = "441 Illegal \"Control\" message rejected\r\n";
			}
		} else {
#ifdef	POST_RESTRICTCANCEL
			char *ptr = zallocStrTrim(&conn->co_MemPool, base+15, l - 15);
			
			if (ptr && *ptr && (strstr(ptr,
			       conn->co_Auth.dr_VServerDef->vs_ClusterName) == 0))
			    errMsg = "441 Illegal Cancel Message-ID\r\n";
			zfreeStr(&conn->co_MemPool, &ptr);
#endif
		}
	} else if (strncasecmp(base, "Also-Control:", 13) == 0) {
		if (strncasecmp(base, "Also-Control: cancel", 20)) {
			if (! (conn->co_Auth.dr_Flags & DF_CONTROLPOST))
				errMsg = "441 Illegal \"Control\" message rejected\r\n";
		} else {
#ifdef	POST_RESTRICTCANCEL
			char *ptr = zallocStrTrim(&conn->co_MemPool, base+20, l - 20);
			
			if (ptr && *ptr && (strstr(ptr, 
			       conn->co_Auth.dr_VServerDef->vs_ClusterName) == 0))
			    errMsg = "441 Illegal Cancel Message-ID\r\n";
			zfreeStr(&conn->co_MemPool, &ptr);
#endif
		}
	} else if (strncasecmp(base, "Supersedes:", 11) == 0) {
#ifdef	POST_RESTRICTCANCEL
		char *ptr = zallocStrTrim(&conn->co_MemPool, base+11, l - 11);
		
		if (ptr && *ptr && (strstr(ptr,
			       conn->co_Auth.dr_VServerDef->vs_ClusterName) == 0))
		    errMsg = "441 Illegal Supersedes Message-ID\r\n";
		zfreeStr(&conn->co_MemPool, &ptr);
#endif
	}
	if (keep)
	    MBWrite(&tmbuf, base, l);
	base += l;
	artLen -= l;
    }

    /*
     * Check errors
     */

    if (haveNewsgroups == 0) {
	errMsg = "441 Required \"Newsgroups\" header is missing\r\n";
    } else if (conn->co_Auth.dr_ReaderDef->rd_CheckPostGroups &&
	       BreakIfBadGroup(newsGrps, conn) != 0) {
	errMsg = "441 Nonexistent newsgroup(s)\r\n";
    }

    if (haveFrom == 0)
	errMsg = "441 Required \"From\" header is missing\r\n";
    if (haveSubject == 0)
	errMsg = "441 Required \"Subject\" header is missing\r\n";
    if (artLen <= 2)
	errMsg = "441 Article has no body -- just headers\r\n";

    /*
     * If no error, add our own headers
     */
    if (errMsg == NULL) {
	time_t t = time(NULL);

	/*
	 * Add Date:
	 */
	if (haveDate == 0) {
	    struct tm *tp = gmtime(&t);
	    char buf[64];

	    strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S GMT", tp);
	    MBPrintf(&tmbuf, "Date: %s\r\n", buf);
	}
	/*
	 * Add Lines:
	 */
	{
	    int lines = 0;
	    int i;

	    for (i = 2; i < artLen; ++i) {
		if (base[i] == '\n')
		    ++lines;
	    }
	    if (base[i-1] != '\n')	/* last line not terminated? */
		++lines;
	    MBPrintf(&tmbuf, "Lines: %d\r\n", lines);
	}
	/*
	 * Add Message-ID:
	 */
	if (haveMsgId == 0) {
	    if (conn->co_IHaveMsgId == NULL)
		GenerateMessageID(conn);
	    MBPrintf(&tmbuf, "Message-ID: %s\r\n", conn->co_IHaveMsgId);
	}
	/*
	 * Add Distribution:
	 */
	if (haveDist == 0 && newsGrps) {
	    GenerateDistribution(&tmbuf, newsGrps);
	}

	if (!haveOrganization && *conn->co_Auth.dr_VServerDef->vs_Org) {
		MBPrintf(&tmbuf, "Organization: %s\r\n",
			conn->co_Auth.dr_VServerDef->vs_Org);
	}
	if (*conn->co_Auth.dr_VServerDef->vs_Comments) {
		if (! strstr(conn->co_Auth.dr_VServerDef->vs_Comments, "\\n")) {
			MBPrintf(&tmbuf, "X-Comments: %s\r\n",
				conn->co_Auth.dr_VServerDef->vs_Comments);
		} else {
			/* This is sinful even for me. JG20030118 */
			char buf[512], *ptr, *head;
			int commentnum = 1;

			snprintf(buf, sizeof(buf), "%s", conn->co_Auth.dr_VServerDef->vs_Comments);
			head = buf;
			while ((ptr = strstr(head, "\\n"))) {
				*ptr = '\0';
				MBPrintf(&tmbuf, "X-Comments-%d: %s\r\n",
					commentnum++, head);
				head = ptr + 2;
			}
			MBPrintf(&tmbuf, "X-Comments-%d: %s\r\n",
				commentnum++, head);
		}
	}

#if defined  POST_NNTP_POSTING_DATE_GMT || defined POST_NNTP_POSTING_DATE_LOCALTIME
	/*
	 * Add NNTP-Posting-Date:
	 */
	{
	    char buf[64];
	    struct tm *tp;

#ifdef POST_NNTP_POSTING_DATE_GMT
	    tp = gmtime(&t);
	    strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S GMT", tp);
#else
	    tp = localtime(&t);
	    strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S %Z", tp);
#endif
	    MBPrintf(&tmbuf, "NNTP-Posting-Date: %s\r\n", buf);
	}
#endif

	/*
	 * Add NNTP-Posting-Host:
	 */
#ifdef  POST_HIDENNTPPOSTHOST
	if (*conn->co_Auth.dr_VServerDef->vs_CryptPw)
	    MBPrintf(&tmbuf, "NNTP-Posting-Host: %s\r\n",
		NNPHMangle(conn->co_Auth.dr_VServerDef->vs_ClusterName,
			conn->co_Auth.dr_Host,
			conn->co_Auth.dr_VServerDef->vs_CryptPw));
	else
#endif
	    MBPrintf(&tmbuf, "NNTP-Posting-Host: %s\r\n", conn->co_Auth.dr_Host);

	/*
	 * Add X-Trace:
	 */
#ifdef  POST_CRYPTXTRACE
	if (*conn->co_Auth.dr_VServerDef->vs_CryptPw) {
	    snprintf(xtrace, sizeof(xtrace), "%lu %s %d %s%s%s%s%s",
		(unsigned long)t,
		conn->co_Auth.dr_VServerDef->vs_HostName,
		(int)getpid(),              /* should probably be slot id */
		*conn->co_Auth.dr_AuthUser ? conn->co_Auth.dr_AuthUser : "",
		*conn->co_Auth.dr_AuthUser ? "/" : "",
		*conn->co_Auth.dr_IdentUser ? conn->co_Auth.dr_IdentUser : "",
		*conn->co_Auth.dr_IdentUser ? "@" : "",
		NetAddrToSt(0, (struct sockaddr *)&conn->co_Auth.dr_Addr, 1, 1, 1)
	    );
	    MBPrintf(&tmbuf, "X-Trace: DXC=%s\r\n",
		XTRAMangle(xtrace, conn->co_Auth.dr_VServerDef->vs_CryptPw));
	} else
#endif
	    MBPrintf(&tmbuf, "X-Trace: %lu %s %d %s%s%s%s%s\r\n",
		(unsigned long)t,
		conn->co_Auth.dr_VServerDef->vs_HostName,
		(int)getpid(),		/* should probably be slot id */
		*conn->co_Auth.dr_AuthUser ? conn->co_Auth.dr_AuthUser : "",
		*conn->co_Auth.dr_AuthUser ? "/" : "",
		*conn->co_Auth.dr_IdentUser ? conn->co_Auth.dr_IdentUser : "",
		*conn->co_Auth.dr_IdentUser ? "@" : "",
		NetAddrToSt(0, (struct sockaddr *)&conn->co_Auth.dr_Addr, 1, 1, 1)
	    );
	if (*conn->co_Auth.dr_VServerDef->vs_AbuseTo)
		MBPrintf(&tmbuf, "X-Complaints-To: %s\r\n",
			conn->co_Auth.dr_VServerDef->vs_AbuseTo);
    }

    /*
     * blank line and article body
     */

    if (errMsg == NULL) {
	MBWrite(&tmbuf, base, artLen);
    }
    if (*conn->co_Auth.dr_VServerDef->vs_PostTrailer) {
	/* This is sinful even for me. JG20030206 */
	char buf[512], *ptr, *head;

	snprintf(buf, sizeof(buf), "%s", conn->co_Auth.dr_VServerDef->vs_PostTrailer);
	head = buf;
	while ((ptr = strstr(head, "\\n"))) {
		*ptr = '\0';
		MBPrintf(&tmbuf, "%s\r\n", head);
		head = ptr + 2;
	}
	MBPrintf(&tmbuf, "%s\r\n", head);
    }
    MBFree(&conn->co_ArtBuf);

    /*
     * If no error, pump out to ready server or queue for transmission, 
     * else write the error out and throw away.
     *
     * When POSTing to a moderated group, the article is mailed to the
     * moderator and NOT otherwise posted, even if crossposted to non-moderated
     * groups.
     */

    if (errMsg == NULL) {
	/*
	 * Copy into co_ArtBuf for transmission/mailing
	 */
	MBCopy(&tmbuf, &conn->co_ArtBuf);
	MBFree(&tmbuf);

	logit(
	    LOG_NOTICE,
	    "%s%s%s%s%s (%s) post ok %s %d", 
	    *conn->co_Auth.dr_AuthUser ? conn->co_Auth.dr_AuthUser : "",
	    *conn->co_Auth.dr_AuthUser ? "/" : "",
	    (conn->co_Auth.dr_IdentUser[0] ? conn->co_Auth.dr_IdentUser : ""),
	    (conn->co_Auth.dr_IdentUser[0] ? "@" : ""),
	    conn->co_Auth.dr_Host,
	    NetAddrToSt(0, (struct sockaddr *)&conn->co_Auth.dr_Addr, 1, 1, 1),
	    conn->co_IHaveMsgId,
	    conn->co_ArtBuf.mh_Bytes
	);

	conn->co_ClientPostCount++;
	conn->co_Auth.dr_PostCount++;
	conn->co_Auth.dr_PostBytes += conn->co_ArtBuf.mh_Bytes;

	if (newsGrps && 
	    haveApproved == 0 && 
	    MailIfModerated(newsGrps, conn) == 0
	) {
	    if (conn && conn->co_IHaveMsgId)
		zfreeStr(&conn->co_MemPool, &conn->co_IHaveMsgId);
	    MBFree(&conn->co_ArtBuf);
	    MBLogPrintf(conn, &conn->co_TMBuf, "240 Article mailed to moderator\r\n");
	    NNCommand(conn);
	} else {
	    NNServerRequest(conn, NULL, conn->co_IHaveMsgId, SREQ_POST, 0, -1, 0);
	}
    } else {
	MBLogPrintf(conn, &conn->co_TMBuf, "%s", errMsg);
	MBFree(&tmbuf);
	conn->co_Auth.dr_PostFailCount++;
	NNCommand(conn);
    }
    if (newsGrps)
	zfreeStr(&conn->co_MemPool, &newsGrps);
}

/*
 * NNPostCommand1() - called when article has been queued to a server 
 *		      connection, in the server's context, to post the
 *		      article.
 *
 * SEQUENCE:
 */

void
NNPostCommand1(Connection *conn)
{
    ServReq *sreq = conn->co_SReq;

    MBLogPrintf(conn, &conn->co_TMBuf, "ihave %s\r\n", sreq->sr_MsgId);
    NNPostResponse1(conn);
}

void
NNPostResponse1(Connection *conn)
{
    /*ServReq *sreq = conn->co_SReq;*/
    char *buf;
    int len;

    conn->co_Func = NNPostResponse1;
    conn->co_State = "pores1";

    if ((len = MBReadLine(&conn->co_RMBuf, &buf)) > 0) {
	LogCmd(conn, '<', buf);
	if (strtol(buf, NULL, 10) == 335) {
	    NNPostArticle1(conn);
	} else {
	    char errMsg[256];

	    MBFree(&conn->co_ArtBuf);

	    if (len >= 1 && buf[len-1] == 0)
		--len;
	    if (len >= 1 && buf[len-1] == '\r')
		--len;
	    if (len > sizeof(errMsg) - 32)
		len = sizeof(errMsg) - 32;
	    strcpy(errMsg, "441 ");
	    bcopy(buf, errMsg + 4, len);
	    errMsg[4+len] = '\r';
	    errMsg[5+len] = '\n';
	    errMsg[6+len] = 0;
	    NNFinishSReq(conn, errMsg, 0);
	}
    } else if (len < 0) {
	NNServerTerminate(conn);
    } /* else we haven't gotten the reponse yet */
}

void
NNPostArticle1(Connection *conn)
{
    ServReq *sreq = conn->co_SReq;

    conn->co_Func = NNPostArticle1;
    conn->co_State = "poart1";

    /* Verify that the client has not disconnected yet. If such is the
     * case, don't bother returning an error message, there's no client
     */
    if (sreq == NULL || sreq->sr_CConn == NULL) {
	logit(LOG_DEBUG, "Client vanished during post (%s)", sreq->sr_MsgId);
	MBPrintf(&conn->co_TMBuf, ".\r\n"); /* Just junk the post. */
	NNPostResponse2(conn);
	return;
    }

    while (conn->co_TMBuf.mh_Bytes < 800) {
	char *buf;
	int len;

	if ((len = MBReadLine(&sreq->sr_CConn->co_ArtBuf, &buf)) > 0) {
	    buf[len-1] = '\n';
	    MBWrite(&conn->co_TMBuf, buf, len);
	} else if (len <= 0) {
	    MBPrintf(&conn->co_TMBuf, ".\r\n");
	    NNPostResponse2(conn);
	    return;
	}
    }
}

void
NNPostResponse2(Connection *conn)
{
    char *buf;
    int len;

    conn->co_Func = NNPostResponse2;
    conn->co_State = "pores2";

    if ((len = MBReadLine(&conn->co_RMBuf, &buf)) > 0) {
	LogCmd(conn, '<', buf);
	if (conn->co_SReq == NULL || conn->co_SReq->sr_CConn == NULL) {
	    /* Client has gone away. we don't care any more */
	    NNFinishSReq(conn, NULL, 0);
	} else if (strtol(buf, NULL, 10) == 235) {
	    char msgbuf[1024];
	    snprintf(msgbuf, sizeof(msgbuf), "240 %s Article posted\r\n",
			conn->co_SReq->sr_CConn->co_IHaveMsgId != NULL ?
				conn->co_SReq->sr_CConn->co_IHaveMsgId : "");
	    NNFinishSReq(conn, msgbuf, 0);
	} else {
	    char errMsg[256];

	    MBFree(&conn->co_ArtBuf);

	    if (len >= 1 && buf[len-1] == 0)
		--len;
	    if (len >= 1 && buf[len-1] == '\r')
		--len;
	    if (len > sizeof(errMsg) - 32)
		len = sizeof(errMsg) - 32;
	    strcpy(errMsg, "441 ");
	    bcopy(buf, errMsg + 4, len);
	    errMsg[4+len] = '\r';
	    errMsg[5+len] = '\n';
	    errMsg[6+len] = 0;
	    NNFinishSReq(conn, errMsg, 0);
	}
	if (conn->co_SReq != NULL && conn->co_SReq->sr_CConn != NULL &&
				conn->co_SReq->sr_CConn->co_IHaveMsgId != NULL)
	    zfreeStr(&conn->co_SReq->sr_CConn->co_MemPool,
				&conn->co_SReq->sr_CConn->co_IHaveMsgId);
    } else if (len < 0) {
	NNServerTerminate(conn);
    } /* else we haven't gotten the reponse yet */
}

/*
 * BreakIfBadGroup() -	if posting to a nonexistent or S=x group, stop
 *
 */

int
BreakIfBadGroup(char *groups, Connection *conn)
{
    int r = 0;
    int rval = 0;
    struct GroupList *accessgroups = conn->co_Auth.dr_PostGroupDef->gr_Groups;

    int i = 0;

    while (groups[i]) {
	int j;
	char c;

	for (j = i; groups[j] && groups[j] != ','; ++j)
	    ;

	if (j - i > MAXGNAME) {
	    r = -1;
	    break;
	}

	c = groups[j];
	groups[j] = 0;

	if (i != j) {
		if (accessgroups != NULL && !GroupFindWild(groups + i, accessgroups)) {
		    r = -1;
		    break;
		}
		if ((rval = CheckValidRestrictedGroup(groups + i))) {
			r = rval;
		}
	}
	i = j;
	if ((groups[j] = c) == ',')
	    ++i;
    }
    return(r);
}


/*
 * MailIfModerated() -	if posting to moderated groups and the article
 *			was not approved, mail the message to the moderators 
 *			instead
 *
 *	Return 0 if article was mailed, -1 otherwise
 *
 */

int
MailIfModerated(char *groups, Connection *conn)
{
    FILE *fi;
    int r = -1;
    int saveCount = 0;
    char buf[256];
    int save[256];

    if ((fi = fopen(PatLibExpand(ModeratorsPat), "r")) != NULL) {
	while (fgets(buf, sizeof(buf), fi) != NULL) {
	    char *modGroupWild = strtok(buf, ": \t\r\n");
	    char *modMail = (modGroupWild) ? strtok(NULL, ": \t\r\n") : NULL;
	    int i = 0;

	    if (modGroupWild == NULL || modMail == NULL)
		continue;

	    while (groups[i]) {
		int j;
		char c;

		for (j = i; groups[j] && groups[j] != ','; ++j)
		    ;
		c = groups[j];
		groups[j] = 0;

		if (i != j && 
		    groupAlready(i, save, saveCount) < 0 &&
		    saveCount != arysize(save)
		) {
		    if (CheckGroupModeration(groups + i) < 0)
			save[saveCount++] = i;
		}

		if (i != j && 
		    groupAlready(i, save, saveCount) < 0 &&
		    saveCount != arysize(save) &&
		    WildCmp(modGroupWild, groups + i
		) == 0) {
		    r = 0;
		    MailToModerator(groups + i, conn, modMail);
		    save[saveCount++] = i;
		}
		i = j;
		if ((groups[j] = c) == ',')
		    ++i;
	    }
	}
	fclose(fi);
    }
    return(r);
}

int
groupAlready(int v, int *save, int saveCount)
{
    int i;
    int r = -1;

    for (i = 0; i < saveCount; ++i) {
	if (save[i] == v) {
	    r = 0;
	    break;
	}
    }
    return(r);
}

/*
 * MailToModerator() -  send mail to the moderator.  
 *
 *	Note: we have to unescape the posted article and we cannot pass
 *	certain headers.  We cannot pass Path: because the moderator may
 *	forward it, causing the posting news machine to miss the approved
 *	posting made by the moderator (because it will have locked itself
 *	out due to the Path: ).  We also do not pass To:, Cc:, or Bcc:
 *	headers for two reasons:  First, because the news client is 
 *	supposed to handle those fields and we don't parse them for
 *	non-moderated postings, and second, for security reasons.
 */

void
MailToModerator(const char *group, Connection *conn, const char *modMail)
{
    char mailDest[256];
    pid_t pid;
    int fds[3];
    char *argv[5] = { SENDMAIL_PATH, SENDMAIL_ARG0, "-t", "-i", NULL };

    {
	char *mgroup = zallocStr(&conn->co_MemPool, group);
	int i;

	for (i = 0; mgroup[i]; ++i) {
	    if (mgroup[i] == '.')			/* dots to dashes */
		mgroup[i] = '-';
	    if (mgroup[i] == '@' || mgroup[i] == ',')	/* shouldn't happen */
		mgroup[i] = '-';
	}
	snprintf(mailDest, sizeof(mailDest), modMail, mgroup);
	zfreeStr(&conn->co_MemPool, &mgroup);
    }

    if ((pid = RunProgramPipe(fds, RPF_STDOUT, argv, NULL)) > 0) {
	FILE *fo;

	if ((fo = fdopen(fds[1], "w")) != NULL) {
	    int artLen = 0;
	    int lastLineOk = 1;
	    int inHeaders = 1;
	    int b = 0;
	    char *base = MBNormalize(&conn->co_ArtBuf, &artLen);

	    fprintf(fo, "To: %s\n", mailDest);

	    while (b < artLen) {
		int i;
		int l;
		int lineOk = 1;

		for (i = b; i < artLen && base[i] != '\n'; ++i)
		    ;
		if (i < artLen && base[i] == '\n')
		    ++i;

		/*
		 * Length of line including CR+LF
		 */
		l = i - b;

		/*
		 * Remove CR+LF or LF (LF will be added back in the email)
		 */
		if (l > 0 && base[i-1] == '\n') {
		    --l;
		    if (l > 0 && base[i-2] == '\r')
			--l;
		}

		/*
		 * Get rid of leading dot if dot-escaping
		 */
		if (l > 0 && base[b] == '.') {
		    --l;
		    ++b;
		}

		/*
		 * Process headers, output line.   The lastLineOk stuff is
		 * to handle header line continuation.
		 */

		if (inHeaders) {
		    if (l == 0) {
			inHeaders = 0;
		    } else if (base[b] == ' ' || base[b] == '\t') {
			lineOk = lastLineOk;
		    } else if (l >= 3 && !strncasecmp(base + b, "To:", 3)) {
			lineOk = 0;
		    } else if (l >= 3 && !strncasecmp(base + b, "Cc:", 3)) {
			lineOk = 0;
		    } else if (l >= 4 && !strncasecmp(base + b, "Bcc:", 4)) {
			lineOk = 0;
		    } else if (l >= 5 && !strncasecmp(base + b, "Path:", 5)) {
			lineOk = 0;
		    } 
		}
		if (lineOk) {
		    fwrite(base + b, l, 1, fo);
		    fputc('\n', fo);
		}
		lastLineOk = lineOk;
		b = i;
	    }
	    fclose(fo);
	} else {
	    close(fds[1]);
	}
	waitpid(pid, NULL, 0);
    }
}

int
CheckGroupModeration(const char *group)
{
    const char *rec;
    int recLen;
    int r = -1;

    rec = KPDBReadRecord(KDBActive, group, KP_LOCK, &recLen);
    if (rec != NULL) {
        int slen;
        const char *s = KPDBGetField(rec, recLen, "S", &slen, NULL);
        if (s) {
            int i;

	    for (i = 0; i < slen; ++i) {
		if (s[i] == 'm')
                    r = 0;
            }
        }
        KPDBUnlock(KDBActive, rec);
    }
    return(r);
}

int
CheckValidRestrictedGroup(const char *group)
{
    const char *rec;
    int recLen;
    int r = -1;

    /* Allow posting into a (currently) nonexistent group.  We will create */
    if (DOpts.ReaderAutoAddToActive)
	r = 0;

    rec = KPDBReadRecord(KDBActive, group, KP_LOCK, &recLen);

    if (rec != NULL) {
        int slen;
        const char *s = KPDBGetField(rec, recLen, "S", &slen, NULL);
	r = 0;
        if (s) {
            int i;

	    for (i = 0; i < slen; ++i) {
		if (s[i] == 'x')
                    r = 1;
            }
        }
        KPDBUnlock(KDBActive, rec);
    }
    return(r);
}

/*
 * GenerateDistribution(): generate a distribution header.  At the moment
 *			   we only generate one distribution tag, even if
 *			   crossposting, using the earliest highest-weight
 *			   tag found in distrib.pats.
 *
 *			   If we cannot find any matches, no Distribution:
 *			   header is generated.
 */

void
GenerateDistribution(MBufHead *mbuf, char *newsGrps)
{
    FILE *fi; 
    int bestWeight = -1;
    char *bestDist = NULL;

    if ((fi = fopen(PatLibExpand(DistribDotPatsPat), "r")) != NULL) {
        char buf[256];
    
        while (fgets(buf, sizeof(buf), fi) != NULL) {
	    char *weight = strtok(buf, ":\r\n");
	    char *pattern = weight ? strtok(NULL, ":\r\n") : NULL;
	    char *value = pattern ? strtok(NULL, ":\r\n") : NULL;
	    int i = 0;

	    if (value == NULL || 
	        weight == NULL || 
		weight[0] == '#' ||
		weight[0] == 0
	    ) {
		continue;
	    }
	    while (newsGrps[i]) {
		int j;
		char c;

		for (j = i; newsGrps[j] && newsGrps[j] != ','; ++j)
		    ;
		c = newsGrps[j];
		newsGrps[j] = 0;
		if (WildCmp(pattern, newsGrps + i) == 0) {
		    if (strtol(weight, NULL, 0) > bestWeight) {
			zfreeStr(&SysMemPool, &bestDist);
			bestWeight = strtol(weight, NULL, 0);
			bestDist = zallocStr(&SysMemPool, value);
		    }
		}
		newsGrps[j] = c;
		if (newsGrps[j] != ',')
		    break;
		i = j + 1;
	    }
        }
        fclose(fi);
    }
    if (bestDist) {
	MBPrintf(mbuf, "Distribution: %s\r\n", bestDist);
	zfreeStr(&SysMemPool, &bestDist);
    }
}


syntax highlighted by Code2HTML, v. 0.9.1