/* This file is part of gacopyz.
   Copyright (C) 2007-2025 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>

typedef struct gacopyz_macro_def *gacopyz_macro_def_t;
struct gacopyz_macro_def {
	char *name;
	char *value;
};

#define _SRV_CONNECTED 0x0100
#define _SRV_READY     0x0200
#define _SRV_CLRDIS    0x0400
#define _SRV_SYS_MASK  0xff00

struct gacopyz_srv {
	char *id;                      /* Server identifier (not used yet) */
	char *portspec;                /* Port spec */
	struct gacopyz_iod iod;
	int flags;

	gacopyz_macro_def_t def;
	size_t ndefs;
	size_t maxdefs;
	struct sockaddr *source_addr;
	socklen_t source_addr_len;
	
	int onerror;
	void (*memerror)(gacopyz_srv_t, const char *, unsigned int);

	unsigned long version;
	unsigned long acts;
	unsigned long proto;
	char **req_macros[gacopyz_stage_max]; /* Required macros */
	
	int (*cb_reply)(gacopyz_srv_t srv, int cmd, int rcmd, void *data);
	void *cb_data;
	
	char *buf;
	size_t bufsize;

	char *resp_ptr;
	size_t resp_size;
};

void
default_memerror(gacopyz_srv_t srv, const char *file, unsigned int line)
{
	gacopyz_io_log(&srv->iod, SMI_LOG_FATAL,
		       _("%s:%lu: not enough memory"),
		       file, line);
	abort();
}

