/* @(#)src/transports/tcpsmtp.c	1.19 9/16/92 09:10:41 */

/*
 *    Copyright (C) 1987, 1988 Ronald S. Karr and Landon Curt Noll
 *    Copyright (C) 1992  Ronald S. Karr
 * 
 * See the file COPYING, distributed with smail, for restriction
 * and warranty information.
 */

/*
 * tcpsmtp.c:
 *	Send mail using the SMTP protocol.
 */

#define NEED_SOCKETS
#include <stdio.h>
#include <signal.h>
#include <ctype.h>
#include <sys/types.h>
#include "defs.h"
#include "../smail.h"
#include "../dys.h"
#include "../addr.h"
#include "../bindsmtpth.h"
#include "../transport.h"
#include "../route.h"
#include "../spool.h"
#include "../exitcodes.h"
#include "../smailconf.h"
#include "../parse.h"
#include "../lookup.h"
#include "tcpsmtp.h"
#include "smtplib.h"
#ifndef DEPEND
# include "../extern.h"
# include "../error.h"
# include "../debug.h"
#endif

/* functions local to this file */
#ifdef HAVE_BIND
static int tcpsmtp_bind();
#endif
static int tcpsmtp_internal();
static char *set_short_timeout();
static char *set_long_timeout();
static char *set_timeout();
static int tcpsmtp_connect();
static struct error *connect_failure();
static int ip_address();

/*
 * tpd_tcpsmtp - transport to remote machine using SMTP on top of TCP/IP.
 */
void
tpd_tcpsmtp(addr, succeed, defer, fail)
    struct addr *addr;			/* recipient addresses for transport */
    struct addr **succeed;		/* successful deliveries */
    struct addr **defer;		/* defer until a later queue run */
    struct addr **fail;			/* failed deliveries */
{
#ifdef HAVE_BIND
    struct transport_hints *hints;
    struct transport_hints *mx_hints;
    struct ipaddr_hint *ip_addr;
#endif
    char *service;
    char *error_text;
    struct error *error;		/* error structure */
    struct tcpsmtp_private *priv;
    time_t started;
    int defer_failure, ret;

    priv = (struct tcpsmtp_private *)addr->transport->private;
    service = expand_string(priv->service, addr, (char *)0, (char *)0);
    if (service == NULL) {
	error_text = xprintf("failed to expand service, %s", priv->service);
	insert_addr_list(addr, defer,connect_failure(addr->transport, error_text));
	return;
    }

#ifdef HAVE_BIND

    for (hints = addr->tphint_list;
	 hints && !EQ(hints->hint_name,"mx");
	 hints = hints->succ)
	    ;

    if (!hints && (addr->transport->flags & TCPSMTP_USE_BIND)) {
	if (tcpsmtp_bind(addr, defer, fail) != SUCCEED)
	    return;

	for (hints = addr->tphint_list;
	     hints && !EQ(hints->hint_name,"mx");
	     hints = hints->succ)
		;
    }

    /* assume the worst */
    defer_failure = FALSE;

    /* but we don't know why the worst, yet */
    error = NULL;

    if (hints) {
	for (mx_hints = hints; mx_hints; mx_hints = mx_hints->succ) {
	    if (!EQ(mx_hints->hint_name,"mx")) {
		continue;
	    }

#define mx_hint ((struct mx_transport_hint *)(mx_hints->private))
	    for (ip_addr = mx_hint->ipaddrs;
		 ip_addr;
		 ip_addr = ip_addr->succ)
	    {
		if (retry_host_lock(addr->transport, ip_addr->hostname,
				    &defer_failure, &error) == SUCCEED)
		{
		    time(&started);
		    error = NULL;
		    ret = tcpsmtp_internal(addr, succeed, defer, fail,
					   ip_addr->hostname, &ip_addr->addr,
					   AF_INET, service, &error);
		    retry_host_unlock(started, error);
		    if (ret != SMTP_AGAIN) {
			return;
		    }
		}
	    }
#undef mx_hint
	}
    }
    else

#endif /* HAVE_BIND */

