/*
 * Copyright (c) 2003-2012
 * Distributed Systems Software.  All rights reserved.
 * See the file LICENSE for redistribution information.
 */

/*
 * MIME (Multipurpose Internet Mail Extensions) support library.
 * Refer to RFCs 2045, 2046, 2047, 2183, 2184, 2231, 1867, 2388, 822, etc.,
 * as well as HTML 4.0.
 *
 * This was NOT originally intended to be a fully functional MIME library,
 * although it's possible that it may slowly approach one.  Rather, it was
 * designed to provide support for RFC 1867/2388 functionality.
 *
 * NB: With respect to form-based file upload (RFC 1867, RFC 2388), browsers
 * do not seem to be conformant.  HTML 4.01 17.13.4 says that
 * multipart/form-data "follows the rules of all multipart MIME data streams
 * as outlined in RFC 2045" and that the default Content-Transfer-Encoding
 * is 7BIT, as per RFC 2045 Section 6.  But RFC 2045 2.7 says that NULs are
 * not allowed within 7bit data, but browsers don't seem to respect that
 * (upload a GIF file and inspect POST data stream - you should see some
 * NULs and even 8bit data).  See also RFC 2045 6.2.
 * 
 * XXX This is not yet a complete implementation
 * RFC 2388 5.4 is not implemented
 *
 * Compiled into a command, it merely parses the initial MIME headers
 * and prints them to stdout.
 * Usage:
 *   mime [
 *
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2012\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: mime.c 2610 2012-10-19 18:02:36Z brachman $";
#endif

#ifdef DSSLIB
#include "dsslib.h"
#else
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

#include "local.h"
#endif

static const char *log_module_name = "mime";

static inline char *
skip_lwsp(char *str)
{
  char *p;

  /* RFC 822 S3.1.4 and S3.3 */
  /* XXX Should this be skipping leading CRLF, or has the caller done this? */
  for (p = str; *p == ' ' || *p == '\t'; p++)
	;

  return(p);
}

static inline char *
compress_lwsp(char *str)
{
  char *p;

  p = str;
  if (*p != ' ' && *p != '\t')
	return(str);

  p++;
  while (*p == ' ' || *p == '\t')
	p++;

  return(p - 1);
}

/*
 * Read a MIME header line, unfolding continued lines into one long one.
 * RFC 822, 3.1.1
 * Return NULL upon EOF or error, otherwise the line.
 */
static char *
mime_next_header_line(CGI_input *in, char **raw)
{
  int is_header_start, skip_lwsp, st;
  char ch, next_ch;
  Ds ds, ds_raw;

  ds_init(&ds);
  ds_init(&ds_raw);
  skip_lwsp = 0;
  is_header_start = 1;

  while (cgiparse_next_char(in, &ch) == 1) {
	ds_appendc(&ds_raw, ch);
	if (ch == '\r') {
	  if (cgiparse_next_char(in, &ch) != 1 || ch != '\n') {
		log_msg((LOG_ERROR_LEVEL,
				 "mime_next_header_line: no \\n following \\r?"));
		return(NULL);
	  }
	  ds_appendc(&ds_raw, ch);

	  if (!is_header_start && cgiparse_peek_next_char(in, &next_ch) == 1
		  && (next_ch == ' ' || next_ch == '\t')) {
		ch = ' ';
		skip_lwsp = 1;
	  }
	  else
		break;
	}
	else if (ch == '\n') {
	  if (!is_header_start && cgiparse_peek_next_char(in, &next_ch) == 1
		  && (next_ch == ' ' || next_ch == '\t')) {
		ch = ' ';
		skip_lwsp = 1;
	  }
	  else
		break;
	}

	if (skip_lwsp && (ch == ' ' || ch == '\t')) {
	  if (skip_lwsp == 1)
		ds_appendc(&ds, ch);
	  skip_lwsp++;
	}
	else {
	  ds_appendc(&ds, ch);
	  skip_lwsp = 0;
	}

	is_header_start = 0;
  }

  if ((st = cgiparse_is_end(in)) != 0) {
	log_msg((LOG_TRACE_LEVEL,
			 "mime_next_header_line: %s", st == 1 ? "EOF" : "ERROR"));
	if (ds_len(&ds) == 0)
	  return(NULL);
  }

  ds_appendc(&ds, '\0');
  ds_appendc(&ds_raw, '\0');

  log_msg((LOG_TRACE_LEVEL, "mime_next_header_line: %s", ds_buf(&ds)));

  if (raw != NULL)
	*raw = ds_buf(&ds_raw);

  return(ds_buf(&ds));
}

/*
 * Read a MIME preamble, body, or epilogue line.
 * Data in the body might have any valid Content-Transfer-Encoding
 * (e.g., 7BIT or BINARY).
 * Return NULL upon EOF or error, otherwise the line.
 * Note that the line is not null terminated.
 */
Ds *
mime_next_line(CGI_input *in)
{
  char ch;
  int st;
  Ds *ds;

  ds = ds_init(NULL);
  while (cgiparse_next_char(in, &ch) == 1) {
	if (ch != '\r')
	  ds_appendc(ds, ch);
	else {
	  if (cgiparse_next_char(in, &ch) != 1) {
		ds_appendc(ds, '\r');
		break;
	  }
	  if (ch == '\n')
		break;
	  ds_appendc(ds, '\r');
	  ds_appendc(ds, ch);
	}
  }

  if ((st = cgiparse_is_end(in)) != 0) {
	log_msg((LOG_TRACE_LEVEL,
			 "mime_next_line: %s", st == 1 ? "EOF" : "ERROR"));
	if (ds_len(ds) == 0)
	  return(NULL);
  }

  log_msg((LOG_TRACE_LEVEL, "mime_next_line: %d bytes", ds_len(ds)));

  return(ds);
}

static int
parse_header_field(char *str, char **name, char **body)
{
  char *p;

  if ((p = strchr(str, ':')) == NULL)
	return(-1);
  *p++ = '\0';

  *name = str;
  *body = p;

  return(0);
}

/*
 * RFC 2045, 2046, 822
 * XXX This is careful about catching errors.
 */
