/*
 * evolution-mdn.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/>
 *
 */

#include <config.h>
#include <string.h>
#include <glib/gi18n-lib.h>

#include <libebackend/libebackend.h>

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

#include <mail/em-utils.h>
#include <mail/e-mail-reader.h>
#include <mail/mail-send-recv.h>
#include <mail/message-list.h>
#include <mail/em-composer-utils.h>

#define MDN_USER_FLAG "receipt-handled"

#define E_TYPE_MDN (e_mdn_get_type ())
#define E_MDN(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), E_TYPE_MDN, EMdn))
#define E_IS_MDN(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), E_TYPE_MDN))

typedef struct _EMdn EMdn;
typedef struct _EMdnClass EMdnClass;

struct _EMdn {
	EExtension parent;
	gpointer alert;  /* weak pointer */
};

struct _EMdnClass {
	EExtensionClass parent_class;
};

typedef struct _MdnContext MdnContext;

struct _MdnContext {
	ESource *source;
	EMailReader *reader;
	CamelFolder *folder;
	CamelMessageInfo *info;
	CamelMimeMessage *message;
	gchar *notify_to;
};

typedef enum {
	MDN_ACTION_MODE_MANUAL,
	MDN_ACTION_MODE_AUTOMATIC
} MdnActionMode;

typedef enum {
	MDN_SENDING_MODE_MANUAL,
	MDN_SENDING_MODE_AUTOMATIC
} MdnSendingMode;

/* Module Entry Points */
void e_module_load (GTypeModule *type_module);
void e_module_unload (GTypeModule *type_module);

/* Forward Declarations */
GType e_mdn_get_type (void);

G_DEFINE_DYNAMIC_TYPE (EMdn, e_mdn, E_TYPE_EXTENSION)

static void
mdn_context_free (MdnContext *context)
{
	if (context->info != NULL)
		camel_folder_free_message_info (
			context->folder, context->info);

	g_object_unref (context->source);
	g_object_unref (context->reader);
	g_object_unref (context->folder);
	g_object_unref (context->message);

	g_free (context->notify_to);

	g_slice_free (MdnContext, context);
}

static void
mdn_remove_alert (EMdn *mdn)
{
	g_return_if_fail (E_IS_MDN (mdn));

	if (mdn->alert != NULL)
		e_alert_response (mdn->alert, GTK_RESPONSE_OK);
}

static void
mdn_submit_alert (EMdn *mdn,
                  EMailReader *reader,
                  EAlert *alert)
{
	EPreviewPane *preview_pane;

	g_return_if_fail (E_IS_MDN (mdn));

	mdn_remove_alert (mdn);

	g_return_if_fail (mdn->alert == NULL);

	/* Make sure alerts are shown in the preview pane and not
	 * wherever e_mail_reader_get_alert_sink() might show it. */
	preview_pane = e_mail_reader_get_preview_pane (reader);
	e_alert_sink_submit_alert (E_ALERT_SINK (preview_pane), alert);

	mdn->alert = alert;
	g_object_add_weak_pointer (G_OBJECT (mdn->alert), &mdn->alert);
}

static gchar *
mdn_get_notify_to (CamelMimeMessage *message)
{
	CamelMedium *medium;
	const gchar *address;
	const gchar *header_name;

	medium = CAMEL_MEDIUM (message);
	header_name = "Disposition-Notification-To";
	address = camel_medium_get_header (medium, header_name);

	/* TODO Should probably decode/format the address,
	 *      since it could be in RFC 2047 format. */
	if (address != NULL)
		while (camel_mime_is_lwsp (*address))
			address++;

	return g_strdup (address);
}

static gchar *
mdn_get_disposition (MdnActionMode action_mode,
                     MdnSendingMode sending_mode)
{
	GString *string;

	string = g_string_sized_new (64);

	switch (action_mode) {
		case MDN_ACTION_MODE_MANUAL:
			g_string_append (string, "manual-action");
			break;
		case MDN_ACTION_MODE_AUTOMATIC:
			g_string_append (string, "automatic-action");
			break;
		default:
			g_warn_if_reached ();
	}

	g_string_append_c (string, '/');

	switch (sending_mode) {
		case MDN_SENDING_MODE_MANUAL:
			g_string_append (string, "MDN-sent-manually");
			break;
		case MDN_SENDING_MODE_AUTOMATIC:
			g_string_append (string, "MDN-sent-automatically");
			break;
		default:
			g_warn_if_reached ();
	}

	g_string_append (string, ";displayed");

	return g_string_free (string, FALSE);
}

