/* This file is part of gacopyz.
   Copyright (C) 2006-2024 Sergey Poznyakoff

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3, 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 License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>. */

#include <gacopyz_priv.h>

#define TRACE(ctx,cmd,size,buf) do {                                          \
        if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_DEBUG)) {                      \
                gacopyz_log(SMI_LOG_DEBUG, _("send header: size=%lu, cmd=%c"),\
		            size, cmd);                                       \
                if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_PROTO))                \
                	gacopyz_logdump(SMI_LOG_PROTO,                        \
                	                _("send data"), buf, size);           \
                else                                                          \
                	gacopyz_log(SMI_LOG_DEBUG, _("send data"));           \
        }                                                                     \
  } while (0)

int
gacopyz_init(gacopyz_conn_t *pconn, struct smfiDesc *desc)
{
	size_t len;
	char *name;
	gacopyz_conn_t conn;

	name = desc->xxfi_name ? desc->xxfi_name : "Unknown";
	len = strlen(name);
	if (desc->xxfi_version != SMFI_VERSION) {
		gacopyz_log(SMI_LOG_ERR,
			       _("smfi_register: %s: version mismatch; "
			         "application %d != implementation %d"),
			      name, desc->xxfi_version, SMFI_VERSION);
		return MI_FAILURE;
	}

	conn = malloc(sizeof(*conn) + len + 1);

	if (!conn) {
		gacopyz_log(SMI_LOG_ERR,
			       "smfi_register: %s: %s", name, 
			       strerror(errno));
		return MI_FAILURE;
	}

	memset(conn, 0, sizeof(*conn));
	conn->sd = -1;
	conn->master_timeout.tv_usec = 0;
	conn->master_timeout.tv_sec = 5;
	conn->desc.ctx_timeout.tv_usec = 0;
	conn->desc.ctx_timeout.tv_sec = GACOPYZ_TIMEOUT;
	if (!conn->desc.logmask)
		conn->desc.logmask = SMI_DEFAULT_LOG_MASK;
	conn->desc = *desc;
	conn->desc.xxfi_name = (char*)(conn + 1);
	strcpy(conn->desc.xxfi_name, name);
	*pconn = conn;
	return MI_SUCCESS;
}	

void
gacopyz_free(gacopyz_conn_t conn)
{
	if (conn) {
		free(conn->pidtab);
		free(conn);
	}
}

static int
copy_part(const char *cstr, const char *p, char **pbuf)
{
	size_t len = p - cstr;
	char *buf = malloc(len + 1);
	if (!buf) {
		free(buf);
		return MI_FAILURE;
	}
	memcpy(buf, cstr, len);
	buf[len] = 0;
	*pbuf = buf;
	return 0;
}
	

static int
parse_url(const char *proto, const char *cstr, char **pport, char **ppath)
{
	const char *p;

	if (cstr[0] == '[') {
		p = strchr(cstr + 1, ']');
		if (p) {
			if (copy_part(cstr + 1, p, ppath))
				return MI_FAILURE;
			if (*++p == ':') {
				*pport = strdup(p + 1);
				return *pport == NULL ? MI_FAILURE : 0;
			} else if (p == NULL) {
				*pport = NULL;
				return 0;
			}
			free(*ppath);
			/* Retry parsing as a usual address:port */
		}
	}

	p = strchr(cstr, ':');

	if (!p) {
		*pport = NULL;
		*ppath = strdup(cstr);
		return *ppath == NULL ? MI_FAILURE : 0;
	} else if (copy_part(cstr, p, ppath))
		return MI_FAILURE;
	else
		cstr = p + 1;
	*pport = strdup(cstr);
	return *pport == NULL ? MI_FAILURE : 0;
}
	
int
gacopyz_parse_connection(const char *cstr, 
			 char **pproto, char **pport, char **ppath)
{
	const char *p;
	size_t len;
	
	p = strchr(cstr, ':');
	if (!p) 
		*pproto = NULL;
	else {
		char *proto;
		if (copy_part(cstr, p, &proto))
			return MI_FAILURE;
		cstr = p + 1;
		if (cstr[0] == '/' && cstr[1] == '/') {
			int rc = parse_url(proto, cstr + 2, pport, ppath);
			if (rc)
				free(proto);
			else
				*pproto = proto;
			return rc;
		}
		*pproto = proto;
	}

	p = strchr(cstr, '@');
	if (!p)
		*pport = NULL;
	else if (copy_part(cstr, p, pport))
		return MI_FAILURE;
	else
		cstr = p + 1;

	len = strlen (cstr);
	if (cstr[0] == '[' && cstr[len-1] == ']') {
		if (copy_part(cstr + 1, cstr + len - 1, ppath)) {
			free(*pproto);
			free(*pport);
			return MI_FAILURE;
		}
	} else
		*ppath = strdup(cstr);
	if (!*ppath) {
		free(*pproto);
		free(*pport);
		free(*ppath);
		return MI_FAILURE;
	}
	return MI_SUCCESS;
}

static int
parse_connection(gacopyz_conn_t conn,
		 const char *cstr,
		 char **pproto, char **pport, char **ppath)
{
	int rc = gacopyz_parse_connection(cstr, pproto, pport, ppath);
	if (rc && GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
	    gacopyz_log(SMI_LOG_ERR,
			"parse_connection: %s", strerror(ENOMEM));
	return rc;
}

static void
cleanup_unix_socket(gacopyz_conn_t conn, void *data)
{
	if (unlink(data) && errno != ENOENT) 
		if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR)) 
		    gacopyz_log(SMI_LOG_ERR,
				_("%s: %s: cannot unlink: %s"),
				conn->desc.xxfi_name, (char*)data,
				strerror(errno));
	
	free(data);
}

