Merge branch 'CHUNKING'
[exim.git] / src / src / receive.c
index 108e8d436428c7a5c30a973b68ede31b93244038..6a8ce884152a90924766e1ad2cb566f7760376f8 100644 (file)
@@ -2,7 +2,7 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2012 */
+/* Copyright (c) University of Cambridge 1995 - 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Code for receiving a message and setting up spool files. */
 extern int dcc_ok;
 #endif
 
+#ifdef EXPERIMENTAL_DMARC
+# include "dmarc.h"
+#endif /* EXPERIMENTAL_DMARC */
+
 /*************************************************
 *                Local static variables          *
 *************************************************/
 
 static FILE   *data_file = NULL;
 static int     data_fd = -1;
-static uschar  spool_name[256];
+static uschar *spool_name = US"";
 
 
 
@@ -83,7 +87,7 @@ if (newsender == NULL || untrusted_set_sender == NULL) return FALSE;
 qnewsender = (Ustrchr(newsender, '@') != NULL)?
   newsender : string_sprintf("%s@%s", newsender, qualify_domain_sender);
 return
-  match_address_list(qnewsender, TRUE, TRUE, &untrusted_set_sender, NULL, -1,
+  match_address_list(qnewsender, TRUE, TRUE, CUSS &untrusted_set_sender, NULL, -1,
     0, NULL) == OK;
 }
 
@@ -138,17 +142,16 @@ appearance of "syslog" in it. */
 else
   {
   int sep = ':';              /* Not variable - outside scripts use */
-  uschar *p = log_file_path;
+  const uschar *p = log_file_path;
   name = US"log";
 
   /* An empty log_file_path means "use the default". This is the same as an
   empty item in a list. */
 
   if (*p == 0) p = US":";
-  while ((path = string_nextinlist(&p, &sep, buffer, sizeof(buffer))) != NULL)
-    {
-    if (Ustrcmp(path, "syslog") != 0) break;
-    }
+  while ((path = string_nextinlist(&p, &sep, buffer, sizeof(buffer))))
+    if (Ustrcmp(path, "syslog") != 0)
+      break;
 
   if (path == NULL)  /* No log files */
     {
@@ -286,32 +289,50 @@ Returns:     it doesn't
 void
 receive_bomb_out(uschar *reason, uschar *msg)
 {
+  static BOOL already_bombing_out;
+/* The smtp_notquit_exit() below can call ACLs which can trigger recursive
+timeouts, if someone has something slow in their quit ACL.  Since the only
+things we should be doing are to close down cleanly ASAP, on the second
+pass we also close down stuff that might be opened again, before bypassing
+the ACL call and exiting. */
+
 /* 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
 process. Ensure that any header file is also removed. */
 
-if (spool_name[0] != 0)
+if (spool_name[0] != '\0')
   {
   Uunlink(spool_name);
   spool_name[Ustrlen(spool_name) - 1] = 'H';
   Uunlink(spool_name);
+  spool_name[0] = '\0';
   }
 
 /* Now close the file if it is open, either as a fd or a stream. */
 
-if (data_file != NULL) (void)fclose(data_file);
-  else if (data_fd >= 0) (void)close(data_fd);
+if (data_file != NULL)
+  {
+  (void)fclose(data_file);
+  data_file = NULL;
+} else if (data_fd >= 0) {
+  (void)close(data_fd);
+  data_fd = -1;
+  }
 
 /* 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 (!already_bombing_out)
   {
-  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);
+  already_bombing_out = TRUE;
+  if (smtp_input)
+    {
+    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) */
@@ -475,12 +496,44 @@ recipients_list[recipients_count].bmi_optin = bmi_current_optin;
 /* reset optin string pointer for next recipient */
 bmi_current_optin = NULL;
 #endif
+recipients_list[recipients_count].orcpt = NULL;
+recipients_list[recipients_count].dsn_flags = 0;
 recipients_list[recipients_count++].errors_to = NULL;
 }
 
 
 
 
+/*************************************************
+*        Send user response message              *
+*************************************************/
+
+/* This function is passed a default response code and a user message. It calls
+smtp_message_code() to check and possibly modify the response code, and then
+calls smtp_respond() to transmit the response. I put this into a function
+just to avoid a lot of repetition.
+
+Arguments:
+  code         the response code
+  user_msg     the user message
+
+Returns:       nothing
+*/
+
+#ifndef DISABLE_PRDR
+static void
+smtp_user_msg(uschar *code, uschar *user_msg)
+{
+int len = 3;
+smtp_message_code(&code, &len, &user_msg, NULL, TRUE);
+smtp_respond(code, len, TRUE, user_msg);
+}
+#endif
+
+
+
+
+
 /*************************************************
 *        Remove a recipient from the list        *
 *************************************************/
@@ -628,7 +681,9 @@ while ((ch = (receive_getc)()) != EOF)
 
     case 1:                         /* After written "\n" */
     if (ch == '.') { ch_state = 3; continue; }
-    if (ch != '\n') ch_state = 0; else linelength = -1;
+    if (ch == '\r') { ch_state = 2; continue; }
+    if (ch == '\n') { body_linecount++; linelength = -1; }
+    else ch_state = 0;
     break;
 
     case 2:
@@ -716,16 +771,13 @@ 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;
 int ch;
-register int linelength = 0;
+int linelength = 0;
 
 while ((ch = (receive_getc)()) != EOF)
   {
@@ -784,7 +836,15 @@ while ((ch = (receive_getc)()) != EOF)
       ch_state = 4;
       continue;
       }
-    ch_state = 1;                       /* The dot itself is removed */
+    /* The dot was removed at state 3. For a doubled dot, here, reinstate
+    it to cutthrough. The current ch, dot or not, is passed both to cutthrough
+    and to file below. */
+    if (ch == '.')
+      {
+      uschar c= ch;
+      (void) cutthrough_puts(&c, 1);
+      }
+    ch_state = 1;
     break;
 
     case 4:                             /* After [CR] LF . CR */
@@ -807,7 +867,7 @@ while ((ch = (receive_getc)()) != EOF)
 
   message_size++;
   linelength++;
-  if (fout != NULL)
+  if (fout)
     {
     if (fputc(ch, fout) == EOF) return END_WERROR;
     if (message_size > thismessage_size_limit) return END_SIZE;
@@ -816,7 +876,7 @@ while ((ch = (receive_getc)()) != EOF)
     (void) cutthrough_put_nl();
   else
     {
-    uschar c= ch;
+    uschar c = ch;
     (void) cutthrough_puts(&c, 1);
     }
   }
@@ -830,6 +890,63 @@ return END_EOF;
 
 
 
+/* Variant of the above read_message_data_smtp() specialised for RFC 3030
+CHUNKING.  We assume that the incoming has proper CRLF, so only have to scan
+for and strip CR.  On the downside there are more protocol reasons to stop.
+
+Arguments:
+  fout      a FILE to which to write the message; NULL if skipping
+
+Returns:    One of the END_xxx values indicating why it stopped reading
+*/
+
+static int
+read_message_bdat_smtp(FILE *fout)
+{
+int ch;
+int linelength = 0;
+
+for (;;) switch (ch = bdat_getc())
+  {
+  case EOF: return END_EOF;
+  case EOD: return END_DOT;
+  case ERR: return END_PROTOCOL;
+
+  case '\r':
+    body_linecount++;
+    if (linelength > max_received_linelength)
+      max_received_linelength = linelength;
+    linelength = -1;
+    break;
+
+  case 0:
+    body_zerocount++;
+    /*FALLTHROUGH*/
+  default:
+    message_size++;
+    linelength++;
+    if (fout)
+      {
+      if (fputc(ch, fout) == EOF) return END_WERROR;
+      if (message_size > thismessage_size_limit) return END_SIZE;
+      }
+#ifdef notyet
+    if(ch == '\n')
+      (void) cutthrough_put_nl();
+    else
+      {
+      uschar c = ch;
+      (void) cutthrough_puts(&c, 1);
+      }
+#endif
+    break;
+  }
+/*NOTREACHED*/
+}
+
+
+
+
 /*************************************************
 *             Swallow SMTP message               *
 *************************************************/
@@ -846,6 +963,7 @@ Returns:     nothing
 void
 receive_swallow_smtp(void)
 {
+/*XXX CHUNKING: not enough.  read chunks until RSET? */
 if (message_ended >= END_NOTENDED)
   message_ended = read_message_data_smtp(NULL);
 }
@@ -902,10 +1020,12 @@ if (error_handling == ERRORS_SENDER)
   error_block eblock;
   eblock.next = NULL;
   eblock.text1 = text1;
+  eblock.text2 = US"";
   if (!moan_to_sender(errcode, &eblock, hptr, f, FALSE))
     error_rc = EXIT_FAILURE;
   }
-else fprintf(stderr, "exim: %s%s\n", text2, text1);  /* Sic */
+else
+  fprintf(stderr, "exim: %s%s\n", text2, text1);  /* Sic */
 (void)fclose(f);
 exim_exit(error_rc);
 }
