/*
 * e-mail-session-utils.c
 *
 * 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/>
 *
 */

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

#include "e-mail-session-utils.h"

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

#include <libemail-engine/e-mail-folder-utils.h>
#include <libemail-engine/e-mail-utils.h>
#include <libemail-engine/mail-tools.h>

/* X-Mailer header value */
#define X_MAILER ("Evolution " VERSION SUB_VERSION " " VERSION_COMMENT)

/* FIXME: Temporary - remove this after we move filter/ to eds */
#define E_FILTER_SOURCE_OUTGOING "outgoing"

typedef struct _AsyncContext AsyncContext;

struct _AsyncContext {
	CamelFolder *folder;

	CamelMimeMessage *message;
	CamelMessageInfo *info;

	CamelAddress *from;
	CamelAddress *recipients;

	CamelFilterDriver *driver;

	CamelService *transport;

	GCancellable *cancellable;
	gint io_priority;

	/* X-Evolution headers */
	struct _camel_header_raw *xev;

	GPtrArray *post_to_uris;

	EMailLocalFolder local_id;

	gchar *folder_uri;
	gchar *message_uid;
};

static void
async_context_free (AsyncContext *context)
{
	if (context->folder != NULL)
		g_object_unref (context->folder);

	if (context->message != NULL)
		g_object_unref (context->message);

	if (context->info != NULL)
		camel_message_info_free (context->info);

	if (context->from != NULL)
		g_object_unref (context->from);

	if (context->recipients != NULL)
		g_object_unref (context->recipients);

	if (context->driver != NULL)
		g_object_unref (context->driver);

	if (context->transport != NULL)
		g_object_unref (context->transport);

	if (context->cancellable != NULL) {
		camel_operation_pop_message (context->cancellable);
		g_object_unref (context->cancellable);
	}

	if (context->xev != NULL)
		camel_header_raw_clear (&context->xev);

	if (context->post_to_uris != NULL) {
		g_ptr_array_foreach (
			context->post_to_uris, (GFunc) g_free, NULL);
		g_ptr_array_free (context->post_to_uris, TRUE);
	}

	g_free (context->folder_uri);
	g_free (context->message_uid);

	g_slice_free (AsyncContext, context);
}

GQuark
e_mail_error_quark (void)
{
	static GQuark quark = 0;

	if (G_UNLIKELY (quark == 0)) {
		const gchar *string = "e-mail-error-quark";
		quark = g_quark_from_static_string (string);
	}

	return quark;
}

static void
mail_session_append_to_local_folder_thread (GSimpleAsyncResult *simple,
                                            GObject *object,
                                            GCancellable *cancellable)
{
	AsyncContext *context;
	GError *error = NULL;

	context = g_simple_async_result_get_op_res_gpointer (simple);

	e_mail_session_append_to_local_folder_sync (
		E_MAIL_SESSION (object),
		context->local_id, context->message,
		context->info, &context->message_uid,
		cancellable, &error);

	if (error != NULL)
		g_simple_async_result_take_error (simple, error);
}

gboolean
e_mail_session_append_to_local_folder_sync (EMailSession *session,
                                            EMailLocalFolder local_id,
                                            CamelMimeMessage *message,
                                            CamelMessageInfo *info,
                                            gchar **appended_uid,
                                            GCancellable *cancellable,
                                            GError **error)
{
	CamelFolder *folder;
	const gchar *folder_uri;
	gboolean success = FALSE;

	g_return_val_if_fail (E_IS_MAIL_SESSION (session), FALSE);
	g_return_val_if_fail (CAMEL_IS_MIME_MESSAGE (message), FALSE);

	folder_uri = e_mail_session_get_local_folder_uri (session, local_id);
	g_return_val_if_fail (folder_uri != NULL, FALSE);

	folder = e_mail_session_uri_to_folder_sync (
		session, folder_uri, CAMEL_STORE_FOLDER_CREATE,
		cancellable, error);

	if (folder != NULL) {
		success = e_mail_folder_append_message_sync (
			folder, message, info, appended_uid,
			cancellable, error);
		g_object_unref (folder);
	}

	return success;
}

void
e_mail_session_append_to_local_folder (EMailSession *session,
                                       EMailLocalFolder local_id,
                                       CamelMimeMessage *message,
                                       CamelMessageInfo *info,
                                       gint io_priority,
                                       GCancellable *cancellable,
                                       GAsyncReadyCallback callback,
                                       gpointer user_data)
{
	GSimpleAsyncResult *simple;
	AsyncContext *context;

	g_return_if_fail (E_IS_MAIL_SESSION (session));
	g_return_if_fail (CAMEL_IS_MIME_MESSAGE (message));

	context = g_slice_new0 (AsyncContext);
	context->local_id = local_id;
	context->message = g_object_ref (message);

	if (info != NULL)
		context->info = camel_message_info_ref (info);

	simple = g_simple_async_result_new (
		G_OBJECT (session), callback, user_data,
		e_mail_session_append_to_local_folder);

	g_simple_async_result_set_check_cancellable (simple, cancellable);

	g_simple_async_result_set_op_res_gpointer (
		simple, context, (GDestroyNotify) async_context_free);

	g_simple_async_result_run_in_thread (
		simple, mail_session_append_to_local_folder_thread,
		io_priority, cancellable);

	g_object_unref (simple);
}

gboolean
e_mail_session_append_to_local_folder_finish (EMailSession *session,
                                              GAsyncResult *result,
                                              gchar **appended_uid,
                                              GError **error)
{
	GSimpleAsyncResult *simple;
	AsyncContext *context;

	g_return_val_if_fail (
		g_simple_async_result_is_valid (
		result, G_OBJECT (session),
		e_mail_session_append_to_local_folder), FALSE);

	simple = G_SIMPLE_ASYNC_RESULT (result);
	context = g_simple_async_result_get_op_res_gpointer (simple);

	if (appended_uid != NULL) {
		*appended_uid = context->message_uid;
		context->message_uid = NULL;
	}

	/* Assume success unless a GError is set. */
	return !g_simple_async_result_propagate_error (simple, error);
}