static int
do_connect(gacopyz_conn_t conn,
	   const char *cstr, char *proto, char *port, char *path,
	   int backlog, int rmsocket)
{
	milter_sockaddr_t addr;
	int socklen;
	int fd, flags;
	int yes = 1, rc;
	mode_t old_mask;
	
	if (!proto
	    || strcmp(proto, "unix") == 0 || strcmp(proto, "local") == 0) {
		struct stat st;
		
		if (port) {
			if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
				gacopyz_log(SMI_LOG_ERR,
				    _("%s: invalid connection type: %s; "
				      "port is meaningless for UNIX sockets"),
				   conn->desc.xxfi_name, cstr);
			return -1;
		}
		
		if (strlen(path) > sizeof addr.sunix.sun_path) {
			errno = EINVAL;
			if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
			    gacopyz_log(SMI_LOG_ERR,
					_("%s: %s: UNIX socket name too long"),
					conn->desc.xxfi_name, path);
			return -1;
		}
		
		addr.sa.sa_family = PF_UNIX;
		socklen = sizeof(addr.sunix);
		strcpy(addr.sunix.sun_path, path);
		
		if (stat(path, &st)) {
			if (errno == ENOENT) {
				conn->cleanup = cleanup_unix_socket;
				conn->cleanup_data = strdup(path);
			} else {
				if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
				    gacopyz_log(SMI_LOG_ERR,
					  _("%s: %s: cannot stat socket: %s"),
						conn->desc.xxfi_name, path,
						strerror(errno));
				return -1;
			}
		} else {
			/* FIXME: Check permissions? */
			if (!S_ISSOCK(st.st_mode)) {
				if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
					gacopyz_log(SMI_LOG_ERR,
						    _("%s: %s: not a socket"),
						    conn->desc.xxfi_name,
						    path);
				return -1;
			}
			if (rmsocket && unlink(path)) {
				if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
					gacopyz_log(SMI_LOG_ERR,
					    _("%s: %s: cannot unlink: %s"),
					   conn->desc.xxfi_name, path,
					   strerror(errno));
				return -1;
			}
		}
		
	} else if (strcmp(proto, "inet") == 0) {
		short pnum;
		long num;
		char *p;
		
		addr.sa.sa_family = PF_INET;
		socklen = sizeof(addr.sin);

		if (!port) {
			if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
				gacopyz_log(SMI_LOG_ERR, 
					 _("%s: invalid connection type: %s; "
					   "missing port number"),
					   conn->desc.xxfi_name, cstr);
			return -1;
		}

		num = pnum = strtol(port, &p, 0);
		if (*p == 0) {
			if (num != pnum) {
				if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
					gacopyz_log(SMI_LOG_ERR,
					    _("%s: invalid connection type: "
					      "%s; bad port number"),
						    conn->desc.xxfi_name,
						    cstr);
				return -1;
			}
			pnum = htons(pnum);
		} else {
			struct servent *sp = getservbyname(port, "tcp");
			if (!sp) {
				if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
					gacopyz_log(SMI_LOG_ERR,
					      _("%s: invalid connection type: "
						"%s; unknown port name"),
						    conn->desc.xxfi_name,
						    cstr);
				return -1;
			}
			pnum = sp->s_port;
		}
		
		if (!path)
			addr.sin.sin_addr.s_addr = INADDR_ANY;
		else {
			struct hostent *hp = gethostbyname(path);
			if (!hp) {
				if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
					gacopyz_log(SMI_LOG_ERR,
						 _("%s: unknown host name %s"),
						    conn->desc.xxfi_name,
						    path);
				return -1;
			}
			addr.sa.sa_family = hp->h_addrtype;
			switch (hp->h_addrtype) {
			case AF_INET:
				memmove(&addr.sin.sin_addr, hp->h_addr, 4);
				addr.sin.sin_port = pnum;
				break;

			default:
				if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
					gacopyz_log(SMI_LOG_ERR,
					   _("%s: invalid connection type: "
					     "%s; unsupported address family"),
						    conn->desc.xxfi_name,
						    cstr);
				return -1;
			}
		}
	} else if (strcmp(proto, "inet6") == 0) {
		struct addrinfo hints;
		struct addrinfo *res;
			
		memset(&hints, 0, sizeof(hints));
		hints.ai_family = AF_INET6;
		hints.ai_socktype = SOCK_STREAM;
		if (!path)
			hints.ai_flags |= AI_PASSIVE;
		rc = getaddrinfo(path, port, &hints, &res);

		switch (rc) {
		case 0:
			break;
		case EAI_SYSTEM:
			gacopyz_log(SMI_LOG_ERR,
				    _("%s:%s: cannot parse address: %s"),
				    path, port, strerror(errno));
			return -1;
			
		case EAI_BADFLAGS:
		case EAI_SOCKTYPE:
			gacopyz_log(SMI_LOG_ERR,
				    _("%s:%d: internal error converting %s:%s"),
				    __FILE__, __LINE__, path, port);
			return -1;
			
		case EAI_MEMORY:
			gacopyz_log(SMI_LOG_ERR, "not enogh memory");
			return -1;
			
		default:
			gacopyz_log(SMI_LOG_ERR,
				    "%s:%s: %s",
				    path, port, gai_strerror(rc));
			return -1;
		}

		if (res->ai_addrlen > sizeof(addr)) {
			gacopyz_log(SMI_LOG_ERR,
				    _("%s:%s: address length too big (%lu)"),
				    path, port,
				    (unsigned long) res->ai_addrlen);
			freeaddrinfo(res);
			return -1;
		}
		
		if (res->ai_next) {
			char host[NI_MAXHOST], serv[NI_MAXSERV];
			rc = getnameinfo(res->ai_addr, res->ai_addrlen,
					 host, sizeof host,
					 serv, sizeof serv,
					 NI_NUMERICHOST|NI_NUMERICSERV);
			
			gacopyz_log(SMI_LOG_WARN,
				    _("%s:%s resolves to several addresses; "
				      "using %s:%s"),
				    path, port, host, serv);
		}

		memcpy(&addr, res->ai_addr, res->ai_addrlen);
		freeaddrinfo(res);
	} else {
		if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
			gacopyz_log(SMI_LOG_ERR,
				    _("%s: unsupported protocol: %s"),
				    conn->desc.xxfi_name, proto);
		return -1;
	}

	fd = socket(addr.sa.sa_family, SOCK_STREAM, 0);
	if (fd == -1) {
		if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
			gacopyz_log(SMI_LOG_ERR,
				    _("%s: unable to create new socket: %s"),
				    conn->desc.xxfi_name, strerror(errno));
		return -1;
	}
	
	if ((flags = fcntl(fd, F_GETFD, 0)) == -1 ||
	    fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) {
		if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
			gacopyz_log(SMI_LOG_ERR,
				    _("%s: cannot set close-on-exec: %s"),
				    conn->desc.xxfi_name, strerror(errno));
		close(fd);
		return -1;
	}	  

	if (addr.sa.sa_family != PF_UNIX
	    && setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void *) &yes,
			  sizeof(yes)) == -1) {
		if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
			gacopyz_log(SMI_LOG_ERR,
				    _("%s: set reuseaddr failed (%s)"),
				    conn->desc.xxfi_name, strerror(errno));
		close(fd);
		return -1;
	}

	old_mask = umask(0117);
	rc = bind(fd, &addr.sa, socklen);
	umask(old_mask);
	if (rc < 0) {
		if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
			gacopyz_log(SMI_LOG_ERR,
				    _("%s: cannot bind to port %s: %s"),
				    conn->desc.xxfi_name, cstr,
				    strerror(errno));
		close(fd);
		return -1;
	}

	if (listen(fd, backlog)) {
		if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
			gacopyz_log(SMI_LOG_ERR,
				    _("%s: cannot listen on port %s: %s"),
				    conn->desc.xxfi_name, cstr,
				    strerror(errno));
		close(fd);
		return -1;
	}
	

	return fd;
}

int
gacopyz_open(gacopyz_conn_t conn, const char *cstr, int backlog, int rmsocket)
{
	char *proto;
	char *port;
	char *path;

	if (!conn) {
		gacopyz_log(SMI_LOG_ERR,
			    _("empty or missing socket information"));
		errno = EINVAL;
		return MI_FAILURE;
	}

	if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_DEBUG))
		gacopyz_log(SMI_LOG_DEBUG,
			    _("%s: opening listen socket on %s"),
			    conn->desc.xxfi_name, cstr);
	if (parse_connection(conn, cstr, &proto, &port, &path)) {
		errno = ENOENT;
		return MI_FAILURE;
	}

	conn->sd = do_connect(conn, cstr, proto, port, path, backlog,
			      rmsocket);

	free(proto);
	free(port);
	free(path);
	return conn->sd == -1 ? MI_FAILURE : MI_SUCCESS;
}

int
gacopyz_get_logmask(gacopyz_conn_t conn, int *mask)
{
	if (!conn || !mask)
		return MI_FAILURE;
	*mask = conn->desc.logmask;
	return MI_SUCCESS;
}

int
gacopyz_set_logmask(gacopyz_conn_t conn, int mask)
{
	if (!conn)
		return MI_FAILURE;
	conn->desc.logmask = mask;
	return MI_SUCCESS;
}
	
int
gacopyz_set_foreground(gacopyz_conn_t conn, int fg)
{
	if (!conn)
		return MI_FAILURE;
	conn->foreground = fg;
	return MI_SUCCESS;
}

int
gacopyz_connect(gacopyz_conn_t *pconn, struct smfiDesc *desc,
		 const char *cstr, int backlog, int rmsocket)
{
	gacopyz_conn_t conn;

	if (gacopyz_init(&conn, desc)) 
		return MI_FAILURE;

	if (gacopyz_open(conn, cstr, backlog, rmsocket) == -1) {
		gacopyz_free(conn);
		return MI_FAILURE;
	}
	*pconn = conn;
	return MI_SUCCESS;
}

int
gacopyz_get_fd(gacopyz_conn_t conn)
{
	if (!conn)
		return -1;
	return conn->sd;
}

int
gacopyz_set_master_timeout(gacopyz_conn_t conn, struct timeval *timeout)
{
	if (!conn)
		return MI_FAILURE;
	conn->master_timeout = *timeout;
	return MI_SUCCESS;
}

int
gacopyz_get_master_timeout(gacopyz_conn_t conn, struct timeval *timeout)
{
	if (!conn)
		return MI_FAILURE;
	*timeout = conn->master_timeout;
	return MI_SUCCESS;
}

int
gacopyz_set_ctx_timeout(gacopyz_conn_t conn, struct timeval *timeout)
{
	if (!conn)
		return MI_FAILURE;
	conn->desc.ctx_timeout = *timeout;
	return MI_SUCCESS;
}	

int
gacopyz_get_ctx_timeout(gacopyz_conn_t conn, struct timeval *timeout)
{
	if (!conn)
		return MI_FAILURE;
	*timeout = conn->desc.ctx_timeout;
	return MI_SUCCESS;
}

#include "trans.h"

int
trans_ok(enum state from, enum state to)
{
	while (1) {
		if (transtab[from][to])
			return 1;
		if (++from == st_skip)
			return 0;
		if (!transtab[from][st_skip])
			return 0;
	}
}

