Update version number and copyright year.
[exim.git] / src / src / acl.c
index 2a7b5afe8c836d03bd5a887679a29189534c2be6..73f0614af92b3ee1483ff827dc0100e24f683bbf 100644 (file)
@@ -1,10 +1,10 @@
-/* $Cambridge: exim/src/src/acl.c,v 1.56 2006/03/02 12:25:48 ph10 Exp $ */
+/* $Cambridge: exim/src/src/acl.c,v 1.68 2007/01/08 10:50:17 ph10 Exp $ */
 
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2006 */
+/* Copyright (c) University of Cambridge 1995 - 2007 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Code for handling Access Control Lists (ACLs) */
@@ -27,18 +27,33 @@ 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_CONTROL,
 #ifdef WITH_CONTENT_SCAN
        ACLC_DECODE,
 #endif
@@ -54,8 +69,15 @@ ACLC_CONDITION, ACLC_CONTROL,
        ACLC_DK_SENDERS,
        ACLC_DK_STATUS,
 #endif
-       ACLC_DNSLISTS, ACLC_DOMAINS, ACLC_ENCRYPTED, ACLC_ENDPASS,
-       ACLC_HOSTS, ACLC_LOCAL_PARTS, ACLC_LOG_MESSAGE, ACLC_LOGWRITE,
+       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
@@ -68,7 +90,9 @@ ACLC_CONDITION, ACLC_CONTROL,
 #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,
 #endif
@@ -78,11 +102,13 @@ ACLC_CONDITION, ACLC_CONTROL,
        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. */
+"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"add_header",
   US"authenticated",
 #ifdef EXPERIMENTAL_BRIGHTMAIL
   US"bmi_optin",