static int
parse_header_field_body(char *str, char **token, Mime_parameter **params)
{
  char *p, *s;
  Ds ds;
  Mime_parameter *np, **prev_ptr;
  static char *tspecials = "()<>@,;:\\\"/[]?=";

  p = strdup(str);
  *params = NULL;

  p = skip_lwsp(p);
  if (*p == '\0')
	return(-1);

  *token = p;
  while (*p != ';' && *p != '\0')
	p++;
  if (*p == '\0')
	return(0);
  *p++ = '\0';

  prev_ptr = params;
  while (*p != '\0') {
	char *q;

	/* A semicolon precedes a parameter - skip initial LWS */
	p = skip_lwsp(p);
	if (*p == '\0')
	  return(-1);

	np = ALLOC(Mime_parameter);
	np->attrname = NULL;
	np->attrvalue = "";
	np->next = NULL;

	*prev_ptr = np;
	prev_ptr = &np->next;

	/* Note the start of the token, then advance to its end. */
	s = p;
	while (strchr(tspecials, (int) *p) == NULL && *p != ' '
		   && !iscntrl((int) *p))
	  p++;

	/* LWS may follow a token - RFC 822 S3.1.4 */
	q = skip_lwsp(p);

	if (*q == ';') {
	  /* Terminate the token. */
	  *p = '\0';
	  np->attrname = s;
	  p = q + 1;
	}
	else if (*q == '=') {
	  int quote;

	  /* Terminate the token. */
	  *p = '\0';
	  np->attrname = s;
	  p = q + 1;

	  /* LWS may follow a token - RFC 822 S3.1.4 */
	  p = skip_lwsp(p);

	  /* Attribute with a (possibly quoted) value */
	  if (*p == '"') {
		quote = '"';
		p++;
	  }
	  else
		quote = '\0';

	  s = p;
	  while (*p != quote && *p != '\0') {
		if (quote == '\0' && *p == ';')
		  break;
		p++;
	  }

	  if (p - s) {
		ds_init(&ds);
		ds_concatn(&ds, s, p - s);
		np->attrvalue = ds_buf(&ds);
	  }

	  if (*p != '\0' && *p == quote) {
		p++;
		p = skip_lwsp(p);
	  }

	  if (*p == ';') {
		p++;
		p = skip_lwsp(p);
	  }
	}
	else if (*q == '\0') {
	  if (np->attrname == NULL) {
		/* No '=' was found... */
		return(-1);
	  }
	  p = q;
	  break;
	}
	else
	  return(-1);
  }

  return(0);
}

/*
 * Return 0 if LINE is not a boundary delimiter.
 * Return 1 if LINE is a boundary delimiter.
 * Return 2 if LINE is the distinguished (terminating) boundary delimiter.
 */
static int
boundary_match(Ds *line, Mime_content_type *ct)
{
  char *ptr;

  if (ds_len(line) < ct->boundary_length + 2)
	return(0);

  ptr = ds_buf(line);
  if (ptr[0] == '-' && ptr[1] == '-'
	  && memcmp(ptr + 2, ct->boundary, ct->boundary_length) == 0) {
	if (ds_len(line) >= ct->boundary_length + 4
		&& ptr[ct->boundary_length + 2] == '-'
		&& ptr[ct->boundary_length + 3] == '-')
	  return(2);

	return(1);
  }

  return(0);
}

/*
 * RFC 2183
 */
static Mime_content_disposition *
new_content_disposition(Mime_content_disposition *ocd)
{
  Mime_content_disposition *cd;

  if (ocd == NULL)
	cd = ALLOC(Mime_content_disposition);
  else
	cd = ocd;

  cd->disposition = MIME_DISPOSITION_UNKNOWN;
  cd->name = NULL;
  cd->filename = NULL;
  cd->creation_date = NULL;
  cd->modification_date = NULL;
  cd->read_date = NULL;
  cd->size = NULL;

  return(cd);
}

static Mime_content_type *
new_content_type(Mime_content_type *oct)
{
  Mime_content_type *ct;

  if (oct == NULL)
	ct = ALLOC(Mime_content_type);
  else
	ct = oct;

  ct->content_type = MIME_TYPE_TEXT;
  ct->subtype = "plain";
  ct->charset = "us-ascii";
  ct->name = NULL;
  ct->boundary = NULL;
  ct->boundary_length = 0;
  ct->format = NULL;
  ct->octet_stream_type = NULL;
  ct->octet_stream_padding = NULL;
  ct->partial_id = NULL;
  ct->partial_number = NULL;
  ct->partial_total = NULL;
  ct->params = NULL;

  return(ct);
}

static Mime_part *
new_part(Mime_part *omp)
{
  Mime_part *mp;

  if (omp == NULL)
	mp = ALLOC(Mime_part);
  else
	mp = omp;

  mp->version = NULL;
  mp->type = new_content_type(NULL);
  mp->encoding = MIME_ENCODING_7BIT;
  mp->disposition = NULL;
  mp->format = MIME_FORMAT_UNKNOWN;
  mp->content_id = NULL;
  mp->description = NULL;
  mp->body = NULL;
  mp->truncated = 0;
  mp->decoded = 0;
  mp->next = NULL;

  return(mp);
}

void
mime_dump_content_type(Mime_content_type *ct)
{
  Mime_parameter *param;
  static char *types[] = {
	"text", "image", "audio", "video", "application",
	"message", "multipart", "unknown", "error"
  };

  if (ct != NULL) {
	Ds ds;

	ds_init(&ds);
	ds_asprintf(&ds, "Content-Type: %s", types[ct->content_type]);
	if (ct->subtype != NULL)
	  ds_asprintf(&ds, ", subtype: %s", ct->subtype);
	if (ct->charset != NULL)
	  ds_asprintf(&ds, ", charset: %s", ct->charset);
	if (ct->name != NULL)
	  ds_asprintf(&ds, ", name: %s", ct->name);
	if (ct->boundary != NULL)
	  ds_asprintf(&ds, ", boundary: %s", ct->boundary);
	if (ct->format != NULL)
	  ds_asprintf(&ds, ", format: %s", ct->format);
	if (ct->octet_stream_type != NULL)
	  ds_asprintf(&ds, ", type: %s", ct->octet_stream_type);
	if (ct->octet_stream_padding != NULL)
	  ds_asprintf(&ds, ", padding: %s", ct->octet_stream_padding);
	if (ct->partial_id != NULL)
	  ds_asprintf(&ds, ", id: %s", ct->partial_id);
	if (ct->partial_number != NULL)
	  ds_asprintf(&ds, ", number: %s", ct->partial_number);
	if (ct->partial_total != NULL)
	  ds_asprintf(&ds, ", total: %s", ct->partial_total);

	if (ct->params != NULL) {
	  ds_asprintf(&ds, "\nOther parameters:");
	  for (param = ct->params; param != NULL; param = param->next)
		ds_asprintf(&ds, "\n  %s: %s\n", param->attrname, param->attrvalue);
	}
	log_msg((LOG_TRACE_LEVEL, "%s", ds_buf(&ds)));
  }
}