/* Fix up the transition table */
static void
trans_fixup(SMFICTX *ctx)
{
	transtab[st_conn][st_skip] = !!(ctx->pflags & SMFIP_NOCONNECT);
	transtab[st_helo][st_skip] = !!(ctx->pflags & SMFIP_NOHELO);
	transtab[st_mail][st_skip] = !!(ctx->pflags & SMFIP_NOMAIL);
	transtab[st_rcpt][st_skip] = !!(ctx->pflags & SMFIP_NORCPT);
	transtab[st_hdrs][st_skip] = !!(ctx->pflags & SMFIP_NOHDRS);
	transtab[st_eohs][st_skip] = !!(ctx->pflags & SMFIP_NOEOH);
	transtab[st_body][st_skip] = !!(ctx->pflags & SMFIP_NOBODY);
	transtab[st_data][st_skip] = !!(ctx->pflags & SMFIP_NODATA);
	transtab[st_unkn][st_skip] = !!(ctx->pflags & SMFIP_NOUNKNOWN);
}

static int
ctx_read(SMFICTX *ctx, char *buf, size_t size)
{
	int rc = MI_SUCCESS;
	
	while (size) {
		fd_set rset;
		fd_set xset;
		int res;
		struct timeval to;
		
		FD_ZERO(&rset);
		FD_ZERO(&xset);
		FD_SET(ctx->sd, &rset);
		FD_SET(ctx->sd, &xset);

		to = ctx->desc->ctx_timeout;

		res = select(ctx->sd + 1, &rset, NULL, &xset, &to);
		if (res == 0) {
			if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
				gacopyz_log(SMI_LOG_ERR,
					    _("%s: connection timed out"),
					    "ctx_read");
			errno = ETIMEDOUT;
			rc = MI_FAILURE;
			break;
		} else if (res < 0) {
			if (errno == EINTR)
				continue;
			rc = MI_FAILURE;
			break;
		} else if (rc > 0) {
			if (FD_ISSET(ctx->sd, &xset)) {
				if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
					gacopyz_log(SMI_LOG_ERR,
					      _("%s: exception on control fd"),
						    "ctx_read");
				rc = MI_FAILURE;
				break;
			}
			/* Otherwise, FD_ISSET(ctx->sd, &rset) is true */
		}

		res = read(ctx->sd, buf, size);
		if (res == -1) {
			if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
				gacopyz_log(SMI_LOG_ERR,
					    _("%s: read failed: %s"),
					    "ctx_read",
					    strerror(errno));
			rc = MI_FAILURE;
			break;
		} else if (res == 0) {
			if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
				gacopyz_log(SMI_LOG_ERR,
					    _("%s: end of file"),
					    "ctx_read");
			rc = MI_FAILURE;
			break;
		}

		buf += res;
		size -= res;
	}
	return rc;
}

static int
ctx_write(SMFICTX *ctx, const char *buf, size_t size)
{
	int rc = MI_SUCCESS;
	
	while (size) {
		fd_set wset;
		fd_set xset;
		int res;
		struct timeval to;
		
		FD_ZERO(&wset);
		FD_ZERO(&xset);
		FD_SET(ctx->sd, &wset);
		FD_SET(ctx->sd, &xset);

		to = ctx->desc->ctx_timeout;

		res = select(ctx->sd + 1, NULL, &wset, &xset, &to);
		if (res == 0) {
			if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
				gacopyz_log(SMI_LOG_ERR,
					    _("%s: connection timed out"),
					    "ctx_write");
			errno = ETIMEDOUT;
			rc = MI_FAILURE;
			break;
		} else if (res < 0) {
			if (errno == EINTR)
				continue;
			rc = MI_FAILURE;
			break;
		} else if (rc > 0) {
			if (FD_ISSET(ctx->sd, &xset)) {
				if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
					gacopyz_log(SMI_LOG_ERR,
					    _("%s: exception on control fd"),
					    "ctx_write");
				rc = MI_FAILURE;
				break;
			}
			/* Otherwise, FD_ISSET(ctx->sd, &wset) is true */
		}

		res = write(ctx->sd, buf, size);
		if (res == -1) {
			if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
				gacopyz_log(SMI_LOG_ERR,
					    _("%s: write failed: %s"),
					    "ctx_write",
					    strerror(errno));
			rc = MI_FAILURE;
			break;
		} else if (res == 0) {
			if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
				gacopyz_log(SMI_LOG_ERR,
					    _("%s: wrote 0 bytes"),
					    "ctx_write");
			rc = MI_FAILURE;
			break;
		} 

		buf += res;
		size -= res;
	}
	return rc;
}


union header {
	struct {
		uint32_t size;
		unsigned char cmd;
	} hdr;
	char buf[5];
};

static int
get_command(SMFICTX *ctx, unsigned char *cmd, size_t *pcount,
	    char **pbuf, size_t *psize)
{
	union header header;
	size_t size;
	int rc;

	if ((rc = ctx_read(ctx, header.buf, sizeof header.buf)) != MI_SUCCESS)
		return rc;
	
	size = ntohl(header.hdr.size) - 1;
	if (size + 1 > *psize) {
		char *p = realloc(*pbuf, size + 1);
		if (!p) {
			if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
				gacopyz_log(SMI_LOG_ERR, "%s",
					    strerror(errno));
			return MI_FAILURE;
		}
		*pbuf = p;
		*psize = size + 1;
	}

	if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_DEBUG))
		gacopyz_log(SMI_LOG_DEBUG,
			    _("read header: size=%lu, cmd=%c"),
			    size, header.hdr.cmd);
	
	if ((rc = ctx_read(ctx, *pbuf, size)) != MI_SUCCESS)
		return rc;
	(*pbuf)[size] = 0;

	if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_PROTO))
		gacopyz_logdump(SMI_LOG_PROTO, _("read data"), *pbuf, size);
	else if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_DEBUG))
	        gacopyz_log(SMI_LOG_DEBUG, _("read data"));
	*pcount = size;
	*cmd = header.hdr.cmd;
	return MI_SUCCESS;
}

#define _GACOPYZ_R_NOREPLY 0

static unsigned char 
convert_sfsistat(sfsistat stat)
{
	switch (stat) {
	case SMFIS_CONTINUE:
		return SMFIR_CONTINUE;
	case SMFIS_REJECT:
		return SMFIR_REJECT;
	case SMFIS_DISCARD:
		return SMFIR_DISCARD;
	case SMFIS_ACCEPT:
		return SMFIR_ACCEPT;
	case SMFIS_TEMPFAIL:
		return SMFIR_TEMPFAIL;
	case SMFIS_NOREPLY:
		return _GACOPYZ_R_NOREPLY;
	case SMFIS_SKIP:
		return SMFIR_SKIP;
	default:
		break;
	}
	return SMFIR_TEMPFAIL; /* Just in case */
}

#define OPTLEN 3*sizeof(uint32_t)

static int
make_optneg_buf(SMFICTX *ctx, uint32_t *vbuf,
		size_t *psize, char **pbuf)
{
	char *buf;
	size_t bufsize = 0;
	int i;
	
	for (i = 0; i < gacopyz_stage_max; i++) {
		if (ctx->req_macros[i])
			bufsize += strlen(ctx->req_macros[i]) + 1 +
				     sizeof(uint32_t);
	}

	if (bufsize == 0)
		buf = (char*) vbuf;
	else {
		bufsize += OPTLEN;
		buf = malloc(bufsize);
		if (!buf)
			return MI_FAILURE;
		vbuf = (uint32_t*)buf;
	}
	vbuf[0] = htonl(ctx->version);
	vbuf[1] = htonl(ctx->aflags);
	vbuf[2] = htonl(ctx->pflags);

	if (bufsize) {
		/*char *endp = buf + bufsize;*/
		*psize = bufsize;
		*pbuf = buf;

		buf += OPTLEN;
		for (i = 0; i < gacopyz_stage_max; i++) {
			if (ctx->req_macros[i]) {
				uint32_t v;
				size_t len;

				v = htonl(i);
				memcpy(buf, &v, sizeof(v));
				buf += sizeof(v);
				len = strlen(ctx->req_macros[i]) + 1;
				memcpy(buf, ctx->req_macros[i], len);
				buf += len;
			}
		}
	}
	return MI_SUCCESS;
}

