/*
 * mn-imap-mailbox.gob - IMAP 4rev1 support for Mail Notification
 *
 * Compliance:
 *
 *	- RFC 3501
 *	- RFC 2177
 *	- RFC 2192 (subset)
 *
 * 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 "mn-pi-mailbox.h"
%}

%h{
#define MN_IMAP_MAILBOX_N_USE_IDLE	3

extern int mn_imap_mailbox_default_ports[MN_PI_MAILBOX_N_CONNECTION_TYPES];
%}

%privateheader{
#include "mn-client-session.h"
%}

%{
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
#include <glib/gi18n.h>
#include <libgnomevfs/gnome-vfs-utils.h>
#include "mn-mailbox-private.h"
#include "mn-authenticated-mailbox-private.h"
#include "mn-pi-mailbox-private.h"
#include "mn-util.h"
#include "mn-message-mime.h"

#define HAS_CURRENT_TAG(response, priv)	(! strcmp((response)->tag, (priv)->tag))
#define IS(response, token)		(! g_ascii_strcasecmp((response)->response, (token)))
#define IS_OK(response)			IS(response, "OK")
#define IS_NO(response)			IS(response, "NO")
#define IS_BAD(response)		IS(response, "BAD")
#define IS_BYE(response)		IS(response, "BYE")

enum
{
  STATE_GREETING = MN_CLIENT_SESSION_INITIAL_STATE,
  STATE_CAPABILITY,
#if WITH_SSL
  STATE_STARTTLS,
#endif
#if WITH_SASL
  STATE_AUTHENTICATE,
#endif
  STATE_LOGIN,
  STATE_EXAMINE,
  STATE_SEARCH_UNSEEN,
  STATE_SEARCH_RECENT,
  STATE_FETCH_UID,
  STATE_FETCH,
  STATE_IDLE,
  STATE_LOGOUT
};

typedef enum
{
  IDLE_STATE_PRE_IDLE,
  IDLE_STATE_IDLE,
  IDLE_STATE_POST_IDLE
} IdleState;

struct _MNClientSessionPrivate
{
  MN_PI_MAILBOX_SESSION_PRIVATE;
  MNIMAPMailbox			*self;

  const char			*server_software;
  gboolean			server_software_supports_idle;

  int				numeric_tag;
  char				tag[5];

  char				**capabilities;
  GSList			*auth_mechanisms;
  gboolean			authenticated;

  /*
   * RFC 3501 specifies that UIDVALIDITY is a 32-bit number, but we do
   * not need it to be one. Use a string for interoperability purposes
   * (in case some server vendors did not read the RFC properly).
   */
  char				*uidvalidity;

  int				num_errors;
  GHashTable			*messages;

#if WITH_SSL
  gboolean			starttls_completed;
#endif
#if WITH_SASL
  GSList			*sasl_remaining_mechanisms;
  const char			*sasl_mechanism;
#endif

  IdleState			idle_state;
  unsigned int			idle_inactivity_timeout_id;
  gboolean			idle_inactivity;
  gboolean			could_idle;	/* could idle at least once */
};

struct _MNClientSessionResponse
{
  char		*continuation;
  char		*tag;
  char		*response;
  char		*code;
  char		*arguments;
};

typedef struct
{
  MNMessage		*message;
  char			*mid;
  int			number;
  MNMessageFlags	flags;
} MessageInfo;

int mn_imap_mailbox_default_ports[MN_PI_MAILBOX_N_CONNECTION_TYPES] = { 143, 143, 993 };

/* variable taken from Evolution (camel-utf8.c) */
static const char *utf7_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";
%}

enum MN_IMAP_MAILBOX_USE_IDLE
{
  NEVER,
  AUTODETECT,
  ALWAYS
} MN:IMAP:Mailbox:Use:IDLE;

class MN:IMAP:Mailbox from MN:PI:Mailbox
{
  private GMutex *mutex = {g_mutex_new()} destroywith g_mutex_free;

  private MNClientSessionPrivate *idle_session;

  public char *mailbox destroywith g_free;
  property STRING mailbox (link,
			   flags = CONSTRUCT
			   | MN_MAILBOX_PARAM_LOAD_SAVE
			   | MN_MAILBOX_PARAM_REQUIRED
			   | MN_MAILBOX_PARAM_IGNORE_CASE,
			   default_value = "INBOX");

  public MNIMAPMailboxUseIDLE use_idle_extension;
  property ENUM use_idle_extension (link,
				    enum_type = MN:IMAP:Mailbox:Use:IDLE,
				    flags = CONSTRUCT
				    | MN_MAILBOX_PARAM_LOAD_SAVE,
				    default_value = MN_IMAP_MAILBOX_USE_IDLE_AUTODETECT);

  class_init (class)
  {
    MN_MAILBOX_CLASS(class)->type = "imap";
    MN_PI_MAILBOX_CLASS(class)->default_ports = mn_imap_mailbox_default_ports;
  }

  init (self)
  {
    mn_mailbox_set_format(MN_MAILBOX(self), "IMAP");
  }

  override (MN:Mailbox) void
    seal (MNMailbox *mailbox)
  {
    MNAuthenticatedMailbox *auth_mailbox = MN_AUTHENTICATED_MAILBOX(mailbox);
    Self *self = SELF(mailbox);

    PARENT_HANDLER(mailbox);

    if (! mailbox->runtime_name)
      mailbox->runtime_name = self_build_name(MN_AUTHENTICATED_MAILBOX(mailbox)->username,
					      MN_PI_MAILBOX(mailbox)->hostname,
					      self->mailbox);

#if WITH_SSL
    if (MN_PI_MAILBOX(self)->connection_type == MN_PI_MAILBOX_CONNECTION_TYPE_SSL)
      auth_mailbox->keyring_protocol = g_strdup("imaps");
    else
#endif
      auth_mailbox->keyring_protocol = g_strdup("imap");
  }

  /*
   * Parses a RFC 2192 IMAP URL.
   *
   * Note: we only handle a subset of the RFC 2192 specification,
   * since mailbox lists, message lists and message parts have no
   * meaning in Mail Notification. Furthermore, Mail Notification
   * requires an username.
   */
  override (MN:Mailbox) MNMailbox *
    parse_uri (MNMailbox *dummy, const char *uri)
  {
    int len;
    int buflen;
    char *username = NULL;
    char *authmech = NULL;
    char *hostname;
    char *path = NULL;
    int port;
    MNMailbox *mailbox;

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

    {
      char scheme_buf[buflen];
      char auth_buf[buflen];
      char location_buf[buflen];
      char hostport_buf[buflen];
      char path_buf[buflen];
      char username_buf[buflen];
      char authmech_buf[buflen];
      char hostname_buf[buflen];
      gboolean has_path;
      gboolean has_authmech = FALSE;

      if (! mn_pi_mailbox_split_uri(uri, len, scheme_buf, auth_buf, location_buf))
	return NULL;

      if (strcmp(scheme_buf, "imap"))
	return NULL;

      if (! self_split_uri_location(location_buf, len, hostport_buf, path_buf, &has_path))
	return NULL;

      if (! mn_pi_mailbox_split_uri_auth(auth_buf, len, username_buf, authmech_buf, &has_authmech))
	return NULL;

      if (has_authmech && ! strcmp(authmech_buf, "*"))
	has_authmech = FALSE;

      mn_pi_mailbox_split_uri_hostport(hostport_buf, len, hostname_buf, &port);

      username = gnome_vfs_unescape_string(username_buf, NULL);
      if (has_authmech)
	authmech = gnome_vfs_unescape_string(authmech_buf, NULL);
      hostname = gnome_vfs_unescape_string(hostname_buf, NULL);
      if (has_path)
	path = gnome_vfs_unescape_string(path_buf, NULL);
    }

    mailbox = mn_mailbox_new("imap",
			     "username", username,
			     "authmech", authmech,
			     "hostname", hostname,
			     "port", port,
			     NULL);

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

    g_free(username);
    g_free(authmech);
    g_free(hostname);
    g_free(path);

    return mailbox;
  }

