/*************************************************
*     Exim - an Internet mail transport agent    *
*************************************************/

/* Copyright (c) University of Cambridge 1995 - 1999 */
/* See the file NOTICE for conditions of use and distribution. */


/* Functions for handling string expansion. */


#include "exim.h"


/* Recursively called function */

static char *expand_string_internal(char *, BOOL, char **, BOOL);



/*************************************************
*            Local statics and tables            *
*************************************************/

/* Type for main variable table */

typedef struct {
  char *name;
  int   type;
  void *value;
} var_entry;

/* Type for entries pointing to address/length pairs. Not currently
in use. */

typedef struct {
  char **address;
  int  *length;
} alblock;

/* Types of table entry */

enum {
  vtype_int,            /* value is address of int */
  vtype_filter_int,     /* ditto, but recognized only when filtering */
  vtype_string,         /* value is address of string */
  vtype_stringptr,      /* value is address of pointer to string */
  vtype_msgbody,        /* as stringptr, but read when first required */
  vtype_msgheaders,     /* the message's headers */
  vtype_localpart,      /* extract local part from string */
  vtype_domain,         /* extract domain from string */
  vtype_recipients,     /* extract recipients from recipients list */
                        /* (enabled only during system filtering */
  vtype_todbsdin,       /* value not used; generate BSD inbox tod */
  vtype_todf,           /* value not used; generate full tod */
  vtype_todl,           /* value not used; generate log tod */
  vtype_reply           /* value not used; get reply from headers */
  };

/* This pointer gets set to values looked up in databases. It must
come before the following table. */

static char *lookup_value = NULL;

/* This table must be kept in alphabetical order. */

static var_entry var_table[] = {
  { "address_file",        vtype_stringptr,   &address_file },
  { "address_pipe",        vtype_stringptr,   &address_pipe },
  { "caller_gid",          vtype_int,         &real_gid },
  { "caller_uid",          vtype_int,         &real_uid },
  { "compile_date",        vtype_stringptr,   &version_date },
  { "compile_number",      vtype_stringptr,   &version_cnumber },
  { "domain",              vtype_stringptr,   &deliver_domain },
  { "domain_data",         vtype_stringptr,   &domain_data },
  { "errmsg_recipient",    vtype_stringptr,   &errmsg_recipient },
  { "home",                vtype_stringptr,   &deliver_home },
  { "host",                vtype_stringptr,   &deliver_host },
  { "host_address",        vtype_stringptr,   &deliver_host_address },
  { "key",                 vtype_stringptr,   &lookup_key },
  { "local_part",          vtype_stringptr,   &deliver_localpart },
  { "local_part_data",     vtype_stringptr,   &local_part_data },
  { "local_part_prefix",   vtype_stringptr,   &deliver_localpart_prefix },
  { "local_part_suffix",   vtype_stringptr,   &deliver_localpart_suffix },
  { "localhost_number",    vtype_int,         &host_number },
  { "message_body",        vtype_msgbody,     &message_body },
  { "message_headers",     vtype_msgheaders,  NULL },
  { "message_id",          vtype_stringptr,   &message_id },
  { "message_precedence",  vtype_stringptr,   &message_precedence },
  { "message_size",        vtype_int,         &message_size },
  { "n0",                  vtype_filter_int,  &filter_n[0] },
  { "n1",                  vtype_filter_int,  &filter_n[1] },
  { "n2",                  vtype_filter_int,  &filter_n[2] },
  { "n3",                  vtype_filter_int,  &filter_n[3] },
  { "n4",                  vtype_filter_int,  &filter_n[4] },
  { "n5",                  vtype_filter_int,  &filter_n[5] },
  { "n6",                  vtype_filter_int,  &filter_n[6] },
  { "n7",                  vtype_filter_int,  &filter_n[7] },
  { "n8",                  vtype_filter_int,  &filter_n[8] },
  { "n9",                  vtype_filter_int,  &filter_n[9] },
  { "original_domain",     vtype_stringptr,   &deliver_domain_orig },
  { "original_local_part", vtype_stringptr,   &deliver_localpart_orig },
  { "originator_gid",      vtype_int,         &originator_gid },
  { "originator_uid",      vtype_int,         &originator_uid },
  { "parent_domain",       vtype_stringptr,   &deliver_domain_parent },
  { "parent_local_part",   vtype_stringptr,   &deliver_localpart_parent },
  { "primary_hostname",    vtype_stringptr,   &primary_hostname },
  { "prohibition_reason",  vtype_stringptr,   &prohibition_reason },
  { "qualify_domain",      vtype_stringptr,   &qualify_domain_sender },
  { "qualify_recipient",   vtype_stringptr,   &qualify_domain_recipient },
  { "received_for",        vtype_stringptr,   &received_for },
  { "received_protocol",   vtype_stringptr,   &received_protocol },
  { "recipients",          vtype_recipients,  NULL },
  { "recipients_count",    vtype_int,         &recipients_count },
  { "reply_address",       vtype_reply,       NULL },
  { "return_path",         vtype_stringptr,   &return_path },
  { "return_size_limit",   vtype_int,         &return_size_limit },
  { "route_option",        vtype_stringptr,   &route_option },
  { "self_hostname",       vtype_stringptr,   &self_hostname },
  { "sender_address",      vtype_stringptr,   &sender_address },
  { "sender_address_domain", vtype_domain,    &sender_address },
  { "sender_address_local_part", vtype_localpart, &sender_address },
  { "sender_fullhost",     vtype_stringptr,   &sender_fullhost },
  { "sender_helo_name",    vtype_stringptr,   &sender_helo_name },
  { "sender_host_address", vtype_stringptr,   &sender_host_address },
  { "sender_host_name",    vtype_stringptr,   &sender_host_name },
  { "sender_ident",        vtype_stringptr,   &sender_ident },
  { "sender_rcvhost",      vtype_stringptr,   &sender_rcvhost },
  { "sn0",                 vtype_filter_int,  &filter_sn[0] },
  { "sn1",                 vtype_filter_int,  &filter_sn[1] },
  { "sn2",                 vtype_filter_int,  &filter_sn[2] },
  { "sn3",                 vtype_filter_int,  &filter_sn[3] },
  { "sn4",                 vtype_filter_int,  &filter_sn[4] },
  { "sn5",                 vtype_filter_int,  &filter_sn[5] },
  { "sn6",                 vtype_filter_int,  &filter_sn[6] },
  { "sn7",                 vtype_filter_int,  &filter_sn[7] },
  { "sn8",                 vtype_filter_int,  &filter_sn[8] },
  { "sn9",                 vtype_filter_int,  &filter_sn[9] },
  { "spool_directory",     vtype_stringptr,   &spool_directory },
  { "thisaddress",         vtype_stringptr,   &filter_thisaddress },
  { "tod_bsdinbox",        vtype_todbsdin,    NULL },
  { "tod_full",            vtype_todf,        NULL },
  { "tod_log",             vtype_todl,        NULL },
  { "value",               vtype_stringptr,   &lookup_value },
  { "version_number",      vtype_stringptr,   &version_string },
  { "warnmsg_delay",       vtype_stringptr,   &warnmsg_delay },
  { "warnmsg_recipient",   vtype_stringptr,   &warnmsg_recipients },
  { "warnmsg_recipients",  vtype_stringptr,   &warnmsg_recipients }
};

static int var_table_size = sizeof(var_table)/sizeof(var_entry);
static char var_buffer[256];

static char *hashcodes = "abcdefghijklmnopqrtsuvwxyz"
                         "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                         "0123456789";





/*************************************************
*          Check a condition string              *
*************************************************/