#define GACOPYZ_ASSERT(e)                                                     \
 if (!(e)) {                                                                  \
       fprintf(stderr, "%s:%d: GACOPYZ usage error: assertion failed: %s\n",  \
               __FILE__, __LINE__, #e);                                       \
       abort();                                                               \
 }

#define GACOPYZ_ALLOC(srv, expr) do                                           \
  if (!(expr)) {                                                              \
	srv->memerror(srv, __FILE__, __LINE__);                               \
	if (!(expr))                                                          \
		default_memerror(srv, __FILE__, __LINE__);                    \
  } while (0)

static struct gacopyz_macro_def *
find_def(gacopyz_srv_t srv, const char *name, size_t len)
{
	size_t i;
	for (i = 0; i < srv->ndefs; i++)
		if (strncmp(srv->def[i].name, name, len) == 0)
			return &srv->def[i];
	return NULL;
}

static void
add_def(gacopyz_srv_t srv, const char *name, size_t len, const char *value)
{
	struct gacopyz_macro_def *def;

	if (def = find_def(srv, name, len)) 
		free(def->value);
	else {
		if (srv->ndefs == srv->maxdefs) {
			size_t maxdefs = srv->maxdefs + 64;
			GACOPYZ_ALLOC(srv, 
				      def = realloc(srv->def,
						    sizeof(srv->def[0]) * maxdefs));
			srv->maxdefs = maxdefs;
			srv->def = def;
		}
		def = srv->def + srv->ndefs++;
		GACOPYZ_ALLOC(srv, def->name = malloc(len + 1));
		memcpy(def->name, name, len);
		def->name[len] = 0;
	}
	GACOPYZ_ALLOC(srv, def->value = strdup(value));
}

void
del_def(gacopyz_srv_t srv, const char *name, size_t len)
{
	struct gacopyz_macro_def *def = find_def(srv, name, len);
  
	if (!def)
		return;
	
	free(def->name);
	free(def->value);
	srv->ndefs--;
	memmove(def, def + 1,
		 sizeof(srv->def[0]) *(srv->ndefs - (def - srv->def)));
}

static void
srv_ensure_space(gacopyz_srv_t srv, size_t size)
{
	if (size > srv->bufsize) {
		char *p;
		GACOPYZ_ALLOC(srv, p = realloc(srv->buf, size));
		srv->buf = p;
		srv->bufsize = size;
	}
}

size_t
macro_size(gacopyz_srv_t srv)
{
	size_t i;
	size_t size = 0;
	struct gacopyz_macro_def *def;
	
	for (i = 0, def = srv->def; i < srv->ndefs; i++, def++) {
		size_t len = strlen(def->name);
		size += len;
		if (len > 1 && def->name[0] != '{')
			size += 2; /* for {} */
		size += 1 + strlen(def->value) + 1;
	}
	return size;
}

void
srv_format_macros(gacopyz_srv_t srv, unsigned char cmd, size_t *psize)
{
	size_t i;
	char *p;
	size_t size = macro_size(srv) + 1;
	struct gacopyz_macro_def *def;

	srv_ensure_space(srv, size);

	p = srv->buf;
	*p++ = cmd;
	for (i = 0, def = srv->def; i < srv->ndefs; i++, def++) {
		size_t len = strlen(def->name);
		if (len > 1 && def->name[0] != '{') {
			*p++ = '{';
			memcpy(p, def->name, len);
			p += len;
			*p++ = '}';
			*p++ = 0;
		} else {
			len++;
			memcpy(p, def->name, len);
			p += len;
		}
		len = strlen(def->value) + 1;
		memcpy(p, def->value, len);
		p += len;
	}
	*psize = size;
}
	


#define GETNAMELEN(name, len)                          \
	len = strlen(name);                            \
	if (name[0] == '{' && name[len-1] == '}') {    \
		name++;                                \
		len -= 2;                              \
	}

int
gacopyz_srv_find_macro(gacopyz_srv_t srv, const char *name, const char **pval)
{
	struct gacopyz_macro_def *def;
	size_t len;
	
	GACOPYZ_ASSERT(srv);
	
	GETNAMELEN(name, len);
	def = find_def(srv, name, len);
	if (def) {
		*pval = def->value;
		return MI_SUCCESS;
	}
	return MI_FAILURE;
}

void
gacopyz_srv_define_macro(gacopyz_srv_t srv,
			  const char *name, const char *value)
{
	size_t len;

	GACOPYZ_ASSERT(srv);

	GETNAMELEN(name, len);
	
	add_def(srv, name, len, value);
}

void
gacopyz_srv_del_macro(gacopyz_srv_t srv, const char *name)
{
	size_t len;

	GACOPYZ_ASSERT(srv);
	
	GETNAMELEN(name, len);
	del_def(srv, name, len);
}

void
gacopyz_srv_clear_macros(gacopyz_srv_t srv)
{
	size_t i;

	GACOPYZ_ASSERT(srv);

	for (i = 0; i < srv->ndefs; i++) {
		free(srv->def[i].name);
		free(srv->def[i].value);
	}
	srv->ndefs = 0;
}

void
gacopyz_srv_clear_macros_pred(gacopyz_srv_t srv,
			      int (*pred)(const char*, void*),
			      void *data)
{
	size_t i, j;
	
	GACOPYZ_ASSERT(srv);

	for (i = j = 0; i < srv->ndefs; i++) {
		if (pred(srv->def[i].name, data)) {
			free(srv->def[i].name);
			free(srv->def[i].value);
		} else
			srv->def[j++] = srv->def[i];
	}
	srv->ndefs = j;
}

void
gacopyz_srv_iterate_macros(gacopyz_srv_t srv,
			    int (*func)(const char *name, const char *value,
					 void *data),
			    void *data)
{
	size_t i;
	
	GACOPYZ_ASSERT(srv);
	
	for (i = 0; i < srv->ndefs; i++)
		if (func(srv->def[i].name, srv->def[i].value, data))
			break;
}

void
gacopyz_srv_count_macros(gacopyz_srv_t srv, size_t *count)
{
	GACOPYZ_ASSERT(srv);
	*count = srv->ndefs;
}


const char **
gacopyz_srv_get_required_macros(gacopyz_srv_t srv, enum gacopyz_stage stage)
{
	return (const char **) srv->req_macros[stage];
}


const char *
gacopyz_srv_get_id(gacopyz_srv_t srv)
{
	return srv->id;
}

const char *
gacopyz_srv_get_portspec(gacopyz_srv_t srv)
{
	return srv->portspec;
}

int
gacopyz_srv_get_flags(gacopyz_srv_t srv)
{
	return srv->flags & ~_SRV_SYS_MASK;
}

int
gacopyz_srv_flags(gacopyz_srv_t srv, int flags, enum gacopyz_flag_op op)
{
	if (flags & _SRV_SYS_MASK)
		return EINVAL;
	switch (op) {
	case gacopyz_flag_rpl:
		srv->flags = (srv->flags & _SRV_SYS_MASK) | flags;
		break;

	case gacopyz_flag_set:
		srv->flags |= flags;
		break;

	case gacopyz_flag_clr:
		srv->flags &= ~flags;
	}
	/* Whether set or cleared, the GACOPYZ_SRV_DISABLED bit
	   implies clearing _SRV_CLRDIS. */
	if (flags & GACOPYZ_SRV_DISABLED)
		flags &= ~_SRV_CLRDIS;
	return 0;
}	

int
gacopyz_srv_get_logmask(gacopyz_srv_t srv)
{
	return srv->iod.logmask;
}

int
gacopyz_srv_get_fd(gacopyz_srv_t srv)
{
	return srv->iod.sd;
}

int
gacopyz_srv_get_onerr(gacopyz_srv_t srv)
{
	return srv->onerror;
}


struct timeval default_gacopyz_timeout[GACOPYZ_TO_COUNT] = {
  { GACOPYZ_WRITE_TIMEOUT, 0 },
  { GACOPYZ_READ_TIMEOUT, 0 },
  { GACOPYZ_EOM_TIMEOUT, 0 },
  { GACOPYZ_CONNECT_TIMEOUT, 0 }
};

int
gacopyz_srv_create(gacopyz_srv_t *p, const char *name,
		   const char *portspec, unsigned logmask)
{
	gacopyz_srv_t srv = calloc(1, sizeof(*srv));
	
	if (!srv)
		return MI_FAILURE;
	srv->id = strdup(name);
	srv->portspec = strdup(portspec);
	srv->iod.sd = -1;
	srv->iod.logmask = logmask;
	memcpy(srv->iod.timeout, default_gacopyz_timeout,
	       sizeof srv->iod.timeout);

	srv->flags = 0;
	srv->onerror = SMFIR_TEMPFAIL;
	srv->version = SMFI_VERSION;
	srv->proto = SMFI_DEFAULT_PROT;
	srv->acts = SMFI_DEFAULT_ACTS;
	srv->memerror = default_memerror;
	*p = srv;
	return MI_SUCCESS;
}

static int
parse_X_spec(const char *spec, char *xspec, char **id, char **port,
	     struct timeval to[], int *onerr, int debug)
{
	char *p, *q;
	int havef = 0, havet = 0;
	
	p = strtok(xspec, ",");
	if (!p) {
		if (debug)
			gacopyz_log(SMI_LOG_DEBUG, 
				    _("%s: not a valid X spec"),
				    spec);
		return MI_FAILURE;
	}
	if (p[1] != '=') {
		*id = p;
		p = strtok(NULL, ",");
		if (!p) {
			if (debug)
				gacopyz_log(SMI_LOG_DEBUG,
					       _("%s: not a valid X spec"),
					       spec);
			return MI_FAILURE;
		}
	}

	do {
		while (*p && isspace(*p))
			p++;
		if (!*p)
			break;
		if (p[1] != '=') {
			if (debug)
				gacopyz_log(SMI_LOG_DEBUG,
					       _("%s: not a valid X spec, "
					         "missing = near %s"),
					       spec, p);
			return MI_FAILURE;
		}

		switch (p[0]) {
		case 'S':
			if (*port) {
				if (debug)
					gacopyz_log(SMI_LOG_DEBUG,
						       _("%s: not a valid X spec, "
						         "duplicate field %s"),
						       spec, p);
				return MI_FAILURE;
			}
			*port = p + 2;
			break;
			
		case 'F':
			if (havef) {
				if (debug)
					gacopyz_log(SMI_LOG_DEBUG,
						       _("%s: not a valid X spec, "
						         "duplicate field %s"),
						       spec, p);
				return MI_FAILURE;
			}
			havef = 1;
			if (strlen(p) != 3) {
				if (debug)
					gacopyz_log(SMI_LOG_DEBUG,
						       _("%s: not a valid X spec, "
						         "invalid field %s"),
						       spec, p);
				return MI_FAILURE;
			}
			switch (p[2]) {
			case 'R':
				*onerr = SMFIR_REJECT;
				break;

			case 'T':
				*onerr = SMFIR_TEMPFAIL;
				break;

			default:
				if (debug)
					gacopyz_log(SMI_LOG_DEBUG,
						       _("%s: not a valid X spec, "
						         "invalid field %s"),
						       spec, p);
				return MI_FAILURE;
			}
			break;

		case 'T':
			if (havet) {
				if (debug)
					gacopyz_log(SMI_LOG_DEBUG,
						       _("%s: not a valid X spec, "
						         "duplicate field %s"),
						       spec, p);
				return MI_FAILURE;
			}
			havet = 1;

			for (q = p + 2; *q; q++) {
				int n;
				unsigned long t;
				char *ep;
				
				switch (*q) {
				case 'C':
					n = GACOPYZ_TO_CONNECT;
					break;
					
				case 'S':
					n = GACOPYZ_TO_WRITE;
					break;
					
				case 'R':
					n = GACOPYZ_TO_READ;
					break;
					
				case 'E':
					n = GACOPYZ_TO_EOM;
					break;
					
				default:
					if (debug)
						gacopyz_log(SMI_LOG_DEBUG,
					           _("%s: not a valid X spec, "
					             "invalid T field near %s"),
							       spec, q);
					return MI_FAILURE;
				}
				if (*++q != ':') {
					if (debug)
						gacopyz_log(SMI_LOG_DEBUG,
					           _("%s: not a valid X spec, "
					             "invalid T field near %s"),
							       spec, q);
					return MI_FAILURE;
				}
				t = strtoul(++q, &ep, 0);
				switch (*ep) {
				case 0:
				case ';':
					break;
				case 's':
					ep++;
					break;
				case 'm':
					t *= 60;
					ep++;
					break;
				default:
					if (debug)
						gacopyz_log(SMI_LOG_DEBUG,
					           _("%s: not a valid X spec, "
					             "invalid T field near %s"),
							       spec, q);
					return MI_FAILURE;
				}

				to[n].tv_sec = t;
				to[n].tv_usec = 0;
				
				q = ep;

				if (*q == 0)
					break;
				else if (*q != ';') {
					if (debug)
						gacopyz_log(SMI_LOG_DEBUG,
					           _("%s: not a valid X spec, "
					             "invalid delimiter near %s"),
							       spec, q);
					return MI_FAILURE;
				}
			}
		}		
	} while (p = strtok(NULL, ","));
	return MI_SUCCESS;
}

int
gacopyz_srv_create_X(gacopyz_srv_t *p, const char *spec, unsigned logmask)
{
	int rc;
	char *xspec = strdup(spec);

	char *id = "default";
	char *port = NULL;
	struct timeval to[GACOPYZ_TO_COUNT];
	int onerr = SMFIR_CONTINUE;

	memcpy(to, default_gacopyz_timeout, sizeof to);
	if (parse_X_spec(spec, xspec, &id, &port, to, &onerr,
			logmask & SMI_LOG_MASK(SMI_LOG_DEBUG)) == MI_FAILURE) {
		free(xspec);
		return MI_FAILURE;
	}
	
	if (!port) {
		if (logmask & SMI_LOG_MASK(SMI_LOG_DEBUG)) 
			gacopyz_log(SMI_LOG_DEBUG,
				       _("%s: not a valid X spec, "
				         "missing port"),
				       spec);
		free(xspec);
		return MI_FAILURE;
	}

	rc = gacopyz_srv_create(p, id, port, logmask);
	free(xspec);
	if (rc)
		return MI_FAILURE;
	gacopyz_srv_set_all_timeouts(*p, to);
	gacopyz_srv_onerror(*p, onerr);
	return MI_SUCCESS;
}

int
gacopyz_srv_onerror(gacopyz_srv_t srv, int code)
{
	GACOPYZ_ASSERT(srv);
	
	switch (code) {
	case SMFIR_CONTINUE:
	case SMFIR_DISCARD:
	case SMFIR_REJECT:
	case SMFIR_TEMPFAIL:
	case SMFIR_SHUTDOWN:
		srv->onerror = code;
		return MI_SUCCESS;
	}
	return MI_FAILURE;
}

void
gacopyz_srv_set_logmask(gacopyz_srv_t srv, int logmask)
{
	GACOPYZ_ASSERT(srv);
	srv->iod.logmask = logmask;
}

void
gacopyz_srv_set_callback(gacopyz_srv_t srv,
			 int (*cb)(gacopyz_srv_t, int, int, void *))
{
	GACOPYZ_ASSERT(srv);
	srv->cb_reply = cb;
	return;
}

void
gacopyz_srv_set_callback_data(gacopyz_srv_t srv, void *data)
{
	GACOPYZ_ASSERT(srv);
	srv->cb_data = data;
	return;
}

void
gacopyz_srv_set_memerror(gacopyz_srv_t srv,
			 void (*memerror)(gacopyz_srv_t, const char *,
					  unsigned int))
{
	GACOPYZ_ASSERT(srv);
	srv->memerror = memerror;
}

void
gacopyz_srv_set_all_timeouts(gacopyz_srv_t srv, struct timeval *tvp)
{
	GACOPYZ_ASSERT(srv);
	memcpy(srv->iod.timeout, tvp, sizeof srv->iod.timeout);
}

int
gacopyz_srv_set_timeout(gacopyz_srv_t srv, unsigned n, struct timeval *tvp)
{
	GACOPYZ_ASSERT(srv);
	
	if (n >= GACOPYZ_TO_COUNT)
		return MI_FAILURE;
	srv->iod.timeout[n] = *tvp;
	return MI_SUCCESS;
}

int
gacopyz_srv_set_timeout_sec(gacopyz_srv_t srv, unsigned n, time_t t)
{
	struct timeval tv;
	tv.tv_usec = 0;
	tv.tv_sec = t;
	return gacopyz_srv_set_timeout(srv, n, &tv);
}

int
gacopyz_srv_set_source(gacopyz_srv_t srv, struct sockaddr *sa, socklen_t len)
{
	GACOPYZ_ASSERT(srv);
	
	srv->source_addr = malloc(len);
	if (!srv->source_addr)
		return MI_FAILURE;
	memcpy (srv->source_addr, sa, len);
	srv->source_addr_len = len;
	return MI_SUCCESS;
}

int
gacopyz_srv_set_version(gacopyz_srv_t srv, unsigned long version)
{
	GACOPYZ_ASSERT(srv);
	
	srv->version = version;
	return MI_SUCCESS;
}

int
gacopyz_srv_set_protocol(gacopyz_srv_t srv, unsigned long proto)
{
	GACOPYZ_ASSERT(srv);
	
	srv->proto = proto;
	return MI_SUCCESS;
}

int
gacopyz_srv_set_actions(gacopyz_srv_t srv, unsigned long acts)
{
	GACOPYZ_ASSERT(srv);
	
	srv->acts = acts;
	return MI_SUCCESS;
}

void
gacopyz_srv_destroy(gacopyz_srv_t *p)
{
	int i;
	
	gacopyz_srv_t srv = *p;
	gacopyz_srv_clear_macros(srv);
	free(srv->portspec);
	free(srv->id);
	free(srv->def);
	free(srv->source_addr);
	for (i = 0; i < gacopyz_stage_max; i++) 
		if (srv->req_macros[i]) {
			int j;

			for (j = 0; srv->req_macros[i][j]; j++)
				free(srv->req_macros[i][j]);
			free(srv->req_macros[i]);
		}
	free(srv->buf);
	free(srv);
	*p = 0;
}

static int
srv_connect(gacopyz_srv_t srv, char *proto, char *port, char *path)
{
	milter_sockaddr_t addr;
	int socklen;
	int fd = -1;
	struct timeval start, connect_tv;
	
	if (!proto
	    || strcmp(proto, "unix") == 0 || strcmp(proto, "local") == 0) {
		struct stat st;
		
		if (port) {
			gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
					_("invalid connection type: %s; "
					  "port is meaningless for UNIX sockets"),
				       srv->portspec);
			return -1;
		}
		
		if (strlen(path) >= sizeof addr.sunix.sun_path) {
			errno = EINVAL;
			gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
				       _("%s: UNIX socket name too long"),
				       path);
			return -1;
		}
		
		addr.sa.sa_family = PF_UNIX;
		socklen = sizeof(addr.sunix);
		strcpy(addr.sunix.sun_path, path);
		
		if (stat(path, &st)) {
			gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
				       _("%s: cannot stat socket: %s"),
				       path, strerror(errno));
			return -1;
		} else {
			/* FIXME: Check permissions? */
			if (!S_ISSOCK(st.st_mode)) {
				gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
					       _("%s: not a socket"),
						path);
				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) {
			gacopyz_io_log(&srv->iod, SMI_LOG_ERR, 
				       _("invalid connection type: %s; "
				         "missing port number"),
				       srv->portspec);
			return -1;
		}

		num = pnum = strtol(port, &p, 0);
		if (*p == 0) {
			if (num != pnum) {
				gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
					       _("invalid connection type: %s; "
					         "bad port number"),
					       srv->portspec);
				return -1;
			}
			pnum = htons(pnum);
		} else {
			struct servent *sp = getservbyname(port, "tcp");
			if (!sp) {
				gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
					       _("invalid connection type: %s; "
					         "unknown port name"),
					       srv->portspec);
				return -1;
			}
			pnum = sp->s_port;
		}
		
		if (!path)
			addr.sin.sin_addr.s_addr = INADDR_ANY;
		else {
			struct hostent *hp = gethostbyname(path);
			if (!hp) {
				gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
					       _("unknown host name %s"),
					       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:
				gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
					       _("invalid connection type: %s; "
					         "unsupported address family"),
					       srv->portspec);
				return -1;
			}
		}
	} else if (strcmp(proto, "inet6") == 0) {
		int rc;
		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_io_log(&srv->iod, SMI_LOG_ERR,
				       _("%s:%s: cannot parse address: %s"),
				       path, port, strerror(errno));
			return -1;
			
		case EAI_BADFLAGS:
		case EAI_SOCKTYPE:
			gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
				       _("%s:%d: internal error "
					 "converting %s:%s"),
				       __FILE__, __LINE__, path, port);
			return -1;
			
		case EAI_MEMORY:
			gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
				       "not enogh memory");
			return -1;
			
		default:
			gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
				       "%s:%s: %s",
				       path, port, gai_strerror(rc));
			return -1;
		}

		if (res->ai_addrlen > sizeof(addr)) {
			gacopyz_io_log(&srv->iod, 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_io_log(&srv->iod, SMI_LOG_WARN,
				       _("%s:%s resolves to several addresses; "
					 "using %s:%s"),
				       path, port, host, serv);
		}
		memcpy(&addr, res->ai_addr, res->ai_addrlen);
		socklen = sizeof(addr.sin6);
		freeaddrinfo(res);
	} else {
		gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
			       _("unsupported protocol: %s"),
			       proto);
		return -1;
	}

	fd = socket(addr.sa.sa_family, SOCK_STREAM, 0);
	if (fd == -1) {
		gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
			       _("unable to create new socket: %s"),
			       strerror(errno));
		return -1;
	}
	
	if (srv->source_addr && addr.sa.sa_family != PF_UNIX) {
		if (bind(fd, srv->source_addr, srv->source_addr_len) < 0) {
			gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
				       _("cannot bind to local address: %s"),
				       strerror(errno));
			close(fd);
			return -1;
		}
	}

	connect_tv = srv->iod.timeout[GACOPYZ_TO_CONNECT];
	gettimeofday(&start, NULL);
	for (;;) {
		int rc = connect(fd, &addr.sa, socklen);
		if (rc == 0)
			break;
		else if (errno == ECONNREFUSED) {
			struct timeval tv, now;
			fd_set rset, wset, xset;
			
			gettimeofday(&now, NULL);
			timersub(&now, &start, &tv);
			
			if (timercmp(&tv, &connect_tv, < )) {
				FD_ZERO(&rset);
				FD_SET(fd, &rset);
				FD_ZERO(&wset);
				FD_SET(fd, &wset);
				FD_ZERO(&xset);
				FD_SET(fd, &xset);
				select(fd + 1, &rset, &wset, &xset, &tv);
				continue;
			}
		}
		
		gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
			       _("cannot connect to %s: %s"),
			       srv->portspec, strerror(errno));
		close(fd);
		return -1;
	}
	
	return fd;
}