  private gboolean
    split_uri_location (const char *location (check null),
			int maxlen,
			char *hostport (check null),
			char *path (check null),
			gboolean *has_path (check null))
  {
    char *pat;
    int n;

    pat = g_strdup_printf("%%%i[^/]/%%%is", maxlen, maxlen);
    n = sscanf(location, pat, hostport, path);
    g_free(pat);

    g_return_val_if_fail(n >= 1, FALSE);

    *has_path = n == 2;
    if (*has_path)
      {
	/* we only handle enc_mailbox (see the RFC 2192 ABNF) */
	if (strspn(path, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$-_.+!*'(),%&=~:@/") != strlen(path))
	  return FALSE;		/* more than enc_mailbox, unhandled */
      }

    return TRUE;
  }

  override (MN:Mailbox) void
    removed (MNMailbox *mailbox)
  {
    Self *self = SELF(mailbox);

    PARENT_HANDLER(mailbox);

    self_lock(self);
    if (selfp->idle_session)
      {
	mn_client_session_write(selfp->idle_session->session, "DONE");
	selfp->idle_session->idle_state = IDLE_STATE_POST_IDLE;
      }
    self_unlock(self);
  }

  private int
    handle_greeting_cb (MNClientSession *session,
			MNClientSessionResponse *response,
			MNClientSessionPrivate *priv)
  {
    priv->session = session;

    if (response->continuation)
      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
    else if (! response->tag && IS_OK(response))
      {
	self_session_detect_imapd(session, response, priv);
	return self_session_handle_capability_code(priv, response)
	  ? self_session_after_capability(priv)
	  : STATE_CAPABILITY;
      }
    else if (! response->tag && IS(response, "PREAUTH"))
      {
	priv->authenticated = TRUE;
	self_session_detect_imapd(session, response, priv);
	return self_session_handle_capability_code(priv, response)
	  ? self_session_after_capability(priv)
	  : STATE_CAPABILITY;
      }
    else if (! response->tag && IS_BYE(response))
      return mn_client_session_set_error_from_response(session, MN_CLIENT_SESSION_ERROR_OTHER, response->arguments);
    else
      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
  }

  private int
    enter_capability_cb (MNClientSession *session,
			 MNClientSessionPrivate *priv)
  {
    return self_session_write(priv, "CAPABILITY");
  }

  private int
    handle_capability_cb (MNClientSession *session,
			  MNClientSessionResponse *response,
			  MNClientSessionPrivate *priv)
  {
    if (response->continuation)
      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
    else if (response->tag)
      {
	if (HAS_CURRENT_TAG(response, priv))
	  {
	    if (IS_OK(response))
	      {
		return priv->capabilities
		  ? self_session_after_capability(priv)
		  : mn_client_session_set_error(session, MN_CLIENT_SESSION_ERROR_OTHER, _("server did not send capabilities"));
	      }
	    else if (IS_BAD(response))
	      {
		mn_client_session_set_error_from_response(session, MN_CLIENT_SESSION_ERROR_OTHER, response->arguments);
		return STATE_LOGOUT;
	      }
	    else
	      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
	  }
      }
    else if (IS(response, "CAPABILITY"))
      {
	self_session_parse_capabilities(priv, response->arguments);
	return MN_CLIENT_SESSION_RESULT_CONTINUE;
      }

    return self_default_handler(response, priv, MN_CLIENT_SESSION_ERROR_OTHER);
  }

  private int
    enter_starttls_cb (MNClientSession *session,
		       MNClientSessionPrivate *priv)
  {
#if WITH_SSL
    return self_session_write(priv, "STARTTLS");
#else
    g_assert_not_reached();
    return 0;
#endif /* WITH_SSL */
  }

  private int
    handle_starttls_cb (MNClientSession *session,
			MNClientSessionResponse *response,
			MNClientSessionPrivate *priv)
  {
#if WITH_SSL
    if (response->continuation)
      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
    else if (response->tag)
      {
	if (HAS_CURRENT_TAG(response, priv))
	  {
	    if (IS_OK(response))
	      {
		priv->starttls_completed = TRUE;
		return mn_client_session_enable_ssl(session)
		  ? STATE_CAPABILITY
		  : MN_CLIENT_SESSION_RESULT_DISCONNECT;
	      }
	    else if (IS_BAD(response))
	      {
		mn_client_session_set_error_from_response(session, MN_CLIENT_SESSION_ERROR_OTHER, response->arguments);
		return STATE_LOGOUT;
	      }
	    else
	      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
	  }
      }

    return self_default_handler(response, priv, MN_CLIENT_SESSION_ERROR_OTHER);
#else
    g_assert_not_reached();
    return 0;
#endif /* WITH_SSL */
  }

  private int
    enter_authenticate_cb (MNClientSession *session,
			   MNClientSessionPrivate *priv)
  {
#if WITH_SASL
    priv->sasl_mechanism = NULL;

    if (mn_client_session_sasl_authentication_start(priv->session,
						    "imap",
						    priv->sasl_remaining_mechanisms,
						    priv->pi_mailbox->authmech,
						    &priv->sasl_mechanism,
						    NULL,	/* [1] */
						    NULL))	/* [1] */
      {
	g_assert(priv->sasl_mechanism != NULL);
	return self_session_write(priv, "AUTHENTICATE %s", priv->sasl_mechanism);
      }
    else
      return priv->auth_mailbox->auth_cancelled
	? STATE_LOGOUT
	: self_session_authenticate_fallback(priv, FALSE);

    /*
     * [1] RFC 3501 6.2.2 specifies that the IMAP protocol does not
     * support the initial client response feature of SASL.
     */
#else
    g_assert_not_reached();
    return 0;
#endif /* WITH_SASL */
  }

  private int
    handle_authenticate_cb (MNClientSession *session,
			    MNClientSessionResponse *response,
			    MNClientSessionPrivate *priv)
  {
#if WITH_SASL
    if (response->tag)
      {
	if (HAS_CURRENT_TAG(response, priv))
	  {
	    if (IS_OK(response))
	      {
		if (mn_client_session_sasl_authentication_done(session))
		  {
		    priv->authenticated = TRUE;

		    /*
		     * RFC 3501 2.2.2:
		     *
		     * A server MAY include a CAPABILITY response code
		     * in the tagged OK response of a successful
		     * AUTHENTICATE command in order to send
		     * capabilities automatically.  It is unnecessary
		     * for a client to send a separate CAPABILITY
		     * command if it recognizes these automatic
		     * capabilities.  This should only be done if a
		     * security layer was not negotiated by the
		     * AUTHENTICATE command, because the tagged OK
		     * response as part of an AUTHENTICATE command is
		     * not protected by encryption/integrity checking.
		     * [SASL] requires the client to re-issue a
		     * CAPABILITY command in this case.
		     */
		    return self_session_handle_capability_code(priv, response) && mn_client_session_sasl_get_ssf(session)
		      ? STATE_EXAMINE
		      : STATE_CAPABILITY;
		  }
		else
		  return MN_CLIENT_SESSION_RESULT_DISCONNECT;
	      }
	    else if (IS_NO(response) || IS_BAD(response))
	      {
		return priv->auth_mailbox->auth_cancelled
		  ? STATE_LOGOUT
		  : self_session_authenticate_fallback(priv, FALSE);
	      }
	    else
	      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
	  }
      }
    else if (response->continuation)
      return mn_client_session_sasl_authentication_step(session, response->continuation);

    return self_default_handler(response, priv, MN_CLIENT_SESSION_ERROR_OTHER);
#else
    g_assert_not_reached();
    return 0;
#endif /* WITH_SASL */
  }

  private int
    enter_login_cb (MNClientSession *session,
		    MNClientSessionPrivate *priv)
  {
    if (self_session_has_capability(priv, "LOGINDISABLED"))
      {
	mn_client_session_notice(session, _("server advertised LOGINDISABLED, not using LOGIN authentication"));
	mn_client_session_set_error(session, MN_CLIENT_SESSION_ERROR_OTHER, _("unable to login"));
	return STATE_LOGOUT;
      }
    else
      {
	char *quoted_username;
	char *quoted_password;
	int result;

	if (! mn_authenticated_mailbox_fill_password(priv->auth_mailbox, TRUE))
	  return STATE_LOGOUT;

	quoted_username = self_quote(priv->auth_mailbox->username);
	quoted_password = self_quote(priv->auth_mailbox->runtime_password);
	result = self_session_write(priv, "LOGIN %s %s", quoted_username, quoted_password);
	g_free(quoted_username);
	g_free(quoted_password);

	return result;
      }
  }

  private int
    handle_login_cb (MNClientSession *session,
		     MNClientSessionResponse *response,
		     MNClientSessionPrivate *priv)
  {
    if (response->continuation)
      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
    else if (response->tag)
      {
	if (HAS_CURRENT_TAG(response, priv))
	  {
	    if (IS_OK(response))
	      {
		priv->authenticated = TRUE;
		return self_session_handle_capability_code(priv, response)
		  ? STATE_EXAMINE
		  : STATE_CAPABILITY;
	      }
	    else if (IS_NO(response) || IS_BAD(response))
	      return self_session_authenticate_fallback(priv, TRUE);
	    else
	      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
	  }
      }

    return self_default_handler(response, priv, MN_CLIENT_SESSION_ERROR_OTHER);
  }

  private int
    enter_examine_cb (MNClientSession *session,
		      MNClientSessionPrivate *priv)
  {
    char *utf7_mailbox;
    char *quoted_mailbox;
    int result;

    utf7_mailbox = self_utf8_to_imap_utf7(priv->self->mailbox);
    quoted_mailbox = self_quote(utf7_mailbox);
    g_free(utf7_mailbox);

    result = self_session_write(priv, "EXAMINE %s", quoted_mailbox);
    g_free(quoted_mailbox);

    return result;
  }

  private int
    handle_examine_cb (MNClientSession *session,
		       MNClientSessionResponse *response,
		       MNClientSessionPrivate *priv)
  {
    if (response->continuation)
      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
    else if (response->tag)
      {
	if (HAS_CURRENT_TAG(response, priv))
	  {
	    if (IS_OK(response))
	      return STATE_SEARCH_UNSEEN;
	    else if (IS_NO(response) || IS_BAD(response))
	      {
		mn_client_session_set_error_from_response(session, MN_CLIENT_SESSION_ERROR_OTHER, response->arguments);
		return STATE_LOGOUT;
	      }
	    else
	      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
	  }
      }
    else if (IS(response, "OK"))
      {
	if (response->code)
	  {
	    char **fields;

	    fields = g_strsplit(response->code, " ", 0);
	    if (g_strv_length(fields) == 2
		&& ! g_ascii_strcasecmp(fields[0], "UIDVALIDITY"))
	      {
		g_free(priv->uidvalidity);
		priv->uidvalidity = g_strdup(fields[1]);
	      }
	    g_strfreev(fields);
	  }
      }

    return self_default_handler(response, priv, MN_CLIENT_SESSION_ERROR_OTHER);
  }

  private int
    enter_search_unseen_cb (MNClientSession *session,
			    MNClientSessionPrivate *priv)
  {
    priv->num_errors = 0;

    if (priv->messages)
      g_hash_table_remove_all(priv->messages);
    else
      priv->messages = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) self_message_info_free);

    return self_session_write(priv, "SEARCH UNSEEN");
  }