@@ -935,11 +1055,46 @@ Returns:     nothing
 */
 
 static void
-add_acl_headers(uschar *acl_name)
+add_acl_headers(int where, uschar *acl_name)
 {
 header_line *h, *next;
 header_line *last_received = NULL;
 
+switch(where)
+  {
+  case ACL_WHERE_DKIM:
+  case ACL_WHERE_MIME:
+  case ACL_WHERE_DATA:
+    if (cutthrough.fd >= 0 && (acl_removed_headers || acl_added_headers))
+    {
+    log_write(0, LOG_MAIN|LOG_PANIC, "Header modification in data ACLs"
+                       " will not take effect on cutthrough deliveries");
+    return;
+    }
+  }
+
+if (acl_removed_headers != NULL)
+  {
+  DEBUG(D_receive|D_acl) debug_printf(">>Headers removed by %s ACL:\n", acl_name);
+
+  for (h = header_list; h != NULL; h = h->next) if (h->type != htype_old)
+    {
+    const uschar * list = acl_removed_headers;
+    int sep = ':';         /* This is specified as a colon-separated list */
+    uschar *s;
+    uschar buffer[128];
+
+    while ((s = string_nextinlist(&list, &sep, buffer, sizeof(buffer))))
+      if (header_testname(h, s, Ustrlen(s), FALSE))
+       {
+       h->type = htype_old;
+        DEBUG(D_receive|D_acl) debug_printf("  %s", h->text);
+       }
+    }
+  acl_removed_headers = NULL;
+  DEBUG(D_receive|D_acl) debug_printf(">>\n");
+  }
+
 if (acl_added_headers == NULL) return;
 DEBUG(D_receive|D_acl) debug_printf(">>Headers added by %s ACL:\n", acl_name);
 
@@ -1027,17 +1182,17 @@ Returns:      the extended string
 */
 
 static uschar *
-add_host_info_for_log(uschar *s, int *sizeptr, int *ptrptr)
+add_host_info_for_log(uschar * s, int * sizeptr, int * ptrptr)
 {
-if (sender_fullhost != NULL)
+if (sender_fullhost)
   {
+  if (LOGGING(dnssec) && sender_host_dnssec)   /*XXX sender_helo_dnssec? */
+    s = string_cat(s, sizeptr, ptrptr, US" DS");
   s = string_append(s, sizeptr, ptrptr, 2, US" H=", sender_fullhost);
-  if ((log_extra_selector & LX_incoming_interface) != 0 &&
-       interface_address != NULL)
+  if (LOGGING(incoming_interface) && interface_address != NULL)
     {
-    uschar *ss = string_sprintf(" I=[%s]:%d", interface_address,
-      interface_port);
-    s = string_cat(s, sizeptr, ptrptr, ss, Ustrlen(ss));
+    s = string_cat(s, sizeptr, ptrptr,
+      string_sprintf(" I=[%s]:%d", interface_address, interface_port));
     }
   }
 if (sender_ident != NULL)
@@ -1143,47 +1298,45 @@ if (Ustrlen(rfc822_file_path) > 0)
 if (rc == OK)
   {
   uschar temp_path[1024];
-  int n;
-  struct dirent *entry;
-  DIR *tempdir;
+  struct dirent * entry;
+  DIR * tempdir;
 
-  (void)string_format(temp_path, 1024, "%s/scan/%s", spool_directory,
-    message_id);
+  (void) string_format(temp_path, sizeof(temp_path), "%s/scan/%s",
+    spool_directory, message_id);
 
   tempdir = opendir(CS temp_path);
-  n = 0;
-  do
+  for (;;)
     {
-    entry = readdir(tempdir);
-    if (entry == NULL) break;
-    if (strncmpic(US entry->d_name,US"__rfc822_",9) == 0)
+    if (!(entry = readdir(tempdir)))
+      break;
+    if (strncmpic(US entry->d_name, US"__rfc822_", 9) == 0)
       {
-      (void)string_format(rfc822_file_path, 2048,"%s/scan/%s/%s", spool_directory, message_id, entry->d_name);
-      debug_printf("RFC822 attachment detected: running MIME ACL for '%s'\n", rfc822_file_path);
+      (void) string_format(rfc822_file_path, sizeof(rfc822_file_path),
+       "%s/scan/%s/%s", spool_directory, message_id, entry->d_name);
+      debug_printf("RFC822 attachment detected: running MIME ACL for '%s'\n",
+       rfc822_file_path);
       break;
       }
-    } while (1);
+    }
   closedir(tempdir);
 
-  if (entry != NULL)
+  if (entry)
     {
-    mbox_file = Ufopen(rfc822_file_path,"rb");
-    if (mbox_file == NULL)
+    if ((mbox_file = Ufopen(rfc822_file_path, "rb")))
       {
-      log_write(0, LOG_PANIC,
-         "acl_smtp_mime: can't open RFC822 spool file, skipping.");
-      unlink(CS rfc822_file_path);
-      goto END_MIME_ACL;
+      /* set RFC822 expansion variable */
+      mime_is_rfc822 = 1;
+      mime_part_count_buffer = mime_part_count;
+      goto MIME_ACL_CHECK;
       }
-    /* set RFC822 expansion variable */
-    mime_is_rfc822 = 1;
-    mime_part_count_buffer = mime_part_count;
-    goto MIME_ACL_CHECK;
+    log_write(0, LOG_PANIC,
+       "acl_smtp_mime: can't open RFC822 spool file, skipping.");
+    unlink(CS rfc822_file_path);
     }
   }
 
 END_MIME_ACL:
-add_acl_headers(US"MIME");
+add_acl_headers(ACL_WHERE_MIME, US"MIME");
 if (rc == DISCARD)
   {
   recipients_count = 0;
@@ -1196,9 +1349,12 @@ else if (rc != OK)
 #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 */
+  if (  smtp_input
+     && smtp_handle_acl_fail(ACL_WHERE_MIME, rc, user_msg, log_msg) != 0)
+    {
+    *smtp_yield_ptr = FALSE;    /* No more messages after dropped connection */
+    *smtp_reply_ptr = US"";     /* Indicate reply already sent */
+    }
   message_id[0] = 0;            /* Indicate no message accepted */
   return FALSE;                 /* Cause skip to end of receive function */
   }
@@ -1222,7 +1378,7 @@ if (recipients_count == 1) received_for = recipients_list[0].address;
 received = expand_string(received_header_text);
 received_for = NULL;
 
-if (received == NULL)
+if (!received)
   {
   if(spool_name[0] != 0)
     Uunlink(spool_name);           /* Lose the data file */
@@ -1372,6 +1528,7 @@ BOOL resents_exist = FALSE;
 uschar *resent_prefix = US"";
 uschar *blackholed_by = NULL;
 uschar *blackhole_log_msg = US"";
+enum {NOT_TRIED, TMP_REJ, PERM_REJ, ACCEPTED} cutthrough_done = NOT_TRIED;
 
 flock_t lock_data;
 error_block *bad_addresses = NULL;
@@ -1402,6 +1559,10 @@ header_line *subject_header = NULL;
 header_line *msgid_header = NULL;
 header_line *received_header;
 
+#ifdef EXPERIMENTAL_DMARC
+int dmarc_up = 0;
+#endif /* EXPERIMENTAL_DMARC */
+
 /* Variables for use when building the Received: header. */
 
 uschar *timestamp;
@@ -1415,10 +1576,9 @@ 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 */
+to set up the combination, but just in case kill any ongoing connection. */
 if (extract_recip || !smtp_input)
-  cancel_cutthrough_connection();
+  cancel_cutthrough_connection("not smtp input");
 
 /* 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
@@ -1442,7 +1602,7 @@ yet, initialize the size and warning count, and deal with no size limit. */
 message_id[0] = 0;
 data_file = NULL;
 data_fd = -1;
-spool_name[0] = 0;
+spool_name = US"";
 message_size = 0;
 warning_count = 0;
 received_count = 1;            /* For the one we will add */
@@ -1459,6 +1619,11 @@ message_linecount = body_linecount = body_zerocount =
 if (smtp_input && !smtp_batched_input && !dkim_disable_verify) dkim_exim_verify_init();
 #endif
 
+#ifdef EXPERIMENTAL_DMARC
+/* initialize libopendmarc */
+dmarc_up = dmarc_init();
+#endif
+
 /* Remember the time of reception. Exim uses time+pid for uniqueness of message
 ids, and fractions of a second are required. See the comments that precede the
 message id creation below. */
@@ -2006,9 +2171,12 @@ for (h = header_list->next; h != NULL; h = h->next)
       from_header = h;
       if (!smtp_input)
         {
+        int len;
         uschar *s = Ustrchr(h->text, ':') + 1;
         while (isspace(*s)) s++;
-        if (strncmpic(s, originator_login, h->slen - (s - h->text) - 1) == 0)
+        len = h->slen - (s - h->text) - 1;
+        if (Ustrlen(originator_login) == len &&
+           strncmpic(s, originator_login, len) == 0)
           {
           uschar *name = is_resent? US"Resent-From" : US"From";
           header_add(htype_from, "%s: %s <%s@%s>\n", name, originator_name,
@@ -2038,7 +2206,6 @@ 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;
 
@@ -2207,9 +2374,23 @@ if (extract_recip)
         pp = recipient = store_get(ss - s + 1);
         for (p = s; p < ss; p++) if (*p != '\n') *pp++ = *p;
         *pp = 0;
+
+#ifdef SUPPORT_I18N
+       {
+       BOOL b = allow_utf8_domains;
+       allow_utf8_domains = TRUE;
+#endif
         recipient = parse_extract_address(recipient, &errmess, &start, &end,
           &domain, FALSE);
 
+#ifdef SUPPORT_I18N
+       if (string_is_utf8(recipient))
+         message_smtputf8 = TRUE;
+       else
+         allow_utf8_domains = b;
+       }
+#endif
+
         /* Keep a list of all the bad addresses so we can send a single
         error message at the end. However, an empty address is not an error;
         just ignore it. This can come from an empty group list like
@@ -2350,7 +2531,7 @@ it will fit. */
 to be the least significant base-62 digit of the time of arrival. Otherwise
 ensure that it is an empty string. */
 
-message_subdir[0] = split_spool_directory? message_id[5] : 0;
+message_subdir[0] = split_spool_directory ? message_id[5] : 0;
 
 /* Now that we have the message-id, if there is no message-id: header, generate
 one, but only for local (without suppress_local_fixups) or submission mode
@@ -2419,7 +2600,7 @@ if (msgid_header == NULL &&
 rewriting. Must copy the count, because later ACLs and the local_scan()
 function may mess with the real recipients. */
 
-if ((log_extra_selector & LX_received_recipients) != 0)
+if (LOGGING(received_recipients))
   {
   raw_recipients = store_get(recipients_count * sizeof(uschar *));
   for (i = 0; i < recipients_count; i++)
@@ -2627,7 +2808,6 @@ if (from_header != NULL &&
     }
   }
 
-
 /* If there are any rewriting rules, apply them to the sender address, unless
 it has already been rewritten as part of verification for SMTP input. */
 
@@ -2710,39 +2890,56 @@ 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.
+/*XXX CHUNKING: need to cancel cutthrough under BDAT, for now.  In future,
+think more if it could be handled.  Cannot do onward CHUNKING unless
+inbound is, but inbound chunking ought to be ok with outbound plain.
+Could we do onward CHUNKING given inbound CHUNKING?
 */
-if (cutthrough_fd >= 0)
+if (chunking_state > CHUNKING_OFFERED)
+  cancel_cutthrough_connection("chunking active");
+
+/* Cutthrough delivery:
+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.
+Having created it, send the headers to the destination. */
+if (cutthrough.fd >= 0)
   {
+  if (received_count > received_headers_max)
+    {
+    cancel_cutthrough_connection("too many headers");
+    if (smtp_input) receive_swallow_smtp();  /* Swallow incoming SMTP */
+    log_write(0, LOG_MAIN|LOG_REJECT, "rejected from <%s>%s%s%s%s: "
+      "Too many \"Received\" headers",
+      sender_address,
+      (sender_fullhost == NULL)? "" : " H=",
+      (sender_fullhost == NULL)? US"" : sender_fullhost,
+      (sender_ident == NULL)? "" : " U=",
+      (sender_ident == NULL)? US"" : sender_ident);
+    message_id[0] = 0;                       /* Indicate no message accepted */
+    smtp_reply = US"550 Too many \"Received\" headers - suspected mail loop";
+    goto TIDYUP;                             /* Skip to end of function */
+    }
   received_header_gen();
-  add_acl_headers(US"MAIL or RCPT");
+  add_acl_headers(ACL_WHERE_RCPT, 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
-is checked on input to be < 200 characters long. */
+directory if it isn't there. */
+
+spool_name = spool_fname(US"input", message_subdir, message_id, US"-D");
+DEBUG(D_receive) debug_printf("Data file name: %s\n", spool_name);
 
-sprintf(CS spool_name, "%s/input/%s/%s-D", spool_directory, message_subdir,
-  message_id);
-data_fd = Uopen(spool_name, O_RDWR|O_CREAT|O_EXCL, SPOOL_MODE);
-if (data_fd < 0)
+if ((data_fd = Uopen(spool_name, O_RDWR|O_CREAT|O_EXCL, SPOOL_MODE)) < 0)
   {
   if (errno == ENOENT)
     {
-    uschar temp[16];
-    sprintf(CS temp, "input/%s", message_subdir);
-    if (message_subdir[0] == 0) temp[5] = 0;
-    (void)directory_make(spool_directory, temp, INPUT_DIRECTORY_MODE, TRUE);
+    (void) directory_make(spool_directory,
+                       spool_sname(US"input", message_subdir),
+                       INPUT_DIRECTORY_MODE, TRUE);
     data_fd = Uopen(spool_name, O_RDWR|O_CREAT|O_EXCL, SPOOL_MODE);
     }
   if (data_fd < 0)
@@ -2753,7 +2950,10 @@ if (data_fd < 0)
 /* Make sure the file's group is the Exim gid, and double-check the mode
 because the group setting doesn't always get set automatically. */
 
-(void)fchown(data_fd, exim_uid, exim_gid);
+if (fchown(data_fd, exim_uid, exim_gid))
+  log_write(0, LOG_MAIN|LOG_PANIC_DIE,
+    "Failed setting ownership on spool file %s: %s",
+    spool_name, strerror(errno));
 (void)fchmod(data_fd, SPOOL_MODE);
 
 /* We now have data file open. Build a stream for it and lock it. We lock only
@@ -2783,8 +2983,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);
+  len = fwrite(s, 1, len, data_file);  len = len; /* compiler quietening */
   body_linecount++;                 /* Assumes only 1 line */
   }
 
@@ -2792,14 +2991,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);
+    message_ended = chunking_state > CHUNKING_OFFERED
+      ? read_message_bdat_smtp(data_file)
+      : read_message_data_smtp(data_file);
     receive_linecount++;                /* The terminating "." line */
     }
   else message_ended = read_message_data(data_file);
@@ -2807,51 +3005,64 @@ if (!ferror(data_file) && !(receive_feof)() && message_ended != END_DOT)
   receive_linecount += body_linecount;  /* For BSMTP errors mainly */
   message_linecount += body_linecount;
 
-  /* Handle premature termination of SMTP */
-
-  if (smtp_input && message_ended == END_EOF)
+  switch (message_ended)
     {
-    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;
-    goto TIDYUP;                             /* Skip to end of function */
-    }
+    /* Handle premature termination of SMTP */
 
-  /* Handle message that is too big. Don't use host_or_ident() in the log
-  message; we want to see the ident value even for non-remote messages. */
+    case END_EOF:
+      if (smtp_input)
+       {
+       Uunlink(spool_name);                 /* Lose data file when closed */
+       cancel_cutthrough_connection("sender closed connection");
+       message_id[0] = 0;                   /* Indicate no message accepted */
+       smtp_reply = handle_lost_connection(US"");
+       smtp_yield = FALSE;
+       goto TIDYUP;                         /* Skip to end of function */
+       }
+      break;
 
-  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 */
+    /* Handle message that is too big. Don't use host_or_ident() in the log
+    message; we want to see the ident value even for non-remote messages. */
 
-    log_write(L_size_reject, LOG_MAIN|LOG_REJECT, "rejected from <%s>%s%s%s%s: "
-      "message too big: read=%d max=%d",
-      sender_address,
-      (sender_fullhost == NULL)? "" : " H=",
-      (sender_fullhost == NULL)? US"" : sender_fullhost,
-      (sender_ident == NULL)? "" : " U=",
-      (sender_ident == NULL)? US"" : sender_ident,
-      message_size,
-      thismessage_size_limit);
+    case END_SIZE:
+      Uunlink(spool_name);                /* Lose the data file when closed */
+      cancel_cutthrough_connection("mail too big");
+      if (smtp_input) receive_swallow_smtp();  /* Swallow incoming SMTP */
 
-    if (smtp_input)
-      {
-      smtp_reply = US"552 Message size exceeds maximum permitted";
-      message_id[0] = 0;               /* Indicate no message accepted */
-      goto TIDYUP;                     /* Skip to end of function */
-      }
-    else
-      {
-      fseek(data_file, (long int)SPOOL_DATA_START_OFFSET, SEEK_SET);
-      give_local_error(ERRMESS_TOOBIG,
-        string_sprintf("message too big (max=%d)", thismessage_size_limit),
-        US"message rejected: ", error_rc, data_file, header_list);
-      /* Does not return */
-      }
+      log_write(L_size_reject, LOG_MAIN|LOG_REJECT, "rejected from <%s>%s%s%s%s: "
+       "message too big: read=%d max=%d",
+       sender_address,
+       (sender_fullhost == NULL)? "" : " H=",
+       (sender_fullhost == NULL)? US"" : sender_fullhost,
+       (sender_ident == NULL)? "" : " U=",
+       (sender_ident == NULL)? US"" : sender_ident,
+       message_size,
+       thismessage_size_limit);
+
+      if (smtp_input)
+       {
+       smtp_reply = US"552 Message size exceeds maximum permitted";
+       message_id[0] = 0;               /* Indicate no message accepted */
+       goto TIDYUP;                     /* Skip to end of function */
+       }
+      else
+       {
+       fseek(data_file, (long int)SPOOL_DATA_START_OFFSET, SEEK_SET);
+       give_local_error(ERRMESS_TOOBIG,
+         string_sprintf("message too big (max=%d)", thismessage_size_limit),
+         US"message rejected: ", error_rc, data_file, header_list);
+       /* Does not return */
+       }
+      break;
+
+    /* Handle bad BDAT protocol sequence */
+
+    case END_PROTOCOL:
+      Uunlink(spool_name);             /* Lose the data file when closed */
+      cancel_cutthrough_connection("sender protocol error");
+      smtp_reply = US"";               /* Response already sent */
+      message_id[0] = 0;               /* Indicate no message accepted */
+      goto TIDYUP;                     /* Skip to end of function */
     }
   }
 
@@ -2869,7 +3080,6 @@ 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) ||
     EXIMfsync(fileno(data_file)) < 0 || (receive_ferror)())
   {
@@ -2882,7 +3092,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();
+  cancel_cutthrough_connection("error writing spoolfile");
 
   if (smtp_input)
     {
@@ -2909,7 +3119,6 @@ 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);
 
 
@@ -2924,9 +3133,6 @@ 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)
@@ -3020,15 +3226,11 @@ if (received_header->text == NULL)      /* Non-cutthrough case */
   /* 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(ACL_WHERE_RCPT, US"MAIL or RCPT");
   }
-else if (data_fd >= 0)
+else
   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
@@ -3043,9 +3245,8 @@ user_msg = NULL;
 enable_dollar_recipients = TRUE;
 
 if (recipients_count == 0)
-  {
-  blackholed_by = recipients_discarded? US"MAIL ACL" : US"RCPT ACL";
-  }
+  blackholed_by = recipients_discarded ? US"MAIL ACL" : US"RCPT ACL";
+
 else
   {
   /* Handle interactive SMTP messages */
@@ -3056,89 +3257,85 @@ else
 #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'))
+      if (acl_smtp_dkim && dkim_verify_signers && *dkim_verify_signers)
         {
         uschar *dkim_verify_signers_expanded =
           expand_string(dkim_verify_signers);
-        if (dkim_verify_signers_expanded == NULL)
-          {
+        if (!dkim_verify_signers_expanded)
           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;
+          const 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)
+          while ((item = string_nextinlist(&ptr, &sep, NULL, 0)))
             {
             /* 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)
+            if (!item || !*item) continue;
+
+            /* Only run ACL once for each domain or identity,
+           no matter how often it appears in the expanded list. */
+            if (seen_items)
               {
               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;
-                    } 
-                }
+              const uschar *seen_items_list = seen_items;
+              BOOL seen_this_item = FALSE;
 
-              if (seen_this_item > 0)
+              while ((seen_item = string_nextinlist(&seen_items_list, &sep,
+                                                    NULL, 0)))
+               if (Ustrcmp(seen_item,item) == 0)
+                 {
+                 seen_this_item = TRUE;
+                 break;
+                 }
+
+              if (seen_this_item)
                 {
                 DEBUG(D_receive)
-                  debug_printf("acl_smtp_dkim: skipping signer %s, already seen\n", item);
+                  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, ":");
               }
 
-            seen_items = string_append(seen_items,&seen_items_size,&seen_items_offset,1,item);
+            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);
+              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);
+            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;
-              }
+             {
+             DEBUG(D_receive)
+               debug_printf("acl_smtp_dkim: acl_check returned %d on %s, "
+                 "skipping remaining items\n", rc, item);
+             cancel_cutthrough_connection("dkim acl not ok");
+             break;
+             }
             }