static void
mail_session_handle_draft_headers_thread (GSimpleAsyncResult *simple,
                                          EMailSession *session,
                                          GCancellable *cancellable)
{
	AsyncContext *context;
	GError *error = NULL;

	context = g_simple_async_result_get_op_res_gpointer (simple);

	e_mail_session_handle_draft_headers_sync (
		session, context->message, cancellable, &error);

	if (error != NULL)
		g_simple_async_result_take_error (simple, error);
}

gboolean
e_mail_session_handle_draft_headers_sync (EMailSession *session,
                                          CamelMimeMessage *message,
                                          GCancellable *cancellable,
                                          GError **error)
{
	CamelFolder *folder;
	CamelMedium *medium;
	const gchar *folder_uri;
	const gchar *message_uid;
	const gchar *header_name;
	gboolean success;

	g_return_val_if_fail (E_IS_MAIL_SESSION (session), FALSE);
	g_return_val_if_fail (CAMEL_IS_MIME_MESSAGE (message), FALSE);

	medium = CAMEL_MEDIUM (message);

	header_name = "X-Evolution-Draft-Folder";
	folder_uri = camel_medium_get_header (medium, header_name);

	header_name = "X-Evolution-Draft-Message";
	message_uid = camel_medium_get_header (medium, header_name);

	/* Don't report errors about missing X-Evolution-Draft
	 * headers.  These headers are optional, so their absence
	 * is handled by doing nothing. */
	if (folder_uri == NULL || message_uid == NULL)
		return TRUE;

	folder = e_mail_session_uri_to_folder_sync (
		session, folder_uri, 0, cancellable, error);

	if (folder == NULL)
		return FALSE;

	camel_folder_set_message_flags (
		folder, message_uid,
		CAMEL_MESSAGE_DELETED | CAMEL_MESSAGE_SEEN,
		CAMEL_MESSAGE_DELETED | CAMEL_MESSAGE_SEEN);

	success = camel_folder_synchronize_message_sync (
		folder, message_uid, cancellable, error);

	g_object_unref (folder);

	return success;
}

void
e_mail_session_handle_draft_headers (EMailSession *session,
                                     CamelMimeMessage *message,
                                     gint io_priority,
                                     GCancellable *cancellable,
                                     GAsyncReadyCallback callback,
                                     gpointer user_data)
{
	GSimpleAsyncResult *simple;
	AsyncContext *context;

	g_return_if_fail (E_IS_MAIL_SESSION (session));
	g_return_if_fail (CAMEL_IS_MIME_MESSAGE (message));

	context = g_slice_new0 (AsyncContext);
	context->message = g_object_ref (message);

	simple = g_simple_async_result_new (
		G_OBJECT (session), callback, user_data,
		e_mail_session_handle_draft_headers);

	g_simple_async_result_set_check_cancellable (simple, cancellable);

	g_simple_async_result_set_op_res_gpointer (
		simple, context, (GDestroyNotify) async_context_free);

	g_simple_async_result_run_in_thread (
		simple, (GSimpleAsyncThreadFunc)
		mail_session_handle_draft_headers_thread,
		io_priority, cancellable);

	g_object_unref (simple);
}

gboolean
e_mail_session_handle_draft_headers_finish (EMailSession *session,
                                            GAsyncResult *result,
                                            GError **error)
{
	GSimpleAsyncResult *simple;

	g_return_val_if_fail (
		g_simple_async_result_is_valid (
		result, G_OBJECT (session),
		e_mail_session_handle_draft_headers), FALSE);

	simple = G_SIMPLE_ASYNC_RESULT (result);

	/* Assume success unless a GError is set. */
	return !g_simple_async_result_propagate_error (simple, error);
}

static void
mail_session_handle_source_headers_thread (GSimpleAsyncResult *simple,
                                           EMailSession *session,
                                           GCancellable *cancellable)
{
	AsyncContext *context;
	GError *error = NULL;

	context = g_simple_async_result_get_op_res_gpointer (simple);

	e_mail_session_handle_source_headers_sync (
		session, context->message, cancellable, &error);

	if (error != NULL)
		g_simple_async_result_take_error (simple, error);
}

gboolean
e_mail_session_handle_source_headers_sync (EMailSession *session,
                                           CamelMimeMessage *message,
                                           GCancellable *cancellable,
                                           GError **error)
{
	CamelFolder *folder;
	CamelMedium *medium;
	CamelMessageFlags flags = 0;
	const gchar *folder_uri;
	const gchar *message_uid;
	const gchar *flag_string;
	const gchar *header_name;
	gboolean success;
	guint length, ii;
	gchar **tokens;
	gchar *string;

	g_return_val_if_fail (E_IS_MAIL_SESSION (session), FALSE);
	g_return_val_if_fail (CAMEL_IS_MIME_MESSAGE (message), FALSE);

	medium = CAMEL_MEDIUM (message);

	header_name = "X-Evolution-Source-Folder";
	folder_uri = camel_medium_get_header (medium, header_name);

	header_name = "X-Evolution-Source-Message";
	message_uid = camel_medium_get_header (medium, header_name);

	header_name = "X-Evolution-Source-Flags";
	flag_string = camel_medium_get_header (medium, header_name);

	/* Don't report errors about missing X-Evolution-Source
	 * headers.  These headers are optional, so their absence
	 * is handled by doing nothing. */
	if (folder_uri == NULL || message_uid == NULL || flag_string == NULL)
		return TRUE;

	/* Convert the flag string to CamelMessageFlags. */

	string = g_strstrip (g_strdup (flag_string));
	tokens = g_strsplit (string, " ", 0);
	g_free (string);

	/* If tokens is NULL, a length of 0 will skip the loop. */
	length = (tokens != NULL) ? g_strv_length (tokens) : 0;

	for (ii = 0; ii < length; ii++) {
		/* Note: We're only checking for flags known to
		 * be used in X-Evolution-Source-Flags headers.
		 * Add more as needed. */
		if (g_strcmp0 (tokens[ii], "ANSWERED") == 0)
			flags |= CAMEL_MESSAGE_ANSWERED;
		else if (g_strcmp0 (tokens[ii], "ANSWERED_ALL") == 0)
			flags |= CAMEL_MESSAGE_ANSWERED_ALL;
		else if (g_strcmp0 (tokens[ii], "FORWARDED") == 0)
			flags |= CAMEL_MESSAGE_FORWARDED;
		else if (g_strcmp0 (tokens[ii], "SEEN") == 0)
			flags |= CAMEL_MESSAGE_SEEN;
		else
			g_warning (
				"Unknown flag '%s' in %s",
				tokens[ii], header_name);
	}

	g_strfreev (tokens);

	folder = e_mail_session_uri_to_folder_sync (
		session, folder_uri, 0, cancellable, error);

	if (folder == NULL)
		return FALSE;

	camel_folder_set_message_flags (
		folder, message_uid, flags, flags);

	success = camel_folder_synchronize_message_sync (
		folder, message_uid, cancellable, error);

	g_object_unref (folder);

	return success;
}

