/* $Cambridge: exim/exim-src/src/demime.c,v 1.9 2006/02/22 14:46:44 ph10 Exp $ */

/*************************************************
*     Exim - an Internet mail transport agent    *
*************************************************/

/* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003-???? */
/* License: GPL */

/* Code for unpacking MIME containers. Called from acl.c. */

#include "exim.h"
#ifdef WITH_OLD_DEMIME

#include "demime.h"

uschar demime_reason_buffer[1024];
struct file_extension *file_extensions = NULL;

int demime(uschar **listptr) {
  int sep = 0;
  uschar *list = *listptr;
  uschar *option;
  uschar option_buffer[64];
  unsigned long mbox_size;
  FILE *mbox_file;
  uschar defer_error_buffer[1024];
  int demime_rc = 0;

  /* reset found_extension variable */
  found_extension = NULL;

  /* try to find 1st option */
  if ((option = string_nextinlist(&list, &sep,
                                  option_buffer,
                                  sizeof(option_buffer))) != NULL) {

    /* parse 1st option */
    if ( (Ustrcmp(option,"false") == 0) || (Ustrcmp(option,"0") == 0) ) {
      /* explicitly no demimeing */
      return FAIL;
    };
  }
  else {
    /* no options -> no demimeing */
    return FAIL;
  };

  /* make sure the eml mbox file is spooled up */
  mbox_file = spool_mbox(&mbox_size);

  if (mbox_file == NULL) {
    /* error while spooling */
    log_write(0, LOG_MAIN|LOG_PANIC,
           "demime acl condition: error while creating mbox spool file");
    return DEFER;
  };

  /* call demimer if not already done earlier */
  if (!demime_ok)
    demime_rc = mime_demux(mbox_file, defer_error_buffer);

  (void)fclose(mbox_file);

  if (demime_rc == DEFER) {
    /* temporary failure (DEFER => DEFER) */
    log_write(0, LOG_MAIN,
        "demime acl condition: %s", defer_error_buffer);
    return DEFER;
  };

  /* set demime_ok to avoid unpacking again */
  demime_ok = 1;

  /* check for file extensions, if there */
  while (option != NULL) {
    struct file_extension *this_extension = file_extensions;

    /* Look for the wildcard. If it is found, we always return true.
    The user must then use a custom condition to evaluate demime_errorlevel */
    if (Ustrcmp(option,"*") == 0) {
      found_extension = NULL;
      return OK;
    };

    /* loop thru extension list */
    while (this_extension != NULL) {
      if (strcmpic(option, this_extension->file_extension_string) == 0) {
        /* found one */
        found_extension = this_extension->file_extension_string;
        return OK;
      };
      this_extension = this_extension->next;
    };

    /* grab next extension from option list */
    option = string_nextinlist(&list, &sep,
                               option_buffer,
                               sizeof(option_buffer));
  };

  /* nothing found */
  return FAIL;
}


/*************************************************
* small hex_str -> integer conversion function   *
*************************************************/

/* needed for quoted-printable
*/

unsigned int mime_hstr_i(uschar *cptr) {
  unsigned int i, j = 0;

  while (cptr && *cptr && isxdigit(*cptr)) {
    i = *cptr++ - '0';
    if (9 < i) i -= 7;
    j <<= 4;
    j |= (i & 0x0f);
  }

  return(j);
}


/*************************************************
* decode quoted-printable chars                  *
*************************************************/

/* gets called when we hit a =
   returns: new pointer position
   result code in c:
          -2 - decode error
          -1 - soft line break, no char
           0-255 - char to write
*/

uschar *mime_decode_qp(uschar *qp_p,int *c) {
  uschar hex[] = {0,0,0};
  int nan = 0;
  uschar *initial_pos = qp_p;

  /* advance one char */
  qp_p++;

  REPEAT_FIRST:
  if ( (*qp_p == '\t') || (*qp_p == ' ') || (*qp_p == '\r') )  {
    /* tab or whitespace may follow
       just ignore it, but remember
       that this is not a valid hex
       encoding any more */
    nan = 1;
    qp_p++;
    goto REPEAT_FIRST;
  }
  else if ( (('0' <= *qp_p) && (*qp_p <= '9')) || (('A' <= *qp_p) && (*qp_p <= 'F'))  || (('a' <= *qp_p) && (*qp_p <= 'f')) ) {
    /* this is a valid hex char, if nan is unset */
    if (nan) {
      /* this is illegal */
      *c = -2;
      return initial_pos;
    }
    else {
      hex[0] = *qp_p;
      qp_p++;
    };
  }
  else if (*qp_p == '\n') {
    /* hit soft line break already, continue */
    *c = -1;
    return qp_p;
  }
  else {
    /* illegal char here */
    *c = -2;
    return initial_pos;
  };

  if ( (('0' <= *qp_p) && (*qp_p <= '9')) || (('A' <= *qp_p) && (*qp_p <= 'F')) || (('a' <= *qp_p) && (*qp_p <= 'f')) ) {
    if (hex[0] > 0) {
      hex[1] = *qp_p;
      /* do hex conversion */
      *c = mime_hstr_i(hex);
      qp_p++;
      return qp_p;
    }
    else {
      /* huh ? */
      *c = -2;
      return initial_pos;
    };
  }
  else {
    /* illegal char */
    *c = -2;
    return initial_pos;
  };

}


