/*
 * evolution-spamassassin.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 <sys/types.h>
#include <sys/wait.h>
#include <glib/gi18n-lib.h>

#include <camel/camel.h>

#include <shell/e-shell.h>
#include <e-util/e-mktemp.h>
#include <e-util/gconf-bridge.h>
#include <mail/e-mail-junk-filter.h>

/* Standard GObject macros */
#define E_TYPE_SPAM_ASSASSIN \
	(e_spam_assassin_get_type ())
#define E_SPAM_ASSASSIN(obj) \
	(G_TYPE_CHECK_INSTANCE_CAST \
	((obj), E_TYPE_SPAM_ASSASSIN, ESpamAssassin))

#ifndef SPAMASSASSIN_BINARY
#define SPAMASSASSIN_BINARY "/usr/bin/spamassassin"
#endif

#ifndef SA_LEARN_BINARY
#define SA_LEARN_BINARY "/usr/bin/sa-learn"
#endif

#ifndef SPAMC_BINARY
#define SPAMC_BINARY "/usr/bin/spamc"
#endif

#ifndef SPAMD_BINARY
#define SPAMD_BINARY "/usr/bin/spamd"
#endif

/* For starting our own daemon. */
#define DAEMON_MAX_RETRIES 100
#define DAEMON_RETRY_DELAY 0.05  /* seconds */

#define SPAM_ASSASSIN_EXIT_STATUS_SUCCESS	0
#define SPAM_ASSASSIN_EXIT_STATUS_ERROR		-1

typedef struct _ESpamAssassin ESpamAssassin;
typedef struct _ESpamAssassinClass ESpamAssassinClass;

struct _ESpamAssassin {
	EMailJunkFilter parent;

	GMutex *socket_path_mutex;

	gchar *pid_file;
	gchar *socket_path;
	gchar *spamc_binary;
	gchar *spamd_binary;
	gint version;

	gboolean local_only;
	gboolean use_daemon;
	gboolean version_set;

	/* spamc/spamd state */
	gboolean spamd_tested;
	gboolean spamd_using_allow_tell;
	gboolean system_spamd_available;
	gboolean use_spamc;
};

struct _ESpamAssassinClass {
	EMailJunkFilterClass parent_class;
};

enum {
	PROP_0,
	PROP_LOCAL_ONLY,
	PROP_SPAMC_BINARY,
	PROP_SPAMD_BINARY,
	PROP_SOCKET_PATH,
	PROP_USE_DAEMON
};

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

/* Forward Declarations */
GType e_spam_assassin_get_type (void);
static void e_spam_assassin_interface_init (CamelJunkFilterInterface *interface);

G_DEFINE_DYNAMIC_TYPE_EXTENDED (
	ESpamAssassin,
	e_spam_assassin,
	E_TYPE_MAIL_JUNK_FILTER, 0,
	G_IMPLEMENT_INTERFACE_DYNAMIC (
		CAMEL_TYPE_JUNK_FILTER,
		e_spam_assassin_interface_init))

#ifdef G_OS_UNIX
static void
spam_assassin_cancelled_cb (GCancellable *cancellable,
                            GPid *pid)
{
	/* XXX On UNIX-like systems we can safely assume a GPid is the
	 *     process ID and use it to terminate the process via signal. */
	kill (*pid, SIGTERM);
}
#endif

static void
spam_assassin_exited_cb (GPid *pid,
                         gint status,
                         gpointer user_data)
{
	struct {
		GMainLoop *loop;
		gint exit_code;
	} *source_data = user_data;

	if (WIFEXITED (status))
		source_data->exit_code = WEXITSTATUS (status);
	else
		source_data->exit_code = SPAM_ASSASSIN_EXIT_STATUS_ERROR;

	g_main_loop_quit (source_data->loop);
}