/* This function is called to expand a string, and test the result for a "true"
or "false" value. Failure of the expansion is panic-worthy. All store used by
the function can be released on exit.

Arguments:
  condition     the condition string
  m1            text to be incorporated in panic error
  m2            ditto

Returns:        TRUE if condition is met, FALSE if not
*/

BOOL
expand_check_condition(char *condition, char *m1, char *m2)
{
int rc;
void *reset_point = store_get(0);
char *ss = expand_string(condition);
if (ss == NULL && !expand_string_forcedfail)
  log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to expand condition\"%s\" "
    "for %s %s: %s", condition, m1, m2, expand_string_message);
if (ss == NULL) return FALSE;
rc = ss[0] != 0 && strcmp(ss, "0") != 0 && strcmpic(ss, "no") != 0 &&
  strcmpic(ss, "false") != 0;
store_reset(reset_point);
return rc;
}



/*************************************************
*             Pick out a name from a string      *
*************************************************/

/* If the name is too long, it is silently truncated.

Arguments:
  name      points to a buffer into which to put the name
  max       is the length of the buffer
  s         points to the first alphabetic character of the name
  extras    chars other than alphanumerics to permit

Returns:    pointer to the first character after the name

Note: The test for *s != 0 in the while loop is necessary because
strchr() yields non-NULL if the character is zero (which is not something
I expected). */

static char *
read_name(char *name, int max, char *s, char *extras)
{
int ptr = 0;
while (*s != 0 && (isalnum(*s) || strchr(extras, *s) != NULL))
  {
  if (ptr < max-1) name[ptr++] = *s;
  s++;
  }
name[ptr] = 0;
return s;
}



/*************************************************
*     Pick out the rest of a header name         *
*************************************************/

/* A variable name starting $header_ (or just $h_ for those who like
abbreviations) might not be the complete header name because headers can
contain any printing characters in their names, except ':'. This function is
called to read the rest of the name, chop h[eader]_ off the front, and put ':'
on the end, if the name was terminated by white space.

Arguments:
  name      points to a buffer in which the name read so far exists
  max       is the length of the buffer
  s         points to the first character after the name so far, i.e. the
            first non-alphameric character after $header_xxxxx

Returns:    a pointer to the first character after the header name
*/

static char *
read_header_name(char *name, int max, char *s)
{
int prelen = strchr(name, '_') - name + 1;
int ptr = strlen(name) - prelen;
if (ptr > 0) memmove(name, name+prelen, ptr);
while (mac_isgraph(*s) && *s != ':')
  {
  if (ptr < max-1) name[ptr++] = *s;
  s++;
  }
if (*s == ':') s++;
name[ptr++] = ':';
name[ptr] = 0;
return s;
}



/*************************************************
*           Pick out a number from a string      *
*************************************************/

/* Arguments:
  n     points to an integer into which to put the number
  s     points to the first digit of the number

Returns:  a pointer to the character after the last digit
*/

static char *
read_number(int *n, char *s)
{
*n = 0;
while (isdigit(*s)) *n = *n * 10 + (*s++ - '0');
return s;
}



/*************************************************
*        Extract keyed subfield from a string    *
*************************************************/

/* This function is also used by the search routines when given a multi-level
key to search for. The yield is in dynamic store; NULL means that the key
was not found.

Arguments:
  key       points to the name of the subkey
  s         points to the string from which to extract the subfield

Returns:    NULL if the subfield was not found, or
            a pointer to the subfield's data
*/

char *
expand_getkeyed(char *key, char *s)
{
int length = (int)strlen(key);
while (isspace(*s)) s++;

/* Loop to search for the key */

while (*s != 0)
  {
  int subkeylength, datalength;
  char *data;
  char *subkey = s;

  while (*s != 0 && *s != '=' && !isspace(*s)) s++;
  subkeylength = s - subkey;

  while (isspace(*s)) s++;
  if (*s == '=') while (isspace(*(++s)));

  /* For now, just find the end of the data field - interpret quoted
  string later if it is actually wanted. */

  data = s;
  if (*s == '\"')
    {
    while (*(++s) != 0 && *s != '\"')
      {
      if (*s == '\\' && s[1] != 0) s++;
      }
    if (*s == '\"') s++;
    }
  else while (*s != 0 && !isspace(*s)) s++;
  datalength = s - data;

  /* If keys match, set up the subfield as the yield and return. If
  the value is quoted, interpret the string (which cannot be longer than
  the original). */

  if (length == subkeylength && strncmp(key, subkey, length) == 0)
    {
    char *yield = store_get(datalength + 1);

    if (*data == '\"')
      {
      int i = 0;
      for (;;)
        {
        int ch = *(++data);
        if (ch == 0 || ch == '\"') break;
        if (ch == '\\') ch = string_interpret_escape(&data);
        yield[i++] = ch;
        }
      yield[i] = 0;
      }

    /* Not a quoted string */
    else
      {
      strncpy(yield, data, datalength);
      yield[datalength] = 0;
      }
    return yield;
    }

  /* Move on to next subkey */

  while (isspace(*s)) s++;
  }

return NULL;
}




/*************************************************
*   Extract numbered subfield from string        *
*************************************************/

/* Extracts a numbered field from a string that is divided by tokens - for
example a line from /etc/passwd is divided by colon characters.  First field is
numbered one.  Returns NULL if the field number is < 0, or if there are
insufficient tokens in the string

***WARNING***
Modifies final argument - this is a dynamically generated string, so that's OK.

Arguments:
  field       number of field to be extracted,
                first field = 1, whole string = 0
  separators  characters that are used to break string into tokens
  s           points to the string from which to extract the subfield

Returns:      NULL if the field was not found,
              a pointer to the field's data inside s (modified to add 0)
*/

static char *
expand_gettokened (int field, char *separators, char *s)
{
int sep = 1;
char *fieldtext;

/* Invalid field number returns NULL; zero field number returns the whole
subject string. */

if (field < 0) return NULL;
if (field == 0) return s;

/* Search for the required field; create it in place. */

while (field-- > 0)
  {
  size_t len;
  if (sep == 0) return NULL;   /* Previous field was end of string */
  fieldtext = s;
  len = strcspn(s, separators);
  sep = s[len];
  s[len] = 0;
  s += len + 1;
  }

return fieldtext;
}




/*************************************************
*               Find value of a variable         *
*************************************************/

/* The table of variables is kept in alphabetic order, so we
can search it using a binary chop. The "choplen" variable is
nothing to do with the binary chop. It can be set non-zero to
cause chars at the end of the returned string to be disregarded.
It should already be zero on entry.

Arguments:
  name        the name of the variable being sought
  choplen     a pointer to an int which is set if characters at the end
              of the returned data are to be ignored (typically '\n' at the
              end of header lines); it should normally be set zero before
              calling this function

Returns:      NULL if the variable does not exist, or
              a pointer to the variable's contents
*/