-          add_acl_headers(US"DKIM");
+          add_acl_headers(ACL_WHERE_DKIM, US"DKIM");
           if (rc == DISCARD)
             {
             recipients_count = 0;
@@ -3150,7 +3347,7 @@ else
             {
             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_yield = FALSE;    /* No more messages 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 */
@@ -3161,35 +3358,106 @@ else
 #endif /* DISABLE_DKIM */
 
 #ifdef WITH_CONTENT_SCAN
-/* 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 */
 
+#ifdef EXPERIMENTAL_DMARC
+    dmarc_up = dmarc_store_data(from_header);
+#endif /* EXPERIMENTAL_DMARC */
+
+#ifndef DISABLE_PRDR
+    if (prdr_requested && recipients_count > 1 && acl_smtp_data_prdr)
+      {
+      unsigned int c;
+      int all_pass = OK;
+      int all_fail = FAIL;
+
+      smtp_printf("353 PRDR content analysis beginning\r\n");
+      /* Loop through recipients, responses must be in same order received */
+      for (c = 0; recipients_count > c; c++)
+        {
+       uschar * addr= recipients_list[c].address;
+       uschar * msg= US"PRDR R=<%s> %s";
+       uschar * code;
+        DEBUG(D_receive)
+          debug_printf("PRDR processing recipient %s (%d of %d)\n",
+                       addr, c+1, recipients_count);
+        rc = acl_check(ACL_WHERE_PRDR, addr,
+                       acl_smtp_data_prdr, &user_msg, &log_msg);
+
+        /* If any recipient rejected content, indicate it in final message */
+        all_pass |= rc;
+        /* If all recipients rejected, indicate in final message */
+        all_fail &= rc;
+
+        switch (rc)
+          {
+          case OK: case DISCARD: code = US"250"; break;
+          case DEFER:            code = US"450"; break;
+          default:               code = US"550"; break;
+          }
+       if (user_msg != NULL)
+         smtp_user_msg(code, user_msg);
+       else
+         {
+         switch (rc)
+            {
+            case OK: case DISCARD:
+              msg = string_sprintf(CS msg, addr, "acceptance");        break;
+            case DEFER:
+              msg = string_sprintf(CS msg, addr, "temporary refusal"); break;
+            default:
+              msg = string_sprintf(CS msg, addr, "refusal");           break;
+            }
+          smtp_user_msg(code, msg);
+         }
+       if (log_msg)       log_write(0, LOG_MAIN, "PRDR %s %s", addr, log_msg);
+       else if (user_msg) log_write(0, LOG_MAIN, "PRDR %s %s", addr, user_msg);
+       else               log_write(0, LOG_MAIN, "%s", CS msg);
+
+       if (rc != OK) { receive_remove_recipient(addr); c--; }
+        }
+      /* Set up final message, used if data acl gives OK */
+      smtp_reply = string_sprintf("%s id=%s message %s",
+                      all_fail == FAIL ? US"550" : US"250",
+                      message_id,
+                       all_fail == FAIL
+                        ? US"rejected for all recipients"
+                        : all_pass == OK
+                          ? US"accepted"
+                          : US"accepted for some recipients");
+      if (recipients_count == 0)
+        {
+        message_id[0] = 0;       /* Indicate no message accepted */
+       goto TIDYUP;
+       }
+      }
+    else
+      prdr_requested = FALSE;
+#endif /* !DISABLE_PRDR */
+
     /* 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);
-      add_acl_headers(US"DATA");
+      add_acl_headers(ACL_WHERE_DATA, US"DATA");
       if (rc == DISCARD)
         {
         recipients_count = 0;
         blackholed_by = US"DATA ACL";
         if (log_msg != NULL)
           blackhole_log_msg = string_sprintf(": %s", log_msg);
-       cancel_cutthrough_connection();
+       cancel_cutthrough_connection("data acl discard");
         }
       else if (rc != OK)
         {
         Uunlink(spool_name);
-       cancel_cutthrough_connection();
+       cancel_cutthrough_connection("data acl not ok");
 #ifdef WITH_CONTENT_SCAN
         unspool_mbox();
 #endif
@@ -3197,7 +3465,7 @@ else
        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 */
+          smtp_yield = FALSE;    /* No more messages 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 */
@@ -3260,7 +3528,7 @@ else
           /* Does not return */
           }
         }
