view libpurple/protocols/msn/contact.c @ 32797:aacfb71133cc

Fix a possible MSN remote crash Incoming messages with certain characters or character encodings can cause clients to crash. The fix is for the contents of all incoming plaintext messages are converted to UTF-8 and validated before used. This was reported to us by Fabian Yamaguchi and this patch was written by Elliott Sales de Andrade (maybe with small, insignificant changes by me)
author Mark Doliner <mark@kingant.net>
date Mon, 07 May 2012 03:18:08 +0000
parents e091c8ea292e
children e10e419e6067 01ff09d4a463
line wrap: on
line source

/**
 * @file contact.c
 * 	get MSN contacts via SOAP request
 *	created by MaYuan<mayuan2006@gmail.com>
 *
 * purple
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * 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 2 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
 */

#include "internal.h"
#include "debug.h"

#include "contact.h"
#include "xmlnode.h"
#include "group.h"
#include "msnutils.h"
#include "soap.h"
#include "nexus.h"
#include "user.h"

const char *MsnSoapPartnerScenarioText[] =
{
	"Initial",
	"ContactSave",
	"MessengerPendingList",
	"ContactMsgrAPI",
	"BlockUnblock",
	"Timer"
};

const char *MsnMemberRole[] =
{
	"Forward",
	"Allow",
	"Block",
	"Reverse",
	"Pending"
};

typedef struct {
	MsnSession *session;
	MsnSoapPartnerScenario which;
} GetContactListCbData;

MsnCallbackState *
msn_callback_state_new(MsnSession *session)
{
	MsnCallbackState *state = g_new0(MsnCallbackState, 1);

	state->session = session;

	return state;
}

MsnCallbackState *
msn_callback_state_dup(MsnCallbackState *state)
{
	MsnCallbackState *new_state = g_new0(MsnCallbackState, 1);

	new_state->session = state->session;
	new_state->who = g_strdup(state->who);
	new_state->uid = g_strdup(state->uid);
	new_state->old_group_name = g_strdup(state->old_group_name);
	new_state->new_group_name = g_strdup(state->new_group_name);
	new_state->guid = g_strdup(state->guid);
	/* The rest should be made new */

	return new_state;
}

void
msn_callback_state_free(MsnCallbackState *state)
{
	if (state == NULL)
		return;

	g_free(state->who);
	g_free(state->uid);
	g_free(state->old_group_name);
	g_free(state->new_group_name);
	g_free(state->guid);
	xmlnode_free(state->body);

	g_free(state);
}

void
msn_callback_state_set_who(MsnCallbackState *state, const gchar *who)
{
	g_return_if_fail(state != NULL);

	g_free(state->who);
	state->who = g_strdup(who);
}

void
msn_callback_state_set_uid(MsnCallbackState *state, const gchar *uid)
{
	g_return_if_fail(state != NULL);

	g_free(state->uid);
	state->uid = g_strdup(uid);
}

void
msn_callback_state_set_old_group_name(MsnCallbackState *state, const gchar *old_group_name)
{
	g_return_if_fail(state != NULL);

	g_free(state->old_group_name);
	state->old_group_name = g_strdup(old_group_name);
}

void
msn_callback_state_set_new_group_name(MsnCallbackState *state, const gchar *new_group_name)
{
	g_return_if_fail(state != NULL);

	g_free(state->new_group_name);
	state->new_group_name = g_strdup(new_group_name);
}

void
msn_callback_state_set_guid(MsnCallbackState *state, const gchar *guid)
{
	g_return_if_fail(state != NULL);

	g_free(state->guid);
	state->guid = g_strdup(guid);
}


void
msn_callback_state_set_list_id(MsnCallbackState *state, MsnListId list_id)
{
	g_return_if_fail(state != NULL);

	state->list_id = list_id;
}

void
msn_callback_state_set_action(MsnCallbackState *state, MsnCallbackAction action)
{
	g_return_if_fail(state != NULL);

	state->action |= action;
}

/***************************************************************
 * General SOAP handling
 ***************************************************************/

static const char *
msn_contact_operation_str(MsnCallbackAction action)
{
	/* Make sure this is large enough when adding more */
	static char buf[BUF_LEN];
	buf[0] = '\0';

	if (action & MSN_ADD_BUDDY)
		strcat(buf, "Adding Buddy,");
	if (action & MSN_MOVE_BUDDY)
		strcat(buf, "Moving Buddy,");
	if (action & MSN_ACCEPTED_BUDDY)
		strcat(buf, "Accepted Buddy,");
	if (action & MSN_DENIED_BUDDY)
		strcat(buf, "Denied Buddy,");
	if (action & MSN_ADD_GROUP)
		strcat(buf, "Adding Group,");
	if (action & MSN_DEL_GROUP)
		strcat(buf, "Deleting Group,");
	if (action & MSN_RENAME_GROUP)
		strcat(buf, "Renaming Group,");
	if (action & MSN_UPDATE_INFO)
		strcat(buf, "Updating Contact Info,");
	if (action & MSN_ANNOTATE_USER)
		strcat(buf, "Annotating Contact,");

	return buf;
}

static gboolean msn_contact_request(MsnCallbackState *state);

static void
msn_contact_request_cb(MsnSoapMessage *req, MsnSoapMessage *resp,
	gpointer data)
{
	MsnCallbackState *state = data;
	xmlnode *fault;
	char *faultcode_str;
	xmlnode *cachekey;
	char *changed;

	if (resp == NULL) {
		purple_debug_error("msn",
		                   "Operation {%s} failed. No response received from server.\n",
		                   msn_contact_operation_str(state->action));
		msn_session_set_error(state->session, MSN_ERROR_BAD_BLIST, NULL);
		msn_callback_state_free(state);
		return;
	}

 	/* Update CacheKey if necessary */
 	cachekey = xmlnode_get_child(resp->xml, "Header/ServiceHeader/CacheKeyChanged");
 	if (cachekey != NULL) {
 		changed = xmlnode_get_data(cachekey);
 		if (changed && !strcmp(changed, "true")) {
 			cachekey = xmlnode_get_child(resp->xml, "Header/ServiceHeader/CacheKey");
 			g_free(state->session->abch_cachekey);
 			state->session->abch_cachekey = xmlnode_get_data(cachekey);
 			purple_debug_info("msn", "Updated CacheKey for %s to '%s'.\n",
 			                  purple_account_get_username(state->session->account),
 			                  state->session->abch_cachekey);
 		}
 		g_free(changed);
 	}

	fault = xmlnode_get_child(resp->xml, "Body/Fault");

	if (fault == NULL) {
		/* No errors */
		if (state->cb)
			state->cb(req, resp, data);
		msn_callback_state_free(state);
		return;
	}

	faultcode_str = xmlnode_get_data(xmlnode_get_child(fault, "faultcode"));

	if (faultcode_str && g_str_equal(faultcode_str, "q0:BadContextToken")) {
		purple_debug_info("msn",
		                  "Contact Operation {%s} failed because of bad token."
		                  " Updating token now and retrying operation.\n",
		                  msn_contact_operation_str(state->action));
		/* Token has expired, so renew it, and try again later */
		msn_nexus_update_token(state->session->nexus, MSN_AUTH_CONTACTS,
		                       (GSourceFunc)msn_contact_request, data);
	}
	else
	{
		if (state->cb) {
			state->cb(req, resp, data);
		} else {
			/* We don't know how to respond to this faultcode, so log it */
			char *str = xmlnode_to_str(fault, NULL);
			purple_debug_error("msn", "Operation {%s} Failed, SOAP Fault was: %s\n",
			                   msn_contact_operation_str(state->action), str);
			g_free(str);
		}
		msn_callback_state_free(state);
	}

	g_free(faultcode_str);
}

static gboolean
msn_contact_request(MsnCallbackState *state)
{
	xmlnode *cachekey = xmlnode_get_child(state->body,
	                                      "Header/ABApplicationHeader/CacheKey");
	if (cachekey != NULL)
		xmlnode_free(cachekey);
	if (state->session->abch_cachekey != NULL) {
		cachekey = xmlnode_new_child(xmlnode_get_child(state->body, "Header/ABApplicationHeader"), "CacheKey");
		xmlnode_insert_data(cachekey, state->session->abch_cachekey, -1);
	}
	if (state->token == NULL)
		state->token = xmlnode_get_child(state->body,
			"Header/ABAuthHeader/TicketToken");
	/* delete old & replace with new token */
	xmlnode_free(state->token->child);
	xmlnode_insert_data(state->token,
		msn_nexus_get_token_str(state->session->nexus, MSN_AUTH_CONTACTS), -1);
	msn_soap_message_send(state->session,
		msn_soap_message_new(state->post_action, xmlnode_copy(state->body)),
		MSN_CONTACT_SERVER, state->post_url, FALSE,
		msn_contact_request_cb, state);
	return FALSE;
}

/***************************************************************
 * Address Book and Membership List Operations
 ***************************************************************/