void
e_mail_session_handle_source_headers (EMailSession *session,
                                      CamelMimeMessage *message,
                                      gint io_priority,
                                      GCancellable *cancellable,
                                      GAsyncReadyCallback callback,
                                      gpointer user_data)
{
	GSimpleAsyncResult *simple;
	AsyncContext *context;

	g_return_if_fail (E_IS_MAIL_SESSION (session));
	g_return_if_fail (CAMEL_IS_MIME_MESSAGE (message));

	context = g_slice_new0 (AsyncContext);
	context->message = g_object_ref (message);

	simple = g_simple_async_result_new (
		G_OBJECT (session), callback, user_data,
		e_mail_session_handle_source_headers);

	g_simple_async_result_set_check_cancellable (simple, cancellable);

	g_simple_async_result_set_op_res_gpointer (
		simple, context, (GDestroyNotify) async_context_free);

	g_simple_async_result_run_in_thread (
		simple, (GSimpleAsyncThreadFunc)
		mail_session_handle_source_headers_thread,
		io_priority, cancellable);

	g_object_unref (simple);
}

gboolean
e_mail_session_handle_source_headers_finish (EMailSession *session,
                                             GAsyncResult *result,
                                             GError **error)
{
	GSimpleAsyncResult *simple;

	g_return_val_if_fail (
		g_simple_async_result_is_valid (
		result, G_OBJECT (session),
		e_mail_session_handle_draft_headers), FALSE);

	simple = G_SIMPLE_ASYNC_RESULT (result);

	/* Assume success unless a GError is set. */
	return !g_simple_async_result_propagate_error (simple, error);
}