static void
mime_dump_content_disposition(Mime_content_disposition *cd)
{
  static char *disp[] = {
	"inline", "attachment", "form-data", "unknown"
  };
  
  if (cd != NULL) {
	Ds ds;

	ds_init(&ds);
	ds_asprintf(&ds, "Content-Disposition: %s", disp[cd->disposition]);
	if (cd->name != NULL)
	  ds_asprintf(&ds, " name: %s", cd->name);
	if (cd->filename != NULL)
	  ds_asprintf(&ds, " filename: %s", cd->filename);
	if (cd->creation_date != NULL)
	  ds_asprintf(&ds, " creation-date: %s", cd->creation_date);
	if (cd->modification_date != NULL)
	  ds_asprintf(&ds, " modification-date: %s", cd->modification_date);
	if (cd->read_date != NULL)
	  ds_asprintf(&ds, " read-date: %s", cd->read_date);
	if (cd->size != NULL)
	  ds_asprintf(&ds, " size: %s", cd->size);

	log_msg((LOG_TRACE_LEVEL, "%s", ds_buf(&ds)));
  }
}

static int
is_printable_mime_body(Mime_part *mp)
{
  char *p;
  size_t len;

  /*
   * The only thing that really matters to us is that it doesn't contain
   * any funny characters.  It may not be null-terminated.
   */
  if (mp->body != NULL) {
	p = ds_buf(mp->body);
	len = ds_len(mp->body);
	if (len && p[len - 1] == '\0')
	  len--;
	if (len == 0 || strprintable(p, len, 1))
	  return(1);
  }

  return(0);
}

void
mime_dump_part(Mime_part *part)
{
  static char *enc[] = {
	"7bit", "8bit", "binary", "quoted-printable",
	"base64", "*error*"
  };

  if (part != NULL) {
	mime_dump_content_type(part->type);
	mime_dump_content_disposition(part->disposition);
	log_msg((LOG_TRACE_LEVEL,
			 "Content-Transfer-Encoding: %s", enc[part->encoding]));
	if (part->body != NULL) {
	  log_msg((LOG_TRACE_LEVEL, "Body is %d bytes long", ds_len(part->body)));
	  if (is_printable_mime_body(part)) {
		log_msg((LOG_TRACE_LEVEL, "Body follows..."));
		log_msg((LOG_TRACE_LEVEL, "%s", ds_buf(part->body)));
	  }
	  else
		log_msg((LOG_TRACE_LEVEL, "Body is unprintable"));
	}
  }
}

void
mime_dump(Mime *mime)
{
  int i;
  Mime_part *part;

  log_msg((LOG_TRACE_LEVEL, "** Mime dump begins: **"));

  if (mime->preamble != NULL) {
	log_msg((LOG_TRACE_LEVEL, "Preamble..."));
	log_msg((LOG_TRACE_LEVEL, "%s", ds_buf(mime->preamble)));
  }

  if (mime->version != NULL)
	log_msg((LOG_TRACE_LEVEL, "MIME-version: %s", mime->version));
  mime_dump_content_type(&mime->content_type);

  for (i = 0, part = mime->parts; part != NULL; part = part->next, i++) {
	log_msg((LOG_TRACE_LEVEL, "Mime part %d...", i + 1));
	mime_dump_part(part);
  }

  if (mime->epilogue != NULL) {
	log_msg((LOG_TRACE_LEVEL, "Epilogue..."));
	log_msg((LOG_TRACE_LEVEL, "%s", ds_buf(mime->epilogue)));
  }

  log_msg((LOG_TRACE_LEVEL, "** Mime dump ends **"));
}

/*
 * RFC 2045 5.2, RFC 2388 3 say the default content type is "text/plain".
 */
void
mime_init(Mime *mime, char *(*resolver)(char *, void *), void *resolver_arg)
{

  mime->version = NULL;
  mime->raw_mime_headers = NULL;
  mime->mime_headers = NULL;
  mime->content_type.content_type = MIME_TYPE_TEXT;
  mime->content_type.subtype = "plain";
  mime->content_type.charset = "us-ascii";
  mime->content_type.name = NULL;
  mime->content_type.boundary = NULL;
  mime->content_type.boundary_length = 0;
  mime->content_type.format = NULL;
  mime->content_type.octet_stream_type = NULL;
  mime->content_type.octet_stream_padding = NULL;
  mime->content_type.partial_id = NULL;
  mime->content_type.partial_number = NULL;
  mime->content_type.partial_total = NULL;
  mime->content_type.params = NULL;
  mime->encoding = MIME_ENCODING_7BIT;
  mime->parts = NULL;
  mime->preamble = NULL;
  mime->epilogue = NULL;
  mime->error_occurred = 0;
  mime->sandbox = NULL;
  mime->in_mem_limit = 0;
  mime->header_resolver = resolver;
  mime->header_resolver_arg = resolver_arg;

  if (resolver != NULL) {
#ifdef NOTDEF
	if ((p = resolver("sasdf", resolver_arg)) != NULL)
	  ;
#endif
  }
}

static int
parse_mime_version(Mime *mime, Mime_part *part, char *token,
				   Mime_parameter *param_list)
{

  if (part->version != NULL) {
	log_msg((LOG_ERROR_LEVEL, "parse_mime_version: duplicate header"));
	return(-1);
  }

  part->version = strdup(token);
  if (param_list != NULL)
	log_msg((LOG_ERROR_LEVEL,
			 "parse_mime_version: unexpected parameters (ignored)"));

  return(0);
}

