SPDX: Mass-update to GPL-2.0-or-later
[exim.git] / src / src / transport.c
index c4f3745096205ab29b300667b0bc14e6e53a1ac1..d04ea516a5c4e3bc59366ed619393cf2160c0385 100644 (file)
@@ -2,8 +2,10 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* 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. */
@@ -15,75 +17,76 @@ transports. */
 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 = nelem(optionlist_transports);
@@ -251,7 +254,6 @@ for (int i = 0; i < 100; i++)
 
   for(;;)
     {
-    fd_set fds;
     /* 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
@@ -264,6 +266,7 @@ for (int i = 0; i < 100; i++)
       }
     else                               /* Timeout wanted. */
       {
+      sigalrm_seen = FALSE;
       ALARM(local_timeout);
        rc = tpt_write(fd, block, len, more, tctx->options);
        save_errno = errno;
@@ -278,8 +281,7 @@ for (int i = 0; i < 100; i++)
     if (rc >= 0 || errno != ENOTCONN || connretry <= 0)
       break;
 
-    FD_ZERO(&fds); FD_SET(fd, &fds);
-    select(fd+1, NULL, &fds, NULL, NULL);      /* could set timout? */
+    poll_one_fd(fd, POLLOUT, -1);              /* could set timeout? retval check? */
     connretry--;
     }
 
@@ -590,14 +592,14 @@ if (include_affixes)
   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", (int)(at - addr->address - plen - slen),
@@ -650,7 +652,7 @@ so that we don't handle it again. */
 
 for (ppp = *pdlist; ppp; ppp = ppp->next) if (p == ppp->ptr) return TRUE;
 
-ppp = store_get(sizeof(struct aci), FALSE);
+ppp = store_get(sizeof(struct aci), GET_UNTAINTED);
 ppp->next = *pdlist;
 *pdlist = ppp;
 ppp->ptr = p;
@@ -674,7 +676,7 @@ if (ppp) return TRUE;
 
 /* Remember what we have output, and output it. */
 
-ppp = store_get(sizeof(struct aci), FALSE);
+ppp = store_get(sizeof(struct aci), GET_UNTAINTED);
 ppp->next = *pplist;
 *pplist = ppp;
 ppp->ptr = pp;
@@ -737,10 +739,17 @@ for (header_line * h = header_list; h; h = h->next) if (h->type != htype_old)
            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) { include_header = FALSE; break; }
       }
@@ -774,7 +783,7 @@ for (header_line * h = header_list; h; h = h->next) if (h->type != htype_old)
   /* 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,
@@ -790,8 +799,8 @@ Headers added to an address by a router are guaranteed to end with a newline.
 
 if (addr)
   {
-  header_line *hprev = addr->prop.extra_headers;
-  header_line *hnext, * h;
+  header_line * hprev = addr->prop.extra_headers, * hnext, * h;
+
   for (int i = 0; i < 2; i++)
     for (h = hprev, hprev = NULL; h; h = hnext)
       {
@@ -802,7 +811,7 @@ if (addr)
        {
        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);
        }
       }
   }
@@ -830,7 +839,7 @@ if (tblock && (list = CUS tblock->add_headers))
          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");
          }
@@ -876,7 +885,7 @@ transport_write_timeout non-zero.
 
 Arguments:
   tctx
-    (fd, msg)          Either and fd, to write the message to,
+    (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;
@@ -895,6 +904,7 @@ Arguments:
       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
@@ -948,10 +958,10 @@ if (!(tctx->options & topt_no_headers))
 
   if (tctx->options & topt_add_return_path)
     {
-    uschar buffer[ADDRESS_MAXLENGTH + 20];
-    int n = sprintf(CS buffer, "Return-path: <%.*s>\n", ADDRESS_MAXLENGTH,
-      return_path);
-    if (!write_chunk(tctx, buffer, n)) goto bad;
+    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 */
@@ -1146,13 +1156,18 @@ f.spool_file_wireformat = FALSE;
 
 /* If requested, add a terminating "." line (SMTP output). */
 