static gint
spam_assassin_command_full (const gchar **argv,
                            CamelMimeMessage *message,
                            const gchar *input_data,
                            GByteArray *output_buffer,
                            gboolean wait_for_termination,
                            GCancellable *cancellable,
                            GError **error)
{
	GMainContext *context;
	GSpawnFlags flags = 0;
	GSource *source;
	GPid child_pid;
	gint standard_input;
	gint standard_output;
	gulong handler_id = 0;
	gboolean success;

	struct {
		GMainLoop *loop;
		gint exit_code;
	} source_data;

	if (wait_for_termination)
		flags |= G_SPAWN_DO_NOT_REAP_CHILD;
	if (output_buffer == NULL)
		flags |= G_SPAWN_STDOUT_TO_DEV_NULL;
	flags |= G_SPAWN_STDERR_TO_DEV_NULL;

	/* Spawn SpamAssassin with an open stdin pipe. */
	success = g_spawn_async_with_pipes (
		NULL,
		(gchar **) argv,
		NULL,
		flags,
		NULL, NULL,
		&child_pid,
		&standard_input,
		(output_buffer != NULL) ? &standard_output : NULL,
		NULL,
		error);

	if (!success) {
		gchar *command_line;

		command_line = g_strjoinv (" ", (gchar **) argv);
		g_prefix_error (
			error, _("Failed to spawn SpamAssassin (%s): "),
			command_line);
		g_free (command_line);

		return SPAM_ASSASSIN_EXIT_STATUS_ERROR;
	}

	if (message != NULL) {
		CamelStream *stream;
		gssize bytes_written;

		/* Stream the CamelMimeMessage to SpamAssassin. */
		stream = camel_stream_fs_new_with_fd (standard_input);
		bytes_written = camel_data_wrapper_write_to_stream_sync (
			CAMEL_DATA_WRAPPER (message),
			stream, cancellable, error);
		success = (bytes_written >= 0) &&
			(camel_stream_close (stream, cancellable, error) == 0);
		g_object_unref (stream);

		if (!success) {
			g_spawn_close_pid (child_pid);
			g_prefix_error (
				error, _("Failed to stream mail "
				"message content to SpamAssassin: "));
			return SPAM_ASSASSIN_EXIT_STATUS_ERROR;
		}

	} else if (input_data != NULL) {
		gssize bytes_written;

		/* Write raw data directly to SpamAssassin. */
		bytes_written = camel_write (
			standard_input, input_data,
			strlen (input_data), cancellable, error);
		success = (bytes_written >= 0);

		close (standard_input);

		if (!success) {
			g_spawn_close_pid (child_pid);
			g_prefix_error (
				error, _("Failed to write '%s' "
				"to SpamAssassin: "), input_data);
			return SPAM_ASSASSIN_EXIT_STATUS_ERROR;
		}
	}

	if (output_buffer != NULL) {
		CamelStream *input_stream;
		CamelStream *output_stream;
		gssize bytes_written;

		input_stream = camel_stream_fs_new_with_fd (standard_output);

		output_stream = camel_stream_mem_new ();
		camel_stream_mem_set_byte_array (
			CAMEL_STREAM_MEM (output_stream), output_buffer);

		bytes_written = camel_stream_write_to_stream (
			input_stream, output_stream, cancellable, error);
		g_byte_array_append (output_buffer, (guint8 *) "", 1);
		success = (bytes_written >= 0);

		g_object_unref (input_stream);
		g_object_unref (output_stream);

		if (!success) {
			g_spawn_close_pid (child_pid);
			g_prefix_error (
				error, _("Failed to read "
				"output from SpamAssassin: "));
			return SPAM_ASSASSIN_EXIT_STATUS_ERROR;
		}
	}

	/* XXX I'm not sure if we should call g_spawn_close_pid()
	 *     here or not.  Only really matters on Windows anyway. */
	if (!wait_for_termination)
		return 0;

	/* Wait for the SpamAssassin process to terminate
	 * using GLib's main loop for better portability. */

	context = g_main_context_new ();

	source = g_child_watch_source_new (child_pid);
	g_source_set_callback (
		source, (GSourceFunc)
		spam_assassin_exited_cb,
		&source_data, NULL);
	g_source_attach (source, context);
	g_source_unref (source);

	source_data.loop = g_main_loop_new (context, TRUE);
	source_data.exit_code = 0;

#ifdef G_OS_UNIX
	if (G_IS_CANCELLABLE (cancellable))
		handler_id = g_cancellable_connect (
			cancellable,
			G_CALLBACK (spam_assassin_cancelled_cb),
			&child_pid, (GDestroyNotify) NULL);
#endif

	g_main_loop_run (source_data.loop);

	if (handler_id > 0)
		g_cancellable_disconnect (cancellable, handler_id);

	g_main_loop_unref (source_data.loop);
	source_data.loop = NULL;

	g_main_context_unref (context);

	/* Clean up. */

	g_spawn_close_pid (child_pid);

	if (g_cancellable_set_error_if_cancelled (cancellable, error))
		source_data.exit_code = SPAM_ASSASSIN_EXIT_STATUS_ERROR;

	else if (source_data.exit_code == SPAM_ASSASSIN_EXIT_STATUS_ERROR)
		g_set_error_literal (
			error, CAMEL_ERROR, CAMEL_ERROR_GENERIC,
			_("SpamAssassin either crashed or "
			  "failed to process a mail message"));

	return source_data.exit_code;
}