  private int
    handle_search_unseen_cb (MNClientSession *session,
			     MNClientSessionResponse *response,
			     MNClientSessionPrivate *priv)
  {
    if (response->continuation)
      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
    else if (response->tag)
      {
	if (HAS_CURRENT_TAG(response, priv))
	  {
	    if (IS_OK(response))
	      {
		/*
		 * Some uncompliant IMAP server implementations do not
		 * send an untagged SEARCH response when there are no
		 * search results (#18876). We are tolerant.
		 */
		if (g_hash_table_size(priv->messages) != 0)
		  return STATE_SEARCH_RECENT;
		else
		  return self_session_got_messages(priv);
	      }
	    else if (IS_NO(response) || IS_BAD(response))
	      {
		mn_client_session_set_error_from_response(session, MN_CLIENT_SESSION_ERROR_OTHER, response->arguments);
		return STATE_LOGOUT;
	      }
	    else
	      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
	  }
      }
    else if (IS(response, "SEARCH"))
      {
	if (response->arguments)
	  {
	    char **strings;
	    int i;

	    strings = g_strsplit(response->arguments, " ", 0);

	    for (i = 0; strings[i]; i++)
	      if (mn_str_isnumeric(strings[i]))
		{
		  int n = atoi(strings[i]);
		  self_session_ensure_message_info(priv, n);
		}

	    g_strfreev(strings);
	  }

	return MN_CLIENT_SESSION_RESULT_CONTINUE;
      }

    return self_default_handler(response, priv, MN_CLIENT_SESSION_ERROR_OTHER);
  }

