/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) version 3.
 *
 * 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with the program; if not, see <http://www.gnu.org/licenses/>
 *
 *
 * Authors:
 *		Michael Zucchi <notzed@ximian.com>
 *      Jeffrey Stedfast <fejj@ximian.com>
 *
 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <string.h>

#include <gio/gio.h>
#include <glib/gi18n-lib.h>

#include "em-format.h"
#include "e-util/e-util.h"
#include "shell/e-shell.h"
#include "shell/e-shell-settings.h"

#define d(x)

typedef struct _EMFormatCache EMFormatCache;

struct _EMFormatPrivate {
	guint redraw_idle_id;
};

/* Used to cache various data/info for redraws
   The validity stuff could be cached at a higher level but this is easier
   This absolutely relies on the partid being _globally unique_
   This is still kind of yucky, we should maintian a full tree of all this data,
   along with/as part of the puri tree */
struct _EMFormatCache {
	CamelCipherValidity *valid; /* validity copy */
	CamelMimePart *secured;	/* encrypted subpart */

	guint state:2;		/* inline state */

	gchar partid[1];
};

#define INLINE_UNSET (0)
#define INLINE_ON (1)
#define INLINE_OFF (2)

static void emf_builtin_init (EMFormatClass *);

enum {
	EMF_COMPLETE,
	EMF_LAST_SIGNAL
};

static gpointer parent_class;
static guint signals[EMF_LAST_SIGNAL];

static void
emf_free_cache (EMFormatCache *efc)
{
	if (efc->valid)
		camel_cipher_validity_free (efc->valid);
	if (efc->secured)
		g_object_unref (efc->secured);
	g_free (efc);
}

static EMFormatCache *
emf_insert_cache (EMFormat *emf, const gchar *partid)
{
	EMFormatCache *new;

	new = g_malloc0 (sizeof (*new)+strlen (partid));
	strcpy (new->partid, partid);
	g_hash_table_insert (emf->inline_table, new->partid, new);

	return new;
}

static void
emf_clone_inlines (gpointer key, gpointer val, gpointer data)
{
	EMFormatCache *emfc = val, *new;

	new = emf_insert_cache ((EMFormat *) data, emfc->partid);
	new->state = emfc->state;
	if (emfc->valid)
		new->valid = camel_cipher_validity_clone (emfc->valid);
	if (emfc->secured)
		g_object_ref ((new->secured = emfc->secured));
}

static gboolean
emf_clear_puri_node (GNode *node)
{
	GQueue *queue = node->data;
	EMFormatPURI *pn;

	while ((pn = g_queue_pop_head (queue)) != NULL) {
		if (pn->free != NULL)
			pn->free (pn);
		g_free (pn->uri);
		g_free (pn->cid);
		g_free (pn->part_id);
		if (pn->part != NULL)
			g_object_unref (pn->part);
		g_free (pn);
	}

	g_queue_free (queue);

	return FALSE;
}

static void
emf_finalize (GObject *object)
{
	EMFormat *emf = EM_FORMAT (object);

	if (emf->priv->redraw_idle_id > 0)
		g_source_remove (emf->priv->redraw_idle_id);

	if (emf->session)
		g_object_unref (emf->session);

	if (emf->message)
		g_object_unref (emf->message);

	g_hash_table_destroy (emf->inline_table);

	em_format_clear_headers (emf);
	camel_cipher_validity_free (emf->valid);
	g_free (emf->charset);
	g_free (emf->default_charset);
	g_string_free (emf->part_id, TRUE);
	g_free (emf->current_message_part_id);
	g_free (emf->uid);

	if (emf->pending_uri_table != NULL)
		g_hash_table_destroy (emf->pending_uri_table);

	if (emf->pending_uri_tree != NULL) {
		g_node_traverse (
			emf->pending_uri_tree,
			G_IN_ORDER, G_TRAVERSE_ALL, -1,
			(GNodeTraverseFunc) emf_clear_puri_node, NULL);
		g_node_destroy (emf->pending_uri_tree);
	}

	/* FIXME: check pending jobs */

	/* Chain up to parent's finalize() method. */
	G_OBJECT_CLASS (parent_class)->finalize (object);
}

static const EMFormatHandler *
emf_find_handler (EMFormat *emf,
                  const gchar *mime_type)
{
	EMFormatClass *emfc = (EMFormatClass *) G_OBJECT_GET_CLASS (emf);

	return g_hash_table_lookup (emfc->type_handlers, mime_type);
}

static void
emf_format_clone (EMFormat *emf,
                  CamelFolder *folder,
                  const gchar *uid,
                  CamelMimeMessage *msg,
                  EMFormat *emfsource,
                  GCancellable *cancellable)
{
	/* Cancel any pending redraws. */
	if (emf->priv->redraw_idle_id > 0) {
		g_source_remove (emf->priv->redraw_idle_id);
		emf->priv->redraw_idle_id = 0;
	}

	em_format_clear_puri_tree (emf);

	if (emf != emfsource) {
		g_hash_table_remove_all (emf->inline_table);
		if (emfsource) {
			GList *link;

			/* We clone the current state here */
			g_hash_table_foreach (emfsource->inline_table, emf_clone_inlines, emf);
			emf->mode = emfsource->mode;
			g_free (emf->charset);
			emf->charset = g_strdup (emfsource->charset);
			g_free (emf->default_charset);
			emf->default_charset = g_strdup (emfsource->default_charset);

			em_format_clear_headers (emf);

			link = g_queue_peek_head_link (&emfsource->header_list);
			while (link != NULL) {
				struct _EMFormatHeader *h = link->data;
				em_format_add_header (emf, h->name, h->flags);
				link = g_list_next (link);
			}
		}
	}

	/* what a mess */
	if (folder != emf->folder) {
		if (emf->folder)
			g_object_unref (emf->folder);
		if (folder)
			g_object_ref (folder);
		emf->folder = folder;
	}

	if (uid != emf->uid) {
		g_free (emf->uid);
		emf->uid = g_strdup (uid);
	}

	if (msg != emf->message) {
		if (emf->message)
			g_object_unref (emf->message);
		if (msg)
			g_object_ref (msg);
		emf->message = msg;
	}

	g_free (emf->current_message_part_id);
	emf->current_message_part_id = g_strdup ("root-message");
	g_string_truncate (emf->part_id, 0);
	if (folder != NULL)
		/* TODO build some string based on the folder name/location? */
		g_string_append_printf(emf->part_id, ".%p", (gpointer) folder);
	if (uid != NULL)
		g_string_append_printf(emf->part_id, ".%s", uid);
}

static void
emf_format_secure (EMFormat *emf,
                   CamelStream *stream,
                   CamelMimePart *part,
                   CamelCipherValidity *valid,
                   GCancellable *cancellable)
{
	CamelCipherValidity *save = emf->valid_parent;
	gint len;

	/* Note that this also requires support from higher up in the class chain
	    - validity needs to be cleared when you start output
	    - also needs to be cleared (but saved) whenever you start a new message. */

	if (emf->valid == NULL) {
		emf->valid = valid;
	} else {
		camel_dlist_addtail (&emf->valid_parent->children, (CamelDListNode *) valid);
		camel_cipher_validity_envelope (emf->valid_parent, valid);
	}

	emf->valid_parent = valid;

	len = emf->part_id->len;
	g_string_append_printf(emf->part_id, ".secured");
	em_format_part (emf, stream, part, cancellable);
	g_string_truncate (emf->part_id, len);

	emf->valid_parent = save;
}

static gboolean
emf_busy (EMFormat *emf)
{
	return FALSE;
}

static gboolean
emf_is_inline (EMFormat *emf,
               const gchar *part_id,
               CamelMimePart *mime_part,
               const EMFormatHandler *handle)
{
	EMFormatCache *emfc;
	const gchar *disposition;

	if (handle == NULL)
		return FALSE;

	emfc = g_hash_table_lookup (emf->inline_table, part_id);
	if (emfc && emfc->state != INLINE_UNSET)
		return emfc->state & 1;

	/* Some types need to override the disposition.
	 * e.g. application/x-pkcs7-mime */
	if (handle->flags & EM_FORMAT_HANDLER_INLINE_DISPOSITION)
		return TRUE;

	disposition = camel_mime_part_get_disposition (mime_part);
	if (disposition != NULL)
		return g_ascii_strcasecmp (disposition, "inline") == 0;

	/* Otherwise, use the default for this handler type. */
	return (handle->flags & EM_FORMAT_HANDLER_INLINE) != 0;
}

static void
emf_base_init (EMFormatClass *class)
{
	class->type_handlers = g_hash_table_new (g_str_hash, g_str_equal);
	emf_builtin_init (class);
}

static void
emf_class_init (EMFormatClass *class)
{
	GObjectClass *object_class;

	parent_class = g_type_class_peek_parent (class);
	g_type_class_add_private (class, sizeof (EMFormatPrivate));

	object_class = G_OBJECT_CLASS (class);
	object_class->finalize = emf_finalize;

	class->find_handler = emf_find_handler;
	class->format_clone = emf_format_clone;
	class->format_secure = emf_format_secure;
	class->busy = emf_busy;
	class->is_inline = emf_is_inline;

	signals[EMF_COMPLETE] = g_signal_new (
		"complete",
		G_OBJECT_CLASS_TYPE (class),
		G_SIGNAL_RUN_LAST,
		G_STRUCT_OFFSET (EMFormatClass, complete),
		NULL, NULL,
		g_cclosure_marshal_VOID__VOID,
		G_TYPE_NONE, 0);
}

