view libpurple/protocols/mxit/markup.c @ 31172:e89df17f5ae7

certificate: Better validation of chains which have an intermediate signed w/ MD5. We already distribute the CAcert class 3 root as a trusted root. Newer versions of GnuTLS (combined with the changes to deal with MSN's cert breakage) require us to check if the last cert (not just its issuer) is in our trusted store.
author Paul Aurich <paul@darkrain42.org>
date Sun, 30 Jan 2011 17:51:02 +0000
parents a8cc50c2279f
children 249ffc44061e
line wrap: on
line source

/*
 *					MXit Protocol libPurple Plugin
 *
 *			-- convert between MXit and libPurple markup --
 *
 *				Pieter Loubser	<libpurple@mxit.com>
 *
 *			(C) Copyright 2009	MXit Lifestyle (Pty) Ltd.
 *				<http://www.mxitlifestyle.com>
 *
 * 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  02111-1301  USA
 */

#include    "internal.h"
#include	"purple.h"

#include	"protocol.h"
#include	"mxit.h"
#include	"markup.h"
#include	"chunk.h"
#include	"formcmds.h"
#include	"roster.h"


/* define this to enable emoticon (markup) debugging */
#undef		MXIT_DEBUG_EMO
/* define this to enable markup conversion debugging */
#undef		MXIT_DEBUG_MARKUP


#define		MXIT_FRAME_MAGIC		"MXF\x01"			/* mxit emoticon magic number */
#define		MXIT_MAX_EMO_ID			16					/* maximum emoticon ID length */
#define		COLORCODE_LEN			6					/* colour code ID length */


/* HTML tag types */
#define		MXIT_TAG_COLOR			0x01				/* font color tag */
#define		MXIT_TAG_SIZE			0x02				/* font size tag */
#define		MXIT_MAX_MSG_TAGS		90					/* maximum tags per message (pigdin hack work around) */

/*
 * a HTML tag object
 */
struct tag {
	char	type;
	char*	value;
};


#define		MXIT_VIBE_MSG_COLOR		"#9933FF"

/* vibes */
static const char*	vibes[] = {
	/* 0 */		N_( "Cool Vibrations" ),
	/* 1 */		N_( "Purple Rain" ),
	/* 2 */		N_( "Polite" ),
	/* 3 */		N_( "Rock n Roll" ),
	/* 4 */		N_( "Summer Slumber" ),
	/* 5 */		N_( "Electric Razor" ),
	/* 6 */		N_( "S.O.S" ),
	/* 7 */		N_( "Jack Hammer" ),
	/* 8 */		N_( "Bumble Bee" ),
	/* 9 */		N_( "Ripple" )
};



#ifdef	MXIT_DEBUG_EMO
/*------------------------------------------------------------------------
 * Dump a byte buffer as hexadecimal to the console for debugging purposes.
 *
 *  @param buf				The data to dump
 *  @param len				The length of the data
 */
static void hex_dump( const char* buf, int len )
{
	char		msg[256];
	int			pos;
	int			i;

	purple_debug_info( MXIT_PLUGIN_ID, "Dumping data (%i bytes)\n", len );

	memset( msg, 0x00, sizeof( msg ) );
	pos = 0;

	for ( i = 0; i < len; i++ ) {

		if ( pos == 0 )
			pos += sprintf( &msg[pos], "%04i:  ", i );

		pos += sprintf( &msg[pos], "0x%02X ", (unsigned char) buf[i] );

		if ( i % 16 == 15 ) {
			pos += sprintf( &msg[pos], "\n" );
			purple_debug_info( MXIT_PLUGIN_ID, msg );
			pos = 0;
		}
		else if ( i % 16 == 7 )
			pos += sprintf( &msg[pos], " " );
	}

	if ( pos > 0 ) {
		pos += sprintf( &msg[pos], "\n" );
		purple_debug_info( MXIT_PLUGIN_ID, msg );
		pos = 0;
	}
}
#endif


/*------------------------------------------------------------------------
 * Adds a link to a message
 *
 *  @param mx				The Markup message object
 *	@param linkname			This is the what will be returned when the link gets clicked
 *	@param displayname		This is the name for the link which will be displayed in the UI
 */
void mxit_add_html_link( struct RXMsgData* mx, const char* linkname, const char* displayname )
{
#ifdef	MXIT_LINK_CLICK
	char	retstr[256];
	gchar*	retstr64;
	char	link[256];
	int		len;

	len = g_snprintf( retstr, sizeof( retstr ), "%s|%s|%s|%s|%s", MXIT_LINK_KEY, purple_account_get_username( mx->session->acc ),
											purple_account_get_protocol_id( mx->session->acc ), mx->from, linkname );
	retstr64 = purple_base64_encode( (const unsigned char*) retstr, len );
	g_snprintf( link, sizeof( link ), "%s%s", MXIT_LINK_PREFIX, retstr64 );
	g_free( retstr64 );

	g_string_append_printf( mx->msg, "<a href=\"%s\">%s</a>", link, displayname );
#else
	g_string_append_printf( mx->msg, "<b>%s</b>", linkname );
#endif
}


/*------------------------------------------------------------------------
 * Extract an ASN.1 formatted length field from the data.
 *
 *  @param data				The source data
 *  @param size				The extracted length
 *  @return					The number of bytes extracted
 */
static unsigned int asn_getlength( const char* data, int* size )
{
	unsigned int	len		= 0;
	unsigned char	bytes;
	unsigned char	byte;
	int				i;

	/* first byte specifies the number of bytes in the length */
	bytes = ( data[0] & ~0x80 );
	if ( bytes > sizeof( unsigned int ) ) {
		/* file too big! */
		return -1;
	}
	data++;

	/* parse out the actual length */
	for ( i = 0; i < bytes; i++ ) {
		byte = data[i];
		len <<= 8;
		len += byte;
	}

	*size = len;
	return bytes + 1;
}


/*------------------------------------------------------------------------
 * Extract an ASN.1 formatted UTF-8 string field from the data.
 *
 *  @param data				The source data
 *  @param type				Expected type of string
 *  @param utf8				The extracted string.  Must be deallocated by caller.
 *  @return					The number of bytes extracted
 */
static int asn_getUtf8( const char* data, char type, char** utf8 )
{
	int		len;

	/* validate the field type [1 byte] */
	if ( data[0] != type ) {
		/* this is not a utf-8 string! */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid UTF-8 encoded string in ASN data (0x%02X)\n", (unsigned char) data[0] );
		return -1;
	}

	len = data[1];						/* length field [1 bytes] */
	*utf8 = g_malloc( len + 1 );
	memcpy( *utf8, &data[2], len );		/* data field */
	(*utf8)[len] = '\0';

	return ( len + 2 );
}


/*------------------------------------------------------------------------
 * Free data associated with a Markup message object.
 *
 *  @param mx				The Markup message object
 */
static void free_markupdata( struct RXMsgData* mx )
{
	if ( mx ) {
		if ( mx->msg )
			g_string_free( mx->msg, TRUE );
		if ( mx->from )
			g_free( mx->from );
		g_free( mx );
	}
}


/*------------------------------------------------------------------------
 * Split the message into smaller messages and send them one at a time
 * to pidgin to be displayed on the UI
 *
 *  @param mx				The received message object
 */
static void mxit_show_split_message( struct RXMsgData* mx )
{
	const char*		cont	= "<font color=\"#999999\">continuing...</font>\n";
	GString*		msg		= NULL;
	char*			ch		= NULL;
	int				pos		= 0;
	int				start	= 0;
	int				l_nl	= 0;
	int				l_sp	= 0;
	int				l_gt	= 0;
	int				stop	= 0;
	int				tags	= 0;
	int				segs	= 0;
	gboolean		intag	= FALSE;

	/*
	 * awful hack to work around the awful hack in pidgin to work around GtkIMHtml's
	 * inefficient rendering of messages with lots of formatting changes.
	 * (reference: see the function pidgin_conv_write_conv() in gtkconv.c) the issue
	 * is that when you have more than 100 '<' characters in the message passed to
	 * pidgin, none of the markup (including links) are rendered and thus just dump
	 * all the text as is to the conversation window. this message dump is very
	 * confusing and makes it totally unusable. to work around this we will count
	 * the amount of tags and if its more than the pidgin threshold, we will just
	 * break the message up into smaller parts and send them separately to pidgin.
	 * to the user it will look like multiple messages, but at least he will be able
	 * to use and understand it.
	 */

	ch = mx->msg->str;
	pos = start;
	while ( ch[pos] ) {

		if ( ch[pos] == '<' ) {
			tags++;
			intag = TRUE;
		}
		else if ( ch[pos] == '\n' ) {
			l_nl = pos;
		}
		else if ( ch[pos] == '>' ) {
			l_gt = pos;
			intag = FALSE;
		}
		else if ( ch[pos] == ' ' ) {
			/* ignore spaces inside tags */
			if ( !intag )
				l_sp = pos;
		}
		else if ( ( ch[pos] == 'w' ) && ( pos + 4 < mx->msg->len ) && ( memcmp( &ch[pos], "www.", 4 ) == 0 ) ) {
			tags += 2;
		}
		else if ( ( ch[pos] == 'h' ) && ( pos + 8 < mx->msg->len ) && ( memcmp( &ch[pos], "http://", 7 ) == 0 ) ) {
			tags += 2;
		}

		if ( tags > MXIT_MAX_MSG_TAGS ) {
			/* we have reached the maximum amount of tags pidgin (gtk) can handle per message.
			   so its time to send what we have and then start building a new message */

			/* now find the right place to break the message */
			if ( l_nl > start ) {
				/* break at last '\n' char */
				stop = l_nl;
				ch[stop] = '\0';
				msg = g_string_new( &ch[start] );
				ch[stop] = '\n';
			}
			else if ( l_sp > start ) {
				/* break at last ' ' char */
				stop = l_sp;
				ch[stop] = '\0';
				msg = g_string_new( &ch[start] );
				ch[stop] = ' ';
			}
			else {
				/* break at the last '>' char */
				char t;
				stop = l_gt + 1;
				t = ch[stop];
				ch[stop] = '\0';
				msg = g_string_new( &ch[start] );
				ch[stop] = t;
				stop--;
			}

			/* build the string */
			if ( segs )
				g_string_prepend( msg, cont );

			/* push message to pidgin */
			serv_got_im( mx->session->con, mx->from, msg->str, mx->flags, mx->timestamp );
			g_string_free( msg, TRUE );
			msg = NULL;

			tags = 0;
			segs++;
			start = stop + 1;
		}

		pos++;
	}

	if ( start != pos ) {
		/* send the last part of the message */

		/* build the string */
		ch[pos] = '\0';
		msg = g_string_new( &ch[start] );
		ch[pos] = '\n';
		if ( segs )
			g_string_prepend( msg, cont );

		/* push message to pidgin */
		serv_got_im( mx->session->con, mx->from, msg->str, mx->flags, mx->timestamp );
		g_string_free( msg, TRUE );
		msg = NULL;
	}
}


/*------------------------------------------------------------------------
 * Insert custom emoticons and inline images into the message (if there
 * are any), then give the message to the UI to display to the user.
 *
 *  @param mx				The received message object
 */
void mxit_show_message( struct RXMsgData* mx )
{
	char*				pos;
	int					start;
	unsigned int		end;
	int					emo_ofs;
	char				ii[128];
	char				tag[64];
	int*				img_id;

	if ( mx->got_img ) {
		/* search and replace all emoticon tags with proper image tags */

		while ( ( pos = strstr( mx->msg->str, MXIT_II_TAG ) ) != NULL ) {
			start = pos - mx->msg->str;					/* offset at which MXIT_II_TAG starts */
			emo_ofs = start + strlen( MXIT_II_TAG );	/* offset at which EMO's ID starts */
			end = emo_ofs + 1;							/* offset at which MXIT_II_TAG ends */

			while ( ( end < mx->msg->len ) && ( mx->msg->str[end] != '>' ) )
				end++;

			if ( end == mx->msg->len )			/* end of emoticon tag not found */
				break;

			memset( ii, 0x00, sizeof( ii ) );
			memcpy( ii, &mx->msg->str[emo_ofs], end - emo_ofs );

			/* remove inline image tag */
			g_string_erase( mx->msg, start, ( end - start ) + 1 );

			/* find the image entry */
			img_id = (int*) g_hash_table_lookup( mx->session->iimages, ii );
			if ( !img_id ) {
				/* inline image not found, so we will just skip it */
				purple_debug_error( MXIT_PLUGIN_ID, "inline image NOT found (%s)\n", ii );
			}
			else {
				/* insert img tag */
				g_snprintf( tag, sizeof( tag ), "<img id=\"%i\">", *img_id );
				g_string_insert( mx->msg, start, tag );
			}
		}
	}

#ifdef MXIT_DEBUG_MARKUP
	purple_debug_info( MXIT_PLUGIN_ID, "Markup RX (converted): '%s'\n", mx->msg->str );
#endif

	if ( mx->processed ) {
		/* this message has already been taken care of, so just ignore it here */
	}
	else if ( mx->chatid < 0 ) {
		/* normal chat message */
		mxit_show_split_message( mx );
	}
	else {
		/* this is a multimx message */
		serv_got_chat_in( mx->session->con, mx->chatid, mx->from, mx->flags, mx->msg->str, mx->timestamp);
	}

	/* freeup resource */
	free_markupdata( mx );
}


/*------------------------------------------------------------------------
 * Extract the custom emoticon ID from the message.
 *
 *  @param message			The input data
 *  @param emid				The extracted emoticon ID
 */
static void parse_emoticon_str( const char* message, char* emid )
{
	int		i;

	for ( i = 0; ( message[i] != '\0' && message[i] != '}' && i < MXIT_MAX_EMO_ID ); i++ ) {
		emid[i] = message[i];
	}

	if ( message[i] == '\0' ) {
		/* end of message reached, ignore the tag */
		emid[0] = '\0';
	}
	else if ( i == MXIT_MAX_EMO_ID ) {
		/* invalid tag length, ignore the tag */
		emid[0] = '\0';
	}
	else
		emid[i] = '\0';
}


/*------------------------------------------------------------------------
 * Callback function invoked when a custom emoticon request to the WAP site completes.
 *
 *  @param url_data
 *  @param user_data		The Markup message object
 *  @param url_text			The data returned from the WAP site
 *  @param len				The length of the data returned
 *  @param error_message	Descriptive error message
 */
static void emoticon_returned( PurpleUtilFetchUrlData* url_data, gpointer user_data, const gchar* url_text, gsize len, const gchar* error_message )
{
	struct RXMsgData*	mx			= (struct RXMsgData*) user_data;
	const char*			data		= url_text;
	unsigned int		pos			= 0;
	char				emo[16];
	int					id;
	char*				str;
	int					em_size		= 0;
	char*				em_data		= NULL;
	char*				em_id		= NULL;
	int*				intptr		= NULL;
	int					res;

#ifdef	MXIT_DEBUG_EMO
	purple_debug_info( MXIT_PLUGIN_ID, "emoticon_returned\n" );
#endif

	if ( !url_text ) {
		/* no reply from the WAP site */
		purple_debug_error( MXIT_PLUGIN_ID, "Error contacting the MXit WAP site. Please try again later (emoticon).\n" );
		goto done;
	}

#ifdef	MXIT_DEBUG_EMO
	hex_dump( data, len );
#endif

	/* parse out the emoticon */
	pos = 0;

	/* validate the binary data received from the wapsite */
	if ( memcmp( MXIT_FRAME_MAGIC, &data[pos], strlen( MXIT_FRAME_MAGIC ) ) != 0 ) {
		/* bad data, magic constant is wrong */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad magic)\n" );
		goto done;
	}
	pos += strlen( MXIT_FRAME_MAGIC );

	/* validate the image frame desc byte */
	if ( data[pos] != '\x6F' ) {
		/* bad frame desc */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad frame desc)\n" );
		goto done;
	}
	pos++;

	/* get the data length */
	res = asn_getlength( &data[pos], &em_size );
	if ( res <= 0 ) {
		/* bad frame length */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad frame length)\n" );
		goto done;
	}
	pos += res;
