Basic cutthrough delivery.
[exim.git] / src / src / receive.c
index 8fba1caeb8774287b38c37112b86311b0c3866b8..108e8d436428c7a5c30a973b68ede31b93244038 100644 (file)
@@ -1,22 +1,16 @@
-/* $Cambridge: exim/src/src/receive.c,v 1.31 2006/11/14 16:40:36 ph10 Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2006 */
+/* Copyright (c) University of Cambridge 1995 - 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Code for receiving a message and setting up spool files. */
 
 #include "exim.h"
 
-#ifdef EXPERIMENTAL_DOMAINKEYS
-#define RECEIVE_GETC dk_receive_getc
-#define RECEIVE_UNGETC dk_receive_ungetc
-#else
-#define RECEIVE_GETC receive_getc
-#define RECEIVE_UNGETC receive_ungetc
+#ifdef EXPERIMENTAL_DCC
+extern int dcc_ok;
 #endif
 
 /*************************************************
@@ -178,7 +172,7 @@ else
     }
   }
 
-/* We now have the patch; do the business */
+/* We now have the path; do the business */
 
 memset(&statbuf, 0, sizeof(statbuf));
 
@@ -283,12 +277,14 @@ that case is done by setting a flag to cause the log functions to call this
 function if there is an ultimate disaster. That is why it is globally
 accessible.
 
-Arguments:   SMTP response to give if in an SMTP session
+Arguments:
+  reason     text reason to pass to the not-quit ACL
+  msg        default SMTP response to give if in an SMTP session
 Returns:     it doesn't
 */
 
 void
-receive_bomb_out(uschar *msg)
+receive_bomb_out(uschar *reason, uschar *msg)
 {
 /* If spool_name is set, it contains the name of the data file that is being
 written. Unlink it before closing so that it cannot be picked up by a delivery
@@ -306,20 +302,16 @@ if (spool_name[0] != 0)
 if (data_file != NULL) (void)fclose(data_file);
   else if (data_fd >= 0) (void)close(data_fd);
 
-/* Attempt to close down an SMTP connection tidily. */
+/* Attempt to close down an SMTP connection tidily. For non-batched SMTP, call
+smtp_notquit_exit(), which runs the NOTQUIT ACL, if present, and handles the
+SMTP response. */
 
 if (smtp_input)
   {
-  if (!smtp_batched_input)
-    {
-    smtp_printf("421 %s %s - closing connection.\r\n", smtp_active_hostname,
-      msg);
-    mac_smtp_fflush();
-    }
-
-  /* Control does not return from moan_smtp_batch(). */
-
-  else moan_smtp_batch(NULL, "421 %s - message abandoned", msg);
+  if (smtp_batched_input)
+    moan_smtp_batch(NULL, "421 %s - message abandoned", msg);  /* No return */
+  smtp_notquit_exit(reason, US"421", US"%s %s - closing connection.",
+    smtp_active_hostname, msg);
   }
 
 /* Exit from the program (non-BSMTP cases) */
@@ -362,7 +354,7 @@ else
             LOG_MAIN, "timed out while reading local message");
   }
 
-receive_bomb_out(msg);   /* Does not return */
+receive_bomb_out(US"data-timeout", msg);   /* Does not return */
 }
 
 
@@ -384,7 +376,8 @@ local_scan_timeout_handler(int sig)
 sig = sig;    /* Keep picky compilers happy */
 log_write(0, LOG_MAIN|LOG_REJECT, "local_scan() function timed out - "
   "message temporarily rejected (size %d)", message_size);
-receive_bomb_out(US"local verification problem");   /* Does not return */
+/* Does not return */
+receive_bomb_out(US"local-scan-timeout", US"local verification problem");
 }
 
 
@@ -405,7 +398,8 @@ local_scan_crash_handler(int sig)
 {
 log_write(0, LOG_MAIN|LOG_REJECT, "local_scan() function crashed with "
   "signal %d - message temporarily rejected (size %d)", sig, message_size);
-receive_bomb_out(US"local verification problem");   /* Does not return */
+/* Does not return */
+receive_bomb_out(US"local-scan-error", US"local verification problem");
 }
 
 
@@ -442,7 +436,7 @@ else
     }
   }
 
-receive_bomb_out(msg);    /* Does not return */
+receive_bomb_out(US"signal-exit", msg);    /* Does not return */
 }
 
 