/*get MSN member role utility*/
static MsnListId
msn_get_memberrole(const char *role)
{
	g_return_val_if_fail(role != NULL, 0);

	if (!strcmp(role,"Allow")) {
		return MSN_LIST_AL;
	} else if (!strcmp(role,"Block")) {
		return MSN_LIST_BL;
	} else if (!strcmp(role,"Reverse")) {
		return MSN_LIST_RL;
	} else if (!strcmp(role,"Pending")) {
		return MSN_LIST_PL;
	}
	return 0;
}

/* Create the AddressBook in the server, if we don't have one */
static void
msn_create_address_cb(MsnSoapMessage *req, MsnSoapMessage *resp, gpointer data)
{
	MsnCallbackState *state = data;
	if (resp && xmlnode_get_child(resp->xml, "Body/Fault") == NULL) {
		purple_debug_info("msn", "Address Book successfully created!\n");
		msn_get_address_book(state->session, MSN_PS_INITIAL, NULL, NULL);
	} else {
		purple_debug_info("msn", "Address Book creation failed!\n");
	}
}

static void
msn_create_address_book(MsnSession *session)
{
	gchar *body;
	MsnCallbackState *state;

	g_return_if_fail(session != NULL);
	g_return_if_fail(session->user != NULL);
	g_return_if_fail(session->user->passport != NULL);

	purple_debug_info("msn", "Creating an Address Book.\n");

	body = g_strdup_printf(MSN_ADD_ADDRESSBOOK_TEMPLATE,
	                       session->user->passport);

	state = msn_callback_state_new(session);
	state->body = xmlnode_from_str(body, -1);
	state->post_action = MSN_ADD_ADDRESSBOOK_SOAP_ACTION;
	state->post_url = MSN_ADDRESS_BOOK_POST_URL;
	state->cb = msn_create_address_cb;
	msn_contact_request(state);

	g_free(body);
}

static void
msn_parse_each_member(MsnSession *session, xmlnode *member, const char *node,
	MsnListId list)
{
	char *passport;
	char *type;
	char *member_id;
	MsnUser *user;
	xmlnode *annotation;
	guint nid = MSN_NETWORK_UNKNOWN;
	char *invite = NULL;

	passport = xmlnode_get_data(xmlnode_get_child(member, node));
	if (!msn_email_is_valid(passport)) {
		g_free(passport);
		return;
	}

	type = xmlnode_get_data(xmlnode_get_child(member, "Type"));
	member_id = xmlnode_get_data(xmlnode_get_child(member, "MembershipId"));

	user = msn_userlist_find_add_user(session->userlist, passport, NULL);

	for (annotation = xmlnode_get_child(member, "Annotations/Annotation");
	     annotation;
	     annotation = xmlnode_get_next_twin(annotation)) {
		char *name = xmlnode_get_data(xmlnode_get_child(annotation, "Name"));
		char *value = xmlnode_get_data(xmlnode_get_child(annotation, "Value"));
		if (name && value) {
			if (!strcmp(name, "MSN.IM.BuddyType")) {
				nid = strtoul(value, NULL, 10);
			}
			else if (!strcmp(name, "MSN.IM.InviteMessage")) {
				invite = value;
				value = NULL;
			}
		}
		g_free(name);
		g_free(value);
	}

	/* For EmailMembers, the network must be found in the annotations, above.
	   Otherwise, PassportMembers are on the Passport network. */
	if (!strcmp(node, "PassportName"))
		nid = MSN_NETWORK_PASSPORT;

	purple_debug_info("msn", "CL: %s name: %s, Type: %s, MembershipID: %s, NetworkID: %u\n",
		node, passport, type, member_id == NULL ? "(null)" : member_id, nid);

	msn_user_set_network(user, nid);
	msn_user_set_invite_message(user, invite);

	if (list == MSN_LIST_PL && member_id) {
		user->member_id_on_pending_list = atoi(member_id);
	}

	msn_got_lst_user(session, user, 1 << list, NULL);

	g_free(passport);
	g_free(type);
	g_free(member_id);
	g_free(invite);
}

static void
msn_parse_each_service(MsnSession *session, xmlnode *service)
{
	xmlnode *type;

	if ((type = xmlnode_get_child(service, "Info/Handle/Type"))) {
		char *type_str = xmlnode_get_data(type);

		if (g_str_equal(type_str, "Profile")) {
			/* Process Windows Live 'Messenger Roaming Identity' */
		} else if (g_str_equal(type_str, "Messenger")) {
			xmlnode *lastchange = xmlnode_get_child(service, "LastChange");
			char *lastchange_str = xmlnode_get_data(lastchange);
			xmlnode *membership;

			purple_debug_info("msn", "CL last change: %s\n", lastchange_str);
			purple_account_set_string(session->account,	"CLLastChange",
				lastchange_str);

			for (membership = xmlnode_get_child(service,
					"Memberships/Membership");
				 membership; membership = xmlnode_get_next_twin(membership)) {

				xmlnode *role = xmlnode_get_child(membership, "MemberRole");
				char *role_str = xmlnode_get_data(role);
				MsnListId list = msn_get_memberrole(role_str);
				xmlnode *member;

				purple_debug_info("msn", "CL MemberRole role: %s, list: %d\n",
					role_str, list);

				for (member = xmlnode_get_child(membership, "Members/Member");
					 member; member = xmlnode_get_next_twin(member)) {
					const char *member_type = xmlnode_get_attrib(member, "type");
					if (g_str_equal(member_type, "PassportMember")) {
						msn_parse_each_member(session, member, "PassportName",
							list);
					} else if (g_str_equal(member_type, "PhoneMember")) {

					} else if (g_str_equal(member_type, "EmailMember")) {
						msn_parse_each_member(session, member, "Email",	list);
					}
				}

				g_free(role_str);
			}

			g_free(lastchange_str);
		}

		g_free(type_str);
	}
}

/*parse contact list*/
static gboolean
msn_parse_contact_list(MsnSession *session, xmlnode *node)
{
	xmlnode *fault, *faultnode;

	/* we may get a response if our cache data is too old:
	 *
	 * <faultstring>Need to do full sync. Can't sync deltas Client
	 * has too old a copy for us to do a delta sync</faultstring>
	 *
	 * this is not handled yet
	 */
	if ((fault = xmlnode_get_child(node, "Body/Fault"))) {
		if ((faultnode = xmlnode_get_child(fault, "faultstring"))) {
			char *faultstring = xmlnode_get_data(faultnode);
			purple_debug_info("msn", "Retrieving contact list failed: %s\n",
				faultstring);
			g_free(faultstring);
		}
		if ((faultnode = xmlnode_get_child(fault, "detail/errorcode"))) {
			char *errorcode = xmlnode_get_data(faultnode);

			if (g_str_equal(errorcode, "ABDoesNotExist")) {
				msn_create_address_book(session);
				g_free(errorcode);
				return FALSE;
			}

			g_free(errorcode);
		}

		msn_get_contact_list(session, MSN_PS_INITIAL, NULL);
		return FALSE;
	} else {
		xmlnode *service;

		for (service = xmlnode_get_child(node, "Body/FindMembershipResponse/"
				"FindMembershipResult/Services/Service");
			 service; service = xmlnode_get_next_twin(service)) {
			msn_parse_each_service(session, service);
		}
		return TRUE;
	}
}

static void
msn_get_contact_list_cb(MsnSoapMessage *req, MsnSoapMessage *resp,
	gpointer data)
{
	MsnCallbackState *state = data;
	MsnSession *session = state->session;

	g_return_if_fail(session != NULL);

	if (resp != NULL) {
#ifdef MSN_PARTIAL_LISTS
		const char *abLastChange;
		const char *dynamicItemLastChange;
#endif

		purple_debug_misc("msn", "Got the contact list!\n");

		if (msn_parse_contact_list(session, resp->xml)) {
#ifdef MSN_PARTIAL_LISTS
			abLastChange = purple_account_get_string(session->account,
				"ablastChange", NULL);
			dynamicItemLastChange = purple_account_get_string(session->account,
				"DynamicItemLastChanged", NULL);
#endif

			if (state->partner_scenario == MSN_PS_INITIAL) {
#ifdef MSN_PARTIAL_LISTS
				/* XXX: this should be enabled when we can correctly do partial
				   syncs with the server. Currently we need to retrieve the whole
				   list to detect sync issues */
				msn_get_address_book(session, MSN_PS_INITIAL, abLastChange, dynamicItemLastChange);
#else
				msn_get_address_book(session, MSN_PS_INITIAL, NULL, NULL);
#endif
			}
		}
	}
}