/*************************************************
* open new dump file                             *
*************************************************/

/* open new dump file
   returns: -2 soft error
            or file #, FILE * in f
*/

int mime_get_dump_file(uschar *extension, FILE **f, uschar *info) {
  uschar file_name[1024];
  int result;
  unsigned int file_nr;
  uschar default_extension[] = ".com";
  uschar *p;

  if (extension == NULL)
    extension = default_extension;

  /* scan the proposed extension.
     if it is longer than 4 chars, or
     contains exotic chars, use the default extension */

/*  if (Ustrlen(extension) > 4) {
    extension = default_extension;
  };
*/

  p = extension+1;

  while (*p != 0) {
    *p = (uschar)tolower((uschar)*p);
    if ( (*p < 97) || (*p > 122) ) {
      extension = default_extension;
      break;
    };
    p++;
  };

  /* find a new file to write to */
  file_nr = 0;
  do {
    struct stat mystat;

    (void)string_format(file_name,1024,"%s/scan/%s/%s-%05u%s",spool_directory,message_id,message_id,file_nr,extension);
    file_nr++;
    if (file_nr >= MIME_SANITY_MAX_DUMP_FILES) {
      /* max parts reached */
      mime_trigger_error(MIME_ERRORLEVEL_TOO_MANY_PARTS);
      break;
    };
    result = stat(CS file_name,&mystat);
  }
  while(result != -1);

  *f = modefopen(file_name,"wb+",SPOOL_MODE);
  if (*f == NULL) {
    /* cannot open new dump file, disk full ? -> soft error */
    (void)string_format(info, 1024,"unable to open dump file");
    return -2;
  };

  return file_nr;
}


/*************************************************
* Find a string in a mime header                 *
*************************************************/

/* Find a string in a mime header, and optionally fill in
   the value associated with it into *value

   returns: 0 - nothing found
            1 - found param
            2 - found param + value
*/

int mime_header_find(uschar *header, uschar *param, uschar **value) {
  uschar *needle;

  needle = strstric(header,param,FALSE);
  if (needle != NULL) {
    if (value != NULL) {
      needle += Ustrlen(param);
      if (*needle == '=') {
        uschar *value_start;
        uschar *value_end;

        value_start = needle + 1;
        value_end = strstric(value_start,US";",FALSE);
        if (value_end != NULL) {
          /* allocate mem for value */
          *value = (uschar *)malloc((value_end - value_start)+1);
          if (*value == NULL)
            return 0;

          Ustrncpy(*value,value_start,(value_end - value_start));
          (*value)[(value_end - value_start)] = '\0';
          return 2;
        };
      };
    };
    return 1;
  };
  return 0;
}


/*************************************************
* Read a line of MIME input                      *
*************************************************/
/* returns status code, one of
   MIME_READ_LINE_EOF 0
   MIME_READ_LINE_OK 1
   MIME_READ_LINE_OVERFLOW 2

   In header mode, the line will be "cooked".
*/

