diff options
Diffstat (limited to 'src/smtp/smtp_connect.c')
-rw-r--r-- | src/smtp/smtp_connect.c | 1231 |
1 files changed, 1231 insertions, 0 deletions
diff --git a/src/smtp/smtp_connect.c b/src/smtp/smtp_connect.c new file mode 100644 index 0000000..ed58180 --- /dev/null +++ b/src/smtp/smtp_connect.c @@ -0,0 +1,1231 @@ +/*++ +/* NAME +/* smtp_connect 3 +/* SUMMARY +/* connect to SMTP/LMTP server and deliver +/* SYNOPSIS +/* #include "smtp.h" +/* +/* int smtp_connect(state) +/* SMTP_STATE *state; +/* DESCRIPTION +/* This module implements SMTP/LMTP connection management and controls +/* mail delivery. +/* +/* smtp_connect() attempts to establish an SMTP/LMTP session with a host +/* that represents the destination domain, or with an optional fallback +/* relay when {the destination cannot be found, or when all the +/* destination servers are unavailable}. It skips over IP addresses +/* that fail to complete the SMTP/LMTP handshake and tries to find +/* an alternate server when an SMTP/LMTP session fails to deliver. +/* +/* This layer also controls what connections are retrieved from +/* the connection cache, and what connections are saved to the cache. +/* +/* The destination is either a host (or domain) name or a numeric +/* address. Symbolic or numeric service port information may be +/* appended, separated by a colon (":"). In the case of LMTP, +/* destinations may be specified as "unix:pathname", "inet:host" +/* or "inet:host:port". +/* +/* With SMTP, the Internet domain name service is queried for mail +/* exchanger hosts. Quote the domain name with `[' and `]' to +/* suppress mail exchanger lookups. +/* +/* Numerical address information should always be quoted with `[]'. +/* DIAGNOSTICS +/* The delivery status is the result value. +/* SEE ALSO +/* smtp_proto(3) SMTP client protocol +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/* +/* Connection caching in cooperation with: +/* Victor Duchovni +/* Morgan Stanley +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <stdlib.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <errno.h> +#include <netdb.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <fcntl.h> +#include <ctype.h> + +#ifndef IPPORT_SMTP +#define IPPORT_SMTP 25 +#endif + +/* Utility library. */ + +#include <msg.h> +#include <vstream.h> +#include <vstring.h> +#include <split_at.h> +#include <mymalloc.h> +#include <inet_addr_list.h> +#include <iostuff.h> +#include <timed_connect.h> +#include <stringops.h> +#include <host_port.h> +#include <sane_connect.h> +#include <myaddrinfo.h> +#include <sock_addr.h> +#include <inet_proto.h> +#include <known_tcp_ports.h> + +/* Global library. */ + +#include <mail_params.h> +#include <own_inet_addr.h> +#include <deliver_pass.h> +#include <mail_error.h> +#include <dsn_buf.h> +#include <mail_addr.h> + +/* DNS library. */ + +#include <dns.h> + +/* Application-specific. */ + +#include <smtp.h> +#include <smtp_addr.h> +#include <smtp_reuse.h> + + /* + * Forward declaration. + */ +static SMTP_SESSION *smtp_connect_sock(int, struct sockaddr *, int, + SMTP_ITERATOR *, DSN_BUF *, + int); + +/* smtp_connect_unix - connect to UNIX-domain address */ + +static SMTP_SESSION *smtp_connect_unix(SMTP_ITERATOR *iter, DSN_BUF *why, + int sess_flags) +{ + const char *myname = "smtp_connect_unix"; + struct sockaddr_un sock_un; + const char *addr = STR(iter->addr); + int len = strlen(addr); + int sock; + + dsb_reset(why); /* Paranoia */ + + /* + * Sanity checks. + */ + if (len >= (int) sizeof(sock_un.sun_path)) { + msg_warn("unix-domain name too long: %s", addr); + dsb_simple(why, "4.3.5", "Server configuration error"); + return (0); + } + + /* + * Initialize. + */ + memset((void *) &sock_un, 0, sizeof(sock_un)); + sock_un.sun_family = AF_UNIX; +#ifdef HAS_SUN_LEN + sock_un.sun_len = len + 1; +#endif + memcpy(sock_un.sun_path, addr, len + 1); + + /* + * Create a client socket. + */ + if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) + msg_fatal("%s: socket: %m", myname); + + /* + * Connect to the server. + */ + if (msg_verbose) + msg_info("%s: trying: %s...", myname, addr); + + return (smtp_connect_sock(sock, (struct sockaddr *) &sock_un, + sizeof(sock_un), iter, why, sess_flags)); +} + +/* smtp_connect_addr - connect to explicit address */ + +static SMTP_SESSION *smtp_connect_addr(SMTP_ITERATOR *iter, DSN_BUF *why, + int sess_flags) +{ + const char *myname = "smtp_connect_addr"; + struct sockaddr_storage ss; /* remote */ + struct sockaddr *sa = (struct sockaddr *) &ss; + SOCKADDR_SIZE salen = sizeof(ss); + MAI_HOSTADDR_STR hostaddr; + DNS_RR *addr = iter->rr; + unsigned port = iter->port; + int sock; + char *bind_addr; + char *bind_var; + char *saved_bind_addr = 0; + char *tail; + + dsb_reset(why); /* Paranoia */ + + /* + * Sanity checks. + */ + if (dns_rr_to_sa(addr, port, sa, &salen) != 0) { + msg_warn("%s: skip address type %s: %m", + myname, dns_strtype(addr->type)); + dsb_simple(why, "4.4.0", "network address conversion failed: %m"); + return (0); + } + + /* + * Initialize. + */ + if ((sock = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) + msg_fatal("%s: socket: %m", myname); + +#define RETURN_EARLY() do { \ + if (saved_bind_addr) \ + myfree(saved_bind_addr); \ + (void) close(sock); \ + return (0); \ + } while (0) + + if (inet_windowsize > 0) + set_inet_windowsize(sock, inet_windowsize); + + /* + * Allow the sysadmin to specify the source address, for example, as "-o + * smtp_bind_address=x.x.x.x" in the master.cf file. + */ +#ifdef HAS_IPV6 + if (sa->sa_family == AF_INET6) { + bind_addr = var_smtp_bind_addr6; + bind_var = VAR_LMTP_SMTP(BIND_ADDR6); + } else +#endif + if (sa->sa_family == AF_INET) { + bind_addr = var_smtp_bind_addr; + bind_var = VAR_LMTP_SMTP(BIND_ADDR); + } else + bind_var = bind_addr = ""; + if (*bind_addr) { + int aierr; + struct addrinfo *res0; + + if (*bind_addr == '[') { + saved_bind_addr = mystrdup(bind_addr + 1); + if ((tail = split_at(saved_bind_addr, ']')) == 0 || *tail) + msg_fatal("%s: malformed %s parameter: %s", + myname, bind_var, bind_addr); + bind_addr = saved_bind_addr; + } + if ((aierr = hostaddr_to_sockaddr(bind_addr, (char *) 0, 0, &res0)) != 0) + msg_fatal("%s: bad %s parameter: %s: %s", + myname, bind_var, bind_addr, MAI_STRERROR(aierr)); + if (bind(sock, res0->ai_addr, res0->ai_addrlen) < 0) { + msg_warn("%s: bind %s: %m", myname, bind_addr); + if (var_smtp_bind_addr_enforce) { + freeaddrinfo(res0); + dsb_simple(why, "4.4.0", "server configuration error"); + RETURN_EARLY(); + } + } else if (msg_verbose) + msg_info("%s: bind %s", myname, bind_addr); + if (saved_bind_addr) + myfree(saved_bind_addr); + freeaddrinfo(res0); + } + + /* + * When running as a virtual host, bind to the virtual interface so that + * the mail appears to come from the "right" machine address. + * + * XXX The IPv6 patch expands the null host (as client endpoint) and uses + * the result as the loopback address list. + */ + else { + int count = 0; + struct sockaddr *own_addr = 0; + INET_ADDR_LIST *addr_list = own_inet_addr_list(); + struct sockaddr_storage *s; + + for (s = addr_list->addrs; s < addr_list->addrs + addr_list->used; s++) { + if (SOCK_ADDR_FAMILY(s) == sa->sa_family) { + if (count++ > 0) + break; + own_addr = SOCK_ADDR_PTR(s); + } + } + if (count == 1 && !sock_addr_in_loopback(own_addr)) { + if (bind(sock, own_addr, SOCK_ADDR_LEN(own_addr)) < 0) { + SOCKADDR_TO_HOSTADDR(own_addr, SOCK_ADDR_LEN(own_addr), + &hostaddr, (MAI_SERVPORT_STR *) 0, 0); + msg_warn("%s: bind %s: %m", myname, hostaddr.buf); + } else if (msg_verbose) { + SOCKADDR_TO_HOSTADDR(own_addr, SOCK_ADDR_LEN(own_addr), + &hostaddr, (MAI_SERVPORT_STR *) 0, 0); + msg_info("%s: bind %s", myname, hostaddr.buf); + } + } + } + + /* + * Connect to the server. + */ + if (msg_verbose) + msg_info("%s: trying: %s[%s] port %d...", + myname, STR(iter->host), STR(iter->addr), ntohs(port)); + + return (smtp_connect_sock(sock, sa, salen, iter, why, sess_flags)); +} + +/* smtp_connect_sock - connect a socket over some transport */ + +static SMTP_SESSION *smtp_connect_sock(int sock, struct sockaddr *sa, + int salen, + SMTP_ITERATOR *iter, + DSN_BUF *why, + int sess_flags) +{ + int conn_stat; + int saved_errno; + VSTREAM *stream; + time_t start_time; + const char *name = STR(iter->host); + const char *addr = STR(iter->addr); + unsigned port = iter->port; + + start_time = time((time_t *) 0); + if (var_smtp_conn_tmout > 0) { + non_blocking(sock, NON_BLOCKING); + conn_stat = timed_connect(sock, sa, salen, var_smtp_conn_tmout); + saved_errno = errno; + non_blocking(sock, BLOCKING); + errno = saved_errno; + } else { + conn_stat = sane_connect(sock, sa, salen); + } + if (conn_stat < 0) { + if (port) + dsb_simple(why, "4.4.1", "connect to %s[%s]:%d: %m", + name, addr, ntohs(port)); + else + dsb_simple(why, "4.4.1", "connect to %s[%s]: %m", name, addr); + close(sock); + return (0); + } + stream = vstream_fdopen(sock, O_RDWR); + + /* + * Avoid poor performance when TCP MSS > VSTREAM_BUFSIZE. + */ + if (sa->sa_family == AF_INET +#ifdef AF_INET6 + || sa->sa_family == AF_INET6 +#endif + ) + vstream_tweak_tcp(stream); + + /* + * Bundle up what we have into a nice SMTP_SESSION object. + */ + return (smtp_session_alloc(stream, iter, start_time, sess_flags)); +} + +/* smtp_parse_destination - parse host/port destination */ + +static char *smtp_parse_destination(char *destination, char *def_service, + char **hostp, unsigned *portp) +{ + char *buf = mystrdup(destination); + char *service; + struct servent *sp; + char *protocol = "tcp"; /* XXX configurable? */ + unsigned port; + const char *err; + + if (msg_verbose) + msg_info("smtp_parse_destination: %s %s", destination, def_service); + + /* + * Parse the host/port information. We're working with a copy of the + * destination argument so the parsing can be destructive. + */ + if ((err = host_port(buf, hostp, (char *) 0, &service, def_service)) != 0) + msg_fatal("%s in server description: %s", err, destination); + + /* + * Convert service to port number, network byte order. + */ + service = (char *) filter_known_tcp_port(service); + if (alldig(service)) { + if ((port = atoi(service)) >= 65536 || port == 0) + msg_fatal("bad network port: %s for destination: %s", + service, destination); + *portp = htons(port); + } else { + if ((sp = getservbyname(service, protocol)) == 0) + msg_fatal("unknown service: %s/%s", service, protocol); + *portp = sp->s_port; + } + return (buf); +} + +/* smtp_cleanup_session - clean up after using a session */ + +static void smtp_cleanup_session(SMTP_STATE *state) +{ + DELIVER_REQUEST *request = state->request; + SMTP_SESSION *session = state->session; + int throttled; + + /* + * Inform the postmaster of trouble. + * + * XXX Don't send notifications about errors while sending notifications. + */ +#define POSSIBLE_NOTIFICATION(sender) \ + (*sender == 0 || strcmp(sender, mail_addr_double_bounce()) == 0) + + if (session->history != 0 + && (session->error_mask & name_mask(VAR_NOTIFY_CLASSES, + mail_error_masks, + var_notify_classes)) != 0 + && POSSIBLE_NOTIFICATION(request->sender) == 0) + smtp_chat_notify(session); + + /* + * When session caching is enabled, cache the first good session for this + * delivery request under the next-hop destination, and cache all good + * sessions under their server network address (destroying the session in + * the process). + * + * Caching under the next-hop destination name (rather than the fall-back + * destination) allows us to skip over non-responding primary or backup + * hosts. In fact, this is the only benefit of caching logical to + * physical bindings; caching a session under its own hostname provides + * no performance benefit, given the way smtp_connect() works. + */ + throttled = THIS_SESSION_IS_THROTTLED; /* smtp_quit() may fail */ + if (THIS_SESSION_IS_EXPIRED) + smtp_quit(state); /* also disables caching */ + if (THIS_SESSION_IS_CACHED + /* Redundant tests for safety... */ + && vstream_ferror(session->stream) == 0 + && vstream_feof(session->stream) == 0) { + smtp_save_session(state, SMTP_KEY_MASK_SCACHE_DEST_LABEL, + SMTP_KEY_MASK_SCACHE_ENDP_LABEL); + } else { + smtp_session_free(session); + } + state->session = 0; + + /* + * If this session was good, reset the scache next-hop destination, so + * that we won't cache connections to less-preferred servers under the + * same next-hop destination. Otherwise we could end up skipping over the + * available and more-preferred servers. + */ + if (HAVE_SCACHE_REQUEST_NEXTHOP(state) && !throttled) + CLEAR_SCACHE_REQUEST_NEXTHOP(state); + + /* + * Clean up the lists with todo and dropped recipients. + */ + smtp_rcpt_cleanup(state); + + /* + * Reset profiling info. + * + * XXX When one delivery request results in multiple sessions, the set-up + * and transmission latencies of the earlier sessions will count as + * connection set-up time for the later sessions. + * + * XXX On the other hand, when we first try to connect to one or more dead + * hosts before we reach a good host, then all that time must be counted + * as connection set-up time for the session with the good host. + * + * XXX So this set-up attribution problem exists only when we actually + * engage in a session, spend a lot of time delivering a message, find + * that it fails, and then connect to an alternate host. + */ + memset((void *) &request->msg_stats.conn_setup_done, 0, + sizeof(request->msg_stats.conn_setup_done)); + memset((void *) &request->msg_stats.deliver_done, 0, + sizeof(request->msg_stats.deliver_done)); + request->msg_stats.reuse_count = 0; +} + +static void smtp_cache_policy(SMTP_STATE *state, const char *dest) +{ + DELIVER_REQUEST *request = state->request; + + state->misc_flags &= ~SMTP_MISC_FLAG_CONN_CACHE_MASK; + + if (smtp_cache_dest && string_list_match(smtp_cache_dest, dest)) { + state->misc_flags |= SMTP_MISC_FLAG_CONN_CACHE_MASK; + } else if (var_smtp_cache_demand) { + if (request->flags & DEL_REQ_FLAG_CONN_LOAD) + state->misc_flags |= SMTP_MISC_FLAG_CONN_LOAD; + if (request->flags & DEL_REQ_FLAG_CONN_STORE) + state->misc_flags |= SMTP_MISC_FLAG_CONN_STORE; + } +} + +/* smtp_connect_local - connect to local server */ + +static void smtp_connect_local(SMTP_STATE *state, const char *path) +{ + const char *myname = "smtp_connect_local"; + SMTP_ITERATOR *iter = state->iterator; + SMTP_SESSION *session; + DSN_BUF *why = state->why; + + /* + * Do not silently ignore an unused setting. + */ + if (*var_fallback_relay) + msg_warn("ignoring \"%s = %s\" setting for non-TCP connections", + VAR_LMTP_FALLBACK, var_fallback_relay); + + /* + * It's too painful to weave this code into the SMTP connection + * management routine. + * + * Connection cache management is based on the UNIX-domain pathname, without + * the "unix:" prefix. + */ + smtp_cache_policy(state, path); + if (state->misc_flags & SMTP_MISC_FLAG_CONN_CACHE_MASK) + SET_SCACHE_REQUEST_NEXTHOP(state, path); + + /* + * Here we ensure that the iter->addr member refers to a copy of the + * UNIX-domain pathname, so that smtp_save_session() will cache the + * connection using the pathname as the physical endpoint name. + * + * We set dest=path for backwards compatibility. + */ +#define NO_PORT 0 + + SMTP_ITER_INIT(iter, path, var_myhostname, path, NO_PORT, state); + + /* + * Opportunistic TLS for unix domain sockets does not make much sense, + * since the channel is private, mere encryption without authentication + * is just wasted cycles and opportunity for breakage. Since we are not + * willing to retry after TLS handshake failures here, we downgrade "may" + * no "none". Nothing is lost, and much waste is avoided. + * + * We don't know who is authenticating whom, so if a client cert is + * available, "encrypt" may be a sensible policy. Otherwise, we also + * downgrade "encrypt" to "none", this time just to avoid waste. + * + * We use smtp_reuse_nexthop() instead of smtp_reuse_addr(), so that we can + * reuse a SASL-authenticated connection (however unlikely this scenario + * may be). The smtp_reuse_addr() interface currently supports only reuse + * of SASL-unauthenticated connections. + */ +#ifdef USE_TLS + if (!smtp_tls_policy_cache_query(why, state->tls, iter)) { + msg_warn("TLS policy lookup error for %s/%s: %s", + STR(iter->host), STR(iter->addr), STR(why->reason)); + return; + } +#endif + if ((state->misc_flags & SMTP_MISC_FLAG_CONN_LOAD) == 0 + || (session = smtp_reuse_nexthop(state, + SMTP_KEY_MASK_SCACHE_DEST_LABEL)) == 0) + session = smtp_connect_unix(iter, why, state->misc_flags); + if ((state->session = session) != 0) { + session->state = state; +#ifdef USE_TLS + session->tls_nexthop = var_myhostname; /* for TLS_LEV_SECURE */ + if (state->tls->level == TLS_LEV_MAY) { + msg_warn("%s: opportunistic TLS encryption is not appropriate " + "for unix-domain destinations.", myname); + state->tls->level = TLS_LEV_NONE; + } +#endif + /* All delivery errors bounce or defer. */ + state->misc_flags |= SMTP_MISC_FLAG_FINAL_SERVER; + + /* + * When a TLS handshake fails, the stream is marked "dead" to avoid + * further I/O over a broken channel. + */ + if ((session->features & SMTP_FEATURE_FROM_CACHE) == 0 + && smtp_helo(state) != 0) { + if (!THIS_SESSION_IS_FORBIDDEN + && vstream_ferror(session->stream) == 0 + && vstream_feof(session->stream) == 0) + smtp_quit(state); + } else { + smtp_xfer(state); + } + + /* + * With opportunistic TLS disabled we don't expect to be asked to + * retry connections without TLS, and so we expect the final server + * flag to stay on. + */ + if ((state->misc_flags & SMTP_MISC_FLAG_FINAL_SERVER) == 0) + msg_panic("%s: unix-domain destination not final!", myname); + smtp_cleanup_session(state); + } + + /* + * Cleanup. + */ + if (HAVE_SCACHE_REQUEST_NEXTHOP(state)) + CLEAR_SCACHE_REQUEST_NEXTHOP(state); +} + +/* smtp_scrub_address_list - delete all cached addresses from list */ + +static void smtp_scrub_addr_list(HTABLE *cached_addr, DNS_RR **addr_list) +{ + MAI_HOSTADDR_STR hostaddr; + DNS_RR *addr; + DNS_RR *next; + + /* + * XXX Extend the DNS_RR structure with fields for the printable address + * and/or binary sockaddr representations, so that we can avoid repeated + * binary->string transformations for the same address. + */ + for (addr = *addr_list; addr; addr = next) { + next = addr->next; + if (dns_rr_to_pa(addr, &hostaddr) == 0) { + msg_warn("cannot convert type %s record to printable address", + dns_strtype(addr->type)); + continue; + } + if (htable_locate(cached_addr, hostaddr.buf)) + *addr_list = dns_rr_remove(*addr_list, addr); + } +} + +/* smtp_update_addr_list - common address list update */ + +static void smtp_update_addr_list(DNS_RR **addr_list, const char *server_addr, + int session_count) +{ + DNS_RR *addr; + DNS_RR *next; + int aierr; + struct addrinfo *res0; + + if (*addr_list == 0) + return; + + /* + * Truncate the address list if we are not going to use it anyway. + */ + if (session_count == var_smtp_mxsess_limit + || session_count == var_smtp_mxaddr_limit) { + dns_rr_free(*addr_list); + *addr_list = 0; + return; + } + + /* + * Convert server address to internal form, and look it up in the address + * list. + * + * XXX smtp_reuse_session() breaks if we remove two or more adjacent list + * elements but do not truncate the list to zero length. + * + * XXX Extend the SMTP_SESSION structure with sockaddr information so that + * we can avoid repeated string->binary transformations for the same + * address. + */ + if ((aierr = hostaddr_to_sockaddr(server_addr, (char *) 0, 0, &res0)) != 0) { + msg_warn("hostaddr_to_sockaddr %s: %s", + server_addr, MAI_STRERROR(aierr)); + } else { + for (addr = *addr_list; addr; addr = next) { + next = addr->next; + if (DNS_RR_EQ_SA(addr, (struct sockaddr *) res0->ai_addr)) { + *addr_list = dns_rr_remove(*addr_list, addr); + break; + } + } + freeaddrinfo(res0); + } +} + +/* smtp_reuse_session - try to use existing connection, return session count */ + +static int smtp_reuse_session(SMTP_STATE *state, DNS_RR **addr_list, + int domain_best_pref) +{ + int session_count = 0; + DNS_RR *addr; + DNS_RR *next; + MAI_HOSTADDR_STR hostaddr; + SMTP_SESSION *session; + SMTP_ITERATOR *iter = state->iterator; + DSN_BUF *why = state->why; + + /* + * First, search the cache by delivery request nexthop. We truncate the + * server address list when all the sessions for this destination are + * used up, to reduce the number of variables that need to be checked + * later. + * + * Note: connection reuse by delivery request nexthop restores the "best MX" + * bit. + * + * smtp_reuse_nexthop() clobbers the iterators's "dest" attribute. We save + * and restore it here, so that subsequent connections will use the + * proper nexthop information. + * + * We don't use TLS level info for nexthop-based connection cache storage + * keys. The combination of (service, nexthop, etc.) should be stable + * over the time range of interest, and the policy is still enforced on + * an individual connection to an MX host, before that connection is + * stored under a nexthop- or host-based storage key. + */ +#ifdef USE_TLS + smtp_tls_policy_dummy(state->tls); +#endif + SMTP_ITER_SAVE_DEST(state->iterator); + if (*addr_list && SMTP_RCPT_LEFT(state) > 0 + && HAVE_SCACHE_REQUEST_NEXTHOP(state) + && (session = smtp_reuse_nexthop(state, SMTP_KEY_MASK_SCACHE_DEST_LABEL)) != 0) { + session_count = 1; + smtp_update_addr_list(addr_list, STR(iter->addr), session_count); + if ((state->misc_flags & SMTP_MISC_FLAG_FINAL_NEXTHOP) + && *addr_list == 0) + state->misc_flags |= SMTP_MISC_FLAG_FINAL_SERVER; + smtp_xfer(state); + smtp_cleanup_session(state); + } + SMTP_ITER_RESTORE_DEST(state->iterator); + + /* + * Second, search the cache by primary MX address. Again, we use address + * list truncation so that we have to check fewer variables later. + * + * XXX This loop is safe because smtp_update_addr_list() either truncates + * the list to zero length, or removes at most one list element. + * + * Currently, we use smtp_reuse_addr() only for SASL-unauthenticated + * connections. Furthermore, we rely on smtp_reuse_addr() to look up an + * existing SASL-unauthenticated connection only when a new connection + * would be guaranteed not to require SASL authentication. + * + * In addition, we rely on smtp_reuse_addr() to look up an existing + * plaintext connection only when a new connection would be guaranteed + * not to use TLS. + * + * For more precise control over reuse, the iterator should look up SASL and + * TLS policy as it evaluates mail exchangers in order, instead of + * relying on duplicate lookup request code in smtp_reuse(3) and + * smtp_session(3). + */ + for (addr = *addr_list; SMTP_RCPT_LEFT(state) > 0 && addr; addr = next) { + if (addr->pref != domain_best_pref) + break; + next = addr->next; + if (dns_rr_to_pa(addr, &hostaddr) == 0) { + msg_warn("cannot convert type %s record to printable address", + dns_strtype(addr->type)); + /* XXX Assume there is no code at the end of this loop. */ + continue; + } + vstring_strcpy(iter->addr, hostaddr.buf); + vstring_strcpy(iter->host, SMTP_HNAME(addr)); + iter->rr = addr; +#ifdef USE_TLS + if (!smtp_tls_policy_cache_query(why, state->tls, iter)) { + msg_warn("TLS policy lookup error for %s/%s: %s", + STR(iter->dest), STR(iter->host), STR(why->reason)); + continue; + /* XXX Assume there is no code at the end of this loop. */ + } +#endif + if ((session = smtp_reuse_addr(state, + SMTP_KEY_MASK_SCACHE_ENDP_LABEL)) != 0) { + session->features |= SMTP_FEATURE_BEST_MX; + session_count += 1; + smtp_update_addr_list(addr_list, STR(iter->addr), session_count); + if (*addr_list == 0) + next = 0; + if ((state->misc_flags & SMTP_MISC_FLAG_FINAL_NEXTHOP) + && next == 0) + state->misc_flags |= SMTP_MISC_FLAG_FINAL_SERVER; + smtp_xfer(state); + smtp_cleanup_session(state); + } + } + return (session_count); +} + +/* smtp_connect_inet - establish network connection */ + +static void smtp_connect_inet(SMTP_STATE *state, const char *nexthop, + char *def_service) +{ + DELIVER_REQUEST *request = state->request; + SMTP_ITERATOR *iter = state->iterator; + ARGV *sites; + char *dest; + char **cpp; + int non_fallback_sites; + int retry_plain = 0; + DSN_BUF *why = state->why; + + /* + * For sanity, require that at least one of INET or INET6 is enabled. + * Otherwise, we can't look up interface information, and we can't + * convert names or addresses. + */ + if (inet_proto_info()->ai_family_list[0] == 0) { + dsb_simple(why, "4.4.4", "all network protocols are disabled"); + return; + } + + /* + * Do a null destination sanity check in case the primary destination is + * a list that consists of only separators. + */ + sites = argv_split(nexthop, CHARS_COMMA_SP); + if (sites->argc == 0) + msg_panic("null destination: \"%s\"", nexthop); + non_fallback_sites = sites->argc; + argv_split_append(sites, var_fallback_relay, CHARS_COMMA_SP); + + /* + * Don't give up after a hard host lookup error until we have tried the + * fallback relay servers. + * + * Don't bounce mail after a host lookup problem with a relayhost or with a + * fallback relay. + * + * Don't give up after a qualifying soft error until we have tried all + * qualifying backup mail servers. + * + * All this means that error handling and error reporting depends on whether + * the error qualifies for trying to deliver to a backup mail server, or + * whether we're looking up a relayhost or fallback relay. The challenge + * then is to build this into the pre-existing SMTP client without + * getting lost in the complexity. + */ +#define IS_FALLBACK_RELAY(cpp, sites, non_fallback_sites) \ + (*(cpp) && (cpp) >= (sites)->argv + (non_fallback_sites)) + + for (cpp = sites->argv, (state->misc_flags |= SMTP_MISC_FLAG_FIRST_NEXTHOP); + SMTP_RCPT_LEFT(state) > 0 && (dest = *cpp) != 0; + cpp++, (state->misc_flags &= ~SMTP_MISC_FLAG_FIRST_NEXTHOP)) { + char *dest_buf; + char *domain; + unsigned port; + DNS_RR *addr_list; + DNS_RR *addr; + DNS_RR *next; + int addr_count; + int sess_count; + SMTP_SESSION *session; + int lookup_mx; + unsigned domain_best_pref; + MAI_HOSTADDR_STR hostaddr; + + if (cpp[1] == 0) + state->misc_flags |= SMTP_MISC_FLAG_FINAL_NEXTHOP; + + /* + * Parse the destination. If no TCP port is specified, use the port + * that is reserved for the protocol (SMTP or LMTP). + */ + dest_buf = smtp_parse_destination(dest, def_service, &domain, &port); + if (var_helpful_warnings && var_smtp_tls_wrappermode == 0 + && ntohs(port) == 465) { + msg_info("SMTPS wrappermode (TCP port 465) requires setting " + "\"%s = yes\", and \"%s = encrypt\" (or stronger)", + VAR_LMTP_SMTP(TLS_WRAPPER), VAR_LMTP_SMTP(TLS_LEVEL)); + } +#define NO_HOST "" /* safety */ +#define NO_ADDR "" /* safety */ + + SMTP_ITER_INIT(iter, dest, NO_HOST, NO_ADDR, port, state); + + /* + * Resolve an SMTP or LMTP server. In the case of SMTP, skip mail + * exchanger lookups when a quoted host is specified or when DNS + * lookups are disabled. + */ + if (msg_verbose) + msg_info("connecting to %s port %d", domain, ntohs(port)); + if (smtp_mode) { + if (ntohs(port) == IPPORT_SMTP) + state->misc_flags |= SMTP_MISC_FLAG_LOOP_DETECT; + else + state->misc_flags &= ~SMTP_MISC_FLAG_LOOP_DETECT; + lookup_mx = (smtp_dns_support != SMTP_DNS_DISABLED && *dest != '['); + } else + lookup_mx = 0; + if (!lookup_mx) { + addr_list = smtp_host_addr(domain, state->misc_flags, why); + /* XXX We could be an MX host for this destination... */ + } else { + int i_am_mx = 0; + + addr_list = smtp_domain_addr(domain, &iter->mx, state->misc_flags, + why, &i_am_mx); + /* If we're MX host, don't connect to non-MX backups. */ + if (i_am_mx) + state->misc_flags |= SMTP_MISC_FLAG_FINAL_NEXTHOP; + } + + /* + * Don't try fall-back hosts if mail loops to myself. That would just + * make the problem worse. + */ + if (addr_list == 0 && SMTP_HAS_LOOP_DSN(why)) + state->misc_flags |= SMTP_MISC_FLAG_FINAL_NEXTHOP; + + /* + * No early loop exit or we have a memory leak with dest_buf. + */ + if (addr_list) + domain_best_pref = addr_list->pref; + + /* + * When connection caching is enabled, store the first good + * connection for this delivery request under the delivery request + * next-hop name. Good connections will also be stored under their + * specific server IP address. + * + * XXX smtp_session_cache_destinations specifies domain names without + * :port, because : is already used for maptype:mapname. Because of + * this limitation we use the bare domain without the optional [] or + * non-default TCP port. + * + * Opportunistic (a.k.a. on-demand) session caching on request by the + * queue manager. This is turned temporarily when a destination has a + * high volume of mail in the active queue. When the surge reaches + * its end, the queue manager requests that connections be retrieved + * but not stored. + */ + if (addr_list && (state->misc_flags & SMTP_MISC_FLAG_FIRST_NEXTHOP)) { + smtp_cache_policy(state, domain); + if (state->misc_flags & SMTP_MISC_FLAG_CONN_CACHE_MASK) + SET_SCACHE_REQUEST_NEXTHOP(state, dest); + } + + /* + * Delete visited cached hosts from the address list. + * + * Optionally search the connection cache by domain name or by primary + * MX address before we try to create new connections. + * + * Enforce the MX session and MX address counts per next-hop or + * fall-back destination. smtp_reuse_session() will truncate the + * address list when either limit is reached. + */ + if (addr_list && (state->misc_flags & SMTP_MISC_FLAG_CONN_LOAD)) { + if (state->cache_used->used > 0) + smtp_scrub_addr_list(state->cache_used, &addr_list); + sess_count = addr_count = + smtp_reuse_session(state, &addr_list, domain_best_pref); + } else + sess_count = addr_count = 0; + + /* + * Connect to an SMTP server: create primary MX connections, and + * reuse or create backup MX connections. + * + * At the start of an SMTP session, all recipients are unmarked. In the + * course of an SMTP session, recipients are marked as KEEP (deliver + * to alternate mail server) or DROP (remove from recipient list). At + * the end of an SMTP session, weed out the recipient list. Unmark + * any left-over recipients and try to deliver them to a backup mail + * server. + * + * Cache the first good session under the next-hop destination name. + * Cache all good sessions under their physical endpoint. + * + * Don't query the session cache for primary MX hosts. We already did + * that in smtp_reuse_session(), and if any were found in the cache, + * they were already deleted from the address list. + * + * Currently, we use smtp_reuse_addr() only for SASL-unauthenticated + * connections. Furthermore, we rely on smtp_reuse_addr() to look up + * an existing SASL-unauthenticated connection only when a new + * connection would be guaranteed not to require SASL authentication. + * + * In addition, we rely on smtp_reuse_addr() to look up an existing + * plaintext connection only when a new connection would be + * guaranteed not to use TLS. + */ + for (addr = addr_list; SMTP_RCPT_LEFT(state) > 0 && addr; addr = next) { + next = addr->next; + if (++addr_count == var_smtp_mxaddr_limit) + next = 0; + if (dns_rr_to_pa(addr, &hostaddr) == 0) { + msg_warn("cannot convert type %s record to printable address", + dns_strtype(addr->type)); + /* XXX Assume there is no code at the end of this loop. */ + continue; + } + vstring_strcpy(iter->addr, hostaddr.buf); + vstring_strcpy(iter->host, SMTP_HNAME(addr)); + iter->rr = addr; +#ifdef USE_TLS + if (!smtp_tls_policy_cache_query(why, state->tls, iter)) { + msg_warn("TLS policy lookup for %s/%s: %s", + STR(iter->dest), STR(iter->host), STR(why->reason)); + continue; + /* XXX Assume there is no code at the end of this loop. */ + } + if (var_smtp_tls_wrappermode + && state->tls->level < TLS_LEV_ENCRYPT) { + msg_warn("%s requires \"%s = encrypt\" (or stronger)", + VAR_LMTP_SMTP(TLS_WRAPPER), VAR_LMTP_SMTP(TLS_LEVEL)); + continue; + /* XXX Assume there is no code at the end of this loop. */ + } + /* Disable TLS when retrying after a handshake failure */ + if (retry_plain) { + state->tls->level = TLS_LEV_NONE; + retry_plain = 0; + } +#endif + if ((state->misc_flags & SMTP_MISC_FLAG_CONN_LOAD) == 0 + || addr->pref == domain_best_pref + || !(session = smtp_reuse_addr(state, + SMTP_KEY_MASK_SCACHE_ENDP_LABEL))) + session = smtp_connect_addr(iter, why, state->misc_flags); + if ((state->session = session) != 0) { + session->state = state; +#ifdef USE_TLS + session->tls_nexthop = domain; +#endif + if (addr->pref == domain_best_pref) + session->features |= SMTP_FEATURE_BEST_MX; + /* Don't count handshake errors towards the session limit. */ + if ((state->misc_flags & SMTP_MISC_FLAG_FINAL_NEXTHOP) + && next == 0) + state->misc_flags |= SMTP_MISC_FLAG_FINAL_SERVER; + if ((session->features & SMTP_FEATURE_FROM_CACHE) == 0 + && smtp_helo(state) != 0) { +#ifdef USE_TLS + + /* + * When an opportunistic TLS handshake fails, try the + * same address again, with TLS disabled. See also the + * RETRY_AS_PLAINTEXT macro. + */ + if ((retry_plain = session->tls_retry_plain) != 0) { + --addr_count; + next = addr; + } +#endif + + /* + * When a TLS handshake fails, the stream is marked + * "dead" to avoid further I/O over a broken channel. + */ + if (!THIS_SESSION_IS_FORBIDDEN + && vstream_ferror(session->stream) == 0 + && vstream_feof(session->stream) == 0) + smtp_quit(state); + } else { + /* Do count delivery errors towards the session limit. */ + if (++sess_count == var_smtp_mxsess_limit) + next = 0; + if ((state->misc_flags & SMTP_MISC_FLAG_FINAL_NEXTHOP) + && next == 0) + state->misc_flags |= SMTP_MISC_FLAG_FINAL_SERVER; + smtp_xfer(state); +#ifdef USE_TLS + + /* + * When opportunistic TLS fails after the STARTTLS + * handshake, try the same address again, with TLS + * disabled. See also the RETRY_AS_PLAINTEXT macro. + */ + if ((retry_plain = session->tls_retry_plain) != 0) { + --sess_count; + --addr_count; + next = addr; + } +#endif + } + smtp_cleanup_session(state); + } else { + /* The reason already includes the IP address and TCP port. */ + msg_info("%s", STR(why->reason)); + } + /* XXX Code above assumes there is no code at this loop ending. */ + } + dns_rr_free(addr_list); + if (iter->mx) { + dns_rr_free(iter->mx); + iter->mx = 0; /* Just in case */ + } + myfree(dest_buf); + if (state->misc_flags & SMTP_MISC_FLAG_FINAL_NEXTHOP) + break; + } + + /* + * We still need to deliver, bounce or defer some left-over recipients: + * either mail loops or some backup mail server was unavailable. + */ + if (SMTP_RCPT_LEFT(state) > 0) { + + /* + * In case of a "no error" indication we make up an excuse: we did + * find the host address, but we did not attempt to connect to it. + * This can happen when the fall-back relay was already tried via a + * cached connection, so that the address list scrubber left behind + * an empty list. + */ + if (!SMTP_HAS_DSN(why)) { + dsb_simple(why, "4.3.0", + "server unavailable or unable to receive mail"); + } + + /* + * Pay attention to what could be configuration problems, and pretend + * that these are recoverable rather than bouncing the mail. + */ + else if (!SMTP_HAS_SOFT_DSN(why)) { + + /* + * The fall-back destination did not resolve as expected, or it + * is refusing to talk to us, or mail for it loops back to us. + */ + if (IS_FALLBACK_RELAY(cpp, sites, non_fallback_sites)) { + msg_warn("%s configuration problem", VAR_SMTP_FALLBACK); + vstring_strcpy(why->status, "4.3.5"); + /* XXX Keep the diagnostic code and MTA. */ + } + + /* + * The next-hop relayhost did not resolve as expected, or it is + * refusing to talk to us, or mail for it loops back to us. + * + * XXX There is no equivalent safety net for mis-configured + * sender-dependent relay hosts. The trivial-rewrite resolver + * would have to flag the result, and the queue manager would + * have to provide that information to delivery agents. + */ + else if (smtp_mode && strcmp(sites->argv[0], var_relayhost) == 0) { + msg_warn("%s configuration problem", VAR_RELAYHOST); + vstring_strcpy(why->status, "4.3.5"); + /* XXX Keep the diagnostic code and MTA. */ + } + + /* + * Mail for the next-hop destination loops back to myself. Pass + * the mail to the best_mx_transport or bounce it. + */ + else if (smtp_mode && SMTP_HAS_LOOP_DSN(why) && *var_bestmx_transp) { + dsb_reset(why); /* XXX */ + state->status = deliver_pass_all(MAIL_CLASS_PRIVATE, + var_bestmx_transp, + request); + SMTP_RCPT_LEFT(state) = 0; /* XXX */ + } + } + } + + /* + * Cleanup. + */ + if (HAVE_SCACHE_REQUEST_NEXTHOP(state)) + CLEAR_SCACHE_REQUEST_NEXTHOP(state); + argv_free(sites); +} + +/* smtp_connect - establish SMTP connection */ + +int smtp_connect(SMTP_STATE *state) +{ + DELIVER_REQUEST *request = state->request; + char *destination = request->nexthop; + + /* + * All deliveries proceed along the same lines, whether they are over TCP + * or UNIX-domain sockets, and whether they use SMTP or LMTP: get a + * connection from the cache or create a new connection; deliver mail; + * update the connection cache or disconnect. + * + * The major differences appear at a higher level: the expansion from + * destination to address list, and whether to stop before we reach the + * end of that list. + */ + + /* + * With LMTP we have direct-to-host delivery only. The destination may + * have multiple IP addresses. + */ + if (!smtp_mode) { + if (strncmp(destination, "unix:", 5) == 0) { + smtp_connect_local(state, destination + 5); + } else { + if (strncmp(destination, "inet:", 5) == 0) + destination += 5; + smtp_connect_inet(state, destination, var_smtp_tcp_port); + } + } + + /* + * XXX We don't add support for "unix:" or "inet:" prefixes in SMTP + * destinations, because that would break compatibility with existing + * Postfix configurations that have a host with such a name. + */ + else { + smtp_connect_inet(state, destination, var_smtp_tcp_port); + } + + /* + * We still need to bounce or defer some left-over recipients: either + * (SMTP) mail loops or some server was unavailable. + * + * We could avoid this (and the "final server" complexity) by keeping one + * DSN structure per recipient in memory, by updating those in-memory + * structures with each delivery attempt, and by always flushing all + * deferred recipients at the end. We'd probably still want to bounce + * recipients immediately, so we'd end up with another chunk of code for + * defer logging only. + */ + if (SMTP_RCPT_LEFT(state) > 0) { + state->misc_flags |= SMTP_MISC_FLAG_FINAL_SERVER; /* XXX */ + smtp_sess_fail(state); + + /* + * Sanity check. Don't silently lose recipients. + */ + smtp_rcpt_cleanup(state); + if (SMTP_RCPT_LEFT(state) > 0) + msg_panic("smtp_connect: left-over recipients"); + } + return (state->status); +} |