/*
 * Mail Notification
 * Copyright (C) 2003-2008 Jean-Yves Lefort <jylefort@brutele.be>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

%headertop{
#include <stdarg.h>
#include <time.h>
#include <libxml/tree.h>
#include "mn-decls.h"
%}

%h{
#define MN_MAILBOX_MAX_TYPES	8
extern GType mn_mailbox_types[MN_MAILBOX_MAX_TYPES + 1];

typedef struct _MNMailboxConfiguration MNMailboxConfiguration;
%}

%privateheader{
#include "mn-xml.h"

typedef enum
{
  /* load from mailboxes.xml */
  MN_MAILBOX_PARAM_LOAD		= MN_XML_PARAM_IMPORT,

  /* save to mailboxes.xml */
  MN_MAILBOX_PARAM_SAVE		= MN_XML_PARAM_EXPORT,

  /* load from and save to mailboxes.xml */
  MN_MAILBOX_PARAM_LOAD_SAVE	= MN_MAILBOX_PARAM_LOAD | MN_MAILBOX_PARAM_SAVE,

  /* ignore the case of a string property when checking if it has the default value */
  MN_MAILBOX_PARAM_IGNORE_CASE	= MN_XML_PARAM_IGNORE_CASE,

  /* require a string property to be non-empty */
  MN_MAILBOX_PARAM_REQUIRED	= 1 << (MN_XML_PARAM_USER_SHIFT + 0),
} MNMailboxParamFlags;
%}

%{
#include <glib/gi18n.h>
#include <libgnomevfs/gnome-vfs.h>
#if WITH_MBOX || WITH_MOZILLA || WITH_MH || WITH_MAILDIR || WITH_SYLPHEED
#include "mn-system-vfs-mailbox.h"
#include "mn-custom-vfs-mailbox.h"
#endif
#if WITH_POP3
#include "mn-pop3-mailbox.h"
#endif
#if WITH_IMAP
#include "mn-imap-mailbox.h"
#endif
#if WITH_GMAIL
#include "mn-gmail-mailbox.h"
#endif
#if WITH_YAHOO
#include "mn-yahoo-mailbox.h"
#endif
#if WITH_HOTMAIL
#include "mn-hotmail-mailbox.h"
#endif
#if WITH_EVOLUTION
#include "mn-evolution-mailbox.h"
#endif
#include "mn-util.h"
#include "mn-message.h"
#include "mn-conf.h"
#include "mn-locked-callback.h"
#include "mn-shell.h"

struct _MNMailboxConfiguration
{
  GType		type;
  unsigned int	n_parameters;
  GParameter	*parameters;
};

GType mn_mailbox_types[MN_MAILBOX_MAX_TYPES + 1];

typedef struct
{
  MNMailbox	*self;
  GHashTable	*messages;
  GHashTable	*messages_considered_as_read;
  gboolean	display_seen_mail;
} FilterMessagesInfo;

typedef struct
{
  GHashTable	*other;
  gboolean	changed;
} CompareMessagesInfo;

static unsigned int cleanup_messages_considered_as_read_idle_id = 0;
%}

class MN:Mailbox from G:Object (abstract)
{
  classwide const char *type;
  classwide int default_check_delay = -1;

  /*
   * If enabled, the base MNMailbox implementation of added() will
   * call mn_mailbox_enable_checking().
   */
  classwide gboolean enable_checking_when_added = TRUE;

  /*
   * Whether the mailbox is in the mailboxes list (a mailbox can be
   * removed from the mailboxes list without being finalized
   * immediately if a threaded check is in progress).
   *
   * Used for performance (testing it is faster than searching the
   * list) and for thread safety. Do not access directly, use the
   * accessors.
   */
  private gboolean _active;

  public gboolean
    get_active (self)
  {
    return g_atomic_int_get(&selfp->_active);
  }

  private void
    set_active (self, gboolean value)
  {
    g_atomic_int_set(&selfp->_active, value);
  }

  /**
   * added:
   * @self: the object which received the signal
   *
   * This signal gets emitted after the mailbox is added to the
   * mailboxes list.
   **/
  signal NONE (NONE)
    void added (self)
  {
    self_set_active(self, TRUE);

    if (SELF_GET_CLASS(self)->enable_checking_when_added)
      self_enable_checking(self);
  }