  private int
    enter_search_recent_cb (MNClientSession *session,
			    MNClientSessionPrivate *priv)
  {
    return self_session_write(priv, "SEARCH RECENT");
  }

  private int
    handle_search_recent_cb (MNClientSession *session,
			     MNClientSessionResponse *response,
			     MNClientSessionPrivate *priv)
  {
    if (response->continuation)
      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
    else if (response->tag)
      {
	if (HAS_CURRENT_TAG(response, priv))
	  {
	    if (IS_OK(response))
	      {
		if (priv->uidvalidity)
		  return STATE_FETCH_UID;
		else
		  return STATE_FETCH;
	      }
	    else if (IS_NO(response) || IS_BAD(response))
	      {
		mn_client_session_set_error_from_response(session, MN_CLIENT_SESSION_ERROR_OTHER, response->arguments);
		return STATE_LOGOUT;
	      }
	    else
	      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
	  }
      }
    else if (IS(response, "SEARCH"))
      {
	if (response->arguments)
	  {
	    char **strings;
	    int i;

	    strings = g_strsplit(response->arguments, " ", 0);

	    for (i = 0; strings[i]; i++)
	      if (mn_str_isnumeric(strings[i]))
		{
		  int n = atoi(strings[i]);
		  MessageInfo *info;

		  info = self_session_get_message_info(priv, n);
		  if (info)
		    info->flags |= MN_MESSAGE_NEW;
		  /*
		   * Otherwise the message is recent but not unseen,
		   * and we must ignore it (fixes #230425).
		   */
		}

	    g_strfreev(strings);
	  }

	return MN_CLIENT_SESSION_RESULT_CONTINUE;
      }

    return self_default_handler(response, priv, MN_CLIENT_SESSION_ERROR_OTHER);
  }

  private int
    enter_fetch_uid_cb (MNClientSession *session, MNClientSessionPrivate *priv)
  {
    GString *set;
    int result;

    g_assert(priv->messages != NULL);
    g_assert(g_hash_table_size(priv->messages) > 0);

    set = g_string_new(NULL);

    g_hash_table_foreach(priv->messages, self_build_fetch_uid_set_cb, set);

    result = self_session_write(priv, "FETCH %s UID", set->str);

    g_string_free(set, TRUE);

    return result;
  }

  private void
    build_fetch_uid_set_cb (gpointer key, gpointer value, gpointer user_data)
  {
    MessageInfo *info = value;
    GString *set = user_data;

    if (*set->str)
      g_string_append_c(set, ',');

    g_string_append_printf(set, "%i", info->number);
  }

  private char *
    parse_fetch_uid_response (const char *response (check null))
  {
    char *start;
    char *end;

    start = mn_ascii_strcasestr_span(response, "UID ");
    if (! start)
      return NULL;

    /*
     * The UID should be the only list element, but we are tolerant
     * and allow a space in case other elements are present, eg:
     *
     *		* 1 FETCH (UID 17 OTHER_ELEMENT)
     */
    end = strpbrk(start, " )");
    if (! end || end == start)
      return NULL;

    return g_strndup(start, end - start);
  }

  private int
    handle_fetch_uid_cb (MNClientSession *session,
			 MNClientSessionResponse *response,
			 MNClientSessionPrivate *priv)
  {
    if (response->continuation)
      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
    else if (response->tag)
      {
	if (HAS_CURRENT_TAG(response, priv))
	  {
	    /* a failure (NO or BAD) is not fatal */
	    if (IS_OK(response) || IS_NO(response) || IS_BAD(response))
	      return STATE_FETCH;
	    else
	      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
	  }
      }
    else if (mn_str_isnumeric(response->response)
	     && response->arguments
	     && mn_ascii_str_case_has_prefix(response->arguments, "FETCH "))
      {
	char *uid;

	uid = self_parse_fetch_uid_response(response->arguments);
	if (uid)
	  {
	    int n;
	    MessageInfo *info;

	    n = atoi(response->response);

	    info = self_session_get_message_info(priv, n);
	    if (info && ! info->mid)
	      {
		MNMessage *message;

		g_assert(info->message == NULL);

		/*
		 * RFC 3501 specifies that UID is a 32-bit number,
		 * but we do not need it to be one. Use a string
		 * for interoperability purposes (in case some
		 * server vendors did not read the RFC properly).
		 */
		info->mid = g_strdup_printf("%s:%s", priv->uidvalidity, uid);

		message = mn_mailbox_get_message_from_mid(priv->mailbox, info->mid);
		if (message)
		  /*
		   * We create a new instance rather than reusing
		   * the existing one since the flags might be
		   * different and a MNMessage is immutable (so we
		   * cannot change the flags of the existing
		   * message).
		   */
		  info->message = mn_g_object_clone(message,
						    MN_MESSAGE_PROP_FLAGS(info->flags),
						    NULL);
	      }

	    g_free(uid);
	  }

	return MN_CLIENT_SESSION_RESULT_CONTINUE;
      }

    return self_default_handler(response, priv, MN_CLIENT_SESSION_ERROR_OTHER);
  }

  private int
    enter_fetch_cb (MNClientSession *session, MNClientSessionPrivate *priv)
  {
    GString *set;
    int result;

    g_assert(priv->messages != NULL);
    g_assert(g_hash_table_size(priv->messages) > 0);

    set = g_string_new(NULL);

    g_hash_table_foreach(priv->messages, self_build_fetch_set_cb, set);

    if (*set->str)
      result = self_session_write(priv, "FETCH %s BODY.PEEK[HEADER]", set->str);
    else
      /* all the unseen messages were cached, no message to fetch */
      result = self_session_got_messages(priv);

    g_string_free(set, TRUE);

    return result;
  }

  private void
    build_fetch_set_cb (gpointer key, gpointer value, gpointer user_data)
  {
    MessageInfo *info = value;
    GString *set = user_data;

    if (! info->message)
      {
	if (*set->str)
	  g_string_append_c(set, ',');

	g_string_append_printf(set, "%i", info->number);
      }
  }

