* Exim - an Internet mail transport agent *
*************************************************/
-/* Copyright (c) University of Cambridge 1995 - 2016 */
+/* Copyright (c) The Exim Maintainers 2020 - 2023 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
/* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
/* General functions concerned with transportation, and generic options for all
transports. */
#include "exim.h"
-#ifdef HAVE_LINUX_SENDFILE
-#include <sys/sendfile.h>
-#endif
-
-/* Structure for keeping list of addresses that have been added to
-Envelope-To:, in order to avoid duplication. */
-
-struct aci {
- struct aci *next;
- address_item *ptr;
- };
-
-
-/* Static data for write_chunk() */
-
-static uschar *chunk_ptr; /* chunk pointer */
-static uschar *nl_check; /* string to look for at line start */
-static int nl_check_length; /* length of same */
-static uschar *nl_escape; /* string to insert */
-static int nl_escape_length; /* length of same */
-static int nl_partial_match; /* length matched at chunk end */
-
-
/* Generic options for transports, all of which live inside transport_instance
data blocks and which therefore have the opt_public flag set. Note that there
are other options living inside this structure which can be set only from
certain transports. */
+#define LOFF(field) OPT_OFF(transport_instance, field)
optionlist optionlist_transports[] = {
+ /* name type value */
{ "*expand_group", opt_stringptr|opt_hidden|opt_public,
- (void *)offsetof(transport_instance, expand_gid) },
+ LOFF(expand_gid) },
{ "*expand_user", opt_stringptr|opt_hidden|opt_public,
- (void *)offsetof(transport_instance, expand_uid) },
+ LOFF(expand_uid) },
{ "*headers_rewrite_flags", opt_int|opt_public|opt_hidden,
- (void *)offsetof(transport_instance, rewrite_existflags) },
+ LOFF(rewrite_existflags) },
{ "*headers_rewrite_rules", opt_void|opt_public|opt_hidden,
- (void *)offsetof(transport_instance, rewrite_rules) },
+ LOFF(rewrite_rules) },
{ "*set_group", opt_bool|opt_hidden|opt_public,
- (void *)offsetof(transport_instance, gid_set) },
+ LOFF(gid_set) },
{ "*set_user", opt_bool|opt_hidden|opt_public,
- (void *)offsetof(transport_instance, uid_set) },
+ LOFF(uid_set) },
{ "body_only", opt_bool|opt_public,
- (void *)offsetof(transport_instance, body_only) },
+ LOFF(body_only) },
{ "current_directory", opt_stringptr|opt_public,
- (void *)offsetof(transport_instance, current_dir) },
+ LOFF(current_dir) },
{ "debug_print", opt_stringptr | opt_public,
- (void *)offsetof(transport_instance, debug_string) },
+ LOFF(debug_string) },
{ "delivery_date_add", opt_bool|opt_public,
- (void *)(offsetof(transport_instance, delivery_date_add)) },
+ LOFF(delivery_date_add) },
{ "disable_logging", opt_bool|opt_public,
- (void *)(offsetof(transport_instance, disable_logging)) },
+ LOFF(disable_logging) },
{ "driver", opt_stringptr|opt_public,
- (void *)offsetof(transport_instance, driver_name) },
+ LOFF(driver_name) },
{ "envelope_to_add", opt_bool|opt_public,
- (void *)(offsetof(transport_instance, envelope_to_add)) },
+ LOFF(envelope_to_add) },
#ifndef DISABLE_EVENT
{ "event_action", opt_stringptr | opt_public,
- (void *)offsetof(transport_instance, event_action) },
+ LOFF(event_action) },
#endif
{ "group", opt_expand_gid|opt_public,
- (void *)offsetof(transport_instance, gid) },
+ LOFF(gid) },
{ "headers_add", opt_stringptr|opt_public|opt_rep_str,
- (void *)offsetof(transport_instance, add_headers) },
+ LOFF(add_headers) },
{ "headers_only", opt_bool|opt_public,
- (void *)offsetof(transport_instance, headers_only) },
+ LOFF(headers_only) },
{ "headers_remove", opt_stringptr|opt_public|opt_rep_str,
- (void *)offsetof(transport_instance, remove_headers) },
+ LOFF(remove_headers) },
{ "headers_rewrite", opt_rewrite|opt_public,
- (void *)offsetof(transport_instance, headers_rewrite) },
+ LOFF(headers_rewrite) },
{ "home_directory", opt_stringptr|opt_public,
- (void *)offsetof(transport_instance, home_dir) },
+ LOFF(home_dir) },
{ "initgroups", opt_bool|opt_public,
- (void *)offsetof(transport_instance, initgroups) },
+ LOFF(initgroups) },
{ "max_parallel", opt_stringptr|opt_public,
- (void *)offsetof(transport_instance, max_parallel) },
+ LOFF(max_parallel) },
{ "message_size_limit", opt_stringptr|opt_public,
- (void *)offsetof(transport_instance, message_size_limit) },
+ LOFF(message_size_limit) },
{ "rcpt_include_affixes", opt_bool|opt_public,
- (void *)offsetof(transport_instance, rcpt_include_affixes) },
+ LOFF(rcpt_include_affixes) },
{ "retry_use_local_part", opt_bool|opt_public,
- (void *)offsetof(transport_instance, retry_use_local_part) },
+ LOFF(retry_use_local_part) },
{ "return_path", opt_stringptr|opt_public,
- (void *)(offsetof(transport_instance, return_path)) },
+ LOFF(return_path) },
{ "return_path_add", opt_bool|opt_public,
- (void *)(offsetof(transport_instance, return_path_add)) },
+ LOFF(return_path_add) },
{ "shadow_condition", opt_stringptr|opt_public,
- (void *)offsetof(transport_instance, shadow_condition) },
+ LOFF(shadow_condition) },
{ "shadow_transport", opt_stringptr|opt_public,
- (void *)offsetof(transport_instance, shadow) },
+ LOFF(shadow) },
{ "transport_filter", opt_stringptr|opt_public,
- (void *)offsetof(transport_instance, filter_command) },
+ LOFF(filter_command) },
{ "transport_filter_timeout", opt_time|opt_public,
- (void *)offsetof(transport_instance, filter_timeout) },
+ LOFF(filter_timeout) },
{ "user", opt_expand_uid|opt_public,
- (void *)offsetof(transport_instance, uid) }
+ LOFF(uid) }
};
-int optionlist_transports_size =
- sizeof(optionlist_transports)/sizeof(optionlist);
+int optionlist_transports_size = nelem(optionlist_transports);
+
+#ifdef MACRO_PREDEF
+
+# include "macro_predef.h"
+
+void
+options_transports(void)
+{
+uschar buf[64];
+
+options_from_list(optionlist_transports, nelem(optionlist_transports), US"TRANSPORTS", NULL);
+
+for (transport_info * ti = transports_available; ti->driver_name[0]; ti++)
+ {
+ spf(buf, sizeof(buf), US"_DRIVER_TRANSPORT_%T", ti->driver_name);
+ builtin_macro_create(buf);
+ options_from_list(ti->options, (unsigned)*ti->options_count, US"TRANSPORT", ti->driver_name);
+ }
+}
+
+#else /*!MACRO_PREDEF*/
+
+/* Structure for keeping list of addresses that have been added to
+Envelope-To:, in order to avoid duplication. */
+
+struct aci {
+ struct aci *next;
+ address_item *ptr;
+ };
+
+
+/* Static data for write_chunk() */
+
+static uschar *chunk_ptr; /* chunk pointer */
+static uschar *nl_check; /* string to look for at line start */
+static int nl_check_length; /* length of same */
+static uschar *nl_escape; /* string to insert */
+static int nl_escape_length; /* length of same */
+static int nl_partial_match; /* length matched at chunk end */
/*************************************************
void
transport_init(void)
{
-transport_instance *t;
-
readconf_driver_init(US"transport",
(driver_instance **)(&transports), /* chain anchor */
(driver_info *)transports_available, /* available drivers */
/* Now scan the configured transports and check inconsistencies. A shadow
transport is permitted only for local transports. */
-for (t = transports; t != NULL; t = t->next)
+for (transport_instance * t = transports; t; t = t->next)
{
- if (!t->info->local)
- {
- if (t->shadow != NULL)
- log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
- "shadow transport not allowed on non-local transport %s", t->name);
- }
+ if (!t->info->local && t->shadow)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "shadow transport not allowed on non-local transport %s", t->name);
if (t->body_only && t->headers_only)
log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
* Write block of data *
*************************************************/
+static int
+tpt_write(int fd, uschar * block, int len, BOOL more, int options)
+{
+return
+#ifndef DISABLE_TLS
+ tls_out.active.sock == fd
+ ? tls_write(tls_out.active.tls_ctx, block, len, more) :
+#endif
+#ifdef MSG_MORE
+ more && !(options & topt_not_socket) ? send(fd, block, len, MSG_MORE) :
+#endif
+ write(fd, block, len);
+}
+
/* Subroutine called by write_chunk() and at the end of the message actually
to write a data block. Also called directly by some transports to write
additional data to the file descriptor (e.g. prefix, suffix).
longstop.
Arguments:
- fd file descriptor to write to
+ tctx transport context: file descriptor or string to write to
block block of bytes to write
len number of bytes to write
+ more further data expected soon
Returns: TRUE on success, FALSE on failure (with errno preserved);
transport_count is incremented by the number of bytes written
*/
-BOOL
-transport_write_block(int fd, uschar *block, int len)
+static BOOL
+transport_write_block_fd(transport_ctx * tctx, uschar * block, int len, BOOL more)
{
-int i, rc, save_errno;
+int rc, save_errno;
int local_timeout = transport_write_timeout;
+int connretry = 1;
+int fd = tctx->u.fd;
/* This loop is for handling incomplete writes and other retries. In most
normal cases, it is only ever executed once. */
-for (i = 0; i < 100; i++)
+for (int i = 0; i < 100; i++)
{
DEBUG(D_transport)
- debug_printf("writing data block fd=%d size=%d timeout=%d\n",
- fd, len, local_timeout);
+ debug_printf("writing data block fd=%d size=%d timeout=%d%s\n",
+ fd, len, local_timeout, more ? " (more expected)" : "");
- /* This code makes use of alarm() in order to implement the timeout. This
- isn't a very tidy way of doing things. Using non-blocking I/O with select()
- provides a neater approach. However, I don't know how to do this when TLS is
- in use. */
+ /* When doing TCP Fast Open we may get this far before the 3-way handshake
+ is complete, and write returns ENOTCONN. Detect that, wait for the socket
+ to become writable, and retry once only. */
- if (transport_write_timeout <= 0) /* No timeout wanted */
+ for(;;)
{
- #ifdef SUPPORT_TLS
- if (tls_out.active == fd) rc = tls_write(FALSE, block, len); else
- #endif
- rc = write(fd, block, len);
- save_errno = errno;
- }
-
- /* Timeout wanted. */
+ /* This code makes use of alarm() in order to implement the timeout. This
+ isn't a very tidy way of doing things. Using non-blocking I/O with select()
+ provides a neater approach. However, I don't know how to do this when TLS is
+ in use. */
- else
- {
- alarm(local_timeout);
- #ifdef SUPPORT_TLS
- if (tls_out.active == fd) rc = tls_write(FALSE, block, len); else
- #endif
- rc = write(fd, block, len);
- save_errno = errno;
- local_timeout = alarm(0);
- if (sigalrm_seen)
+ if (transport_write_timeout <= 0) /* No timeout wanted */
{
- errno = ETIMEDOUT;
- return FALSE;
+ rc = tpt_write(fd, block, len, more, tctx->options);
+ save_errno = errno;
+ }
+ else /* Timeout wanted. */
+ {
+ sigalrm_seen = FALSE;
+ ALARM(local_timeout);
+ rc = tpt_write(fd, block, len, more, tctx->options);
+ save_errno = errno;
+ local_timeout = ALARM_CLR(0);
+ if (sigalrm_seen)
+ {
+ errno = ETIMEDOUT;
+ return FALSE;
+ }
}
+
+ if (rc >= 0 || errno != ENOTCONN || connretry <= 0)
+ break;
+
+ poll_one_fd(fd, POLLOUT, -1); /* could set timeout? retval check? */
+ connretry--;
}
/* Hopefully, the most common case is success, so test that first. */
}
+BOOL
+transport_write_block(transport_ctx * tctx, uschar *block, int len, BOOL more)
+{
+if (!(tctx->options & topt_output_string))
+ return transport_write_block_fd(tctx, block, len, more);
+
+/* Write to expanding-string. NOTE: not NUL-terminated */
+
+if (!tctx->u.msg)
+ tctx->u.msg = string_get(1024);
+
+tctx->u.msg = string_catn(tctx->u.msg, block, len);
+return TRUE;
+}
+
+
/*************************************************
BOOL
transport_write_string(int fd, const char *format, ...)
{
+transport_ctx tctx = {{0}};
+gstring gs = { .size = big_buffer_size, .ptr = 0, .s = big_buffer };
va_list ap;
+
+/* Use taint-unchecked routines for writing into big_buffer, trusting
+that the result will never be expanded. */
+
va_start(ap, format);
-if (!string_vformat(big_buffer, big_buffer_size, format, ap))
+if (!string_vformat(&gs, SVFMT_TAINT_NOCHK, format, ap))
log_write(0, LOG_MAIN|LOG_PANIC_DIE, "overlong formatted string in transport");
va_end(ap);
-return transport_write_block(fd, big_buffer, Ustrlen(big_buffer));
+tctx.u.fd = fd;
+return transport_write_block(&tctx, gs.s, gs.ptr, FALSE);
}
+void
+transport_write_reset(int options)
+{
+if (!(options & topt_continuation)) chunk_ptr = deliver_out_buffer;
+nl_partial_match = -1;
+nl_check_length = nl_escape_length = 0;
+}
+
+
+
/*************************************************
* Write character chunk *
*************************************************/
chunk was NL, or matched part of the data that has to be escaped.
Arguments:
- fd file descript to write to
+ tctx transport context - processing to be done during output,
+ and file descriptor to write to
chunk pointer to data to write
len length of data to write
- usr_crlf TRUE if CR LF is wanted at the end of each line
In addition, the static nl_xxx variables must be set as required.
Returns: TRUE on success, FALSE on failure (with errno preserved)
*/
-static BOOL
-write_chunk(int fd, uschar *chunk, int len, BOOL use_crlf)
+BOOL
+write_chunk(transport_ctx * tctx, const uschar * chunk, int len)
{
-uschar *start = chunk;
-uschar *end = chunk + len;
-register uschar *ptr;
+const uschar * start = chunk;
+const uschar * end = chunk + len;
int mlen = DELIVER_OUT_BUFFER_SIZE - nl_escape_length - 2;
/* The assumption is made that the check string will never stretch over move
for possible escaping. The code for the non-NL route should be as fast as
possible. */
-for (ptr = start; ptr < end; ptr++)
+for (const uschar * ptr = start; ptr < end; ptr++)
{
- register int ch;
+ int ch, len;
/* Flush the buffer if it has reached the threshold - we want to leave enough
room for the next uschar, plus a possible extra CR for an LF, plus the escape
string. */
- if (chunk_ptr - deliver_out_buffer > mlen)
+ if ((len = chunk_ptr - deliver_out_buffer) > mlen)
{
- if (!transport_write_block(fd, deliver_out_buffer,
- chunk_ptr - deliver_out_buffer))
- return FALSE;
+ DEBUG(D_transport) debug_printf("flushing headers buffer\n");
+
+ /* If CHUNKING, prefix with BDAT (size) NON-LAST. Also, reap responses
+ from previous SMTP commands. */
+
+ if (tctx->options & topt_use_bdat && tctx->chunk_cb)
+ {
+ if ( tctx->chunk_cb(tctx, (unsigned)len, 0) != OK
+ || !transport_write_block(tctx, deliver_out_buffer, len, FALSE)
+ || tctx->chunk_cb(tctx, 0, tc_reap_prev) != OK
+ )
+ return FALSE;
+ }
+ else
+ if (!transport_write_block(tctx, deliver_out_buffer, len, FALSE))
+ return FALSE;
chunk_ptr = deliver_out_buffer;
}
+ /* Remove CR before NL if required */
+
+ if ( *ptr == '\r' && ptr[1] == '\n'
+ && !(tctx->options & topt_use_crlf)
+ && f.spool_file_wireformat
+ )
+ ptr++;
+
if ((ch = *ptr) == '\n')
{
int left = end - ptr - 1; /* count of chars left after NL */
/* Insert CR before NL if required */
- if (use_crlf) *chunk_ptr++ = '\r';
+ if (tctx->options & topt_use_crlf && !f.spool_file_wireformat)
+ *chunk_ptr++ = '\r';
*chunk_ptr++ = '\n';
transport_newlines++;
Returns: a string
*/
-uschar *
+const uschar *
transport_rcpt_address(address_item *addr, BOOL include_affixes)
{
uschar *at;
return addr->address;
}
-if (addr->suffix == NULL)
+if (!addr->suffix)
{
- if (addr->prefix == NULL) return addr->address;
+ if (!addr->prefix) return addr->address;
return addr->address + Ustrlen(addr->prefix);
}
at = Ustrrchr(addr->address, '@');
-plen = (addr->prefix == NULL)? 0 : Ustrlen(addr->prefix);
+plen = addr->prefix ? Ustrlen(addr->prefix) : 0;
slen = Ustrlen(addr->suffix);
-return string_sprintf("%.*s@%s", (at - addr->address - plen - slen),
+return string_sprintf("%.*s@%s", (int)(at - addr->address - plen - slen),
addr->address + plen, at + 1);
}
pplist address of anchor of the list of addresses not to output
pdlist address of anchor of the list of processed addresses
first TRUE if this is the first address; set it FALSE afterwards
- fd the file descriptor to write to
- use_crlf to be passed on to write_chunk()
+ tctx transport context - processing to be done during output
+ and the file descriptor to write to
Returns: FALSE if writing failed
*/
static BOOL
write_env_to(address_item *p, struct aci **pplist, struct aci **pdlist,
- BOOL *first, int fd, BOOL use_crlf)
+ BOOL *first, transport_ctx * tctx)
{
address_item *pp;
struct aci *ppp;
/* Do nothing if we have already handled this address. If not, remember it
so that we don't handle it again. */
-for (ppp = *pdlist; ppp != NULL; ppp = ppp->next)
- { if (p == ppp->ptr) return TRUE; }
+for (ppp = *pdlist; ppp; ppp = ppp->next) if (p == ppp->ptr) return TRUE;
-ppp = store_get(sizeof(struct aci));
+ppp = store_get(sizeof(struct aci), GET_UNTAINTED);
ppp->next = *pdlist;
*pdlist = ppp;
ppp->ptr = p;
for (pp = p;; pp = pp->parent)
{
address_item *dup;
- for (dup = addr_duplicate; dup != NULL; dup = dup->next)
- {
- if (dup->dupof != pp) continue; /* Not a dup of our address */
- if (!write_env_to(dup, pplist, pdlist, first, fd, use_crlf)) return FALSE;
- }
- if (pp->parent == NULL) break;
+ for (dup = addr_duplicate; dup; dup = dup->next)
+ if (dup->dupof == pp) /* a dup of our address */
+ if (!write_env_to(dup, pplist, pdlist, first, tctx))
+ return FALSE;
+ if (!pp->parent) break;
}
/* Check to see if we have already output the progenitor. */
-for (ppp = *pplist; ppp != NULL; ppp = ppp->next)
- { if (pp == ppp->ptr) break; }
-if (ppp != NULL) return TRUE;
+for (ppp = *pplist; ppp; ppp = ppp->next) if (pp == ppp->ptr) break;
+if (ppp) return TRUE;
/* Remember what we have output, and output it. */
-ppp = store_get(sizeof(struct aci));
+ppp = store_get(sizeof(struct aci), GET_UNTAINTED);
ppp->next = *pplist;
*pplist = ppp;
ppp->ptr = pp;
-if (!(*first) && !write_chunk(fd, US",\n ", 3, use_crlf)) return FALSE;
+if (!*first && !write_chunk(tctx, US",\n ", 3)) return FALSE;
*first = FALSE;
-return write_chunk(fd, pp->address, Ustrlen(pp->address), use_crlf);
+return write_chunk(tctx, pp->address, Ustrlen(pp->address));
}
-/* Add/remove/rewwrite headers, and send them plus the empty-line sparator.
+/* Add/remove/rewrite headers, and send them plus the empty-line separator.
Globals:
header_list
Arguments:
addr (chain of) addresses (for extra headers), or NULL;
only the first address is used
- fd file descriptor to write the message to
- sendfn function for output
- use_crlf turn NL into CR LF
- rewrite_rules chain of header rewriting rules
- rewrite_existflags flags for the rewriting rules
+ tctx transport context
+ sendfn function for output (transport or verify)
Returns: TRUE on success; FALSE on failure.
*/
BOOL
-transport_headers_send(address_item *addr, int fd, uschar *add_headers, uschar *remove_headers,
- BOOL (*sendfn)(int fd, uschar * s, int len, BOOL use_crlf),
- BOOL use_crlf, rewrite_rule *rewrite_rules, int rewrite_existflags)
+transport_headers_send(transport_ctx * tctx,
+ BOOL (*sendfn)(transport_ctx * tctx, const uschar * s, int len))
{
-header_line *h;
+const uschar * list;
+transport_instance * tblock = tctx ? tctx->tblock : NULL;
+address_item * addr = tctx ? tctx->addr : NULL;
/* Then the message's headers. Don't write any that are flagged as "old";
that means they were rewritten, or are a record of envelope rewriting, or
separately and squash any empty ones.
Then check addr->prop.remove_headers too, provided that addr is not NULL. */
-for (h = header_list; h != NULL; h = h->next) if (h->type != htype_old)
+for (header_line * h = header_list; h; h = h->next) if (h->type != htype_old)
{
- int i;
- const uschar *list = remove_headers;
-
BOOL include_header = TRUE;
- for (i = 0; i < 2; i++) /* For remove_headers && addr->prop.remove_headers */
+ list = tblock ? tblock->remove_headers : NULL;
+ for (int i = 0; i < 2; i++) /* For remove_headers && addr->prop.remove_headers */
{
if (list)
{
int len;
if (i == 0)
- if (!(s = expand_string(s)) && !expand_string_forcedfail)
+ if (!(s = expand_string(s)) && !f.expand_string_forcedfail)
{
errno = ERRNO_CHHEADER_FAIL;
return FALSE;
}
len = s ? Ustrlen(s) : 0;
- if (strncmpic(h->text, s, len) != 0) continue;
- ss = h->text + len;
- while (*ss == ' ' || *ss == '\t') ss++;
- if (*ss == ':') break;
+ if (len && s[len-1] == '*') /* trailing glob */
+ {
+ if (strncmpic(h->text, s, len-1) == 0) break;
+ }
+ else
+ {
+ if (strncmpic(h->text, s, len) != 0) continue;
+ ss = h->text + len;
+ while (*ss == ' ' || *ss == '\t') ss++;
+ if (*ss == ':') break;
+ }
}
- if (s != NULL) { include_header = FALSE; break; }
+ if (s) { include_header = FALSE; break; }
}
- if (addr != NULL) list = addr->prop.remove_headers;
+ if (addr) list = addr->prop.remove_headers;
}
/* If this header is to be output, try to rewrite it if there are rewriting
if (include_header)
{
- if (rewrite_rules)
+ int len;
+ if (tblock && tblock->rewrite_rules)
{
- void *reset_point = store_get(0);
- header_line *hh;
+ rmark reset_point = store_mark();
+ header_line * hh;
- if ((hh = rewrite_header(h, NULL, NULL, rewrite_rules, rewrite_existflags, FALSE)))
+ if ((hh = rewrite_header(h, NULL, NULL, tblock->rewrite_rules,
+ tblock->rewrite_existflags, FALSE)))
{
- if (!sendfn(fd, hh->text, hh->slen, use_crlf)) return FALSE;
+ len = hh->slen;
+ if (tctx->options & topt_truncate_headers && len > 998) len = 998;
+ if (!sendfn(tctx, hh->text, len)) return FALSE;
store_reset(reset_point);
continue; /* With the next header line */
}
/* Either no rewriting rules, or it didn't get rewritten */
- if (!sendfn(fd, h->text, h->slen, use_crlf)) return FALSE;
+ len = h->slen;
+ if (tctx->options & topt_truncate_headers && len > 998) len = 998;
+ if (!sendfn(tctx, h->text, len)) return FALSE;
}
/* Header removed */
else
- {
- DEBUG(D_transport) debug_printf("removed header line:\n%s---\n", h->text);
- }
+ DEBUG(D_transport) debug_printf("removed header line:\n %s---\n", h->text);
}
/* Add on any address-specific headers. If there are multiple addresses,
if (addr)
{
- int i;
- header_line *hprev = addr->prop.extra_headers;
- header_line *hnext;
- for (i = 0; i < 2; i++)
- {
- for (h = hprev, hprev = NULL; h != NULL; h = hnext)
+ header_line * hprev = addr->prop.extra_headers, * hnext, * h;
+
+ for (int i = 0; i < 2; i++)
+ for (h = hprev, hprev = NULL; h; h = hnext)
{
hnext = h->next;
h->next = hprev;
hprev = h;
if (i == 1)
{
- if (!sendfn(fd, h->text, h->slen, use_crlf)) return FALSE;
+ if (!sendfn(tctx, h->text, h->slen)) return FALSE;
DEBUG(D_transport)
- debug_printf("added header line(s):\n%s---\n", h->text);
+ debug_printf("added header line(s):\n %s---\n", h->text);
}
}
- }
}
/* If a string containing additional headers exists it is a newline-sep
noops. An added header string from a transport may not end with a newline;
add one if it does not. */
-if (add_headers)
+if (tblock && (list = CUS tblock->add_headers))
{
int sep = '\n';
uschar * s;
- while ((s = string_nextinlist(CUSS &add_headers, &sep, NULL, 0)))
- if (!(s = expand_string(s)))
- {
- if (!expand_string_forcedfail)
- { errno = ERRNO_CHHEADER_FAIL; return FALSE; }
- }
- else
+ while ((s = string_nextinlist(&list, &sep, NULL, 0)))
+ if ((s = expand_string(s)))
{
int len = Ustrlen(s);
if (len > 0)
{
- if (!sendfn(fd, s, len, use_crlf)) return FALSE;
- if (s[len-1] != '\n' && !sendfn(fd, US"\n", 1, use_crlf))
+ if (!sendfn(tctx, s, len)) return FALSE;
+ if (s[len-1] != '\n' && !sendfn(tctx, US"\n", 1))
return FALSE;
DEBUG(D_transport)
{
- debug_printf("added header line:\n%s", s);
+ debug_printf("added header line:\n %s", s);
if (s[len-1] != '\n') debug_printf("\n");
debug_printf("---\n");
}
}
}
+ else if (!f.expand_string_forcedfail)
+ { errno = ERRNO_CHHEADER_FAIL; return FALSE; }
}
/* Separate headers from body with a blank line */
-return sendfn(fd, US"\n", 1, use_crlf);
+return sendfn(tctx, US"\n", 1);
}
transport_write_timeout non-zero.
Arguments:
- addr (chain of) addresses (for extra headers), or NULL;
+ tctx
+ (fd, msg) Either an fd, to write the message to,
+ or a string: if null write message to allocated space
+ otherwire take content as headers.
+ addr (chain of) addresses (for extra headers), or NULL;
only the first address is used
- fd file descriptor to write the message to
- options bit-wise options:
- add_return_path if TRUE, add a "return-path" header
- add_envelope_to if TRUE, add a "envelope-to" header
- add_delivery_date if TRUE, add a "delivery-date" header
- use_crlf if TRUE, turn NL into CR LF
- end_dot if TRUE, send a terminating "." line at the end
- no_headers if TRUE, omit the headers
- no_body if TRUE, omit the body
- size_limit if > 0, this is a limit to the size of message written;
- it is used when returning messages to their senders,
- and is approximate rather than exact, owing to chunk
- buffering
- add_headers a string containing one or more headers to add; it is
- expanded, and must be in correct RFC 822 format as
- it is transmitted verbatim; NULL => no additions,
- and so does empty string or forced expansion fail
- remove_headers a colon-separated list of headers to remove, or NULL
- check_string a string to check for at the start of lines, or NULL
- escape_string a string to insert in front of any check string
- rewrite_rules chain of header rewriting rules
- rewrite_existflags flags for the rewriting rules
+ tblock optional transport instance block (NULL signifies NULL/0):
+ add_headers a string containing one or more headers to add; it is
+ expanded, and must be in correct RFC 822 format as
+ it is transmitted verbatim; NULL => no additions,
+ and so does empty string or forced expansion fail
+ remove_headers a colon-separated list of headers to remove, or NULL
+ rewrite_rules chain of header rewriting rules
+ rewrite_existflags flags for the rewriting rules
+ options bit-wise options:
+ add_return_path if TRUE, add a "return-path" header
+ add_envelope_to if TRUE, add a "envelope-to" header
+ add_delivery_date if TRUE, add a "delivery-date" header
+ use_crlf if TRUE, turn NL into CR LF
+ end_dot if TRUE, send a terminating "." line at the end
+ no_flush if TRUE, do not flush at end
+ no_headers if TRUE, omit the headers
+ no_body if TRUE, omit the body
+ check_string a string to check for at the start of lines, or NULL
+ escape_string a string to insert in front of any check string
+ size_limit if > 0, this is a limit to the size of message written;
+ it is used when returning messages to their senders,
+ and is approximate rather than exact, owing to chunk
+ buffering
Returns: TRUE on success; FALSE (with errno) on failure.
In addition, the global variable transport_count
*/
static BOOL
-internal_transport_write_message(address_item *addr, int fd, int options,
- int size_limit, uschar *add_headers, uschar *remove_headers, uschar *check_string,
- uschar *escape_string, rewrite_rule *rewrite_rules, int rewrite_existflags)
+internal_transport_write_message(transport_ctx * tctx, int size_limit)
{
-int written = 0;
-int len;
-BOOL use_crlf = (options & topt_use_crlf) != 0;
+int len, size = 0;
/* Initialize pointer in output buffer. */
-chunk_ptr = deliver_out_buffer;
+transport_write_reset(tctx->options);
/* Set up the data for start-of-line data checking and escaping */
-nl_partial_match = -1;
-if (check_string != NULL && escape_string != NULL)
+if (tctx->check_string && tctx->escape_string)
{
- nl_check = check_string;
+ nl_check = tctx->check_string;
nl_check_length = Ustrlen(nl_check);
- nl_escape = escape_string;
+ nl_escape = tctx->escape_string;
nl_escape_length = Ustrlen(nl_escape);
}
-else nl_check_length = nl_escape_length = 0;
/* Whether the escaping mechanism is applied to headers or not is controlled by
an option (set for SMTP, not otherwise). Negate the length if not wanted till
after the headers. */
-if ((options & topt_escape_headers) == 0) nl_check_length = -nl_check_length;
+if (!(tctx->options & topt_escape_headers))
+ nl_check_length = -nl_check_length;
/* Write the headers if required, including any that have to be added. If there
-are header rewriting rules, apply them. */
+are header rewriting rules, apply them. The datasource is not the -D spoolfile
+so temporarily hide the global that adjusts for its format. */
-if ((options & topt_no_headers) == 0)
+if (!(tctx->options & topt_no_headers))
{
+ BOOL save_wireformat = f.spool_file_wireformat;
+ f.spool_file_wireformat = FALSE;
+
/* Add return-path: if requested. */
- if ((options & topt_add_return_path) != 0)
+ if (tctx->options & topt_add_return_path)
{
- uschar buffer[ADDRESS_MAXLENGTH + 20];
- sprintf(CS buffer, "Return-path: <%.*s>\n", ADDRESS_MAXLENGTH,
- return_path);
- if (!write_chunk(fd, buffer, Ustrlen(buffer), use_crlf)) return FALSE;
+ int n;
+ uschar * s = string_sprintf("Return-path: <%.*s>\n%n",
+ EXIM_EMAILADDR_MAX, return_path, &n);
+ if (!write_chunk(tctx, s, n)) goto bad;
}
/* Add envelope-to: if requested */
- if ((options & topt_add_envelope_to) != 0)
+ if (tctx->options & topt_add_envelope_to)
{
BOOL first = TRUE;
- address_item *p;
struct aci *plist = NULL;
struct aci *dlist = NULL;
- void *reset_point = store_get(0);
+ rmark reset_point = store_mark();
- if (!write_chunk(fd, US"Envelope-to: ", 13, use_crlf)) return FALSE;
+ if (!write_chunk(tctx, US"Envelope-to: ", 13)) goto bad;
/* Pick up from all the addresses. The plist and dlist variables are
anchors for lists of addresses already handled; they have to be defined at
- this level becuase write_env_to() calls itself recursively. */
+ this level because write_env_to() calls itself recursively. */
- for (p = addr; p != NULL; p = p->next)
- {
- if (!write_env_to(p, &plist, &dlist, &first, fd, use_crlf)) return FALSE;
- }
+ for (address_item * p = tctx->addr; p; p = p->next)
+ if (!write_env_to(p, &plist, &dlist, &first, tctx))
+ goto bad;
/* Add a final newline and reset the store used for tracking duplicates */
- if (!write_chunk(fd, US"\n", 1, use_crlf)) return FALSE;
+ if (!write_chunk(tctx, US"\n", 1)) goto bad;
store_reset(reset_point);
}
/* Add delivery-date: if requested. */
- if ((options & topt_add_delivery_date) != 0)
+ if (tctx->options & topt_add_delivery_date)
{
- uschar buffer[100];
- sprintf(CS buffer, "Delivery-date: %s\n", tod_stamp(tod_full));
- if (!write_chunk(fd, buffer, Ustrlen(buffer), use_crlf)) return FALSE;
+ uschar * s = tod_stamp(tod_full);
+
+ if ( !write_chunk(tctx, US"Delivery-date: ", 15)
+ || !write_chunk(tctx, s, Ustrlen(s))
+ || !write_chunk(tctx, US"\n", 1)) goto bad;
}
/* Then the message's headers. Don't write any that are flagged as "old";
were removed (e.g. Bcc). If remove_headers is not null, skip any headers that
match any entries therein. Then check addr->prop.remove_headers too, provided that
addr is not NULL. */
- if (!transport_headers_send(addr, fd, add_headers, remove_headers, &write_chunk,
- use_crlf, rewrite_rules, rewrite_existflags))
- return FALSE;
- }
-/* If the body is required, ensure that the data for check strings (formerly
-the "from hack") is enabled by negating the length if necessary. (It will be
-negative in cases where it isn't to apply to the headers). Then ensure the body
-is positioned at the start of its file (following the message id), then write
-it, applying the size limit if required. */
-
-if ((options & topt_no_body) == 0)
- {
- nl_check_length = abs(nl_check_length);
- nl_partial_match = 0;
- if (lseek(deliver_datafile, SPOOL_DATA_START_OFFSET, SEEK_SET) < 0)
- return FALSE;
- while ((len = read(deliver_datafile, deliver_in_buffer,
- DELIVER_IN_BUFFER_SIZE)) > 0)
+ if (!transport_headers_send(tctx, &write_chunk))
{
- if (!write_chunk(fd, deliver_in_buffer, len, use_crlf)) return FALSE;
- if (size_limit > 0)
- {
- written += len;
- if (written > size_limit)
- {
- len = 0; /* Pretend EOF */
- break;
- }
- }
+bad:
+ f.spool_file_wireformat = save_wireformat;
+ return FALSE;
}
- /* A read error on the body will have left len == -1 and errno set. */
-
- if (len != 0) return FALSE;
+ f.spool_file_wireformat = save_wireformat;
}
-/* Finished with the check string */
-
-nl_check_length = nl_escape_length = 0;
-
-/* If requested, add a terminating "." line (SMTP output). */
-
-if ((options & topt_end_dot) != 0 && !write_chunk(fd, US".\n", 2, use_crlf))
- return FALSE;
-
-/* Write out any remaining data in the buffer before returning. */
-
-return (len = chunk_ptr - deliver_out_buffer) <= 0 ||
- transport_write_block(fd, deliver_out_buffer, len);
-}
+/* When doing RFC3030 CHUNKING output, work out how much data would be in a
+last-BDAT, consisting of the current write_chunk() output buffer fill
+(optimally, all of the headers - but it does not matter if we already had to
+flush that buffer with non-last BDAT prependix) plus the amount of body data
+(as expanded for CRLF lines). Then create and write BDAT(s), and ensure
+that further use of write_chunk() will not prepend BDATs.
+The first BDAT written will also first flush any outstanding MAIL and RCPT
+commands which were buffered thans to PIPELINING.
+Commands go out (using a send()) from a different buffer to data (using a
+write()). They might not end up in the same TCP segment, which is
+suboptimal. */
+
+if (tctx->options & topt_use_bdat)
+ {
+ off_t fsize;
+ int hsize;
+ if ((hsize = chunk_ptr - deliver_out_buffer) < 0)
+ hsize = 0;
+ if (!(tctx->options & topt_no_body))
+ {
+ if ((fsize = lseek(deliver_datafile, 0, SEEK_END)) < 0) return FALSE;
+ fsize -= spool_data_start_offset(message_id);
+ if (size_limit > 0 && fsize > size_limit)
+ fsize = size_limit;
+ size = hsize + fsize;
+ if (tctx->options & topt_use_crlf && !f.spool_file_wireformat)
+ size += body_linecount; /* account for CRLF-expansion */
+
+ /* With topt_use_bdat we never do dot-stuffing; no need to
+ account for any expansion due to that. */
+ }
-#ifndef DISABLE_DKIM
+ /* If the message is large, emit first a non-LAST chunk with just the
+ headers, and reap the command responses. This lets us error out early
+ on RCPT rejects rather than sending megabytes of data. Include headers
+ on the assumption they are cheap enough and some clever implementations
+ might errorcheck them too, on-the-fly, and reject that chunk. */
-/***************************************************************************************************
-* External interface to write the message, while signing it with DKIM and/or Domainkeys *
-***************************************************************************************************/
+ if (size > DELIVER_OUT_BUFFER_SIZE && hsize > 0)
+ {
+ DEBUG(D_transport)
+ debug_printf("sending small initial BDAT; hsize=%d\n", hsize);
+ if ( tctx->chunk_cb(tctx, hsize, 0) != OK
+ || !transport_write_block(tctx, deliver_out_buffer, hsize, FALSE)
+ || tctx->chunk_cb(tctx, 0, tc_reap_prev) != OK
+ )
+ return FALSE;
+ chunk_ptr = deliver_out_buffer;
+ size -= hsize;
+ }
-/* This function is a wrapper around transport_write_message().
- It is only called from the smtp transport if DKIM or Domainkeys support
- is compiled in. The function sets up a replacement fd into a -K file,
- then calls the normal function. This way, the exact bits that exim would
- have put "on the wire" will end up in the file (except for TLS
- encapsulation, which is the very very last thing). When we are done
- signing the file, send the signed message down the original fd (or TLS fd).
+ /* Emit a LAST datachunk command, and unmark the context for further
+ BDAT commands. */
-Arguments:
- as for internal_transport_write_message() above, with additional arguments:
- uschar *dkim_private_key DKIM: The private key to use (filename or
- plain data)
- uschar *dkim_domain DKIM: The domain to use
- uschar *dkim_selector DKIM: The selector to use.
- uschar *dkim_canon DKIM: The canonalization scheme to use,
- "simple" or "relaxed"
- uschar *dkim_strict DKIM: What to do if signing fails:
- 1/true => throw error
- 0/false => send anyway
- uschar *dkim_sign_headers DKIM: List of headers that should be included
- in signature generation
+ if (tctx->chunk_cb(tctx, size, tc_chunk_last) != OK)
+ return FALSE;
+ tctx->options &= ~topt_use_bdat;
+ }
-Returns: TRUE on success; FALSE (with errno) for any failure
-*/
+/* If the body is required, ensure that the data for check strings (formerly
+the "from hack") is enabled by negating the length if necessary. (It will be
+negative in cases where it isn't to apply to the headers). Then ensure the body
+is positioned at the start of its file (following the message id), then write
+it, applying the size limit if required. */
-BOOL
-dkim_transport_write_message(address_item *addr, int fd, int options,
- int size_limit, uschar *add_headers, uschar *remove_headers,
- uschar *check_string, uschar *escape_string, rewrite_rule *rewrite_rules,
- int rewrite_existflags, uschar *dkim_private_key, uschar *dkim_domain,
- uschar *dkim_selector, uschar *dkim_canon, uschar *dkim_strict, uschar *dkim_sign_headers
- )
-{
-int dkim_fd;
-int save_errno = 0;
-BOOL rc;
-uschar dkim_spool_name[256];
-char sbuf[2048];
-int sread = 0;
-int wwritten = 0;
-uschar *dkim_signature = NULL;
-
-/* If we can't sign, just call the original function. */
-
-if (!(dkim_private_key && dkim_domain && dkim_selector))
- return transport_write_message(addr, fd, options,
- size_limit, add_headers, remove_headers,
- check_string, escape_string, rewrite_rules,
- rewrite_existflags);
-
-(void)string_format(dkim_spool_name, 256, "%s/input/%s/%s-%d-K",
- spool_directory, message_subdir, message_id, (int)getpid());
-
-if ((dkim_fd = Uopen(dkim_spool_name, O_RDWR|O_CREAT|O_TRUNC, SPOOL_MODE)) < 0)
+/* If we have a wireformat -D file (CRNL lines, non-dotstuffed, no ending dot)
+and we want to send a body without dotstuffing or ending-dot, in-clear,
+then we can just dump it using sendfile.
+This should get used for CHUNKING output and also for writing the -K file for
+dkim signing, when we had CHUNKING input. */
+
+#ifdef OS_SENDFILE
+if ( f.spool_file_wireformat
+ && !(tctx->options & (topt_no_body | topt_end_dot))
+ && !nl_check_length
+ && tls_out.active.sock != tctx->u.fd
+ )
{
- /* Can't create spool file. Ugh. */
- rc = FALSE;
- save_errno = errno;
- goto CLEANUP;
- }
+ ssize_t copied = 0;
+ off_t offset = spool_data_start_offset(message_id);
-/* Call original function to write the -K file */
+ /* Write out any header data in the buffer */
-rc = transport_write_message(addr, dkim_fd, options,
- size_limit, add_headers, remove_headers,
- check_string, escape_string, rewrite_rules,
- rewrite_existflags);
+ if ((len = chunk_ptr - deliver_out_buffer) > 0)
+ {
+ if (!transport_write_block(tctx, deliver_out_buffer, len, TRUE))
+ return FALSE;
+ size -= len;
+ }
-/* Save error state. We must clean up before returning. */
-if (!rc)
- {
- save_errno = errno;
- goto CLEANUP;
- }
+ DEBUG(D_transport) debug_printf("using sendfile for body\n");
-if (dkim_private_key && dkim_domain && dkim_selector)
- {
- /* Rewind file and feed it to the goats^W DKIM lib */
- lseek(dkim_fd, 0, SEEK_SET);
- dkim_signature = dkim_exim_sign(dkim_fd,
- dkim_private_key,
- dkim_domain,
- dkim_selector,
- dkim_canon,
- dkim_sign_headers);
- if (!dkim_signature)
+ while(size > 0)
{
- if (dkim_strict)
- {
- uschar *dkim_strict_result = expand_string(dkim_strict);
- if (dkim_strict_result)
- if ( (strcmpic(dkim_strict,US"1") == 0) ||
- (strcmpic(dkim_strict,US"true") == 0) )
- {
- /* Set errno to something halfway meaningful */
- save_errno = EACCES;
- log_write(0, LOG_MAIN, "DKIM: message could not be signed,"
- " and dkim_strict is set. Deferring message delivery.");
- rc = FALSE;
- goto CLEANUP;
- }
- }
+ if ((copied = os_sendfile(tctx->u.fd, deliver_datafile, &offset, size)) <= 0) break;
+ size -= copied;
}
- else
- {
- int siglen = Ustrlen(dkim_signature);
- while(siglen > 0)
- {
-#ifdef SUPPORT_TLS
- wwritten = tls_out.active == fd
- ? tls_write(FALSE, dkim_signature, siglen)
- : write(fd, dkim_signature, siglen);
+ return copied >= 0;
+ }
#else
- wwritten = write(fd, dkim_signature, siglen);
+DEBUG(D_transport) debug_printf("cannot use sendfile for body: no support\n");
#endif
- if (wwritten == -1)
- {
- /* error, bail out */
- save_errno = errno;
- rc = FALSE;
- goto CLEANUP;
- }
- siglen -= wwritten;
- dkim_signature += wwritten;
- }
- }
- }
-#ifdef HAVE_LINUX_SENDFILE
-/* We can use sendfile() to shove the file contents
- to the socket. However only if we don't use TLS,
- as then there's another layer of indirection
- before the data finally hits the socket. */
-if (tls_out.active != fd)
+DEBUG(D_transport)
+ if (!(tctx->options & topt_no_body))
+ debug_printf("cannot use sendfile for body: %s\n",
+ !f.spool_file_wireformat ? "spoolfile not wireformat"
+ : tctx->options & topt_end_dot ? "terminating dot wanted"
+ : nl_check_length ? "dot- or From-stuffing wanted"
+ : "TLS output wanted");
+
+if (!(tctx->options & topt_no_body))
{
- off_t size = lseek(dkim_fd, 0, SEEK_END); /* Fetch file size */
- ssize_t copied = 0;
- off_t offset = 0;
+ unsigned long size = size_limit > 0 ? size_limit : ULONG_MAX;
- /* Rewind file */
- lseek(dkim_fd, 0, SEEK_SET);
-
- while(copied >= 0 && offset < size)
- copied = sendfile(fd, dkim_fd, &offset, size - offset);
- if (copied < 0)
+ nl_check_length = abs(nl_check_length);
+ nl_partial_match = 0;
+ if (lseek(deliver_datafile, spool_data_start_offset(message_id), SEEK_SET) < 0)
+ return FALSE;
+ while ( (len = MIN(DELIVER_IN_BUFFER_SIZE, size)) > 0
+ && (len = read(deliver_datafile, deliver_in_buffer, len)) > 0)
{
- save_errno = errno;
- rc = FALSE;
+ if (!write_chunk(tctx, deliver_in_buffer, len))
+ return FALSE;
+ size -= len;
}
- }
-else
-#endif
+ /* A read error on the body will have left len == -1 and errno set. */
- {
- /* Rewind file */
- lseek(dkim_fd, 0, SEEK_SET);
+ if (len != 0) return FALSE;
+ }
- /* Send file down the original fd */
- while((sread = read(dkim_fd, sbuf, 2048)) > 0)
- {
- char *p = sbuf;
- /* write the chunk */
+/* Finished with the check string, and spool-format consideration */
- while (sread)
- {
-#ifdef SUPPORT_TLS
- wwritten = tls_out.active == fd
- ? tls_write(FALSE, US p, sread)
- : write(fd, p, sread);
-#else
- wwritten = write(fd, p, sread);
-#endif
- if (wwritten == -1)
- {
- /* error, bail out */
- save_errno = errno;
- rc = FALSE;
- goto CLEANUP;
- }
- p += wwritten;
- sread -= wwritten;
- }
- }
+nl_check_length = nl_escape_length = 0;
+f.spool_file_wireformat = FALSE;
- if (sread == -1)
- {
- save_errno = errno;
- rc = FALSE;
- }
+/* If requested, add a terminating "." line (SMTP output). */
+
+if (tctx->options & topt_end_dot)
+ {
+ smtp_debug_cmd(US".", 0);
+ if (!write_chunk(tctx, US".\n", 2))
+ return FALSE;
}
-CLEANUP:
-/* unlink -K file */
-(void)close(dkim_fd);
-Uunlink(dkim_spool_name);
-errno = save_errno;
-return rc;
+/* Write out any remaining data in the buffer before returning. */
+
+return (len = chunk_ptr - deliver_out_buffer) <= 0
+ || transport_write_block(tctx, deliver_out_buffer, len,
+ !!(tctx->options & topt_no_flush));
}
-#endif
the real work, passing over all the arguments from this function. Otherwise,
set up a filtering process, fork another process to call the internal function
to write to the filter, and in this process just suck from the filter and write
-down the given fd. At the end, tidy up the pipes and the processes.
+down the fd in the transport context. At the end, tidy up the pipes and the
+processes.
Arguments: as for internal_transport_write_message() above
*/
BOOL
-transport_write_message(address_item *addr, int fd, int options,
- int size_limit, uschar *add_headers, uschar *remove_headers,
- uschar *check_string, uschar *escape_string, rewrite_rule *rewrite_rules,
- int rewrite_existflags)
+transport_write_message(transport_ctx * tctx, int size_limit)
{
-BOOL use_crlf;
BOOL last_filter_was_NL = TRUE;
-int rc, len, yield, fd_read, fd_write, save_errno;
+BOOL save_spool_file_wireformat = f.spool_file_wireformat;
+BOOL yield;
+int rc, len, fd_read, fd_write, save_errno;
int pfd[2] = {-1, -1};
pid_t filter_pid, write_pid;
-transport_filter_timed_out = FALSE;
+f.transport_filter_timed_out = FALSE;
/* If there is no filter command set up, call the internal function that does
the actual work, passing it the incoming fd, and return its result. */
|| !*transport_filter_argv
|| !**transport_filter_argv
)
- return internal_transport_write_message(addr, fd, options, size_limit,
- add_headers, remove_headers, check_string, escape_string,
- rewrite_rules, rewrite_existflags);
+ return internal_transport_write_message(tctx, size_limit);
/* Otherwise the message must be written to a filter process and read back
before being written to the incoming fd. First set up the special processing to
be done during the copying. */
-use_crlf = (options & topt_use_crlf) != 0;
nl_partial_match = -1;
-if (check_string != NULL && escape_string != NULL)
+if (tctx->check_string && tctx->escape_string)
{
- nl_check = check_string;
+ nl_check = tctx->check_string;
nl_check_length = Ustrlen(nl_check);
- nl_escape = escape_string;
+ nl_escape = tctx->escape_string;
nl_escape_length = Ustrlen(nl_escape);
}
else nl_check_length = nl_escape_length = 0;
yield = FALSE;
write_pid = (pid_t)(-1);
-(void)fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC);
-filter_pid = child_open(USS transport_filter_argv, NULL, 077,
- &fd_write, &fd_read, FALSE);
-(void)fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) & ~FD_CLOEXEC);
+ {
+ int bits = fcntl(tctx->u.fd, F_GETFD);
+ (void) fcntl(tctx->u.fd, F_SETFD, bits | FD_CLOEXEC);
+ filter_pid = child_open(USS transport_filter_argv, NULL, 077,
+ &fd_write, &fd_read, FALSE, US"transport-filter");
+ (void) fcntl(tctx->u.fd, F_SETFD, bits & ~FD_CLOEXEC);
+ }
if (filter_pid < 0) goto TIDY_UP; /* errno set */
DEBUG(D_transport)
- debug_printf("process %d running as transport filter: write=%d read=%d\n",
+ debug_printf("process %d running as transport filter: fd_write=%d fd_read=%d\n",
(int)filter_pid, fd_write, fd_read);
/* Fork subprocess to write the message to the filter, and return the result
smtp dots, or check string processing. */
if (pipe(pfd) != 0) goto TIDY_UP; /* errno set */
-if ((write_pid = fork()) == 0)
+if ((write_pid = exim_fork(US"tpt-filter-writer")) == 0)
{
BOOL rc;
(void)close(fd_read);
(void)close(pfd[pipe_read]);
nl_check_length = nl_escape_length = 0;
- rc = internal_transport_write_message(addr, fd_write,
- (options & ~(topt_use_crlf | topt_end_dot)),
- size_limit, add_headers, remove_headers, NULL, NULL,
- rewrite_rules, rewrite_existflags);
+
+ tctx->u.fd = fd_write;
+ tctx->check_string = tctx->escape_string = NULL;
+ tctx->options &= ~(topt_use_crlf | topt_end_dot | topt_use_bdat | topt_no_flush);
+
+ rc = internal_transport_write_message(tctx, size_limit);
+
save_errno = errno;
if ( write(pfd[pipe_write], (void *)&rc, sizeof(BOOL))
!= sizeof(BOOL)
|| write(pfd[pipe_write], (void *)&save_errno, sizeof(int))
!= sizeof(int)
- || write(pfd[pipe_write], (void *)&(addr->more_errno), sizeof(int))
+ || write(pfd[pipe_write], (void *)&tctx->addr->more_errno, sizeof(int))
!= sizeof(int)
+ || write(pfd[pipe_write], (void *)&tctx->addr->delivery_time, sizeof(struct timeval))
+ != sizeof(struct timeval)
)
rc = FALSE; /* compiler quietening */
- _exit(0);
+ exim_underbar_exit(EXIT_SUCCESS);
}
save_errno = errno;
/* When testing, let the subprocess get going */
-if (running_in_test_harness) millisleep(250);
+testharness_pause_ms(250);
DEBUG(D_transport)
debug_printf("process %d writing to transport filter\n", (int)write_pid);
/* Copy the output of the filter, remembering if the last character was NL. If
no data is returned, that counts as "ended with NL" (default setting of the
-variable is TRUE). */
+variable is TRUE). The output should always be unix-format as we converted
+any wireformat source on writing input to the filter. */
+f.spool_file_wireformat = FALSE;
chunk_ptr = deliver_out_buffer;
for (;;)
{
sigalrm_seen = FALSE;
- alarm(transport_filter_timeout);
+ ALARM(transport_filter_timeout);
len = read(fd_read, deliver_in_buffer, DELIVER_IN_BUFFER_SIZE);
- alarm(0);
+ ALARM_CLR(0);
if (sigalrm_seen)
{
+ DEBUG(D_transport) debug_printf("timed out reading from filter\n");
errno = ETIMEDOUT;
- transport_filter_timed_out = TRUE;
+ f.transport_filter_timed_out = TRUE;
goto TIDY_UP;
}
if (len > 0)
{
- if (!write_chunk(fd, deliver_in_buffer, len, use_crlf)) goto TIDY_UP;
+ if (!write_chunk(tctx, deliver_in_buffer, len)) goto TIDY_UP;
last_filter_was_NL = (deliver_in_buffer[len-1] == '\n');
}
sure. Also apply a paranoia timeout. */
TIDY_UP:
+f.spool_file_wireformat = save_spool_file_wireformat;
save_errno = errno;
(void)close(fd_read);
{
yield = FALSE;
save_errno = ERRNO_FILTER_FAIL;
- addr->more_errno = rc;
+ tctx->addr->more_errno = rc;
DEBUG(D_transport) debug_printf("filter process returned %d\n", rc);
}
{
rc = child_close(write_pid, 30);
if (yield)
- {
if (rc == 0)
{
BOOL ok;
- int dummy = read(pfd[pipe_read], (void *)&ok, sizeof(BOOL));
- if (!ok)
- {
- dummy = read(pfd[pipe_read], (void *)&save_errno, sizeof(int));
- dummy = read(pfd[pipe_read], (void *)&(addr->more_errno), sizeof(int));
+ if (read(pfd[pipe_read], (void *)&ok, sizeof(BOOL)) != sizeof(BOOL))
+ {
+ DEBUG(D_transport)
+ debug_printf("pipe read from writing process: %s\n", strerror(errno));
+ save_errno = ERRNO_FILTER_FAIL;
+ yield = FALSE;
+ }
+ else if (!ok)
+ { /* Try to drain the pipe; read fails are don't care */
+ int dummy = read(pfd[pipe_read], (void *)&save_errno, sizeof(int));
+ dummy = read(pfd[pipe_read], (void *)&tctx->addr->more_errno, sizeof(int));
+ dummy = read(pfd[pipe_read], (void *)&tctx->addr->delivery_time, sizeof(struct timeval));
yield = FALSE;
}
}
{
yield = FALSE;
save_errno = ERRNO_FILTER_FAIL;
- addr->more_errno = rc;
+ tctx->addr->more_errno = rc;
DEBUG(D_transport) debug_printf("writing process returned %d\n", rc);
}
- }
}
(void)close(pfd[pipe_read]);
if (yield)
{
nl_check_length = nl_escape_length = 0;
- if ((options & topt_end_dot) != 0 && (last_filter_was_NL?
- !write_chunk(fd, US".\n", 2, use_crlf) :
- !write_chunk(fd, US"\n.\n", 3, use_crlf)))
- {
- yield = FALSE;
- }
+ f.spool_file_wireformat = FALSE;
+ if ( tctx->options & topt_end_dot
+ && ( last_filter_was_NL
+ ? !write_chunk(tctx, US".\n", 2)
+ : !write_chunk(tctx, US"\n.\n", 3)
+ ) )
+ { smtp_debug_cmd(US".", 0); yield = FALSE; }
/* Write out any remaining data in the buffer. */
else
- {
- yield = (len = chunk_ptr - deliver_out_buffer) <= 0 ||
- transport_write_block(fd, deliver_out_buffer, len);
- }
+ yield = (len = chunk_ptr - deliver_out_buffer) <= 0
+ || transport_write_block(tctx, deliver_out_buffer, len, FALSE);
}
-else errno = save_errno; /* From some earlier error */
+else
+ errno = save_errno; /* From some earlier error */
DEBUG(D_transport)
{
debug_printf("end of filtering transport writing: yield=%d\n", yield);
if (!yield)
- debug_printf("errno=%d more_errno=%d\n", errno, addr->more_errno);
+ debug_printf(" errno=%d more_errno=%d\n", errno, tctx->addr->more_errno);
}
return yield;
*/
void
-transport_update_waiting(host_item *hostlist, uschar *tpname)
+transport_update_waiting(host_item * hostlist, uschar * tpname)
{
-uschar buffer[256];
const uschar *prevname = US"";
-host_item *host;
open_db dbblock;
open_db *dbm_file;
+if (!is_new_message_id(message_id))
+ {
+ DEBUG(D_transport) debug_printf("message_id %s is not new format; "
+ "skipping wait-%s database update\n", message_id, tpname);
+ return;
+ }
+
DEBUG(D_transport) debug_printf("updating wait-%s database\n", tpname);
/* Open the database for this transport */
-sprintf(CS buffer, "wait-%.200s", tpname);
-dbm_file = dbfn_open(buffer, O_RDWR, &dbblock, TRUE);
-if (dbm_file == NULL) return;
+if (!(dbm_file = dbfn_open(string_sprintf("wait-%.200s", tpname),
+ O_RDWR, &dbblock, TRUE, TRUE)))
+ return;
/* Scan the list of hosts for which this message is waiting, and ensure
that the message id is in each host record. */
-for (host = hostlist; host!= NULL; host = host->next)
+for (host_item * host = hostlist; host; host = host->next)
{
BOOL already = FALSE;
- dbdata_wait *host_record;
- uschar *s;
- int i, host_length;
+ dbdata_wait * host_record;
+ int host_length;
+ uschar buffer[256];
/* Skip if this is the same host as we just processed; otherwise remember
the name for next time. */
/* Look up the host record; if there isn't one, make an empty one. */
- host_record = dbfn_read(dbm_file, host->name);
- if (host_record == NULL)
+ if (!(host_record = dbfn_read(dbm_file, host->name)))
{
- host_record = store_get(sizeof(dbdata_wait) + MESSAGE_ID_LENGTH);
+ host_record = store_get(sizeof(dbdata_wait) + MESSAGE_ID_LENGTH, GET_UNTAINTED);
host_record->count = host_record->sequence = 0;
}
/* Search the record to see if the current message is already in it. */
- for (s = host_record->text; s < host_record->text + host_length;
+ for (uschar * s = host_record->text; s < host_record->text + host_length;
s += MESSAGE_ID_LENGTH)
{
+ /* If any ID is seen which is not new-format, wipe the record and
+ any continuations */
+
+ if (!is_new_message_id(s))
+ {
+ DEBUG(D_hints_lookup)
+ debug_printf_indent("NOTE: old or corrupt message-id found in wait=%.200s"
+ " hints DB; deleting records for %s\n", tpname, host->name);
+
+ (void) dbfn_delete(dbm_file, host->name);
+ for (int i = host_record->sequence - 1; i >= 0; i--)
+ (void) dbfn_delete(dbm_file,
+ (sprintf(CS buffer, "%.200s:%d", host->name, i), buffer));
+
+ host_record->count = host_record->sequence = 0;
+ break;
+ }
if (Ustrncmp(s, message_id, MESSAGE_ID_LENGTH) == 0)
{ already = TRUE; break; }
}
/* If we haven't found this message in the main record, search any
continuation records that exist. */
- for (i = host_record->sequence - 1; i >= 0 && !already; i--)
+ for (int i = host_record->sequence - 1; i >= 0 && !already; i--)
{
dbdata_wait *cont;
sprintf(CS buffer, "%.200s:%d", host->name, i);
- cont = dbfn_read(dbm_file, buffer);
- if (cont != NULL)
+ if ((cont = dbfn_read(dbm_file, buffer)))
{
int clen = cont->count * MESSAGE_ID_LENGTH;
- for (s = cont->text; s < cont->text + clen; s += MESSAGE_ID_LENGTH)
- {
+ for (uschar * s = cont->text; s < cont->text + clen; s += MESSAGE_ID_LENGTH)
if (Ustrncmp(s, message_id, MESSAGE_ID_LENGTH) == 0)
{ already = TRUE; break; }
- }
}
}
/* If this record is full, write it out with a new name constructed
from the sequence number, increase the sequence number, and empty
- the record. */
+ the record. If we're doing a two-phase queue run initial phase, ping the
+ daemon to consider running a delivery on this host. */
if (host_record->count >= WAIT_NAME_MAX)
{
sprintf(CS buffer, "%.200s:%d", host->name, host_record->sequence);
dbfn_write(dbm_file, buffer, host_record, sizeof(dbdata_wait) + host_length);
+#ifndef DISABLE_QUEUE_RAMP
+ if (f.queue_2stage && queue_fast_ramp && !queue_run_in_order)
+ queue_notify_daemon(message_id);
+#endif
host_record->sequence++;
host_record->count = 0;
host_length = 0;
else
{
dbdata_wait *newr =
- store_get(sizeof(dbdata_wait) + host_length + MESSAGE_ID_LENGTH);
+ store_get(sizeof(dbdata_wait) + host_length + MESSAGE_ID_LENGTH, GET_UNTAINTED);
memcpy(newr, host_record, sizeof(dbdata_wait) + host_length);
host_record = newr;
}
/* Update the database */
dbfn_write(dbm_file, host->name, host_record, sizeof(dbdata_wait) + host_length);
- DEBUG(D_transport) debug_printf("added to list for %s\n", host->name);
+ DEBUG(D_transport) debug_printf("added %.*s to queue for %s\n",
+ MESSAGE_ID_LENGTH, message_id, host->name);
}
/* All now done */
current continue sequence is greater than the maximum supplied as an argument,
or greater than the global connection_max_messages, which, if set, overrides.
+It is also called if conditions are otherwise right for pipelining a QUIT after
+the message data, since if there is another message waiting we do not want to
+send that QUIT.
+
Arguments:
transport_name name of the transport
hostname name of the host
local_message_max maximum number of messages down one connection
as set by the caller transport
new_message_id set to the message id of a waiting message
- more set TRUE if there are yet more messages waiting
oicf_func function to call to validate if it is ok to send
to this message_id from the current instance.
oicf_data opaque data for oicf_func
} msgq_t;
BOOL
-transport_check_waiting(const uschar *transport_name, const uschar *hostname,
- int local_message_max, uschar *new_message_id, BOOL *more, oicf oicf_func, void *oicf_data)
+transport_check_waiting(const uschar * transport_name, const uschar * hostname,
+ int local_message_max, uschar * new_message_id,
+ oicf oicf_func, void * oicf_data)
{
-dbdata_wait *host_record;
+dbdata_wait * host_record;
int host_length;
open_db dbblock;
-open_db *dbm_file;
-uschar buffer[256];
+open_db * dbm_file;
-msgq_t *msgq = NULL;
-int msgq_count = 0;
-int msgq_actual = 0;
int i;
-BOOL bFound = FALSE;
-uschar spool_dir [PATH_MAX];
-uschar spool_file [PATH_MAX];
struct stat statbuf;
-BOOL bContinuation = FALSE;
-
-*more = FALSE;
DEBUG(D_transport)
{
debug_printf("transport_check_waiting entered\n");
debug_printf(" sequence=%d local_max=%d global_max=%d\n",
continue_sequence, local_message_max, connection_max_messages);
+ acl_level++;
}
/* Do nothing if we have hit the maximum number that can be send down one
if (local_message_max > 0 && continue_sequence >= local_message_max)
{
DEBUG(D_transport)
- debug_printf("max messages for one connection reached: returning\n");
- return FALSE;
+ debug_printf_indent("max messages for one connection reached: returning\n");
+ goto retfalse;
}
/* Open the waiting information database. */
-sprintf(CS buffer, "wait-%.200s", transport_name);
-dbm_file = dbfn_open(buffer, O_RDWR, &dbblock, TRUE);
-if (dbm_file == NULL) return FALSE;
+if (!(dbm_file = dbfn_open(string_sprintf("wait-%.200s", transport_name),
+ O_RDWR, &dbblock, TRUE, TRUE)))
+ goto retfalse;
/* See if there is a record for this host; if not, there's nothing to do. */
-host_record = dbfn_read(dbm_file, hostname);
-if (host_record == NULL)
+if (!(host_record = dbfn_read(dbm_file, hostname)))
{
dbfn_close(dbm_file);
- DEBUG(D_transport) debug_printf("no messages waiting for %s\n", hostname);
- return FALSE;
+ DEBUG(D_transport) debug_printf_indent("no messages waiting for %s\n", hostname);
+ goto retfalse;
}
/* If the data in the record looks corrupt, just log something and
dbfn_close(dbm_file);
log_write(0, LOG_MAIN|LOG_PANIC, "smtp-wait database entry for %s has bad "
"count=%d (max=%d)", hostname, host_record->count, WAIT_NAME_MAX);
- return FALSE;
+ goto retfalse;
}
/* Scan the message ids in the record from the end towards the beginning,
but the 1 off will remain without it. This code now allows me to SKIP over
a message I do not want to send out on this run. */
-sprintf(CS spool_dir, "%s/input/", spool_directory);
-
host_length = host_record->count * MESSAGE_ID_LENGTH;
while (1)
{
+ msgq_t *msgq;
+ int msgq_count = 0;
+ int msgq_actual = 0;
+ BOOL bFound = FALSE;
+ BOOL bContinuation = FALSE;
+
/* create an array to read entire message queue into memory for processing */
- msgq = (msgq_t*) malloc(sizeof(msgq_t) * host_record->count);
+ msgq = store_get(sizeof(msgq_t) * host_record->count, GET_UNTAINTED);
msgq_count = host_record->count;
msgq_actual = msgq_count;
for (i = 0; i < host_record->count; ++i)
{
+ /* If any ID is seen which is not new-format, wipe the record and
+ any continuations */
+
+ if (!is_new_message_id(host_record->text + (i * MESSAGE_ID_LENGTH)))
+ {
+ uschar buffer[256];
+ DEBUG(D_hints_lookup)
+ debug_printf_indent("NOTE: old or corrupt message-id found in wait=%.200s"
+ " hints DB; deleting records for %s\n", transport_name, hostname);
+ (void) dbfn_delete(dbm_file, hostname);
+ for (int i = host_record->sequence - 1; i >= 0; i--)
+ (void) dbfn_delete(dbm_file,
+ (sprintf(CS buffer, "%.200s:%d", hostname, i), buffer));
+ dbfn_close(dbm_file);
+ goto retfalse;
+ }
msgq[i].bKeep = TRUE;
- Ustrncpy(msgq[i].message_id, host_record->text + (i * MESSAGE_ID_LENGTH),
+ Ustrncpy_nt(msgq[i].message_id, host_record->text + (i * MESSAGE_ID_LENGTH),
MESSAGE_ID_LENGTH);
msgq[i].message_id[MESSAGE_ID_LENGTH] = 0;
}
/* first thing remove current message id if it exists */
+ /*XXX but what if it has un-sent addrs? */
for (i = 0; i < msgq_count; ++i)
if (Ustrcmp(msgq[i].message_id, message_id) == 0)
/* now find the next acceptable message_id */
- bFound = FALSE;
-
for (i = msgq_count - 1; i >= 0; --i) if (msgq[i].bKeep)
{
- if (split_spool_directory)
- sprintf(CS spool_file, "%s%c/%s-D",
- spool_dir, msgq[i].message_id[5], msgq[i].message_id);
- else
- sprintf(CS spool_file, "%s%s-D", spool_dir, msgq[i].message_id);
+ uschar subdir[2];
+ uschar * mid = msgq[i].message_id;
- if (Ustat(spool_file, &statbuf) != 0)
+ set_subdir_str(subdir, mid, 0);
+ if (Ustat(spool_fname(US"input", subdir, mid, US"-D"), &statbuf) != 0)
msgq[i].bKeep = FALSE;
- else if (!oicf_func || oicf_func(msgq[i].message_id, oicf_data))
+ else if (!oicf_func || oicf_func(mid, oicf_data))
{
- Ustrcpy(new_message_id, msgq[i].message_id);
+ Ustrcpy_nt(new_message_id, mid);
msgq[i].bKeep = FALSE;
bFound = TRUE;
break;
msgq_actual++;
/* reassemble the host record, based on removed message ids, from in
- * memory queue.
- */
+ memory queue */
if (msgq_actual <= 0)
{
}
}
-/* Jeremy: check for a continuation record, this code I do not know how to
-test but the code should work */
-
- bContinuation = FALSE;
+ /* Check for a continuation record. */
while (host_length <= 0)
{
- int i;
dbdata_wait * newr = NULL;
+ uschar buffer[256];
/* Search for a continuation */
- for (i = host_record->sequence - 1; i >= 0 && !newr; i--)
+ for (int i = host_record->sequence - 1; i >= 0 && !newr; i--)
{
sprintf(CS buffer, "%.200s:%d", hostname, i);
newr = dbfn_read(dbm_file, buffer);
bContinuation = TRUE;
}
- if (bFound)
+ if (bFound) /* Usual exit from main loop */
break;
/* If host_length <= 0 we have emptied a record and not found a good message,
if (host_length <= 0)
{
dbfn_close(dbm_file);
- DEBUG(D_transport) debug_printf("waiting messages already delivered\n");
- return FALSE;
+ DEBUG(D_transport) debug_printf_indent("waiting messages already delivered\n");
+ goto retfalse;
}
/* we were not able to find an acceptable message, nor was there a
if (!bContinuation)
{
- Ustrcpy (new_message_id, message_id);
+ Ustrcpy(new_message_id, message_id);
dbfn_close(dbm_file);
- return FALSE;
+ goto retfalse;
}
} /* we need to process a continuation record */
-/* clean up in memory queue */
-if (msgq)
- {
- free (msgq);
- msgq = NULL;
- msgq_count = 0;
- msgq_actual = 0;
- }
-
/* Control gets here when an existing message has been encountered; its
id is in new_message_id, and host_length is the revised length of the
host record. If it is zero, the record has been removed. Update the
if (host_length > 0)
{
- uschar msg [MESSAGE_ID_LENGTH + 1];
- int i;
-
host_record->count = host_length/MESSAGE_ID_LENGTH;
-
- /* rebuild the host_record->text */
-
- for (i = 0; i < host_record->count; ++i)
- {
- Ustrncpy(msg, host_record->text + (i*MESSAGE_ID_LENGTH), MESSAGE_ID_LENGTH);
- msg[MESSAGE_ID_LENGTH] = 0;
- }
-
dbfn_write(dbm_file, hostname, host_record, (int)sizeof(dbdata_wait) + host_length);
- *more = TRUE;
}
dbfn_close(dbm_file);
+DEBUG(D_transport) {acl_level--; debug_printf("transport_check_waiting: TRUE\n"); }
return TRUE;
+
+retfalse:
+DEBUG(D_transport) {acl_level--; debug_printf("transport_check_waiting: FALSE\n"); }
+return FALSE;
}
/*************************************************
* Deliver waiting message down same socket *
*************************************************/
+/* Just the regain-root-privilege exec portion */
+void
+transport_do_pass_socket(const uschar * transport_name, const uschar * hostname,
+ const uschar * hostaddress, uschar * id, int socket_fd)
+{
+int i = 13;
+const uschar **argv;
+
+#ifndef DISABLE_TLS
+if (smtp_peer_options & OPTION_TLS) i += 6;
+#endif
+#ifndef DISABLE_ESMTP_LIMITS
+if (continue_limit_mail || continue_limit_rcpt || continue_limit_rcptdom)
+ i += 4;
+#endif
+if (queue_run_pid != (pid_t)0) i += 3;
+#ifdef SUPPORT_SOCKS
+if (proxy_session) i += 5;
+#endif
+
+/* Set up the calling arguments; use the standard function for the basics,
+but we have a number of extras that may be added. */
+
+argv = CUSS child_exec_exim(CEE_RETURN_ARGV, TRUE, &i, FALSE, 0);
+
+if (f.smtp_authenticated) argv[i++] = US"-MCA";
+if (smtp_peer_options & OPTION_CHUNKING) argv[i++] = US"-MCK";
+if (smtp_peer_options & OPTION_DSN) argv[i++] = US"-MCD";
+if (smtp_peer_options & OPTION_PIPE) argv[i++] = US"-MCP";
+if (smtp_peer_options & OPTION_SIZE) argv[i++] = US"-MCS";
+#ifndef DISABLE_TLS
+if (smtp_peer_options & OPTION_TLS)
+ if (tls_out.active.sock >= 0 || continue_proxy_cipher)
+ {
+ argv[i++] = US"-MCt";
+ argv[i++] = sending_ip_address;
+ argv[i++] = string_sprintf("%d", sending_port);
+ argv[i++] = tls_out.active.sock >= 0 ? tls_out.cipher : continue_proxy_cipher;
+
+ if (tls_out.sni)
+ {
+ argv[i++] =
+#ifdef SUPPORT_DANE
+ tls_out.dane_verified ? US"-MCr" :
+#endif
+ US"-MCs";
+ argv[i++] = tls_out.sni;
+ }
+ }
+ else
+ argv[i++] = US"-MCT";
+#endif
+
+#ifndef DISABLE_ESMTP_LIMITS
+if (continue_limit_rcpt || continue_limit_rcptdom)
+ {
+ argv[i++] = US"-MCL";
+ argv[i++] = string_sprintf("%u", continue_limit_mail);
+ argv[i++] = string_sprintf("%u", continue_limit_rcpt);
+ argv[i++] = string_sprintf("%u", continue_limit_rcptdom);
+ }
+#endif
+
+if (queue_run_pid != (pid_t)0)
+ {
+ argv[i++] = US"-MCQ";
+ argv[i++] = string_sprintf("%d", queue_run_pid);
+ argv[i++] = string_sprintf("%d", queue_run_pipe);
+ }
+
+#ifdef SUPPORT_SOCKS
+if (proxy_session)
+ {
+ argv[i++] = US"-MCp";
+ argv[i++] = proxy_local_address;
+ argv[i++] = string_sprintf("%d", proxy_local_port);
+ argv[i++] = proxy_external_address;
+ argv[i++] = string_sprintf("%d", proxy_external_port);
+ }
+#endif
+
+argv[i++] = US"-MC";
+argv[i++] = US transport_name;
+argv[i++] = US hostname;
+argv[i++] = US hostaddress;
+argv[i++] = string_sprintf("%d", continue_sequence + 1);
+argv[i++] = id;
+argv[i++] = NULL;
+
+/* Arrange for the channel to be on stdin. */
+
+if (socket_fd != 0)
+ {
+ (void)dup2(socket_fd, 0);
+ (void)close(socket_fd);
+ }
+
+DEBUG(D_exec) debug_print_argv(argv);
+exim_nullstd(); /* Ensure std{out,err} exist */
+/* argv[0] should be untainted, from child_exec_exim() */
+execv(CS argv[0], (char *const *)argv);
+
+DEBUG(D_any) debug_printf("execv failed: %s\n", strerror(errno));
+_exit(errno); /* Note: must be _exit(), NOT exit() */
+}
+
+
+
/* Fork a new exim process to deliver the message, and do a re-exec, both to
get a clean delivery process, and to regain root privilege in cases where it
has been given away.
BOOL
transport_pass_socket(const uschar *transport_name, const uschar *hostname,
- const uschar *hostaddress, uschar *id, int socket_fd)
+ const uschar *hostaddress, uschar *id, int socket_fd
+#ifndef DISABLE_ESMTP_LIMITS
+ , unsigned peer_limit_mail, unsigned peer_limit_rcpt, unsigned peer_limit_rcptdom
+#endif
+ )
{
pid_t pid;
int status;
DEBUG(D_transport) debug_printf("transport_pass_socket entered\n");
-if ((pid = fork()) == 0)
- {
- int i = 16;
- const uschar **argv;
+#ifndef DISABLE_ESMTP_LIMITS
+continue_limit_mail = peer_limit_mail;
+continue_limit_rcpt = peer_limit_rcpt;
+continue_limit_rcptdom = peer_limit_rcptdom;
+#endif
+if ((pid = exim_fork(US"continued-transport-interproc")) == 0)
+ {
/* Disconnect entirely from the parent process. If we are running in the
test harness, wait for a bit to allow the previous process time to finish,
write the log, etc., so that the output is always in the same order for
automatic comparison. */
- if ((pid = fork()) != 0) _exit(EXIT_SUCCESS);
- if (running_in_test_harness) sleep(1);
-
- /* Set up the calling arguments; use the standard function for the basics,
- but we have a number of extras that may be added. */
-
- argv = CUSS child_exec_exim(CEE_RETURN_ARGV, TRUE, &i, FALSE, 0);
-
- /* Call with the dsn flag */
- if (smtp_use_dsn) argv[i++] = US"-MCD";
-
- if (smtp_authenticated) argv[i++] = US"-MCA";
-
- #ifdef SUPPORT_TLS
- if (tls_offered) argv[i++] = US"-MCT";
- #endif
-
- if (smtp_use_size) argv[i++] = US"-MCS";
- if (smtp_use_pipelining) argv[i++] = US"-MCP";
-
- if (queue_run_pid != (pid_t)0)
- {
- argv[i++] = US"-MCQ";
- argv[i++] = string_sprintf("%d", queue_run_pid);
- argv[i++] = string_sprintf("%d", queue_run_pipe);
- }
+ if ((pid = exim_fork(US"continued-transport")) != 0)
+ _exit(EXIT_SUCCESS);
+ testharness_pause_ms(1000);
- argv[i++] = US"-MC";
- argv[i++] = US transport_name;
- argv[i++] = US hostname;
- argv[i++] = US hostaddress;
- argv[i++] = string_sprintf("%d", continue_sequence + 1);
- argv[i++] = id;
- argv[i++] = NULL;
-
- /* Arrange for the channel to be on stdin. */
-
- if (socket_fd != 0)
- {
- (void)dup2(socket_fd, 0);
- (void)close(socket_fd);
- }
-
- DEBUG(D_exec) debug_print_argv(argv);
- exim_nullstd(); /* Ensure std{out,err} exist */
- execv(CS argv[0], (char *const *)argv);
-
- DEBUG(D_any) debug_printf("execv failed: %s\n", strerror(errno));
- _exit(errno); /* Note: must be _exit(), NOT exit() */
+ transport_do_pass_socket(transport_name, hostname, hostaddress,
+ id, socket_fd);
}
/* If the process creation succeeded, wait for the first-level child, which
{
int rc;
while ((rc = wait(&status)) != pid && (rc >= 0 || errno != ECHILD));
- DEBUG(D_transport) debug_printf("transport_pass_socket succeeded\n");
return TRUE;
}
else
+/* Enforce all args untainted, for consistency with a router-sourced pipe
+command, where (because the whole line is passed as one to the tpt) a
+tainted arg taints the executable name. It's unclear also that letting an
+attacker supply command arguments is wise. */
+
+static BOOL
+arg_is_tainted(const uschar * s, int argn, address_item * addr,
+ const uschar * etext, uschar ** errptr)
+{
+if (is_tainted(s))
+ {
+ uschar * msg = string_sprintf("Tainted arg %d for %s command: '%s'",
+ argn, etext, s);
+ if (addr)
+ {
+ addr->transport_return = FAIL;
+ addr->message = msg;
+ }
+ else *errptr = msg;
+ return TRUE;
+ }
+return FALSE;
+}
+
+
/*************************************************
* Set up direct (non-shell) command *
*************************************************/
/* This function is called when a command line is to be parsed and executed
directly, without the use of /bin/sh. It is called by the pipe transport,
-the queryprogram router, and also from the main delivery code when setting up a
+the queryprogram router, for any ${run } expansion,
+and also from the main delivery code when setting up a
transport filter process. The code for ETRN also makes use of this; in that
case, no addresses are passed.
Arguments:
argvptr pointer to anchor for argv vector
cmd points to the command string (modified IN PLACE)
- expand_arguments true if expansion is to occur
+ flags bits for expand-args, allow taint, allow $recipients
expand_failed error value to set if expansion fails; not relevant if
addr == NULL
addr chain of addresses, or NULL
*/
BOOL
-transport_set_up_command(const uschar ***argvptr, uschar *cmd,
- BOOL expand_arguments, int expand_failed, address_item *addr,
- uschar *etext, uschar **errptr)
+transport_set_up_command(const uschar *** argvptr, const uschar * cmd,
+ unsigned flags, int expand_failed, address_item * addr,
+ const uschar * etext, uschar ** errptr)
{
-address_item *ad;
-const uschar **argv;
-uschar *s, *ss;
-int address_count = 0;
-int argcount = 0;
-int i, max_args;
+const uschar ** argv, * s;
+int address_count = 0, argcount = 0, max_args;
/* Get store in which to build an argument list. Count the number of addresses
supplied, and allow for that many arguments, plus an additional 60, which
should be enough for anybody. Multiple addresses happen only when the local
delivery batch option is set. */
-for (ad = addr; ad != NULL; ad = ad->next) address_count++;
+for (address_item * ad = addr; ad; ad = ad->next) address_count++;
max_args = address_count + 60;
-*argvptr = argv = store_get((max_args+1)*sizeof(uschar *));
+*argvptr = argv = store_get((max_args+1)*sizeof(uschar *), GET_UNTAINTED);
/* Split the command up into arguments terminated by white space. Lose
trailing space at the start and end. Double-quoted arguments can contain \\ and
arguments are verbatim. Copy each argument into a new string. */
s = cmd;
-while (isspace(*s)) s++;
+Uskip_whitespace(&s);
-while (*s != 0 && argcount < max_args)
+for (; *s && argcount < max_args; argcount++)
{
if (*s == '\'')
{
- ss = s + 1;
- while (*ss != 0 && *ss != '\'') ss++;
- argv[argcount++] = ss = store_get(ss - s++);
- while (*s != 0 && *s != '\'') *ss++ = *s++;
- if (*s != 0) s++;
- *ss++ = 0;
+ int n = Ustrcspn(++s, "'");
+ argv[argcount] = string_copyn(s, n);
+ if (*(s += n) == '\'') s++;
}
- else argv[argcount++] = string_copy(string_dequote(CUSS &s));
- while (isspace(*s)) s++;
+ else
+ argv[argcount] = string_dequote(CUSS &s);
+ Uskip_whitespace(&s);
}
-argv[argcount] = (uschar *)0;
+argv[argcount] = NULL;
/* If *s != 0 we have run out of argument slots. */
-if (*s != 0)
+if (*s)
{
uschar *msg = string_sprintf("Too many arguments in command \"%s\" in "
"%s", cmd, etext);
- if (addr != NULL)
+ if (addr)
{
addr->transport_return = FAIL;
addr->message = msg;
DEBUG(D_transport)
{
debug_printf("direct command:\n");
- for (i = 0; argv[i] != (uschar *)0; i++)
- debug_printf(" argv[%d] = %s\n", i, string_printing(argv[i]));
+ for (int i = 0; argv[i]; i++)
+ debug_printf(" argv[%d] = '%s'\n", i, string_printing(argv[i]));
}
-if (expand_arguments)
+if (flags & TSUC_EXPAND_ARGS)
{
- BOOL allow_dollar_recipients = addr != NULL &&
- addr->parent != NULL &&
- Ustrcmp(addr->parent->address, "system-filter") == 0;
+ BOOL allow_dollar_recipients = (flags & TSUC_ALLOW_RECIPIENTS)
+ || (addr && addr->parent && Ustrcmp(addr->parent->address, "system-filter") == 0); /*XXX could we check this at caller? */
- for (i = 0; argv[i] != (uschar *)0; i++)
+ for (int i = 0; argv[i]; i++)
{
+ DEBUG(D_expand) debug_printf_indent("arg %d\n", i);
/* Handle special fudge for passing an address list */
- if (addr != NULL &&
+ if (addr &&
(Ustrcmp(argv[i], "$pipe_addresses") == 0 ||
Ustrcmp(argv[i], "${pipe_addresses}") == 0))
{
memmove(argv + i + 1 + additional, argv + i + 1,
(argcount - i)*sizeof(uschar *));
- for (ad = addr; ad != NULL; ad = ad->next) {
- argv[i++] = ad->address;
- argcount++;
- }
+ for (address_item * ad = addr; ad; ad = ad->next)
+ {
+ /* $pipe_addresses is spefically not checked for taint, because there is
+ a testcase (321) depending on it. It's unclear if the exact thing being
+ done really needs to be legitimate, though I suspect it reflects an
+ actual use-case that showed up a bug.
+ This is a hole in the taint-pretection, mitigated only in that
+ shell-syntax metachars cannot be injected via this route. */
+
+ DEBUG(D_transport) if (is_tainted(ad->address))
+ debug_printf("tainted element '%s' from $pipe_addresses\n", ad->address);
+
+ argv[i++] = ad->address;
+ argcount++;
+ }
/* Subtract one since we replace $pipe_addresses */
argcount--;
/* Handle special case of $address_pipe when af_force_command is set */
- else if (addr != NULL && testflag(addr,af_force_command) &&
+ else if (addr && testflag(addr,af_force_command) &&
(Ustrcmp(argv[i], "$address_pipe") == 0 ||
Ustrcmp(argv[i], "${address_pipe}") == 0))
{
- int address_pipe_i;
int address_pipe_argcount = 0;
int address_pipe_max_args;
uschar **address_pipe_argv;
debug_printf("address_pipe_max_args=%d\n", address_pipe_max_args);
/* We allocate an additional for (uschar *)0 */
- address_pipe_argv = store_get((address_pipe_max_args+1)*sizeof(uschar *));
+ address_pipe_argv = store_get((address_pipe_max_args+1)*sizeof(uschar *), GET_UNTAINTED);
/* +1 because addr->local_part[0] == '|' since af_force_command is set */
- s = expand_string(addr->local_part + 1);
+ s = expand_cstring(addr->local_part + 1);
- if (s == NULL || *s == '\0')
+ if (!s || !*s)
{
addr->transport_return = FAIL;
addr->message = string_sprintf("Expansion of \"%s\" "
return FALSE;
}
- while (isspace(*s)) s++; /* strip leading space */
+ Uskip_whitespace(&s); /* strip leading space */
- while (*s != 0 && address_pipe_argcount < address_pipe_max_args)
+ while (*s && address_pipe_argcount < address_pipe_max_args)
{
if (*s == '\'')
- {
- ss = s + 1;
- while (*ss != 0 && *ss != '\'') ss++;
- address_pipe_argv[address_pipe_argcount++] = ss = store_get(ss - s++);
- while (*s != 0 && *s != '\'') *ss++ = *s++;
- if (*s != 0) s++;
- *ss++ = 0;
- }
- else address_pipe_argv[address_pipe_argcount++] =
- string_copy(string_dequote(CUSS &s));
- while (isspace(*s)) s++; /* strip space after arg */
+ {
+ int n = Ustrcspn(++s, "'");
+ argv[argcount] = string_copyn(s, n);
+ if (*(s += n) == '\'') s++;
+ }
+ else
+ address_pipe_argv[address_pipe_argcount++] = string_dequote(CUSS &s);
+ Uskip_whitespace(&s); /* strip space after arg */
}
- address_pipe_argv[address_pipe_argcount] = (uschar *)0;
+ address_pipe_argv[address_pipe_argcount] = NULL;
/* If *s != 0 we have run out of argument slots. */
- if (*s != 0)
+ if (*s)
{
uschar *msg = string_sprintf("Too many arguments in $address_pipe "
"\"%s\" in %s", addr->local_part + 1, etext);
- if (addr != NULL)
+ if (addr)
{
addr->transport_return = FAIL;
addr->message = msg;
}
/* address_pipe_argcount - 1
- * because we are replacing $address_pipe in the argument list
- * with the first thing it expands to */
+ because we are replacing $address_pipe in the argument list
+ with the first thing it expands to */
+
if (argcount + address_pipe_argcount - 1 > max_args)
{
addr->transport_return = FAIL;
}
/* If we are not just able to replace the slot that contained
- * $address_pipe (address_pipe_argcount == 1)
- * We have to move the existing argv by address_pipe_argcount - 1
- * Visually if address_pipe_argcount == 2:
- * [argv 0][argv 1][argv 2($address_pipe)][argv 3][0]
- * [argv 0][argv 1][ap_arg0][ap_arg1][old argv 3][0]
- */
+ $address_pipe (address_pipe_argcount == 1)
+ We have to move the existing argv by address_pipe_argcount - 1
+ Visually if address_pipe_argcount == 2:
+ [argv 0][argv 1][argv 2($address_pipe)][argv 3][0]
+ [argv 0][argv 1][ap_arg0][ap_arg1][old argv 3][0] */
+
if (address_pipe_argcount > 1)
memmove(
- /* current position + additonal args */
+ /* current position + additional args */
argv + i + address_pipe_argcount,
/* current position + 1 (for the (uschar *)0 at the end) */
argv + i + 1,
);
/* Now we fill in the slots we just moved argv out of
- * [argv 0][argv 1][argv 2=pipeargv[0]][argv 3=pipeargv[1]][old argv 3][0]
- */
- for (address_pipe_i = 0;
- address_pipe_argv[address_pipe_i] != (uschar *)0;
- address_pipe_i++)
- {
- argv[i++] = address_pipe_argv[address_pipe_i];
- argcount++;
- }
+ [argv 0][argv 1][argv 2=pipeargv[0]][argv 3=pipeargv[1]][old argv 3][0] */
+
+ for (int address_pipe_i = 0;
+ address_pipe_argv[address_pipe_i];
+ address_pipe_i++, argcount++)
+ {
+ uschar * s = address_pipe_argv[address_pipe_i];
+ if (arg_is_tainted(s, i, addr, etext, errptr)) return FALSE;
+ argv[i++] = s;
+ }
/* Subtract one since we replace $address_pipe */
argcount--;
else
{
const uschar *expanded_arg;
- enable_dollar_recipients = allow_dollar_recipients;
+ BOOL enable_dollar_recipients_g = f.enable_dollar_recipients;
+ f.enable_dollar_recipients = allow_dollar_recipients;
expanded_arg = expand_cstring(argv[i]);
- enable_dollar_recipients = FALSE;
+ f.enable_dollar_recipients = enable_dollar_recipients_g;
- if (expanded_arg == NULL)
+ if (!expanded_arg)
{
uschar *msg = string_sprintf("Expansion of \"%s\" "
"from command \"%s\" in %s failed: %s",
argv[i], cmd, etext, expand_string_message);
- if (addr != NULL)
+ if (addr)
{
addr->transport_return = expand_failed;
addr->message = msg;
else *errptr = msg;
return FALSE;
}
+
+ if ( f.running_in_test_harness && is_tainted(expanded_arg)
+ && Ustrcmp(etext, "queryprogram router") == 0)
+ { /* hack, would be good to not need it */
+ DEBUG(D_transport)
+ debug_printf("SPECIFIC TESTSUITE EXEMPTION: tainted arg '%s'\n",
+ expanded_arg);
+ }
+ else if ( !(flags & TSUC_ALLOW_TAINTED_ARGS)
+ && arg_is_tainted(expanded_arg, i, addr, etext, errptr))
+ return FALSE;
argv[i] = expanded_arg;
}
}
DEBUG(D_transport)
{
debug_printf("direct command after expansion:\n");
- for (i = 0; argv[i] != (uschar *)0; i++)
- debug_printf(" argv[%d] = %s\n", i, string_printing(argv[i]));
+ for (int i = 0; argv[i]; i++)
+ {
+ debug_printf(" argv[%d] = '%s'\n", i, string_printing(argv[i]));
+ debug_print_taint(argv[i]);
+ }
}
}
return TRUE;
}
+
+
+/* For error messages, a string describing the config location associated
+with current processing. NULL if we are not in a transport. */
+/* Name only, for now */
+
+uschar *
+transport_current_name(void)
+{
+if (!transport_name) return NULL;
+return string_sprintf(" (transport %s, %s %d)", transport_name, driver_srcfile, driver_srcline);
+}
+
+#endif /*!MACRO_PREDEF*/
/* vi: aw ai sw=2
*/
/* End of transport.c */