static int
send_reply(SMFICTX *ctx, unsigned char cmd)
{
	int rc;
	union header header;
	char *buf = NULL;
	size_t bufsize = 0;
	char *alloc_mem = NULL;
	uint32_t v[3];
	unsigned long nrmask = state_nr_mask[ctx->state];

	if (nrmask && (ctx->pflags & nrmask) && cmd != _GACOPYZ_R_NOREPLY) {
		if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_WARN))
			gacopyz_log(SMI_LOG_WARN,
				    _("%s: milter claimed not to reply in state %d, but it did"),
				    ctx->desc->xxfi_name,
				    ctx->state);
		cmd = _GACOPYZ_R_NOREPLY;
	}
	
	switch (cmd) {
	case _GACOPYZ_R_NOREPLY:
		if (nrmask && (ctx->pflags & nrmask)) {
			if (ctx->mta_pflags & nrmask)
				return 0;
			else
				cmd = SMFIR_CONTINUE;
		}
		break;
			
	case SMFIR_CONTINUE:
		break;
		
	case SMFIR_TEMPFAIL:
		if (ctx->reply && *ctx->reply == '4') {
			cmd = SMFIR_REPLYCODE;
			buf = ctx->reply;
			bufsize = strlen(ctx->reply) + 1;
		} 
		break;
		
	case SMFIR_REJECT:
		if (ctx->reply && *ctx->reply == '5') {
			cmd = SMFIR_REPLYCODE;
			buf = ctx->reply;
			bufsize = strlen(ctx->reply) + 1;
		}
		break;

	case SMFIR_DISCARD:
		break;

	case SMFIR_ACCEPT:
		break;

	case SMFIC_OPTNEG:
		buf = (char*) v;
		bufsize = sizeof v;
		if (make_optneg_buf(ctx, v, &bufsize, &alloc_mem)
		    != MI_SUCCESS)
			return MI_FAILURE;
		if (alloc_mem)
			buf = alloc_mem;
		break;
		
	default: /* Ignore */
		break;
	}

	TRACE(ctx, cmd, bufsize, buf);

	header.hdr.size = htonl(bufsize + 1);
	header.hdr.cmd = cmd;
	rc = ctx_write(ctx, header.buf, sizeof header.buf);
	if (rc == MI_SUCCESS) {
		if (bufsize)
			rc = ctx_write(ctx, buf, bufsize);
	}
	if (alloc_mem)
		free(alloc_mem);
	return rc;
}

static void
macro_assoc_free(macro_assoc_t *p)
{
	free(p->argv);
	free(p->buffer);
	p->argv = NULL;
	p->buffer = NULL;
}

static void
clear_macros(SMFICTX *ctx, int i)
{
	for (; i < gacopyz_stage_max; i++)
		macro_assoc_free(&ctx->macros[i]);
}

enum state_arg_type {
	arg_no_args,           /* no arguments */
	arg_one_string,        /* one string */
	arg_two_strings,       /* two strings */
	arg_ints,              /* three integers */
	arg_argv,              /* list of nul-terminated strings */
	arg_argvc              /* 1 byte command + arg_argv */
};

union state_arg {
	struct {
		char *ptr;
		size_t len;
	} string;
	char *strings[2];
	uint32_t ints[3];
	struct {
		char cmd;
		char **v;
		char *buffer;
	} argv;
};

typedef enum {
	sret_fail,
	sret_abort,
	sret_noreply,
	sret_reply,
	sret_reject
} state_ret_type;

typedef state_ret_type (*state_handler_fn) (SMFICTX *,
					    union state_arg *,
					    unsigned char *);

struct state_disp {
	int cmd;
	char *name;
	enum state_arg_type arg_type;
	state_handler_fn fn;
	enum state next;
	int flags;
	int macro_ind;
};

static state_ret_type
shan_abort(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	if (ctx->desc->xxfi_abort)
		ctx->desc->xxfi_abort(ctx);
	return sret_noreply;
}

static state_ret_type
shan_macro(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	enum gacopyz_stage ind;
	char **p;
	
	if (!arg->argv.v)
		return sret_noreply;

	switch (arg->argv.cmd) {
	case SMFIC_CONNECT:
		ind = gacopyz_stage_conn;
		break;
		
	case SMFIC_HELO:
		ind = gacopyz_stage_helo;
		break;
		
	case SMFIC_MAIL:
		ind = gacopyz_stage_mail;
		break;
		
	case SMFIC_RCPT:
		ind = gacopyz_stage_rcpt;
		break;
		
	case SMFIC_DATA:
		ind = gacopyz_stage_data;
		break;
		
	case SMFIC_BODYEOB:
		ind = gacopyz_stage_eom;
		break;

	case SMFIC_EOH:
		ind = gacopyz_stage_eoh;
		break;
		
	default:
		return sret_fail;
	}

	macro_assoc_free(&ctx->macros[ind]);
	ctx->macros[ind].argv = arg->argv.v;
	arg->argv.v = NULL;
	ctx->macros[ind].buffer = arg->argv.buffer;
	arg->argv.buffer = NULL;
	for (p = ctx->macros[ind].argv; *p; p += 2) {
		int len;
		if ((*p)[0] == '{')
			++*p;
		len = strlen(*p);
		if (len > 0 && (*p)[len - 1] == '}')
			(*p)[len - 1] = 0;
	}
	return sret_noreply;
}

static state_ret_type
shan_body(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	if (ctx->desc->xxfi_body) {
                *cmd = convert_sfsistat(
			ctx->desc->xxfi_body(ctx,
					     (unsigned char*) arg->string.ptr,
					     arg->string.len));
	} else
		*cmd = SMFIR_CONTINUE;
	return sret_reply;
}

static state_ret_type
shan_connect(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	char *s;
	size_t len;
	int family;
	milter_sockaddr_t sockaddr;
	unsigned short port = 0;
	
	*cmd = SMFIR_CONTINUE;
	if (!ctx->desc->xxfi_connect)
		return sret_reply;

	memset(&sockaddr, 0, sizeof sockaddr);
	
	s = arg->strings[1];
	family = *s++;
	if (family != SMFIA_UNKNOWN) {
		port = *(unsigned short*) s;
		s += sizeof port;

		len = strlen(s);

		if (len > 0 && s[len])
			return sret_abort;

		switch (family) {
		case SMFIA_UNIX:
			if (len >= sizeof sockaddr.sunix.sun_path) {
				if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
					gacopyz_log(SMI_LOG_ERR,
						"%s: shan_connect: %s: %lu",
						    ctx->desc->xxfi_name,
						    _("path too long"),
						    (unsigned long) len);
				return sret_abort;
			}
			strcpy(sockaddr.sunix.sun_path, s);
			sockaddr.sunix.sun_family = AF_UNIX;
			break;
			
		case SMFIA_INET:
			if (inet_aton(s,
		             (struct in_addr *) &sockaddr.sin.sin_addr) != 1) {
				if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
					gacopyz_log(SMI_LOG_ERR,
				   _("%s: shan_connect: inet_aton(%s) failed"),
						    ctx->desc->xxfi_name,
						    s);
				return sret_abort;
			}
			sockaddr.sa.sa_family = AF_INET;
			sockaddr.sin.sin_port = port;
			break;
			
		case SMFIA_INET6: {
			int rc;
			struct addrinfo hints;
			struct addrinfo *res;
			char service[64];

			if (len > GACOPYZ_IPV6PREFIX_LEN &&
			    memcmp (s, GACOPYZ_IPV6PREFIX_STR,
				    GACOPYZ_IPV6PREFIX_LEN) == 0) {
				s += GACOPYZ_IPV6PREFIX_LEN;
				len -= GACOPYZ_IPV6PREFIX_LEN;
			}
			
			memset(&hints, 0, sizeof(hints));
			hints.ai_family = AF_INET6;
			hints.ai_socktype = SOCK_STREAM;
			snprintf(service, sizeof service, "%hu", ntohs(port));
			rc = getaddrinfo(s, service, &hints, &res);

			switch (rc) {
			case 0:
				break;
				
			case EAI_SYSTEM:
				gacopyz_log(SMI_LOG_ERR,
					    _("%s:%s: cannot parse "
					      "address: %s"),
					    s, service, strerror(errno));
				return sret_abort;
				
			case EAI_BADFLAGS:
			case EAI_SOCKTYPE:
				gacopyz_log(SMI_LOG_ERR,
					    _("%s:%d: internal error "
					      "converting %s:%s"),
					    __FILE__, __LINE__, s, service);
				return sret_abort;
				
			case EAI_MEMORY:
				gacopyz_log(SMI_LOG_ERR, "not enogh memory");
				return sret_abort;
				
			default:
				gacopyz_log(SMI_LOG_ERR,
					    "%s:%s: %s",
					    s, service, gai_strerror(rc));
				return sret_abort;
			}

		        if (res->ai_addrlen > sizeof(sockaddr)) {
			         gacopyz_log(SMI_LOG_ERR,
				             _("%s:%s: address length too "
                                             "big (%lu)"),
				             s, service,
                                             (unsigned long) res->ai_addrlen);
			         freeaddrinfo(res);
			         return sret_abort;
		        }
			memcpy(&sockaddr, res->ai_addr, res->ai_addrlen);
                        freeaddrinfo(res);
			break;
		}
			
		default:
			if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
				gacopyz_log(SMI_LOG_ERR,
				    _("%s: shan_connect: unknown family %d"),
					    ctx->desc->xxfi_name,
					    family);
			return sret_abort;
		}			
	}

        *cmd = convert_sfsistat(
		ctx->desc->xxfi_connect(ctx,
					arg->strings[0],
					family != SMFIA_UNKNOWN ?
					  &sockaddr : NULL));
	return sret_reply;
}