-      add_acl_headers(US"non-SMTP");
+      add_acl_headers(ACL_WHERE_NOTSMTP, US"non-SMTP");
       }
     }
 
@@ -3300,7 +3568,6 @@ 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);
@@ -3389,7 +3656,7 @@ else
     goto TEMPREJECT;
 
     case LOCAL_SCAN_REJECT_NOLOGHDR:
-    log_extra_selector &= ~LX_rejected_header;
+    BIT_CLEAR(log_selector, log_selector_size, Li_rejected_header);
     /* Fall through */
 
     case LOCAL_SCAN_REJECT:
@@ -3398,7 +3665,7 @@ else
     break;
 
     case LOCAL_SCAN_TEMPREJECT_NOLOGHDR:
-    log_extra_selector &= ~LX_rejected_header;
+    BIT_CLEAR(log_selector, log_selector_size, Li_rejected_header);
     /* Fall through */
 
     case LOCAL_SCAN_TEMPREJECT:
@@ -3454,12 +3721,11 @@ signal(SIGINT, SIG_IGN);
 deliver_firsttime = TRUE;
 
 #ifdef EXPERIMENTAL_BRIGHTMAIL
-if (bmi_run == 1) {
-  /* rewind data file */
-  /* XXX cutthrough - no-spool option..... */
+if (bmi_run == 1)
+  { /* rewind data file */
   lseek(data_fd, (long int)SPOOL_DATA_START_OFFSET, SEEK_SET);
   bmi_verdicts = bmi_process_message(header_list, data_fd);
-};
+  }
 #endif
 
 /* Update the timstamp in our Received: header to account for any time taken by
@@ -3497,10 +3763,6 @@ if (host_checking || blackholed_by != NULL)
 /* Write the -H file */
 
 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);