static gint
spam_assassin_command (const gchar **argv,
                       CamelMimeMessage *message,
                       const gchar *input_data,
                       GCancellable *cancellable,
                       GError **error)
{
	return spam_assassin_command_full (
		argv, message, input_data, NULL, TRUE, cancellable, error);
}

static gboolean
spam_assassin_get_local_only (ESpamAssassin *extension)
{
	return extension->local_only;
}

static void
spam_assassin_set_local_only (ESpamAssassin *extension,
                              gboolean local_only)
{
	extension->local_only = local_only;

	g_object_notify (G_OBJECT (extension), "local-only");
}

static const gchar *
spam_assassin_get_spamc_binary (ESpamAssassin *extension)
{
	return extension->spamc_binary;
}

static void
spam_assassin_set_spamc_binary (ESpamAssassin *extension,
                                const gchar *spamc_binary)
{
	g_free (extension->spamc_binary);
	extension->spamc_binary = g_strdup (spamc_binary);

	g_object_notify (G_OBJECT (extension), "spamc-binary");
}

static const gchar *
spam_assassin_get_spamd_binary (ESpamAssassin *extension)
{
	return extension->spamd_binary;
}

static void
spam_assassin_set_spamd_binary (ESpamAssassin *extension,
                                const gchar *spamd_binary)
{
	g_free (extension->spamd_binary);
	extension->spamd_binary = g_strdup (spamd_binary);

	g_object_notify (G_OBJECT (extension), "spamd-binary");
}

static const gchar *
spam_assassin_get_socket_path (ESpamAssassin *extension)
{
	return extension->socket_path;
}

static void
spam_assassin_set_socket_path (ESpamAssassin *extension,
                               const gchar *socket_path)
{
	g_free (extension->socket_path);
	extension->socket_path = g_strdup (socket_path);

	g_object_notify (G_OBJECT (extension), "socket-path");
}

static gboolean
spam_assassin_get_use_daemon (ESpamAssassin *extension)
{
	return extension->use_daemon;
}

static void
spam_assassin_set_use_daemon (ESpamAssassin *extension,
                              gboolean use_daemon)
{
	extension->use_daemon = use_daemon;

	g_object_notify (G_OBJECT (extension), "use-daemon");
}

static gboolean
spam_assassin_get_version (ESpamAssassin *extension,
                           gint *spam_assassin_version,
                           GCancellable *cancellable,
                           GError **error)
{
	GByteArray *output_buffer;
	gint exit_code;
	guint ii;

	const gchar *argv[] = {
		SA_LEARN_BINARY,
		"--version",
		NULL
	};

	if (extension->version_set) {
		if (spam_assassin_version != NULL)
			*spam_assassin_version = extension->version;
		return TRUE;
	}

	output_buffer = g_byte_array_new ();

	exit_code = spam_assassin_command_full (
		argv, NULL, NULL, output_buffer, TRUE, cancellable, error);

	if (exit_code != 0) {
		g_byte_array_free (output_buffer, TRUE);
		return FALSE;
	}

	for (ii = 0; ii < output_buffer->len; ii++) {
		if (g_ascii_isdigit (output_buffer->data[ii])) {
			guint8 ch = output_buffer->data[ii];
			extension->version = (ch - '0');
			extension->version_set = TRUE;
			break;
		}
	}

	if (spam_assassin_version != NULL)
		*spam_assassin_version = extension->version;

	g_byte_array_free (output_buffer, TRUE);

	return TRUE;
}

