/*

  silcmime.c

  Author: Pekka Riikonen <priikone@silcnet.org>

  Copyright (C) 2005 - 2007 Pekka Riikonen

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; version 2 of the License.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

*/

#include "silc.h"

/************************ Static utility functions **************************/

/* MIME fields destructor */

static void silc_mime_field_dest(void *key, void *context, void *user_context)
{
  silc_free(key);
  silc_free(context);
}

/* Assembler fragment destructor */

static void silc_mime_assembler_dest(void *key, void *context,
				     void *user_context)
{
  silc_free(key);
  silc_hash_table_free(context);
}

/* Assembler partial MIME destructor */

static void silc_mime_assemble_dest(void *key, void *context,
				    void *user_context)
{
  silc_mime_free(context);
}


/******************************* Public API *********************************/

/* Allocate MIME context */

SilcMime silc_mime_alloc(void)
{
  SilcMime mime;

  mime = silc_calloc(1, sizeof(*mime));
  if (!mime)
    return NULL;

  mime->fields = silc_hash_table_alloc(0, silc_hash_string, mime,
				       silc_hash_string_compare, mime,
				       silc_mime_field_dest, mime, TRUE);
  if (!mime->fields) {
    silc_mime_free(mime);
    return NULL;
  }

  return mime;
}

/* Free MIME context */

void silc_mime_free(SilcMime mime)
{
  SilcMime m;

  if (mime->fields)
    silc_hash_table_free(mime->fields);

  if (mime->multiparts) {
    silc_dlist_start(mime->multiparts);
    while ((m = silc_dlist_get(mime->multiparts)) != SILC_LIST_END)
      silc_mime_free(m);
    silc_dlist_uninit(mime->multiparts);
  }
  silc_free(mime->boundary);
  silc_free(mime->multitype);
  silc_free(mime->data);
  silc_free(mime);
}

/* Allocate MIME assembler */

SilcMimeAssembler silc_mime_assembler_alloc(void)
{
  SilcMimeAssembler assembler;

  assembler = silc_calloc(1, sizeof(*assembler));
  if (!assembler)
    return NULL;

  assembler->fragments =
    silc_hash_table_alloc(0, silc_hash_string, NULL,
			  silc_hash_string_compare, NULL,
			  silc_mime_assembler_dest, assembler, TRUE);
  if (!assembler->fragments) {
    silc_mime_assembler_free(assembler);
    return NULL;
  }

  return assembler;
}

/* Free MIME assembler */

void silc_mime_assembler_free(SilcMimeAssembler assembler)
{
  silc_hash_table_free(assembler->fragments);
  silc_free(assembler);
}

/* Decode MIME message */