int mime_read_line(FILE *f, int mime_demux_mode, uschar *buffer, long *num_copied) {
  int c = EOF;
  int done = 0;
  int header_value_mode = 0;
  int header_open_brackets = 0;

  *num_copied = 0;

  while(!done) {

    c = fgetc(f);
    if (c == EOF) break;

    /* --------- header mode -------------- */
    if (mime_demux_mode == MIME_DEMUX_MODE_MIME_HEADERS) {

      /* always skip CRs */
      if (c == '\r') continue;

      if (c == '\n') {
        if ((*num_copied) > 0) {
          /* look if next char is '\t' or ' ' */
          c = fgetc(f);
          if (c == EOF) break;
          if ( (c == '\t') || (c == ' ') ) continue;
          (void)ungetc(c,f);
        };
        /* end of the header, terminate with ';' */
        c = ';';
        done = 1;
      };

      /* skip control characters */
      if (c < 32) continue;

      /* skip whitespace + tabs */
      if ( (c == ' ') || (c == '\t') )
        continue;

      if (header_value_mode) {
        /* --------- value mode ----------- */
        /* skip quotes */
        if (c == '"') continue;

        /* leave value mode on ';' */
        if (c == ';') {
          header_value_mode = 0;
        };
        /* -------------------------------- */
      }
      else {
        /* -------- non-value mode -------- */
        if (c == '\\') {
          /* quote next char. can be used
          to escape brackets. */
          c = fgetc(f);
          if (c == EOF) break;
        }
        else if (c == '(') {
          header_open_brackets++;
          continue;
        }
        else if ((c == ')') && header_open_brackets) {
          header_open_brackets--;
          continue;
        }
        else if ( (c == '=') && !header_open_brackets ) {
          /* enter value mode */
          header_value_mode = 1;
        };

        /* skip chars while we are in a comment */
        if (header_open_brackets > 0)
          continue;
        /* -------------------------------- */
      };
    }
    /* ------------------------------------ */
    else {
    /* ----------- non-header mode -------- */
      /* break on '\n' */
      if (c == '\n')
        done = 1;
    /* ------------------------------------ */
    };

    /* copy the char to the buffer */
    buffer[*num_copied] = (uschar)c;
    /* raise counter */
    (*num_copied)++;

    /* break if buffer is full */
    if (*num_copied > MIME_SANITY_MAX_LINE_LENGTH-1) {
      done = 1;
    };
  }

  /* 0-terminate */
  buffer[*num_copied] = '\0';

  if (*num_copied > MIME_SANITY_MAX_LINE_LENGTH-1)
    return MIME_READ_LINE_OVERFLOW;
  else
    if (c == EOF)
      return MIME_READ_LINE_EOF;
    else
      return MIME_READ_LINE_OK;
}


/*************************************************
* Check for a MIME boundary                      *
*************************************************/

/* returns: 0 - no boundary found
            1 - start boundary found
            2 - end boundary found
*/

int mime_check_boundary(uschar *line, struct boundary *boundaries) {
  struct boundary *thisboundary = boundaries;
  uschar workbuf[MIME_SANITY_MAX_LINE_LENGTH+1];
  unsigned int i,j=0;

  /* check for '--' first */
  if (Ustrncmp(line,"--",2) == 0) {

    /* strip tab and space */
    for (i = 2; i < Ustrlen(line); i++) {
      if ((line[i] != ' ') && (line[i] != '\t')) {
        workbuf[j] = line[i];
        j++;
      };
    };
    workbuf[j+1]='\0';

    while(thisboundary != NULL) {
      if (Ustrncmp(workbuf,thisboundary->boundary_string,Ustrlen(thisboundary->boundary_string)) == 0) {
        if (Ustrncmp(&workbuf[Ustrlen(thisboundary->boundary_string)],"--",2) == 0) {
          /* final boundary found */
          return 2;
        };
        return 1;
      };
      thisboundary = thisboundary->next;
    };
  };

  return 0;
}


/*************************************************
* Check for start of a UUENCODE block            *
*************************************************/

/* returns 0 for no hit,
           >0 for hit
*/

int mime_check_uu_start(uschar *line, uschar *uu_file_extension, int *has_tnef) {

  if ( (strncmpic(line,US"begin ",6) == 0)) {
    uschar *uu_filename = &line[6];

    /* skip perms, if present */
    Ustrtoul(&line[6],&uu_filename,10);

    /* advance one char */
    uu_filename++;

    /* This should be the filename.
    Check if winmail.dat is present,
    which indicates TNEF. */
    if (strncmpic(uu_filename,US"winmail.dat",11) == 0) {
      *has_tnef = 1;
    };

    /* reverse to dot if present,
    copy up to 4 chars for the extension */
    if (Ustrrchr(uu_filename,'.') != NULL)
      uu_filename = Ustrrchr(uu_filename,'.');

    return sscanf(CS uu_filename, "%4[.0-9A-Za-z]",CS uu_file_extension);
  }
  else {
    /* nothing found */
    return 0;
  };
}


/*************************************************
* Decode a uu line                               *
*************************************************/

/* returns number of decoded bytes
         -2 for soft errors
*/

