/*	$Id: femail.c,v 1.6 2005/12/20 21:53:09 ca Exp $ */

/*
 * Copyright (c) 2005 Henning Brauer <henning@bulabula.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "sm/generic.h"
SM_RCSID("@(#)$Id: femail.c,v 1.6 2005/12/20 21:53:09 ca Exp $")
#include "sm/error.h"
#include "sm/assert.h"
#include "sm/types.h"
#include "sm/ctype.h"
#include "sm/sysexits.h"
#include "sm/io.h"
#include "sm/signal.h"

#include "sm/param.h"
#include "sm/socket.h"

#include "sm/net.h"
#include "sm/pwd.h"
#include "sm/string.h"

#include "sm/err.h"
#include <stdio.h>

#if !HAVE_FGETLN
char * fgetln(FILE *_stream, size_t *_len);
#endif

#if !HAVE_VSNPRINTF
#define vsnprintf sm_vsnprintf
#endif

void	 usage(const char *);
void	 sighdlr(int);
int	 main(int, char *[]);
void	 femail_write(const void *, size_t);
void	 femail_put(const char *, ...);
void	 send_cmd(const char *);
void	 build_from(char *, struct passwd *);
int	 parse_message(FILE *, int, int);
void	 parse_addr(char *, size_t, int);
void	 parse_addr_terminal(int);
char	*qualify_addr(char *);
void	 rcpt_add(char *);
void	 received(void);
void	 hdr_add_env(const char *, const char *);
int	 open_connection(const char *, const char *, sa_family_t);
int	 read_reply(int);
void	 greeting(int);
void	 mailfrom(char *);
void	 rcptto(char *);
void	 start_data(void);
void	 send_message(int);
void	 end_data(void);
void	 parse_config(void);
char	*next_token(char **, size_t *);

enum headerfields {
	HDR_NONE,
	HDR_FROM,
	HDR_TO,
	HDR_CC,
	HDR_BCC,
	HDR_SUBJECT,
	HDR_DATE,
	HDR_MSGID
};

struct {
	char			*word;
	enum headerfields	 type;
} keywords[] = {
	{ "From:",		HDR_FROM },
	{ "To:",		HDR_TO },
	{ "Cc:",		HDR_CC },
	{ "Bcc:",		HDR_BCC },
	{ "Subject:",		HDR_SUBJECT },
	{ "Date:",		HDR_DATE },
	{ "Message-Id:",	HDR_MSGID }
};

enum cftokens {
	CF_NONE,
	CF_SMTPHOST,
	CF_SMTPPORT,
	CF_MYNAME
};

struct {
	enum cftokens	 key;
	const char	*word;
} cfwords[] = {
	{ CF_SMTPHOST,	"smtphost" },
	{ CF_SMTPPORT,	"smtpport" },
	{ CF_MYNAME,	"myname" }
};

#define	F_VERSION		"femail 0.97"
#define	CONFIGFILE		"/etc/mail/msp.conf"
#define	STATUS_GREETING		220
#define	STATUS_HELO		250
#define	STATUS_MAILFROM		250
#define	STATUS_RCPTTO		250
#define	STATUS_DATA		354
#define	STATUS_QUEUED		250
#define	SMTP_LINELEN		1000
#define	SMTP_TIMEOUT		120
#define	TIMEOUTMSG		"Timeout\n"

#if HAVE_GETADDRINFO
#define	SMTP_HOST		"localhost"
#else
#define	SMTP_HOST		"127.0.0.1"
#endif

#define WSP(c)			(c == ' ' || c == '\t')

int	  verbose = 0;
int	  verp = 0;
int	  cap_verp = 0;
char	  host[MAXHOSTNAMELEN];
char	 *user = NULL;
char	 *smtphost = NULL;
char	 *smtpport = NULL;

struct {
	int	  fd;
	char	 *from;
	char	 *fromname;
	char	**rcpts;
	int	  rcpt_cnt;
	char	 *data;
	size_t	  len;
	int	  saw_date;
	int	  saw_msgid;
	int	  saw_from;
} msg;

struct {
	u_int		quote;
	u_int		comment;
	u_int		esc;
	u_int		brackets;
	size_t		wpos;
	char		buf[SMTP_LINELEN];
} pstate;

void
usage(const char *prg)
{
	fprintf(stderr, "usage: %s [-46tVv] [-f from] [-F name] [to [...]]\n",
	    prg);
	exit (EX_USAGE);
}

void
sighdlr(int sig)
{
	if (sig == SIGALRM) {
		write(STDERR_FILENO, TIMEOUTMSG, sizeof(TIMEOUTMSG));
		_exit (2);
	}
}

int
main(int argc, char *argv[])
{
	int		 i, ch, tflag = 0, status, noheader;
	char		*fake_from = NULL;
	char		*prg = F_VERSION;
	sa_family_t	 af = PF_UNSPEC;
	struct passwd	*pw;

	bzero(&msg, sizeof(msg));

	prg = argv[0];
	while ((ch = getopt(argc, argv, "46B:b:E::e:F:f:iJ::mo:p:tVvx")) != -1)
	{
		switch (ch) {
		case '4':
			af = AF_INET;
			break;
#ifdef AF_INET6
		case '6':
			af = AF_INET6;
			break;
#endif
		case 'f':
			fake_from = optarg;
			break;
		case 'F':
			msg.fromname = optarg;
			break;
		case 't':
			tflag = 1;
			break;
		case 'V':
			verp = 1;
			break;
		case 'v':
			verbose = 1;
			break;
		/* all remaining: ignored, sendmail compat */
		case 'B':
		case 'b':
		case 'E':
		case 'e':
		case 'i':
		case 'm':
		case 'o':
		case 'p':
		case 'x':
			break;
		default:
			usage(prg);
		}
	}

	argc -= optind;
	argv += optind;

	if (gethostname(host, sizeof(host)) == -1)
		err(1, "gethostname");
	if ((pw = getpwuid(getuid())) == NULL)
		user = "anonymous";
	if (pw != NULL && (user = strdup(pw->pw_name)) == NULL)
		err(1, "strdup");

	parse_config();
	build_from(fake_from, pw);

	while(argc > 0) {
		rcpt_add(argv[0]);
		argv++;
		argc--;
	}

	if (smtphost == NULL && (smtphost = getenv("SMTPHOST")) == NULL)
		smtphost = SMTP_HOST;
	if (smtpport == NULL && (smtpport = getenv("SMTPPORT")) == NULL)
		smtpport = "25";

	noheader = parse_message(stdin, fake_from == NULL, tflag);

	if (msg.rcpt_cnt == 0)
		errx(1, "no recipients");

	signal(SIGALRM, sighdlr);
	alarm(SMTP_TIMEOUT);

	msg.fd = open_connection(smtphost, smtpport, af);
	if ((status = read_reply(0)) != STATUS_GREETING)
		errx(1, "remote host greets us with status %d", status);
	greeting(1);
	mailfrom(msg.from);
	for (i = 0; i < msg.rcpt_cnt; i++)
		rcptto(msg.rcpts[i]);
	start_data();
	send_message(noheader);
	end_data();

	close(msg.fd);
	exit (0);
}