static state_ret_type
shan_endm(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	if (arg->string.len && ctx->desc->xxfi_body) {
                sfsistat r =
			ctx->desc->xxfi_body(ctx,
					     (unsigned char*) arg->string.ptr,
					     arg->string.len);
		if (r != SMFIS_CONTINUE
		    && send_reply(ctx, convert_sfsistat(r)) != MI_SUCCESS)
			return sret_abort;
	}
	if (ctx->desc->xxfi_eom)
		*cmd = convert_sfsistat(ctx->desc->xxfi_eom(ctx));
	else
		*cmd = SMFIR_CONTINUE;
	return sret_reply;
}

static state_ret_type
shan_helo(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	if (ctx->desc->xxfi_helo) 
                *cmd = convert_sfsistat(
			ctx->desc->xxfi_helo(ctx, arg->string.ptr));
	else
		*cmd = SMFIR_CONTINUE;
	return sret_reply;
}

static state_ret_type
shan_header(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	if (ctx->desc->xxfi_header)
                *cmd = convert_sfsistat(
			ctx->desc->xxfi_header(ctx,
					       arg->strings[0],
					       arg->strings[1]));
	else
		*cmd = SMFIR_CONTINUE;
	return sret_reply;
}

static state_ret_type
shan_mail(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	if (ctx->desc->xxfi_envfrom) {
                *cmd = convert_sfsistat(
			ctx->desc->xxfi_envfrom(ctx, arg->argv.v));
	} else
		*cmd = SMFIR_CONTINUE;
	return sret_reply;
}

#define ALL_NR_FLAGS \
   (SMFIP_NR_CONN|\
    SMFIP_NR_HELO|\
    SMFIP_NR_MAIL|\
    SMFIP_NR_RCPT|\
    SMFIP_NR_DATA|\
    SMFIP_NR_UNKN|\
    SMFIP_NR_HDR|\
    SMFIP_NR_EOH|\
    SMFIP_NR_BODY)
			

static state_ret_type
shan_optneg(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	uint32_t val;
	unsigned long mta_aflags, mta_pflags;

	val = arg->ints[0];
	if (val < SMFI_PROT_VERSION_MIN) {
		if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
			gacopyz_log(SMI_LOG_ERR,
				    _("protocol version too old: %lu"),
				    val);
		return sret_abort;
	}
	if (val > SMFI_PROT_VERSION)
		val = SMFI_PROT_VERSION;
	ctx->version = val;
	
	val = arg->ints[1];
	if (!val)
		val = SMFI_V1_ACTS;
	mta_aflags = val;

	val = arg->ints[2];
	if (!val)
		val = SMFI_V1_PROT;
	ctx->mta_pflags = mta_pflags = val;

	ctx->aflags = ctx->desc->xxfi_flags;
	if (ctx->version > SMFI_PROT_VERSION_MIN
	    && ctx->desc->xxfi_negotiate) {
		int res;
		unsigned long m_aflags = mta_aflags, m_pflags = ctx->pflags;
		unsigned long m_f2 = 0, m_f3 = 0; /* reserved */

		if (mta_pflags & SMFIP_SKIP)
			m_pflags |= SMFIP_SKIP;
		res = ctx->desc->xxfi_negotiate(ctx,
						mta_aflags,
						mta_pflags|ALL_NR_FLAGS,
						0, 0,
						&m_aflags, &m_pflags,
						&m_f2, &m_f3);
		switch (res) {
		case SMFIS_ALL_OPTS:
			ctx->aflags = mta_aflags;
			/* ctx->pflags not changed */
			if (mta_pflags & SMFIP_SKIP)
				ctx->pflags |= SMFIP_SKIP;
			break;

		case SMFIS_CONTINUE:
			ctx->aflags = m_aflags;
			ctx->pflags = m_pflags;
			break;

		default:
			if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
				gacopyz_log(SMI_LOG_ERR,
					    _("xxfi_negotiate returned %d (protocol options=0x%lx, actions=0x%lx)"),
					    res, mta_pflags, mta_aflags);
			return sret_abort;
		}

		if ((mta_pflags & ctx->pflags) != ctx->pflags) {
			unsigned i;

			for (i = 0; i < 32; i++) {
				unsigned long bit = 1 << i;
				if ((mta_pflags & bit) != bit
				    && (bit & ALL_NR_FLAGS) == bit)
				    ctx->pflags &= ~bit;
			}
		}
	} 
		
	if ((mta_aflags & ctx->aflags) != ctx->aflags) {
		if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
			gacopyz_log(SMI_LOG_ERR,
			    _("%s: shan_optneg: MTA flags %#lx do not match "
				    "actions requirements %#lx"),
				    ctx->desc->xxfi_name,
				    mta_aflags,
				    ctx->aflags);
		return sret_abort;
	}
	
	if ((mta_pflags & ctx->pflags) != ctx->pflags) {
		/* Disable protocol steps not supported by older MTAs */
		if ((ctx->pflags & SMFIP_NODATA)
		    && !(mta_pflags & SMFIP_NODATA))
			ctx->pflags &= ~SMFIP_NODATA;
		    
		if ((ctx->pflags & SMFIP_NOUNKNOWN)
		    && !(mta_pflags & SMFIP_NOUNKNOWN))
			ctx->pflags &= ~SMFIP_NOUNKNOWN;
	}
	
	if ((mta_pflags & ctx->pflags) != ctx->pflags) {
		if (GACOPYZ_CTX_LOG_MATCH(ctx, SMI_LOG_ERR))
			gacopyz_log(SMI_LOG_ERR,
			    _("%s: shan_optneg: MTA flags %#lx do not match "
			      "protocol requirements %#lx"),
				    ctx->desc->xxfi_name,
				    mta_pflags,
				    ctx->pflags);
		return sret_abort;
	}

	if (GACOPYZ_DESC_LOG_MATCH(ctx->desc, SMI_LOG_DEBUG))
		gacopyz_log(SMI_LOG_DEBUG,
			    "version=%#lx, mta_aflags=%#lx, mta_pflags=%#lx"
			    " aflags=%#lx, pflags=%#lx",
			    ctx->version,
			    mta_aflags, mta_pflags,
			    ctx->aflags, ctx->pflags);
	trans_fixup(ctx);
	
	*cmd = SMFIC_OPTNEG;
	return sret_reply;
}

static state_ret_type
shan_eoh(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	if (ctx->desc->xxfi_eoh)
		*cmd = convert_sfsistat(ctx->desc->xxfi_eoh(ctx));
	else
		*cmd = SMFIR_CONTINUE;
	return sret_reply;
}

static state_ret_type
shan_quit(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	return sret_noreply;
}