@@ -3520,7 +3782,6 @@ else
       /* Does not return */
       }
     }
-  }
 
 
 /* The message has now been successfully received. */
@@ -3559,44 +3820,74 @@ size = 256;
 sptr = 0;
 s = store_get(size);
 
-s = string_append(s, &size, &sptr, 2, US"<= ",
-  (sender_address[0] == 0)? US"<>" : sender_address);
-if (message_reference != NULL)
+s = string_append(s, &size, &sptr, 2,
+  fake_response == FAIL ? US"(= " : US"<= ",
+  sender_address[0] == 0 ? US"<>" : sender_address);
+if (message_reference)
   s = string_append(s, &size, &sptr, 2, US" R=", message_reference);
 
 s = add_host_info_for_log(s, &size, &sptr);
 
 #ifdef SUPPORT_TLS
-if ((log_extra_selector & LX_tls_cipher) != 0 && tls_cipher != NULL)
-  s = string_append(s, &size, &sptr, 2, US" X=", tls_cipher);
-if ((log_extra_selector & LX_tls_certificate_verified) != 0 &&
-     tls_cipher != NULL)
+if (LOGGING(tls_cipher) && tls_in.cipher)
+  s = string_append(s, &size, &sptr, 2, US" X=", tls_in.cipher);
+if (LOGGING(tls_certificate_verified) && tls_in.cipher)
   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)