/*SOAP  get contact list*/
void
msn_get_contact_list(MsnSession *session,
	const MsnSoapPartnerScenario partner_scenario, const char *update_time)
{
	gchar *body = NULL;
	gchar *update_str = NULL;
	MsnCallbackState *state;
	const gchar *partner_scenario_str = MsnSoapPartnerScenarioText[partner_scenario];

	purple_debug_misc("msn", "Getting Contact List.\n");

	if (update_time != NULL) {
		purple_debug_info("msn", "CL Last update time: %s\n", update_time);
		update_str = g_strdup_printf(MSN_GET_CONTACT_UPDATE_XML, update_time);
	}

	body = g_strdup_printf(MSN_GET_CONTACT_TEMPLATE, partner_scenario_str,
	                       update_str ? update_str : "");

	state = msn_callback_state_new(session);
	state->partner_scenario = partner_scenario;
	state->body = xmlnode_from_str(body, -1);
	state->post_action = MSN_GET_CONTACT_SOAP_ACTION;
	state->post_url = MSN_GET_CONTACT_POST_URL;
	state->cb = msn_get_contact_list_cb;
	msn_contact_request(state);

	g_free(update_str);
	g_free(body);
}

static void
msn_parse_addressbook_groups(MsnSession *session, xmlnode *node)
{
	xmlnode *group;

	purple_debug_info("msn", "msn_parse_addressbook_groups()\n");

	for(group = xmlnode_get_child(node, "Group"); group;
					group = xmlnode_get_next_twin(group)){
		xmlnode *groupId, *groupInfo, *groupname;
		char *group_id = NULL, *group_name = NULL;

		if ((groupId = xmlnode_get_child(group, "groupId")))
			group_id = xmlnode_get_data(groupId);
		if ((groupInfo = xmlnode_get_child(group, "groupInfo")) && (groupname = xmlnode_get_child(groupInfo, "name")))
			group_name = xmlnode_get_data(groupname);

		if (group_id == NULL) {
			/* Group of ungroupped buddies */
			g_free(group_name);
			continue;
		}

		msn_group_new(session->userlist, group_id, group_name);

		purple_debug_info("msn", "AB group_id: %s, name: %s\n", group_id, group_name ? group_name : "(null)");
		if ((purple_find_group(group_name)) == NULL) {
			PurpleGroup *g = purple_group_new(group_name);
			purple_blist_add_group(g, NULL);
		}
		g_free(group_id);
		g_free(group_name);
	}
}

static gboolean
msn_parse_addressbook_mobile(xmlnode *contactInfo, char **inout_mobile_number)
{
	xmlnode *phones;
	char *mobile_number = NULL;
	gboolean mobile = FALSE;

	*inout_mobile_number = NULL;

	if ((phones = xmlnode_get_child(contactInfo, "phones"))) {
		xmlnode *contact_phone;
		char *phone_type = NULL;

		for (contact_phone = xmlnode_get_child(phones, "ContactPhone");
			 contact_phone;
			 contact_phone = xmlnode_get_next_twin(contact_phone)) {
			xmlnode *contact_phone_type;

			if (!(contact_phone_type =
					xmlnode_get_child(contact_phone, "contactPhoneType")))
				continue;

			phone_type = xmlnode_get_data(contact_phone_type);

			if (phone_type && !strcmp(phone_type, "ContactPhoneMobile")) {
				xmlnode *number;

				if ((number = xmlnode_get_child(contact_phone, "number"))) {
					xmlnode *messenger_enabled;
					char *is_messenger_enabled = NULL;

					g_free(mobile_number);
					mobile_number = xmlnode_get_data(number);

					if (mobile_number &&
							(messenger_enabled = xmlnode_get_child(contact_phone, "isMessengerEnabled"))
							&& (is_messenger_enabled = xmlnode_get_data(messenger_enabled))
							&& !strcmp(is_messenger_enabled, "true"))
						mobile = TRUE;

					g_free(is_messenger_enabled);
				}
			}

			g_free(phone_type);
		}
	}

	*inout_mobile_number = mobile_number;
	return mobile;
}

static void
msn_parse_addressbook_contacts(MsnSession *session, xmlnode *node)
{
	xmlnode *contactNode;
	char *passport = NULL, *Name = NULL, *uid = NULL, *type = NULL, *mobile_number = NULL, *alias = NULL;
	gboolean mobile = FALSE;
	PurpleConnection *pc = purple_account_get_connection(session->account);

	for(contactNode = xmlnode_get_child(node, "Contact"); contactNode;
				contactNode = xmlnode_get_next_twin(contactNode)) {
		xmlnode *contactId, *contactInfo, *contactType, *passportName, *displayName, *guid, *groupIds;
		xmlnode *annotation;
		MsnUser *user;

		g_free(passport);
		g_free(Name);
		g_free(uid);
		g_free(type);
		g_free(mobile_number);
		g_free(alias);
		passport = Name = uid = type = mobile_number = alias = NULL;
		mobile = FALSE;

		if (!(contactId = xmlnode_get_child(contactNode,"contactId"))
				|| !(contactInfo = xmlnode_get_child(contactNode, "contactInfo"))
				|| !(contactType = xmlnode_get_child(contactInfo, "contactType")))
			continue;

		uid = xmlnode_get_data(contactId);
		type = xmlnode_get_data(contactType);

		/* Find out our settings */
		if (type && !strcmp(type, "Me")) {
			/* setup the Display Name */
			if (purple_connection_get_display_name(pc) == NULL) {
				char *friendly = NULL;
				if ((displayName = xmlnode_get_child(contactInfo, "displayName")))
					friendly = xmlnode_get_data(displayName);
				purple_connection_set_display_name(pc,
					friendly ? purple_url_decode(friendly) : NULL);
				g_free(friendly);
			}

			for (annotation = xmlnode_get_child(contactInfo, "annotations/Annotation");
			     annotation;
			     annotation = xmlnode_get_next_twin(annotation)) {
				char *name, *value;
				name = xmlnode_get_data(xmlnode_get_child(annotation, "Name"));
				value = xmlnode_get_data(xmlnode_get_child(annotation, "Value"));
				if (!strcmp(name, "MSN.IM.MPOP")) {
					if (!value || atoi(value) != 0)
						session->enable_mpop = TRUE;
					else
						session->enable_mpop = FALSE;
				}
				g_free(name);
				g_free(value);
			}

			continue; /* Not adding own account as buddy to buddylist */
		}

		passportName = xmlnode_get_child(contactInfo, "passportName");
		if (passportName == NULL) {
			xmlnode *emailsNode, *contactEmailNode, *emailNode;
			xmlnode *messengerEnabledNode;
			char *msnEnabled;

			/*TODO: add it to the non-instant Messenger group and recognize as email Membership*/
			/* Yahoo/Federated User? */
			emailsNode = xmlnode_get_child(contactInfo, "emails");
			if (emailsNode == NULL) {
				/*TODO:  need to support the Mobile type*/
				continue;
			}
			for (contactEmailNode = xmlnode_get_child(emailsNode, "ContactEmail");
			     contactEmailNode;
			     contactEmailNode = xmlnode_get_next_twin(contactEmailNode)) {
				if ((messengerEnabledNode = xmlnode_get_child(contactEmailNode, "isMessengerEnabled"))) {

					msnEnabled = xmlnode_get_data(messengerEnabledNode);

					if (msnEnabled && !strcmp(msnEnabled, "true")) {
						if ((emailNode = xmlnode_get_child(contactEmailNode, "email")))
							passport = xmlnode_get_data(emailNode);

						/* Messenger enabled, Get the Passport*/
						purple_debug_info("msn", "AB Yahoo/Federated User %s\n", passport ? passport : "(null)");
						g_free(msnEnabled);
						break;
					}

					g_free(msnEnabled);
				}
			}
		} else {
			xmlnode *messenger_user;
			/* ignore non-messenger contacts */
			if ((messenger_user = xmlnode_get_child(contactInfo, "isMessengerUser"))) {
				char *is_messenger_user = xmlnode_get_data(messenger_user);

				if (is_messenger_user && !strcmp(is_messenger_user, "false")) {
					g_free(is_messenger_user);
					continue;
				}

				g_free(is_messenger_user);
			}

			passport = xmlnode_get_data(passportName);
		}

		/* Couldn't find anything */
		if (passport == NULL)
			continue;

		if (!msn_email_is_valid(passport))
			continue;

		if ((displayName = xmlnode_get_child(contactInfo, "displayName")))
			Name = xmlnode_get_data(displayName);
		else
			Name = g_strdup(passport);

		for (annotation = xmlnode_get_child(contactInfo, "annotations/Annotation");
				annotation; annotation = xmlnode_get_next_twin(annotation)) {
			char *name;
			name = xmlnode_get_data(xmlnode_get_child(annotation, "Name"));
			if (!strcmp(name, "AB.NickName"))
				alias = xmlnode_get_data(xmlnode_get_child(annotation, "Value"));
			else if (!strcmp(name, "MSN.IM.HasSharedFolder"))
				; /* Do nothing yet... */
			else if (!strcmp(name, "AB.Spouse"))
				; /* Do nothing yet... */
			else if (!strcmp(name, "MSN.Mobile.ContactId"))
				; /* Do nothing yet... */
			else
				purple_debug_info("msn",
				                  "Unknown AB contact annotation: %s\n", name);
			g_free(name);
		}

		mobile = msn_parse_addressbook_mobile(contactInfo, &mobile_number);

		purple_debug_misc("msn", "AB passport:{%s} uid:{%s} display:{%s} alias: {%s} mobile:{%s} mobile number:{%s}\n",
			passport, uid ? uid : "(null)", Name ? Name : "(null)", alias ? alias : "(null)",
			mobile ? "true" : "false", mobile_number ? mobile_number : "(null)");

		user = msn_userlist_find_add_user(session->userlist, passport, Name);
		msn_user_set_uid(user, uid);
		msn_user_set_mobile_phone(user, mobile_number);

		groupIds = xmlnode_get_child(contactInfo, "groupIds");
		if (groupIds) {
			for (guid = xmlnode_get_child(groupIds, "guid"); guid;
							guid = xmlnode_get_next_twin(guid)) {
				char *group_id = xmlnode_get_data(guid);
				msn_user_add_group_id(user, group_id);
				purple_debug_misc("msn", "AB guid:%s\n", group_id ? group_id : "(null)");
				g_free(group_id);
			}
		} else {
			purple_debug_info("msn", "User not in any groups, adding to default group.\n");
			/*not in any group,Then set default group*/
			msn_user_add_group_id(user, MSN_INDIVIDUALS_GROUP_ID);
		}

		msn_got_lst_user(session, user, MSN_LIST_FL_OP, NULL);

		if (mobile && user)
		{
			user->mobile = TRUE;
			purple_prpl_got_user_status(session->account, user->passport, "mobile", NULL);
			purple_prpl_got_user_status(session->account, user->passport, "available", NULL);
		}
		if (alias)
			purple_serv_got_private_alias(pc, passport, alias);
	}

	g_free(passport);
	g_free(Name);
	g_free(uid);
	g_free(type);
	g_free(mobile_number);
	g_free(alias);
}