-if (tctx->options & topt_end_dot && !write_chunk(tctx, US".\n", 2))
-  return FALSE;
+if (tctx->options & topt_end_dot)
+  {
+  smtp_debug_cmd(US".", 0);
+  if (!write_chunk(tctx, US".\n", 2))
+    return FALSE;
+  }
 
 /* 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, FALSE);
+return (len = chunk_ptr - deliver_out_buffer) <= 0
+  || transport_write_block(tctx, deliver_out_buffer, len,
+                           !!(tctx->options & topt_no_flush));
 }
 
 
@@ -1180,7 +1195,8 @@ transport_write_message(transport_ctx * tctx, int size_limit)
 {
 BOOL last_filter_was_NL = TRUE;
 BOOL save_spool_file_wireformat = f.spool_file_wireformat;
-int rc, len, yield, fd_read, fd_write, save_errno;
+BOOL yield;
+int rc, len, fd_read, fd_write, save_errno;
 int pfd[2] = {-1, -1};
 pid_t filter_pid, write_pid;
 
@@ -1224,10 +1240,10 @@ write_pid = (pid_t)(-1);
 
   {
   int bits = fcntl(tctx->u.fd, F_GETFD);
-  (void)fcntl(tctx->u.fd, F_SETFD, bits | FD_CLOEXEC);
+  (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);
-  (void)fcntl(tctx->u.fd, F_SETFD, bits & ~FD_CLOEXEC);
+                         &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 */
 
@@ -1240,7 +1256,7 @@ via a(nother) pipe. While writing to the filter, we do not do the CRLF,
 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);
@@ -1249,30 +1265,22 @@ if ((write_pid = fork()) == 0)
 
   tctx->u.fd = fd_write;
   tctx->check_string = tctx->escape_string = NULL;
-  tctx->options &= ~(topt_use_crlf | topt_end_dot | topt_use_bdat);
+  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;
-  errno = 0;
-  for (int retries = 10;;)
-    {
-    if (  os_pipe_write(pfd[pipe_write], (void *)&rc, sizeof(BOOL))
-         != sizeof(BOOL)
-       || os_pipe_write(pfd[pipe_write], (void *)&save_errno, sizeof(int))
-         != sizeof(int)
-       || os_pipe_write(pfd[pipe_write], (void *)&tctx->addr->more_errno, sizeof(int))
-         != sizeof(int)
-       || os_pipe_write(pfd[pipe_write], (void *)&tctx->addr->delivery_usec, sizeof(int))
-         != sizeof(int)
-       )
-      if (errno == EINTR && --retries > 0)
-       continue;
-      else
-       rc = FALSE;     /* compiler quietening */
-    break;
-    }
-  exim_underbar_exit(0);
+  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 *)&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 */
+  exim_underbar_exit(EXIT_SUCCESS);
   }
 save_errno = errno;
 
@@ -1320,6 +1328,7 @@ for (;;)
   ALARM_CLR(0);
   if (sigalrm_seen)
     {
+    DEBUG(D_transport) debug_printf("timed out reading from filter\n");
     errno = ETIMEDOUT;
     f.transport_filter_timed_out = TRUE;
     goto TIDY_UP;
@@ -1384,7 +1393,7 @@ if (write_pid > 0)
     if (rc == 0)
       {
       BOOL ok;
-      if (os_pipe_read(pfd[pipe_read], (void *)&ok, sizeof(BOOL)) != sizeof(BOOL))
+      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));
@@ -1392,11 +1401,10 @@ if (write_pid > 0)
         yield = FALSE;
        }
       else if (!ok)
-        {
-       int dummy = os_pipe_read(pfd[pipe_read], (void *)&save_errno, sizeof(int));
-        dummy = os_pipe_read(pfd[pipe_read], (void *)&tctx->addr->more_errno, sizeof(int));
-        dummy = os_pipe_read(pfd[pipe_read], (void *)&tctx->addr->delivery_usec, sizeof(int));
-       dummy = dummy;          /* compiler quietening */
+        {              /* 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;
         }
       }
@@ -1423,7 +1431,7 @@ if (yield)
         ? !write_chunk(tctx, US".\n", 2)
        : !write_chunk(tctx, US"\n.\n", 3)
      )  )
-    yield = FALSE;
+    { smtp_debug_cmd(US".", 0); yield = FALSE; }
 
   /* Write out any remaining data in the buffer. */
 
@@ -1438,7 +1446,7 @@ 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, tctx->addr->more_errno);
+    debug_printf(" errno=%d more_errno=%d\n", errno, tctx->addr->more_errno);
   }
 
 return yield;
@@ -1518,7 +1526,7 @@ for (host_item * host = hostlist; host; host = host->next)
 
   if (!(host_record = dbfn_read(dbm_file, host->name)))
     {
-    host_record = store_get(sizeof(dbdata_wait) + MESSAGE_ID_LENGTH, FALSE);
+    host_record = store_get(sizeof(dbdata_wait) + MESSAGE_ID_LENGTH, GET_UNTAINTED);
     host_record->count = host_record->sequence = 0;
     }
 