void
femail_write(const void *buf, size_t nbytes)
{
	ssize_t	n;

	do {
		n = write(msg.fd, buf, nbytes);
	} while (n == -1 && errno == EINTR);

	if (n == 0)
		errx(1, "write: connection closed");
	if (n == -1)
		err(1, "write");
	if ((size_t)n < nbytes)
		errx(1, "short write: %ld of %lu bytes written",
		    (long)n, (u_long)nbytes);
}

void
femail_put(const char *fmt, ...)
{
	va_list	ap;
	char	buf[SMTP_LINELEN];

	va_start(ap, fmt);
	if (vsnprintf(buf, sizeof(buf), fmt, ap) >= (int)sizeof(buf))
		errx(1, "femail_put: line length exceeded");
	va_end(ap);

	femail_write(buf, strlen(buf));
}

void
send_cmd(const char *cmd)
{
	if (verbose)
		printf(">>> %s\n", cmd);

	femail_put("%s\r\n", cmd);
}

void
build_from(char *fake_from, struct passwd *pw)
{
	char	*p;

	if (fake_from == NULL)
		msg.from = qualify_addr(user);
	else {
		if (fake_from[0] == '<') {
			if (fake_from[strlen(fake_from) - 1] != '>')
				errx(1, "leading < but no trailing >");
			fake_from[strlen(fake_from) - 1] = 0;
			if ((p = malloc(strlen(fake_from))) == NULL)
				err(1, "malloc");
			strlcpy(p, fake_from + 1, strlen(fake_from));

			msg.from = qualify_addr(p);
			free(p);
		} else
			msg.from = qualify_addr(fake_from);
	}

	if (msg.fromname == NULL && fake_from == NULL && pw != NULL) {
		size_t		 len;

		len = strcspn(pw->pw_gecos, ",");
		len++;	/* null termination */
		if ((msg.fromname = malloc(len)) == NULL)
			err(1, NULL);
		strlcpy(msg.fromname, pw->pw_gecos, len);
	}
}

