Testsuite: perl version oddity
[exim.git] / src / src / filter.c
index 1773a8f2ff805c97e2dae540d18c5d2ff76034b5..8f29eda3e2b66c09ee9b28ef01f37297f7140100 100644 (file)
@@ -1,11 +1,11 @@
-/* $Cambridge: exim/src/src/filter.c,v 1.7 2005/11/11 10:02:04 ph10 Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2005 */
+/* Copyright (c) The Exim Maintainers 2020 - 2023 */
+/* 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 mail filtering functions. */
@@ -28,7 +28,7 @@ union argtypes {
   struct condition_block *c;
   struct filter_cmd      *f;
   int                     i;
-  uschar                 *u;
+  const uschar            *u;
 };
 
 /* Local structures used in this module */
@@ -52,7 +52,7 @@ typedef struct condition_block {
 /* Miscellaneous other declarations */
 
 static uschar **error_pointer;
-static uschar *log_filename;
+static const uschar *log_filename;
 static int  filter_options;
 static int  line_number;
 static int  expect_endif;
@@ -68,37 +68,8 @@ static BOOL noerror_force;
 
 enum { had_neither, had_else, had_elif, had_endif };
 
-static BOOL read_command_list(uschar **, filter_cmd ***, BOOL);
-
-
-/* The string arguments for the mail command. The header line ones (that are
-permitted to include \n followed by white space) first, and then the body text
-one (it can have \n anywhere). Then the file names and once_repeat, which may
-not contain \n. */
-
-static char *mailargs[] = {  /* "to" must be first, and */
-  "to",                      /* "cc" and "bcc" must follow */
-  "cc",
-  "bcc",
-  "from",
-  "reply_to",
-  "subject",
-  "extra_headers",           /* miscellaneous added header lines */
-  "text",
-  "file",
-  "log",
-  "once",
-  "once_repeat"
-};
-
-/* The count of string arguments */
-
-#define MAILARGS_STRING_COUNT (sizeof(mailargs)/sizeof(uschar *))
-
-/* The count of string arguments that are actually passed over as strings
-(once_repeat is converted to an int). */
+static BOOL read_command_list(const uschar **, filter_cmd ***, BOOL);
 
-#define mailargs_string_passed (MAILARGS_STRING_COUNT - 1)
 
 /* This defines the offsets for the arguments; first the string ones, and
 then the non-string ones. The order must be as above. */
@@ -120,21 +91,50 @@ enum { mailarg_index_to,
        mailargs_total              /* total number of arguments */
        };
 
+/* The string arguments for the mail command. The header line ones (that are
+permitted to include \n followed by white space) first, and then the body text
+one (it can have \n anywhere). Then the file names and once_repeat, which may
+not contain \n. */
+
+static const char *mailargs[] = {  /* "to" must be first, and */
+  [mailarg_index_to] = "to",       /* "cc" and "bcc" must follow */
+  [mailarg_index_cc] = "cc",
+  [mailarg_index_bcc] = "bcc",
+  [mailarg_index_from] = "from",
+  [mailarg_index_reply_to] = "reply_to",
+  [mailarg_index_subject] = "subject",
+  [mailarg_index_headers] = "extra_headers", /* misc added header lines */
+  [mailarg_index_text] = "text",
+  [mailarg_index_file] = "file",
+  [mailarg_index_log] = "log",
+  [mailarg_index_once] = "once",
+  [mailarg_index_once_repeat] = "once_repeat"
+};
+
+/* The count of string arguments */
+
+#define MAILARGS_STRING_COUNT (nelem(mailargs))
+
+/* The count of string arguments that are actually passed over as strings
+(once_repeat is converted to an int). */
+
+#define mailargs_string_passed (MAILARGS_STRING_COUNT - 1)
+
 /* Offsets in the data structure for the string arguments (note that
 once_repeat isn't a string argument at this point.) */
 
-static int reply_offsets[] = {  /* must be in same order as above */
-  offsetof(reply_item, to),
-  offsetof(reply_item, cc),
-  offsetof(reply_item, bcc),
-  offsetof(reply_item, from),
-  offsetof(reply_item, reply_to),
-  offsetof(reply_item, subject),
-  offsetof(reply_item, headers),
-  offsetof(reply_item, text),
-  offsetof(reply_item, file),
-  offsetof(reply_item, logfile),
-  offsetof(reply_item, oncelog),
+static int reply_offsets[] = {
+  [mailarg_index_to] = offsetof(reply_item, to),
+  [mailarg_index_cc] = offsetof(reply_item, cc),
+  [mailarg_index_bcc] = offsetof(reply_item, bcc),
+  [mailarg_index_from] = offsetof(reply_item, from),
+  [mailarg_index_reply_to] = offsetof(reply_item, reply_to),
+  [mailarg_index_subject] = offsetof(reply_item, subject),
+  [mailarg_index_headers] = offsetof(reply_item, headers),
+  [mailarg_index_text] = offsetof(reply_item, text),
+  [mailarg_index_file] = offsetof(reply_item, file),
+  [mailarg_index_log] = offsetof(reply_item, logfile),
+  [mailarg_index_once] = offsetof(reply_item, oncelog),
 };
 
 /* Condition identities and names, with negated versions for some
@@ -146,26 +146,54 @@ enum { cond_and, cond_or, cond_personal, cond_begins, cond_BEGINS,
        cond_above, cond_below, cond_errormsg, cond_firsttime,
        cond_manualthaw, cond_foranyaddress };
 
-static char *cond_names[] = {
-  "and", "or", "personal",
-  "begins", "BEGINS", "ends", "ENDS",
-  "is", "IS", "matches", "MATCHES", "contains",
-  "CONTAINS", "delivered", "above", "below", "error_message",
-  "first_delivery", "manually_thawed", "foranyaddress" };
-
-static char *cond_not_names[] = {
-  "", "", "not personal",
-  "does not begin", "does not BEGIN",
-  "does not end", "does not END",
-  "is not", "IS not", "does not match",
-  "does not MATCH", "does not contain", "does not CONTAIN",
-  "not delivered", "not above", "not below", "not error_message",
-  "not first_delivery", "not manually_thawed", "not foranyaddress" };
+static const char *cond_names[] = {
+  [cond_and] = "and",
+  [cond_or] = "or",
+  [cond_personal] = "personal",
+  [cond_begins] = "begins",
+  [cond_BEGINS] = "BEGINS",
+  [cond_ends] = "ends",
+  [cond_ENDS] = "ENDS",
+  [cond_is] = "is",
+  [cond_IS] = "IS",
+  [cond_matches] = "matches",
+  [cond_MATCHES] = "MATCHES",
+  [cond_contains] = "contains",
+  [cond_CONTAINS] = "CONTAINS",
+  [cond_delivered] = "delivered",
+  [cond_above] = "above",
+  [cond_below] = "below",
+  [cond_errormsg] = "error_message",
+  [cond_firsttime] = "first_delivery",
+  [cond_manualthaw] = "manually_thawed",
+  [cond_foranyaddress] = "foranyaddress" };
+
+static const char *cond_not_names[] = {
+  [cond_and] = "",
+  [cond_or] = "",
+  [cond_personal] = "not personal",
+  [cond_begins] = "does not begin",
+  [cond_BEGINS] = "does not BEGIN",
+  [cond_ends] = "does not end",
+  [cond_ENDS] = "does not END",
+  [cond_is] = "is not",
+  [cond_IS] = "IS not",
+  [cond_matches] = "does not match",
+  [cond_MATCHES] = "does not MATCH",
+  [cond_contains] = "does not contain",
+  [cond_CONTAINS] = "does not CONTAIN",
+  [cond_delivered] = "not delivered",
+  [cond_above] = "not above",
+  [cond_below] = "not below",
+  [cond_errormsg] = "not error_message",
+  [cond_firsttime] = "not first_delivery",
+  [cond_manualthaw] = "not manually_thawed",
+  [cond_foranyaddress] = "not foranyaddress" };
 
 /* Tables of binary condition words and their corresponding types. Not easy
 to amalgamate with the above because of the different variants. */
 
-static char *cond_words[] = {
+static const char *cond_words[] = {
    "BEGIN",
    "BEGINS",
    "CONTAIN",
@@ -187,55 +215,72 @@ static char *cond_words[] = {
    "match",
    "matches"};
 
-static int cond_word_count = (sizeof(cond_words)/sizeof(uschar *));
+static int cond_word_count = nelem(cond_words);
 
 static int cond_types[] = { cond_BEGINS, cond_BEGINS, cond_CONTAINS,
   cond_CONTAINS, cond_ENDS, cond_ENDS, cond_IS, cond_MATCHES, cond_MATCHES,
   cond_above, cond_begins, cond_begins, cond_below, cond_contains,
   cond_contains, cond_ends, cond_ends, cond_is, cond_matches, cond_matches };
 
-/* Command identities: must be kept in step with the list of command words
-and the list of expanded argument counts which follow. */
-
-enum { add_command, defer_command, deliver_command, elif_command, else_command,
-       endif_command, finish_command, fail_command, freeze_command,
-       headers_command, if_command, logfile_command, logwrite_command,
-       mail_command, noerror_command, pipe_command, save_command, seen_command,
-       testprint_command, unseen_command, vacation_command };
-
-static char *command_list[] = {
-  "add",     "defer",   "deliver", "elif", "else",      "endif",    "finish",
-  "fail",    "freeze",  "headers", "if",   "logfile",   "logwrite", "mail",
-  "noerror", "pipe",    "save",    "seen", "testprint", "unseen",   "vacation"
+/* Command identities */
+
+enum { ADD_COMMAND, DEFER_COMMAND, DELIVER_COMMAND, ELIF_COMMAND, ELSE_COMMAND,
+       ENDIF_COMMAND, FINISH_COMMAND, FAIL_COMMAND, FREEZE_COMMAND,
+       HEADERS_COMMAND, IF_COMMAND, LOGFILE_COMMAND, LOGWRITE_COMMAND,
+       MAIL_COMMAND, NOERROR_COMMAND, PIPE_COMMAND, SAVE_COMMAND, SEEN_COMMAND,
+       TESTPRINT_COMMAND, UNSEEN_COMMAND, VACATION_COMMAND };
+
+static const char * command_list[] = {
+  [ADD_COMMAND] =      "add",
+  [DEFER_COMMAND] =    "defer",
+  [DELIVER_COMMAND] =  "deliver",
+  [ELIF_COMMAND] =     "elif",
+  [ELSE_COMMAND] =     "else",
+  [ENDIF_COMMAND] =    "endif",
+  [FINISH_COMMAND] =   "finish",
+  [FAIL_COMMAND] =     "fail",
+  [FREEZE_COMMAND] =   "freeze",
+  [HEADERS_COMMAND] =  "headers",
+  [IF_COMMAND] =       "if",
+  [LOGFILE_COMMAND] =  "logfile",
+  [LOGWRITE_COMMAND] = "logwrite",
+  [MAIL_COMMAND] =     "mail",
+  [NOERROR_COMMAND] =  "noerror",
+  [PIPE_COMMAND] =     "pipe",
+  [SAVE_COMMAND] =     "save",
+  [SEEN_COMMAND] =     "seen",
+  [TESTPRINT_COMMAND] =        "testprint",
+  [UNSEEN_COMMAND] =   "unseen",
+  [VACATION_COMMAND] = "vacation"
 };
 
-static int command_list_count = sizeof(command_list)/sizeof(uschar *);
+static int command_list_count = nelem(command_list);
 
 /* This table contains the number of expanded arguments in the bottom 4 bits.
 If the top bit is set, it means that the default for the command is "seen". */
 
 static uschar command_exparg_count[] = {
-      2, /* add */
-      1, /* defer */
-  128+2, /* deliver */
-      0, /* elif */
-      0, /* else */
-      0, /* endif */
-      0, /* finish */
-      1, /* fail */
-      1, /* freeze */
-      1, /* headers */
-      0, /* if */
-      1, /* logfile */
-      1, /* logwrite */
-      MAILARGS_STRING_COUNT, /* mail */
-      0, /* noerror */
-  128+0, /* pipe */
-  128+1, /* save */
-      0, /* seen */
-      1, /* testprint */
-      0, /* unseen */
-      MAILARGS_STRING_COUNT /* vacation */
+  [ADD_COMMAND] =      2,
+  [DEFER_COMMAND] =    1,
+  [DELIVER_COMMAND] =  128+2,
+  [ELIF_COMMAND] =     0,
+  [ELSE_COMMAND] =     0,
+  [ENDIF_COMMAND] =    0,
+  [FINISH_COMMAND] =   0,
+  [FAIL_COMMAND] =     1,
+  [FREEZE_COMMAND] =   1,
+  [HEADERS_COMMAND] =  1,
+  [IF_COMMAND] =       0,
+  [LOGFILE_COMMAND] =  1,
+  [LOGWRITE_COMMAND] = 1,
+  [MAIL_COMMAND] =     MAILARGS_STRING_COUNT,
+  [NOERROR_COMMAND] =  0,
+  [PIPE_COMMAND] =     128+0,
+  [SAVE_COMMAND] =     128+1,
+  [SEEN_COMMAND] =     0,
+  [TESTPRINT_COMMAND] =        1,
+  [UNSEEN_COMMAND] =   0,
+  [VACATION_COMMAND] = MAILARGS_STRING_COUNT
 };
 
 
@@ -253,22 +298,17 @@ Arguments:
 Returns:           pointer to next non-whitespace character
 */
 
-static uschar *
-nextsigchar(uschar *ptr, BOOL comment_allowed)
+static const uschar *
+nextsigchar(const uschar *ptr, BOOL comment_allowed)
 {
 for (;;)
   {
   while (isspace(*ptr))
-    {
-    if (*ptr == '\n') line_number++;
-    ptr++;
-    }
+    if (*ptr++ == '\n') line_number++;
   if (comment_allowed && *ptr == '#')
-    {
-    while (*(++ptr) != '\n' && *ptr != 0);
-    continue;
-    }
-  else break;
+    while (*++ptr != '\n' && *ptr) ;
+  else
+    break;
   }
 return ptr;
 }
@@ -291,21 +331,22 @@ Arguments
 Returns:    pointer to the next significant character after the word
 */
 
-static uschar *
-nextword(uschar *ptr, uschar *buffer, int size, BOOL bracket)
+static const uschar *
+nextword(const uschar *ptr, uschar *buffer, int size, BOOL bracket)
 {
-uschar *bp = buffer;
-while (*ptr != 0 && !isspace(*ptr) &&
+uschar * bp = buffer;
+while (*ptr && !isspace(*ptr) &&
        (!bracket || (*ptr != '(' && *ptr != ')')))
-  {
-  if (bp - buffer < size - 1) *bp++ = *ptr++; else
+  if (bp - buffer < size - 1)
+    *bp++ = *ptr++;
+  else
     {
     *error_pointer = string_sprintf("word is too long in line %d of "
       "filter file (max = %d chars)", line_number, size);
     break;
     }
-  }
-*bp = 0;
+
+*bp = '\0';
 return nextsigchar(ptr, TRUE);
 }
 
@@ -327,13 +368,13 @@ Arguments:
 Returns:     the next significant character after the item
 */
 
-static uschar *
-nextitem(uschar *ptr, uschar *buffer, int size, BOOL bracket)
+static const uschar *
+nextitem(const uschar *ptr, uschar *buffer, int size, BOOL bracket)
 {
 uschar *bp = buffer;
 if (*ptr != '\"') return nextword(ptr, buffer, size, bracket);
 
-while (*(++ptr) != 0 && *ptr != '\"' && *ptr != '\n')
+while (*++ptr && *ptr != '\"' && *ptr != '\n')
   {
   if (bp - buffer >= size - 1)
     {
@@ -346,7 +387,7 @@ while (*(++ptr) != 0 && *ptr != '\"' && *ptr != '\n')
     {
     if (isspace(ptr[1]))    /* \<whitespace>NL<whitespace> ignored */
       {
-      uschar *p = ptr + 1;
+      const uschar *p = ptr + 1;
       while (*p != '\n' && isspace(*p)) p++;
       if (*p == '\n')
         {
@@ -357,7 +398,7 @@ while (*(++ptr) != 0 && *ptr != '\"' && *ptr != '\n')
         }
       }
 
-    *bp++ = string_interpret_escape(&ptr);
+    *bp++ = string_interpret_escape(CUSS &ptr);
     }
   }
 
@@ -386,15 +427,15 @@ Returns:   the number, or 0 on error (with *OK FALSE)
 */
 
 static int
-get_number(uschar *s, BOOL *ok)
+get_number(const uschar *s, BOOL *ok)
 {
 int value, count;
 *ok = FALSE;
 if (sscanf(CS s, "%i%n", &value, &count) != 1) return 0;
 if (tolower(s[count]) == 'k') { value *= 1024; count++; }
 if (tolower(s[count]) == 'm') { value *= 1024*1024; count++; }
-while (isspace((s[count]))) count++;
-if (s[count] != 0) return 0;
+while (isspace(s[count])) count++;
+if (s[count]) return 0;
 *ok = TRUE;
 return value;
 }
@@ -417,8 +458,8 @@ Arguments:
 Returns:          points to next character after "then"
 */
 
-static uschar *
-read_condition(uschar *ptr, condition_block **cond, BOOL toplevel)
+static const uschar *
+read_condition(const uschar *ptr, condition_block **cond, BOOL toplevel)
 {
 uschar buffer[1024];
 BOOL testfor = TRUE;
@@ -435,9 +476,9 @@ for (;;)
 
   /* reaching the end of the input is an error. */
 
-  if (*ptr == 0)
+  if (!*ptr)
     {
-    *error_pointer = string_sprintf("\"then\" missing at end of filter file");
+    *error_pointer = US"\"then\" missing at end of filter file";
     break;
     }
 
@@ -478,7 +519,7 @@ for (;;)
   else
     {
     ptr = nextitem(ptr, buffer, sizeof(buffer), TRUE);
-    if (*error_pointer != NULL) break;
+    if (*error_pointer) break;
 
     /* "Then" at the start of a condition is an error */
 
@@ -499,7 +540,7 @@ for (;;)
 
     /* Build a condition block from the specific word. */
 
-    c = store_get(sizeof(condition_block));
+    c = store_get(sizeof(condition_block), GET_UNTAINTED);
     c->left.u = c->right.u = NULL;
     c->testfor = testfor;
     testfor = TRUE;
@@ -519,17 +560,17 @@ for (;;)
       for (;;)
         {
         string_item *aa;
-        uschar *saveptr = ptr;
+        const uschar * saveptr = ptr;
         ptr = nextword(ptr, buffer, sizeof(buffer), TRUE);
-        if (*error_pointer != NULL) break;
+        if (*error_pointer) break;
         if (Ustrcmp(buffer, "alias") != 0)
           {
           ptr = saveptr;
           break;
           }
         ptr = nextitem(ptr, buffer, sizeof(buffer), TRUE);
-        if (*error_pointer != NULL) break;
-        aa = store_get(sizeof(string_item));
+        if (*error_pointer) break;
+        aa = store_get(sizeof(string_item), GET_UNTAINTED);
         aa->text = string_copy(buffer);
         aa->next = c->left.a;
         c->left.a = aa;
@@ -542,7 +583,7 @@ for (;;)
     else if (Ustrcmp(buffer, "foranyaddress") == 0)
       {
       ptr = nextitem(ptr, buffer, sizeof(buffer), TRUE);
-      if (*error_pointer != NULL) break;
+      if (*error_pointer) break;
       if (*ptr != '(')
         {
         *error_pointer = string_sprintf("\"(\" expected after \"foranyaddress\" "
@@ -554,18 +595,13 @@ for (;;)
       c->left.u = string_copy(buffer);
 
       ptr = read_condition(nextsigchar(ptr+1, TRUE), &(c->right.c), FALSE);
-      if (*error_pointer != NULL) break;
+      if (*error_pointer) break;
       if (*ptr != ')')
         {
         *error_pointer = string_sprintf("expected \")\" in line %d of "
           "filter file", line_number);
         break;
         }
-      if (!testfor)
-        {
-        c->testfor = !c->testfor;
-        testfor = TRUE;
-        }
       ptr = nextsigchar(ptr+1, TRUE);
       }
 
@@ -575,11 +611,11 @@ for (;;)
     else
       {
       int i;
-      uschar *isptr = NULL;
+      const uschar *isptr = NULL;
 
       c->left.u = string_copy(buffer);
       ptr = nextword(ptr, buffer, sizeof(buffer), TRUE);
-      if (*error_pointer != NULL) break;
+      if (*error_pointer) break;
 
       /* Handle "does|is [not]", preserving the pointer after "is" in
       case it isn't that, but the form "is <string>". */
@@ -590,13 +626,13 @@ for (;;)
         if (buffer[0] == 'I') { c->type = cond_IS; isptr = ptr; }
 
         ptr = nextword(ptr, buffer, sizeof(buffer), TRUE);
-        if (*error_pointer != NULL) break;
+        if (*error_pointer) break;
         if (strcmpic(buffer, US"not") == 0)
           {
           c->testfor = !c->testfor;
-          if (isptr != NULL) isptr = ptr;
+          if (isptr) isptr = ptr;
           ptr = nextword(ptr, buffer, sizeof(buffer), TRUE);
-          if (*error_pointer != NULL) break;
+          if (*error_pointer) break;
           }
         }
 
@@ -614,22 +650,19 @@ for (;;)
 
       if (i >= cond_word_count)
         {
-        if (isptr != NULL)
-          {
-          ptr = isptr;
-          }
-        else
+        if (!isptr)
           {
           *error_pointer = string_sprintf("unrecognized condition word \"%s\" "
             "near line %d of filter file", buffer, line_number);
           break;
           }
+        ptr = isptr;
         }
 
       /* Get the RH argument. */
 
       ptr = nextitem(ptr, buffer, sizeof(buffer), TRUE);
-      if (*error_pointer != NULL) break;
+      if (*error_pointer) break;
       c->right.u = string_copy(buffer);
       }
     }
@@ -664,18 +697,23 @@ for (;;)
 
   else
     {
-    uschar *saveptr = ptr;
+//    const uschar *saveptr = ptr;
     ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
-    if (*error_pointer != NULL) break;
+    if (*error_pointer) break;
 
     /* "Then" terminates a toplevel condition; otherwise a closing bracket
     has been omitted. Put a string terminator at the start of "then" so
     that reflecting the condition can be done when testing. */
+    /*XXX This stops us doing a constification job in this file, unfortunately.
+    Comment it out and see if anything breaks.
+    With one addition down at DEFERFREEZEFAIL it passes the testsuite. */
 
     if (Ustrcmp(buffer, "then") == 0)
       {
-      if (toplevel) *saveptr = 0;
-        else *error_pointer = string_sprintf("missing \")\" at end of "
+//      if (toplevel) *saveptr = 0;
+//      else
+      if (!toplevel)
+        *error_pointer = string_sprintf("missing \")\" at end of "
           "condition near line %d of filter file", line_number);
       break;
       }
@@ -688,7 +726,7 @@ for (;;)
 
     else if (Ustrcmp(buffer, "and") == 0)
       {
-      condition_block *andc = store_get(sizeof(condition_block));
+      condition_block * andc = store_get(sizeof(condition_block), GET_UNTAINTED);
       andc->parent = current_parent;
       andc->type = cond_and;
       andc->testfor = TRUE;
@@ -706,24 +744,24 @@ for (;;)
 
     else if (Ustrcmp(buffer, "or") == 0)
       {
-      condition_block *orc = store_get(sizeof(condition_block));
-      condition_block *or_parent = NULL;
+      condition_block * orc = store_get(sizeof(condition_block), GET_UNTAINTED);
+      condition_block * or_parent = NULL;
 
-      if (current_parent != NULL)
+      if (current_parent)
         {
-        while (current_parent->parent != NULL &&
+        while (current_parent->parent &&
                current_parent->parent->type == cond_and)
           current_parent = current_parent->parent;
 
         /* If the parent has a parent, it must be an "or" parent. */
 
-        if (current_parent->parent != NULL)
+        if (current_parent->parent)
           or_parent = current_parent->parent;
         }
 
       orc->parent = or_parent;
-      if (or_parent == NULL) *cond = orc; else
-        or_parent->right.c = orc;
+      if (!or_parent) *cond = orc;
+      else or_parent->right.c = orc;
       orc->type = cond_or;
       orc->testfor = TRUE;
       orc->left.c = (current_parent == NULL)? c : current_parent;
@@ -750,7 +788,7 @@ return nextsigchar(ptr, TRUE);
 
 
 /*************************************************
-*             Ouput the current indent           *
+*             Output the current indent          *
 *************************************************/
 
 static void
@@ -777,7 +815,7 @@ Returns:      nothing
 static void
 print_condition(condition_block *c, BOOL toplevel)
 {
-char *name = (c->testfor)? cond_names[c->type] : cond_not_names[c->type];
+const char *name = (c->testfor)? cond_names[c->type] : cond_not_names[c->type];
 switch(c->type)
   {
   case cond_personal:
@@ -785,8 +823,8 @@ switch(c->type)
   case cond_errormsg:
   case cond_firsttime:
   case cond_manualthaw:
-  debug_printf("%s", name);
-  break;
+    debug_printf("%s", name);
+    break;
 
   case cond_is:
   case cond_IS:
@@ -800,31 +838,31 @@ switch(c->type)
   case cond_ENDS:
   case cond_above:
   case cond_below:
-  debug_printf("%s %s %s", c->left.u, (char *)name, c->right.u);
-  break;
+    debug_printf("%s %s %s", c->left.u, name, c->right.u);
+    break;
 
   case cond_and:
-  if (!c->testfor) debug_printf("not (");
-  print_condition(c->left.c, FALSE);
-  debug_printf(" %s ", cond_names[c->type]);
-  print_condition(c->right.c, FALSE);
-  if (!c->testfor) debug_printf(")");
-  break;
+    if (!c->testfor) debug_printf("not (");
+    print_condition(c->left.c, FALSE);
+    debug_printf(" %s ", cond_names[c->type]);
+    print_condition(c->right.c, FALSE);
+    if (!c->testfor) debug_printf(")");
+    break;
 
   case cond_or:
-  if (!c->testfor) debug_printf("not (");
-  else if (!toplevel) debug_printf("(");
-  print_condition(c->left.c, FALSE);
-  debug_printf(" %s ", cond_names[c->type]);
-  print_condition(c->right.c, FALSE);
-  if (!toplevel || !c->testfor) debug_printf(")");
-  break;
+    if (!c->testfor) debug_printf("not (");
+    else if (!toplevel) debug_printf("(");
+    print_condition(c->left.c, FALSE);
+    debug_printf(" %s ", cond_names[c->type]);
+    print_condition(c->right.c, FALSE);
+    if (!toplevel || !c->testfor) debug_printf(")");
+    break;
 
   case cond_foranyaddress:
-  debug_printf("%s %s (", name, c->left.u);
-  print_condition(c->right.c, FALSE);
-  debug_printf(")");
-  break;
+    debug_printf("%s %s (", name, c->left.u);
+    print_condition(c->right.c, FALSE);
+    debug_printf(")");
+    break;
   }
 }
 
@@ -846,7 +884,7 @@ Returns:       TRUE if command successfully read, else FALSE
 */
 
 static BOOL
-read_command(uschar **pptr, filter_cmd ***lastcmdptr)
+read_command(const uschar **pptr, filter_cmd ***lastcmdptr)
 {
 int command, i, cmd_bit;
 filter_cmd *new, **newlastcmdptr;
@@ -854,8 +892,8 @@ BOOL yield = TRUE;
 BOOL was_seen_or_unseen = FALSE;
 BOOL was_noerror = FALSE;
 uschar buffer[1024];
-uschar *ptr = *pptr;
-uschar *saveptr;
+const uschar *ptr = *pptr;
+const uschar *saveptr;
 uschar *fmsg = NULL;
 
 /* Read the next word and find which command it is. Command words are normally
@@ -864,20 +902,22 @@ terminated by white space, but there are two exceptions, which are the "if" and
 as brackets are allowed in conditions and users will expect not to require
 white space here. */
 
+*buffer = '\0';        /* compiler quietening */
+
 if (Ustrncmp(ptr, "if(", 3) == 0)
   {
-  Ustrcpy(buffer, "if");
+  Ustrcpy(buffer, US"if");
   ptr += 2;
   }
 else if (Ustrncmp(ptr, "elif(", 5) == 0)
   {
-  Ustrcpy(buffer, "elif");
+  Ustrcpy(buffer, US"elif");
   ptr += 4;
   }
 else
   {
   ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
-  if (*error_pointer != NULL) return FALSE;
+  if (*error_pointer) return FALSE;
   }
 
 for (command = 0; command < command_list_count; command++)
@@ -892,15 +932,15 @@ switch (command)
   stored in the second argument slot. Neither may be preceded by seen, unseen
   or noerror. */
 
-  case add_command:
-  case headers_command:
-  if (seen_force || noerror_force)
-    {
-    *error_pointer = string_sprintf("\"seen\", \"unseen\", or \"noerror\" "
-      "found before an \"%s\" command near line %d",
-        command_list[command], line_number);
-    yield = FALSE;
-    }
+  case ADD_COMMAND:
+  case HEADERS_COMMAND:
+    if (seen_force || noerror_force)
+      {
+      *error_pointer = string_sprintf("\"seen\", \"unseen\", or \"noerror\" "
+       "found before an \"%s\" command near line %d",
+         command_list[command], line_number);
+      yield = FALSE;
+      }
   /* Fall through */
 
   /* Logwrite, logfile, pipe, and testprint all take a single argument, save
@@ -908,295 +948,303 @@ switch (command)
   have "errors_to <address>" in a system filter, or in a user filter if the
   address is the current one. */
 
-  case deliver_command:
-  case logfile_command:
-  case logwrite_command:
-  case pipe_command:
-  case save_command:
-  case testprint_command:
+  case DELIVER_COMMAND:
+  case LOGFILE_COMMAND:
+  case LOGWRITE_COMMAND:
+  case PIPE_COMMAND:
+  case SAVE_COMMAND:
+  case TESTPRINT_COMMAND:
 
-  ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
-  if (*buffer == 0)
-    *error_pointer = string_sprintf("\"%s\" requires an argument "
-      "near line %d of filter file", command_list[command], line_number);
-
-  if (*error_pointer != NULL) yield = FALSE; else
-    {
-    union argtypes argument, second_argument;
-
-    argument.u = second_argument.u = NULL;
+    ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
+    if (!*buffer)
+      *error_pointer = string_sprintf("\"%s\" requires an argument "
+       "near line %d of filter file", command_list[command], line_number);
 
-    if (command == add_command)
+    if (*error_pointer) yield = FALSE; else
       {
-      argument.u = string_copy(buffer);
-      ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
-      if (*buffer == 0 || Ustrcmp(buffer, "to") != 0)
-        *error_pointer = string_sprintf("\"to\" expected in \"add\" command "
-          "near line %d of filter file", line_number);
-      else
-        {
-        ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
-        if (*buffer == 0)
-          *error_pointer = string_sprintf("value missing after \"to\" "
-            "near line %d of filter file", line_number);
-        else second_argument.u = string_copy(buffer);
-        }
-      }
+      union argtypes argument, second_argument;
+
+      argument.u = second_argument.u = NULL;
+
+      if (command == ADD_COMMAND)
+       {
+       argument.u = string_copy(buffer);
+       ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
+       if (!*buffer || Ustrcmp(buffer, "to") != 0)
+         *error_pointer = string_sprintf("\"to\" expected in \"add\" command "
+           "near line %d of filter file", line_number);
+       else
+         {
+         ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
+         if (!*buffer)
+           *error_pointer = string_sprintf("value missing after \"to\" "
+             "near line %d of filter file", line_number);
+         else second_argument.u = string_copy(buffer);
+         }
+       }
+
+      else if (command == HEADERS_COMMAND)
+       {
+       if (Ustrcmp(buffer, "add") == 0)
+         second_argument.b = TRUE;
+       else
+         if (Ustrcmp(buffer, "remove") == 0) second_argument.b = FALSE;
+       else
+         if (Ustrcmp(buffer, "charset") == 0)
+           second_argument.b = TRUE_UNSET;
+       else
+         {
+         *error_pointer = string_sprintf("\"add\", \"remove\", or \"charset\" "
+           "expected after \"headers\" near line %d of filter file",
+             line_number);
+         yield = FALSE;
+         }
+
+       if (!f.system_filtering && second_argument.b != TRUE_UNSET)
+         {
+         *error_pointer = string_sprintf("header addition and removal is "
+           "available only in system filters: near line %d of filter file",
+           line_number);
+         yield = FALSE;
+         break;
+         }
+
+       if (yield)
+         {
+         ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
+         if (!*buffer)
+           *error_pointer = string_sprintf("value missing after \"add\", "
+             "\"remove\", or \"charset\" near line %d of filter file",
+               line_number);
+         else argument.u = string_copy(buffer);
+         }
+       }
+
+      /* The argument for the logwrite command must end in a newline, and the save
+      and logfile commands can have an optional mode argument. The deliver
+      command can have an optional "errors_to <address>" for a system filter,
+      or for a user filter if the address is the user's address. Accept the
+      syntax here - the check is later. */
 
-    else if (command == headers_command)
-      {
-      if (Ustrcmp(buffer, "add") == 0)
-        second_argument.b = TRUE;
       else
-        if (Ustrcmp(buffer, "remove") == 0) second_argument.b = FALSE;
+       {
+       if (command == LOGWRITE_COMMAND)
+         {
+         int len = Ustrlen(buffer);
+         if (len == 0 || buffer[len-1] != '\n') Ustrcat(buffer, US"\n");
+         }
+
+       argument.u = string_copy(buffer);
+
+       if (command == SAVE_COMMAND || command == LOGFILE_COMMAND)
+         {
+         if (isdigit(*ptr))
+           {
+           ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
+           second_argument.i = (int)Ustrtol(buffer, NULL, 8);
+           }
+         else second_argument.i = -1;
+         }
+
+       else if (command == DELIVER_COMMAND)
+         {
+         const uschar *save_ptr = ptr;
+         ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
+         if (Ustrcmp(buffer, "errors_to") == 0)
+           {
+           ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
+           second_argument.u = string_copy(buffer);
+           }
+         else ptr = save_ptr;
+         }
+       }
+
+      /* Set up the command block. Seen defaults TRUE for delivery commands,
+      FALSE for logging commands, and it doesn't matter for testprint, as
+      that doesn't change the "delivered" status. */
+
+      if (*error_pointer) yield = FALSE;
       else
-        if (Ustrcmp(buffer, "charset") == 0)
-          second_argument.b = TRUE_UNSET;
-      else
-        {
-        *error_pointer = string_sprintf("\"add\", \"remove\", or \"charset\" "
-          "expected after \"headers\" near line %d of filter file",
-            line_number);
-        yield = FALSE;
-        }
-
-      if (!system_filtering && second_argument.b != TRUE_UNSET)
-        {
-        *error_pointer = string_sprintf("header addition and removal is "
-          "available only in system filters: near line %d of filter file",
-          line_number);
-        yield = FALSE;
-        break;
-        }
-
-      if (yield)
-        {
-        ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
-        if (*buffer == 0)
-          *error_pointer = string_sprintf("value missing after \"add\", "
-            "\"remove\", or \"charset\" near line %d of filter file",
-              line_number);
-        else argument.u = string_copy(buffer);
-        }
+       {
+       new = store_get(sizeof(filter_cmd) + sizeof(union argtypes), GET_UNTAINTED);
+       new->next = NULL;
+       **lastcmdptr = new;
+       *lastcmdptr = &(new->next);
+       new->command = command;
+       new->seen = seen_force? seen_value : command_exparg_count[command] >= 128;
+       new->noerror = noerror_force;
+       new->args[0] = argument;
+       new->args[1] = second_argument;
+       }
       }
+    break;
 
-    /* The argument for the logwrite command must end in a newline, and the save
-    and logfile commands can have an optional mode argument. The deliver
-    command can have an optional "errors_to <address>" for a system filter,
-    or for a user filter if the address is the user's address. Accept the
-    syntax here - the check is later. */
-
-    else
-      {
-      if (command == logwrite_command)
-        {
-        int len = Ustrlen(buffer);
-        if (len == 0 || buffer[len-1] != '\n') Ustrcat(buffer, "\n");
-        }
-
-      argument.u = string_copy(buffer);
 
-      if (command == save_command || command == logfile_command)
-        {
-        if (isdigit(*ptr))
-          {
-          ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
-          second_argument.i = (int)Ustrtol(buffer, NULL, 8);
-          }
-        else second_argument.i = -1;
-        }
+  /* Elif, else and endif just set a flag if expected. */
 
-      else if (command == deliver_command)
-        {
-        uschar *save_ptr = ptr;
-        ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
-        if (Ustrcmp(buffer, "errors_to") == 0)
-          {
-          ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
-          second_argument.u = string_copy(buffer);
-          }
-        else ptr = save_ptr;
-        }
+  case ELIF_COMMAND:
+  case ELSE_COMMAND:
+  case ENDIF_COMMAND:
+    if (seen_force || noerror_force)
+      {
+      *error_pointer = string_sprintf("\"seen\", \"unseen\", or \"noerror\" "
+       "near line %d is not followed by a command", line_number);
+      yield = FALSE;
       }
 
-    /* Set up the command block. Seen defaults TRUE for delivery commands,
-    FALSE for logging commands, and it doesn't matter for testprint, as
-    that doesn't change the "delivered" status. */
-
-    if (*error_pointer != NULL) yield = FALSE; else
+    if (expect_endif > 0)
+      had_else_endif = (command == ELIF_COMMAND)? had_elif :
+                      (command == ELSE_COMMAND)? had_else : had_endif;
+    else
       {
-      new = store_get(sizeof(filter_cmd) + sizeof(union argtypes));
-      new->next = NULL;
-      **lastcmdptr = new;
-      *lastcmdptr = &(new->next);
-      new->command = command;
-      new->seen = seen_force? seen_value : command_exparg_count[command] >= 128;
-      new->noerror = noerror_force;
-      new->args[0] = argument;
-      new->args[1] = second_argument;
+      *error_pointer = string_sprintf("unexpected \"%s\" command near "
+       "line %d of filter file", buffer, line_number);
+      yield = FALSE;
       }
-    }
-  break;
-
-
-  /* Elif, else and endif just set a flag if expected. */
-
-  case elif_command:
-  case else_command:
-  case endif_command:
-  if (expect_endif > 0)
-    had_else_endif = (command == elif_command)? had_elif :
-                     (command == else_command)? had_else : had_endif;
-  else
-    {
-    *error_pointer = string_sprintf("unexpected \"%s\" command near "
-      "line %d of filter file", buffer, line_number);
-    yield = FALSE;
-    }
-  break;
+    break;
 
 
   /* Defer, freeze, and fail are available only if permitted. */
 
-  case defer_command:
-  cmd_bit = RDO_DEFER;
-  goto DEFER_FREEZE_FAIL;
+  case DEFER_COMMAND:
+    cmd_bit = RDO_DEFER;
+    goto DEFER_FREEZE_FAIL;
 
-  case fail_command:
-  cmd_bit = RDO_FAIL;
-  goto DEFER_FREEZE_FAIL;
+  case FAIL_COMMAND:
+    cmd_bit = RDO_FAIL;
+    goto DEFER_FREEZE_FAIL;
 
-  case freeze_command:
-  cmd_bit = RDO_FREEZE;
+  case FREEZE_COMMAND:
+    cmd_bit = RDO_FREEZE;
 
   DEFER_FREEZE_FAIL:
-  if ((filter_options & cmd_bit) == 0)
-    {
-    *error_pointer = string_sprintf("filtering command \"%s\" is disabled: "
-      "near line %d of filter file", buffer, line_number);
-    yield = FALSE;
-    break;
-    }
+    if ((filter_options & cmd_bit) == 0)
+      {
+      *error_pointer = string_sprintf("filtering command \"%s\" is disabled: "
+       "near line %d of filter file", buffer, line_number);
+      yield = FALSE;
+      break;
+      }
 
-  /* A text message can be provided after the "text" keyword, or
-  as a string in quotes. */
+    /* A text message can be provided after the "text" keyword, or
+    as a string in quotes. */
 
-  saveptr = ptr;
-  ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
-  if (*saveptr != '\"' && (*buffer == 0 || Ustrcmp(buffer, "text") != 0))
-    {
-    ptr = saveptr;
-    fmsg = US"";
-    }
-  else
-    {
-    if (*saveptr != '\"')
-      ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
-    fmsg = string_copy(buffer);
-    }
+    saveptr = ptr;
+    ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
+    if (*saveptr != '\"' && (!*buffer || Ustrcmp(buffer, "text") != 0))
+      {
+      ptr = saveptr;
+      fmsg = US"";
+      }
+    else
+      {
+      if (*saveptr != '\"')
+       ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
+      fmsg = string_copy(buffer);
+      }
 
-  /* Drop through and treat as "finish", but never set "seen". */
+    /* Drop through and treat as "finish", but never set "seen". */
 
-  seen_value = FALSE;
+    seen_value = FALSE;
 
-  /* Finish has no arguments; fmsg defaults to NULL */
+    /* Finish has no arguments; fmsg defaults to NULL */
 
-  case finish_command:
-  new = store_get(sizeof(filter_cmd));
-  new->next = NULL;
-  **lastcmdptr = new;
-  *lastcmdptr = &(new->next);
-  new->command = command;
-  new->seen = seen_force? seen_value : FALSE;
-  new->args[0].u = fmsg;
-  break;
+    case FINISH_COMMAND:
+    new = store_get(sizeof(filter_cmd), GET_UNTAINTED);
+    new->next = NULL;
+    **lastcmdptr = new;
+    *lastcmdptr = &(new->next);
+    new->command = command;
+    new->seen = seen_force ? seen_value : FALSE;
+    new->args[0].u = fmsg;
+    break;
 
 
   /* Seen, unseen, and noerror are not allowed before if, which takes a
   condition argument and then and else sub-commands. */
 
-  case if_command:
-  if (seen_force || noerror_force)
-    {
-    *error_pointer = string_sprintf("\"seen\", \"unseen\", or \"noerror\" "
-      "found before an \"if\" command near line %d",
-        line_number);
-    yield = FALSE;
-    }
+  case IF_COMMAND:
+    if (seen_force || noerror_force)
+      {
+      *error_pointer = string_sprintf("\"seen\", \"unseen\", or \"noerror\" "
+       "found before an \"if\" command near line %d",
+         line_number);
+      yield = FALSE;
+      }
 
-  /* Set up the command block for if */
+    /* Set up the command block for if */
 
-  new = store_get(sizeof(filter_cmd) + 4 * sizeof(union argtypes));
-  new->next = NULL;
-  **lastcmdptr = new;
-  *lastcmdptr = &(new->next);
-  new->command = command;
-  new->seen = FALSE;
-  new->args[0].u = NULL;
-  new->args[1].u = new->args[2].u = NULL;
-  new->args[3].u = ptr;
+    new = store_get(sizeof(filter_cmd) + 4 * sizeof(union argtypes), GET_UNTAINTED);
+    new->next = NULL;
+    **lastcmdptr = new;
+    *lastcmdptr = &new->next;
+    new->command = command;
+    new->seen = FALSE;
+    new->args[0].u = NULL;
+    new->args[1].u = new->args[2].u = NULL;
+    new->args[3].u = ptr;
 
-  /* Read the condition */
+    /* Read the condition */
 
-  ptr = read_condition(ptr, &(new->args[0].c), TRUE);
-  if (*error_pointer != NULL) { yield = FALSE; break; }
+    ptr = read_condition(ptr, &new->args[0].c, TRUE);
+    if (*error_pointer) { yield = FALSE; break; }
 
-  /* Read the commands to be obeyed if the condition is true */
+    /* Read the commands to be obeyed if the condition is true */
 
-  newlastcmdptr = &(new->args[1].f);
-  if (!read_command_list(&ptr, &newlastcmdptr, TRUE)) yield = FALSE;
+    newlastcmdptr = &(new->args[1].f);
+    if (!read_command_list(&ptr, &newlastcmdptr, TRUE)) yield = FALSE;
 
-  /* If commands were successfully read, handle the various possible
-  terminators. There may be a number of successive "elif" sections. */
+    /* If commands were successfully read, handle the various possible
+    terminators. There may be a number of successive "elif" sections. */
 
-  else
-    {
-    while (had_else_endif == had_elif)
-      {
-      filter_cmd *newnew =
-        store_get(sizeof(filter_cmd) + 4 * sizeof(union argtypes));
-      new->args[2].f = newnew;
-      new = newnew;
-      new->next = NULL;
-      new->command = command;
-      new->seen = FALSE;
-      new->args[0].u = NULL;
-      new->args[1].u = new->args[2].u = NULL;
-      new->args[3].u = ptr;
-
-      ptr = read_condition(ptr, &(new->args[0].c), TRUE);
-      if (*error_pointer != NULL) { yield = FALSE; break; }
-      newlastcmdptr = &(new->args[1].f);
-      if (!read_command_list(&ptr, &newlastcmdptr, TRUE))
-        yield = FALSE;
-      }
-
-    if (yield == FALSE) break;
-
-    /* Handle termination by "else", possibly following one or more
-    "elsif" sections. */
-
-    if (had_else_endif == had_else)
+    else
       {
-      newlastcmdptr = &(new->args[2].f);
-      if (!read_command_list(&ptr, &newlastcmdptr, TRUE))
-        yield = FALSE;
-      else if (had_else_endif != had_endif)
-        {
-        *error_pointer = string_sprintf("\"endif\" missing near line %d of "
-          "filter file", line_number);
-        yield = FALSE;
-        }
+      while (had_else_endif == had_elif)
+       {
+       filter_cmd *newnew =
+         store_get(sizeof(filter_cmd) + 4 * sizeof(union argtypes), GET_UNTAINTED);
+       new->args[2].f = newnew;
+       new = newnew;
+       new->next = NULL;
+       new->command = command;
+       new->seen = FALSE;
+       new->args[0].u = NULL;
+       new->args[1].u = new->args[2].u = NULL;
+       new->args[3].u = ptr;
+
+       ptr = read_condition(ptr, &new->args[0].c, TRUE);
+       if (*error_pointer) { yield = FALSE; break; }
+       newlastcmdptr = &(new->args[1].f);
+       if (!read_command_list(&ptr, &newlastcmdptr, TRUE))
+         yield = FALSE;
+       }
+
+      if (yield == FALSE) break;
+
+      /* Handle termination by "else", possibly following one or more
+      "elsif" sections. */
+
+      if (had_else_endif == had_else)
+       {
+       newlastcmdptr = &(new->args[2].f);
+       if (!read_command_list(&ptr, &newlastcmdptr, TRUE))
+         yield = FALSE;
+       else if (had_else_endif != had_endif)
+         {
+         *error_pointer = string_sprintf("\"endif\" missing near line %d of "
+           "filter file", line_number);
+         yield = FALSE;
+         }
+       }
+
+      /* Otherwise the terminator was "endif" - this is checked by
+      read_command_list(). The pointer is already set to NULL. */
       }
 
-    /* Otherwise the terminator was "endif" - this is checked by
-    read_command_list(). The pointer is already set to NULL. */
-    }
+    /* Reset the terminator flag. */
 
-  /* Reset the terminator flag. */
-
-  had_else_endif = had_neither;
-  break;
+    had_else_endif = had_neither;
+    break;
 
 
   /* The mail & vacation commands have a whole slew of keyworded arguments.
@@ -1205,144 +1253,150 @@ switch (command)
   are logically booleans, because they are stored in a uschar * value, we use
   NULL and not FALSE, to keep 64-bit compilers happy. */
 
-  case mail_command:
-  case vacation_command:
-  new = store_get(sizeof(filter_cmd) + mailargs_total * sizeof(union argtypes));
-  new->next = NULL;
-  new->command = command;
-  new->seen = seen_force? seen_value : FALSE;
-  new->noerror = noerror_force;
-  for (i = 0; i < mailargs_total; i++) new->args[i].u = NULL;
-
-  /* Read keyword/value pairs until we hit one that isn't. The data
-  must contain only printing chars plus tab, though the "text" value
-  can also contain newlines. The "file" keyword can be preceded by the
-  word "expand", and "return message" has no data. */
-
-  for (;;)
-    {
-    uschar *saveptr = ptr;
-    ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
-    if (*error_pointer != NULL)
+  case MAIL_COMMAND:
+  case VACATION_COMMAND:
+    new = store_get(sizeof(filter_cmd) + mailargs_total * sizeof(union argtypes), GET_UNTAINTED);
+    new->next = NULL;
+    new->command = command;
+    new->seen = seen_force ? seen_value : FALSE;
+    new->noerror = noerror_force;
+    for (i = 0; i < mailargs_total; i++) new->args[i].u = NULL;
+
+    /* Read keyword/value pairs until we hit one that isn't. The data
+    must contain only printing chars plus tab, though the "text" value
+    can also contain newlines. The "file" keyword can be preceded by the
+    word "expand", and "return message" has no data. */
+
+    for (;;)
       {
-      yield = FALSE;
-      break;
-      }
-
-    /* Ensure "return" is followed by "message"; that's a complete option */
-
-    if (Ustrcmp(buffer, "return") == 0)
-      {
-      new->args[mailarg_index_return].u = US"";  /* not NULL => TRUE */
+      const uschar *saveptr = ptr;
       ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
-      if (Ustrcmp(buffer, "message") != 0)
-        {
-        *error_pointer = string_sprintf("\"return\" not followed by \"message\" "
-          " near line %d of filter file", line_number);
-        yield = FALSE;
-        break;
-        }
-      continue;
+      if (*error_pointer)
+       { yield = FALSE; break; }
+
+      /* Ensure "return" is followed by "message"; that's a complete option */
+
+      if (Ustrcmp(buffer, "return") == 0)
+       {
+       new->args[mailarg_index_return].u = US"";  /* not NULL => TRUE */
+       ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
+       if (Ustrcmp(buffer, "message") != 0)
+         {
+         *error_pointer = string_sprintf("\"return\" not followed by \"message\" "
+           " near line %d of filter file", line_number);
+         yield = FALSE;
+         break;
+         }
+       continue;
+       }
+
+      /* Ensure "expand" is followed by "file", then fall through to process the
+      file keyword. */
+
+      if (Ustrcmp(buffer, "expand") == 0)
+       {
+       new->args[mailarg_index_expand].u = US"";  /* not NULL => TRUE */
+       ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
+       if (Ustrcmp(buffer, "file") != 0)
+         {
+         *error_pointer = string_sprintf("\"expand\" not followed by \"file\" "
+           " near line %d of filter file", line_number);
+         yield = FALSE;
+         break;
+         }
+       }
+
+      /* Scan for the keyword */
+
+      for (i = 0; i < MAILARGS_STRING_COUNT; i++)
+       if (Ustrcmp(buffer, mailargs[i]) == 0) break;
+
+      /* Not found keyword; assume end of this command */
+
+      if (i >= MAILARGS_STRING_COUNT)
+       {
+       ptr = saveptr;
+       break;
+       }
+
+      /* Found keyword, read the data item */
+
+      ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
+      if (*error_pointer)
+       { yield = FALSE; break; }
+      else new->args[i].u = string_copy(buffer);
       }
 
-    /* Ensure "expand" is followed by "file", then fall through to process the
-    file keyword. */
+    /* If this is the vacation command, apply some default settings to
+    some of the arguments. */
 
-    if (Ustrcmp(buffer, "expand") == 0)
+    if (command == VACATION_COMMAND)
       {
-      new->args[mailarg_index_expand].u = US"";  /* not NULL => TRUE */
-      ptr = nextword(ptr, buffer, sizeof(buffer), FALSE);
-      if (Ustrcmp(buffer, "file") != 0)
-        {
-        *error_pointer = string_sprintf("\"expand\" not followed by \"file\" "
-          " near line %d of filter file", line_number);
-        yield = FALSE;
-        break;
-        }
+      if (!new->args[mailarg_index_file].u)
+       {
+       new->args[mailarg_index_file].u = string_copy(US".vacation.msg");
+       new->args[mailarg_index_expand].u = US"";   /* not NULL => TRUE */
+       }
+      if (!new->args[mailarg_index_log].u)
+       new->args[mailarg_index_log].u = string_copy(US".vacation.log");
+      if (!new->args[mailarg_index_once].u)
+       new->args[mailarg_index_once].u = string_copy(US".vacation");
+      if (!new->args[mailarg_index_once_repeat].u)
+       new->args[mailarg_index_once_repeat].u = string_copy(US"7d");
+      if (!new->args[mailarg_index_subject].u)
+       new->args[mailarg_index_subject].u = string_copy(US"On vacation");
       }
 
-    /* Scan for the keyword */
-
-    for (i = 0; i < MAILARGS_STRING_COUNT; i++)
-      if (Ustrcmp(buffer, mailargs[i]) == 0) break;
+    /* Join the address on to the chain of generated addresses */
 
-    /* Not found keyword; assume end of this command */
+    **lastcmdptr = new;
+    *lastcmdptr = &(new->next);
+    break;
 
-    if (i >= MAILARGS_STRING_COUNT)
-      {
-      ptr = saveptr;
-      break;
-      }
 
-    /* Found keyword, read the data item */
+  /* Seen and unseen just set flags */
 
-    ptr = nextitem(ptr, buffer, sizeof(buffer), FALSE);
-    if (*error_pointer != NULL)
+  case SEEN_COMMAND:
+  case UNSEEN_COMMAND:
+    if (!*ptr)
       {
+      *error_pointer = string_sprintf("\"seen\" or \"unseen\" "
+       "near line %d is not followed by a command", line_number);
       yield = FALSE;
-      break;
       }
-    else new->args[i].u = string_copy(buffer);
-    }
-
-  /* If this is the vacation command, apply some default settings to
-  some of the arguments. */
-
-  if (command == vacation_command)
-    {
-    if (new->args[mailarg_index_file].u == NULL)
+    if (seen_force)
       {
-      new->args[mailarg_index_file].u = string_copy(US".vacation.msg");
-      new->args[mailarg_index_expand].u = US"";   /* not NULL => TRUE */
+      *error_pointer = string_sprintf("\"seen\" or \"unseen\" repeated "
+       "near line %d", line_number);
+      yield = FALSE;
       }
-    if (new->args[mailarg_index_log].u == NULL)
-      new->args[mailarg_index_log].u = string_copy(US".vacation.log");
-    if (new->args[mailarg_index_once].u == NULL)
-      new->args[mailarg_index_once].u = string_copy(US".vacation");
-    if (new->args[mailarg_index_once_repeat].u == NULL)
-      new->args[mailarg_index_once_repeat].u = string_copy(US"7d");
-    if (new->args[mailarg_index_subject].u == NULL)
-      new->args[mailarg_index_subject].u = string_copy(US"On vacation");
-    }
-
-  /* Join the address on to the chain of generated addresses */
-
-  **lastcmdptr = new;
-  *lastcmdptr = &(new->next);
-  break;
-
-
-  /* Seen and unseen just set flags */
-
-  case seen_command:
-  case unseen_command:
-  if (seen_force)
-    {
-    *error_pointer = string_sprintf("\"seen\" or \"unseen\" repeated "
-      "near line %d", line_number);
-    yield = FALSE;
-    }
-  seen_value = (command == seen_command);
-  seen_force = TRUE;
-  was_seen_or_unseen = TRUE;
-  break;
+    seen_value = (command == SEEN_COMMAND);
+    seen_force = TRUE;
+    was_seen_or_unseen = TRUE;
+    break;
 
 
   /* So does noerror */
 
-  case noerror_command:
-  noerror_force = TRUE;
-  was_noerror = TRUE;
-  break;
+  case NOERROR_COMMAND:
+    if (!*ptr)
+      {
+      *error_pointer = string_sprintf("\"noerror\" "
+       "near line %d is not followed by a command", line_number);
+      yield = FALSE;
+      }
+    noerror_force = TRUE;
+    was_noerror = TRUE;
+    break;
 
 
   /* Oops */
 
   default:
-  *error_pointer = string_sprintf("unknown filtering command \"%s\" "
-    "near line %d of filter file", buffer, line_number);
-  yield = FALSE;
-  break;
+    *error_pointer = string_sprintf("unknown filtering command \"%s\" "
+      "near line %d of filter file", buffer, line_number);
+    yield = FALSE;
+    break;
   }
 
 if (!was_seen_or_unseen && !was_noerror)
@@ -1361,7 +1415,7 @@ return yield;
 *              Read a list of commands           *
 *************************************************/
 
-/* If condional is TRUE, the list must be terminated
+/* If conditional is TRUE, the list must be terminated
 by the words "else" or "endif".
 
 Arguments:
@@ -1374,11 +1428,11 @@ Returns:      TRUE on success
 */
 
 static BOOL
-read_command_list(uschar **pptr, filter_cmd ***lastcmdptr, BOOL conditional)
+read_command_list(const uschar **pptr, filter_cmd ***lastcmdptr, BOOL conditional)
 {
 if (conditional) expect_endif++;
 had_else_endif = had_neither;
-while (**pptr != 0 && had_else_endif == had_neither)
+while (**pptr && had_else_endif == had_neither)
   {
   if (!read_command(pptr, lastcmdptr)) return FALSE;
   *pptr = nextsigchar(*pptr, TRUE);
@@ -1412,224 +1466,213 @@ Returns:         TRUE if the condition is met
 */
 
 static BOOL
-test_condition(condition_block *c, BOOL toplevel)
+test_condition(condition_block * c, BOOL toplevel)
 {
-BOOL yield;
-const pcre *re;
-uschar *exp[2], *p, *pp;
-const uschar *regcomp_error = NULL;
-int regcomp_error_offset;
+BOOL yield = FALSE, textonly_re;
+const uschar * exp[2], * p, * pp;
 int val[2];
-int i;
 
-if (c == NULL) return TRUE;  /* does this ever occur? */
+if (!c) return TRUE;  /* does this ever occur? */
 
 switch (c->type)
   {
   case cond_and:
-  yield = test_condition(c->left.c, FALSE) &&
-          *error_pointer == NULL &&
-          test_condition(c->right.c, FALSE);
-  break;
+    yield = test_condition(c->left.c, FALSE) &&
+           *error_pointer == NULL &&
+           test_condition(c->right.c, FALSE);
+    break;
 
   case cond_or:
-  yield = test_condition(c->left.c, FALSE) ||
-          (*error_pointer == NULL &&
-          test_condition(c->right.c, FALSE));
-  break;
+    yield = test_condition(c->left.c, FALSE) ||
+           (*error_pointer == NULL &&
+           test_condition(c->right.c, FALSE));
+    break;
 
-  /* The personal test is meaningless in a system filter. The tests are now in
-  a separate function (so Sieve can use them). However, an Exim filter does not
-  scan Cc: (hence the FALSE argument). */
+    /* The personal test is meaningless in a system filter. The tests are now in
+    a separate function (so Sieve can use them). However, an Exim filter does not
+    scan Cc: (hence the FALSE argument). */
 
   case cond_personal:
-  yield = system_filtering? FALSE : filter_personal(c->left.a, FALSE);
-  break;
+    yield = f.system_filtering? FALSE : filter_personal(c->left.a, FALSE);
+    break;
 
   case cond_delivered:
-  yield = filter_delivered;
-  break;
+    yield = filter_delivered;
+    break;
 
-  /* Only TRUE if a message is actually being processed; FALSE for address
-  testing and verification. */
+    /* Only TRUE if a message is actually being processed; FALSE for address
+    testing and verification. */
 
   case cond_errormsg:
-  yield = message_id[0] != 0 &&
-    (sender_address == NULL || sender_address[0] == 0);
-  break;
+    yield = message_id[0] && (!sender_address || !*sender_address);
+    break;
 
-  /* Only FALSE if a message is actually being processed; TRUE for address
-  and filter testing and verification. */
+    /* Only FALSE if a message is actually being processed; TRUE for address
+    and filter testing and verification. */
 
   case cond_firsttime:
-  yield = filter_test != FTEST_NONE || message_id[0] == 0 || deliver_firsttime;
-  break;
+    yield = filter_test != FTEST_NONE || !message_id[0] || f.deliver_firsttime;
+    break;
 
-  /* Only TRUE if a message is actually being processed; FALSE for address
-  testing and verification. */
+    /* Only TRUE if a message is actually being processed; FALSE for address
+    testing and verification. */
 
   case cond_manualthaw:
-  yield = message_id[0] != 0 && deliver_manual_thaw;
-  break;
+    yield = message_id[0] && f.deliver_manual_thaw;
+    break;
 
-  /* The foranyaddress condition loops through a list of addresses */
+    /* The foranyaddress condition loops through a list of addresses */
 
   case cond_foranyaddress:
-  p = c->left.u;
-  pp = expand_string(p);
-  if (pp == NULL)
-    {
-    *error_pointer = string_sprintf("failed to expand \"%s\" in "
-      "filter file: %s", p, expand_string_message);
-    return FALSE;
-    }
-
-  yield = FALSE;
-  parse_allow_group = TRUE;     /* Allow group syntax */
-
-  while (*pp != 0)
-    {
-    uschar *error;
-    int start, end, domain;
-    int saveend;
-
-    p = parse_find_address_end(pp, FALSE);
-    saveend = *p;
-
-    *p = 0;
-    filter_thisaddress =
-      parse_extract_address(pp, &error, &start, &end, &domain, FALSE);
-    *p = saveend;
-
-    if (filter_thisaddress != NULL)
-      {
-      if ((filter_test != FTEST_NONE && debug_selector != 0) ||
-          (debug_selector & D_filter) != 0)
-        {
-        indent();
-        debug_printf("Extracted address %s\n", filter_thisaddress);
-        }
-      yield = test_condition(c->right.c, FALSE);
-      }
-
-    if (yield) break;
-    if (saveend == 0) break;
-    pp = p + 1;
-    }
-
-  parse_allow_group = FALSE;      /* Reset group syntax flags */
-  parse_found_group = FALSE;
-  break;
-
-  /* All other conditions have left and right values that need expanding;
-  on error, it doesn't matter what value is returned. */
-
-  default:
-  p = c->left.u;
-  for (i = 0; i < 2; i++)
-    {
-    exp[i] = expand_string(p);
-    if (exp[i] == NULL)
+    p = c->left.u;
+    if (!(pp = expand_cstring(p)))
       {
       *error_pointer = string_sprintf("failed to expand \"%s\" in "
-        "filter file: %s", p, expand_string_message);
+       "filter file: %s", p, expand_string_message);
       return FALSE;
       }
-    p = c->right.u;
-    }
-
-  /* Inner switch for the different cases */
-
-  switch(c->type)
-    {
-    case cond_is:
-    yield = strcmpic(exp[0], exp[1]) == 0;
-    break;
-
-    case cond_IS:
-    yield = Ustrcmp(exp[0], exp[1]) == 0;
-    break;
 
-    case cond_contains:
-    yield = strstric(exp[0], exp[1], FALSE) != NULL;
-    break;
-
-    case cond_CONTAINS:
-    yield = Ustrstr(exp[0], exp[1]) != NULL;
-    break;
-
-    case cond_begins:
-    yield = strncmpic(exp[0], exp[1], Ustrlen(exp[1])) == 0;
-    break;
-
-    case cond_BEGINS:
-    yield = Ustrncmp(exp[0], exp[1], Ustrlen(exp[1])) == 0;
-    break;
+    yield = FALSE;
+    f.parse_allow_group = TRUE;     /* Allow group syntax */
 
-    case cond_ends:
-    case cond_ENDS:
+    while (*pp)
       {
-      int len = Ustrlen(exp[1]);
-      uschar *s = exp[0] + Ustrlen(exp[0]) - len;
-      yield = (s < exp[0])? FALSE :
-        ((c->type == cond_ends)? strcmpic(s, exp[1]) : Ustrcmp(s, exp[1])) == 0;
+      uschar *error;
+      int start, end, domain;
+      uschar * s;
+
+      p = parse_find_address_end(pp, FALSE);
+      s = string_copyn(pp, p - pp);
+
+      filter_thisaddress =
+       parse_extract_address(s, &error, &start, &end, &domain, FALSE);
+
+      if (filter_thisaddress)
+       {
+       if ((filter_test != FTEST_NONE && debug_selector != 0) ||
+           (debug_selector & D_filter) != 0)
+         {
+         indent();
+         debug_printf_indent("Extracted address %s\n", filter_thisaddress);
+         }
+       yield = test_condition(c->right.c, FALSE);
+       }
+
+      if (yield) break;
+      if (!*p) break;
+      pp = p + 1;
       }
-    break;
 
-    case cond_matches:
-    case cond_MATCHES:
-    if ((filter_test != FTEST_NONE && debug_selector != 0) ||
-        (debug_selector & D_filter) != 0)
-      {
-      debug_printf("Match expanded arguments:\n");
-      debug_printf("  Subject = %s\n", exp[0]);
-      debug_printf("  Pattern = %s\n", exp[1]);
-      }
+    f.parse_allow_group = FALSE;      /* Reset group syntax flags */
+    f.parse_found_group = FALSE;
+    break;
 
-    re = pcre_compile(CS exp[1],
-      PCRE_COPT | ((c->type == cond_matches)? PCRE_CASELESS : 0),
-      (const char **)&regcomp_error, &regcomp_error_offset, NULL);
+    /* All other conditions have left and right values that need expanding;
+    on error, it doesn't matter what value is returned. */
 
-    if (re == NULL)
+    default:
+    p = c->left.u;
+    for (int i = 0; i < 2; i++)
       {
-      *error_pointer = string_sprintf("error while compiling "
-        "regular expression \"%s\": %s at offset %d",
-        exp[1], regcomp_error, regcomp_error_offset);
-      return FALSE;
+      if (!(exp[i] = expand_string_2(p, &textonly_re)))
+       {
+       *error_pointer = string_sprintf("failed to expand \"%s\" in "
+         "filter file: %s", p, expand_string_message);
+       return FALSE;
+       }
+      p = c->right.u;
       }
 
-    yield = regex_match_and_setup(re, exp[0], PCRE_EOPT, -1);
-    break;
-
-    /* For above and below, convert the strings to numbers */
+    /* Inner switch for the different cases */
 
-    case cond_above:
-    case cond_below:
-    for (i = 0; i < 2; i++)
+    switch(c->type)
       {
-      val[i] = get_number(exp[i], &yield);
-      if (!yield)
-        {
-        *error_pointer = string_sprintf("malformed numerical string \"%s\"",
-          exp[i]);
-        return FALSE;
-        }
+      case cond_is:
+       yield = strcmpic(exp[0], exp[1]) == 0;
+       break;
+
+      case cond_IS:
+       yield = Ustrcmp(exp[0], exp[1]) == 0;
+       break;
+
+      case cond_contains:
+       yield = strstric_c(exp[0], exp[1], FALSE) != NULL;
+       break;
+
+      case cond_CONTAINS:
+       yield = Ustrstr(exp[0], exp[1]) != NULL;
+       break;
+
+      case cond_begins:
+       yield = strncmpic(exp[0], exp[1], Ustrlen(exp[1])) == 0;
+       break;
+
+      case cond_BEGINS:
+       yield = Ustrncmp(exp[0], exp[1], Ustrlen(exp[1])) == 0;
+       break;
+
+      case cond_ends:
+      case cond_ENDS:
+       {
+       int len = Ustrlen(exp[1]);
+       const uschar *s = exp[0] + Ustrlen(exp[0]) - len;
+       yield = s < exp[0]
+         ? FALSE
+         : (c->type == cond_ends ? strcmpic(s, exp[1]) : Ustrcmp(s, exp[1])) == 0;
+       break;
+       }
+
+      case cond_matches:
+      case cond_MATCHES:
+       {
+       const pcre2_code * re;
+       mcs_flags flags = textonly_re ? MCS_CACHEABLE : MCS_NOFLAGS;
+
+       if ((filter_test != FTEST_NONE && debug_selector != 0) ||
+           (debug_selector & D_filter) != 0)
+         {
+         debug_printf_indent("Match expanded arguments:\n");
+         debug_printf_indent("  Subject = %s\n", exp[0]);
+         debug_printf_indent("  Pattern = %s\n", exp[1]);
+         }
+
+       if (c->type == cond_matches) flags |= MCS_CASELESS;
+       if (!(re = regex_compile(exp[1], flags, error_pointer, pcre_gen_cmp_ctx)))
+         return FALSE;
+
+       yield = regex_match_and_setup(re, exp[0], PCRE_EOPT, -1);
+       break;
+       }
+
+      /* For above and below, convert the strings to numbers */
+
+      case cond_above:
+      case cond_below:
+       for (int i = 0; i < 2; i++)
+         {
+         val[i] = get_number(exp[i], &yield);
+         if (!yield)
+           {
+           *error_pointer = string_sprintf("malformed numerical string \"%s\"",
+             exp[i]);
+           return FALSE;
+           }
+         }
+       yield = c->type == cond_above ? (val[0] > val[1]) : (val[0] < val[1]);
+       break;
       }
-    yield = (c->type == cond_above)? (val[0] > val[1]) : (val[0] < val[1]);
     break;
-    }
-  break;
   }
 
 if ((filter_test != FTEST_NONE && debug_selector != 0) ||
     (debug_selector & D_filter) != 0)
   {
   indent();
-  debug_printf("%sondition is %s: ",
+  debug_printf_indent("%sondition is %s: ",
     toplevel? "C" : "Sub-c",
     (yield == c->testfor)? "true" : "false");
   print_condition(c, TRUE);
-  debug_printf("\n");
+  debug_printf_indent("\n");
   }
 
 return yield == c->testfor;
@@ -1660,16 +1703,16 @@ Returns:      FF_DELIVERED     success, a significant action was taken
 static int
 interpret_commands(filter_cmd *commands, address_item **generated)
 {
-uschar *s;
+const uschar *s;
 int mode;
 address_item *addr;
 BOOL condition_value;
 
-while (commands != NULL)
+while (commands)
   {
   int ff_ret;
   uschar *fmsg, *ff_name;
-  uschar *expargs[MAILARGS_STRING_COUNT];
+  const uschar *expargs[MAILARGS_STRING_COUNT];
 
   int i, n[2];
 
@@ -1678,21 +1721,15 @@ while (commands != NULL)
 
   for (i = 0; i < (command_exparg_count[commands->command] & 15); i++)
     {
-    uschar *ss = commands->args[i].u;
-    if (ss == NULL)
-      {
+    const uschar *ss = commands->args[i].u;
+    if (!ss)
       expargs[i] = NULL;
-      }
-    else
+    else if (!(expargs[i] = expand_cstring(ss)))
       {
-      expargs[i] = expand_string(ss);
-      if (expargs[i] == NULL)
-        {
-        *error_pointer = string_sprintf("failed to expand \"%s\" in "
-          "%s command: %s", ss, command_list[commands->command],
-          expand_string_message);
-        return FF_ERROR;
-        }
+      *error_pointer = string_sprintf("failed to expand \"%s\" in "
+       "%s command: %s", ss, command_list[commands->command],
+       expand_string_message);
+      return FF_ERROR;
       }
     }
 
@@ -1703,644 +1740,653 @@ while (commands != NULL)
 
   switch(commands->command)
     {
-    case add_command:
-    for (i = 0; i < 2; i++)
-      {
-      uschar *ss = expargs[i];
-      uschar *end;
-
-      if (i == 1 && (*ss++ != 'n' || ss[1] != 0))
-        {
-        *error_pointer = string_sprintf("unknown variable \"%s\" in \"add\" "
-          "command", expargs[i]);
-        return FF_ERROR;
-        }
-
-      /* Allow for "--" at the start of the value (from -$n0) for example */
-      if (i == 0) while (ss[0] == '-' && ss[1] == '-') ss += 2;
-
-      n[i] = (int)Ustrtol(ss, &end, 0);
-      if (*end != 0)
-        {
-        *error_pointer = string_sprintf("malformed number \"%s\" in \"add\" "
-          "command", ss);
-        return FF_ERROR;
-        }
-      }
-
-    filter_n[n[1]] += n[0];
-    if (filter_test != FTEST_NONE) printf("Add %d to n%d\n", n[0], n[1]);
-    break;
-
-    /* A deliver command's argument must be a valid address. Its optional
-    second argument (system filter only) must also be a valid address. */
-
-    case deliver_command:
-    for (i = 0; i < 2; i++)
-      {
-      s = expargs[i];
-      if (s != NULL)
-        {
-        int start, end, domain;
-        uschar *error;
-        uschar *ss = parse_extract_address(s, &error, &start, &end, &domain,
-          FALSE);
-        if (ss != NULL)
-          expargs[i] = ((filter_options & RDO_REWRITE) != 0)?
-            rewrite_address(ss, TRUE, FALSE, global_rewrite_rules,
-              rewrite_existflags) :
-            rewrite_address_qualify(ss, TRUE);
-        else
-          {
-          *error_pointer = string_sprintf("malformed address \"%s\" in "
-            "filter file: %s", s, error);
-          return FF_ERROR;
-          }
-        }
-      }
-
-    /* Stick the errors address into a simple variable, as it will
-    be referenced a few times. Check that the caller is permitted to
-    specify it. */
-
-    s = expargs[1];
-
-    if (s != NULL && !system_filtering)
-      {
-      uschar *ownaddress = expand_string(US"$local_part@$domain");
-      if (strcmpic(ownaddress, s) != 0)
-        {
-        *error_pointer = US"errors_to must point to the caller's address";
-        return FF_ERROR;
-        }
-      }
-
-    /* Test case: report what would happen */
-
-    if (filter_test != FTEST_NONE)
-      {
-      indent();
-      printf("%seliver message to: %s%s%s%s\n",
-        (commands->seen)? "D" : "Unseen d",
-        expargs[0],
-        commands->noerror? " (noerror)" : "",
-        (s != NULL)? " errors_to " : "",
-        (s != NULL)? s : US"");
-      }
-
-    /* Real case. */
-
-    else
-      {
-      DEBUG(D_filter) debug_printf("Filter: %sdeliver message to: %s%s%s%s\n",
-        (commands->seen)? "" : "unseen ",
-        expargs[0],
-        commands->noerror? " (noerror)" : "",
-        (s != NULL)? " errors_to " : "",
-        (s != NULL)? s : US"");
-
-      /* Create the new address and add it to the chain, setting the
-      af_ignore_error flag if necessary, and the errors address, which can be
-      set in a system filter and to the local address in user filters. */
-
-      addr = deliver_make_addr(expargs[0], TRUE);  /* TRUE => copy s */
-      addr->p.errors_address = (s == NULL)?
-        s : string_copy(s);                        /* Default is NULL */
-      if (commands->noerror) setflag(addr, af_ignore_error);
-      addr->next = *generated;
-      *generated = addr;
-      }
-    break;
+    case ADD_COMMAND:
+      for (i = 0; i < 2; i++)
+       {
+       const uschar *ss = expargs[i];
+       uschar *end;
+
+       if (i == 1 && (*ss++ != 'n' || ss[1] != 0))
+         {
+         *error_pointer = string_sprintf("unknown variable \"%s\" in \"add\" "
+           "command", expargs[i]);
+         return FF_ERROR;
+         }
+
+       /* Allow for "--" at the start of the value (from -$n0) for example */
+       if (i == 0) while (ss[0] == '-' && ss[1] == '-') ss += 2;
+
+       n[i] = (int)Ustrtol(ss, &end, 0);
+       if (*end != 0)
+         {
+         *error_pointer = string_sprintf("malformed number \"%s\" in \"add\" "
+           "command", ss);
+         return FF_ERROR;
+         }
+       }
+
+      filter_n[n[1]] += n[0];
+      if (filter_test != FTEST_NONE) printf("Add %d to n%d\n", n[0], n[1]);
+      break;
 
-    case save_command:
-    s = expargs[0];
-    mode = commands->args[1].i;
+      /* A deliver command's argument must be a valid address. Its optional
+      second argument (system filter only) must also be a valid address. */
+
+    case DELIVER_COMMAND:
+      for (i = 0; i < 2; i++)
+       {
+       s = expargs[i];
+       if (s != NULL)
+         {
+         int start, end, domain;
+         uschar *error;
+         uschar *ss = parse_extract_address(s, &error, &start, &end, &domain,
+           FALSE);
+         if (ss)
+           expargs[i] = filter_options & RDO_REWRITE
+             ? rewrite_address(ss, TRUE, FALSE, global_rewrite_rules,
+                               rewrite_existflags)
+             : rewrite_address_qualify(ss, TRUE);
+         else
+           {
+           *error_pointer = string_sprintf("malformed address \"%s\" in "
+             "filter file: %s", s, error);
+           return FF_ERROR;
+           }
+         }
+       }
+
+      /* Stick the errors address into a simple variable, as it will
+      be referenced a few times. Check that the caller is permitted to
+      specify it. */
+
+      s = expargs[1];
+
+      if (s != NULL && !f.system_filtering)
+       {
+       uschar *ownaddress = expand_string(US"$local_part@$domain");
+       if (strcmpic(ownaddress, s) != 0)
+         {
+         *error_pointer = US"errors_to must point to the caller's address";
+         return FF_ERROR;
+         }
+       }
+
+      /* Test case: report what would happen */
 
-    /* Test case: report what would happen */
+      if (filter_test != FTEST_NONE)
+       {
+       indent();
+       printf("%seliver message to: %s%s%s%s\n",
+         (commands->seen)? "D" : "Unseen d",
+         expargs[0],
+         commands->noerror? " (noerror)" : "",
+         (s != NULL)? " errors_to " : "",
+         (s != NULL)? s : US"");
+       }
+
+      /* Real case. */
 
-    if (filter_test != FTEST_NONE)
-      {
-      indent();
-      if (mode < 0)
-        printf("%save message to: %s%s\n", (commands->seen)?
-          "S" : "Unseen s", s, commands->noerror? " (noerror)" : "");
       else
-        printf("%save message to: %s %04o%s\n", (commands->seen)?
-          "S" : "Unseen s", s, mode, commands->noerror? " (noerror)" : "");
-      }
-
-    /* Real case: Ensure save argument starts with / if there is a home
-    directory to prepend. */
-
-    else
-      {
-      DEBUG(D_filter) debug_printf("Filter: %ssave message to: %s%s\n",
-        (commands->seen)? "" : "unseen ", s,
-        commands->noerror? " (noerror)" : "");
-      if (s[0] != '/' && deliver_home != NULL && deliver_home[0] != 0)
-        s = string_sprintf("%s/%s", deliver_home, s);
-
-      /* Create the new address and add it to the chain, setting the
-      af_pfr and af_file flags, the af_ignore_error flag if necessary, and the
-      mode value. */
-
-      addr = deliver_make_addr(s, TRUE);  /* TRUE => copy s */
-      setflag(addr, af_pfr|af_file);
-      if (commands->noerror) setflag(addr, af_ignore_error);
-      addr->mode = mode;
-      addr->next = *generated;
-      *generated = addr;
-      }
-    break;
-
-    case pipe_command:
-    s = string_copy(commands->args[0].u);
-    if (filter_test != FTEST_NONE)
-      {
-      indent();
-      printf("%sipe message to: %s%s\n", (commands->seen)?
-        "P" : "Unseen p", s, commands->noerror? " (noerror)" : "");
-      }
-    else /* Ensure pipe command starts with | */
-      {
-      DEBUG(D_filter) debug_printf("Filter: %spipe message to: %s%s\n",
-        (commands->seen)? "" : "unseen ", s,
-        commands->noerror? " (noerror)" : "");
-      if (s[0] != '|') s = string_sprintf("|%s", s);
-
-      /* Create the new address and add it to the chain, setting the
-      af_ignore_error flag if necessary. Set the af_expand_pipe flag so that
-      each command argument is expanded in the transport after the command
-      has been split up into separate arguments. */
-
-      addr = deliver_make_addr(s, TRUE);  /* TRUE => copy s */
-      setflag(addr, af_pfr|af_expand_pipe);
-      if (commands->noerror) setflag(addr, af_ignore_error);
-      addr->next = *generated;
-      *generated = addr;
-
-      /* If there are any numeric variables in existence (e.g. after a regex
-      condition), or if $thisaddress is set, take a copy for use in the
-      expansion. Note that we can't pass NULL for filter_thisaddress, because
-      NULL terminates the list. */
-
-      if (expand_nmax >= 0 || filter_thisaddress != NULL)
-        {
-        int i;
-        int ecount = (expand_nmax >= 0)? expand_nmax : -1;
-        uschar **ss = store_get(sizeof(uschar *) * (ecount + 3));
-        addr->pipe_expandn = ss;
-        if (filter_thisaddress == NULL) filter_thisaddress = US"";
-        *ss++ = string_copy(filter_thisaddress);
-        for (i = 0; i <= expand_nmax; i++)
-          *ss++ = string_copyn(expand_nstring[i], expand_nlength[i]);
-        *ss = NULL;
-        }
-      }
-    break;
-
-    /* Set up the file name and mode, and close any previously open
-    file. */
+       {
+       DEBUG(D_filter) debug_printf_indent("Filter: %sdeliver message to: %s%s%s%s\n",
+         (commands->seen)? "" : "unseen ",
+         expargs[0],
+         commands->noerror? " (noerror)" : "",
+         (s != NULL)? " errors_to " : "",
+         (s != NULL)? s : US"");
+
+       /* Create the new address and add it to the chain, setting the
+       af_ignore_error flag if necessary, and the errors address, which can be
+       set in a system filter and to the local address in user filters. */
+
+       addr = deliver_make_addr(US expargs[0], TRUE);  /* TRUE => copy s, so deconst ok */
+       addr->prop.errors_address = !s ? NULL : string_copy(s); /* Default is NULL */
+       if (commands->noerror) addr->prop.ignore_error = TRUE;
+       addr->next = *generated;
+       *generated = addr;
+       }
+      break;
 
-    case logfile_command:
-    log_mode = commands->args[1].i;
-    if (log_mode == -1) log_mode = 0600;
-    if (log_fd >= 0)
-      {
-      (void)close(log_fd);
-      log_fd = -1;
-      }
-    log_filename = expargs[0];
-    if (filter_test != FTEST_NONE)
-      {
-      indent();
-      printf("%sogfile %s\n", (commands->seen)? "Seen l" : "L", log_filename);
-      }
-    break;
+    case SAVE_COMMAND:
+      s = expargs[0];
+      mode = commands->args[1].i;
 
-    case logwrite_command:
-    s = expargs[0];
+      /* Test case: report what would happen */
 
-    if (filter_test != FTEST_NONE)
-      {
-      indent();
-      printf("%sogwrite \"%s\"\n", (commands->seen)? "Seen l" : "L",
-        string_printing(s));
-      }
+      if (filter_test != FTEST_NONE)
+       {
+       indent();
+       if (mode < 0)
+         printf("%save message to: %s%s\n", (commands->seen)?
+           "S" : "Unseen s", s, commands->noerror? " (noerror)" : "");
+       else
+         printf("%save message to: %s %04o%s\n", (commands->seen)?
+           "S" : "Unseen s", s, mode, commands->noerror? " (noerror)" : "");
+       }
+
+      /* Real case: Ensure save argument starts with / if there is a home
+      directory to prepend. */
 
-    /* Attempt to write to a log file only if configured as permissible.
-    Logging may be forcibly skipped for verifying or testing. */
+      else
+       {
+       if (s[0] != '/' && (filter_options & RDO_PREPEND_HOME) != 0 &&
+           deliver_home != NULL && deliver_home[0] != 0)
+         s = string_sprintf("%s/%s", deliver_home, s);
+       DEBUG(D_filter) debug_printf_indent("Filter: %ssave message to: %s%s\n",
+         (commands->seen)? "" : "unseen ", s,
+         commands->noerror? " (noerror)" : "");
+
+       /* Create the new address and add it to the chain, setting the
+       af_pfr and af_file flags, the af_ignore_error flag if necessary, and the
+       mode value. */
+
+       addr = deliver_make_addr(US s, TRUE);  /* TRUE => copy s, so deconst ok */
+       setflag(addr, af_pfr);
+       setflag(addr, af_file);
+       if (commands->noerror) addr->prop.ignore_error = TRUE;
+       addr->mode = mode;
+       addr->next = *generated;
+       *generated = addr;
+       }
+      break;
 
-    else if ((filter_options & RDO_LOG) != 0)   /* Locked out */
-      {
-      DEBUG(D_filter)
-        debug_printf("filter log command aborted: euid=%ld\n",
-        (long int)geteuid());
-      *error_pointer = US"logwrite command forbidden";
-      return FF_ERROR;
-      }
-    else if ((filter_options & RDO_REALLOG) != 0)
-      {
-      int len;
-      DEBUG(D_filter) debug_printf("writing filter log as euid %ld\n",
-        (long int)geteuid());
-      if (log_fd < 0)
-        {
-        if (log_filename == NULL)
-          {
-          *error_pointer = US"attempt to obey \"logwrite\" command "
-            "without a previous \"logfile\"";
-          return FF_ERROR;
-          }
-        log_fd = Uopen(log_filename, O_CREAT|O_APPEND|O_WRONLY, log_mode);
-        if (log_fd < 0)
-          {
-          *error_pointer = string_open_failed(errno, "filter log file \"%s\"",
-            log_filename);
-          return FF_ERROR;
-          }
-        }
-      len = Ustrlen(s);
-      if (write(log_fd, s, len) != len)
-        {
-        *error_pointer = string_sprintf("write error on file \"%s\": %s",
-          log_filename, strerror(errno));
-        return FF_ERROR;
-        }
-      }
-    else
-      {
-      DEBUG(D_filter) debug_printf("skipping logwrite (verifying or testing)\n");
-      }
-    break;
+    case PIPE_COMMAND:
+      s = string_copy(commands->args[0].u);
+      if (filter_test != FTEST_NONE)
+       {
+       indent();
+       printf("%sipe message to: %s%s\n", (commands->seen)?
+         "P" : "Unseen p", s, commands->noerror? " (noerror)" : "");
+       }
+      else /* Ensure pipe command starts with | */
+       {
+       DEBUG(D_filter) debug_printf_indent("Filter: %spipe message to: %s%s\n",
+         commands->seen ? "" : "unseen ", s,
+         commands->noerror ? " (noerror)" : "");
+       if (s[0] != '|') s = string_sprintf("|%s", s);
+
+       /* Create the new address and add it to the chain, setting the
+       af_ignore_error flag if necessary. Set the af_expand_pipe flag so that
+       each command argument is expanded in the transport after the command
+       has been split up into separate arguments. */
+
+       addr = deliver_make_addr(US s, TRUE);  /* TRUE => copy s, so deconst ok */
+       setflag(addr, af_pfr);
+       setflag(addr, af_expand_pipe);
+       if (commands->noerror) addr->prop.ignore_error = TRUE;
+       addr->next = *generated;
+       *generated = addr;
+
+       /* If there are any numeric variables in existence (e.g. after a regex
+       condition), or if $thisaddress is set, take a copy for use in the
+       expansion. Note that we can't pass NULL for filter_thisaddress, because
+       NULL terminates the list. */
+
+       if (expand_nmax >= 0 || filter_thisaddress != NULL)
+         {
+         int ecount = expand_nmax >= 0 ? expand_nmax : -1;
+         uschar ** ss = store_get(sizeof(uschar *) * (ecount + 3), GET_UNTAINTED);
+
+         addr->pipe_expandn = ss;
+         if (!filter_thisaddress) filter_thisaddress = US"";
+         *ss++ = string_copy(filter_thisaddress);
+         for (int i = 0; i <= expand_nmax; i++)
+           *ss++ = string_copyn(expand_nstring[i], expand_nlength[i]);
+         *ss = NULL;
+         }
+       }
+      break;
 
-    /* Header addition and removal is available only in the system filter. The
-    command is rejected at parse time otherwise. However "headers charset" is
-    always permitted. */
+      /* Set up the file name and mode, and close any previously open
+      file. */
+
+    case LOGFILE_COMMAND:
+      log_mode = commands->args[1].i;
+      if (log_mode == -1) log_mode = 0600;
+      if (log_fd >= 0)
+       {
+       (void)close(log_fd);
+       log_fd = -1;
+       }
+      log_filename = expargs[0];
+      if (filter_test != FTEST_NONE)
+       {
+       indent();
+       printf("%sogfile %s\n", (commands->seen)? "Seen l" : "L", log_filename);
+       }
+      break;
 
-    case headers_command:
-      {
-      int subtype = commands->args[1].i;
+    case LOGWRITE_COMMAND:
       s = expargs[0];
 
       if (filter_test != FTEST_NONE)
-        printf("Headers %s \"%s\"\n", (subtype == TRUE)? "add" :
-          (subtype == FALSE)? "remove" : "charset", string_printing(s));
-
-      if (subtype == TRUE)
-        {
-        while (isspace(*s)) s++;
-        if (s[0] != 0)
-          {
-          header_add(htype_other, "%s%s", s, (s[Ustrlen(s)-1] == '\n')?
-            "" : "\n");
-          header_last->type = header_checkname(header_last, FALSE);
-          if (header_last->type >= 'a') header_last->type = htype_other;
-          }
-        }
-
-      else if (subtype == FALSE)
-        {
-        int sep = 0;
-        uschar *ss;
-        uschar *list = s;
-        uschar buffer[128];
-        while ((ss = string_nextinlist(&list, &sep, buffer, sizeof(buffer)))
-               != NULL)
-          header_remove(0, ss);
-        }
-
-      /* This setting lasts only while the filter is running; on exit, the
-      variable is reset to the previous value. */
+       {
+       indent();
+       printf("%sogwrite \"%s\"\n", (commands->seen)? "Seen l" : "L",
+         string_printing(s));
+       }
+
+      /* Attempt to write to a log file only if configured as permissible.
+      Logging may be forcibly skipped for verifying or testing. */
+
+      else if ((filter_options & RDO_LOG) != 0)   /* Locked out */
+       {
+       DEBUG(D_filter)
+         debug_printf_indent("filter log command aborted: euid=%ld\n",
+         (long int)geteuid());
+       *error_pointer = US"logwrite command forbidden";
+       return FF_ERROR;
+       }
+      else if ((filter_options & RDO_REALLOG) != 0)
+       {
+       int len;
+       DEBUG(D_filter) debug_printf_indent("writing filter log as euid %ld\n",
+         (long int)geteuid());
+       if (log_fd < 0)
+         {
+         if (!log_filename)
+           {
+           *error_pointer = US"attempt to obey \"logwrite\" command "
+             "without a previous \"logfile\"";
+           return FF_ERROR;
+           }
+         log_fd = Uopen(log_filename, O_CREAT|O_APPEND|O_WRONLY, log_mode);
+         if (log_fd < 0)
+           {
+           *error_pointer = string_open_failed("filter log file \"%s\"",
+             log_filename);
+           return FF_ERROR;
+           }
+         }
+       len = Ustrlen(s);
+       if (write(log_fd, s, len) != len)
+         {
+         *error_pointer = string_sprintf("write error on file \"%s\": %s",
+           log_filename, strerror(errno));
+         return FF_ERROR;
+         }
+       }
+      else
+       DEBUG(D_filter)
+         debug_printf_indent("skipping logwrite (verifying or testing)\n");
+      break;
 
-      else headers_charset = s;
-      }
-    break;
+      /* Header addition and removal is available only in the system filter. The
+      command is rejected at parse time otherwise. However "headers charset" is
+      always permitted. */
+
+    case HEADERS_COMMAND:
+       {
+       int subtype = commands->args[1].i;
+       s = expargs[0];
+
+       if (filter_test != FTEST_NONE)
+         printf("Headers %s \"%s\"\n",
+           subtype == TRUE ? "add"
+           : subtype == FALSE ? "remove"
+           : "charset",
+           string_printing(s));
+
+       if (subtype == TRUE)
+         {
+         if (Uskip_whitespace(&s))
+           {
+           header_add(htype_other, "%s%s", s,
+             s[Ustrlen(s)-1] == '\n' ? "" : "\n");
+           header_last->type = header_checkname(header_last, FALSE);
+           if (header_last->type >= 'a') header_last->type = htype_other;
+           }
+         }
+
+       else if (subtype == FALSE)
+         {
+         int sep = 0;
+         const uschar * list = s;
+
+         for (uschar * ss; ss = string_nextinlist(&list, &sep, NULL, 0); )
+           header_remove(0, ss);
+         }
+
+       /* This setting lasts only while the filter is running; on exit, the
+       variable is reset to the previous value. */
+
+       else headers_charset = s;
+       }
+      break;
 
-    /* Defer, freeze, and fail are available only when explicitly permitted.
-    These commands are rejected at parse time otherwise. The message can get
-    very long by the inclusion of message headers; truncate if it is, and also
-    ensure printing characters so as not to mess up log files. */
+      /* Defer, freeze, and fail are available only when explicitly permitted.
+      These commands are rejected at parse time otherwise. The message can get
+      very long by the inclusion of message headers; truncate if it is, and also
+      ensure printing characters so as not to mess up log files. */
 
-    case defer_command:
-    ff_name = US"defer";
-    ff_ret = FF_DEFER;
-    goto DEFERFREEZEFAIL;
+    case DEFER_COMMAND:
+      ff_name = US"defer";
+      ff_ret = FF_DEFER;
+      goto DEFERFREEZEFAIL;
 
-    case fail_command:
-    ff_name = US"fail";
-    ff_ret = FF_FAIL;
-    goto DEFERFREEZEFAIL;
+    case FAIL_COMMAND:
+      ff_name = US"fail";
+      ff_ret = FF_FAIL;
+      goto DEFERFREEZEFAIL;
 
-    case freeze_command:
-    ff_name = US"freeze";
-    ff_ret = FF_FREEZE;
+    case FREEZE_COMMAND:
+      ff_name = US"freeze";
+      ff_ret = FF_FREEZE;
 
     DEFERFREEZEFAIL:
-    fmsg = expargs[0];
-    if (Ustrlen(fmsg) > 1024) Ustrcpy(fmsg + 1000, " ... (truncated)");
-    fmsg = string_printing(fmsg);
-    *error_pointer = fmsg;
-
-    if (filter_test != FTEST_NONE)
-      {
-      indent();
-      printf("%c%s text \"%s\"\n", toupper(ff_name[0]), ff_name+1, fmsg);
-      }
-    else DEBUG(D_filter) debug_printf("Filter: %s \"%s\"\n", ff_name, fmsg);
-    return ff_ret;
+      *error_pointer = fmsg = US string_printing(Ustrlen(expargs[0]) > 1024
+       ? string_sprintf("%.1000s ... (truncated)", expargs[0])
+       : string_copy(expargs[0]));
+      for(uschar * s = fmsg; *s; s++)
+       if (!s[1] && *s == '\n') { *s = '\0'; break; }  /* drop trailing newline */
 
-    case finish_command:
-    if (filter_test != FTEST_NONE)
-      {
-      indent();
-      printf("%sinish\n", (commands->seen)? "Seen f" : "F");
-      }
-    else
-      {
-      DEBUG(D_filter) debug_printf("Filter: %sfinish\n",
-        (commands->seen)? " Seen " : "");
-      }
-    finish_obeyed = TRUE;
-    return filter_delivered? FF_DELIVERED : FF_NOTDELIVERED;
-
-    case if_command:
-      {
-      uschar *save_address = filter_thisaddress;
-      int ok = FF_DELIVERED;
-      condition_value = test_condition(commands->args[0].c, TRUE);
-      if (*error_pointer != NULL) ok = FF_ERROR; else
-        {
-        output_indent += 2;
-        ok = interpret_commands(commands->args[condition_value? 1:2].f,
-          generated);
-        output_indent -= 2;
-        }
-      filter_thisaddress = save_address;
-      if (finish_obeyed || (ok != FF_DELIVERED && ok != FF_NOTDELIVERED))
-        return ok;
-      }
-    break;
-
-
-    /* To try to catch runaway loops, do not generate mail if the
-    return path is unset or if a non-trusted user supplied -f <>
-    as the return path. */
+      if (filter_test != FTEST_NONE)
+       {
+       indent();
+       printf("%c%s text \"%s\"\n", toupper(ff_name[0]), ff_name+1, fmsg);
+       }
+      else
+        DEBUG(D_filter) debug_printf_indent("Filter: %s \"%s\"\n", ff_name, fmsg);
+      return ff_ret;
 
-    case mail_command:
-    case vacation_command:
-    if (return_path == NULL || return_path[0] == 0)
-      {
+    case FINISH_COMMAND:
       if (filter_test != FTEST_NONE)
-        printf("%s command ignored because return_path is empty\n",
-          command_list[commands->command]);
-      else DEBUG(D_filter) debug_printf("%s command ignored because return_path "
-        "is empty\n", command_list[commands->command]);
+       {
+       indent();
+       printf("%sinish\n", (commands->seen)? "Seen f" : "F");
+       }
+      else
+       DEBUG(D_filter) debug_printf_indent("Filter: %sfinish\n",
+         commands->seen ? " Seen " : "");
+      finish_obeyed = TRUE;
+      return filter_delivered ? FF_DELIVERED : FF_NOTDELIVERED;
+
+    case IF_COMMAND:
+       {
+       uschar *save_address = filter_thisaddress;
+       int ok = FF_DELIVERED;
+       condition_value = test_condition(commands->args[0].c, TRUE);
+       if (*error_pointer)
+         ok = FF_ERROR;
+       else
+         {
+         output_indent += 2;
+         ok = interpret_commands(commands->args[condition_value? 1:2].f,
+           generated);
+         output_indent -= 2;
+         }
+       filter_thisaddress = save_address;
+       if (finish_obeyed  ||  ok != FF_DELIVERED && ok != FF_NOTDELIVERED)
+         return ok;
+       }
       break;
-      }
-
-    /* Check the contents of the strings. The type of string can be deduced
-    from the value of i.
-
-    . If i is equal to mailarg_index_text it's a text string for the body,
-      where anything goes.
-
-    . If i is > mailarg_index_text, we are dealing with a file name, which
-      cannot contain non-printing characters.
 
-    . If i is less than mailarg_index_headers we are dealing with something
-      that will go in a single message header line, where newlines must be
-      followed by white space.
-
-    . If i is equal to mailarg_index_headers, we have a string that contains
-      one or more headers. Newlines that are not followed by white space must
-      be followed by a header name.
-    */
-
-    for (i = 0; i < MAILARGS_STRING_COUNT; i++)
-      {
-      uschar *p;
-      uschar *s = expargs[i];
 
-      if (s == NULL) continue;
-
-      if (i != mailarg_index_text) for (p = s; *p != 0; p++)
-        {
-        int c = *p;
-        if (i > mailarg_index_text)
-          {
-          if (!mac_isprint(c))
-            {
-            *error_pointer = string_sprintf("non-printing character in \"%s\" "
-              "in %s command", string_printing(s),
-              command_list[commands->command]);
-            return FF_ERROR;
-            }
-          }
-
-        /* i < mailarg_index_text */
-
-        else if (c == '\n' && !isspace(p[1]))
-          {
-          if (i < mailarg_index_headers)
-            {
-            *error_pointer = string_sprintf("\\n not followed by space in "
-              "\"%.1024s\" in %s command", string_printing(s),
-              command_list[commands->command]);
-            return FF_ERROR;
-            }
-
-          /* Check for the start of a new header line within the string */
-
-          else
-            {
-            uschar *pp;
-            for (pp = p + 1;; pp++)
-              {
-              c = *pp;
-              if (c == ':' && pp != p + 1) break;
-              if (c == 0 || c == ':' || isspace(*pp))
-                {
-                *error_pointer = string_sprintf("\\n not followed by space or "
-                  "valid header name in \"%.1024s\" in %s command",
-                  string_printing(s), command_list[commands->command]);
-                return FF_ERROR;
-                }
-              pp++;
-              }
-            p = pp;
-            }
-          }
-        }       /* Loop to scan the string */
-
-      /* The string is OK */
-
-      commands->args[i].u = s;
-      }
-
-    /* Proceed with mail or vacation command */
-
-    if (filter_test != FTEST_NONE)
-      {
-      uschar *to = commands->args[mailarg_index_to].u;
-      indent();
-      printf("%sail to: %s%s%s\n", (commands->seen)? "Seen m" : "M",
-        (to == NULL)? US"<default>" : to,
-        (commands->command == vacation_command)? " (vacation)" : "",
-        (commands->noerror)? " (noerror)" : "");
-      for (i = 1; i < MAILARGS_STRING_COUNT; i++)
-        {
-        uschar *arg = commands->args[i].u;
-        if (arg != NULL)
-          {
-          int len = Ustrlen(mailargs[i]);
-          int indent = (debug_selector != 0)? output_indent : 0;
-          while (len++ < 7 + indent) printf(" ");
-          printf("%s: %s%s\n", mailargs[i], string_printing(arg),
-            (commands->args[mailarg_index_expand].u != NULL &&
-              Ustrcmp(mailargs[i], "file") == 0)? " (expanded)" : "");
-          }
-        }
-      if (commands->args[mailarg_index_return].u != NULL)
-        printf("Return original message\n");
-      }
-    else
-      {
-      uschar *tt;
-      uschar *log_addr = NULL;
-      uschar *to = commands->args[mailarg_index_to].u;
-      int size = 0;
-      int ptr = 0;
-
-      if (to == NULL) to = expand_string(US"$reply_address");
-      while (isspace(*to)) to++;
-
-      for (tt = to; *tt != 0; tt++)     /* Get rid of newlines */
-        if (*tt == '\n') *tt = ' ';
-
-      DEBUG(D_filter)
-        {
-        debug_printf("Filter: %smail to: %s%s%s\n",
-          (commands->seen)? "seen " : "",
-          to,
-          (commands->command == vacation_command)? " (vacation)" : "",
-          (commands->noerror)? " (noerror)" : "");
-        for (i = 1; i < MAILARGS_STRING_COUNT; i++)
-          {
-          uschar *arg = commands->args[i].u;
-          if (arg != NULL)
-            {
-            int len = Ustrlen(mailargs[i]);
-            while (len++ < 15) debug_printf(" ");
-            debug_printf("%s: %s%s\n", mailargs[i], string_printing(arg),
-              (commands->args[mailarg_index_expand].u != NULL &&
-                Ustrcmp(mailargs[i], "file") == 0)? " (expanded)" : "");
-            }
-          }
-        }
-
-      /* Create the "address" for the autoreply. This is used only for logging,
-      as the actual recipients are extracted from the To: line by -t. We use the
-      same logic here to extract the working addresses (there may be more than
-      one). Just in case there are a vast number of addresses, stop when the
-      string gets too long. */
-
-      tt = to;
-      while (*tt != 0)
-        {
-        uschar *ss = parse_find_address_end(tt, FALSE);
-        uschar *recipient, *errmess;
-        int start, end, domain;
-        int temp = *ss;
-
-        *ss = 0;
-        recipient = parse_extract_address(tt, &errmess, &start, &end, &domain,
-          FALSE);
-        *ss = temp;
-
-        /* Ignore empty addresses and errors; an error will occur later if
-        there's something really bad. */
-
-        if (recipient != NULL)
-          {
-          log_addr = string_cat(log_addr, &size, &ptr,
-            (log_addr == NULL)? US">" : US",", 1);
-          log_addr = string_cat(log_addr, &size, &ptr, recipient,
-            Ustrlen(recipient));
-          }
-
-        /* Check size */
-
-        if (ptr > 256)
-          {
-          log_addr = string_cat(log_addr, &size, &ptr, US", ...", 5);
-          break;
-          }
-
-        /* Move on past this address */
-
-        tt = ss + (*ss? 1:0);
-        while (isspace(*tt)) tt++;
-        }
-
-      if (log_addr == NULL) log_addr = string_sprintf("invalid-to-line");
-        else log_addr[ptr] = 0;
-
-      addr = deliver_make_addr(log_addr, FALSE);
-      setflag(addr, af_pfr);
-      if (commands->noerror) setflag(addr, af_ignore_error);
-      addr->next = *generated;
-      *generated = addr;
-      addr->reply = store_get(sizeof(reply_item));
-      addr->reply->from = NULL;
-      addr->reply->to = string_copy(to);
-      addr->reply->file_expand =
-        commands->args[mailarg_index_expand].u != NULL;
-      addr->reply->expand_forbid = expand_forbid;
-      addr->reply->return_message =
-        commands->args[mailarg_index_return].u != NULL;
-      addr->reply->once_repeat = 0;
-
-      if (commands->args[mailarg_index_once_repeat].u != NULL)
-        {
-        addr->reply->once_repeat =
-          readconf_readtime(commands->args[mailarg_index_once_repeat].u, 0,
-            FALSE);
-        if (addr->reply->once_repeat < 0)
-          {
-          *error_pointer = string_sprintf("Bad time value for \"once_repeat\" "
-            "in mail or vacation command: %s",
-            commands->args[mailarg_index_once_repeat]);
-          return FF_ERROR;
-          }
-        }
-
-      /* Set up all the remaining string arguments (those other than "to") */
-
-      for (i = 1; i < mailargs_string_passed; i++)
-        {
-        uschar *ss = commands->args[i].u;
-        *((uschar **)(((uschar *)(addr->reply)) + reply_offsets[i])) =
-          (ss == NULL)? NULL : string_copy(ss);
-        }
-      }
-    break;
-
-    case testprint_command:
-    if (filter_test != FTEST_NONE || (debug_selector & D_filter) != 0)
-      {
-      uschar *s = string_printing(expargs[0]);
-      if (filter_test == FTEST_NONE)
-        debug_printf("Filter: testprint: %s\n", s);
-      else
-        printf("Testprint: %s\n", s);
-      }
+      /* To try to catch runaway loops, do not generate mail if the
+      return path is unset or if a non-trusted user supplied -f <>
+      as the return path. */
+
+    case MAIL_COMMAND:
+    case VACATION_COMMAND:
+       if (!return_path || !*return_path)
+         {
+         if (filter_test != FTEST_NONE)
+           printf("%s command ignored because return_path is empty\n",
+             command_list[commands->command]);
+         else DEBUG(D_filter) debug_printf_indent("%s command ignored because return_path "
+           "is empty\n", command_list[commands->command]);
+         break;
+         }
+
+       /* Check the contents of the strings. The type of string can be deduced
+       from the value of i.
+
+       . If i is equal to mailarg_index_text it's a text string for the body,
+         where anything goes.
+
+       . If i is > mailarg_index_text, we are dealing with a file name, which
+         cannot contain non-printing characters.
+
+       . If i is less than mailarg_index_headers we are dealing with something
+         that will go in a single message header line, where newlines must be
+         followed by white space.
+
+       . If i is equal to mailarg_index_headers, we have a string that contains
+         one or more headers. Newlines that are not followed by white space must
+         be followed by a header name.
+       */
+
+       for (i = 0; i < MAILARGS_STRING_COUNT; i++)
+         {
+         const uschar *s = expargs[i];
+
+         if (!s) continue;
+
+         if (i != mailarg_index_text) for (const uschar * p = s; *p; p++)
+           {
+           int c = *p;
+           if (i > mailarg_index_text)
+             {
+             if (!mac_isprint(c))
+               {
+               *error_pointer = string_sprintf("non-printing character in \"%s\" "
+                 "in %s command", string_printing(s),
+                 command_list[commands->command]);
+               return FF_ERROR;
+               }
+             }
+
+           /* i < mailarg_index_text */
+
+           else if (c == '\n' && !isspace(p[1]))
+             {
+             if (i < mailarg_index_headers)
+               {
+               *error_pointer = string_sprintf("\\n not followed by space in "
+                 "\"%.1024s\" in %s command", string_printing(s),
+                 command_list[commands->command]);
+               return FF_ERROR;
+               }
+
+             /* Check for the start of a new header line within the string */
+
+             else
+               {
+               const uschar *pp;
+               for (pp = p + 1;; pp++)
+                 {
+                 c = *pp;
+                 if (c == ':' && pp != p + 1) break;
+                 if (!c || c == ':' || isspace(c))
+                   {
+                   *error_pointer = string_sprintf("\\n not followed by space or "
+                     "valid header name in \"%.1024s\" in %s command",
+                     string_printing(s), command_list[commands->command]);
+                   return FF_ERROR;
+                   }
+                 }
+               p = pp;
+               }
+             }
+           }       /* Loop to scan the string */
+
+         /* The string is OK */
+
+         commands->args[i].u = s;
+         }
+
+       /* Proceed with mail or vacation command */
+
+       if (filter_test != FTEST_NONE)
+         {
+         const uschar *to = commands->args[mailarg_index_to].u;
+         indent();
+         printf("%sail to: %s%s%s\n", (commands->seen)? "Seen m" : "M",
+           to ? to : US"<default>",
+           commands->command == VACATION_COMMAND ? " (vacation)" : "",
+           commands->noerror ? " (noerror)" : "");
+         for (i = 1; i < MAILARGS_STRING_COUNT; i++)
+           {
+           const uschar *arg = commands->args[i].u;
+           if (arg)
+             {
+             int len = Ustrlen(mailargs[i]);
+             int indent = (debug_selector != 0)? output_indent : 0;
+             while (len++ < 7 + indent) printf(" ");
+             printf("%s: %s%s\n", mailargs[i], string_printing(arg),
+               (commands->args[mailarg_index_expand].u != NULL &&
+                 Ustrcmp(mailargs[i], "file") == 0)? " (expanded)" : "");
+             }
+           }
+         if (commands->args[mailarg_index_return].u)
+           printf("Return original message\n");
+         }
+       else
+         {
+         const uschar *tt;
+         const uschar *to = commands->args[mailarg_index_to].u;
+         gstring * log_addr = NULL;
+
+         if (!to) to = expand_string(US"$reply_address");
+         Uskip_whitespace(&to);
+
+         for (tt = to; *tt; tt++)     /* Get rid of newlines */
+           if (*tt == '\n')
+             {
+             uschar * s = string_copy(to);
+             for (uschar * ss = s; *ss; ss++)
+               if (*ss == '\n') *ss = ' ';
+             to = s;
+             break;
+             }
+
+         DEBUG(D_filter)
+           {
+           debug_printf_indent("Filter: %smail to: %s%s%s\n",
+             commands->seen ? "seen " : "",
+             to,
+             commands->command == VACATION_COMMAND ? " (vacation)" : "",
+             commands->noerror ? " (noerror)" : "");
+           for (i = 1; i < MAILARGS_STRING_COUNT; i++)
+             {
+             const uschar *arg = commands->args[i].u;
+             if (arg)
+               {
+               int len = Ustrlen(mailargs[i]);
+               while (len++ < 15) debug_printf_indent(" ");
+               debug_printf_indent("%s: %s%s\n", mailargs[i], string_printing(arg),
+                 (commands->args[mailarg_index_expand].u != NULL &&
+                   Ustrcmp(mailargs[i], "file") == 0)? " (expanded)" : "");
+               }
+             }
+           }
+
+         /* Create the "address" for the autoreply. This is used only for logging,
+         as the actual recipients are extracted from the To: line by -t. We use the
+         same logic here to extract the working addresses (there may be more than
+         one). Just in case there are a vast number of addresses, stop when the
+         string gets too long. */
+
+         tt = to;
+         while (*tt)
+           {
+           uschar *ss = parse_find_address_end(tt, FALSE);
+           uschar *recipient, *errmess;
+           int start, end, domain;
+           int temp = *ss;
+
+           *ss = 0;
+           recipient = parse_extract_address(tt, &errmess, &start, &end, &domain,
+             FALSE);
+           *ss = temp;
+
+           /* Ignore empty addresses and errors; an error will occur later if
+           there's something really bad. */
+
+           if (recipient)
+             {
+             log_addr = string_catn(log_addr, log_addr ? US"," : US">", 1);
+             log_addr = string_cat (log_addr, recipient);
+             }
+
+           /* Check size */
+
+           if (log_addr && log_addr->ptr > 256)
+             {
+             log_addr = string_catn(log_addr, US", ...", 5);
+             break;
+             }
+
+           /* Move on past this address */
+
+           tt = ss + (*ss ? 1 : 0);
+           Uskip_whitespace(&tt);
+           }
+
+         if (log_addr)
+           addr = deliver_make_addr(string_from_gstring(log_addr), FALSE);
+         else
+           {
+           addr = deliver_make_addr(US ">**bad-reply**", FALSE);
+           setflag(addr, af_bad_reply);
+           }
+
+         setflag(addr, af_pfr);
+         if (commands->noerror) addr->prop.ignore_error = TRUE;
+         addr->next = *generated;
+         *generated = addr;
+
+         addr->reply = store_get(sizeof(reply_item), GET_UNTAINTED);
+         addr->reply->from = NULL;
+         addr->reply->to = string_copy(to);
+         addr->reply->file_expand =
+           commands->args[mailarg_index_expand].u != NULL;
+         addr->reply->expand_forbid = expand_forbid;
+         addr->reply->return_message =
+           commands->args[mailarg_index_return].u != NULL;
+         addr->reply->once_repeat = 0;
+
+         if (commands->args[mailarg_index_once_repeat].u != NULL)
+           {
+           addr->reply->once_repeat =
+             readconf_readtime(commands->args[mailarg_index_once_repeat].u, 0,
+               FALSE);
+           if (addr->reply->once_repeat < 0)
+             {
+             *error_pointer = string_sprintf("Bad time value for \"once_repeat\" "
+               "in mail or vacation command: %s",
+               commands->args[mailarg_index_once_repeat].u);
+             return FF_ERROR;
+             }
+           }
+
+         /* Set up all the remaining string arguments (those other than "to") */
+
+         for (i = 1; i < mailargs_string_passed; i++)
+           {
+           const uschar *ss = commands->args[i].u;
+           *(USS((US addr->reply) + reply_offsets[i])) =
+             ss ? string_copy(ss) : NULL;
+           }
+         }
+       break;
+
+    case TESTPRINT_COMMAND:
+       if (filter_test != FTEST_NONE || (debug_selector & D_filter) != 0)
+         {
+         const uschar *s = string_printing(expargs[0]);
+         if (filter_test == FTEST_NONE)
+           debug_printf_indent("Filter: testprint: %s\n", s);
+         else
+           printf("Testprint: %s\n", s);
+         }
     }
 
   commands = commands->next;
   }
 
-return filter_delivered? FF_DELIVERED : FF_NOTDELIVERED;
+return filter_delivered ? FF_DELIVERED : FF_NOTDELIVERED;
 }
 
 
@@ -2362,9 +2408,10 @@ Returns:     TRUE if the message is deemed to be personal
 BOOL
 filter_personal(string_item *aliases, BOOL scan_cc)
 {
-uschar *self, *self_from, *self_to;
-uschar *psself = NULL, *psself_from = NULL, *psself_to = NULL;
-void *reset_point = store_get(0);
+const uschar *self, *self_from, *self_to;
+uschar *psself = NULL;
+const uschar *psself_from = NULL, *psself_to = NULL;
+rmark reset_point = store_mark();
 BOOL yield;
 header_line *h;
 int to_count = 2;
@@ -2379,14 +2426,13 @@ defined in RFC 2369. We also scan for "Auto-Submitted"; if it is found to
 contain any value other than "no", the message is not personal (RFC 3834).
 Previously the test was for "auto-". */
 
-for (h = header_list; h != NULL; h = h->next)
+for (h = header_list; h; h = h->next)
   {
-  uschar *s;
   if (h->type == htype_old) continue;
 
   if (strncmpic(h->text, US"List-", 5) == 0)
     {
-    s = h->text + 5;
+    uschar * s = h->text + 5;
     if (strncmpic(s, US"Id:", 3) == 0 ||
         strncmpic(s, US"Help:", 5) == 0 ||
         strncmpic(s, US"Subscribe:", 10) == 0 ||
@@ -2399,12 +2445,12 @@ for (h = header_list; h != NULL; h = h->next)
 
   else if (strncmpic(h->text, US"Auto-submitted:", 15) == 0)
     {
-    s = h->text + 15;
-    while (isspace(*s)) s++;
+    uschar * s = h->text + 15;
+    Uskip_whitespace(&s);
     if (strncmpic(s, US"no", 2) != 0) return FALSE;
     s += 2;
-    while (isspace(*s)) s++;
-    if (*s != 0) return FALSE;
+    Uskip_whitespace(&s);
+    if (*s) return FALSE;
     }
   }
 
@@ -2417,18 +2463,18 @@ self_to   = rewrite_one(self, rewrite_to, NULL, FALSE, US"",
   global_rewrite_rules);
 
 
-if (self_from == NULL) self_from = self;
-if (self_to == NULL) self_to = self;
+if (!self_from) self_from = self;
+if (self_to) self_to = self;
 
 /* If there's a prefix or suffix set, we must include the prefixed/
 suffixed version of the local part in the tests. */
 
-if (deliver_localpart_prefix != NULL || deliver_localpart_suffix != NULL)
+if (deliver_localpart_prefix || deliver_localpart_suffix)
   {
   psself = string_sprintf("%s%s%s@%s",
-    (deliver_localpart_prefix == NULL)? US"" : deliver_localpart_prefix,
+    deliver_localpart_prefix ? deliver_localpart_prefix : US"",
     deliver_localpart,
-    (deliver_localpart_suffix == NULL)? US"" : deliver_localpart_suffix,
+    deliver_localpart_suffix ? deliver_localpart_suffix : US"",
     deliver_domain);
   psself_from = rewrite_one(psself, rewrite_from, NULL, FALSE, US"",
     global_rewrite_rules);
@@ -2492,24 +2538,25 @@ Returns:      FF_DELIVERED     success, a significant action was taken
 */
 
 int
-filter_interpret(uschar *filter, int options, address_item **generated,
+filter_interpret(const uschar *filter, int options, address_item **generated,
   uschar **error)
 {
 int i;
 int yield = FF_ERROR;
-uschar *ptr = filter;
-uschar *save_headers_charset = headers_charset;
+const uschar *ptr = filter;
+const uschar *save_headers_charset = headers_charset;
 filter_cmd *commands = NULL;
 filter_cmd **lastcmdptr = &commands;
 
 DEBUG(D_route) debug_printf("Filter: start of processing\n");
+acl_level++;
 
 /* Initialize "not in an if command", set the global flag that is always TRUE
 while filtering, and zero the variables. */
 
 expect_endif = 0;
 output_indent = 0;
-filter_running = TRUE;
+f.filter_running = TRUE;
 for (i = 0; i < FILTER_VARIABLE_COUNT; i++) filter_n[i] = 0;
 
 /* To save having to pass certain values about all the time, make them static.
@@ -2542,35 +2589,35 @@ if (filter_test != FTEST_NONE || (debug_selector & D_filter) != 0)
   switch(yield)
     {
     case FF_DEFER:
-    s = US"Filtering ended by \"defer\".";
-    break;
+      s = US"Filtering ended by \"defer\".";
+      break;
 
     case FF_FREEZE:
-    s = US"Filtering ended by \"freeze\".";
-    break;
+      s = US"Filtering ended by \"freeze\".";
+      break;
 
     case FF_FAIL:
-    s = US"Filtering ended by \"fail\".";
-    break;
+      s = US"Filtering ended by \"fail\".";
+      break;
 
     case FF_DELIVERED:
-    s = US"Filtering set up at least one significant delivery "
-           "or other action.\n"
-           "No other deliveries will occur.";
-    break;
+      s = US"Filtering set up at least one significant delivery "
+            "or other action.\n"
+            "No other deliveries will occur.";
+      break;
 
     case FF_NOTDELIVERED:
-    s = US"Filtering did not set up a significant delivery.\n"
-           "Normal delivery will occur.";
-    break;
+      s = US"Filtering did not set up a significant delivery.\n"
+            "Normal delivery will occur.";
+      break;
 
     case FF_ERROR:
-    s = string_sprintf("Filter error: %s", *error);
-    break;
+      s = string_sprintf("Filter error: %s", *error);
+      break;
     }
 
   if (filter_test != FTEST_NONE) printf("%s\n", CS s);
-    else debug_printf("%s\n", s);
+    else debug_printf_indent("%s\n", s);
   }
 
 /* Close the log file if it was opened, and kill off any numerical variables
@@ -2578,12 +2625,15 @@ before returning. Reset the header decoding charset. */
 
 if (log_fd >= 0) (void)close(log_fd);
 expand_nmax = -1;
-filter_running = FALSE;
+f.filter_running = FALSE;
 headers_charset = save_headers_charset;
 
+acl_level--;
 DEBUG(D_route) debug_printf("Filter: end of processing\n");
 return yield;
 }
 
 
 /* End of filter.c */
+/* vi: aw ai sw=2
+*/