Drop server_realm from heimdal_gssapi
[exim.git] / src / src / acl.c
index 47c4f86a258f906f296546e2782cfc40e7b03d5c..77fdc56271b3ceda37ff9a0354dbaa1963767868 100644 (file)
@@ -1,10 +1,8 @@
-/* $Cambridge: exim/src/src/acl.c,v 1.18 2005/01/27 10:26:14 ph10 Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2005 */
+/* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Code for handling Access Control Lists (ACLs) */
@@ -27,27 +25,57 @@ static uschar *verbs[] =
   { US"accept", US"defer", US"deny", US"discard", US"drop", US"require",
     US"warn" };
 
-/* For each verb, the condition for which "message" is used */
-
-static int msgcond[] = { FAIL, OK, OK, FAIL, OK, FAIL, OK };
+/* For each verb, the conditions for which "message" or "log_message" are used
+are held as a bitmap. This is to avoid expanding the strings unnecessarily. For
+"accept", the FAIL case is used only after "endpass", but that is selected in
+the code. */
+
+static int msgcond[] = {
+  (1<<OK) | (1<<FAIL) | (1<<FAIL_DROP),  /* accept */
+  (1<<OK),                               /* defer */
+  (1<<OK),                               /* deny */
+  (1<<OK) | (1<<FAIL) | (1<<FAIL_DROP),  /* discard */
+  (1<<OK),                               /* drop */
+  (1<<FAIL) | (1<<FAIL_DROP),            /* require */
+  (1<<OK)                                /* warn */
+  };
 
 /* ACL condition and modifier codes - keep in step with the table that
-follows. */
+follows, and the cond_expand_at_top and uschar cond_modifiers tables lower
+down. */
 