static void
msn_parse_addressbook_circles(MsnSession *session, xmlnode *node)
{
	xmlnode *ticket;

	/* TODO: Parse groups */

	ticket = xmlnode_get_child(node, "CircleTicket");
	if (ticket) {
		char *data = xmlnode_get_data(ticket);
		msn_notification_send_circle_auth(session, data);
		g_free(data);
	}
}

static gboolean
msn_parse_addressbook(MsnSession *session, xmlnode *node)
{
	xmlnode *result;
	xmlnode *groups;
	xmlnode *contacts;
	xmlnode *abNode;
	xmlnode *circleNode;
	xmlnode *fault;

	if ((fault = xmlnode_get_child(node, "Body/Fault"))) {
		xmlnode *faultnode;

		if ((faultnode = xmlnode_get_child(fault, "faultstring"))) {
			gchar *faultstring = xmlnode_get_data(faultnode);
			purple_debug_info("msn", "AB Faultstring: %s\n", faultstring);
			g_free(faultstring);
		}

		if ((faultnode = xmlnode_get_child(fault, "detail/errorcode"))) {
			gchar *errorcode = xmlnode_get_data(faultnode);

			purple_debug_info("msn", "AB Error Code: %s\n", errorcode);

			if (g_str_equal(errorcode, "ABDoesNotExist")) {
				g_free(errorcode);
				return TRUE;
			}
			g_free(errorcode);
		}

		return FALSE;
	}

	result = xmlnode_get_child(node, "Body/ABFindContactsPagedResponse/ABFindContactsPagedResult");
	if (result == NULL) {
		purple_debug_misc("msn", "Received no address book update\n");
		return TRUE;
	}

	/* I don't see this "groups" tag documented on msnpiki, need to find out
	   if they are really there, and update msnpiki */
	/*Process Group List*/
	groups = xmlnode_get_child(result, "Groups");
	if (groups != NULL) {
		msn_parse_addressbook_groups(session, groups);
	}

	/* Add an "Other Contacts" group for buddies who aren't in a group */
	msn_group_new(session->userlist, MSN_INDIVIDUALS_GROUP_ID,
				  MSN_INDIVIDUALS_GROUP_NAME);
	purple_debug_misc("msn", "AB group_id:%s name:%s\n",
					  MSN_INDIVIDUALS_GROUP_ID, MSN_INDIVIDUALS_GROUP_NAME);
	if ((purple_find_group(MSN_INDIVIDUALS_GROUP_NAME)) == NULL){
		PurpleGroup *g = purple_group_new(MSN_INDIVIDUALS_GROUP_NAME);
		purple_blist_add_group(g, NULL);
	}

	/* Add a "Non-IM Contacts" group */
	msn_group_new(session->userlist, MSN_NON_IM_GROUP_ID, MSN_NON_IM_GROUP_NAME);
	purple_debug_misc("msn", "AB group_id:%s name:%s\n", MSN_NON_IM_GROUP_ID, MSN_NON_IM_GROUP_NAME);
	if ((purple_find_group(MSN_NON_IM_GROUP_NAME)) == NULL) {
		PurpleGroup *g = purple_group_new(MSN_NON_IM_GROUP_NAME);
		purple_blist_add_group(g, NULL);
	}

	/*Process contact List*/
	purple_debug_info("msn", "Process contact list...\n");
	contacts = xmlnode_get_child(result, "Contacts");
	if (contacts != NULL) {
		msn_parse_addressbook_contacts(session, contacts);
	}

	abNode = xmlnode_get_child(result, "Ab");
	if (abNode != NULL) {
		xmlnode *node2;
		char *tmp = NULL;

		if ((node2 = xmlnode_get_child(abNode, "lastChange")))
			tmp = xmlnode_get_data(node2);
		purple_debug_info("msn", "AB lastchanged Time:{%s}\n", tmp ? tmp : "(null)");
		purple_account_set_string(session->account, "ablastChange", tmp);

		g_free(tmp); tmp = NULL;
		if ((node2 = xmlnode_get_child(abNode, "DynamicItemLastChanged")))
			tmp = xmlnode_get_data(node2);
		purple_debug_info("msn", "AB DynamicItemLastChanged :{%s}\n", tmp ? tmp : "(null)");
		purple_account_set_string(session->account, "DynamicItemLastChanged", tmp);
		g_free(tmp);
	}

	circleNode = xmlnode_get_child(result, "CircleResult");
	if (circleNode != NULL) {
		msn_parse_addressbook_circles(session, circleNode);
	}

	return TRUE;
}

static void
msn_get_address_cb(MsnSoapMessage *req, MsnSoapMessage *resp, gpointer data)
{
	MsnCallbackState *state = data;
	MsnSession *session = state->session;

	g_return_if_fail(session != NULL);

	purple_debug_misc("msn", "Got the Address Book!\n");

	if (msn_parse_addressbook(session, resp->xml)) {
		msn_send_privacy(session->account->gc);
		msn_notification_dump_contact(session);
	} else {
		/* This is making us loop infinitely when we fail to parse the
		  address book, disable for now (we should re-enable when we
		  send timestamps)
		*/
		/*
		msn_get_address_book(session, NULL, NULL);
		*/
		msn_session_set_error(session, MSN_ERROR_BAD_BLIST, NULL);
	}
}

/*get the address book*/
void
msn_get_address_book(MsnSession *session,
	MsnSoapPartnerScenario partner_scenario, const char *LastChanged,
	const char *dynamicItemLastChange)
{
	char *body, *update_str = NULL;
	MsnCallbackState *state;

	purple_debug_misc("msn", "Getting Address Book\n");

	/*build SOAP and POST it*/
	if (dynamicItemLastChange != NULL)
		update_str = g_strdup_printf(MSN_GET_ADDRESS_UPDATE_XML, dynamicItemLastChange);
	else if (LastChanged != NULL)
		update_str = g_strdup_printf(MSN_GET_ADDRESS_UPDATE_XML, LastChanged);

	body = g_strdup_printf(MSN_GET_ADDRESS_TEMPLATE,
		MsnSoapPartnerScenarioText[partner_scenario],
		update_str ? update_str : "");

	state = msn_callback_state_new(session);
	state->body = xmlnode_from_str(body, -1);
	state->post_action = MSN_GET_ADDRESS_SOAP_ACTION;
	state->post_url = MSN_ADDRESS_BOOK_POST_URL;
	state->cb = msn_get_address_cb;
	msn_contact_request(state);

	g_free(update_str);
	g_free(body);
}

/***************************************************************
 * Contact Operations
 ***************************************************************/