int
gacopyz_srv_open(gacopyz_srv_t srv)
{
	char *proto;
	char *port;
	char *path;
	
	GACOPYZ_ASSERT(srv);

	gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
		       _("connecting to %s"), srv->portspec);
	if (srv->flags & _SRV_CONNECTED) {
		gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
			       _("%s: already connected, disconnect first"),
			       srv->id);
		return MI_FAILURE;
	}
	GACOPYZ_ALLOC(srv,
		      gacopyz_parse_connection(srv->portspec,
					       &proto, &port, &path)==0);

	srv->iod.sd = srv_connect(srv, proto, port, path);
	free(proto);
	free(port);
	free(path);
	if (srv->iod.sd == -1)
		return MI_FAILURE;
	srv->flags |= _SRV_CONNECTED;
	return MI_SUCCESS;
}

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

static void
parse_macros(gacopyz_srv_t srv, enum gacopyz_stage stage, char *buf)
{
	char *p;
	char **argv;
	size_t count;

	count = 0;
	for (p = buf; *p;) {
		while (*p && ISWS(*p))
			p++;
		if (*p) {
			count++;
			while (*p && !ISWS(*p))
				p++;
		}
	}
	if (count == 0)
		return;
	GACOPYZ_ALLOC(srv, argv = calloc(count + 1, sizeof(argv[0])));
	count = 0;
	for (p = buf; *p;) {
		while (*p && ISWS(*p))
			p++;
		if (*p) {
			size_t i;
			char *str = p;
			
			for (i = 0; *p && !ISWS(*p); p++, i++)
				;
			if (i > 0) {
				char *newstr;

				if (*str == '{' && str[i-1] == '}') {
					str++;
					i -= 2;
				}
				GACOPYZ_ALLOC(srv, newstr = malloc(i + 1));
				memcpy(newstr, str, i);
				newstr[i] = 0;
				argv[count++] = newstr;
			}
		}
	}
	argv[count] = NULL;
	srv->req_macros[stage] = argv;
}
	
	
static void
read_macros(gacopyz_srv_t srv)
{
	char *buf = srv->buf + 3 * sizeof(uint32_t);
	size_t size = srv->bufsize - 3 * sizeof(uint32_t);
		
	while (size > sizeof(uint32_t)) {
		uint32_t v;
		unsigned n;
		size_t len;
			
		memcpy(&v, buf, sizeof(uint32_t));
		n = ntohl(v);
		if (n >= gacopyz_stage_max) {
			gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
				       _("received invalid stage number"));
			break;
		}
		buf += sizeof(uint32_t);
		size -= sizeof(uint32_t);
		if (size == 0) 
			break;
		len = strlen(buf) + 1;
		if (len > size) {
			gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
				       _("invalid macro list"));
			break;
		}
		gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
			       _("macros for stage \"%s\" (%d): %s"),
			       gacopyz_stage_name[n], n, buf);
		parse_macros(srv, n, buf);
		buf += len;
		size -= len;
	}
}

