/*
 *
 * RFC [2]822 address parsing routines borrowed from the Mutt code.
 *
 * Revision in Mutt CVS at time of last update:  3.2 2002/12/09 18:26:26
 *
 * (Note changes 3.3-3.6 are irrelevant to us here.)
 *
 */

/* 
 * Copyright (C) 1996-2000 Michael R. Elkins <me@cs.hmc.edu>
 *
 *     This program is free software; you can redistribute it and/or modify it
 *     under the terms of the GNU General Public Copyright License as published
 *     by the Free Software Foundation; either version 2 of the License, or (at
 *     your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful, but
 *     WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *     General Public Copyright License for more details.
 *
 *     You should have received a copy of the GNU General Public License along
 *     with this program; if not, write to the Free Software Foundation, Inc.,
 *     59 Temple Place - Suite 330, Boston, MA 02111, USA.
 */

/*
 * All Modifications and enhancements from the original:
 *
 * Copyright (c) 2001, 2002	Greg A. Woods <woods@planix.com>
 *
 * See "COPYING" for details.
 */

/*
 * TODO:  (most this is not likely ever to happen -- see librfc2822)
 *
 * - Documentation!
 *
 * - proper full regression testing, not just some example runs needing manual
 *   inspection of the results....
 *
 * - fix the bugs revealed in the tests:
 *
 * 	- parser is not properly recursive inside group lists
 *
 * 	- detect mis-matched closing parenthesis
 *
 * 	- detect missing colon
 *
 * 	- detect missing semicolon
 *
 * 	- detect unqouted special characters in personal name
 *
 * 	- don't lose quotes in quoted local-part
 *
 * 	- don't trash previous "original" with trailing commas
 *
 * 	- think about complaining about missing commas.
 *
 * - add full local-part parsing....
 *
 *   i.e. rfc822_end_of_mbox() still needs to properly parse out just the local part
 *   (mailbox part) of addresses for comparison with aliases instead of
 *   assuming everything before the first "@" is the mailbox.
 */

#include <sys/cdefs.h>

#ifndef lint
__RCSID("@(#)vacation:RELEASE-3_2_0_120:rfc822.c,v 1.2 2004/08/17 20:45:30 woods Exp");
#endif /* not lint */

#include <sys/param.h>		/* mostly just for MIN()... */
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>

#include "vacation.h"
#include "rfc822.h"

#ifdef TESTING
void *safe_malloc __P((size_t));
void *safe_calloc __P((size_t, size_t));
char *safe_strdup __P((const char *));
void safe_free __P((void **));
#endif /* TESTING */

static char *substrdup __P((const char *, const char *));
static void rfc822_dequote_comment __P((char *));
static const char *parse_comment __P((const char *, char *, size_t *, size_t));
static const char *parse_quote __P((const char *, char *, size_t *, size_t));
static const char *next_token __P((const char *, char *, size_t *, size_t));
static const char *parse_mailboxdomain __P((const char *, const char *, char *, size_t *, size_t, char *, size_t *, size_t));
static const char *parse_addr __P((const char *, char *, size_t *, size_t, char *, size_t *, size_t, mbxlst_t *));
static const char *parse_route_addr __P((const char *, char *, size_t *, size_t, mbxlst_t *));
static const char *parse_addr_spec __P((const char *, char *, size_t *, size_t, mbxlst_t *));
static void add_addrspec __P((mbxlst_t **, mbxlst_t **, const char *, char *, size_t *, size_t));

#define SKIPSPACE(x)							\
		{							\
			while (isspace(*x))				\
				x++;					\
		}

/*
 * a pretend-"safe" strncpy() macro which always leaves 'dest' as a properly
 * NUL-terminated string.
 *
 * Note that since not all of 'src' might be copied the only way to find out if
 * the buffer is "full" afterwards is to count its characters with strlen().
 */
#define STRFCPY(dest, src, len)						\
		{							\
			if (len) {					\
				strncpy(dest, src, len);		\
				dest[len - 1] = '\0';			\
			}						\
		}

/*
 * Terminate a string by placing a NUL byte at the specified end index, or at
 * the max index if that's less than the end.
 */
#define terminate_string(str, end, max)	str[MIN(end, max)] = '\0'

/*
 * Terminate a buffer at the specified end index (or at its maximum index if
 * the given end is past the size of the buffer)
 */
#define terminate_buffer(buf, end)	terminate_string(buf, end, sizeof(buf) - 1)


unsigned int    RFC822Error = 0;		/* global parser error code */

/*
 * these must defined in the same order as the numerated errors given in
 * rfc822.h
 */
const char     *RFC822Errors[] = {
	"no error",
	"out of memory",
	"mismatched parenthesis",
	"mismatched quotes",
	"bad route address in <>",
	"bad address in <>",
	"bad address specification"
};
#if (7 < NRFC822Errors) /* ((sizeof(RFC822Errors) / sizeof(char *)) < NRFC822Errors) */
# include "ERROR: not enough strings in RFC822Errors!"
#endif