  /**
   * removed:
   * @self: the object which received the signal
   *
   * This signal gets emitted after the mailbox is removed from the
   * mailboxes list.
   **/
  signal NONE (NONE)
    void removed (self)
  {
    self_set_active(self, FALSE);

    mn_source_clear(&selfp->check_timeout_id);

    /*
     * Do not queue a cleanup of the messages-considered-as-read GConf
     * setting: it should not be done if the mailbox is only being
     * replaced (eg. from mn_mailbox_properties_dialog_apply()).
     *
     * If however the mailbox is being permanently removed, its
     * messages considered as read will be cleaned up the next time
     * another mailbox is checked, which is good enough.
     *
     * Note that we could queue a cleanup from here by adding and
     * testing a "gboolean replacing" signal parameter, but it is not
     * worth the effort.
     */
  }

  public char *runtime_name destroywith g_free;

  public char *name destroywith g_free;
  property STRING name (link, flags = MN_MAILBOX_PARAM_LOAD_SAVE);

  public char *open_command destroywith g_free;
  property STRING open_command (link, flags = MN_MAILBOX_PARAM_LOAD_SAVE);

  public char *mark_as_read_command destroywith g_free;
  property STRING mark_as_read_command (link, flags = MN_MAILBOX_PARAM_LOAD_SAVE);

  public char *mark_as_spam_command destroywith g_free;
  property STRING mark_as_spam_command (link, flags = MN_MAILBOX_PARAM_LOAD_SAVE);

  public char *delete_command destroywith g_free;
  property STRING delete_command (link, flags = MN_MAILBOX_PARAM_LOAD_SAVE);

  public char *stock_id destroywith g_free;
  property STRING stock_id (link, export);

  public char *format destroywith g_free;
  property STRING format (link, export);

  private unsigned int check_timeout_id;

  public int runtime_check_delay;

  public int check_delay;
  property INT check_delay (link,
			    flags = CONSTRUCT | MN_MAILBOX_PARAM_LOAD_SAVE,
			    default_value = -1);

  private gboolean poll = TRUE;
  property BOOLEAN poll (export)
    set
    {
      gboolean new_poll = g_value_get_boolean(VAL);

      /*
       * We do nothing unless the property has changed, because we do
       * not want to reset an already existing check timeout.
       */
      if (new_poll != selfp->poll)
	{
	  selfp->poll = new_poll;
	  if (self_get_active(self) && selfp->checking_enabled)
	    self_update_check_timeout(self);

	  g_object_notify(G_OBJECT(self), "manually-checkable");
	}
    }
    get
    {
      g_value_set_boolean(VAL, selfp->poll);
    };

  /*
   * Whether the user can initiate a check manually (for instance by
   * clicking on an Update menu item or by running mail-notification
   * --update).
   */
  property BOOLEAN manually_checkable (export)
    get
    {
      g_value_set_boolean(VAL, selfp->checking_enabled && selfp->poll);
    };

  private void
    update_check_timeout (self)
  {
    g_assert(self_get_active(self) == TRUE);
    g_assert(selfp->checking_enabled == TRUE);

    mn_source_clear(&selfp->check_timeout_id);
    if (selfp->poll && self->runtime_check_delay > 0)
      selfp->check_timeout_id = gdk_threads_add_timeout(self->runtime_check_delay * 1000, self_check_timeout_cb, self);
  }

  /* whether set_messages() has ever been called */
  private gboolean all_messages_set;