static int
parse_content_disposition(Mime *mime, Mime_part *part, char *token,
						  Mime_parameter *param_list)
{
  Mime_content_disposition *cd;
  Mime_parameter *param;

  if (part->disposition != NULL) {
	log_msg((LOG_ERROR_LEVEL, "parse_content_disposition: duplicate header"));
	return(-1);
  }

  cd = part->disposition = new_content_disposition(NULL);
  if (strcaseeq(token, "inline"))
	cd->disposition = MIME_DISPOSITION_INLINE;
  else if (strcaseeq(token, "attachment"))
	cd->disposition = MIME_DISPOSITION_ATTACHMENT;
  else if (strcaseeq(token, "form-data"))
	cd->disposition = MIME_DISPOSITION_FORMDATA;
  else {
	cd->disposition = MIME_DISPOSITION_UNKNOWN;
	log_msg((LOG_WARN_LEVEL,
			 "parse_content_disposition: unrecognized token"));
	log_msg((LOG_WARN_LEVEL,
			 "parse_content_disposition: %s (ignored)", token));
  }

  for (param = param_list; param != NULL; param = param->next) {
	if (strcaseeq(param->attrname, "name") && param->attrvalue != NULL)
	  cd->name = strdup(param->attrvalue);
	else if (strcaseeq(param->attrname, "filename")
			 && param->attrvalue != NULL)
	  cd->filename = strdup(param->attrvalue);
	else if (strcaseeq(param->attrname, "creation-date")
			 && param->attrvalue != NULL)
	  cd->creation_date = strdup(param->attrvalue);
	else if (strcaseeq(param->attrname, "modification-date")
			 && param->attrvalue != NULL)
	  cd->modification_date = strdup(param->attrvalue);
	else if (strcaseeq(param->attrname, "read-date")
			 && param->attrvalue != NULL)
	  cd->read_date = strdup(param->attrvalue);
	else if (strcaseeq(param->attrname, "size")
			 && param->attrvalue != NULL)
	  cd->size = strdup(param->attrvalue);
	else {
	  log_msg((LOG_WARN_LEVEL,
			   "parse_content_disposition: unrecognized parameter"));
	  log_msg((LOG_WARN_LEVEL, "parse_content_disposition: %s (ignored)",
			   param->attrname));
	}
  }

  return(0);
}

/*
 * RFC 2045, Section 5.1
 */
static int
do_content_type(Mime_content_type *ct, char *token, Mime_parameter *param_list)
{
  char *p, *s;
  Ds ds;
  Mime_parameter **oparams, *param;

  ct->content_type = MIME_TYPE_TEXT;
  ct->subtype = "plain";
  ct->charset = "us-ascii";
  ct->name = NULL;
  ct->boundary = NULL;
  ct->boundary_length = 0;
  ct->format = NULL;
  ct->octet_stream_type = NULL;
  ct->octet_stream_padding = NULL;
  ct->partial_id = NULL;
  ct->partial_number = NULL;
  ct->partial_total = NULL;

  p = token;
  if (strncaseeq(p, "text/", 5)) {
	ct->content_type = MIME_TYPE_TEXT;
	p += 4;
  }
  else if (strncaseeq(p, "image/", 6)) {
	ct->content_type = MIME_TYPE_IMAGE;
	p += 5;
  }
  else if (strncaseeq(p, "audio/", 6)) {
	ct->content_type = MIME_TYPE_AUDIO;
	p += 5;
  }
  else if (strncaseeq(p, "video/", 6)) {
	ct->content_type = MIME_TYPE_VIDEO;
	p += 5;
  }
  else if (strncaseeq(p, "application/", 12)) {
	ct->content_type = MIME_TYPE_APPLICATION;
	p += 11;
  }
  else if (strncaseeq(p, "multipart/", 10)) {
	ct->content_type = MIME_TYPE_MULTIPART;
	p += 9;
  }
  else {
	ct->content_type = MIME_TYPE_UNKNOWN;
	while (*p != '/' && *p != '\0')
	  p++;
  }

  if (*p == '/') {
	s = ++p;
	while (*p != '\0')
	  p++;
	ds_init(&ds);
	ds_concatn(&ds, s, p - s);
	ct->subtype = ds_buf(&ds);
  }
  else if (*p != '\0')
	return(-1);

  oparams = &ct->params;
  for (param = param_list; param != NULL; param = param->next) {
	if (strcaseeq(param->attrname, "charset"))
	  ct->charset = param->attrvalue;
	else if (strcaseeq(param->attrname, "name"))
	  ct->name = param->attrvalue;
	else if (strcaseeq(param->attrname, "boundary")) {
	  ct->boundary = param->attrvalue;
	  ct->boundary_length = strlen(param->attrvalue);
	}
	else if (strcaseeq(param->attrname, "format"))
	  ct->format = param->attrvalue;
	else if (strcaseeq(param->attrname, "type"))
	  ct->octet_stream_type = param->attrvalue;
	else if (strcaseeq(param->attrname, "padding"))
	  ct->octet_stream_padding = param->attrvalue;
	else if (strcaseeq(param->attrname, "id"))
	  ct->partial_id = param->attrvalue;
	else if (strcaseeq(param->attrname, "number"))
	  ct->partial_number = param->attrvalue;
	else if (strcaseeq(param->attrname, "total"))
	  ct->partial_total = param->attrvalue;
	else {
	  *oparams = ALLOC(Mime_parameter);
	  (*oparams)->attrname = strdup(param->attrname);
	  if (param->attrvalue != NULL)
		(*oparams)->attrvalue = strdup(param->attrvalue);
	  else
		(*oparams)->attrvalue = "";
	  (*oparams)->next = NULL;
	  oparams = &(*oparams)->next;
	}
  }

  return(0);
}

static int
parse_content_type(Mime *mime, Mime_part *part, char *token,
				   Mime_parameter *param_list)
{

  if (do_content_type(part->type, token, param_list) == -1)
	return(-1);

  return(0);
}

Mime_type
mime_parse_content_type(char *field_body, Mime_content_type *ct)
{
  char *token;
  Mime_parameter *param_list;

  if (parse_header_field_body(field_body, &token, &param_list) == -1)
	return(MIME_TYPE_ERROR);

  if (do_content_type(ct, token, param_list) == -1)
	return(MIME_TYPE_ERROR);

  return(ct->content_type);
}

static int
parse_content_id(Mime *mime, Mime_part *part, char *token,
				 Mime_parameter *param_list)
{

  if (part->content_id != NULL) {
	log_msg((LOG_ERROR_LEVEL, "parse_mime_content_id: duplicate header"));
	return(-1);
  }
	
  part->content_id = strdup(token);
  if (param_list != NULL)
	log_msg((LOG_ERROR_LEVEL,
			 "parse_mime_content_id: unexpected parameters (ignored)"));

  return(0);
}

static int
parse_content_description(Mime *mime, Mime_part *part, char *token,
						  Mime_parameter *param_list)
{

  if (part->description != NULL) {
	log_msg((LOG_ERROR_LEVEL, "parse_content_description: duplicate header"));
	return(-1);
  }

  part->description = strdup(token);
  if (param_list != NULL)
	log_msg((LOG_ERROR_LEVEL,
			 "parse_content_description: unexpected parameters (ignored)"));

  return(0);
}