static void
mail_session_send_to_thread (GSimpleAsyncResult *simple,
                             EMailSession *session,
                             GCancellable *cancellable)
{
	AsyncContext *context;
	CamelProvider *provider;
	CamelFolder *folder = NULL;
	CamelFolder *local_sent_folder;
	CamelServiceConnectionStatus status;
	GString *error_messages;
	gboolean copy_to_sent = TRUE;
	gboolean did_connect = FALSE;
	guint ii;
	GError *error = NULL;

	context = g_simple_async_result_get_op_res_gpointer (simple);

	if (camel_address_length (context->recipients) == 0)
		goto skip_send;

	/* Send the message to all recipients. */

	if (context->transport == NULL) {
		g_simple_async_result_set_error (
			simple, CAMEL_SERVICE_ERROR,
			CAMEL_SERVICE_ERROR_UNAVAILABLE,
			_("No mail transport service available"));
		return;
	}

	status = camel_service_get_connection_status (context->transport);
	if (status != CAMEL_SERVICE_CONNECTED) {
		did_connect = TRUE;

		camel_service_connect_sync (
			context->transport, cancellable, &error);

		if (error != NULL) {
			g_simple_async_result_take_error (simple, error);
			return;
		}
	}

	provider = camel_service_get_provider (context->transport);

	if (provider->flags & CAMEL_PROVIDER_DISABLE_SENT_FOLDER)
		copy_to_sent = FALSE;

	camel_transport_send_to_sync (
		CAMEL_TRANSPORT (context->transport),
		context->message, context->from,
		context->recipients, cancellable, &error);

	if (did_connect) {
		/* Disconnect regardless of error or cancellation,
		 * but be mindful of these conditions when calling
		 * camel_service_disconnect_sync(). */
		if (g_cancellable_is_cancelled (cancellable)) {
			camel_service_disconnect_sync (
				context->transport, FALSE, NULL, NULL);
		} else if (error != NULL) {
			camel_service_disconnect_sync (
				context->transport, FALSE, cancellable, NULL);
		} else {
			camel_service_disconnect_sync (
				context->transport, TRUE, cancellable, &error);
		}

		if (error != NULL) {
			g_simple_async_result_take_error (simple, error);
			return;
		}
	}

skip_send:
	/* Post the message to requested folders. */
	for (ii = 0; ii < context->post_to_uris->len; ii++) {
		CamelFolder *folder;
		const gchar *folder_uri;

		folder_uri = g_ptr_array_index (context->post_to_uris, ii);

		folder = e_mail_session_uri_to_folder_sync (
			session, folder_uri, 0, cancellable, &error);

		if (error != NULL) {
			g_warn_if_fail (folder == NULL);
			g_simple_async_result_take_error (simple, error);
			return;
		}

		g_return_if_fail (CAMEL_IS_FOLDER (folder));

		camel_folder_append_message_sync (
			folder, context->message, context->info,
			NULL, cancellable, &error);

		g_object_unref (folder);

		if (error != NULL) {
			g_simple_async_result_take_error (simple, error);
			return;
		}
	}

	/*** Post Processing ***/

	/* This accumulates error messages during post-processing. */
	error_messages = g_string_sized_new (256);

	mail_tool_restore_xevolution_headers (context->message, context->xev);

	/* Run filters on the outgoing message. */
	if (context->driver != NULL) {
		camel_filter_driver_filter_message (
			context->driver, context->message, context->info,
			NULL, NULL, NULL, "", cancellable, &error);

		if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
			goto exit;

		if (error != NULL) {
			g_string_append_printf (
				error_messages,
				_("Failed to apply outgoing filters: %s"),
				error->message);
			g_clear_error (&error);
		}

		if ((camel_message_info_flags (context->info) & CAMEL_MESSAGE_DELETED) != 0)
			copy_to_sent = FALSE;
	}

	if (!copy_to_sent)
		goto cleanup;

	/* Append the sent message to a Sent folder. */

	local_sent_folder =
		e_mail_session_get_local_folder (
		session, E_MAIL_LOCAL_FOLDER_SENT);

	folder = e_mail_session_get_fcc_for_message_sync (
		session, context->message, cancellable, &error);

	/* Sanity check. */
	g_return_if_fail (
		((folder != NULL) && (error == NULL)) ||
		((folder == NULL) && (error != NULL)));

	/* Append the message. */
	if (folder != NULL)
		camel_folder_append_message_sync (
			folder, context->message,
			context->info, NULL, cancellable, &error);

	if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
		goto exit;

	if (error == NULL)
		goto cleanup;

	if (folder != NULL && folder != local_sent_folder) {
		const gchar *description;

		description = camel_folder_get_description (folder);

		if (error_messages->len > 0)
			g_string_append (error_messages, "\n\n");
		g_string_append_printf (
			error_messages,
			_("Failed to append to %s: %s\n"
			"Appending to local 'Sent' folder instead."),
			description, error->message);
	}

	/* If appending to a remote Sent folder failed,
	 * try appending to the local Sent folder. */
	if (folder != local_sent_folder) {

		g_clear_error (&error);

		camel_folder_append_message_sync (
			local_sent_folder, context->message,
			context->info, NULL, cancellable, &error);
	}

	if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
		goto exit;

	/* We can't even append to the local Sent folder?
	 * In that case just leave the message in Outbox. */
	if (error != NULL) {
		if (error_messages->len > 0)
			g_string_append (error_messages, "\n\n");
		g_string_append_printf (
			error_messages,
			_("Failed to append to local 'Sent' folder: %s"),
			error->message);
		g_clear_error (&error);
		goto exit;
	}

cleanup:

	/* The send operation was successful; ignore cleanup errors. */

	/* Mark the draft message for deletion, if present. */
	e_mail_session_handle_draft_headers_sync (
		session, context->message, cancellable, &error);
	if (error != NULL) {
		g_warning ("%s", error->message);
		g_clear_error (&error);
	}

	/* Set flags on the original source message, if present.
	 * Source message refers to the message being forwarded
	 * or replied to. */
	e_mail_session_handle_source_headers_sync (
		session, context->message, cancellable, &error);
	if (error != NULL) {
		g_warning ("%s", error->message);
		g_clear_error (&error);
	}

exit:

	/* If we were cancelled, disregard any other errors. */
	if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		g_simple_async_result_take_error (simple, error);

	/* Stuff the accumulated error messages in a GError. */
	} else if (error_messages->len > 0) {
		g_simple_async_result_set_error (
			simple, E_MAIL_ERROR,
			E_MAIL_ERROR_POST_PROCESSING,
			"%s", error_messages->str);
	}

	/* Synchronize the Sent folder. */
	if (folder != NULL) {
		camel_folder_synchronize_sync (
			folder, FALSE, cancellable, NULL);
		g_object_unref (folder);
	}

	g_string_free (error_messages, TRUE);
}

static guint32
get_message_size (CamelMimeMessage *message,
                  GCancellable *cancellable)
{
	CamelStream *null;
	guint32 size;

	null = camel_stream_null_new ();
	camel_data_wrapper_write_to_stream_sync (
		CAMEL_DATA_WRAPPER (message), null, cancellable, NULL);
	size = CAMEL_STREAM_NULL (null)->written;
	g_object_unref (null);

	return size;
}