  private int
    handle_fetch_cb (MNClientSession *session,
		     MNClientSessionResponse *response,
		     MNClientSessionPrivate *priv)
  {
    if (response->continuation)
      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
    else if (response->tag)
      {
	if (HAS_CURRENT_TAG(response, priv))
	  {
	    if (IS_OK(response))
	      {
		/*
		 * Note that in previous versions we required the
		 * fetch results to include all the unseen messages,
		 * but it caused problems in some cases (#20132).
		 */

		return self_session_got_messages(priv);
	      }
	    else if (IS_NO(response) || IS_BAD(response))
	      {
		mn_client_session_set_error_from_response(session, MN_CLIENT_SESSION_ERROR_OTHER, response->arguments);
		return STATE_LOGOUT;
	      }
	    else
	      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
	  }
      }
    else if (mn_str_isnumeric(response->response)
	     && response->arguments
	     && mn_ascii_str_case_has_prefix(response->arguments, "FETCH ")
	     && (mn_ascii_strcasestr(response->arguments, "BODY[HEADER]")
		 || mn_ascii_strcasestr(response->arguments, "BODY[HEADER "))) /* [1] */
      /*
       * [1] Non-compliant response (see RFC 3501 BNF), sent by Binc
       * IMAP and maybe others. We are tolerant.
       */
      {
	int n;
	MessageInfo *info;

	n = atoi(response->response);

	info = self_session_get_message_info(priv, n);
	if (info && ! info->message)
	  {
	    char *p;
	    int len;

	    /* we assume the header string will be in literal form */

	    p = strrchr(response->arguments, '{');
	    if (p && sscanf(p, "{%d}", &len) == 1 && len >= 0)
	      {
		gconstpointer buf;
		GError *err = NULL;

		buf = mn_client_session_read(session, len);
		if (! buf)
		  return MN_CLIENT_SESSION_RESULT_DISCONNECT;

		info->message = mn_message_new_from_buffer(priv->mailbox,
							   buf,
							   len,
							   info->mid,
							   info->flags,
							   FALSE,
							   &err);

		if (err)
		  {
		    mn_client_session_warning(session, "cannot read message %i: %s", n, err->message);
		    g_error_free(err);

		    priv->num_errors++;
		  }

		/* read end of line (after literal) */
		if (! mn_client_session_read_line(session))
		  return MN_CLIENT_SESSION_RESULT_DISCONNECT;
	      }
	    else
	      {
		mn_client_session_warning(session, "cannot retrieve message %i", n);
		priv->num_errors++;
	      }

	    return MN_CLIENT_SESSION_RESULT_CONTINUE;
	  }
      }

    return self_default_handler(response, priv, MN_CLIENT_SESSION_ERROR_OTHER);
  }

  private int
    enter_idle_cb (MNClientSession *session,
		   MNClientSessionPrivate *priv)
  {
    if (mn_mailbox_get_active(priv->mailbox))
      {
	if (priv->self->use_idle_extension == MN_IMAP_MAILBOX_USE_IDLE_NEVER)
	  mn_client_session_notice(session, _("\"Use the IDLE extension\" set to \"never\" in the mailbox properties, logging out"));
	else
	  {
	    if (self_session_has_capability(priv, "IDLE"))
	      {
		switch (priv->self->use_idle_extension)
		  {
		  case MN_IMAP_MAILBOX_USE_IDLE_AUTODETECT:
		    if (priv->server_software_supports_idle)
		      goto idle;
		    else
		      mn_client_session_notice(session, _("the remote server runs %s, not using the IDLE extension"), priv->server_software);
		    break;

		  case MN_IMAP_MAILBOX_USE_IDLE_ALWAYS:
		    if (! priv->server_software_supports_idle)
		      mn_client_session_warning(session, _("the remote server runs %s, the IDLE extension might not function properly"), priv->server_software);
		    goto idle;
		    break;

		  default:
		    g_assert_not_reached();
		  }
	      }
	    else
	      mn_client_session_notice(session, _("the remote server does not support the IDLE extension, logging out"));
	  }
      }

    return STATE_LOGOUT;

  idle:
    priv->idle_state = IDLE_STATE_PRE_IDLE;
    return self_session_write(priv, "IDLE");
  }

  private int
    handle_idle_cb (MNClientSession *session,
		    MNClientSessionResponse *response,
		    MNClientSessionPrivate *priv)
  {
    switch (priv->idle_state)
      {
      case IDLE_STATE_PRE_IDLE:
	if (response->tag)
	  {
	    if (HAS_CURRENT_TAG(response, priv))
	      {
		if (IS_NO(response) || IS_BAD(response))
		  /*
		   * The server advertised IDLE but does not actually
		   * support it.
		   *
		   * Although strictly speaking this can be considered a
		   * compliance fault, we'll be tolerant and just logout
		   * without setting an error.
		   */
		  return STATE_LOGOUT;
		else
		  return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
	      }
	  }
	else if (response->continuation)
	  {
	    /* we're now in the idle loop */
	    priv->idle_state = IDLE_STATE_IDLE;
	    priv->idle_inactivity = FALSE;
	    priv->could_idle = TRUE;

	    GDK_THREADS_ENTER();
	    mn_mailbox_set_poll(priv->mailbox, FALSE);
	    gdk_flush();
	    GDK_THREADS_LEAVE();

	    return MN_CLIENT_SESSION_RESULT_CONTINUE;
	  }
	break;

      case IDLE_STATE_IDLE:
	if (response->continuation || response->tag)
	  return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;

	if (IS_BYE(response))
	  {
	    priv->idle_state = IDLE_STATE_POST_IDLE;
	    return MN_CLIENT_SESSION_RESULT_DISCONNECT; /* we'll reconnect */
	  }
	else if (response->arguments
		 && mn_str_isnumeric(response->response)
		 && (! g_ascii_strcasecmp(response->arguments, "EXISTS")
		     || ! g_ascii_strcasecmp(response->arguments, "RECENT")
		     || ! g_ascii_strcasecmp(response->arguments, "EXPUNGE")
		     || mn_ascii_str_case_has_prefix(response->arguments, "FETCH ")))
	  {
	    priv->idle_state = IDLE_STATE_POST_IDLE;
	    return mn_client_session_write(session, "DONE"); /* wake up */
	  }
	break;

      case IDLE_STATE_POST_IDLE:
	if (response->tag)
	  {
	    if (HAS_CURRENT_TAG(response, priv))
	      {
		if (IS_OK(response))
		  {
		    if (! mn_mailbox_get_active(priv->mailbox))
		      return STATE_LOGOUT;
		    else if (priv->idle_inactivity)
		      return STATE_IDLE; /* anti-inactivity, re-enter */
		    else
		      return STATE_SEARCH_UNSEEN;
		  }
		else if (IS_NO(response) || IS_BAD(response))
		  {
		    mn_client_session_set_error_from_response(session, MN_CLIENT_SESSION_ERROR_OTHER, response->arguments);
		    return STATE_LOGOUT;
		  }
		else
		  return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
	      }
	  }
	else if (response->continuation)
	  return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
	break;

      default:
	g_assert_not_reached();
	break;
      }

    return self_default_handler(response, priv, MN_CLIENT_SESSION_ERROR_OTHER);
  }

  private int
    enter_logout_cb (MNClientSession *session,
		     MNClientSessionPrivate *priv)
  {
    return self_session_write(priv, "LOGOUT");
  }

  private int
    handle_logout_cb (MNClientSession *session,
		      MNClientSessionResponse *response,
		      MNClientSessionPrivate *priv)
  {
    if (response->continuation)
      return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
    else if (response->tag && HAS_CURRENT_TAG(response, priv))
      {
	if (IS_OK(response))
	  return MN_CLIENT_SESSION_RESULT_DISCONNECT;
	else if (IS_BAD(response))
	  return mn_client_session_set_error_from_response(session, MN_CLIENT_SESSION_ERROR_OTHER, response->arguments);
	else
	  return MN_CLIENT_SESSION_RESULT_BAD_RESPONSE_FOR_CONTEXT;
      }
    else
      return MN_CLIENT_SESSION_RESULT_CONTINUE;
  }

