SPDX: Mass-update to GPL-2.0-or-later
[exim.git] / src / src / acl.c
index e2356e18859d11b3a725f7981979bf2d6a57289a..143890668fa6cd1c350bc85ba8f8d0d628e3bd07 100644 (file)
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Code for handling Access Control Lists (ACLs) */
 
@@ -734,6 +735,78 @@ return -1;
 }
 
 
+static BOOL
+acl_varname_to_cond(const uschar ** sp, acl_condition_block * cond, uschar ** error)
+{
+const uschar * s = *sp, * endptr;
+
+#ifndef DISABLE_DKIM
+if (  Ustrncmp(s, "dkim_verify_status", 18) == 0
+   || Ustrncmp(s, "dkim_verify_reason", 18) == 0)
+  {
+  endptr = s+18;
+  if (isalnum(*endptr))
+    {
+    *error = string_sprintf("invalid variable name after \"set\" in ACL "
+      "modifier \"set %s\" "
+      "(only \"dkim_verify_status\" or \"dkim_verify_reason\" permitted)",
+      s);
+    return FALSE;
+    }
+  cond->u.varname = string_copyn(s, 18);
+  }
+else
+#endif
+  {
+  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 FALSE;
+    }
+
+  endptr = s + 5;
+  if (!isdigit(*endptr) && *endptr != '_')
+    {
+    *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 FALSE;
+    }
+
+  for ( ; *endptr && *endptr != '=' && !isspace(*endptr); endptr++)
+    if (!isalnum(*endptr) && *endptr != '_')
+      {
+      *error = string_sprintf("invalid character \"%c\" in variable name "
+       "in ACL modifier \"set %s\"", *endptr, s);
+      return FALSE;
+      }
+
+  cond->u.varname = string_copyn(s + 4, endptr - s - 4);
+  }
+s = endptr;
+Uskip_whitespace(&s);
+*sp = s;
+return TRUE;
+}
+
+
+static BOOL
+acl_data_to_cond(const uschar * s, acl_condition_block * cond,
+  const uschar * name, uschar ** error)
+{
+if (*s++ != '=')
+  {
+  *error = string_sprintf("\"=\" missing after ACL \"%s\" %s", name,
+    conditions[cond->type].is_modifier ? US"modifier" : US"condition");
+  return FALSE;;
+  }
+Uskip_whitespace(&s);
+cond->arg = string_copy(s);
+return TRUE;
+}
+
+
 /*************************************************
 *            Read and parse one ACL              *
 *************************************************/
@@ -760,7 +833,7 @@ acl_block **lastp = &yield;
 acl_block *this = NULL;
 acl_condition_block *cond;
 acl_condition_block **condp = NULL;
-uschar * s;
+const uschar * s;
 
 *error = NULL;
 