int warned_about_uudec_line_sanity_1 = 0;
int warned_about_uudec_line_sanity_2 = 0;
long uu_decode_line(uschar *line, uschar **data, long line_len, uschar *info) {
  uschar *p;
  long num_decoded = 0;
  uschar tmp_c;
  uschar *work;
  int uu_decoded_line_len, uu_encoded_line_len;

  /* allocate memory for data and work buffer */
  *data = (uschar *)malloc(line_len);
  if (*data == NULL) {
    (void)string_format(info, 1024,"unable to allocate %lu bytes",line_len);
    return -2;
  };

  work = (uschar *)malloc(line_len);
  if (work == NULL) {
    (void)string_format(info, 1024,"unable to allocate %lu bytes",line_len);
    return -2;
  };

  memcpy(work,line,line_len);

  /* First char is line length
  This is microsofts way of getting it. Scary. */
  if (work[0] < 32) {
    /* ignore this line */
    return 0;
  }
  else {
    uu_decoded_line_len = uudec[work[0]];
  };

  p = &work[1];

  while (*p > 32) {
    *p = uudec[*p];
    p++;
  };

  uu_encoded_line_len = (p - &work[1]);
  p = &work[1];

  /* check that resulting line length is a multiple of 4 */
  if ( ( uu_encoded_line_len % 4 ) != 0) {
    if (!warned_about_uudec_line_sanity_1) {
      mime_trigger_error(MIME_ERRORLEVEL_UU_MISALIGNED);
      warned_about_uudec_line_sanity_1 = 1;
    };
    return -1;
  };

  /* check that the line length matches */
  if ( ( (((uu_encoded_line_len/4)*3)-2) > uu_decoded_line_len ) || (((uu_encoded_line_len/4)*3) < uu_decoded_line_len) ) {
    if (!warned_about_uudec_line_sanity_2) {
      mime_trigger_error(MIME_ERRORLEVEL_UU_LINE_LENGTH);
      warned_about_uudec_line_sanity_2 = 1;
    };
    return -1;
  };

  while ( ((p - &work[1]) < uu_encoded_line_len) && (num_decoded < uu_decoded_line_len)) {

    /* byte 0 ---------------------- */
    if ((p - &work[1] + 1) >= uu_encoded_line_len) {
      return 0;
    }

    (*data)[num_decoded] = *p;
    (*data)[num_decoded] <<= 2;

    tmp_c = *(p+1);
    tmp_c >>= 4;
    (*data)[num_decoded] |= tmp_c;

    num_decoded++;
    p++;

    /* byte 1 ---------------------- */
    if ((p - &work[1] + 1) >= uu_encoded_line_len) {
      return 0;
    }

    (*data)[num_decoded] = *p;
    (*data)[num_decoded] <<= 4;

    tmp_c = *(p+1);
    tmp_c >>= 2;
    (*data)[num_decoded] |= tmp_c;

    num_decoded++;
    p++;

    /* byte 2 ---------------------- */
    if ((p - &work[1] + 1) >= uu_encoded_line_len) {
      return 0;
    }

    (*data)[num_decoded] = *p;
    (*data)[num_decoded] <<= 6;

    (*data)[num_decoded] |= *(p+1);

    num_decoded++;
    p+=2;

  };

  return uu_decoded_line_len;
}


/*************************************************
* Decode a b64 or qp line                        *
*************************************************/

/* returns number of decoded bytes
         -1 for hard errors
         -2 for soft errors
*/