static int
parse_content_transfer_encoding(Mime *mime, Mime_part *part, char *token,
								Mime_parameter *param_list)
{

  if (strcaseeq(token, "7bit"))
	part->encoding = MIME_ENCODING_7BIT;
  else if (strcaseeq(token, "8bit"))
	part->encoding = MIME_ENCODING_8BIT;
  else if (strcaseeq(token, "binary"))
	part->encoding = MIME_ENCODING_BINARY;
  else if (strcaseeq(token, "quoted-printable"))
	part->encoding = MIME_ENCODING_QUOTEDPRINTABLE;
  else if (strcaseeq(token, "base64"))
	part->encoding = MIME_ENCODING_BASE64;
  else {
	part->encoding = MIME_ENCODING_ERROR;
	log_msg((LOG_ERROR_LEVEL,
			 "parse_content_transfer_encoding: unknown token"));
	log_msg((LOG_ERROR_LEVEL, "parse_content_transfer_encoding: '%s'", token));
	return(-1);
  }

  if (param_list != NULL)
	log_msg((LOG_ERROR_LEVEL,
			 "parse_content_transfer_encoding: unexpected parameters (ignored)"));

  return(0);
}

struct mime_header_tab {
  char *name;
  int (*func)(Mime *mime, Mime_part *part, char *token,
			  Mime_parameter *param_list);
} mime_header_tab[] = {
  { "MIME-Version",        parse_mime_version },
  { "Content-Description", parse_content_description },
  { "Content-Disposition", parse_content_disposition },
  { "Content-ID",          parse_content_id },
  { "Content-Transfer-Encoding", parse_content_transfer_encoding },
  { "Content-Type",        parse_content_type },
  { NULL,                  NULL}
};

/*
 * XXX There is no check for duplicate headers.
 */
static int
parse_header(Mime *mime, Mime_part *part, char *field_name, char *token,
			 Mime_parameter *param_list)
{
  int i, st;
  Mime_parameter *param;

  log_msg((LOG_TRACE_LEVEL, "parse_header: field_name=%s, token=%s",
		   field_name, token));
  for (param = param_list; param != NULL; param = param->next)
	log_msg((LOG_TRACE_LEVEL, "parse_header: attrname='%s', attrvalue='%s'",
			 param->attrname, param->attrvalue));

  st = -1;
  for (i = 0; mime_header_tab[i].name != NULL; i++) {
	if (strcaseeq(field_name, mime_header_tab[i].name)) {
	  st = (*mime_header_tab[i].func)(mime, part, token, param_list);
	  break;
	}
  }

  if (st == -1)
	log_msg((LOG_ERROR_LEVEL,
			 "parse_header: unrecognized field_name: %s", field_name));

  return(st);
}

/*
 * Initialize by scanning an RFC 822 message.
 */
int
mime_parse_message_headers(Mime *mime, CGI_input *input)
{
  char *field_name, *field_body, *line, *raw, *token;
  Mime_header *mh;
  Mime_parameter *param_list;

  while ((line = mime_next_header_line(input, &raw)) != NULL) {
	/* A blank line ends the header area. */
	if (*line == '\0')
	  break;

	if (mime->raw_mime_headers == NULL)
	  mime->raw_mime_headers = dsvec_init(NULL, sizeof(char *));
	dsvec_add_ptr(mime->raw_mime_headers, raw);

	if (parse_header_field(line, &field_name, &field_body) == -1)
	  return(-1);

	mh = ALLOC(Mime_header);
	mh->field_name = field_name;
	mh->field_body = field_body;
	if (mime->mime_headers == NULL)
	  mime->mime_headers = dsvec_init(NULL, sizeof(Mime_header));
	dsvec_add_ptr(mime->mime_headers, mh);

	if (strcaseeq(field_name, "MIME-Version")) {
	  if (parse_header_field_body(field_body, &token, &param_list) == -1)
		return(-1);
	  mime->version = token;
	}
	else if (strcaseeq(field_name, "Content-Type")) {
	  if (parse_header_field_body(field_body, &token, &param_list) == -1) {
		log_msg((LOG_ERROR_LEVEL, "Invalid Content-Type syntax: \"%s\"",
				 strtrim(raw, "\n", 0)));
		return(-1);
	  }
	  if (do_content_type(&mime->content_type, token, param_list) == -1)
		return(-1);
	}
  }

  if (line == NULL) {
	/* There is no message body, but that's allowed (RFC 822 3.1). */
	return(0);
  }

  return(0);
}

/*
 * Returns the number of MIME entities processed or -1 on error.
 */