int
parse_message(FILE *fin, int get_from, int tflag)
{
	char	*buf, *twodots = "..";
	size_t	 len, new_len;
	void	*newp;
	u_int	 i, cur = HDR_NONE, dotonly;
	u_int	 header_seen = 0, header_done = 0;

	bzero(&pstate, sizeof(pstate));
	for (;;) {
		buf = fgetln(fin, &len);
		if (buf == NULL && ferror(fin))
			err(1, "fgetln");
		if (buf == NULL && feof(fin))
			break;

		/* account for \r\n linebreaks */
		if (len >= 2 && buf[len - 2] == '\r' && buf[len - 1] == '\n')
			buf[--len - 1] = '\n';

		if (len == 1 && buf[0] == '\n')		/* end of header */
			header_done = 1;

		if (buf == NULL || len < 1)
			err(1, "fgetln weird");

		if (!WSP(buf[0])) {	/* whitespace -> continuation */
			if (cur == HDR_FROM)
				parse_addr_terminal(1);
			if (cur == HDR_TO || cur == HDR_CC || cur == HDR_BCC)
				parse_addr_terminal(0);
			cur = HDR_NONE;
		}

		for (i = 0; !header_done && cur == HDR_NONE &&
		    i < (sizeof(keywords) / sizeof(keywords[0])); i++)
			if (len > strlen(keywords[i].word) &&
			    !strncasecmp(buf, keywords[i].word,
			    strlen(keywords[i].word)))
				cur = keywords[i].type;

		if (cur != HDR_NONE)
			header_seen = 1;

		if (cur != HDR_BCC) {
			/* save data, \n -> \r\n, . -> .. */
			if (buf[len - 1] == '\n')
				new_len = msg.len + len + 1;
			else
				new_len = msg.len + len + 2;

			if ((len == 1 && buf[0] == '.') ||
			    (len > 1 && buf[0] == '.' && buf[1] == '\n')) {
				dotonly = 1;
				new_len++;
			} else
				dotonly = 0;

			if ((newp = realloc(msg.data, new_len)) == NULL)
				err(1, "realloc header");
			msg.data = newp;
			if (dotonly)
				memcpy(msg.data + msg.len, twodots, 2);
			else
				memcpy(msg.data + msg.len, buf, len);
			msg.len = new_len;
			msg.data[msg.len - 2] = '\r';
			msg.data[msg.len - 1] = '\n';
		}

		/*
		 * using From: as envelope sender is not sendmail compatible,
		 * but I really want it that way - maybe needs a knob
		 */
		if (cur == HDR_FROM) {
			msg.saw_from++;
			if (get_from)
				parse_addr(buf, len, 1);
		}

		if (tflag && (cur == HDR_TO || cur == HDR_CC || cur == HDR_BCC))
			parse_addr(buf, len, 0);

		if (cur == HDR_DATE)
			msg.saw_date++;
		if (cur == HDR_MSGID)
			msg.saw_msgid++;
	}

	return (!header_seen);
}