+    tls_in.certificate_verified ? "yes":"no");
+if (LOGGING(tls_peerdn) && tls_in.peerdn)
   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)
+    string_printing(tls_in.peerdn), US"\"");
+if (LOGGING(tls_sni) && tls_in.sni)
   s = string_append(s, &size, &sptr, 3, US" SNI=\"",
-    string_printing(tls_sni), US"\"");
+    string_printing(tls_in.sni), US"\"");
 #endif
 
-if (sender_host_authenticated != NULL)
+if (sender_host_authenticated)
   {
   s = string_append(s, &size, &sptr, 2, US" A=", sender_host_authenticated);
-  if (authenticated_id != NULL)
+  if (authenticated_id)
+    {
     s = string_append(s, &size, &sptr, 2, US":", authenticated_id);
+    if (LOGGING(smtp_mailauth) && authenticated_sender)
+      s = string_append(s, &size, &sptr, 2, US":", authenticated_sender);
+    }
   }
 
+#ifndef DISABLE_PRDR
+if (prdr_requested)
+  s = string_catn(s, &size, &sptr, US" PRDR", 5);
+#endif
+
+#ifdef SUPPORT_PROXY
+if (proxy_session && LOGGING(proxy))
+  s = string_append(s, &size, &sptr, 2, US" PRX=", proxy_local_address);
+#endif
+
+if (chunking_state > CHUNKING_OFFERED)
+  s = string_catn(s, &size, &sptr, US" K", 2);
+
 sprintf(CS big_buffer, "%d", msg_size);
 s = string_append(s, &size, &sptr, 2, US" S=", big_buffer);
 