#ifdef	MXIT_DEBUG_EMO
	purple_debug_info( MXIT_PLUGIN_ID, "read the length '%i'\n", em_size );
#endif

	/* utf-8 (emoticon name) */
	res = asn_getUtf8( &data[pos], 0x0C, &str );
	if ( res <= 0 ) {
		/* bad utf-8 string */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad name string)\n" );
		goto done;
	}
	pos += res;
#ifdef	MXIT_DEBUG_EMO
	purple_debug_info( MXIT_PLUGIN_ID, "read the string '%s'\n", str );
#endif
	g_free( str );
	str = NULL;

	/* utf-8 (emoticon shortcut) */
	res = asn_getUtf8( &data[pos], 0x81, &str );
	if ( res <= 0 ) {
		/* bad utf-8 string */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad shortcut string)\n" );
		goto done;
	}
	pos += res;
#ifdef	MXIT_DEBUG_EMO
	purple_debug_info( MXIT_PLUGIN_ID, "read the string '%s'\n", str );
#endif
	em_id = str;

	/* validate the image data type */
	if ( data[pos] != '\x82' ) {
		/* bad frame desc */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad data type)\n" );
		g_free( em_id );
		goto done;
	}
	pos++;

	/* get the data length */
	res = asn_getlength( &data[pos], &em_size );
	if ( res <= 0 ) {
		/* bad frame length */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad data length)\n" );
		g_free( em_id );
		goto done;
	}
	pos += res;