static char *
substrdup(begin, end)
	const char     *begin;
	const char     *end;
{
	size_t          len;
	char           *p;

	if (end)
		len = end - begin;
	else
		len = strlen(begin);

	p = safe_malloc(len + 1);
	memcpy(p, begin, len);
	p[len] = 0;

	return p;
}

static void
rfc822_dequote_comment(s)
	char           *s;
{
	char           *w = s;

	for (; *s; s++) {
		if (*s == '\\') {
			if (!*++s)
				break;		/* error? */

			*w++ = *s;
		} else if (*s != '\"') {
			if (w != s)
				*w = *s;

			w++;
		}
	}
	*w = 0;
}

void
rfc822_free_addr_list(addrp)
	mbxlst_t      **addrp;
{
	mbxlst_t       *taddr;

	while (*addrp) {
		taddr = *addrp;
		*addrp = (*addrp)->next;
		safe_free((void **) &taddr->original);
		safe_free((void **) &taddr->personal);
		safe_free((void **) &taddr->mailbox);
		safe_free((void **) &taddr);
	}

	return;	
}

static const char *
parse_comment(s, comment, commentlen, commentmax)
	const char     *s;
	char           *comment;
	size_t         *commentlen;
	size_t          commentmax;
{
	int             level = 1;

	while (*s && level) {
		if (*s == '(')
			level++;
		else if (*s == ')') {
			if (--level == 0) {
				s++;
				break;
			}
		} else if (*s == '\\') {
			if (!*++s)
				break;
		}
		if (*commentlen < commentmax)
			comment[(*commentlen)++] = *s;

		s++;
	}
	if (level) {
		RFC822Error = RFC822_ERR_MISMATCH_PAREN;

		return NULL;
	}

	return s;
}

static const char *
parse_quote(s, token, tokenlen, tokenmax)
	const char     *s;
	char           *token;
	size_t         *tokenlen;
	size_t          tokenmax;
{
	if (*tokenlen < tokenmax)
		token[(*tokenlen)++] = '"';

	while (*s) {
		if (*tokenlen < tokenmax)
			token[*tokenlen] = *s;

		if (*s == '"') {
			(*tokenlen)++;

			return (s + 1);
		}
		if (*s == '\\') {
			if (!*++s)
				break;

			if (*tokenlen < tokenmax)
				token[*tokenlen] = *s;
		}
		(*tokenlen)++;
		s++;
	}
	RFC822Error = RFC822_ERR_MISMATCH_QUOTE;

	return NULL;
}

static const char *
next_token(s, token, tokenlen, tokenmax)
	const char     *s;
	char           *token;
	size_t         *tokenlen;
	size_t          tokenmax;
{
	if (*s == '(')
		return (parse_comment(s + 1, token, tokenlen, tokenmax));

	if (*s == '"')
		return (parse_quote(s + 1, token, tokenlen, tokenmax));

	if (is_rfc822_special(*s)) {
		if (*tokenlen < tokenmax)
			token[(*tokenlen)++] = *s;

		return (s + 1);
	}
	while (*s) {
		if (isspace(*s) || is_rfc822_special(*s))
			break;

		if (*tokenlen < tokenmax)
			token[(*tokenlen)++] = *s;

		s++;
	}

	return s;
}

static const char *
parse_mailboxdomain(s, nonspecial, mailbox, mailboxlen, mailboxmax, comment, commentlen, commentmax)
	const char     *s;
	const char     *nonspecial;
	char           *mailbox;
	size_t         *mailboxlen;
	size_t          mailboxmax;
	char           *comment;
	size_t         *commentlen;
	size_t          commentmax;
{
	const char     *ps;

	while (*s) {
		SKIPSPACE(s);
		if (!strchr(nonspecial, *s) && is_rfc822_special(*s))
			return s;

		if (*s == '(') {
			if (*commentlen && *commentlen < commentmax)
				comment[(*commentlen)++] = ' ';

			ps = next_token(s, comment, commentlen, commentmax);
		} else
			ps = next_token(s, mailbox, mailboxlen, mailboxmax);

		if (!ps)
			return NULL;

		s = ps;
	}

	return s;
}