  override (MN:Authenticated:Mailbox) void
    authenticated_check (MNAuthenticatedMailbox *mailbox)
  {
    Self *self = SELF(mailbox);
    static const MNClientSessionState states[] = {
      { STATE_GREETING,		NULL,				self_handle_greeting_cb },
      { STATE_CAPABILITY,	self_enter_capability_cb,	self_handle_capability_cb },
#if WITH_SSL
      { STATE_STARTTLS,		self_enter_starttls_cb,		self_handle_starttls_cb },
#endif
#if WITH_SASL
      { STATE_AUTHENTICATE,	self_enter_authenticate_cb,	self_handle_authenticate_cb },
#endif
      { STATE_LOGIN,		self_enter_login_cb,		self_handle_login_cb },
      { STATE_EXAMINE,		self_enter_examine_cb,		self_handle_examine_cb },
      { STATE_SEARCH_UNSEEN,	self_enter_search_unseen_cb,	self_handle_search_unseen_cb },
      { STATE_SEARCH_RECENT,	self_enter_search_recent_cb,	self_handle_search_recent_cb },
      { STATE_FETCH_UID,	self_enter_fetch_uid_cb,	self_handle_fetch_uid_cb },
      { STATE_FETCH,		self_enter_fetch_cb,		self_handle_fetch_cb },
      { STATE_IDLE,		self_enter_idle_cb,		self_handle_idle_cb },
      { STATE_LOGOUT,		self_enter_logout_cb,		self_handle_logout_cb },

      MN_CLIENT_SESSION_STATES_END
    };
    static const MNClientSessionCallbacks callbacks = {
      mn_pi_mailbox_notice_cb,
      mn_pi_mailbox_warning_cb,
      self_response_new_cb,
      self_response_free_cb,
      self_pre_read_cb,
      self_post_read_cb,
#if WITH_SASL
      mn_pi_mailbox_sasl_get_credentials_cb,
#endif
#if WITH_SSL
      mn_pi_mailbox_ssl_trust_server_cb,
#endif
    };
    MNClientSessionPrivate priv;
    gboolean status;
    GError *err = NULL;

    PARENT_HANDLER(mailbox);

    /* check if the parent handler has disabled the mailbox */
    if (! mn_mailbox_get_poll(MN_MAILBOX(self)))
      return;

  again:
    g_clear_error(&err);

    memset(&priv, 0, sizeof(priv));
    mn_pi_mailbox_session_private_init(MN_PI_MAILBOX(self), &priv);
    priv.self = self;
    priv.server_software_supports_idle = TRUE; /* assume it does */

    status = mn_client_session_run(states,
				   &callbacks,
#if WITH_SSL
				   priv.pi_mailbox->connection_type == MN_PI_MAILBOX_CONNECTION_TYPE_SSL,
#endif
				   priv.pi_mailbox->hostname,
				   priv.pi_mailbox->runtime_port,
				   &priv,
				   &err);

    g_strfreev(priv.capabilities);
    mn_g_slist_free_deep(priv.auth_mechanisms);

    g_free(priv.uidvalidity);

    if (priv.messages)
      g_hash_table_destroy(priv.messages);

#if WITH_SASL
    g_slist_free(priv.sasl_remaining_mechanisms);
#endif

    if (priv.could_idle && mn_mailbox_get_active(priv.mailbox))
      {
	if (status)
	  goto again;
	/* some servers abruptly disconnect for inactivity */
	else if (g_error_matches(err, MN_CLIENT_SESSION_ERROR, MN_CLIENT_SESSION_ERROR_CONNECTION_LOST))
	  {
	    /* g_log() escapes unsafe and non UTF-8 characters, so this is safe */
	    mn_mailbox_notice(priv.mailbox, "%s", err->message);
	    goto again;
	  }
      }

    GDK_THREADS_ENTER();

    mn_mailbox_set_poll(MN_MAILBOX(self), TRUE);
    if (! status)
      {
	char *escaped;

	escaped = mn_utf8_escape(err->message);
	g_error_free(err);

	mn_mailbox_set_error(MN_MAILBOX(self), "%s", escaped);
	g_free(escaped);
      }

    gdk_flush();
    GDK_THREADS_LEAVE();
  }

  private MNClientSessionResponse *
    response_new_cb (MNClientSession *session,
		     const char *input,
		     MNClientSessionPrivate *priv)
  {
    MNClientSessionResponse *response = NULL;

    /*
     * About character set handling:
     *
     * RFC 3501 section 1.2 specifies that "characters are 7-bit
     * US-ASCII unless otherwise specified", and the ABNF formal
     * syntax found in section 9 confirms it. However, we follow the
     * RFC 793 robustness principle ("be liberal in what you accept")
     * and do not require responses to be valid 7-bit US-ASCII.
     *
     * Safety:
     *   - we ensure that GTK+ will only receive UTF-8 data by
     *     escaping error messages in authenticated_check()
     *   - responses ending up in mn_mailbox_notice() need not be
     *     valid UTF-8, since g_log() escapes unsafe and non UTF-8
     *     characters
     *   - messages do not go through this function; they are handled
     *     in handle_fetch_cb() and passed to our mn-message-mime
     *     implementation, which handles character set conversions
     */

    if (! strcmp(input, "+"))
      {
	response = g_new0(MNClientSessionResponse, 1);
	response->continuation = g_strdup("");
      }
    else if (g_str_has_prefix(input, "+ "))
      {
	response = g_new0(MNClientSessionResponse, 1);
	response->continuation = g_strdup(input + 2);
      }
    else
      {
	char **tokens;

	tokens = g_strsplit(input, " ", 3);
	if (tokens[0] && tokens[1])
	  {
	    if (tokens[2] && tokens[2][0] == '[')
	      {
		char *code_start;
		char *code_end;

		code_start = tokens[2] + 1;
		code_end = strchr(code_start, ']');
		if (code_end)
		  {
		    response = g_new0(MNClientSessionResponse, 1);
		    response->code = g_strndup(code_start, code_end - code_start);
		    response->arguments = code_end[1] ? g_strdup(code_end + 2) : NULL;
		  }
	      }
	    else
	      {
		response = g_new0(MNClientSessionResponse, 1);
		response->arguments = g_strdup(tokens[2]);
	      }

	    if (response)
	      {
		response->tag = ! strcmp(tokens[0], "*") ? NULL : g_strdup(tokens[0]);
		response->response = g_strdup(tokens[1]);
	      }
	  }
	g_strfreev(tokens);
      }

    return response;
  }

  private void
    response_free_cb (MNClientSession *session,
		      MNClientSessionResponse *response,
		      MNClientSessionPrivate *priv)
  {
    g_free(response->continuation);
    g_free(response->tag);
    g_free(response->response);
    g_free(response->code);
    g_free(response->arguments);
    g_free(response);
  }