void
parse_addr(char *s, size_t len, int is_from)
{
	size_t	 pos = 0;
	int	 terminal = 0;

	/* unless this is a continuation... */
	if (!WSP(s[pos]) && s[pos] != ',' && s[pos] != ';') {
		/* ... skip over everything before the ':' */
		for (; pos < len && s[pos] != ':'; pos++)
			;	/* nothing */
		/* ... and check & reset parser state */
		parse_addr_terminal(is_from);
	}

	/* skip over ':' ',' ';' and whitespace */
	for (; pos < len && !pstate.quote && (WSP(s[pos]) || s[pos] == ':' ||
	    s[pos] == ',' || s[pos] == ';'); pos++)
		;	/* nothing */

	for (; pos < len; pos++) {
		if (!pstate.esc && !pstate.quote && s[pos] == '(')
			pstate.comment++;
		if (!pstate.comment && !pstate.esc && s[pos] == '"')
			pstate.quote = !pstate.quote;

		if (!pstate.comment && !pstate.quote && !pstate.esc) {
			if (s[pos] == ':') {	/* group */
				for(pos++; pos < len && WSP(s[pos]); pos++)
					;	/* nothing */
				pstate.wpos = 0;
			}
			if (s[pos] == '\n' || s[pos] == '\r')
				break;
			if (s[pos] == ',' || s[pos] == ';') {
				terminal = 1;
				break;
			}
			if (s[pos] == '<') {
				pstate.brackets = 1;
				pstate.wpos = 0;
			}
			if (pstate.brackets && s[pos] == '>')
				terminal = 1;
		}

		if (!pstate.comment && !terminal && (!(!(pstate.quote ||
		    pstate.esc) && (s[pos] == '<' || WSP(s[pos]))))) {
			if (pstate.wpos >= sizeof(pstate.buf))
				errx(1, "address exceeds buffer size");
			pstate.buf[pstate.wpos++] = s[pos];
		}

		if (!pstate.quote && pstate.comment && s[pos] == ')')
			pstate.comment--;

		if (!pstate.esc && !pstate.comment && !pstate.quote &&
		    s[pos] == '\\')
			pstate.esc = 1;
		else
			pstate.esc = 0;
	}

	if (terminal)
		parse_addr_terminal(is_from);

	for (; pos < len && (s[pos] == '\r' || s[pos] == '\n'); pos++)
		;	/* nothing */

	if (pos < len)
		parse_addr(s + pos, len - pos, is_from);
}

void
parse_addr_terminal(int is_from)
{
	if (pstate.comment || pstate.quote || pstate.esc)
		errx(1, "syntax error in address");
	if (pstate.wpos) {
		if (pstate.wpos >= sizeof(pstate.buf))
			errx(1, "address exceeds buffer size");
		pstate.buf[pstate.wpos] = '\0';
		if (is_from)
			msg.from = qualify_addr(pstate.buf);
		else
			rcpt_add(pstate.buf);
		pstate.wpos = 0;
	}	
}

char *
qualify_addr(char *in)
{
	char	*out;

	if (strchr(in, '@') == NULL) {
		if (asprintf(&out, "%s@%s", in, host) == -1)
			err(1, "qualify asprintf");
	} else
		if ((out = strdup(in)) == NULL)
			err(1, "qualify strdup");

	return (out);
}

void
rcpt_add(char *addr)
{
	void	*nrcpts;

	if ((nrcpts = realloc(msg.rcpts,
	    sizeof(char *) * (msg.rcpt_cnt + 1))) == NULL)
		err(1, "rcpt_add realloc");
	msg.rcpts = nrcpts;
	msg.rcpts[msg.rcpt_cnt++] = qualify_addr(addr);
}

void
received(void)
{
	char	 datestr[256];
	time_t	 now;

	now = time(NULL);
	if (strftime(datestr, sizeof(datestr), "%a, %d %b %Y %T %z",
	    localtime(&now)) == 0)
		err(1, "strftime");

	femail_put(
	    "Received: (from %s@%s, uid %lu)\r\n\tby %s (%s)\r\n\t%s\r\n",
	    user, "localhost", (u_long)getuid(), host, F_VERSION, datestr);
}

void
hdr_add_env(const char *headername, const char *envname)
{
	char	*p;

	if ((p = getenv(envname)) != NULL)
		femail_put("%s: %s\r\n", headername, p);
}