@@ -564,6 +558,7 @@ read_message_data(FILE *fout)
 {
 int ch_state;
 register int ch;
+register int linelength = 0;
 
 /* Handle the case when only EOF terminates the message */
 
@@ -571,11 +566,14 @@ if (!dot_ends)
   {
   register int last_ch = '\n';
 
-  for (; (ch = (RECEIVE_GETC)()) != EOF; last_ch = ch)
+  for (; (ch = (receive_getc)()) != EOF; last_ch = ch)
     {
     if (ch == 0) body_zerocount++;
     if (last_ch == '\r' && ch != '\n')
       {
+      if (linelength > max_received_linelength)
+        max_received_linelength = linelength;
+      linelength = 0;
       if (fputc('\n', fout) == EOF) return END_WERROR;
       message_size++;
       body_linecount++;
@@ -583,12 +581,21 @@ if (!dot_ends)
     if (ch == '\r') continue;
 
     if (fputc(ch, fout) == EOF) return END_WERROR;
-    if (ch == '\n') body_linecount++;
+    if (ch == '\n')
+      {
+      if (linelength > max_received_linelength)
+        max_received_linelength = linelength;
+      linelength = 0;
+      body_linecount++;
+      }
+    else linelength++;
     if (++message_size > thismessage_size_limit) return END_SIZE;
     }
 
   if (last_ch != '\n')
     {
+    if (linelength > max_received_linelength)
+      max_received_linelength = linelength;
     if (fputc('\n', fout) == EOF) return END_WERROR;
     message_size++;
     body_linecount++;
@@ -601,32 +608,44 @@ if (!dot_ends)
 
 ch_state = 1;
 
-while ((ch = (RECEIVE_GETC)()) != EOF)
+while ((ch = (receive_getc)()) != EOF)
   {
   if (ch == 0) body_zerocount++;
   switch (ch_state)
     {
     case 0:                         /* Normal state (previous char written) */
     if (ch == '\n')
-      { body_linecount++; ch_state = 1; }
+      {
+      body_linecount++;
+      if (linelength > max_received_linelength)
+        max_received_linelength = linelength;
+      linelength = -1;
+      ch_state = 1;
+      }
     else if (ch == '\r')
       { ch_state = 2; continue; }
     break;
 
     case 1:                         /* After written "\n" */
     if (ch == '.') { ch_state = 3; continue; }
-    if (ch != '\n') ch_state = 0;
+    if (ch != '\n') ch_state = 0; else linelength = -1;
     break;
 
     case 2:
     body_linecount++;               /* After unwritten "\r" */
+    if (linelength > max_received_linelength)
+      max_received_linelength = linelength;
     if (ch == '\n')
-      { ch_state = 1; }
+      {
+      ch_state = 1;
+      linelength = -1;
+      }
     else
       {
       if (message_size++, fputc('\n', fout) == EOF) return END_WERROR;
       if (ch == '\r') continue;
       ch_state = 0;
+      linelength = 0;
       }
     break;
 
@@ -634,6 +653,7 @@ while ((ch = (RECEIVE_GETC)()) != EOF)
     if (ch == '\n') return END_DOT;
     if (ch == '\r') { ch_state = 4; continue; }
     message_size++;
+    linelength++;
     if (fputc('.', fout) == EOF) return END_WERROR;
     ch_state = 0;
     break;
@@ -648,6 +668,7 @@ while ((ch = (RECEIVE_GETC)()) != EOF)
     break;
     }
 
+  linelength++;
   if (fputc(ch, fout) == EOF) return END_WERROR;
   if (++message_size > thismessage_size_limit) return END_SIZE;
   }
@@ -695,14 +716,18 @@ Arguments:
 
 Returns:    One of the END_xxx values indicating why it stopped reading
 */
+/*XXX cutthrough - need to copy to destination, not including the
+       terminating dot, canonicalizing newlines.
+*/
 
 static int
 read_message_data_smtp(FILE *fout)
 {
 int ch_state = 0;
-register int ch;
+int ch;
+register int linelength = 0;
 
-while ((ch = (RECEIVE_GETC)()) != EOF)
+while ((ch = (receive_getc)()) != EOF)
   {
   if (ch == 0) body_zerocount++;
   switch (ch_state)
@@ -722,6 +747,9 @@ while ((ch = (RECEIVE_GETC)()) != EOF)
       {
       ch_state = 0;
       body_linecount++;
+      if (linelength > max_received_linelength)
+        max_received_linelength = linelength;
+      linelength = -1;
       }
     else if (ch == '\r')
       {
@@ -732,6 +760,9 @@ while ((ch = (RECEIVE_GETC)()) != EOF)
 
     case 2:                             /* After (unwritten) CR */
     body_linecount++;
+    if (linelength > max_received_linelength)
+      max_received_linelength = linelength;
+    linelength = -1;
     if (ch == '\n')
       {
       ch_state = 0;
@@ -740,6 +771,7 @@ while ((ch = (RECEIVE_GETC)()) != EOF)
       {
       message_size++;
       if (fout != NULL && fputc('\n', fout) == EOF) return END_WERROR;
+      (void) cutthrough_put_nl();
       if (ch != '\r') ch_state = 1; else continue;
       }
     break;
@@ -760,6 +792,7 @@ while ((ch = (RECEIVE_GETC)()) != EOF)
     message_size++;
     body_linecount++;
     if (fout != NULL && fputc('\n', fout) == EOF) return END_WERROR;
+    (void) cutthrough_put_nl();
     if (ch == '\r')
       {
       ch_state = 2;
@@ -773,11 +806,19 @@ while ((ch = (RECEIVE_GETC)()) != EOF)
   next. */
 
   message_size++;
+  linelength++;
   if (fout != NULL)
     {
     if (fputc(ch, fout) == EOF) return END_WERROR;
     if (message_size > thismessage_size_limit) return END_SIZE;
     }
+  if(ch == '\n')
+    (void) cutthrough_put_nl();
+  else
+    {
+    uschar c= ch;
+    (void) cutthrough_puts(&c, 1);
+    }
   }
 
 /* Fall through here if EOF encountered. This indicates some kind of error,
@@ -875,12 +916,12 @@ exim_exit(error_rc);
 *          Add header lines set up by ACL        *
 *************************************************/
 
-/* This function is called to add the header lines that were set up by "warn"
-statements in an ACL onto the list of headers in memory. It is done in two
-stages like this, because when the ACL for RCPT is running, the other headers
-have not yet been received. This function is called twice; once just before
-running the DATA ACL, and once after. This is so that header lines added by
-MAIL or RCPT are visible to the DATA ACL.
+/* This function is called to add the header lines that were set up by
+statements in an ACL to the list of headers in memory. It is done in two stages
+like this, because when the ACL for RCPT is running, the other headers have not
+yet been received. This function is called twice; once just before running the
+DATA ACL, and once after. This is so that header lines added by MAIL or RCPT
+are visible to the DATA ACL.
 
 Originally these header lines were added at the end. Now there is support for
 three different places: top, bottom, and after the Received: header(s). There
@@ -1036,7 +1077,7 @@ unsigned long mbox_size;
 header_line *my_headerlist;
 uschar *user_msg, *log_msg;
 int mime_part_count_buffer = -1;
-int rc;
+int rc = OK;
 
 memset(CS rfc822_file_path,0,2048);
 
@@ -1063,13 +1104,16 @@ return TRUE;
 
 DO_MIME_ACL:
 /* make sure the eml mbox file is spooled up */
-mbox_file = spool_mbox(&mbox_size);
+mbox_file = spool_mbox(&mbox_size, NULL);
 if (mbox_file == NULL) {
   /* error while spooling */
   log_write(0, LOG_MAIN|LOG_PANIC,
          "acl_smtp_mime: error while creating mbox spool file, message temporarily rejected.");
   Uunlink(spool_name);
   unspool_mbox();
+#ifdef EXPERIMENTAL_DCC
+  dcc_ok = 0;
+#endif
   smtp_respond(US"451", 3, TRUE, US"temporary local problem");
   message_id[0] = 0;            /* Indicate no message accepted */
   *smtp_reply_ptr = US"";       /* Indicate reply already sent */
@@ -1149,6 +1193,9 @@ else if (rc != OK)
   {
   Uunlink(spool_name);
   unspool_mbox();
+#ifdef EXPERIMENTAL_DCC
+  dcc_ok = 0;
+#endif
   if (smtp_handle_acl_fail(ACL_WHERE_MIME, rc, user_msg, log_msg) != 0)
     *smtp_yield_ptr = FALSE;    /* No more messsages after dropped connection */
   *smtp_reply_ptr = US"";       /* Indicate reply already sent */
@@ -1162,6 +1209,52 @@ return TRUE;
 #endif  /* WITH_CONTENT_SCAN */
 
 
+
+void
+received_header_gen(void)
+{
+uschar *received;
+uschar *timestamp;
+header_line *received_header= header_list;
+
+timestamp = expand_string(US"${tod_full}");
+if (recipients_count == 1) received_for = recipients_list[0].address;
+received = expand_string(received_header_text);
+received_for = NULL;
+
+if (received == NULL)
+  {
+  if(spool_name[0] != 0)
+    Uunlink(spool_name);           /* Lose the data file */
+  log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Expansion of \"%s\" "
+    "(received_header_text) failed: %s", string_printing(received_header_text),
+      expand_string_message);
+  }
+
+/* The first element on the header chain is reserved for the Received header,
+so all we have to do is fill in the text pointer, and set the type. However, if
+the result of the expansion is an empty string, we leave the header marked as
+"old" so as to refrain from adding a Received header. */
+
+if (received[0] == 0)
+  {
+  received_header->text = string_sprintf("Received: ; %s\n", timestamp);
+  received_header->type = htype_old;
+  }
+else
+  {
+  received_header->text = string_sprintf("%s; %s\n", received, timestamp);
+  received_header->type = htype_received;
+  }
+
+received_header->slen = Ustrlen(received_header->text);
+
+DEBUG(D_receive) debug_printf(">>Generated Received: header line\n%c %s",
+  received_header->type, received_header->text);
+}
+
+
+
 /*************************************************
 *                 Receive message                *
 *************************************************/
@@ -1171,7 +1264,8 @@ Either a non-null list of recipients, or the extract flag will be true, or
 both. The flag sender_local is true for locally generated messages. The flag
 submission_mode is true if an ACL has obeyed "control = submission". The flag
 suppress_local_fixups is true if an ACL has obeyed "control =
-suppress_local_fixups". The flag smtp_input is true if the message is to be
+suppress_local_fixups" or -G was passed on the command-line.
+The flag smtp_input is true if the message is to be
 handled using SMTP conventions about termination and lines starting with dots.
 For non-SMTP messages, dot_ends is true for dot-terminated messages.
 
@@ -1254,7 +1348,8 @@ not. */
 BOOL
 receive_msg(BOOL extract_recip)
 {
-int  i, rc;
+int  i;
+int  rc = FAIL;
 int  msg_size = 0;
 int  process_info_len = Ustrlen(process_info);
 int  error_rc = (error_handling == ERRORS_SENDER)?
@@ -1263,6 +1358,7 @@ int  header_size = 256;
 int  start, end, domain, size, sptr;
 int  id_resolution;
 int  had_zero = 0;
+int  prevlines_length = 0;
 
 register int ptr = 0;
 
@@ -1308,7 +1404,6 @@ header_line *received_header;
 
 /* Variables for use when building the Received: header. */
 
-uschar *received;
 uschar *timestamp;
 int tslen;
 
@@ -1318,6 +1413,13 @@ might take a fair bit of real time. */
 
 search_tidyup();
 
+/* Extracting the recipient list from an input file is incompatible with
+cutthrough delivery with the no-spool option.  It shouldn't be possible
+ to set up the combination, but just in case kill any ongoing connection. */
+/*XXX add no-spool */
+if (extract_recip || !smtp_input)
+  cancel_cutthrough_connection();
+
 /* Initialize the chain of headers by setting up a place-holder for Received:
 header. Temporarily mark it as "old", i.e. not to be used. We keep header_last
 pointing to the end of the chain to make adding headers simple. */
@@ -1343,18 +1445,18 @@ data_fd = -1;
 spool_name[0] = 0;
 message_size = 0;
 warning_count = 0;
-received_count = 1;     /* For the one we will add */
+received_count = 1;            /* For the one we will add */
 
 if (thismessage_size_limit <= 0) thismessage_size_limit = INT_MAX;
 
 /* While reading the message, the following counts are computed. */
 
-message_linecount = body_linecount = body_zerocount = 0;
+message_linecount = body_linecount = body_zerocount =
+  max_received_linelength = 0;
 
-#ifdef EXPERIMENTAL_DOMAINKEYS
-/* Call into DK to set up the context. Check if DK is to be run are carried out
-   inside dk_exim_verify_init(). */
-dk_exim_verify_init();
+#ifndef DISABLE_DKIM
+/* Call into DKIM to set up the context. */
+if (smtp_input && !smtp_batched_input && !dkim_disable_verify) dkim_exim_verify_init();
 #endif
 
 /* Remember the time of reception. Exim uses time+pid for uniqueness of message
@@ -1405,7 +1507,7 @@ next->text. */
 
 for (;;)
   {
-  int ch = (RECEIVE_GETC)();
+  int ch = (receive_getc)();
 
   /* If we hit EOF on a SMTP connection, it's an error, since incoming
   SMTP must have a correct "." terminator. */
@@ -1469,7 +1571,7 @@ for (;;)
   if (ch == '\n')
     {
     if (first_line_ended_crlf == TRUE_UNSET) first_line_ended_crlf = FALSE;
-      else if (first_line_ended_crlf) RECEIVE_UNGETC(' ');
+      else if (first_line_ended_crlf) receive_ungetc(' ');
     goto EOL;
     }
 
@@ -1484,13 +1586,13 @@ for (;;)
 
   if (ptr == 0 && ch == '.' && (smtp_input || dot_ends))
     {
-    ch = (RECEIVE_GETC)();
+    ch = (receive_getc)();
     if (ch == '\r')
       {
-      ch = (RECEIVE_GETC)();
+      ch = (receive_getc)();
       if (ch != '\n')
         {
-        RECEIVE_UNGETC(ch);
+        receive_ungetc(ch);
         ch = '\r';              /* Revert to CR */
         }
       }
@@ -1518,7 +1620,7 @@ for (;;)
 
   if (ch == '\r')
     {
-    ch = (RECEIVE_GETC)();
+    ch = (receive_getc)();
     if (ch == '\n')
       {
       if (first_line_ended_crlf == TRUE_UNSET) first_line_ended_crlf = TRUE;
@@ -1528,7 +1630,7 @@ for (;;)
     /* Otherwise, put back the character after CR, and turn the bare CR
     into LF SP. */
 
-    ch = (RECEIVE_UNGETC)(ch);
+    ch = (receive_ungetc)(ch);
     next->text[ptr++] = '\n';
     message_size++;
     ch = ' ';
@@ -1585,6 +1687,12 @@ for (;;)
   receive_linecount++;
   message_linecount++;
 
+  /* Keep track of maximum line length */
+
+  if (ptr - prevlines_length > max_received_linelength)
+    max_received_linelength = ptr - prevlines_length;
+  prevlines_length = ptr + 1;
+
   /* Now put in the terminating newline. There is always space for
   at least two more characters. */
 
@@ -1607,14 +1715,14 @@ for (;;)
 
   if (ch != EOF)
     {
-    int nextch = (RECEIVE_GETC)();
+    int nextch = (receive_getc)();
     if (nextch == ' ' || nextch == '\t')
       {
       next->text[ptr++] = nextch;
       message_size++;
       continue;                      /* Iterate the loop */
       }
-    else if (nextch != EOF) (RECEIVE_UNGETC)(nextch);   /* For next time */
+    else if (nextch != EOF) (receive_ungetc)(nextch);   /* For next time */
     else ch = EOF;                   /* Cause main loop to exit at end */
     }
 
@@ -1813,6 +1921,7 @@ for (;;)
   next->text = store_get(header_size);
   ptr = 0;
   had_zero = 0;
+  prevlines_length = 0;
   }      /* Continue, starting to read the next header */
 
 /* At this point, we have read all the headers into a data structure in main
@@ -1869,7 +1978,7 @@ for (h = header_list->next; h != NULL; h = h->next)
     /* Record whether a Date: or Resent-Date: header exists, as appropriate. */
 
     case htype_date:
-    date_header_exists = !resents_exist || is_resent;
+    if (!resents_exist || is_resent) date_header_exists = TRUE;
     break;
 
     /* Same comments as about Return-Path: below. */
@@ -1929,6 +2038,7 @@ for (h = header_list->next; h != NULL; h = h->next)
 
     case htype_received:
     h->type = htype_received;
+/*XXX cutthrough delivery - need to error on excessive number here */
     received_count++;
     break;
 
@@ -2296,10 +2406,13 @@ if (msgid_header == NULL &&
       }
     }
 
-  /* Add the header line */
+  /* Add the header line
+   * Resent-* headers are prepended, per RFC 5322 3.6.6.  Non-Resent-* are
+   * appended, to preserve classical expectations of header ordering. */
 
-  header_add(htype_id, "%sMessage-Id: <%s%s%s@%s>\n", resent_prefix,
-    message_id_external, (*id_text == 0)? "" : ".", id_text, id_domain);
+  header_add_at_position(!resents_exist, NULL, FALSE, htype_id,
+    "%sMessage-Id: <%s%s%s@%s>\n", resent_prefix, message_id_external,
+    (*id_text == 0)? "" : ".", id_text, id_domain);
   }
 
 /* If we are to log recipients, keep a copy of the raw ones before any possible
@@ -2507,9 +2620,10 @@ if (from_header != NULL &&
     if (sender_address_unrewritten == NULL)
       sender_address_unrewritten = sender_address;
     sender_address = generated_sender_address;
-    log_write(L_address_rewrite, LOG_MAIN,
-      "\"%s\" from env-from rewritten as \"%s\" by submission mode",
-      sender_address_unrewritten, generated_sender_address);
+    if (Ustrcmp(sender_address_unrewritten, generated_sender_address) != 0)
+      log_write(L_address_rewrite, LOG_MAIN,
+        "\"%s\" from env-from rewritten as \"%s\" by submission mode",
+        sender_address_unrewritten, generated_sender_address);
     }
   }
 
@@ -2563,12 +2677,15 @@ changes in RFC 2822, this was dropped in November 2003. */
 /* If there is no date header, generate one if the message originates locally
 (i.e. not over TCP/IP) and suppress_local_fixups is not set, or if the
 submission mode flag is set. Messages without Date: are not valid, but it seems
-to be more confusing if Exim adds one to all remotely-originated messages. */
+to be more confusing if Exim adds one to all remotely-originated messages.
+As per Message-Id, we prepend if resending, else append.
+*/
 
 if (!date_header_exists &&
       ((sender_host_address == NULL && !suppress_local_fixups)
         || submission_mode))
-  header_add(htype_other, "%sDate: %s\n", resent_prefix, tod_stamp(tod_full));
+  header_add_at_position(!resents_exist, NULL, FALSE, htype_other,
+    "%sDate: %s\n", resent_prefix, tod_stamp(tod_full));
 
 search_tidyup();    /* Free any cached resources */
 
@@ -2593,6 +2710,23 @@ if (filter_test != FTEST_NONE)
   return message_ended == END_DOT;
   }
 
+/*XXX cutthrough deliver:
+       We have to create the Received header now rather than at the end of reception,
+       so the timestamp behaviour is a change to the normal case.
+       XXX Ensure this gets documented XXX.
+*/
+if (cutthrough_fd >= 0)
+  {
+  received_header_gen();
+  add_acl_headers(US"MAIL or RCPT");
+  (void) cutthrough_headers_send();
+  }
+
+
+/*XXX cutthrough deliver:
+  Here's where we open the data spoolfile.  Want to optionally avoid.
+*/
+
 /* Open a new spool file for the data portion of the message. We need
 to access it both via a file descriptor and a stream. Try to make the
 directory if it isn't there. Note re use of sprintf: spool_directory
@@ -2649,6 +2783,7 @@ if (next != NULL)
   {
   uschar *s = next->text;
   int len = next->slen;
+  /*XXX cutthrough - writing the data spool file here.  Want to optionally avoid. */
   (void)fwrite(s, 1, len, data_file);
   body_linecount++;                 /* Assumes only 1 line */
   }
@@ -2657,10 +2792,13 @@ if (next != NULL)
 (indicated by '.'), or might have encountered an error while writing the
 message id or "next" line. */
 
+/* XXX cutthrough - no-spool option....... */
 if (!ferror(data_file) && !(receive_feof)() && message_ended != END_DOT)
   {
   if (smtp_input)
     {
+    /*XXX cutthrough - writing the data spool file here.  Want to optionally avoid. */
+    /* Would suffice to leave data_file arg NULL */
     message_ended = read_message_data_smtp(data_file);
     receive_linecount++;                /* The terminating "." line */
     }
@@ -2674,6 +2812,7 @@ if (!ferror(data_file) && !(receive_feof)() && message_ended != END_DOT)
   if (smtp_input && message_ended == END_EOF)
     {
     Uunlink(spool_name);                     /* Lose data file when closed */
+    cancel_cutthrough_connection();
     message_id[0] = 0;                       /* Indicate no message accepted */
     smtp_reply = handle_lost_connection(US"");
     smtp_yield = FALSE;
@@ -2686,6 +2825,7 @@ if (!ferror(data_file) && !(receive_feof)() && message_ended != END_DOT)
   if (message_ended == END_SIZE)
     {
     Uunlink(spool_name);                /* Lose the data file when closed */
+    cancel_cutthrough_connection();
     if (smtp_input) receive_swallow_smtp();  /* Swallow incoming SMTP */
 
     log_write(L_size_reject, LOG_MAIN|LOG_REJECT, "rejected from <%s>%s%s%s%s: "
@@ -2729,8 +2869,9 @@ we can then give up. Note that for SMTP input we must swallow the remainder of
 the input in cases of output errors, since the far end doesn't expect to see
 anything until the terminating dot line is sent. */
 
+/* XXX cutthrough - no-spool option....... */
 if (fflush(data_file) == EOF || ferror(data_file) ||
-    fsync(fileno(data_file)) < 0 || (receive_ferror)())
+    EXIMfsync(fileno(data_file)) < 0 || (receive_ferror)())
   {
   uschar *msg_errno = US strerror(errno);
   BOOL input_error = (receive_ferror)() != 0;
@@ -2741,6 +2882,7 @@ if (fflush(data_file) == EOF || ferror(data_file) ||
 
   log_write(0, LOG_MAIN, "Message abandoned: %s", msg);
   Uunlink(spool_name);                /* Lose the data file */
+  cancel_cutthrough_connection();
 
   if (smtp_input)
     {
@@ -2767,6 +2909,7 @@ if (fflush(data_file) == EOF || ferror(data_file) ||
 
 /* No I/O errors were encountered while writing the data file. */
 
+/*XXX cutthrough - avoid message if no-spool option */
 DEBUG(D_receive) debug_printf("Data file written for message %s\n", message_id);
 
 
@@ -2781,6 +2924,9 @@ recipients or stderr error writing, throw the data file away afterwards, and
 exit. (This can't be SMTP, which always ensures there's at least one
 syntactically good recipient address.) */
 
+/*XXX cutthrough - can't if no-spool option.  extract_recip is a fn arg.
+  Make incompat with no-spool at fn start. */
+
 if (extract_recip && (bad_addresses != NULL || recipients_count == 0))
   {
   DEBUG(D_receive)
@@ -2860,50 +3006,29 @@ for use when we generate the Received: header.
 
 Note: the checking for too many Received: headers is handled by the delivery
 code. */
+/*XXX eventually add excess Received: check for cutthrough case back when classifying them */
 
-timestamp = expand_string(US"${tod_full}");
-if (recipients_count == 1) received_for = recipients_list[0].address;
-received = expand_string(received_header_text);
-received_for = NULL;
-
-if (received == NULL)
+if (received_header->text == NULL)     /* Non-cutthrough case */
   {
-  Uunlink(spool_name);           /* Lose the data file */
-  log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Expansion of \"%s\" "
-    "(received_header_text) failed: %s", string_printing(received_header_text),
-      expand_string_message);
-  }
+  received_header_gen();
 
-/* The first element on the header chain is reserved for the Received header,
-so all we have to do is fill in the text pointer, and set the type. However, if
-the result of the expansion is an empty string, we leave the header marked as
-"old" so as to refrain from adding a Received header. */
+  /* Set the value of message_body_size for the DATA ACL and for local_scan() */
 
-if (received[0] == 0)
-  {
-  received_header->text = string_sprintf("Received: ; %s\n", timestamp);
-  received_header->type = htype_old;
-  }
-else
-  {
-  received_header->text = string_sprintf("%s; %s\n", received, timestamp);
-  received_header->type = htype_received;
-  }
+  message_body_size = (fstat(data_fd, &statbuf) == 0)?
+    statbuf.st_size - SPOOL_DATA_START_OFFSET : -1;
 
-received_header->slen = Ustrlen(received_header->text);
+  /* If an ACL from any RCPT commands set up any warning headers to add, do so
+  now, before running the DATA ACL. */
 
-DEBUG(D_receive) debug_printf(">>Generated Received: header line\n%c %s",
-  received_header->type, received_header->text);
-
-/* Set the value of message_body_size for the DATA ACL and for local_scan() */
-
-message_body_size = (fstat(data_fd, &statbuf) == 0)?
-  statbuf.st_size - SPOOL_DATA_START_OFFSET : -1;
-
-/* If an ACL from any RCPT commands set up any warning headers to add, do so
-now, before running the DATA ACL. */
-
-add_acl_headers(US"MAIL or RCPT");
+  add_acl_headers(US"MAIL or RCPT");
+  }
+else if (data_fd >= 0)
+  message_body_size = (fstat(data_fd, &statbuf) == 0)?
+    statbuf.st_size - SPOOL_DATA_START_OFFSET : -1;
+else
+  /*XXX cutthrough - XXX how to get the body size? */
+  /*   perhaps a header-size to subtract from message_size? */
+  message_body_size = message_size - 1;
 
 /* If an ACL is specified for checking things at this stage of reception of a
 message, run it, unless all the recipients were removed by "discard" in earlier
@@ -2915,25 +3040,130 @@ $message_body_end can be extracted if needed. Allow $recipients in expansions.
 deliver_datafile = data_fd;
 user_msg = NULL;
 
+enable_dollar_recipients = TRUE;
+
 if (recipients_count == 0)
   {
   blackholed_by = recipients_discarded? US"MAIL ACL" : US"RCPT ACL";
   }
 else
   {
-  enable_dollar_recipients = TRUE;
-
   /* Handle interactive SMTP messages */
 
   if (smtp_input && !smtp_batched_input)
     {
 
-#ifdef EXPERIMENTAL_DOMAINKEYS
-    dk_exim_verify_finish();
-#endif
+#ifndef DISABLE_DKIM
+    if (!dkim_disable_verify)
+      {
+/* XXX cutthrough - no-spool option....... */
+      /* Finish verification, this will log individual signature results to
+         the mainlog */
+      dkim_exim_verify_finish();
+
+      /* Check if we must run the DKIM ACL */
+      if ((acl_smtp_dkim != NULL) &&
+          (dkim_verify_signers != NULL) &&
+          (dkim_verify_signers[0] != '\0'))
+        {
+        uschar *dkim_verify_signers_expanded =
+          expand_string(dkim_verify_signers);
+        if (dkim_verify_signers_expanded == NULL)
+          {
+          log_write(0, LOG_MAIN|LOG_PANIC,
+            "expansion of dkim_verify_signers option failed: %s",
+            expand_string_message);
+          }
+        else
+          {
+          int sep = 0;
+          uschar *ptr = dkim_verify_signers_expanded;
+          uschar *item = NULL;
+          uschar *seen_items = NULL;
+          int     seen_items_size = 0;
+          int     seen_items_offset = 0;
+          uschar itembuf[256];
+          /* Default to OK when no items are present */
+          rc = OK;
+          while ((item = string_nextinlist(&ptr, &sep,
+                                           itembuf,
+                                           sizeof(itembuf))) != NULL)
+            {
+            /* Prevent running ACL for an empty item */
+            if (!item || (item[0] == '\0')) continue;
+            /* Only run ACL once for each domain or identity, no matter how often it
+               appears in the expanded list. */
+            if (seen_items != NULL)
+              {
+              uschar *seen_item = NULL;
+              uschar seen_item_buf[256];
+              uschar *seen_items_list = seen_items;
+              int seen_this_item = 0;
+              
+              while ((seen_item = string_nextinlist(&seen_items_list, &sep,
+                                                    seen_item_buf,
+                                                    sizeof(seen_item_buf))) != NULL)
+                {
+                  if (Ustrcmp(seen_item,item) == 0)
+                    {
+                      seen_this_item = 1;
+                      break;
+                    } 
+                }
+
+              if (seen_this_item > 0)
+                {
+                DEBUG(D_receive)
+                  debug_printf("acl_smtp_dkim: skipping signer %s, already seen\n", item);
+                continue;
+                }
+              
+              seen_items = string_append(seen_items,&seen_items_size,&seen_items_offset,1,":");
+              }
+
+            seen_items = string_append(seen_items,&seen_items_size,&seen_items_offset,1,item);
+            seen_items[seen_items_offset] = '\0';
+
+            DEBUG(D_receive)
+              debug_printf("calling acl_smtp_dkim for dkim_cur_signer=%s\n", item);
+
+            dkim_exim_acl_setup(item);
+            rc = acl_check(ACL_WHERE_DKIM, NULL, acl_smtp_dkim, &user_msg, &log_msg);
+
+            if (rc != OK)
+              {
+                DEBUG(D_receive)
+                  debug_printf("acl_smtp_dkim: acl_check returned %d on %s, skipping remaining items\n", rc, item);
+               cancel_cutthrough_connection();
+                break;
+              }
+            }
+          add_acl_headers(US"DKIM");
+          if (rc == DISCARD)
+            {
+            recipients_count = 0;
+            blackholed_by = US"DKIM ACL";
+            if (log_msg != NULL)
+              blackhole_log_msg = string_sprintf(": %s", log_msg);
+            }
+          else if (rc != OK)
+            {
+            Uunlink(spool_name);
+            if (smtp_handle_acl_fail(ACL_WHERE_DKIM, rc, user_msg, log_msg) != 0)
+              smtp_yield = FALSE;    /* No more messsages after dropped connection */
+            smtp_reply = US"";       /* Indicate reply already sent */
+            message_id[0] = 0;       /* Indicate no message accepted */
+            goto TIDYUP;             /* Skip to end of function */
+            }
+          }
+        }
+      }
+#endif /* DISABLE_DKIM */
 
 #ifdef WITH_CONTENT_SCAN
-    if (acl_smtp_mime != NULL &&
+/* XXX cutthrough - no-spool option....... */
+    if (recipients_count > 0 &&
+        acl_smtp_mime != NULL &&
         !run_mime_acl(acl_smtp_mime, &smtp_yield, &smtp_reply, &blackholed_by))
       goto TIDYUP;
 #endif /* WITH_CONTENT_SCAN */
@@ -2941,6 +3171,9 @@ else
     /* Check the recipients count again, as the MIME ACL might have changed
     them. */
 
+/* XXX cutthrough - no-spool option must document that data-acl has no file access */
+/*     but can peek at headers */
+
     if (acl_smtp_data != NULL && recipients_count > 0)
       {
       rc = acl_check(ACL_WHERE_DATA, NULL, acl_smtp_data, &user_msg, &log_msg);
@@ -2951,12 +3184,17 @@ else
         blackholed_by = US"DATA ACL";
         if (log_msg != NULL)
           blackhole_log_msg = string_sprintf(": %s", log_msg);
+       cancel_cutthrough_connection();
         }
       else if (rc != OK)
         {
         Uunlink(spool_name);
+       cancel_cutthrough_connection();
 #ifdef WITH_CONTENT_SCAN
         unspool_mbox();
+#endif
+#ifdef EXPERIMENTAL_DCC
+       dcc_ok = 0;
 #endif
         if (smtp_handle_acl_fail(ACL_WHERE_DATA, rc, user_msg, log_msg) != 0)
           smtp_yield = FALSE;    /* No more messsages after dropped connection */
@@ -2996,6 +3234,9 @@ else
         Uunlink(spool_name);
 #ifdef WITH_CONTENT_SCAN
         unspool_mbox();
+#endif
+#ifdef EXPERIMENTAL_DCC
+       dcc_ok = 0;
 #endif
         /* The ACL can specify where rejections are to be logged, possibly
         nowhere. The default is main and reject logs. */
@@ -3027,14 +3268,17 @@ else
 
   if (deliver_freeze) frozen_by = US"ACL";     /* for later logging */
   if (queue_only_policy) queued_by = US"ACL";
-
-  enable_dollar_recipients = FALSE;
   }
 
 #ifdef WITH_CONTENT_SCAN
 unspool_mbox();
 #endif
 
+#ifdef EXPERIMENTAL_DCC
+dcc_ok = 0;
+#endif
+
+
 /* The final check on the message is to run the scan_local() function. The
 version supplied with Exim always accepts, but this is a hook for sysadmins to
 supply their own checking code. The local_scan() function is run even when all
@@ -3056,10 +3300,13 @@ local_scan_data = NULL;
 
 os_non_restarting_signal(SIGALRM, local_scan_timeout_handler);
 if (local_scan_timeout > 0) alarm(local_scan_timeout);
+/* XXX cutthrough - no-spool option..... */
 rc = local_scan(data_fd, &local_scan_data);
 alarm(0);
 os_non_restarting_signal(SIGALRM, sigalrm_handler);
 
+enable_dollar_recipients = FALSE;
+
 store_pool = POOL_MAIN;   /* In case changed */
 DEBUG(D_receive) debug_printf("local_scan() returned %d %s\n", rc,
   local_scan_data);
@@ -3081,7 +3328,7 @@ if (local_scan_data != NULL)
 
 if (rc == LOCAL_SCAN_ACCEPT_FREEZE)
   {
-  if (!deliver_freeze)      /* ACL might have already frozen */
+  if (!deliver_freeze)         /* ACL might have already frozen */
     {
     deliver_freeze = TRUE;
     deliver_frozen_at = time(NULL);
@@ -3201,6 +3448,7 @@ the message to be abandoned. */
 signal(SIGTERM, SIG_IGN);
 signal(SIGINT, SIG_IGN);
 
+
 /* Ensure the first time flag is set in the newly-received message. */
 
 deliver_firsttime = TRUE;
@@ -3208,6 +3456,7 @@ deliver_firsttime = TRUE;
 #ifdef EXPERIMENTAL_BRIGHTMAIL
 if (bmi_run == 1) {
   /* rewind data file */
+  /* XXX cutthrough - no-spool option..... */
   lseek(data_fd, (long int)SPOOL_DATA_START_OFFSET, SEEK_SET);
   bmi_verdicts = bmi_process_message(header_list, data_fd);
 };
@@ -3249,6 +3498,9 @@ if (host_checking || blackholed_by != NULL)
 
 else
   {
+  /*XXX cutthrough -
+     Optionally want to avoid writing spool files (when no data-time filtering needed) */
+
   if ((msg_size = spool_write_header(message_id, SW_RECEIVING, &errmsg)) < 0)
     {
     log_write(0, LOG_MAIN, "Message abandoned: %s", errmsg);
@@ -3322,7 +3574,11 @@ if ((log_extra_selector & LX_tls_certificate_verified) != 0 &&
   s = string_append(s, &size, &sptr, 2, US" CV=",
     tls_certificate_verified? "yes":"no");
 if ((log_extra_selector & LX_tls_peerdn) != 0 && tls_peerdn != NULL)
-  s = string_append(s, &size, &sptr, 3, US" DN=\"", tls_peerdn, US"\"");
+  s = string_append(s, &size, &sptr, 3, US" DN=\"",
+    string_printing(tls_peerdn), US"\"");
+if ((log_extra_selector & LX_tls_sni) != 0 && tls_sni != NULL)
+  s = string_append(s, &size, &sptr, 3, US" SNI=\"",
+    string_printing(tls_sni), US"\"");
 #endif
 
 if (sender_host_authenticated != NULL)
@@ -3380,25 +3636,9 @@ not put the zero in. */
 
 s[sptr] = 0;
 
-/* While writing to the log, set a flag to cause a call to receive_bomb_out()
-if the log cannot be opened. */
-
-receive_call_bombout = TRUE;
-log_write(0, LOG_MAIN |
-  (((log_extra_selector & LX_received_recipients) != 0)? LOG_RECIPIENTS : 0) |
-  (((log_extra_selector & LX_received_sender) != 0)? LOG_SENDER : 0),
-  "%s", s);
-receive_call_bombout = FALSE;
-
-/* Log any control actions taken by an ACL or local_scan(). */
-
-if (deliver_freeze) log_write(0, LOG_MAIN, "frozen by %s", frozen_by);
-if (queue_only_policy) log_write(L_delay_delivery, LOG_MAIN,
-  "no immediate delivery: queued by %s", queued_by);
-
 /* Create a message log file if message logs are being used and this message is
 not blackholed. Write the reception stuff to it. We used to leave message log
-creation until the first delivery, but this has proved confusing for somep
+creation until the first delivery, but this has proved confusing for some
 people. */
 
 if (message_logs && blackholed_by == NULL)
@@ -3446,6 +3686,152 @@ if (message_logs && blackholed_by == NULL)
     }
   }
 
+/* Everything has now been done for a successful message except logging its
+arrival, and outputting an SMTP response. While writing to the log, set a flag
+to cause a call to receive_bomb_out() if the log cannot be opened. */
+
+receive_call_bombout = TRUE;
+
+/* Before sending an SMTP response in a TCP/IP session, we check to see if the
+connection has gone away. This can only be done if there is no unconsumed input
+waiting in the local input buffer. We can test for this by calling
+receive_smtp_buffered(). RFC 2920 (pipelining) explicitly allows for additional
+input to be sent following the final dot, so the presence of following input is
+not an error.
+
+If the connection is still present, but there is no unread input for the
+socket, the result of a select() call will be zero. If, however, the connection
+has gone away, or if there is pending input, the result of select() will be
+non-zero. The two cases can be distinguished by trying to read the next input
+character. If we succeed, we can unread it so that it remains in the local
+buffer for handling later. If not, the connection has been lost.
+
+Of course, since TCP/IP is asynchronous, there is always a chance that the
+connection will vanish between the time of this test and the sending of the
+response, but the chance of this happening should be small. */
+
+if (smtp_input && sender_host_address != NULL && !sender_host_notsocket &&
+    !receive_smtp_buffered())
+  {
+  struct timeval tv;
+  fd_set select_check;
+  FD_ZERO(&select_check);
+  FD_SET(fileno(smtp_in), &select_check);
+  tv.tv_sec = 0;
+  tv.tv_usec = 0;
+
+  if (select(fileno(smtp_in) + 1, &select_check, NULL, NULL, &tv) != 0)
+    {
+    int c = (receive_getc)();
+    if (c != EOF) (receive_ungetc)(c); else
+      {
+      uschar *msg = US"SMTP connection lost after final dot";
+      smtp_reply = US"";    /* No attempt to send a response */
+      smtp_yield = FALSE;   /* Nothing more on this connection */
+
+      /* Re-use the log line workspace */
+
+      sptr = 0;
+      s = string_cat(s, &size, &sptr, msg, Ustrlen(msg));
+      s = add_host_info_for_log(s, &size, &sptr);
+      s[sptr] = 0;
+      log_write(0, LOG_MAIN, "%s", s);
+
+      /* Delete the files for this aborted message. */
+
+      sprintf(CS spool_name, "%s/input/%s/%s-D", spool_directory,
+        message_subdir, message_id);
+      Uunlink(spool_name);
+
+      sprintf(CS spool_name, "%s/input/%s/%s-H", spool_directory,
+        message_subdir, message_id);
+      Uunlink(spool_name);
+
+      sprintf(CS spool_name, "%s/msglog/%s/%s", spool_directory,
+        message_subdir, message_id);
+      Uunlink(spool_name);
+
+      goto TIDYUP;
+      }
+    }
+  }
+
+/* The connection has not gone away; we really are going to take responsibility
+for this message. */
+
+/*XXX cutthrough - had sender last-dot; assume we've sent or bufferred all
+   data onward by now.
+
+   Send dot onward.  If accepted, can the spooled files, log as delivered and accept
+   the sender's dot (below).
+   If not accepted: copy response to sender, can the spooled files, log approriately.
+
+   Having the normal spool files lets us do data-filtering, and store/forward on temp-reject.
+*/
+if(cutthrough_fd >= 0)
+  {
+  uschar * msg= cutthrough_finaldot(); /* Ask the target system to accept the messsage */
+  switch(msg[0])
+  {
+  case '2':    /* Accept. Do the same to the source; dump any spoolfiles.   */
+    /* logging was done in finaldot() */
+    if(data_file != NULL)
+      {
+      sprintf(CS spool_name, "%s/input/%s/%s-D", spool_directory,
+        message_subdir, message_id);
+      Uunlink(spool_name);
+      sprintf(CS spool_name, "%s/input/%s/%s-H", spool_directory,
+        message_subdir, message_id);
+      Uunlink(spool_name);
+      sprintf(CS spool_name, "%s/msglog/%s/%s", spool_directory,
+        message_subdir, message_id);
+      Uunlink(spool_name);
+      }
+    break;
+
+  default:     /* Unknown response, or error.  Treat as temp-reject.         */
+  case '4':    /* Temp-reject. If we wrote spoolfiles, keep them and accept. */
+               /* If not, temp-reject the source.                            */
+    /*XXX could we mark the spoolfile queue-only or already-tried? */
+    log_write(0, LOG_MAIN, "cutthrough target temp-reject: %s", msg);
+    if(data_file == NULL)
+       smtp_reply= msg;        /* Pass on the exact error */
+    break;
+
+  case '5':    /* Perm-reject.  Do the same to the source.  Dump any spoolfiles */
+    log_write(0, LOG_MAIN, "cutthrough target perm-reject: %s", msg);
+    smtp_reply= msg;           /* Pass on the exact error */
+    if(data_file != NULL)
+      {
+      sprintf(CS spool_name, "%s/input/%s/%s-D", spool_directory,
+        message_subdir, message_id);
+      Uunlink(spool_name);
+      sprintf(CS spool_name, "%s/input/%s/%s-H", spool_directory,
+        message_subdir, message_id);
+      Uunlink(spool_name);
+      sprintf(CS spool_name, "%s/msglog/%s/%s", spool_directory,
+        message_subdir, message_id);
+      Uunlink(spool_name);
+      }
+    break;
+  }
+  }
+
+if(smtp_reply == NULL)
+  {
+  log_write(0, LOG_MAIN |
+    (((log_extra_selector & LX_received_recipients) != 0)? LOG_RECIPIENTS : 0) |
+    (((log_extra_selector & LX_received_sender) != 0)? LOG_SENDER : 0),
+    "%s", s);
+
+  /* Log any control actions taken by an ACL or local_scan(). */
+
+  if (deliver_freeze) log_write(0, LOG_MAIN, "frozen by %s", frozen_by);
+  if (queue_only_policy) log_write(L_delay_delivery, LOG_MAIN,
+    "no immediate delivery: queued by %s", queued_by);
+  }
+receive_call_bombout = FALSE;
+
 store_reset(s);   /* The store for the main log message can be reused */
 
 /* If the message is frozen, and freeze_tell is set, do the telling. */
@@ -3460,9 +3846,9 @@ if (deliver_freeze && freeze_tell != NULL && freeze_tell[0] != 0)
 
 /* Either a message has been successfully received and written to the two spool
 files, or an error in writing the spool has occurred for an SMTP message, or
-an SMTP message has been rejected because of a bad sender. (For a non-SMTP
-message we will have already given up because there's no point in carrying on!)
-In either event, we must now close (and thereby unlock) the data file. In the
+an SMTP message has been rejected for policy reasons. (For a non-SMTP message
+we will have already given up because there's no point in carrying on!) In
+either event, we must now close (and thereby unlock) the data file. In the
 successful case, this leaves the message on the spool, ready for delivery. In
 the error case, the spool file will be deleted. Then tidy up store, interact
 with an SMTP call if necessary, and return.
@@ -3472,6 +3858,7 @@ data file will be flushed(!) out thereby. Nevertheless, it is theoretically
 possible for fclose() to fail - but what to do? What has happened to the lock
 if this happens? */
 
+
 TIDYUP:
 process_info[process_info_len] = 0;                /* Remove message id */
 if (data_file != NULL) (void)fclose(data_file);    /* Frees the lock */
@@ -3491,12 +3878,13 @@ if (smtp_input)
   yield = smtp_yield;
 
   /* Handle interactive SMTP callers. After several kinds of error, smtp_reply
-  is set to the response. However, after an ACL error or local_scan() error,
-  the response has already been sent, and smtp_reply is an empty string to
-  indicate this. */
+  is set to the response that should be sent. When it is NULL, we generate
+  default responses. After an ACL error or local_scan() error, the response has
+  already been sent, and smtp_reply is an empty string to indicate this. */
 
   if (!smtp_batched_input)
     {
+/*XXX cutthrough - here's where the originating sender gets given the data-acceptance */
     if (smtp_reply == NULL)
       {
       if (fake_response != OK)
@@ -3522,7 +3910,7 @@ if (smtp_input)
           "\n**** SMTP testing: that is not a real message id!\n\n");
       }
 
-    /* smtp_reply was previously set */
+    /* smtp_reply is set non-empty */
 
     else if (smtp_reply[0] != 0)
       {
@@ -3532,6 +3920,12 @@ if (smtp_input)
       else
         smtp_printf("%.1024s\r\n", smtp_reply);
       }
+
+    if (cutthrough_delivery)
+      {
+      log_write(0, LOG_MAIN, "Completed");
+      message_id[0] = 0;                       /* Prevent a delivery from starting */
+      }
     }
 
   /* For batched SMTP, generate an error message on failure, and do