void
e_mail_session_send_to (EMailSession *session,
                        CamelMimeMessage *message,
                        gint io_priority,
                        GCancellable *cancellable,
                        CamelFilterGetFolderFunc get_folder_func,
                        gpointer get_folder_data,
                        GAsyncReadyCallback callback,
                        gpointer user_data)
{
	GSimpleAsyncResult *simple;
	AsyncContext *context;
	CamelAddress *from;
	CamelAddress *recipients;
	CamelMedium *medium;
	CamelMessageInfo *info;
	CamelService *transport;
	GPtrArray *post_to_uris;
	struct _camel_header_raw *xev;
	struct _camel_header_raw *header;
	const gchar *resent_from;
	GError *error = NULL;

	g_return_if_fail (E_IS_MAIL_SESSION (session));
	g_return_if_fail (CAMEL_IS_MIME_MESSAGE (message));

	medium = CAMEL_MEDIUM (message);

	camel_medium_set_header (medium, "X-Mailer", X_MAILER);

	/* Do this before removing "X-Evolution" headers. */
	transport = e_mail_session_ref_transport_for_message (
		session, message);

	xev = mail_tool_remove_xevolution_headers (message);

	/* Extract directives from X-Evolution headers. */

	post_to_uris = g_ptr_array_new ();
	for (header = xev; header != NULL; header = header->next) {
		gchar *folder_uri;

		if (g_strcmp0 (header->name, "X-Evolution-PostTo") != 0)
			continue;

		folder_uri = g_strstrip (g_strdup (header->value));
		g_ptr_array_add (post_to_uris, folder_uri);
	}

	/* Collect sender and recipients from headers. */

	from = (CamelAddress *) camel_internet_address_new ();
	recipients = (CamelAddress *) camel_internet_address_new ();
	resent_from = camel_medium_get_header (medium, "Resent-From");

	if (resent_from != NULL) {
		const CamelInternetAddress *addr;
		const gchar *type;

		camel_address_decode (from, resent_from);

		type = CAMEL_RECIPIENT_TYPE_RESENT_TO;
		addr = camel_mime_message_get_recipients (message, type);
		camel_address_cat (recipients, CAMEL_ADDRESS (addr));

		type = CAMEL_RECIPIENT_TYPE_RESENT_CC;
		addr = camel_mime_message_get_recipients (message, type);
		camel_address_cat (recipients, CAMEL_ADDRESS (addr));

		type = CAMEL_RECIPIENT_TYPE_RESENT_BCC;
		addr = camel_mime_message_get_recipients (message, type);
		camel_address_cat (recipients, CAMEL_ADDRESS (addr));

	} else {
		const CamelInternetAddress *addr;
		const gchar *type;

		addr = camel_mime_message_get_from (message);
		camel_address_copy (from, CAMEL_ADDRESS (addr));

		type = CAMEL_RECIPIENT_TYPE_TO;
		addr = camel_mime_message_get_recipients (message, type);
		camel_address_cat (recipients, CAMEL_ADDRESS (addr));

		type = CAMEL_RECIPIENT_TYPE_CC;
		addr = camel_mime_message_get_recipients (message, type);
		camel_address_cat (recipients, CAMEL_ADDRESS (addr));

		type = CAMEL_RECIPIENT_TYPE_BCC;
		addr = camel_mime_message_get_recipients (message, type);
		camel_address_cat (recipients, CAMEL_ADDRESS (addr));
	}

	/* Miscellaneous preparations. */

	info = camel_message_info_new_from_header (
		NULL, CAMEL_MIME_PART (message)->headers);
	((CamelMessageInfoBase *) info)->size =
		get_message_size (message, cancellable);
	camel_message_info_set_flags (info, CAMEL_MESSAGE_SEEN, ~0);

	/* expand, or remove empty, group addresses */
	em_utils_expand_groups (CAMEL_INTERNET_ADDRESS (recipients));

	/* The rest of the processing happens in a thread. */

	context = g_slice_new0 (AsyncContext);
	context->message = g_object_ref (message);
	context->io_priority = io_priority;
	context->from = from;
	context->recipients = recipients;
	context->info = info;
	context->xev = xev;
	context->post_to_uris = post_to_uris;
	context->transport = transport;

	if (G_IS_CANCELLABLE (cancellable))
		context->cancellable = g_object_ref (cancellable);

	/* Failure here emits a runtime warning but is non-fatal. */
	context->driver = camel_session_get_filter_driver (
		CAMEL_SESSION (session), E_FILTER_SOURCE_OUTGOING, &error);
	if (context->driver != NULL && get_folder_func)
		camel_filter_driver_set_folder_func (
			context->driver, get_folder_func, get_folder_data);
	if (error != NULL) {
		g_warn_if_fail (context->driver == NULL);
		g_warning ("%s", error->message);
		g_error_free (error);
	}

	/* This gets popped in async_context_free(). */
	camel_operation_push_message (
		context->cancellable, _("Sending message"));

	simple = g_simple_async_result_new (
		G_OBJECT (session), callback,
		user_data, e_mail_session_send_to);

	g_simple_async_result_set_check_cancellable (simple, cancellable);

	g_simple_async_result_set_op_res_gpointer (
		simple, context, (GDestroyNotify) async_context_free);

	g_simple_async_result_run_in_thread (
		simple, (GSimpleAsyncThreadFunc)
		mail_session_send_to_thread,
		context->io_priority,
		context->cancellable);

	g_object_unref (simple);
}

gboolean
e_mail_session_send_to_finish (EMailSession *session,
                               GAsyncResult *result,
                               GError **error)
{
	GSimpleAsyncResult *simple;

	g_return_val_if_fail (
		g_simple_async_result_is_valid (
		result, G_OBJECT (session),
		e_mail_session_send_to), FALSE);

	simple = G_SIMPLE_ASYNC_RESULT (result);

	/* Assume success unless a GError is set. */
	return !g_simple_async_result_propagate_error (simple, error);
}

/* Helper for e_mail_session_get_fcc_for_message_sync() */
static CamelFolder *
mail_session_try_uri_to_folder (EMailSession *session,
                                const gchar *folder_uri,
                                GCancellable *cancellable,
                                GError **error)
{
	CamelFolder *folder;
	GError *local_error = NULL;

	folder = e_mail_session_uri_to_folder_sync (
		session, folder_uri, 0, cancellable, &local_error);

	/* Sanity check. */
	g_return_val_if_fail (
		((folder != NULL) && (local_error == NULL)) ||
		((folder == NULL) && (local_error != NULL)), NULL);

	/* Disregard specific errors. */

	/* Invalid URI. */
	if (g_error_matches (
		local_error, CAMEL_FOLDER_ERROR,
		CAMEL_FOLDER_ERROR_INVALID))
		g_clear_error (&local_error);

	/* Folder not found. */
	if (g_error_matches (
		local_error, CAMEL_STORE_ERROR,
		CAMEL_STORE_ERROR_NO_FOLDER))
		g_clear_error (&local_error);

	if (local_error != NULL)
		g_propagate_error (error, local_error);

	return folder;
}

/* Helper for e_mail_session_get_fcc_for_message_sync() */
static CamelFolder *
mail_session_ref_origin_folder (EMailSession *session,
                                CamelMimeMessage *message,
                                GCancellable *cancellable,
                                GError **error)
{
	CamelMedium *medium;
	const gchar *header_name;
	const gchar *header_value;

	medium = CAMEL_MEDIUM (message);

	/* Check that a "X-Evolution-Source-Flags" header is present
	 * and its value does not contain the substring "FORWARDED". */

	header_name = "X-Evolution-Source-Flags";
	header_value = camel_medium_get_header (medium, header_name);

	if (header_value == NULL)
		return NULL;

	if (strstr (header_value, "FORWARDED") != NULL)
		return NULL;

	/* Check that a "X-Evolution-Source-Message" header is present. */

	header_name = "X-Evolution-Source-Message";
	header_value = camel_medium_get_header (medium, header_name);

	if (header_value == NULL)
		return NULL;

	/* Check that a "X-Evolution-Source-Folder" header is present.
	 * Its value specifies the origin folder as a folder URI. */

	header_name = "X-Evolution-Source-Folder";
	header_value = camel_medium_get_header (medium, header_name);

	if (header_value == NULL)
		return NULL;

	/* This may return NULL without setting a GError. */
	return mail_session_try_uri_to_folder (
		session, header_value, cancellable, error);
}