static void
mdn_receipt_done (EMailSession *session,
                  GAsyncResult *result,
                  gpointer user_data)
{
	GError *error = NULL;

	e_mail_session_append_to_local_folder_finish (
		session, result, NULL, &error);

	if (error == NULL) {
		mail_send (session);
	} else {
		/* FIXME Poor error handling. */
		g_warning ("%s: %s", G_STRFUNC, error->message);
		g_error_free (error);
	}
}

static void
mdn_notify_sender (ESource *identity_source,
                   EMailReader *reader,
                   CamelFolder *folder,
                   CamelMimeMessage *message,
                   CamelMessageInfo *info,
                   const gchar *notify_to,
                   MdnActionMode action_mode,
                   MdnSendingMode sending_mode)
{
	/* See RFC 3798 for a description of message receipts. */

	CamelMimeMessage *receipt;
	CamelMultipart *body;
	CamelMimePart *part;
	CamelMedium *medium;
	CamelDataWrapper *receipt_text, *receipt_data;
	CamelContentType *type;
	CamelInternetAddress *address;
	CamelStream *stream;
	CamelMessageInfo *receipt_info;
	EMailBackend *backend;
	EMailSession *session;
	ESourceExtension *extension;
	const gchar *message_id;
	const gchar *message_date;
	const gchar *message_subject;
	const gchar *extension_name;
	const gchar *transport_uid;
	const gchar *self_address;
	const gchar *sent_folder_uri;
	gchar *fake_msgid;
	gchar *hostname;
	gchar *receipt_subject;
	gchar *disposition;
	gchar *recipient;
	gchar *content;
	gchar *ua;

	backend = e_mail_reader_get_backend (reader);
	session = e_mail_backend_get_session (backend);

	/* Tag the message immediately even though we haven't actually sent
	 * the read receipt yet.  Not a big deal if we fail to send it, and
	 * we don't want to keep badgering the user about it. */
	camel_message_info_set_user_flag (info, MDN_USER_FLAG, TRUE);

	medium = CAMEL_MEDIUM (message);
	message_id = camel_medium_get_header (medium, "Message-ID");
	message_date = camel_medium_get_header (medium, "Date");
	message_subject = camel_mime_message_get_subject (message);

	if (message_id == NULL)
		message_id = "";

	if (message_date == NULL)
		message_date = "";

	/* Collect information for the receipt. */

	g_return_if_fail (identity_source != NULL);

	extension_name = E_SOURCE_EXTENSION_MAIL_IDENTITY;
	extension = e_source_get_extension (identity_source, extension_name);

	self_address = e_source_mail_identity_get_address (
		E_SOURCE_MAIL_IDENTITY (extension));

	extension_name = E_SOURCE_EXTENSION_MAIL_SUBMISSION;
	extension = e_source_get_extension (identity_source, extension_name);

	sent_folder_uri = e_source_mail_submission_get_sent_folder (
		E_SOURCE_MAIL_SUBMISSION (extension));

	transport_uid = e_source_mail_submission_get_transport_uid (
		E_SOURCE_MAIL_SUBMISSION (extension));

	/* We use camel_header_msgid_generate() to get a canonical
	 * hostname, then skip the part leading to '@' */
	fake_msgid = camel_header_msgid_generate ();
	hostname = strchr (fake_msgid, '@');
	g_return_if_fail (hostname != NULL);

	hostname++;

	/* Create toplevel container. */
	body = camel_multipart_new ();
	camel_data_wrapper_set_mime_type (
		CAMEL_DATA_WRAPPER (body),
		"multipart/report;"
		"report-type=\"disposition-notification\"");
	camel_multipart_set_boundary (body, NULL);

	/* Create textual receipt. */

	receipt_text = camel_data_wrapper_new ();

	type = camel_content_type_new ("text", "plain");
	camel_content_type_set_param (type, "format", "flowed");
	camel_content_type_set_param (type, "charset", "UTF-8");
	camel_data_wrapper_set_mime_type_field (receipt_text, type);
	camel_content_type_unref (type);

	content = g_strdup_printf (
		/* Translators: First %s is an email address, second %s
		 * is the subject of the email, third %s is the date. */
		_("Your message to %s about \"%s\" on %s has been read."),
		self_address, message_subject, message_date);
	stream = camel_stream_mem_new ();
	camel_stream_write_string (stream, content, NULL, NULL);
	camel_data_wrapper_construct_from_stream_sync (
		receipt_text, stream, NULL, NULL);
	g_object_unref (stream);
	g_free (content);

	part = camel_mime_part_new ();
	camel_medium_set_content (CAMEL_MEDIUM (part), receipt_text);
	camel_mime_part_set_encoding (
		part, CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE);
	camel_multipart_add_part (body, part);
	g_object_unref (part);

	g_object_unref (receipt_text);

	/* Create the machine-readable receipt. */

	receipt_data = camel_data_wrapper_new ();

	ua = g_strdup_printf (
		"%s; %s", hostname, "Evolution "
		VERSION SUB_VERSION " " VERSION_COMMENT);
	recipient = g_strdup_printf ("rfc822; %s", self_address);
	disposition = mdn_get_disposition (action_mode, sending_mode);

	type = camel_content_type_new ("message", "disposition-notification");
	camel_data_wrapper_set_mime_type_field (receipt_data, type);
	camel_content_type_unref (type);

	content = g_strdup_printf (
		"Reporting-UA: %s\n"
		"Final-Recipient: %s\n"
		"Original-Message-ID: %s\n"
		"Disposition: %s\n",
		ua, recipient, message_id, disposition);
	stream = camel_stream_mem_new ();
	camel_stream_write_string (stream, content, NULL, NULL);
	camel_data_wrapper_construct_from_stream_sync (
		receipt_data, stream, NULL, NULL);
	g_object_unref (stream);
	g_free (content);

	g_free (ua);
	g_free (recipient);
	g_free (fake_msgid);
	g_free (disposition);

	part = camel_mime_part_new ();
	camel_medium_set_content (CAMEL_MEDIUM (part), receipt_data);
	camel_mime_part_set_encoding (part, CAMEL_TRANSFER_ENCODING_7BIT);
	camel_multipart_add_part (body, part);
	g_object_unref (part);

	g_object_unref (receipt_data);

	/* Finish creating the message. */

	receipt = camel_mime_message_new ();
	camel_medium_set_content (
		CAMEL_MEDIUM (receipt), CAMEL_DATA_WRAPPER (body));
	g_object_unref (body);

	receipt_subject = g_strdup_printf (
		/* Translators: %s is the subject of the email message. */
		_("Delivery Notification for \"%s\""), message_subject);
	camel_mime_message_set_subject (receipt, receipt_subject);
	g_free (receipt_subject);

	address = camel_internet_address_new ();
	camel_address_decode (CAMEL_ADDRESS (address), self_address);
	camel_mime_message_set_from (receipt, address);
	g_object_unref (address);

	address = camel_internet_address_new ();
	camel_address_decode (CAMEL_ADDRESS (address), notify_to);
	camel_mime_message_set_recipients (
		receipt, CAMEL_RECIPIENT_TYPE_TO, address);
	g_object_unref (address);

	camel_medium_set_header (
		CAMEL_MEDIUM (receipt),
		"Return-Path", "<>");
	camel_medium_set_header (
		CAMEL_MEDIUM (receipt),
		"X-Evolution-Identity",
		e_source_get_uid (identity_source));
	camel_medium_set_header (
		CAMEL_MEDIUM (receipt),
		"X-Evolution-Transport",
		transport_uid);
	camel_medium_set_header (
		CAMEL_MEDIUM (receipt),
		"X-Evolution-Fcc",
		sent_folder_uri);

	/* RFC 3834, Section 5 describes this header. */
	camel_medium_set_header (
		CAMEL_MEDIUM (receipt),
		"Auto-Submitted", "auto-replied");

	/* Send the receipt. */
	receipt_info = camel_message_info_new (NULL);
	camel_message_info_set_flags (
		receipt_info, CAMEL_MESSAGE_SEEN, CAMEL_MESSAGE_SEEN);

	/* FIXME Pass a GCancellable. */
	e_mail_session_append_to_local_folder (
		session, E_MAIL_LOCAL_FOLDER_OUTBOX,
		receipt, receipt_info, G_PRIORITY_DEFAULT,
		NULL, (GAsyncReadyCallback) mdn_receipt_done,
		g_object_ref (session));

	camel_message_info_free (receipt_info);
}