    {
	struct in_addr ipaddr;
	short family;

	if (ip_address(addr->next_host,&ipaddr,&family,&error_text) < 0) {
	    error_text = xprintf("IP address for %s not found",
				 addr->next_host);
	    insert_addr_list(addr, defer,
			     connect_failure(addr->transport, error_text));
	    return;
	}

	error = NULL;
	if (retry_host_lock(addr->transport, addr->next_host,
			    &defer_failure, &error) == SUCCEED)
	{
	    time(&started);
	    ret = tcpsmtp_internal(addr, succeed, defer, fail,
				   addr->next_host, &ipaddr,
				   family, service, &error);
	    retry_host_unlock(started, error);
	    if (ret != SMTP_AGAIN) {
		return;
	    }
	}
    }

    if (!defer_failure) {
	if (!error) {
	    /* should never happen */
	    error = connect_failure(ERR_CONFERR|ERR_174, "missing error");
	}
	error->info |= ERR_NSOWNER;
    }
    insert_addr_list(addr, defer_failure ? defer : fail, error);
}

#ifdef HAVE_BIND

static int
tcpsmtp_bind(addr, defer, fail)
    struct addr *addr;
    struct addr **defer;
    struct addr **fail;
{
    struct tcpsmtp_private *priv;
    struct rt_info rt_info;
    struct error *error;
    struct addr **notnow;
    struct transport_hints **h;
    char *what;
    int result;

    priv = (struct tcpsmtp_private *)addr->transport->private;

    rt_info.next_host = NULL;
    rt_info.route = NULL;
    rt_info.transport = NULL;
    rt_info.tphint_list = NULL;

    what = xprintf("transport %s", addr->transport->name);
    result = bind_addr(addr->next_host, addr->transport->flags,
		       &priv->bindlib_attr, what, &rt_info, &error);
    xfree(what);
    if (rt_info.next_host) {
	xfree(rt_info.next_host);
    }
    if (rt_info.route) {
	xfree(rt_info.route);
    }

    notnow = NULL;
    switch (result)
    {
      case DB_SUCCEED:
	/* Found a successful match. */
	/* Append hints to address's list. */
	for (h = &addr->tphint_list; *h; h = &(*h)->succ)
	    continue;
	*h = rt_info.tphint_list;
	break;

      case DB_NOMATCH:
	/* No match was found. */
	break;

      case DB_FAIL:
	/* The address should be failed, with an error of some kind. */
	notnow = fail;
	break;

      case DB_AGAIN:
	/* Routing for this address should be reattempted later. */
	notnow = defer;

      case FILE_NOMATCH:
	/* The file was not found, don't match any addresses. */
	break;

      case FILE_FAIL:
	/* Permanent router error, this is a configuration error. */
	error->info |= ERR_CONFERR;
	notnow = fail;
	break;

      case FILE_AGAIN:
	/* Temporary router database error, retry all addresses. */
	notnow = defer;
	break;
    }

    if (notnow) {
	insert_addr_list(addr, notnow, error);
	return FAIL;
    }

    return SUCCEED;
}

#endif	/* HAVE_BIND */

static int
tcpsmtp_internal(addr,succeed,defer,fail,hostname,ipaddr,family,service,ep)
    struct addr *addr;			/* recipient addresses for transport */
    struct addr **succeed;		/* successful deliveries */
    struct addr **defer;		/* defer until a later queue run */
    struct addr **fail;			/* failed deliveries */
    char *hostname;		/* name of the host to connect to */
    struct in_addr *ipaddr;	/* IP address of the host to connect to */
    short family;		/* address family */
    char *service;		/* service to use */
    struct error **ep;		/* error structure */
{
    struct transport *tp = addr->transport;
    struct tcpsmtp_private *priv;
    int s;				/* socket */
    int s2;				/* dup of s */
    struct smtp smtpbuf;		/* SMTP description buffer */
    char *error_text;
    int success;
    struct addr *ap;

    priv = (struct tcpsmtp_private *)tp->private;

    DEBUG4(DBG_DRIVER_LO, "transport %s: connect to host %s [%s]/%s...",
	   addr->transport->name, hostname, inet_ntoa(*ipaddr), service);

    /*
     * adjust the next_host for the address, so that log entries will
     * reflect the last MX host to be tried.
     */

    for (ap = addr; ap; ap = ap->succ) {
	if (ap->next_host == NULL || !EQIC(ap->next_host, hostname)) {
	    if (ap->next_host) {
		xfree(ap->next_host);
	    }
	    ap->next_host = COPY_STRING(hostname);
	}
    }

    /* reach out and touch someone */

    s = tcpsmtp_connect(hostname, ipaddr, family, service, &error_text);
    if (s >= 0) {
	s2 = dup(s);
	if (s2 < 0) {
	    (void) close(s);
	    s = -1;
	}
    }
    if (s < 0) {
	*ep = connect_failure(tp, error_text);
	return SMTP_AGAIN;
    }