static char *
find_variable(char *name, int *choplen)
{
int first = 0;
int last = var_table_size;

while (last > first)
  {
  header_line *h;
  char *s, *domain, *localpart;
  char **ss;
  int middle = (first + last)/2;
  int c = strcmp(name, var_table[middle].name);

  if (c == 0) switch (var_table[middle].type)
    {
    case vtype_filter_int:
    if (!filter_running) return NULL;
    /* Fall through */

    case vtype_int:
    sprintf(var_buffer, "%d", *(int *)(var_table[middle].value)); /* Integer */
    return var_buffer;

    case vtype_string:                         /* String */
    return (char *)(var_table[middle].value);

    case vtype_stringptr:                      /* Pointer to string */
    s = *((char **)(var_table[middle].value));
    return (s == NULL)? "" : s;

    case vtype_localpart:                      /* Get local part from address */
    s = *((char **)(var_table[middle].value));
    if (s == NULL) return "";
    domain = strrchr(s, '@');
    if (domain == NULL) return s;
    localpart = domain - 1;
    while (localpart > s && localpart[-1] != ':') localpart--;
    if (domain - localpart > sizeof(var_buffer) - 1)
      log_write(0, LOG_PANIC_DIE, "local part longer than %d in string "
        "expansion", sizeof(var_buffer));
    strncpy(var_buffer, localpart, domain - localpart);
    var_buffer[domain - localpart] = 0;
    return var_buffer;

    case vtype_domain:                         /* Get domain from address */
    s = *((char **)(var_table[middle].value));
    if (s == NULL) return "";
    domain = strrchr(s, '@');
    return (domain == NULL)? "" : domain + 1;

    case vtype_msgheaders:
      {
      int ptr = 0;
      int size = 100;
      header_line *h;
      s = store_get(size);
      for (h = header_list; h != NULL; h = h->next)
        {
        if (h->type == '*') continue;
        s = string_cat(s, &size, &ptr, h->text, h->slen);
        }
      s[ptr] = 0;
      }
    return s;

    case vtype_msgbody:                        /* Pointer to msgbody string */
    ss = (char **)(var_table[middle].value);
    if (*ss == NULL && deliver_datafile >= 0)  /* Read body when needed */
      {
      char *body;
      int len = message_body_visible;
      if (len > message_size) len = message_size;
      *ss = body = store_get(len+1);
      body[0] = 0;
      lseek(deliver_datafile, data_start_offset, SEEK_SET);
      len = read(deliver_datafile, body, len);
      if (len >= 0) body[len] = 0;
      while (*body != 0)
        {
        if (*body == '\n') *body = ' ';
        body++;
        }
      }
    return (*ss == NULL)? "" : *ss;

    case vtype_todbsdin:                       /* BSD inbox time of day */
    return tod_stamp(tod_bsdin);

    case vtype_todf:                           /* Full time of day */
    return tod_stamp(tod_full);

    case vtype_todl:                           /* Log format time of day */
    return tod_stamp(tod_log);

    case vtype_reply:                          /* Get reply address */
    s = NULL;
    for (h = header_list; h != NULL; h = h->next)
      {
      if (h->type == htype_replyto) s = strchr(h->text, ':') + 1;
      if (h->type == htype_from && s == NULL) s = strchr(h->text, ':') + 1;
      }
    if (s == NULL) return "";
    while (isspace(*s)) s++;

    /* Disregard final \n in header (but note that isspace() might already
    have skipped over it if the header is empty). */

    if (choplen != NULL && *s != 0) *choplen = 1;
    return s;

    /* A recipients list is available only during system message filtering
    or if there is only one recipient, but not elsewhere. */

    case vtype_recipients:
    if (!enable_dollar_recipients) return NULL; else
      {
      int size = 128;
      int ptr = 0;
      int i;
      s = store_get(size);
      for (i = 0; i < recipients_count; i++)
        {
        if (i != 0) s = string_cat(s, &size, &ptr, ", ", 2);
        s = string_cat(s, &size, &ptr, recipients_list[i].address,
          (int)strlen(recipients_list[i].address));
        }
      s[ptr] = 0;                             /* string_cat leaves room */
      }
    return s;
    }

  else if (c > 0) first = middle + 1;
  else last = middle;
  }

return NULL;
}




/*************************************************
*          Find the value of a header            *
*************************************************/

/*
Arguments:
  name       the name of the header, without the leading $header_ or $h_

Returns:     NULL if the header does not exist, else
             a pointer to the header's contents
*/

static char *
find_header(char *name)
{
int len = (int)strlen(name);
char *yield = NULL;
header_line *h;

for (h = header_list; h != NULL; h = h->next)
  {
  if (h->type != htype_old)
    {
    if (len <= h->slen && strncmpic(name, h->text, len) == 0)
      {
      if (yield == NULL)
        {
        yield = h->text + len;
        while (isspace(*yield)) yield++;
        }
      else
        {
        char *newyield = store_get((int)strlen(yield) + h->slen - len + 1);
        strcpy(newyield, yield);
        strcat(newyield, h->text + len);
        yield = newyield;
        }
      }
    }
  }
return yield;
}




/*************************************************
*        Read and evaluate a condition           *
*************************************************/

/*
Arguments:
  s        points to the start of the condition text
  yield    points to a BOOL to hold the result of the condition test;
           if NULL, we are just reading through a condition that is
           part of an "or" combination to check syntax, or in a state
           where the answer isn't required

Returns:   a pointer to the first character after the condition, or
           NULL after an error
*/

static char *
eval_condition(char *s, BOOL *yield)
{
BOOL testfor = TRUE;
char name[256];

for (;;)
  {
  while (isspace(*s)) s++;
  if (*s == '!') { testfor = !testfor; s++; } else break;
  }

/* Numeric comparisons are symbolic */

if (*s == '=' || *s == '>' || *s == '<')
  {
  int p = 0;
  name[p++] = *s++;
  if (*s == '=')
    {
    name[p++] = '=';
    s++;
    }
  name[p] = 0;
  }

/* All other conditions are named */

else s = read_name(name, 256, s, "_");

/* def: tests for a non-zero or non-NULL variable, or for an existing
header */

if (strcmp(name, "def") == 0 && *s == ':')
  {
  char *value;

  s = read_name(name, 256, s+1, "_");

  /* Test for a header's existence */

  if (strncmp(name, "header_", 7) == 0 || strncmp(name, "h_", 2) == 0)
    {
    s = read_header_name(name, 256, s);
    value = find_header(name);
    if (yield != NULL) *yield = (value != NULL) == testfor;
    }

  /* Test for a variable's having a non-empty value. */

  else
    {
    value = find_variable(name, NULL);
    if (value == NULL)
      {
      expand_string_message = string_sprintf("unknown variable: %s", name);
      return NULL;
      }
    if (yield != NULL)
      *yield = (value[0] != 0 && strcmp(value, "0") != 0) == testfor;
    }

  return s;
  }

/* exists: tests for file existence */

else if (strcmp(name, "exists") == 0)
  {
  char *filename;
  struct stat statbuf;
  while (isspace(*s)) s++;
  if (*s != '{') goto COND_FAILED_CURLY;
  filename = expand_string_internal(s+1, TRUE, &s, FALSE);
  if (filename == NULL) return NULL;
  if (*s++ != '}') goto COND_FAILED_CURLY;
  if (yield != NULL)
    *yield = (stat(filename, &statbuf) == 0) == testfor;
  return s;
  }

/* eq: tests for string equality */

/* symbolic operators for numeric testing */

/* match: does a regular expression match and sets up the numerical
variables if it succeeds */

else if (strcmp(name, "eq") == 0 ||
         strcmp(name, "match") == 0 ||
         !isalpha(name[0]))
  {
  int i;
  int roffset;
  int num[2];
  pcre *re;
  const char *rerror;
  char *sub[2];

  for (i = 0; i < 2; i++)
    {
    while (isspace(*s)) s++;
    if (*s != '{') goto COND_FAILED_CURLY;
    sub[i] = expand_string_internal(s+1, TRUE, &s, yield == NULL);
    if (sub[i] == NULL) return NULL;
    if (*s++ != '}') goto COND_FAILED_CURLY;

    /* Convert to numerical if required */

    if (!isalpha(name[0]))
      {
      char *endptr;
      num[i] = (int)strtol((const char *)sub[i], &endptr, 10);
      if (tolower(*endptr) == 'k')
        {
        num[i] *= 1024;
        endptr++;
        }
      else if (tolower(*endptr) == 'm')
        {
        num[i] *= 1024*1024;
        endptr++;
        }
      if (*endptr != 0)
        {
        expand_string_message = string_sprintf("\"%s\" is not a number",
          sub[i]);
        return NULL;
        }
      }
    }

  /* Result not required */

  if (yield == NULL) return s;

  /* Do an appropriate comparison */

  switch(name[0])
    {
    case '=':
    *yield = (num[0] == num[1]) == testfor;
    break;

    case '>':
    *yield = ((name[1] == '=')? (num[0] >= num[1]) : (num[0] > num[1]))
      == testfor;
    break;

    case '<':
    *yield = ((name[1] == '=')? (num[0] <= num[1]) : (num[0] < num[1]))
      == testfor;
    break;

    case 'e':   /* Straight text comparison */
    *yield = (strcmp(sub[0], sub[1]) == 0) == testfor;
    break;

    case 'm':   /* Regular expression match */
    re = pcre_compile(sub[1], PCRE_COPT, &rerror, &roffset, NULL);
    if (re == NULL)
      {
      expand_string_message = string_sprintf("regular expression error in "
        "\"%s\": %s at offset %d", sub[1], rerror, roffset);
      return NULL;
      }
    *yield = regex_match_and_setup(re, sub[0], 0, -1) == testfor;
    break;
    }

  return s;
  }

/* and/or: computes logical and/or of several conditions */

else if (strcmp(name, "or") == 0 || strcmp(name, "and") == 0)
  {
  BOOL temp;
  BOOL *ptr = (yield == NULL)? NULL : &temp;
  BOOL and = strcmp(name, "and") == 0;
  int comb = and;

  while (isspace(*s)) s++;
  if (*s++ != '{') goto COND_FAILED_CURLY;

  for (;;)
    {
    while (isspace(*s)) s++;
    if (*s != '{') break;
    s = eval_condition(s+1, ptr);
    if (s == NULL) return NULL;
    while (isspace(*s)) s++;
    if (*s++ != '}') goto COND_FAILED_CURLY;
    if (yield != NULL)
      {
      if (and)
        {
        comb &= temp;
        if (!comb) ptr = NULL;  /* once false, don't evaluate any more */
        }
      else
        {
        comb |= temp;
        if (comb) ptr = NULL;   /* once true, don't evaluate any more */
        }
      }
    }

  if (*s++ != '}') goto COND_FAILED_CURLY;
  if (yield != NULL) *yield = (comb == testfor);
  return s;
  }

/* Unknown type of condition */

expand_string_message = string_sprintf("unknown condition \"%s\"", name);
return NULL;

/* Bracketing error */

COND_FAILED_CURLY:
expand_string_message = "missing or misplaced { or }";
return NULL;
}