static void
emf_init (EMFormat *emf)
{
	EShell *shell;
	EShellSettings *shell_settings;

	emf->priv = G_TYPE_INSTANCE_GET_PRIVATE (
		emf, EM_TYPE_FORMAT, EMFormatPrivate);

	emf->inline_table = g_hash_table_new_full (
		g_str_hash, g_str_equal,
		(GDestroyNotify) NULL,
		(GDestroyNotify) emf_free_cache);
	emf->composer = FALSE;
	emf->print = FALSE;
	g_queue_init (&emf->header_list);
	em_format_default_headers (emf);
	emf->part_id = g_string_new("");
	emf->current_message_part_id = NULL;
	emf->validity_found = 0;

	shell = e_shell_get_default ();
	shell_settings = e_shell_get_shell_settings (shell);

	emf->session = e_shell_settings_get_pointer (shell_settings, "mail-session");
	g_return_if_fail (emf->session != NULL);

	g_object_ref (emf->session);
}

GType
em_format_get_type (void)
{
	static GType type = 0;

	if (G_UNLIKELY (type == 0)) {
		static const GTypeInfo type_info = {
			sizeof (EMFormatClass),
			(GBaseInitFunc) emf_base_init,
			(GBaseFinalizeFunc) NULL,
			(GClassInitFunc) emf_class_init,
			(GClassFinalizeFunc) NULL,
			NULL,  /* class_data */
			sizeof (EMFormat),
			0,     /* n_preallocs */
			(GInstanceInitFunc) emf_init,
			NULL   /* value_table */
		};

		type = g_type_register_static (
			G_TYPE_OBJECT, "EMFormat", &type_info, 0);
	}

	return type;
}

/**
 * em_format_class_add_handler:
 * @emfc: EMFormatClass
 * @info: Callback information.
 *
 * Add a mime type handler to this class.  This is only used by
 * implementing classes.  The @info.old pointer will automatically be
 * setup to point to the old hanlder if one was already set.  This can
 * be used for overrides a fallback.
 *
 * When a mime type described by @info is encountered, the callback will
 * be invoked.  Note that @info may be extended by sub-classes if
 * they require additional context information.
 *
 * Use a mime type of "foo/ *" to insert a fallback handler for type "foo".
 **/
void
em_format_class_add_handler (EMFormatClass *emfc,
                             EMFormatHandler *info)
{
	info->old = g_hash_table_lookup (emfc->type_handlers, info->mime_type);
	g_hash_table_insert (emfc->type_handlers, (gpointer) info->mime_type, info);
}

struct _class_handlers {
	EMFormatClass *old;
	EMFormatClass *new;
};

static void
merge_missing (gpointer key, gpointer value, gpointer userdata)
{
	struct _class_handlers *classes = (struct _class_handlers *) userdata;
	EMFormatHandler *info;

	info = g_hash_table_lookup (classes->new->type_handlers, key);
	if (!info) {
		/* Might be from a plugin */
		g_hash_table_insert (classes->new->type_handlers, key, value);
	}

}

void
em_format_merge_handler (EMFormat *new, EMFormat *old)
{
	EMFormatClass *oldc = (EMFormatClass *) G_OBJECT_GET_CLASS (old);
	EMFormatClass *newc = (EMFormatClass *) G_OBJECT_GET_CLASS (new);
	struct _class_handlers fclasses;

	fclasses.old = oldc;
	fclasses.new = newc;

	g_hash_table_foreach (oldc->type_handlers, merge_missing, &fclasses);

}

/**
 * em_format_class_remove_handler:
 * @emfc:
 * @info:
 *
 * Remove a handler.  @info must be a value which was previously
 * added.
 **/
void
em_format_class_remove_handler (EMFormatClass *emfc,
                                EMFormatHandler *info)
{
	EMFormatHandler *current;

	/* TODO: thread issues? */

	current = g_hash_table_lookup (emfc->type_handlers, info->mime_type);
	if (current == info) {
		current = info->old;
		if (current)
			g_hash_table_insert (
				emfc->type_handlers,
				(gpointer) current->mime_type, current);
		else
			g_hash_table_remove (
				emfc->type_handlers, info->mime_type);
	} else {
		while (current && current->old != info)
			current = current->old;
		g_return_if_fail (current != NULL);
		current->old = info->old;
	}
}

/**
 * em_format_find_handler:
 * @emf:
 * @mime_type:
 *
 * Find a format handler by @mime_type.
 *
 * Return value: NULL if no handler is available.
 **/
const EMFormatHandler *
em_format_find_handler (EMFormat *emf,
                        const gchar *mime_type)
{
	EMFormatClass *class;

	g_return_val_if_fail (EM_IS_FORMAT (emf), NULL);
	g_return_val_if_fail (mime_type != NULL, NULL);

	class = EM_FORMAT_GET_CLASS (emf);
	g_return_val_if_fail (class->find_handler != NULL, NULL);

	return class->find_handler (emf, mime_type);
}

/**
 * em_format_fallback_handler:
 * @emf:
 * @mime_type:
 *
 * Try to find a format handler based on the major type of the @mime_type.
 *
 * The subtype is replaced with "*" and a lookup performed.
 *
 * Return value:
 **/
const EMFormatHandler *
em_format_fallback_handler (EMFormat *emf,
                            const gchar *mime_type)
{
	gchar *mime, *s;

	s = strchr (mime_type, '/');
	if (s == NULL)
		mime = (gchar *) mime_type;
	else {
		gsize len = (s-mime_type)+1;

		mime = g_alloca (len+2);
		strncpy (mime, mime_type, len);
		strcpy(mime+len, "*");
	}

	return em_format_find_handler (emf, mime);
}

/**
 * em_format_add_puri:
 * @emf:
 * @size:
 * @cid: Override the autogenerated content id.
 * @part:
 * @func:
 *
 * Add a pending-uri handler.  When formatting parts that reference
 * other parts, a pending-uri (PURI) can be used to track the reference.
 *
 * @size is used to allocate the structure, so that it can be directly
 * subclassed by implementors.
 *
 * @cid can be used to override the key used to retreive the PURI, if NULL,
 * then the content-location and the content-id of the @part are stored
 * as lookup keys for the part.
 *
 * FIXME: This may need a free callback.
 *
 * Return value: A new PURI, with a referenced copy of @part, and the cid
 * always set.  The uri will be set if one is available.  Clashes
 * are resolved by forgetting the old PURI in the global index.
 **/
EMFormatPURI *
em_format_add_puri (EMFormat *emf,
                    gsize size,
                    const gchar *cid,
                    CamelMimePart *part,
                    EMFormatPURIFunc func)
{
	EMFormatPURI *puri;
	const gchar *tmp;

	d(printf("adding puri for part: %s\n", emf->part_id->str));

	if (size < sizeof (*puri)) {
		g_warning (
			"size (%" G_GSIZE_FORMAT
			") less than size of puri\n", size);
		size = sizeof (*puri);
	}

	puri = g_malloc0 (size);

	puri->format = emf;
	puri->func = func;
	puri->use_count = 0;
	puri->cid = g_strdup (cid);
	puri->part_id = g_strdup (emf->part_id->str);

	if (part) {
		g_object_ref (part);
		puri->part = part;
	}

	if (part != NULL && cid == NULL) {
		tmp = camel_mime_part_get_content_id (part);
		if (tmp)
			puri->cid = g_strdup_printf("cid:%s", tmp);
		else
			puri->cid = g_strdup_printf("em-no-cid:%s", emf->part_id->str);

		d(printf("built cid '%s'\n", puri->cid));

		/* Not quite same as old behaviour, it also put in the
		 * relative uri and a fallback for no parent uri. */
		tmp = camel_mime_part_get_content_location (part);
		puri->uri = NULL;
		if (tmp == NULL) {
			/* No location, don't set a uri at all,
			 * html parts do this themselves. */
		} else {
			if (strchr (tmp, ':') == NULL && emf->base != NULL) {
				CamelURL *uri;

				uri = camel_url_new_with_base (emf->base, tmp);
				puri->uri = camel_url_to_string (uri, 0);
				camel_url_free (uri);
			} else {
				puri->uri = g_strdup (tmp);
			}
		}
	}

	g_return_val_if_fail (puri->cid != NULL, NULL);
	g_return_val_if_fail (emf->pending_uri_level != NULL, NULL);
	g_return_val_if_fail (emf->pending_uri_table != NULL, NULL);

	g_queue_push_tail (emf->pending_uri_level->data, puri);

	if (puri->uri)
		g_hash_table_insert (emf->pending_uri_table, puri->uri, puri);
	g_hash_table_insert (emf->pending_uri_table, puri->cid, puri);

	return puri;
}

/**
 * em_format_push_level:
 * @emf:
 *
 * This is used to build a heirarchy of visible PURI objects based on
 * the structure of the message.  Used by multipart/alternative formatter.
 *
 * FIXME: This could probably also take a uri so it can automaticall update
 * the base location.
 **/
void
em_format_push_level (EMFormat *emf)
{
	GNode *node;

	g_return_if_fail (EM_IS_FORMAT (emf));

	node = g_node_new (g_queue_new ());

	if (emf->pending_uri_tree == NULL)
		emf->pending_uri_tree = node;
	else
		g_node_append (emf->pending_uri_tree, node);

	emf->pending_uri_level = node;
}

