Previous fix for concatenated headers wasn't backwards compatible for
[exim.git] / src / src / expand.c
index 5da6f99b8d953bcc11944d7bd5b762beef94e9c3..665d3d8f978ad40638a870749e2fb6c3d231438c 100644 (file)
@@ -1,4 +1,4 @@
-/* $Cambridge: exim/src/src/expand.c,v 1.59 2006/09/05 14:05:43 ph10 Exp $ */
+/* $Cambridge: exim/src/src/expand.c,v 1.66 2006/10/30 14:59:15 ph10 Exp $ */
 
 /*************************************************
 *     Exim - an Internet mail transport agent    *
@@ -274,7 +274,8 @@ enum {
   vtype_stringptr,      /* value is address of pointer to string */
   vtype_msgbody,        /* as stringptr, but read when first required */
   vtype_msgbody_end,    /* ditto, the end of the message */
-  vtype_msgheaders,     /* the message's headers */
+  vtype_msgheaders,     /* the message's headers, processed */
+  vtype_msgheaders_raw, /* the message's headers, unprocessed */
   vtype_localpart,      /* extract local part from string */
   vtype_domain,         /* extract domain from string */
   vtype_recipients,     /* extract recipients from recipients list */
@@ -300,6 +301,8 @@ enum {
 /* This table must be kept in alphabetical order. */
 
 static var_entry var_table[] = {
+  /* WARNING: Do not invent variables whose names start acl_c or acl_m because
+     they will be confused with user-creatable ACL variables. */
   { "acl_verify_message",  vtype_stringptr,   &acl_verify_message },
   { "address_data",        vtype_stringptr,   &deliver_address_data },
   { "address_file",        vtype_stringptr,   &address_file },
@@ -383,6 +386,7 @@ static var_entry var_table[] = {
   { "message_body_size",   vtype_int,         &message_body_size },
   { "message_exim_id",     vtype_stringptr,   &message_id },
   { "message_headers",     vtype_msgheaders,  NULL },
+  { "message_headers_raw", vtype_msgheaders_raw, NULL },
   { "message_id",          vtype_stringptr,   &message_id },
   { "message_linecount",   vtype_int,         &message_linecount },
   { "message_size",        vtype_int,         &message_size },
@@ -1076,7 +1080,8 @@ Arguments:
   newsize       return the size of memory block that was obtained; may be NULL
                 if exists_only is TRUE
   want_raw      TRUE if called for $rh_ or $rheader_ variables; no processing,
-                other than concatenating, will be done on the header
+                other than concatenating, will be done on the header. Also used
+                for $message_headers_raw.
   charset       name of charset to translate MIME words to; used only if
                 want_raw is false; if NULL, no translation is done (this is
                 used for $bh_ and $bheader_)
@@ -1119,6 +1124,12 @@ for (i = 0; i < 2; i++)
           while (isspace(*t)) t++;          /* remove leading white space */
         ilen = h->slen - (t - h->text);     /* length to insert */
 
+        /* Unless wanted raw, remove trailing whitespace, including the
+        newline. */
+
+        if (!want_raw)
+          while (ilen > 0 && isspace(t[ilen-1])) ilen--;
+
         /* Set comma = 1 if handling a single header and it's one of those
         that contains an address list, except when asked for raw headers. Only
         need to do this once. */
@@ -1130,7 +1141,7 @@ for (i = 0; i < 2; i++)
         /* First pass - compute total store needed; second pass - compute
         total store used, including this header. */
 
-        size += ilen + comma;
+        size += ilen + comma + 1;  /* +1 for the newline */
 
         /* Second pass - concatentate the data, up to a maximum. Note that
         the loop stops when size hits the limit. */
@@ -1139,14 +1150,19 @@ for (i = 0; i < 2; i++)
           {
           if (size > header_insert_maxlen)
             {
-            ilen -= size - header_insert_maxlen;
+            ilen -= size - header_insert_maxlen - 1;
             comma = 0;
             }
           Ustrncpy(ptr, t, ilen);
           ptr += ilen;
-          if (comma != 0 && ilen > 0)
+
+          /* For a non-raw header, put in the comma if needed, then add
+          back the newline we removed above, provided there was some text in
+          the header. */
+
+          if (!want_raw && ilen > 0)
             {
-            ptr[-1] = ',';
+            if (comma != 0) *ptr++ = ',';
             *ptr++ = '\n';
             }
           }
@@ -1154,8 +1170,9 @@ for (i = 0; i < 2; i++)
       }
     }
 
-  /* At end of first pass, truncate size if necessary, and get the buffer
-  to hold the data, returning the buffer size. */
+  /* At end of first pass, return NULL if no header found. Then truncate size
+  if necessary, and get the buffer to hold the data, returning the buffer size.
+  */
 
   if (i == 0)
     {
@@ -1166,10 +1183,6 @@ for (i = 0; i < 2; i++)
     }
   }
 
