Bug 954603 - Add a JavaScript implementation of the XMPP protocol; use it for Google Talk and Facebook Chat. Based on the code developed by Varuna JAYASIRI for Google Summer of Code 2011. Partial reviews by Mook, clokep, aleth, Mic.
authorFlorian Quèze <florian@instantbird.org>
Tue, 06 Dec 2011 19:02:39 +0100
changeset 18899 b3f560c1484bf872f0d604856781678ee7c41db8
parent 18898 88e2f1caaf2502d76d24938104bb5eea7f369a4e
child 18900 e6e65ceac1df0d672baf375659f001ca75953dc2
push id181
push usermbanner@mozilla.com
push dateMon, 28 Apr 2014 08:56:28 +0000
bugs954603
Bug 954603 - Add a JavaScript implementation of the XMPP protocol; use it for Google Talk and Facebook Chat. Based on the code developed by Varuna JAYASIRI for Google Summer of Code 2011. Partial reviews by Mook, clokep, aleth, Mic.
chat/Makefile.in
chat/components/public/imIAccount.idl
chat/components/src/imAccounts.js
chat/components/src/imCore.js
chat/locales/en-US/facebook.properties
chat/locales/en-US/xmpp.properties
chat/locales/jar.mn
chat/makefiles.sh
chat/modules/imXPCOMUtils.jsm
chat/modules/jsProtoHelper.jsm
chat/modules/socket.jsm
chat/protocols/facebook/Makefile.in
chat/protocols/facebook/facebook.js
chat/protocols/facebook/facebook.manifest
chat/protocols/facebook/icons/prpl-facebook-32.png
chat/protocols/facebook/icons/prpl-facebook-48.png
chat/protocols/facebook/icons/prpl-facebook.png
chat/protocols/facebook/jar.mn
chat/protocols/gtalk/Makefile.in
chat/protocols/gtalk/gtalk.js
chat/protocols/gtalk/gtalk.manifest
chat/protocols/gtalk/icons/prpl-gtalk-32.png
chat/protocols/gtalk/icons/prpl-gtalk-48.png
chat/protocols/gtalk/icons/prpl-gtalk.png
chat/protocols/gtalk/jar.mn
chat/protocols/jsTest/jsTestProtocol.js
chat/protocols/overrides/Makefile.in
chat/protocols/overrides/facebookOverrideProtocol.js
chat/protocols/overrides/gtalkOverrideProtocol.js
chat/protocols/overrides/icons/prpl-facebook-32.png
chat/protocols/overrides/icons/prpl-facebook-48.png
chat/protocols/overrides/icons/prpl-facebook.png
chat/protocols/overrides/icons/prpl-gtalk-32.png
chat/protocols/overrides/icons/prpl-gtalk-48.png
chat/protocols/overrides/icons/prpl-gtalk.png
chat/protocols/overrides/jar.mn
chat/protocols/overrides/overrideProtocols.manifest
chat/protocols/twitter/twitter.js
chat/protocols/xmpp/Makefile.in
chat/protocols/xmpp/xmpp-authmechs.jsm
chat/protocols/xmpp/xmpp-session.jsm
chat/protocols/xmpp/xmpp-xml.jsm
chat/protocols/xmpp/xmpp.js
chat/protocols/xmpp/xmpp.jsm
chat/protocols/xmpp/xmpp.manifest
im/content/account.js
im/content/accountWizard.js
im/content/buddytooltip.xml
im/content/conversation.xml
im/content/joinchat.js
im/installer/package-manifest.in
purple/purplexpcom/public/purpleIPref.idl
--- a/chat/Makefile.in
+++ b/chat/Makefile.in
@@ -36,17 +36,22 @@
 
 DEPTH		= ..
 topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
-PROTOCOLS = twitter overrides
+PROTOCOLS = \
+		facebook \
+		gtalk \
+		twitter \
+		xmpp \
+		$(NULL)
 
 ifdef MOZ_DEBUG
 PROTOCOLS += jsTest
 endif
 
 PARALLEL_DIRS	= \
 		components/public \
 		components/src \