static state_ret_type
shan_data(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{	
	if (ctx->desc->xxfi_data)
		*cmd = convert_sfsistat(ctx->desc->xxfi_data(ctx));
	else
		*cmd = SMFIR_CONTINUE;
	return sret_reply;
}

static state_ret_type
shan_rcpt(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	if (ctx->desc->xxfi_envrcpt) {
                *cmd = convert_sfsistat(
			ctx->desc->xxfi_envrcpt(ctx, arg->argv.v));
	} else
		*cmd = SMFIR_CONTINUE;
	return sret_reply;
}

static state_ret_type
shan_unkn(SMFICTX *ctx, union state_arg *arg, unsigned char *cmd)
{
	if (ctx->desc->xxfi_unknown) {
                *cmd = convert_sfsistat(
			ctx->desc->xxfi_unknown(ctx, arg->string.ptr));
	} else
		*cmd = SMFIR_CONTINUE;
	return sret_reply;
}

#define CT_CNT 0
#define CT_END 0x1
#define CT_IGN 0x2
#define CT_CLR 0x4

static struct state_disp disp[] = {
  { SMFIC_ABORT,   "abort",
    arg_no_args,     shan_abort,   st_abrt, CT_CNT, gacopyz_stage_none },
  { SMFIC_MACRO,   "macro",
    arg_argvc,       shan_macro,   st_none, CT_CNT, gacopyz_stage_none },
  { SMFIC_BODY,	   "body",
    arg_one_string,  shan_body,    st_body, CT_CNT, gacopyz_stage_none },
  { SMFIC_CONNECT, "connect",
    arg_two_strings, shan_connect, st_conn, CT_CLR, gacopyz_stage_conn },
  { SMFIC_BODYEOB, "endm",
    arg_one_string,  shan_endm,    st_endm, CT_CNT, gacopyz_stage_eom  },
  { SMFIC_HELO,	   "helo",
    arg_one_string,  shan_helo,    st_helo, CT_CLR, gacopyz_stage_helo },
  { SMFIC_HEADER,  "header",
    arg_two_strings, shan_header,  st_hdrs, CT_CNT, gacopyz_stage_none },
  { SMFIC_MAIL,	   "mail",
    arg_argv,        shan_mail,    st_mail, CT_CLR, gacopyz_stage_mail },
  { SMFIC_OPTNEG,  "optneg",
    arg_ints,        shan_optneg,  st_opts, CT_CNT, gacopyz_stage_none },
  { SMFIC_EOH,	   "eoh",
    arg_no_args,     shan_eoh,     st_eohs, CT_CNT, gacopyz_stage_none },
  { SMFIC_QUIT,	   "quit",
    arg_no_args,     shan_quit,    st_quit, CT_END, gacopyz_stage_none },
  { SMFIC_DATA,	   "data",
    arg_no_args,     shan_data,    st_data, CT_CNT, gacopyz_stage_none },
  { SMFIC_RCPT,	   "rcpt",
    arg_argv,        shan_rcpt,    st_rcpt, CT_IGN|CT_CLR, gacopyz_stage_rcpt },
  { SMFIC_UNKNOWN, "unknown",
    arg_one_string,  shan_unkn,    st_unkn, CT_IGN|CT_CLR, gacopyz_stage_none }
};

struct state_disp *
find_disp(int cmd, int allow_unkn)
{
	struct state_disp *def = NULL;
	struct state_disp *sd;
	for (sd = disp; sd < disp + sizeof(disp)/sizeof(disp[0]); sd++) {
		if (sd->cmd == cmd)
			return sd;
		if (allow_unkn && sd->cmd == SMFIC_UNKNOWN)
			def = sd;
	}
	return def;
}

static int
parse_state_arg(union state_arg *arg, enum state_arg_type type,
		char *buffer, size_t size)
{
	switch (type) {
	case arg_no_args:           /* no arguments */
		return MI_SUCCESS;
		
	case arg_one_string:        /* one string */
		arg->string.ptr = buffer;
		arg->string.len = size;
		return MI_SUCCESS;
		
	case arg_two_strings:       /* two strings */
	{
		int len = strlen(buffer);
		arg->strings[0] = buffer;
		arg->strings[1] = buffer + len + 1;
		return MI_SUCCESS;
	}
	
	case arg_ints:          /* three integers */
	{
		if (size < GACOPYZ_OPTLEN) 
			return MI_FAILURE;
				   
		arg->ints[0] = ntohl(((uint32_t*)buffer)[0]);
		arg->ints[1] = ntohl(((uint32_t*)buffer)[1]);
		arg->ints[2] = ntohl(((uint32_t*)buffer)[2]);
		return MI_SUCCESS;
	}
	
	case arg_argvc:
		arg->argv.cmd = *buffer;
		buffer++;
		size--;
		/* arg_arg follows; fall-through */
	case arg_argv: /* list of nul-terminated strings */
	{
		size_t i, count = 0;
		char *p;

		arg->argv.buffer = malloc(size);
		if (!arg->argv.buffer)
			return MI_FAILURE;
		memcpy(arg->argv.buffer, buffer, size);
		
		for (p = arg->argv.buffer; p < arg->argv.buffer + size; p++)
			if (*p == 0)
				count++;
		if (count == 0) {
			arg->argv.v = NULL;
			return MI_SUCCESS;
		}
		arg->argv.v = calloc(count + 1, sizeof arg->argv.v[0]);
		if (!arg->argv.v) {
			free(arg->argv.buffer);
			return MI_FAILURE;
		}
		
		for (i = 0, p = arg->argv.buffer; i < count;
		     i++, p += strlen(p) + 1) 
			arg->argv.v[i] = p;
		arg->argv.v[i] = NULL;
		return MI_SUCCESS;
	}
	}
	return MI_FAILURE;
}

static void
arg_free(union state_arg *arg, enum state_arg_type type)
{
	if (type == arg_argv) {
		free(arg->argv.v);
		free(arg->argv.buffer);
	}
}

/* in a mail transaction? must be before eom according to spec. */
#define ST_IN_MAIL(st) ((st) >= st_mail && (st) < st_endm)
#define STATE_NAME(st) ((st == st_none) ? "st_none" : state_name[st])

static void
report_command(enum state state, enum state next_state,
	       int cmd, char *buffer, size_t size) 	
{					
        gacopyz_log(SMI_LOG_DEBUG,
                    _("state %s is unreachable from %s"),
                    STATE_NAME(next_state), STATE_NAME(state));
	gacopyz_log(SMI_LOG_DEBUG,
	            isascii(cmd) && isprint(cmd)
		       ? _("ignoring milter command: %c")
                       : _("ignoring milter command: \\%03o"), 
                    (unsigned char) cmd); 
        gacopyz_logdump(SMI_LOG_DEBUG, _("buffer"), buffer, size);
}

static void
ctx_free(SMFICTX *ctx)
{
	int i;
	
	free(ctx->reply);
	clear_macros(ctx, 0);
	for (i = 0; i < gacopyz_stage_max; i++)
		free(ctx->req_macros[i]);
}

int
gacopyz_context_loop(int fd, struct smfiDesc const *desc,
		     milter_sockaddr_t *addr, socklen_t addrlen,
		     void *closure)
{
	int rc;
	char *buffer = NULL;
	size_t bufsize = 0;
	size_t size;
	unsigned char cmd;
	SMFICTX ctx;
	int allow_unkn = desc->xxfi_unknown != NULL;
	enum state state = st_init;
	int stop = 0;
	
	if (desc->xxfi_start)
		desc->xxfi_start();

	memset(&ctx, 0, sizeof ctx);
	ctx.desc = desc;
	ctx.sd = fd;
	ctx.addr = *addr;
	ctx.addrlen = addrlen;
	ctx.closure = closure;
	if (!desc->xxfi_connect)
		ctx.pflags |= SMFIP_NOCONNECT;
	if (!desc->xxfi_helo)
		ctx.pflags |= SMFIP_NOHELO;
	if (!desc->xxfi_envfrom)
		ctx.pflags |= SMFIP_NOMAIL;
	if (!desc->xxfi_envrcpt)
		ctx.pflags |= SMFIP_NORCPT;
	if (!desc->xxfi_header)
		ctx.pflags |= SMFIP_NOHDRS;
	if (!desc->xxfi_eoh)
		ctx.pflags |= SMFIP_NOEOH;
	if (!desc->xxfi_body)
		ctx.pflags |= SMFIP_NOBODY;
	if (!desc->xxfi_data)
		ctx.pflags |= SMFIP_NODATA;
	if (!desc->xxfi_unknown)
		ctx.pflags |= SMFIP_NOUNKNOWN;
	
	trans_fixup(&ctx);
	
	if (GACOPYZ_DESC_LOG_MATCH(desc, SMI_LOG_DEBUG))
		gacopyz_log(SMI_LOG_DEBUG, _("start of context loop"));
	
	while (!stop
	       && (rc = get_command(&ctx, &cmd, &size, &buffer, &bufsize))
	           == MI_SUCCESS) {
		enum state next_state;
		state_ret_type ret;
		union state_arg arg;
		struct state_disp *sd = find_disp(cmd, allow_unkn);

		if (sd == NULL) {
			if (GACOPYZ_DESC_LOG_MATCH(desc, SMI_LOG_ERR)) {
				gacopyz_log(SMI_LOG_ERR,
					    isascii(cmd) && isprint(cmd)
					    ? _("unknown command: %c")
					    : _("unknown command: \\%03o"), 
					    (unsigned char) cmd); 
				gacopyz_logdump(SMI_LOG_ERR,
						_("buffer"),
						buffer, size);
			}
			rc = MI_FAILURE;
			break;
		}

		next_state = sd->next;
		stop = sd->flags & CT_END;

		if (GACOPYZ_DESC_LOG_MATCH(desc, SMI_LOG_DEBUG))
			gacopyz_log(SMI_LOG_DEBUG,
				    _("state %s, next state %s"),
				    STATE_NAME(state),
				    next_state == st_none ?
				      STATE_NAME(state) :
				      STATE_NAME(next_state));
		
		if (next_state != st_none && !trans_ok(state, next_state)) {
			if (GACOPYZ_DESC_LOG_MATCH(desc, SMI_LOG_DEBUG))
				gacopyz_log(SMI_LOG_DEBUG,
					_("transition from state %s to %s is "
					  "prohibited"),
					    STATE_NAME(state),
					    STATE_NAME(next_state));
			
			if (ST_IN_MAIL(state) && desc->xxfi_abort)
				desc->xxfi_abort(&ctx);

			/* Retry starting from HELO */
			state = st_helo;
			if (!trans_ok(state, next_state)) {
				if (GACOPYZ_DESC_LOG_MATCH(desc, 
                        		                   SMI_LOG_DEBUG)) 
					report_command(state, next_state,
						       cmd, buffer, size);
				continue; /* Ignore the command */
			}
		}

		if (next_state != st_none) {
			state = next_state;
			ctx.state = state;
		}

		if (parse_state_arg(&arg, sd->arg_type, buffer, size)) {
			if (GACOPYZ_DESC_LOG_MATCH(desc, SMI_LOG_ERR)) {
				gacopyz_log(SMI_LOG_ERR,
					    _("argument parsing failed"));
				gacopyz_logdump(SMI_LOG_ERR,
						_("buffer"),
						buffer, size);
			}
			rc = MI_FAILURE;
			break;
		}

		if (ctx.reply) {
			free(ctx.reply);
			ctx.reply = NULL;
		}
		if (sd->flags & CT_CLR)
			clear_macros(&ctx, sd->macro_ind + 1);
		ret = sd->fn(&ctx, &arg, &cmd);
		arg_free(&arg, sd->arg_type);

		switch (ret) {
		case sret_noreply:
			break;

		case sret_reply:
			rc = send_reply(&ctx, cmd);
			break;

		case sret_reject:
			if (!(sd->flags & CT_IGN))
				state = st_helo;
			break;

		case sret_fail:
			if (GACOPYZ_DESC_LOG_MATCH(desc, SMI_LOG_DEBUG))
				gacopyz_log(SMI_LOG_DEBUG,
					    _("%s handler returned failure"),
					    sd->name);
			break;
			
		case sret_abort:
			if (GACOPYZ_DESC_LOG_MATCH(desc, SMI_LOG_DEBUG))
				gacopyz_log(SMI_LOG_DEBUG,
					    _("%s handler returned abort"),
					    sd->name);
			rc = MI_FAILURE;
		}

		if (rc != MI_SUCCESS) 
			break;
	}

	if (GACOPYZ_DESC_LOG_MATCH(desc, SMI_LOG_DEBUG))
		gacopyz_log(SMI_LOG_DEBUG,
			    _("end of context loop: %d"), rc);
	
	if (rc != MI_SUCCESS && ST_IN_MAIL(state)
	    && desc->xxfi_abort)
		desc->xxfi_abort(&ctx);
	
	free(buffer);
	if (desc->xxfi_close)
		desc->xxfi_close(&ctx);

	ctx_free(&ctx);
	
	if (desc->xxfi_finish)
		desc->xxfi_finish();
	
	return rc;
}	

int
gacopyz_handle_connection(gacopyz_conn_t conn)
{
	milter_sockaddr_t addr;
	socklen_t addrlen = sizeof addr;
	int fd;
	int rc;
	
 	fd = accept(conn->sd, &addr.sa, &addrlen);
	if (fd == -1) {
		if (errno == EINTR)
			return MI_SUCCESS;
		if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
			gacopyz_log(SMI_LOG_ERR,
				    _("accept failed: %s"), strerror(errno));
		return MI_FAILURE;
	}

	if (conn->desc.xxfi_accept
	    && conn->desc.xxfi_accept(conn, fd, &addr, addrlen)) {
		if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_DEBUG))
			gacopyz_log(SMI_LOG_DEBUG,
				    _("connection refused by xxfi_accept"));
		close (fd);
		return MI_SUCCESS;
	}
	
	if (!conn->foreground) {
		pid_t pid = fork();
		if (pid == -1) {
			if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_ERR))
				gacopyz_log(SMI_LOG_ERR,
					    _("fork() failed: %s"),
					    strerror(errno));
			return MI_FAILURE;
		}
		if (pid > 0) {
			gacopyz_register_child(conn, pid);
			close(fd);
			return MI_SUCCESS;
		}
		/* Child: */
		/* Reset signal handlers */
		signal(SIGCHLD, SIG_IGN);
		signal(SIGPIPE, SIG_DFL);
		signal(SIGTERM, SIG_DFL);
		signal(SIGHUP, SIG_DFL);
		/* Close listen descriptor */
		close(conn->sd);
		conn->sd = -1;
	}

	switch (addr.sa.sa_family) {
	case AF_UNIX: {
		char *path;
		
		if (addrlen == sizeof (addr.sa.sa_family))
			path = "[unnamed]";
		else
			path = addr.sunix.sun_path;
		if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_INFO))
			gacopyz_log(SMI_LOG_INFO,
				    _("connect from socket %s"),
				    path);
		break;
	}
		
	case AF_INET:
		if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_INFO))
			gacopyz_log(SMI_LOG_INFO, _("connect from %s:%u"),
				    inet_ntoa(addr.sin.sin_addr), 
				    (unsigned) ntohs(addr.sin.sin_port));
		break;

	default:
		if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_INFO))
			gacopyz_log(SMI_LOG_INFO, 
				    _("connection from unsupported family: %d"), 
				    addr.sa.sa_family);
	}
		
	rc = gacopyz_context_loop(fd, &conn->desc, &addr, addrlen, NULL);
	close(fd);

	if (GACOPYZ_CONN_LOG_MATCH(conn, SMI_LOG_INFO))
		gacopyz_log(SMI_LOG_INFO, _("finishing connection"));
	
	if (!conn->foreground) {
		exit(0);
	}
	return rc;
}