static void
spam_assassin_test_spamd_allow_tell (ESpamAssassin *extension)
{
	gint exit_code;
	GError *error = NULL;

	const gchar *argv[] = {
		SPAMC_BINARY,
		"--learntype=forget",
		NULL
	};

	/* Check if spamd is running with --allow-tell. */

	exit_code = spam_assassin_command (argv, NULL, "\n", NULL, &error);
	extension->spamd_using_allow_tell = (exit_code == 0);

	if (error != NULL) {
		g_warning ("%s", error->message);
		g_error_free (error);
	}
}

static gboolean
spam_assassin_test_spamd_running (ESpamAssassin *extension,
                                  gboolean system_spamd)
{
	const gchar *argv[5];
	gint exit_code;
	gint ii = 0;
	GError *error = NULL;

	g_mutex_lock (extension->socket_path_mutex);

	argv[ii++] = extension->spamc_binary;
	argv[ii++] = "--no-safe-fallback";
	if (!system_spamd) {
		argv[ii++] = "--socket";
		argv[ii++] = extension->socket_path;
	}
	argv[ii] = NULL;

	g_assert (ii < G_N_ELEMENTS (argv));

	exit_code = spam_assassin_command (
		argv, NULL, "From test@127.0.0.1", NULL, &error);

	if (error != NULL) {
		g_warning ("%s", error->message);
		g_error_free (error);
	}

	g_mutex_unlock (extension->socket_path_mutex);

	return (exit_code == 0);
}

static void
spam_assassin_kill_our_own_daemon (ESpamAssassin *extension)
{
	gint pid;
	gchar *contents = NULL;
	GError *error = NULL;

	g_mutex_lock (extension->socket_path_mutex);

	g_free (extension->socket_path);
	extension->socket_path = NULL;

	g_mutex_unlock (extension->socket_path_mutex);

	if (extension->pid_file == NULL)
		return;

	g_file_get_contents (extension->pid_file, &contents, NULL, &error);

	if (error != NULL) {
		g_warn_if_fail (contents == NULL);
		g_warning ("%s", error->message);
		g_error_free (error);
		return;
	}

	g_return_if_fail (contents != NULL);

	pid = atoi (contents);
	g_free (contents);

	if (pid > 0 && kill (pid, SIGTERM) == 0)
		waitpid (pid, NULL, 0);
}

static void
spam_assassin_prepare_for_quit (EShell *shell,
                                EActivity *activity,
                                ESpamAssassin *extension)
{
	spam_assassin_kill_our_own_daemon (extension);
}

static gboolean
spam_assassin_start_our_own_daemon (ESpamAssassin *extension)
{
	const gchar *argv[8];
	gchar *pid_file;
	gchar *socket_path;
	gboolean started = FALSE;
	gint exit_code;
	gint ii = 0;
	GError *error = NULL;

	g_mutex_lock (extension->socket_path_mutex);

	pid_file = e_mktemp ("spamd-pid-file-XXXXXX");
	socket_path = e_mktemp ("spamd-socket-path-XXXXXX");

	argv[ii++] = extension->spamd_binary;
	argv[ii++] = "--socketpath";
	argv[ii++] = socket_path;

	if (spam_assassin_get_local_only (extension))
		argv[ii++] = "--local";

	argv[ii++] = "--max-children=1";
	argv[ii++] = "--pidfile";
	argv[ii++] = pid_file;
	argv[ii] = NULL;

	g_assert (ii < G_N_ELEMENTS (argv));

	exit_code = spam_assassin_command_full (
		argv, NULL, NULL, NULL, FALSE, NULL, &error);

	if (error != NULL) {
		g_warning ("%s", error->message);
		g_error_free (error);
		goto exit;
	}

	if (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS) {
		/* Wait for the socket path to appear. */
		for (ii = 0; ii < DAEMON_MAX_RETRIES; ii++) {
			if (g_file_test (socket_path, G_FILE_TEST_EXISTS)) {
				started = TRUE;
				break;
			}
			g_usleep (DAEMON_RETRY_DELAY * G_USEC_PER_SEC);
		}
	}

	/* Set these directly to avoid emitting "notify" signals. */
	if (started) {
		g_free (extension->pid_file);
		extension->pid_file = pid_file;
		pid_file = NULL;

		g_free (extension->socket_path);
		extension->socket_path = socket_path;
		socket_path = NULL;

		/* XXX EMailSession is too prone to reference leaks to leave
		 *     this for our finalize() method.  We want to be sure to
		 *     kill the spamd process we started when Evolution shuts
		 *     down, so connect to an EShell signal instead. */
		g_signal_connect (
			e_shell_get_default (), "prepare-for-quit",
			G_CALLBACK (spam_assassin_prepare_for_quit),
			extension);
	}

exit:
	g_free (pid_file);
	g_free (socket_path);

	g_mutex_unlock (extension->socket_path_mutex);

	return started;
}