int warned_about_b64_line_length = 0;
int warned_about_b64_line_sanity = 0;
int warned_about_b64_illegal_char = 0;
int warned_about_qp_line_sanity = 0;
long mime_decode_line(int mime_demux_mode,uschar *line, uschar **data, long max_data_len, uschar *info) {
  uschar *p;
  long num_decoded = 0;
  int offset = 0;
  uschar tmp_c;

  /* allocate memory for data */
  *data = (uschar *)malloc(max_data_len);
  if (*data == NULL) {
    (void)string_format(info, 1024,"unable to allocate %lu bytes",max_data_len);
    return -2;
  };

  if (mime_demux_mode == MIME_DEMUX_MODE_BASE64) {
    /* ---------------------------------------------- */

    /* NULL out trailing '\r' and '\n' chars */
    while (Ustrrchr(line,'\r') != NULL) {
      *(Ustrrchr(line,'\r')) = '\0';
    };
    while (Ustrrchr(line,'\n') != NULL) {
      *(Ustrrchr(line,'\n')) = '\0';
    };

    /* check maximum base 64 line length */
    if (Ustrlen(line) > MIME_SANITY_MAX_B64_LINE_LENGTH ) {
      if (!warned_about_b64_line_length) {
        mime_trigger_error(MIME_ERRORLEVEL_B64_LINE_LENGTH);
        warned_about_b64_line_length = 1;
      };
    };

    p = line;
    offset = 0;
    while (*(p+offset) != '\0') {
      /* hit illegal char ? */
      if (b64[*(p+offset)] == 128) {
        if (!warned_about_b64_illegal_char) {
          mime_trigger_error(MIME_ERRORLEVEL_B64_ILLEGAL_CHAR);
          warned_about_b64_illegal_char = 1;
        };
        offset++;
      }
      else {
        *p = b64[*(p+offset)];
        p++;
      };
    };
    *p = 255;

    /* check that resulting line length is a multiple of 4 */
    if ( ( (p - &line[0]) % 4 ) != 0) {
      if (!warned_about_b64_line_sanity) {
        mime_trigger_error(MIME_ERRORLEVEL_B64_MISALIGNED);
        warned_about_b64_line_sanity = 1;
      };
    };

    /* line is translated, start bit shifting */
    p = line;
    num_decoded = 0;

    while(*p != 255) {

      /* byte 0 ---------------------- */
      if (*(p+1) == 255) {
        break;
      }

      (*data)[num_decoded] = *p;
      (*data)[num_decoded] <<= 2;

      tmp_c = *(p+1);
      tmp_c >>= 4;
      (*data)[num_decoded] |= tmp_c;

      num_decoded++;
      p++;

      /* byte 1 ---------------------- */
      if (*(p+1) == 255) {
        break;
      }

      (*data)[num_decoded] = *p;
      (*data)[num_decoded] <<= 4;

      tmp_c = *(p+1);
      tmp_c >>= 2;
      (*data)[num_decoded] |= tmp_c;

      num_decoded++;
      p++;

      /* byte 2 ---------------------- */
      if (*(p+1) == 255) {
        break;
      }

      (*data)[num_decoded] = *p;
      (*data)[num_decoded] <<= 6;

      (*data)[num_decoded] |= *(p+1);

      num_decoded++;
      p+=2;

    };
    return num_decoded;
    /* ---------------------------------------------- */
  }
  else if (mime_demux_mode == MIME_DEMUX_MODE_QP) {
    /* ---------------------------------------------- */
    p = line;

    while (*p != 0) {
      if (*p == '=') {
        int decode_qp_result;

        p = mime_decode_qp(p,&decode_qp_result);

        if (decode_qp_result == -2) {
          /* Error from decoder. p is unchanged. */
          if (!warned_about_qp_line_sanity) {
            mime_trigger_error(MIME_ERRORLEVEL_QP_ILLEGAL_CHAR);
            warned_about_qp_line_sanity = 1;
          };
          (*data)[num_decoded] = '=';
          num_decoded++;
          p++;
        }
        else if (decode_qp_result == -1) {
          /* End of the line with soft line break.
          Bail out. */
          goto QP_RETURN;
        }
        else if (decode_qp_result >= 0) {
          (*data)[num_decoded] = decode_qp_result;
          num_decoded++;
        };
      }
      else {
        (*data)[num_decoded] = *p;
        num_decoded++;
        p++;
      };
    };
    QP_RETURN:
    return num_decoded;
    /* ---------------------------------------------- */
  };

  return 0;
}



/*************************************************
* Log demime errors and set mime error level     *
*************************************************/

/* This sets the global demime_reason expansion
variable and the demime_errorlevel gauge. */

void mime_trigger_error(int level, uschar *format, ...) {
  char *f;
  va_list ap;

  if( (f = malloc(16384+23)) != NULL ) {
    /* first log the incident */
    sprintf(f,"demime acl condition: ");
    f+=22;
    va_start(ap, format);
    (void)string_vformat(US f, 16383,(char *)format, ap);
    va_end(ap);
    f-=22;
    log_write(0, LOG_MAIN, f);
    /* then copy to demime_reason_buffer if new
    level is greater than old level */
    if (level > demime_errorlevel) {
      demime_errorlevel = level;
      Ustrcpy(demime_reason_buffer, US f);
      demime_reason = demime_reason_buffer;
    };
    free(f);
  };
}

/*************************************************
* Demultiplex MIME stream.                       *
*************************************************/

/* We can handle BASE64, QUOTED-PRINTABLE, and UUENCODE.
 UUENCODE does not need to have a proper
 transfer-encoding header, we detect it with "begin"

 This function will report human parsable errors in
 *info.

 returns DEFER -> soft error (see *info)
         OK    -> EOF hit, all ok
*/