#ifdef	MXIT_DEBUG_EMO
	purple_debug_info( MXIT_PLUGIN_ID, "read the length '%i'\n", em_size );
#endif

	if ( g_hash_table_lookup( mx->session->iimages, em_id ) ) {
		/* emoticon found in the table, so ignore this one */
		goto done;
	}

	/* make a copy of the data */
	em_data = g_malloc( em_size );
	memcpy( em_data, &data[pos], em_size );

	/* strip the mxit markup tags from the emoticon id */
	if ( ( em_id[0] == '.' ) && ( em_id[1] == '{' ) ) {
		parse_emoticon_str( &em_id[2], emo );
		strcpy( em_id, emo );
	}

	/* we now have the emoticon, store it in the imagestore */
	id = purple_imgstore_add_with_id( em_data, em_size, NULL );

	/* map the mxit emoticon id to purple image id */
	intptr = g_malloc( sizeof( int ) );
	*intptr = id;
	g_hash_table_insert( mx->session->iimages, em_id, intptr );

	mx->flags |= PURPLE_MESSAGE_IMAGES;
done:
	mx->img_count--;
	if ( ( mx->img_count == 0 ) && ( mx->converted ) ) {
		/*
		 * this was the last outstanding emoticon for this message,
		 * so we can now display it to the user.
		 */
		mxit_show_message( mx );
	}
}