-/* Remove a redundant added comma if present */
-
-if (comma != 0 && ptr > yield) ptr -= 2;
-
 /* That's all we do for raw header expansion. */
 
 if (want_raw)
@@ -1177,15 +1190,16 @@ if (want_raw)
   *ptr = 0;
   }
 
-/* Otherwise, we remove trailing whitespace, including newlines. Then we do RFC
-2047 decoding, translating the charset if requested. The rfc2047_decode2()
+/* Otherwise, remove a final newline and a redundant added comma. Then we do
+RFC 2047 decoding, translating the charset if requested. The rfc2047_decode2()
 function can return an error with decoded data if the charset translation
 fails. If decoding fails, it returns NULL. */
 
 else
   {
   uschar *decoded, *error;
-  while (ptr > yield && isspace(ptr[-1])) ptr--;
+  if (ptr > yield && ptr[-1] == '\n') ptr--;
+  if (ptr > yield && comma != 0 && ptr[-1] == ',') ptr--;
   *ptr = 0;
   decoded = rfc2047_decode2(yield, check_rfc2047_length, charset, '?', NULL,
     newsize, &error);
@@ -1231,37 +1245,26 @@ find_variable(uschar *name, BOOL exists_only, BOOL skipping, int *newsize)
 int first = 0;
 int last = var_table_size;
 
-/* Handle ACL variables, which are not in the table because their number may
-vary depending on a build-time setting. If the variable's name is not of the
-form acl_mddd or acl_cddd, where the d's are digits, fall through to look for
-other names that start with acl_. */
+/* Handle ACL variables, whose names are of the form acl_cxxx or acl_mxxx.
+Originally, xxx had to be a number in the range 0-9 (later 0-19), but from
+release 4.64 onwards arbitrary names are permitted, as long as the first 5
+characters are acl_c or acl_m and the sixth is either a digit or an underscore
+(this gave backwards compatibility at the changeover). There may be built-in
+variables whose names start acl_ but they should never start in this way. This
+slightly messy specification is a consequence of the history, needless to say.
 
-if (Ustrncmp(name, "acl_", 4) == 0)
-  {
-  uschar *endptr;
-  int offset = -1;
-  int max = 0;
+If an ACL variable does not exist, treat it as empty, unless strict_acl_vars is
+set, in which case give an error. */
 
-  if (name[4] == 'm')
-    {
-    offset = ACL_CVARS;
-    max = ACL_MVARS;
-    }
-  else if (name[4] == 'c')
-    {
-    offset = 0;
-    max = ACL_CVARS;
-    }
-
-  if (offset >= 0)
-    {
-    int n = Ustrtoul(name + 5, &endptr, 10);
-    if (*endptr == 0 && n < max)
-      return (acl_var[offset + n] == NULL)? US"" : acl_var[offset + n];
-    }
+if ((Ustrncmp(name, "acl_c", 5) == 0 || Ustrncmp(name, "acl_m", 5) == 0) &&
+     !isalpha(name[5]))
+  {
+  tree_node *node =
+    tree_search((name[4] == 'c')? acl_var_c : acl_var_m, name + 4);
+  return (node == NULL)? (strict_acl_vars? NULL : US"") : node->data.ptr;
   }
 
-/* Similarly for $auth<n> variables. */
+/* Handle $auth<n> variables. */
 
 if (Ustrncmp(name, "auth", 4) == 0)
   {
@@ -1394,6 +1397,9 @@ while (last > first)
     case vtype_msgheaders:
     return find_header(NULL, exists_only, newsize, FALSE, NULL);
 
+    case vtype_msgheaders_raw:
+    return find_header(NULL, exists_only, newsize, TRUE, NULL);
+
     case vtype_msgbody:                        /* Pointer to msgbody string */
     case vtype_msgbody_end:                    /* Ditto, the end of the msg */
     ss = (uschar **)(var_table[middle].value);
@@ -1575,6 +1581,33 @@ return 0;
 
 
 
+/*************************************************
+*     Elaborate message for bad variable         *
+*************************************************/
+
+/* For the "unknown variable" message, take a look at the variable's name, and
+give additional information about possible ACL variables. The extra information
+is added on to expand_string_message.
+
+Argument:   the name of the variable
+Returns:    nothing
+*/
+
+static void
+check_variable_error_message(uschar *name)
+{
+if (Ustrncmp(name, "acl_", 4) == 0)
+  expand_string_message = string_sprintf("%s (%s)", expand_string_message,
+    (name[4] == 'c' || name[4] == 'm')?
+      (isalpha(name[5])?
+        US"6th character of a user-defined ACL variable must be a digit or underscore" :
+        US"strict_acl_vars is set"    /* Syntax is OK, it has to be this */
+      ) :
+      US"user-defined ACL variables must start acl_c or acl_m");
+}
+
+
+
 /*************************************************
 *        Read and evaluate a condition           *
 *************************************************/
@@ -1681,6 +1714,7 @@ switch(cond_type)
       expand_string_message = (name[0] == 0)?
         string_sprintf("variable name omitted after \"def:\"") :
         string_sprintf("unknown variable \"%s\" after \"def:\"", name);
+      check_variable_error_message(name);
       return NULL;
       }
     if (yield != NULL) *yield = (value[0] != 0) == testfor;
@@ -1893,25 +1927,8 @@ switch(cond_type)
 
     if (!isalpha(name[0]))
       {
-      uschar *endptr;
-      num[i] = (int)Ustrtol((const uschar *)sub[i], &endptr, 10);
-      if (tolower(*endptr) == 'k')
-        {
-        num[i] *= 1024;
-        endptr++;
-        }
-      else if (tolower(*endptr) == 'm')
-        {
-        num[i] *= 1024*1024;
-        endptr++;
-        }
-      while (isspace(*endptr)) endptr++;
-      if (*endptr != 0)
-        {
-        expand_string_message = string_sprintf("\"%s\" is not a number",
-          sub[i]);
-        return NULL;
-        }
+      num[i] = expand_string_integer(sub[i], FALSE);
+      if (expand_string_message != NULL) return NULL;
       }
     }
 