/* Helper for e_mail_session_get_fcc_for_message_sync() */
static CamelFolder *
mail_session_ref_fcc_from_identity (EMailSession *session,
                                    ESource *source,
                                    CamelMimeMessage *message,
                                    GCancellable *cancellable,
                                    GError **error)
{
	ESourceRegistry *registry;
	ESourceMailSubmission *extension;
	CamelFolder *folder = NULL;
	const gchar *extension_name;
	gchar *folder_uri;

	registry = e_mail_session_get_registry (session);
	extension_name = E_SOURCE_EXTENSION_MAIL_SUBMISSION;

	if (source == NULL)
		return NULL;

	if (!e_source_registry_check_enabled (registry, source))
		return NULL;

	if (!e_source_has_extension (source, extension_name))
		return NULL;

	extension = e_source_get_extension (source, extension_name);

	if (e_source_mail_submission_get_replies_to_origin_folder (extension)) {
		GError *local_error = NULL;

		/* This may return NULL without setting a GError. */
		folder = mail_session_ref_origin_folder (
			session, message, cancellable, &local_error);

		if (local_error != NULL) {
			g_warn_if_fail (folder == NULL);
			g_propagate_error (error, local_error);
			return NULL;
		}
	}

	folder_uri = e_source_mail_submission_dup_sent_folder (extension);

	if (folder_uri != NULL && folder == NULL) {
		/* This may return NULL without setting a GError. */
		folder = mail_session_try_uri_to_folder (
			session, folder_uri, cancellable, error);
	}

	g_free (folder_uri);

	return folder;
}

/* Helper for e_mail_session_get_fcc_for_message_sync() */
static CamelFolder *
mail_session_ref_fcc_from_x_identity (EMailSession *session,
                                      CamelMimeMessage *message,
                                      GCancellable *cancellable,
                                      GError **error)
{
	ESource *source;
	ESourceRegistry *registry;
	CamelFolder *folder;
	CamelMedium *medium;
	const gchar *header_name;
	const gchar *header_value;
	gchar *uid;

	medium = CAMEL_MEDIUM (message);
	header_name = "X-Evolution-Identity";
	header_value = camel_medium_get_header (medium, header_name);

	if (header_value == NULL)
		return NULL;

	uid = g_strstrip (g_strdup (header_value));

	registry = e_mail_session_get_registry (session);
	source = e_source_registry_ref_source (registry, uid);

	/* This may return NULL without setting a GError. */
	folder = mail_session_ref_fcc_from_identity (
		session, source, message, cancellable, error);

	g_clear_object (&source);

	g_free (uid);

	return folder;
}

/* Helper for e_mail_session_get_fcc_for_message_sync() */
static CamelFolder *
mail_session_ref_fcc_from_x_fcc (EMailSession *session,
                                 CamelMimeMessage *message,
                                 GCancellable *cancellable,
                                 GError **error)
{
	CamelMedium *medium;
	const gchar *header_name;
	const gchar *header_value;

	medium = CAMEL_MEDIUM (message);
	header_name = "X-Evolution-Fcc";
	header_value = camel_medium_get_header (medium, header_name);

	if (header_value == NULL)
		return NULL;

	/* This may return NULL without setting a GError. */
	return mail_session_try_uri_to_folder (
		session, header_value, cancellable, error);
}

/* Helper for e_mail_session_get_fcc_for_message_sync() */
static CamelFolder *
mail_session_ref_fcc_from_default_identity (EMailSession *session,
                                            CamelMimeMessage *message,
                                            GCancellable *cancellable,
                                            GError **error)
{
	ESource *source;
	ESourceRegistry *registry;
	CamelFolder *folder;

	registry = e_mail_session_get_registry (session);
	source = e_source_registry_ref_default_mail_identity (registry);

	/* This may return NULL without setting a GError. */
	folder = mail_session_ref_fcc_from_identity (
		session, source, message, cancellable, error);

	g_clear_object (&source);

	return folder;
}

/**
 * e_mail_session_get_fcc_for_message_sync:
 * @session: an #EMailSession
 * @message: a #CamelMimeMessage
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Obtains the preferred "carbon-copy" folder (a.k.a Fcc) for @message
 * by first checking @message for an "X-Evolution-Identity" header, and
 * then an "X-Evolution-Fcc" header.  Failing that, the function checks
 * the default mail identity (if available), and failing even that, the
 * function falls back to the Sent folder from the built-in mail store.
 *
 * Where applicable, the function attempts to honor the
 * #ESourceMailSubmission:replies-to-origin-folder preference.
 *
 * The returned #CamelFolder is referenced for thread-safety and must be
 * unreferenced with g_object_unref() when finished with it.
 *
 * If a non-recoverable error occurs, the function sets @error and returns
 * %NULL.
 *
 * Returns: a #CamelFolder, or %NULL
 **/