    smtpbuf.in = fdopen(s, "r");
    smtpbuf.out = fdopen(s2, "w");
    smtpbuf.short_timeout = priv->short_timeout;
    smtpbuf.long_timeout = priv->long_timeout;
    smtpbuf.nl = "\r\n";
    tp->flags |= PUT_CRLF;
    smtpbuf.tp = tp;

    DEBUG(DBG_DRIVER_LO, "connected\n");

    switch (smtp_startup(&smtpbuf, ep)) {

    case SMTP_FAIL:
	insert_addr_list(addr, fail, *ep);
	(void) fclose(smtpbuf.in);
	(void) fclose(smtpbuf.out);
	return SMTP_FAIL;

    case SMTP_AGAIN:
	(void) fclose(smtpbuf.in);
	(void) fclose(smtpbuf.out);
	return SMTP_AGAIN;
    }

    if (dont_deliver) {
	insert_addr_list(addr, succeed, (struct error *)NULL);
	smtp_shutdown(&smtpbuf);
    } else {
	success = smtp_send(&smtpbuf, addr, succeed, defer, fail);
	if (success == SUCCEED) {
	    smtp_shutdown(&smtpbuf);
	}
    }

    /* all done */
    (void) fclose(smtpbuf.in);
    (void) fclose(smtpbuf.out);
    return SUCCEED;
}

/*
 * tpd_smtp - obsolescent name for tpd_tcpsmtp driver
 *
 * This routine exists for backward compatibility with Smail alpha
 * releases prior to version 3.1.12.
 */
void
tpd_smtp(addr, succeed, defer, fail)
    struct addr *addr;			/* recipient addresses for transport */
    struct addr **succeed;		/* successful deliveries */
    struct addr **defer;		/* defer until a later queue run */
    struct addr **fail;			/* failed deliveries */
{
    tpd_tcpsmtp(addr, succeed, defer, fail);
}


/*
 * tpb_tcpsmtp - read the configuration file attributes
 */
char *
tpb_tcpsmtp(tp, attrs)
    struct transport *tp;		/* transport entry being defined */
    struct attribute *attrs;		/* list of per-driver attributes */
{
    char *error;
    static struct attr_table tcpsmtp_attributes[] = {
	{ "short_timeout", t_proc, NULL, (tup *)set_short_timeout, 0 },
	{ "long_timeout", t_proc, NULL, (tup *)set_long_timeout, 0 },
	{ "service", t_string, NULL, NULL, OFFSET(tcpsmtp_private, service) },
#ifdef HAVE_BIND
	{ "use_bind", t_boolean, NULL, NULL, TCPSMTP_USE_BIND },
	BIND_ATTRIBUTES(tcpsmtp_private, bindlib_attr),
#endif
    };
    static struct attr_table *end_tcpsmtp_attributes =
	ENDTABLE(tcpsmtp_attributes);
    static struct tcpsmtp_private tcpsmtp_template = {
	5 * 60,				/* short timeout, 5 minutes */
	2 * 60 * 60,			/* long timeout, 2 hours */
	"smtp",				/* use the "smtp" service */
#ifdef HAVE_BIND
	BIND_TEMPLATE_ATTRIBUTES,
#endif
    };
    struct tcpsmtp_private *priv;	/* new tcpsmtp_private structure */

    /* copy the template private data */
    priv = (struct tcpsmtp_private *)xmalloc(sizeof(*priv));
    (void) memcpy((char *)priv, (char *)&tcpsmtp_template, sizeof(*priv));

    tp->private = (char *)priv;
    /* fill in the attributes of the private data */
    error = fill_attributes((char *)priv,
			    attrs,
			    &tp->flags,
			    tcpsmtp_attributes,
			    end_tcpsmtp_attributes);

    if (error) {
	return error;
    }
    return NULL;
}

/*
 * tpb_smtp - obsolescent name for tcpsmtp driver
 *
 * This routine exists for backward compatibility with Smail alpha
 * releases prior to version 3.1.12.
 */
char *
tpb_smtp(tp, attrs)
    struct transport *tp;		/* transport entry being defined */
    struct attribute *attrs;		/* list of per-driver attributes */
{
    return tpb_tcpsmtp(tp, attrs);
}