SilcMime silc_mime_decode(SilcMime mime, const unsigned char *data,
			  SilcUInt32 data_len)
{
  SilcMime m = NULL;
  int i, k;
  char *tmp, *field, *value, *line;

  SILC_LOG_DEBUG(("Parsing MIME message"));

  if (!data)
    return NULL;

  if (!mime) {
    mime = silc_mime_alloc();
    if (!mime)
      return NULL;
    m = mime;
  }

  /* Parse the fields */
  line = tmp = (char *)data;
  for (i = 0; i < data_len; i++) {
    /* Get field line */
    if (data_len - i >= 2 && tmp[i] == '\r' && tmp[i + 1] == '\n') {
      /* Get field */
      field = strchr(line, ':');
      if (!field)
	goto err;
      field = silc_memdup(line, field - line);
      if (!field)
	goto err;

      /* Get value. Remove whitespaces too. */
      value = strchr(line, ':');
      if ((tmp + i) - value < 2)
	goto err;
      value++;
      for (k = 0; k < (tmp + i) - value; k++) {
	if (value[k] == '\r')
	  goto err;
	if (value[k] != ' ' && value[k] != '\t')
	  break;
      }
      value += k;
      if ((tmp + i) - value < 1)
	goto err;
      value = silc_memdup(value, (tmp + i) - value);
      if (!value)
	goto err;

      SILC_LOG_DEBUG(("Header '%s' '%s'", field, value));

      /* Add field and value */
      silc_mime_add_field(mime, field, value);
      silc_free(field);
      silc_free(value);

      /* Mark start of next line */
      line = (tmp + i) + 2;
      i += 2;

      /* Break if this is last header */
      if (data_len - i >= 2 &&
	  tmp[i] == '\r' && tmp[i + 1] == '\n') {
	i += 2;
	break;
      }
    }
  }

  /* Parse multiparts if present */
  field = (char *)silc_mime_get_field(mime, "Content-Type");
  if (field && strstr(field, "multipart")) {
    char b[1024];
    SilcMime p;
    unsigned int len;

    mime->multiparts = silc_dlist_init();
    if (!mime->multiparts)
      goto err;

    /* Get multipart type */
    value = strchr(field, '/');
    if (!value)
      goto err;
    value++;
    if (strchr(field, '"'))
      value++;
    if (!strchr(field, ';'))
      goto err;
    memset(b, 0, sizeof(b));
    len = (unsigned int)(strchr(field, ';') - value);
    if (len > sizeof(b) - 1)
      goto err;
    strncpy(b, value, len);
    if (strchr(b, '"'))
      *strchr(b, '"') = '\0';
    mime->multitype = silc_memdup(b, strlen(b));

    /* Get boundary */
    value = strrchr(field, '=');
    if (value && strlen(value) > 1) {
      value++;

      SILC_LOG_DEBUG(("Boundary '%s'", value));

      memset(b, 0, sizeof(b));
      line = strdup(value);
      if (strrchr(line, '"')) {
	*strrchr(line, '"') = '\0';
	silc_snprintf(b, sizeof(b) - 1, "--%s", line + 1);
	mime->boundary = strdup(line + 1);
      } else {
	silc_snprintf(b, sizeof(b) - 1, "--%s", line);
	mime->boundary = strdup(line);
      }
      silc_free(line);

      for ( ; i < data_len; i++) {
	/* Get boundary data */
	if (data_len - i >= strlen(b) &&
	    tmp[i] == '-' && tmp[i + 1] == '-') {
	  if (memcmp(tmp + i, b, strlen(b)))
	    continue;

	  i += strlen(b);

	  if (data_len - i >= 4 &&
	      tmp[i    ] == '\r' && tmp[i + 1] == '\n' &&
	      tmp[i + 2] == '\r' && tmp[i + 3] == '\n')
	    i += 4;
	  else if (data_len - i >= 2 &&
		   tmp[i] == '\r' && tmp[i + 1] == '\n')
	    i += 2;
	  else if (data_len - i >= 2 &&
		   tmp[i] == '-' && tmp[i + 1] == '-')
	    break;

	  line = tmp + i;

	  /* Find end of boundary */
	  for (k = i; k < data_len; k++)
	    if (data_len - k >= strlen(b) &&
		tmp[k] == '-' && tmp[k + 1] == '-')
	      if (!memcmp(tmp + k, b, strlen(b)))
		break;
	  if (k >= data_len)
	    goto err;

	  /* Remove preceding CRLF */
	  k -= 2;

	  /* Parse the part */
	  p = silc_mime_decode(NULL, line, k - i);
	  if (!p)
	    goto err;

	  silc_dlist_add(mime->multiparts, p);
	  i += (k - i);
	}
      }
    }
  } else {
    /* Get data area.  If we are at the end and we have fields present
       there is no data area present, but, if fields are not present we
       only have data area. */
    if (i >= data_len && !silc_hash_table_count(mime->fields))
      i = 0;
    SILC_LOG_DEBUG(("Data len %d", data_len - i));
    if (data_len - i)
      silc_mime_add_data(mime, tmp + i, data_len - i);
  }

  return mime;

 err:
  if (m)
    silc_mime_free(m);
  return NULL;
}