/**
 * em_format_pull_level:
 * @emf:
 *
 * Drop a level of visibility back to the parent.  Note that
 * no PURI values are actually freed.
 **/
void
em_format_pull_level (EMFormat *emf)
{
	g_return_if_fail (EM_IS_FORMAT (emf));
	g_return_if_fail (emf->pending_uri_level != NULL);

	emf->pending_uri_level = emf->pending_uri_level->parent;
}

/**
 * em_format_find_visible_puri:
 * @emf:
 * @uri:
 *
 * Search for a PURI based on the visibility defined by :push_level()
 * and :pull_level().
 *
 * Return value:
 **/
EMFormatPURI *
em_format_find_visible_puri (EMFormat *emf,
                             const gchar *uri)
{
	GNode *node;

	g_return_val_if_fail (EM_IS_FORMAT (emf), NULL);
	g_return_val_if_fail (uri != NULL, NULL);

	node = emf->pending_uri_level;

	while (node != NULL) {
		GQueue *queue = node->data;
		GList *link;

		link = g_queue_peek_head_link (queue);

		while (link != NULL) {
			EMFormatPURI *pw = link->data;

			if (g_strcmp0 (pw->uri, uri) == 0)
				return pw;

			if (g_strcmp0 (pw->cid, uri) == 0)
				return pw;

			link = g_list_next (link);
		}

		node = node->parent;
	}

	return NULL;
}

/**
 * em_format_find_puri:
 * @emf:
 * @uri:
 *
 * Search for a PURI based on a uri.  Both the content-id
 * and content-location are checked.
 *
 * Return value:
 **/
EMFormatPURI *
em_format_find_puri (EMFormat *emf,
                     const gchar *uri)
{
	g_return_val_if_fail (EM_IS_FORMAT (emf), NULL);
	g_return_val_if_fail (uri != NULL, NULL);

	g_return_val_if_fail (emf->pending_uri_table != NULL, NULL);

	return g_hash_table_lookup (emf->pending_uri_table, uri);
}

/**
 * em_format_clear_puri_tree:
 * @emf:
 *
 * For use by implementors to clear out the message structure
 * data.
 **/
void
em_format_clear_puri_tree (EMFormat *emf)
{
	if (emf->pending_uri_table == NULL)
		emf->pending_uri_table =
			g_hash_table_new (g_str_hash, g_str_equal);

	else {
		g_hash_table_remove_all (emf->pending_uri_table);

		g_node_traverse (
			emf->pending_uri_tree,
			G_IN_ORDER, G_TRAVERSE_ALL, -1,
			(GNodeTraverseFunc) emf_clear_puri_node, NULL);
		g_node_destroy (emf->pending_uri_tree);

		emf->pending_uri_tree = NULL;
		emf->pending_uri_level = NULL;
	}

	em_format_push_level (emf);
}

/* use mime_type == NULL  to force showing as application/octet-stream */
void
em_format_part_as (EMFormat *emf,
                   CamelStream *stream,
                   CamelMimePart *part,
                   const gchar *mime_type,
                   GCancellable *cancellable)
{
	const EMFormatHandler *handle = NULL;
	const gchar *snoop_save = emf->snoop_mime_type, *tmp;
	CamelURL *base_save = emf->base, *base = NULL;
	gchar *basestr = NULL;

	d(printf("format_part_as()\n"));

	emf->snoop_mime_type = NULL;

	/* RFC 2110, we keep track of content-base, and absolute content-location headers
	   This is actually only required for html, but, *shrug* */
	tmp = camel_medium_get_header((CamelMedium *)part, "Content-Base");
	if (tmp == NULL) {
		tmp = camel_mime_part_get_content_location (part);
		if (tmp && strchr (tmp, ':') == NULL)
			tmp = NULL;
	} else {
		tmp = basestr = camel_header_location_decode (tmp);
	}
	d(printf("content-base is '%s'\n", tmp?tmp:"<unset>"));
	if (tmp
	    && (base = camel_url_new (tmp, NULL))) {
		emf->base = base;
		d(printf("Setting content base '%s'\n", tmp));
	}
	g_free (basestr);

	if (mime_type != NULL) {
		gboolean is_fallback = FALSE;
		if (g_ascii_strcasecmp(mime_type, "application/octet-stream") == 0) {
			emf->snoop_mime_type = mime_type = em_format_snoop_type (part);
			if (mime_type == NULL)
				mime_type = "application/octet-stream";
		}

		handle = em_format_find_handler (emf, mime_type);
		if (handle == NULL) {
			handle = em_format_fallback_handler (emf, mime_type);
			is_fallback = TRUE;
		}

		if (handle != NULL
		    && !em_format_is_attachment (emf, part)) {
			d(printf("running handler for type '%s'\n", mime_type));
			handle->handler (
				emf, stream, part, handle,
				cancellable, is_fallback);
			goto finish;
		}
		d(printf("this type is an attachment? '%s'\n", mime_type));
	} else {
		mime_type = "application/octet-stream";
	}

	EM_FORMAT_GET_CLASS (emf)->format_attachment (
		emf, stream, part, mime_type, handle, cancellable);

finish:
	emf->base = base_save;
	emf->snoop_mime_type = snoop_save;

	if (base)
		camel_url_free (base);
}

void
em_format_part (EMFormat *emf,
                CamelStream *stream,
                CamelMimePart *mime_part,
                GCancellable *cancellable)
{
	gchar *mime_type;
	CamelDataWrapper *dw;

	dw = camel_medium_get_content (CAMEL_MEDIUM (mime_part));
	mime_type = camel_data_wrapper_get_mime_type (dw);
	if (mime_type != NULL) {
		camel_strdown (mime_type);
		em_format_part_as (
			emf, stream, mime_part, mime_type, cancellable);
		g_free (mime_type);
	} else
		em_format_part_as (
			emf, stream, mime_part, "text/plain", cancellable);
}

/**
 * em_format_format_clone:
 * @emf: an #EMFormat
 * @folder: a #CamelFolder or %NULL
 * @uid: Message UID or %NULL
 * @msg: a #CamelMimeMessage or %NULL
 * @emfsource: Used as a basis for user-altered layout, e.g. inline viewed
 * attachments.
 * @cancellable: a #GCancellable, or %NULL
 *
 * Format a message @msg.  If @emfsource is non NULL, then the status of
 * inlined expansion and so forth is copied direction from @emfsource.
 *
 * By passing the same value for @emf and @emfsource, you can perform
 * a display refresh, or it can be used to generate an identical layout,
 * e.g. to print what the user has shown inline.
 **/
void
em_format_format_clone (EMFormat *emf,
                        CamelFolder *folder,
                        const gchar *uid,
                        CamelMimeMessage *message,
                        EMFormat *source,
                        GCancellable *cancellable)
{
	EMFormatClass *class;

	g_return_if_fail (EM_IS_FORMAT (emf));
	g_return_if_fail (folder == NULL || CAMEL_IS_FOLDER (folder));
	g_return_if_fail (message == NULL || CAMEL_IS_MIME_MESSAGE (message));
	g_return_if_fail (source == NULL || EM_IS_FORMAT (source));

	class = EM_FORMAT_GET_CLASS (emf);
	g_return_if_fail (class->format_clone != NULL);

	class->format_clone (emf, folder, uid, message, source, cancellable);
}

void
em_format_format (EMFormat *emf,
                  CamelFolder *folder,
                  const gchar *uid,
                  CamelMimeMessage *message,
                  GCancellable *cancellable)
{
	/* em_format_format_clone() will check the arguments. */
	em_format_format_clone (emf, folder, uid, message, NULL, cancellable);
}

static gboolean
format_redraw_idle_cb (EMFormat *emf)
{
	emf->priv->redraw_idle_id = 0;

	/* FIXME Not passing a GCancellable here. */
	em_format_format_clone (
		emf, emf->folder, emf->uid, emf->message, emf, NULL);

	return FALSE;
}

void
em_format_queue_redraw (EMFormat *emf)
{
	g_return_if_fail (EM_IS_FORMAT (emf));

	if (emf->priv->redraw_idle_id == 0)
		emf->priv->redraw_idle_id = g_idle_add (
			(GSourceFunc) format_redraw_idle_cb, emf);
}

/**
 * em_format_set_mode:
 * @emf:
 * @type:
 *
 * Set display mode, EM_FORMAT_MODE_SOURCE, EM_FORMAT_MODE_ALLHEADERS,
 * or EM_FORMAT_MODE_NORMAL.
 **/
void
em_format_set_mode (EMFormat *emf,
                    EMFormatMode mode)
{
	g_return_if_fail (EM_IS_FORMAT (emf));

	if (emf->mode == mode)
		return;

	emf->mode = mode;

	/* force redraw if type changed afterwards */
	if (emf->message != NULL)
		em_format_queue_redraw (emf);
}

/**
 * em_format_set_charset:
 * @emf:
 * @charset:
 *
 * set override charset on formatter.  message will be redisplayed if
 * required.
 **/
void
em_format_set_charset (EMFormat *emf,
                       const gchar *charset)
{
	if ((emf->charset && charset && g_ascii_strcasecmp (emf->charset, charset) == 0)
	    || (emf->charset == NULL && charset == NULL)
	    || (emf->charset == charset))
		return;

	g_free (emf->charset);
	emf->charset = g_strdup (charset);

	if (emf->message)
		em_format_queue_redraw (emf);
}