int
gacopyz_srv_negotiate(gacopyz_srv_t srv)
{
	uint32_t ibuf[3];
	size_t size;
	unsigned char cmd;
	unsigned long milter_version, milter_acts, milter_proto;
	
	GACOPYZ_ASSERT(srv);

	if (!(srv->flags & _SRV_CONNECTED)) {
		gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
			       _("%s: server not connected, cannot negotiate"),
			       srv->id);
		return MI_FAILURE;
	}
	if (srv->flags & _SRV_READY) {
		gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
			       _("%s: server already passed negotiation"),
			       srv->id);
		return MI_FAILURE;
	}
		
	ibuf[0] = htonl(srv->version);
	ibuf[1] = htonl(srv->acts);
	ibuf[2] = htonl(srv->proto);
	if (gacopyz_send_command(&srv->iod, SMFIC_OPTNEG, ibuf, sizeof ibuf)) 
		return MI_FAILURE;

	if (gacopyz_read_command(&srv->iod, GACOPYZ_TO_READ, &cmd, &size,
				 &srv->buf, &srv->bufsize)) {
		gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
			       _("error reading reply to SMFIC_OPTNEG: %s"),
			       strerror(errno));
		return MI_FAILURE;
	}
	if (cmd != SMFIC_OPTNEG) {
		gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
			       _("gacopyz_srv_negotiate: got %c instead of %c"),
			       cmd, SMFIC_OPTNEG);
		return MI_FAILURE;
	}

	if (size < sizeof ibuf) {
		gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
			       _("gacopyz_srv_negotiate: not enough data returned"));
		return MI_FAILURE;
	}

	milter_version = ntohl(((uint32_t*)srv->buf)[0]);
	milter_acts    = ntohl(((uint32_t*)srv->buf)[1]);
	milter_proto   = ntohl(((uint32_t*)srv->buf)[2]);

	/* Check version */
	if (milter_version == 1 || milter_version > SMFI_VERSION
	    || milter_version > srv->version) {
		gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
			       _("client requested unsupported milter "
				 "version: %#lx"), milter_version);
		return MI_FAILURE;
	}

	/* Check action capabilities */
	if ((milter_acts & srv->acts) != milter_acts) {
		gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
			       _("filter abilities %#lx do not match MTA "
			         "milter abilities %#lx"),
			       milter_acts, srv->acts);
		return MI_FAILURE;
	}

	/* Check protocol */
	if ((milter_proto & srv->proto) != milter_proto) {
		gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
			       _("protocol abilities %#lx do not match MTA "
			         "milter abilities %#lx"),
			       milter_proto, srv->proto);
		return MI_FAILURE;
	}

	if (milter_version > 3
	    && srv->bufsize > 3 * sizeof(uint32_t))
		read_macros(srv);
	
	srv->version  = milter_version;
	srv->acts     = milter_acts;
	srv->proto    = milter_proto;

	srv->flags |= _SRV_READY;
	return MI_SUCCESS;
}