/*------------------------------------------------------------------------
 * Send a request to the MXit WAP site to download the specified emoticon.
 *
 *  @param mx				The Markup message object
 *  @param id				The ID for the emoticon
 */
static void emoticon_request( struct RXMsgData* mx, const char* id )
{
	PurpleUtilFetchUrlData*	url_data;
	const char*				wapserver;
	char*					url;

	purple_debug_info( MXIT_PLUGIN_ID, "sending request for emoticon '%s'\n", id );

	wapserver = purple_account_get_string( mx->session->acc, MXIT_CONFIG_WAPSERVER, DEFAULT_WAPSITE );

	/* reference: "libpurple/util.h" */
	url = g_strdup_printf( "%s/res/?type=emo&mlh=%i&sc=%s&ts=%li", wapserver, MXIT_EMOTICON_SIZE, id, time( NULL ) );
	url_data = purple_util_fetch_url_request( url, TRUE, NULL, TRUE, NULL, FALSE, emoticon_returned, mx );
	g_free( url );
}


/*------------------------------------------------------------------------
 * Parse a Vibe command.
 *
 *  @param mx				The Markup message object
 *  @param message			The message text (which contains the vibe)
 *  @return id				The length of the message to skip
 */
static int mxit_parse_vibe( struct RXMsgData* mx, const char* message )
{
	int		vibeid;

	vibeid = message[2] - '0';

	purple_debug_info( MXIT_PLUGIN_ID, "Vibe received (%i)\n", vibeid );

	if ( vibeid > ( ARRAY_SIZE( vibes ) - 1 ) ) {
		purple_debug_warning( MXIT_PLUGIN_ID, "Unsupported vibe received (%i)\n", vibeid );
		/* unsupported vibe */
		return 0;
	}

	g_string_append_printf( mx->msg, "<font color=\"%s\"><i>%s Vibe...</i></font>", MXIT_VIBE_MSG_COLOR, _( vibes[vibeid] ) );
	return 2;
}