/**
 * em_format_set_default_charset:
 * @emf:
 * @charset:
 *
 * Set the fallback, default system charset to use when no other charsets
 * are present.  Message will be redisplayed if required (and sometimes
 * redisplayed when it isn't).
 **/
void
em_format_set_default_charset (EMFormat *emf,
                               const gchar *charset)
{
	if ((emf->default_charset && charset &&
	    g_ascii_strcasecmp (emf->default_charset, charset) == 0)
	    || (emf->default_charset == NULL && charset == NULL)
	    || (emf->default_charset == charset))
		return;

	g_free (emf->default_charset);
	emf->default_charset = g_strdup (charset);

	if (emf->message && emf->charset == NULL)
		em_format_queue_redraw (emf);
}

/**
 * em_format_clear_headers:
 * @emf:
 *
 * Clear the list of headers to be displayed.  This will force all headers to
 * be shown.
 **/
void
em_format_clear_headers (EMFormat *emf)
{
	EMFormatHeader *eh;

	while ((eh = g_queue_pop_head (&emf->header_list)) != NULL)
		g_free (eh);
}

/* note: also copied in em-mailer-prefs.c */
static const struct {
	const gchar *name;
	guint32 flags;
} default_headers[] = {
	{ N_("From"), EM_FORMAT_HEADER_BOLD },
	{ N_("Reply-To"), EM_FORMAT_HEADER_BOLD },
	{ N_("To"), EM_FORMAT_HEADER_BOLD },
	{ N_("Cc"), EM_FORMAT_HEADER_BOLD },
	{ N_("Bcc"), EM_FORMAT_HEADER_BOLD },
	{ N_("Subject"), EM_FORMAT_HEADER_BOLD },
	{ N_("Date"), EM_FORMAT_HEADER_BOLD },
	{ N_("Newsgroups"), EM_FORMAT_HEADER_BOLD },
	{ N_("Face"), 0 },
};

/**
 * em_format_default_headers:
 * @emf:
 *
 * Set the headers to show to the default list.
 *
 * From, Reply-To, To, Cc, Bcc, Subject and Date.
 **/
void
em_format_default_headers (EMFormat *emf)
{
	gint ii;

	em_format_clear_headers (emf);

	for (ii = 0; ii < G_N_ELEMENTS (default_headers); ii++)
		em_format_add_header (
			emf, default_headers[ii].name,
			default_headers[ii].flags);
}

/**
 * em_format_add_header:
 * @emf:
 * @name: The name of the header, as it will appear during output.
 * @flags: EM_FORMAT_HEAD_* defines to control display attributes.
 *
 * Add a specific header to show.  If any headers are set, they will
 * be displayed in the order set by this function.  Certain known
 * headers included in this list will be shown using special
 * formatting routines.
 **/
void
em_format_add_header (EMFormat *emf,
                      const gchar *name,
                      guint32 flags)
{
	EMFormatHeader *h;

	h = g_malloc (sizeof (*h) + strlen (name));
	h->flags = flags;
	strcpy (h->name, name);
	g_queue_push_tail (&emf->header_list, h);
}

/**
 * em_format_is_attachment:
 * @emf:
 * @part: Part to check.
 *
 * Returns true if the part is an attachment.
 *
 * A part is not considered an attachment if it is a
 * multipart, or a text part with no filename.  It is used
 * to determine if an attachment header should be displayed for
 * the part.
 *
 * Content-Disposition is not checked.
 *
 * Return value: TRUE/FALSE
 **/
gint
em_format_is_attachment (EMFormat *emf,
                         CamelMimePart *part)
{
	/*CamelContentType *ct = camel_mime_part_get_content_type(part);*/
	CamelDataWrapper *dw = camel_medium_get_content ((CamelMedium *) part);

	if (!dw)
		return 0;

	/*printf("checking is attachment %s/%s\n", ct->type, ct->subtype);*/
	return !(camel_content_type_is (dw->mime_type, "multipart", "*")
		 || camel_content_type_is(dw->mime_type, "application", "x-pkcs7-mime")
		 || camel_content_type_is(dw->mime_type, "application", "pkcs7-mime")
		 || camel_content_type_is(dw->mime_type, "application", "x-inlinepgp-signed")
		 || camel_content_type_is(dw->mime_type, "application", "x-inlinepgp-encrypted")
		 || camel_content_type_is(dw->mime_type, "x-evolution", "evolution-rss-feed")
		 || camel_content_type_is(dw->mime_type, "text", "calendar")
		 || camel_content_type_is(dw->mime_type, "text", "x-calendar")
		 || (camel_content_type_is (dw->mime_type, "text", "*")
		     && camel_mime_part_get_filename (part) == NULL));
}

/**
 * em_format_is_inline:
 * @emf:
 * @part:
 * @part_id: format->part_id part id of this part.
 * @handle: handler for this part
 *
 * Returns true if the part should be displayed inline.  Any part with
 * a Content-Disposition of inline, or if the @handle has a default
 * inline set, will be shown inline.
 *
 * :set_inline() called on the same part will override any calculated
 * value.
 *
 * Return value:
 **/
gboolean
em_format_is_inline (EMFormat *emf,
                     const gchar *part_id,
                     CamelMimePart *mime_part,
                     const EMFormatHandler *handle)
{
	EMFormatClass *class;

	g_return_val_if_fail (EM_IS_FORMAT (emf), FALSE);
	g_return_val_if_fail (part_id != NULL, FALSE);
	g_return_val_if_fail (CAMEL_IS_MIME_PART (mime_part), FALSE);

	class = EM_FORMAT_GET_CLASS (emf);
	g_return_val_if_fail (class->is_inline != NULL, FALSE);

	return class->is_inline (emf, part_id, mime_part, handle);
}

/**
 * em_format_set_inline:
 * @emf:
 * @part_id: id of part
 * @state:
 *
 * Force the attachment @part to be expanded or hidden explictly to match
 * @state.  This is used only to record the change for a redraw or
 * cloned layout render and does not force a redraw.
 **/
void
em_format_set_inline (EMFormat *emf,
                      const gchar *part_id,
                      gint state)
{
	EMFormatCache *emfc;

	g_return_if_fail (EM_IS_FORMAT (emf));
	g_return_if_fail (part_id != NULL);

	emfc = g_hash_table_lookup (emf->inline_table, part_id);
	if (emfc == NULL) {
		emfc = emf_insert_cache (emf, part_id);
	} else if (emfc->state != INLINE_UNSET && (emfc->state & 1) == state)
		return;

	emfc->state = state?INLINE_ON:INLINE_OFF;

	if (emf->message)
		em_format_queue_redraw (emf);
}

void
em_format_format_attachment (EMFormat *emf,
                             CamelStream *stream,
                             CamelMimePart *mime_part,
                             const gchar *mime_type,
                             const EMFormatHandler *info,
                             GCancellable *cancellable)
{
	EMFormatClass *class;

	g_return_if_fail (EM_IS_FORMAT (emf));
	g_return_if_fail (CAMEL_IS_STREAM (stream));
	g_return_if_fail (CAMEL_IS_MIME_PART (mime_part));
	g_return_if_fail (mime_type != NULL);
	g_return_if_fail (info != NULL);

	class = EM_FORMAT_GET_CLASS (emf);
	g_return_if_fail (class->format_attachment != NULL);

	class->format_attachment (
		emf, stream, mime_part, mime_type, info, cancellable);
}

void
em_format_format_error (EMFormat *emf,
                        CamelStream *stream,
                        const gchar *format,
                        ...)
{
	EMFormatClass *class;
	gchar *errmsg;
	va_list ap;

	g_return_if_fail (EM_IS_FORMAT (emf));
	g_return_if_fail (CAMEL_IS_STREAM (stream));
	g_return_if_fail (format != NULL);

	class = EM_FORMAT_GET_CLASS (emf);
	g_return_if_fail (class->format_error != NULL);

	va_start (ap, format);
	errmsg = g_strdup_vprintf (format, ap);
	class->format_error (emf, stream, errmsg);
	g_free (errmsg);
	va_end (ap);
}

void
em_format_format_secure (EMFormat *emf,
                         CamelStream *stream,
                         CamelMimePart *mime_part,
                         CamelCipherValidity *valid,
                         GCancellable *cancellable)
{
	EMFormatClass *class;

	g_return_if_fail (EM_IS_FORMAT (emf));
	g_return_if_fail (CAMEL_IS_STREAM (stream));
	g_return_if_fail (CAMEL_IS_MIME_PART (mime_part));
	g_return_if_fail (valid != NULL);

	class = EM_FORMAT_GET_CLASS (emf);
	g_return_if_fail (class->format_secure != NULL);

	class->format_secure (emf, stream, mime_part, valid, cancellable);

	if (emf->valid_parent == NULL && emf->valid != NULL) {
		camel_cipher_validity_free (emf->valid);
		emf->valid = NULL;
	}
}

void
em_format_format_source (EMFormat *emf,
                         CamelStream *stream,
                         CamelMimePart *mime_part,
                         GCancellable *cancellable)
{
	EMFormatClass *class;

	g_return_if_fail (EM_IS_FORMAT (emf));
	g_return_if_fail (CAMEL_IS_STREAM (stream));
	g_return_if_fail (CAMEL_IS_MIME_PART (mime_part));

	class = EM_FORMAT_GET_CLASS (emf);
	g_return_if_fail (class->format_source != NULL);

	class->format_source (emf, stream, mime_part, cancellable);
}