static struct command_assoc {
	unsigned char cmd;
	int pflag;
	const char *action;
	const char *code;
} command_tab[] = {	
	{ SMFIC_CONNECT, SMFIP_NOCONNECT,  "connect",  "554" },
	{ SMFIC_HELO,    SMFIP_NOHELO,     "helo",     "550" },
	{ SMFIC_MAIL,    SMFIP_NOMAIL,     "mail",     "550 5.7.1" },
	{ SMFIC_RCPT,    SMFIP_NORCPT,     "rcpt",     "550 5.7.1" },
	{ SMFIC_HEADER,  SMFIP_NOHDRS,     "header",   "550 5.7.1" },
	{ SMFIC_BODY,    SMFIP_NOBODY,     "body",     "554 5.7.1" },
	{ SMFIC_EOH,     SMFIP_NOEOH,      "eoh",      "550 5.7.1" },
	{ SMFIC_UNKNOWN, SMFIP_NOUNKNOWN,  "unknown",  "550 5.7.1" },
	{ SMFIC_DATA,    SMFIP_NODATA,     "data",     "550 5.7.1" },
	{ 0,             0,                "default",  "550 5.7.1" }
};

static struct command_assoc *
find_cmd_assoc(unsigned char cmd)
{
	struct command_assoc *cp;
	for (cp = command_tab; cp->cmd; cp++)
		if (cp->cmd == cmd)
			break;
	return cp;
}