@@ -104,8 +130,15 @@ static uschar *conditions[] = {
   US"dk_senders",
   US"dk_status",
 #endif
-  US"dnslists", US"domains", US"encrypted",
-  US"endpass", US"hosts", US"local_parts", US"log_message", US"logwrite",
+  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
@@ -190,6 +223,7 @@ checking functions. */
 
 static uschar cond_expand_at_top[] = {
   TRUE,    /* acl */
+  TRUE,    /* add_header */
   FALSE,   /* authenticated */
 #ifdef EXPERIMENTAL_BRIGHTMAIL
   TRUE,    /* bmi_optin */
@@ -218,6 +252,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 */
@@ -247,6 +282,7 @@ static uschar cond_expand_at_top[] = {
 
 static uschar cond_modifiers[] = {
   FALSE,   /* acl */
+  TRUE,    /* add_header */
   FALSE,   /* authenticated */
 #ifdef EXPERIMENTAL_BRIGHTMAIL
   TRUE,    /* bmi_optin */
@@ -275,6 +311,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 */
@@ -307,18 +344,26 @@ 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),
+  (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_NOTSMTP_START)),
 
-#ifdef EXPERIMENTAL_BRIGHTMAIL
+  (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_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 */
 
@@ -327,26 +372,26 @@ static unsigned int cond_forbids[] = {
 
   0,                                               /* control */
 
-#ifdef WITH_CONTENT_SCAN
+  #ifdef WITH_CONTENT_SCAN
   (unsigned int)
   ~(1<<ACL_WHERE_MIME),                            /* decode */
-#endif
+  #endif
 
   0,                                               /* delay */
 
-#ifdef WITH_OLD_DEMIME
+  #ifdef WITH_OLD_DEMIME
   (unsigned int)
   ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)),   /* demime */
-#endif
+  #endif
 
-#ifdef EXPERIMENTAL_DOMAINKEYS
+  #ifdef EXPERIMENTAL_DOMAINKEYS
   (1<<ACL_WHERE_AUTH)|                             /* dk_domain_source */
     (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_VRFY)|(1<<ACL_WHERE_NOTSMTP_START),
 
   (1<<ACL_WHERE_AUTH)|                             /* dk_policy */
     (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
@@ -354,7 +399,7 @@ static unsigned int cond_forbids[] = {
     (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_VRFY)|(1<<ACL_WHERE_NOTSMTP_START),
 
   (1<<ACL_WHERE_AUTH)|                             /* dk_sender_domains */
     (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
@@ -362,7 +407,7 @@ static unsigned int cond_forbids[] = {
     (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_VRFY)|(1<<ACL_WHERE_NOTSMTP_START),
 
   (1<<ACL_WHERE_AUTH)|                             /* dk_sender_local_parts */
     (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
@@ -370,7 +415,7 @@ static unsigned int cond_forbids[] = {
     (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_VRFY)|(1<<ACL_WHERE_NOTSMTP_START),
 
   (1<<ACL_WHERE_AUTH)|                             /* dk_senders */
     (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
@@ -378,7 +423,7 @@ static unsigned int cond_forbids[] = {
     (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_VRFY)|(1<<ACL_WHERE_NOTSMTP_START),
 
   (1<<ACL_WHERE_AUTH)|                             /* dk_status */
     (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
@@ -386,50 +431,56 @@ static unsigned int cond_forbids[] = {
     (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),
-#endif
+    (1<<ACL_WHERE_VRFY)|(1<<ACL_WHERE_NOTSMTP_START),
+  #endif
 
-  (1<<ACL_WHERE_NOTSMTP),                          /* dnslists */
+  (1<<ACL_WHERE_NOTSMTP)|                          /* dnslists */
+    (1<<ACL_WHERE_NOTSMTP_START),
 
   (unsigned int)
   ~(1<<ACL_WHERE_RCPT),                            /* domains */
 
-  (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_CONNECT)|   /* encrypted */
+  (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)|                          /* 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
+  #ifdef WITH_CONTENT_SCAN
   (unsigned int)
   ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)),   /* malware */
-#endif
+  #endif
 
   0,                                               /* message */
 
-#ifdef WITH_CONTENT_SCAN
+  #ifdef WITH_CONTENT_SCAN
   (unsigned int)
   ~(1<<ACL_WHERE_MIME),                            /* mime_regex */
-#endif
+  #endif
 
   0,                                               /* ratelimit */
 
   (unsigned int)
   ~(1<<ACL_WHERE_RCPT),                            /* recipients */
 
-#ifdef WITH_CONTENT_SCAN
+  #ifdef WITH_CONTENT_SCAN
   (unsigned int)
   ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)|    /* regex */
     (1<<ACL_WHERE_MIME)),
-#endif
+  #endif
 
   (1<<ACL_WHERE_AUTH)|(1<<ACL_WHERE_CONNECT)|      /* sender_domains */
     (1<<ACL_WHERE_HELO)|
@@ -445,18 +496,20 @@ static unsigned int cond_forbids[] = {
 
   0,                                               /* set */
 
-#ifdef WITH_CONTENT_SCAN
+  #ifdef WITH_CONTENT_SCAN
   (unsigned int)
   ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)),   /* spam */
-#endif
+  #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),
+  #endif
 
   /* Certain types of verify are always allowed, so we let it through
   always and check in the verify function itself */
@@ -478,7 +531,8 @@ static unsigned int control_forbids[] = {
   #endif
 
   #ifdef EXPERIMENTAL_DOMAINKEYS
-  (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP),      /* dk_verify */
+  (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)|      /* dk_verify */
+    (1<<ACL_WHERE_NOTSMTP_START),
   #endif
 
   0,                                               /* error */
@@ -489,9 +543,11 @@ static unsigned int control_forbids[] = {
   (unsigned int)
   ~(1<<ACL_WHERE_RCPT),                            /* caselower_local_part */
 
-  (1<<ACL_WHERE_NOTSMTP),                          /* enforce_sync */
+  (1<<ACL_WHERE_NOTSMTP)|                          /* enforce_sync */
+    (1<<ACL_WHERE_NOTSMTP_START),
 
-  (1<<ACL_WHERE_NOTSMTP),                          /* no_enforce_sync */
+  (1<<ACL_WHERE_NOTSMTP)|                          /* no_enforce_sync */
+    (1<<ACL_WHERE_NOTSMTP_START),
 
   (unsigned int)
   ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)|       /* freeze */
@@ -509,7 +565,8 @@ static unsigned int control_forbids[] = {
 
   (unsigned int)
   ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)|       /* suppress_local_fixups */
-    (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_PREDATA)),
+    (1<<ACL_WHERE_PREDATA)|
+    (1<<ACL_WHERE_NOTSMTP_START)),
 
   #ifdef WITH_CONTENT_SCAN
   (unsigned int)
@@ -528,7 +585,8 @@ static unsigned int control_forbids[] = {
     (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)
 };
 
 /* Structure listing various control arguments, with their characteristics. */
@@ -690,7 +748,7 @@ while ((s = (*func)()) != NULL)
   can be started by a name, or by a macro definition. */
 
   s = readconf_readname(name, sizeof(name), s);
-  if (*s == ':' || isupper(name[0] && *s == '=')) return yield;
+  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. */
@@ -700,7 +758,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;
       }
     }
@@ -770,36 +829,47 @@ 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)
     {
-    int offset, max, n;
     uschar *endptr;
 
-    if (Ustrncmp(s, "acl_", 4) != 0) goto BAD_ACL_VAR;
-    if (s[4] == 'c')
+    if (Ustrncmp(s, "acl_c", 5) != 0 &&
+        Ustrncmp(s, "acl_m", 5) != 0)
       {
-      offset = 0;
-      max = ACL_CVARS;
+      *error = string_sprintf("invalid variable name after \"set\" in ACL "
+        "modifier \"set %s\" (must start \"acl_c\" or \"acl_m\")", s);
+      return NULL;
       }
-    else if (s[4] == 'm')
+
+    endptr = s + 5;
+    if (!isdigit(*endptr) && *endptr != '_')
       {
-      offset = ACL_CVARS;
-      max = ACL_MVARS;
+      *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;
       }
-    else goto BAD_ACL_VAR;
 
-    n = Ustrtoul(s + 5, &endptr, 10);
-    if ((*endptr != 0 && *endptr != '=' && !isspace(*endptr)) || n >= max)
+    while (*endptr != 0 && *endptr != '=' && !isspace(*endptr))
       {
-      BAD_ACL_VAR:
-      *error = string_sprintf("syntax error or unrecognized name after "
-        "\"set\" in ACL modifier \"set %s\"", s);
-      return NULL;
+      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.varnumber = n + offset;
+    cond->u.varname = string_copyn(s + 4, endptr - s - 4);
     s = endptr;
     while (isspace(*s)) s++;
     }
@@ -825,6 +895,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                  *
 *************************************************/
@@ -833,6 +1012,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
@@ -844,8 +1026,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;
@@ -895,99 +1075,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);
+/* 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. */
 
-  /* 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 */
-
-    p = q;
-    }
-  }
+setup_header(user_message);
 }
 
 
@@ -1994,6 +2085,7 @@ 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
@@ -2003,7 +2095,7 @@ Returns:       OK        - Sender's rate is above limit
 */
 
 static int
-acl_ratelimit(uschar *arg, uschar **log_msgptr)
+acl_ratelimit(uschar *arg, int where, uschar **log_msgptr)
 {
 double limit, period;
 uschar *ss, *key;
@@ -2222,6 +2314,9 @@ else
   if (per_byte)
     dbd->rate = (message_size < 0 ? 0.0 : (double)message_size)
               * (1 - a) / i_over_p + a * dbd->rate;
+  else if (per_cmd && where == ACL_WHERE_NOTSMTP)
+    dbd->rate = (double)recipients_count
+              * (1 - a) / i_over_p + a * dbd->rate;
   else
     dbd->rate = (1 - a) / i_over_p + a * dbd->rate;
   }
@@ -2303,7 +2398,7 @@ acl_check_condition(int verb, acl_condition_block *cb, int where,
 {
 uschar *user_message = NULL;
 uschar *log_message = NULL;
-uschar *p;
+uschar *p = NULL;
 int rc = OK;
 #ifdef WITH_CONTENT_SCAN
 int sep = '/';
@@ -2366,11 +2461,8 @@ for (; cb != NULL; cb = cb->next)
 
     if (cb->type == ACLC_SET)
       {
-      int n = cb->u.varnumber;
-      int t = (n < ACL_CVARS)? 'c' : 'm';
-      if (n >= ACL_CVARS) n -= ACL_CVARS;
-      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);
@@ -2395,9 +2487,13 @@ 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. */
-\
+
     case ACLC_ACL:
     rc = acl_check_internal(where, addr, arg, level+1, user_msgptr, log_msgptr);
     if (rc == DISCARD && verb != ACL_ACCEPT && verb != ACL_DISCARD)
@@ -2415,7 +2511,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;
@@ -2424,7 +2520,7 @@ for (; cb != NULL; cb = cb->next)
       store_pool = old_pool;
       }
     break;
-#endif
+    #endif
 
     case ACLC_CONDITION:
     if (Ustrspn(arg, "0123456789") == Ustrlen(arg))     /* Digits, or empty */
@@ -2577,11 +2673,11 @@ for (; cb != NULL; cb = cb->next)
       }
     break;
 
-#ifdef WITH_CONTENT_SCAN
+    #ifdef WITH_CONTENT_SCAN
     case ACLC_DECODE:
     rc = mime_decode(&arg);
     break;
-#endif
+    #endif
 
     case ACLC_DELAY:
       {
@@ -2625,14 +2721,14 @@ for (; cb != NULL; cb = cb->next)
       }
     break;
 
-#ifdef WITH_OLD_DEMIME
+    #ifdef WITH_OLD_DEMIME
     case ACLC_DEMIME:
       rc = demime(&arg);
     break;
-#endif
+    #endif
 
-#ifdef EXPERIMENTAL_DOMAINKEYS
-  case ACLC_DK_DOMAIN_SOURCE:
+    #ifdef EXPERIMENTAL_DOMAINKEYS
+    case ACLC_DK_DOMAIN_SOURCE:
     if (dk_verify_block == NULL) { rc = FAIL; break; };
     /* check header source of domain against given string */
     switch (dk_verify_block->address_source) {
@@ -2648,9 +2744,10 @@ for (; cb != NULL; cb = cb->next)
         rc = match_isinlist(US"none", &arg, 0, NULL,
                             NULL, MCL_STRING, TRUE, NULL);
       break;
-    }
-  break;
-  case ACLC_DK_POLICY:
+      }
+    break;
+
+    case ACLC_DK_POLICY:
     if (dk_verify_block == NULL) { rc = FAIL; break; };
     /* check policy against given string, default FAIL */
     rc = FAIL;
@@ -2660,28 +2757,32 @@ for (; cb != NULL; cb = cb->next)
     if (dk_verify_block->testing)
       rc = match_isinlist(US"testing", &arg, 0, NULL,
                           NULL, MCL_STRING, TRUE, NULL);
-  break;
-  case ACLC_DK_SENDER_DOMAINS:
+    break;
+
+    case ACLC_DK_SENDER_DOMAINS:
     if (dk_verify_block == NULL) { rc = FAIL; break; };
     if (dk_verify_block->domain != NULL)
       rc = match_isinlist(dk_verify_block->domain, &arg, 0, &domainlist_anchor,
                           NULL, MCL_DOMAIN, TRUE, NULL);
     else rc = FAIL;
-  break;
-  case ACLC_DK_SENDER_LOCAL_PARTS:
+    break;
+
+    case ACLC_DK_SENDER_LOCAL_PARTS:
     if (dk_verify_block == NULL) { rc = FAIL; break; };
     if (dk_verify_block->local_part != NULL)
       rc = match_isinlist(dk_verify_block->local_part, &arg, 0, &localpartlist_anchor,
                           NULL, MCL_LOCALPART, TRUE, NULL);
     else rc = FAIL;
-  break;
-  case ACLC_DK_SENDERS:
+    break;
+
+    case ACLC_DK_SENDERS:
     if (dk_verify_block == NULL) { rc = FAIL; break; };
     if (dk_verify_block->address != NULL)
       rc = match_address_list(dk_verify_block->address, TRUE, TRUE, &arg, NULL, -1, 0, NULL);
     else rc = FAIL;
-  break;
-  case ACLC_DK_STATUS:
+    break;
+
+    case ACLC_DK_STATUS:
     if (dk_verify_block == NULL) { rc = FAIL; break; };
     if (dk_verify_block->result > 0) {
       switch(dk_verify_block->result) {
@@ -2713,10 +2814,10 @@ for (; cb != NULL; cb = cb->next)
           rc = match_isinlist(US"bad", &arg, 0, NULL,
                               NULL, MCL_STRING, TRUE, NULL);
         break;
+        }
       }
-    }
-  break;
-#endif
+    break;
+    #endif
 
     case ACLC_DNSLISTS:
     rc = verify_check_dnsbl(&arg);