/*------------------------------------------------------------------------
 * Extract the nickname from a chatroom message and display it nicely in
 * libPurple-style (HTML) markup.
 *
 *  @param mx				The received message data object
 *  @param message			The message text
 *  @return					The length of the message to skip
 */
static int mxit_extract_chatroom_nick( struct RXMsgData* mx, char* message, int len, int msgflags )
{
	int		i;

	if ( message[0] == '<' ) {
		/*
		 * The message MIGHT contains an embedded nickname.  But we can't
		 * be sure unless we find the end-of-nickname sequence: (>\n)
		 * Search for it....
		 */
		gboolean	found	= FALSE;

		for ( i = 1; i < len; i++ ) {
			if ( ( message[i] == '\n' ) && ( message[i-1] == '>' ) ) {
				found = TRUE;
				message[i-1] = '\0';	/* loose the '>' */
				i++;					/* and skip the new-line */
				break;
			}
		}

		if ( found ) {
			gchar*		nickname;

			/*
			 * The message definitely had an embedded nickname - generate a marked-up
			 * message to be displayed.
			 */
			nickname = g_markup_escape_text( &message[1], -1 );

			/* Remove any MXit escaping from nickname ("\X" --> "X") */
			if ( msgflags & CP_MSG_MARKUP ) {
				int	nicklen = strlen( nickname );
				int	j, k;

				for ( j = 0, k = 0; j < nicklen; j++ ) {
					if ( nickname[j] == '\\' )
						j++;

					nickname[k] = nickname[j];
					k++;
				}

				nickname[k] = '\0';		/* terminate string */
			}

			/* add nickname within some BOLD markup to the new converted message */
			g_string_append_printf( mx->msg, "<b>%s:</b> ", nickname );

			/* free up the resources */
			g_free( nickname );

			return i;
		}
	}

	return 0;
}



/*------------------------------------------------------------------------
 * Convert a message containing MXit protocol markup to libPurple-style (HTML) markup.
 *
 *  @param mx				The received message data object
 *  @param message			The message text
 *  @param len				The length of the message
 */