int
mime_parse(Mime *mime, CGI_input *input, Kwv *kwv)
{
  int nparts, rc;
  char *method;
  Ds *line;
  Mime_part *part, **next_part;

  if (log_would_log(LOG_TRACE_LEVEL)) {
	Ds ds;

	ds_init(&ds);
	ds_asprintf(&ds, "Input: ");
	if ((method = http_method_to_string(input->method)) == NULL)
	  ds_asprintf(&ds, "%d", input->method);
	else
	  ds_asprintf(&ds, "%s", method);
	if (input->fp != NULL)
	  ds_asprintf(&ds, " from stream");
	else
	  ds_asprintf(&ds, " from buffer");
	if (input->have_length)
	  ds_asprintf(&ds, ", %ld bytes", input->length);
	else
	  ds_asprintf(&ds, ", indeterminate length");
	log_msg((LOG_TRACE_LEVEL, ds_buf(&ds)));
  }

  /* Content type application/x-www-form-urlencoded is handled specially. */
  if (mime->content_type.content_type == MIME_TYPE_APPLICATION
	  && strcaseeq(mime->content_type.subtype, "x-www-form-urlencoded"))
	return(mime_parse_urlencoded(input, kwv));

#ifdef NOTDEF
  if (mime->content_type.content_type == MIME_TYPE_TEXT) {
	/*
	 * We could handle a text entity-body here, simply by copying it,
	 * except we'd have no way of associating it with a name...
	 * It would have to be passed back flagged as an "unnamed parameter".
	 * See RFC 2616 S3.7
	 */
  }
#endif

  /*
   * We've seen the first boundary delimiter line
   * Begin processing entities, linking the parts together.
   * rc == 0 means processing status ok
   * rc == 1 means saw initial boundary delimiter
   * rc == 2 means saw terminating boundary delimiter
   * rc == -1 means EOF occurred before seeing terminating boundary delimiter
   * rc == -2 means error occurred while processing headers
   */
  rc = 0;
  nparts = 0;

  /* Skip the preamble, which ends when the first boundary is seen. */
  while (1) {
	if ((line = mime_next_line(input)) == NULL) {
	  mime->error_occurred = 1;
	  return(-1);
	}
	if ((rc = boundary_match(line, &mime->content_type)))
	  break;

	if (mime->preamble == NULL) {
	  mime->preamble = ds_init(NULL);
	  log_msg((LOG_TRACE_LEVEL, "Reading preamble..."));
	}
	ds_append(mime->preamble, ds_buf(line));
	ds_append(mime->preamble, "\n");
  }

  if (mime->preamble != NULL)
	ds_appendc(mime->preamble, '\0');

  if (rc == 2) {
	/* Saw the distinguished (terminating) boundary? */
	log_msg((LOG_ERROR_LEVEL,
			 "mime_parse: prematurely saw terminating boundary?"));
	mime->error_occurred = 1;
	return(-1);
  }

  rc = 0;
  next_part = &mime->parts;
  while (rc == 0) {
	Mime_part *part;

	log_msg((LOG_TRACE_LEVEL, "New MIME part begins..."));
	part = new_part(NULL);

	/*
	 * Process a MIME entity.
	 * Look for a header area consisting of zero or more headers, followed
	 * by a blank line, followed by a (possibly empty) body area.
	 * RFC 2046, 5.1.1
	 * General header line syntax is defined in RFC 822 3.1.2
	 */
	while (rc == 0) {
	  char *field_name, *field_body, *hdr, *token;
	  Mime_parameter *param_list;

	  if ((hdr = mime_next_header_line(input, NULL)) == NULL) {
		log_msg((LOG_TRACE_LEVEL, "EOF read in header area"));
		part->truncated = cgiparse_is_end(input);
		rc = -2;
		break;
	  }

	  /* A blank line ends the header area. */
	  if (*hdr == '\0')
		break;

	  log_msg((LOG_TRACE_LEVEL, "Read MIME header: '%s'", hdr));
	  if (parse_header_field(hdr, &field_name, &field_body) == -1) {
		log_msg((LOG_ERROR_LEVEL, "parse_header_field() failed"));
		rc = -2;
		break;
	  }
	  if (parse_header_field_body(field_body, &token, &param_list) == -1) {
		log_msg((LOG_ERROR_LEVEL, "parse_header_field_body() failed"));
		rc = -2;
		break;
	  }

	  if (parse_header(mime, part, field_name, token, param_list) == -1) {
		log_msg((LOG_ERROR_LEVEL, "parse_header() failed"));
		rc = -2;
		break;
	  }
	}

	if (rc == -2) {
	  log_msg((LOG_ERROR_LEVEL, "Error occurred within MIME headers"));
	  mime->error_occurred = 1;
	}
	else {
	  log_msg((LOG_TRACE_LEVEL, "New MIME headers completed, headers are:"));
	  mime_dump_part(part);
	}

	/* Process the body if the headers were ok */
	if (rc == 0) {
	  int linenum;

	  log_msg((LOG_TRACE_LEVEL, "Reading body..."));

	  part->body = ds_init(NULL);
	  linenum = 0;
	  while ((line = mime_next_line(input)) != NULL) {
		if ((rc = boundary_match(line, &mime->content_type)))
		  break;

		log_msg((LOG_TRACE_LEVEL, "Read body data: %d bytes", ds_len(line)));
		/* RFC 2046 S4.1.1, S4.1.3 */
		if (linenum && part->type->content_type == MIME_TYPE_TEXT
			&& streq(part->type->subtype, "plain"))
		  ds_appendc(part->body, '\n');
		ds_copyb(part->body, ds_buf(line), ds_len(line), ds_len(part->body));
		linenum++;
	  }

	  log_msg((LOG_TRACE_LEVEL, "Body length: %d bytes", ds_len(part->body)));

	  if (line == NULL) {
		log_msg((LOG_ERROR_LEVEL, "Saw EOF before distinguished boundary?"));
		part->body = NULL;
		part->truncated = 1;
		mime->error_occurred = 1;
		rc = -1;
	  }
	  else {
		if (part->type != NULL && part->type->content_type == MIME_TYPE_TEXT)
		  ds_appendc(part->body, '\0');
		if (rc == 1)
		  rc = 0;
	  }
	}

	if (rc != -2) {
	  *next_part = part;
	  next_part = &part->next;
	  nparts++;
	  log_msg((LOG_TRACE_LEVEL, "New MIME part added"));
	}
  }

  if (rc != 2) {
	/* Something went wrong... */
	log_msg((LOG_TRACE_LEVEL, "MIME processing error, returning %d part%s",
			 nparts, nparts == 1 ? "" : "s"));
	goto skip_epilogue;
  }

  log_msg((LOG_TRACE_LEVEL, "Saw distinguished boundary delimiter"));

  /* The epilogue may follow the last boundary */
  while (1) {
	if ((line = mime_next_line(input)) == NULL)
	  break;
	if (mime->epilogue == NULL) {
	  mime->epilogue = ds_init(NULL);
	  log_msg((LOG_TRACE_LEVEL, "Reading epilogue..."));
	}
	ds_append(mime->epilogue, ds_buf(line));
	ds_append(mime->epilogue, "\n");
  }
  if (mime->epilogue != NULL)
	ds_appendc(mime->epilogue, '\0');

 skip_epilogue:

  for (part = mime->parts; part != NULL; part = part->next) {
	char *enc, *key;
	Mime_content_disposition *cd;

	if ((cd = part->disposition) != NULL) {
	  if (cd->disposition == MIME_DISPOSITION_FORMDATA && cd->name != NULL) {
		Kwv_pair *pair;

		key = cd->name;
		if (is_printable_mime_body(part)) {
		  ds_appendc(part->body, '\0');
		  pair = kwv_new_pair(key, ds_buf(part->body), NULL);
		}
		else if (part->body != NULL) {
		  mime_encode_base64((unsigned char *) ds_buf(part->body),
							 ds_len(part->body), &enc);
		  pair = kwv_new_pair(key, "", enc);
		}
		else
		  pair = kwv_new_pair(key, "", NULL);

		kwv_add_pair(kwv, pair);
	  }
	}
  }

  log_msg((LOG_TRACE_LEVEL, "MIME processing complete: %d part%s",
		   nparts, nparts == 1 ? "" : "s"));
  return(nparts);
}

static int
hexval(int ch1, int ch2)
{
  int val;

  if (ch1 >= '0' && ch1 <= '9')
	val = ch1 - '0';
  else if (ch1 >= 'a' && ch1 <= 'f')
	val = ch1 - 'a' + 10;
  else if (ch1 >= 'A' && ch1 <= 'F')
	val = ch1 - 'A' + 10;
  else
	return(0);

  val <<= 4;
  if (ch2 >= '0' && ch2 <= '9')
	val += ch2 - '0';
  else if (ch2 >= 'a' && ch2 <= 'f')
	val += ch2 - 'a' + 10;
  else if (ch2 >= 'A' && ch2 <= 'F')
	val += ch2 - 'A' + 10;

  return(val);
}