-enum { ACLC_ACL, ACLC_AUTHENTICATED,
+enum { ACLC_ACL,
+       ACLC_ADD_HEADER,
+       ACLC_AUTHENTICATED,
 #ifdef EXPERIMENTAL_BRIGHTMAIL
        ACLC_BMI_OPTIN,
 #endif
-ACLC_CONDITION, ACLC_CONTROL,
+       ACLC_CONDITION,
+       ACLC_CONTINUE,
+       ACLC_CONTROL,
+#ifdef EXPERIMENTAL_DCC
+       ACLC_DCC,
+#endif
 #ifdef WITH_CONTENT_SCAN
        ACLC_DECODE,
 #endif
        ACLC_DELAY,
 #ifdef WITH_OLD_DEMIME
        ACLC_DEMIME,
-#endif        
-       ACLC_DNSLISTS, ACLC_DOMAINS, ACLC_ENCRYPTED, ACLC_ENDPASS,
-       ACLC_HOSTS, ACLC_LOCAL_PARTS, ACLC_LOG_MESSAGE, ACLC_LOGWRITE,
+#endif
+#ifndef DISABLE_DKIM
+       ACLC_DKIM_SIGNER,
+       ACLC_DKIM_STATUS,
+#endif
+       ACLC_DNSLISTS,
+       ACLC_DOMAINS,
+       ACLC_ENCRYPTED,
+       ACLC_ENDPASS,
+       ACLC_HOSTS,
+       ACLC_LOCAL_PARTS,
+       ACLC_LOG_MESSAGE,
+       ACLC_LOG_REJECT_TARGET,
+       ACLC_LOGWRITE,
 #ifdef WITH_CONTENT_SCAN
        ACLC_MALWARE,
 #endif
@@ -55,29 +83,41 @@ ACLC_CONDITION, ACLC_CONTROL,
 #ifdef WITH_CONTENT_SCAN
        ACLC_MIME_REGEX,
 #endif
+       ACLC_RATELIMIT,
        ACLC_RECIPIENTS,
 #ifdef WITH_CONTENT_SCAN
        ACLC_REGEX,
 #endif
-       ACLC_SENDER_DOMAINS, ACLC_SENDERS, ACLC_SET,
+       ACLC_SENDER_DOMAINS,
+       ACLC_SENDERS,
+       ACLC_SET,
 #ifdef WITH_CONTENT_SCAN
-       ACLC_SPAM,       
+       ACLC_SPAM,
 #endif
 #ifdef EXPERIMENTAL_SPF
        ACLC_SPF,
+       ACLC_SPF_GUESS,
 #endif
        ACLC_VERIFY };
 
-/* ACL conditions/modifiers: "delay", "control", "endpass", "message",
-"log_message", "logwrite", and "set" are modifiers that look like conditions
-but always return TRUE. They are used for their side effects. */
+/* ACL conditions/modifiers: "delay", "control", "continue", "endpass",
+"message", "log_message", "log_reject_target", "logwrite", and "set" are
+modifiers that look like conditions but always return TRUE. They are used for
+their side effects. */
 
-static uschar *conditions[] = { US"acl", US"authenticated",
+static uschar *conditions[] = {
+  US"acl",
+  US"add_header",
+  US"authenticated",
 #ifdef EXPERIMENTAL_BRIGHTMAIL
   US"bmi_optin",
 #endif
   US"condition",
-  US"control", 
+  US"continue",
+  US"control",
+#ifdef EXPERIMENTAL_DCC
+  US"dcc",
+#endif
 #ifdef WITH_CONTENT_SCAN
   US"decode",
 #endif
@@ -85,8 +125,19 @@ static uschar *conditions[] = { US"acl", US"authenticated",
 #ifdef WITH_OLD_DEMIME
   US"demime",
 #endif
-  US"dnslists", US"domains", US"encrypted",
-  US"endpass", US"hosts", US"local_parts", US"log_message", US"logwrite",
+#ifndef DISABLE_DKIM
+  US"dkim_signers",
+  US"dkim_status",
+#endif
+  US"dnslists",
+  US"domains",
+  US"encrypted",
+  US"endpass",
+  US"hosts",
+  US"local_parts",
+  US"log_message",
+  US"log_reject_target",
+  US"logwrite",
 #ifdef WITH_CONTENT_SCAN
   US"malware",
 #endif
@@ -94,6 +145,7 @@ static uschar *conditions[] = { US"acl", US"authenticated",
 #ifdef WITH_CONTENT_SCAN
   US"mime_regex",
 #endif
+  US"ratelimit",
   US"recipients",
 #ifdef WITH_CONTENT_SCAN
   US"regex",
@@ -104,33 +156,104 @@ static uschar *conditions[] = { US"acl", US"authenticated",
 #endif
 #ifdef EXPERIMENTAL_SPF
   US"spf",
+  US"spf_guess",
 #endif
   US"verify" };
-  
-/* ACL control names */
 
-static uschar *controls[] = { US"error", US"caseful_local_part",
-  US"caselower_local_part", US"enforce_sync", US"no_enforce_sync", US"freeze",
-  US"queue_only", US"submission", US"no_multiline"}; 
 
-/* Flags to indicate for which conditions /modifiers a string expansion is done
+/* Return values from decode_control(); keep in step with the table of names
+that follows! */
+
+enum {
+  CONTROL_AUTH_UNADVERTISED,
+  #ifdef EXPERIMENTAL_BRIGHTMAIL
+  CONTROL_BMI_RUN,
+  #endif
+  CONTROL_DEBUG,
+  #ifndef DISABLE_DKIM
+  CONTROL_DKIM_VERIFY,
+  #endif
+  CONTROL_ERROR,
+  CONTROL_CASEFUL_LOCAL_PART,
+  CONTROL_CASELOWER_LOCAL_PART,
+  CONTROL_ENFORCE_SYNC,
+  CONTROL_NO_ENFORCE_SYNC,
+  CONTROL_FREEZE,
+  CONTROL_QUEUE_ONLY,
+  CONTROL_SUBMISSION,
+  CONTROL_SUPPRESS_LOCAL_FIXUPS,
+  #ifdef WITH_CONTENT_SCAN
+  CONTROL_NO_MBOX_UNSPOOL,
+  #endif
+  CONTROL_FAKEDEFER,
+  CONTROL_FAKEREJECT,
+  CONTROL_NO_MULTILINE,
+  CONTROL_NO_PIPELINING,
+  CONTROL_NO_DELAY_FLUSH,
+  CONTROL_NO_CALLOUT_FLUSH
+};
+
+/* ACL control names; keep in step with the table above! This list is used for
+turning ids into names. The actual list of recognized names is in the variable
+control_def controls_list[] below. The fact that there are two lists is a mess
+and should be tidied up. */
+
+static uschar *controls[] = {
+  US"allow_auth_unadvertised",
+  #ifdef EXPERIMENTAL_BRIGHTMAIL
+  US"bmi_run",
+  #endif
+  US"debug",
+  #ifndef DISABLE_DKIM
+  US"dkim_disable_verify",
+  #endif
+  US"error",
+  US"caseful_local_part",
+  US"caselower_local_part",
+  US"enforce_sync",
+  US"no_enforce_sync",
+  US"freeze",
+  US"queue_only",
+  US"submission",
+  US"suppress_local_fixups",
+  #ifdef WITH_CONTENT_SCAN
+  US"no_mbox_unspool",
+  #endif
+  US"fakedefer",
+  US"fakereject",
+  US"no_multiline_responses",
+  US"no_pipelining",
+  US"no_delay_flush",
+  US"no_callout_flush"
+};
+
+/* Flags to indicate for which conditions/modifiers a string expansion is done
 at the outer level. In the other cases, expansion already occurs in the
 checking functions. */
 
 static uschar cond_expand_at_top[] = {
   TRUE,    /* acl */
+  TRUE,    /* add_header */
   FALSE,   /* authenticated */
 #ifdef EXPERIMENTAL_BRIGHTMAIL
   TRUE,    /* bmi_optin */
-#endif  
+#endif
   TRUE,    /* condition */
+  TRUE,    /* continue */
   TRUE,    /* control */
+#ifdef EXPERIMENTAL_DCC
+  TRUE,    /* dcc */
+#endif
 #ifdef WITH_CONTENT_SCAN
   TRUE,    /* decode */
 #endif
   TRUE,    /* delay */
 #ifdef WITH_OLD_DEMIME
   TRUE,    /* demime */
+#endif
+#ifndef DISABLE_DKIM
+  TRUE,    /* dkim_signers */
+  TRUE,    /* dkim_status */
 #endif
   TRUE,    /* dnslists */
   FALSE,   /* domains */
@@ -139,6 +262,7 @@ static uschar cond_expand_at_top[] = {
   FALSE,   /* hosts */
   FALSE,   /* local_parts */
   TRUE,    /* log_message */
+  TRUE,    /* log_reject_target */
   TRUE,    /* logwrite */
 #ifdef WITH_CONTENT_SCAN
   TRUE,    /* malware */
@@ -147,6 +271,7 @@ static uschar cond_expand_at_top[] = {
 #ifdef WITH_CONTENT_SCAN
   TRUE,    /* mime_regex */
 #endif
+  TRUE,    /* ratelimit */
   FALSE,   /* recipients */
 #ifdef WITH_CONTENT_SCAN
   TRUE,    /* regex */
@@ -159,6 +284,7 @@ static uschar cond_expand_at_top[] = {
 #endif
 #ifdef EXPERIMENTAL_SPF
   TRUE,    /* spf */
+  TRUE,    /* spf_guess */
 #endif
   TRUE     /* verify */
 };
@@ -167,18 +293,27 @@ static uschar cond_expand_at_top[] = {
 
 static uschar cond_modifiers[] = {
   FALSE,   /* acl */
+  TRUE,    /* add_header */
   FALSE,   /* authenticated */
 #ifdef EXPERIMENTAL_BRIGHTMAIL
   TRUE,    /* bmi_optin */
-#endif  
+#endif
   FALSE,   /* condition */
+  TRUE,    /* continue */
   TRUE,    /* control */
+#ifdef EXPERIMENTAL_DCC
+  FALSE,   /* dcc */
+#endif
 #ifdef WITH_CONTENT_SCAN
   FALSE,   /* decode */
 #endif
   TRUE,    /* delay */
 #ifdef WITH_OLD_DEMIME
   FALSE,   /* demime */
+#endif
+#ifndef DISABLE_DKIM
+  FALSE,   /* dkim_signers */
+  FALSE,   /* dkim_status */
 #endif
   FALSE,   /* dnslists */
   FALSE,   /* domains */
@@ -187,6 +322,7 @@ static uschar cond_modifiers[] = {
   FALSE,   /* hosts */
   FALSE,   /* local_parts */
   TRUE,    /* log_message */
+  TRUE,    /* log_reject_target */
   TRUE,    /* logwrite */
 #ifdef WITH_CONTENT_SCAN
   FALSE,   /* malware */
@@ -195,6 +331,7 @@ static uschar cond_modifiers[] = {
 #ifdef WITH_CONTENT_SCAN
   FALSE,   /* mime_regex */
 #endif
+  FALSE,   /* ratelimit */
   FALSE,   /* recipients */
 #ifdef WITH_CONTENT_SCAN
   FALSE,   /* regex */
@@ -207,126 +344,122 @@ static uschar cond_modifiers[] = {
 #endif
 #ifdef EXPERIMENTAL_SPF
   FALSE,   /* spf */
+  FALSE,   /* spf_guess */
 #endif
   FALSE    /* verify */
 };
 
-/* Bit map vector of which conditions are not allowed at certain times. For
-each condition, there's a bitmap of dis-allowed times. */
+/* Bit map vector of which conditions and modifiers are not allowed at certain
+times. For each condition and modifier, there's a bitmap of dis-allowed times.
+For some, it is easier to specify the negation of a small number of allowed
+times. */
 
 static unsigned int cond_forbids[] = {
   0,                                               /* acl */
-   
-  (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_CONNECT)|   /* authenticated */
-    (1<<ACL_WHERE_HELO),
-  
-#ifdef EXPERIMENTAL_BRIGHTMAIL
+
+  (unsigned int)
+  ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)|       /* add_header */
+    (1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|
+    (1<<ACL_WHERE_MIME)|(1<<ACL_WHERE_NOTSMTP)|
+    (1<<ACL_WHERE_DKIM)|
+    (1<<ACL_WHERE_NOTSMTP_START)),
+
+  (1<<ACL_WHERE_NOTSMTP)|                          /* authenticated */
+    (1<<ACL_WHERE_NOTSMTP_START)|
+    (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO),
+
+  #ifdef EXPERIMENTAL_BRIGHTMAIL
   (1<<ACL_WHERE_AUTH)|                             /* bmi_optin */
     (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
     (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_MIME)|
-    (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|                                       
+    (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
     (1<<ACL_WHERE_MAILAUTH)|
     (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
-    (1<<ACL_WHERE_VRFY)|(1<<ACL_WHERE_PREDATA),
-#endif
-  
+    (1<<ACL_WHERE_VRFY)|(1<<ACL_WHERE_PREDATA)|
+    (1<<ACL_WHERE_NOTSMTP_START),
+  #endif
+
   0,                                               /* condition */
-  
+
+  0,                                               /* continue */
+
   /* Certain types of control are always allowed, so we let it through
-  always and check in the control processing itself */
-  
+  always and check in the control processing itself. */
+
   0,                                               /* control */
-  
-#ifdef WITH_CONTENT_SCAN
-  (1<<ACL_WHERE_AUTH)|                             /* decode */
-    (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
-    (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_PREDATA)|
-    (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
-    (1<<ACL_WHERE_MAILAUTH)|(1<<ACL_WHERE_QUIT)|
-    (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
-    (1<<ACL_WHERE_VRFY)|(1<<ACL_WHERE_RCPT),
-#endif
 
-  0,                                               /* delay */
-  
-#ifdef WITH_OLD_DEMIME
-  (1<<ACL_WHERE_AUTH)|                             /* demime */
-    (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
-    (1<<ACL_WHERE_RCPT)|(1<<ACL_WHERE_PREDATA)|
-    (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
-    (1<<ACL_WHERE_MAILAUTH)|(1<<ACL_WHERE_QUIT)|
-    (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
-    (1<<ACL_WHERE_VRFY)|(1<<ACL_WHERE_MIME),
-#endif
-  
-  (1<<ACL_WHERE_NOTSMTP),                          /* dnslists */
+  #ifdef EXPERIMENTAL_DCC
+  (unsigned int)
+  ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)),   /* dcc */
+  #endif
 
-  (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_AUTH)|      /* domains */
-    (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
-    (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_PREDATA)|
-    (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
-    (1<<ACL_WHERE_MAILAUTH)|(1<<ACL_WHERE_QUIT)|
-    (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
-    (1<<ACL_WHERE_VRFY),
+  #ifdef WITH_CONTENT_SCAN
+  (unsigned int)
+  ~(1<<ACL_WHERE_MIME),                            /* decode */
+  #endif
 
-  (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_CONNECT)|   /* encrypted */
+  (1<<ACL_WHERE_NOTQUIT),                          /* delay */
+
+  #ifdef WITH_OLD_DEMIME
+  (unsigned int)
+  ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)),   /* demime */
+  #endif
+
+  #ifndef DISABLE_DKIM
+  (unsigned int)
+  ~(1<<ACL_WHERE_DKIM),                            /* dkim_signers */
+
+  (unsigned int)
+  ~(1<<ACL_WHERE_DKIM),                            /* dkim_status */
+  #endif
+
+  (1<<ACL_WHERE_NOTSMTP)|                          /* dnslists */
+    (1<<ACL_WHERE_NOTSMTP_START),
+
+  (unsigned int)
+  ~(1<<ACL_WHERE_RCPT),                            /* domains */
+
+  (1<<ACL_WHERE_NOTSMTP)|                          /* encrypted */
+    (1<<ACL_WHERE_CONNECT)|
+    (1<<ACL_WHERE_NOTSMTP_START)|
     (1<<ACL_WHERE_HELO),
-     
+
   0,                                               /* endpass */
-   
-  (1<<ACL_WHERE_NOTSMTP),                          /* hosts */
 
-  (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_AUTH)|      /* local_parts */
-    (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
-    (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_PREDATA)|
-    (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
-    (1<<ACL_WHERE_MAILAUTH)|(1<<ACL_WHERE_QUIT)|
-    (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
-    (1<<ACL_WHERE_VRFY),
+  (1<<ACL_WHERE_NOTSMTP)|                          /* hosts */
+    (1<<ACL_WHERE_NOTSMTP_START),
+
+  (unsigned int)
+  ~(1<<ACL_WHERE_RCPT),                            /* local_parts */
 
   0,                                               /* log_message */
-   
+
+  0,                                               /* log_reject_target */
+
   0,                                               /* logwrite */
-  
-#ifdef WITH_CONTENT_SCAN
-  (1<<ACL_WHERE_AUTH)|                             /* malware */
-    (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
-    (1<<ACL_WHERE_RCPT)|(1<<ACL_WHERE_PREDATA)|
-    (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
-    (1<<ACL_WHERE_MAILAUTH)|(1<<ACL_WHERE_QUIT)|
-    (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
-    (1<<ACL_WHERE_VRFY)|(1<<ACL_WHERE_MIME),
-#endif
+
+  #ifdef WITH_CONTENT_SCAN
+  (unsigned int)
+  ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)),   /* malware */
+  #endif
 
   0,                                               /* message */
 
-#ifdef WITH_CONTENT_SCAN
-  (1<<ACL_WHERE_AUTH)|                             /* mime_regex */
-    (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
-    (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_PREDATA)|
-    (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
-    (1<<ACL_WHERE_MAILAUTH)|(1<<ACL_WHERE_QUIT)|
-    (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
-    (1<<ACL_WHERE_VRFY)|(1<<ACL_WHERE_RCPT),
-#endif
+  #ifdef WITH_CONTENT_SCAN
+  (unsigned int)
+  ~(1<<ACL_WHERE_MIME),                            /* mime_regex */
+  #endif
 
-  (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_AUTH)|      /* recipients */
-    (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
-    (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_PREDATA)|
-    (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
-    (1<<ACL_WHERE_MAILAUTH)|(1<<ACL_WHERE_QUIT)|
-    (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
-    (1<<ACL_WHERE_VRFY),
+  0,                                               /* ratelimit */
 
-#ifdef WITH_CONTENT_SCAN
-  (1<<ACL_WHERE_AUTH)|                             /* regex */
-    (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
-    (1<<ACL_WHERE_RCPT)|(1<<ACL_WHERE_PREDATA)|
-    (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
-    (1<<ACL_WHERE_MAILAUTH)|(1<<ACL_WHERE_QUIT)|
-    (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
-    (1<<ACL_WHERE_VRFY)|(1<<ACL_WHERE_MIME),
-#endif
+  (unsigned int)
+  ~(1<<ACL_WHERE_RCPT),                            /* recipients */
+
+  #ifdef WITH_CONTENT_SCAN
+  (unsigned int)
+  ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)|    /* regex */
+    (1<<ACL_WHERE_MIME)),
+  #endif
 
   (1<<ACL_WHERE_AUTH)|(1<<ACL_WHERE_CONNECT)|      /* sender_domains */
     (1<<ACL_WHERE_HELO)|
@@ -342,23 +475,28 @@ static unsigned int cond_forbids[] = {
 
   0,                                               /* set */
 
-#ifdef WITH_CONTENT_SCAN
-  (1<<ACL_WHERE_AUTH)|                             /* spam */
-    (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
-    (1<<ACL_WHERE_RCPT)|(1<<ACL_WHERE_PREDATA)|
-    (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
-    (1<<ACL_WHERE_MAILAUTH)|(1<<ACL_WHERE_QUIT)|
-    (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
-    (1<<ACL_WHERE_VRFY)|(1<<ACL_WHERE_MIME),
-#endif
+  #ifdef WITH_CONTENT_SCAN
+  (unsigned int)
+  ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)),   /* spam */
+  #endif
 
-#ifdef EXPERIMENTAL_SPF
+  #ifdef EXPERIMENTAL_SPF
   (1<<ACL_WHERE_AUTH)|(1<<ACL_WHERE_CONNECT)|      /* spf */
     (1<<ACL_WHERE_HELO)|
     (1<<ACL_WHERE_MAILAUTH)|
     (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
-    (1<<ACL_WHERE_STARTTLS)|(1<<ACL_WHERE_VRFY),
-#endif
+    (1<<ACL_WHERE_STARTTLS)|(1<<ACL_WHERE_VRFY)|
+    (1<<ACL_WHERE_NOTSMTP)|
+    (1<<ACL_WHERE_NOTSMTP_START),
+
+  (1<<ACL_WHERE_AUTH)|(1<<ACL_WHERE_CONNECT)|      /* spf_guess */
+    (1<<ACL_WHERE_HELO)|
+    (1<<ACL_WHERE_MAILAUTH)|
+    (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
+    (1<<ACL_WHERE_STARTTLS)|(1<<ACL_WHERE_VRFY)|
+    (1<<ACL_WHERE_NOTSMTP)|
+    (1<<ACL_WHERE_NOTSMTP_START),
+  #endif
 
   /* Certain types of verify are always allowed, so we let it through
   always and check in the verify function itself */
@@ -367,66 +505,86 @@ static unsigned int cond_forbids[] = {
 };
 
 
-/* Return values from decode_control() */
-
-enum { 
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-  CONTROL_BMI_RUN,
-#endif  
-  CONTROL_ERROR, CONTROL_CASEFUL_LOCAL_PART, CONTROL_CASELOWER_LOCAL_PART,
-  CONTROL_ENFORCE_SYNC, CONTROL_NO_ENFORCE_SYNC, CONTROL_FREEZE,
-  CONTROL_QUEUE_ONLY, CONTROL_SUBMISSION,
-#ifdef WITH_CONTENT_SCAN
-  CONTROL_NO_MBOX_UNSPOOL, 
-#endif
-  CONTROL_FAKEREJECT, CONTROL_NO_MULTILINE };
-
 /* Bit map vector of which controls are not allowed at certain times. For
 each control, there's a bitmap of dis-allowed times. For some, it is easier to
 specify the negation of a small number of allowed times. */
 
 static unsigned int control_forbids[] = {
-#ifdef EXPERIMENTAL_BRIGHTMAIL
+  (unsigned int)
+  ~((1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)),   /* allow_auth_unadvertised */
+
+  #ifdef EXPERIMENTAL_BRIGHTMAIL
   0,                                               /* bmi_run */
-#endif
+  #endif
+
+  0,                                               /* debug */
+
+  #ifndef DISABLE_DKIM
+  (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)|      /* dkim_disable_verify */
+    (1<<ACL_WHERE_NOTSMTP_START),
+  #endif
 
   0,                                               /* error */
-  
-  (unsigned int) 
+
+  (unsigned int)
   ~(1<<ACL_WHERE_RCPT),                            /* caseful_local_part */
-   
-  (unsigned int) 
+
+  (unsigned int)
   ~(1<<ACL_WHERE_RCPT),                            /* caselower_local_part */
-   
-  (1<<ACL_WHERE_NOTSMTP),                          /* enforce_sync */
-   
-  (1<<ACL_WHERE_NOTSMTP),                          /* no_enforce_sync */
-   
-  (unsigned int) 
+
+  (1<<ACL_WHERE_NOTSMTP)|                          /* enforce_sync */
+    (1<<ACL_WHERE_NOTSMTP_START),
+
+  (1<<ACL_WHERE_NOTSMTP)|                          /* no_enforce_sync */
+    (1<<ACL_WHERE_NOTSMTP_START),
+
+  (unsigned int)
   ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)|       /* freeze */
     (1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|
-    (1<<ACL_WHERE_NOTSMTP)),
-     
-  (unsigned int) 
+    (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_MIME)),
+
+  (unsigned int)
   ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)|       /* queue_only */
     (1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|
-    (1<<ACL_WHERE_NOTSMTP)),
-     
-  (unsigned int) 
+    (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_MIME)),
+
+  (unsigned int)
   ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)|       /* submission */
-    (1<<ACL_WHERE_PREDATA)),                       
+    (1<<ACL_WHERE_PREDATA)),
 
-#ifdef WITH_CONTENT_SCAN
-  (unsigned int) 
+  (unsigned int)
+  ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)|       /* suppress_local_fixups */
+    (1<<ACL_WHERE_PREDATA)|
+    (1<<ACL_WHERE_NOTSMTP_START)),
+
+  #ifdef WITH_CONTENT_SCAN
+  (unsigned int)
   ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)|       /* no_mbox_unspool */
-    (1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)),
-#endif
+    (1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|
+    (1<<ACL_WHERE_MIME)),
+  #endif
+
+  (unsigned int)
+  ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)|       /* fakedefer */
+    (1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|
+    (1<<ACL_WHERE_MIME)),
 
-  (unsigned int) 
+  (unsigned int)
   ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)|       /* fakereject */
-    (1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)),
+    (1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|
+    (1<<ACL_WHERE_MIME)),
 
-  (1<<ACL_WHERE_NOTSMTP)                           /* no_multiline */
+  (1<<ACL_WHERE_NOTSMTP)|                          /* no_multiline */
+    (1<<ACL_WHERE_NOTSMTP_START),
+
+  (1<<ACL_WHERE_NOTSMTP)|                          /* no_pipelining */
+    (1<<ACL_WHERE_NOTSMTP_START),
+
+  (1<<ACL_WHERE_NOTSMTP)|                          /* no_delay_flush */
+    (1<<ACL_WHERE_NOTSMTP_START),
+
+  (1<<ACL_WHERE_NOTSMTP)|                          /* no_callout_flush */
+    (1<<ACL_WHERE_NOTSMTP_START)
 };
 
 /* Structure listing various control arguments, with their characteristics. */
@@ -438,23 +596,91 @@ typedef struct control_def {
 } control_def;
 
 static control_def controls_list[] = {
+  { US"allow_auth_unadvertised", CONTROL_AUTH_UNADVERTISED, FALSE },
 #ifdef EXPERIMENTAL_BRIGHTMAIL
-  { US"bmi_run",                CONTROL_BMI_RUN, FALSE},
+  { US"bmi_run",                 CONTROL_BMI_RUN, FALSE },
+#endif
+  { US"debug",                   CONTROL_DEBUG, TRUE },
+#ifndef DISABLE_DKIM
+  { US"dkim_disable_verify",     CONTROL_DKIM_VERIFY, FALSE },
 #endif
-  { US"caseful_local_part",     CONTROL_CASEFUL_LOCAL_PART, FALSE},
-  { US"caselower_local_part",   CONTROL_CASELOWER_LOCAL_PART, FALSE},
-  { US"enforce_sync",           CONTROL_ENFORCE_SYNC, FALSE},
-  { US"freeze",                 CONTROL_FREEZE, FALSE},
-  { US"no_enforce_sync",        CONTROL_NO_ENFORCE_SYNC, FALSE},
-  { US"no_multiline_responses", CONTROL_NO_MULTILINE, FALSE},
-  { US"queue_only",             CONTROL_QUEUE_ONLY, FALSE},
+  { US"caseful_local_part",      CONTROL_CASEFUL_LOCAL_PART, FALSE },
+  { US"caselower_local_part",    CONTROL_CASELOWER_LOCAL_PART, FALSE },
+  { US"enforce_sync",            CONTROL_ENFORCE_SYNC, FALSE },
+  { US"freeze",                  CONTROL_FREEZE, TRUE },
+  { US"no_callout_flush",        CONTROL_NO_CALLOUT_FLUSH, FALSE },
+  { US"no_delay_flush",          CONTROL_NO_DELAY_FLUSH, FALSE },
+  { US"no_enforce_sync",         CONTROL_NO_ENFORCE_SYNC, FALSE },
+  { US"no_multiline_responses",  CONTROL_NO_MULTILINE, FALSE },
+  { US"no_pipelining",           CONTROL_NO_PIPELINING, FALSE },
+  { US"queue_only",              CONTROL_QUEUE_ONLY, FALSE },
 #ifdef WITH_CONTENT_SCAN
-  { US"no_mbox_unspool",        CONTROL_NO_MBOX_UNSPOOL, FALSE},
+  { US"no_mbox_unspool",         CONTROL_NO_MBOX_UNSPOOL, FALSE },
 #endif
-  { US"fakereject",             CONTROL_FAKEREJECT, TRUE},
-  { US"submission",             CONTROL_SUBMISSION, TRUE}
+  { US"fakedefer",               CONTROL_FAKEDEFER, TRUE },
+  { US"fakereject",              CONTROL_FAKEREJECT, TRUE },
+  { US"submission",              CONTROL_SUBMISSION, TRUE },
+  { US"suppress_local_fixups",   CONTROL_SUPPRESS_LOCAL_FIXUPS, FALSE }
   };
 
+/* Support data structures for Client SMTP Authorization. acl_verify_csa()
+caches its result in a tree to avoid repeated DNS queries. The result is an
+integer code which is used as an index into the following tables of
+explanatory strings and verification return codes. */
+
+static tree_node *csa_cache = NULL;
+
+enum { CSA_UNKNOWN, CSA_OK, CSA_DEFER_SRV, CSA_DEFER_ADDR,
+ CSA_FAIL_EXPLICIT, CSA_FAIL_DOMAIN, CSA_FAIL_NOADDR, CSA_FAIL_MISMATCH };
+
+/* The acl_verify_csa() return code is translated into an acl_verify() return
+code using the following table. It is OK unless the client is definitely not
+authorized. This is because CSA is supposed to be optional for sending sites,
+so recipients should not be too strict about checking it - especially because
+DNS problems are quite likely to occur. It's possible to use $csa_status in
+further ACL conditions to distinguish ok, unknown, and defer if required, but
+the aim is to make the usual configuration simple. */
+
+static int csa_return_code[] = {
+  OK, OK, OK, OK,
+  FAIL, FAIL, FAIL, FAIL
+};
+
+static uschar *csa_status_string[] = {
+  US"unknown", US"ok", US"defer", US"defer",
+  US"fail", US"fail", US"fail", US"fail"
+};
+
+static uschar *csa_reason_string[] = {
+  US"unknown",
+  US"ok",
+  US"deferred (SRV lookup failed)",
+  US"deferred (target address lookup failed)",
+  US"failed (explicit authorization required)",
+  US"failed (host name not authorized)",
+  US"failed (no authorized addresses)",
+  US"failed (client address mismatch)"
+};
+
+/* Options for the ratelimit condition. Note that there are two variants of
+the per_rcpt option, depending on the ACL that is used to measure the rate.
+However any ACL must be able to look up per_rcpt rates in /noupdate mode,
+so the two variants must have the same internal representation as well as
+the same configuration string. */
+
+enum {
+  RATE_PER_WHAT, RATE_PER_CLASH, RATE_PER_ADDR, RATE_PER_BYTE, RATE_PER_CMD,
+  RATE_PER_CONN, RATE_PER_MAIL, RATE_PER_RCPT, RATE_PER_ALLRCPTS
+};
+
+#define RATE_SET(var,new) \
+  (((var) == RATE_PER_WHAT) ? ((var) = RATE_##new) : ((var) = RATE_PER_CLASH))
+
+static uschar *ratelimit_option_string[] = {
+  US"?", US"!", US"per_addr", US"per_byte", US"per_cmd",
+  US"per_conn", US"per_mail", US"per_rcpt", US"per_rcpt"
+};
+
 /* Enable recursion between acl_check_internal() and acl_check_condition() */
 
 static int acl_check_internal(int, address_item *, uschar *, int, uschar **,
@@ -539,18 +765,11 @@ while ((s = (*func)()) != NULL)
     s++;
     }
 
-  /* Read the name of a verb or a condition, or the start of a new ACL */
+  /* Read the name of a verb or a condition, or the start of a new ACL, which
+  can be started by a name, or by a macro definition. */
 
   s = readconf_readname(name, sizeof(name), s);
-  if (*s == ':')
-    {
-    if (negated || name[0] == 0)
-      {
-      *error = string_sprintf("malformed ACL name in \"%s\"", saveline);
-      return NULL;
-      }
-    break;
-    }
+  if (*s == ':' || (isupper(name[0]) && *s == '=')) return yield;
 
   /* If a verb is unrecognized, it may be another condition or modifier that
   continues the previous verb. */
@@ -560,7 +779,8 @@ while ((s = (*func)()) != NULL)
     {
     if (this == NULL)
       {
-      *error = string_sprintf("unknown ACL verb in \"%s\"", saveline);
+      *error = string_sprintf("unknown ACL verb \"%s\" in \"%s\"", name,
+        saveline);
       return NULL;
       }
     }
@@ -630,21 +850,48 @@ while ((s = (*func)()) != NULL)
 
   /* The "set" modifier is different in that its argument is "name=value"
   rather than just a value, and we can check the validity of the name, which
-  gives us a variable number to insert into the data block. */
+  gives us a variable name to insert into the data block. The original ACL
+  variable names were acl_c0 ... acl_c9 and acl_m0 ... acl_m9. This was
+  extended to 20 of each type, but after that people successfully argued for
+  arbitrary names. In the new scheme, the names must start with acl_c or acl_m.
+  After that, we allow alphanumerics and underscores, but the first character
+  after c or m must be a digit or an underscore. This retains backwards
+  compatibility. */
 
   if (c == ACLC_SET)
     {
-    if (Ustrncmp(s, "acl_", 4) != 0 || (s[4] != 'c' && s[4] != 'm') ||
-        !isdigit(s[5]) || (!isspace(s[6]) && s[6] != '='))
+    uschar *endptr;
+
+    if (Ustrncmp(s, "acl_c", 5) != 0 &&
+        Ustrncmp(s, "acl_m", 5) != 0)
+      {
+      *error = string_sprintf("invalid variable name after \"set\" in ACL "
+        "modifier \"set %s\" (must start \"acl_c\" or \"acl_m\")", s);
+      return NULL;
+      }
+
+    endptr = s + 5;
+    if (!isdigit(*endptr) && *endptr != '_')
       {
-      *error = string_sprintf("unrecognized name after \"set\" in ACL "
-        "modifier \"set %s\"", s);
+      *error = string_sprintf("invalid variable name after \"set\" in ACL "
+        "modifier \"set %s\" (digit or underscore must follow acl_c or acl_m)",
+        s);
       return NULL;
       }
 
-    cond->u.varnumber = s[5] - '0';
-    if (s[4] == 'm') cond->u.varnumber += ACL_C_MAX;
-    s += 6;
+    while (*endptr != 0 && *endptr != '=' && !isspace(*endptr))
+      {
+      if (!isalnum(*endptr) && *endptr != '_')
+        {
+        *error = string_sprintf("invalid character \"%c\" in variable name "
+          "in ACL modifier \"set %s\"", *endptr, s);
+        return NULL;
+        }
+      endptr++;
+      }
+
+    cond->u.varname = string_copyn(s + 4, endptr - s - 4);
+    s = endptr;
     while (isspace(*s)) s++;
     }
 
@@ -669,6 +916,115 @@ return yield;
 
 
 
+/*************************************************
+*         Set up added header line(s)            *
+*************************************************/
+
+/* This function is called by the add_header modifier, and also from acl_warn()
+to implement the now-deprecated way of adding header lines using "message" on a
+"warn" verb. The argument is treated as a sequence of header lines which are
+added to a chain, provided there isn't an identical one already there.
+
+Argument:   string of header lines
+Returns:    nothing
+*/
+
+static void
+setup_header(uschar *hstring)
+{
+uschar *p, *q;
+int hlen = Ustrlen(hstring);
+
+/* An empty string does nothing; otherwise add a final newline if necessary. */
+
+if (hlen <= 0) return;
+if (hstring[hlen-1] != '\n') hstring = string_sprintf("%s\n", hstring);
+
+/* Loop for multiple header lines, taking care about continuations */
+
+for (p = q = hstring; *p != 0; )
+  {
+  uschar *s;
+  int newtype = htype_add_bot;
+  header_line **hptr = &acl_added_headers;
+
+  /* Find next header line within the string */
+
+  for (;;)
+    {
+    q = Ustrchr(q, '\n');
+    if (*(++q) != ' ' && *q != '\t') break;
+    }
+
+  /* If the line starts with a colon, interpret the instruction for where to
+  add it. This temporarily sets up a new type. */
+
+  if (*p == ':')
+    {
+    if (strncmpic(p, US":after_received:", 16) == 0)
+      {
+      newtype = htype_add_rec;
+      p += 16;
+      }
+    else if (strncmpic(p, US":at_start_rfc:", 14) == 0)
+      {
+      newtype = htype_add_rfc;
+      p += 14;
+      }
+    else if (strncmpic(p, US":at_start:", 10) == 0)
+      {
+      newtype = htype_add_top;
+      p += 10;
+      }
+    else if (strncmpic(p, US":at_end:", 8) == 0)
+      {
+      newtype = htype_add_bot;
+      p += 8;
+      }
+    while (*p == ' ' || *p == '\t') p++;
+    }
+
+  /* See if this line starts with a header name, and if not, add X-ACL-Warn:
+  to the front of it. */
+
+  for (s = p; s < q - 1; s++)
+    {
+    if (*s == ':' || !isgraph(*s)) break;
+    }
+
+  s = string_sprintf("%s%.*s", (*s == ':')? "" : "X-ACL-Warn: ", q - p, p);
+  hlen = Ustrlen(s);
+
+  /* See if this line has already been added */
+
+  while (*hptr != NULL)
+    {
+    if (Ustrncmp((*hptr)->text, s, hlen) == 0) break;
+    hptr = &((*hptr)->next);
+    }
+
+  /* Add if not previously present */
+
+  if (*hptr == NULL)
+    {
+    header_line *h = store_get(sizeof(header_line));
+    h->text = s;
+    h->next = NULL;
+    h->type = newtype;
+    h->slen = hlen;
+    *hptr = h;
+    hptr = &(h->next);
+    }
+
+  /* Advance for next header line within the string */
+
+  p = q;
+  }
+}
+
+
+
+
 /*************************************************
 *               Handle warnings                  *
 *************************************************/
@@ -677,6 +1033,9 @@ return yield;
 the message's headers, and/or writes information to the log. In each case, this
 only happens once (per message for headers, per connection for log).
 
+** NOTE: The header adding action using the "message" setting is historic, and
+its use is now deprecated. The new add_header modifier should be used instead.
+
 Arguments:
   where          ACL_WHERE_xxxx indicating which ACL this is
   user_message   message for adding to headers
@@ -688,8 +1047,6 @@ Returns:         nothing
 static void
 acl_warn(int where, uschar *user_message, uschar *log_message)
 {
-int hlen;
-
 if (log_message != NULL && log_message != user_message)
   {
   uschar *text;
@@ -706,8 +1063,8 @@ if (log_message != NULL && log_message != user_message)
       strcmpic(log_message, US"sender verify failed") == 0)
     text = string_sprintf("%s: %s", text, sender_verified_failed->message);
 
-  /* Search previously logged warnings. They are kept in malloc store so they
-  can be freed at the start of a new message. */
+  /* Search previously logged warnings. They are kept in malloc
+  store so they can be freed at the start of a new message. */
 
   for (logged = acl_warn_logged; logged != NULL; logged = logged->next)
     if (Ustrcmp(logged->text, text) == 0) break;
@@ -739,99 +1096,10 @@ if (where > ACL_WHERE_NOTSMTP)
   return;
   }
 
-/* Treat the user message as a sequence of one or more header lines. */
-
-hlen = Ustrlen(user_message);
-if (hlen > 0)
-  {
-  uschar *text, *p, *q;
-
-  /* Add a final newline if not present */
-
-  text = ((user_message)[hlen-1] == '\n')? user_message :
-    string_sprintf("%s\n", user_message);
-
-  /* Loop for multiple header lines, taking care about continuations */
-
-  for (p = q = text; *p != 0; )
-    {
-    uschar *s;
-    int newtype = htype_add_bot;
-    header_line **hptr = &acl_warn_headers;
-
-    /* Find next header line within the string */
-
-    for (;;)
-      {
-      q = Ustrchr(q, '\n');
-      if (*(++q) != ' ' && *q != '\t') break;
-      }
-
-    /* If the line starts with a colon, interpret the instruction for where to
-    add it. This temporarily sets up a new type. */
-
-    if (*p == ':')
-      {
-      if (strncmpic(p, US":after_received:", 16) == 0)
-        {
-        newtype = htype_add_rec;
-        p += 16;
-        }
-      else if (strncmpic(p, US":at_start_rfc:", 14) == 0)
-        {
-        newtype = htype_add_rfc;
-        p += 14;
-        }        
-      else if (strncmpic(p, US":at_start:", 10) == 0)
-        {
-        newtype = htype_add_top;
-        p += 10;
-        }
-      else if (strncmpic(p, US":at_end:", 8) == 0)
-        {
-        newtype = htype_add_bot;
-        p += 8;
-        }
-      while (*p == ' ' || *p == '\t') p++;
-      }
-
-    /* See if this line starts with a header name, and if not, add X-ACL-Warn:
-    to the front of it. */
-
-    for (s = p; s < q - 1; s++)
-      {
-      if (*s == ':' || !isgraph(*s)) break;
-      }
-
-    s = string_sprintf("%s%.*s", (*s == ':')? "" : "X-ACL-Warn: ", q - p, p);
-    hlen = Ustrlen(s);
-
-    /* See if this line has already been added */
-
-    while (*hptr != NULL)
-      {
-      if (Ustrncmp((*hptr)->text, s, hlen) == 0) break;
-      hptr = &((*hptr)->next);
-      }
-
-    /* Add if not previously present */
-
-    if (*hptr == NULL)
-      {
-      header_line *h = store_get(sizeof(header_line));
-      h->text = s;
-      h->next = NULL;
-      h->type = newtype;
-      h->slen = hlen;
-      *hptr = h;
-      hptr = &(h->next);
-      }
-
-    /* Advance for next header line within the string */
+/* The code for setting up header lines is now abstracted into a separate
+function so that it can be used for the add_header modifier as well. */
 
-    p = q;
-    }
-  }
+setup_header(user_message);
 }
 
 
@@ -895,12 +1163,310 @@ return OK;
 
 
 /*************************************************
-*     Handle verification (address & other)      *
+*   Check client IP address matches CSA target   *
 *************************************************/
 
-/* This function implements the "verify" condition. It is called when
-encountered in any ACL, because some tests are almost always permitted. Some
-just don't make sense, and always fail (for example, an attempt to test a host
+/* Called from acl_verify_csa() below. This routine scans a section of a DNS
+response for address records belonging to the CSA target hostname. The section
+is specified by the reset argument, either RESET_ADDITIONAL or RESET_ANSWERS.
+If one of the addresses matches the client's IP address, then the client is
+authorized by CSA. If there are target IP addresses but none of them match
+then the client is using an unauthorized IP address. If there are no target IP
+addresses then the client cannot be using an authorized IP address. (This is
+an odd configuration - why didn't the SRV record have a weight of 1 instead?)
+
+Arguments:
+  dnsa       the DNS answer block
+  dnss       a DNS scan block for us to use
+  reset      option specifing what portion to scan, as described above
+  target     the target hostname to use for matching RR names
+
+Returns:     CSA_OK             successfully authorized
+             CSA_FAIL_MISMATCH  addresses found but none matched
+             CSA_FAIL_NOADDR    no target addresses found
+*/
+
+static int
+acl_verify_csa_address(dns_answer *dnsa, dns_scan *dnss, int reset,
+                       uschar *target)
+{
+dns_record *rr;
+dns_address *da;
+
+BOOL target_found = FALSE;
+
+for (rr = dns_next_rr(dnsa, dnss, reset);
+     rr != NULL;
+     rr = dns_next_rr(dnsa, dnss, RESET_NEXT))
+  {
+  /* Check this is an address RR for the target hostname. */
+
+  if (rr->type != T_A
+    #if HAVE_IPV6
+      && rr->type != T_AAAA
+      #ifdef SUPPORT_A6
+        && rr->type != T_A6
+      #endif
+    #endif
+  ) continue;
+
+  if (strcmpic(target, rr->name) != 0) continue;
+
+  target_found = TRUE;
+
+  /* Turn the target address RR into a list of textual IP addresses and scan
+  the list. There may be more than one if it is an A6 RR. */
+
+  for (da = dns_address_from_rr(dnsa, rr); da != NULL; da = da->next)
+    {
+    /* If the client IP address matches the target IP address, it's good! */
+
+    DEBUG(D_acl) debug_printf("CSA target address is %s\n", da->address);
+
+    if (strcmpic(sender_host_address, da->address) == 0) return CSA_OK;
+    }
+  }
+
+/* If we found some target addresses but none of them matched, the client is
+using an unauthorized IP address, otherwise the target has no authorized IP
+addresses. */
+
+if (target_found) return CSA_FAIL_MISMATCH;
+else return CSA_FAIL_NOADDR;
+}
+
+
+
+/*************************************************
+*       Verify Client SMTP Authorization         *
+*************************************************/
+
+/* Called from acl_verify() below. This routine calls dns_lookup_special()
+to find the CSA SRV record corresponding to the domain argument, or
+$sender_helo_name if no argument is provided. It then checks that the
+client is authorized, and that its IP address corresponds to the SRV
+target's address by calling acl_verify_csa_address() above. The address
+should have been returned in the DNS response's ADDITIONAL section, but if
+not we perform another DNS lookup to get it.
+
+Arguments:
+  domain    pointer to optional parameter following verify = csa
+
+Returns:    CSA_UNKNOWN    no valid CSA record found
+            CSA_OK         successfully authorized
+            CSA_FAIL_*     client is definitely not authorized
+            CSA_DEFER_*    there was a DNS problem
+*/
+
+static int
+acl_verify_csa(uschar *domain)
+{
+tree_node *t;
+uschar *found, *p;
+int priority, weight, port;
+dns_answer dnsa;
+dns_scan dnss;
+dns_record *rr;
+int rc, type;
+uschar target[256];
+
+/* Work out the domain we are using for the CSA lookup. The default is the
+client's HELO domain. If the client has not said HELO, use its IP address
+instead. If it's a local client (exim -bs), CSA isn't applicable. */
+
+while (isspace(*domain) && *domain != '\0') ++domain;
+if (*domain == '\0') domain = sender_helo_name;
+if (domain == NULL) domain = sender_host_address;
+if (sender_host_address == NULL) return CSA_UNKNOWN;
+
+/* If we have an address literal, strip off the framing ready for turning it
+into a domain. The framing consists of matched square brackets possibly
+containing a keyword and a colon before the actual IP address. */
+
+if (domain[0] == '[')
+  {
+  uschar *start = Ustrchr(domain, ':');
+  if (start == NULL) start = domain;
+  domain = string_copyn(start + 1, Ustrlen(start) - 2);
+  }
+
+/* Turn domains that look like bare IP addresses into domains in the reverse
+DNS. This code also deals with address literals and $sender_host_address. It's
+not quite kosher to treat bare domains such as EHLO 192.0.2.57 the same as
+address literals, but it's probably the most friendly thing to do. This is an
+extension to CSA, so we allow it to be turned off for proper conformance. */
+
+if (string_is_ip_address(domain, NULL) != 0)
+  {
+  if (!dns_csa_use_reverse) return CSA_UNKNOWN;
+  dns_build_reverse(domain, target);
+  domain = target;
+  }
+
+/* Find out if we've already done the CSA check for this domain. If we have,
+return the same result again. Otherwise build a new cached result structure
+for this domain. The name is filled in now, and the value is filled in when
+we return from this function. */
+
+t = tree_search(csa_cache, domain);
+if (t != NULL) return t->data.val;
+
+t = store_get_perm(sizeof(tree_node) + Ustrlen(domain));
+Ustrcpy(t->name, domain);
+(void)tree_insertnode(&csa_cache, t);
+
+/* Now we are ready to do the actual DNS lookup(s). */
+
+found = domain;
+switch (dns_special_lookup(&dnsa, domain, T_CSA, &found))
+  {
+  /* If something bad happened (most commonly DNS_AGAIN), defer. */
+
+  default:
+  return t->data.val = CSA_DEFER_SRV;
+
+  /* If we found nothing, the client's authorization is unknown. */
+
+  case DNS_NOMATCH:
+  case DNS_NODATA:
+  return t->data.val = CSA_UNKNOWN;
+
+  /* We got something! Go on to look at the reply in more detail. */
+
+  case DNS_SUCCEED:
+  break;
+  }
+
+/* Scan the reply for well-formed CSA SRV records. */
+
+for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS);
+     rr != NULL;
+     rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT))
+  {
+  if (rr->type != T_SRV) continue;
+
+  /* Extract the numerical SRV fields (p is incremented) */
+
+  p = rr->data;
+  GETSHORT(priority, p);
+  GETSHORT(weight, p);
+  GETSHORT(port, p);
+
+  DEBUG(D_acl)
+    debug_printf("CSA priority=%d weight=%d port=%d\n", priority, weight, port);
+
+  /* Check the CSA version number */
+
+  if (priority != 1) continue;
+
+  /* If the domain does not have a CSA SRV record of its own (i.e. the domain
+  found by dns_special_lookup() is a parent of the one we asked for), we check
+  the subdomain assertions in the port field. At the moment there's only one
+  assertion: legitimate SMTP clients are all explicitly authorized with CSA
+  SRV records of their own. */
+
+  if (found != domain)
+    {
+    if (port & 1)
+      return t->data.val = CSA_FAIL_EXPLICIT;
+    else
+      return t->data.val = CSA_UNKNOWN;
+    }
+
+  /* This CSA SRV record refers directly to our domain, so we check the value
+  in the weight field to work out the domain's authorization. 0 and 1 are
+  unauthorized; 3 means the client is authorized but we can't check the IP
+  address in order to authenticate it, so we treat it as unknown; values
+  greater than 3 are undefined. */
+
+  if (weight < 2) return t->data.val = CSA_FAIL_DOMAIN;
+
+  if (weight > 2) continue;
+
+  /* Weight == 2, which means the domain is authorized. We must check that the
+  client's IP address is listed as one of the SRV target addresses. Save the
+  target hostname then break to scan the additional data for its addresses. */
+
+  (void)dn_expand(dnsa.answer, dnsa.answer + dnsa.answerlen, p,
+    (DN_EXPAND_ARG4_TYPE)target, sizeof(target));
+
+  DEBUG(D_acl) debug_printf("CSA target is %s\n", target);
+
+  break;
+  }
+
+/* If we didn't break the loop then no appropriate records were found. */
+
+if (rr == NULL) return t->data.val = CSA_UNKNOWN;
+
+/* Do not check addresses if the target is ".", in accordance with RFC 2782.
+A target of "." indicates there are no valid addresses, so the client cannot
+be authorized. (This is an odd configuration because weight=2 target=. is
+equivalent to weight=1, but we check for it in order to keep load off the
+root name servers.) Note that dn_expand() turns "." into "". */
+
+if (Ustrcmp(target, "") == 0) return t->data.val = CSA_FAIL_NOADDR;
+
+/* Scan the additional section of the CSA SRV reply for addresses belonging
+to the target. If the name server didn't return any additional data (e.g.
+because it does not fully support SRV records), we need to do another lookup
+to obtain the target addresses; otherwise we have a definitive result. */
+
+rc = acl_verify_csa_address(&dnsa, &dnss, RESET_ADDITIONAL, target);
+if (rc != CSA_FAIL_NOADDR) return t->data.val = rc;
+
+/* The DNS lookup type corresponds to the IP version used by the client. */
+
+#if HAVE_IPV6
+if (Ustrchr(sender_host_address, ':') != NULL)
+  type = T_AAAA;
+else
+#endif /* HAVE_IPV6 */
+  type = T_A;
+
+
+#if HAVE_IPV6 && defined(SUPPORT_A6)
+DNS_LOOKUP_AGAIN:
+#endif
+
+switch (dns_lookup(&dnsa, target, type, NULL))
+  {
+  /* If something bad happened (most commonly DNS_AGAIN), defer. */
+
+  default:
+  return t->data.val = CSA_DEFER_ADDR;
+
+  /* If the query succeeded, scan the addresses and return the result. */
+
+  case DNS_SUCCEED:
+  rc = acl_verify_csa_address(&dnsa, &dnss, RESET_ANSWERS, target);
+  if (rc != CSA_FAIL_NOADDR) return t->data.val = rc;
+  /* else fall through */
+
+  /* If the target has no IP addresses, the client cannot have an authorized
+  IP address. However, if the target site uses A6 records (not AAAA records)
+  we have to do yet another lookup in order to check them. */
+
+  case DNS_NOMATCH:
+  case DNS_NODATA:
+
+  #if HAVE_IPV6 && defined(SUPPORT_A6)
+  if (type == T_AAAA) { type = T_A6; goto DNS_LOOKUP_AGAIN; }
+  #endif
+
+  return t->data.val = CSA_FAIL_NOADDR;
+  }
+}
+
+
+
+/*************************************************
+*     Handle verification (address & other)      *
+*************************************************/
+
+/* This function implements the "verify" condition. It is called when
+encountered in any ACL, because some tests are almost always permitted. Some
+just don't make sense, and always fail (for example, an attempt to test a host
 lookup for a non-TCP/IP message). Others are restricted to certain ACLs.
 
 Arguments:
@@ -931,10 +1497,18 @@ BOOL verify_header_sender = FALSE;
 BOOL defer_ok = FALSE;
 BOOL callout_defer_ok = FALSE;
 BOOL no_details = FALSE;
+BOOL success_on_redirect = FALSE;
 address_item *sender_vaddr = NULL;
 uschar *verify_sender_address = NULL;
 uschar *pm_mailfrom = NULL;
 uschar *se_mailfrom = NULL;
+
+/* Some of the verify items have slash-separated options; some do not. Diagnose
+an error if options are given for items that don't expect them. This code has
+now got very message. Refactoring to use a table would be a good idea one day.
+*/
+
+uschar *slash = Ustrchr(arg, '/');
 uschar *list = arg;
 uschar *ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size);
 
@@ -944,6 +1518,7 @@ if (ss == NULL) goto BAD_VERIFY;
 
 if (strcmpic(ss, US"reverse_host_lookup") == 0)
   {
+  if (slash != NULL) goto NO_OPTIONS;
   if (sender_host_address == NULL) return OK;
   return acl_verify_reverse(user_msgptr, log_msgptr);
   }
@@ -954,49 +1529,80 @@ mandatory verification, the connection doesn't last this long.) */
 
 if (strcmpic(ss, US"certificate") == 0)
   {
+  if (slash != NULL) goto NO_OPTIONS;
   if (tls_certificate_verified) return OK;
   *user_msgptr = US"no verified certificate";
   return FAIL;
   }
 
-/* We can test the result of optional HELO verification */
+/* We can test the result of optional HELO verification that might have
+occurred earlier. If not, we can attempt the verification now. */
 
-if (strcmpic(ss, US"helo") == 0) return helo_verified? OK : FAIL;
+if (strcmpic(ss, US"helo") == 0)
+  {
+  if (slash != NULL) goto NO_OPTIONS;
+  if (!helo_verified && !helo_verify_failed) smtp_verify_helo();
+  return helo_verified? OK : FAIL;
+  }
 
-/* Handle header verification options - permitted only after DATA or a non-SMTP
-message. */
+/* Do Client SMTP Authorization checks in a separate function, and turn the
+result code into user-friendly strings. */
 
-if (strncmpic(ss, US"header_", 7) == 0)
+if (strcmpic(ss, US"csa") == 0)
   {
-  if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP)
-    {
-    *log_msgptr = string_sprintf("cannot check header contents in ACL for %s "
-      "(only possible in ACL for DATA)", acl_wherenames[where]);
-    return ERROR;
-    }
+  rc = acl_verify_csa(list);
+  *log_msgptr = *user_msgptr = string_sprintf("client SMTP authorization %s",
+                                              csa_reason_string[rc]);
+  csa_status = csa_status_string[rc];
+  DEBUG(D_acl) debug_printf("CSA result %s\n", csa_status);
+  return csa_return_code[rc];
+  }
 
-  /* Check that all relevant header lines have the correct syntax. If there is
-  a syntax error, we return details of the error to the sender if configured to
-  send out full details. (But a "message" setting on the ACL can override, as
-  always). */
+/* Check that all relevant header lines have the correct syntax. If there is
+a syntax error, we return details of the error to the sender if configured to
+send out full details. (But a "message" setting on the ACL can override, as
+always). */
 
-  if (strcmpic(ss+7, US"syntax") == 0)
+if (strcmpic(ss, US"header_syntax") == 0)
+  {
+  if (slash != NULL) goto NO_OPTIONS;
+  if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP) goto WRONG_ACL;
+  rc = verify_check_headers(log_msgptr);
+  if (rc != OK && smtp_return_error_details && *log_msgptr != NULL)
+    *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr);
+  return rc;
+  }
+
+/* Check that no recipient of this message is "blind", that is, every envelope
+recipient must be mentioned in either To: or Cc:. */
+
+if (strcmpic(ss, US"not_blind") == 0)
+  {
+  if (slash != NULL) goto NO_OPTIONS;
+  if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP) goto WRONG_ACL;
+  rc = verify_check_notblind();
+  if (rc != OK)
     {
-    int rc = verify_check_headers(log_msgptr);
-    if (rc != OK && smtp_return_error_details && *log_msgptr != NULL)
+    *log_msgptr = string_sprintf("bcc recipient detected");
+    if (smtp_return_error_details)
       *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr);
-    return rc;
     }
+  return rc;
+  }
 
-  /* Check that there is at least one verifiable sender address in the relevant
-  header lines. This can be followed by callout and defer options, just like
-  sender and recipient. */
+/* The remaining verification tests check recipient and sender addresses,
+either from the envelope or from the header. There are a number of
+slash-separated options that are common to all of them. */
 
-  else if (strcmpic(ss+7, US"sender") == 0) verify_header_sender = TRUE;
 
-  /* Unknown verify argument starting with "header_" */
+/* Check that there is at least one verifiable sender address in the relevant
+header lines. This can be followed by callout and defer options, just like
+sender and recipient. */
 
-  else goto BAD_VERIFY;
+if (strcmpic(ss, US"header_sender") == 0)
+  {
+  if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP) goto WRONG_ACL;
+  verify_header_sender = TRUE;
   }
 
 /* Otherwise, first item in verify argument must be "sender" or "recipient".
@@ -1034,13 +1640,15 @@ else
     }
   }
 
-/* Remaining items are optional */
+/* Remaining items are optional; they apply to sender and recipient
+verification, including "header sender" verification. */
 
 while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size))
       != NULL)
   {
   if (strcmpic(ss, US"defer_ok") == 0) defer_ok = TRUE;
   else if (strcmpic(ss, US"no_details") == 0) no_details = TRUE;
+  else if (strcmpic(ss, US"success_on_redirect") == 0) success_on_redirect = TRUE;
 
   /* These two old options are left for backwards compatibility */
 
@@ -1071,11 +1679,11 @@ while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size))
         uschar *opt;
         uschar buffer[256];
         while (isspace(*ss)) ss++;
-        
-        /* This callout option handling code has become a mess as new options 
-        have been added in an ad hoc manner. It should be tidied up into some 
+
+        /* This callout option handling code has become a mess as new options
+        have been added in an ad hoc manner. It should be tidied up into some
         kind of table-driven thing. */
+
         while ((opt = string_nextinlist(&ss, &optsep, buffer, sizeof(buffer)))
               != NULL)
           {
@@ -1089,6 +1697,11 @@ while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size))
           else if (strcmpic(opt, US"use_postmaster") == 0)
              verify_options |= vopt_callout_recippmaster;
           else if (strcmpic(opt, US"postmaster") == 0) pm_mailfrom = US"";
+          else if (strcmpic(opt, US"fullpostmaster") == 0)
+            {
+            pm_mailfrom = US"";
+            verify_options |= vopt_callout_fullpm;
+            }
 
           else if (strncmpic(opt, US"mailfrom", 8) == 0)
             {
@@ -1207,12 +1820,12 @@ message if giving out verification details. */
 
 if (verify_header_sender)
   {
-  int verrno; 
+  int verrno;
   rc = verify_check_header_address(user_msgptr, log_msgptr, callout,
     callout_overall, callout_connect, se_mailfrom, pm_mailfrom, verify_options,
     &verrno);
   if (rc != OK)
-    { 
+    {
     *basic_errno = verrno;
     if (smtp_return_error_details)
       {
@@ -1220,7 +1833,7 @@ if (verify_header_sender)
         *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr);
       if (rc == DEFER) acl_temp_details = TRUE;
       }
-    }   
+    }
   }
 
 /* Handle a sender address. The default is to verify *the* sender address, but
@@ -1279,7 +1892,7 @@ else if (verify_sender_address != NULL)
     {
     BOOL routed = TRUE;
     uschar *save_address_data = deliver_address_data;
-      
+
     sender_vaddr = deliver_make_addr(verify_sender_address, TRUE);
     if (no_details) setflag(sender_vaddr, af_sverify_told);
     if (verify_sender_address[0] != 0)
@@ -1293,6 +1906,9 @@ else if (verify_sender_address != NULL)
       else
         verify_options |= vopt_fake_sender;
 
+      if (success_on_redirect)
+        verify_options |= vopt_success_on_redirect;
+
       /* The recipient, qualify, and expn options are never set in
       verify_options. */
 
@@ -1325,16 +1941,16 @@ else if (verify_sender_address != NULL)
     sender_vaddr->special_action = rc;
     sender_vaddr->next = sender_verified_list;
     sender_verified_list = sender_vaddr;
-    
-    /* Restore the recipient address data, which might have been clobbered by 
+
+    /* Restore the recipient address data, which might have been clobbered by
     the sender verification. */
-  
+
     deliver_address_data = save_address_data;
     }
-    
+
   /* Put the sender address_data value into $sender_address_data */
 
-  sender_address_data = sender_vaddr->p.address_data; 
+  sender_address_data = sender_vaddr->p.address_data;
   }
 
 /* A recipient address just gets a straightforward verify; again we must handle
@@ -1344,6 +1960,9 @@ else
   {
   address_item addr2;
 
+  if (success_on_redirect)
+    verify_options |= vopt_success_on_redirect;
+
   /* We must use a copy of the address for verification, because it might
   get rewritten. */
 
@@ -1351,109 +1970,696 @@ else
   rc = verify_address(&addr2, NULL, verify_options|vopt_is_recipient, callout,
     callout_overall, callout_connect, se_mailfrom, pm_mailfrom, NULL);
   HDEBUG(D_acl) debug_printf("----------- end verify ------------\n");
-  
+
+  *basic_errno = addr2.basic_errno;
   *log_msgptr = addr2.message;
-  *user_msgptr = (addr2.user_message != NULL)? 
+  *user_msgptr = (addr2.user_message != NULL)?
     addr2.user_message : addr2.message;
-  *basic_errno = addr2.basic_errno;
 
-  /* Make $address_data visible */
-  deliver_address_data = addr2.p.address_data;
+  /* Allow details for temporary error if the address is so flagged. */
+  if (testflag((&addr2), af_pass_message)) acl_temp_details = TRUE;
+
+  /* Make $address_data visible */
+  deliver_address_data = addr2.p.address_data;
+  }
+
+/* We have a result from the relevant test. Handle defer overrides first. */
+
+if (rc == DEFER && (defer_ok ||
+   (callout_defer_ok && *basic_errno == ERRNO_CALLOUTDEFER)))
+  {
+  HDEBUG(D_acl) debug_printf("verify defer overridden by %s\n",
+    defer_ok? "defer_ok" : "callout_defer_ok");
+  rc = OK;
+  }
+
+/* If we've failed a sender, set up a recipient message, and point
+sender_verified_failed to the address item that actually failed. */
+
+if (rc != OK && verify_sender_address != NULL)
+  {
+  if (rc != DEFER)
+    {
+    *log_msgptr = *user_msgptr = US"Sender verify failed";
+    }
+  else if (*basic_errno != ERRNO_CALLOUTDEFER)
+    {
+    *log_msgptr = *user_msgptr = US"Could not complete sender verify";
+    }
+  else
+    {
+    *log_msgptr = US"Could not complete sender verify callout";
+    *user_msgptr = smtp_return_error_details? sender_vaddr->user_message :
+      *log_msgptr;
+    }
+
+  sender_verified_failed = sender_vaddr;
+  }
+
+/* Verifying an address messes up the values of $domain and $local_part,
+so reset them before returning if this is a RCPT ACL. */
+
+if (addr != NULL)
+  {
+  deliver_domain = addr->domain;
+  deliver_localpart = addr->local_part;
+  }
+return rc;
+
+/* Syntax errors in the verify argument come here. */
+
+BAD_VERIFY:
+*log_msgptr = string_sprintf("expected \"sender[=address]\", \"recipient\", "
+  "\"helo\", \"header_syntax\", \"header_sender\" or "
+  "\"reverse_host_lookup\" at start of ACL condition "
+  "\"verify %s\"", arg);
+return ERROR;
+
+/* Options supplied when not allowed come here */
+
+NO_OPTIONS:
+*log_msgptr = string_sprintf("unexpected '/' found in \"%s\" "
+  "(this verify item has no options)", arg);
+return ERROR;
+
+/* Calls in the wrong ACL come here */
+
+WRONG_ACL:
+*log_msgptr = string_sprintf("cannot check header contents in ACL for %s "
+  "(only possible in ACL for DATA)", acl_wherenames[where]);
+return ERROR;
+}
+
+
+
+
+/*************************************************
+*        Check argument for control= modifier    *
+*************************************************/
+
+/* Called from acl_check_condition() below
+
+Arguments:
+  arg         the argument string for control=
+  pptr        set to point to the terminating character
+  where       which ACL we are in
+  log_msgptr  for error messages
+
+Returns:      CONTROL_xxx value
+*/
+
+static int
+decode_control(uschar *arg, uschar **pptr, int where, uschar **log_msgptr)
+{
+int len;
+control_def *d;
+
+for (d = controls_list;
+     d < controls_list + sizeof(controls_list)/sizeof(control_def);
+     d++)
+  {
+  len = Ustrlen(d->name);
+  if (Ustrncmp(d->name, arg, len) == 0) break;
+  }
+
+if (d >= controls_list + sizeof(controls_list)/sizeof(control_def) ||
+   (arg[len] != 0 && (!d->has_option || arg[len] != '/')))
+  {
+  *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
+  return CONTROL_ERROR;
+  }
+
+*pptr = arg + len;
+return d->value;
+}
+
+
+
+
+/*************************************************
+*        Return a ratelimit error                *
+*************************************************/
+
+/* Called from acl_ratelimit() below
+
+Arguments:
+  log_msgptr  for error messages
+  format      format string
+  ...         supplementary arguments
+  ss          ratelimit option name
+  where       ACL_WHERE_xxxx indicating which ACL this is
+
+Returns:      ERROR
+*/
+
+static int
+ratelimit_error(uschar **log_msgptr, const char *format, ...)
+{
+va_list ap;
+uschar buffer[STRING_SPRINTF_BUFFER_SIZE];
+va_start(ap, format);
+if (!string_vformat(buffer, sizeof(buffer), format, ap))
+  log_write(0, LOG_MAIN|LOG_PANIC_DIE,
+    "string_sprintf expansion was longer than %d", sizeof(buffer));
+va_end(ap);
+*log_msgptr = string_sprintf(
+  "error in arguments to \"ratelimit\" condition: %s", buffer);
+return ERROR;
+}
+
+
+
+
+/*************************************************
+*            Handle rate limiting                *
+*************************************************/
+
+/* Called by acl_check_condition() below to calculate the result
+of the ACL ratelimit condition.
+
+Note that the return value might be slightly unexpected: if the
+sender's rate is above the limit then the result is OK. This is
+similar to the dnslists condition, and is so that you can write
+ACL clauses like: defer ratelimit = 15 / 1h
+
+Arguments:
+  arg         the option string for ratelimit=
+  where       ACL_WHERE_xxxx indicating which ACL this is
+  log_msgptr  for error messages
+
+Returns:       OK        - Sender's rate is above limit
+               FAIL      - Sender's rate is below limit
+               DEFER     - Problem opening ratelimit database
+               ERROR     - Syntax error in options.
+*/
+
+static int
+acl_ratelimit(uschar *arg, int where, uschar **log_msgptr)
+{
+double limit, period, count;
+uschar *ss;
+uschar *key = NULL;
+uschar *unique = NULL;
+int sep = '/';
+BOOL leaky = FALSE, strict = FALSE, readonly = FALSE;
+BOOL noupdate = FALSE, badacl = FALSE;
+int mode = RATE_PER_WHAT;
+int old_pool, rc;
+tree_node **anchor, *t;
+open_db dbblock, *dbm;
+int dbdb_size;
+dbdata_ratelimit *dbd;
+dbdata_ratelimit_unique *dbdb;
+struct timeval tv;
+
+/* Parse the first two options and record their values in expansion
+variables. These variables allow the configuration to have informative
+error messages based on rate limits obtained from a table lookup. */
+
+/* First is the maximum number of messages per period / maximum burst
+size, which must be greater than or equal to zero. Zero is useful for
+rate measurement as opposed to rate limiting. */
+
+sender_rate_limit = string_nextinlist(&arg, &sep, NULL, 0);
+if (sender_rate_limit == NULL)
+  limit = -1.0;
+else
+  {
+  limit = Ustrtod(sender_rate_limit, &ss);
+  if (tolower(*ss) == 'k') { limit *= 1024.0; ss++; }
+  else if (tolower(*ss) == 'm') { limit *= 1024.0*1024.0; ss++; }
+  else if (tolower(*ss) == 'g') { limit *= 1024.0*1024.0*1024.0; ss++; }
+  }
+if (limit < 0.0 || *ss != '\0')
+  return ratelimit_error(log_msgptr,
+    "\"%s\" is not a positive number", sender_rate_limit);
+
+/* Second is the rate measurement period / exponential smoothing time
+constant. This must be strictly greater than zero, because zero leads to
+run-time division errors. */
+
+sender_rate_period = string_nextinlist(&arg, &sep, NULL, 0);
+if (sender_rate_period == NULL) period = -1.0;
+else period = readconf_readtime(sender_rate_period, 0, FALSE);
+if (period <= 0.0)
+  return ratelimit_error(log_msgptr,
+    "\"%s\" is not a time value", sender_rate_period);
+
+/* By default we are counting one of something, but the per_rcpt,
+per_byte, and count options can change this. */
+
+count = 1.0;
+
+/* Parse the other options. */
+
+while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size))
+       != NULL)
+  {
+  if (strcmpic(ss, US"leaky") == 0) leaky = TRUE;
+  else if (strcmpic(ss, US"strict") == 0) strict = TRUE;
+  else if (strcmpic(ss, US"noupdate") == 0) noupdate = TRUE;
+  else if (strcmpic(ss, US"readonly") == 0) readonly = TRUE;
+  else if (strcmpic(ss, US"per_cmd") == 0) RATE_SET(mode, PER_CMD);
+  else if (strcmpic(ss, US"per_conn") == 0)
+    {
+    RATE_SET(mode, PER_CONN);
+    if (where == ACL_WHERE_NOTSMTP || where == ACL_WHERE_NOTSMTP_START)
+      badacl = TRUE;
+    }
+  else if (strcmpic(ss, US"per_mail") == 0)
+    {
+    RATE_SET(mode, PER_MAIL);
+    if (where > ACL_WHERE_NOTSMTP) badacl = TRUE;
+    }
+  else if (strcmpic(ss, US"per_rcpt") == 0)
+    {
+    /* If we are running in the RCPT ACL, then we'll count the recipients
+    one by one, but if we are running when we have accumulated the whole
+    list then we'll add them all in one batch. */
+    if (where == ACL_WHERE_RCPT)
+      RATE_SET(mode, PER_RCPT);
+    else if (where >= ACL_WHERE_PREDATA && where <= ACL_WHERE_NOTSMTP)
+      RATE_SET(mode, PER_ALLRCPTS), count = (double)recipients_count;
+    else if (where == ACL_WHERE_MAIL || where > ACL_WHERE_NOTSMTP)
+      RATE_SET(mode, PER_RCPT), badacl = TRUE;
+    }
+  else if (strcmpic(ss, US"per_byte") == 0)
+    {
+    /* If we have not yet received the message data and there was no SIZE
+    declaration on the MAIL comand, then it's safe to just use a value of
+    zero and let the recorded rate decay as if nothing happened. */
+    RATE_SET(mode, PER_MAIL);
+    if (where > ACL_WHERE_NOTSMTP) badacl = TRUE;
+      else count = message_size < 0 ? 0.0 : (double)message_size;
+    }
+  else if (strcmpic(ss, US"per_addr") == 0)
+    {
+    RATE_SET(mode, PER_RCPT);
+    if (where != ACL_WHERE_RCPT) badacl = TRUE, unique = "*";
+      else unique = string_sprintf("%s@%s", deliver_localpart, deliver_domain);
+    }
+  else if (strncmpic(ss, US"count=", 6) == 0)
+    {
+    uschar *e;
+    count = Ustrtod(ss+6, &e);
+    if (count < 0.0 || *e != '\0')
+      return ratelimit_error(log_msgptr,
+       "\"%s\" is not a positive number", ss);
+    }
+  else if (strncmpic(ss, US"unique=", 7) == 0)
+    unique = string_copy(ss + 7);
+  else if (key == NULL)
+    key = string_copy(ss);
+  else
+    key = string_sprintf("%s/%s", key, ss);
+  }
+
+/* Sanity check. When the badacl flag is set the update mode must either
+be readonly (which is the default if it is omitted) or, for backwards
+compatibility, a combination of noupdate and strict or leaky. */
+
+if (mode == RATE_PER_CLASH)
+  return ratelimit_error(log_msgptr, "conflicting per_* options");
+if (leaky + strict + readonly > 1)
+  return ratelimit_error(log_msgptr, "conflicting update modes");
+if (badacl && (leaky || strict) && !noupdate)
+  return ratelimit_error(log_msgptr,
+    "\"%s\" must not have /leaky or /strict option in %s ACL",
+    ratelimit_option_string[mode], acl_wherenames[where]);
+
+/* Set the default values of any unset options. In readonly mode we
+perform the rate computation without any increment so that its value
+decays to eventually allow over-limit senders through. */
+
+if (noupdate) readonly = TRUE, leaky = strict = FALSE;
+if (badacl) readonly = TRUE;
+if (readonly) count = 0.0;
+if (!strict && !readonly) leaky = TRUE;
+if (mode == RATE_PER_WHAT) mode = RATE_PER_MAIL;
+
+/* Create the lookup key. If there is no explicit key, use sender_host_address.
+If there is no sender_host_address (e.g. -bs or acl_not_smtp) then we simply
+omit it. The smoothing constant (sender_rate_period) and the per_xxx options
+are added to the key because they alter the meaning of the stored data. */
+
+if (key == NULL)
+  key = (sender_host_address == NULL)? US"" : sender_host_address;
+
+key = string_sprintf("%s/%s/%s%s",
+  sender_rate_period,
+  ratelimit_option_string[mode],
+  unique == NULL ? "" : "unique/",
+  key);
+
+HDEBUG(D_acl)
+  debug_printf("ratelimit condition count=%.0f %.1f/%s\n", count, limit, key);
+
+/* See if we have already computed the rate by looking in the relevant tree.
+For per-connection rate limiting, store tree nodes and dbdata in the permanent
+pool so that they survive across resets. In readonly mode we only remember the
+result for the rest of this command in case a later command changes it. After
+this bit of logic the code is independent of the per_* mode. */
+
+old_pool = store_pool;
+
+if (readonly)
+  anchor = &ratelimiters_cmd;
+else switch(mode) {
+case RATE_PER_CONN:
+  anchor = &ratelimiters_conn;
+  store_pool = POOL_PERM;
+  break;
+case RATE_PER_BYTE:
+case RATE_PER_MAIL:
+case RATE_PER_ALLRCPTS:
+  anchor = &ratelimiters_mail;
+  break;
+case RATE_PER_ADDR:
+case RATE_PER_CMD:
+case RATE_PER_RCPT:
+  anchor = &ratelimiters_cmd;
+  break;
+default:
+  anchor = NULL; /* silence an "unused" complaint */
+  log_write(0, LOG_MAIN|LOG_PANIC_DIE,
+    "internal ACL error: unknown ratelimit mode %d", mode);
+  break;
+}
+
+t = tree_search(*anchor, key);
+if (t != NULL)
+  {
+  dbd = t->data.ptr;
+  /* The following few lines duplicate some of the code below. */
+  rc = (dbd->rate < limit)? FAIL : OK;
+  store_pool = old_pool;
+  sender_rate = string_sprintf("%.1f", dbd->rate);
+  HDEBUG(D_acl)
+    debug_printf("ratelimit found pre-computed rate %s\n", sender_rate);
+  return rc;
   }
 
-/* We have a result from the relevant test. Handle defer overrides first. */
+/* We aren't using a pre-computed rate, so get a previously recorded rate
+from the database, which will be updated and written back if required. */
 
-if (rc == DEFER && (defer_ok ||
-   (callout_defer_ok && *basic_errno == ERRNO_CALLOUTDEFER)))
+dbm = dbfn_open(US"ratelimit", O_RDWR, &dbblock, TRUE);
+if (dbm == NULL)
   {
-  HDEBUG(D_acl) debug_printf("verify defer overridden by %s\n",
-    defer_ok? "defer_ok" : "callout_defer_ok");
-  rc = OK;
+  store_pool = old_pool;
+  sender_rate = NULL;
+  HDEBUG(D_acl) debug_printf("ratelimit database not available\n");
+  *log_msgptr = US"ratelimit database not available";
+  return DEFER;
   }
+dbdb = dbfn_read_with_length(dbm, key, &dbdb_size);
+dbd = NULL;
 
-/* If we've failed a sender, set up a recipient message, and point
-sender_verified_failed to the address item that actually failed. */
+gettimeofday(&tv, NULL);
 
-if (rc != OK && verify_sender_address != NULL)
+if (dbdb != NULL)
   {
-  if (rc != DEFER)
+  /* Locate the basic ratelimit block inside the DB data. */
+  HDEBUG(D_acl) debug_printf("ratelimit found key in database\n");
+  dbd = &dbdb->dbd;
+
+  /* Forget the old Bloom filter if it is too old, so that we count each
+  repeating event once per period. We don't simply clear and re-use the old
+  filter because we want its size to change if the limit changes. Note that
+  we keep the dbd pointer for copying the rate into the new data block. */
+
+  if(unique != NULL && tv.tv_sec > dbdb->bloom_epoch + period)
     {
-    *log_msgptr = *user_msgptr = US"Sender verify failed";
+    HDEBUG(D_acl) debug_printf("ratelimit discarding old Bloom filter\n");
+    dbdb = NULL;
     }
-  else if (*basic_errno != ERRNO_CALLOUTDEFER)
+
+  /* Sanity check. */
+
+  if(unique != NULL && dbdb_size < sizeof(*dbdb))
     {
-    *log_msgptr = *user_msgptr = US"Could not complete sender verify";
+    HDEBUG(D_acl) debug_printf("ratelimit discarding undersize Bloom filter\n");
+    dbdb = NULL;
     }
-  else
+  }
+
+/* Allocate a new data block if the database lookup failed
+or the Bloom filter passed its age limit. */
+
+if (dbdb == NULL)
+  {
+  if (unique == NULL)
     {
-    *log_msgptr = US"Could not complete sender verify callout";
-    *user_msgptr = smtp_return_error_details? sender_vaddr->user_message :
-      *log_msgptr;
+    /* No Bloom filter. This basic ratelimit block is initialized below. */
+    HDEBUG(D_acl) debug_printf("ratelimit creating new rate data block\n");
+    dbdb_size = sizeof(*dbd);
+    dbdb = store_get(dbdb_size);
     }
+  else
+    {
+    int extra;
+    HDEBUG(D_acl) debug_printf("ratelimit creating new Bloom filter\n");
 
-  sender_verified_failed = sender_vaddr;
-  }
+    /* See the long comment below for an explanation of the magic number 2.
+    The filter has a minimum size in case the rate limit is very small;
+    this is determined by the definition of dbdata_ratelimit_unique. */
 
-/* Verifying an address messes up the values of $domain and $local_part,
-so reset them before returning if this is a RCPT ACL. */
+    extra = (int)limit * 2 - sizeof(dbdb->bloom);
+    if (extra < 0) extra = 0;
+    dbdb_size = sizeof(*dbdb) + extra;
+    dbdb = store_get(dbdb_size);
+    dbdb->bloom_epoch = tv.tv_sec;
+    dbdb->bloom_size = sizeof(dbdb->bloom) + extra;
+    memset(dbdb->bloom, 0, dbdb->bloom_size);
 
-if (addr != NULL)
-  {
-  deliver_domain = addr->domain;
-  deliver_localpart = addr->local_part;
-  }
-return rc;
+    /* Preserve any basic ratelimit data (which is our longer-term memory)
+    by copying it from the discarded block. */
 
-/* Syntax errors in the verify argument come here. */
+    if (dbd != NULL)
+      {
+      dbdb->dbd = *dbd;
+      dbd = &dbdb->dbd;
+      }
+    }
+  }
 
-BAD_VERIFY:
-*log_msgptr = string_sprintf("expected \"sender[=address]\", \"recipient\", "
-  "\"header_syntax\" or \"header_sender\" at start of ACL condition "
-  "\"verify %s\"", arg);
-return ERROR;
-}
+/* If we are counting unique events, find out if this event is new or not.
+If the client repeats the event during the current period then it should be
+counted. We skip this code in readonly mode for efficiency, because any
+changes to the filter will be discarded and because count is already set to
+zero. */
 