@@ -2976,6 +2993,7 @@ while (*s != 0)
         {
         expand_string_message =
           string_sprintf("unknown variable name \"%s\"", name);
+          check_variable_error_message(name);
         goto EXPAND_FAILED;
         }
       }
@@ -3739,7 +3757,8 @@ while (*s != 0)
           else
             {
             shost.name = server_name;
-            if (host_find_byname(&shost, NULL, NULL, FALSE) != HOST_FOUND)
+            if (host_find_byname(&shost, NULL, HOST_FIND_QUALIFY_SINGLE, NULL,
+                FALSE) != HOST_FOUND)
               {
               expand_string_message =
                 string_sprintf("no IP address found for host %s", shost.name);
@@ -5135,6 +5154,7 @@ while (*s != 0)
       {
       expand_string_message =
         string_sprintf("unknown variable in \"${%s}\"", name);
+      check_variable_error_message(name);
       goto EXPAND_FAILED;
       }
     len = Ustrlen(value);
@@ -5260,22 +5280,26 @@ return yield;
 
 /* Expand a string, and convert the result into an integer.
 
-Argument: the string to be expanded
+Arguments:
+  string  the string to be expanded
+  isplus  TRUE if a non-negative number is expected
 
 Returns:  the integer value, or
           -1 for an expansion error               ) in both cases, message in
           -2 for an integer interpretation error  ) expand_string_message
-
+          expand_string_message is set NULL for an OK integer
 */
 
 int
-expand_string_integer(uschar *string)
+expand_string_integer(uschar *string, BOOL isplus)
 {
 long int value;
 uschar *s = expand_string(string);
 uschar *msg = US"invalid integer \"%s\"";
 uschar *endptr;
 
+/* If expansion failed, expand_string_message will be set. */
+
 if (s == NULL) return -1;
 
 /* On an overflow, strtol() returns LONG_MAX or LONG_MIN, and sets errno
@@ -5283,12 +5307,17 @@ to ERANGE. When there isn't an overflow, errno is not changed, at least on some
 systems, so we set it zero ourselves. */
 
 errno = 0;
+expand_string_message = NULL;               /* Indicates no error */
 value = strtol(CS s, CSS &endptr, 0);
 
 if (endptr == s)
   {
   msg = US"integer expected but \"%s\" found";
   }
+else if (value < 0 && isplus)
+  {
+  msg = US"non-negative integer expected but \"%s\" found";
+  }
 else
   {
   /* Ensure we can cast this down to an int */