static void
spam_assassin_test_spamd (ESpamAssassin *extension)
{
	const gchar *spamd_binary;
	gboolean try_system_spamd;

	/* XXX SpamAssassin could really benefit from a D-Bus interface
	 *     these days.  These tests are just needlessly painful for
	 *     clients trying to talk to an already-running spamd. */

	extension->use_spamc = FALSE;
	spamd_binary = extension->spamd_binary;
	try_system_spamd = (g_strcmp0 (spamd_binary, SPAMD_BINARY) == 0);

	if (extension->local_only && try_system_spamd) {
		gint exit_code;

		/* Run a shell command to check for a running
		 * spamd process with a -L/--local option or a
		 * -p/--port option. */

		const gchar *argv[] = {
			"/bin/sh",
			"-c",
			"ps ax | grep -v grep | "
			"grep -E 'spamd.*(\\-L|\\-\\-local)' | "
			"grep -E -v '\\ \\-p\\ |\\ \\-\\-port\\ '",
			NULL
		};

		exit_code = spam_assassin_command (
			argv, NULL, NULL, NULL, NULL);
		try_system_spamd = (exit_code == 0);
	}

	/* Try to use the system spamd first. */
	if (try_system_spamd) {
		if (spam_assassin_test_spamd_running (extension, TRUE)) {
			extension->use_spamc = TRUE;
			extension->system_spamd_available = TRUE;
		}
	}

	/* If there's no system spamd running, try
	 * to use one with a user specified socket. */
	if (!extension->use_spamc && extension->socket_path != NULL) {
		if (spam_assassin_test_spamd_running (extension, FALSE)) {
			extension->use_spamc = TRUE;
			extension->system_spamd_available = FALSE;
		}
	}

	/* Still unsuccessful?  Try to start our own spamd. */
	if (!extension->use_spamc) {
		extension->use_spamc =
			spam_assassin_start_our_own_daemon (extension) &&
			spam_assassin_test_spamd_running (extension, FALSE);
	}
}