#define is_rfc822_reject_code(s) \
   (((s)[0] == '4' || (s)[0] == '5') \
    && isascii((s)[1]) && isdigit((s)[1]) \
    && isascii((s)[2]) && isdigit((s)[2]))

#define DEFREPLY "Command rejected"

static void
verify_reply(gacopyz_srv_t srv, size_t size, const char *reject_code)
{
	if (size < 3 || !is_rfc822_reject_code(srv->buf)) {
		srv_ensure_space(srv,
				 strlen(reject_code) + 1
				 + sizeof(DEFREPLY));

		strcpy(srv->buf, reject_code);
		strcat(srv->buf, " ");
		strcat(srv->buf, DEFREPLY);
	}
}

int
gacopyz_srv_send_macros(gacopyz_srv_t srv, unsigned char cmd)
{
	size_t size;

	switch (cmd) {
	case SMFIC_CONNECT:
	case SMFIC_HELO:
	case SMFIC_MAIL:
	case SMFIC_RCPT:
	case SMFIC_DATA:
	case SMFIC_BODYEOB:
	case SMFIC_EOH:
		srv_format_macros(srv, cmd, &size);
		if (gacopyz_send_command(&srv->iod, SMFIC_MACRO,
					 srv->buf, size))
			return MI_FAILURE;
	}
	return MI_SUCCESS;
}