@@ -768,7 +841,7 @@ while ((s = (*func)()))
   {
   int v, c;
   BOOL negated = FALSE;
-  uschar *saveline = s;
+  const uschar * saveline = s;
   uschar name[EXIM_DRIVERNAME_MAX];
 
   /* Conditions (but not verbs) are allowed to be negated by an initial
@@ -808,16 +881,15 @@ while ((s = (*func)()))
       *error = string_sprintf("malformed ACL line \"%s\"", saveline);
       return NULL;
       }
-    this = store_get(sizeof(acl_block), GET_UNTAINTED);
-    *lastp = this;
-    lastp = &(this->next);
+    *lastp = this = store_get(sizeof(acl_block), GET_UNTAINTED);
+    lastp = &this->next;
     this->next = NULL;
     this->condition = NULL;
     this->verb = v;
     this->srcline = config_lineno;     /* for debug output */
     this->srcfile = config_filename;   /**/
-    condp = &(this->condition);
-    if (*s == 0) continue;               /* No condition on this line */
+    condp = &this->condition;
+    if (!*s) continue;               /* No condition on this line */
     if (*s == '!')
       {
       negated = TRUE;
@@ -861,7 +933,7 @@ while ((s = (*func)()))
   cond->u.negated = negated;
 
   *condp = cond;
-  condp = &(cond->next);
+  condp = &cond->next;
 
   /* 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
@@ -874,75 +946,13 @@ while ((s = (*func)()))
   compatibility. */
 
   if (c == ACLC_SET)
-#ifndef DISABLE_DKIM
-    if (  Ustrncmp(s, "dkim_verify_status", 18) == 0
-       || Ustrncmp(s, "dkim_verify_reason", 18) == 0)
-      {
-      uschar * endptr = s+18;
-
-      if (isalnum(*endptr))
-       {
-       *error = string_sprintf("invalid variable name after \"set\" in ACL "
-         "modifier \"set %s\" "
-         "(only \"dkim_verify_status\" or \"dkim_verify_reason\" permitted)",
-         s);
-       return NULL;
-       }
-      cond->u.varname = string_copyn(s, 18);
-      s = endptr;
-      Uskip_whitespace(&s);
-      }
-    else
-#endif
-    {
-    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("invalid variable name after \"set\" in ACL "
-       "modifier \"set %s\" (digit or underscore must follow acl_c or acl_m)",
-       s);
-      return NULL;
-      }
-
-    while (*endptr && *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;
-    Uskip_whitespace(&s);
-    }
+    if (!acl_varname_to_cond(&s, cond, error)) return NULL;
 
   /* For "set", we are now positioned for the data. For the others, only
   "endpass" has no data */
 
   if (c != ACLC_ENDPASS)
-    {
-    if (*s++ != '=')
-      {
-      *error = string_sprintf("\"=\" missing after ACL \"%s\" %s", name,
-        conditions[c].is_modifier ? US"modifier" : US"condition");
-      return NULL;
-      }
-    Uskip_whitespace(&s);
-    cond->arg = string_copy(s);
-    }
+    if (!acl_data_to_cond(s, cond, name, error)) return NULL;
   }
 
 return yield;
@@ -3116,17 +3126,15 @@ acl_check_condition(int verb, acl_condition_block *cb, int where,
   address_item *addr, int level, BOOL *epp, uschar **user_msgptr,
   uschar **log_msgptr, int *basic_errno)
 {
-uschar *user_message = NULL;
-uschar *log_message = NULL;
+uschar * user_message = NULL;
+uschar * log_message = NULL;
 int rc = OK;
-#ifdef WITH_CONTENT_SCAN
-int sep = -'/';
-#endif
 
 for (; cb; cb = cb->next)
   {
-  const uschar *arg;
+  const uschar * arg;
   int control_type;
+  BOOL textonly = FALSE;
 
   /* The message and log_message items set up messages to be used in
   case of rejection. They are expanded later. */
@@ -3160,7 +3168,8 @@ for (; cb; cb = cb->next)
 
   if (!conditions[cb->type].expand_at_top)
     arg = cb->arg;
-  else if (!(arg = expand_string(cb->arg)))
+
+  else if (!(arg = expand_string_2(cb->arg, &textonly)))
     {
     if (f.expand_string_forcedfail) continue;
     *log_msgptr = string_sprintf("failed to expand ACL string \"%s\": %s",
@@ -3217,8 +3226,8 @@ for (; cb; cb = cb->next)
   switch(cb->type)
     {
     case ACLC_ADD_HEADER:
-    setup_header(arg);
-    break;
+      setup_header(arg);
+      break;
 
     /* A nested ACL that returns "discard" makes sense only for an "accept" or
     "discard" verb. */
@@ -3232,12 +3241,12 @@ for (; cb; cb = cb->next)
           verbs[verb]);
         return ERROR;
         }
-    break;
+      break;
 
     case ACLC_AUTHENTICATED:
       rc = sender_host_authenticated ? match_isinlist(sender_host_authenticated,
              &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL) : FAIL;
-    break;
+      break;
 
     #ifdef EXPERIMENTAL_BRIGHTMAIL
     case ACLC_BMI_OPTIN:
@@ -3254,21 +3263,21 @@ for (; cb; cb = cb->next)
     /* 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 (*arg == '-'
-       ? Ustrspn(arg+1, "0123456789") == Ustrlen(arg+1)    /* Negative number */
-       : Ustrspn(arg,   "0123456789") == Ustrlen(arg))     /* Digits, or empty */
-      rc = (Uatoi(arg) == 0)? FAIL : OK;
-    else
-      rc = (strcmpic(arg, US"no") == 0 ||
-            strcmpic(arg, US"false") == 0)? FAIL :
-           (strcmpic(arg, US"yes") == 0 ||
-            strcmpic(arg, US"true") == 0)? OK : DEFER;
-    if (rc == DEFER)
-      *log_msgptr = string_sprintf("invalid \"condition\" value \"%s\"", arg);
-    break;
+      if (*arg == '-'
+         ? Ustrspn(arg+1, "0123456789") == Ustrlen(arg+1)    /* Negative number */
+         : Ustrspn(arg,   "0123456789") == Ustrlen(arg))     /* Digits, or empty */
+       rc = (Uatoi(arg) == 0)? FAIL : OK;
+      else
+       rc = (strcmpic(arg, US"no") == 0 ||
+             strcmpic(arg, US"false") == 0)? FAIL :
+            (strcmpic(arg, US"yes") == 0 ||
+             strcmpic(arg, US"true") == 0)? OK : DEFER;
+      if (rc == DEFER)
+       *log_msgptr = string_sprintf("invalid \"condition\" value \"%s\"", arg);
+      break;
 
     case ACLC_CONTINUE:    /* Always succeeds */
-    break;
+      break;
 
     case ACLC_CONTROL:
       {
@@ -3481,8 +3490,7 @@ for (; cb; cb = cb->next)
 
        case CONTROL_DEBUG:
          {
-         uschar * debug_tag = NULL;
-         uschar * debug_opts = NULL;
+         uschar * debug_tag = NULL, * debug_opts = NULL;
          BOOL kill = FALSE, stop = FALSE;
 
          while (*p == '/')
@@ -3636,27 +3644,28 @@ for (; cb; cb = cb->next)
       break;
       }
 
-    #ifdef EXPERIMENTAL_DCC
+#ifdef EXPERIMENTAL_DCC
     case ACLC_DCC:
       {
       /* Separate the regular expression and any optional parameters. */
       const uschar * list = arg;
-      uschar *ss = string_nextinlist(&list, &sep, NULL, 0);
+      int sep = -'/';
+      uschar * ss = string_nextinlist(&list, &sep, NULL, 0);
       /* Run the dcc backend. */
       rc = dcc_process(&ss);
       /* Modify return code based upon the existence of options. */
       while ((ss = string_nextinlist(&list, &sep, NULL, 0)))
         if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER)
           rc = FAIL;   /* FAIL so that the message is passed to the next ACL */
+      break;
       }
-    break;
-    #endif
+#endif
 
-    #ifdef WITH_CONTENT_SCAN
+#ifdef WITH_CONTENT_SCAN
     case ACLC_DECODE:
-    rc = mime_decode(&arg);
-    break;
-    #endif
+      rc = mime_decode(&arg);
+      break;
+#endif
 
     case ACLC_DELAY:
       {
@@ -3720,44 +3729,44 @@ for (; cb; cb = cb->next)
 #endif
           }
         }
+      break;
       }
-    break;
 
 #ifndef DISABLE_DKIM
     case ACLC_DKIM_SIGNER:
-    if (dkim_cur_signer)
-      rc = match_isinlist(dkim_cur_signer,
+      if (dkim_cur_signer)
+       rc = match_isinlist(dkim_cur_signer,
                           &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
-    else
-      rc = FAIL;
-    break;
+      else
+       rc = FAIL;
+      break;
 
     case ACLC_DKIM_STATUS:
-    rc = match_isinlist(dkim_verify_status,
-                        &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
-    break;
+      rc = match_isinlist(dkim_verify_status,
+                         &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
+      break;
 #endif
 
 #ifdef SUPPORT_DMARC
     case ACLC_DMARC_STATUS:
-    if (!f.dmarc_has_been_checked)
-      dmarc_process();
-    f.dmarc_has_been_checked = TRUE;
-    /* used long way of dmarc_exim_expand_query() in case we need more
-     * view into the process in the future. */
-    rc = match_isinlist(dmarc_exim_expand_query(DMARC_VERIFY_STATUS),
-                        &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
-    break;
+      if (!f.dmarc_has_been_checked)
+       dmarc_process();
+      f.dmarc_has_been_checked = TRUE;
+      /* used long way of dmarc_exim_expand_query() in case we need more
+       * view into the process in the future. */
+      rc = match_isinlist(dmarc_exim_expand_query(DMARC_VERIFY_STATUS),
+                         &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
+      break;
 #endif
 
     case ACLC_DNSLISTS:
-    rc = verify_check_dnsbl(where, &arg, log_msgptr);
-    break;
+      rc = verify_check_dnsbl(where, &arg, log_msgptr);
+      break;
 
     case ACLC_DOMAINS:
-    rc = match_isinlist(addr->domain, &arg, 0, &domainlist_anchor,
-      addr->domain_cache, MCL_DOMAIN, TRUE, CUSS &deliver_domain_data);
-    break;
+      rc = match_isinlist(addr->domain, &arg, 0, &domainlist_anchor,
+       addr->domain_cache, MCL_DOMAIN, TRUE, CUSS &deliver_domain_data);
+      break;
 
     /* The value in tls_cipher is the full cipher name, for example,
     TLSv1:DES-CBC3-SHA:168, whereas the values to test for are just the
@@ -3766,19 +3775,20 @@ for (; cb; cb = cb->next)
     writing is poorly documented. */
 
     case ACLC_ENCRYPTED:
-    if (tls_in.cipher == NULL) rc = FAIL; else
-      {
-      uschar *endcipher = NULL;
-      uschar *cipher = Ustrchr(tls_in.cipher, ':');
-      if (!cipher) cipher = tls_in.cipher; else
-        {
-        endcipher = Ustrchr(++cipher, ':');
-        if (endcipher) *endcipher = 0;
-        }
-      rc = match_isinlist(cipher, &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
-      if (endcipher) *endcipher = ':';
-      }
-    break;
+      if (!tls_in.cipher) rc = FAIL;
+      else
+       {
+       uschar *endcipher = NULL;
+       uschar *cipher = Ustrchr(tls_in.cipher, ':');
+       if (!cipher) cipher = tls_in.cipher; else
+         {
+         endcipher = Ustrchr(++cipher, ':');
+         if (endcipher) *endcipher = 0;
+         }
+       rc = match_isinlist(cipher, &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
+       if (endcipher) *endcipher = ':';
+       }
+      break;
 
     /* Use verify_check_this_host() instead of verify_check_host() so that
     we can pass over &host_data to catch any looked up data. Once it has been
@@ -3788,25 +3798,24 @@ for (; cb; cb = cb->next)
     message in the same SMTP connection. */
 
     case ACLC_HOSTS:
-    rc = verify_check_this_host(&arg, sender_host_cache, NULL,
-      sender_host_address ? sender_host_address : US"", CUSS &host_data);
-    if (rc == DEFER) *log_msgptr = search_error_message;
-    if (host_data) host_data = string_copy_perm(host_data, TRUE);
-    break;
+      rc = verify_check_this_host(&arg, sender_host_cache, NULL,
+       sender_host_address ? sender_host_address : US"", CUSS &host_data);
+      if (rc == DEFER) *log_msgptr = search_error_message;
+      if (host_data) host_data = string_copy_perm(host_data, TRUE);
+      break;
 
     case ACLC_LOCAL_PARTS:
-    rc = match_isinlist(addr->cc_local_part, &arg, 0,
-      &localpartlist_anchor, addr->localpart_cache, MCL_LOCALPART, TRUE,
-      CUSS &deliver_localpart_data);
-    break;
+      rc = match_isinlist(addr->cc_local_part, &arg, 0,
+       &localpartlist_anchor, addr->localpart_cache, MCL_LOCALPART, TRUE,
+       CUSS &deliver_localpart_data);
+      break;
 
     case ACLC_LOG_REJECT_TARGET:
       {
-      int logbits = 0;
-      int sep = 0;
-      const uschar *s = arg;
-      uschar * ss;
-      while ((ss = string_nextinlist(&s, &sep, NULL, 0)))
+      int logbits = 0, sep = 0;
+      const uschar * s = arg;
+
+      for (uschar * ss; ss = string_nextinlist(&s, &sep, NULL, 0); )
         {
         if (Ustrcmp(ss, "main") == 0) logbits |= LOG_MAIN;
         else if (Ustrcmp(ss, "panic") == 0) logbits |= LOG_PANIC;
@@ -3819,8 +3828,8 @@ for (; cb; cb = cb->next)
           }
         }
       log_reject_target = logbits;
+      break;
       }
-    break;
 
     case ACLC_LOGWRITE:
       {
@@ -3851,20 +3860,19 @@ for (; cb; cb = cb->next)
 
       if (logbits == 0) logbits = LOG_MAIN;
       log_write(0, logbits, "%s", string_printing(s));
+      break;
       }
-    break;
 
-    #ifdef WITH_CONTENT_SCAN
+#ifdef WITH_CONTENT_SCAN
     case ACLC_MALWARE:                 /* Run the malware backend. */
       {
       /* Separate the regular expression and any optional parameters. */
       const uschar * list = arg;
-      uschar * ss = string_nextinlist(&list, &sep, NULL, 0);
-      uschar * opt;
       BOOL defer_ok = FALSE;
-      int timeout = 0;
+      int timeout = 0, sep = -'/';
+      uschar * ss = string_nextinlist(&list, &sep, NULL, 0);
 
-      while ((opt = string_nextinlist(&list, &sep, NULL, 0)))
+      for (uschar * opt; opt = string_nextinlist(&list, &sep, NULL, 0); )
         if (strcmpic(opt, US"defer_ok") == 0)
          defer_ok = TRUE;
        else if (  strncmpic(opt, US"tmo=", 4) == 0
@@ -3875,55 +3883,55 @@ for (; cb; cb = cb->next)
          return ERROR;
          }
 
-      rc = malware(ss, timeout);
+      rc = malware(ss, textonly, timeout);
       if (rc == DEFER && defer_ok)
        rc = FAIL;      /* FAIL so that the message is passed to the next ACL */
+      break;
       }
-    break;
 
     case ACLC_MIME_REGEX:
-    rc = mime_regex(&arg);
-    break;
-    #endif
+      rc = mime_regex(&arg, textonly);
+      break;
+#endif
 
     case ACLC_QUEUE:
-    if (is_tainted(arg))
-      {
-      *log_msgptr = string_sprintf("Tainted name '%s' for queue not permitted",
-                                   arg);
-      return ERROR;
-      }
-    if (Ustrchr(arg, '/'))
-      {
-      *log_msgptr = string_sprintf(
-             "Directory separator not permitted in queue name: '%s'", arg);
-      return ERROR;
-      }
-    queue_name = string_copy_perm(arg, FALSE);
-    break;
+      if (is_tainted(arg))
+       {
+       *log_msgptr = string_sprintf("Tainted name '%s' for queue not permitted",
+                                     arg);
+       return ERROR;
+       }
+      if (Ustrchr(arg, '/'))
+       {
+       *log_msgptr = string_sprintf(
+               "Directory separator not permitted in queue name: '%s'", arg);
+       return ERROR;
+       }
+      queue_name = string_copy_perm(arg, FALSE);
+      break;
 
     case ACLC_RATELIMIT:
-    rc = acl_ratelimit(arg, where, log_msgptr);
-    break;
+      rc = acl_ratelimit(arg, where, log_msgptr);
+      break;
 
     case ACLC_RECIPIENTS:
-    rc = match_address_list(CUS addr->address, TRUE, TRUE, &arg, NULL, -1, 0,
-      CUSS &recipient_data);
-    break;
+      rc = match_address_list(CUS addr->address, TRUE, TRUE, &arg, NULL, -1, 0,
+       CUSS &recipient_data);
+      break;
 
     #ifdef WITH_CONTENT_SCAN
     case ACLC_REGEX:
-    rc = regex(&arg);
-    break;
+      rc = regex(&arg, textonly);
+      break;
     #endif
 
     case ACLC_REMOVE_HEADER:
-    setup_remove_header(arg);
-    break;
+      setup_remove_header(arg);
+      break;
 
     case ACLC_SEEN:
-    rc = acl_seen(arg, where, log_msgptr);
-    break;
+      rc = acl_seen(arg, where, log_msgptr);
+      break;
 
     case ACLC_SENDER_DOMAINS:
       {
@@ -3932,13 +3940,13 @@ for (; cb; cb = cb->next)
       sdomain = sdomain ? sdomain + 1 : US"";
       rc = match_isinlist(sdomain, &arg, 0, &domainlist_anchor,
         sender_domain_cache, MCL_DOMAIN, TRUE, NULL);
+      break;
       }
-    break;
 
     case ACLC_SENDERS:
-    rc = match_address_list(CUS sender_address, TRUE, TRUE, &arg,
-      sender_address_cache, -1, 0, CUSS &sender_data);
-    break;
+      rc = match_address_list(CUS sender_address, TRUE, TRUE, &arg,
+       sender_address_cache, -1, 0, CUSS &sender_data);
+      break;
 
     /* Connection variables must persist forever; message variables not */
 
@@ -3960,37 +3968,39 @@ for (; cb; cb = cb->next)
 #endif
        acl_var_create(cb->u.varname)->data.ptr = string_copy(arg);
       store_pool = old_pool;
+      break;
       }
-    break;
 
 #ifdef WITH_CONTENT_SCAN
     case ACLC_SPAM:
       {
       /* Separate the regular expression and any optional parameters. */
       const uschar * list = arg;
-      uschar *ss = string_nextinlist(&list, &sep, NULL, 0);
+      int sep = -'/';
+      uschar * ss = string_nextinlist(&list, &sep, NULL, 0);
 
       rc = spam(CUSS &ss);
       /* Modify return code based upon the existence of options. */
       while ((ss = string_nextinlist(&list, &sep, NULL, 0)))
         if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER)
           rc = FAIL;   /* FAIL so that the message is passed to the next ACL */
+      break;
       }
-    break;
 #endif
 
 #ifdef SUPPORT_SPF
     case ACLC_SPF:
       rc = spf_process(&arg, sender_address, SPF_PROCESS_NORMAL);
-    break;
+      break;
+
     case ACLC_SPF_GUESS:
       rc = spf_process(&arg, sender_address, SPF_PROCESS_GUESS);
-    break;
+      break;
 #endif
 
     case ACLC_UDPSEND:
-    rc = acl_udpsend(arg, log_msgptr);
-    break;
+      rc = acl_udpsend(arg, log_msgptr);
+      break;
 
     /* If the verb is WARN, discard any user message from verification, because
     such messages are SMTP responses, not header additions. The latter come
@@ -3999,16 +4009,16 @@ for (; cb; cb = cb->next)
     (until something changes it). */
 
     case ACLC_VERIFY:
-    rc = acl_verify(where, addr, arg, user_msgptr, log_msgptr, basic_errno);
-    if (*user_msgptr)
-      acl_verify_message = *user_msgptr;
-    if (verb == ACL_WARN) *user_msgptr = NULL;
-    break;
+      rc = acl_verify(where, addr, arg, user_msgptr, log_msgptr, basic_errno);
+      if (*user_msgptr)
+       acl_verify_message = *user_msgptr;
+      if (verb == ACL_WARN) *user_msgptr = NULL;
+      break;
 
     default:
-    log_write(0, LOG_MAIN|LOG_PANIC_DIE, "internal ACL error: unknown "
-      "condition %d", cb->type);
-    break;
+      log_write(0, LOG_MAIN|LOG_PANIC_DIE, "internal ACL error: unknown "
+       "condition %d", cb->type);
+      break;
     }
 
   /* If a condition was negated, invert OK/FAIL. */
@@ -4891,6 +4901,29 @@ if (is_tainted(value))
 fprintf(f, "acl%c %s %d\n%s\n", name[0], name+1, Ustrlen(value), value);
 }
 
+
+
+
+uschar *
+acl_standalone_setvar(const uschar * s)
+{
+acl_condition_block * cond = store_get(sizeof(acl_condition_block), GET_UNTAINTED);
+uschar * errstr = NULL, * log_msg = NULL;
+BOOL endpass_seen;
+int e;
+
+cond->next = NULL;
+cond->type = ACLC_SET;
+if (!acl_varname_to_cond(&s, cond, &errstr)) return errstr;
+if (!acl_data_to_cond(s, cond, US"'-be'", &errstr)) return errstr;
+
+if (acl_check_condition(ACL_WARN, cond, ACL_WHERE_UNKNOWN,
+                           NULL, 0, &endpass_seen, &errstr, &log_msg, &e) != OK)
+  return string_sprintf("oops: %s", errstr);
+return string_sprintf("variable %s set", cond->u.varname);
+}
+
+
 #endif /* !MACRO_PREDEF */
 /* vi: aw ai sw=2
 */