void mxit_parse_markup( struct RXMsgData* mx, char* message, int len, short msgtype, int msgflags )
{
	char		tmpstr1[128];
	char*		ch;
	int			i			= 0;

	/* tags */
	gboolean	tag_bold	= FALSE;
	gboolean	tag_under	= FALSE;
	gboolean	tag_italic	= FALSE;

#ifdef MXIT_DEBUG_MARKUP
	purple_debug_info( MXIT_PLUGIN_ID, "Markup RX (original): '%s'\n", message );
#endif


	/*
	 * supported MXit markup:
	 * '*'			bold
	 * '_'			underline
	 * '/'			italics
	 * '$'			highlight text
	 * '.+' 		inc font size
	 * '.-'			dec font size
	 * '#XXXXXX'	foreground color
	 * '.{XX}'		custom emoticon
	 * '\'			escape the following character
	 * '::'			MXit commands
	 */


	if ( is_mxit_chatroom_contact( mx->session, mx->from ) ) {
		/* chatroom message, so we need to extract and skip the sender's nickname
		 * which is embedded inside the message */
		i = mxit_extract_chatroom_nick( mx, message, len, msgflags );
	}

	/* run through the message and check for custom emoticons and markup */
	for ( ; i < len; i++ ) {
		switch ( message[i] ) {


			/* mxit markup parsing */
			case '*' :
					if ( !( msgflags & CP_MSG_MARKUP ) ) {
						g_string_append_c( mx->msg, message[i] );
						break;
					}

					/* bold markup */
					if ( !tag_bold )
						g_string_append( mx->msg, "<b>" );
					else
						g_string_append( mx->msg, "</b>" );
					tag_bold = !tag_bold;
					break;
			case '_' :
					if ( !( msgflags & CP_MSG_MARKUP ) ) {
						g_string_append_c( mx->msg, message[i] );
						break;
					}

					/* underscore markup */
					if ( !tag_under )
						g_string_append( mx->msg, "<u>" );
					else
						g_string_append( mx->msg, "</u>" );
					tag_under = !tag_under;
					break;
			case '/' :
					if ( !( msgflags & CP_MSG_MARKUP ) ) {
						g_string_append_c( mx->msg, message[i] );
						break;
					}

					/* italics markup */
					if ( !tag_italic )
						g_string_append( mx->msg, "<i>" );
					else
						g_string_append( mx->msg, "</i>" );
					tag_italic = !tag_italic;
					break;
			case '$' :
					if ( !( msgflags & CP_MSG_MARKUP ) ) {
						g_string_append_c( mx->msg, message[i] );
						break;
					}
					else if ( i + 1 >= len ) {
						/* message too short for complete link */
						g_string_append_c( mx->msg, '$' );
						break;
					}

					/* find the end tag */
					ch = strstr( &message[i + 1], "$" );
					if ( ch ) {
						/* end found */
						*ch = '\0';
						mxit_add_html_link( mx, &message[i + 1], &message[i + 1] );
						*ch = '$';
						i += ( ch - &message[i + 1] ) + 1;
					}
					else {
						g_string_append_c( mx->msg, message[i] );
					}
					/* highlight text */
					break;
			case '#' :
					if ( !( msgflags & CP_MSG_MARKUP ) ) {
						g_string_append_c( mx->msg, message[i] );
						break;
					}
					else if ( i + COLORCODE_LEN >= len ) {
						/* message too short for complete colour code */
						g_string_append_c( mx->msg, '#' );
						break;
					}

					/* foreground (text) color */
					memcpy( tmpstr1, &message[i + 1], COLORCODE_LEN );
					tmpstr1[ COLORCODE_LEN ] = '\0';			/* terminate string */
					if ( strcmp( tmpstr1, "??????" ) == 0 ) {
						/* need to reset the font */
						g_string_append( mx->msg, "</font>" );
						i += COLORCODE_LEN;
					}
					else if ( strspn( tmpstr1, "0123456789abcdefABCDEF") == COLORCODE_LEN ) {
						/* definitely a numeric colour code */
						g_string_append_printf( mx->msg, "<font color=\"#%s\">", tmpstr1 );
						i += COLORCODE_LEN;
					}
					else {
						/* not valid colour markup */
						g_string_append_c( mx->msg, '#' );
					}
					break;
			case '.' :
					if ( !( msgflags & CP_MSG_EMOTICON ) ) {
						g_string_append_c( mx->msg, message[i] );
						break;
					}
					else if ( i + 1 >= len ) {
						/* message too short */
						g_string_append_c( mx->msg, '.' );
						break;
					}

					switch ( message[i+1] ) {
						case '+' :
								/* increment text size */
								g_string_append( mx->msg, "<font size=\"+1\">" );
								i++;
								break;
						case '-' :
								/* decrement text size */
								g_string_append( mx->msg, "<font size=\"-1\">" );
								i++;
								break;
						case '{' :
								/* custom emoticon */
								if ( i + 2 >= len ) {
									/* message too short */
									g_string_append_c( mx->msg, '.' );
									break;
								}

								parse_emoticon_str( &message[i+2], tmpstr1 );
								if ( tmpstr1[0] != '\0' ) {
									mx->got_img = TRUE;

									if ( g_hash_table_lookup( mx->session->iimages, tmpstr1 ) ) {
										/* emoticon found in the cache, so we do not have to request it from the WAPsite */
									}
									else {
										/* request emoticon from the WAPsite */
										mx->img_count++;
										emoticon_request( mx, tmpstr1 );
									}

									g_string_append_printf( mx->msg, MXIT_II_TAG"%s>", tmpstr1 );
									i += strlen( tmpstr1 ) + 2;
								}
								else
									g_string_append_c( mx->msg, '.' );

								break;
						default :
								g_string_append_c( mx->msg, '.' );
								break;
					}
					break;
			case '\\' :
					if ( i + 1 >= len ) {
						/* message too short for an escaped character */
						g_string_append_c( mx->msg, '\\' );
					}
					else {
						/* ignore the next character, because its been escaped */
						g_string_append_c( mx->msg, message[i + 1] );
						i++;
					}
					break;


			/* command parsing */
			case ':' :
					if ( i + 1 >= len ) {
						/* message too short */
						g_string_append_c( mx->msg, ':' );
						break;
					}

					if ( message[i+1] == '@' ) {
						/* this is a vibe! */
						int		size;

						if ( i + 2 >= len ) {
							/* message too short */
							g_string_append_c( mx->msg, message[i] );
							break;
						}

						size = mxit_parse_vibe( mx, &message[i] );
						if ( size == 0 )
							g_string_append_c( mx->msg, message[i] );
						else
							i += size;
					}
					else if ( msgtype != CP_MSGTYPE_COMMAND ) {
						/* this is not a command message */
						g_string_append_c( mx->msg, message[i] );
					}
					else if ( message[i+1] == ':' ) {
						/* parse out the command */
						int		size;

						size = mxit_parse_command( mx, &message[i] );
						if ( size == 0 )
							g_string_append_c( mx->msg, ':' );
						else
							i += size;
					}
					else {
						g_string_append_c( mx->msg, ':' );
					}
					break;


			/* these aren't MXit markup, but are interpreted by libPurple */
			case '<' :
					g_string_append( mx->msg, "&lt;" );
					break;
			case '>' :
					g_string_append( mx->msg, "&gt;" );
					break;
			case '&' :
					g_string_append( mx->msg, "&amp;" );
					break;
			case '"' :
					g_string_append( mx->msg, "&quot;" );
					break;

			default :
					/* text */
					g_string_append_c( mx->msg, message[i] );
					break;
		}
	}
}