+/* log 8BITMIME mode announced in MAIL_FROM
+   0 ... no BODY= used
+   7 ... 7BIT
+   8 ... 8BITMIME */
+if (LOGGING(8bitmime))
+  {
+  sprintf(CS big_buffer, "%d", body_8bitmime);
+  s = string_append(s, &size, &sptr, 2, US" M8S=", big_buffer);
+  }
+
+if (*queue_name)
+  s = string_append(s, &size, &sptr, 2, US" Q=", queue_name);
+
 /* If an addr-spec in a message-id contains a quoted string, it can contain
 any characters except " \ and CR and so in particular it can contain NL!
 Therefore, make sure we use a printing-characters only version for the log.
 Also, allow for domain literals in the message id. */
 
-if (msgid_header != NULL)
+if (msgid_header)
   {
   uschar *old_id;
   BOOL save_allow_domain_literals = allow_domain_literals;
@@ -3611,7 +3902,7 @@ if (msgid_header != NULL)
 /* If subject logging is turned on, create suitable printing-character
 text. By expanding $h_subject: we make use of the MIME decoding. */
 
-if ((log_extra_selector & LX_subject) != 0 && subject_header != NULL)
+if (LOGGING(subject) && subject_header != NULL)
   {
   int i;
   uschar *p = big_buffer;
@@ -3645,16 +3936,15 @@ if (message_logs && blackholed_by == NULL)
   {
   int fd;
 
-  sprintf(CS spool_name, "%s/msglog/%s/%s", spool_directory, message_subdir,
-    message_id);
-  fd = Uopen(spool_name, O_WRONLY|O_APPEND|O_CREAT, SPOOL_MODE);
-
-  if (fd < 0 && errno == ENOENT)
+  spool_name = spool_fname(US"msglog", message_subdir, message_id, US"");
+  
+  if (  (fd = Uopen(spool_name, O_WRONLY|O_APPEND|O_CREAT, SPOOL_MODE)) < 0
+     && errno == ENOENT
+     )
     {
-    uschar temp[16];
-    sprintf(CS temp, "msglog/%s", message_subdir);
-    if (message_subdir[0] == 0) temp[6] = 0;
-    (void)directory_make(spool_directory, temp, MSGLOG_DIRECTORY_MODE, TRUE);
+    (void)directory_make(spool_directory,
+                       spool_sname(US"msglog", message_subdir),
+                       MSGLOG_DIRECTORY_MODE, TRUE);
     fd = Uopen(spool_name, O_WRONLY|O_APPEND|O_CREAT, SPOOL_MODE);
     }
 
@@ -3680,7 +3970,9 @@ if (message_logs && blackholed_by == NULL)
       if (deliver_freeze) fprintf(message_log, "%s frozen by %s\n", now,
         frozen_by);
       if (queue_only_policy) fprintf(message_log,
-        "%s no immediate delivery: queued by %s\n", now, queued_by);
+        "%s no immediate delivery: queued%s%s by %s\n", now,
+        *queue_name ? " in " : "", *queue_name ? CS queue_name : "",
+       queued_by);
       (void)fclose(message_log);
       }
     }