+if (unique != NULL && !readonly)
+  {
+  /* We identify unique events using a Bloom filter. (You can find my
+  notes on Bloom filters at http://fanf.livejournal.com/81696.html)
+  With the per_addr option, an "event" is a recipient address, though the
+  user can use the unique option to define their own events. We only count
+  an event if we have not seen it before.
+
+  We size the filter according to the rate limit, which (in leaky mode)
+  is the limit on the population of the filter. We allow 16 bits of space
+  per entry (see the construction code above) and we set (up to) 8 of them
+  when inserting an element (see the loop below). The probability of a false
+  positive (an event we have not seen before but which we fail to count) is
+
+    size    = limit * 16
+    numhash = 8
+    allzero = exp(-numhash * pop / size)
+            = exp(-0.5 * pop / limit)
+    fpr     = pow(1 - allzero, numhash)
+
+  For senders at the limit the fpr is      0.06%    or  1 in 1700
+  and for senders at half the limit it is  0.0006%  or  1 in 170000
+
+  In strict mode the Bloom filter can fill up beyond the normal limit, in
+  which case the false positive rate will rise. This means that the
+  measured rate for very fast senders can bogusly drop off after a while.
+
+  At twice the limit, the fpr is  2.5%  or  1 in 40
+  At four times the limit, it is  31%   or  1 in 3.2
+
+  It takes ln(pop/limit) periods for an over-limit burst of pop events to
+  decay below the limit, and if this is more than one then the Bloom filter
+  will be discarded before the decay gets that far. The false positive rate
+  at this threshold is 9.3% or 1 in 10.7. */
+
+  BOOL seen;
+  unsigned n, hash, hinc;
+  uschar md5sum[16];
+  md5 md5info;
+
+  /* Instead of using eight independent hash values, we combine two values
+  using the formula h1 + n * h2. This does not harm the Bloom filter's
+  performance, and means the amount of hash we need is independent of the
+  number of bits we set in the filter. */
+
+  md5_start(&md5info);
+  md5_end(&md5info, unique, Ustrlen(unique), md5sum);
+  hash = md5sum[0] | md5sum[1] << 8 | md5sum[2] << 16 | md5sum[3] << 24;
+  hinc = md5sum[4] | md5sum[5] << 8 | md5sum[6] << 16 | md5sum[7] << 24;
+
+  /* Scan the bits corresponding to this event. A zero bit means we have
+  not seen it before. Ensure all bits are set to record this event. */
+
+  HDEBUG(D_acl) debug_printf("ratelimit checking uniqueness of %s\n", unique);
+
+  seen = TRUE;
+  for (n = 0; n < 8; n++, hash += hinc)
+    {
+    int bit = 1 << (hash % 8);
+    int byte = (hash / 8) % dbdb->bloom_size;
+    if ((dbdb->bloom[byte] & bit) == 0)
+      {
+      dbdb->bloom[byte] |= bit;
+      seen = FALSE;
+      }
+    }
 
+  /* If this event has occurred before, do not count it. */
 
+  if (seen)
+    {
+    HDEBUG(D_acl) debug_printf("ratelimit event found in Bloom filter\n");
+    count = 0.0;
+    }
+  else
+    HDEBUG(D_acl) debug_printf("ratelimit event added to Bloom filter\n");
+  }
 