int
gacopyz_run(gacopyz_conn_t conn)
{
	while (!conn->stop) {
		fd_set rfd;
		struct timeval timeout = conn->master_timeout;
		int rc;

		gacopyz_cleanup_children(conn);

		FD_ZERO(&rfd);
		FD_SET(conn->sd, &rfd);
		rc = select(conn->sd + 1, &rfd, NULL, NULL, &timeout);
		if (rc < 0) {
			if (errno == EINTR)
				continue;
			return MI_FAILURE;
		} else if (rc == 0) {
			if (conn->desc.xxfi_idle)
				conn->desc.xxfi_idle(conn);
		} else 
			gacopyz_handle_connection(conn);
	}
	gacopyz_cleanup_conn(conn);
	return MI_SUCCESS;
}

int
gacopyz_stop(gacopyz_conn_t conn)
{
	if (!conn)
		return MI_FAILURE;
	conn->stop = 1;
	return MI_SUCCESS;
}

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

const char *
gacopyz_safe_header_value(const char *input, char **poutput)
{
	char *output;
	const char *p;
	size_t len;
	size_t count;
	int tab, newline;
	
	for (count = 0, p = input; *p && (p = strchr(p, '\n')); ) {
		if (p == input || (p[1] && !ISWS(p[1])))
			count++;
		p++;
	}

	if (!count) {
		*poutput = NULL;
		return input;
	}

	output = malloc(strlen(input) + count + 1);
	*poutput = output;
	if (!output)
		return NULL;
	newline = tab = 0;
	while (*input) {
		len = strcspn(input, "\n");
		if (len == 0 && input[1] == 0)
			break;
		if (newline) 
			*output++ = '\n';
		else
			newline = 1;
		if (tab)
			*output++ = '\t';
		tab = !ISWS(input[len+1]);
		if (len) {
			memcpy(output, input, len);
			input += len;
			output += len;
		}
		if (*input)
			input++;
	}
	*output = 0;
	return *poutput;
}

static int
gacopyz_header_command0(SMFICTX *ctx, unsigned char cmd,
			int idx, const char *headerf,
			const char *headerv)
{
	char *buf;
	size_t bufsize;
	union header *hptr;
	char *cptr;
	size_t lenf, lenv;
	int rc;
	
	if (!headerf || !*headerf || !headerv)
		return MI_FAILURE;
	lenf = strlen(headerf) + 1;
	lenv = strlen(headerv) + 1;
	bufsize = lenf + lenv;
	if (idx >= 0)
		bufsize += sizeof(uint32_t);
	buf = malloc(sizeof hptr->buf + bufsize);
	if (!buf)
		return MI_FAILURE;
	hptr = (union header *) buf;
	cptr = buf + sizeof hptr->buf;
	if (idx >= 0) {
		*(uint32_t*)cptr = htonl(idx);
		cptr += sizeof(uint32_t);
	}
	memcpy(cptr, headerf, lenf);
	cptr += lenf;
	memcpy(cptr, headerv, lenv);
	cptr += lenv;

	TRACE(ctx, cmd, sizeof hptr->buf + bufsize, buf);
	
	hptr->hdr.cmd = cmd;
	hptr->hdr.size = htonl(bufsize + 1);
	rc = ctx_write(ctx, buf, sizeof hptr->buf + bufsize);
	free(buf);
	return rc;
}