static const char *
parse_addr(s, token, tokenlen, tokenmax, comment, commentlen, commentmax, addr)
	const char *s;
	char *token;
	size_t *tokenlen;
	size_t tokenmax;
	char *comment;
	size_t *commentlen;
	size_t commentmax;
	mbxlst_t *addr;
{
	s = parse_mailboxdomain(s, ".\"(\\",
				token, tokenlen, tokenmax,
				comment, commentlen, commentmax);
	if (!s)
		return NULL;

	if (*s == '@') {
		if (*tokenlen < tokenmax)
			token[(*tokenlen)++] = '@';

		s = parse_mailboxdomain(s + 1, ".([]\\",
					token, tokenlen, tokenmax,
					comment, commentlen, commentmax);
		if (!s)
			return NULL;
	}
	terminate_string(token, *tokenlen, tokenmax);
	addr->mailbox = safe_strdup(token);

	/*
	 * Convert this commment into a personal name iff a personal name has
	 * not yet been seen.  This may or may not always be a smart thing to
	 * do....
	 */
	if (*commentlen && !addr->personal) {
		terminate_string(comment, *commentlen, commentmax);
		addr->personal = safe_strdup(comment);
	}

	return s;
}

static const char *
parse_route_addr(s, comment, commentlen, commentmax, addr)
	const char     *s;
	char           *comment;
	size_t         *commentlen;
	size_t          commentmax;
	mbxlst_t       *addr;
{
	char            token[BUFSIZ];
	size_t          tokenlen = 0;

	SKIPSPACE(s);

	/* find the end of the route */
	if (*s == '@') {
		while (s && *s == '@') {
			if (tokenlen < sizeof(token) - 1)
				token[tokenlen++] = '@';

			s = parse_mailboxdomain(s + 1, ",.\\[](", token,
						&tokenlen, sizeof(token) - 1,
						comment, commentlen, commentmax);
		}
		if (!s || *s != ':') {
			RFC822Error = RFC822_ERR_BAD_ROUTE;
			return NULL;		/* invalid route */
		}
		if (tokenlen < sizeof(token) - 1)
			token[tokenlen++] = ':';

		s++;
	}
	if (!(s = parse_addr(s, token, &tokenlen, sizeof(token) - 1, comment, commentlen, commentmax, addr)))
		return NULL;

	if (*s != '>') {
		RFC822Error = RFC822_ERR_BAD_ROUTE_ADDR;
		return NULL;
	}
	if (!addr->mailbox)
		addr->mailbox = safe_strdup("@");

	s++;

	return s;
}

static const char *
parse_addr_spec(s, comment, commentlen, commentmax, addr)
	const char     *s;
	char           *comment;
	size_t         *commentlen;
	size_t          commentmax;
	mbxlst_t       *addr;
{
	char            token[BUFSIZ];
	size_t          tokenlen = 0;

	s = parse_addr(s, token, &tokenlen, sizeof(token) - 1, comment, commentlen, commentmax, addr);
	if (s && *s && *s != ',' && *s != ';') {
		RFC822Error = RFC822_ERR_BAD_ADDR_SPEC;
		return NULL;
	}

	return s;
}

static void
add_addrspec(top, last, phrase, comment, commentlen, commentmax)
	mbxlst_t      **top;
	mbxlst_t      **last;
	const char     *phrase;
	char           *comment;
	size_t         *commentlen;
	size_t          commentmax;
{
	mbxlst_t       *cur = rfc822_new_addr();

	if (parse_addr_spec(phrase, comment, commentlen, commentmax, cur) == NULL) {
		rfc822_free_addr_list(&cur);
		return;
	}
	if (*last)
		(*last)->next = cur;
	else
		*top = cur;

	*last = cur;

	return;
}