int mime_demux(FILE *f, uschar *info) {
  int mime_demux_mode = MIME_DEMUX_MODE_MIME_HEADERS;
  int uu_mode = MIME_UU_MODE_OFF;
  FILE *mime_dump_file = NULL;
  FILE *uu_dump_file = NULL;
  uschar *line;
  int mime_read_line_status = MIME_READ_LINE_OK;
  long line_len;
  struct boundary *boundaries = NULL;
  struct mime_part mime_part_p;
  int has_tnef = 0;
  int has_rfc822 = 0;

  /* allocate room for our linebuffer */
  line = (uschar *)malloc(MIME_SANITY_MAX_LINE_LENGTH);
  if (line == NULL) {
    (void)string_format(info, 1024,"unable to allocate %u bytes",MIME_SANITY_MAX_LINE_LENGTH);
    return DEFER;
  };

  /* clear MIME header structure */
  memset(&mime_part_p,0,sizeof(mime_part));

  /* ----------------------- start demux loop --------------------- */
  while (mime_read_line_status == MIME_READ_LINE_OK) {

    /* read a line of input. Depending on the mode we are in,
    the returned format will differ. */
    mime_read_line_status = mime_read_line(f,mime_demux_mode,line,&line_len);

    if (mime_read_line_status == MIME_READ_LINE_OVERFLOW) {
      mime_trigger_error(MIME_ERRORLEVEL_LONG_LINE);
      /* despite the error, continue  .. */
      mime_read_line_status = MIME_READ_LINE_OK;
      continue;
    }
    else if (mime_read_line_status == MIME_READ_LINE_EOF) {
      break;
    };

    if (mime_demux_mode == MIME_DEMUX_MODE_MIME_HEADERS) {
      /* -------------- header mode --------------------- */

      /* Check for an empty line, which is the end of the headers.
       In HEADER mode, the line is returned "cooked", with the
       final '\n' replaced by a ';' */
      if (line_len == 1) {
        int tmp;

        /* We have reached the end of the headers. Start decoding
        with the collected settings. */
        if (mime_part_p.seen_content_transfer_encoding > 1) {
          mime_demux_mode = mime_part_p.seen_content_transfer_encoding;
        }
        else {
          /* default to plain mode if no specific encoding type found */
          mime_demux_mode = MIME_DEMUX_MODE_PLAIN;
        };

        /* open new dump file */
        tmp = mime_get_dump_file(mime_part_p.extension, &mime_dump_file, info);
        if (tmp < 0) {
          return DEFER;
        };

        /* clear out mime_part */
        memset(&mime_part_p,0,sizeof(mime_part));
      }
      else {
        /* Another header to check for file extensions,
        encoding type and boundaries */
        if (strncmpic(US"content-type:",line,Ustrlen("content-type:")) == 0) {
          /* ---------------------------- Content-Type header ------------------------------- */
          uschar *value = line;

          /* check for message/partial MIME type and reject it */
          if (mime_header_find(line,US"message/partial",NULL) > 0)
            mime_trigger_error(MIME_ERRORLEVEL_MESSAGE_PARTIAL);

          /* check for TNEF content type, remember to unpack TNEF later. */
          if (mime_header_find(line,US"application/ms-tnef",NULL) > 0)
            has_tnef = 1;

          /* check for message/rfcxxx attachments */
          if (mime_header_find(line,US"message/rfc822",NULL) > 0)
            has_rfc822 = 1;

          /* find the file extension, but do not fill it in
          it is already set, since content-disposition has
          precedence. */
          if (mime_part_p.extension == NULL) {
            if (mime_header_find(line,US"name",&value) == 2) {
              if (Ustrlen(value) > MIME_SANITY_MAX_FILENAME)
                mime_trigger_error(MIME_ERRORLEVEL_FILENAME_LENGTH);
              mime_part_p.extension = value;
              mime_part_p.extension = Ustrrchr(value,'.');
              if (mime_part_p.extension == NULL) {
                /* file without extension, setting
                NULL will use the default extension later */
                mime_part_p.extension = NULL;
              }
              else {
                struct file_extension *this_extension =
                  (struct file_extension *)malloc(sizeof(file_extension));

                this_extension->file_extension_string =
                  (uschar *)malloc(Ustrlen(mime_part_p.extension)+1);
                Ustrcpy(this_extension->file_extension_string,
                        mime_part_p.extension+1);
                this_extension->next = file_extensions;
                file_extensions = this_extension;
              };
            };
          };

          /* find a boundary and add it to the list, if present */
          value = line;
          if (mime_header_find(line,US"boundary",&value) == 2) {
            struct boundary *thisboundary;

            if (Ustrlen(value) > MIME_SANITY_MAX_BOUNDARY_LENGTH) {
              mime_trigger_error(MIME_ERRORLEVEL_BOUNDARY_LENGTH);
            }
            else {
              thisboundary = (struct boundary*)malloc(sizeof(boundary));
              thisboundary->next = boundaries;
              thisboundary->boundary_string = value;
              boundaries = thisboundary;
            };
          };

          if (mime_part_p.seen_content_type == 0) {
            mime_part_p.seen_content_type = 1;
          }
          else {
            mime_trigger_error(MIME_ERRORLEVEL_DOUBLE_HEADERS);
          };
          /* ---------------------------------------------------------------------------- */
        }
        else if (strncmpic(US"content-transfer-encoding:",line,Ustrlen("content-transfer-encoding:")) == 0) {
          /* ---------------------------- Content-Transfer-Encoding header -------------- */

         if (mime_part_p.seen_content_transfer_encoding == 0) {
            if (mime_header_find(line,US"base64",NULL) > 0) {
              mime_part_p.seen_content_transfer_encoding = MIME_DEMUX_MODE_BASE64;
            }
            else if (mime_header_find(line,US"quoted-printable",NULL) > 0) {
              mime_part_p.seen_content_transfer_encoding = MIME_DEMUX_MODE_QP;
            }
            else {
              mime_part_p.seen_content_transfer_encoding = MIME_DEMUX_MODE_PLAIN;
            };
          }
          else {
            mime_trigger_error(MIME_ERRORLEVEL_DOUBLE_HEADERS);
          };
          /* ---------------------------------------------------------------------------- */
        }
        else if (strncmpic(US"content-disposition:",line,Ustrlen("content-disposition:")) == 0) {
          /* ---------------------------- Content-Disposition header -------------------- */
          uschar *value = line;

          if (mime_part_p.seen_content_disposition == 0) {
            mime_part_p.seen_content_disposition = 1;

            if (mime_header_find(line,US"filename",&value) == 2) {
              if (Ustrlen(value) > MIME_SANITY_MAX_FILENAME)
                mime_trigger_error(MIME_ERRORLEVEL_FILENAME_LENGTH);
              mime_part_p.extension = value;
              mime_part_p.extension = Ustrrchr(value,'.');
              if (mime_part_p.extension == NULL) {
                /* file without extension, setting
                NULL will use the default extension later */
                mime_part_p.extension = NULL;
              }
              else {
                struct file_extension *this_extension =
                  (struct file_extension *)malloc(sizeof(file_extension));

                this_extension->file_extension_string =
                  (uschar *)malloc(Ustrlen(mime_part_p.extension)+1);
                Ustrcpy(this_extension->file_extension_string,
                        mime_part_p.extension+1);
                this_extension->next = file_extensions;
                file_extensions = this_extension;
              };
            };
          }
          else {
            mime_trigger_error(MIME_ERRORLEVEL_DOUBLE_HEADERS);
          };
          /* ---------------------------------------------------------------------------- */
        };
      };    /* End of header checks */
      /* ------------------------------------------------ */
    }
    else {
      /* -------------- non-header mode ----------------- */
      int tmp;

      if (uu_mode == MIME_UU_MODE_OFF) {
        uschar uu_file_extension[5];
        /* We are not currently decoding UUENCODE
        Check for possible UUENCODE start tag. */
        if (mime_check_uu_start(line,uu_file_extension,&has_tnef)) {
          /* possible UUENCODING start detected.
          Set unconfirmed mode first. */
          uu_mode = MIME_UU_MODE_UNCONFIRMED;
          /* open new uu dump file */
          tmp = mime_get_dump_file(uu_file_extension, &uu_dump_file, info);
          if (tmp < 0) {
            free(line);
            return DEFER;
          };
        };
      }
      else {
        uschar *data;
        long data_len = 0;

        if (uu_mode == MIME_UU_MODE_UNCONFIRMED) {
         /* We are in unconfirmed UUENCODE mode. */

         data_len = uu_decode_line(line,&data,line_len,info);

         if (data_len == -2) {
           /* temp error, turn off uudecode mode */
           if (uu_dump_file != NULL) {
            (void)fclose(uu_dump_file); uu_dump_file = NULL;
           };
           uu_mode = MIME_UU_MODE_OFF;
           return DEFER;
         }
         else if (data_len == -1) {
           if (uu_dump_file != NULL) {
            (void)fclose(uu_dump_file); uu_dump_file = NULL;
           };
           uu_mode = MIME_UU_MODE_OFF;
           data_len = 0;
         }
         else if (data_len > 0) {
           /* we have at least decoded a valid byte
           turn on confirmed mode */
           uu_mode = MIME_UU_MODE_CONFIRMED;
         };
        }
        else if (uu_mode == MIME_UU_MODE_CONFIRMED) {
          /* If we are in confirmed UU mode,
          check for single "end" tag on line */
          if ((strncmpic(line,US"end",3) == 0) && (line[3] < 32)) {
            if (uu_dump_file != NULL) {
              (void)fclose(uu_dump_file); uu_dump_file = NULL;
            };
            uu_mode = MIME_UU_MODE_OFF;
          }
          else {
            data_len = uu_decode_line(line,&data,line_len,info);
            if (data_len == -2) {
               /* temp error, turn off uudecode mode */
               if (uu_dump_file != NULL) {
                 (void)fclose(uu_dump_file); uu_dump_file = NULL;
               };
               uu_mode = MIME_UU_MODE_OFF;
               return DEFER;
             }
             else if (data_len == -1) {
               /* skip this line */
               data_len = 0;
             };
          };
        };

        /* write data to dump file, if available */
        if (data_len > 0) {
          if (fwrite(data,1,data_len,uu_dump_file) < data_len) {
            /* short write */
            (void)string_format(info, 1024,"short write on uudecode dump file");
            free(line);
            return DEFER;
          };
        };
      };

      if (mime_demux_mode != MIME_DEMUX_MODE_SCANNING) {
        /* Non-scanning and Non-header mode. That means
        we are currently decoding data to the dump
        file. */

        /* Check for a known boundary. */
        tmp = mime_check_boundary(line,boundaries);
        if (tmp == 1) {
          /* We have hit a known start boundary.
          That will put us back in header mode. */
          mime_demux_mode = MIME_DEMUX_MODE_MIME_HEADERS;
          if (mime_dump_file != NULL) {
            /* if the attachment was a RFC822 message, recurse into it */
            if (has_rfc822) {
              has_rfc822 = 0;
              rewind(mime_dump_file);
              mime_demux(mime_dump_file,info);
            };

            (void)fclose(mime_dump_file); mime_dump_file = NULL;
          };
        }
        else if (tmp == 2) {
          /* We have hit a known end boundary.
          That puts us into scanning mode, which will end when we hit another known start boundary */
          mime_demux_mode = MIME_DEMUX_MODE_SCANNING;
          if (mime_dump_file != NULL) {
            /* if the attachment was a RFC822 message, recurse into it */
            if (has_rfc822) {
              has_rfc822 = 0;
              rewind(mime_dump_file);
              mime_demux(mime_dump_file,info);
            };

            (void)fclose(mime_dump_file); mime_dump_file = NULL;
          };
        }
        else {
          uschar *data;
          long data_len = 0;

          /* decode the line with the appropriate method */
          if (mime_demux_mode == MIME_DEMUX_MODE_PLAIN) {
            /* in plain mode, just dump the line */
            data = line;
            data_len = line_len;
          }
          else if ( (mime_demux_mode == MIME_DEMUX_MODE_QP) || (mime_demux_mode == MIME_DEMUX_MODE_BASE64) ) {
            data_len = mime_decode_line(mime_demux_mode,line,&data,line_len,info);
            if (data_len < 0) {
              /* Error reported from the line decoder. */
              data_len = 0;
            };
          };

          /* write data to dump file */
          if (data_len > 0) {
            if (fwrite(data,1,data_len,mime_dump_file) < data_len) {
              /* short write */
              (void)string_format(info, 1024,"short write on dump file");
              free(line);
              return DEFER;
            };
          };

        };
      }
      else {
        /* Scanning mode. We end up here after a end boundary.
        This will usually be at the end of a message or at
        the end of a MIME container.
        We need to look for another start boundary to get
        back into header mode. */
        if (mime_check_boundary(line,boundaries) == 1) {
          mime_demux_mode = MIME_DEMUX_MODE_MIME_HEADERS;
        };

      };
      /* ------------------------------------------------ */
    };
  };
  /* ----------------------- end demux loop ----------------------- */

  /* close files, they could still be open */
  if (mime_dump_file != NULL)
    (void)fclose(mime_dump_file);
  if (uu_dump_file != NULL)
    (void)fclose(uu_dump_file);

  /* release line buffer */
  free(line);

  /* FIXME: release boundary buffers.
  Not too much of a problem since
  this instance of exim is not resident. */

  if (has_tnef) {
    uschar file_name[1024];
    /* at least one file could be TNEF encoded.
    attempt to send all decoded files thru the TNEF decoder */

    (void)string_format(file_name,1024,"%s/scan/%s",spool_directory,message_id);
    /* Removed FTTB. We need to decide on TNEF inclusion */
    /* mime_unpack_tnef(file_name); */
  };

  return 0;
}

#endif