enum {
  QUOTED_PRINTABLE_MAX_LINE_LEN = 76
};

static void
c2x(Ds *ds, unsigned int what)
{
  static const char c2x_table[] = "0123456789ABCDEF";

  ds_appendc(ds, (int) '=');
  ds_appendc(ds, (int) c2x_table[what >> 4]);
  ds_appendc(ds, (int) c2x_table[what & 0xf]);
}

/*
 * RFC 2045 6.7
 * XXX NOT TESTED YET
 */
Ds *
mime_encode_quotedprintable(char *str, size_t len)
{
  int curr_line_len;
  char *p;
  size_t slen;
  Ds *ds;

  if ((slen = len) == 0)
	slen = strlen(str);

  ds = ds_init(NULL);
  curr_line_len = 0;
  for (p = str; p < (str + slen); p++) {
	if ((*p >= 33 && *p <= 60) || (*p >= 62 && *p <= 126)) {
	  if (curr_line_len == QUOTED_PRINTABLE_MAX_LINE_LEN) {
		ds_append(ds, "=\r\n");
		curr_line_len = 0;
	  }
	  ds_appendc(ds, (int) *p);
	  curr_line_len++;
	}
	else if (*p == '\n') {
	  ds_append(ds, "\r\n");
	  curr_line_len = 0;
	}
	else if (*p == '\t' || *p == ' ') {
	  if (curr_line_len == QUOTED_PRINTABLE_MAX_LINE_LEN) {
		ds_append(ds, "=\r\n");
		ds_appendc(ds, (int) *p);
		curr_line_len = 1;
	  }
	  else if (curr_line_len >= (QUOTED_PRINTABLE_MAX_LINE_LEN - 3)) {
		if (*p == '\t')
		  ds_append(ds, "=09\r\n");
		else
		  ds_append(ds, "=20\r\n");
		curr_line_len = 0;
	  }
	  else {
		ds_appendc(ds, (int) *p);
		curr_line_len++;
	  }
	}
	else {
	  if (curr_line_len >= (QUOTED_PRINTABLE_MAX_LINE_LEN - 3)) {
		ds_append(ds, "=\r\n");
		curr_line_len = 0;
	  }
	  c2x(ds, (int) *p);
	  curr_line_len = 3;
	}
  }

  ds_appendc(ds, (int) '\0');

  return(ds);
}

static int
test_encode_qp(char *path)
{
  char *buf;
  size_t buflen;
  Ds *ds;

  if (load_file(path, &buf, &buflen) == -1)
	return(-1);
  if ((ds = mime_encode_quotedprintable(buf, buflen)) == NULL)
	return(-1);

  printf("%s", ds_buf(ds));

  return(0);
}

/*
 * RFC 2045 6.7
 */
long
mime_decode_quotedprintable(char *str, char **decoded)
{
  char *p;
  Ds ds;
  static char *hexchars = "0123456789ABCDEF";

  ds_init(&ds);
  for (p = str; *p != '\0'; p++) {
	if (*p == '=') {
	  p++;
	  if (*p == '\n')
		continue;
	  if (strchr(hexchars, (int) *p) == NULL
		  || strchr(hexchars, (int) *(p + 1)) == NULL)
		return(-1);
	  ds_appendc(&ds, hexval(*p, *(p + 1)));
	  p++;
	}
	else if ((*p >= '!' && *p <= '<') || (*p >= '>' && *p <= '~'))
	  ds_appendc(&ds, *p);
	else if (*p == ' ' || *p == '\t')
	  ds_appendc(&ds, *p);
  }

  ds_appendc(&ds, '\0');
  *decoded = ds_buf(&ds);

  return(ds_len(&ds) - 1);
}

enum {
  CONV_TABLE_BASE = 0x2b,		/* 43 decimal == '+' */
  CONV_TABLE_SIZE = 85,
  CONV_BAD        = 99
};

static const char a64_map[CONV_TABLE_SIZE] = {
  /* 2b */		       62, 99, 99, 99, 63,
  /* 30 */ 52, 53, 54, 55, 56, 57, 58, 59,
  /* 38 */ 60, 61, 99, 99, 99, 99, 99, 99,
  /* 40 */ 99,  0,  1,  2,  3,  4,  5,  6,
  /* 48 */  7,  8,  9, 10, 11, 12, 13, 14,
  /* 50 */ 15, 16, 17, 18, 19, 20, 21, 22,
  /* 58 */ 23, 24, 25, 99, 99, 99, 99, 99,
  /* 60 */ 99, 26, 27, 28, 29, 30, 31, 32,
  /* 68 */ 33, 34, 35, 36, 37, 38, 39, 40,
  /* 70 */ 41, 42, 43, 44, 45, 46, 47, 48,
  /* 78 */ 49, 50, 51, 99, 99, 99, 99, 99
};

static const char conv_table[64] = {
  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
  'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
  'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
  'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
  'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
  'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
  'w', 'x', 'y', 'z', '0', '1', '2', '3',
  '4', '5', '6', '7', '8', '9', '+', '/'
};

/*
 * RFC 2045 6.8
 *
 * Return the number of bytes stored (excluding the final null byte, which is
 * always appended), or -1 if an error occurs.
 */
long
mime_decode_base64(char *str, unsigned char **decoded)
{
  int got_bits, ind, val, x;
  char *p;
  Ds ds;

  ds_init(&ds);
  got_bits = 0;
  val = 0;

  for (p = str; *p != '\0'; p++) {
	if (*p == '\n')
	  continue;
	if (*p == '=')
	  break;
	ind = *p - CONV_TABLE_BASE;
	if (ind < 0 || ind >= CONV_TABLE_SIZE || (x = a64_map[ind]) == CONV_BAD)
	  return(-1);
	switch (got_bits) {
	case 0:
	  val = x << 2;
	  got_bits = 6;
	  break;
	case 6:
      val |= ((x & 0x30) >> 4);
      ds_appendc(&ds, val);
      val = (x & 0x0f) << 4;
      got_bits = 4;
      break;
    case 4:
      val |= (x & 0x3c) >> 2;
      ds_appendc(&ds, val);
      val = (x & 0x03) << 6;
      got_bits = 2;
      break;
    case 2:
      val |= x;
      ds_appendc(&ds, val);
      got_bits = 0;
      break;
    }
  }

  if (*p == '=') {
	if (got_bits == 0)
	  return(-1);
  }
  else if (got_bits != 0)
	return(-1);

  ds_appendc(&ds, '\0');
  *decoded = (unsigned char *) ds_buf(&ds);

  return(ds_len(&ds) - 1);
}