/*************************************************
*                 Expand string                  *
*************************************************/

/* Returns either an unchanged string, or the expanded string in stacking pool
store. Interpreted sequences are:

   \c                    where c is any character -> copies c literally
   $name                 substitutes the variable
   ${name}               ditto
   ${op:string}          operates on the string value
   ${extract {key} {string}}
                         extracts keyed substring; null if not found
   ${extract {field} {separator} {string}}
                         extracts numbered field from separated string;
                         null if not found; field 0 is the whole string
   ${if cond {s1} {s2}}  conditional; the yielded string is expanded
                         {s2} can be replaced by "fail" or be omitted

One-key+file lookups:
   ${lookup{key}search-type{file}{found}{not-found}}
                         the key, file, & strings are expanded; $value available
                         and {not-found} can be replaced by "fail" or be
                         omitted.
                         The key can in fact consist of mainkey:subkey, in
                         which case a subfield is extracted from the found
                         string, which must consist of key=value pairs.

Database-query lookups:
   ${lookup search-type{query}{found}{not-found}}
                         the query and strings are expanded; $value available
                         and {not-found} can be replaced by "fail" or be
                         omitted.

Operators:
   domain                  extracts domain from an address
   expand                  expands the string one more time
   hash_<n>_<m>            hash the string, making one that is of length n,
                             using m (default 26) characters from hashcodes
   lc                      lowercase the string
   uc                      uppercase the string
   length_<n>              take first n chars only
   l_<n>                   ditto
   local_part              extracts local part from an address
   quote                   quote the argument if it contains anything other
                             than letters, digits, underscores, dots, & hyphens;
                             quoting means putting inside "" and \-quoting any
                             \ or " in the string
   rxquote                 regular expression quote: any non-alphameric is
                             quoted with \
   substr_<m>_<n>          substring n chars from offset m
   s_<m>_<n>               ditto
                             negative offset works from the rhs
                             omitted length => rest (either to left or right)

Conditions:
   !cond                   negates condition
   def:variable            variable is defined and not empty
   def:$h_xxx              header xxx exists
   exists {string}         file exists
   match {string}{re}      regular expression match
   eq {string1}{string2}   strings are equal, case included
   == {num1}{num2}         numbers are equal
   > >= < <=               similar numeric comparisons
   or {{cond1}{cond2}...}  as it says
   and {{cond1}{cond2}...} ditto

We use an internal routine recursively to handle embedded substrings. The
external function follows. The yield is NULL if the expansion failed, and there
are two cases: if something collapsed syntactically, or if "fail" was given
as the action on a lookup failure. These can be distinguised by looking at the
variable expand_string_forcedfail, which is TRUE in the latter case.

The skipping flag is set true when expanding a substring that isn't actually
going to be used (after "if" or "lookup") and it prevents lookups from
happening lower down.

Store usage: At start, a store block of the length of the input plus 64
is obtained. This is expanded as necessary by string_cat(), which might have to
get a new block, or might be able to expand the original. At the end of the
function we can release any store above that portion of the yield block that
was actually used. In many cases this will be optimal.

Arguments:
  string         the string to be expanded
  ket_ends       true if expansion is to stop at }
  left           if not NULL, a pointer to the first character after the
                 expansion is placed here (typically used with ket_ends)
  skipping       TRUE for recursive calls when the value isn't actually going
                 to be used (to allow for optimisation)

Returns:         NULL if expansion fails:
                   expand_string_forcedfail is set TRUE if failure was forced
                   expand_string_message contains a textual error message
                 a pointer to the expanded string on success
*/