mbxlst_t *
rfc822_parse_addrlist(top, s)
	mbxlst_t       *top;
	const char     *s;
{
	int             ws_pending;
	const char     *begin;
	const char     *ps;
	char            comment[BUFSIZ];
	char            phrase[BUFSIZ];
	size_t          phraselen = 0;
	size_t          commentlen = 0;
	mbxlst_t       *cur;
	mbxlst_t       *last = NULL;

	RFC822Error = 0;

	if (!s || !*s)
		return NULL;

	last = top;
	while (last && last->next)
		last = last->next;

	ws_pending = isspace (*s);

	SKIPSPACE(s);
	begin = s;
	while (*s) {
		if (*s == ',') {
			if (phraselen) {
				terminate_buffer(phrase, phraselen);
				add_addrspec(&top, &last, phrase, comment, &commentlen, sizeof(comment) - 1);
			} else if (commentlen && last && !last->personal) {
				terminate_buffer(comment, commentlen);
				last->personal = safe_strdup(comment);
			}
			if (last && !last->original)
				last->original = substrdup(begin, s);

			commentlen = 0;
			phraselen = 0;
			s++;
			begin = s;
			SKIPSPACE(begin);
		} else if (*s == '(') {
			if (commentlen && commentlen < sizeof(comment) - 1)
				comment[commentlen++] = ' ';

			if ((ps = next_token(s, comment, &commentlen, sizeof(comment) - 1)) == NULL) {
				rfc822_free_addr_list(&top);
				return NULL;
			}
			s = ps;
		} else if (*s == ':') {
			cur = rfc822_new_addr();
			terminate_buffer(phrase, phraselen);
			cur->mailbox = safe_strdup(phrase);
			cur->start_group = 1;

			if (last)
				last->next = cur;
			else
				top = cur;

			last = cur;
			last->original = substrdup(begin, s);
			phraselen = 0;
			commentlen = 0;
			s++;
			begin = s;
			SKIPSPACE(begin);
		} else if (*s == ';') {
			if (phraselen) {
				terminate_buffer(phrase, phraselen);
				add_addrspec(&top, &last, phrase, comment, &commentlen, sizeof(comment) - 1);
			} else if (commentlen && last && !last->personal) {
				terminate_buffer(comment, commentlen);
				last->personal = safe_strdup(comment);
			}
			if (last && !last->original)
				last->original = substrdup(begin, s);

			/* add group terminator */
			cur = rfc822_new_addr();
			if (last) {
				last->next = cur;
				last = cur;
			}
			phraselen = 0;
			commentlen = 0;
			s++;
			begin = s;
			SKIPSPACE(begin);
		} else if (*s == '<') {
			terminate_buffer(phrase, phraselen);
			cur = rfc822_new_addr();
			if (phraselen) {
				if (cur->personal)
					safe_free((void **) &cur->personal);

				/* if we get something like "Michael R. Elkins"
				 * remove the quotes */
				rfc822_dequote_comment(phrase);
				cur->personal = safe_strdup(phrase);
			}
			if ((ps = parse_route_addr(s + 1, comment, &commentlen, sizeof(comment) - 1, cur)) == NULL) {
				rfc822_free_addr_list(&top);
				rfc822_free_addr_list(&cur);
				return NULL;
			}
			if (last)
				last->next = cur;
			else
				top = cur;

			last = cur;
			phraselen = 0;
			commentlen = 0;
			s = ps;
		} else {
			if (phraselen && phraselen < sizeof(phrase) - 1 && ws_pending)
				phrase[phraselen++] = ' ';

			if ((ps = next_token(s, phrase, &phraselen, sizeof(phrase) - 1)) == NULL) {
				rfc822_free_addr_list(&top);
				return NULL;
			}
			s = ps;
		}
		ws_pending = isspace (*s);
		SKIPSPACE(s);
	}

	if (phraselen) {
		terminate_buffer(phrase, phraselen);
		terminate_buffer(comment, commentlen);
		add_addrspec(&top, &last, phrase, comment, &commentlen, sizeof(comment) - 1);
	} else if (commentlen && last && !last->personal) {
		/*
		 * Convert this commment into a personal name iff a personal
		 * name has not yet been seen.  This may or may not always be a
		 * smart thing to do....
		 */
		terminate_buffer(comment, commentlen);
		last->personal = safe_strdup(comment);
	}
	if (last)
		last->original = substrdup(begin, s);

	return top;
}

/*
 * If any mailbox name is missing the host part then rewrite it as a fully
 * qualified address using the supplied hostname.
 */
void
rfc822_qualify_addr(addr, host)
	mbxlst_t       *addr;
	const char     *host;
{

	for (; addr; addr = addr->next) {
		if (!addr->start_group && addr->mailbox && !strchr(addr->mailbox, '@')) {
			char           *p;

			p = safe_malloc((size_t) strlen(addr->mailbox) + strlen(host) + 2);
			sprintf(p, "%s@%s", addr->mailbox, host);	/* __SPRINTF_CHECKED__ */
			safe_free((void **) &addr->mailbox);
			addr->mailbox = p;
		}
	}

	return;
}

void
rfc822_cat(buf, buflen, value, specials)
	char           *buf;
	size_t          buflen;
	const char     *value;
	const char     *specials;
{
	if (strpbrk(value, specials)) {
		char            tmp[BUFSIZ * 2];
		char           *pc = tmp;
		size_t          tmplen = sizeof(tmp) - 3;

		*pc++ = '"';
		for (; *value && tmplen > 1; value++) {
			if (*value == '\\' || *value == '"') {
				*pc++ = '\\';
				tmplen--;
			}
			*pc++ = *value;
			tmplen--;
		}
		*pc++ = '"';
		*pc = 0;
		STRFCPY(buf, tmp, buflen);
	} else
		STRFCPY(buf, value, buflen);

	return;
}