/* Encode MIME message */

unsigned char *silc_mime_encode(SilcMime mime, SilcUInt32 *encoded_len)
{
  SilcMime part;
  SilcHashTableList htl;
  SilcBufferStruct buf;
  SilcBuffer buffer;
  char *field, *value, tmp[1024], tmp2[4];
  unsigned char *ret;
  int i;

  SILC_LOG_DEBUG(("Encoding MIME message"));

  if (!mime)
    return NULL;

  memset(&buf, 0, sizeof(buf));

  /* Encode the headers. Order doesn't matter */
  i = 0;
  silc_hash_table_list(mime->fields, &htl);
  while (silc_hash_table_get(&htl, (void *)&field, (void *)&value)) {
    memset(tmp, 0, sizeof(tmp));
    SILC_LOG_DEBUG(("Header %s: %s", field, value));
    silc_snprintf(tmp, sizeof(tmp) - 1, "%s: %s\r\n", field, value);
    silc_buffer_strformat(&buf, tmp, SILC_STRFMT_END);
    i++;
  }
  silc_hash_table_list_reset(&htl);
  if (i)
    silc_buffer_strformat(&buf, "\r\n", SILC_STRFMT_END);

  /* Assemble the whole buffer */
  buffer = silc_buffer_alloc_size(mime->data_len + silc_buffer_len(&buf));
  if (!buffer)
    return NULL;

  /* Add headers */
  if (silc_buffer_len(&buf)) {
    silc_buffer_put(buffer, buf.head, silc_buffer_len(&buf));
    silc_buffer_pull(buffer, silc_buffer_len(&buf));
    silc_buffer_purge(&buf);
  }

  /* Add data */
  if (mime->data) {
    SILC_LOG_DEBUG(("Data len %d", mime->data_len));
    silc_buffer_put(buffer, mime->data, mime->data_len);
  }

  /* Add multiparts */
  if (mime->multiparts) {
    SILC_LOG_DEBUG(("Encoding multiparts"));

    silc_dlist_start(mime->multiparts);
    i = 0;
    while ((part = silc_dlist_get(mime->multiparts)) != SILC_LIST_END) {
      unsigned char *pd;
      SilcUInt32 pd_len;

      /* Recursive encoding */
      pd = silc_mime_encode(part, &pd_len);
      if (!pd)
	return NULL;

      memset(tmp, 0, sizeof(tmp));
      memset(tmp2, 0, sizeof(tmp2));

      /* If fields are not present, add extra CRLF */
      if (!silc_hash_table_count(part->fields))
	silc_snprintf(tmp2, sizeof(tmp2) - 1, "\r\n");
      silc_snprintf(tmp, sizeof(tmp) - 1, "%s--%s\r\n%s",
	       i != 0 ? "\r\n" : "", mime->boundary, tmp2);
      i = 1;

      buffer = silc_buffer_realloc(buffer, silc_buffer_truelen(buffer) +
				   pd_len + strlen(tmp));
      if (!buffer)
	return NULL;
      silc_buffer_put_tail(buffer, tmp, strlen(tmp));
      silc_buffer_pull_tail(buffer, strlen(tmp));
      silc_buffer_put_tail(buffer, pd, pd_len);
      silc_buffer_pull_tail(buffer, pd_len);
      silc_free(pd);
    }

    memset(tmp, 0, sizeof(tmp));
    silc_snprintf(tmp, sizeof(tmp) - 1, "\r\n--%s--\r\n", mime->boundary);
    buffer = silc_buffer_realloc(buffer, silc_buffer_truelen(buffer) +
				 strlen(tmp));
    if (!buffer)
      return NULL;
    silc_buffer_put_tail(buffer, tmp, strlen(tmp));
    silc_buffer_pull_tail(buffer, strlen(tmp));
  }

  ret = silc_buffer_steal(buffer, encoded_len);
  silc_buffer_free(buffer);

  return ret;
}