/*------------------------------------------------------------------------
 * Insert an inline image command.
 *
 *  @param mx				The message text as processed so far.
 *  @oaram id				The imgstore ID of the inline image.
 */
static void inline_image_add( GString* mx, int id )
{
	PurpleStoredImage *image;
	gconstpointer img_data;
	gsize img_size;
	gchar* enc;

	image = purple_imgstore_find_by_id( id );
	if ( image == NULL )
		return;

	img_data = purple_imgstore_get_data( image );
	img_size = purple_imgstore_get_size( image );

	enc = purple_base64_encode( img_data, img_size );

	g_string_append( mx, "::op=img|dat=" );
	g_string_append( mx, enc );
	g_string_append_c( mx, ':' );

	g_free( enc );
}


/*------------------------------------------------------------------------
 * Convert libpurple (HTML) markup to MXit protocol markup (for sending to MXit).
 * Any MXit markup codes in the original message also need to be escaped.
 *
 *  @param message			The message text containing libPurple (HTML) markup
 *  @return					The message text containing MXit markup
 */
char* mxit_convert_markup_tx( const char* message, int* msgtype )
{
	GString*			mx;
	struct tag*			tag;
	GList*				entry;
	GList*				tagstack	= NULL;
	char*				reply;
	char				color[8];
	int					len			= strlen ( message );
	int					i;

#ifdef MXIT_DEBUG_MARKUP
	purple_debug_info( MXIT_PLUGIN_ID, "Markup TX (original): '%s'\n", message );
#endif

	/*
	 * libPurple uses the following HTML markup codes:
	 *   Bold:			<b>...</b>
	 *   Italics:		<i>...</i>
	 *   Underline:		<u>...</u>
	 *   Strikethrough:	<s>...</s>					(NO MXIT SUPPORT)
	 *   Font size:		<font size="">...</font>
	 *   Font type:		<font face="">...</font>	(NO MXIT SUPPORT)
	 *   Font colour:	<font color=#">...</font>
	 *   Links:			<a href="">...</a>
	 *   Newline:		<br>
	 *   Inline image:  <IMG ID="">
	 * The following characters are also encoded:
	 *   &amp;  &quot;  &lt;  &gt;
	 */

	/* new message data */
	mx = g_string_sized_new( len );

	/* run through the message and check for HTML markup */
	for ( i = 0; i < len; i++ ) {

		switch ( message[i] ) {
			case '<' :
				if ( purple_str_has_prefix( &message[i], "<b>" ) || purple_str_has_prefix( &message[i], "</b>" ) ) {
					/* bold */
					g_string_append_c( mx, '*' );
				}
				else if ( purple_str_has_prefix( &message[i], "<i>" ) || purple_str_has_prefix( &message[i], "</i>" ) ) {
					/* italics */
					g_string_append_c( mx, '/' );
				}
				else if ( purple_str_has_prefix( &message[i], "<u>" ) || purple_str_has_prefix( &message[i], "</u>" ) ) {
					/* underline */
					g_string_append_c( mx, '_' );
				}
				else if ( purple_str_has_prefix( &message[i], "<br>" ) ) {
					/* newline */
					g_string_append_c( mx, '\n' );
				}
				else if ( purple_str_has_prefix( &message[i], "<font size=" ) ) {
					/* font size */
					tag = g_new0( struct tag, 1 );
					tag->type = MXIT_TAG_SIZE;
					tagstack = g_list_prepend( tagstack, tag );
					// TODO: implement size control
				}
				else if ( purple_str_has_prefix( &message[i], "<font color=" ) ) {
					/* font colour */
					tag = g_new0( struct tag, 1 );
					tag->type = MXIT_TAG_COLOR;
					tagstack = g_list_append( tagstack, tag );
					memset( color, 0x00, sizeof( color ) );
					memcpy( color, &message[i + 13], 7 );
					g_string_append( mx, color );
				}
				else if ( purple_str_has_prefix( &message[i], "</font>" ) ) {
					/* end of font tag */
					entry = g_list_last( tagstack );
					if ( entry ) {
						tag = entry->data;
						if ( tag->type == MXIT_TAG_COLOR ) {
							/* font color reset */
							g_string_append( mx, "#??????" );
						}
						else if ( tag->type == MXIT_TAG_SIZE ) {
							/* font size */
							// TODO: implement size control
						}
						tagstack = g_list_remove( tagstack, tag );
						g_free( tag );
					}
				}
				else if ( purple_str_has_prefix( &message[i], "<IMG ID=" ) ) {
					/* inline image */
					int imgid;

					if ( sscanf( &message[i+9], "%i", &imgid ) ) {
						inline_image_add( mx, imgid );
						*msgtype = CP_MSGTYPE_COMMAND;		/* inline image must be sent as a MXit command */
					}
				}

				/* skip to end of tag ('>') */
				for ( i++; ( i < len ) && ( message[i] != '>' ) ; i++ );

				break;

			case '*' :	/* MXit bold */
			case '_' :	/* MXit underline */
			case '/' :	/* MXit italic */
			case '#' :	/* MXit font color */
			case '$' :	/* MXit highlight text */
			case '\\' :	/* MXit escape backslash */
				g_string_append( mx, "\\" );				/* escape character */
				g_string_append_c( mx, message[i] );		/* character to escape */
				break;

			default:
				g_string_append_c( mx, message[i] );
				break;
		}
	}

	/* unescape HTML entities to their literal characters (reference: "libpurple/utils.h") */
	reply = purple_unescape_html( mx->str );

	g_string_free( mx, TRUE );

#ifdef MXIT_DEBUG_MARKUP
	purple_debug_info( MXIT_PLUGIN_ID, "Markup TX (converted): '%s'\n", reply );
#endif

	return reply;
}


/*------------------------------------------------------------------------
 * Free an emoticon entry.
 *
 *  @param key				MXit emoticon ID
 *  @param value			Imagestore ID for emoticon
 *  @param user_data		NULL (unused)
 *  @return					TRUE
 */
static gboolean emoticon_entry_free( gpointer key, gpointer value, gpointer user_data )
{
	int* imgid = value;

	/* key is a string */
	g_free( key );

	/* value is 'id' in imagestore */
	purple_imgstore_unref_by_id( *imgid );
	g_free( value );

	return TRUE;
}


/*------------------------------------------------------------------------
 * Free all entries in the emoticon cache.
 *
 *  @param session			The MXit session object
 */
void mxit_free_emoticon_cache( struct MXitSession* session )
{
	g_hash_table_foreach_remove( session->iimages, emoticon_entry_free, NULL );
	g_hash_table_destroy ( session->iimages );
}