gboolean
em_format_busy (EMFormat *emf)
{
	EMFormatClass *class;

	g_return_val_if_fail (EM_IS_FORMAT (emf), FALSE);

	class = EM_FORMAT_GET_CLASS (emf);
	g_return_val_if_fail (class->busy != NULL, FALSE);

	return class->busy (emf);
}

/* should this be virtual? */
void
em_format_format_content (EMFormat *emf,
                          CamelStream *stream,
                          CamelMimePart *part,
                          GCancellable *cancellable)
{
	CamelDataWrapper *dw = camel_medium_get_content ((CamelMedium *) part);

	if (camel_content_type_is (dw->mime_type, "text", "*"))
		em_format_format_text (
			emf, stream, (CamelDataWrapper *) part, cancellable);
	else
		camel_data_wrapper_decode_to_stream_sync (
			dw, stream, cancellable, NULL);
}

/**
 * em_format_format_content:
 * @emf:
 * @stream: Where to write the converted text
 * @part: Part whose container is to be formatted
 * @cancellable: optional #GCancellable object, or %NULL
 *
 * Decode/output a part's content to @stream.
 **/
void
em_format_format_text (EMFormat *emf,
                       CamelStream *stream,
                       CamelDataWrapper *dw,
                       GCancellable *cancellable)
{
	CamelStream *filter_stream;
	CamelMimeFilter *filter;
	const gchar *charset = NULL;
	CamelMimeFilterWindows *windows = NULL;
	CamelStream *mem_stream = NULL;
	const gchar *key;
	gsize size;
	gsize max;
	GConfClient *gconf;

	if (emf->charset) {
		charset = emf->charset;
	} else if (dw->mime_type
		   && (charset = camel_content_type_param (dw->mime_type, "charset"))
		   && g_ascii_strncasecmp(charset, "iso-8859-", 9) == 0) {
		CamelStream *null;

		/* Since a few Windows mailers like to claim they sent
		 * out iso-8859-# encoded text when they really sent
		 * out windows-cp125#, do some simple sanity checking
		 * before we move on... */

		null = camel_stream_null_new ();
		filter_stream = camel_stream_filter_new (null);
		g_object_unref (null);

		windows = (CamelMimeFilterWindows *) camel_mime_filter_windows_new (charset);
		camel_stream_filter_add (
			CAMEL_STREAM_FILTER (filter_stream),
			CAMEL_MIME_FILTER (windows));

		camel_data_wrapper_decode_to_stream_sync (
			dw, (CamelStream *) filter_stream, cancellable, NULL);
		camel_stream_flush ((CamelStream *) filter_stream, cancellable, NULL);
		g_object_unref (filter_stream);

		charset = camel_mime_filter_windows_real_charset (windows);
	} else if (charset == NULL) {
		charset = emf->default_charset;
	}

	mem_stream = (CamelStream *) camel_stream_mem_new ();
	filter_stream = camel_stream_filter_new (mem_stream);

	if ((filter = camel_mime_filter_charset_new (charset, "UTF-8"))) {
		camel_stream_filter_add (
			CAMEL_STREAM_FILTER (filter_stream),
			CAMEL_MIME_FILTER (filter));
		g_object_unref (filter);
	}

	max = -1;

	gconf = gconf_client_get_default ();
	key = "/apps/evolution/mail/display/force_message_limit";
	if (gconf_client_get_bool (gconf, key, NULL)) {
		key = "/apps/evolution/mail/display/message_text_part_limit";
		max = gconf_client_get_int (gconf, key, NULL);
		if (max == 0)
			max = -1;
	}
	g_object_unref (gconf);

	size = camel_data_wrapper_decode_to_stream_sync (
		emf->mode == EM_FORMAT_MODE_SOURCE ?
			(CamelDataWrapper *) dw :
			camel_medium_get_content ((CamelMedium *) dw),
		(CamelStream *) filter_stream, cancellable, NULL);
	camel_stream_flush ((CamelStream *) filter_stream, cancellable, NULL);
	g_object_unref (filter_stream);
	camel_stream_reset (mem_stream, NULL);

	if (max == -1 || size == -1 || size < (max * 1024) || emf->composer) {
		camel_stream_write_to_stream (
			mem_stream, (CamelStream *) stream, cancellable, NULL);
		camel_stream_flush ((CamelStream *) stream, cancellable, NULL);
	} else {
		EM_FORMAT_GET_CLASS (emf)->format_optional (
			emf, stream, (CamelMimePart *) dw,
			mem_stream, cancellable);
	}

	if (windows)
		g_object_unref (windows);

	g_object_unref (mem_stream);
}

/**
 * em_format_describe_part:
 * @part:
 * @mimetype:
 *
 * Generate a simple textual description of a part, @mime_type represents the
 * the content.
 *
 * Return value:
 **/
gchar *
em_format_describe_part (CamelMimePart *part,
                         const gchar *mime_type)
{
	GString *stext;
	const gchar *filename, *description;
	gchar *content_type, *desc;

	stext = g_string_new("");
	content_type = g_content_type_from_mime_type (mime_type);
	desc = g_content_type_get_description (content_type ? content_type : mime_type);
	g_free (content_type);
	g_string_append_printf (stext, _("%s attachment"), desc ? desc : mime_type);
	g_free (desc);

	filename = camel_mime_part_get_filename (part);
	description = camel_mime_part_get_description (part);

	if (!filename || !*filename) {
		CamelDataWrapper *content = camel_medium_get_content (CAMEL_MEDIUM (part));

		if (content && CAMEL_IS_MIME_MESSAGE (content))
			filename = camel_mime_message_get_subject (CAMEL_MIME_MESSAGE (content));
	}

	if (filename != NULL && *filename != '\0') {
		gchar *basename = g_path_get_basename (filename);
		g_string_append_printf (stext, " (%s)", basename);
		g_free (basename);
	}

	if (description != NULL && *description != '\0' &&
		g_strcmp0 (filename, description) != 0)
		g_string_append_printf (stext, ", \"%s\"", description);

	return g_string_free (stext, FALSE);
}

static void
add_validity_found (EMFormat *emf,
                    CamelCipherValidity *valid)
{
	g_return_if_fail (emf != NULL);

	if (!valid)
		return;

	if (valid->sign.status != CAMEL_CIPHER_VALIDITY_SIGN_NONE)
		emf->validity_found |= EM_FORMAT_VALIDITY_FOUND_SIGNED;

	if (valid->encrypt.status != CAMEL_CIPHER_VALIDITY_ENCRYPT_NONE)
		emf->validity_found |= EM_FORMAT_VALIDITY_FOUND_ENCRYPTED;
}

/* ********************************************************************** */

static void
preserve_charset_in_content_type (CamelMimePart *ipart,
                                  CamelMimePart *opart)
{
	CamelDataWrapper *data_wrapper;
	CamelContentType *content_type;
	const gchar *charset;

	g_return_if_fail (ipart != NULL);
	g_return_if_fail (opart != NULL);

	data_wrapper = camel_medium_get_content (CAMEL_MEDIUM (ipart));
	content_type = camel_data_wrapper_get_mime_type_field (data_wrapper);

	if (content_type == NULL)
		return;

	charset = camel_content_type_param (content_type, "charset");

	if (charset == NULL || *charset == '\0')
		return;

	data_wrapper = camel_medium_get_content (CAMEL_MEDIUM (opart));
	content_type = camel_data_wrapper_get_mime_type_field (data_wrapper);

	camel_content_type_set_param (content_type, "charset", charset);
}

#ifdef ENABLE_SMIME
static void
emf_application_xpkcs7mime (EMFormat *emf,
                            CamelStream *stream,
                            CamelMimePart *part,
                            const EMFormatHandler *info,
                            GCancellable *cancellable,
                            gboolean is_fallback)
{
	CamelCipherContext *context;
	CamelMimePart *opart;
	CamelCipherValidity *valid;
	EMFormatCache *emfc;
	GError *local_error = NULL;

	/* should this perhaps run off a key of ".secured" ? */
	emfc = g_hash_table_lookup (emf->inline_table, emf->part_id->str);
	if (emfc && emfc->valid) {
		em_format_format_secure (
			emf, stream, emfc->secured,
			camel_cipher_validity_clone (emfc->valid),
			cancellable);
		return;
	}

	context = camel_smime_context_new (emf->session);

	emf->validity_found |=
		EM_FORMAT_VALIDITY_FOUND_ENCRYPTED |
		EM_FORMAT_VALIDITY_FOUND_SMIME;

	opart = camel_mime_part_new ();
	valid = camel_cipher_context_decrypt_sync (
		context, part, opart, cancellable, &local_error);
	preserve_charset_in_content_type (part, opart);
	if (valid == NULL) {
		em_format_format_error (
			emf, stream, "%s",
			local_error->message ? local_error->message :
			_("Could not parse S/MIME message: Unknown error"));
		g_clear_error (&local_error);

		em_format_part_as (emf, stream, part, NULL, cancellable);
	} else {
		if (emfc == NULL)
			emfc = emf_insert_cache (emf, emf->part_id->str);

		emfc->valid = camel_cipher_validity_clone (valid);
		g_object_ref ((emfc->secured = opart));

		add_validity_found (emf, valid);
		em_format_format_secure (
			emf, stream, opart, valid, cancellable);
	}

	g_object_unref (opart);
	g_object_unref (context);
}
#endif