/* Assembles MIME message from partial MIME messages */

SilcMime silc_mime_assemble(SilcMimeAssembler assembler, SilcMime partial)
{
  char *type, *id = NULL, *tmp;
  SilcHashTable f;
  SilcMime p, complete;
  int i, number, total = -1;
  const unsigned char *data;
  SilcUInt32 data_len;
  SilcBuffer compbuf = NULL;

  SILC_LOG_DEBUG(("Assembling MIME fragments"));

  if (!assembler || !partial)
    goto err;

  type = (char *)silc_mime_get_field(partial, "Content-Type");
  if (!type)
    goto err;

  /* Get ID */
  tmp = strstr(type, "id=");
  if (!tmp)
    goto err;
  if (strlen(tmp) <= 4)
    goto err;
  tmp += 3;
  if (*tmp == '"')
    tmp++;
  id = strdup(tmp);
  if (strchr(id, ';'))
    *strchr(id, ';') = '\0';
  if (strrchr(id, '"'))
    *strrchr(id, '"') = '\0';

  SILC_LOG_DEBUG(("Fragment ID %s", id));

  /* Get fragment number */
  tmp = strstr(type, "number=");
  if (!tmp)
    goto err;
  tmp = strchr(tmp, '=');
  if (strlen(tmp) < 2)
    goto err;
  tmp++;
  if (strchr(tmp, ';')) {
    tmp = strdup(tmp);
    *strchr(tmp, ';') = '\0';
    number = atoi(tmp);
    silc_free(tmp);
  } else {
    number = atoi(tmp);
  }

  SILC_LOG_DEBUG(("Fragment number %d", number));

  /* Find fragments with this ID. */
  if (!silc_hash_table_find(assembler->fragments, (void *)id,
			    NULL, (void *)&f)) {
    /* This is new fragment to new message.  Add to hash table and return. */
    f = silc_hash_table_alloc(0, silc_hash_uint, NULL, NULL, NULL,
			      silc_mime_assemble_dest, NULL, TRUE);
    if (!f)
	 goto err;
    silc_hash_table_add(f, SILC_32_TO_PTR(number), partial);
    silc_hash_table_add(assembler->fragments, id, f);
    return NULL;
  }

  /* Try to get total number */
  tmp = strstr(type, "total=");
  if (tmp) {
    tmp = strchr(tmp, '=');
    if (strlen(tmp) < 2)
      goto err;
    tmp++;
    if (strchr(tmp, ';')) {
      tmp = strdup(tmp);
      *strchr(tmp, ';') = '\0';
      total = atoi(tmp);
      silc_free(tmp);
    } else {
      total = atoi(tmp);
    }

    SILC_LOG_DEBUG(("Fragment total %d", total));
  }

  /* If more fragments to come, add to hash table */
  if (number != total) {
    silc_hash_table_add(f, SILC_32_TO_PTR(number), partial);
    return NULL;
  }

  silc_hash_table_add(f, SILC_32_TO_PTR(number), partial);

  /* Verify that we really have all the fragments */
  if (silc_hash_table_count(f) < total)
    return NULL;

  /* Assemble the complete MIME message now. We get them in order from
     the hash table. */
  for (i = 1; i <= total; i++) {
    if (!silc_hash_table_find(f, SILC_32_TO_PTR(i), NULL, (void *)&p))
      goto err;

    /* The fragment is in the data portion of the partial message */
    data = silc_mime_get_data(p, &data_len);
    if (!data)
      goto err;

    /* Assemble */
    if (!compbuf) {
      compbuf = silc_buffer_alloc_size(data_len);
      if (!compbuf)
	goto err;
      silc_buffer_put(compbuf, data, data_len);
    } else {
      compbuf = silc_buffer_realloc(compbuf, silc_buffer_truelen(compbuf) +
				    data_len);
      if (!compbuf)
	goto err;
      silc_buffer_put_tail(compbuf, data, data_len);
      silc_buffer_pull_tail(compbuf, data_len);
    }
  }

  if (!compbuf)
    goto err;

  /* Now parse the complete MIME message and deliver it */
  complete = silc_mime_decode(NULL, (const unsigned char *)compbuf->head,
			      silc_buffer_truelen(compbuf));
  if (!complete)
    goto err;

  /* Delete the hash table entry. Destructors will free memory */
  silc_hash_table_del(assembler->fragments, (void *)id);
  silc_free(id);
  silc_buffer_free(compbuf);

  return complete;

 err:
  silc_free(id);
  if (compbuf)
    silc_buffer_free(compbuf);
  silc_mime_free(partial);
  return NULL;
}