static void
spam_assassin_set_property (GObject *object,
                            guint property_id,
                            const GValue *value,
                            GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_LOCAL_ONLY:
			spam_assassin_set_local_only (
				E_SPAM_ASSASSIN (object),
				g_value_get_boolean (value));
			return;

		case PROP_SPAMC_BINARY:
			spam_assassin_set_spamc_binary (
				E_SPAM_ASSASSIN (object),
				g_value_get_string (value));
			return;

		case PROP_SPAMD_BINARY:
			spam_assassin_set_spamd_binary (
				E_SPAM_ASSASSIN (object),
				g_value_get_string (value));
			return;

		case PROP_SOCKET_PATH:
			spam_assassin_set_socket_path (
				E_SPAM_ASSASSIN (object),
				g_value_get_string (value));
			return;

		case PROP_USE_DAEMON:
			spam_assassin_set_use_daemon (
				E_SPAM_ASSASSIN (object),
				g_value_get_boolean (value));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
spam_assassin_get_property (GObject *object,
                            guint property_id,
                            GValue *value,
                            GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_LOCAL_ONLY:
			g_value_set_boolean (
				value, spam_assassin_get_local_only (
				E_SPAM_ASSASSIN (object)));
			return;

		case PROP_SPAMC_BINARY:
			g_value_set_string (
				value, spam_assassin_get_spamc_binary (
				E_SPAM_ASSASSIN (object)));
			return;

		case PROP_SPAMD_BINARY:
			g_value_set_string (
				value, spam_assassin_get_spamd_binary (
				E_SPAM_ASSASSIN (object)));
			return;

		case PROP_SOCKET_PATH:
			g_value_set_string (
				value, spam_assassin_get_socket_path (
				E_SPAM_ASSASSIN (object)));
			return;

		case PROP_USE_DAEMON:
			g_value_set_boolean (
				value, spam_assassin_get_use_daemon (
				E_SPAM_ASSASSIN (object)));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
spam_assassin_finalize (GObject *object)
{
	ESpamAssassin *extension = E_SPAM_ASSASSIN (object);

	g_mutex_free (extension->socket_path_mutex);

	g_free (extension->pid_file);
	g_free (extension->socket_path);
	g_free (extension->spamc_binary);
	g_free (extension->spamd_binary);

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

static gboolean
spam_assassin_available (EMailJunkFilter *junk_filter)
{
	ESpamAssassin *extension = E_SPAM_ASSASSIN (junk_filter);
	gboolean available;
	GError *error = NULL;

	available = spam_assassin_get_version (extension, NULL, NULL, &error);

	/* XXX These tests block like crazy so maybe this isn't the best
	 *     place to be doing this, but the first available() call is
	 *     done at startup before the UI is shown.  So hopefully the
	 *     delay will not be noticeable. */
	if (available && extension->use_daemon && !extension->spamd_tested) {
		extension->spamd_tested = TRUE;
		spam_assassin_test_spamd (extension);
		spam_assassin_test_spamd_allow_tell (extension);
	}

	if (error != NULL) {
		g_warning ("%s", error->message);
		g_error_free (error);
	}

	return available;
}

static GtkWidget *
spam_assassin_new_config_widget (EMailJunkFilter *junk_filter)
{
	GtkWidget *box;
	GtkWidget *widget;
	GtkWidget *container;
	gchar *markup;

	box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);

	markup = g_markup_printf_escaped (
		"<b>%s</b>", _("SpamAssassin Options"));
	widget = gtk_label_new (markup);
	gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5);
	gtk_label_set_use_markup (GTK_LABEL (widget), TRUE);
	gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0);
	gtk_widget_show (widget);
	g_free (markup);

	widget = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
	gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0);
	gtk_widget_show (widget);

	container = widget;

	widget = gtk_check_button_new_with_mnemonic (
		_("I_nclude remote tests"));
	gtk_widget_set_margin_left (widget, 12);
	gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0);
	gtk_widget_show (widget);

	g_object_bind_property (
		junk_filter, "local-only",
		widget, "active",
		G_BINDING_BIDIRECTIONAL |
		G_BINDING_SYNC_CREATE |
		G_BINDING_INVERT_BOOLEAN);

	markup = g_markup_printf_escaped (
		"<small>%s</small>",
		_("This will make SpamAssassin more reliable, but slower."));
	widget = gtk_label_new (markup);
	gtk_widget_set_margin_left (widget, 36);
	gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5);
	gtk_label_set_use_markup (GTK_LABEL (widget), TRUE);
	gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0);
	gtk_widget_show (widget);
	g_free (markup);

	return box;
}

static gboolean
spam_assassin_classify (CamelJunkFilter *junk_filter,
                        CamelMimeMessage *message,
                        CamelJunkStatus *status,
                        GCancellable *cancellable,
                        GError **error)
{
	ESpamAssassin *extension = E_SPAM_ASSASSIN (junk_filter);
	const gchar *argv[7];
	gboolean using_spamc;
	gint exit_code;
	gint ii = 0;

	g_mutex_lock (extension->socket_path_mutex);

	using_spamc = (extension->use_spamc && extension->use_daemon);

	if (using_spamc) {
		argv[ii++] = extension->spamc_binary;
		argv[ii++] = "--check";
		argv[ii++] = "--timeout=60";
		if (!extension->system_spamd_available) {
			argv[ii++] = "--socket";
			argv[ii++] = extension->socket_path;
		}
	} else {
		argv[ii++] = SPAMASSASSIN_BINARY;
		argv[ii++] = "--exit-code";
		if (extension->local_only)
			argv[ii++] = "--local";
	}
	argv[ii] = NULL;

	g_assert (ii < G_N_ELEMENTS (argv));

	exit_code = spam_assassin_command (
		argv, message, NULL, cancellable, error);

	/* For either program, exit code 0 means the message is ham. */
	if (exit_code == 0)
		*status = CAMEL_JUNK_STATUS_MESSAGE_IS_NOT_JUNK;

	/* spamassassin(1) only specifies zero and non-zero exit codes. */
	else if (!using_spamc)
		*status = CAMEL_JUNK_STATUS_MESSAGE_IS_JUNK;

	/* Whereas spamc(1) explicitly states exit code 1 means spam. */
	else if (exit_code == 1)
		*status = CAMEL_JUNK_STATUS_MESSAGE_IS_JUNK;

	/* Consider any other spamc(1) exit code to be inconclusive
	 * since it most likely failed to process the message. */
	else
		*status = CAMEL_JUNK_STATUS_INCONCLUSIVE;

	/* Check that the return value and GError agree. */
	if (exit_code != SPAM_ASSASSIN_EXIT_STATUS_ERROR)
		g_warn_if_fail (error == NULL || *error == NULL);
	else
		g_warn_if_fail (error == NULL || *error != NULL);

	g_mutex_unlock (extension->socket_path_mutex);

	return (exit_code != SPAM_ASSASSIN_EXIT_STATUS_ERROR);
}