  private int
    default_handler (MNClientSessionResponse *response (check null),
		     MNClientSessionPrivate *priv (check null),
		     int error_code_when_bye)
  {
    if (! response->tag && IS_BYE(response))
      return mn_client_session_set_error_from_response(priv->session, error_code_when_bye, response->arguments);
    else
      return MN_CLIENT_SESSION_RESULT_CONTINUE;
  }

  private void
    pre_read_cb (MNClientSession *session,
		 MNClientSessionPrivate *priv)
  {
    if (priv->idle_state == IDLE_STATE_IDLE)
      {
	Self *self = priv->self;

	self_lock(self);

	selfp->idle_session = priv;

	/* cycle IDLE in 29 minutes, as advised by RFC 2177 */

	g_assert(priv->idle_inactivity_timeout_id == 0);
	priv->idle_inactivity_timeout_id = g_timeout_add(60 * 29 * 1000, self_idle_inactivity_timeout_cb, self);

	self_unlock(self);
      }
  }

  private void
    post_read_cb (MNClientSession *session,
		  MNClientSessionPrivate *priv)
  {
    Self *self = priv->self;

    self_lock(self);

    if (priv->idle_state >= IDLE_STATE_IDLE)
      {
	selfp->idle_session = NULL;
	mn_source_clear(&priv->idle_inactivity_timeout_id);
      }

    self_unlock(self);
  }

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

    self_lock(self);

    if (selfp->idle_session)
      {
	mn_client_session_write(selfp->idle_session->session, "DONE");
	selfp->idle_session->idle_state = IDLE_STATE_POST_IDLE;
	selfp->idle_session->idle_inactivity = TRUE;
	selfp->idle_session->idle_inactivity_timeout_id = 0;
      }

    self_unlock(self);