static void
msn_add_contact_read_cb(MsnSoapMessage *req, MsnSoapMessage *resp,
	gpointer data)
{
	MsnCallbackState *state = data;
	MsnSession *session = state->session;

	MsnUserList *userlist;
	MsnUser *user;
	xmlnode *guid;

	xmlnode *fault;

	g_return_if_fail(session != NULL);
	userlist = session->userlist;

	fault = xmlnode_get_child(resp->xml, "Body/Fault");
	if (fault != NULL) {
		char *errorcode = xmlnode_get_data(xmlnode_get_child(fault, "detail/errorcode"));
		if (errorcode && !strcmp(errorcode, "EmailDomainIsFederated")) {
			/* Do something special! */
			purple_debug_error("msn", "Contact is from a federated domain, but don't know what to do yet!\n");

		} else if (errorcode && !strcmp(errorcode, "InvalidPassportUser")) {
			PurpleBuddy *buddy = purple_find_buddy(session->account, state->who);
			char *str = g_strdup_printf(_("Unable to add \"%s\"."), state->who);
			purple_notify_error(state->session, _("Buddy Add error"), str,
			                    _("The username specified does not exist."));
			g_free(str);
			msn_userlist_rem_buddy(userlist, state->who);
			if (buddy != NULL)
				purple_blist_remove_buddy(buddy);

		} else {
			/* We don't know how to respond to this faultcode, so log it */
			char *fault_str = xmlnode_to_str(fault, NULL);
			if (fault_str != NULL) {
				purple_debug_error("msn", "Operation {%s} Failed, SOAP Fault was: %s\n",
				                   msn_contact_operation_str(state->action), fault_str);
				g_free(fault_str);
			}
		}
		return;
	}

	purple_debug_info("msn", "Contact added successfully\n");

	msn_userlist_add_buddy_to_list(userlist, state->who, MSN_LIST_AL);
	msn_userlist_add_buddy_to_list(userlist, state->who, MSN_LIST_FL);

	user = msn_userlist_find_add_user(userlist, state->who, state->who);
	msn_user_add_group_id(user, state->guid);

	guid = xmlnode_get_child(resp->xml,
		"Body/ABContactAddResponse/ABContactAddResult/guid");
	if (guid != NULL) {
		char *uid = xmlnode_get_data(guid);
		msn_user_set_uid(user, uid);
		purple_debug_info("msn", "Set %s guid to %s.\n", state->who, uid);
		g_free(uid);
	}
}

/* add a Contact in MSN_INDIVIDUALS_GROUP */
void
msn_add_contact(MsnSession *session, MsnCallbackState *state, const char *passport)
{
	MsnUser *user;
	gchar *body = NULL;
	gchar *contact_xml = NULL;

	purple_debug_info("msn", "Adding contact %s to contact list\n", passport);

	user = msn_userlist_find_user(session->userlist, passport);
	if (user == NULL) {
		purple_debug_warning("msn", "Unable to retrieve user %s from the userlist!\n", passport);
		return; /* guess this never happened! */
	}

	if (user->networkid != MSN_NETWORK_PASSPORT) {
		contact_xml = g_strdup_printf(MSN_CONTACT_EMAIL_XML,
		                              user->networkid == MSN_NETWORK_YAHOO ?
		                                  "Messenger2" :
		                                  "Messenger3",
		                              passport, 0);
	} else {
		contact_xml = g_strdup_printf(MSN_CONTACT_XML, passport);
	}
	body = g_strdup_printf(MSN_ADD_CONTACT_TEMPLATE, contact_xml);

	state->body = xmlnode_from_str(body, -1);
	state->post_action = MSN_CONTACT_ADD_SOAP_ACTION;
	state->post_url = MSN_ADDRESS_BOOK_POST_URL;
	state->cb = msn_add_contact_read_cb;
	msn_contact_request(state);

	g_free(contact_xml);
	g_free(body);
}

static void
msn_add_contact_to_group_read_cb(MsnSoapMessage *req, MsnSoapMessage *resp,
	gpointer data)
{
	MsnCallbackState *state = data;
	MsnSession *session = state->session;
	MsnUserList *userlist;
	xmlnode *fault;

	g_return_if_fail(session != NULL);
	userlist = session->userlist;

	fault = xmlnode_get_child(resp->xml, "Body/Fault");
	if (fault != NULL) {
		char *errorcode = xmlnode_get_data(xmlnode_get_child(fault, "detail/errorcode"));
		if (errorcode && !strcmp(errorcode, "EmailDomainIsFederated")) {
			/* Do something special! */
			purple_debug_error("msn", "Contact is from a federated domain, but don't know what to do yet!\n");

		} else if (errorcode && !strcmp(errorcode, "InvalidPassportUser")) {
			PurpleBuddy *buddy = purple_find_buddy(session->account, state->who);
			char *str = g_strdup_printf(_("Unable to add \"%s\"."), state->who);
			purple_notify_error(session, _("Buddy Add error"), str,
			                    _("The username specified does not exist."));
			g_free(str);
			msn_userlist_rem_buddy(userlist, state->who);
			if (buddy != NULL)
				purple_blist_remove_buddy(buddy);

		} else {
			/* We don't know how to respond to this faultcode, so log it */
			char *fault_str = xmlnode_to_str(fault, NULL);
			if (fault_str != NULL) {
				purple_debug_error("msn", "Operation {%s} Failed, SOAP Fault was: %s\n",
				                   msn_contact_operation_str(state->action), fault_str);
				g_free(fault_str);
			}
		}
		return;
	}

	if (msn_userlist_add_buddy_to_group(userlist, state->who,
			state->new_group_name)) {
		purple_debug_info("msn", "Contact %s added to group %s successfully!\n", state->who, state->new_group_name);
	} else {
		purple_debug_info("msn", "Contact %s added to group %s successfully on server, but failed in the local list\n", state->who, state->new_group_name);
	}

	if (state->action & MSN_ADD_BUDDY) {
		MsnUser *user = msn_userlist_find_user(userlist, state->who);
		xmlnode *guid = xmlnode_get_child(resp->xml,
			"Body/ABGroupContactAddResponse/ABGroupContactAddResult/guid");

		if (guid != NULL) {
			char *uid = xmlnode_get_data(guid);
			msn_user_set_uid(user, uid);
			purple_debug_info("msn", "Set %s guid to %s.\n", state->who, uid);
			g_free(uid);
		}

		msn_userlist_add_buddy_to_list(userlist, state->who, MSN_LIST_AL);
		msn_userlist_add_buddy_to_list(userlist, state->who, MSN_LIST_FL);

		if (msn_user_is_in_list(user, MSN_LIST_PL)) {
			msn_del_contact_from_list(state->session, NULL, state->who, MSN_LIST_PL);
			return;
		}
	}

	if (state->action & MSN_MOVE_BUDDY) {
		msn_del_contact_from_group(state->session, state->who, state->old_group_name);
	}
}

void
msn_add_contact_to_group(MsnSession *session, MsnCallbackState *state,
			 const char *passport, const char *groupId)
{
	MsnUserList *userlist;
	MsnUser *user;
	gchar *body = NULL, *contact_xml, *invite;

	g_return_if_fail(passport != NULL);
	g_return_if_fail(groupId != NULL);

	g_return_if_fail(session != NULL);

	userlist = session->userlist;

	if (!strcmp(groupId, MSN_INDIVIDUALS_GROUP_ID) || !strcmp(groupId, MSN_NON_IM_GROUP_ID)) {

		user = msn_userlist_find_add_user(userlist, passport, passport);

		if (state->action & MSN_ADD_BUDDY) {
			msn_add_contact(session, state, passport);
			return;
		}

		if (state->action & MSN_MOVE_BUDDY) {
			msn_user_add_group_id(user, groupId);
			msn_del_contact_from_group(session, passport, state->old_group_name);
		}

		return;
	}

	purple_debug_info("msn", "Adding user %s to group %s\n", passport,
			  msn_userlist_find_group_name(userlist, groupId));

	user = msn_userlist_find_user(userlist, passport);
	if (user == NULL) {
		purple_debug_warning("msn", "Unable to retrieve user %s from the userlist!\n", passport);
		msn_callback_state_free(state);
		return; /* guess this never happened! */
	}

	if (user->uid != NULL) {
		contact_xml = g_strdup_printf(MSN_CONTACT_ID_XML, user->uid);
	} else if (user->networkid != MSN_NETWORK_PASSPORT) {
		contact_xml = g_strdup_printf(MSN_CONTACT_EMAIL_XML,
		                              user->networkid == MSN_NETWORK_YAHOO ?
		                                  "Messenger2" :
		                                  "Messenger3",
		                              passport, 0);
	} else {
		contact_xml = g_strdup_printf(MSN_CONTACT_XML, passport);
	}

	if (user->invite_message) {
		char *tmp;
		body = g_markup_escape_text(user->invite_message, -1);

		/* Ignore the cast, we treat it as const anyway. */
		tmp = (char *)purple_connection_get_display_name(session->account->gc);
		tmp = tmp ? g_markup_escape_text(tmp, -1) : g_strdup("");

		invite = g_strdup_printf(MSN_CONTACT_INVITE_MESSAGE_XML, body, tmp);

		g_free(body);
		g_free(tmp);

		/* We can free this now */
		g_free(user->invite_message);
		user->invite_message = NULL;

	} else {
		invite = g_strdup("");
	}

	body = g_strdup_printf(MSN_ADD_CONTACT_GROUP_TEMPLATE, groupId, contact_xml, invite);

	state->body = xmlnode_from_str(body, -1);
	state->post_action = MSN_ADD_CONTACT_GROUP_SOAP_ACTION;
	state->post_url = MSN_ADDRESS_BOOK_POST_URL;
	state->cb = msn_add_contact_to_group_read_cb;
	msn_contact_request(state);

	g_free(invite);
	g_free(contact_xml);
	g_free(body);
}