static char *
expand_string_internal(char *string, BOOL ket_ends, char **left, BOOL skipping)
{
int ptr = 0;
int size = (int)strlen(string) + 64;
char *s = string;
char *yield = store_get(size);

expand_string_forcedfail = FALSE;
expand_string_message = "";

while (*s != 0)
  {
  char *value;
  char name[256];

  /* \ escapes the next character, which must exist, or else
  the expansion fails. */

  if (*s == '\\')
    {
    char ch[1];
    if (*s == 0)
      {
      expand_string_message = "\\ at end of string";
      goto EXPAND_FAILED;
      }
    ch[0] = string_interpret_escape(&s);
    s++;
    yield = string_cat(yield, &size, &ptr, ch, 1);
    continue;
    }

  /* Anything other than $ is just copied verbatim, unless we are
  looking for a terminating } character. */

  if (ket_ends && *s == '}') break;

  if (*s != '$')
    {
    yield = string_cat(yield, &size, &ptr, s++, 1);
    continue;
    }

  /* No { after the $ - must be a plain name or a number for string
  match variable. There has to be a fudge for variables that are the
  names of header fields preceded by "$header_" because header field
  names can contain any printing characters except space and colon.
  For those that don't like typing this much, "$h_" is a synonym for
  "$header_". A non-existent header yields a NULL value; nothing is
  inserted. */

  if (isalpha(*(++s)))
    {
    int choplen = 0;
    s = read_name(name, sizeof(name), s, "_");
    if (strncmp(name, "header_", 7) == 0 || strncmp(name, "h_", 2) == 0)
      {
      s = read_header_name(name, sizeof(name), s);
      value = find_header(name);
      choplen = 1;
      }
    else
      {
      value = find_variable(name, &choplen);
      if (value == NULL)
        {
        expand_string_message = string_sprintf("unknown variable: %s", name);
        goto EXPAND_FAILED;
        }
      }
    if (value != NULL)
      {
      int len = (int)strlen(value) - choplen;
      if (len < 0) len = 0;
      yield = string_cat(yield, &size, &ptr, value, len);
      }
    continue;
    }

  if (isdigit(*s))
    {
    int n;
    s = read_number(&n, s);
    if (n <= expand_nmax)
      yield = string_cat(yield, &size, &ptr, expand_nstring[n],
        expand_nlength[n]);
    continue;
    }

  /* Otherwise, if there's no '{' after $ it's an error. */

  if (*s != '{')
    {
    expand_string_message = "$ not followed by letter, digit, or {";
    goto EXPAND_FAILED;
    }

  /* After { there can be various things, but they all start with
  an initial word, except for a number for a string match variable. */

  if (isdigit(*(++s)))
    {
    int n;
    s = read_number(&n, s);
    if (*s++ != '}')
      {
      expand_string_message = "} expected after number";
      goto EXPAND_FAILED;
      }
    if (n <= expand_nmax)
      yield = string_cat(yield, &size, &ptr, expand_nstring[n],
        expand_nlength[n]);
    continue;
    }

  if (!isalpha(*s))
    {
    expand_string_message = "letter or digit expected after ${";
    goto EXPAND_FAILED;
    }

  /* Allow "-" in names to cater for substrings with negative
  arguments. Since we are checking for known names after { this is
  OK. */

  s = read_name(name, sizeof(name), s, "_-");

  /* Handle conditionals - preserve the values of the numerical expansion
  variables in case they get changed by a regular expression match in the
  condition. If not, they retain their external settings. At the end
  of this "if" section, they get restored to their previous values. */

  if (strcmp(name, "if") == 0)
    {
    BOOL cond = TRUE;
    char *sub1, *sub2;
    char *save_expand_nstring[EXPAND_MAXN+1];
    int save_expand_nlength[EXPAND_MAXN+1];
    int save_expand_nmax = expand_nmax;
    int i;

    for (i = 0; i <= expand_nmax; i++)
      {
      save_expand_nstring[i] = expand_nstring[i];
      save_expand_nlength[i] = expand_nlength[i];
      }

    s = eval_condition(s, skipping? NULL : &cond);
    if (s == NULL) goto EXPAND_FAILED;  /* message already set */

    /* The condition must be followed by one or two substrings,
    enclosed in braces, with optional space between them and
    after them. Then there must be a final } to end. */

    while (isspace(*s)) s++;
    if (*s != '{') goto EXPAND_FAILED_CURLY;
    sub1 = expand_string_internal(s+1, TRUE, &s, skipping);

    /* Forced failures are noticed only if the condition was true */

    if (sub1 == NULL && (cond || !expand_string_forcedfail))
      goto EXPAND_FAILED;

    expand_string_forcedfail = FALSE;
    if (*s++ != '}') goto EXPAND_FAILED_CURLY;

    while (isspace(*s)) s++;
    if (*s == '}') sub2 = NULL;
    else if (*s == '{')
      {
      sub2 = expand_string_internal(s+1, TRUE, &s, skipping);
      if (sub2 == NULL && (!cond || !expand_string_forcedfail))
        goto EXPAND_FAILED;
      expand_string_forcedfail = FALSE;
      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
      }
    else
      {
      s = read_name(name, sizeof(name), s, "_");
      if (strcmp(name, "fail") == 0)
        {
        if (!cond)
          {
          expand_string_message = "\"if\" failed and \"fail\" requested";
          expand_string_forcedfail = TRUE;
          goto EXPAND_FAILED;
          }
        else sub2 = NULL;
        }
      else
        {
        expand_string_message = "syntax error in \"else\" substring";
        goto EXPAND_FAILED;
        }
      }

    while (isspace(*s)) s++;
    if (*s++ != '}') goto EXPAND_FAILED_CURLY;

    /* Add the appropriate string to the result */

    if (cond)
      yield = string_cat(yield, &size, &ptr, sub1, (int)strlen(sub1));
    else if (sub2 != NULL)
      yield = string_cat(yield, &size, &ptr, sub2, (int)strlen(sub2));

    /* Restore external setting of expansion variables for continuation
    at this level. */

    expand_nmax = save_expand_nmax;
    for (i = 0; i <= expand_nmax; i++)
      {
      expand_nstring[i] = save_expand_nstring[i];
      expand_nlength[i] = save_expand_nlength[i];
      }

    continue;
    }

  /* Handle database lookups. If "skipping" is TRUE, we are expanding an
  internal string that isn't actually going to be used. All we need to
  do is check the syntax, so don't do a lookup at all. Preserve the values
  of the numerical expansion variables in case they get changed by a partial
  lookup. If not, they retain their external settings. At the end
  of this "lookup" section, they get restored to their previous values. */

  if (strcmp(name, "lookup") == 0)
    {
    int stype;
    int pv = -1;
    BOOL looked_up;
    char *ss, *key, *filename, *sub1;
    char *lookup_errmsg = NULL;
    char *save_lookup_value = lookup_value;

    char *save_expand_nstring[EXPAND_MAXN+1];
    int save_expand_nlength[EXPAND_MAXN+1];
    int save_expand_nmax = expand_nmax;
    int expand_setup = 0;
    int i;

    for (i = 0; i <= expand_nmax; i++)
      {
      save_expand_nstring[i] = expand_nstring[i];
      save_expand_nlength[i] = expand_nlength[i];
      }

    lookup_value = NULL;

    /* Get the key we are to look up for single-key+file style lookups.
    Otherwise set the key NULL pro-tem. */

    while (isspace(*s)) s++;
    if (*s == '{')
      {
      key = expand_string_internal(s+1, TRUE, &s, FALSE);
      if (key == NULL) goto EXPAND_FAILED;
      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
      while (isspace(*s)) s++;
      }
    else key = NULL;

    /* Find out the type of database */

    if (!isalpha(*s))
      {
      expand_string_message = "missing lookup type";
      goto EXPAND_FAILED;
      }
    s = read_name(name, sizeof(name), s, "-_");
    ss = name;
    while (isspace(*s)) s++;

    /* Handle request for partial lookup */

    if (strncmp(ss, "partial", 7) == 0)
      {
      ss += 7;
      if (isdigit (*ss))
        {
        pv = 0;
        while (isdigit(*ss)) pv = pv*10 + *ss++ - '0';
        }
      else pv = 2;
      if (*ss++ != '-')
        {
        expand_string_message = string_sprintf("unknown lookup type \"%s\"",
          name);
        goto EXPAND_FAILED;
        }
      }

    /* If a single-key lookup type is followed by "*" it requests looking
    for a default "*" entry if all else fails. If it is followed by "*@" it
    requests replacing everying before @ by * first. These are independent of
    partial matching, but are encoded by adding 1024 and 2048 to the partial
    match value. */

    if (strncmp(s, "*@", 2) == 0)
      {
      s += 2;
      pv += 1024 + 2048;
      }
    else if (*s == '*')
      {
      s++;
      pv += 1024;
      }

    /* Now check for the individual search type. Only those that are actually
    in the binary are valid. */

    stype = search_findtype(ss, &expand_string_message);
    if (stype < 0) goto EXPAND_FAILED;

    /* Check that a key was provided for those lookup types that need it,
    and was not supplied for those that use the query style, and that
    "partial" was provided only for a non-query lookup. */

    if (!mac_islookup(stype, lookup_querystyle))
      {
      if (key == NULL)
        {
        expand_string_message = string_sprintf("missing {key} for single-"
          "key \"%s\" lookup", name);
        goto EXPAND_FAILED;
        }
      }
    else
      {
      if (pv >= 0)
        {
        expand_string_message = string_sprintf("\"partial\" is not permitted "
          "for lookup type \"%s\"", ss);
        goto EXPAND_FAILED;
        }

      if (key != NULL)
        {
        expand_string_message = string_sprintf("a single key was given for "
          "lookup type \"%s\", which is not a single-key lookup type", name);
        goto EXPAND_FAILED;
        }
      }

    /* Get the next string in brackets and expand it. It is the file name for
    single-key+file lookups, and the whole query otherwise. */

    if (*s != '{') goto EXPAND_FAILED_CURLY;
    filename = expand_string_internal(s+1, TRUE, &s, FALSE);
    if (filename == NULL) goto EXPAND_FAILED;
    if (*s++ != '}') goto EXPAND_FAILED_CURLY;
    while (isspace(*s)) s++;

    /* If this isn't a single-key+file lookup, re-arrange the variables
    to be appropriate for the search_ functions. */

    if (key == NULL)
      {
      key = filename;
      filename = NULL;
      }

    /* If skipping, don't do the next bit - it has the result of leaving
    lookup_value == NULL, as if the entry was not found. Note that there is no
    search_close() function. Files are left open in case of re-use. At suitable
    places in higher logic, search_tidyup() is called to tidy all open files.
    This can save opening the same file several times. However, files may also
    get closed when others are opened, if too many are open at once. The rule
    is that a handle should not be used after a second search_open().

    Request that a partial search sets up $1 and maybe $2 by passing
    expand_setup containing zero. If its value changes, reset expand_nmax,
    since new variables will have been set. Note that at the end of this
    "lookup" section, the old numeric variables are restored. */

    if (!skipping)
      {
      void *handle = search_open(filename, stype, 0, NULL, NULL,
        &expand_string_message);
      if (handle == NULL) goto EXPAND_FAILED;
      lookup_value = search_find(handle, filename, key, pv, &expand_setup,
        &lookup_errmsg);
      if (search_find_defer)
        {
        expand_string_message =
          string_sprintf("lookup of \"%s\" gave DEFER: %s", key, lookup_errmsg);
        goto EXPAND_FAILED;
        }
      if (expand_setup > 0) expand_nmax = expand_setup;
      }

    /* Remember whether this lookup succeeded or not; the actual value
    of lookup_value may get changed if the nested expansions also do
    lookups. */

    looked_up = lookup_value != NULL;

    /* Expand the first substring; $value will get the looked up value. */

    while (isspace(*s)) s++;
    if (*s != '{') goto EXPAND_FAILED_CURLY;
    sub1 = expand_string_internal(s+1, TRUE, &s, !looked_up);

    /* Forced failures are noticed only if the original lookup succeeded. */

    if (sub1 == NULL && (looked_up || !expand_string_forcedfail))
      goto EXPAND_FAILED;
    expand_string_forcedfail = FALSE;

    if (*s++ != '}') goto EXPAND_FAILED_CURLY;

    /* If the lookup succeeded, add the new string to the output. */

    if (looked_up)
      yield = string_cat(yield, &size, &ptr, sub1, (int)strlen(sub1));

    /* There now follows either another substring, or "fail", or nothing.
    This time, forced failures are noticed only if the original lookup failed.
    */

    while (isspace(*s)) s++;
    if (*s == '{')
      {
      char *sub2 = expand_string_internal(s+1, TRUE, &s, looked_up);
      if (sub2 == NULL && (!looked_up || !expand_string_forcedfail))
        goto EXPAND_FAILED;
      expand_string_forcedfail = FALSE;
      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
      if (!looked_up)
        yield = string_cat(yield, &size, &ptr, sub2, (int)strlen(sub2));
      }

    /* If the word "fail" is present, and lookup failed, set a flag indicating
    it was a forced failure rather than a syntactic error. Swallow the
    terminating } in case this is nested inside another lookup or if. */

    else if (*s != '}')
      {
      s = read_name(name, sizeof(name), s, "_");
      if (strcmp(name, "fail") == 0)
        {
        if (!looked_up)
          {
          while (isspace(*s)) s++;
          if (*s++ != '}') goto EXPAND_FAILED_CURLY;
          expand_string_message =
            string_sprintf("lookup failed and \"fail\" requested%s%s",
              (lookup_errmsg == NULL)? "" : ": ",
              (lookup_errmsg == NULL)? "" : lookup_errmsg);
          expand_string_forcedfail = TRUE;
          goto EXPAND_FAILED;
          }
        }
      else
        {
        expand_string_message = "syntax error in lookup substring";
        goto EXPAND_FAILED;
        }
      }

    while (isspace(*s)) s++;
    if (*s++ != '}') goto EXPAND_FAILED_CURLY;

    /* Restore external setting of expansion variables for carrying on
    at this level. */

    expand_nmax = save_expand_nmax;
    for (i = 0; i <= expand_nmax; i++)
      {
      expand_nstring[i] = save_expand_nstring[i];
      expand_nlength[i] = save_expand_nlength[i];
      }

    /* Restore lookup_value and continue */

    lookup_value = save_lookup_value;
    continue;
    }

  /* If Perl support is configured, handle calling embedded perl subroutines.
  Syntax is ${perl{sub}} or ${perl{sub}{arg}} or ${perl{sub}{arg1}{arg2}} or
  up to a maximum of EXIM_PERL_MAX_ARGS arguments (defined below). */

  #ifdef EXIM_PERL
  #define EXIM_PERL_MAX_ARGS 8

  if (strcmp(name, "perl") == 0)
    {
    int i = 0;
    char *sub_name;
    char *sub_arg[EXIM_PERL_MAX_ARGS + 1];
    char *new_yield;

    while (isspace(*s)) s++;
    if (*s != '{') goto EXPAND_FAILED_CURLY;
    sub_name = expand_string_internal(s+1, TRUE, &s, FALSE);
    if (sub_name == NULL) goto EXPAND_FAILED;
    while (isspace(*s)) s++;
    if (*s++ != '}') goto EXPAND_FAILED_CURLY;

    while (isspace(*s)) s++;

    while (*s == '{')
      {
      if (i == EXIM_PERL_MAX_ARGS)
	{
	expand_string_message =
	  string_sprintf("Too many arguments passed to Perl subroutine \"%s\" "
	    "(max is %d)", sub_name, EXIM_PERL_MAX_ARGS);
	goto EXPAND_FAILED;
	}
      sub_arg[i] = expand_string_internal(s+1, TRUE, &s, FALSE);
      if (sub_arg[i++] == NULL) goto EXPAND_FAILED;
      while (isspace(*s)) s++;
      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
      while (isspace(*s)) s++;
      }

    if (*s++ != '}') goto EXPAND_FAILED_CURLY;
    sub_arg[i] = 0;

    /* Start the interpreter if necessary */

    if (!opt_perl_started)
      {
      char *initerror;
      if (opt_perl_startup == NULL)
        {
        expand_string_message = "A setting of perl_startup is needed when "
          "using the Perl interpreter";
        goto EXPAND_FAILED;
        }
      DEBUG(9) debug_printf("Starting Perl interpreter\n");
      initerror = init_perl(opt_perl_startup);
      if (initerror != NULL)
        {
        expand_string_message =
          string_sprintf("error in perl_startup code: %s\n", initerror);
        goto EXPAND_FAILED;
        }
      opt_perl_started = TRUE;
      }

    /* Call the function */

    new_yield = call_perl_cat(yield, &size, &ptr, &expand_string_message,
      sub_name, sub_arg);

    /* NULL yield indicates failure; if the message pointer has been set to
    NULL, the yield was undef, indicating a forced failure. Otherwise the
    message will indicate some kind of Perl error. */

    if (new_yield == NULL)
      {
      if (expand_string_message == NULL)
        {
        expand_string_message =
	  string_sprintf("Perl subroutine \"%s\" returned undef to force "
	    "failure", sub_name);
        expand_string_forcedfail = TRUE;
        }
      goto EXPAND_FAILED;
      }

    /* Yield succeeded. Ensure forcedfail is unset, just in case it got
    set during a callback from Perl. */

    expand_string_forcedfail = FALSE;
    yield = new_yield;
    continue;
    }
  #endif /* EXIM_PERL */

  /* Handle keyed and numbered substring extraction. They are distinguised
  by different numbers of arguments. */

  if (strcmp(name, "extract") == 0)
    {
    int i;
    char *sub[4];
    for (i = 0; i < 3; i++)
      {
      while (isspace(*s)) s++;
      if (*s == '{')
	{
	sub[i] = expand_string_internal(s+1, TRUE, &s, FALSE);
	if (sub[i] == NULL) goto EXPAND_FAILED;
	if (*s++ != '}') goto EXPAND_FAILED_CURLY;
	}
      else
	{
	/* Handle either 2 or 3 arguments for 2 versions of extract */
	if (i < 2) goto EXPAND_FAILED_CURLY;
	sub[i] = NULL;
	}
      }

    while (isspace(*s)) s++;
    if (*s++ != '}') goto EXPAND_FAILED_CURLY;

    /* If there are three arguments, this is a numbered field extraction,
    and the first argument must be numerical. If there are two arguments,
    this is a keyed field extraction. */

    if (sub[2] != NULL)
      {
      char *endptr;
      int field_number = (int)strtoul(sub[0], &endptr, 10);
      if (*endptr != 0)
        {
        expand_string_message = string_sprintf("field number \"%s\" for "
          "extract is not numeric", sub[0]);
        goto EXPAND_FAILED;
        }
      sub[3] = expand_gettokened(field_number, sub[1], sub[2]);
      }
    else sub[3] = expand_getkeyed(sub[0], sub[1]);

    /* Insert yield if extraction succeeded; otherwise nothing. */

    if (sub[3] != NULL)
      yield = string_cat(yield, &size, &ptr, sub[3], (int)strlen(sub[3]));
    continue;
    }

  /* Handle operations on a subsequent string */

  if (*s == ':')
    {
    char *sub = expand_string_internal(s+1, TRUE, &s, FALSE);
    if (sub == NULL) goto EXPAND_FAILED;
    s++;

    /* expand expands another time */

    if (strcmp(name, "expand") == 0)
      {
      char *expanded = expand_string_internal(sub, FALSE, NULL, FALSE);
      if (expanded == NULL)
        {
        expand_string_message =
          string_sprintf("internal expansion of \"%s\" failed: %s", sub,
            expand_string_message);
        goto EXPAND_FAILED;
        }
      yield = string_cat(yield, &size, &ptr, expanded, (int)strlen(expanded));
      continue;
      }

    /* lc lowercases */

    if (strcmp(name, "lc") == 0)
      {
      int count = 0;
      char *t = sub - 1;
      while (*(++t) != 0) { *t = tolower(*t); count++; }
      yield = string_cat(yield, &size, &ptr, sub, count);
      continue;
      }

    /* uc lowercases */

    if (strcmp(name, "uc") == 0)
      {
      int count = 0;
      char *t = sub - 1;
      while (*(++t) != 0) { *t = toupper(*t); count++; }
      yield = string_cat(yield, &size, &ptr, sub, count);
      continue;
      }

    /* local_part and domain split up an address; parse fail yields null */

    if (strcmp(name, "local_part") == 0 || strcmp(name, "domain") == 0)
      {
      char *error;
      int start, end, domain;
      char *t = parse_extract_address(sub, &error, &start, &end, &domain,
        FALSE);
      if (t != NULL)
        {
        if (name[0] == 'l')
          {
          if (domain != 0) end = start + domain - 1;
          yield = string_cat(yield, &size, &ptr, sub+start, end-start);
          }
        else if (domain != 0)
          {
          domain += start;
          yield = string_cat(yield, &size, &ptr, sub+domain, end-domain);
          }
        }
      continue;
      }

    /* quote sticks it in quotes if it contains anything other than
    alphamerics, underscore, dot, or hyphen. */

    if (strcmp(name, "quote") == 0)
      {
      BOOL needs_quote = FALSE;
      char *t = sub - 1;
      while (*(++t) != 0)
        {
        if (*t != '_' && *t != '-' && *t != '.' && !isalnum(*t))
          { needs_quote = TRUE; break; }
        }

      if (needs_quote)
        {
        yield = string_cat(yield, &size, &ptr, "\"", 1);
        t = sub - 1;
        while (*(++t) != 0)
          {
          if (*t == '\\' || *t == '"')
            yield = string_cat(yield, &size, &ptr, "\\", 1);
          yield = string_cat(yield, &size, &ptr, t, 1);
          }
        yield = string_cat(yield, &size, &ptr, "\"", 1);
        }
      else yield = string_cat(yield, &size, &ptr, sub, (int)strlen(sub));
      continue;
      }

    /* rx quote sticks in \ before any non-alphameric character so that
    the insertion works in a regular expression. */

    if (strcmp(name, "rxquote") == 0)
      {
      char *t = sub -1;
      while (*(++t) != 0)
        {
        if (!isalnum(*t)) yield = string_cat(yield, &size, &ptr, "\\", 1);
        yield = string_cat(yield, &size, &ptr, t, 1);
        }
      continue;
      }

    /* length_n or l_n takes just the first n characters or the whole string,
    whichever is the shorter; substr_m_n, and s_m_n take n characters from
    offset m; negative m take from the end; l_n is synonymous with s_0_n. If
    n is omitted in substr it takes the rest, either to the right or to the
    left. hash_n or h_n makes a hash of length n from the string, yielding
    n characters from the set a-z; hash_n_m makes a hash of length n, but
    uses m characters from the set a-zA-Z0-9. */

    if (strncmp(name, "length_", 7) == 0 || strncmp(name, "l_", 2) == 0 ||
        strncmp(name, "substr_", 7) == 0 || strncmp(name, "s_", 2) == 0 ||
        strncmp(name, "hash_",   5) == 0 || strncmp(name, "h_", 2) == 0)
      {
      int sign = 1;
      int len = -1;
      int offset = 0;
      int hashcount;
      int *pn;
      int sublen = (int)strlen(sub);
      char *num = strchr(name, '_') + 1;

      /* First argument of substr may be negative; len and hash have only
      one argument. */

      if (name[0] != 'l')
        {
        pn = &offset;
        if (name[0] == 's' && *num == '-') { sign = -1; num++; }
        }
      else { pn = &len; len = 0; }

      while (*num != 0)
        {
        if (*num == '_' && pn == &offset)
          {
          pn = &len;
          len = 0;
          num++;
          }
        else if (!isdigit(*num))
          {
          expand_string_message =
            string_sprintf("non-digit after underscore in \"%s\"", name);
          goto EXPAND_FAILED;
          }
        else *pn = (*pn)*10 + *num++ - '0';
        }
      offset *= sign;

      /* For a hash, the first argument is in fact the length, and the
      second is the number of characters to use. */

      if (name[0] == 'h')
        {
        hashcount = (len < 0)? 26 : len;
        if (len > (int)strlen(hashcodes))
          {
          expand_string_message =
            string_sprintf("hash char count too big in \"%s\"", name);
          goto EXPAND_FAILED;
          }
        len = offset;
        offset = 0;
        }

      /* Otherwise, a negative offset positions from the rhs (happens only
      for substr) */

      else if (offset < 0)
        {
        offset += sublen;

        /* If the position is before the start, skip to the start, unless
        length is unset, in which case the substring is null. */

        if (offset < 0)
          {
          if (len < 0) len = 0; else len += offset;
          offset = 0;
          }

        /* Otherwise an unset length => characters before the offset */

        else if (len < 0)
          {
          len = offset;
          offset = 0;
          }
        }

      /* For a non-negative offset, no length means "rest"; just set
      it to the maximum. */

      else if (len < 0) len = sublen;

      /* Cut length down to maximum possible and if non-zero, get the
      required characters or compute the hash and continue. For a hash,
      offset is always zero. */

      if (sublen < offset + len) len = sublen - offset;
      if (len > 0)
        {
        if (name[0] == 'h' && len < sublen)
          {
          int c;
          int i = 0;
          int j = len;
          while ((c = (uschar)(sub[j])) != 0)
            {
            int shift = (c + j++) & 7;
            sub[i] ^= (c << shift) | (c >> (8-shift));
            if (++i >= len) i = 0;
            }
          for (i = 0; i < len; i++)
            sub[i] = hashcodes[(uschar)(sub[i]) % hashcount];
          }
        yield = string_cat(yield, &size, &ptr, sub + offset, len);
        }
      continue;
      }

    /* Unknown operator */

    expand_string_message =
      string_sprintf("unknown expansion operator \"%s\"", name);
    goto EXPAND_FAILED;
    }

  /* Handle a plain name */

  if (*s++ == '}')
    {
    int choplen = 0;
    if (strncmp(name, "header_", 7) == 0 || strncmp(name, "h_", 2) == 0)
      {
      s = read_header_name(name, 256, s);
      value = find_header(name);
      choplen = 1;
      }
    else
      {
      value = find_variable(name, &choplen);
      if (value == NULL)
        {
        expand_string_message = string_sprintf("unknown variable: %s", name);
        goto EXPAND_FAILED;
        }
      }
    if (value != NULL)
      {
      yield = string_cat(yield, &size, &ptr, value,
        (int)strlen(value) - choplen);
      }
    continue;
    }

  /* Else there's something wrong */

  expand_string_message = string_sprintf("\"${%s%.3s\" unrecognized", name,
    (*(--s) == 0)? "" : s);
  goto EXPAND_FAILED;
  }

/* Expansion succeeded; add a terminating zero, and if left != NULL, return a
pointer to the terminator. */

yield[ptr] = 0;
if (left != NULL) *left = s;

/* Any stacking store that was used above the final string is no longer needed.
In many cases the final string will be the one that was got at the start and so
there will be optimal store usage. */

store_reset(yield + ptr + 1);
return yield;

/* This is the failure exit: easiest to program with a goto. We still need
to update the pointer to the terminator, for cases of nested calls with "fail".
*/

EXPAND_FAILED_CURLY:
expand_string_message = "missing or misplaced { or }";

EXPAND_FAILED:
if (left != NULL) *left = s;
store_reset(yield);
return NULL;
}


