Transport: fix smtp under combo of mua_wrapper and limited max_rcpt
[exim.git] / src / src / transports / smtp.c
index 52e04b8a57a032082679a7a5929796b2903439e5..997281901b7b90d546e6899a1304074bdd18a00e 100644 (file)
@@ -220,8 +220,10 @@ smtp_transport_options_block smtp_transport_option_defaults = {
   NULL,                /* hosts_verify_avoid_tls */
   NULL,                /* hosts_avoid_pipelining */
   NULL,                /* hosts_avoid_esmtp */
+#ifdef SUPPORT_TLS
   NULL,                /* hosts_nopass_tls */
   US"*",              /* hosts_noproxy_tls */
+#endif
   5*60,                /* command_timeout */
   5*60,                /* connect_timeout; shorter system default overrides */
   5*60,                /* data timeout */
@@ -794,7 +796,9 @@ with an address by scanning for the next address whose status is PENDING_DEFER.
 
 while (count-- > 0)
   {
-  while (addr->transport_return != PENDING_DEFER) addr = addr->next;
+  while (addr->transport_return != PENDING_DEFER)
+    if (!(addr = addr->next))
+      return -2;
 
   /* The address was accepted */
   addr->host_used = sx->host;
@@ -1345,15 +1349,22 @@ return checks;
 If given a nonzero size, first flush any buffered SMTP commands
 then emit the command.
 
-Reap previous SMTP command responses if requested.
-Reap one SMTP command response if requested.
+Reap previous SMTP command responses if requested, and always reap
+the response from a previous BDAT command.
+
+Args:
+ tctx          transport context
+ chunk_size    value for SMTP BDAT command
+ flags
+   tc_chunk_last       add LAST option to SMTP BDAT command
+   tc_reap_prev                reap response to previous SMTP commands
 
 Returns:       OK or ERROR
 */
 
 static int
-smtp_chunk_cmd_callback(int fd, transport_ctx * tctx,
-  unsigned chunk_size, unsigned flags)
+smtp_chunk_cmd_callback(transport_ctx * tctx, unsigned chunk_size,
+  unsigned flags)
 {
 smtp_transport_options_block * ob =
   (smtp_transport_options_block *)(tctx->tblock->options_block);
@@ -1801,9 +1812,11 @@ goto SEND_QUIT;
     }
   }
 
-/* For continuing deliveries down the same channel, the socket is the standard
-input, and we don't need to redo EHLO here (but may need to do so for TLS - see
-below). Set up the pointer to where subsequent commands will be left, for
+/* For continuing deliveries down the same channel, having re-exec'd  the socket
+is the standard input; for a socket held open from verify it is recorded
+in the cutthrough context block.  Either way we don't need to redo EHLO here
+(but may need to do so for TLS - see below).
+Set up the pointer to where subsequent commands will be left, for
 error messages. Note that smtp_peer_options will have been
 set from the command line if they were set in the process that passed the
 connection on. */
@@ -1815,19 +1828,30 @@ separate - we could match up by host ip+port as a bodge. */
 
 else
   {
-  sx->inblock.sock = sx->outblock.sock = 0;    /* stdin */
+  if (cutthrough.fd >= 0 && cutthrough.callout_hold_only)
+    {
+    sx->inblock.sock = sx->outblock.sock = cutthrough.fd;
+    sx->host->port = sx->port = cutthrough.host.port;
+    }
+  else
+    {
+    sx->inblock.sock = sx->outblock.sock = 0;  /* stdin */
+    sx->host->port = sx->port;    /* Record the port that was used */
+    }
   smtp_command = big_buffer;
-  sx->host->port = sx->port;    /* Record the port that was used */
   sx->helo_data = NULL;                /* ensure we re-expand ob->helo_data */
 
-  /* For a continued connection with TLS being proxied for us, nothing
-  more to do. */
+  /* For a continued connection with TLS being proxied for us, or a
+  held-open verify connection with TLS, nothing more to do. */
 
-  if (continue_proxy)
+  if (  continue_proxy_cipher
+     || (cutthrough.fd >= 0 && cutthrough.callout_hold_only && cutthrough.is_tls)
+     )
     {
     sx->peer_offered = smtp_peer_options;
     pipelining_active = !!(smtp_peer_options & PEER_OFFERED_PIPE);
-    HDEBUG(D_transport) debug_printf("continued connection, proxied TLS\n");
+    HDEBUG(D_transport) debug_printf("continued connection, %s TLS\n",
+      continue_proxy_cipher ? "proxied" : "verify conn with");
     return OK;
     }
   HDEBUG(D_transport) debug_printf("continued connection, no TLS\n");
@@ -2453,7 +2477,8 @@ for (addr = sx->first_addr, address_count = 0;
     ? dsn_support_yes : dsn_support_no;
 
   address_count++;
-  no_flush = pipelining_active && !sx->verify && (!mua_wrapper || addr->next);
+  no_flush = pipelining_active && !sx->verify
+         && (!mua_wrapper || addr->next && address_count < sx->max_rcpt);
 
   build_rcptcmd_options(sx, addr);
 
@@ -2511,19 +2536,20 @@ return 0;
 * Proxy TLS connection for another transport process *
 ******************************************************/
 /*
-Use the smtp-context buffer as a staging area, and select on both the slave
-process and the TLS'd fd for data to read (per the coding in ip_recv() and
+Use the given buffer as a staging area, and select on both the given fd
+and the TLS'd client-fd for data to read (per the coding in ip_recv() and
 fd_ready() this is legitimate).  Do blocking full-size writes, and reads
 under a timeout.
 
 Arguments:
-  sx           smtp context block
+  buf          space to use for buffering
+  bufsiz       size of buffer
   proxy_fd     comms to proxied process
   timeout      per-read timeout, seconds
 */
 
-static void
-smtp_proxy_tls(smtp_context * sx, int proxy_fd, int timeout)
+void
+smtp_proxy_tls(uschar * buf, size_t bsize, int proxy_fd, int timeout)
 {
 fd_set fds;
 int max_fd = MAX(proxy_fd, tls_out.active) + 1;
@@ -2559,7 +2585,7 @@ for (fd_bits = 3; fd_bits; )
 
   /* handle inbound data */
   if (FD_ISSET(tls_out.active, &fds))
-    if ((rc = tls_read(FALSE, sx->buffer, sizeof(sx->buffer))) <= 0)
+    if ((rc = tls_read(FALSE, buf, bsize)) <= 0)
       {
       fd_bits &= ~1;
       FD_CLR(tls_out.active, &fds);
@@ -2568,14 +2594,14 @@ for (fd_bits = 3; fd_bits; )
     else
       {
       for (nbytes = 0; rc - nbytes > 0; nbytes += i)
-       if ((i = write(proxy_fd, sx->buffer + nbytes, rc - nbytes)) < 0) return;
+       if ((i = write(proxy_fd, buf + nbytes, rc - nbytes)) < 0) return;
       }
   else if (fd_bits & 1)
     FD_SET(tls_out.active, &fds);
 
   /* handle outbound data */
   if (FD_ISSET(proxy_fd, &fds))
-    if ((rc = read(proxy_fd, sx->buffer, sizeof(sx->buffer))) <= 0)
+    if ((rc = read(proxy_fd, buf, bsize)) <= 0)
       {
       fd_bits &= ~2;
       FD_CLR(proxy_fd, &fds);
@@ -2584,7 +2610,7 @@ for (fd_bits = 3; fd_bits; )
     else
       {
       for (nbytes = 0; rc - nbytes > 0; nbytes += i)
-       if ((i = tls_write(FALSE, sx->buffer + nbytes, rc - nbytes)) < 0) return;
+       if ((i = tls_write(FALSE, buf + nbytes, rc - nbytes)) < 0) return;
       }
   else if (fd_bits & 2)
     FD_SET(proxy_fd, &fds);
@@ -2678,17 +2704,14 @@ set it up. This cannot be done until the identify of the host is known. */
 
 if (tblock->filter_command)
   {
-  BOOL rc;
-  uschar fbuf[64];
-  sprintf(CS fbuf, "%.50s transport", tblock->name);
-  rc = transport_set_up_command(&transport_filter_argv, tblock->filter_command,
-    TRUE, DEFER, addrlist, fbuf, NULL);
   transport_filter_timeout = tblock->filter_timeout;
 
   /* On failure, copy the error to all addresses, abandon the SMTP call, and
   yield ERROR. */
 
-  if (!rc)
+  if (!transport_set_up_command(&transport_filter_argv,
+       tblock->filter_command, TRUE, DEFER, addrlist,
+       string_sprintf("%.50s transport", tblock->name), NULL))
     {
     set_errno_nohost(addrlist->next, addrlist->basic_errno, addrlist->message, DEFER,
       FALSE);
@@ -2707,6 +2730,7 @@ if (tblock->filter_command)
     }
   }
 
+sx.first_addr = addrlist;
 
 /* For messages that have more than the maximum number of envelope recipients,
 we want to send several transactions down the same SMTP connection. (See
@@ -2718,39 +2742,61 @@ transaction to handle. */
 
 SEND_MESSAGE:
 sx.from_addr = return_path;
-sx.first_addr = sx.sync_addr = addrlist;
+sx.sync_addr = sx.first_addr;
 sx.ok = FALSE;
 sx.send_rset = TRUE;
 sx.completed_addr = FALSE;
 
 
-/* Initiate a message transfer. */
+/* If we are a continued-connection-after-verify the MAIL and RCPT
+commands were already sent; do not re-send but do mark the addrs as
+having been accepted up to RCPT stage.  A traditional cont-conn
+always has a sequence number greater than one. */
 
-switch(smtp_write_mail_and_rcpt_cmds(&sx, &yield))
+if (continue_hostname && continue_sequence == 1)
   {
-  case 0:              break;
-  case -1: case -2:    goto RESPONSE_FAILED;
-  case -3:             goto END_OFF;
-  case -4:             goto SEND_QUIT;
-  default:             goto SEND_FAILED;
-  }
+  address_item * addr;
 
-/* If we are an MUA wrapper, abort if any RCPTs were rejected, either
-permanently or temporarily. We should have flushed and synced after the last
-RCPT. */
+  sx.peer_offered = smtp_peer_options;
+  sx.pending_MAIL = FALSE;
+  sx.ok = TRUE;
+  sx.next_addr = NULL;
 
-if (mua_wrapper)
+  for (addr = addrlist; addr; addr = addr->next)
+    addr->transport_return = PENDING_OK;
+  }
+else
   {
-  address_item *badaddr;
-  for (badaddr = sx.first_addr; badaddr; badaddr = badaddr->next)
-    if (badaddr->transport_return != PENDING_OK)
-      {
-      /*XXX could we find a better errno than 0 here? */
-      set_errno_nohost(addrlist, 0, badaddr->message, FAIL,
-       testflag(badaddr, af_pass_message));
-      sx.ok = FALSE;
-      break;
-      }
+  /* Initiate a message transfer. */
+
+  switch(smtp_write_mail_and_rcpt_cmds(&sx, &yield))
+    {
+    case 0:            break;
+    case -1: case -2:  goto RESPONSE_FAILED;
+    case -3:           goto END_OFF;
+    case -4:           goto SEND_QUIT;
+    default:           goto SEND_FAILED;
+    }
+
+  /* If we are an MUA wrapper, abort if any RCPTs were rejected, either
+  permanently or temporarily. We should have flushed and synced after the last
+  RCPT. */
+
+  if (mua_wrapper)
+    {
+    address_item * a;
+    unsigned cnt;
+
+    for (a = sx.first_addr, cnt = 0; a && cnt < sx.max_rcpt; a = a->next, cnt++)
+      if (a->transport_return != PENDING_OK)
+       {
+       /*XXX could we find a better errno than 0 here? */
+       set_errno_nohost(addrlist, 0, a->message, FAIL,
+         testflag(a, af_pass_message));
+       sx.ok = FALSE;
+       break;
+       }
+    }
   }
 
 /* If ok is TRUE, we know we have got at least one good recipient, and must now
@@ -3050,7 +3096,7 @@ else
         else
           sprintf(CS sx.buffer, "%.500s\n", addr->unique);
 
-        DEBUG(D_deliver) debug_printf("journalling %s\n", sx.buffer);
+        DEBUG(D_deliver) debug_printf("S:journalling %s\n", sx.buffer);
         len = Ustrlen(CS sx.buffer);
         if (write(journal_fd, sx.buffer, len) != len)
           log_write(0, LOG_MAIN|LOG_PANIC, "failed to write journal for "
@@ -3277,7 +3323,7 @@ if (sx.completed_addr && sx.ok && sx.send_quit)
      || continue_more
      || (
 #ifdef SUPPORT_TLS
-          (  tls_out.active < 0  &&  !continue_proxy
+          (  tls_out.active < 0  &&  !continue_proxy_cipher
            || verify_check_given_host(&sx.ob->hosts_nopass_tls, host) != OK
           )
         &&
@@ -3376,24 +3422,28 @@ propagate it from the initial
        just passed the baton to.  Fork a child to to do it, and return to
        get logging done asap.  Which way to place the work makes assumptions
        about post-fork prioritisation which may not hold on all platforms. */
-
+#ifdef SUPPORT_TLS
        if (tls_out.active >= 0)
          {
          int pid = fork();
          if (pid > 0)          /* parent */
            {
+           waitpid(pid, NULL, 0);
            tls_close(FALSE, FALSE);
            (void)close(sx.inblock.sock);
            continue_transport = NULL;
            continue_hostname = NULL;
            return yield;
            }
-         else if (pid == 0)    /* child */
+         else if (pid == 0)    /* child; fork again to disconnect totally */
            {
-           smtp_proxy_tls(&sx, pfd[0], sx.ob->command_timeout);
+           if ((pid = fork()))
+             _exit(pid ? EXIT_FAILURE : EXIT_SUCCESS);
+           smtp_proxy_tls(sx.buffer, sizeof(sx.buffer), pfd[0], sx.ob->command_timeout);
            exim_exit(0);
            }
          }
+#endif
        }
       }
 
@@ -3608,8 +3658,10 @@ DEBUG(D_transport)
     for (host = hostlist; host; host = host->next)
       debug_printf("  %s:%d\n", host->name, host->port);
     }
-  if (continue_hostname) debug_printf("already connected to %s [%s]\n",
-      continue_hostname, continue_host_address);
+  if (continue_hostname)
+    debug_printf("already connected to %s [%s] (on fd %d)\n",
+      continue_hostname, continue_host_address,
+      cutthrough.fd >= 0 ? cutthrough.fd : 0);
   }
 
 /* Set the flag requesting that these hosts be added to the waiting