int
gacopyz_srv_send_command(gacopyz_srv_t srv, unsigned char cmd,
			 const void *data, size_t datasize)
{
	unsigned char rcmd;
	size_t size;
	struct command_assoc *cp;
	int again;
	int t;
	
	gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
		       _("gacopyz_srv_send_command: size=%lu, cmd=%c"),   
		       datasize, cmd);

	cp = find_cmd_assoc(cmd);

	srv->resp_ptr = NULL;
	srv->resp_size = 0;
	if (cp->pflag && (srv->proto & cp->pflag))
		return SMFIR_CONTINUE;
	
	if (gacopyz_srv_send_macros(srv, cmd))
		return srv->onerror;
	
	if (gacopyz_send_command(&srv->iod, cmd, data, datasize))
		return srv->onerror;

	if (cmd == SMFIC_HEADER && (srv->proto & SMFIP_NOHREPL)) 
		return SMFIR_CONTINUE;

	if (cmd == SMFIC_BODYEOB)
		t = GACOPYZ_TO_EOM;
	else
		t = GACOPYZ_TO_READ;
	do {
		again = 0;
		if (gacopyz_read_command(&srv->iod, t, &rcmd, &size,
					 &srv->buf, &srv->bufsize)) {
			gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
				       _("error reading reply to '%c': %s"),
				       cmd, strerror(errno));
			return srv->onerror;
		}
		
		switch (rcmd) {
		case SMFIR_REPLYCODE:
			verify_reply(srv, size, cp->code);
			gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
				       "action=%s, reject=%s",
				       cp->action, srv->buf);
			srv->resp_ptr = srv->buf;
			srv->resp_size = size;
			switch (cmd) {
			case SMFIC_BODY:
			case SMFIC_HEADER:
			case SMFIC_EOH:
				srv->flags |= GACOPYZ_SRV_DISABLED
					       | _SRV_CLRDIS;
			}
			break;

		case SMFIR_REJECT:
			gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
				       "action=%s, reject",
				       cp->action);
			break;
			
		case SMFIR_DISCARD:
			gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
				       "action=%s, discard",
				       cp->action);
			srv->flags |= GACOPYZ_SRV_DISABLED | _SRV_CLRDIS;
			break;
			
		case SMFIR_TEMPFAIL:
			gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
				       "action=%s, tempfail",
				       cp->action);
			break;

		case SMFIR_ACCEPT:
			gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
				       "action=%s, accept",
				       cp->action);
			srv->flags |= GACOPYZ_SRV_DISABLED | _SRV_CLRDIS;
			break;
			
		case SMFIR_CONTINUE:
			gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
				       "action=%s, continue",
				       cp->action);
			break;

		case SMFIR_ADDRCPT:
		case SMFIR_DELRCPT:
		case SMFIR_REPLBODY:
		case SMFIR_CHGHEADER:
		case SMFIR_ADDHEADER:
		case SMFIR_INSHEADER:
			again = 1;
			srv->resp_ptr = srv->buf;
			srv->resp_size = size;
			gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
				       "action=%s, response=%c",
				       cp->action, rcmd);
			break;

		case SMFIR_PROGRESS:
			if (cmd == SMFIC_BODYEOB) {
				gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
					       "action=%s, progress",
					       cp->action);
				again = 1;
				break;
			}
			/* fall through */
			
		default:
			gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
				       "action=%s, unhandled response=%c",
				       cp->action, rcmd);
			/*FIXME: Change to error state? */
			rcmd = srv->onerror;
			break;
		}

		if (srv->cb_reply)
			srv->cb_reply(srv, cmd, rcmd, srv->cb_data);
	} while (again);

	return rcmd;
}

int
gacopyz_srv_reply(gacopyz_srv_t srv, char **pmsg, size_t *psize)
{
	size_t i, j;
	char *buf;

	if (!srv->resp_ptr) {
		errno = ENOMSG;
		return MI_FAILURE;
	}
	buf = malloc(srv->resp_size + 1);
	if (!buf)
		return MI_FAILURE;
	for (i = j = 0; i < srv->resp_size; i++, j++) {
		if (srv->resp_ptr[i] == '%' && srv->resp_ptr[i+1] == '%')
			i++;
		buf[j] = srv->resp_ptr[i];
	}
	buf[j] = 0;
	
	*pmsg = buf;
	if (psize)
		*psize = j;
	return 0;
}

void
gacopyz_srv_reply_raw(gacopyz_srv_t srv, char **msg, size_t *size)
{
	*msg = srv->resp_ptr;
	*size = srv->resp_size;
}

int
gacopyz_srv_abort(gacopyz_srv_t srv)
{
	GACOPYZ_ASSERT(srv);
	gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG, "gacopyz_srv_abort");
	if (srv->flags & _SRV_CLRDIS)
		gacopyz_srv_flags(srv, GACOPYZ_SRV_DISABLED, gacopyz_flag_clr);
	return gacopyz_send_command(&srv->iod, SMFIC_ABORT, NULL, 0);
}

int
gacopyz_srv_quit(gacopyz_srv_t srv)
{
	GACOPYZ_ASSERT(srv);
	gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG, "gacopyz_srv_quit");
	srv->flags = _SRV_CONNECTED;
	return gacopyz_send_command(&srv->iod, SMFIC_QUIT, NULL, 0);
}

#define _SRV_ASSERT_FLAG(srv, mask, fun)				\
	do {								\
		if (!((srv)->flags & mask)) {				\
			gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,	\
				       _("%s: %s called with flags=%x"), \
				       (srv)->id, mask);		\
			return srv->onerror;				\
		}							\
	} while (0)

static void
format_argv(gacopyz_srv_t srv, char **argv, char **buf, size_t *psize)
{
	size_t len;
	char **ap;
	char *p;
	
	for (len = 0, ap = argv; *ap; ap++)
		len += strlen(*ap) + 1;
	
	GACOPYZ_ALLOC(srv, *buf = malloc(len));
		
	for (p = *buf, ap = argv; *ap; ap++) {
		size_t len = strlen(*ap) + 1;
		memcpy(p, *ap, len);
		p += len;
	}
	*psize = len;
}

int
gacopyz_srv_connect(gacopyz_srv_t srv, const char *hostname,
		    struct sockaddr *sa)
{
	int rc;
	int family = SMFIA_UNKNOWN;
	char *arg;
	size_t argsize, len;
	size_t size;
	char *s = "";
	char *sbuf = NULL;
	char *buf;
	unsigned short port = 0;
	
	GACOPYZ_ASSERT(srv);
	_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_connect");
	if (srv->flags & GACOPYZ_SRV_DISABLED)
		return SMFIR_CONTINUE;