int
open_connection(const char *server, const char *port, sa_family_t af)
{
	int		 error, s = -1;
#if HAVE_GETADDRINFO
	struct addrinfo	 hints, *res, *res0;
	char		*cause = NULL;

	bzero(&hints, sizeof(hints));
	hints.ai_family = af;
	hints.ai_socktype = SOCK_STREAM;

	if ((error = getaddrinfo(server, port, &hints, &res0)))
		errx(1, "%s", gai_strerror(error));

	for (res = res0; res; res = res->ai_next) {
		s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (s == -1) {
			cause = "connect";
			continue;
		}

		if (connect(s, res->ai_addr, res->ai_addrlen) == -1) {
			cause = "socket";
			close (s);
			s = -1;
			continue;
		}

		break;	/* got one */
	}

	if (s == -1)
		err(1, "%s", cause);

	freeaddrinfo(res0);
#else /* HAVE_GETADDRINFO */
	int port_n;

	port_n = (int) strtoul(port, NULL, 0);
	error = net_client_connect(server, port_n, &s);
	if (sm_is_err(error) || s == -1)
		err(1, "connect");
#endif /* HAVE_GETADDRINFO */

	return (s);
}

int
read_reply(int readcapabilities)
{
	size_t		 len, pos, spos;
	long		 status = 0;
	char		 buf[BUFSIZ];
	ssize_t		 rlen;
	int		 done = 0;

	len = pos = spos = 0;
	while (!done) {
		if (pos == 0 ||
		    (pos > 0 && memchr(buf + pos, '\n', len - pos) == NULL)) {
			memmove(buf, buf + pos, len - pos);
			len -= pos;
			pos = 0;
			if ((rlen = read(msg.fd, buf + len,
			    sizeof(buf) - len)) == -1)
				err(1, "read");
			len += rlen;
		}
		spos = pos;

		/* status code */
		for (; pos < len && buf[pos] >= '0' && buf[pos] <= '9'; pos++)
			;	/* nothing */

		if (pos == len)
			return (0);

		if (buf[pos] == ' ')
			done = 1;
		else if (buf[pos] != '-')
			errx(1, "invalid syntax in reply from server");

		if (readcapabilities)
		{
			if (strncasecmp(buf + pos + 1, "xverp", 5) == 0)
				cap_verp = 1;
		}

		/* skip up to \n */
		for (; pos < len && buf[pos - 1] != '\n'; pos++)
			;	/* nothing */

		if (verbose) {
			char	*lbuf = NULL;
			size_t	clen;

			clen = pos - spos + 1;	/* + 1 for trailing \0 */
			if (buf[pos - 1] == '\n')
				clen--;
			if (buf[pos - 2] == '\r')
				clen--;
			if ((lbuf = malloc(clen)) == NULL)
				err(1, NULL);
			strlcpy(lbuf, buf + spos, clen);
			printf("<<< %s\n", lbuf);
			free(lbuf);
		}
	}

	status = strtol(buf, NULL, 10);
	if (status < 100 || status > 999)
		errx(1, "error reading status: out of range");

	return (status);
}

void
greeting(int use_ehlo)
{
	int	 status;
	char	*cmd, *how;

	if (use_ehlo)
		how = "EHLO";
	else
		how = "HELO";

	if (asprintf(&cmd, "%s %s", how, host) == -1)
		err(1, "asprintf");
	send_cmd(cmd);
	free(cmd);

	if ((status = read_reply(use_ehlo)) != STATUS_HELO) {
		if (use_ehlo)
			greeting(0);
		else
			errx(1, "remote host refuses our greeting");
	}
}

void
mailfrom(char *addr)
{
	int	 status;
	char	*cmd;

	if (asprintf(&cmd, "MAIL FROM:<%s>%s", addr,
		(verp && cap_verp) ? " XVERP" : "") == -1)
		err(1, "asprintf");
	send_cmd(cmd);
	free(cmd);

	if ((status = read_reply(0)) != STATUS_MAILFROM)
		errx(1, "mail from %s refused by server", addr);
}

void
rcptto(char *addr)
{
	int	 status;
	char	*cmd;

	if (asprintf(&cmd, "RCPT TO:<%s>", addr) == -1)
		err(1, "asprintf");
	send_cmd(cmd);
	free(cmd);

	if ((status = read_reply(0)) != STATUS_RCPTTO)
		errx(1, "rcpt to %s refused by server", addr);
}

void
start_data(void)
{
	int	 status;

	send_cmd("DATA");

	if ((status = read_reply(0)) != STATUS_DATA)
		errx(1, "server sends error after DATA");
}