@@ -2767,6 +2868,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;
@@ -2793,15 +2917,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);
@@ -2818,12 +2944,12 @@ for (; cb != NULL; cb = cb->next)
     break;
 
     case ACLC_MIME_REGEX:
-      rc = mime_regex(&arg);
+    rc = mime_regex(&arg);
     break;
-#endif
+    #endif
 
     case ACLC_RATELIMIT:
-      rc = acl_ratelimit(arg, log_msgptr);
+    rc = acl_ratelimit(arg, where, log_msgptr);
     break;
 
     case ACLC_RECIPIENTS:
@@ -2831,11 +2957,11 @@ for (; cb != NULL; cb = cb->next)
       &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:
       {
@@ -2857,13 +2983,13 @@ for (; cb != NULL; cb = cb->next)
     case ACLC_SET:
       {
       int old_pool = store_pool;
-      if (cb->u.varnumber < ACL_CVARS) 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. */
@@ -2881,13 +3007,13 @@ for (; cb != NULL; cb = cb->next)
         }
       }
     break;
-#endif
+    #endif
 
-#ifdef EXPERIMENTAL_SPF
+    #ifdef EXPERIMENTAL_SPF
     case ACLC_SPF:
       rc = spf_process(&arg, sender_address);
     break;
-#endif
+    #endif
 
     /* If the verb is WARN, discard any user message from verification, because
     such messages are SMTP responses, not header additions. The latter come
@@ -2920,13 +3046,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.
@@ -2934,23 +3055,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)
     {
@@ -3436,6 +3563,7 @@ 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)
   {
@@ -3479,10 +3607,10 @@ 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)
+if (*user_msgptr != NULL && Ustrlen(*user_msgptr) > 75)
   {
   uschar *s = *user_msgptr = string_copy(*user_msgptr);
   uschar *ss = s;
@@ -3528,4 +3656,64 @@ if (rc != OK && *user_msgptr != NULL && Ustrlen(*user_msgptr) > 75)
 return rc;
 }
 
+
+
+/*************************************************
+*             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;
+}
+
+
+
+/*************************************************
+*       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 */