static void
msn_delete_contact_read_cb(MsnSoapMessage *req, MsnSoapMessage *resp,
	gpointer data)
{
	MsnCallbackState *state = data;
	MsnUserList *userlist = state->session->userlist;
	MsnUser *user = msn_userlist_find_user_with_id(userlist, state->uid);
	xmlnode *fault;

	/* We don't know how to respond to this faultcode, so log it */
	fault = xmlnode_get_child(resp->xml, "Body/Fault");
	if (fault != NULL) {
		char *fault_str = xmlnode_to_str(fault, NULL);
		purple_debug_error("msn", "Operation {%s} Failed, SOAP Fault was: %s\n",
		                   msn_contact_operation_str(state->action), fault_str);
		g_free(fault_str);
		return;
	}

	purple_debug_info("msn", "Delete contact successful\n");

	if (user != NULL) {
		msn_userlist_remove_user(userlist, user);
	}
}

/*delete a Contact*/
void
msn_delete_contact(MsnSession *session, MsnUser *user)
{
	gchar *body = NULL;
	gchar *contact_id_xml = NULL ;
	MsnCallbackState *state;

	if (user->uid != NULL) {
		contact_id_xml = g_strdup_printf(MSN_CONTACT_ID_XML, user->uid);
		purple_debug_info("msn", "Deleting contact with contactId: %s\n", user->uid);
	} else {
		purple_debug_info("msn", "Unable to delete contact %s without a ContactId\n", user->passport);
		return;
	}

	state = msn_callback_state_new(session);
	msn_callback_state_set_uid(state, user->uid);

	/* build SOAP request */
	body = g_strdup_printf(MSN_DEL_CONTACT_TEMPLATE, contact_id_xml);

	state->body = xmlnode_from_str(body, -1);
	state->post_action = MSN_CONTACT_DEL_SOAP_ACTION;
	state->post_url = MSN_ADDRESS_BOOK_POST_URL;
	state->cb = msn_delete_contact_read_cb;
	msn_contact_request(state);

	g_free(contact_id_xml);
	g_free(body);
}

static void
msn_del_contact_from_group_read_cb(MsnSoapMessage *req, MsnSoapMessage *resp,
	gpointer data)
{
	MsnCallbackState *state = data;
	xmlnode *fault;

	/* We don't know how to respond to this faultcode, so log it */
	fault = xmlnode_get_child(resp->xml, "Body/Fault");
	if (fault != NULL) {
		char *fault_str = xmlnode_to_str(fault, NULL);
		purple_debug_error("msn", "Operation {%s} Failed, SOAP Fault was: %s\n",
		                   msn_contact_operation_str(state->action), fault_str);
		g_free(fault_str);
		return;
	}

	if (msn_userlist_rem_buddy_from_group(state->session->userlist,
			state->who, state->old_group_name)) {
		purple_debug_info("msn", "Contact %s deleted successfully from group %s\n", state->who, state->old_group_name);
	} else {
		purple_debug_info("msn", "Contact %s deleted successfully from group %s in the server, but failed in the local list\n", state->who, state->old_group_name);
	}
}

void
msn_del_contact_from_group(MsnSession *session, const char *passport, const char *group_name)
{
	MsnUserList * userlist;
	MsnUser *user;
	MsnCallbackState *state;
	gchar *body, *contact_id_xml;
	const gchar *groupId;

	g_return_if_fail(passport != NULL);
	g_return_if_fail(group_name != NULL);
	g_return_if_fail(session != NULL);

	userlist = session->userlist;

	groupId = msn_userlist_find_group_id(userlist, group_name);
	if (groupId != NULL) {
		purple_debug_info("msn", "Deleting user %s from group %s\n", passport, group_name);
	} else {
		purple_debug_warning("msn", "Unable to retrieve group id from group %s !\n", group_name);
		return;
	}

	user = msn_userlist_find_user(userlist, passport);

	if (user == NULL) {
		purple_debug_warning("msn", "Unable to retrieve user from passport %s!\n", passport);
		return;
	}

	if ( !strcmp(groupId, MSN_INDIVIDUALS_GROUP_ID) || !strcmp(groupId, MSN_NON_IM_GROUP_ID)) {
		msn_user_remove_group_id(user, groupId);
		return;
	}

	state = msn_callback_state_new(session);
	msn_callback_state_set_who(state, passport);
	msn_callback_state_set_guid(state, groupId);
	msn_callback_state_set_old_group_name(state, group_name);

	if (user->uid != NULL)
		contact_id_xml = g_strdup_printf(MSN_CONTACT_ID_XML, user->uid);
	else
		contact_id_xml = g_strdup_printf(MSN_CONTACT_XML, passport);
	body = g_strdup_printf(MSN_CONTACT_DEL_GROUP_TEMPLATE, contact_id_xml, groupId);

	state->body = xmlnode_from_str(body, -1);
	state->post_action = MSN_CONTACT_DEL_GROUP_SOAP_ACTION;
	state->post_url = MSN_ADDRESS_BOOK_POST_URL;
	state->cb = msn_del_contact_from_group_read_cb;
	msn_contact_request(state);

	g_free(contact_id_xml);
	g_free(body);
}


static void
msn_update_contact_read_cb(MsnSoapMessage *req, MsnSoapMessage *resp,
	gpointer data)
{
	MsnCallbackState *state = (MsnCallbackState *)data;
	xmlnode *fault;

	/* We don't know how to respond to this faultcode, so log it */
	fault = xmlnode_get_child(resp->xml, "Body/Fault");
	if (fault != NULL) {
		char *fault_str = xmlnode_to_str(fault, NULL);
		purple_debug_error("msn", "Operation {%s} Failed, SOAP Fault was: %s\n",
		                   msn_contact_operation_str(state->action), fault_str);
		g_free(fault_str);
		return;
	}

	purple_debug_info("msn", "Contact updated successfully\n");
}

/* Update a contact's info */
void
msn_update_contact(MsnSession *session, const char *passport, MsnContactUpdateType type, const char* value)
{
	MsnCallbackState *state;
	xmlnode *contact;
	xmlnode *contact_info;
	xmlnode *changes;
	MsnUser *user = NULL;

	purple_debug_info("msn", "Update contact information for %s with new %s: %s\n",
		passport ? passport : "(null)",
		type == MSN_UPDATE_DISPLAY ? "display name" : "alias",
		value ? value : "(null)");
	g_return_if_fail(passport != NULL);

	if (strcmp(passport, "Me") != 0) {
		user = msn_userlist_find_user(session->userlist, passport);
		if (!user)
			return;
	}

	contact_info = xmlnode_new("contactInfo");
	changes = xmlnode_new("propertiesChanged");

	switch (type) {
		xmlnode *annotations;
		xmlnode *display;
		xmlnode *a, *n, *v;
		case MSN_UPDATE_DISPLAY:
			display = xmlnode_new_child(contact_info, "displayName");
			xmlnode_insert_data(display, value, -1);
			xmlnode_insert_data(changes, "DisplayName", -1);
			break;

		case MSN_UPDATE_ALIAS:
			annotations = xmlnode_new_child(contact_info, "annotations");
			xmlnode_insert_data(changes, "Annotation ", -1);

			a = xmlnode_new_child(annotations, "Annotation");
			n = xmlnode_new_child(a, "Name");
			xmlnode_insert_data(n, "AB.NickName", -1);
			v = xmlnode_new_child(a, "Value");
			xmlnode_insert_data(v, value, -1);
			break;

		default:
			g_return_if_reached();
	}

	state = msn_callback_state_new(session);

	state->body = xmlnode_from_str(MSN_CONTACT_UPDATE_TEMPLATE, -1);
	state->action = MSN_UPDATE_INFO;
	state->post_action = MSN_CONTACT_UPDATE_SOAP_ACTION;
	state->post_url = MSN_ADDRESS_BOOK_POST_URL;
	state->cb = msn_update_contact_read_cb;

	contact = xmlnode_get_child(state->body, "Body/ABContactUpdate/contacts/Contact");
	xmlnode_insert_child(contact, contact_info);
	xmlnode_insert_child(contact, changes);

	xmlnode_insert_data(xmlnode_get_child(state->body,
	                                      "Header/ABApplicationHeader/PartnerScenario"),
	                    MsnSoapPartnerScenarioText[MSN_PS_SAVE_CONTACT], -1);

	if (user) {
		xmlnode *contactId = xmlnode_new_child(contact, "contactId");
		msn_callback_state_set_uid(state, user->uid);
		xmlnode_insert_data(contactId, state->uid, -1);
	} else {
		xmlnode *contactType = xmlnode_new_child(contact_info, "contactType");
		xmlnode_insert_data(contactType, "Me", -1);
	}

	msn_contact_request(state);
}