static char *
set_short_timeout(struct_p, attr)
    char *struct_p;			/* passed private structure */
    struct attribute *attr;		/* parsed attribute */
{
    struct tcpsmtp_private *priv = (struct tcpsmtp_private *)struct_p;

    return set_timeout(&priv->short_timeout, attr);
}

static char *
set_long_timeout(struct_p, attr)
    char *struct_p;			/* passed private structure */
    struct attribute *attr;		/* parsed attribute */
{
    struct tcpsmtp_private *priv = (struct tcpsmtp_private *)struct_p;

    return set_timeout(&priv->long_timeout, attr);
}

static char *
set_timeout(timeout, attr)
    unsigned *timeout;			/* set this timeout variable */
    struct attribute *attr;		/* parsed attribute */
{
    long l;

    if (attr->value == on) {
	return xprintf("%s: boolean form for non-boolean attribute",
		       attr->name);
    }
    l = ivaltol(attr->value);
    if (l < 0) {
	return xprintf("%s: %s: malformed interval",
		       attr->name, attr->value);
    }
    *timeout = (unsigned)l;
    if (*timeout != l) {
	return xprintf("%s: %s: interval too large", attr->name, attr->value);
    }
    return NULL;
}


/*
 * tcpsmtp_connect - return a socket connected to the remote host
 *
 * if the remote host is of the form [192.2.12.3] then use an explicit
 * inet address.
 */
static int
tcpsmtp_connect(remote_host, ip_addr, family, service, error)
    char *remote_host;
    struct in_addr *ip_addr;
    short family;
    char *service;
    char **error;
{
    static int port = 0;		/* port to connect to */
    struct servent *smtp_service;	/* service entry */
    struct sockaddr_in sin;		/* inet socket address */
    static char *save_error = NULL;	/* keep handle to free error msgs */
    int s;				/* socket */
    char *error_text;

    if (isdigit(*service)) {
	error_text = NULL;
	port = c_atol(service, &error_text);
	if (error_text) {
	    *error = xprintf("invalid port: %s", service, error_text);
	    return -1;
	}
    } else if (port == NULL) {
	smtp_service = getservbyname(service, "tcp");
	if (smtp_service == NULL) {
	    *error = xprintf("service name %s not found", service);
	    return -1;
	}
	port = smtp_service->s_port;
    }

    bzero((char *)&sin, sizeof(sin));
    sin.sin_addr = *ip_addr;
    sin.sin_family = family;
    sin.sin_port = port;
    s = socket(AF_INET, SOCK_STREAM, 0);
    if (s < 0) {
	if (save_error) {
	    xfree(save_error);
	}
	*error = save_error = xprintf("socket: %s", strerrno());
	return -1;
    }

    if (connect(s, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
	if (save_error) {
	    xfree(save_error);
	}
	*error = save_error = xprintf("connect: %s", strerrno());
	(void) close(s);
	return -1;
    }

    return s;
}

static struct error *
connect_failure(tp, connect_error_text)
    struct transport *tp;
    char *connect_error_text;
{
    char *error_text;

    /*
     * ERR_148 - smtp connection failure
     *
     * DESCRIPTION
     *      We failed to connect to the smtp service of the remote
     *      host.  The reason is stored in `error'.
     *
     * ACTIONS
     *      The input addresses are deferred.
     *
     * RESOLUTION
     *      Hopefully we will connect on a retry.
     */
    error_text = xprintf("transport %s: %s", tp->name, connect_error_text);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_148, error_text);
}


static int
ip_address(remote_host,ipaddr,family,error)
    char *remote_host;
    struct in_addr *ipaddr;
    short *family;
    char **error;
{
    if (remote_host[0] == '[') {
	/* INET addr literal address */
	char *p = index(remote_host, ']');
	unsigned long inet_number;

	if (p == NULL || p[1] != '\0') {
	    *error = "Invalid host address";
	    return -1;
	} else {
	    *p = '\0';
	    inet_number = inet_addr(remote_host + 1);
	    *p = ']';
	}
	ipaddr->s_addr = inet_number;
	*family = AF_INET;
    } else {
	struct hostent *hostentp;		/* addr for remote host */

	hostentp = gethostbyname(remote_host);
	if (hostentp == NULL) {
	    *error = "Unknown hostname";
	    return -1;
	}
	bcopy(hostentp->h_addr, (char *)ipaddr, hostentp->h_length);
	*family = hostentp->h_addrtype;
    }
    return 0;
}