CamelFolder *
e_mail_session_get_fcc_for_message_sync (EMailSession *session,
                                         CamelMimeMessage *message,
                                         GCancellable *cancellable,
                                         GError **error)
{
	CamelFolder *folder = NULL;

	g_return_val_if_fail (E_IS_MAIL_SESSION (session), NULL);
	g_return_val_if_fail (CAMEL_IS_MIME_MESSAGE (message), NULL);

	/* Check for "X-Evolution-Identity" header. */
	if (folder == NULL) {
		GError *local_error = NULL;

		/* This may return NULL without setting a GError. */
		folder = mail_session_ref_fcc_from_x_identity (
			session, message, cancellable, &local_error);

		if (local_error != NULL) {
			g_warn_if_fail (folder == NULL);
			g_propagate_error (error, local_error);
			return NULL;
		}
	}

	/* Check for "X-Evolution-Fcc" header. */
	if (folder == NULL) {
		GError *local_error = NULL;

		/* This may return NULL without setting a GError. */
		folder = mail_session_ref_fcc_from_x_fcc (
			session, message, cancellable, &local_error);

		if (local_error != NULL) {
			g_warn_if_fail (folder == NULL);
			g_propagate_error (error, local_error);
			return NULL;
		}
	}

	/* Check the default mail identity. */
	if (folder == NULL) {
		GError *local_error = NULL;

		/* This may return NULL without setting a GError. */
		folder = mail_session_ref_fcc_from_default_identity (
			session, message, cancellable, &local_error);

		if (local_error != NULL) {
			g_warn_if_fail (folder == NULL);
			g_propagate_error (error, local_error);
			return NULL;
		}
	}

	/* Last resort - local Sent folder. */
	if (folder == NULL) {
		folder = e_mail_session_get_local_folder (
			session, E_MAIL_LOCAL_FOLDER_SENT);
		g_object_ref (folder);
	}

	return folder;
}

/* Helper for e_mail_session_get_fcc_for_message() */
static void
mail_session_get_fcc_for_message_thread (GSimpleAsyncResult *simple,
                                         GObject *source_object,
                                         GCancellable *cancellable)
{
	AsyncContext *async_context;
	GError *local_error = NULL;

	async_context = g_simple_async_result_get_op_res_gpointer (simple);

	async_context->folder =
		e_mail_session_get_fcc_for_message_sync (
			E_MAIL_SESSION (source_object),
			async_context->message,
			cancellable, &local_error);

	if (local_error != NULL)
		g_simple_async_result_take_error (simple, local_error);
}

/**
 * e_mail_session_get_fcc_for_message:
 * @session: an #EMailSession
 * @message: a #CamelMimeMessage
 * @io_priority: the I/O priority of the request
 * @cancellable: optional #GCancellable object, or %NULL
 * @callback: a #GAsyncReadyCallback to call when the request is satisfied
 * @user_data: data to pass to the callback function
 *
 * Asynchronously obtains the preferred "carbon-copy" folder (a.k.a Fcc) for
 * @message by first checking @message for an "X-Evolution-Identity" header,
 * and then an "X-Evolution-Fcc" header.  Failing that, the function checks
 * the default mail identity (if available), and failing even that, the
 * function falls back to the Sent folder from the built-in mail store.
 *
 * Where applicable, the function attempts to honor the
 * #ESourceMailSubmission:replies-to-origin-folder preference.
 *
 * When the operation is finished, @callback will be called.  You can then
 * call e_mail_session_get_fcc_for_message_finish() to get the result of the
 * operation.
 **/
void
e_mail_session_get_fcc_for_message (EMailSession *session,
                                    CamelMimeMessage *message,
                                    gint io_priority,
                                    GCancellable *cancellable,
                                    GAsyncReadyCallback callback,
                                    gpointer user_data)
{
	GSimpleAsyncResult *simple;
	AsyncContext *async_context;

	g_return_if_fail (E_IS_MAIL_SESSION (session));
	g_return_if_fail (CAMEL_IS_MIME_MESSAGE (message));

	async_context = g_slice_new0 (AsyncContext);
	async_context->message = g_object_ref (message);

	simple = g_simple_async_result_new (
		G_OBJECT (session), callback, user_data,
		e_mail_session_get_fcc_for_message);

	g_simple_async_result_set_check_cancellable (simple, cancellable);

	g_simple_async_result_set_op_res_gpointer (
		simple, async_context, (GDestroyNotify) async_context_free);

	g_simple_async_result_run_in_thread (
		simple, mail_session_get_fcc_for_message_thread,
		io_priority, cancellable);

	g_object_unref (simple);
}

/**
 * e_mail_session_get_fcc_for_message_finish:
 * @session: an #EMailSession
 * @result: a #GAsyncResult
 * @error: return location for a #GError, or %NULL
 *
 * Finishes the operation started with e_mail_session_get_fcc_for_message().
 *
 * The returned #CamelFolder is referenced for thread-safety and must be
 * unreferenced with g_object_unref() when finished with it.
 *
 * If a non-recoverable error occurred, the function sets @error and
 * returns %NULL.
 *
 * Returns: a #CamelFolder, or %NULL
 **/
CamelFolder *
e_mail_session_get_fcc_for_message_finish (EMailSession *session,
                                           GAsyncResult *result,
                                           GError **error)
{
	GSimpleAsyncResult *simple;
	AsyncContext *async_context;

	g_return_val_if_fail (
		g_simple_async_result_is_valid (
		result, G_OBJECT (session),
		e_mail_session_get_fcc_for_message), NULL);

	simple = G_SIMPLE_ASYNC_RESULT (result);
	async_context = g_simple_async_result_get_op_res_gpointer (simple);

	if (g_simple_async_result_propagate_error (simple, error))
		return NULL;

	g_return_val_if_fail (async_context->folder != NULL, NULL);

	return g_object_ref (async_context->folder);
}

/**
 * e_mail_session_ref_transport:
 * @session: an #EMailSession
 * @transport_uid: the UID of a mail transport
 *
 * Returns the transport #CamelService instance for @transport_uid,
 * verifying first that the @transport_uid is indeed a mail transport and
 * that the corresponding #ESource is enabled.  If these checks fail, the
 * function returns %NULL.
 *
 * The returned #CamelService is referenced for thread-safety and must be
 * unreferenced with g_object_unref() when finished with it.
 *
 * Returns: a #CamelService, or %NULL
 **/