static int
gacopyz_header_command(SMFICTX *ctx, unsigned char cmd,
			int idx, const char *headerf,
			const char *headerv)
{
	int rc;
	char *header_tmp;

	if (!headerf || !*headerf || !headerv)
		return MI_FAILURE;
	headerv = gacopyz_safe_header_value(headerv, &header_tmp);
	if (!headerv)
		return MI_FAILURE;
	rc = gacopyz_header_command0(ctx, cmd, idx, headerf, headerv);
	free(header_tmp);
	return rc;
}

static int
gacopyz_rcpt_command(SMFICTX *ctx, unsigned char cmd, const char *rcpt)
{
	char *buf;
	size_t bufsize;
	union header *hptr;
	char *cptr;
	int rc;
	
	if (!rcpt || !*rcpt)
		return MI_FAILURE;
	bufsize = strlen(rcpt) + 1;
	buf = malloc(sizeof hptr->buf + bufsize);
	if (!buf)
		return MI_FAILURE;
	hptr = (union header *) buf;
	cptr = buf + sizeof hptr->buf;
	strcpy(cptr, rcpt);

	TRACE(ctx, cmd, bufsize, buf);

	hptr->hdr.cmd = cmd;
	hptr->hdr.size = htonl(bufsize + 1);
	rc = ctx_write(ctx, buf, sizeof hptr->buf + bufsize);
	free(buf);
	return rc;
}

static int
gacopyz_argn_command(SMFICTX *ctx, unsigned char cmd, int argc, char const **argv)
{
	char *buf;
	size_t bufsize;
	union header *hptr;
	char *cptr;
	int i, rc;
	
	if (argc == 0)
		return MI_FAILURE;
	bufsize = 0;
	for (i = 0; i < argc; i++)
		bufsize += strlen(argv[i]) + 1;
	buf = malloc(sizeof hptr->buf + bufsize);
	if (!buf)
		return MI_FAILURE;
	hptr = (union header *) buf;
	cptr = buf + sizeof hptr->buf;
	for (i = 0; i < argc; i++) {
		char const *q;
		for (q = argv[i]; *cptr++ = *q++; );
	}

	TRACE(ctx, cmd, sizeof hptr->buf + bufsize, buf);
	
	hptr->hdr.cmd = cmd;
	hptr->hdr.size = htonl(bufsize + 1);
	rc = ctx_write(ctx, buf, sizeof hptr->buf + bufsize);
	free(buf);
	return rc;
}
		
static int
ok_to_send(SMFICTX *ctx, int cmd)
{
	if (!ctx)
		return 0;
	if (cmd && !(ctx->aflags & cmd))
		return 0;
	return ctx->state == st_endm;
}

int
gacopyz_add_header(SMFICTX *ctx, const char *headerf, const char *headerv)
{
	if (!ok_to_send(ctx, SMFIF_ADDHDRS))
		return MI_FAILURE;
	return gacopyz_header_command(ctx, SMFIR_ADDHEADER, -1,
				       headerf, headerv);
}

int
gacopyz_insert_header(SMFICTX *ctx, int index, const char *headerf,
		       const char *headerv)
{
	if (!ok_to_send(ctx, SMFIF_ADDHDRS) || index < 0)
		return MI_FAILURE;
	return gacopyz_header_command(ctx, SMFIR_INSHEADER, index,
				       headerf, headerv);
}

int
gacopyz_change_header(SMFICTX *ctx, int index, const char *headerf,
		       const char *headerv)
{
	if (!ok_to_send(ctx, SMFIF_CHGHDRS) || index < 0)
		return MI_FAILURE;
	return gacopyz_header_command(ctx, SMFIR_CHGHEADER, index,
				       headerf, headerv ? headerv : "");
}
	
int
gacopyz_add_rcpt(SMFICTX *ctx, const char *rcpt)
{
	if (!ok_to_send(ctx, SMFIF_ADDRCPT))
		return MI_FAILURE;
	return gacopyz_rcpt_command(ctx, SMFIR_ADDRCPT, rcpt);
}

int
gacopyz_del_rcpt(SMFICTX *ctx, const char *rcpt)
{
	if (!ok_to_send(ctx, SMFIF_DELRCPT))
		return MI_FAILURE;
	return gacopyz_rcpt_command(ctx, SMFIR_DELRCPT, rcpt);
}

#define GACOPYZ_CHUNK_SIZE 65535

int
gacopyz_replace_body_fn(SMFICTX *ctx,
			void *bodyp, 
			ssize_t (*cpf)(char*,void*,size_t))
{
	union header *hptr;
	char buf[sizeof hptr->buf + GACOPYZ_CHUNK_SIZE];
	
	if (!bodyp || !cpf)
		return MI_FAILURE;
	if (!ok_to_send(ctx, SMFIF_CHGBODY))
		return MI_FAILURE;

	hptr = (union header *)buf;
	hptr->hdr.cmd = SMFIR_REPLBODY;
	while (1) {
		ssize_t wrs = cpf(buf + sizeof hptr->buf, bodyp,
				  GACOPYZ_CHUNK_SIZE);
		if (wrs == 0)
			break;
		if (wrs < 0)
			return MI_FAILURE;
		hptr->hdr.size = htonl(wrs + 1);		
		TRACE(ctx, hptr->hdr.cmd, wrs, buf);
		if (ctx_write(ctx, buf, sizeof hptr->buf + wrs))
			return MI_FAILURE;
	}
	return MI_SUCCESS;
}

struct bodyptr {
	const unsigned char *bodyp;
	size_t bodylen;
};

static ssize_t
cpf_mem(char *dst, void *src, size_t size)
{
	struct bodyptr *bp = src;
	if (size > bp->bodylen) {
		size = bp->bodylen;
		if (size == 0)
			return size;
	}
	memcpy(dst, bp->bodyp, size);
	bp->bodyp += size;
	bp->bodylen -= size;
	return size;
}

int
gacopyz_replace_body(SMFICTX *ctx, const unsigned char *bodyp, size_t bodylen)
{
	struct bodyptr bp;
	bp.bodyp = bodyp;
	bp.bodylen = bodylen;
	return gacopyz_replace_body_fn(ctx, &bp, cpf_mem);
}

struct bodyfd {
	int fd;
};

static ssize_t
cpf_fd(char *dst, void *src, size_t size)
{
	struct bodyfd *bp = src;
	return read(bp->fd, dst, size);
}

int
gacopyz_replace_body_fd(SMFICTX *ctx, int fd)
{
	struct bodyfd bp;
	bp.fd = fd;
	return gacopyz_replace_body_fn(ctx, &bp, cpf_fd);
}

int
gacopyz_progress(SMFICTX *ctx)
{
	union header hdr;
	hdr.hdr.cmd = SMFIR_PROGRESS;
	hdr.hdr.size = htonl(1);
	TRACE(ctx, hdr.hdr.cmd, 0, NULL);
	return ctx_write(ctx, hdr.buf, sizeof hdr.buf);
}

int
gacopyz_quarantine(SMFICTX *ctx, const char *reason)
{
	if (reason == NULL || *reason == '\0')
		return MI_FAILURE;
	if (!ok_to_send(ctx, SMFIF_QUARANTINE))
		return MI_FAILURE;
	return gacopyz_rcpt_command(ctx, SMFIR_QUARANTINE, reason);
}

int
gacopyz_add_rcpt_par(SMFICTX *ctx, const char *rcpt, const char *args)
{
	int n;
	char const *argv[2];

	if (!rcpt || !*rcpt)
		return MI_FAILURE;
	if (!ok_to_send(ctx, SMFIF_ADDRCPT_PAR))
		return MI_FAILURE;

	argv[0] = rcpt;
	if (args) {
		argv[1] = args;
		n = 2;
	} else
		n = 1;
	return gacopyz_argn_command(ctx, SMFIR_ADDRCPT_PAR, n, argv);
}

int
gacopyz_chgfrom(SMFICTX *ctx, const char *from, const char *args)
{
	int n;
	char const *argv[2];

	if (!from || !*from)
		return MI_FAILURE;
	if (!ok_to_send(ctx, SMFIF_CHGFROM))
		return MI_FAILURE;
	argv[0] = from;
	if (args) {
		argv[1] = args;
		n = 2;
	} else
		n = 1;
	return gacopyz_argn_command(ctx, SMFIR_CHGFROM, n, argv);
}

int
gacopyz_setsymlist(SMFICTX *ctx, enum gacopyz_stage ind, const char *macros)
{
	if (ind < 0 || ind >= gacopyz_stage_max)
		return MI_FAILURE;
	if (ctx->req_macros[ind])
		free(ctx->req_macros[ind]);
	if (macros == NULL)
		ctx->req_macros[ind] = NULL;
	else {
		ctx->req_macros[ind] = strdup(macros);
		if (!ctx->req_macros[ind])
			return MI_FAILURE;
	}
	return MI_SUCCESS;
}