void
rfc822_write_addr(buf, buflen, addr)
	char           *buf;
	size_t          buflen;
	mbxlst_t       *addr;
{
	size_t          len;
	char           *pbuf = buf;
	char           *pc;

	if (!addr) {
		STRFCPY(pbuf, "(empty)", buflen);
		return;
	}
	buflen--;				/* save room for the terminal nul */

	if (addr->personal) {
		if (strpbrk(addr->personal, RFC822Specials)) {
			if (!buflen)
				goto done;

			*pbuf++ = '"';
			buflen--;
			for (pc = addr->personal; *pc && buflen > 0; pc++) {
				if (*pc == '"' || *pc == '\\') {
					if (!buflen)
						goto done;

					*pbuf++ = '\\';
					buflen--;
				}
				if (!buflen)
					goto done;

				*pbuf++ = *pc;
				buflen--;
			}
			if (!buflen)
				goto done;

			*pbuf++ = '"';
			buflen--;
		} else {
			if (!buflen)
				goto done;

			STRFCPY(pbuf, addr->personal, buflen);
			len = strlen(pbuf);
			pbuf += len;
			buflen -= len;
		}
		if (!buflen)
			goto done;

		*pbuf++ = ' ';
		buflen--;
	}
	if (addr->personal || (addr->mailbox && *addr->mailbox == '@')) {
		if (!buflen)
			goto done;

		*pbuf++ = '<';
		buflen--;
	}
	if (addr->mailbox) {
		if (!buflen)
			goto done;

		if (strcmp(addr->mailbox, "@")) {
			STRFCPY(pbuf, addr->mailbox, buflen);
			len = strlen(pbuf);
		} else {
			*pbuf = '\0';
			len = 0;
		}
		pbuf += len;
		buflen -= len;
		if (addr->personal || (addr->mailbox && *addr->mailbox == '@')) {
			if (!buflen)
				goto done;

			*pbuf++ = '>';
			buflen--;
		}
		if (addr->start_group) {
			STRFCPY(pbuf, ": ", buflen);
			pbuf += 2;
			buflen -= 2;
		}
	} else {
		if (!buflen)
			goto done;

		*pbuf++ = ';';
		buflen--;
	}
  done:
	/*
	 * no need to check for length here since we already save space at the
	 * beginning of this routine
	 */
	*pbuf = 0;

	return;
}

/* note: it is assumed that `buf' is NUL terminated! */
void
rfc822_write_addr_list(buf, buflen, addr)
	char           *buf;
	size_t          buflen;
	mbxlst_t       *addr;
{
	char           *pbuf = buf;
	size_t          len = strlen(buf);

	buflen--;				/* save room for the terminal nul */

	if (len > 0) {
		if (len > buflen)
			return;			/* safety check for bogus arguments */

		pbuf += len;
		buflen -= len;
		if (!buflen)
			goto done;

		*pbuf++ = ',';
		buflen--;
		if (!buflen)
			goto done;

		*pbuf++ = ' ';
		buflen--;
	}
	for (; addr && buflen > 0; addr = addr->next) {
		/*
		 * use buflen+1 here because we already saved space for the
		 * trailing NUL char, and the subroutine can make use of it
		 */
		rfc822_write_addr(pbuf, buflen + 1, addr);

		/*
		 * this should be safe since we always have at least 1 char
		 * passed into the above call, which means `pbuf' should always
		 * be NUL terminated
		 */
		len = strlen(pbuf);
		pbuf += len;
		buflen -= len;

		/*
		 * if there is another address (that's not a group terminator),
		 * and this address is not a group mailbox name, then add a
		 * comma to separate the addresses
		 */
		if (addr->next && addr->next->mailbox && addr->start_group == 0) {
			STRFCPY(pbuf, ", ", buflen);
			pbuf += 2;
			buflen -= 2;
		}
	}
  done:
	*pbuf = 0;

	return;
}

mbxlst_t *
rfc822_cpy_addr(addr)
	mbxlst_t       *addr;
{
	mbxlst_t       *p = rfc822_new_addr();

	p->original = safe_strdup(addr->original);
	p->personal = safe_strdup(addr->personal);
	p->mailbox = safe_strdup(addr->mailbox);
	p->start_group = addr->start_group;

	return p;
}

mbxlst_t *
rfc822_cpy_addr_list(addr)
	mbxlst_t       *addr;
{
	mbxlst_t       *top = NULL;
	mbxlst_t       *last = NULL;

	for (; addr; addr = addr->next) {
		if (last) {
			last->next = rfc822_cpy_addr(addr);
			last = last->next;
		} else
			top = last = rfc822_cpy_addr(addr);
	}

	return top;
}

/*
 * append list 'b' to list 'a' and return the last element in the new list
 */
mbxlst_t *
rfc822_append_list(a, b)
	mbxlst_t      **a;
	mbxlst_t       *b;
{
	mbxlst_t       *tmp = *a;

	while (tmp && tmp->next)
		tmp = tmp->next;

	if (!b)
		return tmp;

	if (tmp)
		tmp->next = rfc822_cpy_addr_list(b);
	else
		tmp = *a = rfc822_cpy_addr_list(b);

	while (tmp && tmp->next)
		tmp = tmp->next;

	return tmp;
}