CamelService *
e_mail_session_ref_transport (EMailSession *session,
                              const gchar *transport_uid)
{
	ESourceRegistry *registry;
	ESource *source = NULL;
	CamelService *transport = NULL;
	const gchar *extension_name;

	g_return_val_if_fail (E_IS_MAIL_SESSION (session), NULL);
	g_return_val_if_fail (transport_uid != NULL, NULL);

	registry = e_mail_session_get_registry (session);
	extension_name = E_SOURCE_EXTENSION_MAIL_TRANSPORT;

	source = e_source_registry_ref_source (registry, transport_uid);

	if (source == NULL)
		goto exit;

	if (!e_source_registry_check_enabled (registry, source))
		goto exit;

	if (!e_source_has_extension (source, extension_name))
		goto exit;

	transport = camel_session_ref_service (
		CAMEL_SESSION (session), transport_uid);

	/* Sanity check. */
	if (transport != NULL)
		g_warn_if_fail (CAMEL_IS_TRANSPORT (transport));

exit:
	g_clear_object (&source);

	return transport;
}

/* Helper for e_mail_session_ref_default_transport()
 * and mail_session_ref_transport_from_x_identity(). */
static CamelService *
mail_session_ref_transport_for_identity (EMailSession *session,
                                         ESource *source)
{
	ESourceRegistry *registry;
	ESourceMailSubmission *extension;
	CamelService *transport = NULL;
	const gchar *extension_name;
	gchar *uid;

	registry = e_mail_session_get_registry (session);
	extension_name = E_SOURCE_EXTENSION_MAIL_SUBMISSION;

	if (source == NULL)
		return NULL;

	if (!e_source_registry_check_enabled (registry, source))
		return NULL;

	if (!e_source_has_extension (source, extension_name))
		return NULL;

	extension = e_source_get_extension (source, extension_name);
	uid = e_source_mail_submission_dup_transport_uid (extension);

	if (uid != NULL) {
		transport = e_mail_session_ref_transport (session, uid);
		g_free (uid);
	}

	return transport;
}

/**
 * e_mail_session_ref_default_transport:
 * @session: an #EMailSession
 *
 * Returns the default transport #CamelService instance according to
 * #ESourceRegistry's #ESourceRegistry:default-mail-identity setting,
 * verifying first that the #ESourceMailSubmission:transport-uid named by
 * the #ESourceRegistry:default-mail-identity is indeed a mail transport,
 * and that the corresponding #ESource is enabled.  If these checks fail,
 * the function returns %NULL.
 *
 * The returned #CamelService is referenced for thread-safety and must be
 * unreferenced with g_object_unref() when finished with it.
 *
 * Returns: a #CamelService, or %NULL
 **/
CamelService *
e_mail_session_ref_default_transport (EMailSession *session)
{
	ESource *source;
	ESourceRegistry *registry;
	CamelService *transport;

	g_return_val_if_fail (E_IS_MAIL_SESSION (session), NULL);

	registry = e_mail_session_get_registry (session);
	source = e_source_registry_ref_default_mail_identity (registry);
	transport = mail_session_ref_transport_for_identity (session, source);
	g_clear_object (&source);

	return transport;
}

/* Helper for e_mail_session_ref_transport_for_message() */
static CamelService *
mail_session_ref_transport_from_x_identity (EMailSession *session,
                                            CamelMimeMessage *message)
{
	ESource *source;
	ESourceRegistry *registry;
	CamelMedium *medium;
	CamelService *transport;
	const gchar *header_name;
	const gchar *header_value;
	gchar *uid;

	medium = CAMEL_MEDIUM (message);
	header_name = "X-Evolution-Identity";
	header_value = camel_medium_get_header (medium, header_name);

	if (header_value == NULL)
		return NULL;

	uid = g_strstrip (g_strdup (header_value));

	registry = e_mail_session_get_registry (session);
	source = e_source_registry_ref_source (registry, uid);
	transport = mail_session_ref_transport_for_identity (session, source);
	g_clear_object (&source);

	g_free (uid);

	return transport;
}

/* Helper for e_mail_session_ref_transport_for_message() */
static CamelService *
mail_session_ref_transport_from_x_transport (EMailSession *session,
                                             CamelMimeMessage *message)
{
	CamelMedium *medium;
	CamelService *transport;
	const gchar *header_name;
	const gchar *header_value;
	gchar *uid;

	medium = CAMEL_MEDIUM (message);
	header_name = "X-Evolution-Transport";
	header_value = camel_medium_get_header (medium, header_name);

	if (header_value == NULL)
		return NULL;

	uid = g_strstrip (g_strdup (header_value));

	transport = e_mail_session_ref_transport (session, uid);

	g_free (uid);

	return transport;
}

/**
 * e_mail_session_ref_transport_for_message:
 * @session: an #EMailSession
 * @message: a #CamelMimeMessage
 *
 * Returns the preferred transport #CamelService instance for @message by
 * first checking @message for an "X-Evolution-Identity" header, and then
 * an "X-Evolution-Transport" header.  Failing that, the function returns
 * the default transport #CamelService instance (if available).
 *
 * The returned #CamelService is referenced for thread-safety and must be
 * unreferenced with g_object_unref() when finished with it.
 *
 * Returns: a #CamelService, or %NULL
 **/
CamelService *
e_mail_session_ref_transport_for_message (EMailSession *session,
                                          CamelMimeMessage *message)
{
	CamelService *transport = NULL;

	g_return_val_if_fail (E_IS_MAIL_SESSION (session), NULL);
	g_return_val_if_fail (CAMEL_IS_MIME_MESSAGE (message), NULL);

	/* Check for "X-Evolution-Identity" header. */
	if (transport == NULL)
		transport = mail_session_ref_transport_from_x_identity (
			session, message);

	/* Check for "X-Evolution-Transport" header. */
	if (transport == NULL)
		transport = mail_session_ref_transport_from_x_transport (
			session, message);

	/* Fall back to the default mail transport. */
	if (transport == NULL)
		transport = e_mail_session_ref_default_transport (session);

	return transport;
}