/*

Jabber Protocol Plugin for Miranda IM
Copyright (C) 2002-04  Santithorn Bunchua
Copyright (C) 2005-12  George Hazan
Copyright (C) 2007     Maxim Mluhov
Copyright (C) 2012-13  Miranda NG Project

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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/

#include "jabber.h"

#include <fcntl.h>
#include <io.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <m_addcontact.h>
#include <m_file.h>
#include <m_genmenu.h>
#include <m_icolib.h>

#include "jabber_list.h"
#include "jabber_iq.h"
#include "jabber_caps.h"
#include "jabber_disco.h"

#include "m_proto_listeningto.h"
#include "m_modernopt.h"

#pragma warning(disable:4355)

static int compareTransports(const TCHAR *p1, const TCHAR *p2)
{	return _tcsicmp(p1, p2);
}

static int compareListItems(const JABBER_LIST_ITEM *p1, const JABBER_LIST_ITEM *p2)
{
	if (p1->list != p2->list)
		return p1->list - p2->list;

	// for bookmarks, temporary contacts & groupchat members
	// resource must be used in the comparison
	if ((p1->list == LIST_ROSTER && (p1->bUseResource == TRUE || p2->bUseResource == TRUE))
		|| (p1->list == LIST_BOOKMARK) || (p1->list == LIST_VCARD_TEMP))
		return lstrcmpi(p1->jid, p2->jid);

	TCHAR szp1[JABBER_MAX_JID_LEN], szp2[JABBER_MAX_JID_LEN];
	JabberStripJid(p1->jid, szp1, SIZEOF(szp1));
	JabberStripJid(p2->jid, szp2, SIZEOF(szp2));
	return lstrcmpi(szp1, szp2);
}

CJabberProto::CJabberProto(const char* aProtoName, const TCHAR *aUserName) :
	m_options(this),
	m_lstTransports(50, compareTransports),
	m_lstRoster(50, compareListItems),
	m_iqManager(this),
	m_messageManager(this),
	m_presenceManager(this),
	m_sendManager(this),
	m_adhocManager(this),
	m_clientCapsManager(this),
	m_privacyListManager(this),
	m_privacyMenuServiceAllocated(-1),
	m_priorityMenuVal(0),
	m_priorityMenuValSet(false),
	m_hPrivacyMenuRoot(0),
	m_hPrivacyMenuItems(10),
	m_pLastResourceList(NULL),
	m_lstJabberFeatCapPairsDynamic(2),
	m_uEnabledFeatCapsDynamic(0)
{
	ProtoConstructor(this, aProtoName, aUserName);

	InitializeCriticalSection(&m_csModeMsgMutex);
	InitializeCriticalSection(&m_csLists);
	InitializeCriticalSection(&m_csLastResourceMap);

	m_szXmlStreamToBeInitialized = NULL;

	Log("Setting protocol/module name to '%s'", m_szModuleName);

	// Initialize Jabber API
	m_JabberApi.m_psProto = this;
	m_JabberSysApi.m_psProto = this;
	m_JabberNetApi.m_psProto = this;

	// Jabber dialog list
	m_windowList = (HANDLE)CallService(MS_UTILS_ALLOCWINDOWLIST, 0, 0);

	// Protocol services and events...
	m_hEventNudge = JCreateHookableEvent(JE_NUDGE);
	m_hEventXStatusIconChanged = JCreateHookableEvent(JE_CUSTOMSTATUS_EXTRAICON_CHANGED);
	m_hEventXStatusChanged = JCreateHookableEvent(JE_CUSTOMSTATUS_CHANGED);

	JCreateService(PS_CREATEACCMGRUI, &CJabberProto::SvcCreateAccMgrUI);

	JCreateService(PS_GETAVATARINFOT, &CJabberProto::JabberGetAvatarInfo);
	JCreateService(PS_GETMYAWAYMSG, &CJabberProto::GetMyAwayMsg);
	JCreateService(PS_SET_LISTENINGTO, &CJabberProto::OnSetListeningTo);

	JCreateService(PS_JOINCHAT, &CJabberProto::OnJoinChat);
	JCreateService(PS_LEAVECHAT, &CJabberProto::OnLeaveChat);

	JCreateService(PS_GETCUSTOMSTATUSEX, &CJabberProto::OnGetXStatusEx);
	JCreateService(PS_SETCUSTOMSTATUSEX, &CJabberProto::OnSetXStatusEx);
	JCreateService(PS_GETCUSTOMSTATUSICON, &CJabberProto::OnGetXStatusIcon);
	JCreateService(PS_GETADVANCEDSTATUSICON, &CJabberProto::JGetAdvancedStatusIcon);

	JCreateService(JS_HTTP_AUTH, &CJabberProto::OnHttpAuthRequest);
	JCreateService(JS_INCOMING_NOTE_EVENT, &CJabberProto::OnIncomingNoteEvent);

	JCreateService(JS_SENDXML, &CJabberProto::ServiceSendXML);
	JCreateService(PS_GETMYAVATART, &CJabberProto::JabberGetAvatar);
	JCreateService(PS_GETAVATARCAPS, &CJabberProto::JabberGetAvatarCaps);
	JCreateService(PS_SETMYAVATART, &CJabberProto::JabberSetAvatar);
	JCreateService(PS_SETMYNICKNAME, &CJabberProto::JabberSetNickname);

	JCreateService(JS_DB_GETEVENTTEXT_CHATSTATES, &CJabberProto::OnGetEventTextChatStates);
	JCreateService(JS_DB_GETEVENTTEXT_PRESENCE, &CJabberProto::OnGetEventTextPresence);

	JCreateService(JS_GETJABBERAPI, &CJabberProto::JabberGetApi);

	// XEP-0224 support (Attention/Nudge)
	JCreateService(JS_SEND_NUDGE, &CJabberProto::JabberSendNudge);

	// service to get from protocol chat buddy info
	JCreateService(MS_GC_PROTO_GETTOOLTIPTEXT, &CJabberProto::JabberGCGetToolTipText);

	// XMPP URI parser service for "File Association Manager" plugin
	JCreateService(JS_PARSE_XMPP_URI, &CJabberProto::JabberServiceParseXmppURI);

	JHookEvent(ME_MODERNOPT_INITIALIZE, &CJabberProto::OnModernOptInit);
	JHookEvent(ME_OPT_INITIALISE, &CJabberProto::OnOptionsInit);
	JHookEvent(ME_SKIN2_ICONSCHANGED, &CJabberProto::OnReloadIcons);

	m_iqManager.FillPermanentHandlers();
	m_iqManager.Start();
	m_messageManager.FillPermanentHandlers();
	m_messageManager.Start();
	m_presenceManager.FillPermanentHandlers();
	m_presenceManager.Start();
	m_sendManager.Start();
	m_adhocManager.FillDefaultNodes();
	m_clientCapsManager.AddDefaultCaps();

	IconsInit();
	InitPopups();
	GlobalMenuInit();
	WsInit();
	IqInit();
	SerialInit();
	ConsoleInit();
	InitCustomFolders();

	m_pepServices.insert(new CPepMood(this));
	m_pepServices.insert(new CPepActivity(this));

	*m_savedPassword = 0;

	char text[ MAX_PATH ];
	mir_snprintf(text, sizeof(text), "%s/Status", m_szModuleName);
	CallService(MS_DB_SETSETTINGRESIDENT, TRUE, (LPARAM)text);
	mir_snprintf(text, sizeof(text), "%s/%s", m_szModuleName, DBSETTING_DISPLAY_UID);
	CallService(MS_DB_SETSETTINGRESIDENT, TRUE, (LPARAM)text);

	mir_snprintf(text, sizeof(text), "%s/SubscriptionText", m_szModuleName);
	CallService(MS_DB_SETSETTINGRESIDENT, TRUE, (LPARAM)text);
	mir_snprintf(text, sizeof(text), "%s/Subscription", m_szModuleName);
	CallService(MS_DB_SETSETTINGRESIDENT, TRUE, (LPARAM)text);
	mir_snprintf(text, sizeof(text), "%s/Auth", m_szModuleName);
	CallService(MS_DB_SETSETTINGRESIDENT, TRUE, (LPARAM)text);
	mir_snprintf(text, sizeof(text), "%s/Grant", m_szModuleName);
	CallService(MS_DB_SETSETTINGRESIDENT, TRUE, (LPARAM)text);

	DBVARIANT dbv;
	if ( !JGetStringT(NULL, "XmlLang", &dbv)) {
		m_tszSelectedLang = mir_tstrdup(dbv.ptszVal);
		db_free(&dbv);
	}
	else m_tszSelectedLang = mir_tstrdup(_T("en"));

	if ( !DBGetContactSettingString(NULL, m_szModuleName, "Password", &dbv)) {
		CallService(MS_DB_CRYPT_DECODESTRING, lstrlenA(dbv.pszVal) + 1, (LPARAM)dbv.pszVal);
		TCHAR *pssw = mir_a2t(dbv.pszVal);
		JSetStringCrypt(NULL, "LoginPassword", pssw);
		mir_free(pssw);
		db_free(&dbv);
		JDeleteSetting(NULL, "Password");
	}

	CleanLastResourceMap();
}

CJabberProto::~CJabberProto()
{
	WsUninit();
	IqUninit();
	SerialUninit();
	ConsoleUninit();
	GlobalMenuUninit();

	if (m_hPopupClass)
		Popup_UnregisterClass(m_hPopupClass);

	delete m_pInfoFrame;

	DestroyHookableEvent(m_hEventNudge);
	DestroyHookableEvent(m_hEventXStatusIconChanged);
	DestroyHookableEvent(m_hEventXStatusChanged);

	CleanLastResourceMap();

	ListWipe();
	DeleteCriticalSection(&m_csLists);

	mir_free(m_tszSelectedLang);
	mir_free(m_AuthMechs.m_gssapiHostName);

	DeleteCriticalSection(&m_filterInfo.csPatternLock);
	DeleteCriticalSection(&m_csModeMsgMutex);
	DeleteCriticalSection(&m_csLastResourceMap);

	mir_free(m_modeMsgs.szOnline);
	mir_free(m_modeMsgs.szAway);
	mir_free(m_modeMsgs.szNa);
	mir_free(m_modeMsgs.szDnd);
	mir_free(m_modeMsgs.szFreechat);

	mir_free(m_transportProtoTableStartIndex);

	mir_free(m_szStreamId);

	int i;
	for (i=0; i < m_lstTransports.getCount(); i++)
		mir_free(m_lstTransports[i]);
	m_lstTransports.destroy();

	for (i=0; i < m_lstJabberFeatCapPairsDynamic.getCount(); i++) {
		mir_free(m_lstJabberFeatCapPairsDynamic[i]->szExt);
		mir_free(m_lstJabberFeatCapPairsDynamic[i]->szFeature);
		if (m_lstJabberFeatCapPairsDynamic[i]->szDescription)
			mir_free(m_lstJabberFeatCapPairsDynamic[i]->szDescription);
		delete m_lstJabberFeatCapPairsDynamic[i];
	}
	m_lstJabberFeatCapPairsDynamic.destroy();
	m_hPrivacyMenuItems.destroy();

	ProtoDestructor(this);
}

////////////////////////////////////////////////////////////////////////////////////////
// OnModulesLoadedEx - performs hook registration

static COLORREF crCols[16] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};

int CJabberProto::OnModulesLoadedEx(WPARAM, LPARAM)
{
	JHookEvent(ME_USERINFO_INITIALISE, &CJabberProto::OnUserInfoInit);
	XStatusInit();
	m_pepServices.InitGui();

	m_pInfoFrame = new CJabberInfoFrame(this);

	if (ServiceExists(MS_GC_REGISTER)) {
		jabberChatDllPresent = true;

		GCREGISTER gcr = {0};
		gcr.cbSize = sizeof(GCREGISTER);
		gcr.dwFlags = GC_TYPNOTIF | GC_CHANMGR | GC_TCHAR;
		gcr.iMaxText = 0;
		gcr.nColors = 16;
		gcr.pColors = &crCols[0];
		gcr.ptszModuleDispName = m_tszUserName;
		gcr.pszModule = m_szModuleName;
		CallServiceSync(MS_GC_REGISTER, NULL, (LPARAM)&gcr);

		JHookEvent(ME_GC_EVENT, &CJabberProto::JabberGcEventHook);
		JHookEvent(ME_GC_BUILDMENU, &CJabberProto::JabberGcMenuHook);
	}

	if (ServiceExists(MS_MSG_ADDICON)) {
		StatusIconData sid = {0};
		sid.cbSize = sizeof(sid);
		sid.szModule = m_szModuleName;
		sid.hIcon = LoadIconEx("main");
		sid.hIconDisabled = LoadIconEx("main");
		sid.flags = MBF_HIDDEN;
		sid.szTooltip = Translate("Jabber Resource");
		CallService(MS_MSG_ADDICON, 0, (LPARAM) &sid);
		JHookEvent(ME_MSG_ICONPRESSED, &CJabberProto::OnProcessSrmmIconClick);
		JHookEvent(ME_MSG_WINDOWEVENT, &CJabberProto::OnProcessSrmmEvent);

		HANDLE hContact = (HANDLE)db_find_first();
		while (hContact != NULL) {
			char *szProto = GetContactProto(hContact);
			if (szProto != NULL && !strcmp(szProto, m_szModuleName))
				MenuHideSrmmIcon(hContact);
			hContact = db_find_next(hContact);
	}	}

	DBEVENTTYPEDESCR dbEventType = {0};
	dbEventType.cbSize = sizeof(DBEVENTTYPEDESCR);
	dbEventType.eventType = JABBER_DB_EVENT_TYPE_CHATSTATES;
	dbEventType.module = m_szModuleName;
	dbEventType.descr = "Chat state notifications";
	CallService(MS_DB_EVENT_REGISTERTYPE, 0, (LPARAM)&dbEventType);

	dbEventType.eventType = JABBER_DB_EVENT_TYPE_PRESENCE;
	dbEventType.module = m_szModuleName;
	dbEventType.descr = "Presence notifications";
	CallService(MS_DB_EVENT_REGISTERTYPE, 0, (LPARAM)&dbEventType);

	JHookEvent(ME_IDLE_CHANGED, &CJabberProto::OnIdleChanged);

	CheckAllContactsAreTransported();

	// Set all contacts to offline
	HANDLE hContact = db_find_first();
	while (hContact != NULL) {
		char *szProto = GetContactProto(hContact);
		if (szProto != NULL && !strcmp(szProto, m_szModuleName)) {
			SetContactOfflineStatus(hContact);

			if (JGetByte(hContact, "IsTransport", 0)) {
				DBVARIANT dbv;
				if ( !JGetStringT(hContact, "jid", &dbv)) {
					TCHAR* domain = NEWTSTR_ALLOCA(dbv.ptszVal);
					TCHAR* resourcepos = _tcschr(domain, '/');
					if (resourcepos != NULL)
						*resourcepos = '\0';
					m_lstTransports.insert(mir_tstrdup(domain));
					db_free(&dbv);
		}	}	}

		hContact = db_find_next(hContact);
	}

	CleanLastResourceMap();
	return 0;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberAddToList - adds a contact to the contact list

HANDLE CJabberProto::AddToListByJID(const TCHAR *newJid, DWORD flags)
{
	HANDLE hContact;
	TCHAR* jid, *nick;

	Log("AddToListByJID jid = %S", newJid);

	if ((hContact=HContactFromJID(newJid)) == NULL) {
		// not already there: add
		jid = mir_tstrdup(newJid);
		Log("Add new jid to contact jid = %S", jid);
		hContact = (HANDLE)CallService(MS_DB_CONTACT_ADD, 0, 0);
		CallService(MS_PROTO_ADDTOCONTACT, (WPARAM)hContact, (LPARAM)m_szModuleName);
		JSetStringT(hContact, "jid", jid);
		if ((nick=JabberNickFromJID(newJid)) == NULL)
			nick = mir_tstrdup(newJid);
//		JSetStringT(hContact, "Nick", nick);
		mir_free(nick);
		mir_free(jid);

		// Note that by removing or disable the "NotOnList" will trigger
		// the plugin to add a particular contact to the roster list.
		// See DBSettingChanged hook at the bottom part of this source file.
		// But the add module will delete "NotOnList". So we will not do it here.
		// Also because we need "MyHandle" and "Group" info, which are set after
		// PS_ADDTOLIST is called but before the add dialog issue deletion of
		// "NotOnList".
		// If temporary add, "NotOnList" won't be deleted, and that's expected.
		db_set_b(hContact, "CList", "NotOnList", 1);
		if (flags & PALF_TEMPORARY)
			db_set_b(hContact, "CList", "Hidden", 1);
	}
	else {
		// already exist
		// Set up a dummy "NotOnList" when adding permanently only
		if ( !(flags & PALF_TEMPORARY))
			db_set_b(hContact, "CList", "NotOnList", 1);
	}

	if (hContact && newJid)
		DBCheckIsTransportedContact(newJid, hContact);
	return hContact;
}

HANDLE CJabberProto::AddToList(int flags, PROTOSEARCHRESULT* psr)
{
	if (psr->cbSize != sizeof(JABBER_SEARCH_RESULT) && psr->id == NULL)
		return NULL;

	JABBER_SEARCH_RESULT *jsr = (JABBER_SEARCH_RESULT*)psr;
	TCHAR *jid = psr->id ? psr->id : jsr->jid;
	return AddToListByJID(jid, flags);
}

HANDLE __cdecl CJabberProto::AddToListByEvent(int flags, int /*iContact*/, HANDLE hDbEvent)
{
	DBEVENTINFO dbei;
	HANDLE hContact;
	char* nick, *firstName, *lastName, *jid;

	Log("AddToListByEvent");
	ZeroMemory(&dbei, sizeof(dbei));
	dbei.cbSize = sizeof(dbei);
	if ((dbei.cbBlob = CallService(MS_DB_EVENT_GETBLOBSIZE, (WPARAM)hDbEvent, 0)) == (DWORD)(-1))
		return NULL;
	if ((dbei.pBlob=(PBYTE)alloca(dbei.cbBlob)) == NULL)
		return NULL;
	if (CallService(MS_DB_EVENT_GET, (WPARAM)hDbEvent, (LPARAM)&dbei))
		return NULL;
	if (strcmp(dbei.szModule, m_szModuleName))
		return NULL;

/*
	// EVENTTYPE_CONTACTS is when adding from when we receive contact list (not used in Jabber)
	// EVENTTYPE_ADDED is when adding from when we receive "You are added" (also not used in Jabber)
	// Jabber will only handle the case of EVENTTYPE_AUTHREQUEST
	// EVENTTYPE_AUTHREQUEST is when adding from the authorization request dialog
*/

	if (dbei.eventType != EVENTTYPE_AUTHREQUEST)
		return NULL;

	nick = (char*)(dbei.pBlob + sizeof(DWORD)*2);
	firstName = nick + strlen(nick) + 1;
	lastName = firstName + strlen(firstName) + 1;
	jid = lastName + strlen(lastName) + 1;

	TCHAR *newJid = (dbei.flags & DBEF_UTF) ? mir_utf8decodeT(jid) : mir_a2t(jid);
	hContact = (HANDLE)AddToListByJID(newJid, flags);
	mir_free(newJid);
	return hContact;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberAuthAllow - processes the successful authorization

int CJabberProto::Authorize(HANDLE hDbEvent)
{
	if ( !m_bJabberOnline)
		return 1;

	DBEVENTINFO dbei = { sizeof(dbei) };
	if ((dbei.cbBlob = CallService(MS_DB_EVENT_GETBLOBSIZE, (WPARAM)hDbEvent, 0)) == (DWORD)(-1))
		return 1;
	if ((dbei.pBlob = (PBYTE)alloca(dbei.cbBlob)) == NULL)
		return 1;
	if (CallService(MS_DB_EVENT_GET, (WPARAM)hDbEvent, (LPARAM)&dbei))
		return 1;
	if (dbei.eventType != EVENTTYPE_AUTHREQUEST)
		return 1;
	if (strcmp(dbei.szModule, m_szModuleName))
		return 1;

	char *nick = (char*)(dbei.pBlob + sizeof(DWORD)*2);
	char *firstName = nick + strlen(nick) + 1;
	char *lastName = firstName + strlen(firstName) + 1;
	char *jid = lastName + strlen(lastName) + 1;

	Log("Send 'authorization allowed' to %S", jid);

	TCHAR *newJid = (dbei.flags & DBEF_UTF) ? mir_utf8decodeT(jid) : mir_a2t(jid);

	m_ThreadInfo->send(XmlNode(_T("presence")) << XATTR(_T("to"), newJid) << XATTR(_T("type"), _T("subscribed")));

	// Automatically add this user to my roster if option is enabled
	if (m_options.AutoAdd == TRUE) {
		HANDLE hContact;
		JABBER_LIST_ITEM *item;

		if ((item = ListGetItemPtr(LIST_ROSTER, newJid)) == NULL || (item->subscription != SUB_BOTH && item->subscription != SUB_TO)) {
			Log("Try adding contact automatically jid = %S", jid);
			if ((hContact = AddToListByJID(newJid, 0)) != NULL) {
				// Trigger actual add by removing the "NotOnList" added by AddToListByJID()
				// See AddToListByJID() and JabberDbSettingChanged().
				db_unset(hContact, "CList", "NotOnList");
	}	}	}

	mir_free(newJid);
	return 0;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberAuthDeny - handles the unsuccessful authorization

int CJabberProto::AuthDeny(HANDLE hDbEvent, const TCHAR*)
{
	if ( !m_bJabberOnline)
		return 1;

	Log("Entering AuthDeny");

	DBEVENTINFO dbei = { sizeof(dbei) };
	if ((dbei.cbBlob=CallService(MS_DB_EVENT_GETBLOBSIZE, (WPARAM)hDbEvent, 0)) == (DWORD)(-1))
		return 1;
	if ((dbei.pBlob=(PBYTE) mir_alloc(dbei.cbBlob)) == NULL)
		return 1;
	if (CallService(MS_DB_EVENT_GET, (WPARAM)hDbEvent, (LPARAM)&dbei)) {
		mir_free(dbei.pBlob);
		return 1;
	}
	if (dbei.eventType != EVENTTYPE_AUTHREQUEST) {
		mir_free(dbei.pBlob);
		return 1;
	}
	if (strcmp(dbei.szModule, m_szModuleName)) {
		mir_free(dbei.pBlob);
		return 1;
	}

	char *nick = (char*)(dbei.pBlob + sizeof(DWORD)*2);
	char *firstName = nick + strlen(nick) + 1;
	char *lastName = firstName + strlen(firstName) + 1;
	char *jid = lastName + strlen(lastName) + 1;

	Log("Send 'authorization denied' to %s", jid);

	TCHAR *newJid = dbei.flags & DBEF_UTF ? mir_utf8decodeT(jid) : mir_a2t(jid);

	m_ThreadInfo->send(XmlNode(_T("presence")) << XATTR(_T("to"), newJid) << XATTR(_T("type"), _T("unsubscribed")));

	mir_free(newJid);
	mir_free(dbei.pBlob);
	return 0;
}

////////////////////////////////////////////////////////////////////////////////////////
// PSR_AUTH

int __cdecl CJabberProto::AuthRecv(HANDLE, PROTORECVEVENT*)
{
	return 1;
}

////////////////////////////////////////////////////////////////////////////////////////
// PSS_AUTHREQUEST

int __cdecl CJabberProto::AuthRequest(HANDLE, const TCHAR*)
{
	return 1;
}

////////////////////////////////////////////////////////////////////////////////////////
// ChangeInfo

HANDLE __cdecl CJabberProto::ChangeInfo(int /*iInfoType*/, void*)
{
	return NULL;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberFileAllow - starts a file transfer

HANDLE __cdecl CJabberProto::FileAllow(HANDLE /*hContact*/, HANDLE hTransfer, const TCHAR *szPath)
{
	if ( !m_bJabberOnline)
		return 0;

	filetransfer *ft = (filetransfer*)hTransfer;
	ft->std.tszWorkingDir = mir_tstrdup(szPath);
	size_t len = _tcslen(ft->std.tszWorkingDir)-1;
	if (ft->std.tszWorkingDir[len] == '/' || ft->std.tszWorkingDir[len] == '\\')
		ft->std.tszWorkingDir[len] = 0;

	switch (ft->type) {
	case FT_OOB:
		JForkThread((JThreadFunc)&CJabberProto::FileReceiveThread, ft);
		break;
	case FT_BYTESTREAM:
		FtAcceptSiRequest(ft);
		break;
	case FT_IBB:
		FtAcceptIbbRequest(ft);
		break;
	}
	return hTransfer;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberFileCancel - cancels a file transfer

int __cdecl CJabberProto::FileCancel(HANDLE /*hContact*/, HANDLE hTransfer)
{
	filetransfer *ft = (filetransfer*)hTransfer;
	HANDLE hEvent;

	Log("Invoking FileCancel()");
	if (ft->type == FT_OOB) {
		if (ft->s) {
			Log("FT canceled");
			Log("Closing ft->s = %d", ft->s);
			ft->state = FT_ERROR;
			Netlib_CloseHandle(ft->s);
			ft->s = NULL;
			if (ft->hFileEvent != NULL) {
				hEvent = ft->hFileEvent;
				ft->hFileEvent = NULL;
				SetEvent(hEvent);
			}
			Log("ft->s is now NULL, ft->state is now FT_ERROR");
		}
	}
	else FtCancel(ft);
	return 0;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberFileDeny - denies a file transfer

int __cdecl CJabberProto::FileDeny(HANDLE, HANDLE hTransfer, const TCHAR *)
{
	if ( !m_bJabberOnline)
		return 1;

	filetransfer *ft = (filetransfer*)hTransfer;

	switch (ft->type) {
	case FT_OOB:
		m_ThreadInfo->send( XmlNodeIq(_T("error"), ft->iqId, ft->jid) << XCHILD(_T("error"), _T("File transfer refused")) << XATTRI(_T("code"), 406));
		break;

	case FT_BYTESTREAM:
	case FT_IBB:
		m_ThreadInfo->send(
			XmlNodeIq(_T("error"), ft->iqId, ft->jid)
				<< XCHILD(_T("error"), _T("File transfer refused")) << XATTRI(_T("code"), 403) << XATTR(_T("type"), _T("cancel"))
					<< XCHILDNS(_T("forbidden"), _T("urn:ietf:params:xml:ns:xmpp-stanzas"))
						<< XCHILD(_T("text"), _T("File transfer refused")) << XATTR(_T("xmlns"), _T("urn:ietf:params:xml:ns:xmpp-stanzas")));
		break;
	}
	delete ft;
	return 0;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberFileResume - processes file renaming etc

int __cdecl CJabberProto::FileResume(HANDLE hTransfer, int *action, const TCHAR **szFilename)
{
	filetransfer *ft = (filetransfer*)hTransfer;
	if ( !m_bJabberOnline || ft == NULL)
		return 1;

	if (*action == FILERESUME_RENAME)
		replaceStrT(ft->std.tszCurrentFile, *szFilename);

	SetEvent(ft->hWaitEvent);
	return 0;
}

////////////////////////////////////////////////////////////////////////////////////////
// GetCaps - return protocol capabilities bits

DWORD_PTR __cdecl CJabberProto::GetCaps(int type, HANDLE hContact)
{
	switch(type) {
	case PFLAGNUM_1:
		return PF1_IM | PF1_AUTHREQ | PF1_CHAT | PF1_SERVERCLIST | PF1_MODEMSG | PF1_BASICSEARCH | PF1_EXTSEARCH | PF1_FILE | PF1_CONTACT;
	case PFLAGNUM_2:
		return PF2_ONLINE | PF2_INVISIBLE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_HEAVYDND | PF2_FREECHAT;
	case PFLAGNUM_3:
		return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_HEAVYDND | PF2_FREECHAT;
	case PFLAGNUM_4:
		return PF4_FORCEAUTH | PF4_NOCUSTOMAUTH | PF4_NOAUTHDENYREASON | PF4_SUPPORTTYPING | PF4_AVATARS | PF4_IMSENDUTF | PF4_FORCEADDED;
	case PFLAG_UNIQUEIDTEXT:
		return (DWORD_PTR)Translate("JID");
	case PFLAG_UNIQUEIDSETTING:
		return (DWORD_PTR)"jid";
	case PFLAG_MAXCONTACTSPERPACKET:
		{
			DBVARIANT dbv;
			if (JGetStringT(hContact, "jid", &dbv))
				return 0;
			TCHAR szClientJid[ JABBER_MAX_JID_LEN ];
			GetClientJID(dbv.ptszVal, szClientJid, SIZEOF(szClientJid));
			db_free(&dbv);
			JabberCapsBits jcb = GetResourceCapabilites(szClientJid, TRUE);
			return ((~jcb & JABBER_CAPS_ROSTER_EXCHANGE) ? 0 : 50);
		}
	}
	return 0;
}

////////////////////////////////////////////////////////////////////////////////////////
// GetInfo - retrieves a contact info

int __cdecl CJabberProto::GetInfo(HANDLE hContact, int /*infoType*/)
{
	if ( !m_bJabberOnline)
		return 1;

	int result = 1;
	DBVARIANT dbv;
	if (JGetByte(hContact, "ChatRoom", 0))
		return 1;

	if ( !JGetStringT(hContact, "jid", &dbv)) {
		if (m_ThreadInfo) {
			TCHAR jid[ JABBER_MAX_JID_LEN ];
			GetClientJID(dbv.ptszVal, jid, SIZEOF(jid));

			m_ThreadInfo->send(
				XmlNodeIq(m_iqManager.AddHandler(&CJabberProto::OnIqResultEntityTime, JABBER_IQ_TYPE_GET, jid, JABBER_IQ_PARSE_HCONTACT))
					<< XCHILDNS(_T("time"), _T(JABBER_FEAT_ENTITY_TIME)));

			// XEP-0012, last logoff time
			XmlNodeIq iq2(m_iqManager.AddHandler(&CJabberProto::OnIqResultLastActivity, JABBER_IQ_TYPE_GET, dbv.ptszVal, JABBER_IQ_PARSE_FROM));
			iq2 << XQUERY(_T(JABBER_FEAT_LAST_ACTIVITY));
			m_ThreadInfo->send(iq2);

			JABBER_LIST_ITEM *item = NULL;

			if ((item = ListGetItemPtr(LIST_VCARD_TEMP, dbv.ptszVal)) == NULL)
				item = ListGetItemPtr(LIST_ROSTER, dbv.ptszVal);

			if ( !item) {
				TCHAR szBareJid[ JABBER_MAX_JID_LEN ];
				_tcsncpy(szBareJid, dbv.ptszVal, SIZEOF(szBareJid));
				TCHAR* pDelimiter = _tcschr(szBareJid, _T('/'));
				if (pDelimiter) {
					*pDelimiter = 0;
					pDelimiter++;
					if ( !*pDelimiter)
						pDelimiter = NULL;
				}
				JABBER_LIST_ITEM *tmpItem = NULL;
				if (pDelimiter && (tmpItem  = ListGetItemPtr(LIST_CHATROOM, szBareJid))) {
					JABBER_RESOURCE_STATUS *him = NULL;
					for (int i=0; i < tmpItem->resourceCount; i++) {
						JABBER_RESOURCE_STATUS &p = tmpItem->resource[i];
						if ( !lstrcmp(p.resourceName, pDelimiter))
							him = &p;
					}
					if (him) {
						item = ListAdd(LIST_VCARD_TEMP, dbv.ptszVal);
						ListAddResource(LIST_VCARD_TEMP, dbv.ptszVal, him->status, him->statusMessage, him->priority);
					}
				}
				else
					item = ListAdd(LIST_VCARD_TEMP, dbv.ptszVal);
			}

			if (item) {
				if (item->resource) {
					for (int i = 0; i < item->resourceCount; i++) {
						TCHAR szp1[ JABBER_MAX_JID_LEN ];
						JabberStripJid(dbv.ptszVal, szp1, SIZEOF(szp1));
						mir_sntprintf(jid, 256, _T("%s/%s"), szp1, item->resource[i].resourceName);

						XmlNodeIq iq3(m_iqManager.AddHandler(&CJabberProto::OnIqResultLastActivity, JABBER_IQ_TYPE_GET, jid, JABBER_IQ_PARSE_FROM));
						iq3 << XQUERY(_T(JABBER_FEAT_LAST_ACTIVITY));
						m_ThreadInfo->send(iq3);

						if ( !item->resource[i].dwVersionRequestTime) {
							XmlNodeIq iq4(m_iqManager.AddHandler(&CJabberProto::OnIqResultVersion, JABBER_IQ_TYPE_GET, jid, JABBER_IQ_PARSE_FROM | JABBER_IQ_PARSE_HCONTACT | JABBER_IQ_PARSE_CHILD_TAG_NODE));
							iq4 << XQUERY(_T(JABBER_FEAT_VERSION));
							m_ThreadInfo->send(iq4);
						}

						if ( !item->resource[i].pSoftwareInfo) {
							XmlNodeIq iq5(m_iqManager.AddHandler(&CJabberProto::OnIqResultCapsDiscoInfoSI, JABBER_IQ_TYPE_GET, jid, JABBER_IQ_PARSE_FROM | JABBER_IQ_PARSE_CHILD_TAG_NODE | JABBER_IQ_PARSE_HCONTACT));
							iq5 << XQUERY(_T(JABBER_FEAT_DISCO_INFO));
							m_ThreadInfo->send(iq5);
						}
					}
				}
				else if ( !item->itemResource.dwVersionRequestTime) {
					XmlNodeIq iq4(m_iqManager.AddHandler(&CJabberProto::OnIqResultVersion, JABBER_IQ_TYPE_GET, item->jid, JABBER_IQ_PARSE_FROM | JABBER_IQ_PARSE_HCONTACT | JABBER_IQ_PARSE_CHILD_TAG_NODE));
					iq4 << XQUERY(_T(JABBER_FEAT_VERSION));
					m_ThreadInfo->send(iq4);
		}	}	}

		SendGetVcard(dbv.ptszVal);
		db_free(&dbv);
		result = 0;
	}

	return result;
}

////////////////////////////////////////////////////////////////////////////////////////
// SearchBasic - searches the contact by JID

struct JABBER_SEARCH_BASIC
{
	int hSearch;
	TCHAR jid[128];
};

void __cdecl CJabberProto::BasicSearchThread(JABBER_SEARCH_BASIC *jsb)
{
	Sleep(100);

	JABBER_SEARCH_RESULT jsr = { 0 };
	jsr.hdr.cbSize = sizeof(JABBER_SEARCH_RESULT);
	jsr.hdr.flags = PSR_TCHAR;
	jsr.hdr.nick = jsb->jid;
	jsr.hdr.firstName = _T("");
	jsr.hdr.lastName = _T("");
	jsr.hdr.id = jsb->jid;

	_tcsncpy(jsr.jid, jsb->jid, SIZEOF(jsr.jid));

	jsr.jid[SIZEOF(jsr.jid)-1] = '\0';
	JSendBroadcast(NULL, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)jsb->hSearch, (LPARAM)&jsr);
	JSendBroadcast(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)jsb->hSearch, 0);
	mir_free(jsb);
}

HANDLE __cdecl CJabberProto::SearchBasic(const TCHAR *szJid)
{
	Log("JabberBasicSearch called with lParam = '%s'", szJid);

	JABBER_SEARCH_BASIC *jsb;
	if ( !m_bJabberOnline || (jsb=(JABBER_SEARCH_BASIC*)mir_alloc(sizeof(JABBER_SEARCH_BASIC))) == NULL)
		return 0;

	if (_tcschr(szJid, '@') == NULL) {
		TCHAR *szServer = mir_a2t(m_ThreadInfo->server);
		const TCHAR *p = _tcsstr(szJid, szServer);
		if (p == NULL) {
			bool numericjid = true;
			for (const TCHAR *i = szJid; *i && numericjid; i++)
				numericjid = (*i >= '0') && (*i <= '9');

			mir_free(szServer);
			szServer = JGetStringT(NULL, "LoginServer");
			if (szServer == NULL)
				szServer = mir_tstrdup(_T("jabber.org"));
			else if (numericjid && !_tcsicmp(szServer, _T("S.ms"))) {
				mir_free(szServer);
				szServer = mir_tstrdup(_T("sms"));
			}
			mir_sntprintf(jsb->jid, SIZEOF(jsb->jid), _T("%s@%s"), szJid, szServer);
		}
		else _tcsncpy(jsb->jid, szJid, SIZEOF(jsb->jid));
		mir_free(szServer);
	}
	else _tcsncpy(jsb->jid, szJid, SIZEOF(jsb->jid));

	Log("Adding '%s' without validation", jsb->jid);
	jsb->hSearch = SerialNext();
	JForkThread((JThreadFunc)&CJabberProto::BasicSearchThread, jsb);
	return (HANDLE)jsb->hSearch;
}

////////////////////////////////////////////////////////////////////////////////////////
// SearchByEmail - searches the contact by its e-mail

HANDLE __cdecl CJabberProto::SearchByEmail(const TCHAR *email)
{
	if ( !m_bJabberOnline || email == NULL)
		return 0;

	char szServerName[100];
	if (JGetStaticString("Jud", NULL, szServerName, sizeof(szServerName)))
		strcpy(szServerName, "users.jabber.org");

	int iqId = SerialNext();
	IqAdd(iqId, IQ_PROC_GETSEARCH, &CJabberProto::OnIqResultSetSearch);
	m_ThreadInfo->send( XmlNodeIq(_T("set"), iqId, _A2T(szServerName)) << XQUERY(_T("jabber:iq:search"))
		<< XCHILD(_T("email"), email));
	return (HANDLE)iqId;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberSearchByName - searches the contact by its first or last name, or by a nickname

HANDLE __cdecl CJabberProto::SearchByName(const TCHAR *nick, const TCHAR *firstName, const TCHAR *lastName)
{
	if ( !m_bJabberOnline)
		return NULL;

	BOOL bIsExtFormat = m_options.ExtendedSearch;

	char szServerName[100];
	if (JGetStaticString("Jud", NULL, szServerName, sizeof(szServerName)))
		strcpy(szServerName, "users.jabber.org");

	int iqId = SerialNext();
	XmlNodeIq iq(_T("set"), iqId, _A2T(szServerName));
	HXML query = iq << XQUERY(_T("jabber:iq:search"));

	if (bIsExtFormat) {
		IqAdd(iqId, IQ_PROC_GETSEARCH, &CJabberProto::OnIqResultExtSearch);

		if (m_tszSelectedLang)
			iq << XATTR(_T("xml:lang"), m_tszSelectedLang);

		HXML x = query << XCHILDNS(_T("x"), _T(JABBER_FEAT_DATA_FORMS)) << XATTR(_T("type"), _T("submit"));
		if (nick[0] != '\0')
			x << XCHILD(_T("field")) << XATTR(_T("var"), _T("user")) << XATTR(_T("value"), nick);

		if (firstName[0] != '\0')
			x << XCHILD(_T("field")) << XATTR(_T("var"), _T("fn")) << XATTR(_T("value"), firstName);

		if (lastName[0] != '\0')
			x << XCHILD(_T("field")) << XATTR(_T("var"), _T("given")) << XATTR(_T("value"), lastName);
	}
	else {
		IqAdd(iqId, IQ_PROC_GETSEARCH, &CJabberProto::OnIqResultSetSearch);
		if (nick[0] != '\0')
			query << XCHILD(_T("nick"), nick);

		if (firstName[0] != '\0')
			query << XCHILD(_T("first"), firstName);

		if (lastName[0] != '\0')
			query << XCHILD(_T("last"), lastName);
	}

	m_ThreadInfo->send(iq);
	return (HANDLE)iqId;
}

////////////////////////////////////////////////////////////////////////////////////////
// RecvContacts

int __cdecl CJabberProto::RecvContacts(HANDLE /*hContact*/, PROTORECVEVENT*)
{
	return 1;
}

////////////////////////////////////////////////////////////////////////////////////////
// RecvFile

int __cdecl CJabberProto::RecvFile(HANDLE hContact, PROTORECVFILET* evt)
{
	return Proto_RecvFile(hContact, evt);
}

////////////////////////////////////////////////////////////////////////////////////////
// RecvMsg

int __cdecl CJabberProto::RecvMsg(HANDLE hContact, PROTORECVEVENT* evt)
{
	INT_PTR nDbEvent = Proto_RecvMessage(hContact, evt);

	EnterCriticalSection(&m_csLastResourceMap);
	if (IsLastResourceExists((void*)evt->lParam)) {
		m_ulpResourceToDbEventMap[ m_dwResourceMapPointer++ ] = nDbEvent;
		m_ulpResourceToDbEventMap[ m_dwResourceMapPointer++ ] = evt->lParam;
		if (m_dwResourceMapPointer >= SIZEOF(m_ulpResourceToDbEventMap))
			m_dwResourceMapPointer = 0;
	}
	LeaveCriticalSection(&m_csLastResourceMap);

	return 0;
}

////////////////////////////////////////////////////////////////////////////////////////
// RecvUrl

int __cdecl CJabberProto::RecvUrl(HANDLE /*hContact*/, PROTORECVEVENT*)
{
	return 1;
}

////////////////////////////////////////////////////////////////////////////////////////
// SendContacts

int __cdecl CJabberProto::SendContacts(HANDLE hContact, int flags, int nContacts, HANDLE* hContactsList)
{
	DBVARIANT dbv;
	if ( !m_bJabberOnline || JGetStringT(hContact, "jid", &dbv))
		return 0;

	TCHAR szClientJid[ JABBER_MAX_JID_LEN ];
	GetClientJID(dbv.ptszVal, szClientJid, SIZEOF(szClientJid));
	db_free(&dbv);

	JabberCapsBits jcb = GetResourceCapabilites(szClientJid, TRUE);
	if (~jcb & JABBER_CAPS_ROSTER_EXCHANGE)
		return 0;

	XmlNode m(_T("message"));
//	m << XCHILD(_T("body"), msg);
	HXML x = m << XCHILDNS(_T("x"), _T(JABBER_FEAT_ROSTER_EXCHANGE));

	for (int i = 0; i < nContacts; i++) {
		if ( !JGetStringT(hContactsList[i], "jid", &dbv)) {
			x << XCHILD(_T("item")) << XATTR(_T("action"), _T("add")) <<
				XATTR(_T("jid"), dbv.ptszVal);
			db_free(&dbv);
		}
	}

	int id = SerialNext();
	m << XATTR(_T("to"), szClientJid) << XATTRID(id);

	m_ThreadInfo->send(m);
	return 1;
}

////////////////////////////////////////////////////////////////////////////////////////
// SendFile - sends a file

HANDLE __cdecl CJabberProto::SendFile(HANDLE hContact, const TCHAR *szDescription, TCHAR** ppszFiles)
{
	if ( !m_bJabberOnline) return 0;

	if (JGetWord(hContact, "Status", ID_STATUS_OFFLINE) == ID_STATUS_OFFLINE)
		return 0;

	DBVARIANT dbv;
	if (JGetStringT(hContact, "jid", &dbv))
		return 0;

	int i, j;
	struct _stati64 statbuf;
	JABBER_LIST_ITEM* item = ListGetItemPtr(LIST_ROSTER, dbv.ptszVal);
	if (item == NULL) {
		db_free(&dbv);
		return 0;
	}

	// Check if another file transfer session request is pending (waiting for disco result)
	if (item->ft != NULL) {
		db_free(&dbv);
		return 0;
	}

	JabberCapsBits jcb = GetResourceCapabilites(item->jid, TRUE);
	if (jcb == JABBER_RESOURCE_CAPS_IN_PROGRESS) {
		Sleep(600);
		jcb = GetResourceCapabilites(item->jid, TRUE);
	}

	// fix for very smart clients, like gajim
	if ( !m_options.BsDirect && !m_options.BsProxyManual) {
		// disable bytestreams
		jcb &= ~JABBER_CAPS_BYTESTREAMS;
	}

	// if only JABBER_CAPS_SI_FT feature set (without BS or IBB), disable JABBER_CAPS_SI_FT
	if ((jcb & (JABBER_CAPS_SI_FT | JABBER_CAPS_IBB | JABBER_CAPS_BYTESTREAMS)) == JABBER_CAPS_SI_FT)
		jcb &= ~JABBER_CAPS_SI_FT;

	if (
		// can't get caps
		(jcb & JABBER_RESOURCE_CAPS_ERROR)
		// caps not already received
		|| (jcb == JABBER_RESOURCE_CAPS_NONE)
		// XEP-0096 and OOB not supported?
		|| !(jcb & (JABBER_CAPS_SI_FT | JABBER_CAPS_OOB)))
	{
		db_free(&dbv);
		MsgPopup(hContact, TranslateT("No compatible file transfer machanism exist"), item->jid);
		return 0;
	}

	filetransfer *ft = new filetransfer(this);
	ft->std.hContact = hContact;
	while(ppszFiles[ ft->std.totalFiles ] != NULL)
		ft->std.totalFiles++;

	ft->std.ptszFiles = (TCHAR**) mir_calloc(sizeof(TCHAR*)* ft->std.totalFiles);
	ft->fileSize = (unsigned __int64*) mir_calloc(sizeof(unsigned __int64) * ft->std.totalFiles);
	for (i=j=0; i < ft->std.totalFiles; i++) {
		if (_tstati64(ppszFiles[i], &statbuf))
			Log("'%s' is an invalid filename", ppszFiles[i]);
		else {
			ft->std.ptszFiles[j] = mir_tstrdup(ppszFiles[i]);
			ft->fileSize[j] = statbuf.st_size;
			j++;
			ft->std.totalBytes += statbuf.st_size;
	}	}
	if (j == 0) {
		delete ft;
		db_free(&dbv);
		return NULL;
	}

	ft->std.tszCurrentFile = mir_tstrdup(ppszFiles[0]);
	ft->szDescription = mir_tstrdup(szDescription);
	ft->jid = mir_tstrdup(dbv.ptszVal);
	db_free(&dbv);

	if (jcb & JABBER_CAPS_SI_FT)
		FtInitiate(item->jid, ft);
	else if (jcb & JABBER_CAPS_OOB)
		JForkThread((JThreadFunc)&CJabberProto::FileServerThread, ft);

	return ft;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberSendMessage - sends a message

struct TFakeAckParams
{
	inline TFakeAckParams(HANDLE p1, const char* p2)
		: hContact(p1), msg(p2) {}

	HANDLE hContact;
	const char *msg;
};

void __cdecl CJabberProto::SendMessageAckThread(void* param)
{
	TFakeAckParams *par = (TFakeAckParams*)param;
	Sleep(100);
	Log("Broadcast ACK");
	JSendBroadcast(par->hContact, ACKTYPE_MESSAGE,
		par->msg ? ACKRESULT_FAILED : ACKRESULT_SUCCESS,
		(HANDLE)1, (LPARAM) par->msg);
	Log("Returning from thread");
	delete par;
}

static char PGP_PROLOG[] = "-----BEGIN PGP MESSAGE-----\r\n\r\n";
static char PGP_EPILOG[] = "\r\n-----END PGP MESSAGE-----\r\n";

int __cdecl CJabberProto::SendMsg(HANDLE hContact, int flags, const char* pszSrc)
{
	int id;

	DBVARIANT dbv;
	if ( !m_bJabberOnline || JGetStringT(hContact, "jid", &dbv)) {
		TFakeAckParams *param = new TFakeAckParams(hContact, Translate("Protocol is offline or no jid"));
		JForkThread(&CJabberProto::SendMessageAckThread, param);
		return 1;
	}

	TCHAR *msg;
	int  isEncrypted;

	if ( !strncmp(pszSrc, PGP_PROLOG, strlen(PGP_PROLOG))) {
		const char* szEnd = strstr(pszSrc, PGP_EPILOG);
		char* tempstring = (char*)alloca(strlen(pszSrc) + 1);
		size_t nStrippedLength = strlen(pszSrc) - strlen(PGP_PROLOG) - (szEnd ? strlen(szEnd) : 0);
		strncpy(tempstring, pszSrc + strlen(PGP_PROLOG), nStrippedLength);
		tempstring[ nStrippedLength ] = 0;
		pszSrc = tempstring;
		isEncrypted = 1;
		flags &= ~PREF_UNICODE;
	}
	else isEncrypted = 0;

	if (flags & PREF_UTF)
		mir_utf8decode(NEWSTR_ALLOCA(pszSrc), &msg);
	else if (flags & PREF_UNICODE)
		msg = mir_u2t((wchar_t*)&pszSrc[ strlen(pszSrc)+1 ]);
	else
		msg = mir_a2t(pszSrc);

	int nSentMsgId = 0;

	if (msg != NULL) {
		TCHAR *msgType;
		if (ListExist(LIST_CHATROOM, dbv.ptszVal) && _tcschr(dbv.ptszVal, '/') == NULL)
			msgType = _T("groupchat");
		else
			msgType = _T("chat");

		XmlNode m(_T("message")); xmlAddAttr(m, _T("type"), msgType);
		if ( !isEncrypted)
			m << XCHILD(_T("body"), msg);
		else {
			m << XCHILD(_T("body"), _T("[This message is encrypted.]"));
			m << XCHILD(_T("x"), msg) << XATTR(_T("xmlns"), _T("jabber:x:encrypted"));
		}
		mir_free(msg);

		TCHAR szClientJid[ JABBER_MAX_JID_LEN ];
		GetClientJID(dbv.ptszVal, szClientJid, SIZEOF(szClientJid));

		JABBER_RESOURCE_STATUS *r = ResourceInfoFromJID(szClientJid);
		if (r)
			r->bMessageSessionActive = TRUE;

		JabberCapsBits jcb = GetResourceCapabilites(szClientJid, TRUE);

		if (jcb & JABBER_RESOURCE_CAPS_ERROR)
			jcb = JABBER_RESOURCE_CAPS_NONE;

		if (jcb & JABBER_CAPS_CHATSTATES)
			m << XCHILDNS(_T("active"), _T(JABBER_FEAT_CHATSTATES));

		if (
			// if message delivery check disabled by entity caps manager
			(jcb & JABBER_CAPS_MESSAGE_EVENTS_NO_DELIVERY) ||
			// if client knows nothing about delivery
			!(jcb & (JABBER_CAPS_MESSAGE_EVENTS | JABBER_CAPS_MESSAGE_RECEIPTS)) ||
			// if message sent to groupchat
			!lstrcmp(msgType, _T("groupchat")) ||
			// if message delivery check disabled in settings
			!m_options.MsgAck || !JGetByte(hContact, "MsgAck", TRUE)) {
			if ( !lstrcmp(msgType, _T("groupchat")))
				xmlAddAttr(m, _T("to"), dbv.ptszVal);
			else {
				id = SerialNext();
				xmlAddAttr(m, _T("to"), szClientJid); xmlAddAttrID(m, id);
			}
			m_ThreadInfo->send(m);

			JForkThread(&CJabberProto::SendMessageAckThread, new TFakeAckParams(hContact, 0));

			nSentMsgId = 1;
		}
		else {
			id = SerialNext();
			xmlAddAttr(m, _T("to"), szClientJid); xmlAddAttrID(m, id);

			// message receipts XEP priority
			if (jcb & JABBER_CAPS_MESSAGE_RECEIPTS)
				m << XCHILDNS(_T("request"), _T(JABBER_FEAT_MESSAGE_RECEIPTS));
			else if (jcb & JABBER_CAPS_MESSAGE_EVENTS) {
				HXML x = m << XCHILDNS(_T("x"), _T(JABBER_FEAT_MESSAGE_EVENTS));
				x << XCHILD(_T("delivered")); x << XCHILD(_T("offline"));
			}
			else id = 1;

			m_ThreadInfo->send(m);
			nSentMsgId = id;
	}	}

	db_free(&dbv);
	return nSentMsgId;
}

////////////////////////////////////////////////////////////////////////////////////////
// SendUrl

int __cdecl CJabberProto::SendUrl(HANDLE /*hContact*/, int /*flags*/, const char* /*url*/)
{
	return 1;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberSetApparentMode - sets the visibility status

int __cdecl CJabberProto::SetApparentMode(HANDLE hContact, int mode)
{
	if (mode != 0 && mode != ID_STATUS_ONLINE && mode != ID_STATUS_OFFLINE)
		return 1;

	int oldMode = JGetWord(hContact, "ApparentMode", 0);
	if (mode == oldMode)
		return 1;

	JSetWord(hContact, "ApparentMode", (WORD)mode);
	if ( !m_bJabberOnline)
		return 0;

	DBVARIANT dbv;
	if ( !JGetStringT(hContact, "jid", &dbv)) {
		TCHAR* jid = dbv.ptszVal;
		switch (mode) {
		case ID_STATUS_ONLINE:
			if (m_iStatus == ID_STATUS_INVISIBLE || oldMode == ID_STATUS_OFFLINE)
				m_ThreadInfo->send(XmlNode(_T("presence")) << XATTR(_T("to"), jid));
			break;
		case ID_STATUS_OFFLINE:
			if (m_iStatus != ID_STATUS_INVISIBLE || oldMode == ID_STATUS_ONLINE)
				SendPresenceTo(ID_STATUS_INVISIBLE, jid, NULL);
			break;
		case 0:
			if (oldMode == ID_STATUS_ONLINE && m_iStatus == ID_STATUS_INVISIBLE)
				SendPresenceTo(ID_STATUS_INVISIBLE, jid, NULL);
			else if (oldMode == ID_STATUS_OFFLINE && m_iStatus != ID_STATUS_INVISIBLE)
				SendPresenceTo(m_iStatus, jid, NULL);
			break;
		}
		db_free(&dbv);
	}

	// TODO: update the zebra list (jabber:iq:privacy)

	return 0;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberSetStatus - sets the protocol status

int __cdecl CJabberProto::SetStatus(int iNewStatus)
{
	if (m_iDesiredStatus == iNewStatus)
		return 0;

	int oldStatus = m_iStatus;

	Log("PS_SETSTATUS(%d)", iNewStatus);
	m_iDesiredStatus = iNewStatus;

 	if (iNewStatus == ID_STATUS_OFFLINE) {
		if (m_ThreadInfo) {
			if (m_bJabberOnline) {
				// Quit all chatrooms (will send quit message)
				LISTFOREACH(i, this, LIST_CHATROOM)
					if (JABBER_LIST_ITEM *item = ListGetItemPtrFromIndex(i))
						GcQuit(item, 0, NULL);
			}

			m_ThreadInfo->send("</stream:stream>");
			m_ThreadInfo->shutdown();

			if (m_bJabberConnected) {
				m_bJabberConnected = m_bJabberOnline = FALSE;
				RebuildInfoFrame();
			}
		}

		m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE;
		JSendBroadcast(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus);
	}
	else if ( !m_bJabberConnected && !m_ThreadInfo && !(m_iStatus >= ID_STATUS_CONNECTING && m_iStatus < ID_STATUS_CONNECTING + MAX_CONNECT_RETRIES)) {
		m_iStatus = ID_STATUS_CONNECTING;
		ThreadData* thread = new ThreadData(this, JABBER_SESSION_NORMAL);
		JSendBroadcast(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus);
		thread->hThread = JForkThreadEx((JThreadFunc)&CJabberProto::ServerThread, thread);

		RebuildInfoFrame();
	}
	else if (m_bJabberOnline)
		SetServerStatus(iNewStatus);
	else
		JSendBroadcast(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus);

	return 0;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberGetAwayMsg - returns a contact's away message

void __cdecl CJabberProto::GetAwayMsgThread(void* hContact)
{
	DBVARIANT dbv;
	JABBER_LIST_ITEM *item;
	JABBER_RESOURCE_STATUS *r;
	int i, msgCount;
	size_t len;

	if ( !JGetStringT(hContact, "jid", &dbv)) {
		if ((item = ListGetItemPtr(LIST_ROSTER, dbv.ptszVal)) != NULL) {
			db_free(&dbv);
			if (item->resourceCount > 0) {
				Log("resourceCount > 0");
				r = item->resource;
				len = msgCount = 0;
				for (i=0; i<item->resourceCount; i++)
					if (r[i].statusMessage) {
						msgCount++;
						len += (_tcslen(r[i].resourceName) + _tcslen(r[i].statusMessage) + 8);
					}

				TCHAR* str = (TCHAR*)alloca(sizeof(TCHAR)*(len+1));
				str[0] = str[len] = '\0';
				for (i=0; i < item->resourceCount; i++)
					if (r[i].statusMessage) {
						if (str[0] != '\0') _tcscat(str, _T("\r\n"));
						if (msgCount > 1) {
							_tcscat(str, _T("("));
							_tcscat(str, r[i].resourceName);
							_tcscat(str, _T("): "));
						}
						_tcscat(str, r[i].statusMessage);
					}

				JSendBroadcast(hContact, ACKTYPE_AWAYMSG, ACKRESULT_SUCCESS, (HANDLE)1, (LPARAM)str);
				return;
			}

			if (item->itemResource.statusMessage != NULL) {
				JSendBroadcast(hContact, ACKTYPE_AWAYMSG, ACKRESULT_SUCCESS, (HANDLE)1, (LPARAM)item->itemResource.statusMessage);
				return;
			}
		}
		else db_free(&dbv);
	}

	JSendBroadcast(hContact, ACKTYPE_AWAYMSG, ACKRESULT_SUCCESS, (HANDLE)1, (LPARAM)0);
}

HANDLE __cdecl CJabberProto::GetAwayMsg(HANDLE hContact)
{
	Log("GetAwayMsg called, hContact=%08X", hContact);

	JForkThread(&CJabberProto::GetAwayMsgThread, hContact);
	return (HANDLE)1;
}

////////////////////////////////////////////////////////////////////////////////////////
// PSR_AWAYMSG

int __cdecl CJabberProto::RecvAwayMsg(HANDLE /*hContact*/, int /*statusMode*/, PROTORECVEVENT*)
{
	return 1;
}

////////////////////////////////////////////////////////////////////////////////////////
// PSS_AWAYMSG

int __cdecl CJabberProto::SendAwayMsg(HANDLE /*hContact*/, HANDLE /*hProcess*/, const char*)
{
	return 1;
}

////////////////////////////////////////////////////////////////////////////////////////
// JabberSetAwayMsg - sets the away status message

int __cdecl CJabberProto::SetAwayMsg(int status, const TCHAR *msg)
{
	Log("SetAwayMsg called, wParam=%d lParam=%S", status, msg);

	EnterCriticalSection(&m_csModeMsgMutex);

	TCHAR **szMsg;

	switch (status) {
	case ID_STATUS_ONLINE:
		szMsg = &m_modeMsgs.szOnline;
		break;

	case ID_STATUS_AWAY:
	case ID_STATUS_ONTHEPHONE:
	case ID_STATUS_OUTTOLUNCH:
		szMsg = &m_modeMsgs.szAway;
		status = ID_STATUS_AWAY;
		break;

	case ID_STATUS_NA:
		szMsg = &m_modeMsgs.szNa;
		break;

	case ID_STATUS_DND:
	case ID_STATUS_OCCUPIED:
		szMsg = &m_modeMsgs.szDnd;
		status = ID_STATUS_DND;
		break;

	case ID_STATUS_FREECHAT:
		szMsg = &m_modeMsgs.szFreechat;
		break;

	default:
		LeaveCriticalSection(&m_csModeMsgMutex);
		return 1;
	}

	TCHAR* newModeMsg = mir_tstrdup(msg);

	if ((*szMsg == NULL && newModeMsg == NULL) ||
		(*szMsg != NULL && newModeMsg != NULL && !lstrcmp(*szMsg, newModeMsg))) {
		// Message is the same, no update needed
		mir_free(newModeMsg);
		LeaveCriticalSection(&m_csModeMsgMutex);
	}
	else {
		// Update with the new mode message
		if (*szMsg != NULL)
			mir_free(*szMsg);
		*szMsg = newModeMsg;
		// Send a presence update if needed
		LeaveCriticalSection(&m_csModeMsgMutex);
		if (status == m_iStatus)
			SendPresence(m_iStatus, true);
	}

	return 0;
}

/////////////////////////////////////////////////////////////////////////////////////////
// JabberUserIsTyping - sends a UTN notification

int __cdecl CJabberProto::UserIsTyping(HANDLE hContact, int type)
{
	if ( !m_bJabberOnline) return 0;

	DBVARIANT dbv;
	if (JGetStringT(hContact, "jid", &dbv))
		return 0;

	JABBER_LIST_ITEM *item;
	if ((item = ListGetItemPtr(LIST_ROSTER, dbv.ptszVal)) != NULL) {
		TCHAR szClientJid[ JABBER_MAX_JID_LEN ];
		GetClientJID(dbv.ptszVal, szClientJid, SIZEOF(szClientJid));

		JabberCapsBits jcb = GetResourceCapabilites(szClientJid, TRUE);
		if (jcb & JABBER_RESOURCE_CAPS_ERROR)
			jcb = JABBER_RESOURCE_CAPS_NONE;

		XmlNode m(_T("message")); xmlAddAttr(m, _T("to"), szClientJid);

		if (jcb & JABBER_CAPS_CHATSTATES) {
			m << XATTR(_T("type"), _T("chat")) << XATTRID(SerialNext());
			switch (type) {
			case PROTOTYPE_SELFTYPING_OFF:
				m << XCHILDNS(_T("paused"), _T(JABBER_FEAT_CHATSTATES));
				m_ThreadInfo->send(m);
				break;
			case PROTOTYPE_SELFTYPING_ON:
				m << XCHILDNS(_T("composing"), _T(JABBER_FEAT_CHATSTATES));
				m_ThreadInfo->send(m);
				break;
			}
		}
		else if (jcb & JABBER_CAPS_MESSAGE_EVENTS) {
			HXML x = m << XCHILDNS(_T("x"), _T(JABBER_FEAT_MESSAGE_EVENTS));
			if (item->messageEventIdStr != NULL)
				x << XCHILD(_T("id"), item->messageEventIdStr);

			switch (type) {
			case PROTOTYPE_SELFTYPING_OFF:
				m_ThreadInfo->send(m);
				break;
			case PROTOTYPE_SELFTYPING_ON:
				x << XCHILD(_T("composing"));
				m_ThreadInfo->send(m);
				break;
	}	}	}

	db_free(&dbv);
	return 0;
}

/////////////////////////////////////////////////////////////////////////////////////////
// Notify dialogs

void CJabberProto::WindowSubscribe(HWND hwnd)
{
	WindowList_Add(m_windowList, hwnd, NULL);
}

void CJabberProto::WindowUnsubscribe(HWND hwnd)
{
	WindowList_Remove(m_windowList, hwnd);
}

void CJabberProto::WindowNotify(UINT msg, bool async)
{
	if (async)
		WindowList_BroadcastAsync(m_windowList, msg, 0, 0);
	else
		WindowList_Broadcast(m_windowList, msg, 0, 0);
}

/////////////////////////////////////////////////////////////////////////////////////////
// InfoFrame events

void CJabberProto::InfoFrame_OnSetup(CJabberInfoFrame_Event*)
{
	OnMenuOptions(0, 0);
}

void CJabberProto::InfoFrame_OnTransport(CJabberInfoFrame_Event *evt)
{
	if (evt->m_event == CJabberInfoFrame_Event::CLICK) {
		HANDLE hContact = (HANDLE)evt->m_pUserData;
		HMENU hContactMenu = (HMENU)CallService(MS_CLIST_MENUBUILDCONTACT, (WPARAM)hContact, 0);
		POINT pt;
		GetCursorPos(&pt);
		int res = TrackPopupMenu(hContactMenu, TPM_RETURNCMD, pt.x, pt.y, 0, (HWND)CallService(MS_CLUI_GETHWND, 0, 0), NULL);
		CallService(MS_CLIST_MENUPROCESSCOMMAND, MAKEWPARAM(res, MPCF_CONTACTMENU), (LPARAM)hContact);
	}
}

/////////////////////////////////////////////////////////////////////////////////////////
// OnEvent - maintain protocol events

int __cdecl CJabberProto::OnEvent(PROTOEVENTTYPE eventType, WPARAM wParam, LPARAM lParam)
{
	switch(eventType) {
	case EV_PROTO_ONLOAD:    return OnModulesLoadedEx(0, 0);
	case EV_PROTO_ONEXIT:    return OnPreShutdown(0, 0);
	case EV_PROTO_ONOPTIONS: return OnOptionsInit(wParam, lParam);

	case EV_PROTO_ONMENU:
		MenuInit();
		break;

	case EV_PROTO_ONRENAME:
		if (m_hMenuRoot) {
			CLISTMENUITEM clmi = { sizeof(clmi) };
			clmi.flags = CMIM_NAME | CMIF_TCHAR | CMIF_KEEPUNTRANSLATED;
			clmi.ptszName = m_tszUserName;
			CallService(MS_CLIST_MODIFYMENUITEM, (WPARAM)m_hMenuRoot, (LPARAM)&clmi);
		}
		break;

	case EV_PROTO_ONCONTACTDELETED:
		return OnContactDeleted(wParam, lParam);

	case EV_PROTO_DBSETTINGSCHANGED:
		return OnDbSettingChanged(wParam, lParam);
	}
	return 1;
}