/*
 * Try finding a pointer to the _end_ of the mailbox name.
 *
 * It tries to find the local part, i.e. the mailbox, assuming that addresses
 * will be something as complex as this:
 *
 *	site!site!SENDER%site.domain%site.domain@site.domain
 *
 * This is a rather lame, and possibly incorrect, implementation.
 */
char *
rfc822_end_of_mbox(mbox)
	char *mbox;
{
	char *p;

	if (!(p = strchr(mbox, '%'))) {
		if (!(p = strchr(mbox, '@'))) {
			if (!(p = strrchr(mbox, '!')))
				p = mbox;
			else
				++p;

			for (; *p; ++p)
				;
		}
	}

	return p;
}

#ifdef TESTING

# include <rfc2822.h>

static void dotest __P((const char     *));
int main __P((int, char **));

void *
safe_malloc(len)
	size_t          len;
{
	void           *np;

	if (len == 0)
		return NULL;

	if (!(np = malloc(len))) {
		perror("malloc()");
		exit(1);
		/* NOTREACHED */
	}

	return np;
}

void *
safe_calloc(nelem, size)
	size_t          nelem;
	size_t          size;
{
	void           *np;

	if (!nelem || !size)
		return NULL;

	if (!(np = calloc(nelem, size))) {
		perror("calloc()");
		exit(1);
		/* NOTREACHED */
	}

	return np;
}

char *
safe_strdup(s)
	const char     *s;
{
	char           *ns;

	if (!s)
		return NULL;

	if (!(ns = strdup(s))) {
		perror("strdup()");
		exit(1);
		/* NOTREACHED */
	}

	return ns;
}

void
safe_free(p)
	void          **p;
{
	if (*p) {
		free(*p);
		*p = 0;
	}

	return;
}

static void
dotest(str)
	const char     *str;
{
	mbxlst_t       *list;
	mbxlst_t       *addr;
	char            buf[BUFSIZ];
	int             group_state = 0;

	if (!(list = rfc822_parse_addrlist((mbxlst_t *) NULL, str))) {
		assert(RFC822Error <= NRFC822Errors);
		printf("ERROR: %s\n", RFC822Errors[RFC822Error]);

		return;
	}

	putchar('\n');
	for (addr = list; addr; addr = addr->next) {
		if (!addr->mailbox) {
			group_state = 0;
			fputs("END_OF_GROUP_ADDRESS\n\n", stdout);
		}
		printf("original = '%s'\n", addr->original);
		printf("personal = '%s'\n", addr->personal);
		printf("%s = '%s'\n", addr->start_group ? "group_name" : "mailbox", addr->mailbox);
		printf("start_group = %d\n\n", addr->start_group);
	}
	group_state = 0;
		
	fputs("\nFIRST_ADDRESS: ", stdout);
	for (addr = list; addr; addr = addr->next) {

		/*
		 * if this address is not a group terminator
		 */
		if (!addr->mailbox)
			group_state = 0;
		else {
			if (addr->start_group) {
				fputs("(GROUP): ", stdout);
				group_state = 1;
			}
			rfc822_write_addr(buf, sizeof(buf), addr);
			fputs(buf, stdout);
		}
		/*
		 * if there is another address (that's not a group terminator),
		 * and this address is not a group mailbox name, then add a
		 * comma to separate the addresses
		 */
		if (addr->next && addr->next->mailbox && !addr->start_group)
			fprintf(stdout, "\nNEXT__%s: ", group_state ? "IN_GROUP" : "ADDRESS");
	}

	fputs("\n\nRECON: ", stdout);
	buf[0] = '\0';
	rfc822_write_addr_list(buf, sizeof(buf), list);
	puts(buf);

	rfc822_free_addr_list(&list);

	return;
}