-/*************************************************
-*        Check argument for control= modifier    *
-*************************************************/
+/* If there was no previous ratelimit data block for this key, initialize
+the new one, otherwise update the block from the database. The initial rate
+is what would be computed by the code below for an infinite interval. */
 
-/* Called from acl_check_condition() below
+if (dbd == NULL)
+  {
+  HDEBUG(D_acl) debug_printf("ratelimit initializing new key's rate data\n");
+  dbd = &dbdb->dbd;
+  dbd->time_stamp = tv.tv_sec;
+  dbd->time_usec = tv.tv_usec;
+  dbd->rate = count;
+  }
+else
+  {
+  /* The smoothed rate is computed using an exponentially weighted moving
+  average adjusted for variable sampling intervals. The standard EWMA for
+  a fixed sampling interval is:  f'(t) = (1 - a) * f(t) + a * f'(t - 1)
+  where f() is the measured value and f'() is the smoothed value.
+
+  Old data decays out of the smoothed value exponentially, such that data n
+  samples old is multiplied by a^n. The exponential decay time constant p
+  is defined such that data p samples old is multiplied by 1/e, which means
+  that a = exp(-1/p). We can maintain the same time constant for a variable
+  sampling interval i by using a = exp(-i/p).
+
+  The rate we are measuring is messages per period, suitable for directly
+  comparing with the limit. The average rate between now and the previous
+  message is period / interval, which we feed into the EWMA as the sample.
+
+  It turns out that the number of messages required for the smoothed rate
+  to reach the limit when they are sent in a burst is equal to the limit.
+  This can be seen by analysing the value of the smoothed rate after N
+  messages sent at even intervals. Let k = (1 - a) * p/i
+
+    rate_1 = (1 - a) * p/i + a * rate_0
+           = k + a * rate_0
+    rate_2 = k + a * rate_1
+           = k + a * k + a^2 * rate_0
+    rate_3 = k + a * k + a^2 * k + a^3 * rate_0
+    rate_N = rate_0 * a^N + k * SUM(x=0..N-1)(a^x)
+           = rate_0 * a^N + k * (1 - a^N) / (1 - a)
+           = rate_0 * a^N + p/i * (1 - a^N)
+
+  When N is large, a^N -> 0 so rate_N -> p/i as desired.
+
+    rate_N = p/i + (rate_0 - p/i) * a^N
+    a^N = (rate_N - p/i) / (rate_0 - p/i)
+    N * -i/p = log((rate_N - p/i) / (rate_0 - p/i))
+    N = p/i * log((rate_0 - p/i) / (rate_N - p/i))
+
+  Numerical analysis of the above equation, setting the computed rate to
+  increase from rate_0 = 0 to rate_N = limit, shows that for large sending
+  rates, p/i, the number of messages N = limit. So limit serves as both the
+  maximum rate measured in messages per period, and the maximum number of
+  messages that can be sent in a fast burst. */
+
+  double this_time = (double)tv.tv_sec
+                   + (double)tv.tv_usec / 1000000.0;
+  double prev_time = (double)dbd->time_stamp
+                   + (double)dbd->time_usec / 1000000.0;
+
+  /* We must avoid division by zero, and deal gracefully with the clock going
+  backwards. If we blunder ahead when time is in reverse then the computed
+  rate will be bogus. To be safe we clamp interval to a very small number. */
+
+  double interval = this_time - prev_time <= 0.0 ? 1e-9
+                  : this_time - prev_time;
+
+  double i_over_p = interval / period;
+  double a = exp(-i_over_p);
+
+  /* Combine the instantaneous rate (period / interval) with the previous rate
+  using the smoothing factor a. In order to measure sized events, multiply the
+  instantaneous rate by the count of bytes or recipients etc. */
+
+  dbd->time_stamp = tv.tv_sec;
+  dbd->time_usec = tv.tv_usec;
+  dbd->rate = (1 - a) * count / i_over_p + a * dbd->rate;
+
+  /* When events are very widely spaced the computed rate tends towards zero.
+  Although this is accurate it turns out not to be useful for our purposes,
+  especially when the first event after a long silence is the start of a spam
+  run. A more useful model is that the rate for an isolated event should be the
+  size of the event per the period size, ignoring the lack of events outside
+  the current period and regardless of where the event falls in the period. So,
+  if the interval was so long that the calculated rate is unhelpfully small, we
+  re-intialize the rate. In the absence of higher-rate bursts, the condition
+  below is true if the interval is greater than the period. */
+
+  if (dbd->rate < count) dbd->rate = count;
+  }
 