	if (sa) {
		switch (sa->sa_family) {
		case AF_UNIX: {
			struct sockaddr_un *sptr = (struct sockaddr_un *) sa;
			family = SMFIA_UNIX;
			port = 0;
			len = strlen(sptr->sun_path);
			s = sptr->sun_path;
			break;
		}
		
		case AF_INET: {
			struct sockaddr_in *sptr = (struct sockaddr_in *) sa;
			
			family = SMFIA_INET;
			port = sptr->sin_port;
			s = inet_ntoa(sptr->sin_addr);
			if (!s) {
				gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
					       "inet_ntoa: %s",
					       strerror(errno));
				return MI_FAILURE;
			}
			break;
		}
		
		case AF_INET6: {
			char host[NI_MAXHOST];
			char service[NI_MAXSERV];
			int rc;
			
			rc = getnameinfo(sa, sizeof(struct sockaddr_in6),
					 host, sizeof(host),
					 service, sizeof(service),
					 NI_NUMERICHOST|NI_NUMERICSERV);
			if (rc) {
				gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
					       "getnameinfo: %s",
					       gai_strerror(rc));
				return MI_FAILURE;
			}
		
			family = SMFIA_INET6;
			port = htons(atoi(service));
			GACOPYZ_ALLOC(srv,
				      sbuf = malloc(GACOPYZ_IPV6PREFIX_LEN +
						    strlen(host) + 1));
			s = sbuf;
			strcat(strcpy(s, GACOPYZ_IPV6PREFIX_STR), host);
			break;
		}
		
		default:
			break;
		}
	}
	
	len = strlen(s);
	argsize = 2 + sizeof(port) + len;
	
	GACOPYZ_ALLOC(srv, arg = malloc(argsize));
	arg[0] = family;
	memcpy(arg + 1, &port, sizeof(port));
	memcpy(arg + 1 + sizeof(port), s, len + 1);
	free(sbuf);

	len = strlen(hostname);
	size = 1 + len + 1 + argsize;
	GACOPYZ_ALLOC(srv, buf = malloc(size));

	memcpy(buf, hostname, len + 1); 
	memcpy(buf + len + 1, arg, argsize);
	buf[len + argsize + 1] = 0;
	free(arg);
	
	rc = gacopyz_srv_send_command(srv, SMFIC_CONNECT, buf, size);
	free(buf);
	
	return rc;
}     

int
gacopyz_srv_helo(gacopyz_srv_t srv, const char *domain)
{
	GACOPYZ_ASSERT(srv);
	_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_helo");
	if (srv->flags & GACOPYZ_SRV_DISABLED)
		return SMFIR_CONTINUE;
	return gacopyz_srv_send_command(srv, SMFIC_HELO,
					domain, strlen(domain) + 1);
}     

int
gacopyz_srv_envfrom(gacopyz_srv_t srv, char **argv)
{
	int rc;
	size_t size;
	char *buf;

	GACOPYZ_ASSERT(srv);
	_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_envfrom");
	if (srv->flags & GACOPYZ_SRV_DISABLED)
		return SMFIR_CONTINUE;

	format_argv(srv, argv, &buf, &size);
	rc = gacopyz_srv_send_command(srv, SMFIC_MAIL, buf, size);
	free(buf);

	return rc;
}     

int
gacopyz_srv_envrcpt(gacopyz_srv_t srv, char **argv)
{
	int rc;
	size_t size;
	char *buf;

	GACOPYZ_ASSERT(srv);
	_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_envrcpt");
	if (srv->flags & GACOPYZ_SRV_DISABLED)
		return SMFIR_CONTINUE;

	format_argv(srv, argv, &buf, &size);
	rc = gacopyz_srv_send_command(srv, SMFIC_RCPT, buf, size);
	free(buf);
	
	return rc;
}     

int
gacopyz_srv_data(gacopyz_srv_t srv)
{
	GACOPYZ_ASSERT(srv);
	_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_data");
	if (srv->flags & GACOPYZ_SRV_DISABLED)
		return SMFIR_CONTINUE;
	return gacopyz_srv_send_command(srv, SMFIC_DATA, NULL, 0);
}

int
gacopyz_srv_header(gacopyz_srv_t srv, char *name, char *value)
{
	int rc;
	size_t nlen, vlen, size;
	char *buf;

	GACOPYZ_ASSERT(srv && name && value);
	_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_header");
	if (srv->flags & GACOPYZ_SRV_DISABLED)
		return SMFIR_CONTINUE;
		
	nlen = strlen(name);
	vlen = strlen(value);
	size = nlen + vlen + 2;
	GACOPYZ_ALLOC(srv, buf = malloc(size));
	memcpy(buf, name, nlen + 1);
	memcpy(buf + nlen + 1, value, vlen + 1);
	rc = gacopyz_srv_send_command(srv, SMFIC_HEADER, buf, size);
	free(buf);
	
	return rc;
}

int
gacopyz_srv_eoh(gacopyz_srv_t srv)
{
	GACOPYZ_ASSERT(srv);
	_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_eoh");
	if (srv->flags & GACOPYZ_SRV_DISABLED)
		return SMFIR_CONTINUE;
	return gacopyz_srv_send_command(srv, SMFIC_EOH, NULL, 0);
}

int
gacopyz_srv_body(gacopyz_srv_t srv, unsigned char *str, size_t size)
{
	GACOPYZ_ASSERT(srv);
	_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_body");
	if (srv->flags & GACOPYZ_SRV_DISABLED)
		return SMFIR_CONTINUE;
	return gacopyz_srv_send_command(srv, SMFIC_BODY, str, size);
}
     
int
gacopyz_srv_eom(gacopyz_srv_t srv, unsigned char *chunk, size_t size)
{
	int disabled;
	
	GACOPYZ_ASSERT(srv);
	_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_eom");
	disabled = srv->flags & GACOPYZ_SRV_DISABLED;
	if (srv->flags & _SRV_CLRDIS)
		gacopyz_srv_flags(srv, GACOPYZ_SRV_DISABLED, gacopyz_flag_clr);
	if (disabled)
		return SMFIR_CONTINUE;
	return gacopyz_srv_send_command(srv, SMFIC_BODYEOB, chunk, size);
}

int
gacopyz_srv_close(gacopyz_srv_t srv)
{
	GACOPYZ_ASSERT(srv);
	_SRV_ASSERT_FLAG(srv, _SRV_CONNECTED, "gacopyz_srv_close");
	close(srv->iod.sd);
	srv->flags = 0;
	return MI_SUCCESS;
}