void
send_message(int noheader)
{
	/* our own headers */
	received();
#if ADD_X_HTTP
	hdr_add_env("X-HTTP-ServerName", "SERVER_NAME");
	hdr_add_env("X-HTTP-Host", "HTTP_HOST");
	hdr_add_env("X-HTTP-RemoteAddr", "REMOTE_ADDR");
	hdr_add_env("X-HTTP-RemotePort", "REMOTE_PORT");
	hdr_add_env("X-HTTP-RemoteUser", "REMOTE_USER");
	hdr_add_env("X-HTTP-URI", "REQUEST_URI");
#endif

	if (!msg.saw_from) {
		if (msg.fromname != NULL)
			femail_put("From: %s <%s>\r\n", msg.fromname, msg.from);
		else
			femail_put("From: %s\r\n", msg.from);
	}

	if (!msg.saw_date) {
		char	 datestr[256];
		time_t	 now;

		now = time(NULL);
		if (strftime(datestr, sizeof(datestr), "%a, %d %b %Y %T %z",
		    localtime(&now)) == 0)
			err(1, "strftime");
		femail_put("Date: %s\r\n", datestr);
	}

	if (!msg.saw_msgid)	/* leaks pid :( */
		femail_put("Message-Id: <%lu.%lu.femail@%s>\r\n",
		    (u_long)time(NULL), (u_long)getpid(), host);

	if (noheader)
		femail_write("\r\n", 2);

	femail_write(msg.data, msg.len);
}

void
end_data(void)
{
	int	status;

	femail_write(".\r\n", 3);

	if ((status = read_reply(0)) != STATUS_QUEUED)
		errx(1, "error after sending mail, got status %d", status);

	send_cmd("QUIT");
}

void
parse_config(void)
{
	FILE	*cfile;
	size_t	 len;
	char	*buf, *p, *t;
	u_int	 i, key, line = 0;

	if ((cfile = fopen(CONFIGFILE, "r")) == NULL) {
		if (errno != ENOENT)
			warn("%s", CONFIGFILE);
		return;
	}

	while ((buf = fgetln(cfile, &len)) != NULL) {
		line++;
		p = buf;
		key = CF_NONE;
		if ((t = next_token(&p, &len)) != NULL) {
			for (i = 0; key == CF_NONE &&
			    i < (sizeof(cfwords) / sizeof(cfwords[0])); i++)
				if (!strcmp(cfwords[i].word, t))
					key = cfwords[i].key;
			free(t);
			if (key == CF_NONE)
				errx(1, "%s:%u: syntax error 1\n", CONFIGFILE,
				    line);
			if ((t = next_token(&p, &len)) == NULL ||
			    strcmp(t, "="))
				errx(1, "%s:%u: syntax error 2\n", CONFIGFILE,
				    line);
			free(t);
			if ((t = next_token(&p, &len)) == NULL)
				errx(1, "%s:%u: syntax error 3\n", CONFIGFILE,
				    line);

			switch (key) {
			case CF_SMTPHOST:
				smtphost = t;
				break;
			case CF_SMTPPORT:
				smtpport = t;
				break;
			case CF_MYNAME:
				strlcpy(host, t, sizeof(host));
				break;
			default:
				errx(1, "unhandled token");
			}

			if ((t = next_token(&p, &len)) != NULL)
				errx(1, "%s:%d: syntax error\n", CONFIGFILE,
				    line);
		}
	}
	fclose(cfile);
}

char *
next_token(char **s, size_t *len)
{
	char	*p, *r;
	size_t	 tlen = 0;

	while (*len > 0 && isspace(*s[0])) {
		(*s)++;
		(*len)--;
	}

	if (*len == 0)
		return (NULL);

	if (*s[0] == '#')
		return (NULL);

	if (*s[0] == '=') {
		if ((r = strdup("=")) == NULL)
			err(1, "next_token strdup");
		(*s)++;
		(*len)--;
		return (r);
	}

	p = *s;
	while(*len > 0 && !isspace(*s[0]) && *s[0] != '=') {
		(*s)++;
		(*len)--;
		tlen++;
	}

	tlen++;	/* null termination */
	if ((r = malloc(tlen)) == NULL)
		err(1, "next_token malloc");
	strlcpy(r, p, tlen);

	return (r);
}


syntax highlighted by Code2HTML, v. 0.9.1