-Arguments:
-  arg         the argument string for control=
-  pptr        set to point to the terminating character
-  where       which ACL we are in
-  log_msgptr  for error messages
+/* Clients sending at the limit are considered to be over the limit.
+This matters for edge cases such as a limit of zero, when the client
+should be completely blocked. */
 
-Returns:      CONTROL_xxx value
-*/
+rc = (dbd->rate < limit)? FAIL : OK;
 
-static int
-decode_control(uschar *arg, uschar **pptr, int where, uschar **log_msgptr)
-{
-int len;
-control_def *d;
+/* Update the state if the rate is low or if we are being strict. If we
+are in leaky mode and the sender's rate is too high, we do not update
+the recorded rate in order to avoid an over-aggressive sender's retry
+rate preventing them from getting any email through. If readonly is set,
+neither leaky nor strict are set, so we do not do any updates. */
 
-for (d = controls_list;
-     d < controls_list + sizeof(controls_list)/sizeof(control_def);
-     d++)
+if ((rc == FAIL && leaky) || strict)
   {
-  len = Ustrlen(d->name);
-  if (Ustrncmp(d->name, arg, len) == 0) break;
+  dbfn_write(dbm, key, dbdb, dbdb_size);
+  HDEBUG(D_acl) debug_printf("ratelimit db updated\n");
   }
-
-if (d >= controls_list + sizeof(controls_list)/sizeof(control_def) ||
-   (arg[len] != 0 && (!d->has_option || arg[len] != '/')))
+else
   {
-  *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
-  return CONTROL_ERROR;
+  HDEBUG(D_acl) debug_printf("ratelimit db not updated: %s\n",
+    readonly? "readonly mode" : "over the limit, but leaky");
   }
 
-*pptr = arg + len;
-return d->value;
+dbfn_close(dbm);
+
+/* Store the result in the tree for future reference. */
+
+t = store_get(sizeof(tree_node) + Ustrlen(key));
+t->data.ptr = dbd;
+Ustrcpy(t->name, key);
+(void)tree_insertnode(anchor, t);
+
+/* We create the formatted version of the sender's rate very late in
+order to ensure that it is done using the correct storage pool. */
+
+store_pool = old_pool;
+sender_rate = string_sprintf("%.1f", dbd->rate);
+
+HDEBUG(D_acl)
+  debug_printf("ratelimit computed rate %s\n", sender_rate);
+
+return rc;
 }
 
 