int
main(argc, argv)
	int             argc;
	char           *argv[];
{
	char *argv0 = argv[0];
	char    *buffer;
	char    *colon;
	char    *content;

	argc--;				/* silly use of argc */

	printf("%s: starting tests....\n", argv0);

#if 1
	rfc2822_clear_done_headers();

	while ((buffer = rfc2822_gethfield(stdin, &colon, 0))) {
		char *header_name;

# if 0
		printf("Read %sheader: '%s'\n", colon ? "" : "non-", buffer);
# endif

		/*
		 * Normally we'd know what header we were looking for, but here
		 * in test land we'll allow anything so we need to know what it
		 * is before we check if the format is OK....
		 */
		if (colon) {
			*colon = '\0';
			header_name = strdup(buffer);
			*colon = ':';
		} else {
			header_name = strdup("none");
		}

		if (!(content = rfc2822_ishfield(buffer, colon, header_name))) {
			printf("Bad header: %s\n\n", rfc2822_strerror(rfc2822_errno));
			free(header_name);
			free(buffer);
			continue;
		}


		puts("----------------------------------------------------------------------\n");
		printf("Header name: '%s'\n", header_name);
		printf("Header content: '%s'\n", content);

		dotest(content);

# if 0
		if (rc != RFC2822_OK) {
			/* note the phrase "error for" is used by the regression test */
			printf("Parser error for '%s': %s\n", header_name, rfc2822_strerror(rc));
			if (rc != RFC2822_SYNTAX_ERROR) {
				free((void *) header_name);
				free((void *) buffer);
				/* XXX assume addresses was not set.... */
				continue;
			}
		} else if (rfc2822_errno != RFC2822_OK) {
			/* note the phrase "error for" is used by the regression test */
			printf("Syntax error for '%s': %s\n", header_name, rfc2822_strerror(rfc2822_errno));
		}
		printf("Parsed result for '%s': '%s'\n\n", header_name, addresses);
# endif

		free((void *) header_name);
		free((void *) buffer);
# if 0
		if (addresses)
			free((void *) addresses);
# endif
	}
	if (!buffer && rfc2822_errno != RFC2822_OK)
		printf("Header error: %s\n", rfc2822_strerror(rfc2822_errno));

#else

	dotest("a, b, c");
	dotest("a, b, c, ");			/* XXX "original" for 'c' gets lost!!! */
	dotest("a, b, ");			/* XXX "original" for 'b' gets lost!!! */
	dotest("a, b, , , ");			/* XXX "original" for 'b' gets lost!!! */
	dotest("a b, c");
	dotest("a b c");
	dotest("a b
	            c");
	dotest("<a>, <b>, <c>");
	dotest("<a> <b> <c>");			/* XXX missing commas not noticed */
	dotest("A <a>, B <b>, C <c>");
	dotest("A <a> B <b> C <c>");		/* XXX missing commas not noticed */

	dotest("a b@c");
	dotest("a b @ c");
	dotest("a <b@c>");

	dotest("\"Joe & J. Harvey\" <ddd @Org>, JJV @ BBN");
	dotest("\"George, Ted\" <Shared@Group.Arpanet>");
	dotest("\"Full Name\"@Domain");
	dotest("\"Full.Name.with.dots.\"@Domain");
	dotest("\"Full.Name.with <angle.brackets>\"@Domain");
	dotest("\"Full.Name.with at @ end\"@Domain");
	dotest("\"Full.Name.with weirdness <at @ the . end>\"@Domain");
	dotest("\"Full.Name.with more weirdness <at @ the . end\"@Domain");
	dotest("\"Full.Name.with extra weirdness at @ the . end>\"@Domain");
	dotest("\"<bracket.weirdness @ beg.in.ning> this.time\"@Domain");
	dotest("\"<bracket.weirdness @ beg.in.ning this.time\"@Domain");
	dotest("\"bracket.weirdness @ beg.in.ning> this.time\"@Domain");
	dotest("sub-net.mailbox@sub-domain.domain");
	dotest("sub-net.domain-literal@[10.0.3.19]");
	dotest("John Doe <jdoe@machine(comment).  example>");
	dotest("(John (Johnny) K. Doe) jdoe@one.test");
	dotest("(John \"Johnny\" K. Doe) jdoe@one.test");

	dotest("\"John \\\"Johnny\\\" K. Doe\" <jdoe@one.test>"); /* XXX personal = 'John Johnny K. Doe' */
	dotest("\"John \\\"Johnny\\\" K. Doe\"@one.test");/* XXX mailbox = '"John "Johnny" K. Doe"@one.test' */

	dotest("\"Joe & J. (Jay) Harvey\" <ddd @Org>, Jay <JJV @ BBN>");

	dotest("Pete (A wonderful \\) chap) <pete (his account) @ silly.test (his host)>");
	dotest("Pete (A wonderful \\( chap) <pete (his account) @ silly.test (his host)>");

	dotest("this example <@contains.a.source.route,@with.multiple.hosts:address@example.com>");
	
	/*
	 * GROUPS:
	 */
	dotest("The Committee: Jones@Host.Net,
                               Smith@Other.Org,
                               Doe@Somewhere-Else;");
	dotest("Gourmets:  Pompous Person <WhoZiWhatZit@Cordon-Bleu>,
                           Childs@WGBH.Boston, Galloping Gourmet@
                           ANT.Down-Under (Australian National Television),
                           Cheapie@Discount-Liquors;,
                Cruisers:  Port@Portugal, Jones@SEA;,
                           Another@Somewhere.SomeOrg");

	/*
	 * MIXED:
	 */
	dotest("michael, Michael Elkins <me@cs.hmc.edu>, testing a really complex address: this example <@contains.a.source.route,@with.multiple.hosts:address@example.com>;, lothar@of.the.hillpeople (lothar), (foobar) funny.man@not.here (this I. fiddle), Others:<a>,<b>,<c>;");

	dotest("Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>");

	dotest("<boss@nil.test>, \"Giant; \\\"Big\\\" Box\" <sysservices@example.net>"); /* also loses quotes */

	dotest("Important folk: Tom Softwood <Balsa@Tree.Root>,
	                       \"Sam Irving\"@Other-Host;,
                Standard Distribution:
		                /main/davis/people/standard@Other-Host,
               \"<Jones>standard.dist.3\"@Tops-20-Host;");

	dotest("SomeGroup: /main/davis/people/standard@Other-Host,
               \"<Jones>standard.dist.3\"@Tops-20-Host;");

	dotest("Pete <pete@silly.test>, A Group: Chris Jones <c@public.example>, joe@example.org, John <jdoe@one.test>;, \"nobody(that I know)\" <Undisclosed recipients>: ;");

	/*
	 * this (from rfc2822.txt) is somewhat broken.  It parses correctly, but:
	 *
	 * The extra comment after the first group "(the end of the group)"
	 * completely foils the reconstructor -- it thinks a new address is
	 * started and ends up generating "John <jdoe@one.test>the end of the
	 * group <;" as the last address in the group.
	 *
	 * the "personal" field for the last group is derived incorrectly from
	 * the comment after the colon.
	 */
	dotest("Pete(A wonderful \\) chap) <pete(his account)@silly.test(his host)>,
	       A Group(Some people)
     :Chris Jones <c@(Chris's host.)public.example>,
         joe@example.org,
  John <jdoe@one.test> (my dear friend); (the end of the group),
  (Empty list)(start)Undisclosed recipients  :(nobody(that I know))  ;");

	/* above broken recon bug, but with just one address in group.... */
	dotest("A Group (Some people): John <jdoe@one.test> (comment before end) ; (the end of the group)");

	dotest("A Group(Some people)
     :Chris Jones <c@(Chris's host.)public.example>,
         joe@example.org,
  John <jdoe@one.test> (my dear friend) (the end of the group);");

	dotest("testing my parser : peter.simons@gmd.de,
                   (peter.)simons@rhein.de ,,,,,
                   testing my parser <simons@ieee.org>,
                   it rules <@peti.gmd.de,@listserv.gmd.de:simons @ cys .de>
                   ;
                   ,
                   peter.simons@acm.org");

	/* is this legal? */
	/* XXX this creates an empty end-of-group node after never having a start_group */
	dotest("John <jdoe@one.test> something.at.the.end ;");

	/*
	 * ERROR HANDLING
	 */
	dotest("John <jdoe@one.test> a word <");
	dotest("John <jdoe@one.test>, another personal name with borked mbox <");

	/* XXX these two work, but shouldn't ("incomplete group specification") */
	dotest("John <jdoe@one.test> (an extra semicolon) ;"); /* "missing colon"? */
	dotest("\"John's Group\": John <jdoe@one.test>, Joe <joe@two.test> (no final semicolon) ");

	/* XXX these four work, but really shouldn't ("unqoted special") */
	dotest("John (Johnny) K. Doe <jdoe@one.test>");
	dotest("John K. Doe <jdoe@one.test>");
	dotest("group: John (Johnny) K. Doe <jdoe@one.test> ;");
	dotest("group: John K. Doe <jdoe@one.test> ;");

	dotest("\"John (Johnny) K. Doe <jdoe@one.test>");
	dotest("John (Johnny) K. Doe\" <jdoe@one.test>");
	dotest("group: \"John (Johnny) K. Doe <jdoe@one.test> ;");
	dotest("group: John (Johnny) K. Doe\" <jdoe@one.test> ;");

	dotest("John (Johnny) Doe <jdoe@one.test");
	dotest("John (Johnny) Doe jdoe@one.test>");
	dotest("group: John (Johnny) Doe <jdoe@one.test ;");
	dotest("group: John (Johnny) Doe jdoe@one.test> ;"); /* XXX no error, but only sees empty group */

	dotest("John Doe <jdoe@one.test> (some dude");
	dotest("John (Johnny Doe <jdoe@one.test>");
	dotest("group: John Doe <jdoe@one.test> (some dude ;");
	dotest("group: John (Johnny Doe <jdoe@one.test> ;");

	dotest("jdoe@one.test (some dude");
	dotest("jdoe@one.test some dude)"); /* XXX gives "bad address specification" */
	dotest("group: jdoe@one.test (some dude ;");
	dotest("group: jdoe@one.test some dude) ;"); /* XXX no error, but only sees empty group */

	/* XXX these four should be a "mismatched parenthesis" error */
	dotest("John Doe <jdoe@one.test> some dude)");
	dotest("John Johnny) Doe <jdoe@one.test>"); /* recon does correctly quote the name */
	dotest("group: John Doe <jdoe@one.test> some dude) ;");
	dotest("group: John Johnny) Doe <jdoe@one.test> ;"); /* recon does correctly quote the name */

#endif

	exit(0);
}

#endif