/* RFC 1740 */
static void
emf_multipart_appledouble (EMFormat *emf,
                           CamelStream *stream,
                           CamelMimePart *part,
                           const EMFormatHandler *info,
                           GCancellable *cancellable,
                           gboolean is_fallback)
{
	CamelMultipart *mp;
	CamelMimePart *mime_part;
	gint len;

	mp = (CamelMultipart *) camel_medium_get_content ((CamelMedium *) part);

	if (!CAMEL_IS_MULTIPART (mp)) {
		em_format_format_source (emf, stream, part, cancellable);
		return;
	}

	mime_part = camel_multipart_get_part (mp, 1);
	if (mime_part) {
		/* try the data fork for something useful, doubtful but who knows */
		len = emf->part_id->len;
		g_string_append_printf(emf->part_id, ".appledouble.1");
		em_format_part (emf, stream, mime_part, cancellable);
		g_string_truncate (emf->part_id, len);
	} else
		em_format_format_source (emf, stream, part, cancellable);

}

/* RFC ??? */
static void
emf_multipart_mixed (EMFormat *emf,
                     CamelStream *stream,
                     CamelMimePart *part,
                     const EMFormatHandler *info,
                     GCancellable *cancellable,
                     gboolean is_fallback)
{
	CamelMultipart *mp;
	gint i, nparts, len;

	mp = (CamelMultipart *) camel_medium_get_content ((CamelMedium *) part);

	if (!CAMEL_IS_MULTIPART (mp)) {
		em_format_format_source (emf, stream, part, cancellable);
		return;
	}

	len = emf->part_id->len;
	nparts = camel_multipart_get_number (mp);
	for (i = 0; i < nparts; i++) {
		part = camel_multipart_get_part (mp, i);
		g_string_append_printf(emf->part_id, ".mixed.%d", i);
		em_format_part (emf, stream, part, cancellable);
		g_string_truncate (emf->part_id, len);
	}
}

/* RFC 1740 */
static void
emf_multipart_alternative (EMFormat *emf,
                           CamelStream *stream,
                           CamelMimePart *part,
                           const EMFormatHandler *info,
                           GCancellable *cancellable,
                           gboolean is_fallback)
{
	CamelMultipart *mp;
	gint i, nparts, bestid = 0;
	CamelMimePart *best = NULL;

	mp = (CamelMultipart *) camel_medium_get_content ((CamelMedium *) part);

	if (!CAMEL_IS_MULTIPART (mp)) {
		em_format_format_source (emf, stream, part, cancellable);
		return;
	}

	/* as per rfc, find the last part we know how to display */
	nparts = camel_multipart_get_number (mp);
	for (i = 0; i < nparts; i++) {
		CamelDataWrapper *data_wrapper;
		CamelContentType *type;
		CamelStream *null_stream;
		gchar *mime_type;
		gsize content_size;

		/* is it correct to use the passed in *part here? */
		part = camel_multipart_get_part (mp, i);

		if (part == NULL)
			continue;

		/* This may block even though the stream does not.
		 * XXX Pretty inefficient way to test if the MIME part
		 *     is empty.  Surely there's a quicker way? */
		null_stream = camel_stream_null_new ();
		data_wrapper = camel_medium_get_content (CAMEL_MEDIUM (part));
		camel_data_wrapper_decode_to_stream_sync (
			data_wrapper, null_stream, cancellable, NULL);
		content_size = CAMEL_STREAM_NULL (null_stream)->written;
		g_object_unref (null_stream);

		if (content_size == 0)
			continue;

		type = camel_mime_part_get_content_type (part);
		mime_type = camel_content_type_simple (type);

		camel_strdown (mime_type);

		/*if (want_plain && !strcmp (mime_type, "text/plain"))
		  return part;*/

		if (!em_format_is_attachment (emf, part) &&
		    (em_format_find_handler (emf, mime_type)
		    || (best == NULL && em_format_fallback_handler (emf, mime_type)))) {
			best = part;
			bestid = i;
		}

		g_free (mime_type);
	}

	if (best) {
		gint len = emf->part_id->len;

		g_string_append_printf(emf->part_id, ".alternative.%d", bestid);
		em_format_part (emf, stream, best, cancellable);
		g_string_truncate (emf->part_id, len);
	} else
		emf_multipart_mixed (
			emf, stream, part, info, cancellable, is_fallback);
}

static void
emf_multipart_encrypted (EMFormat *emf,
                         CamelStream *stream,
                         CamelMimePart *part,
                         const EMFormatHandler *info,
                         GCancellable *cancellable,
                         gboolean is_fallback)
{
	CamelCipherContext *context;
	const gchar *protocol;
	CamelMimePart *opart;
	CamelCipherValidity *valid;
	CamelMultipartEncrypted *mpe;
	EMFormatCache *emfc;
	GError *local_error = NULL;

	/* should this perhaps run off a key of ".secured" ? */
	emfc = g_hash_table_lookup (emf->inline_table, emf->part_id->str);
	if (emfc && emfc->valid) {
		em_format_format_secure (
			emf, stream, emfc->secured,
			camel_cipher_validity_clone (emfc->valid),
			cancellable);
		return;
	}

	mpe = (CamelMultipartEncrypted*) camel_medium_get_content ((CamelMedium *) part);
	if (!CAMEL_IS_MULTIPART_ENCRYPTED (mpe)) {
		em_format_format_error (
			emf, stream, _("Could not parse MIME message. "
			"Displaying as source."));
		em_format_format_source (emf, stream, part, cancellable);
		return;
	}

	/* Currently we only handle RFC2015-style PGP encryption. */
	protocol = camel_content_type_param (
		((CamelDataWrapper *)mpe)->mime_type, "protocol");
	if (!protocol || g_ascii_strcasecmp (protocol, "application/pgp-encrypted") != 0) {
		em_format_format_error (
			emf, stream, _("Unsupported encryption "
			"type for multipart/encrypted"));
		em_format_part_as (
			emf, stream, part,
			"multipart/mixed", cancellable);
		return;
	}

	emf->validity_found |=
		EM_FORMAT_VALIDITY_FOUND_ENCRYPTED |
		EM_FORMAT_VALIDITY_FOUND_PGP;

	context = camel_gpg_context_new (emf->session);
	opart = camel_mime_part_new ();
	valid = camel_cipher_context_decrypt_sync (
		context, part, opart, cancellable, &local_error);
	preserve_charset_in_content_type (part, opart);
	if (valid == NULL) {
		em_format_format_error (
			emf, stream, local_error->message ?
			_("Could not parse PGP/MIME message") :
			_("Could not parse PGP/MIME message: Unknown error"));
		if (local_error->message != NULL)
			em_format_format_error (
				emf, stream, "%s", local_error->message);
		g_clear_error (&local_error);

		em_format_part_as (
			emf, stream, part,
			"multipart/mixed", cancellable);
	} else {
		if (emfc == NULL)
			emfc = emf_insert_cache (emf, emf->part_id->str);

		emfc->valid = camel_cipher_validity_clone (valid);
		g_object_ref ((emfc->secured = opart));

		add_validity_found (emf, valid);
		em_format_format_secure (
			emf, stream, opart, valid, cancellable);
	}

	/* TODO: Make sure when we finalize this part, it is zero'd out */
	g_object_unref (opart);
	g_object_unref (context);
}

static void
emf_write_related (EMFormat *emf,
                   CamelStream *stream,
                   EMFormatPURI *puri,
                   GCancellable *cancellable)
{
	em_format_format_content (emf, stream, puri->part, cancellable);
	camel_stream_close (stream, cancellable, NULL);
}

/* RFC 2387 */
static void
emf_multipart_related (EMFormat *emf,
                       CamelStream *stream,
                       CamelMimePart *part,
                       const EMFormatHandler *info,
                       GCancellable *cancellable,
                       gboolean is_fallback)
{
	CamelMultipart *mp;
	CamelMimePart *body_part, *display_part = NULL;
	CamelContentType *content_type;
	const gchar *start;
	gint i, nparts, partidlen, displayid = 0;
	gchar *oldpartid;
	GList *link;

	mp = (CamelMultipart *) camel_medium_get_content ((CamelMedium *) part);

	if (!CAMEL_IS_MULTIPART (mp)) {
		em_format_format_source (emf, stream, part, cancellable);
		return;
	}

	/* FIXME: put this stuff in a shared function */
	nparts = camel_multipart_get_number (mp);
	content_type = camel_mime_part_get_content_type (part);
	start = camel_content_type_param (content_type, "start");
	if (start && strlen (start)>2) {
		gint len;
		const gchar *cid;

		/* strip <>'s */
		len = strlen (start) - 2;
		start++;

		for (i=0; i<nparts; i++) {
			body_part = camel_multipart_get_part (mp, i);
			cid = camel_mime_part_get_content_id (body_part);

			if (cid && !strncmp (cid, start, len) && strlen (cid) == len) {
				display_part = body_part;
				displayid = i;
				break;
			}
		}
	} else {
		display_part = camel_multipart_get_part (mp, 0);
	}

	if (display_part == NULL) {
		emf_multipart_mixed (
			emf, stream, part, info, cancellable, is_fallback);
		return;
	}

	em_format_push_level (emf);

	oldpartid = g_strdup (emf->part_id->str);
	partidlen = emf->part_id->len;

	/* queue up the parts for possible inclusion */
	for (i = 0; i < nparts; i++) {
		body_part = camel_multipart_get_part (mp, i);
		if (body_part != display_part) {
			/* set the partid since add_puri uses it */
			g_string_append_printf(emf->part_id, ".related.%d", i);
			em_format_add_puri (
				emf, sizeof (EMFormatPURI), NULL,
				body_part, emf_write_related);
			g_string_truncate (emf->part_id, partidlen);
		}
	}

	g_string_append_printf(emf->part_id, ".related.%d", displayid);
	em_format_part (emf, stream, display_part, cancellable);
	g_string_truncate (emf->part_id, partidlen);
	camel_stream_flush (stream, NULL, NULL);

	link = g_queue_peek_head_link (emf->pending_uri_level->data);

	while (link && link->next != NULL) {
		EMFormatPURI *puri = link->data;

		if (puri->use_count == 0) {
			if (puri->func == emf_write_related) {
				g_string_printf(emf->part_id, "%s", puri->part_id);
				em_format_part (
					emf, stream, puri->part, cancellable);
			}
		}

		link = g_list_next (link);
	}

	g_string_printf(emf->part_id, "%s", oldpartid);
	g_free (oldpartid);

	em_format_pull_level (emf);
}