/* Encodes partial MIME messages */

SilcDList silc_mime_encode_partial(SilcMime mime, int max_size)
{
  unsigned char *buf, *tmp;
  SilcUInt32 buf_len, len, tmp_len, off;
  SilcDList list;
  SilcBuffer buffer;
  SilcMime partial;
  char type[128], id[64];
  int num;

  SILC_LOG_DEBUG(("Fragmenting MIME message"));

  /* Encode as normal */
  buf = silc_mime_encode(mime, &buf_len);
  if (!buf)
    return NULL;

  list = silc_dlist_init();

  /* Fragment if it is too large */
  if (buf_len > max_size) {
    memset(id, 0, sizeof(id));
    memset(type, 0, sizeof(type));
    gethostname(type, sizeof(type) - 1);
    srand((time(NULL) + buf_len) ^ rand());
    silc_snprintf(id, sizeof(id) - 1, "%X%X%X%s",
	     (unsigned int)rand(), (unsigned int)time(NULL),
	     (unsigned int)buf_len, type);

    SILC_LOG_DEBUG(("Fragment ID %s", id));

    partial = silc_mime_alloc();
    if (!partial)
      return NULL;

    silc_mime_add_field(partial, "MIME-Version", "1.0");
    memset(type, 0, sizeof(type));
    silc_snprintf(type, sizeof(type) - 1,
	     "message/partial; id=\"%s\"; number=1", id);
    silc_mime_add_field(partial, "Content-Type", type);
    silc_mime_add_data(partial, buf, max_size);

    tmp = silc_mime_encode(partial, &tmp_len);
    if (!tmp)
      return NULL;
    silc_mime_free(partial);

    /* Add to list */
    buffer = silc_buffer_alloc_size(tmp_len);
    if (!buffer)
      return NULL;
    silc_buffer_put(buffer, tmp, tmp_len);
    silc_dlist_add(list, buffer);
    silc_free(tmp);

    len = buf_len - max_size;
    off = max_size;
    num = 2;
    while (len > 0) {
      partial = silc_mime_alloc();
      if (!partial)
	return NULL;

      memset(type, 0, sizeof(type));
      silc_mime_add_field(partial, "MIME-Version", "1.0");

      if (len > max_size) {
	silc_snprintf(type, sizeof(type) - 1,
		 "message/partial; id=\"%s\"; number=%d",
		 id, num++);
	silc_mime_add_data(partial, buf + off, max_size);
	off += max_size;
	len -= max_size;
      } else {
	silc_snprintf(type, sizeof(type) - 1,
		 "message/partial; id=\"%s\"; number=%d; total=%d",
		 id, num, num);
	silc_mime_add_data(partial, buf + off, len);
	len = 0;
      }

      silc_mime_add_field(partial, "Content-Type", type);

      tmp = silc_mime_encode(partial, &tmp_len);
      if (!tmp)
	return NULL;
      silc_mime_free(partial);

      /* Add to list */
      buffer = silc_buffer_alloc_size(tmp_len);
      if (!buffer)
	return NULL;
      silc_buffer_put(buffer, tmp, tmp_len);
      silc_dlist_add(list, buffer);
      silc_free(tmp);
    }
  } else {
    /* No need to fragment */
    buffer = silc_buffer_alloc_size(buf_len);
    if (!buffer)
      return NULL;
    silc_buffer_put(buffer, buf, buf_len);
    silc_dlist_add(list, buffer);
  }

  silc_free(buf);

  return list;
}

