Fix crash in transport, on second smtp-connect fail for a list of target hosts
[exim.git] / src / src / transports / smtp.c
index 43b62351358e6d483161422aa7ed019083f25dc8..3217d81ada886f3126dd494b5e3355f854bbc951 100644 (file)
@@ -789,7 +789,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;
@@ -1291,44 +1293,44 @@ ehlo_response(uschar * buf, uschar checks)
 size_t bsize = Ustrlen(buf);
 
 #ifdef SUPPORT_TLS
-if (  checks & PEER_OFFERED_TLS
+if (  checks & OPTION_TLS
    && pcre_exec(regex_STARTTLS, NULL, CS buf, bsize, 0, PCRE_EOPT, NULL, 0) < 0)
-  checks &= ~PEER_OFFERED_TLS;
+  checks &= ~OPTION_TLS;
 #endif
 
-if (  checks & PEER_OFFERED_IGNQ
+if (  checks & OPTION_IGNQ
    && pcre_exec(regex_IGNOREQUOTA, NULL, CS buf, bsize, 0,
                PCRE_EOPT, NULL, 0) < 0)
-  checks &= ~PEER_OFFERED_IGNQ;
+  checks &= ~OPTION_IGNQ;
 
-if (  checks & PEER_OFFERED_CHUNKING
+if (  checks & OPTION_CHUNKING
    && pcre_exec(regex_CHUNKING, NULL, CS buf, bsize, 0, PCRE_EOPT, NULL, 0) < 0)
-  checks &= ~PEER_OFFERED_CHUNKING;
+  checks &= ~OPTION_CHUNKING;
 
 #ifndef DISABLE_PRDR
-if (  checks & PEER_OFFERED_PRDR
+if (  checks & OPTION_PRDR
    && pcre_exec(regex_PRDR, NULL, CS buf, bsize, 0, PCRE_EOPT, NULL, 0) < 0)
-  checks &= ~PEER_OFFERED_PRDR;
+  checks &= ~OPTION_PRDR;
 #endif
 
 #ifdef SUPPORT_I18N
-if (  checks & PEER_OFFERED_UTF8
+if (  checks & OPTION_UTF8
    && pcre_exec(regex_UTF8, NULL, CS buf, bsize, 0, PCRE_EOPT, NULL, 0) < 0)
-  checks &= ~PEER_OFFERED_UTF8;
+  checks &= ~OPTION_UTF8;
 #endif
 
-if (  checks & PEER_OFFERED_DSN
+if (  checks & OPTION_DSN
    && pcre_exec(regex_DSN, NULL, CS buf, bsize, 0, PCRE_EOPT, NULL, 0) < 0)
-  checks &= ~PEER_OFFERED_DSN;
+  checks &= ~OPTION_DSN;
 
-if (  checks & PEER_OFFERED_PIPE
+if (  checks & OPTION_PIPE
    && pcre_exec(regex_PIPELINING, NULL, CS buf, bsize, 0,
                PCRE_EOPT, NULL, 0) < 0)
-  checks &= ~PEER_OFFERED_PIPE;
+  checks &= ~OPTION_PIPE;
 
-if (  checks & PEER_OFFERED_SIZE
+if (  checks & OPTION_SIZE
    && pcre_exec(regex_SIZE, NULL, CS buf, bsize, 0, PCRE_EOPT, NULL, 0) < 0)
-  checks &= ~PEER_OFFERED_SIZE;
+  checks &= ~OPTION_SIZE;
 
 return checks;
 }
@@ -1409,7 +1411,7 @@ if (sx->pending_BDAT)
     if (errno == 0 && sx->buffer[0] == '4')
       {
       errno = ERRNO_DATA4XX;   /*XXX does this actually get used? */
-      sx->first_addr->more_errno |=
+      sx->addrlist->more_errno |=
        ((sx->buffer[1] - '0')*10 + sx->buffer[2] - '0') << 8;
       }
     return ERROR;
@@ -1454,9 +1456,7 @@ smtp_setup_conn(smtp_context * sx, BOOL suppress_tls)
 dns_answer tlsa_dnsa;
 #endif
 BOOL pass_message = FALSE;
-
 uschar * message = NULL;
-int save_errno;
 int yield = OK;
 int rc;
 
@@ -1481,6 +1481,7 @@ sx->dane_required = verify_check_given_host(&sx->ob->hosts_require_dane, sx->hos
 
 if ((sx->max_rcpt = sx->tblock->max_addresses) == 0) sx->max_rcpt = 999999;
 sx->peer_offered = 0;
+sx->avoid_option = 0;
 sx->igquotstr = US"";
 if (!sx->helo_data) sx->helo_data = sx->ob->helo_data;
 #ifdef EXPERIMENTAL_DSN_INFO
@@ -1551,14 +1552,13 @@ if (continue_hostname == NULL)
   if (sx->inblock.sock < 0)
     {
     uschar * msg = NULL;
-    int save_errno = errno;
     if (sx->verify)
       {
-      msg = strerror(errno);
+      msg = US strerror(errno);
       HDEBUG(D_verify) debug_printf("connect: %s\n", msg);
       }
     set_errno_nohost(sx->addrlist,
-      save_errno == ETIMEDOUT ? ERRNO_CONNECTTIMEOUT : save_errno,
+      errno == ETIMEDOUT ? ERRNO_CONNECTTIMEOUT : errno,
       sx->verify ? string_sprintf("could not connect: %s", msg)
             : NULL,
       DEFER, FALSE);
@@ -1718,7 +1718,7 @@ goto SEND_QUIT;
 #ifdef SUPPORT_TLS
   if (sx->smtps)
     {
-    smtp_peer_options |= PEER_OFFERED_TLS;
+    smtp_peer_options |= OPTION_TLS;
     suppress_tls = FALSE;
     sx->ob->tls_tempfail_tryclear = FALSE;
     smtp_command = US"SSL-on-connect";
@@ -1783,18 +1783,18 @@ goto SEND_QUIT;
       }
     }
 
-  sx->peer_offered = smtp_peer_options = 0;
+  sx->avoid_option = sx->peer_offered = smtp_peer_options = 0;
 
   if (sx->esmtp || sx->lmtp)
     {
     sx->peer_offered = ehlo_response(sx->buffer,
-      PEER_OFFERED_TLS /* others checked later */
+      OPTION_TLS       /* others checked later */
       );
 
   /* Set tls_offered if the response to EHLO specifies support for STARTTLS. */
 
 #ifdef SUPPORT_TLS
-    smtp_peer_options |= sx->peer_offered & PEER_OFFERED_TLS;
+    smtp_peer_options |= sx->peer_offered & OPTION_TLS;
 #endif
     }
   }
@@ -1828,7 +1828,7 @@ the client not be required to use TLS. If the response is bad, copy the buffer
 for error analysis. */
 
 #ifdef SUPPORT_TLS
-if (  smtp_peer_options & PEER_OFFERED_TLS
+if (  smtp_peer_options & OPTION_TLS
    && !suppress_tls
    && verify_check_given_host(&sx->ob->hosts_avoid_tls, sx->host) != OK
    && (  !sx->verify
@@ -1973,7 +1973,7 @@ else if (  sx->smtps
   {
   errno = ERRNO_TLSREQUIRED;
   message = string_sprintf("a TLS session is required, but %s",
-    smtp_peer_options & PEER_OFFERED_TLS
+    smtp_peer_options & OPTION_TLS
     ? "an attempt to start TLS failed" : "the server did not offer TLS support");
   goto TLS_FAILED;
   }
@@ -1994,60 +1994,60 @@ if (continue_hostname == NULL
     {
     sx->peer_offered = ehlo_response(sx->buffer,
        0 /* no TLS */
-       | (sx->lmtp && sx->ob->lmtp_ignore_quota ? PEER_OFFERED_IGNQ : 0)
-       | PEER_OFFERED_CHUNKING
-       | PEER_OFFERED_PRDR
+       | (sx->lmtp && sx->ob->lmtp_ignore_quota ? OPTION_IGNQ : 0)
+       | OPTION_CHUNKING
+       | OPTION_PRDR
 #ifdef SUPPORT_I18N
-       | (sx->addrlist->prop.utf8_msg ? PEER_OFFERED_UTF8 : 0)
+       | (sx->addrlist->prop.utf8_msg ? OPTION_UTF8 : 0)
          /*XXX if we hand peercaps on to continued-conn processes,
                must not depend on this addr */
 #endif
-       | PEER_OFFERED_DSN
-       | PEER_OFFERED_PIPE
-       | (sx->ob->size_addition >= 0 ? PEER_OFFERED_SIZE : 0)
+       | OPTION_DSN
+       | OPTION_PIPE
+       | (sx->ob->size_addition >= 0 ? OPTION_SIZE : 0)
       );
 
     /* Set for IGNOREQUOTA if the response to LHLO specifies support and the
     lmtp_ignore_quota option was set. */
 
-    sx->igquotstr = sx->peer_offered & PEER_OFFERED_IGNQ ? US" IGNOREQUOTA" : US"";
+    sx->igquotstr = sx->peer_offered & OPTION_IGNQ ? US" IGNOREQUOTA" : US"";
 
     /* If the response to EHLO specified support for the SIZE parameter, note
     this, provided size_addition is non-negative. */
 
-    smtp_peer_options |= sx->peer_offered & PEER_OFFERED_SIZE;
+    smtp_peer_options |= sx->peer_offered & OPTION_SIZE;
 
     /* Note whether the server supports PIPELINING. If hosts_avoid_esmtp matched
     the current host, esmtp will be false, so PIPELINING can never be used. If
     the current host matches hosts_avoid_pipelining, don't do it. */
 
-    if (  sx->peer_offered & PEER_OFFERED_PIPE
+    if (  sx->peer_offered & OPTION_PIPE
        && verify_check_given_host(&sx->ob->hosts_avoid_pipelining, sx->host) != OK)
-      smtp_peer_options |= PEER_OFFERED_PIPE;
+      smtp_peer_options |= OPTION_PIPE;
 
     DEBUG(D_transport) debug_printf("%susing PIPELINING\n",
-      smtp_peer_options & PEER_OFFERED_PIPE ? "" : "not ");
+      smtp_peer_options & OPTION_PIPE ? "" : "not ");
 
-    if (  sx->peer_offered & PEER_OFFERED_CHUNKING
+    if (  sx->peer_offered & OPTION_CHUNKING
        && verify_check_given_host(&sx->ob->hosts_try_chunking, sx->host) != OK)
-      sx->peer_offered &= ~PEER_OFFERED_CHUNKING;
+      sx->peer_offered &= ~OPTION_CHUNKING;
 
-    if (sx->peer_offered & PEER_OFFERED_CHUNKING)
+    if (sx->peer_offered & OPTION_CHUNKING)
       {DEBUG(D_transport) debug_printf("CHUNKING usable\n");}
 
 #ifndef DISABLE_PRDR
-    if (  sx->peer_offered & PEER_OFFERED_PRDR
+    if (  sx->peer_offered & OPTION_PRDR
        && verify_check_given_host(&sx->ob->hosts_try_prdr, sx->host) != OK)
-      sx->peer_offered &= ~PEER_OFFERED_PRDR;
+      sx->peer_offered &= ~OPTION_PRDR;
 
-    if (sx->peer_offered & PEER_OFFERED_PRDR)
+    if (sx->peer_offered & OPTION_PRDR)
       {DEBUG(D_transport) debug_printf("PRDR usable\n");}
 #endif
 
     /* Note if the server supports DSN */
-    smtp_peer_options |= sx->peer_offered & PEER_OFFERED_DSN;
+    smtp_peer_options |= sx->peer_offered & OPTION_DSN;
     DEBUG(D_transport) debug_printf("%susing DSN\n",
-                       sx->peer_offered & PEER_OFFERED_DSN ? "" : "not ");
+                       sx->peer_offered & OPTION_DSN ? "" : "not ");
 
     /* Note if the response to EHLO specifies support for the AUTH extension.
     If it has, check that this host is one we want to authenticate to, and do
@@ -2064,7 +2064,7 @@ if (continue_hostname == NULL
       }
     }
   }
-pipelining_active = !!(smtp_peer_options & PEER_OFFERED_PIPE);
+pipelining_active = !!(smtp_peer_options & OPTION_PIPE);
 
 /* The setting up of the SMTP call is now complete. Any subsequent errors are
 message-specific. */
@@ -2082,7 +2082,7 @@ if (sx->addrlist->prop.utf8_msg)
   }
 
 /* If this is an international message we need the host to speak SMTPUTF8 */
-if (sx->utf8_needed && !(sx->peer_offered & PEER_OFFERED_UTF8))
+if (sx->utf8_needed && !(sx->peer_offered & OPTION_UTF8))
   {
   errno = ERRNO_UTF8_FWD;
   goto RESPONSE_FAILED;
@@ -2104,13 +2104,13 @@ return OK;
   SEND_FAILED:
     code = '4';
     message = US string_sprintf("send() to %s [%s] failed: %s",
-      sx->host->name, sx->host->address, strerror(save_errno));
+      sx->host->name, sx->host->address, strerror(errno));
     sx->send_quit = FALSE;
     goto FAILED;
 
   /* This label is jumped to directly when a TLS negotiation has failed,
   or was not done for a host for which it is required. Values will be set
-  in message and save_errno, and setting_up will always be true. Treat as
+  in message and errno, and setting_up will always be true. Treat as
   a temporary error. */
 
   EHLOHELO_FAILED:
@@ -2128,26 +2128,34 @@ return OK;
 
   /* The failure happened while setting up the call; see if the failure was
   a 5xx response (this will either be on connection, or following HELO - a 5xx
-  after EHLO causes it to try HELO). If so, fail all addresses, as this host is
-  never going to accept them. For other errors during setting up (timeouts or
-  whatever), defer all addresses, and yield DEFER, so that the host is not
-  tried again for a while. */
+  after EHLO causes it to try HELO). If so, and there are no more hosts to try,
+  fail all addresses, as this host is never going to accept them. For other
+  errors during setting up (timeouts or whatever), defer all addresses, and
+  yield DEFER, so that the host is not tried again for a while.
+
+  XXX This peeking for another host feels like a layering violation. We want
+  to note the host as unusable, but down here we shouldn't know if this was
+  the last host to try for the addr(list).  Perhaps the upper layer should be
+  the one to do set_errno() ?  The problem is that currently the addr is where
+  errno etc. are stashed, but until we run out of hosts to try the errors are
+  host-specific.  Maybe we should enhance the host_item definition? */
 
 FAILED:
-  save_errno = errno;
   sx->ok = FALSE;                /* For when reached by GOTO */
-
-  yield = code == '5'
+  set_errno(sx->addrlist, errno, message,
+           sx->host->next
+           ? DEFER
+           : code == '5'
 #ifdef SUPPORT_I18N
-         || errno == ERRNO_UTF8_FWD
+                       || errno == ERRNO_UTF8_FWD
 #endif
-    ? FAIL : DEFER;
-
-  set_errno(sx->addrlist, save_errno, message, yield, pass_message, sx->host
+           ? FAIL : DEFER,
+           pass_message, sx->host
 #ifdef EXPERIMENTAL_DSN_INFO
            , sx->smtp_greeting, sx->helo_response
 #endif
            );
+  yield = DEFER;
   }
 
 
@@ -2170,7 +2178,7 @@ writing RSET might have failed, or there may be other addresses whose hosts are
 specified in the transports, and therefore not visible at top level, in which
 case continue_more won't get set. */
 
-HDEBUG(D_transport|D_acl|D_v) debug_printf("  SMTP(close)>>\n");
+HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP(close)>>\n");
 if (sx->send_quit)
   {
   shutdown(sx->outblock.sock, SHUT_WR);
@@ -2206,14 +2214,15 @@ int address_count;
 
 *p = 0;
 
-/* If we know the receiving MTA supports the SIZE qualification,
+/* If we know the receiving MTA supports the SIZE qualification, and we know it,
 send it, adding something to the message size to allow for imprecision
 and things that get added en route. Exim keeps the number of lines
 in a message, so we can give an accurate value for the original message, but we
 need some additional to handle added headers. (Double "." characters don't get
 included in the count.) */
 
-if (sx->peer_offered & PEER_OFFERED_SIZE)
+if (  message_size > 0
+   && sx->peer_offered & OPTION_SIZE && !(sx->avoid_option & OPTION_SIZE))
   {
   sprintf(CS p, " SIZE=%d", message_size+message_linecount+sx->ob->size_addition);
   while (*p) p++;
@@ -2224,7 +2233,7 @@ if (sx->peer_offered & PEER_OFFERED_SIZE)
 request that */
 
 sx->prdr_active = FALSE;
-if (sx->peer_offered & PEER_OFFERED_PRDR)
+if (sx->peer_offered & OPTION_PRDR)
   for (addr = addrlist; addr; addr = addr->next)
     if (addr->transport_return == PENDING_DEFER)
       {
@@ -2243,7 +2252,7 @@ if (sx->peer_offered & PEER_OFFERED_PRDR)
 /* If it supports internationalised messages, and this meesage need that,
 request it */
 
-if (  sx->peer_offered & PEER_OFFERED_UTF8
+if (  sx->peer_offered & OPTION_UTF8
    && addrlist->prop.utf8_msg
    && !addrlist->prop.utf8_downcvt
    )
@@ -2265,7 +2274,7 @@ for (sx->dsn_all_lasthop = TRUE, addr = addrlist, address_count = 0;
 
 /* Add any DSN flags to the mail command */
 
-if (sx->peer_offered & PEER_OFFERED_DSN && !sx->dsn_all_lasthop)
+if (sx->peer_offered & OPTION_DSN && !sx->dsn_all_lasthop)
   {
   if (dsn_ret == dsn_ret_hdrs)
     { Ustrcpy(p, " RET=HDRS"); p += 9; }
@@ -2301,7 +2310,7 @@ uschar * p = sx->buffer;
 
 /* Add any DSN flags to the rcpt command */
 
-if (sx->peer_offered & PEER_OFFERED_DSN && !(addr->dsn_flags & rf_dsnlasthop))
+if (sx->peer_offered & OPTION_DSN && !(addr->dsn_flags & rf_dsnlasthop))
   {
   if (addr->dsn_flags & rf_dsnflags)
     {
@@ -2371,7 +2380,7 @@ sx->pending_MAIL = TRUE;     /* The block starts with MAIL */
   the delivery log line. */
 
   if (  sx->addrlist->prop.utf8_msg
-     && (sx->addrlist->prop.utf8_downcvt || !(sx->peer_offered & PEER_OFFERED_UTF8))
+     && (sx->addrlist->prop.utf8_downcvt || !(sx->peer_offered & OPTION_UTF8))
      )
     {
     if (s = string_address_utf8_to_alabel(s, &errstr), errstr)
@@ -2435,11 +2444,12 @@ for (addr = sx->first_addr, address_count = 0;
   BOOL no_flush;
   uschar * rcpt_addr;
 
-  addr->dsn_aware = sx->peer_offered & PEER_OFFERED_DSN
+  addr->dsn_aware = sx->peer_offered & OPTION_DSN
     ? 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);
 
@@ -2598,10 +2608,10 @@ if (tblock->filter_command)
   if (  transport_filter_argv
      && *transport_filter_argv
      && **transport_filter_argv
-     && sx.peer_offered & PEER_OFFERED_CHUNKING
+     && sx.peer_offered & OPTION_CHUNKING
      )
     {
-    sx.peer_offered &= ~PEER_OFFERED_CHUNKING;
+    sx.peer_offered &= ~OPTION_CHUNKING;
     DEBUG(D_transport) debug_printf("CHUNKING not usable due to transport filter\n");
     }
   }
@@ -2640,13 +2650,15 @@ RCPT. */
 
 if (mua_wrapper)
   {
-  address_item *badaddr;
-  for (badaddr = sx.first_addr; badaddr; badaddr = badaddr->next)
-    if (badaddr->transport_return != PENDING_OK)
+  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, badaddr->message, FAIL,
-       testflag(badaddr, af_pass_message));
+      set_errno_nohost(addrlist, 0, a->message, FAIL,
+       testflag(a, af_pass_message));
       sx.ok = FALSE;
       break;
       }
@@ -2660,7 +2672,7 @@ are pipelining. The responses are all handled by sync_responses().
 If using CHUNKING, do not send a BDAT until we know how big a chunk we want
 to send is. */
 
-if (  !(sx.peer_offered & PEER_OFFERED_CHUNKING)
+if (  !(sx.peer_offered & OPTION_CHUNKING)
    && (sx.ok || (pipelining_active && !mua_wrapper)))
   {
   int count = smtp_write_command(&sx.outblock, FALSE, "DATA\r\n");
@@ -2690,7 +2702,7 @@ for handling the SMTP dot-handling protocol, flagging to apply to headers as
 well as body. Set the appropriate timeout value to be used for each chunk.
 (Haven't been able to make it work using select() for writing yet.) */
 
-if (!(sx.peer_offered & PEER_OFFERED_CHUNKING) && !sx.ok)
+if (!(sx.peer_offered & OPTION_CHUNKING) && !sx.ok)
   {
   /* Save the first address of the next batch. */
   sx.first_addr = sx.next_addr;
@@ -2716,7 +2728,7 @@ else
   of responses.  The callback needs a whole bunch of state so set up
   a transport-context structure to be passed around. */
 
-  if (sx.peer_offered & PEER_OFFERED_CHUNKING)
+  if (sx.peer_offered & OPTION_CHUNKING)
     {
     tctx.check_string = tctx.escape_string = NULL;
     tctx.options |= topt_use_bdat;
@@ -2741,7 +2753,7 @@ else
   transport_write_timeout = sx.ob->data_timeout;
   smtp_command = US"sending data block";   /* For error messages */
   DEBUG(D_transport|D_v)
-    if (sx.peer_offered & PEER_OFFERED_CHUNKING)
+    if (sx.peer_offered & OPTION_CHUNKING)
       debug_printf("         will write message using CHUNKING\n");
     else
       debug_printf("  SMTP>> writing message and terminating \".\"\n");
@@ -2775,7 +2787,7 @@ else
 
   smtp_command = US"end of data";
 
-  if (sx.peer_offered & PEER_OFFERED_CHUNKING && sx.cmd_count > 1)
+  if (sx.peer_offered & OPTION_CHUNKING && sx.cmd_count > 1)
     {
     /* Reap any outstanding MAIL & RCPT commands, but not a DATA-go-ahead */
     switch(sync_responses(&sx, sx.cmd_count-1, 0))
@@ -2930,7 +2942,7 @@ else
 #ifndef DISABLE_PRDR
       if (sx.prdr_active) addr->flags |= af_prdr_used;
 #endif
-      if (sx.peer_offered & PEER_OFFERED_CHUNKING) addr->flags |= af_chunking_used;
+      if (sx.peer_offered & OPTION_CHUNKING) addr->flags |= af_chunking_used;
       flag = '-';
 
 #ifndef DISABLE_PRDR
@@ -3291,7 +3303,7 @@ writing RSET might have failed, or there may be other addresses whose hosts are
 specified in the transports, and therefore not visible at top level, in which
 case continue_more won't get set. */
 
-HDEBUG(D_transport|D_acl|D_v) debug_printf("  SMTP(close)>>\n");
+HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP(close)>>\n");
 if (sx.send_quit)
   {
   shutdown(sx.outblock.sock, SHUT_WR);