  /* all unread (unseen and seen) messages, indexed by id */
  private GHashTable *all_messages = {g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify) g_object_unref)} destroywith g_hash_table_destroy;

  /* all_messages, indexed by mid (message references held by all_message) */
  private GHashTable *all_messages_by_mid = {g_hash_table_new(g_str_hash, g_str_equal)} destroywith g_hash_table_destroy;

  /* all_messages after applying the "seen messages" filter, indexed by id */
  public GHashTable *messages = {g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify) g_object_unref)} destroywith g_hash_table_destroy;

  /* for performance */
  public time_t timestamp; /* timestamp of most recent message in self->messages */

  property POINTER messages (type = GHashTable *)
    get
    {
      g_value_set_pointer(VAL, self->messages);
    };

  protected void
    set_messages (self, GSList *messages)
  {
    GSList *l;

    selfp->all_messages_set = TRUE;

    g_hash_table_remove_all(selfp->all_messages);
    g_hash_table_remove_all(selfp->all_messages_by_mid);

    MN_LIST_FOREACH(l, messages)
      {
	MNMessage *message = l->data;

	g_hash_table_replace(selfp->all_messages, message->id, g_object_ref(message));

	if (message->mid)
	  /* do not ref message, it is owned by all_messages */
	  g_hash_table_replace(selfp->all_messages_by_mid, message->mid, message);
      }

    /*
     * Some messages might be gone, queue a cleanup of the
     * considered-as-read GConf setting.
     */
    self_queue_cleanup_messages_considered_as_read();

    self_filter_messages(self);
  }

  /**
   * filter_messages:
   * @self: the mailbox to act upon
   *
   * Filters @self->all_messages with the "seen mail" filter and the
   * considered-as-read GConf list, and stores the resulting set in
   * @self->messages. Additionally, if @self->messages has changed,
   * emits the "messages-changed" signal.
   **/
  private void
    filter_messages (self)
  {
    FilterMessagesInfo info;
    gboolean changed = FALSE;
    gboolean has_new = FALSE;

    /* filter messages */

    self->timestamp = 0;

    info.self = self;
    info.messages = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify) g_object_unref);
    info.messages_considered_as_read = mn_conf_get_string_hash_set(MN_CONF_MESSAGES_CONSIDERED_AS_READ);
    info.display_seen_mail = mn_conf_get_bool(MN_CONF_DISPLAY_SEEN_MAIL);

    g_hash_table_foreach(selfp->all_messages, (GHFunc) self_filter_messages_cb, &info);

    g_hash_table_destroy(info.messages_considered_as_read);

    /* detect changes */

    if (self_compare_messages(self->messages, info.messages))
      changed = TRUE;
    if (self_compare_messages(info.messages, self->messages))
      changed = has_new = TRUE;

    g_hash_table_destroy(self->messages);
    self->messages = info.messages;

    g_object_notify(G_OBJECT(self), "messages");

    if (changed)
      self_messages_changed(self, has_new);
  }

  private void
    filter_messages_cb (const char *id,
			MNMessage *message,
			FilterMessagesInfo *info)
  {
    Self *self = info->self;

    if (! info->display_seen_mail && (message->flags & MN_MESSAGE_NEW) == 0)
      return;

    if (g_hash_table_lookup(info->messages_considered_as_read, message->id))
      return;

    if (message->sent_time > self->timestamp)
      self->timestamp = message->sent_time;

    g_hash_table_replace(info->messages, message->id, g_object_ref(message));
  }

  /**
   * compare_messages:
   * @messages1: a %GHashTable containing %MNMessage objects
   * @messages2: another %GHashTable containing %MNMessage objects
   *
   * Compares @messages1 and @messages2.
   *
   * Return value: %TRUE if @messages1 contains messages not present
   * in @messages2, %FALSE otherwise.
   **/
  private gboolean
    compare_messages (GHashTable *messages1, GHashTable *messages2)
  {
    CompareMessagesInfo info;

    info.other = messages2;
    info.changed = FALSE;
    g_hash_table_foreach(messages1, (GHFunc) self_compare_messages_cb, &info);

    return info.changed;
  }

  private void
    compare_messages_cb (const char *id,
			 MNMessage *message,
			 CompareMessagesInfo *info)
  {
    if (! info->changed && ! g_hash_table_lookup(info->other, id))
      info->changed = TRUE;
  }

  private void
    queue_cleanup_messages_considered_as_read (void)
  {
    if (! cleanup_messages_considered_as_read_idle_id)
      cleanup_messages_considered_as_read_idle_id = gdk_threads_add_idle(self_cleanup_messages_considered_as_read_cb, NULL);
  }

  private gboolean
    cleanup_messages_considered_as_read_cb (gpointer data)
  {
    self_cleanup_messages_considered_as_read();

    cleanup_messages_considered_as_read_idle_id = 0;
    return FALSE;		/* remove source */
  }

  /*
   * Remove messages which no longer exist from the considered-as-read
   * GConf list.
   *
   * The primary goal is to ensure that a message which has been
   * opened or marked as read (these functions implicitly add the
   * message to the considered-as-read list) and then marked back as
   * unread in the mail reader will reappear in MN.
   *
   * The secondary goal is to ensure that the considered-as-list will
   * not grow forever.
   */
  private void
    cleanup_messages_considered_as_read (void)
  {
    GList *l;
    GHashTable *set;

    /*
     * If there is a mailbox which has not been successfully checked
     * yet, abort. Otherwise, the messages of that mailbox could be
     * mistakenly cleaned up.
     */
    MN_LIST_FOREACH(l, mn_shell->mailboxes->list)
      {
	MNMailbox *mailbox = l->data;

	if (! mailbox->_priv->all_messages_set)
	  return;
      }

    set = mn_conf_get_string_hash_set(MN_CONF_MESSAGES_CONSIDERED_AS_READ);

    if (g_hash_table_foreach_remove(set, (GHRFunc) self_cleanup_messages_considered_as_read_remove_cb, NULL))
      /* one or more messages were removed, reflect the changes */
      mn_conf_set_string_hash_set(MN_CONF_MESSAGES_CONSIDERED_AS_READ, set);

    g_hash_table_destroy(set);
  }

  private gboolean
    cleanup_messages_considered_as_read_remove_cb (const char *id,
						   gpointer value,
						   gpointer user_data)
  {
    GList *l;

    MN_LIST_FOREACH(l, mn_shell->mailboxes->list)
      {
	MNMailbox *mailbox = l->data;

	if (g_hash_table_lookup(mailbox->_priv->all_messages, id))
	  return FALSE;		/* message still exists, do not remove it */
      }

    return TRUE;		/* message no longer exists, remove it */
  }

  /**
   * messages-changed:
   * @self: the object which received the signal
   * @has_new: whether a new message has been received or not
   *
   * This signal gets emitted whenever the messages property changes.
   *
   * It is considered that the property changes if a new message
   * appears or if an old message disappears. If the contents of an
   * existing message change but the message keeps the same id, this
   * signal is not emitted (use the "notify::messages" signal if you
   * need notification of such events).
   **/
  signal private NONE (BOOLEAN)
    void messages_changed (self, gboolean has_new);

  public char *error destroywith g_free;
  property STRING error (link);

  protected void
    set_error (self, const char *format, ...)
    attr {G_GNUC_PRINTF(2, 3)}
  {
    char *error = NULL;

    if (format)
      MN_STRDUP_VPRINTF(error, format);

    g_object_set(G_OBJECT(self), MN_MAILBOX_PROP_ERROR(error), NULL);
    g_free(error);
  }

  public void
    init_types (void)
  {
    int i = 0;

#if WITH_MBOX || WITH_MOZILLA || WITH_MH || WITH_MAILDIR || WITH_SYLPHEED
    /*
     * MNSystemVFSMailbox must be registered before
     * MNCustomVFSMailbox, because the latter's parse_uri() method
     * will accept any URI.
     */
    mn_mailbox_types[i++] = MN_TYPE_SYSTEM_VFS_MAILBOX;
    mn_mailbox_types[i++] = MN_TYPE_CUSTOM_VFS_MAILBOX;
#endif
#if WITH_POP3
    mn_mailbox_types[i++] = MN_TYPE_POP3_MAILBOX;
#endif
#if WITH_IMAP
    mn_mailbox_types[i++] = MN_TYPE_IMAP_MAILBOX;
#endif
#if WITH_GMAIL
    mn_mailbox_types[i++] = MN_TYPE_GMAIL_MAILBOX;
#endif
#if WITH_YAHOO
    mn_mailbox_types[i++] = MN_TYPE_YAHOO_MAILBOX;
#endif
#if WITH_HOTMAIL
    mn_mailbox_types[i++] = MN_TYPE_HOTMAIL_MAILBOX;
#endif
#if WITH_EVOLUTION
    mn_mailbox_types[i++] = MN_TYPE_EVOLUTION_MAILBOX;
#endif
    mn_mailbox_types[i] = 0;
  }

  /* references the returned class */
  public MNMailboxClass *
    get_class_from_name (const char *type (check null))
  {
    int i;

    for (i = 0; mn_mailbox_types[i]; i++)
      {
	SelfClass *class;

	class = g_type_class_ref(mn_mailbox_types[i]);
	if (! strcmp(class->type, type))
	  return class;

	g_type_class_unref(class);
      }

    return NULL;
  }

  public GType
    get_type_from_name (const char *type (check null))
  {
    SelfClass *class;

    class = self_get_class_from_name(type);
    if (class)
      {
	GType gtype;

	gtype = G_OBJECT_CLASS_TYPE(class);
	g_type_class_unref(class);

	return gtype;
      }

    return 0;
  }

  init (self)
  {
    mn_g_object_gconf_notifications_add_gdk_locked(self,
						   MN_CONF_DISPLAY_SEEN_MAIL, self_notify_display_seen_messages_cb, self,
						   MN_CONF_MESSAGES_CONSIDERED_AS_READ, self_notify_messages_considered_as_read_cb, self,
						   NULL);
  }

  finalize (self)
  {
    /*
     * Even though we clear the source in removed(), it might have
     * been reinstalled afterwards (by a mn_mailbox_set_poll() call
     * from a check thread, etc).
     */
    if (selfp->check_timeout_id)
      g_source_remove(selfp->check_timeout_id);
  }

  private void
    notify_display_seen_messages_cb (GConfClient *client,
				     unsigned int cnxn_id,
				     GConfEntry *entry,
				     gpointer user_data)
  {
    Self *self = user_data;

    self_filter_messages(self);
  }

  private void
    notify_messages_considered_as_read_cb (GConfClient *client,
					   unsigned int cnxn_id,
					   GConfEntry *entry,
					   gpointer user_data)
  {
    Self *self = user_data;

    self_filter_messages(self);
  }

  public MNMailbox *
    new (const char *type (check null), ...)
    attr {G_GNUC_NULL_TERMINATED}
  {
    va_list args;
    GType type_id;
    const char *first_property_name;
    GObject *object;

    type_id = self_get_type_from_name(type);
    if (! type_id)
      return NULL;

    va_start(args, type);
    first_property_name = va_arg(args, const char *);
    object = g_object_new_valist(type_id, first_property_name, args);
    va_end(args);

    return SELF(object);
  }

  public MNMailbox *
    new_from_xml_node (xmlNode *node (check null), GError **err)
  {
    char *type;
    Self *self = NULL;

    type = xmlGetProp(node, "type");
    if (! type)
      {
	g_set_error(err, 0, 0, _("\"type\" attribute missing"));
	return NULL;
      }

    self = self_new(type, NULL);
    if (! self)
      {
	g_set_error(err, 0, 0, _("unknown mailbox type \"%s\""), type);
	goto end;
      }

    mn_xml_import_properties(G_OBJECT(self), node);

    if (! self_validate(self, err))
      {
	g_object_unref(self);
	self = NULL;
      }

  end:
    g_free(type);
    return self;
  }

  public xmlNode *
    xml_node_new (self)
  {
    xmlNode *node;

    node = xmlNewNode(NULL, "mailbox");
    xmlSetProp(node, "type", SELF_GET_CLASS(self)->type);
    mn_xml_export_properties(G_OBJECT(self), node);

    return node;
  }

  public MNMailbox *
    new_from_uri (const char *uri (check null))
  {
    int i;

    for (i = 0; mn_mailbox_types[i]; i++)
      {
	MNMailboxClass *class;
	MNMailbox *mailbox;

	class = g_type_class_ref(mn_mailbox_types[i]);
	mailbox = class->parse_uri ? class->parse_uri(NULL, uri) : NULL;
	g_type_class_unref(class);

	if (mailbox)
	  return mailbox;
      }

    return NULL;
  }

  public MNMailbox *
    new_from_configuration (MNMailboxConfiguration *config (check null))
  {
    return g_object_newv(config->type, config->n_parameters, config->parameters);
  }

  public MNMailboxConfiguration *
    get_configuration (self)
  {
    GObject *object = G_OBJECT(self);
    GArray *parameters;
    GParamSpec **properties;
    unsigned int n_properties;
    int i;
    MNMailboxConfiguration *config;

    parameters = g_array_new(FALSE, FALSE, sizeof(GParameter));

    properties = g_object_class_list_properties(G_OBJECT_GET_CLASS(self), &n_properties);
    for (i = 0; i < n_properties; i++)
      if ((properties[i]->flags & MN_MAILBOX_PARAM_SAVE) != 0)
	{
	  GParameter parameter = { NULL, { 0, } };

	  parameter.name = g_param_spec_get_name(properties[i]);

	  g_value_init(&parameter.value, G_PARAM_SPEC_VALUE_TYPE(properties[i]));
	  g_object_get_property(object, parameter.name, &parameter.value);

	  g_array_append_val(parameters, parameter);
	}
    g_free(properties);

    config = g_new0(MNMailboxConfiguration, 1);
    config->type = G_OBJECT_TYPE(self);
    config->n_parameters = parameters->len;
    config->parameters = (GParameter *) g_array_free(parameters, FALSE);

    return config;
  }

  public void
    configuration_free (MNMailboxConfiguration *config (check null))
  {
    int i;

    for (i = 0; i < config->n_parameters; i++)
      g_value_unset(&config->parameters[i].value);

    g_free(config->parameters);
    g_free(config);
  }

  public MNMailbox *
    new_from_obsolete_uri (const char *uri (check null))
  {
    char *real_uri;
    char *scheme;
    gboolean obsolete = FALSE;
    Self *self = NULL;

    real_uri = g_str_has_prefix(uri, "pop3:") /* also handle very old pop3 locators */
      ? g_strconcat("pop://", uri + 5, NULL)
      : g_strdup(uri);

    scheme = gnome_vfs_get_uri_scheme(real_uri);
    if (scheme)
      {
	if (! strcmp(scheme, "pop") || ! strcmp(scheme, "pops")
	    || ! strcmp(scheme, "imap") || ! strcmp(scheme, "imaps")
	    || ! strcmp(scheme, "gmail"))
	  obsolete = TRUE;
	g_free(scheme);
      }

    self = obsolete ? self_parse_obsolete_uri(real_uri) : self_new_from_uri(real_uri);
    g_free(real_uri);

    if (self && ! self_validate(self, NULL))
      {
	g_object_unref(self);
	self = NULL;
      }

    return self;
  }

  private MNMailbox *
    parse_obsolete_uri (const char *uri (check null))
  {
    int len;
    int buflen;
    char *scheme;
    char *username;
    char *password;
    char *authmech;
    char *hostname;
    int port;
    char *path;
    char **queries;
    Self *self = NULL;

    len = strlen(uri);
    buflen = len + 1;

    {
      char *pat;
      char scheme_buf[buflen];
      char auth_buf[buflen];
      char location_buf[buflen];
      char username_buf[buflen];
      char password_buf[buflen];
      char authmech_buf[buflen];
      char hostname_buf[buflen];
      int _port;
      char path_buf[buflen];
      char queries_buf[buflen];
      gboolean has_location = FALSE;
      gboolean has_password = FALSE;
      gboolean has_authmech = FALSE;
      gboolean has_port = FALSE;
      gboolean has_path = FALSE;
      gboolean has_queries = FALSE;
      int n;

      /* split URI in 3 parts: scheme, auth and location */

      pat = g_strdup_printf("%%%i[^:]://%%%i[^@]@%%%is", len, len, len);
      n = sscanf(uri, pat, scheme_buf, auth_buf, location_buf);
      g_free(pat);

      if (n >= 2)
	{
	  if (n == 3)
	    has_location = TRUE;
	}
      else
	return NULL;		/* unparsable */

      /* split auth part in 3 subparts: username, password and authmech */

      /*
       * For backward compatibility with previous versions of Mail
       * Notification, we also support ;auth= (in lowercase).
       */

      pat = g_strdup_printf("%%%i[^:]:%%%i[^;];%%*1[aA]%%*1[uU]%%*1[tT]%%*1[hH]=%%%is", len, len, len);
      n = sscanf(auth_buf, pat, username_buf, password_buf, authmech_buf);
      g_free(pat);

      if (n >= 2)
	{
	  has_password = TRUE;
	  if (n == 3)
	    has_authmech = TRUE;
	}
      else
	{
	  pat = g_strdup_printf("%%%i[^;];%%*1[aA]%%*1[uU]%%*1[tT]%%*1[hH]=%%%is", len, len);
	  n = sscanf(auth_buf, pat, username_buf, authmech_buf);
	  g_free(pat);

	  if (n >= 1)
	    {
	      if (n == 2)
		has_authmech = TRUE;
	    }
	  else
	    return NULL;	/* unparsable */
	}

      if (has_location)
	{
	  char hostport_buf[buflen];

	  /* split location part in 3 subparts: hostport, path and queries */

	  pat = g_strdup_printf("%%%i[^/]/%%%i[^?]?%%%is", len, len, len);
	  n = sscanf(location_buf, pat, hostport_buf, path_buf, queries_buf);
	  g_free(pat);

	  if (n >= 2)
	    {
	      has_path = TRUE;
	      if (n == 3)
		has_queries = TRUE;
	    }
	  else
	    {
	      pat = g_strdup_printf("%%%i[^?]?%%%is", len, len);
	      n = sscanf(location_buf, pat, hostport_buf, queries_buf);
	      g_free(pat);

	      if (n == 2)
		has_queries = TRUE;
	    }

	  /* split hostport in 2 subparts: host and port */

	  pat = g_strdup_printf("[%%%i[^]]]:%%u", len);
	  n = sscanf(hostport_buf, pat, hostname_buf, &_port);
	  g_free(pat);

	  if (n < 1)
	    {
	      pat = g_strdup_printf("%%%i[^:]:%%u", len);
	      n = sscanf(hostport_buf, pat, hostname_buf, &_port);
	      g_free(pat);
	    }

	  if (n == 2)
	    has_port = TRUE;
	}

      scheme = gnome_vfs_unescape_string(scheme_buf, NULL);
      username = gnome_vfs_unescape_string(username_buf, NULL);
      password = has_password ? gnome_vfs_unescape_string(password_buf, NULL) : NULL;
      authmech = has_authmech ? gnome_vfs_unescape_string(authmech_buf, NULL) : NULL;
      hostname = has_location ? gnome_vfs_unescape_string(hostname_buf, NULL) : NULL;
      port = has_port ? _port : 0;
      path = has_path ? gnome_vfs_unescape_string(path_buf, NULL) : NULL;
      if (has_queries)
	{
	  int i;

	  queries = g_strsplit(queries_buf, "&", 0);
	  for (i = 0; queries[i]; i++)
	    {
	      char *unescaped;

	      unescaped = gnome_vfs_unescape_string(queries[i], NULL);

	      g_free(queries[i]);
	      queries[i] = unescaped;
	    }
	}
      else
	queries = NULL;
    }

    if (! strcmp(scheme, "pop") || ! strcmp(scheme, "pops"))
      {
#if WITH_POP3
	MNPIMailboxConnectionType connection_type;

	if (queries && mn_strv_find(queries, "STLS") != -1)
	  connection_type = MN_PI_MAILBOX_CONNECTION_TYPE_INBAND_SSL;
	else
	  connection_type = ! strcmp(scheme, "pops")
	    ? MN_PI_MAILBOX_CONNECTION_TYPE_SSL
	    : MN_PI_MAILBOX_CONNECTION_TYPE_NORMAL;

	self = self_new("pop3",
			"connection-type", connection_type,
			"username", username,
			"password", password,
			"authmech", authmech,
			"hostname", hostname,
			"port", port,
			NULL);
#endif
      }
    else if (! strcmp(scheme, "imap") || ! strcmp(scheme, "imaps"))
      {
#if WITH_IMAP
	MNPIMailboxConnectionType connection_type;

	if (queries && mn_strv_find(queries, "STARTTLS") != -1)
	  connection_type = MN_PI_MAILBOX_CONNECTION_TYPE_INBAND_SSL;
	else
	  connection_type = ! strcmp(scheme, "imaps")
	    ? MN_PI_MAILBOX_CONNECTION_TYPE_SSL
	    : MN_PI_MAILBOX_CONNECTION_TYPE_NORMAL;

	self = self_new("imap",
			"connection-type", connection_type,
			"username", username,
			"password", password,
			"authmech", authmech,
			"hostname", hostname,
			"port", port,
			NULL);

	if (path)
	  g_object_set(self, MN_IMAP_MAILBOX_PROP_MAILBOX(path), NULL);

	if (queries && mn_strv_find(queries, "noidle") != -1)
	  g_object_set(G_OBJECT(self), MN_IMAP_MAILBOX_PROP_USE_IDLE_EXTENSION(MN_IMAP_MAILBOX_USE_IDLE_NEVER), NULL);
#endif
      }
    else if (! strcmp(scheme, "gmail"))
      {
#if WITH_GMAIL
	self = self_new("gmail",
			"username", username,
			"password", password,
			NULL);
#endif
      }

    g_free(scheme);
    g_free(username);
    g_free(password);
    g_free(authmech);
    g_free(hostname);
    g_free(path);
    g_strfreev(queries);

    return self;
  }

  private gboolean
    check_timeout_cb (gpointer data)
  {
    Self *self = data;

    self_check(self);

    return TRUE;		/* continue */
  }

  private gboolean
    validate (self, GError **err)
  {
    GParamSpec **properties;
    unsigned int n_properties;
    int i;
    gboolean status = TRUE;

    properties = g_object_class_list_properties(G_OBJECT_GET_CLASS(self), &n_properties);
    for (i = 0; i < n_properties; i++)
      if ((properties[i]->flags & MN_MAILBOX_PARAM_REQUIRED) != 0)
	{
	  GValue value = { 0, };
	  const char *str;
	  gboolean is_empty;

	  g_assert(G_IS_PARAM_SPEC_STRING(properties[i]));

	  g_value_init(&value, G_TYPE_STRING);
	  g_object_get_property(G_OBJECT(self), g_param_spec_get_name(properties[i]), &value);

	  str = g_value_get_string(&value);
	  is_empty = ! str || ! *str;

	  g_value_unset(&value);

	  if (is_empty)
	    {
	      g_set_error(err, 0, 0, _("property \"%s\" has no value"), g_param_spec_get_name(properties[i]));
	      status = FALSE;
	      break;
	    }
	}
    g_free(properties);

    return status;
  }

  /**
   * seal:
   * @self: a mailbox
   *
   * Seals the mailbox before it is made operational by being added to
   * the mailboxes list. The point of this function is to allow
   * subclasses to perform initialization which needs to consult the
   * value of properties loaded from mailboxes.xml or set by the
   * properties dialog. That would not be possible from init(), since
   * these properties are only set after the mailbox is constructed.
   **/
  virtual public void
    seal (self)
  {
    if (self->name)
      {
	g_free(self->runtime_name);
	self->runtime_name = g_strdup(self->name);
      }

    self->runtime_check_delay = self->check_delay != -1
      ? self->check_delay
      : SELF_GET_CLASS(self)->default_check_delay;
  }

  //[G_GNUC_UNUSED]              /* invoked via the class pointer */
  virtual private MNMailbox *
    parse_uri (self, const char *uri (check null));

  virtual public void
    check (self)
  {
    g_assert(self_get_active(self) == TRUE);
    g_assert(selfp->checking_enabled == TRUE);
  }

  /*
   * Mailboxes start with this property disabled. As long as this
   * property is disabled, the Update menu items are insensitive, the
   * periodic poll timeout is not installed, and mn_mailbox_check() is
   * not called. This allows subclasses to prevent checks while they
   * are saving the password, etc.
   */
  private gboolean checking_enabled;

  protected void
    enable_checking (self)
  {
    selfp->checking_enabled = TRUE;
    g_object_notify(G_OBJECT(self), "manually-checkable");

    self_update_check_timeout(self);
    self_check(self);
  }

  protected void
    notice (self, const char *format (check null), ...)
    attr {G_GNUC_PRINTF(2, 3)}
  {
    char *message;

    /*
     * Disregard messages sent by a threaded check still in progress
     * after the mailbox has been removed.
     */
    if (! self_get_active(self))
      return;

    MN_STRDUP_VPRINTF(message, format);
    mn_info(_("%s: %s"), self->runtime_name, message);
    g_free(message);
  }

  protected void
    warning (self, const char *format (check null), ...)
    attr {G_GNUC_PRINTF(2, 3)}
  {
    char *message;

    /*
     * Disregard messages sent by a threaded check still in progress
     * after the mailbox has been removed.
     */
    if (! self_get_active(self))
      return;

    MN_STRDUP_VPRINTF(message, format);
    g_warning(_("%s: %s"), self->runtime_name, message);
    g_free(message);
  }

  protected MNMessage *
    get_message_from_mid (self, const char *mid (check null))
  {
    return g_hash_table_lookup(selfp->all_messages_by_mid, mid);
  }

  public char *
    get_command (self, const char *id (check null))
  {
    char *prop;
    char *command;

    prop = g_strconcat(id, "-command", NULL);
    g_object_get(self, prop, &command, NULL);
    g_free(prop);

    if (command && ! *command)
      {
	g_free(command);
	return NULL;
      }

    return command;
  }

  public gboolean
    has_command (self, const char *id (check null))
  {
    char *command;
    gboolean has;

    command = self_get_command(self, id);
    has = command != NULL;
    g_free(command);

    return has;
  }
}