@@ -1494,7 +2700,9 @@ acl_check_condition(int verb, acl_condition_block *cb, int where,
 {
 uschar *user_message = NULL;
 uschar *log_message = NULL;
-uschar *p;
+uschar *debug_tag = NULL;
+uschar *debug_opts = NULL;
+uschar *p = NULL;
 int rc = OK;
 #ifdef WITH_CONTENT_SCAN
 int sep = '/';
@@ -1503,7 +2711,7 @@ int sep = '/';
 for (; cb != NULL; cb = cb->next)
   {
   uschar *arg;
-  int control_type; 
+  int control_type;
 
   /* The message and log_message items set up messages to be used in
   case of rejection. They are expanded later. */
@@ -1557,11 +2765,8 @@ for (; cb != NULL; cb = cb->next)
 
     if (cb->type == ACLC_SET)
       {
-      int n = cb->u.varnumber;
-      int t = (n < ACL_C_MAX)? 'c' : 'm';
-      if (n >= ACL_C_MAX) n -= ACL_C_MAX;
-      debug_printf("acl_%c%d ", t, n);
-      lhswidth += 7;
+      debug_printf("acl_%s ", cb->u.varname);
+      lhswidth += 5 + Ustrlen(cb->u.varname);
       }
 
     debug_printf("= %s\n", cb->arg);
@@ -1586,6 +2791,10 @@ for (; cb != NULL; cb = cb->next)
 
   switch(cb->type)
     {
+    case ACLC_ADD_HEADER:
+    setup_header(arg);
+    break;
+
     /* A nested ACL that returns "discard" makes sense only for an "accept" or
     "discard" verb. */
 
@@ -1606,7 +2815,7 @@ for (; cb != NULL; cb = cb->next)
         TRUE, NULL);
     break;
 
-#ifdef EXPERIMENTAL_BRIGHTMAIL
+    #ifdef EXPERIMENTAL_BRIGHTMAIL
     case ACLC_BMI_OPTIN:
       {
       int old_pool = store_pool;
@@ -1615,9 +2824,12 @@ for (; cb != NULL; cb = cb->next)
       store_pool = old_pool;
       }
     break;
-#endif
+    #endif
 
     case ACLC_CONDITION:
+    /* The true/false parsing here should be kept in sync with that used in
+    expand.c when dealing with ECOND_BOOL so that we don't have too many
+    different definitions of what can be a boolean. */
     if (Ustrspn(arg, "0123456789") == Ustrlen(arg))     /* Digits, or empty */
       rc = (Uatoi(arg) == 0)? FAIL : OK;
     else
@@ -1629,6 +2841,9 @@ for (; cb != NULL; cb = cb->next)
       *log_msgptr = string_sprintf("invalid \"condition\" value \"%s\"", arg);
     break;
 
+    case ACLC_CONTINUE:    /* Always succeeds */
+    break;
+
     case ACLC_CONTROL:
     control_type = decode_control(arg, &p, where, log_msgptr);
 
@@ -1639,16 +2854,26 @@ for (; cb != NULL; cb = cb->next)
       *log_msgptr = string_sprintf("cannot use \"control=%s\" in %s ACL",
         controls[control_type], acl_wherenames[where]);
       return ERROR;
-      }                                                     
+      }
 
     switch(control_type)
       {
-#ifdef EXPERIMENTAL_BRIGHTMAIL
+      case CONTROL_AUTH_UNADVERTISED:
+      allow_auth_unadvertised = TRUE;
+      break;
+
+      #ifdef EXPERIMENTAL_BRIGHTMAIL
       case CONTROL_BMI_RUN:
       bmi_run = 1;
       break;
-#endif
-      
+      #endif
+
+      #ifndef DISABLE_DKIM
+      case CONTROL_DKIM_VERIFY:
+      dkim_disable_verify = TRUE;
+      break;
+      #endif
+
       case CONTROL_ERROR:
       return ERROR;
 
@@ -1668,35 +2893,59 @@ for (; cb != NULL; cb = cb->next)
       smtp_enforce_sync = FALSE;
       break;
 
-#ifdef WITH_CONTENT_SCAN
+      #ifdef WITH_CONTENT_SCAN
       case CONTROL_NO_MBOX_UNSPOOL:
       no_mbox_unspool = TRUE;
       break;
-#endif
+      #endif
 
       case CONTROL_NO_MULTILINE:
       no_multiline_responses = TRUE;
       break;
 
+      case CONTROL_NO_PIPELINING:
+      pipelining_enable = FALSE;
+      break;
+
+      case CONTROL_NO_DELAY_FLUSH:
+      disable_delay_flush = TRUE;
+      break;
+
+      case CONTROL_NO_CALLOUT_FLUSH:
+      disable_callout_flush = TRUE;
+      break;
+
+      case CONTROL_FAKEDEFER:
       case CONTROL_FAKEREJECT:
-      fake_reject = TRUE;
+      fake_response = (control_type == CONTROL_FAKEDEFER) ? DEFER : FAIL;
       if (*p == '/')
-        { 
+        {
         uschar *pp = p + 1;
-        while (*pp != 0) pp++; 
-        fake_reject_text = expand_string(string_copyn(p+1, pp-p));
+        while (*pp != 0) pp++;
+        fake_response_text = expand_string(string_copyn(p+1, pp-p-1));
         p = pp;
         }
        else
         {
         /* Explicitly reset to default string */
-        fake_reject_text = US"Your message has been rejected but is being kept for evaluation.\nIf it was a legitimate message, it may still be delivered to the target recipient(s).";
+        fake_response_text = US"Your message has been rejected but is being kept for evaluation.\nIf it was a legitimate message, it may still be delivered to the target recipient(s).";
         }
       break;
 
       case CONTROL_FREEZE:
       deliver_freeze = TRUE;
       deliver_frozen_at = time(NULL);
+      freeze_tell = freeze_tell_config;       /* Reset to configured value */
+      if (Ustrncmp(p, "/no_tell", 8) == 0)
+        {
+        p += 8;
+        freeze_tell = NULL;
+        }
+      if (*p != 0)
+        {
+        *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
+        return ERROR;
+        }
       break;
 
       case CONTROL_QUEUE_ONLY:
@@ -1704,38 +2953,94 @@ for (; cb != NULL; cb = cb->next)
       break;
 
       case CONTROL_SUBMISSION:
+      originator_name = US"";
       submission_mode = TRUE;
       while (*p == '/')
-        { 
+        {
         if (Ustrncmp(p, "/sender_retain", 14) == 0)
           {
           p += 14;
           active_local_sender_retain = TRUE;
-          active_local_from_check = FALSE;   
-          }  
+          active_local_from_check = FALSE;
+          }
         else if (Ustrncmp(p, "/domain=", 8) == 0)
           {
           uschar *pp = p + 8;
-          while (*pp != 0 && *pp != '/') pp++; 
-          submission_domain = string_copyn(p+8, pp-p);
-          p = pp; 
+          while (*pp != 0 && *pp != '/') pp++;
+          submission_domain = string_copyn(p+8, pp-p-8);
+          p = pp;
+          }
+        /* The name= option must be last, because it swallows the rest of
+        the string. */
+        else if (Ustrncmp(p, "/name=", 6) == 0)
+          {
+          uschar *pp = p + 6;
+          while (*pp != 0) pp++;
+          submission_name = string_copy(parse_fix_phrase(p+6, pp-p-6,
+            big_buffer, big_buffer_size));
+          p = pp;
           }
-        else break;   
-        }   
+        else break;
+        }
       if (*p != 0)
         {
         *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
         return ERROR;
         }
       break;
+
+      case CONTROL_DEBUG:
+      while (*p == '/')
+        {
+        if (Ustrncmp(p, "/tag=", 5) == 0)
+          {
+          uschar *pp = p + 5;
+          while (*pp != '\0' && *pp != '/') pp++;
+          debug_tag = string_copyn(p+5, pp-p-5);
+          p = pp;
+          }
+        else if (Ustrncmp(p, "/opts=", 6) == 0)
+          {
+          uschar *pp = p + 6;
+          while (*pp != '\0' && *pp != '/') pp++;
+          debug_opts = string_copyn(p+6, pp-p-6);
+          p = pp;
+          }
+        }
+        debug_logging_activate(debug_tag, debug_opts);
+      break;
+
+      case CONTROL_SUPPRESS_LOCAL_FIXUPS:
+      suppress_local_fixups = TRUE;
+      break;
       }
     break;
 
-#ifdef WITH_CONTENT_SCAN
+    #ifdef EXPERIMENTAL_DCC
+    case ACLC_DCC:
+      {
+      /* Seperate the regular expression and any optional parameters. */
+      uschar *ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size);
+      /* Run the dcc backend. */
+      rc = dcc_process(&ss);
+      /* Modify return code based upon the existance of options. */
+      while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size))
+            != NULL) {
+        if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER)
+          {
+          /* FAIL so that the message is passed to the next ACL */
+          rc = FAIL;
+          }
+        }
+      }
+    break;
+    #endif
+
+    #ifdef WITH_CONTENT_SCAN
     case ACLC_DECODE:
     rc = mime_decode(&arg);
     break;