@@ -1560,12 +1568,17 @@ for (host_item * host = hostlist; host; host = host->next)
 
   /* 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;
@@ -1577,7 +1590,7 @@ for (host_item * host = hostlist; host; host = host->next)
   else
     {
     dbdata_wait *newr =
-      store_get(sizeof(dbdata_wait) + host_length + MESSAGE_ID_LENGTH, FALSE);
+      store_get(sizeof(dbdata_wait) + host_length + MESSAGE_ID_LENGTH, GET_UNTAINTED);
     memcpy(newr, host_record, sizeof(dbdata_wait) + host_length);
     host_record = newr;
     }
@@ -1591,7 +1604,8 @@ for (host_item * host = hostlist; host; host = host->next)
   /* 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 */
@@ -1619,7 +1633,6 @@ Arguments:
   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
@@ -1635,7 +1648,7 @@ typedef struct msgq_s
 
 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)
+  int local_message_max, uschar *new_message_id, oicf oicf_func, void *oicf_data)
 {
 dbdata_wait *host_record;
 int host_length;
@@ -1645,13 +1658,12 @@ open_db *dbm_file;
 int         i;
 struct stat statbuf;
 
-*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
@@ -1661,23 +1673,23 @@ if (connection_max_messages >= 0) local_message_max = connection_max_messages;
 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. */
 
 if (!(dbm_file = dbfn_open(string_sprintf("wait-%.200s", transport_name),
                          O_RDWR, &dbblock, TRUE, TRUE)))
-  return FALSE;
+  goto retfalse;
 
 /* See if there is a record for this host; if not, there's nothing to do. */
 
 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
@@ -1688,7 +1700,7 @@ if (host_record->count > WAIT_NAME_MAX)
   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,
@@ -1712,7 +1724,7 @@ while (1)
 
   /* create an array to read entire message queue into memory for processing  */
 
-  msgq = store_get(sizeof(msgq_t) * host_record->count, FALSE);
+  msgq = store_get(sizeof(msgq_t) * host_record->count, GET_UNTAINTED);
   msgq_count = host_record->count;
   msgq_actual = msgq_count;
 
@@ -1720,12 +1732,13 @@ while (1)
     {
     msgq[i].bKeep = TRUE;
 
-    Ustrncpy_nt(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)
@@ -1825,8 +1838,8 @@ while (1)
   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
@@ -1837,7 +1850,7 @@ while (1)
     {
     Ustrcpy(new_message_id, message_id);
     dbfn_close(dbm_file);
-    return FALSE;
+    goto retfalse;
     }
   }            /* we need to process a continuation record */
 
@@ -1849,13 +1862,16 @@ record if required, close the database, and return TRUE. */
 if (host_length > 0)
   {
   host_record->count = host_length/MESSAGE_ID_LENGTH;
-
   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;
 }
 
 /*************************************************
@@ -1867,9 +1883,21 @@ void
 transport_do_pass_socket(const uschar *transport_name, const uschar *hostname,
   const uschar *hostaddress, uschar *id, int socket_fd)
 {
-int i = 20;
+int i = 13;
 const uschar **argv;
 
+#ifndef DISABLE_TLS
+if (smtp_peer_options & OPTION_TLS) i += 6;
+#endif
+#ifdef EXPERIMENTAL_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. */
 
@@ -1888,11 +1916,31 @@ if (smtp_peer_options & OPTION_TLS)
     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
 
+#ifdef EXPERIMENTAL_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";
@@ -1900,6 +1948,17 @@ if (queue_run_pid != (pid_t)0)
   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;
@@ -1918,6 +1977,7 @@ if (socket_fd != 0)
 
 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));
@@ -1942,25 +2002,32 @@ Returns:          FALSE if fork fails; TRUE otherwise
 
 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