static gboolean
spam_assassin_learn_junk (CamelJunkFilter *junk_filter,
                          CamelMimeMessage *message,
                          GCancellable *cancellable,
                          GError **error)
{
	ESpamAssassin *extension = E_SPAM_ASSASSIN (junk_filter);
	const gchar *argv[5];
	gint exit_code;
	gint ii = 0;

	if (extension->spamd_using_allow_tell) {
		argv[ii++] = extension->spamc_binary;
		argv[ii++] = "--learntype=spam";
	} else {
		argv[ii++] = SA_LEARN_BINARY;
		argv[ii++] = "--spam";
		if (extension->version >= 3)
			argv[ii++] = "--no-sync";
		else
			argv[ii++] = "--no-rebuild";
		if (extension->local_only)
			argv[ii++] = "--local";
	}
	argv[ii] = NULL;

	g_assert (ii < G_N_ELEMENTS (argv));

	exit_code = spam_assassin_command (
		argv, message, NULL, cancellable, error);

	/* Check that the return value and GError agree. */
	if (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS)
		g_warn_if_fail (error == NULL || *error == NULL);
	else
		g_warn_if_fail (error == NULL || *error != NULL);

	return (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS);
}

static gboolean
spam_assassin_learn_not_junk (CamelJunkFilter *junk_filter,
                              CamelMimeMessage *message,
                              GCancellable *cancellable,
                              GError **error)
{
	ESpamAssassin *extension = E_SPAM_ASSASSIN (junk_filter);
	const gchar *argv[5];
	gint exit_code;
	gint ii = 0;

	if (extension->spamd_using_allow_tell) {
		argv[ii++] = extension->spamc_binary;
		argv[ii++] = "--learntype=ham";
	} else {
		argv[ii++] = SA_LEARN_BINARY;
		argv[ii++] = "--ham";
		if (extension->version >= 3)
			argv[ii++] = "--no-sync";
		else
			argv[ii++] = "--no-rebuild";
		if (extension->local_only)
			argv[ii++] = "--local";
	}
	argv[ii] = NULL;

	g_assert (ii < G_N_ELEMENTS (argv));

	exit_code = spam_assassin_command (
		argv, message, NULL, cancellable, error);

	/* Check that the return value and GError agree. */
	if (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS)
		g_warn_if_fail (error == NULL || *error == NULL);
	else
		g_warn_if_fail (error == NULL || *error != NULL);

	return (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS);
}

static gboolean
spam_assassin_synchronize (CamelJunkFilter *junk_filter,
                           GCancellable *cancellable,
                           GError **error)
{
	ESpamAssassin *extension = E_SPAM_ASSASSIN (junk_filter);
	const gchar *argv[4];
	gint exit_code;
	gint ii = 0;

	/* If we're using a spamd that allows learning,
	 * there's no need to synchronize anything. */
	if (extension->spamd_using_allow_tell)
		return TRUE;

	argv[ii++] = SA_LEARN_BINARY;
	if (extension->version >= 3)
		argv[ii++] = "--sync";
	else
		argv[ii++] = "--rebuild";
	if (extension->local_only)
		argv[ii++] = "--local";
	argv[ii] = NULL;

	g_assert (ii < G_N_ELEMENTS (argv));

	exit_code = spam_assassin_command (
		argv, NULL, NULL, cancellable, error);

	/* Check that the return value and GError agree. */
	if (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS)
		g_warn_if_fail (error == NULL || *error == NULL);
	else
		g_warn_if_fail (error == NULL || *error != NULL);

	return (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS);
}