/* Free partial MIME list */

void silc_mime_partial_free(SilcDList partials)
{
  SilcBuffer buf;

  if (!partials)
    return;

  silc_dlist_start(partials);
  while ((buf = silc_dlist_get(partials)) != SILC_LIST_END)
    silc_buffer_free(buf);
  silc_dlist_uninit(partials);
}

/* Add field */

void silc_mime_add_field(SilcMime mime, const char *field, const char *value)
{
  if (!mime || !field || !value)
    return;

  silc_hash_table_add(mime->fields, strdup(field), strdup(value));
}

/* Get field */

const char *silc_mime_get_field(SilcMime mime, const char *field)
{
  char *value;

  if (!mime || !field)
    return NULL;

  if (!silc_hash_table_find(mime->fields, (void *)field,
			    NULL, (void *)&value))
    return NULL;

  return (const char *)value;
}

/* Add data */

void silc_mime_add_data(SilcMime mime, const unsigned char *data,
			SilcUInt32 data_len)
{
  if (!mime || !data)
    return;

  if (mime->data)
    silc_free(mime->data);

  mime->data = silc_memdup(data, data_len);
  mime->data_len = data_len;
}

/* Get data */

const unsigned char *silc_mime_get_data(SilcMime mime, SilcUInt32 *data_len)
{
  if (!mime)
    return NULL;

  if (data_len)
    *data_len = mime->data_len;

  return mime->data;
}

/* Steal data */

unsigned char *silc_mime_steal_data(SilcMime mime, SilcUInt32 *data_len)
{
  unsigned char *data;

  if (!mime)
    return NULL;

  if (data_len)
    *data_len = mime->data_len;

  data = mime->data;

  mime->data = NULL;
  mime->data_len = 0;

  return data;
}

/* Returns TRUE if partial message */

SilcBool silc_mime_is_partial(SilcMime mime)
{
  const char *type = silc_mime_get_field(mime, "Content-Type");
  if (!type)
    return FALSE;

  if (!strstr(type, "message/partial"))
    return FALSE;

  return TRUE;
}

/* Set as multipart message */

void silc_mime_set_multipart(SilcMime mime, const char *type,
			     const char *boundary)
{
  char tmp[1024];

  if (!mime || !type || !boundary)
    return;

  memset(tmp, 0, sizeof(tmp));
  silc_snprintf(tmp, sizeof(tmp) - 1, "multipart/%s; boundary=%s", type, boundary);
  silc_mime_add_field(mime, "Content-Type", tmp);
  silc_free(mime->boundary);
  mime->boundary = strdup(boundary);

  if (mime->multiparts)
    return;
  mime->multiparts = silc_dlist_init();
}

/* Add multipart */

SilcBool silc_mime_add_multipart(SilcMime mime, SilcMime part)
{
  if (!mime || !mime->multiparts || !part)
    return FALSE;

  silc_dlist_add(mime->multiparts, part);
  return TRUE;
}

/* Return TRUE if has multiparts */

SilcBool silc_mime_is_multipart(SilcMime mime)
{
  if (!mime)
    return FALSE;

  return mime->multiparts != NULL;
}

/* Returns multiparts */

SilcDList silc_mime_get_multiparts(SilcMime mime, const char **type)
{
  if (!mime)
    return NULL;

  if (type)
    *type = (const char *)mime->multitype;

  return mime->multiparts;
}