+#ifdef EXPERIMENTAL_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)
+#ifdef EXPERIMENTAL_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)
-    {
-    DEBUG(D_transport) debug_printf("transport_pass_socket succeeded (final-pid %d)\n", pid);
+  if ((pid = exim_fork(US"continued-transport")) != 0)
     _exit(EXIT_SUCCESS);
-    }
   testharness_pause_ms(1000);
 
   transport_do_pass_socket(transport_name, hostname, hostaddress,
@@ -1975,7 +2042,6 @@ if (pid > 0)
   {
   int rc;
   while ((rc = wait(&status)) != pid && (rc >= 0 || errno != ECHILD));
-  DEBUG(D_transport) debug_printf("transport_pass_socket succeeded (inter-pid %d)\n", pid);
   return TRUE;
   }
 else
@@ -1988,6 +2054,31 @@ 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     *
 *************************************************/
@@ -2005,6 +2096,7 @@ Arguments:
   expand_failed      error value to set if expansion fails; not relevant if
                      addr == NULL
   addr               chain of addresses, or NULL
+  allow_tainted_args as it says; used for ${run}
   etext              text for use in error messages
   errptr             where to put error message if addr is NULL;
                      otherwise it is put in the first address
@@ -2014,15 +2106,12 @@ Returns:             TRUE if all went well; otherwise an error will be
 */
 
 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,
+  BOOL expand_arguments, int expand_failed, address_item * addr,
+  BOOL allow_tainted_args, const uschar * etext, uschar ** errptr)
 {
-const uschar **argv;
-uschar *s, *ss;
-int address_count = 0;
-int argcount = 0;
-int 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
@@ -2031,7 +2120,7 @@ delivery batch option is set. */
 
 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 *), FALSE);
+*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
@@ -2039,33 +2128,30 @@ 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);
 
-for (; *s != 0 && argcount < max_args; argcount++)
+for (; *s && argcount < max_args; argcount++)
   {
   if (*s == '\'')
     {
-    ss = s + 1;
-    while (*ss != 0 && *ss != '\'') ss++;
-    argv[argcount] = ss = store_get(ss - s++, is_tainted(cmd));
-    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_dequote(CUSS &s);
-  while (isspace(*s)) s++;
+  Uskip_whitespace(&s);
   }
 
-argv[argcount] = US 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;
@@ -2099,16 +2185,14 @@ DEBUG(D_transport)
 
 if (expand_arguments)
   {
-  BOOL allow_dollar_recipients = addr != NULL &&
-    addr->parent != NULL &&
-    Ustrcmp(addr->parent->address, "system-filter") == 0;
+  BOOL allow_dollar_recipients = addr && addr->parent
+    && Ustrcmp(addr->parent->address, "system-filter") == 0;
 
-  for (int i = 0; argv[i] != US 0; i++)
+  for (int i = 0; argv[i]; 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))
       {
@@ -2129,6 +2213,16 @@ if (expand_arguments)
 
       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++;
        }
@@ -2140,14 +2234,13 @@ if (expand_arguments)
 
       /* 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_argcount = 0;
       int address_pipe_max_args;
       uschar **address_pipe_argv;
-      BOOL tainted;
 
       /* We can never have more then the argv we will be loading into */
       address_pipe_max_args = max_args - argcount + 1;
@@ -2156,13 +2249,12 @@ if (expand_arguments)
         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 *), FALSE);
+      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);
-      tainted = is_tainted(s);
 
-      if (s == NULL || *s == '\0')
+      if (!s || !*s)
         {
         addr->transport_return = FAIL;
         addr->message = string_sprintf("Expansion of \"%s\" "
@@ -2171,32 +2263,29 @@ if (expand_arguments)
         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++, tainted);
-          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] = US 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;
@@ -2206,8 +2295,9 @@ if (expand_arguments)
         }
 
       /* 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;
@@ -2217,12 +2307,12 @@ if (expand_arguments)
         }
 
       /* 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 + additional args */
@@ -2234,15 +2324,16 @@ if (expand_arguments)
         );
 
       /* 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]
-       */
+      [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] != US 0;
-           address_pipe_i++)
-        {
-        argv[i++] = address_pipe_argv[address_pipe_i];
-        argcount++;
-        }
+           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--;
@@ -2254,9 +2345,10 @@ if (expand_arguments)
     else
       {
       const uschar *expanded_arg;
+      BOOL enable_dollar_recipients_g = f.enable_dollar_recipients;
       f.enable_dollar_recipients = allow_dollar_recipients;
       expanded_arg = expand_cstring(argv[i]);
-      f.enable_dollar_recipients = FALSE;
+      f.enable_dollar_recipients = enable_dollar_recipients_g;
 
       if (!expanded_arg)
         {
@@ -2271,6 +2363,17 @@ if (expand_arguments)
         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 (  !allow_tainted_args
+             && arg_is_tainted(expanded_arg, i, addr, etext, errptr))
+       return FALSE;
       argv[i] = expanded_arg;
       }
     }
@@ -2278,14 +2381,30 @@ if (expand_arguments)
   DEBUG(D_transport)
     {
     debug_printf("direct command after expansion:\n");
-    for (int i = 0; argv[i] != US 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
 */