/*
 * RFC 2045 6.8
 * See also: RFC3548
 */
long
mime_encode_base64(unsigned char *str, unsigned int nbytes, char **encoded)
{
  unsigned int n, rem, rem_bits, val;
  const unsigned char *p;
  Ds ds;

  n = nbytes;
  p = str;
  ds_init(&ds);
  rem = 0;

  rem_bits = 0;
  while (n > 0) {
	switch (rem_bits) {
	case 0:
	  val = (*p & 0xfc) >> 2;
	  ds_appendc(&ds, conv_table[val]);
	  rem = *p & 0x03;
	  rem_bits = 2;
	  break;
	case 2:
	  val = (rem << 4) | (*p & 0xf0) >> 4;
	  ds_appendc(&ds, conv_table[val]);
	  rem = *p & 0x0f;
	  rem_bits = 4;
	  break;
	case 4:
	  val = (rem << 2) | (*p & 0xc0) >> 6;
	  ds_appendc(&ds, conv_table[val]);
	  val = *p & 0x3f;
	  ds_appendc(&ds, conv_table[val]);
	  rem_bits = 0;
	  break;
	}
	p++;
	n--;
  }

  if (rem_bits == 2) {
	val = rem << 4;
	ds_appendc(&ds, conv_table[val]);
	ds_appendc(&ds, '=');
	ds_appendc(&ds, '=');
  }
  else if (rem_bits == 4) {
	val = rem << 2;
	ds_appendc(&ds, conv_table[val]);
	ds_appendc(&ds, '=');
  }

  ds_appendc(&ds, '\0');

  *encoded = ds_buf(&ds);
  return(ds_len(&ds));
}

int
mime_parse_urlencoded(CGI_input *in, Kwv *kwv)
{

  return(cgiparse_urlencoded(in, kwv));
}

/*
 * Return the Mime_file_type description for EXTENSION (which omits the leading
 * dot) and or NULL.
 * The first match is used.  Matching is case insensitive.
 */
Mime_file_type *
mime_find_file_type(Dsvec *dsv, char *extension)
{
  int i, j;
  char **exts;
  Mime_file_type *mft;

  if (dsv == NULL)
	return(NULL);

  for (i = 0; i < dsvec_len(dsv); i++) {
	if ((mft = dsvec_ptr(dsv, i, Mime_file_type *)) == NULL)
	  return(NULL);
	if ((exts = mft->extensions) != NULL) {
	  for (j = 0; exts[j] != NULL; j++) {
		if (strcaseeq(exts[j], extension))
		  return(mft);
	  }
	}
  }

  return(NULL);
}


/*
 * Load and parse MIME file extension mappings from PATHNAME.
 */
Dsvec *
mime_get_file_types(char *pathname)
{
  char *errmsg, *line, *p;
  Ds ds;
  Dsvec *dsv, *fields;
  FILE *fp;
  Mime_file_type *mft;

  if ((fp = fopen(pathname, "r")) == NULL)
	return(NULL);

  ds_init(&ds);
  ds.delnl_flag = 1;
  dsv = dsvec_init(NULL, sizeof(Mime_file_type *));
  fields = dsvec_init(NULL, sizeof(char *));
  while ((line = ds_gets(&ds, fp)) != NULL) {
	for (p = line; (*p == ' ' || *p == '\t') && *p != '\0'; p++)
	  ;
	if (*p == '\0' || *p == '#')
	  continue;

	if ((fields = strsplit_re(p, "[ \t]+", 0, 0, &errmsg)) == NULL)
	  return(NULL);

	mft = ALLOC(Mime_file_type);
	mft->type_name = dsvec_ptr(fields, 0, char *);
	if (dsvec_len(fields) == 1)
	  mft->extensions = NULL;
	else {
	  /* Add a NULL pointer to terminate the vector. */
	  dsvec_add_ptr(fields, NULL);
	  mft->extensions = dsvec_obj(fields, 1, char **);
	}
	dsvec_add_ptr(dsv, mft);
  }

  fclose(fp);

  return(dsv);
}

#ifdef PROG

#include "local.h"

static void
show_mime(FILE *fp_out, char *infile, Mime *mime)
{
  int i;
  Mime_part *part;

  if (mime == NULL)
	return;

  if (mime->mime_headers != NULL) {
	Mime_header *mh;

	for (i = 0; i < dsvec_len(mime->mime_headers); i++) {
	  mh = dsvec_ptr_index(mime->mime_headers, i);
	  if (infile != NULL)
		fprintf(fp_out, "%s: ", infile);
	  fprintf(fp_out, "%s: %s\n", mh->field_name, skip_lwsp(mh->field_body));
	}
  }

}

static void
show_mime_file(FILE *fp_in, char *infile, FILE *fp_out)
{
  CGI_input *input;
  Mime *mime;

  mime = ALLOC(Mime);
  input = cgiparse_set_input(HTTP_POST_METHOD, fp_in, NULL, 0, 0, NULL);
  mime_init(mime, NULL, NULL);
  if (mime_parse_message_headers(mime, input) == -1) {
	fprintf(stderr, "Error parsing MIME headers\n");
	exit(1);
  }

  show_mime(fp_out, infile, mime);
}

/*
 * This doesn't do very much at present.
 * It can be useful when debugging the MIME code.
 */
int
main(int argc, char **argv)
{
  int i, multi_files;
  FILE *fp;
  Log_desc *ld;

  ld = log_init(NULL, 0, NULL, "mime", LOG_NONE_LEVEL, NULL);
  log_set_level(ld, LOG_WARN_LEVEL);
  log_set_desc(ld, LOG_DISABLED);

  for (i = 1; i < argc; i++) {
	if (streq(argv[i], "--"))
	  break;
	if (argv[i][0] != '-')
	  break;
  }

  if (i < argc) {
	multi_files = i < (argc - 1);
	while (i < argc) {
	  if ((fp = fopen(argv[i], "r")) == NULL)
		fprintf(stderr, "Can't open \"%s\"\n", argv[i]);
	  else {
		show_mime_file(fp, multi_files ? argv[i] : NULL, stdout);
		fclose(fp);
	  }
	  i++;
	}
  }
  else
	show_mime_file(stdin, NULL, stdout);

  exit(0);
}
#endif