static void
emf_multipart_signed (EMFormat *emf,
                      CamelStream *stream,
                      CamelMimePart *part,
                      const EMFormatHandler *info,
                      GCancellable *cancellable,
                      gboolean is_fallback)
{
	CamelMimePart *cpart;
	CamelMultipartSigned *mps;
	CamelCipherContext *cipher = NULL;
	EMFormatCache *emfc;

	/* should this perhaps run off a key of ".secured" ? */
	emfc = g_hash_table_lookup (emf->inline_table, emf->part_id->str);
	if (emfc && emfc->valid) {
		em_format_format_secure (
			emf, stream, emfc->secured,
			camel_cipher_validity_clone (emfc->valid),
			cancellable);
		return;
	}

	mps = (CamelMultipartSigned *) camel_medium_get_content ((CamelMedium *) part);
	if (!CAMEL_IS_MULTIPART_SIGNED (mps)
	    || (cpart = camel_multipart_get_part ((CamelMultipart *) mps,
		CAMEL_MULTIPART_SIGNED_CONTENT)) == NULL) {
		em_format_format_error (
			emf, stream, _("Could not parse MIME message. "
			"Displaying as source."));
		em_format_format_source (emf, stream, part, cancellable);
		return;
	}

	/* FIXME: Should be done via a plugin interface */
	/* FIXME: duplicated in em-format-html-display.c */
	if (mps->protocol) {
#ifdef ENABLE_SMIME
		if (g_ascii_strcasecmp("application/x-pkcs7-signature", mps->protocol) == 0
		    || g_ascii_strcasecmp("application/pkcs7-signature", mps->protocol) == 0) {
			cipher = camel_smime_context_new (emf->session);
			emf->validity_found |= EM_FORMAT_VALIDITY_FOUND_SMIME;
		} else
#endif
			if (g_ascii_strcasecmp("application/pgp-signature", mps->protocol) == 0) {
				cipher = camel_gpg_context_new (emf->session);
				emf->validity_found |= EM_FORMAT_VALIDITY_FOUND_PGP;
			}
	}

	emf->validity_found |= EM_FORMAT_VALIDITY_FOUND_SIGNED;

	if (cipher == NULL) {
		em_format_format_error(emf, stream, _("Unsupported signature format"));
		em_format_part_as (
			emf, stream, part,
			"multipart/mixed", cancellable);
	} else {
		CamelCipherValidity *valid;
		GError *local_error = NULL;

		valid = camel_cipher_context_verify_sync (
			cipher, part, cancellable, &local_error);
		if (valid == NULL) {
			em_format_format_error (
				emf, stream, local_error->message ?
				_("Error verifying signature") :
				_("Unknown error verifying signature"));
			if (local_error->message != NULL)
				em_format_format_error (
					emf, stream, "%s",
					local_error->message);
			g_clear_error (&local_error);

			em_format_part_as (
				emf, stream, part,
				"multipart/mixed", cancellable);
		} else {
			if (emfc == NULL)
				emfc = emf_insert_cache (emf, emf->part_id->str);

			emfc->valid = camel_cipher_validity_clone (valid);
			g_object_ref ((emfc->secured = cpart));

			add_validity_found (emf, valid);
			em_format_format_secure (
				emf, stream, cpart, valid, cancellable);
		}

		g_object_unref (cipher);
	}
}

/* RFC 4155 */
static void
emf_application_mbox (EMFormat *emf,
                      CamelStream *stream,
                      CamelMimePart *mime_part,
                      const EMFormatHandler *info,
                      GCancellable *cancellable,
                      gboolean is_fallback)
{
	const EMFormatHandler *handle;
	CamelMimeParser *parser;
	CamelStream *mem_stream;
	camel_mime_parser_state_t state;

	/* Extract messages from the application/mbox part and
	 * render them as a flat list of messages. */

	/* XXX If the mbox has multiple messages, maybe render them
	 *     as a multipart/digest so each message can be expanded
	 *     or collapsed individually.
	 *
	 *     See attachment_handler_mail_x_uid_list() for example. */

	/* XXX This is based on em_utils_read_messages_from_stream().
	 *     Perhaps refactor that function to return an array of
	 *     messages instead of assuming we want to append them
	 *     to a folder? */

	handle = em_format_find_handler (emf, "x-evolution/message/rfc822");
	g_return_if_fail (handle != NULL);

	parser = camel_mime_parser_new ();
	camel_mime_parser_scan_from (parser, TRUE);

	mem_stream = camel_stream_mem_new ();
	camel_data_wrapper_decode_to_stream_sync (
		camel_medium_get_content (CAMEL_MEDIUM (mime_part)),
		mem_stream, NULL, NULL);
	g_seekable_seek (G_SEEKABLE (mem_stream), 0, G_SEEK_SET, NULL, NULL);
	camel_mime_parser_init_with_stream (parser, mem_stream, NULL);
	g_object_unref (mem_stream);

	/* Extract messages from the mbox. */
	state = camel_mime_parser_step (parser, NULL, NULL);
	while (state == CAMEL_MIME_PARSER_STATE_FROM) {
		CamelMimeMessage *message;

		message = camel_mime_message_new ();
		mime_part = CAMEL_MIME_PART (message);

		if (!camel_mime_part_construct_from_parser_sync (mime_part, parser, NULL, NULL)) {
			g_object_unref (message);
			break;
		}

		/* Render the message. */
		handle->handler (
			emf, stream, mime_part,
			handle, cancellable, FALSE);

		g_object_unref (message);

		/* Skip past CAMEL_MIME_PARSER_STATE_FROM_END. */
		camel_mime_parser_step (parser, NULL, NULL);

		state = camel_mime_parser_step (parser, NULL, NULL);
	}

	g_object_unref (parser);
}

static void
emf_message_rfc822 (EMFormat *emf,
                    CamelStream *stream,
                    CamelMimePart *part,
                    const EMFormatHandler *info,
                    GCancellable *cancellable,
                    gboolean is_fallback)
{
	CamelDataWrapper *dw = camel_medium_get_content ((CamelMedium *) part);
	const EMFormatHandler *handle;
	gint len;
	gchar *parent_message_part_id;

	if (!CAMEL_IS_MIME_MESSAGE (dw)) {
		em_format_format_source (emf, stream, part, cancellable);
		return;
	}

	parent_message_part_id = emf->current_message_part_id;
	emf->current_message_part_id = g_strdup (emf->part_id->str);

	len = emf->part_id->len;
	g_string_append_printf(emf->part_id, ".rfc822");

	handle = em_format_find_handler(emf, "x-evolution/message/rfc822");
	if (handle)
		handle->handler (
			emf, stream, CAMEL_MIME_PART (dw),
			handle, cancellable, FALSE);

	g_string_truncate (emf->part_id, len);

	g_free (emf->current_message_part_id);
	emf->current_message_part_id = parent_message_part_id;
}

static void
emf_message_deliverystatus (EMFormat *emf,
                            CamelStream *stream,
                            CamelMimePart *part,
                            const EMFormatHandler *info,
                            GCancellable *cancellable,
                            gboolean is_fallback)
{
	em_format_format_text (
		emf, stream, (CamelDataWrapper *) part, cancellable);
}