/* This is the external function call. Do a quick check for any expansion
metacharacters, and if there are none, just return the input string.

Argument: the string to be expanded
Returns:  the expanded string, or NULL if expansion failed; if failure was
          due to a lookup deferring, search_find_defer will be TRUE
*/

char *
expand_string(char *string)
{
lookup_value = NULL;
search_find_defer = FALSE;
return (strpbrk(string, "$\\") == NULL)? string :
  expand_string_internal(string, FALSE, NULL, FALSE);
}



/*************************************************
*              Expand and copy                   *
*************************************************/

/* Now and again we want to expand a string and be sure that the result is in a
new bit of store. This function does that.

Argument: the string to be expanded
Returns:  the expanded string, always in a new bit of store, or NULL
*/

char *
expand_string_copy(char *string)
{
char *yield = expand_string(string);
if (yield == string) yield = string_copy(string);
return yield;
}



/*************************************************
*               Expand or panic                  *
*************************************************/

/* Sometimes Exim can't continue if an expansion fails. This function ensures
that it panics.

Arguments:
  string     the string to be expanded
  text1      ) two texts to be placed in the
  text2      ) error message on failure

Returns:     the expanded string
*/

char *
expand_string_panic(char *string, char *text1, char *text2)
{
char *yield;
if (string == NULL) return NULL;
yield = expand_string(string);
if (yield == NULL)
  log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s %s: failed to expand \"%s\" %s",
    text1, text2, string, expand_string_message);