static void
mdn_notify_action_cb (GtkAction *action,
                      MdnContext *context)
{
	mdn_notify_sender (
		context->source,
		context->reader,
		context->folder,
		context->message,
		context->info,
		context->notify_to,
		MDN_ACTION_MODE_MANUAL,
		MDN_SENDING_MODE_MANUAL);

	/* Make sure the newly-added user flag gets saved. */
	camel_folder_free_message_info (context->folder, context->info);
	context->info = NULL;
}

static void
mdn_mail_reader_changed_cb (EMailReader *reader,
                            EMdn *mdn)
{
	MessageList *message_list;

	g_return_if_fail (E_IS_MAIL_READER (reader));

	message_list = MESSAGE_LIST (e_mail_reader_get_message_list (reader));

	if (!message_list || message_list_selected_count (message_list) != 1)
		mdn_remove_alert (mdn);
}

static void
mdn_message_loaded_cb (EMailReader *reader,
                       const gchar *message_uid,
                       CamelMimeMessage *message,
                       EMdn *mdn)
{
	EAlert *alert;
	ESource *source;
	ESourceMDN *extension;
	ESourceRegistry *registry;
	EMailBackend *backend;
	EMailSession *session;
	CamelFolder *folder;
	CamelMessageInfo *info;
	EMdnResponsePolicy response_policy;
	const gchar *extension_name;
	gchar *notify_to = NULL;

	backend = e_mail_reader_get_backend (reader);
	session = e_mail_backend_get_session (backend);
	registry = e_mail_session_get_registry (session);

	folder = e_mail_reader_ref_folder (reader);

	mdn_remove_alert (mdn);

	info = camel_folder_get_message_info (folder, message_uid);
	if (info == NULL)
		goto exit;

	if (camel_message_info_user_flag (info, MDN_USER_FLAG)) {
		alert = e_alert_new ("mdn:sender-notified", NULL);
		mdn_submit_alert (mdn, reader, alert);
		g_object_unref (alert);
		goto exit;
	}

	notify_to = mdn_get_notify_to (message);
	if (notify_to == NULL)
		goto exit;

	/* Do not show the notice in special folders. */

	if (em_utils_folder_is_drafts (registry, folder))
		goto exit;

	if (em_utils_folder_is_templates (registry, folder))
		goto exit;

	if (em_utils_folder_is_sent (registry, folder))
		goto exit;

	if (em_utils_folder_is_outbox (registry, folder))
		goto exit;

	/* This returns a new ESource reference. */
	source = em_utils_guess_mail_identity_with_recipients (
		registry, message, folder, message_uid);
	if (source == NULL)
		goto exit;

	extension_name = E_SOURCE_EXTENSION_MDN;
	extension = e_source_get_extension (source, extension_name);
	response_policy = e_source_mdn_get_response_policy (extension);

	if (response_policy == E_MDN_RESPONSE_POLICY_ASK) {
		MdnContext *context;
		GtkAction *action;
		gchar *tooltip;

		context = g_slice_new0 (MdnContext);
		context->source = g_object_ref (source);
		context->reader = g_object_ref (reader);
		context->folder = g_object_ref (folder);
		context->message = g_object_ref (message);
		context->info = camel_message_info_ref (info);

		context->notify_to = notify_to;
		notify_to = NULL;

		tooltip = g_strdup_printf (
			_("Send a read receipt to '%s'"),
			context->notify_to);

		action = gtk_action_new (
			"notify-sender",  /* name doesn't matter */
			_("_Notify Sender"),
			tooltip, NULL);

		g_signal_connect_data (
			action, "activate",
			G_CALLBACK (mdn_notify_action_cb),
			context,
			(GClosureNotify) mdn_context_free,
			(GConnectFlags) 0);

		alert = e_alert_new ("mdn:notify-sender", NULL);
		e_alert_add_action (alert, action, GTK_RESPONSE_APPLY);
		mdn_submit_alert (mdn, reader, alert);
		g_object_unref (alert);

		g_object_unref (action);
		g_free (tooltip);
	}

	g_object_unref (source);

exit:
	if (info != NULL)
		camel_folder_free_message_info (folder, info);

	g_clear_object (&folder);
	g_free (notify_to);
}