-#endif
+    #endif
 
     case ACLC_DELAY:
       {
@@ -1755,19 +3060,55 @@ for (; cb != NULL; cb = cb->next)
           HDEBUG(D_acl)
             debug_printf("delay skipped in -bh checking mode\n");
           }
-        else 
+
+        /* It appears to be impossible to detect that a TCP/IP connection has
+        gone away without reading from it. This means that we cannot shorten
+        the delay below if the client goes away, because we cannot discover
+        that the client has closed its end of the connection. (The connection
+        is actually in a half-closed state, waiting for the server to close its
+        end.) It would be nice to be able to detect this state, so that the
+        Exim process is not held up unnecessarily. However, it seems that we
+        can't. The poll() function does not do the right thing, and in any case
+        it is not always available.
+
+        NOTE 1: If ever this state of affairs changes, remember that we may be
+        dealing with stdin/stdout here, in addition to TCP/IP connections.
+        Also, delays may be specified for non-SMTP input, where smtp_out and
+        smtp_in will be NULL. Whatever is done must work in all cases.
+
+        NOTE 2: The added feature of flushing the output before a delay must
+        apply only to SMTP input. Hence the test for smtp_out being non-NULL.
+        */
+
+        else
           {
+          if (smtp_out != NULL && !disable_delay_flush) mac_smtp_fflush();
           while (delay > 0) delay = sleep(delay);
-          } 
+          }
         }
       }
     break;
 
-#ifdef WITH_OLD_DEMIME
+    #ifdef WITH_OLD_DEMIME
     case ACLC_DEMIME:
       rc = demime(&arg);
     break;
-#endif
+    #endif
+
+    #ifndef DISABLE_DKIM
+    case ACLC_DKIM_SIGNER:
+    if (dkim_cur_signer != NULL)
+      rc = match_isinlist(dkim_cur_signer,
+                          &arg,0,NULL,NULL,MCL_STRING,TRUE,NULL);
+    else
+       rc = FAIL;
+    break;
+
+    case ACLC_DKIM_STATUS:
+    rc = match_isinlist(dkim_exim_expand_query(DKIM_VERIFY_STATUS),
+                        &arg,0,NULL,NULL,MCL_STRING,TRUE,NULL);
+    break;
+    #endif
 
     case ACLC_DNSLISTS:
     rc = verify_check_dnsbl(&arg);