return yield;
}



/*************************************************
**************************************************
*             Stand-alone test program           *
**************************************************
*************************************************/

#ifdef STAND_ALONE


BOOL
regex_match_and_setup(pcre *re, char *subject, int options, int setup)
{
int ovector[3*(EXPAND_MAXN+1)];
int n = pcre_exec(re, NULL, subject, (int)strlen(subject), PCRE_EOPT|options,
  ovector, sizeof(ovector)/sizeof(int));
BOOL yield = n >= 0;
if (n == 0) n = EXPAND_MAXN + 1;
if (yield)
  {
  int nn;
  expand_nmax = (setup < 0)? 0 : setup + 1;
  for (nn = (setup < 0)? 0 : 2; nn < n*2; nn += 2)
    {
    expand_nstring[expand_nmax] = subject + ovector[nn];
    expand_nlength[expand_nmax++] = ovector[nn+1] - ovector[nn];
    }
  expand_nmax--;
  }
return yield;
}


int main(int argc, char **argv)
{
int i;
char buffer[256];

debug_level = 1;
debug_file = stderr;
debug_trace_memory = 0;
big_buffer = malloc(big_buffer_size);

for (i = 1; i < argc; i++)
  {
  if (argv[i][0] == '+')
    {
    debug_trace_memory = 1;
    argv[i]++;
    }
  if (isdigit(argv[i][0])) debug_level = atoi(argv[i]);
#ifdef EXIM_PERL
    else opt_perl_startup = argv[i];
#endif
  }

printf("Testing string expansion: debug_level = %d\n\n", debug_level);

expand_nstring[1] = "string 1....";
expand_nlength[1] = 8;
expand_nmax = 1;

#ifdef EXIM_PERL
if (opt_perl_startup != NULL)
  {
  char *errstr;
  printf("Starting Perl interpreter\n");
  errstr = init_perl(opt_perl_startup);
  if (errstr != NULL)
    {
    printf("** error in perl_startup code: %s\n", errstr);
    return EXIT_FAILURE;
    }
  }
#endif /* EXIM_PERL */

while (fgets(buffer, 256, stdin) != NULL)
  {
  void *reset_point = store_get(0);
  char *yield = expand_string(buffer);
  if (yield != NULL)
    {
    printf("%s\n", yield);
    store_reset(reset_point);
    }
  else
    {
    if (search_find_defer) printf("search_find deferred\n");
    printf("Failed: %s\n", expand_string_message);
    if (expand_string_forcedfail) printf("Forced failure\n");
    printf("\n");
    }
  }

search_tidyup();

return 0;
}

#endif

/* End of expand.c */