--- a/chat/components/public/imIAccount.idl
+++ b/chat/components/public/imIAccount.idl
@@ -158,16 +158,20 @@ interface prplIAccount: nsISupports {
   const short ERROR_OTHER_ERROR = 16;
 
   /* From PurpleConnectionFlags */
 
   //   PURPLE_CONNECTION_HTML
   //    Connection sends/receives in 'HTML'.
   readonly attribute boolean HTMLEnabled;
 
+  // libpurple expects messages to be HTML escaped even when HTML
+  // isn't enabled. Our js-prpls most likely don't want that behavior.
+  readonly attribute boolean HTMLEscapePlainText;
+
   //   PURPLE_CONNECTION_NO_BGCOLOR
   //    Connection does not send/receive background colors.
   readonly attribute boolean noBackgroundColors;
 
   //   PURPLE_CONNECTION_AUTO_RESP
   //    Send auto responses when away.
   readonly attribute boolean autoResponses;
 
--- a/chat/components/src/imAccounts.js
+++ b/chat/components/src/imAccounts.js
@@ -472,16 +472,17 @@ imAccount.prototype = {
     this.prefBranch.setCharPref(kAccountOptionPrefPrefix + aName, aVal);
     if (this.prplAccount)
       this.prplAccount.setString(aName, aVal);
     SavePrefTimer.initTimer();
   },
   save: function() { SavePrefTimer.saveNow(); },
 
   get HTMLEnabled() this._ensurePrplAccount.HTMLEnabled,
+  get HTMLEscapePlainText() this._ensurePrplAccount.HTMLEscapePlainText,
   get noBackgroundColors() this._ensurePrplAccount.noBackgroundColors,
   get autoResponses() this._ensurePrplAccount.autoResponses,
   get singleFormatting() this._ensurePrplAccount.singleFormatting,
   get noNewlines() this._ensurePrplAccount.noNewlines,
   get noFontSizes() this._ensurePrplAccount.noFontSizes,
   get noUrlDesc() this._ensurePrplAccount.noUrlDesc,
   get noImages() this._ensurePrplAccount.noImages,
   get maxMessageLength() this._ensurePrplAccount.maxMessageLength,
--- a/chat/components/src/imCore.js
+++ b/chat/components/src/imCore.js
@@ -153,17 +153,17 @@ UserStatus.prototype = {
   set idleTime(aIdleTime) {
     this._idleTime = aIdleTime;
     this._notifyObservers("idle-time-changed", aIdleTime);
   },
   _idle: false,
   _idleStatusText: "",
   _idleStatusType: Ci.imIStatusInfo.STATUS_AVAILABLE,
   _checkIdle: function() {
-    let idleTime = this._idleService.idleTime / 1000;
+    let idleTime = Math.floor(this._idleService.idleTime / 1000);
     let idle = this._timeBeforeIdle && idleTime >= this._timeBeforeIdle;
     if (idle == this._idle)
       return;
 
     let statusType = this.statusType;
     let statusText = this.statusText;
     this._idle = idle;
     if (idle) {
@@ -202,17 +202,17 @@ UserStatus.prototype = {
   setUserIcon: function(aIconFile) {
     let folder = this._getProfileDir();
 
     let newName = "";
     if (aIconFile) {
       // Get the extension (remove trailing dots - invalid Windows extension).
       let ext = aIconFile.leafName.replace(/.*(\.[a-z0-9]+)\.*/i, "$1");
       // newName = userIcon-<timestamp(now)>.<aIconFile.extension>
-      newName = "userIcon-" + (Date.now() / 1000) + ext;
+      newName = "userIcon-" + Math.floor(Date.now() / 1000) + ext;
 
       // Copy the new icon file to newName in the profile folder.
       aIconFile.copyTo(folder, newName);
     }
 
     // Get the previous file name before saving the new file name.
     let oldFileName = Services.prefs.getCharPref(kPrefUserIconFilename);
     Services.prefs.setCharPref(kPrefUserIconFilename, newName);
@@ -348,22 +348,30 @@ CoreService.prototype = {
       return null; // no protocol registered for this id.
     }
 
     let proto = null;
     try {
       proto = Cc[cid].createInstance(Ci.prplIProtocol);
     } catch (e) {
       // This is a real error, the protocol is registered and failed to init.
-      Cu.reportError(e);
+      let error = "failed to create an instance of " + cid + ": " + e;
+      dump(error + "\n");
+      Cu.reportError(error);
     }
     if (!proto)
       return null;
 
-    proto.init(aPrplId);
+    try {
+      proto.init(aPrplId);
+    } catch (e) {
+      Cu.reportError(e);
+      return null;
+    }
+
     this._protos[aPrplId] = proto;
     return proto;
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.imICoreService]),
   classDescription: "Core",
   classID: Components.ID("{073f5953-853c-4a38-bd81-255510c31c2e}"),
   contractID: "@instantbird.org/purple/core-service;1"
new file mode 100644
--- /dev/null
+++ b/chat/locales/en-US/facebook.properties
@@ -0,0 +1,1 @@
+connection.error.useUsernameNotEmailAddress=Please use your Facebook username, not an email address
new file mode 100644
--- /dev/null
+++ b/chat/locales/en-US/xmpp.properties
@@ -0,0 +1,66 @@
+# LOCALIZATION NOTE (connection.*)
+#   These will be displayed in the account manager in order to show the progress
+#   of the connection.
+#   (These will be displayed in account.connection.progress from
+#    accounts.properties, which adds … at the end, so do not include
+#    periods at the end of these messages.)
+connection.initializingStream=Initializing stream
+connection.initializingEncryption=Initializing encryption
+connection.authenticating=Authenticating
+connection.gettingResource=Getting resource
+connection.downloadingRoster=Downloading contact list
+
+# LOCALIZATION NOTE (connection.error.*)
+#   These will show in the account manager if an error occurs during the
+#   connection attempt.
+connection.error.failedToCreateASocket=Failed to create a socket (offline?)
+connection.error.serverClosedConnection=The server closed the connection
+connection.error.resetByPeer=Connection reset by peer
+connection.error.timedOut=The connection timed out
+connection.error.receivedUnexpectedData=Received unexpected data
+connection.error.incorrectResponse=Received an incorrect response
+connection.error.startTLSRequired=The server requires encryption but you disabled it
+connection.error.startTLSNotSupported=The server doesn't support encryption but your configuration requires it
+connection.error.failedToStartTLS=Failed to start encryption
+connection.error.noAuthMec=No authentication mechanism offered by the server
+connection.error.noCompatibleAuthMec=None of the authentication mechanisms offered by the server is supported
+connection.error.notSendingPasswordInClear=The server only supports authentication by sending the password in clear
+connection.error.authenticationFailure=Authentication failure
+connection.error.notAuthorized=Not authorized (wrong password?)
+connection.error.failedToGetAResource=Failed to get a resource
+
+# LOCALIZATION NOTE (tooltip.*):
+#   These are the titles of lines of information that will appear in
+#   the tooltip showing details about a contact or conversation.
+# LOCALIZATION NOTE (tooltip.status):
+#   %S will be replaced by the XMPP resource identifier
+tooltip.status=Status (%S)
+tooltip.statusNoResource=Status
+tooltip.subscription=Subscription
+
+# LOCALIZATION NOTE (chatRoomField.*):
+#   These are the name of fields displayed in the 'Join Chat' dialog
+#   for XMPP accounts.
+#   The _ character won't be displayed; it indicates the next
+#   character of the string should be used as the access key for this
+#   field.
+chatRoomField.room=_Room
+chatRoomField.server=_Server
+chatRoomField.nick=_Nick
+chatRoomField.password=_Password
+
+# LOCALIZATION NOTE
+#  Buddies that aren't in any group on the server will appear in this group.
+#  Try to use the same translation as for defaultGroup in instantbird.properties
+defaultGroup=Contacts
+
+# LOCALIZATION NOTE (options.*):
+#   These are the protocol specific options shown in the account manager and
+#   account wizard windows.
+options.resource=Resource
+options.connectionSecurity=Connection security
+options.connectionSecurity.requireEncryption=Require encryption
+options.connectionSecurity.opportunisticTLS=Use encryption if available
+options.connectionSecurity.allowUnencryptedAuth=Accept sending the password unencrypted
+options.connectServer=Server
+options.connectPort=Port
--- a/chat/locales/jar.mn
+++ b/chat/locales/jar.mn
@@ -1,8 +1,10 @@
 #filter substitution
 
 @AB_CD@.jar:
 % locale purple @AB_CD@ %locale/@AB_CD@/purple/
 	locale/@AB_CD@/purple/commands.properties (%commands.properties)
 	locale/@AB_CD@/purple/conversations.properties (%conversations.properties)
+	locale/@AB_CD@/purple/facebook.properties	(%facebook.properties)
 	locale/@AB_CD@/purple/status.properties (%status.properties)
 	locale/@AB_CD@/purple/twitter.properties	(%twitter.properties)
+	locale/@AB_CD@/purple/xmpp.properties	(%xmpp.properties)
--- a/chat/makefiles.sh
+++ b/chat/makefiles.sh
@@ -36,13 +36,15 @@
 
 add_makefiles "
 chat/Makefile
 chat/components/public/Makefile
 chat/components/src/Makefile
 chat/content/Makefile
 chat/locales/Makefile
 chat/modules/Makefile
+chat/protocols/facebook/Makefile
+chat/protocols/gtalk/Makefile
 chat/protocols/jsTest/Makefile
-chat/protocols/overrides/Makefile
 chat/protocols/twitter/Makefile
+chat/protocols/xmpp/Makefile
 chat/themes/Makefile
 "
--- a/chat/modules/imXPCOMUtils.jsm
+++ b/chat/modules/imXPCOMUtils.jsm
@@ -60,16 +60,18 @@ const DEBUG_WARNING = 3;
 const DEBUG_ERROR = 4;
 
 function scriptError(aModule, aLevel, aMessage) {
   // Only continue if we want to see this level of logging.
   let logLevel = Services.prefs.getIntPref("purple.debug.loglevel");
   if (logLevel > aLevel)
     return;
 
+  dump(aModule + ": " + aMessage + "\n");
+
   // Log a debug statement.
   if (aLevel == DEBUG_INFO && logLevel == DEBUG_INFO) {
     Services.console.logStringMessage(aMessage);
     return;
   }
 
   let flag = Ci.nsIScriptError.warningFlag;
   if (aLevel >= DEBUG_ERROR)
@@ -164,21 +166,27 @@ ClassInfo.prototype = {
   implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
   flags: 0
 };
 
 function l10nHelper(aChromeURL)
 {
   let bundle = Services.strings.createBundle(aChromeURL);
   return function (aStringId) {
-    if (arguments.length == 1)
-      return bundle.GetStringFromName(aStringId);
-    return bundle.formatStringFromName(aStringId,
-                                       Array.prototype.slice.call(arguments, 1),
-                                       arguments.length - 1);
+    try {
+      if (arguments.length == 1)
+        return bundle.GetStringFromName(aStringId);
+      return bundle.formatStringFromName(aStringId,
+                                         Array.prototype.slice.call(arguments, 1),
+                                         arguments.length - 1);
+    } catch (e) {
+      Cu.reportError(e);
+      dump("Failed to get " + aStringId + "\n");
+      return aStringId;
+    }
   };
 }
 
 /**
  * Constructs an nsISimpleEnumerator for the given array of items.
  * Copied from netwerk/test/httpserver/httpd.js
  *
  * @param items : Array
--- a/chat/modules/jsProtoHelper.jsm
+++ b/chat/modules/jsProtoHelper.jsm
@@ -193,17 +193,18 @@ const GenericAccountPrototype = {
   get prefs() this._prefs ||
     (this._prefs = Services.prefs.getBranch("messenger.account." +
                                             this.imAccount.id + ".options.")),
 
   get normalizedName() normalize(this.name),
   get proxyInfo() { throw Cr.NS_ERROR_NOT_IMPLEMENTED; },
   set proxyInfo(val) { throw Cr.NS_ERROR_NOT_IMPLEMENTED; },
 
-  get HTMLEnabled() true,
+  get HTMLEnabled() false,
+  get HTMLEscapePlainText() false,
   get noBackgroundColors() true,
   get autoResponses() false,
   get singleFormatting() false,
   get noNewlines() false,
   get noFontSizes() false,
   get noUrlDesc() false,
   get noImages() true,
   get maxMessageLength() 0
@@ -212,22 +213,27 @@ const GenericAccountPrototype = {
 
 const GenericAccountBuddyPrototype = {
   __proto__: ClassInfo("imIAccountBuddy", "generic account buddy object"),
   _init: function(aAccount, aBuddy, aTag, aUserName) {
     if (!aBuddy && !aUserName)
       throw "aUserName is required when aBuddy is null";
 
     this._tag = aTag;
-    this._account = aAccount.imAccount;
+    this._account = aAccount;
     this._buddy = aBuddy;
+    if (aBuddy) {
+      let displayName = aBuddy.displayName;
+      if (displayName != aUserName)
+        this._serverAlias = displayName;
+    }
     this._userName = aUserName;
   },
 
-  get account() this._account,
+  get account() this._account.imAccount,
   set buddy(aBuddy) {
     if (this._buddy)
       throw Cr.NS_ERROR_ALREADY_INITIALIZED;
     this._buddy = aBuddy;
   },
   get buddy() this._buddy,
   get tag() this._tag,
   set tag(aNewTag) {
@@ -379,17 +385,17 @@ function Message(aWho, aMessage, aObject
 Message.prototype = GenericMessagePrototype;
 
 
 const GenericConversationPrototype = {
   __proto__: ClassInfo("purpleIConversation", "generic conversation object"),
   flags: Ci.nsIClassInfo.DOM_OBJECT,
 
   _init: function(aAccount, aName) {
-    this.account = aAccount.imAccount;
+    this._account = aAccount;
     this._name = aName;
     this._observers = [];
     Services.conversations.addConversation(this);
   },
 
   _id: 0,
   get id() this._id,
   set id(aId) {
@@ -420,20 +426,20 @@ const GenericConversationPrototype = {
     Services.conversations.removeConversation(this);
   },
   unInit: function() { },
 
   writeMessage: function(aWho, aText, aProperties) {
     (new Message(aWho, aText, aProperties)).conversation = this;
   },
 
+  get account() this._account.imAccount,
   get name() this._name,
   get normalizedName() normalize(this.name),
-  get title() this.name,
-  account: null
+  get title() this.name
 };
 
 const GenericConvIMPrototype = {
   __proto__: GenericConversationPrototype,
   _interfaces: [Ci.purpleIConversation, Ci.purpleIConvIM],
   classDescription: "generic ConvIM object",
 
   updateTyping: function(aState) {
@@ -526,41 +532,73 @@ function TooltipInfo(aLabel, aValue)
     else {
       this.type = Ci.purpleITooltipInfo.pair;
       this.value = aValue;
     }
   }
 }
 TooltipInfo.prototype = ClassInfo("purpleITooltipInfo", "generic tooltip info");
 
-function purplePref(aName, aLabel, aType, aDefaultValue, aMasked) {
+/* aOption is an object containing:
+ *  - label: localized text to display (recommended: use a getter with _)
+ *  - default: the default value for this option. The type of the
+ *      option will be determined based on the type of the default value.
+ *      If the default value is a string, the option will be of type
+ *      list if listValues has been provided. In that case the default
+ *      value should be one of the listed values.
+ *  - [optional] listValues: only if this option can only take a list of
+ *      predefined values. This is an object of the form:
+ *        {value1: localizedLabel, value2: ...}.
+ *  - [optional] masked: boolean, if true the UI shouldn't display the value.
+ *      This could typically be used for password field.
+ *      Warning: The UI currently doesn't support this.
+ */
+function purplePref(aName, aOption) {
   this.name = aName; // Preference name
-  this.label = aLabel; // Text to display
-  this.type = aType;
-  this._defaultValue = aDefaultValue;
-  this.masked = !!aMasked; // Obscured from view, ensure boolean
+  this.label = aOption.label; // Text to display
+
+  if (aOption.default === undefined || aOption.default === null)
+    throw "A default value for the option is required to determine its type.";
+  this._defaultValue = aOption.default;
+
+  const types = {boolean: "Bool", string: "String", number: "Int"};
+  let type = types[typeof aOption.default];
+  if (!type)
+    throw "Invalid option type";
+
+  if (type == "String" && ("listValues" in aOption)) {
+    type = "List";
+    this._listValues = aOption.listValues;
+  }
+  this.type = Ci.purpleIPref["type" + type];
+
+  if ("masked" in aOption && aOption.masked)
+    this.masked = true;
 }
 purplePref.prototype = {
   __proto__: ClassInfo("purpleIPref", "generic account option preference"),
 
+  masked: false,
+
   // Default value
   getBool: function() this._defaultValue,
   getInt: function() this._defaultValue,
   getString: function() this._defaultValue,
   getList: function() {
     // Convert a JavaScript object map {"value 1": "label 1", ...}
-    let keys = Object.keys(this._defaultValue);
+    let keys = Object.keys(this._listValues);
     if (!keys.length)
       return EmptyEnumerator;
 
     return new nsSimpleEnumerator(
       keys.map(function(key) new purpleKeyValuePair(this[key], key),
-               this._defaultValue)
+               this._listValues)
     );
-  }
+  },
+  getListDefault: function() this._defaultValue
 };
 
 function purpleKeyValuePair(aName, aValue) {
   this.name = aName;
   this.value = aValue;
 }
 purpleKeyValuePair.prototype =
   ClassInfo("purpleIKeyValuePair", "generic Key Value Pair");
@@ -627,29 +665,19 @@ const GenericProtocolPrototype = {
     if (this.options && this.options.hasOwnProperty(aName))
       return this.options[aName].default;
     throw aName + " has no default value in " + this.id + ".";
   },
   getOptions: function() {
     if (!this.options)
       return EmptyEnumerator;
 
-    const types =
-      {boolean: "Bool", string: "String", number: "Int", object: "List"};
-
     let purplePrefs = [];
-    for (let optionName in this.options) {
-      let option = this.options[optionName];
-      if (!((typeof option.default) in types))
-        throw "Invalid type for preference: " + optionName + ".";
-
-      let type = Ci.purpleIPref["type" + types[typeof option.default]];
-      purplePrefs.push(new purplePref(optionName, option.label, type,
-                                      option.default, option.masked));
-    }
+    for (let [name, option] in Iterator(this.options))
+      purplePrefs.push(new purplePref(name, option));
     return new nsSimpleEnumerator(purplePrefs);
   },
   getUsernameSplit: function() {
     if (!this.usernameSplits || !this.usernameSplits.length)
       return EmptyEnumerator;
 
     return new nsSimpleEnumerator(
       this.usernameSplits.map(function(split) new UsernameSplit(split)));
--- a/chat/modules/socket.jsm
+++ b/chat/modules/socket.jsm
@@ -108,16 +108,19 @@ const ScriptableInputStream =
   Components.Constructor("@mozilla.org/scriptableinputstream;1",
                          "nsIScriptableInputStream", "init");
 const ServerSocket =
   Components.Constructor("@mozilla.org/network/server-socket;1",
                          "nsIServerSocket", "init");
 const InputStreamPump =
   Components.Constructor("@mozilla.org/network/input-stream-pump;1",
                          "nsIInputStreamPump", "init");
+const ScriptableUnicodeConverter =
+  Components.Constructor("@mozilla.org/intl/scriptableunicodeconverter",
+                         "nsIScriptableUnicodeConverter");
 
 const Socket = {
   // Use this to use binary mode for the
   binaryMode: false,
 
   // Set this for non-binary mode to automatically parse the stream into chunks
   // separated by delimiter.
   delimiter: "",
@@ -233,16 +236,29 @@ const Socket = {
     try {
       this._outputStream.write(aData + this.delimiter,
                                aData.length + this.delimiter.length);
     } catch(e) {
       Cu.reportError(e);
     }
   },
 
+  sendString: function(aString, aEncoding) {
+    this.log("Sending:\n" + aString + "\n");
+
+    let converter = new ScriptableUnicodeConverter();
+    converter.charset = aEncoding || "UTF-8";
+    try {
+      let stream = converter.convertToInputStream(aString);
+      this._outputStream.writeFrom(stream, stream.available());
+    } catch(e) {
+      Cu.reportError(e);
+    }
+  },
+
   sendBinaryData: function(/* ArrayBuffer */ aData) {
     this.log("Sending binary data data: <" + aData + ">");
 
     let uint8 = Uint8Array(aData);
 
     // Since there doesn't seem to be a uint8.get() method for the byte array
     let byteArray = [];
     for (let i = 0; i < uint8.byteLength; i++)
@@ -372,16 +388,38 @@ const Socket = {
   notifyCertProblem: function(aSocketInfo, aStatus, aTargetSite) true,
 
   /*
    * nsISSLErrorListener
    */
   notifySSLError: function(aSocketInfo, aError, aTargetSite) true,
 
   /*
+   * nsITransportEventSink methods
+   */
+  onTransportStatus: function(aTransport, aStatus, aProgress, aProgressmax) {
+    const nsITransportEventSinkStatus = {
+         0x804b0003: "STATUS_RESOLVING",
+         0x804b000b: "STATUS_RESOLVED",
+         0x804b0007: "STATUS_CONNECTING_TO",
+         0x804b0004: "STATUS_CONNECTED_TO",
+         0x804b0005: "STATUS_SENDING_TO",
+         0x804b000a: "STATUS_WAITING_FOR",
+         0x804b0006: "STATUS_RECEIVING_FROM"
+    };
+    let status = nsITransportEventSinkStatus[aStatus];
+    this.log("onTransportStatus(" + (status || ("0x" + aStatus.toString(16))) +")");
+
+    if (status == "STATUS_CONNECTED_TO") {
+      // Notify that the connection has been established.
+      this.onConnection();
+    }
+  },
+
+  /*
    *****************************************************************************
    ****************************** Private methods ******************************
    *****************************************************************************
    */
   _resetBuffers: function() {
     this._incomingDataBuffer = this.binaryMode ? [] : "";
     this._outgoingDataBuffer = [];
   },
@@ -397,17 +435,17 @@ const Socket = {
                       .getService(Ci.nsISocketTransportService);
     this.transport = socketTS.createTransport(this.security,
                                               this.security.length, this.host,
                                               this.port, this.proxy);
 
     this._openStreams();
   },
 
-  // Open the incoming and outgoing sockets.
+  // Open the incoming and outgoing streams, and init the nsISocketTransport.
   _openStreams: function() {
     // Security notification callbacks (must support nsIBadCertListener2 and
     // nsISSLErrorListener for SSL connections, and possibly other interfaces).
     this.transport.securityCallbacks = this;
 
     // Set the timeouts for the nsISocketTransport for both a connect event and
     // a read/write. Only set them if the user has provided them.
     if (this.connectTimeout) {
@@ -446,19 +484,16 @@ const Socket = {
 
     this.pump = new InputStreamPump(this._inputStream, // Data to read
                                     -1, // Current offset
                                     -1, // Read all data
                                     0, // Use default segment size
                                     0, // Use default segment length
                                     false); // Do not close when done
     this.pump.asyncRead(this, this);
-
-    // Notify that the connection has been established.
-    this.onConnection();
   },
 
   /*
    *****************************************************************************
    ********************* Methods for subtypes to override **********************
    *****************************************************************************
    */
   log: function(aString) { },
@@ -474,21 +509,17 @@ const Socket = {
   onConnectionClosed: function() { },
 
   // Called when ASCII data is available.
   onDataReceived: function(/* string */ aData) { },
 
   // Called when binary data is available.
   onBinaryDataReceived: function(/* ArrayBuffer */ aData) { },
 
-  /*
-   * nsITransportEventSink methods
-   */
-  onTransportStatus: function(aTransport, aStatus, aProgress, aProgressmax) { },
-
+  /* QueryInterface and nsIInterfaceRequestor implementations */
   _interfaces: [Ci.nsIServerSocketListener, Ci.nsIStreamListener,
                 Ci.nsIRequestObserver, Ci.nsITransportEventSink,
                 Ci.nsIBadCertListener2, Ci.nsISSLErrorListener,
                 Ci.nsIProtocolProxyCallback],
   QueryInterface: function(iid) {
     if (iid.equals(Ci.nsISupports) ||
         this._interfaces.some(function(i) i.equals(iid)))
       return this;
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/Makefile.in
@@ -0,0 +1,49 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Florian Quèze <florian@queze.net>.
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH		= ../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+EXTRA_COMPONENTS = \
+		facebook.js \
+		facebook.manifest \
+		$(NULL)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/facebook.js
@@ -0,0 +1,85 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Instantbird.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian Quèze <florian@queze.net>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const {interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/jsProtoHelper.jsm");
+Cu.import("resource:///modules/xmpp.jsm");
+Cu.import("resource:///modules/xmpp-session.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", function()
+  l10nHelper("chrome://purple/locale/facebook.properties")
+);
+
+function FacebookAccount(aProtoInstance, aImAccount) {
+  this._init(aProtoInstance, aImAccount);
+}
+FacebookAccount.prototype = {
+  __proto__: XMPPAccountPrototype,
+  get canJoinChat() false,
+  connect: function() {
+    if (this.name.indexOf("@") == -1)
+      this._jid = this._parseJID(this.name + "@chat.facebook.com/instantbird");
+    else {
+      this._jid = this._parseJID(this.name);
+      if (this._jid.domain != "chat.facebook.com") {
+        // We can't use this.onError because this._connection doesn't exist.
+        this.reportDisconnecting(Ci.prplIAccount.ERROR_INVALID_USERNAME,
+                                 _("connection.error.useUsernameNotEmailAddress"));
+        this.reportDisconnected();
+        return;
+      }
+    }
+
+    this._connection = new XMPPSession("chat.facebook.com", 5222,
+                                       "opportunistic_tls", this._jid,
+                                       this.imAccount.password, this);
+  }
+};
+
+function FacebookProtocol() {
+}
+FacebookProtocol.prototype = {
+  __proto__: GenericProtocolPrototype,
+  get normalizedName() "facebook",
+  get name() "Facebook Chat",
+  get iconBaseURI() "chrome://prpl-facebook/skin/",
+  getAccount: function(aImAccount) new FacebookAccount(this, aImAccount),
+  classID: Components.ID("{1d1d0bc5-610c-472f-b2cb-4b89857d80dc}")
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([FacebookProtocol]);
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/facebook.manifest
@@ -0,0 +1,3 @@
+component {1d1d0bc5-610c-472f-b2cb-4b89857d80dc} facebook.js
+contract @instantbird.org/purple/facebook;1 {1d1d0bc5-610c-472f-b2cb-4b89857d80dc}
+category im-protocol-plugin prpl-facebook @instantbird.org/purple/facebook;1
rename from chat/protocols/overrides/icons/prpl-facebook-32.png
rename to chat/protocols/facebook/icons/prpl-facebook-32.png
rename from chat/protocols/overrides/icons/prpl-facebook-48.png
rename to chat/protocols/facebook/icons/prpl-facebook-48.png
rename from chat/protocols/overrides/icons/prpl-facebook.png
rename to chat/protocols/facebook/icons/prpl-facebook.png
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/jar.mn
@@ -0,0 +1,5 @@
+instantbird.jar:
+% skin prpl-facebook classic/1.0 %skin/classic/prpl/facebook/
+	skin/classic/prpl/facebook/icon32.png	(icons/prpl-facebook-32.png)
+	skin/classic/prpl/facebook/icon48.png	(icons/prpl-facebook-48.png)
+	skin/classic/prpl/facebook/icon.png	(icons/prpl-facebook.png)
new file mode 100644
--- /dev/null
+++ b/chat/protocols/gtalk/Makefile.in
@@ -0,0 +1,49 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Florian Quèze <florian@queze.net>.
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH		= ../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+EXTRA_COMPONENTS = \
+		gtalk.js \
+		gtalk.manifest \
+		$(NULL)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/chat/protocols/gtalk/gtalk.js
@@ -0,0 +1,92 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Instantbird.
+ *
+ * The Initial Developer of the Original Code is
+ * Varuna JAYASIRI <vpjayasiri@gmail.com>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Florian Quèze <florian@queze.net>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cu = Components.utils;
+
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/jsProtoHelper.jsm");
+Cu.import("resource:///modules/xmpp.jsm");
+Cu.import("resource:///modules/xmpp-session.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", function()
+  l10nHelper("chrome://purple/locale/xmpp.properties")
+);
+
+function GTalkAccount(aProtoInstance, aImAccount) {
+  this._init(aProtoInstance, aImAccount);
+}
+GTalkAccount.prototype = {
+  __proto__: XMPPAccountPrototype,
+  connect: function() {
+    this._jid = this._parseJID(this.name);
+
+    // For the resource, if the user has edited the option to a non
+    // empty value, use that.
+    if (this.prefs.prefHasUserValue("resource")) {
+      let resource = this.getString("resource");
+      if (resource)
+        this._jid.resource = resource;
+    }
+    // Otherwise, if the username doesn't contain a resource, use the
+    // value of the resource option (it will be the default value).
+    // If we set an empty resource, XMPPSession will fallback to "instantbird"
+    if (!this._jid.resource)
+      this._jid.resource = this.getString("resource");
+
+    this._connection =
+      new XMPPSession("talk.google.com", 443,
+                      "require_tls", this._jid,
+                      this.imAccount.password, this);
+  }
+};
+
+function GTalkProtocol() {
+}
+GTalkProtocol.prototype = {
+  __proto__: GenericProtocolPrototype,
+  get normalizedName() "gtalk",
+  get name() "Google Talk",
+  get iconBaseURI() "chrome://prpl-gtalk/skin/",
+  getAccount: function(aImAccount) new GTalkAccount(this, aImAccount),
+  options: {
+    resource: {get label() _("options.resource"), default: "instantbird"}
+  },
+  get chatHasTopic() true,
+  classID: Components.ID("{38a224c1-6748-49a9-8ab2-efc362b1000d}")
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([GTalkProtocol]);
new file mode 100644
--- /dev/null
+++ b/chat/protocols/gtalk/gtalk.manifest
@@ -0,0 +1,3 @@
+component {38a224c1-6748-49a9-8ab2-efc362b1000d} gtalk.js
+contract @instantbird.org/purple/gtalk;1 {38a224c1-6748-49a9-8ab2-efc362b1000d}
+category im-protocol-plugin prpl-gtalk @instantbird.org/purple/gtalk;1
rename from chat/protocols/overrides/icons/prpl-gtalk-32.png
rename to chat/protocols/gtalk/icons/prpl-gtalk-32.png
rename from chat/protocols/overrides/icons/prpl-gtalk-48.png
rename to chat/protocols/gtalk/icons/prpl-gtalk-48.png
rename from chat/protocols/overrides/icons/prpl-gtalk.png
rename to chat/protocols/gtalk/icons/prpl-gtalk.png
new file mode 100644
--- /dev/null
+++ b/chat/protocols/gtalk/jar.mn
@@ -0,0 +1,5 @@
+instantbird.jar:
+% skin prpl-gtalk classic/1.0 %skin/classic/prpl/gtalk/
+	skin/classic/prpl/gtalk/icon32.png	(icons/prpl-gtalk-32.png)
+	skin/classic/prpl/gtalk/icon48.png	(icons/prpl-gtalk-48.png)
+	skin/classic/prpl/gtalk/icon.png	(icons/prpl-gtalk.png)
--- a/chat/protocols/jsTest/jsTestProtocol.js
+++ b/chat/protocols/jsTest/jsTestProtocol.js
@@ -106,18 +106,20 @@ Account.prototype.__proto__ = GenericAcc
 
 function jsTestProtocol() { }
 jsTestProtocol.prototype = {
   get name() "JS Test",
   options: {
     "text": {label: "Text option",    default: "foo"},
     "bool": {label: "Boolean option", default: true},
     "int" : {label: "Integer option", default: 42},
-    "list": {label: "Select option",  default: {"option1": "Default option",
-                                                "option2": "Other option"}}
+    "list": {label: "Select option",  default: "option2",
+             listValues: {"option1": "First option",
+                          "option2": "Default option",
+                          "option3": "Other option"}}
   },
   usernameSplits: [
     {label: "Server", separator: "@", defaultValue: "default.server",
      reverse: true}
   ],
   getAccount: function(aImAccount) new Account(this, aImAccount),
   classID: Components.ID("{a0774c5a-4aea-458b-9fbc-8d3cbf1a4630}"),
 };
deleted file mode 100644
--- a/chat/protocols/overrides/Makefile.in
+++ /dev/null
@@ -1,50 +0,0 @@
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is mozilla.org code.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either of the GNU General Public License Version 2 or later (the "GPL"),
-# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
-
-DEPTH		= ../../..
-topsrcdir	= @top_srcdir@
-srcdir		= @srcdir@
-VPATH		= @srcdir@
-
-include $(DEPTH)/config/autoconf.mk
-
-EXTRA_COMPONENTS = \
-		gtalkOverrideProtocol.js \
-		facebookOverrideProtocol.js \
-		overrideProtocols.manifest \
-		$(NULL)
-
-include $(topsrcdir)/config/rules.mk
deleted file mode 100644
--- a/chat/protocols/overrides/facebookOverrideProtocol.js
+++ /dev/null
@@ -1,87 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is the Instantbird messenging client, released
- * 2009.
- *
- * The Initial Developer of the Original Code is
- * Florian QUEZE <florian@instantbird.org>.
- * Portions created by the Initial Developer are Copyright (C) 2009
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-const {interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource:///modules/imXPCOMUtils.jsm");
-Cu.import("resource:///modules/jsProtoHelper.jsm");
-
-function UsernameSplit(aBase, aDefaultValue)
-{
-  this.base = aBase;
-  this.defaultValue = aDefaultValue;
-}
-UsernameSplit.prototype = {
-  __proto__: ClassInfo("purpleIUsernameSplit", "username split object"),
-
-  get reverse() this.base.reverse,
-  get separator() this.base.separator,
-  get label() this.base.label
-}
-
-function facebookProtocol() { }
-facebookProtocol.prototype = {
-  __proto__: ForwardProtocolPrototype,
-  get normalizedName() "facebook",
-  get name() "Facebook Chat",
-  get iconBaseURI() "chrome://prpl-facebook/skin/",
-  get baseId() "prpl-jabber",
-
-  getAccount: function(aImAccount) {
-    let account = ForwardProtocolPrototype.getAccount.call(this, aImAccount);
-    account.__defineGetter__("canJoinChat", function() false);
-    aImAccount.setString("connection_security", "opportunistic_tls");
-    return account;
-  },
-  getOptions: function() EmptyEnumerator,
-  getUsernameSplit: function() {
-    var splits = this.base.getUsernameSplit();
-    let newSplits = [];
-    while (splits.hasMoreElements()) {
-      let split = splits.getNext();
-      if (split.defaultValue != "gmail.com")
-        newSplits.push(split);
-      else
-        newSplits.push(new UsernameSplit(split, "chat.facebook.com"));
-    }
-    return new nsSimpleEnumerator(newSplits);
-  },
-
-  classID: Components.ID("{61bc3528-df53-4481-a61a-74c3a2e8c9fd}")
-};
-
-const NSGetFactory = XPCOMUtils.generateNSGetFactory([facebookProtocol]);
deleted file mode 100644
--- a/chat/protocols/overrides/gtalkOverrideProtocol.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is the Instantbird messenging client, released
- * 2009.
- *
- * The Initial Developer of the Original Code is
- * Florian QUEZE <florian@instantbird.org>.
- * Portions created by the Initial Developer are Copyright (C) 2009
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-const Cu = Components.utils;
-
-Cu.import("resource:///modules/imXPCOMUtils.jsm");
-Cu.import("resource:///modules/jsProtoHelper.jsm");
-
-function gtalkProtocol() {
-  this.registerCommands();
-}
-gtalkProtocol.prototype = {
-  __proto__: ForwardProtocolPrototype,
-  get normalizedName() "gtalk",
-  get name() "Google Talk",
-  get iconBaseURI() "chrome://prpl-gtalk/skin/",
-  get baseId() "prpl-jabber",
-
-  getAccount: function(aImAccount) {
-    let account = ForwardProtocolPrototype.getAccount.call(this, aImAccount);
-    let connectServer =
-      /@g(oogle)?mail.com(\/|$)/.test(aImAccount.name) ? "" : "talk.google.com";
-    aImAccount.setString("connect_server", connectServer);
-    return account;
-  },
-  getOptions: function() EmptyEnumerator,
-
-  classID: Components.ID("{ad8a6454-2f5a-40c2-86ca-30062408125e}")
-};
-
-const NSGetFactory = XPCOMUtils.generateNSGetFactory([gtalkProtocol]);
deleted file mode 100644
--- a/chat/protocols/overrides/jar.mn
+++ /dev/null
@@ -1,9 +0,0 @@
-instantbird.jar:
-% skin prpl-gtalk classic/1.0 %skin/classic/prpl/gtalk/
-	skin/classic/prpl/gtalk/icon32.png	(icons/prpl-gtalk-32.png)
-	skin/classic/prpl/gtalk/icon48.png	(icons/prpl-gtalk-48.png)
-	skin/classic/prpl/gtalk/icon.png	(icons/prpl-gtalk.png)
-% skin prpl-facebook classic/1.0 %skin/classic/prpl/facebook/
-	skin/classic/prpl/facebook/icon32.png	(icons/prpl-facebook-32.png)
-	skin/classic/prpl/facebook/icon48.png	(icons/prpl-facebook-48.png)
-	skin/classic/prpl/facebook/icon.png	(icons/prpl-facebook.png)
deleted file mode 100644
--- a/chat/protocols/overrides/overrideProtocols.manifest
+++ /dev/null
@@ -1,6 +0,0 @@
-component {ad8a6454-2f5a-40c2-86ca-30062408125e} gtalkOverrideProtocol.js
-contract @instantbird.org/purple/gtalk;1 {ad8a6454-2f5a-40c2-86ca-30062408125e}
-category im-protocol-plugin prpl-gtalk @instantbird.org/purple/gtalk;1
-component {61bc3528-df53-4481-a61a-74c3a2e8c9fd} facebookOverrideProtocol.js
-contract @instantbird.org/purple/facebook;1 {61bc3528-df53-4481-a61a-74c3a2e8c9fd}
-category im-protocol-plugin prpl-facebook @instantbird.org/purple/facebook;1
--- a/chat/protocols/twitter/twitter.js
+++ b/chat/protocols/twitter/twitter.js
@@ -110,17 +110,16 @@ function Action(aLabel, aAction, aTweet)
 }
 Action.prototype = {
   __proto__: ClassInfo("purpleIMessageAction", "generic message action object"),
   get run() this._action.bind(this._tweet)
 };
 
 function Conversation(aAccount)
 {
-  this._account = aAccount;
   this._init(aAccount);
   this._ensureParticipantExists(aAccount.name);
 }
 Conversation.prototype = {
   __proto__: GenericConvChatPrototype,
   unInit: function() { delete this._account._timeline; },
   inReplyToStatusId: null,
   startReply: function(aTweet) {
@@ -323,17 +322,16 @@ function Account(aProtocol, aImAccount)
 {
   this._init(aProtocol, aImAccount);
   this._knownMessageIds = {};
   this._userInfo = {};
 }
 Account.prototype = {
   __proto__: GenericAccountPrototype,
 
-  get HTMLEnabled() false,
   get maxMessageLength() 140,
 
   consumerKey: "TSuyS1ieRAkB3qWv8yyEw",
   consumerSecret: "DKtKaSf5a7pBNhdBsSZHTnI5Y03hRlPFYWmb4xXBlkU",
   completionURI: "http://oauthcallback.local/",
   baseURI: "https://api.twitter.com/",
 
   // Use this to keep track of the pending timeline requests. We attempt to fetch
new file mode 100644
--- /dev/null
+++ b/chat/protocols/xmpp/Makefile.in
@@ -0,0 +1,56 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Florian Quèze <florian@queze.net>.
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH		= ../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+EXTRA_COMPONENTS = \
+		xmpp.js \
+		xmpp.manifest \
+		$(NULL)
+
+EXTRA_JS_MODULES = \
+		xmpp.jsm \
+		xmpp-authmechs.jsm \
+		xmpp-session.jsm \
+		xmpp-xml.jsm \
+		$(NULL)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/chat/protocols/xmpp/xmpp-authmechs.jsm
@@ -0,0 +1,149 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Instantbird.
+ *
+ * The Initial Developer of the Original Code is
+ * Varuna JAYASIRI <vpjayasiri@gmail.com>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Florian Quèze <florian@queze.net>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// This module exports XMPPAuthMechanisms, an object containing all
+// the supported SASL authentication mechanisms.
+// By default we currently support the PLAIN and the DIGEST-MD5 mechanisms.
+// As this is only used by XMPPSession, it may seem like an internal
+// detail of the XMPP implementation, but exporting it is valuable so that
+// add-ons can add support for more auth mechanisms easily by adding them
+// in XMPPAuthMechanisms without having to modify XMPPSession.
+
+var EXPORTED_SYMBOLS = ["XMPPAuthMechanisms"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource:///modules/xmpp-xml.jsm");
+
+/* Handle PLAIN authorization mechanism */
+function PlainAuth(username, password, domain) {
+  this._username = username;
+  this._password = password;
+}
+PlainAuth.prototype = {
+  next: function(aStanza) ({
+    done: true,
+    send: Stanza.node("auth", Stanza.NS.sasl, {mechanism: "PLAIN"},
+                      btoa("\0"+ this._username + "\0" + this._password))
+  })
+};
+
+
+/* Handles DIGEST-MD5 authorization mechanism */
+
+// md5 function adapted from netwerk/test/unit/test_authentication.js
+function md5(aString) {
+  let ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+  ch.init(ch.MD5);
+  let data = [aString.charCodeAt(i) for (i in aString)];
+  ch.update(data, data.length);
+  return ch.finish(false);
+}
+function md5hex(aString) {
+  let hash = md5(aString);
+  function toHexString(charCode) ("0" + charCode.toString(16)).slice(-2)
+  return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
+}
+
+function digestMD5(aName, aRealm, aPassword, aNonce, aCnonce, aDigestUri) {
+  let y = md5(aName + ":" + aRealm + ":" + aPassword);
+  return md5hex(md5hex(y + ":" + aNonce + ":" + aCnonce) +
+                ":" + aNonce + ":00000001:" + aCnonce + ":auth:" +
+                md5hex("AUTHENTICATE:" + aDigestUri));
+}
+
+function DigestMD5Auth(username, password, domain) {
+  this._username = username;
+  this._password = password;
+  this._domain = domain;
+  this.next = this._init;
+}
+DigestMD5Auth.prototype = {
+  _init: function(aStanza) {
+    this.next = this._generateResponse;
+    return {
+      done: false,
+      send: Stanza.node("auth", Stanza.NS.sasl, {mechanism: "DIGEST-MD5"})
+    };
+  },
+
+  _generateResponse: function(aStanza) {
+    let decoded = atob(aStanza.innerText.replace(/[^A-Za-z0-9\+\/\=]/g, ""));
+    let data = {realm: ""};
+
+    for each (let elem in decoded.split(",")) {
+      let e = elem.split("=");
+      if (e.length != 2)
+        throw "Error decoding: " + elem;
+
+      data[e[0]] = e[1].replace(/"|'/g, "");
+    }
+
+    data.username = this._username;
+    data.cnonce = md5hex(Math.random() * 1234567890);
+    data.nc = "00000001";
+    data.qop = "auth",
+    data["digest-uri"] = "xmpp/" + this._domain + (data.host ? "/" + host : "");
+    data.response = digestMD5(this._username, data.realm, this._password,
+                              data.nonce, data.cnonce, data["digest-uri"]);
+    data.charset = "utf-8";
+
+    let response =
+      ["username", "realm", "nonce", "cnonce", "nc", "qop", "digest-uri",
+       "response", "charset"].map(function(key) key + "=\"" + data[key] + "\"")
+                             .join(",");
+
+    this.next = this._finish;
+
+    return {
+      done: false,
+      send: Stanza.node("response", Stanza.NS.sasl, null, btoa(response))
+    };
+  },
+
+  _finish: function(aStanza) {
+    if (aStanza.localName != "challenge")
+      throw "Not authorized";
+
+    return {
+      done: true,
+      send: Stanza.node("response", Stanza.NS.sasl)
+    };
+  }
+};
+
+var XMPPAuthMechanisms = {"PLAIN": PlainAuth, "DIGEST-MD5": DigestMD5Auth};
new file mode 100644
--- /dev/null
+++ b/chat/protocols/xmpp/xmpp-session.jsm
@@ -0,0 +1,378 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Instantbird.
+ *
+ * The Initial Developer of the Original Code is
+ * Varuna JAYASIRI <vpjayasiri@gmail.com>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Florian Quèze <florian@queze.net>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var EXPORTED_SYMBOLS = ["XMPPSession"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/socket.jsm");
+Cu.import("resource:///modules/xmpp-xml.jsm");
+Cu.import("resource:///modules/xmpp-authmechs.jsm");
+
+initLogModule("xmpp-session", this);
+
+XPCOMUtils.defineLazyGetter(this, "_", function()
+  l10nHelper("chrome://purple/locale/xmpp.properties")
+);
+
+function XMPPSession(aHost, aPort, aSecurity, aJID, aPassword, aAccount) {
+  this._host = aHost;
+  this._port = aPort;
+
+  this._connectionSecurity = aSecurity;
+  if (this._connectionSecurity == "old_ssl")
+    this._security = ["ssl"];
+  else if (this._connectionSecurity != "none")
+    this._security = [(aPort == 5223 || aPort == 443) ? "ssl" : "starttls"];
+
+  this._jid = aJID;
+  this._domain = aJID.domain;
+  this._password = aPassword;
+  this._account = aAccount;
+
+  this._auth = null;
+  this._resource = aJID.resource || "instantbird";
+  this._handlers = {};
+  this._stanzaId = 0;
+
+  this._account.reportConnecting();
+  try {
+    this.connect(this._host, this._port, this._security);
+  } catch (e) {
+    Cu.reportError(e);
+    // We can't use _networkError because this._account._connection
+    // isn't set until we return from the XMPPSession constructor.
+    this._account.reportDisconnecting(Ci.prplIAccount.ERROR_NETWORK_ERROR,
+                                      _("connection.error.failedToCreateASocket"));
+    this._account.reportDisconnected();
+  }
+}
+
+XMPPSession.prototype = {
+  /* for the socket.jsm helper */
+  __proto__: Socket,
+  connectTimeout: 60,
+  readWriteTimeout: 0,
+
+  _security: null,
+  _encrypted: false,
+
+  /* Disconnect from the server */
+  disconnect: function() {
+    if (this.onXmppStanza == this.stanzaListeners.accountListening)
+      this.send("</stream:stream>");
+    delete this.onXmppStanza;
+    Socket.disconnect.call(this);
+  },
+
+  /* Report errors to the account */
+  onError: function(aError, aException) {
+    this._account.onError(aError, aException);
+  },
+
+  /* Send a text message to the server */
+  send: function(aMsg) {
+    this.sendString(aMsg);
+  },
+
+  /* Send a stanza to the server.
+   * Can set a callback if required, which will be called
+   * when the server responds to the stanza with
+   * a stanza of the same id. */
+  sendStanza: function(aStanza, aCallback, aObject) {
+    if (!aStanza.attributes.hasOwnProperty("id"))
+      aStanza.attributes["id"] = ++this._stanzaId;
+    if (aCallback)
+      this.addHandler(aStanza.attributes.id, aCallback.bind(aObject));
+    this.send(aStanza.getXML());
+    return aStanza.attributes.id;
+  },
+
+
+  /* these 3 methods handle callbacks for specific ids. */
+  addHandler: function(aId, aCallback) {
+    this._handlers[aId] = aCallback;
+  },
+  removeHandler: function(aId) {
+    delete this._handlers[aId];
+  },
+  execHandler: function(aId, aStanza) {
+    if (!this._handlers.hasOwnProperty(aId))
+      return;
+    this._handlers[aId](aStanza);
+    this.removeHandler(aId);
+  },
+
+  /* Start the XMPP stream */
+  startStream: function() {
+    this._parser = new XMPPParser(this);
+    this.send('<?xml version="1.0"?><stream:stream to="' + this._domain +
+              '" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">');
+  },
+
+  /* Log a message (called by the socket code) */
+  log: LOG,
+
+  /* Socket events */
+  /* The connection is established */
+  onConnection: function() {
+    if (this._security.indexOf("ssl") != -1) {
+      this.onXmppStanza = this.stanzaListeners.startAuth;
+      this._encrypted = true;
+    }
+    else
+      this.onXmppStanza = this.stanzaListeners.initStream;
+    this._account.reportConnecting(_("connection.initializingStream"));
+    this.startStream();
+  },
+
+  /* When incoming data is available to be read */
+  onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
+    try {
+      this._parser.onDataAvailable(aInputStream, aOffset, aCount);
+    } catch(e) {
+      Cu.reportError(e);
+      this.onXMLError("parser-exception", e);
+    }
+  },
+
+  /* The connection got disconnected without us closing it. */
+  onConnectionClosed: function() {
+    this._networkError(_("connection.error.serverClosedConnection"));
+  },
+  onConnectionReset: function() {
+    this._networkError(_("connection.error.resetByPeer"));
+  },
+  onConnectionTimedOut: function() {
+    this._networkError(_("connection.error.timedOut"));
+  },
+  _networkError: function(aMessage) {
+    this.onError(Ci.prplIAccount.ERROR_NETWORK_ERROR, aMessage);
+  },
+
+
+  /* Methods called by the XMPPParser instance */
+  onXMLError: function(aError, aException) {
+    if (aError == "parsing-characters")
+      WARN(aError + ": " + aException);
+    else
+      ERROR(aError + ": " + aException);
+    if (aError != "parse-warning" && aError != "parsing-characters")
+      this._networkError(_("connection.error.receivedUnexpectedData"));
+  },
+
+  // All the functions in stanzaListeners are used as onXmppStanza
+  // implementations at various steps of establishing the session.
+  stanzaListeners: {
+    initStream: function(aStanza) {
+      if (aStanza.localName != "features") {
+        ERROR("Unexpected stanza " + aStanza.localName + ", expected 'features'");
+        this._networkError(_("connection.error.incorrectResponse"));
+        return;
+      }
+
+      let starttls = aStanza.getElement(["starttls"]);
+      if (starttls && this._security.indexOf("starttls") != -1) {
+        this._account.reportConnecting(_("connection.initializingEncryption"));
+        this.sendStanza(Stanza.node("starttls", Stanza.NS.tls));
+        this.onXmppStanza = this.stanzaListeners.startTLS;
+        return;
+      }
+      if (starttls &&
+          starttls.children.some(function (c) c.localName == "required")) {
+        this.onError(Ci.prplIAccount.ERROR_ENCRYPTION_ERROR,
+                     _("connection.error.startTLSRequired"));
+        return;
+      }
+      if (!starttls && this._connectionSecurity == "require_tls") {
+        this.onError(Ci.prplIAccount.ERROR_ENCRYPTION_ERROR,
+                     _("connection.error.startTLSNotSupported"));
+        return;
+      }
+
+      // If we aren't starting TLS, jump to the auth step.
+      this.onXmppStanza = this.stanzaListeners.startAuth;
+      this.onXmppStanza(aStanza);
+    },
+    startTLS: function(aStanza) {
+      if (aStanza.localName != "proceed") {
+        this._networkError(_("connection.error.failedToStartTLS"));
+        return;
+      }
+
+      this.startTLS();
+      this._encrypted = true;
+      this.startStream();
+      this.onXmppStanza = this.stanzaListeners.startAuth;
+    },
+    startAuth: function(aStanza) {
+      if (aStanza.localName != "features") {
+        ERROR("Unexpected stanza " + aStanza.localName + ", expected 'features'");
+        this._networkError(_("connection.error.incorrectResponse"));
+        return;
+      }
+
+      let mechs = aStanza.getElement(["mechanisms"]);
+      if (!mechs) {
+        this._networkError(_("connection.error.noAuthMec"));
+        return;
+      }
+
+      // Select the auth mechanism we will use. PLAIN will be treated
+      // a bit differently as we want to avoid it over an unencrypted
+      // connection, except if the user has explicly allowed that
+      // behavior.
+      let selectedMech = "";
+      let canUsePlain = false;
+      mechs = mechs.getChildren("mechanism");
+      for each (let m in mechs) {
+        let mech = m.innerText;
+        if (mech == "PLAIN" && !this._encrypted)
+          canUsePlain = true;
+        else if (XMPPAuthMechanisms.hasOwnProperty(mech)) {
+          selectedMech = mech;
+          break;
+        }
+      }
+      if (!selectedMech && canUsePlain) {
+        if (this._security == "allow_unencrypted_plain_auth")
+          selectedMech = "PLAIN";
+        else {
+          this.onError(Ci.prplIAccount.ERROR_AUTHENTICATION_IMPOSSIBLE,
+                       _("connection.error.notSendingPasswordInClear"));
+          return;
+        }
+      }
+      if (!selectedMech) {
+        this.onError(Ci.prplIAccount.ERROR_AUTHENTICATION_IMPOSSIBLE,
+                     _("connection.error.noCompatibleAuthMec"));
+        return;
+      }
+      this._auth = new XMPPAuthMechanisms[selectedMech](this._jid.node,
+                                                        this._password,
+                                                        this._domain);
+
+      this._account.reportConnecting(_("connection.authenticating"));
+      this.onXmppStanza = this.stanzaListeners.authDialog;
+      this.onXmppStanza(null); // the first auth step doesn't read anything
+    },
+    authDialog: function(aStanza) {
+      if (aStanza && aStanza.localName == "failure") {
+        let errorMsg = "authenticationFailure";
+        if (aStanza.getElement(["not-authorized"]))
+          errorMsg = "notAuthorized";
+        this.onError(Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED,
+                     _("connection.error." + errorMsg));
+        return;
+      }
+
+      let rv;
+      try {
+        rv = this._auth.next(aStanza);
+      } catch(e) {
+        ERROR(e);
+        this.onError(Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED,
+                     _("connection.error.authenticationFailure"));
+        return;
+      }
+
+      if (rv.send)
+        this.send(rv.send.getXML());
+      if (rv.done)
+        this.onXmppStanza = this.stanzaListeners.authResult;
+    },
+    authResult: function(aStanza) {
+      if (aStanza.localName != "success") {
+        this.onError(Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED,
+                     _("connection.error.notAuthorized"));
+        return;
+      }
+
+      this.startStream();
+      this.onXmppStanza = this.stanzaListeners.startBind;
+    },
+    startBind: function(aStanza) {
+      if (!aStanza.getElement(["bind"])) {
+        ERROR("Unexpected lack of the bind feature");
+        this._networkError(_("connection.error.incorrectResponse"));
+        return;
+      }
+
+      this._account.reportConnecting(_("connection.gettingResource"));
+      this.sendStanza(Stanza.iq("set", null, null,
+                                Stanza.node("bind", Stanza.NS.bind, null,
+                                            Stanza.node("resource", null, null,
+                                                        this._resource))));
+      this.onXmppStanza = this.stanzaListeners.bindResult;
+    },
+    bindResult: function(aStanza) {
+      let jid = aStanza.getElement(["bind", "jid"]);
+      if (!jid) {
+        this._networkError(_("connection.error.failedToGetAResource"));
+        return;
+      }
+      jid = jid.innerText;
+      DEBUG("jid = " + jid);
+      this._jid = this._account._parseJID(jid);
+      this.sendStanza(Stanza.iq("set", null, null,
+                                Stanza.node("session", Stanza.NS.session)));
+      this.onXmppStanza = this.stanzaListeners.sessionStarted;
+    },
+    sessionStarted: function(aStanza) {
+      this._account.onConnection();
+      this.onXmppStanza = this.stanzaListeners.accountListening;
+    },
+    accountListening: function(aStanza) {
+      this._account.onXmppStanza(aStanza);
+      let name = aStanza.qName;
+      if (name == "presence")
+        this._account.onPresenceStanza(aStanza);
+      else if (name == "message")
+        this._account.onMessageStanza(aStanza);
+      else if (name == "iq")
+        this._account.onIQStanza(aStanza);
+
+      if (aStanza.attributes.id)
+        this.execHandler(aStanza.attributes.id, aStanza);
+    }
+  },
+  onXmppStanza: function(aStanza) {
+    ERROR("should not be reached\n");
+  }
+};
new file mode 100644
--- /dev/null
+++ b/chat/protocols/xmpp/xmpp-xml.jsm
@@ -0,0 +1,422 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Instantbird.
+ *
+ * The Initial Developer of the Original Code is
+ * Varuna JAYASIRI <vpjayasiri@gmail.com>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Florian Quèze <florian@queze.net>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var EXPORTED_SYMBOLS = ["Stanza", "XMPPParser"];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+const NS = {
+  xml                       : "http://www.w3.org/XML/1998/namespace",
+  xhtml                     : "http://www.w3.org/1999/xhtml",
+  xhtml_im                  : "http://jabber.org/protocol/xhtml-im",
+
+  //auth
+  client                    : "jabber:client",
+  streams                   : "http://etherx.jabber.org/streams",
+  stream                    : "urn:ietf:params:xml:ns:xmpp-streams",
+  sasl                      : "urn:ietf:params:xml:ns:xmpp-sasl",
+  tls                       : "urn:ietf:params:xml:ns:xmpp-tls",
+  bind                      : "urn:ietf:params:xml:ns:xmpp-bind",
+  session                   : "urn:ietf:params:xml:ns:xmpp-session",
+  auth                      : "jabber:iq:auth",
+  http_bind                 : "http://jabber.org/protocol/httpbind",
+  http_auth                 : "http://jabber.org/protocol/http-auth",
+  xbosh                     : "urn:xmpp:xbosh",
+
+  "private"                 : "jabber:iq:private",
+  xdata                     : "jabber:x:data",
+
+  //roster
+  roster                    : "jabber:iq:roster",
+  roster_versioning         : "urn:xmpp:features:rosterver",
+  roster_delimiter          : "roster:delimiter",
+
+  //privacy lists
+  privacy                   : "jabber:iq:privacy",
+
+  //discovering
+  disco_info                : "http://jabber.org/protocol/disco#info",
+  disco_items               : "http://jabber.org/protocol/disco#items",
+  caps                      : "http://jabber.org/protocol/caps",
+
+  //addressing
+  address                   : "http://jabber.org/protocol/address",
+
+  muc_user                  : "http://jabber.org/protocol/muc#user",
+  muc                       : "http://jabber.org/protocol/muc",
+  register                  : "jabber:iq:register",
+  delay                     : "urn:xmpp:delay",
+  delay_legacy              : "jabber:x:delay",
+  bookmarks                 : "storage:bookmarks",
+  chatstates                : "http://jabber.org/protocol/chatstates",
+  event                     : "jabber:x:event",
+  stanzas                   : "urn:ietf:params:xml:ns:xmpp-stanzas",
+  vcard                     : "vcard-temp",
+  vcard_update              : "vcard-temp:x:update",
+  ping                      : "urn:xmpp:ping",
+
+  geoloc                    : "http://jabber.org/protocol/geoloc",
+  geoloc_notify             : "http://jabber.org/protocol/geoloc+notify",
+  mood                      : "http://jabber.org/protocol/mood",
+  tune                      : "http://jabber.org/protocol/tune",
+  nick                      : "http://jabber.org/protocol/nick",
+  nick_notify               : "http://jabber.org/protocol/nick+notify",
+  activity                  : "http://jabber.org/protocol/activity",
+  last                      : "jabber:iq:last",
+  avatar_data               : "urn:xmpp:avatar:data",
+  avatar_data_notify        : "urn:xmpp:avatar:data+notify",
+  avatar_metadata           : "urn:xmpp:avatar:metadata",
+  avatar_metadata_notify    : "urn:xmpp:avatar:metadata+notify",
+  pubsub                    : "http://jabber.org/protocol/pubsub",
+  pubsub_event              : "http://jabber.org/protocol/pubsub#event"
+};
+
+
+var TOP_LEVEL_ELEMENTS = {
+  "message"             : "jabber:client",
+  "presence"            : "jabber:client",
+  "iq"                  : "jabber:client",
+  "stream:features"     : "http://etherx.jabber.org/streams",
+  "proceed"             : "urn:ietf:params:xml:ns:xmpp-tls",
+  "failure"             : ["urn:ietf:params:xml:ns:xmpp-tls",
+                           "urn:ietf:params:xml:ns:xmpp-sasl"],
+  "success"             : "urn:ietf:params:xml:ns:xmpp-sasl",
+  "challenge"           : "urn:ietf:params:xml:ns:xmpp-sasl",
+  "error"               : "urn:ietf:params:xml:ns:xmpp-streams"
+};
+
+/* Stanza Builder */
+const Stanza = {
+  NS: NS,
+
+  /* Create a presence stanza */
+  presence: function(aAttr, aData) Stanza.node("presence", null, aAttr, aData),
+
+  /* Create a message stanza */
+  message: function(aTo, aMsg, aState, aAttr, aData) {
+    if (!aAttr)
+      aAttr = {};
+
+    aAttr.to = aTo;
+    if (!("type" in aAttr))
+      aAttr.type = "chat";
+
+    if (!aData)
+      aData = [];
+
+    if (aMsg)
+      aData.push(Stanza.node("body", null, null, aMsg));
+
+    if (aState)
+      aData.push(Stanza.node(aState, Stanza.NS.chatstates));
+
+    return Stanza.node("message", null, aAttr, aData);
+  },
+
+  /* Create a iq stanza */
+  iq: function(aType, aId, aTo, aData) {
+    let attrs = {type: aType};
+    if (aId)
+      attrs.id = aId;
+    if (aTo)
+      attrs.to = aTo;
+    return this.node("iq", null, attrs, aData);
+  },
+
+  /* Create a XML node */
+  node: function(aName, aNs, aAttr, aData) {
+    let n = new XMLNode(null, aNs, aName, aName, null);
+
+    if (aAttr) {
+      for (let at in aAttr)
+        n.attributes[at] = aAttr[at];
+    }
+
+    if (aData) {
+      if (!Array.isArray(aData))
+        aData = [aData];
+      for each (let child in aData)
+         n[typeof(child) == "string" ? "addText" : "addChild"](child);
+    }
+
+    return n;
+  }
+};
+
+/* Text node
+ * Contains a text */
+function TextNode(aText) {
+  this.text = aText;
+}
+
+TextNode.prototype = {
+  get type() "text",
+
+  append: function(aText) {
+    this.text += aText;
+  },
+
+  /* For debug purposes, returns an indented (unencoded) string */
+  convertToString: function(aIndent) aIndent + this.text + "\n",
+
+  /* Returns the encoded XML */
+  getXML: function()
+    Components.classes["@mozilla.org/txttohtmlconv;1"]
+              .getService(Ci.mozITXTToHTMLConv)
+              .scanTXT(this.text, Ci.mozITXTToHTMLConv.kEntities),
+
+  /* To read the unencoded data. */
+  get innerText() this.text
+};
+
+/* XML node */
+function XMLNode(aParentNode, aUri, aLocalName, aQName, aAttr) {
+  this._parentNode = aParentNode; // Used only for parsing
+  this.uri = aUri;
+  this.localName = aLocalName;
+  this.qName = aQName;
+  this.attributes = {};
+  this.children = [];
+
+  if (aAttr) {
+    for (let i = 0; i < aAttr.length; ++i)
+      this.attributes[aAttr.getQName(i)] = aAttr.getValue(i);
+  }
+}
+
+XMLNode.prototype = {
+  get type() "node",
+
+  /* Add a new child node */
+  addChild: function(aNode) {
+    this.children.push(aNode);
+  },
+
+  /* Add text node */
+  addText: function(aText) {
+    let lastIndex = this.children.length - 1;
+    if (lastIndex >= 0 && this.children[lastIndex] instanceof TextNode)
+      this.children[lastIndex].append(aText);
+    else
+      this.children.push(new TextNode(aText));
+  },
+
+  /* Get child elements by namespace */
+  getChildrenByNS: function(aNS)
+    this.children.filter(function(c) c.uri == aNS),
+
+  /* Get the first element inside the node that matches a query. */
+  getElement: function(aQuery) {
+   if (aQuery.length == 0)
+     return this;
+
+   let nq = aQuery.slice(1);
+   for each (let child in this.children) {
+     if (child.qName != aQuery[0])
+       continue;
+     let n = child.getElement(nq);
+     if (n)
+       return n;
+   }
+
+   return null;
+  },
+
+  /* Get all elements matching the query */
+  getElements: function(aQuery) {
+   if (aQuery.length == 0)
+     return [this];
+
+   let c = this.getChildren(aQuery[0]);
+   let nq = aQuery.slice(1);
+   let res = [];
+   for each (let child in c) {
+     let n = child.getElements(nq);
+     res = res.concat(n);
+   }
+
+   return res;
+  },
+
+  /* Get immediate children by the node name */
+  getChildren: function(aName)
+    this.children.filter(function(c) c.qName == aName),
+
+  /* Test if the node is a stanza */
+  isXmppStanza: function() {
+    if (!TOP_LEVEL_ELEMENTS.hasOwnProperty(this.qName))
+      return false;
+    let ns = TOP_LEVEL_ELEMENTS[this.qName];
+    return ns == this.uri || (Array.isArray(ns) && ns.indexOf(this.uri) != -1);
+  },
+
+  /* Returns indented XML */
+  convertToString: function(aIndent) {
+    if (!aIndent)
+      aIndent = "";
+
+    let s =
+      aIndent + "<" + this.qName + this._getXmlns() + this._getAttributeText();
+    let content = "";
+    for each (let child in this.children)
+      content += child.convertToString(aIndent + " ");
+    return s + (content ? ">\n" + content + aIndent + "</" + this.qName : "/") + ">\n";
+  },
+
+  /* Returns the XML */
+  getXML: function() {
+    let s = "<" + this.qName + this._getXmlns() + this._getAttributeText();
+    let innerXML = this.children.map(function(c) c.getXML()).join("");
+    return s + (innerXML ? ">" + innerXML + "</" + this.qName : "/") + ">";
+  },
+
+  get innerText() this.children.map(function(c) c.innerText).join(""),
+
+  /* Private methods */
+  _getXmlns: function() this.uri ? " xmlns=\"" + this.uri + "\"" : "",
+  _getAttributeText: function() {
+    let s = "";
+    for (let name in this.attributes)
+      s += " " +name + "=\"" + this.attributes[name] + "\"";
+    return s;
+  }
+};
+
+function XMPPParser(aListener) {
+  this._parser = Cc["@mozilla.org/saxparser/xmlreader;1"]
+                 .createInstance(Ci.nsISAXXMLReader);
+  this._parser.contentHandler = this;
+  this._parser.errorHandler = this;
+  this._parser.parseAsync(null);
+  this._listener = aListener;
+  this._parser.onStartRequest(this._dummyRequest, null);
+}
+XMPPParser.prototype = {
+  _dummyRequest: {
+    cancel: function() { },
+    isPending: function() { },
+    resume: function() { },
+    suspend: function() { }
+  },
+
+  onDataAvailable: function(aInputStream, aOffset, aCount) {
+    this._parser.onDataAvailable(this._dummyRequest, null,
+                                 aInputStream, aOffset, aCount);
+  },
+
+  /* nsISAXContentHandler implementation */
+  startDocument: function() { },
+  endDocument: function() { },
+
+  startElement: function(aUri, aLocalName, aQName, aAttributes) {
+    if (aQName == "stream:stream") {
+      if ("_node" in this)
+        this._listener.onXMLError("unexpected-stream-start",
+                                  "stream:stream inside an already started stream");
+      this._node = null;
+      return;
+    }
+
+    let node = new XMLNode(this._node, aUri, aLocalName, aQName, aAttributes);
+    if (this._node)
+      this._node.addChild(node);
+
+    this._node = node;
+  },
+
+  characters: function(aCharacters) {
+    if (!this._node) {
+      // Ignore whitespace received on the stream to keep the connection alive.
+      if (aCharacters.trim()) {
+        this._listener.onXMLError("parsing-characters",
+                                  "No parent for characters: " + aCharacters);
+      }
+      return;
+    }
+
+    this._node.addText(aCharacters);
+  },
+
+  endElement: function(aUri, aLocalName, aQName) {
+    if (aQName == "stream:stream") {
+      delete this._node;
+      return;
+    }
+
+    if (!this._node) {
+      this._listener.onXMLError("parsing-node",
+                                "No parent for node : " + aLocalName);
+      return;
+    }
+
+    if (this._node.isXmppStanza()) {
+      this._listener.log("received:\n" + this._node.convertToString());
+      try {
+        this._listener.onXmppStanza(this._node);
+      } catch (e) {
+        Cu.reportError(e);
+        dump(e + "\n");
+      }
+    }
+
+    this._node = this._node._parentNode;
+  },
+
+  processingInstruction: function(aTarget, aData) { },
+  ignorableWhitespace: function(aWhitespace) { },
+  startPrefixMapping: function(aPrefix, aUri) { },
+  endPrefixMapping: function(aPrefix) { },
+
+  /* nsISAXErrorHandler implementation */
+  error: function(aLocator, aError) {
+    this._listener.onXMLError("parse-error", aError);
+  },
+  fatalError: function(aLocator, aError) {
+    this._listener.onXMLError("parse-fatal-error", aError);
+  },
+  ignorableWarning: function(aLocator, aError) {
+    this._listener.onXMLError("parse-warning", aError);
+  },
+
+  QueryInterface: function(aInterfaceId) {
+    if (!aInterfaceId.equals(Ci.nsISupports) &&
+        !aInterfaceId.equals(Ci.nsISAXContentHandler) &&
+        !aInterfaceId.equals(Ci.nsISAXErrorHandler))
+      throw Cr.NS_ERROR_NO_INTERFACE;
+    return this;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/chat/protocols/xmpp/xmpp.js
@@ -0,0 +1,82 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Instantbird.
+ *
+ * The Initial Developer of the Original Code is
+ * Varuna JAYASIRI <vpjayasiri@gmail.com>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Florian Quèze <florian@queze.net>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cu = Components.utils;
+
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/jsProtoHelper.jsm");
+Cu.import("resource:///modules/xmpp.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", function()
+  l10nHelper("chrome://purple/locale/xmpp.properties")
+);
+
+function XMPPAccount(aProtoInstance, aImAccount) {
+  this._init(aProtoInstance, aImAccount);
+}
+XMPPAccount.prototype = XMPPAccountPrototype;
+
+function XMPPProtocol() {
+}
+XMPPProtocol.prototype = {
+  __proto__: GenericProtocolPrototype,
+  get normalizedName() "jabber",
+  get name() "XMPP (JS)",
+  get iconBaseURI() "chrome://prpl-jabber/skin/",
+  getAccount: function(aImAccount) new XMPPAccount(this, aImAccount),
+  options: {
+    resource: {get label() _("options.resource"), default: "instantbird"},
+    connection_security: {
+      get label() _("options.connectionSecurity"),
+      listValues: {
+        get require_tls() _("options.connectionSecurity.requireEncryption"),
+        get opportunistic_tls() _("options.connectionSecurity.opportunisticTLS"),
+        get allow_unencrypted_plain_auth() _("options.connectionSecurity.allowUnencryptedAuth"),
+        // "old_ssl" and "none" are also supported, but not exposed in the UI.
+        // Any unknown value will fallback to the opportunistic_tls behavior.
+      },
+      default: "opportunistic_tls"
+    },
+    server: {get label() _("options.connectServer"), default: ""},
+    port: {get label() _("options.connectPort"), default: 5222}
+  },
+  get chatHasTopic() true,
+
+  classID: Components.ID("{dde786d1-6f59-43d0-9bc8-b505a757fb30}")
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([XMPPProtocol]);
new file mode 100644
--- /dev/null
+++ b/chat/protocols/xmpp/xmpp.jsm
@@ -0,0 +1,900 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Instantbird.
+ *
+ * The Initial Developer of the Original Code is
+ * Varuna JAYASIRI <vpjayasiri@gmail.com>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Florian Quèze <florian@queze.net>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var EXPORTED_SYMBOLS = ["XMPPConversationPrototype",
+                        "XMPPMUCConversationPrototype",
+                        "XMPPAccountBuddyPrototype",
+                        "XMPPAccountPrototype"];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource:///modules/imServices.jsm");
+Cu.import("resource:///modules/imStatusUtils.jsm");
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/jsProtoHelper.jsm");
+Cu.import("resource:///modules/socket.jsm");
+Cu.import("resource:///modules/xmpp-xml.jsm");
+Cu.import("resource:///modules/xmpp-session.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "DownloadUtils", function() {
+  Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
+  return DownloadUtils;
+});
+XPCOMUtils.defineLazyGetter(this, "FileUtils", function() {
+  Components.utils.import("resource://gre/modules/FileUtils.jsm");
+  return FileUtils;
+});
+XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
+  Components.utils.import("resource://gre/modules/NetUtil.jsm");
+  return NetUtil;
+});
+
+initLogModule("xmpp", this);
+
+XPCOMUtils.defineLazyGetter(this, "_", function()
+  l10nHelper("chrome://purple/locale/xmpp.properties")
+);
+
+/* This is an ordered list, used to determine chat buddy flags:
+ *  index < member    -> noFlags
+ *  index = member    -> voiced
+ *          moderator -> halfOp
+ *          admin     -> op
+ *          owner     -> founder
+ */
+const kRoles = ["outcast", "visitor", "participant", "member", "moderator",
+                "admin", "owner"];
+
+function MUCParticipant(aNick, aName, aStanza)
+{
+  this._jid = aName;
+  this.name = aNick;
+  this.stanza = aStanza;
+}
+MUCParticipant.prototype = {
+  __proto__: ClassInfo("purpleIConvChatBuddy", "XMPP ConvChatBuddy object"),
+
+  buddy: false,
+  get alias() this.name,
+
+  role: 2, // "participant" by default
+  set stanza(aStanza) {
+    this._stanza = aStanza;
+
+    let x =
+      aStanza.getChildren("x").filter(function (c) c.uri == Stanza.NS.muc_user);
+    if (x.length == 0)
+      return;
+    x = x[0];
+    let item = x.getElement(["item"]);
+    if (!item)
+      return;
+
+    this.role = Math.max(kRoles.indexOf(item.attributes["role"]),
+                         kRoles.indexOf(item.attributes["affiliation"]));
+  },
+
+  get noFlags() this.role < kRoles.indexOf("member"),
+  get voiced() this.role == kRoles.indexOf("member"),
+  get halfOp() this.role == kRoles.indexOf("moderator"),
+  get op() this.role == kRoles.indexOf("admin"),
+  get founder() this.role == kRoles.indexOf("owner"),
+  typing: false
+};
+
+const XMPPMUCConversationPrototype = {
+  __proto__: GenericConvChatPrototype,
+
+  _init: function(aAccount, aJID, aNick) {
+    GenericConvChatPrototype._init.call(this, aAccount, aJID, aNick);
+  },
+
+  get normalizedName() this.name,
+
+  _targetResource: "",
+
+  /* Called when the user enters a chat message */
+  sendMsg: function (aMsg) {
+    let s = Stanza.message(this.name, aMsg, null, {type: "groupchat"});
+    this._account.sendStanza(s);
+  },
+
+  /* Called by the account when a presence stanza is received for this muc */
+  onPresenceStanza: function(aStanza) {
+    let from = aStanza.attributes["from"];
+    let nick = this._account._parseJID(from).resource;
+    if (aStanza.attributes["type"] == "unavailable") {
+      if (!(nick in this._participants)) {
+        WARN("received unavailable presence for an unknown MUC participant: " +
+             from);
+        return;
+      }
+      delete this._participants[nick];
+      let nickString = Cc["@mozilla.org/supports-string;1"]
+                       .createInstance(Ci.nsISupportsString);
+      nickString.data = nick;
+      this.notifyObservers(new nsSimpleEnumerator([nickString]),
+                           "chat-buddy-remove");
+      return;
+    }
+    if (!hasOwnProperty(this._participants, nick)) {
+      this._participants[nick] = new MUCParticipant(nick, from, aStanza);
+      this.notifyObservers(new nsSimpleEnumerator([this._participants[nick]]),
+                           "chat-buddy-add");
+    }
+    else {
+      this._participants[nick].stanza = aStanza;
+      this.notifyObservers(this._participants[nick], "chat-buddy-update");
+    }
+  },
+
+  /* Called by the account when a messsage is received for this muc */
+  incomingMessage: function(aMsg, aStanza, aDate) {
+    let from = this._account._parseJID(aStanza.attributes["from"]).resource;
+    let flags = {};
+    if (!from) {
+      flags.system = true;
+      from = this.name;
+    }
+    else if (from == this._nick)
+      flags.outgoing = true;
+    else
+      flags.incoming = true;
+    if (aDate) {
+      flags.time = aDate / 1000;
+      flags.delayed = true;
+    }
+    this.writeMessage(from, aMsg, flags);
+  },
+
+  getNormalizedChatBuddyName: function(aNick) this.name + "/" + aNick,
+
+  /* Called when the user closed the conversation */
+  close: function() {
+    this._account.sendStanza(Stanza.presence({to: this.name + "/" + this._nick,
+                                             type: "unavailable"}));
+    GenericConvChatPrototype.close.call(this);
+    this._account.removeConversation(this.name);
+  }
+};
+function XMPPMUCConversation(aAccount, aJID, aNick)
+{
+  this._init(aAccount, aJID, aNick);
+}
+XMPPMUCConversation.prototype = XMPPMUCConversationPrototype;
+
+/* Helper class for buddy conversations */
+const XMPPConversationPrototype = {
+  __proto__: GenericConvIMPrototype,
+
+  _typingTimer: null,
+  _supportChatStateNotifications: true,
+  _typingState: "active",
+
+  _init: function(aAccount, aBuddy) {
+    this.buddy = aBuddy;
+    GenericConvIMPrototype._init.call(this, aAccount, aBuddy.normalizedName);
+  },
+
+  get title() this.buddy.contactDisplayName,
+  get normalizedName() this.buddy.normalizedName,
+
+  set supportChatStateNotifications(val) {
+    this._supportChatStateNotifications = val;
+  },
+
+  /* Called when the user is typing a message
+   * aLength - length of the typed message */
+  sendTyping: function(aLength) {
+    if (!this._supportChatStateNotifications)
+      return;
+
+    this._cancelTypingTimer();
+    if (aLength)
+      this._typingTimer = setTimeout(this.finishedComposing.bind(this), 10000);
+
+    this._setTypingState(aLength ? "composing" : "active");
+  },
+
+  finishedComposing: function() {
+    if (!this._supportChatStateNotifications)
+      return;
+
+    this._setTypingState("paused");
+  },
+
+  _setTypingState: function(aNewState) {
+    if (this._typingState == aNewState)
+      return;
+
+    /* to, msg, state, attrib, data */
+    let s = Stanza.message(this.to, null, aNewState);
+    this._account.sendStanza(s);
+    this._typingState = aNewState;
+  },
+  _cancelTypingTimer: function() {
+    if (this._typingTimer) {
+      clearTimeout(this._typingTimer);
+      delete this._typingTimer;
+    }
+  },
+
+  _targetResource: "",
+  get to() {
+    let to = this.buddy.userName;
+    if (this._targetResource)
+      to += "/" + this._targetResource;
+    return to;
+  },
+
+  /* Called when the user enters a chat message */
+  sendMsg: function (aMsg) {
+    this._cancelTypingTimer();
+    let cs = this._supportChatStateNotifications ? "active" : null;
+    let s = Stanza.message(this.to, aMsg, cs);
+    this._account.sendStanza(s);
+    let who;
+    if (this._account._connection)
+      who = this._account._connection._jid.jid;
+    if (!who)
+      who = this._account.name;
+    let alias = this.account.alias || this.account.statusInfo.displayName;
+    let msg = Components.classes["@mozilla.org/txttohtmlconv;1"]
+                        .getService(Ci.mozITXTToHTMLConv)
+                        .scanTXT(aMsg, Ci.mozITXTToHTMLConv.kEntities);
+    this.writeMessage(who, msg, {outgoing: true, _alias: alias});
+    delete this._typingState;
+  },
+
+  /* Called by the account when a messsage is received from the buddy */
+  incomingMessage: function(aMsg, aStanza, aDate) {
+    let from = aStanza.attributes["from"];
+    this._targetResource = this._account._parseJID(from).resource;
+    let flags = {incoming: true, _alias: this.buddy.contactDisplayName};
+    if (aDate) {
+      flags.time = aDate / 1000;
+      flags.delayed = true;
+    }
+    this.writeMessage(from, aMsg, flags);
+  },
+
+  /* Called when the user closed the conversation */
+  close: function() {
+    GenericConvIMPrototype.close.call(this);
+    this._account.removeConversation(this.buddy.normalizedName);
+  }
+};
+function XMPPConversation(aAccount, aBuddy)
+{
+  this._init(aAccount, aBuddy);
+}
+XMPPConversation.prototype = XMPPConversationPrototype;
+
+/* Helper class for buddies */
+const XMPPAccountBuddyPrototype = {
+  __proto__: GenericAccountBuddyPrototype,
+
+  subscription: "none",
+  /* Returns a list of TooltipInfo objects to be displayed when the user hovers over the buddy */
+  getTooltipInfo: function() {
+    if (!this._account.connected)
+      return null;
+
+    let tooltipInfo = [];
+    if (this._resources) {
+      for (let r in this._resources) {
+        let status = this._resources[r];
+        let statusString = Status.toLabel(status.statusType);
+        if (status.statusType == Ci.imIStatusInfo.STATUS_IDLE &&
+            status.idleSince) {
+          let now = Math.floor(Date.now() / 1000);
+          let valuesAndUnits =
+            DownloadUtils.convertTimeUnits(now - status.idleSince);
+          if (!valuesAndUnits[2])
+            valuesAndUnits.splice(2, 2);
+          statusString += " (" + valuesAndUnits.join(" ") + ")";
+        }
+        if (status.statusText)
+          statusString += " - " + status.statusText;
+        let label = r ? _("tooltip.status", r) : _("tooltip.statusNoResource");
+        tooltipInfo.push(new TooltipInfo(label, statusString));
+      }
+    }
+
+    // The subscription value is interesting to display only in unusual cases.
+    if (this.subscription != "both") {
+      tooltipInfo.push(new TooltipInfo(_("tooltip.subscription"),
+                                       this.subscription));
+    }
+
+    return new nsSimpleEnumerator(tooltipInfo);
+  },
+
+  get normalizedName() this.userName,
+  /* Display name of the buddy */
+  get contactDisplayName() this.buddy.contact.displayName || this.displayName,
+
+  _saveIcon: function(aPhotoNode) {
+    let type = aPhotoNode.getElement(["TYPE"]).innerText;
+    const ext = {"image/gif": "gif", "image/jpeg": "jpg", "image/png": "png"};
+    if (!ext.hasOwnProperty(type))
+      return;
+
+    let data = aPhotoNode.getElement(["BINVAL"]).innerText;
+    let content = atob(data.replace(/[^A-Za-z0-9\+\/\=]/g, ""));
+    let istream = Cc["@mozilla.org/io/string-input-stream;1"]
+                  .createInstance(Ci.nsIStringInputStream);
+    istream.setData(content, content.length);
+
+    let fileName = this.normalizedName + "." + ext[type];
+    let file = FileUtils.getFile("ProfD", ["icons",
+                                           this.account.protocol.normalizedName,
+                                           this.account.normalizedName,
+                                           fileName]);
+    let ostream = FileUtils.openSafeFileOutputStream(file);
+    let buddy = this;
+    NetUtil.asyncCopy(istream, ostream, function(rc) {
+      if (Components.isSuccessCode(rc))
+        buddy.buddyIconFilename = Services.io.newFileURI(file).spec;
+    });
+  },
+
+  _preferredResource: undefined,
+  _resources: null,
+  // Called by the account when a presence stanza is received for this buddy.
+  onPresenceStanza: function(aStanza) {
+    let preferred = this._preferredResource;
+
+    // Facebook chat's XMPP server doesn't send resources, let's
+    // replace undefined resources with empty resources.
+    let resource =
+      this._account._parseJID(aStanza.attributes["from"]).resource || "";
+
+    if (aStanza.attributes["type"] == "unavailable") {
+      if (!this._resources || !(resource in this._resources))
+        return; // ignore for already offline resources.
+      delete this._resources[resource];
+      if (preferred == resource)
+        preferred = undefined;
+    }
+    else {
+      let statusType = Ci.imIStatusInfo.STATUS_AVAILABLE;
+      let show = aStanza.getElement(["show"]);
+      if (show) {
+        show = show.innerText;
+        if (show == "away")
+          statusType = Ci.imIStatusInfo.STATUS_AWAY;
+        else if (show == "chat")
+          statusType = Ci.imIStatusInfo.STATUS_AVAILABLE; //FIXME
+        else if (show == "dnd")
+          statusType = Ci.imIStatusInfo.STATUS_UNAVAILABLE;
+        else if (show == "xa")
+          statusType = Ci.imIStatusInfo.STATUS_IDLE;
+      }
+
+      let idleSince = 0;
+      let query = aStanza.getElement(["query"]);
+      if (query && query.uri == Stanza.NS.last) {
+        let now = Math.floor(Date.now() / 1000);
+        idleSince = now - parseInt(query.attributes["seconds"], 10);
+        statusType = Ci.imIStatusInfo.STATUS_IDLE;
+      }
+
+      let status = aStanza.getElement(["status"]);
+      if (status)
+        status = status.innerText;
+
+      let priority = aStanza.getElement(["priority"]);
+      priority = priority ? parseInt(priority.innerText, 10) : 0;
+
+      if (!this._resources)
+        this._resources = {};
+      this._resources[resource] = {
+        statusType: statusType,
+        statusText: status,
+        idleSince: idleSince,
+        priority: priority,
+        stanza: aStanza
+      };
+    }
+
+    for (let r in this._resources) {
+      if (preferred === undefined ||
+          this._resources[r].statusType > this._resources[preferred].statusType)
+        // FIXME also compare priorities...
+        preferred = r;
+    }
+
+    if (preferred == this._preferredResource && resource != preferred) {
+      // The presence information change is only for an unused resource,
+      // only potential buddy tooltips need to be refreshed.
+      this._notifyObservers("status-detail-changed");
+      return;
+    }
+
+    // Presence info has changed enough that if we are having a
+    // conversation with one resource of this buddy, we should send
+    // the next message to all resources.
+    // FIXME: the test here isn't exactly right...
+    if (this._preferredResource != preferred &&
+        this._account._conv.hasOwnProperty(this.normalizedName))
+      delete this._account._conv[this.normalizedName]._targetResource;
+
+    this._preferredResource = preferred;
+    if (preferred === undefined)
+      this.setStatus(Ci.imIStatusInfo.STATUS_OFFLINE, "");
+    else {
+      preferred = this._resources[preferred];
+      this.setStatus(preferred.statusType, preferred.statusText);
+    }
+  },
+
+  /* Can send messages to buddies who appear offline */
+  get canSendMessage() this.account.connected,
+
+  /* Called when the user wants to chat with the buddy */
+  createConversation: function()
+    this._account.createConversation(this.normalizedName)
+};
+function XMPPAccountBuddy(aAccount, aBuddy, aTag, aUserName)
+{
+  this._init(aAccount, aBuddy, aTag, aUserName);
+}
+XMPPAccountBuddy.prototype = XMPPAccountBuddyPrototype;
+
+/* Helper class for account */
+const XMPPAccountPrototype = {
+  __proto__: GenericAccountPrototype,
+
+  _jid: null, // parsed Jabber ID: node, domain, resource
+  _connection: null, // XMPP Connection
+
+  _init: function(aProtoInstance, aImAccount) {
+    GenericAccountPrototype._init.call(this, aProtoInstance, aImAccount);
+
+    /* Ongoing conversations */
+    this._conv = {};
+    this._buddies = {};
+    this._mucs = {};
+  },
+
+  get canJoinChat() true,
+  chatRoomFields: {
+    room: {get label() _("chatRoomField.room"), required: true},
+    server: {get label() _("chatRoomField.server"), required: true},
+    nick: {get label() _("chatRoomField.nick"), required: true},
+    password: {get label() _("chatRoomField.password"), isPassword: true}
+  },
+  parseDefaultChatName: function(aDefaultChatName) {
+    if (!aDefaultChatName)
+      return {nick: this._jid.node};
+
+    let jid = this._parseJID(aDefaultChatName);
+    return {
+      room: jid.node,
+      server: jid.domain,
+      nick: jid.resource || this._jid.node
+    };
+  },
+  getChatRoomDefaultFieldValues: function(aDefaultChatName) {
+    let rv = GenericAccountPrototype.getChatRoomDefaultFieldValues
+                                    .call(this, aDefaultChatName);
+    if (!rv.values.nick)
+      rv.values.nick = this._jid.node;
+
+    return rv;
+  },
+  joinChat: function(aComponents) {
+    let jid =
+      aComponents.getValue("room") + "@" + aComponents.getValue("server");
+    let nick = aComponents.getValue("nick");
+    if (jid in this._mucs)
+      return; // FIXME, check if we need to rejoin
+
+    let x;
+    let password = aComponents.getValue("password");
+    if (password) {
+      x = Stanza.node("x", Stanza.NS.muc, null,
+                      Stanza.node("password", null, null, password));
+    }
+    this._mucs[jid] = nick;
+    this._connection.sendStanza(Stanza.presence({to: jid + "/" + nick}, x));
+  },
+
+  get normalizedName() this._normalizeJID(this.name),
+
+  _idleSince: 0,
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic == "idle-time-changed") {
+      let idleTime = parseInt(aData, 10);
+      if (idleTime)
+        this._idleSince = Math.floor(Date.now() / 1000) - idleTime;
+      else
+        delete this._idleSince;
+      this._shouldSendPresenceForIdlenessChange = true;
+      executeSoon((function() {
+        if ("_shouldSendPresenceForIdlenessChange" in this)
+          this._sendPresence();
+      }).bind(this));
+    }
+
+    if (aTopic != "status-changed")
+      return;
+
+    this._sendPresence();
+  },
+
+  /* GenericAccountPrototype events */
+  /* Connect to the server */
+  connect: function() {
+    this._jid = this._parseJID(this.name);
+
+    // For the resource, if the user has edited the option to a non
+    // empty value, use that.
+    if (this.prefs.prefHasUserValue("resource")) {
+      let resource = this.getString("resource");
+      if (resource)
+        this._jid.resource = resource;
+    }
+    // Otherwise, if the username doesn't contain a resource, use the
+    // value of the resource option (it will be the default value).
+    // If we set an empty resource, XMPPSession will fallback to "instantbird"
+    if (!this._jid.resource)
+      this._jid.resource = this.getString("resource");
+
+    //FIXME if we have changed this._jid.resource, then this._jid.jid
+    // needs to be updated. This value is however never used because
+    // while connected it's the jid of the session that's interesting.
+
+    this._connection =
+      new XMPPSession(this.getString("server") || this._jid.domain,
+                      this.getInt("port") || 5222,
+                      this.getString("connection_security"), this._jid,
+                      this.imAccount.password, this);
+  },
+
+  /* Disconnect from the server */
+  disconnect: function() {
+    this._disconnect();
+  },
+
+  /* Loads a buddy from the local storage.
+   * Called for each buddy locally stored before connecting
+   * to the server. */
+  loadBuddy: function(aBuddy, aTag) {
+    let buddy = new this._accountBuddyConstructor(this, aBuddy, aTag);
+    this._buddies[buddy.normalizedName] = buddy;
+    return buddy;
+  },
+
+  /* XMPPSession events */
+  /* Called when the XMPP session is started */
+  onConnection: function() {
+    this.reportConnecting(_("connection.downloadingRoster"));
+    let s = Stanza.iq("get", null, null, Stanza.node("query", Stanza.NS.roster));
+
+    /* Set the call back onRoster */
+    this._connection.sendStanza(s, this.onRoster, this);
+  },
+
+
+  /* Called whenever a stanza is received */
+  onXmppStanza: function(aStanza) {
+  },
+
+  /* Called when a iq stanza is received */
+  onIQStanza: function(aStanza) {
+    if (aStanza.attributes["from"] == this._jid.domain) {
+      let ping = aStanza.getElement(["ping"]);
+      if (ping && ping.uri == Stanza.NS.ping) {
+        let s = Stanza.iq("result", aStanza.attributes["id"], this._jid.domain);
+        this._connection.sendStanza(s);
+      }
+    }
+  },
+
+  /* Called when a presence stanza is received */
+  onPresenceStanza: function(aStanza) {
+    let from = aStanza.attributes["from"];
+    DEBUG("Received presence stanza for " + from);
+
+    let jid = this._normalizeJID(from);
+    if (jid in this._buddies)
+      this._buddies[jid].onPresenceStanza(aStanza);
+    else if (jid in this._mucs) {
+      if (typeof(this._mucs[jid]) == "string") {
+        // We have attempted to join, but not created the conversation yet.
+        if (aStanza.attributes["type"] == "error") {
+          delete this._mucs[jid];
+          ERROR("Failed to join MUC: " + aStanza.convertToString());
+          return;
+        }
+        let nick = this._mucs[jid];
+        this._mucs[jid] = new this._MUCConversationConstructor(this, jid, nick);
+      }
+      this._mucs[jid].onPresenceStanza(aStanza);
+    }
+    else if (from != this._connection._jid.jid)
+      WARN("received presence stanza for unknown buddy " + from);
+  },
+
+  /* Called when a message stanza is received */
+  onMessageStanza: function(aStanza) {
+    let norm = this._normalizeJID(aStanza.attributes["from"]);
+
+    let body;
+    let b = aStanza.getElement(["body"]);
+    if (b)
+      body = b.getXML();
+    if (body) {
+      let date;
+      let delay = aStanza.getElement(["delay"]);
+      if (delay && delay.uri == Stanza.NS.delay) {
+        if (delay.attributes["stamp"])
+          date = new Date(delay.attributes["stamp"]);
+      }
+      if (date && isNaN(date))
+        date = undefined;
+      if (aStanza.attributes["type"] == "groupchat") {
+        if (!this._mucs.hasOwnProperty(norm)) {
+          WARN("Received a groupchat message for unknown MUC " + norm);
+          return;
+        }
+        this._mucs[norm].incomingMessage(body, aStanza, date);
+        return;
+      }
+
+      if (!this.createConversation(norm))
+        return;
+      this._conv[norm].incomingMessage(body, aStanza, date);
+    }
+
+    // Don't create a conversation to only display the typing notifications.
+    if (!this._conv.hasOwnProperty(norm))
+      return;
+
+    let state;
+    let s = aStanza.getChildrenByNS(Stanza.NS.chatstates);
+    if (s.length > 0)
+      state = s[0].localName;
+    if (state) {
+      DEBUG(state);
+      if (state == "active")
+        this._conv[norm].updateTyping(Ci.purpleIConvIM.NOT_TYPING);
+      else if (state == "composing")
+        this._conv[norm].updateTyping(Ci.purpleIConvIM.TYPING);
+      else if (state == "paused")
+        this._conv[norm].updateTyping(Ci.purpleIConvIM.TYPED);
+    }
+    else
+      this._conv[norm].supportChatStateNotifications = false;
+  },
+
+  /* Called when there is an error in the xmpp session */
+  onError: function(aError, aException) {
+    if (aError === null || aError === undefined)
+      aError = Ci.prplIAccount.ERROR_OTHER_ERROR;
+    this._disconnect(aError, aException.toString());
+  },
+
+  /* Callbacks for Query stanzas */
+  /* When a vCard is recieved */
+  onVCard: function(aStanza) {
+    let jid = this._normalizeJID(aStanza.attributes["from"]);
+    if (!jid || !this._buddies.hasOwnProperty(jid))
+      return;
+    let buddy = this._buddies[jid];
+
+    let vCard = aStanza.getElement(["vCard"]);
+    if (!vCard)
+      return;
+
+    for each (let c in vCard.children) {
+      if (c.type != "node")
+        continue;
+      if (c.localName == "FN")
+        buddy.serverAlias = c.innerText;
+      if (c.localName == "PHOTO")
+        buddy._saveIcon(c);
+    }
+  },
+
+  _normalizeJID: function(aJID) {
+    let slashIndex = aJID.indexOf("/");
+    if (slashIndex != -1)
+      aJID = aJID.substr(0, slashIndex);
+    return aJID.toLowerCase();
+  },
+
+  _parseJID: function(aJid) {
+    let match =
+      /^(?:([^@/<>'\"]+)@)?([^@/<>'\"]+)(?:\/([^<>'\"]*))?$/.exec(aJid);
+    if (!match)
+      return null;
+
+    return {jid: match[0], node: match[1], domain: match[2],
+            resource: match[3]};
+  },
+
+  /* When the roster is received */
+  onRoster: function(aStanza) {
+    for each (let qe in aStanza.getChildren("query")) {
+      if (qe.uri != Stanza.NS.roster)
+        continue;
+
+      for each (let item in qe.getChildren("item")) {
+        let jid = item.attributes["jid"];
+        if (!jid) {
+          WARN("Received a roster item without jid: " + item.getXML());
+          continue;
+        }
+        jid = this._normalizeJID(jid);
+
+        let subscription =  "";
+        if ("subscription" in item.attributes)
+          subscription = item.attributes["subscription"];
+        if (subscription == "both" || subscription == "to") {
+          let s = Stanza.iq("get", null, jid,
+                            Stanza.node("vCard", Stanza.NS.vcard));
+          this._connection.sendStanza(s, this.onVCard, this);
+        }
+        else
+          DEBUG("not subscribed to " + jid + "'s presence");
+
+        let buddy;
+        if (this._buddies.hasOwnProperty(jid))
+          buddy = this._buddies[jid];
+        else {
+          let tagName = _("defaultGroup");
+          for each (let group in item.getChildren("group")) {
+            let name = group.innerText;
+            if (name) {
+              tagName = name;
+              break; // TODO we should create an accountBuddy per group,
+                     // but this._buddies would probably not like that...
+            }
+          }
+          let tag = Services.tags.createTag(tagName);
+          buddy = new this._accountBuddyConstructor(this, null, tag, jid);
+        }
+
+        let alias = "name" in item.attributes ? item.attributes["name"] : "";
+        if (alias)
+          buddy._serverAlias = alias;
+        if (subscription)
+          buddy.subscription = subscription;
+        if (!this._buddies.hasOwnProperty(jid)) {
+          this._buddies[jid] = buddy;
+          Services.contacts.accountBuddyAdded(buddy);
+        }
+      }
+    }
+
+    this._sendPresence();
+    for each (let b in this._buddies) {
+      if (b.subscription == "both" || b.subscription == "to")
+        b.setStatus(Ci.imIStatusInfo.STATUS_OFFLINE, "");
+    }
+    this.reportConnected();
+  },
+
+  /* Public methods */
+  /* Send a stanza to a buddy */
+  sendStanza: function(aStanza) {
+    this._connection.sendStanza(aStanza);
+  },
+
+  // Variations of the XMPP protocol can change these default constructors:
+  _conversationConstructor: XMPPConversation,
+  _MUCConversationConstructor: XMPPMUCConversation,
+  _accountBuddyConstructor: XMPPAccountBuddy,
+
+  /* Create a new conversation */
+  createConversation: function(aNormalizedName) {
+    if (!this._buddies.hasOwnProperty(aNormalizedName)) {
+      ERROR("Trying to create a conversation; buddy not present: " + aNormalizedName);
+      return null;
+    }
+
+    if (!this._conv.hasOwnProperty(aNormalizedName)) {
+      this._conv[aNormalizedName] =
+        new this._conversationConstructor(this, this._buddies[aNormalizedName]);
+    }
+
+    return this._conv[aNormalizedName];
+  },
+
+  /* Remove an existing conversation */
+  removeConversation: function(aNormalizedName) {
+    if (aNormalizedName in this._conv)
+      delete this._conv[aNormalizedName];
+    else if (aNormalizedName in this._mucs)
+      delete this._mucs[aNormalizedName];
+  },
+
+  /* Private methods */
+
+  /* Disconnect from the server */
+  _disconnect: function(aError, aErrorMessage) {
+    if (!this._connection)
+      return;
+
+    if (aError === undefined)
+      aError = Ci.prplIAccount.NO_ERROR;
+    this.reportDisconnecting(aError, aErrorMessage);
+
+    for each (let b in this._buddies)
+      b.setStatus(Ci.imIStatusInfo.STATUS_UNKNOWN, "");
+
+    this._connection.disconnect();
+    delete this._connection;
+
+    this.reportDisconnected();
+  },
+
+  /* Set the user status on the server */
+  _sendPresence: function() {
+    delete this._shouldSendPresenceForIdlenessChange;
+
+    if (!this._connection)
+      return;
+
+    let si = this.imAccount.statusInfo;
+    let statusType = si.statusType;
+    let show = "";
+    if (statusType == Ci.imIStatusInfo.STATUS_UNAVAILABLE)
+      show = "dnd";
+    else if (statusType == Ci.imIStatusInfo.STATUS_AWAY ||
+             statusType == Ci.imIStatusInfo.STATUS_IDLE)
+      show = "away";
+    else if (statusType == Ci.imIStatusInfo.STATUS_OFFLINE) {
+      this.disconnect();
+      return;
+    }
+    let children = [];
+    if (show)
+      children.push(Stanza.node("show", null, null, show));
+    let statusText = si.statusText;
+    if (statusText)
+      children.push(Stanza.node("status", null, null, statusText));
+    if (this._idleSince) {
+      let time = Math.floor(Date.now() / 1000) - this._idleSince;
+      children.push(Stanza.node("query", Stanza.NS.last, {seconds: time}));
+    }
+    this._connection.sendStanza(Stanza.presence({"xml:lang": "en"}, children));
+  }
+};
new file mode 100644
--- /dev/null
+++ b/chat/protocols/xmpp/xmpp.manifest
@@ -0,0 +1,3 @@
+component {dde786d1-6f59-43d0-9bc8-b505a757fb30} xmpp.js
+contract @instantbird.org/purple/xmpp;1 {dde786d1-6f59-43d0-9bc8-b505a757fb30}
+#category im-protocol-plugin prpl-jabber @instantbird.org/purple/xmpp;1
--- a/im/content/account.js
+++ b/im/content/account.js
@@ -204,19 +204,17 @@ var account = {
 
     return aOpt.getString();
   },
 
   getListValue: function account_getListValue(aOpt) {
     if (this.prefs.prefHasUserValue(aOpt.name))
       return this.prefs.getCharPref(aOpt.name);
 
-    var list = aOpt.getList().QueryInterface(Ci.nsISimpleEnumerator);
-    return list.hasMoreElements() &&
-           list.getNext().value || "";
+    return aOpt.getListDefault();
   },
 
   populateProtoSpecificBox: function account_populate() {
     var gbox = document.getElementById("protoSpecific");
     var id = this.proto.id;
     for (let opt in this.getProtoOptions()) {
       var text = opt.label;
       var name = id + "-" + opt.name;
--- a/im/content/accountWizard.js
+++ b/im/content/accountWizard.js
@@ -324,16 +324,17 @@ var accountWizard = {
         box.appendChild(this.createTextbox("number", opt.getInt(),
                                            text, name));
         break;
       case opt.typeString:
         box.appendChild(this.createTextbox(null, opt.getString(), text, name));
         break;
       case opt.typeList:
         box.appendChild(this.createMenulist(opt.getList(), text, name));
+        document.getElementById(name).value = opt.getListDefault();
         break;
       default:
         throw "unknown preference type " + opt.type;
       }
       visible = true;
     }
     document.getElementById("protoSpecificGroupbox").hidden = !visible;
     if (visible) {
@@ -415,20 +416,17 @@ var accountWizard = {
         if (val != opt.getInt())
           this.prefs.push({opt: opt, name: name, value: val});
         break;
       case opt.typeString:
         if (val != opt.getString())
           this.prefs.push({opt: opt, name: name, value: val});
         break;
       case opt.typeList:
-        var list = opt.getList().QueryInterface(Ci.nsISimpleEnumerator);
-        var defaultVal = list.hasMoreElements() &&
-          list.getNext().value || "";
-        if (val != defaultVal)
+        if (val != opt.getListDefault())
           this.prefs.push({opt: opt, name: name, value: val});
         break;
       default:
         throw "unknown preference type " + opt.type;
       }
     }
 
     for (let i = 0; i < this.prefs.length; ++i) {
--- a/im/content/buddytooltip.xml
+++ b/im/content/buddytooltip.xml
@@ -377,16 +377,17 @@
      <method name="observe">
        <parameter name="aSubject"/>
        <parameter name="aTopic"/>
        <parameter name="aData"/>
        <body>
        <![CDATA[
          if (aSubject == this.buddy &&
              (aTopic == "account-buddy-status-changed" ||
+              aTopic == "account-buddy-status-detail-changed" ||
               aTopic == "account-buddy-display-name-changed" ||
               aTopic == "account-buddy-icon-changed"))
            this.updateTooltipFromBuddy(this.buddy, this.elt);
          else if (aTopic == "contact-preferred-buddy-changed" &&
                   aSubject.id == this.contact.id) {
            let buddy = this.contact.preferredBuddy;
            this.updateTooltipFromBuddy(buddy.preferredAccountBuddy, this.elt);
          }
--- a/im/content/conversation.xml
+++ b/im/content/conversation.xml
@@ -347,17 +347,17 @@
               style = styleProperties.join("; ");
               if (style)
                 msg = "<span style=\"" + style + "\">" + msg + "</span>";
             }
           }
           this._conv.sendMsg(msg);
         }
         else
-          this._conv.sendMsg(msg);
+          this._conv.sendMsg(account.HTMLEscapePlainText ? msg : aMsg);
         // reset the textbox to its original size
         this.resetInput();
       ]]>
       </body>
      </method>
 
      <method name="_onSplitterChange">
       <parameter name="aEvent"/>
--- a/im/content/joinchat.js
+++ b/im/content/joinchat.js
@@ -66,19 +66,19 @@ var joinChat = {
     let acc = document.getElementById("accountlist").selectedItem.account;
     let sep = document.getElementById("separatorRow2");
     let defaultValues = acc.getChatRoomDefaultFieldValues();
     joinChat._values = defaultValues;
     joinChat._fields = [];
     joinChat._account = acc;
 
     let protoId = acc.protocol.id;
-    document.getElementById("autojoin").visible = 
-      protoId == "prpl-irc" || protoId == "prpl-jabber" ||
-      protoId == "prpl-gtalk";
+    document.getElementById("autojoin").hidden =
+      !(protoId == "prpl-irc" || protoId == "prpl-jabber" ||
+      protoId == "prpl-gtalk");
 
     for (let field in getIter(acc.getChatRoomFields())) {
       let row = document.createElement("row");
 
       let label = document.createElement("label");
       let text = field.label;
       let match = /_(.)/.exec(text);
       if (match) {
@@ -148,17 +148,19 @@ var joinChat = {
 
     let conv = Services.conversations.getConversationByNameAndAccount(name,
                                                                       account,
                                                                       true);
     if (conv)
       Conversations.focusConversation(conv);
 
     if (document.getElementById("autojoin").checked) {
-      if (protoId != "prpl-irc")
+      if (protoId == "prpl-gtalk")
+        name += "/" + values.getValue("nick");
+      else if (protoId != "prpl-irc")
         name += "/" + values.getValue("handle");
 
       let prefBranch =
         Services.prefs.getBranch("messenger.account." + account.id + ".");
       let autojoin = [ ];
       if (prefBranch.prefHasUserValue(autoJoinPref)) {
         let prefValue = prefBranch.getCharPref(autoJoinPref);
         if (prefValue)
--- a/im/installer/package-manifest.in
+++ b/im/installer/package-manifest.in
@@ -423,21 +423,24 @@
 @BINPATH@/components/profileMigrator.manifest
 #endif
 @BINPATH@/components/contentHandler.js
 @BINPATH@/components/contentHandler.manifest
 @BINPATH@/components/ibCommandLineHandler.js
 @BINPATH@/components/ibCommandLineHandler.manifest
 @BINPATH@/components/ibStatusCommandLineHandler.js
 @BINPATH@/components/ibStatusCommandLineHandler.manifest
-@BINPATH@/components/facebookOverrideProtocol.js
-@BINPATH@/components/gtalkOverrideProtocol.js
-@BINPATH@/components/overrideProtocols.manifest
+@BINPATH@/components/facebook.js
+@BINPATH@/components/facebook.manifest
+@BINPATH@/components/gtalk.js
+@BINPATH@/components/gtalk.manifest
 @BINPATH@/components/twitter.js
 @BINPATH@/components/twitter.manifest
+@BINPATH@/components/xmpp.js
+@BINPATH@/components/xmpp.manifest
 @BINPATH@/components/smileProtocolHandler.js
 @BINPATH@/components/smileProtocolHandler.manifest
 @BINPATH@/components/logger.js
 @BINPATH@/components/logger.manifest
 
 ; [Default Preferences]
 ; All the pref files must be part of base to prevent migration bugs
 @BINPATH@/@PREF_DIR@/all-instantbird.js
--- a/purple/purplexpcom/public/purpleIPref.idl
+++ b/purple/purplexpcom/public/purpleIPref.idl
@@ -54,15 +54,16 @@ interface purpleIPref: nsISupports {
   readonly attribute short type;
   readonly attribute boolean masked;
 
   boolean     getBool();
   long        getInt();
   AUTF8String getString();
   // enumerator of purpleIKeyValuePair
   nsISimpleEnumerator getList();
+  AUTF8String getListDefault();
 };
 
 [scriptable, uuid(8fc16882-ba8e-432a-999f-0d4dc104234b)]
 interface purpleIKeyValuePair: nsISupports {
   readonly attribute AUTF8String name;
   readonly attribute AUTF8String value;
 };