static void
msn_annotate_contact_read_cb(MsnSoapMessage *req, MsnSoapMessage *resp,
	gpointer data)
{
	MsnCallbackState *state = (MsnCallbackState *)data;
	xmlnode *fault;

	/* We don't know how to respond to this faultcode, so log it */
	fault = xmlnode_get_child(resp->xml, "Body/Fault");
	if (fault != NULL) {
		char *fault_str = xmlnode_to_str(fault, NULL);
		purple_debug_error("msn", "Operation {%s} Failed, SOAP Fault was: %s\n",
		                   msn_contact_operation_str(state->action), fault_str);
		g_free(fault_str);
		return;
	}

	purple_debug_info("msn", "Contact annotated successfully\n");
}

/* Update a contact's annotations */
void
msn_annotate_contact(MsnSession *session, const char *passport, ...)
{
	va_list params;
	MsnCallbackState *state;
	xmlnode *contact;
	xmlnode *contact_info;
	xmlnode *annotations;
	MsnUser *user = NULL;

	g_return_if_fail(passport != NULL);

	if (strcmp(passport, "Me") != 0) {
		user = msn_userlist_find_user(session->userlist, passport);
		if (!user)
			return;
	}

	contact_info = xmlnode_new("contactInfo");
	annotations = xmlnode_new_child(contact_info, "annotations");

	va_start(params, passport);
	while (TRUE) {
		const char *name;
		const char *value;
		xmlnode *a, *n, *v;

		name = va_arg(params, const char *);
		if (!name)
			break;

		value = va_arg(params, const char *);
		if (!value)
			break;

		a = xmlnode_new_child(annotations, "Annotation");
		n = xmlnode_new_child(a, "Name");
		xmlnode_insert_data(n, name, -1);
		v = xmlnode_new_child(a, "Value");
		xmlnode_insert_data(v, value, -1);
	}
	va_end(params);

	state = msn_callback_state_new(session);

	state->body = xmlnode_from_str(MSN_CONTACT_ANNOTATE_TEMPLATE, -1);
	state->action = MSN_ANNOTATE_USER;
	state->post_action = MSN_CONTACT_ANNOTATE_SOAP_ACTION;
	state->post_url = MSN_ADDRESS_BOOK_POST_URL;
	state->cb = msn_annotate_contact_read_cb;

	xmlnode_insert_data(xmlnode_get_child(state->body,
	                                      "Header/ABApplicationHeader/PartnerScenario"),
	                    MsnSoapPartnerScenarioText[MSN_PS_SAVE_CONTACT], -1);

	contact = xmlnode_get_child(state->body, "Body/ABContactUpdate/contacts/Contact");
	xmlnode_insert_child(contact, contact_info);

	if (user) {
		xmlnode *contactId = xmlnode_new_child(contact, "contactId");
		msn_callback_state_set_uid(state, user->uid);
		xmlnode_insert_data(contactId, state->uid, -1);
	} else {
		xmlnode *contactType = xmlnode_new_child(contact_info, "contactType");
		xmlnode_insert_data(contactType, "Me", -1);
	}

	msn_contact_request(state);
}

static void
msn_del_contact_from_list_read_cb(MsnSoapMessage *req, MsnSoapMessage *resp,
	gpointer data)
{
	MsnCallbackState *state = data;
	MsnSession *session = state->session;
	xmlnode *fault;

	/* We don't know how to respond to this faultcode, so log it */
	fault = xmlnode_get_child(resp->xml, "Body/Fault");
	if (fault != NULL) {
		char *fault_str = xmlnode_to_str(fault, NULL);
		purple_debug_error("msn", "Operation {%s} Failed, SOAP Fault was: %s\n",
		                   msn_contact_operation_str(state->action), fault_str);
		g_free(fault_str);
		return;
	}

	purple_debug_info("msn", "Contact %s deleted successfully from %s list on server!\n", state->who, MsnMemberRole[state->list_id]);

	if (state->list_id == MSN_LIST_PL) {
		MsnUser *user = msn_userlist_find_user(session->userlist, state->who);
		MsnCallbackState *new_state = msn_callback_state_dup(state);

		if (user != NULL)
			msn_user_unset_op(user, MSN_LIST_PL_OP);

		msn_add_contact_to_list(session, new_state, state->who, MSN_LIST_RL);
		return;
	} else if (state->list_id == MSN_LIST_AL) {
		purple_privacy_permit_remove(session->account, state->who, TRUE);
		msn_add_contact_to_list(session, NULL, state->who, MSN_LIST_BL);
	} else if (state->list_id == MSN_LIST_BL) {
		purple_privacy_deny_remove(session->account, state->who, TRUE);
		msn_add_contact_to_list(session, NULL, state->who, MSN_LIST_AL);
	}

}

void
msn_del_contact_from_list(MsnSession *session, MsnCallbackState *state,
			  const gchar *passport, const MsnListId list)
{
	gchar *body = NULL, *member = NULL;
	MsnSoapPartnerScenario partner_scenario;
	MsnUser *user;

	g_return_if_fail(session != NULL);
	g_return_if_fail(session->userlist != NULL);
	g_return_if_fail(passport != NULL);
	g_return_if_fail(list < 5);

	purple_debug_info("msn", "Deleting contact %s from %s list\n", passport, MsnMemberRole[list]);

	if (state == NULL) {
		state = msn_callback_state_new(session);
	}
	msn_callback_state_set_list_id(state, list);
	msn_callback_state_set_who(state, passport);

	user = msn_userlist_find_user(session->userlist, passport);

	if (list == MSN_LIST_PL) {
		partner_scenario = MSN_PS_CONTACT_API;
		if (user->networkid != MSN_NETWORK_PASSPORT)
			member = g_strdup_printf(MSN_MEMBER_MEMBERSHIPID_XML,
			                         "EmailMember", "Email",
			                         user->member_id_on_pending_list);
		else
			member = g_strdup_printf(MSN_MEMBER_MEMBERSHIPID_XML,
			                         "PassportMember", "Passport",
			                         user->member_id_on_pending_list);
	} else {
		/* list == MSN_LIST_AL || list == MSN_LIST_BL */
		partner_scenario = MSN_PS_BLOCK_UNBLOCK;
		if (user && user->networkid != MSN_NETWORK_PASSPORT)
			member = g_strdup_printf(MSN_MEMBER_PASSPORT_XML,
			                         "EmailMember", "Email",
			                         "Email", passport, "Email");
		else
			member = g_strdup_printf(MSN_MEMBER_PASSPORT_XML,
			                         "PassportMember", "Passport",
			                         "PassportName", passport, "PassportName");
	}

	body = g_strdup_printf(MSN_CONTACT_DELETE_FROM_LIST_TEMPLATE,
		MsnSoapPartnerScenarioText[partner_scenario],
		MsnMemberRole[list], member);

	state->body = xmlnode_from_str(body, -1);
	state->post_action = MSN_DELETE_MEMBER_FROM_LIST_SOAP_ACTION;
	state->post_url = MSN_SHARE_POST_URL;
	state->cb = msn_del_contact_from_list_read_cb;
	msn_contact_request(state);

	g_free(member);
	g_free(body);
}

static void
msn_add_contact_to_list_read_cb(MsnSoapMessage *req, MsnSoapMessage *resp,
	gpointer data)
{
	MsnCallbackState *state = data;
	xmlnode *fault;

	/* We don't know how to respond to this faultcode, so log it */
	fault = xmlnode_get_child(resp->xml, "Body/Fault");
	if (fault != NULL) {
		char *fault_str = xmlnode_to_str(fault, NULL);
		purple_debug_error("msn", "Operation {%s} Failed, SOAP Fault was: %s\n",
		                   msn_contact_operation_str(state->action), fault_str);
		g_free(fault_str);
		return;
	}

	g_return_if_fail(state->session != NULL);

	purple_debug_info("msn", "Contact %s added successfully to %s list on server!\n", state->who, MsnMemberRole[state->list_id]);

	if (state->list_id == MSN_LIST_RL) {
		MsnUser *user = msn_userlist_find_user(state->session->userlist, state->who);

		if (user != NULL) {
			msn_user_set_op(user, MSN_LIST_RL_OP);
		}

		if (state->action & MSN_DENIED_BUDDY) {
			msn_add_contact_to_list(state->session, NULL, state->who, MSN_LIST_BL);
		} else if (state->list_id == MSN_LIST_AL) {
			purple_privacy_permit_add(state->session->account, state->who, TRUE);
		} else if (state->list_id == MSN_LIST_BL) {
			purple_privacy_deny_add(state->session->account, state->who, TRUE);
		}
	}
}