static void
mdn_message_seen_cb (EMailReader *reader,
                     const gchar *message_uid,
                     CamelMimeMessage *message)
{
	ESource *source;
	ESourceMDN *extension;
	ESourceRegistry *registry;
	EMailBackend *backend;
	EMailSession *session;
	CamelFolder *folder;
	CamelMessageInfo *info;
	EMdnResponsePolicy response_policy;
	const gchar *extension_name;
	gchar *notify_to = NULL;

	backend = e_mail_reader_get_backend (reader);
	session = e_mail_backend_get_session (backend);
	registry = e_mail_session_get_registry (session);

	folder = e_mail_reader_ref_folder (reader);

	info = camel_folder_get_message_info (folder, message_uid);
	if (info == NULL)
		goto exit;

	if (camel_message_info_user_flag (info, MDN_USER_FLAG))
		goto exit;

	notify_to = mdn_get_notify_to (message);
	if (notify_to == NULL)
		goto exit;

	/* This returns a new ESource reference. */
	source = em_utils_guess_mail_identity_with_recipients (
		registry, message, folder, message_uid);
	if (source == NULL)
		goto exit;

	extension_name = E_SOURCE_EXTENSION_MDN;
	extension = e_source_get_extension (source, extension_name);
	response_policy = e_source_mdn_get_response_policy (extension);

	if (response_policy == E_MDN_RESPONSE_POLICY_ALWAYS)
		mdn_notify_sender (
			source, reader, folder,
			message, info, notify_to,
			MDN_ACTION_MODE_AUTOMATIC,
			MDN_SENDING_MODE_AUTOMATIC);

	g_object_unref (source);

exit:
	if (info != NULL)
		camel_folder_free_message_info (folder, info);

	g_clear_object (&folder);
	g_free (notify_to);
}