static void
emf_inlinepgp_signed (EMFormat *emf,
                      CamelStream *stream,
                      CamelMimePart *ipart,
                      const EMFormatHandler *info,
                      GCancellable *cancellable,
                      gboolean is_fallback)
{
	CamelStream *filtered_stream;
	CamelMimeFilterPgp *pgp_filter;
	CamelContentType *content_type;
	CamelCipherContext *cipher;
	CamelCipherValidity *valid;
	CamelDataWrapper *dw;
	CamelMimePart *opart;
	CamelStream *ostream;
	gchar *type;
	GError *local_error = NULL;

	if (!ipart) {
		em_format_format_error(emf, stream, _("Unknown error verifying signature"));
		return;
	}

	emf->validity_found |=
		EM_FORMAT_VALIDITY_FOUND_SIGNED |
		EM_FORMAT_VALIDITY_FOUND_PGP;

	cipher = camel_gpg_context_new (emf->session);
	/* Verify the signature of the message */
	valid = camel_cipher_context_verify_sync (
		cipher, ipart, cancellable, &local_error);
	if (!valid) {
		em_format_format_error (
			emf, stream, local_error->message ?
			_("Error verifying signature") :
			_("Unknown error verifying signature"));
		if (local_error->message)
			em_format_format_error (
				emf, stream, "%s", local_error->message);
		em_format_format_source (emf, stream, ipart, cancellable);
		/* I think this will loop: em_format_part_as(emf, stream, part, "text/plain"); */
		g_clear_error (&local_error);
		g_object_unref (cipher);
		return;
	}

	/* Setup output stream */
	ostream = camel_stream_mem_new ();
	filtered_stream = camel_stream_filter_new (ostream);

	/* Add PGP header / footer filter */
	pgp_filter = (CamelMimeFilterPgp *) camel_mime_filter_pgp_new ();
	camel_stream_filter_add (
		CAMEL_STREAM_FILTER (filtered_stream),
		CAMEL_MIME_FILTER (pgp_filter));
	g_object_unref (pgp_filter);

	/* Pass through the filters that have been setup */
	dw = camel_medium_get_content ((CamelMedium *) ipart);
	camel_data_wrapper_decode_to_stream_sync (
		dw, (CamelStream *) filtered_stream, NULL, NULL);
	camel_stream_flush ((CamelStream *) filtered_stream, NULL, NULL);
	g_object_unref (filtered_stream);

	/* Create a new text/plain MIME part containing the signed
	 * content preserving the original part's Content-Type params. */
	content_type = camel_mime_part_get_content_type (ipart);
	type = camel_content_type_format (content_type);
	content_type = camel_content_type_decode (type);
	g_free (type);

	g_free (content_type->type);
	content_type->type = g_strdup ("text");
	g_free (content_type->subtype);
	content_type->subtype = g_strdup ("plain");
	type = camel_content_type_format (content_type);
	camel_content_type_unref (content_type);

	dw = camel_data_wrapper_new ();
	camel_data_wrapper_construct_from_stream_sync (dw, ostream, NULL, NULL);
	camel_data_wrapper_set_mime_type (dw, type);
	g_free (type);

	opart = camel_mime_part_new ();
	camel_medium_set_content ((CamelMedium *) opart, dw);
	camel_data_wrapper_set_mime_type_field ((CamelDataWrapper *) opart, dw->mime_type);

	add_validity_found (emf, valid);
	/* Pass it off to the real formatter */
	em_format_format_secure (emf, stream, opart, valid, cancellable);

	/* Clean Up */
	g_object_unref (dw);
	g_object_unref (opart);
	g_object_unref (ostream);
	g_object_unref (cipher);
}

static void
emf_inlinepgp_encrypted (EMFormat *emf,
                         CamelStream *stream,
                         CamelMimePart *ipart,
                         const EMFormatHandler *info,
                         GCancellable *cancellable,
                         gboolean is_fallback)
{
	CamelCipherContext *cipher;
	CamelCipherValidity *valid;
	CamelMimePart *opart;
	CamelDataWrapper *dw;
	gchar *mime_type;
	GError *local_error = NULL;

	emf->validity_found |=
		EM_FORMAT_VALIDITY_FOUND_ENCRYPTED |
		EM_FORMAT_VALIDITY_FOUND_PGP;

	cipher = camel_gpg_context_new (emf->session);
	opart = camel_mime_part_new ();

	/* Decrypt the message */
	valid = camel_cipher_context_decrypt_sync (
		cipher, ipart, opart, cancellable, &local_error);

	if (!valid) {
		em_format_format_error (
			emf, stream, _("Could not parse PGP message: "));
		if (local_error->message != NULL)
			em_format_format_error (
				emf, stream, "%s", local_error->message);
		else
			em_format_format_error (
				emf, stream, _("Unknown error"));
		em_format_format_source (emf, stream, ipart, cancellable);
		/* I think this will loop: em_format_part_as(emf, stream, part, "text/plain"); */

		g_clear_error (&local_error);
		g_object_unref (cipher);
		g_object_unref (opart);
		return;
	}

	dw = camel_medium_get_content ((CamelMedium *) opart);
	mime_type = camel_data_wrapper_get_mime_type (dw);

	/* this ensures to show the 'opart' as inlined, if possible */
	if (mime_type && g_ascii_strcasecmp (mime_type, "application/octet-stream") == 0) {
		const gchar *snoop = em_format_snoop_type (opart);

		if (snoop)
			camel_data_wrapper_set_mime_type (dw, snoop);
	}

	preserve_charset_in_content_type (ipart, opart);
	g_free (mime_type);

	add_validity_found (emf, valid);
	/* Pass it off to the real formatter */
	em_format_format_secure (emf, stream, opart, valid, cancellable);

	/* Clean Up */
	g_object_unref (opart);
	g_object_unref (cipher);
}

static EMFormatHandler type_builtin_table[] = {
#ifdef ENABLE_SMIME
	{ (gchar *) "application/x-pkcs7-mime",
	  emf_application_xpkcs7mime,
	  EM_FORMAT_HANDLER_INLINE_DISPOSITION },
#endif
	{ (gchar *) "application/mbox", emf_application_mbox, EM_FORMAT_HANDLER_INLINE },
	{ (gchar *) "multipart/alternative", emf_multipart_alternative },
	{ (gchar *) "multipart/appledouble", emf_multipart_appledouble },
	{ (gchar *) "multipart/encrypted", emf_multipart_encrypted },
	{ (gchar *) "multipart/mixed", emf_multipart_mixed },
	{ (gchar *) "multipart/signed", emf_multipart_signed },
	{ (gchar *) "multipart/related", emf_multipart_related },
	{ (gchar *) "multipart/*", emf_multipart_mixed },
	{ (gchar *) "message/rfc822", emf_message_rfc822, EM_FORMAT_HANDLER_INLINE },
	{ (gchar *) "message/news", emf_message_rfc822, EM_FORMAT_HANDLER_INLINE },
	{ (gchar *) "message/delivery-status", emf_message_deliverystatus },
	{ (gchar *) "message/*", emf_message_rfc822, EM_FORMAT_HANDLER_INLINE },

	/* Insert brokenly-named parts here */
#ifdef ENABLE_SMIME
	{ (gchar *) "application/pkcs7-mime",
	  emf_application_xpkcs7mime,
	  EM_FORMAT_HANDLER_INLINE_DISPOSITION },
#endif

	/* internal types */
	{ (gchar *) "application/x-inlinepgp-signed", emf_inlinepgp_signed },
	{ (gchar *) "application/x-inlinepgp-encrypted", emf_inlinepgp_encrypted },
};

static void
emf_builtin_init (EMFormatClass *class)
{
	gint ii;

	for (ii = 0; ii < G_N_ELEMENTS (type_builtin_table); ii++)
		g_hash_table_insert (
			class->type_handlers,
			type_builtin_table[ii].mime_type,
			&type_builtin_table[ii]);
}

/**
 * em_format_snoop_type:
 * @part:
 *
 * Tries to snoop the mime type of a part.
 *
 * Return value: NULL if unknown (more likely application/octet-stream).
 **/
const gchar *
em_format_snoop_type (CamelMimePart *part)
{
	/* cache is here only to be able still return const gchar * */
	static GHashTable *types_cache = NULL;

	const gchar *filename;
	gchar *name_type = NULL, *magic_type = NULL, *res, *tmp;
	CamelDataWrapper *dw;

	filename = camel_mime_part_get_filename (part);
	if (filename != NULL)
		name_type = e_util_guess_mime_type (filename, FALSE);

	dw = camel_medium_get_content ((CamelMedium *) part);
	if (!camel_data_wrapper_is_offline (dw)) {
		GByteArray *byte_array;
		CamelStream *stream;

		byte_array = g_byte_array_new ();
		stream = camel_stream_mem_new_with_byte_array (byte_array);

		if (camel_data_wrapper_decode_to_stream_sync (dw, stream, NULL, NULL) > 0) {
			gchar *content_type;

			content_type = g_content_type_guess (
				filename, byte_array->data,
				byte_array->len, NULL);

			if (content_type != NULL)
				magic_type = g_content_type_get_mime_type (content_type);

			g_free (content_type);
		}

		g_object_unref (stream);
	}

	d(printf("snooped part, magic_type '%s' name_type '%s'\n", magic_type, name_type));

	/* If gvfs doesn't recognize the data by magic, but it
	 * contains English words, it will call it text/plain. If the
	 * filename-based check came up with something different, use
	 * that instead and if it returns "application/octet-stream"
	 * try to do better with the filename check.
	 */

	if (magic_type) {
		if (name_type
		    && (!strcmp(magic_type, "text/plain")
			|| !strcmp(magic_type, "application/octet-stream")))
			res = name_type;
		else
			res = magic_type;
	} else
		res = name_type;

	if (res != name_type)
		g_free (name_type);

	if (res != magic_type)
		g_free (magic_type);

	if (!types_cache)
		types_cache = g_hash_table_new_full (
			g_str_hash, g_str_equal,
			(GDestroyNotify) g_free,
			(GDestroyNotify) NULL);

	if (res) {
		tmp = g_hash_table_lookup (types_cache, res);
		if (tmp) {
			g_free (res);
			res = tmp;
		} else {
			g_hash_table_insert (types_cache, res, res);
		}
	}

	return res;

	/* We used to load parts to check their type, we dont anymore,
	   see bug #11778 for some discussion */
}