void
msn_add_contact_to_list(MsnSession *session, MsnCallbackState *state,
			const gchar *passport, const MsnListId list)
{
	gchar *body = NULL, *member = NULL;
	MsnSoapPartnerScenario partner_scenario;
	MsnUser *user;

	g_return_if_fail(session != NULL);
	g_return_if_fail(passport != NULL);
	g_return_if_fail(list < 5);

	purple_debug_info("msn", "Adding contact %s to %s list\n", passport, MsnMemberRole[list]);

	if (state == NULL) {
		state = msn_callback_state_new(session);
	}
	msn_callback_state_set_list_id(state, list);
	msn_callback_state_set_who(state, passport);

	user = msn_userlist_find_user(session->userlist, passport);

	partner_scenario = (list == MSN_LIST_RL) ? MSN_PS_CONTACT_API : MSN_PS_BLOCK_UNBLOCK;
	if (user && user->networkid != MSN_NETWORK_PASSPORT)
		member = g_strdup_printf(MSN_MEMBER_PASSPORT_XML,
		                         "EmailMember", "Email",
		                         "Email", state->who, "Email");
	else
		member = g_strdup_printf(MSN_MEMBER_PASSPORT_XML,
		                         "PassportMember", "Passport",
		                         "PassportName", state->who, "PassportName");

	body = g_strdup_printf(MSN_CONTACT_ADD_TO_LIST_TEMPLATE,
		MsnSoapPartnerScenarioText[partner_scenario],
		MsnMemberRole[list], member);

	state->body = xmlnode_from_str(body, -1);
	state->post_action = MSN_ADD_MEMBER_TO_LIST_SOAP_ACTION;
	state->post_url = MSN_SHARE_POST_URL;
	state->cb = msn_add_contact_to_list_read_cb;
	msn_contact_request(state);

	g_free(member);
	g_free(body);
}

#if 0
static void
msn_gleams_read_cb(MsnSoapMessage *req, MsnSoapMessage *resp, gpointer data)
{
	purple_debug_info("msn", "Gleams read done\n");
}

/*get the gleams info*/
void
msn_get_gleams(MsnSession *session)
{
	MsnSoapReq *soap_request;

	purple_debug_info("msn", "msn get gleams info...\n");

	state = msn_callback_state_new(session);
	state->body = xmlnode_from_str(MSN_GLEAMS_TEMPLATE, -1);
	state->post_action = MSN_GET_GLEAMS_SOAP_ACTION;
	state->post_url = MSN_ADDRESS_BOOK_POST_URL;
	state->cb = msn_gleams_read_cb;
	msn_contact_request(state);
}
#endif


/***************************************************************
 * Group Operations
 ***************************************************************/

static void
msn_group_read_cb(MsnSoapMessage *req, MsnSoapMessage *resp, gpointer data)
{
	MsnCallbackState *state = data;
	MsnSession *session;
	MsnUserList *userlist;
	xmlnode *fault;

	/* We don't know how to respond to this faultcode, so log it */
	fault = xmlnode_get_child(resp->xml, "Body/Fault");
	if (fault != NULL) {
		char *fault_str = xmlnode_to_str(fault, NULL);
		purple_debug_error("msn", "Operation {%s} Failed, SOAP Fault was: %s\n",
		                   msn_contact_operation_str(state->action), fault_str);
		g_free(fault_str);
		return;
	}

	purple_debug_info("msn", "Group request successful.\n");

	g_return_if_fail(state->session != NULL);
	g_return_if_fail(state->session->userlist != NULL);

	session = state->session;
	userlist = session->userlist;

	if (state->action & MSN_RENAME_GROUP) {
		msn_userlist_rename_group_id(session->userlist,
					     state->guid,
					     state->new_group_name);
	}

	if (state->action & MSN_ADD_GROUP) {
		/* the response is taken from
		   http://telepathy.freedesktop.org/wiki/Pymsn/MSNP/ContactListActions
		   should copy it to msnpiki some day */
		xmlnode *guid_node = xmlnode_get_child(resp->xml,
			"Body/ABGroupAddResponse/ABGroupAddResult/guid");

		if (guid_node) {
			char *guid = xmlnode_get_data(guid_node);

			/* create and add the new group to the userlist */
			purple_debug_info("msn", "Adding group %s with guid = %s to the userlist\n", state->new_group_name, guid);
			msn_group_new(session->userlist, guid, state->new_group_name);

			if (state->action & MSN_ADD_BUDDY) {
				msn_userlist_add_buddy(session->userlist,
					state->who,
					state->new_group_name);
			} else if (state->action & MSN_MOVE_BUDDY) {
				/* This will be freed when the add contact callback fires */
				MsnCallbackState *new_state = msn_callback_state_dup(state);
				msn_add_contact_to_group(session, new_state, state->who, guid);
				g_free(guid);
				return;
			}
			g_free(guid);
		} else {
			purple_debug_info("msn", "Adding group %s failed\n",
				state->new_group_name);
		}
	}

	if (state->action & MSN_DEL_GROUP) {
		GList *l;

		msn_userlist_remove_group_id(session->userlist, state->guid);
		for (l = userlist->users; l != NULL; l = l->next) {
			msn_user_remove_group_id( (MsnUser *)l->data, state->guid);
		}
	}
}

/* add group */
void
msn_add_group(MsnSession *session, MsnCallbackState *state, const char* group_name)
{
	char *body = NULL;
	char *escaped_group_name = NULL;

	g_return_if_fail(session != NULL);
	g_return_if_fail(group_name != NULL);

	purple_debug_info("msn", "Adding group %s to contact list.\n", group_name);

	if (state == NULL) {
		state = msn_callback_state_new(session);
	}

	msn_callback_state_set_action(state, MSN_ADD_GROUP);
	msn_callback_state_set_new_group_name(state, group_name);

	/* escape group name's html special chars so it can safely be sent
	* in a XML SOAP request
	*/
	escaped_group_name = g_markup_escape_text(group_name, -1);
	body = g_strdup_printf(MSN_GROUP_ADD_TEMPLATE, escaped_group_name);

	state->body = xmlnode_from_str(body, -1);
	state->post_action = MSN_GROUP_ADD_SOAP_ACTION;
	state->post_url = MSN_ADDRESS_BOOK_POST_URL;
	state->cb = msn_group_read_cb;
	msn_contact_request(state);

	g_free(escaped_group_name);
	g_free(body);
}

/* delete group */
void
msn_del_group(MsnSession *session, const gchar *group_name)
{
	MsnCallbackState *state;
	char *body = NULL;
	const gchar *guid;

	g_return_if_fail(session != NULL);

	g_return_if_fail(group_name != NULL);
	purple_debug_info("msn", "Deleting group %s from contact list\n", group_name);

	guid = msn_userlist_find_group_id(session->userlist, group_name);

	/* if group uid we need to del is NULL,
	*  we need to delete nothing
	*/
	if (guid == NULL) {
		purple_debug_info("msn", "Group %s guid not found, returning.\n", group_name);
		return;
	}

	if ( !strcmp(guid, MSN_INDIVIDUALS_GROUP_ID) || !strcmp(guid, MSN_NON_IM_GROUP_ID) ) {
		/* XXX add back PurpleGroup since it isn't really removed in the server? */
		return;
	}

	state = msn_callback_state_new(session);
	msn_callback_state_set_action(state, MSN_DEL_GROUP);
	msn_callback_state_set_guid(state, guid);

	body = g_strdup_printf(MSN_GROUP_DEL_TEMPLATE, guid);

	state->body = xmlnode_from_str(body, -1);
	state->post_action = MSN_GROUP_DEL_SOAP_ACTION;
	state->post_url = MSN_ADDRESS_BOOK_POST_URL;
	state->cb = msn_group_read_cb;
	msn_contact_request(state);

	g_free(body);
}

/* rename group */
void
msn_contact_rename_group(MsnSession *session, const char *old_group_name, const char *new_group_name)
{
	gchar *body = NULL;
	const gchar * guid;
	MsnCallbackState *state;
	char *escaped_group_name;

	g_return_if_fail(session != NULL);
	g_return_if_fail(session->userlist != NULL);
	g_return_if_fail(old_group_name != NULL);
	g_return_if_fail(new_group_name != NULL);

	purple_debug_info("msn", "Renaming group %s to %s.\n", old_group_name, new_group_name);

	guid = msn_userlist_find_group_id(session->userlist, old_group_name);
	if (guid == NULL)
		return;

	state = msn_callback_state_new(session);
	msn_callback_state_set_guid(state, guid);
	msn_callback_state_set_new_group_name(state, new_group_name);

	if ( !strcmp(guid, MSN_INDIVIDUALS_GROUP_ID) || !strcmp(guid, MSN_NON_IM_GROUP_ID) ) {
		MsnCallbackState *new_state = msn_callback_state_dup(state);
		msn_add_group(session, new_state, new_group_name);
		/* XXX move every buddy there (we probably need to fix concurrent SOAP reqs first) */
	}

	msn_callback_state_set_action(state, MSN_RENAME_GROUP);

	escaped_group_name = g_markup_escape_text(new_group_name, -1);
	body = g_strdup_printf(MSN_GROUP_RENAME_TEMPLATE, guid, escaped_group_name);

	state->body = xmlnode_from_str(body, -1);
	state->post_action = MSN_GROUP_RENAME_SOAP_ACTION;
	state->post_url = MSN_ADDRESS_BOOK_POST_URL;
	state->cb = msn_group_read_cb;
	msn_contact_request(state);

	g_free(escaped_group_name);
	g_free(body);
}