@@ -3732,24 +4024,16 @@ if (smtp_input && sender_host_address != NULL && !sender_host_notsocket &&
       /* Re-use the log line workspace */
 
       sptr = 0;
-      s = string_cat(s, &size, &sptr, msg, Ustrlen(msg));
+      s = string_cat(s, &size, &sptr, 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);
+      Uunlink(spool_fname(US"input", message_subdir, message_id, US"-D"));
+      Uunlink(spool_fname(US"input", message_subdir, message_id, US"-H"));
+      Uunlink(spool_fname(US"msglog", message_subdir, message_id, US""));
 
       goto TIDYUP;
       }
@@ -3759,76 +4043,58 @@ if (smtp_input && sender_host_address != NULL && !sender_host_notsocket &&
 /* 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
+/* 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
+   Send dot onward.  If accepted, wipe 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.
+   If rejected: copy response to sender, wipe the spooled files, log approriately.
+   If temp-reject: accept to sender, keep the spooled files.
 
    Having the normal spool files lets us do data-filtering, and store/forward on temp-reject.
+
+   XXX We do not handle queue-only, freezing, or blackholes.
 */
-if(cutthrough_fd >= 0)
+if(cutthrough.fd >= 0)
   {
-  uschar * msg= cutthrough_finaldot(); /* Ask the target system to accept the messsage */
+  uschar * msg= cutthrough_finaldot(); /* Ask the target system to accept the message */
+                                       /* Logging was done in finaldot() */
   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;
-  }
+    {
+    case '2':  /* Accept. Do the same to the source; dump any spoolfiles.   */
+      cutthrough_done = ACCEPTED;
+      break;                                   /* message_id needed for SMTP accept below */
+
+    default:   /* Unknown response, or error.  Treat as temp-reject.         */
+    case '4':  /* Temp-reject. Keep spoolfiles and accept. */
+      cutthrough_done = TMP_REJ;               /* Avoid the usual immediate delivery attempt */
+      break;                                   /* message_id needed for SMTP accept below */
+
+    case '5':  /* Perm-reject.  Do the same to the source.  Dump any spoolfiles */
+      smtp_reply= msg;         /* Pass on the exact error */
+      cutthrough_done = PERM_REJ;
+      break;
+    }
   }
 
-if(smtp_reply == NULL)
+#ifndef DISABLE_PRDR
+if(!smtp_reply || prdr_requested)
+#else
+if(!smtp_reply)
+#endif
   {
   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),
+    (LOGGING(received_recipients)? LOG_RECIPIENTS : 0) |
+    (LOGGING(received_sender)? 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);
+    "no immediate delivery: queued%s%s by %s",
+    *queue_name ? " in " : "", *queue_name ? CS queue_name : "",       
+    queued_by);
   }
 receive_call_bombout = FALSE;
 
@@ -3884,27 +4150,33 @@ if (smtp_input)
 
   if (!smtp_batched_input)
     {
-/*XXX cutthrough - here's where the originating sender gets given the data-acceptance */
-    if (smtp_reply == NULL)
+    if (!smtp_reply)
       {
       if (fake_response != OK)
-        smtp_respond((fake_response == DEFER)? US"450" : US"550", 3, TRUE,
-          fake_response_text);
+        smtp_respond(fake_response == DEFER ? US"450" : US"550",
+         3, TRUE, fake_response_text);
 
       /* An OK response is required; use "message" text if present. */
 
-      else if (user_msg != NULL)
+      else if (user_msg)
         {
         uschar *code = US"250";
         int len = 3;
-        smtp_message_code(&code, &len, &user_msg, NULL);
+        smtp_message_code(&code, &len, &user_msg, NULL, TRUE);
         smtp_respond(code, len, TRUE, user_msg);
         }
 
       /* Default OK response */
 
+      else if (chunking_state > CHUNKING_OFFERED)
+       {
+        smtp_printf("250- %u byte chunk, total %d\r\n250 OK id=%s\r\n",
+           chunking_datasize, message_size+message_linecount, message_id);
+       chunking_state = CHUNKING_OFFERED;
+       }
       else
         smtp_printf("250 OK id=%s\r\n", message_id);
+
       if (host_checking)
         fprintf(stdout,
           "\n**** SMTP testing: that is not a real message id!\n\n");
@@ -3921,18 +4193,27 @@ if (smtp_input)
         smtp_printf("%.1024s\r\n", smtp_reply);
       }
 
-    if (cutthrough_delivery)
+    switch (cutthrough_done)
       {
-      log_write(0, LOG_MAIN, "Completed");
-      message_id[0] = 0;                       /* Prevent a delivery from starting */
+      case ACCEPTED: log_write(0, LOG_MAIN, "Completed");/* Delivery was done */
+      case PERM_REJ:
+             {                                          /* Delete spool files */
+             Uunlink(spool_fname(US"input", message_subdir, message_id, US"-D"));
+             Uunlink(spool_fname(US"input", message_subdir, message_id, US"-H"));
+             Uunlink(spool_fname(US"msglog", message_subdir, message_id, US""));
+             }
+      case TMP_REJ: message_id[0] = 0;   /* Prevent a delivery from starting */
+      default:break;
       }
+    cutthrough.delivery = FALSE;
     }
 
   /* For batched SMTP, generate an error message on failure, and do
   nothing on success. The function moan_smtp_batch() does not return -
   it exits from the program with a non-zero return code. */
 
-  else if (smtp_reply != NULL) moan_smtp_batch(NULL, "%s", smtp_reply);
+  else if (smtp_reply)
+    moan_smtp_batch(NULL, "%s", smtp_reply);
   }
 
 
@@ -3941,11 +4222,11 @@ file has already been unlinked, and the header file was never written to disk.
 We must now indicate that nothing was received, to prevent a delivery from
 starting. */
 
-if (blackholed_by != NULL)
+if (blackholed_by)
   {
-  uschar *detail = (local_scan_data != NULL)?
-    string_printing(local_scan_data) :
-    string_sprintf("(%s discarded recipients)", blackholed_by);
+  const uschar *detail = local_scan_data
+    ? string_printing(local_scan_data)
+    string_sprintf("(%s discarded recipients)", blackholed_by);
   log_write(0, LOG_MAIN, "=> blackhole %s%s", detail, blackhole_log_msg);
   log_write(0, LOG_MAIN, "Completed");
   message_id[0] = 0;