static void
e_spam_assassin_class_init (ESpamAssassinClass *class)
{
	GObjectClass *object_class;
	EMailJunkFilterClass *junk_filter_class;

	object_class = G_OBJECT_CLASS (class);
	object_class->set_property = spam_assassin_set_property;
	object_class->get_property = spam_assassin_get_property;
	object_class->finalize = spam_assassin_finalize;

	junk_filter_class = E_MAIL_JUNK_FILTER_CLASS (class);
	junk_filter_class->filter_name = "SpamAssassin";
	junk_filter_class->display_name = _("SpamAssassin");
	junk_filter_class->available = spam_assassin_available;
	junk_filter_class->new_config_widget = spam_assassin_new_config_widget;

	/* XXX Argh, the boolean sense of the GConf key is inverted from
	 *     that of the checkbox widget.  The checkbox wording is more
	 *     natural, but GConfBridge doesn't support transform functions
	 *     so the property has to match the sense of the GConf key. */
	g_object_class_install_property (
		object_class,
		PROP_LOCAL_ONLY,
		g_param_spec_boolean (
			"local-only",
			"Local Only",
			"Do not use tests requiring DNS lookups",
			TRUE,
			G_PARAM_READWRITE));

	g_object_class_install_property (
		object_class,
		PROP_SPAMC_BINARY,
		g_param_spec_string (
			"spamc-binary",
			"spamc Binary",
			"File path for the spamc binary",
			NULL,
			G_PARAM_READWRITE));

	g_object_class_install_property (
		object_class,
		PROP_SPAMD_BINARY,
		g_param_spec_string (
			"spamd-binary",
			"spamd Binary",
			"File path for the spamd binary",
			NULL,
			G_PARAM_READWRITE));

	g_object_class_install_property (
		object_class,
		PROP_SOCKET_PATH,
		g_param_spec_string (
			"socket-path",
			"Socket Path",
			"Socket path for a SpamAssassin daemon",
			NULL,
			G_PARAM_READWRITE));

	g_object_class_install_property (
		object_class,
		PROP_USE_DAEMON,
		g_param_spec_boolean (
			"use-daemon",
			"Use Daemon",
			"Whether to use a SpamAssassin daemon",
			FALSE,
			G_PARAM_READWRITE));
}

static void
e_spam_assassin_class_finalize (ESpamAssassinClass *class)
{
}

static void
e_spam_assassin_interface_init (CamelJunkFilterInterface *interface)
{
	interface->classify = spam_assassin_classify;
	interface->learn_junk = spam_assassin_learn_junk;
	interface->learn_not_junk = spam_assassin_learn_not_junk;
	interface->synchronize = spam_assassin_synchronize;
}

static void
e_spam_assassin_init (ESpamAssassin *extension)
{
	extension->socket_path_mutex = g_mutex_new ();

	/* XXX Once we move to GSettings these probably don't
	 *     need to be properties anymore.  GConfBridge is
	 *     just easier to deal with than GConfClient. */

	gconf_bridge_bind_property (
		gconf_bridge_get (),
		"/apps/evolution/mail/junk/sa/local_only",
		G_OBJECT (extension), "local-only");

	gconf_bridge_bind_property (
		gconf_bridge_get (),
		"/apps/evolution/mail/junk/sa/spamc_binary",
		G_OBJECT (extension), "spamc-binary");

	gconf_bridge_bind_property (
		gconf_bridge_get (),
		"/apps/evolution/mail/junk/sa/spamd_binary",
		G_OBJECT (extension), "spamd-binary");

	gconf_bridge_bind_property (
		gconf_bridge_get (),
		"/apps/evolution/mail/junk/sa/socket_path",
		G_OBJECT (extension), "socket-path");

	gconf_bridge_bind_property (
		gconf_bridge_get (),
		"/apps/evolution/mail/junk/sa/use_daemon",
		G_OBJECT (extension), "use-daemon");

	if (extension->spamc_binary == NULL)
		extension->spamc_binary = g_strdup (SPAMC_BINARY);

	if (extension->spamd_binary == NULL)
		extension->spamd_binary = g_strdup (SPAMD_BINARY);
}

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

G_MODULE_EXPORT void
e_module_unload (GTypeModule *type_module)
{
}