@@ -1818,6 +3159,29 @@ for (; cb != NULL; cb = cb->next)
       &deliver_localpart_data);
     break;
 
+    case ACLC_LOG_REJECT_TARGET:
+      {
+      int logbits = 0;
+      int sep = 0;
+      uschar *s = arg;
+      uschar *ss;
+      while ((ss = string_nextinlist(&s, &sep, big_buffer, big_buffer_size))
+              != NULL)
+        {
+        if (Ustrcmp(ss, "main") == 0) logbits |= LOG_MAIN;
+        else if (Ustrcmp(ss, "panic") == 0) logbits |= LOG_PANIC;
+        else if (Ustrcmp(ss, "reject") == 0) logbits |= LOG_REJECT;
+        else
+          {
+          logbits |= LOG_MAIN|LOG_REJECT;
+          log_write(0, LOG_MAIN|LOG_PANIC, "unknown log name \"%s\" in "
+            "\"log_reject_target\" in %s ACL", ss, acl_wherenames[where]);
+          }
+        }
+      log_reject_target = logbits;
+      }
+    break;
+
     case ACLC_LOGWRITE:
       {
       int logbits = 0;
@@ -1844,15 +3208,17 @@ for (; cb != NULL; cb = cb->next)
         s++;
         }
       while (isspace(*s)) s++;
+
+
       if (logbits == 0) logbits = LOG_MAIN;
       log_write(0, logbits, "%s", string_printing(s));
       }
     break;
-    
-#ifdef WITH_CONTENT_SCAN
+
+    #ifdef WITH_CONTENT_SCAN
     case ACLC_MALWARE:
       {
-      /* Seperate the regular expression and any optional parameters. */
+      /* Separate the regular expression and any optional parameters. */
       uschar *ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size);
       /* Run the malware backend. */
       rc = malware(&ss);
@@ -1869,20 +3235,24 @@ for (; cb != NULL; cb = cb->next)
     break;
 
     case ACLC_MIME_REGEX:
-      rc = mime_regex(&arg);
+    rc = mime_regex(&arg);
+    break;
+    #endif
+
+    case ACLC_RATELIMIT:
+    rc = acl_ratelimit(arg, where, log_msgptr);
     break;
-#endif
 
     case ACLC_RECIPIENTS:
     rc = match_address_list(addr->address, TRUE, TRUE, &arg, NULL, -1, 0,
       &recipient_data);
     break;
 
-#ifdef WITH_CONTENT_SCAN
-   case ACLC_REGEX:
-      rc = regex(&arg);
+    #ifdef WITH_CONTENT_SCAN
+    case ACLC_REGEX:
+    rc = regex(&arg);
     break;
-#endif
+    #endif
 
     case ACLC_SENDER_DOMAINS:
       {
@@ -1904,13 +3274,13 @@ for (; cb != NULL; cb = cb->next)
     case ACLC_SET:
       {
       int old_pool = store_pool;
-      if (cb->u.varnumber < ACL_C_MAX) store_pool = POOL_PERM;
-      acl_var[cb->u.varnumber] = string_copy(arg);
+      if (cb->u.varname[0] == 'c') store_pool = POOL_PERM;
+      acl_var_create(cb->u.varname)->data.ptr = string_copy(arg);
       store_pool = old_pool;
       }
     break;
 
-#ifdef WITH_CONTENT_SCAN
+    #ifdef WITH_CONTENT_SCAN
     case ACLC_SPAM:
       {
       /* Seperate the regular expression and any optional parameters. */
@@ -1928,20 +3298,26 @@ for (; cb != NULL; cb = cb->next)
         }
       }
     break;
-#endif
+    #endif
 
-#ifdef EXPERIMENTAL_SPF
+    #ifdef EXPERIMENTAL_SPF
     case ACLC_SPF:
-      rc = spf_process(&arg, sender_address);
+      rc = spf_process(&arg, sender_address, SPF_PROCESS_NORMAL);
     break;
-#endif
+    case ACLC_SPF_GUESS:
+      rc = spf_process(&arg, sender_address, SPF_PROCESS_GUESS);
+    break;
+    #endif
 
     /* If the verb is WARN, discard any user message from verification, because
     such messages are SMTP responses, not header additions. The latter come
-    only from explicit "message" modifiers. */
+    only from explicit "message" modifiers. However, put the user message into
+    $acl_verify_message so it can be used in subsequent conditions or modifiers
+    (until something changes it). */
 
     case ACLC_VERIFY:
     rc = acl_verify(where, addr, arg, user_msgptr, log_msgptr, basic_errno);
+    acl_verify_message = *user_msgptr;
     if (verb == ACL_WARN) *user_msgptr = NULL;
     break;
 
@@ -1964,13 +3340,8 @@ for (; cb != NULL; cb = cb->next)
 
 
 /* If the result is the one for which "message" and/or "log_message" are used,
-handle the values of these options. Most verbs have but a single return for
-which the messages are relevant, but for "discard", it's useful to have the log
-message both when it succeeds and when it fails. Also, for an "accept" that
-appears in a QUIT ACL, we want to handle the user message. Since only "accept"
-and "warn" are permitted in that ACL, we don't need to test the verb.
-
-These modifiers act in different ways:
+handle the values of these modifiers. If there isn't a log message set, we make
+it the same as the user message.
 
 "message" is a user message that will be included in an SMTP response. Unless
 it is empty, it overrides any previously set user message.
@@ -1978,23 +3349,29 @@ it is empty, it overrides any previously set user message.
 "log_message" is a non-user message, and it adds to any existing non-user
 message that is already set.
 
-If there isn't a log message set, we make it the same as the user message. */
+Most verbs have but a single return for which the messages are relevant, but
+for "discard", it's useful to have the log message both when it succeeds and
+when it fails. For "accept", the message is used in the OK case if there is no
+"endpass", but (for backwards compatibility) in the FAIL case if "endpass" is
+present. */
+
+if (*epp && rc == OK) user_message = NULL;
 
-if (((rc == FAIL_DROP)? FAIL : rc) == msgcond[verb] ||
-    (verb == ACL_DISCARD && rc == OK) ||
-    (where == ACL_WHERE_QUIT))
+if (((1<<rc) & msgcond[verb]) != 0)
   {
   uschar *expmessage;
+  uschar *old_user_msgptr = *user_msgptr;
+  uschar *old_log_msgptr = (*log_msgptr != NULL)? *log_msgptr : old_user_msgptr;
 
   /* If the verb is "warn", messages generated by conditions (verification or
-  nested ACLs) are discarded. Only messages specified at this level are used.
+  nested ACLs) are always discarded. This also happens for acceptance verbs
+  when they actually do accept. Only messages specified at this level are used.
   However, the value of an existing message is available in $acl_verify_message
   during expansions. */
 
-  uschar *old_user_msgptr = *user_msgptr;
-  uschar *old_log_msgptr = (*log_msgptr != NULL)? *log_msgptr : old_user_msgptr;
-
-  if (verb == ACL_WARN) *log_msgptr = *user_msgptr = NULL;
+  if (verb == ACL_WARN ||
+      (rc == OK && (verb == ACL_ACCEPT || verb == ACL_DISCARD)))
+    *log_msgptr = *user_msgptr = NULL;
 
   if (user_message != NULL)
     {
@@ -2269,7 +3646,7 @@ if (Ustrchr(ss, ' ') == NULL)
       return ERROR;
       }
     acl_text[statbuf.st_size] = 0;
-    close(fd);
+    (void)close(fd);
 
     acl_name = string_sprintf("ACL \"%s\"", ss);
     HDEBUG(D_acl) debug_printf("read ACL from file %s\n", ss);
@@ -2307,11 +3684,11 @@ while (acl != NULL)
   *log_msgptr = *user_msgptr = NULL;
   acl_temp_details = FALSE;
 
-  if (where == ACL_WHERE_QUIT &&
+  if ((where == ACL_WHERE_QUIT || where == ACL_WHERE_NOTQUIT) &&
       acl->verb != ACL_ACCEPT &&
       acl->verb != ACL_WARN)
     {
-    *log_msgptr = string_sprintf("\"%s\" is not allowed in a QUIT ACL",
+    *log_msgptr = string_sprintf("\"%s\" is not allowed in a QUIT or not-QUIT ACL",
       verbs[acl->verb]);
     return ERROR;
     }
@@ -2420,10 +3797,11 @@ while (acl != NULL)
     case ACL_WARN:
     if (cond == OK)
       acl_warn(where, *user_msgptr, *log_msgptr);
-    else if (cond == DEFER)
-      acl_warn(where, NULL, string_sprintf("ACL \"warn\" statement skipped: "
-        "condition test deferred: %s",
-        (*log_msgptr == NULL)? US"" : *log_msgptr));
+    else if (cond == DEFER && (log_extra_selector & LX_acl_warn_skipped) != 0)
+      log_write(0, LOG_MAIN, "%s Warning: ACL \"warn\" statement skipped: "
+        "condition test deferred%s%s", host_and_ident(TRUE),
+        (*log_msgptr == NULL)? US"" : US": ",
+        (*log_msgptr == NULL)? US"" : *log_msgptr);
     *log_msgptr = *user_msgptr = NULL;  /* In case implicit DENY follows */
     break;
 
@@ -2455,7 +3833,7 @@ acl_check_internal() to do the actual work.
 
 Arguments:
   where        ACL_WHERE_xxxx indicating where called from
-  data_string  RCPT address, or SMTP command argument, or NULL
+  recipient    RCPT address for RCPT check, else NULL
   s            the input string; NULL is the same as an empty ACL => DENY
   user_msgptr  where to put a user error (for SMTP response)
   log_msgptr   where to put a logging message (not for SMTP response)
@@ -2469,21 +3847,23 @@ Returns:       OK         access is granted by an ACCEPT verb
 */
 
 int
-acl_check(int where, uschar *data_string, uschar *s, uschar **user_msgptr,
+acl_check(int where, uschar *recipient, uschar *s, uschar **user_msgptr,
   uschar **log_msgptr)
 {
 int rc;
 address_item adb;
-address_item *addr;
+address_item *addr = NULL;
 
 *user_msgptr = *log_msgptr = NULL;
 sender_verified_failed = NULL;
+ratelimiters_cmd = NULL;
+log_reject_target = LOG_MAIN|LOG_REJECT;
 
 if (where == ACL_WHERE_RCPT)
   {
   adb = address_defaults;
   addr = &adb;
-  addr->address = data_string;
+  addr->address = recipient;
   if (deliver_split_address(addr) == DEFER)
     {
     *log_msgptr = US"defer in percent_hack_domains check";
@@ -2492,16 +3872,11 @@ if (where == ACL_WHERE_RCPT)
   deliver_domain = addr->domain;
   deliver_localpart = addr->local_part;
   }
-else
-  {
-  addr = NULL;
-  smtp_command_argument = data_string;
-  }
 
 rc = acl_check_internal(where, addr, s, 0, user_msgptr, log_msgptr);
 
-smtp_command_argument = deliver_domain =
-  deliver_localpart = deliver_address_data = sender_address_data = NULL;
+deliver_domain = deliver_localpart = deliver_address_data =
+  sender_address_data = NULL;
 
 /* A DISCARD response is permitted only for message ACLs, excluding the PREDATA
 ACL, which is really in the middle of an SMTP command. */
@@ -2526,53 +3901,74 @@ if (rc == FAIL_DROP && where == ACL_WHERE_MAILAUTH)
   return ERROR;
   }
 
-/* Before giving an error response, take a look at the length of any user
-message, and split it up into multiple lines if possible. */
+/* Before giving a response, take a look at the length of any user message, and
+split it up into multiple lines if possible. */
 
-if (rc != OK && *user_msgptr != NULL && Ustrlen(*user_msgptr) > 75)
-  {
-  uschar *s = *user_msgptr = string_copy(*user_msgptr);
-  uschar *ss = s;
+*user_msgptr = string_split_message(*user_msgptr);
+if (fake_response != OK)
+  fake_response_text = string_split_message(fake_response_text);
 
-  for (;;)
-    {
-    int i = 0;
-    while (i < 75 && *ss != 0 && *ss != '\n') ss++, i++;
-    if (*ss == 0) break;
-    if (*ss == '\n')
-      s = ++ss;
-    else
-      {
-      uschar *t = ss + 1;
-      uschar *tt = NULL;
-      while (--t > s + 35)
-        {
-        if (*t == ' ')
-          {
-          if (t[-1] == ':') { tt = t; break; }
-          if (tt == NULL) tt = t;
-          }
-        }
+return rc;
+}
 
-      if (tt == NULL)          /* Can't split behind - try ahead */
-        {
-        t = ss + 1;
-        while (*t != 0)
-          {
-          if (*t == ' ' || *t == '\n')
-            { tt = t; break; }
-          t++;
-          }
-        }
 
-      if (tt == NULL) break;   /* Can't find anywhere to split */
-      *tt = '\n';
-      s = ss = tt+1;
-      }
-    }
+
+/*************************************************
+*             Create ACL variable                *
+*************************************************/
+
+/* Create an ACL variable or reuse an existing one. ACL variables are in a
+binary tree (see tree.c) with acl_var_c and acl_var_m as root nodes.
+
+Argument:
+  name    pointer to the variable's name, starting with c or m
+
+Returns   the pointer to variable's tree node
+*/
+
+tree_node *
+acl_var_create(uschar *name)
+{
+tree_node *node, **root;
+root = (name[0] == 'c')? &acl_var_c : &acl_var_m;
+node = tree_search(*root, name);
+if (node == NULL)
+  {
+  node = store_get(sizeof(tree_node) + Ustrlen(name));
+  Ustrcpy(node->name, name);
+  (void)tree_insertnode(root, node);
   }
+node->data.ptr = NULL;
+return node;
+}
 
-return rc;
+
+
+/*************************************************
+*       Write an ACL variable in spool format    *
+*************************************************/
+
+/* This function is used as a callback for tree_walk when writing variables to
+the spool file. To retain spool file compatibility, what is written is -aclc or
+-aclm followed by the rest of the name and the data length, space separated,
+then the value itself, starting on a new line, and terminated by an additional
+newline. When we had only numbered ACL variables, the first line might look
+like this: "-aclc 5 20". Now it might be "-aclc foo 20" for the variable called
+acl_cfoo.
+
+Arguments:
+  name    of the variable
+  value   of the variable
+  ctx     FILE pointer (as a void pointer)
+
+Returns:  nothing
+*/
+
+void
+acl_var_write(uschar *name, uschar *value, void *ctx)
+{
+FILE *f = (FILE *)ctx;
+fprintf(f, "-acl%c %s %d\n%s\n", name[0], name+1, Ustrlen(value), value);
 }
 
 /* End of acl.c */