    return FALSE;
  }

  private int
    session_write (MNClientSessionPrivate *priv (check null),
		   const char *format (check null),
		   ...)
    attr {G_GNUC_PRINTF(2, 3)}
  {
    char *command;
    int result;

    MN_STRDUP_VPRINTF(command, format);

    if (priv->numeric_tag == 1000)
      priv->numeric_tag = 0;
    sprintf(priv->tag, "a%03i", priv->numeric_tag++);

    result = mn_client_session_write(priv->session, "%s %s", priv->tag, command);
    g_free(command);

    return result;
  }

  private gboolean
    session_handle_capability_code (MNClientSessionPrivate *priv (check null),
				    MNClientSessionResponse *response (check null))
  {
    if (response->code)
      {
	if (! g_ascii_strcasecmp(response->code, "CAPABILITY"))
	  {
	    self_session_parse_capabilities(priv, NULL);
	    return TRUE;
	  }
	else if (mn_ascii_str_case_has_prefix(response->code, "CAPABILITY "))
	  {
	    self_session_parse_capabilities(priv, response->code + 11);
	    return TRUE;
	  }
      }

    return FALSE;
  }

  private void
    session_parse_capabilities (MNClientSessionPrivate *priv (check null),
				const char *capabilities)
  {
    g_strfreev(priv->capabilities);
    priv->capabilities = NULL;

    mn_g_slist_clear_deep(&priv->auth_mechanisms);

#if WITH_SASL
    mn_g_slist_clear(&priv->sasl_remaining_mechanisms);
#endif

    if (capabilities)
      {
	int i;

	priv->capabilities = g_strsplit(capabilities, " ", 0);

	for (i = 0; priv->capabilities[i]; i++)
	  if (g_str_has_prefix(priv->capabilities[i], "AUTH="))
	    priv->auth_mechanisms = g_slist_append(priv->auth_mechanisms, g_strdup(priv->capabilities[i] + 5));
      }
    else
      priv->capabilities = g_new0(char *, 1);
  }

  private gboolean
    session_has_capability (MNClientSessionPrivate *priv (check null),
			    const char *capability (check null))
  {
    int i;

    g_return_val_if_fail(priv->capabilities != NULL, FALSE);

    for (i = 0; priv->capabilities[i]; i++)
      if (! g_ascii_strcasecmp(priv->capabilities[i], capability))
	return TRUE;

    return FALSE;
  }

  private int
    session_after_capability (MNClientSessionPrivate *priv (check null))
  {
    if (priv->authenticated)
      return STATE_EXAMINE;
    else
      {
#if WITH_SSL
	if (priv->pi_mailbox->connection_type == MN_PI_MAILBOX_CONNECTION_TYPE_INBAND_SSL
	    && ! priv->starttls_completed)
	  {
	    if (self_session_has_capability(priv, "STARTTLS"))
	      return STATE_STARTTLS;
	    else
	      {
		mn_client_session_set_error(priv->session, MN_CLIENT_SESSION_ERROR_OTHER, _("server does not support in-band SSL/TLS"));
		return STATE_LOGOUT;
	      }
	  }
#endif /* WITH_SSL */
	return self_session_authenticate(priv);
      }
  }

  private int
    session_authenticate (MNClientSessionPrivate *priv (check null))
  {
#if WITH_SASL
    g_slist_free(priv->sasl_remaining_mechanisms);
    priv->sasl_remaining_mechanisms = g_slist_copy(priv->auth_mechanisms);
#endif /* WITH_SASL */

    if (priv->pi_mailbox->authmech)
      {
	if (*priv->pi_mailbox->authmech != '+')
	  {
#if WITH_SASL
	    return STATE_AUTHENTICATE;
#else
	    mn_client_session_set_error(priv->session, MN_CLIENT_SESSION_ERROR_OTHER, _("a SASL authentication mechanism was selected but SASL support has not been compiled in"));
	    return STATE_LOGOUT;
#endif /* WITH_SASL */
	  }
	else
	  {
	    if (! strcmp(priv->pi_mailbox->authmech, "+LOGIN"))
	      return STATE_LOGIN;
	    else
	      {
		mn_client_session_set_error(priv->session, MN_CLIENT_SESSION_ERROR_OTHER, _("unknown authentication mechanism \"%s\""), priv->pi_mailbox->authmech);
		return STATE_LOGOUT;
	      }
	  }
      }
    else
      {
#if WITH_SASL
	if (priv->sasl_remaining_mechanisms)
	  return STATE_AUTHENTICATE;
#endif /* WITH_SASL */
	return STATE_LOGIN;
      }
  }

  private int
    session_authenticate_fallback (MNClientSessionPrivate *priv (check null),
				   gboolean tried_login)
  {
    if (! priv->pi_mailbox->authmech)
      {
#if WITH_SASL
	if (priv->sasl_mechanism)
	  {
	    GSList *elem;

	    elem = mn_g_str_slist_find(priv->sasl_remaining_mechanisms, priv->sasl_mechanism);
	    if (elem)
	      {
		priv->sasl_remaining_mechanisms = g_slist_delete_link(priv->sasl_remaining_mechanisms, elem);
		if (priv->sasl_remaining_mechanisms)
		  {
		    mn_client_session_notice(priv->session, _("disabling mechanism \"%s\" and retrying SASL authentication"), priv->sasl_mechanism);
		    return STATE_AUTHENTICATE;
		  }
	      }
	  }

	/* SASL is not needed anymore, save some memory */
	mn_client_session_sasl_dispose(priv->session);
#endif /* WITH_SASL */

	if (! tried_login)
	  {
	    mn_client_session_notice(priv->session, _("falling back to IMAP LOGIN authentication"));
	    return STATE_LOGIN;
	  }
      }

    if (priv->auth_mailbox->auth_prompted)
      {
	mn_authenticated_mailbox_auth_failed(priv->auth_mailbox);
	return self_session_authenticate(priv);
      }
    else
      {
	mn_client_session_set_error(priv->session, MN_CLIENT_SESSION_ERROR_OTHER, _("authentication failed"));
	return STATE_LOGOUT;
      }
  }

  private void
    session_detect_imapd (MNClientSession *session (check null),
			  MNClientSessionResponse *greeting_response (check null),
			  MNClientSessionPrivate *priv (check null))
  {
    if (greeting_response->arguments)
      {
	char *str;

	/*
	 * Try to detect UW imapd by looking for " IMAP4rev1 YEAR."
	 */

	if ((str = mn_strstr_span(greeting_response->arguments, " IMAP4rev1 "))
	    && strspn(str, "0123456789") == 4
	    && str[4] == '.')
	  {
	    priv->server_software = "UW imapd";
	    priv->server_software_supports_idle = FALSE;
	    return;
	  }
      }
  }

  private int
    session_got_messages (MNClientSessionPrivate *priv (check null))
  {
    GSList *messages = NULL;

    g_hash_table_foreach(priv->messages, self_get_messages_list_cb, &messages);

    GDK_THREADS_ENTER();

    mn_mailbox_set_messages(priv->mailbox, messages);

    if (priv->num_errors == 0)
      mn_mailbox_set_error(priv->mailbox, NULL);
    else
      mn_mailbox_set_error(priv->mailbox,
			   ngettext("cannot retrieve %i message",
				    "cannot retrieve %i messages",
				    priv->num_errors),
			   priv->num_errors);

    /*
     * In authenticated_check(), we do not hold the GDK lock while
     * destroying the messages hash table. We have just exposed the
     * messages to other threads through our mn_mailbox_set_messages()
     * call, so destroy the hash table here, while we hold the GDK
     * lock.
     */
    g_hash_table_destroy(priv->messages);
    priv->messages = NULL;

    gdk_flush();
    GDK_THREADS_LEAVE();

    g_slist_free(messages);

    return STATE_IDLE;
  }

  private void
    get_messages_list_cb (gpointer key, gpointer value, gpointer user_data)
  {
    MessageInfo *info = value;
    GSList **list = user_data;

    if (info->message)
      *list = g_slist_prepend(*list, info->message);
  }

  private MessageInfo *
    session_get_message_info (MNClientSessionPrivate *priv (check null),
			      int number)
  {
    return g_hash_table_lookup(priv->messages, GINT_TO_POINTER(number));
  }

  private MessageInfo *
    session_ensure_message_info (MNClientSessionPrivate *priv (check null),
				 int number)
  {
    MessageInfo *info;

    info = self_session_get_message_info(priv, number);
    if (! info)
      {
	info = self_message_info_new(number);
	g_hash_table_insert(priv->messages, GINT_TO_POINTER(number), info);
      }

    return info;
  }

  private MessageInfo *
    message_info_new (int number)
  {
    MessageInfo *info;

    info = g_new0(MessageInfo, 1);
    info->number = number;

    return info;
  }

  private void
    message_info_free (MessageInfo *info (check null))
  {
    if (info->message)
      g_object_unref(info->message);
    g_free(info->mid);
    g_free(info);
  }

  /**
   * quote:
   * @str: the string to quote
   *
   * Quotes a string using RFC 3501 BNF rules.
   *
   * Return value: the quoted string.
   **/
  private char *
    quote (const char *str (check null))
  {
    GString *quoted;
    int i;

    quoted = g_string_new("\"");
    for (i = 0; str[i]; i++)
      if (str[i] == '"' || str[i] == '\\') /* quoted-specials in BNF */
	g_string_append_printf(quoted, "\\%c", str[i]);
      else
	g_string_append_c(quoted, str[i]);
    g_string_append_c(quoted, '"');

    return g_string_free(quoted, FALSE);
  }

  /**
   * utf8_to_imap_utf7:
   * @str: the UTF-8 string to convert to modified UTF-7
   *
   * Converts a string from UTF-8 to modified UTF-7 as defined by RFC 3501.
   *
   * Return value: the string converted to modified UTF-7.
   **/
  private char *
    utf8_to_imap_utf7 (const char *str (check null))
  {
    gunichar c;
    guint32 x, v = 0;
    int state = 0;
    GString *out;
    int i = 0;

    /*
     * Taken from the Ximian Evolution sources (camel-utf8.c) and
     * edited for style.
     */

    out = g_string_new(NULL);

    while ((c = g_utf8_get_char(str)))
      {
	if (c >= 0x20 && c <= 0x7e)
	  {
	    if (state == 1)
	      {
		self_imap_utf7_closeb64(out, v, i);
		state = 0;
		i = 0;
	      }
	    if (c == '&')
	      g_string_append(out, "&-");
	    else
	      g_string_append_c(out, c);
	  }
	else
	  {
	    if (state == 0)
	      {
		g_string_append_c(out, '&');
		state = 1;
	      }

	    v = (v << 16) | c;
	    i += 16;

	    while (i >= 6)
	      {
		x = (v >> (i - 6)) & 0x3f;
		g_string_append_c(out, utf7_alphabet[x]);
		i -= 6;
	      }
	  }

	str = g_utf8_next_char(str);
      }

    if (state == 1)
      self_imap_utf7_closeb64(out, v, i);

    return g_string_free(out, FALSE);
  }

  private void
    imap_utf7_closeb64 (GString *out (check null), guint32 v, guint32 i)
  {
    /*
     * Taken from the Ximian Evolution sources (camel-utf8.c) and
     * edited for style.
     */

    if (i > 0)
      {
	guint32 x;

	x = (v << (6 - i)) & 0x3f;
	g_string_append_c(out, utf7_alphabet[x]);
      }

    g_string_append_c(out, '-');
  }

  public char *
    build_name (const char *username (check null),
		const char *server (check null),
		const char *mailbox)
  {
    GString *name;

    name = g_string_new(NULL);

    g_string_append_printf(name, "%s@%s", username, server);

    if (mailbox && mn_utf8_strcasecmp(mailbox, "INBOX"))
      g_string_append_printf(name, "/%s", mailbox);

    return g_string_free(name, FALSE);
  }

  private void
    lock (self)
  {
    g_mutex_lock(selfp->mutex);
  }

  private void
    unlock (self)
  {
    g_mutex_unlock(selfp->mutex);
  }
}