static void
mdn_constructed (GObject *object)
{
	EExtension *extension;
	EExtensible *extensible;

	extension = E_EXTENSION (object);
	extensible = e_extension_get_extensible (extension);
	g_return_if_fail (E_IS_MAIL_READER (extensible));

	g_signal_connect (
		extensible, "changed",
		G_CALLBACK (mdn_mail_reader_changed_cb), extension);

	g_signal_connect (
		extensible, "message-loaded",
		G_CALLBACK (mdn_message_loaded_cb), extension);

	g_signal_connect (
		extensible, "message-seen",
		G_CALLBACK (mdn_message_seen_cb), NULL);

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

static void
mdn_dispose (GObject *object)
{
	mdn_remove_alert (E_MDN (object));

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

static void
e_mdn_class_init (EMdnClass *class)
{
	GObjectClass *object_class;
	EExtensionClass *extension_class;

	object_class = G_OBJECT_CLASS (class);
	object_class->constructed = mdn_constructed;
	object_class->dispose = mdn_dispose;

	extension_class = E_EXTENSION_CLASS (class);
	extension_class->extensible_type = E_TYPE_MAIL_READER;
}

static void
e_mdn_class_finalize (EMdnClass *class)
{
}

static void
e_mdn_init (EMdn *extension)
{
}

G_MODULE_EXPORT void
e_module_load (GTypeModule *type_module)
{
	e_mdn_register_type (type_module);
}

G_MODULE_EXPORT void
e_module_unload (GTypeModule *type_module)
{
}