Bug 714733 - Instant messaging in Thunderbird, r=bienvenu,bwinton,Standard8,mconley.
authorFlorian Quèze <florian@queze.net>
Tue, 13 Mar 2012 02:14:45 +0100
changeset 10250 8cdfed92867f885fda98664395236b7829947a1d
parent 10249 4b5da7e5d0680c6617ec743109e6efc88ca413da
child 10251 f3dd079d3da97f57cce376943b24fd456675f5ec
push id433
push userbugzilla@standard8.plus.com
push dateTue, 13 Mar 2012 21:38:53 +0000
treeherdercomm-aurora@f18ea070b063 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbienvenu, bwinton, Standard8, mconley
bugs714733
Bug 714733 - Instant messaging in Thunderbird, r=bienvenu,bwinton,Standard8,mconley.
chat/Makefile.in
chat/chat-prefs.js
chat/components/public/Makefile.in
chat/components/public/imIAccount.idl
chat/components/public/imIAccountsService.idl
chat/components/public/imICommandsService.idl
chat/components/public/imIContactsService.idl
chat/components/public/imIConversationsService.idl
chat/components/public/imICoreService.idl
chat/components/public/imILogger.idl
chat/components/public/imIStatusInfo.idl
chat/components/public/imITagsService.idl
chat/components/public/imIUserStatusInfo.idl
chat/components/public/prplIConversation.idl
chat/components/public/prplIMessage.idl
chat/components/public/prplIPref.idl
chat/components/public/prplIProtocol.idl
chat/components/public/prplIRequest.idl
chat/components/public/prplITooltipInfo.idl
chat/components/src/Makefile.in
chat/components/src/imAccounts.js
chat/components/src/imAccounts.manifest
chat/components/src/imCommands.js
chat/components/src/imCommands.manifest
chat/components/src/imContacts.js
chat/components/src/imContacts.manifest
chat/components/src/imConversations.js
chat/components/src/imConversations.manifest
chat/components/src/imCore.js
chat/components/src/imCore.manifest
chat/components/src/logger.js
chat/components/src/logger.manifest
chat/components/src/smileProtocolHandler.js
chat/components/src/smileProtocolHandler.manifest
chat/content/Makefile.in
chat/content/browserRequest.js
chat/content/browserRequest.xul
chat/content/conv.html
chat/content/convbrowser.xml
chat/content/jar.mn
chat/locales/Makefile.in
chat/locales/en-US/accounts.properties
chat/locales/en-US/commands.properties
chat/locales/en-US/conversations.properties
chat/locales/en-US/facebook.properties
chat/locales/en-US/irc.properties
chat/locales/en-US/status.properties
chat/locales/en-US/twitter.properties
chat/locales/en-US/xmpp.properties
chat/locales/jar.mn
chat/makefiles.sh
chat/modules/Makefile.in
chat/modules/hiddenWindow.jsm
chat/modules/http.jsm
chat/modules/imContentSink.jsm
chat/modules/imServices.jsm
chat/modules/imSmileys.jsm
chat/modules/imStatusUtils.jsm
chat/modules/imTextboxUtils.jsm
chat/modules/imThemes.jsm
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/irc/Makefile.in
chat/protocols/irc/icons/prpl-irc-32.png
chat/protocols/irc/icons/prpl-irc-48.png
chat/protocols/irc/icons/prpl-irc.png
chat/protocols/irc/irc.js
chat/protocols/irc/irc.manifest
chat/protocols/irc/ircBase.jsm
chat/protocols/irc/ircCTCP.jsm
chat/protocols/irc/ircCommands.jsm
chat/protocols/irc/ircDCC.jsm
chat/protocols/irc/ircHandlers.jsm
chat/protocols/irc/ircISUPPORT.jsm
chat/protocols/irc/ircUtils.jsm
chat/protocols/irc/jar.mn
chat/protocols/irc/test/test_ctcpColoring.js
chat/protocols/irc/test/test_ctcpFormatting.js
chat/protocols/irc/test/xpcshell.ini
chat/protocols/jsTest/Makefile.in
chat/protocols/jsTest/jsTestProtocol.js
chat/protocols/jsTest/jsTestProtocol.manifest
chat/protocols/twitter/Makefile.in
chat/protocols/twitter/icons/prpl-twitter-32.png
chat/protocols/twitter/icons/prpl-twitter-48.png
chat/protocols/twitter/icons/prpl-twitter.png
chat/protocols/twitter/jar.mn
chat/protocols/twitter/twitter.js
chat/protocols/twitter/twitter.manifest
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
chat/themes/Makefile.in
chat/themes/available-16.png
chat/themes/available.png
chat/themes/away-16.png
chat/themes/away.png
chat/themes/browserRequest.css
chat/themes/chat-16.png
chat/themes/chat-left-16.png
chat/themes/conv.css
chat/themes/icons/insecure.png
chat/themes/icons/prpl-generic-32.png
chat/themes/icons/prpl-generic-48.png
chat/themes/icons/prpl-generic.png
chat/themes/icons/prpl-unknown-32.png
chat/themes/icons/prpl-unknown-48.png
chat/themes/icons/prpl-unknown.png
chat/themes/icons/secure.png
chat/themes/idle-16.png
chat/themes/idle.png
chat/themes/jar.mn
chat/themes/mobile-16.png
chat/themes/mobile.png
chat/themes/offline-16.png
chat/themes/offline.png
chat/themes/typed-16.png
chat/themes/typing-16.png
chat/themes/unknown-16.png
chat/themes/unknown.png
mail/app/profile/all-thunderbird.js
mail/base/content/folderPane.js
mail/base/content/glodaFacetBindings.xml
mail/base/content/glodaFacetView.css
mail/base/content/glodaFacetView.js
mail/base/content/hiddenWindow.xul
mail/base/content/mailCore.js
mail/base/content/mailWindowOverlay.xul
mail/base/modules/mailMigrator.js
mail/build.mk
mail/components/Makefile.in
mail/components/im/Makefile.in
mail/components/im/all-im.js
mail/components/im/content/addbuddy.js
mail/components/im/content/addbuddy.xul
mail/components/im/content/am-im.js
mail/components/im/content/am-im.xul
mail/components/im/content/chat-messenger-overlay.js
mail/components/im/content/chat-messenger-overlay.xul
mail/components/im/content/chat.css
mail/components/im/content/imAccount.xml
mail/components/im/content/imAccountWizard.js
mail/components/im/content/imAccountWizard.xul
mail/components/im/content/imAccounts.css
mail/components/im/content/imAccounts.js
mail/components/im/content/imAccounts.xul
mail/components/im/content/imContextMenu.js
mail/components/im/content/imStatusSelector.js
mail/components/im/content/imbuddytooltip.xml
mail/components/im/content/imcontact.xml
mail/components/im/content/imconv.xml
mail/components/im/content/imconversation.xml
mail/components/im/content/imgroup.xml
mail/components/im/content/imsearch.xml
mail/components/im/content/joinchat.js
mail/components/im/content/joinchat.xul
mail/components/im/im.manifest
mail/components/im/imIncomingServer.js
mail/components/im/imProtocolInfo.js
mail/components/im/jar.mn
mail/components/im/messages/Bitmaps/minus-hover.png
mail/components/im/messages/Bitmaps/minus.png
mail/components/im/messages/Bitmaps/plus-hover.png
mail/components/im/messages/Bitmaps/plus.png
mail/components/im/messages/Footer.html
mail/components/im/messages/Incoming/Content.html
mail/components/im/messages/Incoming/Context.html
mail/components/im/messages/Incoming/NextContent.html
mail/components/im/messages/Info.plist
mail/components/im/messages/NextStatus.html
mail/components/im/messages/Status.html
mail/components/im/messages/main.css
mail/components/im/modules/index_im.js
mail/components/im/modules/search_im.js
mail/components/im/smileys/angry.png
mail/components/im/smileys/confused.png
mail/components/im/smileys/cool.png
mail/components/im/smileys/cry.png
mail/components/im/smileys/embarrassed.png
mail/components/im/smileys/grin.png
mail/components/im/smileys/heart.png
mail/components/im/smileys/manga_annoyed.png
mail/components/im/smileys/manga_embarrassed.png
mail/components/im/smileys/manga_smile.png
mail/components/im/smileys/manga_stunned.png
mail/components/im/smileys/manga_tired.png
mail/components/im/smileys/sad.png
mail/components/im/smileys/shocked.png
mail/components/im/smileys/slant.png
mail/components/im/smileys/slant2.png
mail/components/im/smileys/smile.png
mail/components/im/smileys/sp_laugh.png
mail/components/im/smileys/straight_face.png
mail/components/im/smileys/theme.js
mail/components/im/smileys/tongue.png
mail/components/im/smileys/wink.png
mail/components/im/themes/chat.css
mail/components/im/themes/founder.png
mail/components/im/themes/half-operator.png
mail/components/im/themes/imAccountWizard.css
mail/components/im/themes/imAccounts.css
mail/components/im/themes/imBuddytooltip.css
mail/components/im/themes/imMenulist.css
mail/components/im/themes/imRichlistbox.css
mail/components/im/themes/imStatus.css
mail/components/im/themes/operator.png
mail/components/im/themes/voice.png
mail/components/preferences/chat.js
mail/components/preferences/chat.xul
mail/components/preferences/jar.mn
mail/components/preferences/preferences.xul
mail/installer/package-manifest.in
mail/locales/Makefile.in
mail/locales/en-US/chrome/messenger/AccountManager.dtd
mail/locales/en-US/chrome/messenger/addbuddy.dtd
mail/locales/en-US/chrome/messenger/am-im.dtd
mail/locales/en-US/chrome/messenger/chat.dtd
mail/locales/en-US/chrome/messenger/chat.properties
mail/locales/en-US/chrome/messenger/imAccountWizard.dtd
mail/locales/en-US/chrome/messenger/imAccounts.dtd
mail/locales/en-US/chrome/messenger/imAccounts.properties
mail/locales/en-US/chrome/messenger/joinChat.dtd
mail/locales/en-US/chrome/messenger/messenger.dtd
mail/locales/en-US/chrome/messenger/preferences/chat.dtd
mail/locales/en-US/chrome/messenger/preferences/preferences.dtd
mail/locales/filter.py
mail/locales/jar.mn
mail/locales/l10n-aurora.ini
mail/locales/l10n-beta.ini
mail/locales/l10n-central.ini
mail/locales/l10n.ini
mail/makefiles.sh
mail/test/mozmill/migration/test-toolbar.js
mail/test/xpcshell.ini
mail/themes/gnomestripe/jar.mn
mail/themes/gnomestripe/mail/chat.css
mail/themes/gnomestripe/mail/icons/chat-toolbar-small.png
mail/themes/gnomestripe/mail/icons/chat-toolbar.png
mail/themes/gnomestripe/mail/icons/mail-toolbar-small.png
mail/themes/gnomestripe/mail/icons/mail-toolbar.png
mail/themes/gnomestripe/mail/icons/status-small.png
mail/themes/gnomestripe/mail/icons/status.png
mail/themes/gnomestripe/mail/preferences/chat.png
mail/themes/gnomestripe/mail/preferences/preferences.css
mail/themes/gnomestripe/mail/primaryToolbar.css
mail/themes/gnomestripe/mail/userIcon.png
mail/themes/pinstripe/jar.mn
mail/themes/pinstripe/mail/chat.css
mail/themes/pinstripe/mail/icons/chat-toolbar-small.png
mail/themes/pinstripe/mail/icons/chat-toolbar.png
mail/themes/pinstripe/mail/icons/mail-toolbar-small.png
mail/themes/pinstripe/mail/icons/mail-toolbar.png
mail/themes/pinstripe/mail/icons/status-small.png
mail/themes/pinstripe/mail/icons/status.png
mail/themes/pinstripe/mail/preferences/mail-options.png
mail/themes/pinstripe/mail/preferences/preferences.css
mail/themes/pinstripe/mail/primaryToolbar.css
mail/themes/pinstripe/mail/userIcon.png
mail/themes/qute/jar.mn
mail/themes/qute/mail/chat-aero.css
mail/themes/qute/mail/chat.css
mail/themes/qute/mail/icons/chat-toolbar-aero.png
mail/themes/qute/mail/icons/chat-toolbar-small.png
mail/themes/qute/mail/icons/chat-toolbar.png
mail/themes/qute/mail/icons/mail-toolbar-aero.png
mail/themes/qute/mail/icons/mail-toolbar-small.png
mail/themes/qute/mail/icons/mail-toolbar.png
mail/themes/qute/mail/icons/status-aero.png
mail/themes/qute/mail/icons/status-small-aero.png
mail/themes/qute/mail/icons/status-small.png
mail/themes/qute/mail/icons/status.png
mail/themes/qute/mail/preferences/chat-aero.png
mail/themes/qute/mail/preferences/chat.png
mail/themes/qute/mail/preferences/preferences.css
mail/themes/qute/mail/primaryToolbar-aero.css
mail/themes/qute/mail/primaryToolbar.css
mail/themes/qute/mail/userIcon.png
mailnews/base/prefs/content/AccountManager.js
mailnews/base/prefs/content/AccountManager.xul
mailnews/base/util/errUtils.js
mailnews/extensions/mdn/src/mdn-service.js
mailnews/extensions/smime/src/smime-service.js
new file mode 100644
--- /dev/null
+++ b/chat/Makefile.in
@@ -0,0 +1,68 @@
+# ***** 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 QUEZE <florian@instantbird.org>
+# Portions created by the Initial Developer are Copyright (C) 2008
+# 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
+
+PROTOCOLS = \
+		facebook \
+		gtalk \
+		irc \
+		twitter \
+		xmpp \
+		$(NULL)
+
+ifdef MOZ_DEBUG
+PROTOCOLS += jsTest
+endif
+
+PREF_JS_EXPORTS = $(srcdir)/chat-prefs.js
+
+PARALLEL_DIRS	= \
+		components/public \
+		components/src \
+		modules \
+		content \
+		themes \
+		locales \
+		$(foreach proto,$(PROTOCOLS),protocols/$(proto)) \
+		$(NULL)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/chat/chat-prefs.js
@@ -0,0 +1,93 @@
+// What to do when starting up
+//  0 = do not connect / show the account manager
+//  1 = connect automatically
+//  Other values will be added later, for example to start minimized
+pref("messenger.startup.action", 1);
+
+pref("messenger.accounts", "");
+
+// Should the accounts service stored in the password manager the
+// passwords that are currently stored in the preferences?
+pref("messenger.accounts.convertOldPasswords", false);
+
+// The intervals in seconds between automatic reconnection attempts.
+// The last value will be reused for the rest of the reconnection attempts.
+// A value of 0 means that there will be no more reconnection attempts.
+pref("messenger.accounts.reconnectTimer", "1,5,30,60,90,300,600,1200,3600");
+
+// List of tags ids whose contacts should be shown in the special
+// "Other contacts" group.
+pref("messenger.buddies.hiddenTags", "");
+
+//  1 accepts invitations automatically,
+//  0 ignores the invitations,
+// -1 rejects the invitations.
+pref("messenger.conversations.autoAcceptChatInvitations", 1);
+
+// Indicates whether the core should always close conversations closed
+// by the UI or if they can be put on hold instead.
+pref("messenger.conversations.alwaysClose", false);
+
+pref("messenger.conversations.selections.magicCopyEnabled", true);
+pref("messenger.conversations.selections.ellipsis", "chrome://chat/locale/conversations.properties");
+pref("messenger.conversations.selections.systemMessagesTemplate", "chrome://chat/locale/conversations.properties");
+pref("messenger.conversations.selections.contentMessagesTemplate", "chrome://chat/locale/conversations.properties");
+pref("messenger.conversations.selections.actionMessagesTemplate", "chrome://chat/locale/conversations.properties");
+
+pref("messenger.conversations.textbox.autoResize", true);
+pref("messenger.conversations.textbox.defaultMaxLines", 5);
+
+pref("messenger.conversations.sendFormat", true);
+
+// this preference changes how we filter incoming messages
+// 0 = no formattings
+// 1 = basic formattings (bold, italic, underlined)
+// 2 = permissive mode (colors, font face, font size, ...)
+pref("messenger.options.filterMode", 2);
+
+// use "none" to disable
+pref("messenger.options.emoticonsTheme", "default");
+pref("messenger.options.messagesStyle.theme", "bubbles");
+pref("messenger.options.messagesStyle.variant", "default");
+pref("messenger.options.messagesStyle.showHeader", false);
+pref("messenger.options.messagesStyle.combineConsecutive", true);
+// if the time interval in seconds between two messages is longer than
+// this value, the messages will not be combined
+pref("messenger.options.messagesStyle.combineConsecutiveInterval", 300); // 5 minutes
+
+pref("messenger.status.reportIdle", true);
+pref("messenger.status.timeBeforeIdle", 300); // 5 minutes
+pref("messenger.status.awayWhenIdle", true);
+pref("messenger.status.defaultIdleAwayMessage", "chrome://chat/locale/status.properties");
+pref("messenger.status.userIconFileName", "");
+pref("messenger.status.userDisplayName", "");
+
+// Default message used when quitting IRC. This is overridable per account.
+pref("chat.irc.defaultQuitMessage", "");
+
+// loglevel is the minimum severity level that a libpurple message
+// must have to be reported in the Error Console.
+//
+// The possible values are:
+//   0  Show all libpurple messages (PURPLE_DEBUG_ALL)
+//   1  Very verbose (PURPLE_DEBUG_MISC)
+//   2  Verbose (PURPLE_DEBUG_INFO)
+//   3  Show warnings (PURPLE_DEBUG_WARNING)
+//   4  Show errors (PURPLE_DEBUG_ERROR)
+//   5  Show only fatal errors (PURPLE_DEBUG_FATAL)
+
+// Setting the loglevel to a value smaller than 2 will cause messages
+// with an INFO or MISC severity to be displayed as warnings so that
+// their file URL is clickable
+#ifndef DEBUG
+// By default, show only warning and errors
+pref("purple.debug.loglevel", 3);
+#else
+// On debug builds, show warning, errors and debug information.
+pref("purple.debug.loglevel", 2);
+#endif
+
+pref("purple.logging.format", "json");
+pref("purple.logging.log_chats", true);
+pref("purple.logging.log_ims", true);
+pref("purple.logging.log_system", true);
new file mode 100644
--- /dev/null
+++ b/chat/components/public/Makefile.in
@@ -0,0 +1,65 @@
+# ***** 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
+
+MODULE		= chat
+
+XPIDLSRCS	= \
+		imIAccount.idl \
+		imIAccountsService.idl \
+		imICommandsService.idl \
+		imIContactsService.idl \
+		imIConversationsService.idl \
+		imICoreService.idl \
+		imILogger.idl \
+		imIStatusInfo.idl \
+		imITagsService.idl \
+		imIUserStatusInfo.idl \
+		prplIConversation.idl \
+		prplIMessage.idl \
+		prplIPref.idl \
+		prplIProtocol.idl \
+		prplIRequest.idl \
+		prplITooltipInfo.idl \
+		$(NULL)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imIAccount.idl
@@ -0,0 +1,295 @@
+/* ***** 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
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * 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 ***** */
+
+#include "nsISupports.idl"
+#include "prplIConversation.idl"
+#include "imIUserStatusInfo.idl"
+
+interface imITag;
+interface imIBuddy;
+interface imIAccountBuddy;
+interface imIAccount;
+interface prplIProtocol;
+interface purpleIProxyInfo;
+
+/*
+ * Used to join chat rooms.
+ */
+
+[ptr] native GHashTablePtr(GHashTable);
+
+[scriptable, uuid(ea7ab156-d25e-46cc-93ef-29f77b3c0795)]
+interface prplIChatRoomFieldValues: nsISupports {
+  AUTF8String getValue(in AUTF8String aIdentifier);
+  void setValue(in AUTF8String aIdentifier, in AUTF8String aValue);
+
+  [noscript] readonly attribute GHashTablePtr hashTable;
+};
+
+[scriptable, uuid(19dff981-b125-4a70-bc1a-efc783d07137)]
+interface prplIChatRoomField: nsISupports {
+  readonly attribute AUTF8String label;
+  readonly attribute AUTF8String identifier;
+  readonly attribute boolean required;
+
+  const short TYPE_TEXT = 0;
+  const short TYPE_PASSWORD = 1;
+  const short TYPE_INT = 2;
+
+  readonly attribute short type;
+  readonly attribute long min;
+  readonly attribute long max;
+};
+
+
+/*
+ * This interface should be implemented by the protocol plugin.
+ */
+[scriptable, uuid(fb1b29cb-63ba-4335-9f44-63aea3f616a3)]
+interface prplIAccount: nsISupports {
+  readonly attribute imIAccount imAccount;
+
+  // observe should only be called by the imIAccount
+  // implementation to report user status changes that affect this account.
+  void observe(in nsISupports aObj, in string aEvent,
+               [optional] in wstring aData);
+
+  /* Uninitialize the prplIAccount instance. This is typically done
+     automatically at shutdown (by the core service) or as part of
+     the 'remove' method. */
+  void unInit();
+
+  void connect();
+  void disconnect();
+
+  prplIConversation createConversation(in AUTF8String aName);
+
+  // Used when the user wants to add a buddy to the buddy list
+  void addBuddy(in imITag aTag, in AUTF8String aName);
+
+  // Used while loading the buddy list at startup.
+  imIAccountBuddy loadBuddy(in imIBuddy aBuddy, in imITag aTag);
+
+  /* Request more info on a buddy (typically a chat buddy).
+   * The result (if any) will be provided by a user-info-received
+   * notification dispatched through the observer service:
+   *  - aSubject will be an nsISimpleEnumerator of prplITooltipInfo.
+   *  - aData will be aBuddyName.
+   */
+  void requestBuddyInfo(in AUTF8String aBuddyName);
+
+  readonly attribute boolean canJoinChat;
+  nsISimpleEnumerator getChatRoomFields();
+  prplIChatRoomFieldValues getChatRoomDefaultFieldValues([optional] in AUTF8String aDefaultChatName);
+  /*
+   * Create a new chat conversation if it doesn't already exist.
+   */
+  void joinChat(in prplIChatRoomFieldValues aComponents);
+
+  readonly attribute AUTF8String normalizedName;
+
+  attribute purpleIProxyInfo proxyInfo;
+
+  // protocol specific options: those functions set the protocol
+  // specific options for the PurpleAccount
+  void setBool(in string aName, in boolean aVal);
+  void setInt(in string aName, in long aVal);
+  void setString(in string aName, in AUTF8String aVal);
+
+  /* When a connection error occurred, this value indicates the type of error */
+  readonly attribute short connectionErrorReason;
+
+  /* Possible connection error reasons:
+     ERROR_NETWORK_ERROR and ERROR_ENCRYPTION_ERROR are not fatal and
+     should enable the automatic reconnection feature. */
+  const short NO_ERROR = -1;
+  const short ERROR_NETWORK_ERROR = 0;
+  const short ERROR_INVALID_USERNAME = 1;
+  const short ERROR_AUTHENTICATION_FAILED = 2;
+  const short ERROR_AUTHENTICATION_IMPOSSIBLE = 3;
+  const short ERROR_NO_SSL_SUPPORT = 4;
+  const short ERROR_ENCRYPTION_ERROR = 5;
+  const short ERROR_NAME_IN_USE = 6;
+  const short ERROR_INVALID_SETTINGS = 7;
+  const short ERROR_CERT_NOT_PROVIDED = 8;
+  const short ERROR_CERT_UNTRUSTED = 9;
+  const short ERROR_CERT_EXPIRED = 10;
+  const short ERROR_CERT_NOT_ACTIVATED = 11;
+  const short ERROR_CERT_HOSTNAME_MISMATCH = 12;
+  const short ERROR_CERT_FINGERPRINT_MISMATCH = 13;
+  const short ERROR_CERT_SELF_SIGNED = 14;
+  const short ERROR_CERT_OTHER_ERROR = 15;
+  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;
+
+  //   PURPLE_CONNECTION_FORMATTING_WBFO
+  //    The text buffer must be formatted as a whole.
+  readonly attribute boolean singleFormatting;
+
+  //   PURPLE_CONNECTION_NO_NEWLINES
+  //    No new lines are allowed in outgoing messages.
+  readonly attribute boolean noNewlines;
+
+  //   PURPLE_CONNECTION_NO_FONTSIZE
+  //    Connection does not send/receive font sizes.
+  readonly attribute boolean noFontSizes;
+
+  //   PURPLE_CONNECTION_NO_URLDESC
+  //    Connection does not support descriptions with links.
+  readonly attribute boolean noUrlDesc;
+
+  //   PURPLE_CONNECTION_NO_IMAGES
+  //    Connection does not support sending of images.
+  readonly attribute boolean noImages;
+
+  // This is currently used only by Twitter.
+  readonly attribute long maxMessageLength;
+};
+
+/* This interface should be implemented by the im core. It inherits
+from prplIAccount and in most cases will forward the calls for the
+inherited members to a prplIAccount account instance implemented by
+the protocol plugin. */
+[scriptable, uuid(20a85b44-e220-4f23-85bf-f8523d1a2b08)]
+interface imIAccount: prplIAccount {
+  /* Check if autologin is enabled for this account, connect it now. */
+  void checkAutoLogin();
+
+  /* Cancel the timer that automatically reconnects the account if it was
+     disconnected because of a non fatal error. */
+  void cancelReconnection();
+
+  readonly attribute AUTF8String name;
+  readonly attribute AUTF8String id;
+  readonly attribute unsigned long numericId;
+  readonly attribute prplIProtocol protocol;
+  readonly attribute prplIAccount prplAccount;
+
+  // Save account specific preferences to disk.
+  void save();
+
+  attribute boolean autoLogin;
+
+  /* This is the value when the preference firstConnectionState is not set.
+     It indicates that the account has already been successfully connected at
+     least once with the current parameters. */
+  const short FIRST_CONNECTION_OK = 0;
+  /* Set when the account has never had a successful connection
+     with the current parameters */
+  const short FIRST_CONNECTION_UNKNOWN = 1;
+  /* Set when the account is trying to connect for the first time
+     with the current parameters (removed after a successsful connection) */
+  const short FIRST_CONNECTION_PENDING = 2;
+  /* Set at startup when the previous state was pending */
+  const short FIRST_CONNECTION_CRASHED = 4;
+
+  attribute short firstConnectionState;
+
+  // Passwords are stored in the toolkit Password Manager.
+  attribute AUTF8String password;
+
+  attribute AUTF8String alias;
+
+  /* While an account is connecting, this attribute contains a message
+     indicating the current step of the connection */
+  readonly attribute AUTF8String connectionStateMsg;
+
+  /* Number of the reconnection attempt
+   *  0 means that no automatic reconnection currently pending
+   *  n means the nth reconnection attempt is pending
+   */
+  readonly attribute unsigned short reconnectAttempt;
+
+  /* Time stamp of the next reconnection attempt */
+  readonly attribute long long timeOfNextReconnect;
+
+  /* Time stamp of the last connection (value not reliable if not connected) */
+  readonly attribute long long timeOfLastConnect;
+
+  /* Additional possible connection error reasons:
+   * (Use a big enough number that it can't conflict with error
+   *  codes used in prplIAccount).
+   */
+  const short ERROR_UNKNOWN_PRPL = 42;
+  const short ERROR_CRASHED = 43;
+  const short ERROR_MISSING_PASSWORD = 44;
+
+  /* A message describing the connection error */
+  readonly attribute AUTF8String connectionErrorMessage;
+
+  /* Info about the connection state and flags */
+  const short STATE_DISCONNECTED = 0;
+  const short STATE_CONNECTED = 1;
+  const short STATE_CONNECTING = 2;
+  const short STATE_DISCONNECTING = 3;
+
+  readonly attribute short connectionState;
+
+  /* The following 4 properties use the above connectionState value. */
+  readonly attribute boolean disconnected;
+  readonly attribute boolean connected;
+  readonly attribute boolean connecting;
+  readonly attribute boolean disconnecting;
+
+  /* The imIUserStatusInfo instance this account should observe for
+     status changes. When this is null (the default value), the
+     account will observe the global status. */
+  attribute imIUserStatusInfo observedStatusInfo;
+  // Same as above, but never null (it fallbacks to the global status info).
+  attribute imIUserStatusInfo statusInfo;
+
+  // imIAccount also implements an observe method but this
+  // observe should only be called by the prplIAccount
+  // implementations to report connection status changes.
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imIAccountsService.idl
@@ -0,0 +1,97 @@
+/* ***** 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
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * 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 ***** */
+
+#include "nsISupports.idl"
+#include "nsISimpleEnumerator.idl"
+#include "imIAccount.idl"
+
+[scriptable, uuid(b3b6459a-5c26-47b8-8e9c-ba838b6f632a)]
+interface imIAccountsService: nsISupports {
+  void initAccounts();
+  void unInitAccounts();
+
+  /* This attribute is set to AUTOLOGIN_ENABLED by default. It can be set to
+     any other value before the initialization of this service to prevent
+     accounts with autoLogin enabled from being connected when libpurple is
+     initialized.
+     Any value other than the ones listed below will disable autoLogin and
+     display a generic message in the Account Manager. */
+  attribute short autoLoginStatus;
+
+  const short AUTOLOGIN_ENABLED = 0;
+  const short AUTOLOGIN_USER_DISABLED = 1;
+  const short AUTOLOGIN_SAFE_MODE = 2;
+  const short AUTOLOGIN_CRASH = 3;
+  const short AUTOLOGIN_START_OFFLINE = 4;
+
+  /* The method should be used to connect all accounts with autoLogin enabled.
+     Some use cases:
+       - if the autologin was disabled at startup
+       - after a loss of internet connectivity that disconnected all accounts.
+  */
+  void processAutoLogin();
+
+  imIAccount getAccountById(in AUTF8String aAccountId);
+
+  /* will throw NS_ERROR_FAILURE if not found */
+  imIAccount getAccountByNumericId(in unsigned long aAccountId);
+
+  nsISimpleEnumerator getAccounts();
+
+  /* will fire the event account-added */
+  imIAccount createAccount(in AUTF8String aName, in AUTF8String aPrpl);
+
+  /* will fire the event account-removed */
+  void deleteAccount(in AUTF8String aAccountId);
+};
+
+/*
+ account related notifications sent to nsIObserverService:
+   - account-added: a new account has been created
+   - account-removed: the account has been deleted
+   - account-connecting: the account is being connected
+   - account-connected: the account is now connected
+   - account-connect-error: the account is disconnect with an error.
+     (before account-disconnecting)
+   - account-disconnecting: the account is being disconnected
+   - account-disconnected: the account is now disconnected
+   - account-updated: when some settings have changed
+   - account-list-updated: when the list of account is reordered.
+   These events can be watched using an nsIObserver.
+   The associated imIAccount will be given as a parameter
+   (except for account-list-updated).
+*/
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imICommandsService.idl
@@ -0,0 +1,110 @@
+/* ***** 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
+ * 2011.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * 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 ***** */
+
+#include "nsISupports.idl"
+interface prplIConversation;
+
+[scriptable, uuid(dae17fae-0d04-4d89-a817-637ef433383f)]
+interface imICommand: nsISupports {
+  readonly attribute AUTF8String name;
+
+  // Help message displayed when the user types /help <name>.
+  // Format: <command name> <parameters>: <help message>
+  // Example: "help &lt;name&gt;: show the help message for the &lt;name&gt;
+  //           command, or the list of possible commands when used without
+  //           parameter."
+  readonly attribute AUTF8String helpString;
+
+  const short CONTEXT_IM   = 1;
+  const short CONTEXT_CHAT = 2;
+  const short CONTEXT_ALL  = CONTEXT_IM | CONTEXT_CHAT;
+  readonly attribute long usageContext;
+
+  const short PRIORITY_LOW     = -1000;
+  const short PRIORITY_DEFAULT = 0;
+  const short PRIORITY_PRPL    = 1000;
+  const short PRIORITY_HIGH    = 4000;
+  // Any integer value is usable as a priority.
+  //    0 is the default priority.
+  //  < 0 is lower priority.
+  //  > 0 is higher priority.
+  // Commands registered by protocol plugins will usually use PRIORITY_PRPL.
+  readonly attribute long priority;
+
+  // Will return true if the command handled the message (it should not be sent).
+  // The leading slash, the command name and the following space are not included
+  // in the aMessage parameter.
+  boolean run(in AUTF8String aMessage,
+              [optional] in prplIConversation aConversation);
+};
+
+[scriptable, uuid(467709a0-0bed-4f44-9bdc-13f78b9eaeba)]
+interface imICommandsService: nsISupports {
+  void initCommands();
+  void unInitCommands();
+
+  // Commands registered without a protocol id will work for all protocols.
+  // Registering several commands of the same name with the same
+  // protocol id or no protocol id will replace the former command
+  // with the latter.
+  void registerCommand(in imICommand aCommand,
+                       [optional] in AUTF8String aPrplId);
+
+  // aPrplId should be the same as what was used for the command registration.
+  void unregisterCommand(in AUTF8String aCommandName,
+                         [optional] in AUTF8String aPrplId);
+
+  void listCommandsForConversation(
+    [optional] in prplIConversation aConversation,
+    [optional] out unsigned long commandCount,
+    [retval, array, size_is(commandCount)] out imICommand commands);
+
+  void listCommandsForProtocol(in AUTF8String aPrplId,
+    [optional] out unsigned long commandCount,
+    [retval, array, size_is(commandCount)] out imICommand commands);
+
+  // Will return true if a command handled the message (it should not be sent).
+  // The aConversation parameters is required to execute protocol specific
+  // commands. Application global commands will work without it.
+  boolean executeCommand(in AUTF8String aMessage,
+                         [optional] in prplIConversation aConversation);
+};
+
+%{ C++
+#define IM_COMMANDS_SERVICE_CONTRACTID \
+  "@mozilla.org/chat/commands-service;1"
+%}
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imIContactsService.idl
@@ -0,0 +1,269 @@
+/* ***** 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
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * 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 ***** */
+
+#include "imIStatusInfo.idl"
+#include "imITagsService.idl"
+#include "nsISupports.idl"
+#include "nsIObserver.idl"
+#include "nsISimpleEnumerator.idl"
+
+interface imIContact;
+interface imIBuddy;
+interface imIAccountBuddy;
+interface imIAccount;
+interface prplIProtocol;
+
+[scriptable, uuid(f1619b49-310b-47aa-ab1c-238aba084c62)]
+interface imIContactsService: nsISupports {
+  void initContacts();
+  void unInitContacts();
+
+  imIContact getContactById(in long aId);
+  imIBuddy getBuddyById(in long aId);
+  imIBuddy getBuddyByNameAndProtocol(in AUTF8String aNormalizedName,
+                                     in prplIProtocol aPrpl);
+
+  // These 3 functions are called by the protocol plugins when
+  // synchronizing the buddy list with the server stored list,
+  // or after user operations have been performed.
+  void accountBuddyAdded(in imIAccountBuddy aAccountBuddy);
+  void accountBuddyRemoved(in imIAccountBuddy aAccountBuddy);
+  void accountBuddyMoved(in imIAccountBuddy aAccountBuddy,
+                         in imITag aOldTag, in imITag aNewTag);
+
+  // These methods are called by the imIAccountsService implementation
+  // to keep the accounts table in sync with accounts stored in the
+  // preferences.
+
+  // Called when an account is created or loaded to store the new
+  // account or ensure it doesn't conflict with an existing account
+  // (to detect database corruption).
+  // Will throw if a stored account has the id aId but a different
+  // username or prplId.
+  void storeAccount(in unsigned long aId, in AUTF8String aUserName,
+                    in AUTF8String aPrplId);
+  // Check if an account id already exists in the database.
+  boolean accountIdExists(in unsigned long aId);
+  // Called when deleting an account to remove it from blist.sqlite.
+  void forgetAccount(in unsigned long aId);
+};
+
+[scriptable, uuid(f585b0df-f6ad-40d5-9de4-c58b14af13e4)]
+interface imIContact: imIStatusInfo {
+  // The id will be positive if the contact is real (stored in the
+  // SQLite database) and negative if the instance is a dummy contact
+  // holding only a single buddy without aliases or additional tags.
+  readonly attribute long id;
+  attribute AUTF8String alias;
+
+  void getTags([optional] out unsigned long tagCount,
+               [retval, array, size_is(tagCount)] out imITag tags);
+
+  // Will do nothing if the contact already has aTag.
+  void addTag(in imITag aTag);
+  // Will throw if the contact doesn't have aTag or doesn't have any other tag.
+  void removeTag(in imITag aTag);
+
+  readonly attribute imIBuddy preferredBuddy;
+  void getBuddies([optional] out unsigned long buddyCount,
+                  [retval, array, size_is(buddyCount)] out imIBuddy buddies);
+
+  // Move all the buddies of aContact into the current contact,
+  // and copy all its tags.
+  void mergeContact(in imIContact aContact);
+
+  // Change the position of aBuddy in the current contact.
+  // The new position is the current position of aBeforeBuddy if it is
+  // specified, or at the end otherwise.
+  void moveBuddyBefore(in imIBuddy aBuddy, [optional] in imIBuddy aBeforeBuddy);
+
+  // Remove aBuddy from its current contact and append it to the list
+  // of buddies of the current contact.
+  // aBuddy should not already be attached to the current contact.
+  void adoptBuddy(in imIBuddy aBuddy);
+
+  // Returns a new contact that contains only aBuddy, and has the same
+  // list of tags.
+  // Will throw if aBuddy is not a buddy of the contact.
+  imIContact detachBuddy(in imIBuddy aBuddy);
+
+  // remove the contact from the buddy list. Will also remove the
+  // associated buddies.
+  void remove();
+
+  void addObserver(in nsIObserver aObserver);
+  void removeObserver(in nsIObserver aObserver);
+  /* Observers will be notified of changes related to the contact.
+   *  aSubject will point to the imIContact object
+   *  (with some exceptions for contact-moved-* notifications).
+   *
+   *  Fired notifications:
+   *   contact-availability-changed
+   *     when either statusType or availabilityDetails has changed.
+   *   contact-signed-on
+   *   contact-signed-off
+   *   contact-status-changed
+   *     when either statusType or statusText has changed.
+   *   contact-display-name-changed
+   *     when the alias (or serverAlias of the most available buddy if
+   *     no alias is set) has changed.
+   *     The old display name is provided in aData.
+   *   contact-preferred-buddy-changed
+   *     The buddy that would be favored to start a conversation has changed.
+   *   contact-moved, contact-moved-in, contact-moved-out
+   *     contact-moved     is notified through the observer service
+   *     contact-moved-in  is notified to
+   *      - the contact observers (aSubject is the new tag)
+   *      - the new tag           (aSubject is the contact instance)
+   *     contact-moved-out is notified to
+   *      - the contact observers (aSubject is the old tag)
+   *      - the old tag           (aSubject is the contact instance)
+   *   contact-no-longer-dummy
+   *     When a real contact is created to replace a dummy contact.
+   *     The old (negative) id will be given in aData.
+   *     See also the comment above the 'id' attribute.
+   *   contact-icon-changed
+   *
+   * Observers will also receive all the (forwarded) notifications
+   * from the linked buddies (imIBuddy instances) and their account
+   * buddies (imIAccountBuddy instances).
+   */
+
+  // Exposed for add-on authors. All internal calls will come from the
+  // imIContact implementation itself so it wasn't required to expose this.
+  // This can be used to dispatch custom notifications to the
+  // observers of the contact and its tags.
+  // The notification will also be forwarded to the observer service.
+  void notifyObservers(in nsISupports aObj, in string aEvent,
+                       [optional] in wstring aData);
+};
+
+
+[scriptable, uuid(fea582a0-3839-4d80-bcab-0ff82ae8f97f)]
+interface imIBuddy: imIStatusInfo {
+  readonly attribute long id;
+  readonly attribute prplIProtocol protocol;
+  readonly attribute AUTF8String userName; // may be formatted
+  readonly attribute AUTF8String normalizedName; // normalized userName
+  // The optional server alias is in displayName (inherited from imIStatusInfo)
+  // displayName = serverAlias || userName.
+
+  readonly attribute imIContact contact;
+  readonly attribute imIAccountBuddy preferredAccountBuddy;
+  void getAccountBuddies([optional] out unsigned long accountBuddyCount,
+                         [retval, array, size_is(accountBuddyCount)] out imIAccountBuddy accountBuddies);
+
+  // remove the buddy from the buddy list. If the contact becomes empty, it will be removed too.
+  void remove();
+
+  void addObserver(in nsIObserver aObserver);
+  void removeObserver(in nsIObserver aObserver);
+  /* Observers will be notified of changes related to the buddy.
+   *  aSubject will point to the imIBuddy object.
+   *  Fired notifications:
+   *   buddy-availability-changed
+   *     when either statusType or availabilityDetails has changed.
+   *   buddy-signed-on
+   *   buddy-signed-off
+   *   buddy-status-changed
+   *     when either statusType or statusText has changed.
+   *   buddy-display-name-changed
+   *     when the serverAlias has changed.
+   *     The old display name is provided in aData.
+   *   buddy-preferred-account-changed
+   *     The account that would be favored to start a conversation has changed.
+   *   buddy-icon-changed
+   *
+   * Observers will also receive all the (forwarded) notifications
+   * from the linked account buddies (imIAccountBuddy instances).
+   */
+
+  // Exposed for add-on authors. All internal calls will come from the
+  // imIBuddy implementation itself so it wasn't required to expose this.
+  // This can be used to dispatch custom notifications to the
+  // observers of the buddy, its contact and its tags.
+  // The contact will forward the notifications to the observer service.
+  void notifyObservers(in nsISupports aObj, in string aEvent,
+                       [optional] in wstring aData);
+
+  // observe should only be called by the imIAccountBuddy
+  // implementations to report changes.
+  void observe(in nsISupports aObj, in string aEvent,
+               [optional] in wstring aData);
+};
+
+/* imIAccountBuddy implementations can send notifications to their buddy:
+ *
+ * For all of them, aSubject points to the imIAccountBuddy object.
+ *
+ * Supported notifications:
+ *  account-buddy-availability-changed
+ *    when either statusType or availabilityDetails has changed.
+ *  account-buddy-signed-on
+ *  account-buddy-signed-off
+ *  account-buddy-status-changed
+ *    when either statusType or statusText has changed.
+ *  account-buddy-display-name-changed
+ *    when the serverAlias has changed.
+ *    The old display name is provided in aData.
+ *  account-buddy-icon-changed
+ *
+ * All notifications (even unsupported ones) will be forwarded to the contact,
+ * its tags and nsObserverService.
+ */
+[scriptable, uuid(114d24ff-56a1-4fd6-9822-4992efb6e036)]
+interface imIAccountBuddy: imIStatusInfo {
+  // The setter is for internal use only. buddy will be set by the
+  // Contacts service when accountBuddyAdded is called on this
+  // instance of imIAccountBuddy.
+           attribute imIBuddy buddy;
+  readonly attribute imIAccount account;
+  // Setting the tag will move the buddy to a different group on the
+  // server-stored buddy list.
+           attribute imITag tag;
+  readonly attribute AUTF8String userName;
+  readonly attribute AUTF8String normalizedName;
+           attribute AUTF8String serverAlias;
+
+  // remove the buddy from the buddy list of this account.
+  void remove();
+
+  // Called by the contacts service during its uninitialization to
+  // notify that all references kept to imIBuddy or imIAccount
+  // instances should be released now.
+  void unInit();
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imIConversationsService.idl
@@ -0,0 +1,98 @@
+/* ***** 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.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * 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 ***** */
+
+#include "nsISupports.idl"
+#include "prplIConversation.idl"
+#include "imIContactsService.idl"
+
+interface prplIMessage;
+
+[scriptable, uuid(a09faf46-bb9d-402f-b460-89f8d7827ff1)]
+interface imIConversation: prplIConversation {
+  // Will be null for MUCs and IMs from people not in the contacts list.
+  readonly attribute imIContact contact;
+
+  // Write a system message into the conversation.
+  // Note: this will not be logged.
+  void systemMessage(in AUTF8String aMessage, [optional] in boolean aIsError);
+
+  attribute prplIConversation target;
+
+  // Number of unread messages (all messages, including system
+  // messages are counted).
+  readonly attribute unsigned long unreadMessageCount;
+  // Number of unread incoming messages targeted at the user (= IMs or
+  // message containing the user's nick in MUCs).
+  readonly attribute unsigned long unreadTargetedMessageCount;
+  // Number of unread incoming messages (both targeted and untargeted
+  // messages are counted).
+  readonly attribute unsigned long unreadIncomingMessageCount;
+  // Reset all unread message counts.
+  void markAsRead();
+
+  // Call this to give the core an opportunity to close an inactive
+  // conversation.  If the conversation is a left MUC or an IM
+  // conversation without unread message, the implementation will call
+  // close().
+  // The returned value indicates if the conversation was closed.
+  boolean checkClose();
+
+  // Get an array of all messages of the conversation.
+  void getMessages([optional] out unsigned long messageCount,
+                   [retval, array, size_is(messageCount)] out prplIMessage messages);
+};
+
+[scriptable, uuid(984e182c-d395-4fba-ba6e-cc80c71f57bf)]
+interface imIConversationsService: nsISupports {
+  void initConversations();
+  void unInitConversations();
+
+  // register a conversation. This will create a unique id for the
+  // conversation and set it.
+  void addConversation(in prplIConversation aConversation);
+  void removeConversation(in prplIConversation aConversation);
+
+  void getUIConversations([optional] out unsigned long conversationCount,
+                          [retval, array, size_is(conversationCount)] out imIConversation conversations);
+  imIConversation getUIConversation(in prplIConversation aConversation);
+  imIConversation getUIConversationByContactId(in long aId);
+
+  nsISimpleEnumerator getConversations();
+  prplIConversation getConversationById(in unsigned long aId);
+  prplIConversation getConversationByNameAndAccount(in AUTF8String aName,
+                                                      in imIAccount aAccount,
+                                                      in boolean aIsChat);
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imICoreService.idl
@@ -0,0 +1,54 @@
+/* ***** 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
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * 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 ***** */
+
+#include "nsISupports.idl"
+#include "imIUserStatusInfo.idl"
+#include "nsISimpleEnumerator.idl"
+#include "prplIProtocol.idl"
+
+[scriptable, uuid(205d4b2b-1ccf-4879-9ef1-f08942566151)]
+interface imICoreService: nsISupports {
+  void init();
+  void quit(); // will emit a prpl-quit notification.
+
+  // returns an enumerator on a pplIProtocol array
+  nsISimpleEnumerator getProtocols();
+
+  prplIProtocol getProtocolById(in AUTF8String aProtocolId);
+
+  readonly attribute imIUserStatusInfo globalUserStatus;
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imILogger.idl
@@ -0,0 +1,87 @@
+/* ***** 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
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * 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 ***** */
+
+#include "nsISupports.idl"
+#include "nsISimpleEnumerator.idl"
+#include "nsIFile.idl"
+
+interface imIAccount;
+interface imIAccountBuddy;
+interface imIBuddy;
+interface imIContact;
+interface prplIConversation;
+interface prplIMessage;
+
+[scriptable, uuid(5bc06f3b-33a1-412b-a4d8-4fc7ba4c962b)]
+interface imILogConversation: nsISupports {
+  readonly attribute AUTF8String title;
+  readonly attribute AUTF8String name;
+
+  // Simplified account implementation:
+  //  - alias will always be empty
+  //  - name (always the normalizedName)
+  //  - statusInfo will return Services.core.globalUserStatus
+  //  - protocol will only contain a "name" attribute, with the prpl's normalized name.
+  // Other methods/attributes aren't implemented.
+  readonly attribute imIAccount account;
+
+  readonly attribute boolean isChat; // always false (compatibility with prplIConversation).
+  readonly attribute imIAccountBuddy buddy; // always null (compatibility with prplIConvIM).
+
+  void getMessages([optional] out unsigned long messageCount,
+                   [retval, array, size_is(messageCount)] out prplIMessage messages);
+};
+
+[scriptable, uuid(164ff6c3-ca64-4880-b8f3-67eb1817955f)]
+interface imILog: nsISupports {
+  readonly attribute AUTF8String path;
+  readonly attribute PRTime time;
+  readonly attribute AUTF8String format;
+  // Will return null if the log format isn't json.
+  imILogConversation getConversation();
+};
+
+[scriptable, uuid(ab38c01c-2245-4279-9437-1d6bcc69d556)]
+interface imILogger: nsISupports {
+  imILog getLogFromFile(in nsIFile aFile);
+  nsISimpleEnumerator getLogsForAccountBuddy(in imIAccountBuddy aAccountBuddy);
+  nsISimpleEnumerator getLogsForBuddy(in imIBuddy aBuddy);
+  nsISimpleEnumerator getLogsForContact(in imIContact aContact);
+  nsISimpleEnumerator getLogsForConversation(in prplIConversation aConversation);
+  nsISimpleEnumerator getSystemLogsForAccount(in imIAccount aAccount);
+  nsISimpleEnumerator getSimilarLogs(in imILog aLog);
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imIStatusInfo.idl
@@ -0,0 +1,87 @@
+/* ***** 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
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * 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 ***** */
+
+#include "nsISupports.idl"
+#include "nsISimpleEnumerator.idl"
+#include "prplIConversation.idl"
+
+[scriptable, uuid(f13dc4fc-5334-45cb-aa58-a92851955e55)]
+interface imIStatusInfo: nsISupports {
+  // Name suitable for display in the UI. Can either be the username,
+  // the server side alias, or the user set local alias of the contact.
+  readonly attribute AUTF8String displayName;
+  readonly attribute AUTF8String buddyIconFilename;
+
+  const short STATUS_UNKNOWN = 0;
+  const short STATUS_OFFLINE = 1;
+  const short STATUS_INVISIBLE = 2;
+  const short STATUS_MOBILE = 3;
+  const short STATUS_IDLE = 4;
+  const short STATUS_AWAY = 5;
+  const short STATUS_UNAVAILABLE = 6;
+  const short STATUS_AVAILABLE = 7;
+
+  // numerical value used to compare the availability of two buddies
+  // based on their current status.
+  //  Use it only for immediate comparisons, do not store the value,
+  //  it can change between versions for a same status of the buddy.
+  readonly attribute long statusType;
+
+  readonly attribute boolean online;    // (statusType > STATUS_OFFLINE)
+  readonly attribute boolean available; // (statusType == STATUS_AVAILABLE)
+  readonly attribute boolean idle;      // (statusType == STATUS_IDLE)
+  readonly attribute boolean mobile;    // (statusType == STATUS_MOBILE)
+
+  readonly attribute AUTF8String statusText;
+
+  // Gives more detail to compare the availability of two buddies with the same
+  // status type.
+  //  Example: 2 buddies may have been idle for various amounts of times.
+  readonly attribute long availabilityDetails;
+
+  // True if the buddy is online or if the account supports sending
+  // offline messages to the buddy.
+  readonly attribute boolean canSendMessage;
+
+  // enumerator of purpleTooltipInfo components
+  nsISimpleEnumerator getTooltipInfo();
+
+  // Will select the buddy automatically based on availability, and
+  // the account (if needed) based on the account order in the account
+  // manager.
+  prplIConversation createConversation();
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imITagsService.idl
@@ -0,0 +1,89 @@
+/* ***** 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
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * 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 ***** */
+
+#include "nsISupports.idl"
+#include "nsIObserver.idl"
+
+interface imIContact;
+
+[scriptable, uuid(c211e5e2-f0a4-4a86-9e4c-3f6b905628a5)]
+interface imITag: nsISupports {
+  readonly attribute long id;
+           attribute AUTF8String name;
+
+  // Get an array of all the contacts associated with this tag.
+  //  Contacts can either "have the tag" (added by user action) or
+  //  have inherited the tag because it was the server side group for
+  //  one of the AccountBuddy of the contact.
+  void getContacts([optional] out unsigned long contactCount,
+                   [retval, array, size_is(contactCount)] out imIContact contacts);
+
+  void addObserver(in nsIObserver aObserver);
+  void removeObserver(in nsIObserver aObserver);
+  /* Observers will be notified of changes related to the contacts
+   * that have the tag: contact-*, buddy-*, account-buddy-*
+   * notifications forwarded respectively from the imIContact,
+   * imIBuddy and imIAccountBuddy instances.
+   */
+
+  // Exposed for add-on authors. All internal calls will come from the
+  // imITag implementation itself so it wasn't required to expose this.
+  // This can be used to dispatch custom notifications to the
+  // observers of the tag.
+  void notifyObservers(in nsISupports aObj, in string aEvent,
+                       [optional] in wstring aData);
+};
+
+[scriptable, uuid(f799a9c2-23f2-4fd1-96fb-515bad238f8c)]
+interface imITagsService: nsISupports {
+  // Create a new tags or return the existing one if it already exists
+  imITag createTag(in AUTF8String aName);
+  // Get an existing tag by (numeric) id. Returns null if not found.
+  imITag getTagById(in long aId);
+  // Get an existing tag by name (will do an SQL query). Returns null
+  // if not found.
+  imITag getTagByName(in AUTF8String aName);
+  // Get an array of all existing tags.
+  void getTags([optional] out unsigned long tagCount,
+               [retval, array, size_is(tagCount)] out imITag tags);
+
+  boolean isTagHidden(in imITag aTag);
+  void hideTag(in imITag aTag);
+  void showTag(in imITag aTag);
+
+  readonly attribute imITag otherContactsTag;
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imIUserStatusInfo.idl
@@ -0,0 +1,76 @@
+/* ***** 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
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * 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 ***** */
+
+#include "nsISupports.idl"
+#include "nsIObserver.idl"
+
+//forward declarations
+interface nsILocalFile;
+interface nsIFileURL;
+
+[scriptable, uuid(817918fa-1f4b-4254-9cdb-f906da91c45d)]
+interface imIUserStatusInfo: nsISupports {
+
+  readonly attribute AUTF8String statusText;
+
+  // See imIStatusInfo for the values.
+  readonly attribute short statusType;
+
+  // Only works with STATUS_OFFLINE, STATUS_UNAVAILABLE, STATUS_AWAY,
+  // STATUS_AVAILABLE and STATUS_INVISIBLE.
+  // - When called with the status type STATUS_UNSET, only the status
+  //   message will be changed.
+  // - When called with STATUS_OFFLINE, the aMessage parameter is ignored.
+  void setStatus(in short aStatus, in AUTF8String aMessage);
+
+  /* Will fire an user-icon-changed notificaton. */
+  void setUserIcon(in nsILocalFile aIconFile);
+
+  nsIFileURL getUserIcon();
+
+  /* The setter will fire an user-display-name-changed notificaton. */
+  attribute AUTF8String displayName;
+
+  void addObserver(in nsIObserver aObserver);
+  void removeObserver(in nsIObserver aObserver);
+  /* Observers will receive the following notifications:
+   *   status-changed (when either the status type or text has changed)
+   *   user-icon-changed
+   *   user-display-name-changed
+   *   idle-time-changed
+   */
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/prplIConversation.idl
@@ -0,0 +1,179 @@
+/* ***** 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
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * 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 ***** */
+
+
+#include "nsISupports.idl"
+#include "nsISimpleEnumerator.idl"
+#include "nsIObserver.idl"
+
+interface imIAccountBuddy;
+interface imIAccount;
+interface nsIURI;
+interface nsIDOMDocument;
+
+/*
+ * This is the XPCOM purple conversation component, a proxy for PurpleConversation.
+ */
+
+[scriptable, uuid(74fca337-b8e7-4e5d-81cd-8aba3dba9208)]
+interface prplIConversation: nsISupports {
+
+  /* Indicate if this conversation implements prplIConvIM or prplIConvChat */
+  readonly attribute boolean isChat;
+
+  /* The account used for this conversation */
+  readonly attribute imIAccount account;
+
+  /* The name of the conversation, typically in English */
+  readonly attribute AUTF8String name;
+
+  /* The normalized name of the conversation (suitable for a log file name) */
+  readonly attribute AUTF8String normalizedName;
+
+  /* The title of the conversation, typically localized */
+  readonly attribute AUTF8String title;
+
+  /* Unique identifier of the conversation */
+  /* Setable only once by purpleCoreService while calling addConversation. */
+           attribute unsigned long id;
+
+  /* Send a message in the conversation */
+  void sendMsg(in AUTF8String aMsg);
+
+  /* Send information about the current typing state to the server.
+     aLength should contain the length of the content currently in the text field.
+     A length value of 0 will make the method send a NOT_TYPING message. */
+  void sendTyping(in unsigned long aLength);
+
+  /* Un-initialize the conversation. Will be called by
+     purpleCoreService::RemoveConversation when the conversation is
+     closed or by purpleCoreService::Quit while exiting. */
+  void unInit();
+
+  /* When the conversation is closed from the UI. */
+  void close();
+
+  /* Method to add or remove an observer */
+  void addObserver(in nsIObserver aObserver);
+  void removeObserver(in nsIObserver aObserver);
+
+  /* Observers will be all receive new-text notifications.
+     aSubject will contain the message (prplIMessage) */
+};
+
+[scriptable, uuid(0c072a80-103a-4992-b249-8e442b5f0d46)]
+interface prplIConvIM: prplIConversation {
+
+  /* The buddy at the remote end of the conversation */
+  readonly attribute imIAccountBuddy buddy;
+
+  /* The remote buddy is not currently typing */
+  const short NOT_TYPING = 0;
+
+  /* The remote buddy is currently typing */
+  const short TYPING = 1;
+
+  /* The remote buddy started typing, but has stopped typing */
+  const short TYPED = 2;
+
+  /* The typing state of the remote buddy.
+     The value is NOT_TYPING, TYPING or TYPED. */
+  readonly attribute short typingState;
+};
+
+[scriptable, uuid(c7f11466-f479-4f12-a581-b99713b8ecc0)]
+interface prplIConvChat: prplIConversation {
+
+  /* Get an nsISimpleEnumerator of prplIConvChatBuddy objects:
+     The list of people participating in this chat */
+  nsISimpleEnumerator getParticipants();
+
+  /* The normalized chat buddy name will be suitable for starting a
+     private conversation or calling requestBuddyInfo. */
+  AUTF8String getNormalizedChatBuddyName(in AUTF8String aChatBuddyName);
+
+  /* The topic of this chat room */
+           attribute AUTF8String topic;
+
+  /* The name/nick of the person who set the topic */
+  readonly attribute AUTF8String topicSetter;
+
+  /* Whether the protocol plugin can set a topic. Doesn't check that
+     the user has the necessary rights in the current conversation. */
+  readonly attribute boolean topicSettable;
+
+  /* The nick seen by other people in the room */
+  readonly attribute AUTF8String nick;
+
+  /* This is true when we left the chat but kept the conversation open */
+  readonly attribute boolean left;
+
+  /* Observers will receive chat-buddy-add, chat-buddy-update,
+     chat-buddy-remove and chat-update-topic notifications.
+
+     aSubject will be of type:
+       nsISimpleEnumerator of prplIConvChatBuddy for chat-buddy-add,
+       nsISimpleEnumerator of nsISupportsString for chat-buddy-remove,
+       prplIConvChatBuddy for chat-buddy-update,
+       null for chat-update-topic.
+
+     aData will contain the old nick for chat-buddy-update if the name
+     has changed.
+   */
+};
+
+/* This represents a participant in a chat room */
+[scriptable, uuid(33f6ef81-1d23-484e-b4e0-14fffa0c4392)]
+interface prplIConvChatBuddy: nsISupports {
+
+  /* The name of the buddy */
+  readonly attribute AUTF8String name;
+
+  /* The alias (FIXME: can this be non-null if buddy is null?) */
+  readonly attribute AUTF8String alias;
+
+  /* Indicates if this chat buddy corresponds to a buddy in our buddy list */
+  readonly attribute boolean buddy;
+
+  /* PurpleConvChatBuddyFlags flags; (ops, voice etc.) */
+  readonly attribute boolean noFlags;
+  readonly attribute boolean voiced;
+  readonly attribute boolean halfOp;
+  readonly attribute boolean op;
+  readonly attribute boolean founder;
+  readonly attribute boolean typing;
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/prplIMessage.idl
@@ -0,0 +1,111 @@
+/* ***** 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
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * 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 ***** */
+
+#include "nsISupports.idl"
+#include "nsIRunnable.idl"
+#include "prplIConversation.idl"
+
+/*
+ * An action that the user may perform in relation to a particular message.
+ */
+[scriptable, uuid(7e470f0e-d948-4d9a-b8dc-4beecf6554b9)]
+interface prplIMessageAction: nsIRunnable
+{
+  /* The protocol plugins need to provide a localized label suitable
+     for being shown in the user interface (for example as a context
+     menu item). */
+  readonly attribute ACString label;
+};
+
+[scriptable, uuid(d9f0ca7f-ee59-4657-a3dd-f458c204ca45)]
+interface prplIMessage: nsISupports {
+  /* The uniqueness of the message id is only guaranteed across
+     messages of a conversation, not across all messages created
+     during the execution of the application. */
+  readonly attribute unsigned long id;
+  readonly attribute AUTF8String who;
+  readonly attribute AUTF8String alias;
+  readonly attribute AUTF8String originalMessage;
+           attribute AUTF8String message;
+  readonly attribute AUTF8String iconURL;
+  readonly attribute PRTime time;
+  readonly attribute prplIConversation conversation;
+
+  /* Holds the sender color for Chats.
+     Empty string by default, it is set by the conversation binding. */
+  attribute AUTF8String color;
+
+  /*  PURPLE_MESSAGE_SEND        = 0x0001, /**< Outgoing message. */
+  readonly attribute boolean outgoing;
+  /*  PURPLE_MESSAGE_RECV        = 0x0002, /**< Incoming message. */
+  readonly attribute boolean incoming;
+  /*  PURPLE_MESSAGE_SYSTEM      = 0x0004, /**< System message.   */
+  readonly attribute boolean system;
+  /*  PURPLE_MESSAGE_AUTO_RESP   = 0x0008, /**< Auto response.    */
+  readonly attribute boolean autoResponse;
+  /*  PURPLE_MESSAGE_ACTIVE_ONLY = 0x0010, /**< Hint to the UI that this
+                                                message should not be
+                                                shown in conversations
+                                                which are only open for
+                                                internal UI purposes
+                                                (e.g. for contact-aware
+                                                 conversions).           */
+  /*  PURPLE_MESSAGE_NICK        = 0x0020, /**< Contains your nick.      */
+  readonly attribute boolean containsNick;
+  /*  PURPLE_MESSAGE_NO_LOG      = 0x0040, /**< Do not log.              */
+  readonly attribute boolean noLog;
+  /*  PURPLE_MESSAGE_ERROR       = 0x0200, /**< Error message.           */
+  readonly attribute boolean error;
+  /*  PURPLE_MESSAGE_DELAYED     = 0x0400, /**< Delayed message.         */
+  readonly attribute boolean delayed;
+  /*  PURPLE_MESSAGE_RAW         = 0x0800, /**< "Raw" message - don't
+                                                apply formatting         */
+  readonly attribute boolean noFormat;
+  /*  PURPLE_MESSAGE_IMAGES      = 0x1000, /**< Message contains images  */
+  readonly attribute boolean containsImages;
+  /*  PURPLE_MESSAGE_NOTIFY      = 0x2000, /**< Message is a notification */
+  readonly attribute boolean notification;
+  /*  PURPLE_MESSAGE_NO_LINKIFY  = 0x4000  /**< Message should not be auto-linkified */
+  readonly attribute boolean noLinkification;
+
+  /* An array of actions the user may perform on this message.
+     The first action will be the 'default' and may be performed
+     automatically when the message is double clicked.
+     'Reply' is usually a good default action. */
+  void getActions([optional] out unsigned long actionCount,
+                  [retval, array, size_is(actionCount)] out prplIMessageAction actions);
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/prplIPref.idl
@@ -0,0 +1,69 @@
+/* ***** 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
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * 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 ***** */
+
+#include "nsISupports.idl"
+#include "nsISimpleEnumerator.idl"
+
+/*
+ * This is a proxy for libpurple PurpleAccountOption
+ */
+
+[scriptable, uuid(e781563f-9088-4a96-93e3-4fb6f5ce6a77)]
+interface prplIPref: nsISupports {
+  const short typeBool   = 1;
+  const short typeInt    = 2;
+  const short typeString = 3;
+  const short typeList   = 4;
+
+  readonly attribute AUTF8String name;
+  readonly attribute AUTF8String label;
+  readonly attribute short type;
+  readonly attribute boolean masked;
+
+  boolean     getBool();
+  long        getInt();
+  AUTF8String getString();
+  // enumerator of prplIKeyValuePair
+  nsISimpleEnumerator getList();
+  AUTF8String getListDefault();
+};
+
+[scriptable, uuid(8fc16882-ba8e-432a-999f-0d4dc104234b)]
+interface prplIKeyValuePair: nsISupports {
+  readonly attribute AUTF8String name;
+  readonly attribute AUTF8String value;
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/prplIProtocol.idl
@@ -0,0 +1,133 @@
+/* ***** 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
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * 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 ***** */
+
+#include "nsISupports.idl"
+#include "nsISimpleEnumerator.idl"
+#include "imIAccount.idl"
+
+[scriptable, uuid(7d302db0-3813-4c51-8372-c7eb5fc9f3d3)]
+interface prplIProtocol: nsISupports {
+  // aId is the prpl id, this method is used so that classes
+  // implementing several protocol plugins can know which protocol is
+  // desired for this instance.
+  void init(in AUTF8String aId);
+
+  readonly attribute AUTF8String name;
+  readonly attribute AUTF8String id;
+  readonly attribute AUTF8String normalizedName;
+
+  // returns a chrome URI pointing to a folder that contains the files:
+  // icon.png icon32.png and icon48.png
+  readonly attribute AUTF8String iconBaseURI;
+
+  // returns an enumerator of prplIPref
+  nsISimpleEnumerator getOptions();
+
+  // returns an enumerator of prplIUsernameSplit
+  nsISimpleEnumerator getUsernameSplit();
+
+  // descriptive text to put in the username input box when it is empty
+  readonly attribute AUTF8String usernameEmptyText;
+
+  // Use this function to avoid attempting to create duplicate accounts
+  boolean accountExists(in AUTF8String aName);
+
+  // These should all be stuff that some plugins can do and others can't.
+
+  // OPT_PROTO_UNIQUE_CHATNAME Use a unique name, not an alias, for
+  // chat rooms.
+  //  XMPP lets you choose what name you want for chat. So it
+  //  shouldn't be pulling the alias for when you're in chat; it gets
+  //  annoying.
+  readonly attribute boolean uniqueChatName;
+
+  // OPT_PROTO_CHAT_TOPIC Chat rooms have topics.
+  //  IRC and XMPP support this.
+  readonly attribute boolean chatHasTopic;
+
+  // OPT_PROTO_NO_PASSWORD Don't require passwords for sign-in.
+  //  Zephyr doesn't require passwords, so there's no need for a
+  //  password prompt.
+  readonly attribute boolean noPassword;
+
+  // OPT_PROTO_MAIL_CHECK Notify on new mail.
+  //  MSN and Yahoo notify you when you have new mail.
+  readonly attribute boolean newMailNotification;
+
+  // OPT_PROTO_IM_IMAGE Images in IMs.
+  //  Oscar lets you send images in direct IMs.
+  readonly attribute boolean imagesInIM;
+
+  // OPT_PROTO_PASSWORD_OPTIONAL Allow passwords to be optional.
+  //  Passwords in IRC are optional, and are needed for certain
+  //  functionality.
+  readonly attribute boolean passwordOptional;
+
+  // OPT_PROTO_USE_POINTSIZE Allows font size to be specified in sane
+  // point size.
+  //  Probably just XMPP and Y!M
+  readonly attribute boolean usePointSize;
+
+  // OPT_PROTO_REGISTER_NOSCREENNAME Set the Register button active
+  // when screenname is not given.
+  //  Gadu-Gadu doesn't need a screenname to register new account.
+  readonly attribute boolean registerNoScreenName;
+
+  // OPT_PROTO_SLASH_COMMANDS_NATIVE  Indicates that slash commands
+  // are native to this protocol.
+  // Used as a hint that unknown commands should not be sent as messages.
+  readonly attribute boolean slashCommandsNative;
+
+  // Indicates if the protocol plugin can use a purpleIProxy for the
+  // account, or uses the Mozilla proxy settings for all accounts.
+  readonly attribute boolean usePurpleProxy;
+
+  // Get the protocol specific part of an already initialized
+  // imIAccount instance.
+  prplIAccount getAccount(in imIAccount aImAccount);
+};
+
+[scriptable, uuid(20c4971a-f7c2-4781-8e85-69fee7b83a3d)]
+interface prplIUsernameSplit: nsISupports {
+  readonly attribute AUTF8String label;
+  readonly attribute AUTF8String defaultValue;
+  readonly attribute char separator;
+
+  /* reverse is PR_TRUE if the separator should be found starting at
+     the end of the string, PR_FALSE otherwise. */
+  readonly attribute boolean reverse;
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/prplIRequest.idl
@@ -0,0 +1,69 @@
+/* ***** 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
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * 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 ***** */
+
+#include "nsISupports.idl"
+
+interface imIAccount;
+interface nsIDOMWindow;
+interface nsIWebProgress;
+
+/* This interface is for use in the browser-request notification, to
+   let protocol plugins open a browser window. This is an unfortunate
+   necessity for protocols that require an OAuth authentication. */
+[scriptable, uuid(b89dbb38-0de4-11e0-b3d0-0002e304243c)]
+interface prplIRequestBrowser: nsISupports {
+  readonly attribute AUTF8String promptText;
+  readonly attribute imIAccount account;
+  readonly attribute AUTF8String url;
+  void cancelled();
+  void loaded(in nsIDOMWindow aWindow,
+              in nsIWebProgress aWebProgress);
+};
+
+/* This interface is used for buddy authorization requests, when the
+   user needs to confirm if a remote contact should be allowed to see
+   his presence information.  It is implemented by the aSubject
+   parameter of the buddy-authorization-request and
+   buddy-authorization-request-canceled notifications.
+*/
+[scriptable, uuid(a55c1e24-17cc-4ddc-8c64-3bc315a3c3b1)]
+interface prplIBuddyRequest: nsISupports {
+  readonly attribute imIAccount account;
+  readonly attribute AUTF8String userName;
+  void grant();
+  void deny();
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/prplITooltipInfo.idl
@@ -0,0 +1,54 @@
+/* ***** 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
+ * 2008.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * 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 ***** */
+
+#include "nsISupports.idl"
+
+/*
+ * This interface provides access to the content of a
+ * PurpleNotifyUserInfoEntry structure.
+ */
+
+[scriptable, uuid(dd535741-4b04-49ca-8df6-08f8577fe52b)]
+interface prplITooltipInfo: nsISupports {
+  const short pair          = 0;
+  const short sectionBreak  = 1;
+  const short sectionHeader = 2;
+
+  readonly attribute short type;
+  readonly attribute AUTF8String label;
+  readonly attribute AUTF8String value;
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/src/Makefile.in
@@ -0,0 +1,54 @@
+# ***** 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 = \
+		imAccounts.js imAccounts.manifest \
+		imCommands.js imCommands.manifest \
+		imContacts.js imContacts.manifest \
+		imConversations.js imConversations.manifest \
+		imCore.js imCore.manifest \
+		logger.js logger.manifest \
+		smileProtocolHandler.js smileProtocolHandler.manifest \
+		$(NULL)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imAccounts.js
@@ -0,0 +1,982 @@
+/* ***** 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
+ * 2011.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Romain Bezut <romain@bezut.info>
+ *
+ * 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/imServices.jsm");
+
+const kPrefAutologinPending = "messenger.accounts.autoLoginPending";
+const kPrefMessengerAccounts = "messenger.accounts";
+const kPrefAccountPrefix = "messenger.account.";
+const kAccountKeyPrefix = "account";
+const kAccountOptionPrefPrefix = "options.";
+const kPrefAccountName = "name";
+const kPrefAccountPrpl = "prpl";
+const kPrefAccountAutoLogin = "autoLogin";
+const kPrefAccountAutoJoin = "autoJoin";
+const kPrefAccountAlias = "alias";
+const kPrefAccountFirstConnectionState = "firstConnectionState";
+
+const kPrefConvertOldPasswords = "messenger.accounts.convertOldPasswords";
+const kPrefAccountPassword = "password";
+
+XPCOMUtils.defineLazyGetter(this, "LoginManager", function()
+  Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager)
+);
+
+XPCOMUtils.defineLazyGetter(this, "_", function()
+  l10nHelper("chrome://chat/locale/accounts.properties")
+);
+
+var gUserCanceledMasterPasswordPrompt = false;
+var gConvertingOldPasswords = false;
+
+var SavePrefTimer = {
+  saveNow: function() {
+    if (this._timer) {
+      clearTimeout(this._timer);
+      this._timer = null;
+    }
+    Services.prefs.savePrefFile(null);
+  },
+  _timer: null,
+  unInitTimer: function() {
+    if (this._timer)
+      this.saveNow();
+  },
+  initTimer: function() {
+    if (!this._timer)
+      this._timer = setTimeout(this.saveNow.bind(this), 5000);
+  }
+};
+
+var AutoLoginCounter = {
+  _count: 0,
+  startAutoLogin: function() {
+    ++this._count;
+    if (this._count != 1)
+      return;
+    Services.prefs.setIntPref(kPrefAutologinPending, Date.now() / 1000);
+    SavePrefTimer.saveNow();
+  },
+  finishedAutoLogin: function() {
+    --this._count;
+    if (this._count != 0)
+      return;
+    Services.prefs.deleteBranch(kPrefAutologinPending);
+    SavePrefTimer.initTimer();
+  }
+};
+
+function UnknownProtocol(aPrplId)
+{
+  this.id = aPrplId;
+}
+UnknownProtocol.prototype = {
+  __proto__: ClassInfo("prplIProtocol", "Unknown protocol"),
+  get name() "",
+  get normalizedName() this.name,
+  get iconBaseURI() "chrome://chat/skin/prpl-unknown/",
+  getOptions: function() EmptyEnumerator,
+  getUsernameSplit: function() EmptyEnumerator,
+  get usernameEmptyText() "",
+
+  getAccount: function(aKey, aName) { throw Cr.NS_ERROR_NOT_IMPLEMENTED; },
+  accountExists: function() { throw Cr.NS_ERROR_NOT_IMPLEMENTED; },
+
+  // false seems an acceptable default for all options
+  // (they should never be called anyway).
+  get uniqueChatName() false,
+  get chatHasTopic() false,
+  get noPassword() false,
+  get newMailNotification() false,
+  get imagesInIM() false,
+  get passwordOptional() true,
+  get usePointSize() true,
+  get registerNoScreenName() false,
+  get slashCommandsNative() false,
+  get usePurpleProxy() false
+};
+
+// aName and aPrplId are provided as parameter only if this is a new
+// account that doesn't exist in the preferences. In this case, these
+// 2 values should be stored.
+function imAccount(aKey, aName, aPrplId)
+{
+  if (aKey.indexOf(kAccountKeyPrefix) != 0)
+    throw Cr.NS_ERROR_INVALID_ARG;
+
+  this.id = aKey;
+  this.numericId = parseInt(aKey.substr(kAccountKeyPrefix.length));
+  this.prefBranch = Services.prefs.getBranch(kPrefAccountPrefix + aKey + ".");
+
+  if (aName) {
+    this.name = aName;
+    let str = Cc["@mozilla.org/supports-string;1"]
+              .createInstance(Ci.nsISupportsString);
+    str.data = aName;
+    this.prefBranch.setComplexValue(kPrefAccountName, Ci.nsISupportsString,
+                                    str);
+
+    this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
+  }
+  else {
+    this.name = this.prefBranch.getComplexValue(kPrefAccountName,
+                                                Ci.nsISupportsString).data;
+  }
+
+  let prplId = aPrplId;
+  if (prplId)
+    this.prefBranch.setCharPref(kPrefAccountPrpl, prplId);
+  else
+    prplId = this.prefBranch.getCharPref(kPrefAccountPrpl);
+
+  // Get the protocol plugin, or fallback to an UnknownProtocol instance.
+  this.protocol = Services.core.getProtocolById(prplId);
+  if (!this.protocol) {
+    this.protocol = new UnknownProtocol(prplId);
+    this._connectionErrorReason = Ci.imIAccount.ERROR_UNKNOWN_PRPL;
+    return;
+  }
+
+  // Ensure the account is correctly stored in blist.sqlite.
+  Services.contacts.storeAccount(this.numericId, this.name, prplId);
+
+  // Get the prplIAccount from the protocol plugin.
+  this.prplAccount = this.protocol.getAccount(this);
+
+  // Send status change notifications to the account.
+  this.observedStatusInfo = null; // (To execute the setter).
+
+  // If we have never finished the first connection attempt for this account,
+  // mark the account as having caused a crash.
+  if (this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_PENDING)
+    this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_CRASHED;
+
+  // Try to convert old passwords stored in the preferences.
+  // Don't try too hard if the user has canceled a master password prompt:
+  // we don't want to display several of theses prompts at startup.
+  if (gConvertingOldPasswords && !this.protocol.noPassword) {
+    try {
+      let password = this.prefBranch.getComplexValue(kPrefAccountPassword,
+                                                     Ci.nsISupportsString).data;
+      if (password && !this.password)
+        this.password = password;
+    } catch (e) { /* No password saved in the prefs for this account. */ }
+  }
+
+  // Check for errors that should prevent connection attempts.
+  if (this._passwordRequired && !this.password)
+    this._connectionErrorReason = Ci.imIAccount.ERROR_MISSING_PASSWORD;
+  else if (this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_CRASHED)
+    this._connectionErrorReason = Ci.imIAccount.ERROR_CRASHED;
+}
+
+imAccount.prototype = {
+  __proto__: ClassInfo(["imIAccount", "prplIAccount"], "im account object"),
+
+  name: "",
+  id: "",
+  numericId: 0,
+  protocol: null,
+  prplAccount: null,
+  connectionState: Ci.imIAccount.STATE_DISCONNECTED,
+  connectionStateMsg: "",
+  connectionErrorMessage: "",
+  _connectionErrorReason: Ci.prplIAccount.NO_ERROR,
+  get connectionErrorReason() {
+    if (this._connectionErrorReason != Ci.prplIAccount.NO_ERROR &&
+        (this._connectionErrorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD ||
+         !this._password))
+      return this._connectionErrorReason;
+    else
+      return this.prplAccount.connectionErrorReason;
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic == "account-connect-progress")
+      this.connectionStateMsg = aData;
+    else if (aTopic == "account-connecting") {
+      if (this.prplAccount.connectionErrorReason != Ci.prplIAccount.NO_ERROR) {
+        delete this.connectionErrorMessage;
+        if (this.timeOfNextReconnect - Date.now() > 1000) {
+          // This is a manual reconnection, reset the auto-reconnect stuff
+          this.timeOfLastConnect = 0;
+          this.cancelReconnection();
+        }
+      }
+      if (this.firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK)
+        this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_PENDING;
+      this.connectionState = Ci.imIAccount.STATE_CONNECTING;
+    }
+    else if (aTopic == "account-connected") {
+      this.connectionState = Ci.imIAccount.STATE_CONNECTED;
+      this._finishedAutoLogin();
+      this.timeOfLastConnect = Date.now();
+      if (this.firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK)
+        this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_OK;
+      delete this.connectionStateMsg;
+
+      if (this.canJoinChat &&
+          this.prefBranch.prefHasUserValue(kPrefAccountAutoJoin)) {
+        let autojoin = this.prefBranch.getCharPref(kPrefAccountAutoJoin);
+        if (autojoin) {
+          for each (let room in autojoin.split(","))
+            this.joinChat(this.getChatRoomDefaultFieldValues(room));
+        }
+      }
+    }
+    else if (aTopic == "account-disconnecting") {
+      this.connectionState = Ci.imIAccount.STATE_DISCONNECTING;
+      this.connectionErrorMessage = aData;
+      delete this.connectionStateMsg;
+      this._finishedAutoLogin();
+
+      let firstConnectionState = this.firstConnectionState;
+      if (firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK &&
+          firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_CRASHED)
+        this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
+
+      let connectionErrorReason = this.prplAccount.connectionErrorReason;
+      if (connectionErrorReason != Ci.prplIAccount.NO_ERROR) {
+        if (connectionErrorReason == Ci.prplIAccount.ERROR_NETWORK_ERROR ||
+            connectionErrorReason == Ci.prplIAccount.ERROR_ENCRYPTION_ERROR)
+          this._startReconnectTimer();
+        this._sendNotification("account-connect-error");
+      }
+    }
+    else if (aTopic == "account-disconnected")
+      this.connectionState = Ci.imIAccount.STATE_DISCONNECTED;
+    else
+      throw Cr.NS_ERROR_UNEXPECTED;
+    this._sendNotification(aTopic, aData);
+  },
+
+  _observedStatusInfo: null,
+  get observedStatusInfo() this._observedStatusInfo,
+  _statusObserver: null,
+  set observedStatusInfo(aUserStatusInfo) {
+    if (!this.prplAccount)
+      return;
+    if (this._statusObserver)
+      this.statusInfo.removeObserver(this._statusObserver);
+    this._observedStatusInfo = aUserStatusInfo;
+    if (this._statusObserver)
+      this.statusInfo.addObserver(this._statusObserver);
+  },
+  get statusInfo() this._observedStatusInfo || Services.core.globalUserStatus,
+
+  reconnectAttempt: 0,
+  timeOfLastConnect: 0,
+  timeOfNextReconnect: 0,
+  _reconnectTimer: null,
+  _startReconnectTimer: function() {
+    /* If the last successful connection is older than 10 seconds, reset the
+     number of reconnection attemps. */
+    const kTimeBeforeSuccessfulConnection = 10;
+    if (this.timeOfLastConnect &&
+        this.timeOfLastConnect + kTimeBeforeSuccessfulConnection * 1000 < Date.now()) {
+      delete this.reconnectAttempt;
+      delete this.timeOfLastConnect;
+    }
+
+    let timers =
+      Services.prefs.getCharPref("messenger.accounts.reconnectTimer").split(",");
+    let delay = timers[Math.min(this.reconnectAttempt, timers.length - 1)];
+    let msDelay = parseInt(delay) * 1000;
+    ++this.reconnectAttempt;
+    this.timeOfNextReconnect = Date.now() + msDelay;
+    this._reconnectTimer = setTimeout(this.connect.bind(this), msDelay);
+  },
+
+  _sendNotification: function(aTopic, aData) {
+    Services.obs.notifyObservers(this, aTopic, aData);
+  },
+
+  get firstConnectionState() {
+    try {
+      return this.prefBranch.getIntPref(kPrefAccountFirstConnectionState);
+    } catch (e) {
+      return Ci.imIAccount.FIRST_CONNECTION_OK;
+    }
+  },
+  set firstConnectionState(aState) {
+    if (aState == Ci.imIAccount.FIRST_CONNECTION_OK)
+      this.prefBranch.deleteBranch(kPrefAccountFirstConnectionState);
+    else {
+      this.prefBranch.setIntPref(kPrefAccountFirstConnectionState, aState);
+      // We want to save this pref immediately when trying to connect.
+      if (aState == Ci.imIAccount.FIRST_CONNECTION_PENDING)
+        SavePrefTimer.saveNow();
+      else
+        SavePrefTimer.initTimer();
+    }
+  },
+
+  _pendingReconnectForConnectionInfoChange: false,
+  _connectionInfoChanged: function() {
+    /* This will be the first connection with these parameters */
+    this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
+
+    if (this._pendingReconnectForConnectionInfoChange)
+      return;
+
+    this._pendingReconnectForConnectionInfoChange = true;
+    executeSoon(function () {
+      delete this._pendingReconnectForConnectionInfoChange;
+      // If the connection parameters have changed while we were
+      // trying to connect, cancel the ongoing connection attempt and
+      // try again with the new parameters.
+      if (this.connecting) {
+        this.disconnect();
+        this.connect();
+        return;
+      }
+      // If the account was disconnected because of a non-fatal
+      // connection error, retry now that we have new parameters.
+      let errorReason = this.connectionErrorReason;
+      if (this.disconnected &&
+          errorReason != Ci.prplIAccount.NO_ERROR &&
+          errorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD &&
+          errorReason != Ci.imIAccount.ERROR_CRASHED &&
+          errorReason != Ci.imIAccount.ERROR_UNKNOWN_PRPL) {
+        this.connect();
+      }
+    }.bind(this));
+  },
+
+  get normalizedName() this._ensurePrplAccount.normalizedName,
+
+  _sendUpdateNotification: function() {
+    this._sendNotification("account-updated");
+  },
+
+  set alias(val) {
+    if (val) {
+      let str = Cc["@mozilla.org/supports-string;1"]
+                .createInstance(Ci.nsISupportsString);
+      str.data = val;
+      this.prefBranch.setComplexValue(kPrefAccountAlias, Ci.nsISupportsString,
+                                      str);
+    }
+    else
+      this.prefBranch.deleteBranch(kPrefAccountAlias);
+    this._sendUpdateNotification();
+  },
+  get alias() {
+    try {
+      return this.prefBranch.getComplexValue(kPrefAccountAlias,
+                                             Ci.nsISupportsString).data;
+    } catch (e) {
+      return "";
+    }
+  },
+
+  _password: "",
+  get password() {
+    if (this._password)
+      return this._password;
+
+    // Avoid prompting the user for the master password more than once at startup.
+    if (gUserCanceledMasterPasswordPrompt)
+      return "";
+
+    let passwordURI = "im://" + this.protocol.id;
+    let logins;
+    try {
+      logins = LoginManager.findLogins({}, passwordURI, null, passwordURI);
+    } catch (e) {
+      this._handleMasterPasswordException(e);
+      return "";
+    }
+    let normalizedName = this.normalizedName;
+    for each (let login in logins) {
+      if (login.username == normalizedName) {
+        this._password = login.password;
+        if (this._connectionErrorReason == Ci.imIAccount.ERROR_MISSING_PASSWORD) {
+          // We have found a password for an account marked as missing password,
+          // re-check all others accounts missing a password. But first,
+          // remove the error on our own account to avoid re-checking it.
+          delete this._connectionErrorReason;
+          gAccountsService._checkIfPasswordStillMissing();
+        }
+        return this._password;
+      }
+    }
+    return "";
+  },
+  _checkIfPasswordStillMissing: function() {
+    if (this._connectionErrorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD ||
+        !this.password)
+      return;
+
+    delete this._connectionErrorReason;
+    this._sendUpdateNotification();
+  },
+  get _passwordRequired()
+    !this.protocol.noPassword && !this.protocol.passwordOptional,
+  set password(aPassword) {
+    this._password = aPassword;
+    if (gUserCanceledMasterPasswordPrompt)
+      return;
+    let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]
+                   .createInstance(Ci.nsILoginInfo);
+    let passwordURI = "im://" + this.protocol.id;
+    newLogin.init(passwordURI, null, passwordURI, this.normalizedName,
+                  aPassword, "", "");
+    try {
+      let logins = LoginManager.findLogins({}, passwordURI, null, passwordURI);
+      let saved = false;
+      for each (let login in logins) {
+        if (newLogin.matches(login, true)) {
+          if (aPassword)
+            LoginManager.modifyLogin(login, newLogin);
+          else
+            LoginManager.removeLogin(login);
+          saved = true;
+          break;
+        }
+      }
+      if (!saved && aPassword)
+        LoginManager.addLogin(newLogin);
+    } catch (e) {
+      this._handleMasterPasswordException(e);
+    }
+
+    this._connectionInfoChanged();
+    if (aPassword &&
+        this._connectionErrorReason == Ci.imIAccount.ERROR_MISSING_PASSWORD)
+      this._connectionErrorReason = Ci.imIAccount.NO_ERROR;
+    else if (!aPassword && this._passwordRequired)
+      this._connectionErrorReason = Ci.imIAccount.ERROR_MISSING_PASSWORD;
+    this._sendUpdateNotification();
+  },
+  _handleMasterPasswordException: function(aException) {
+    if (aException.result != Components.results.NS_ERROR_ABORT)
+      throw aException;
+
+    gUserCanceledMasterPasswordPrompt = true;
+    executeSoon(function () { gUserCanceledMasterPasswordPrompt = false; });
+  },
+
+  get autoLogin() {
+    let autoLogin = true;
+    try {
+      autoLogin = this.prefBranch.getBoolPref(kPrefAccountAutoLogin);
+    } catch (e) { }
+    return autoLogin;
+  },
+  set autoLogin(val) {
+    this.prefBranch.setBoolPref(kPrefAccountAutoLogin, val);
+    SavePrefTimer.initTimer();
+    this._sendUpdateNotification();
+  },
+  _autoLoginPending: false,
+  checkAutoLogin: function() {
+    // No auto-login if: the account has an error at the imIAccount level
+    // (unknown protocol, missing password, first connection crashed),
+    // the account is already connected or connecting, or autoLogin is off.
+    if (this._connectionErrorReason != Ci.prplIAccount.NO_ERROR ||
+        this.connecting || this.connected || !this.autoLogin)
+      return;
+
+    this._autoLoginPending = true;
+    AutoLoginCounter.startAutoLogin();
+    try {
+      this.connect();
+    } catch (e) {
+      Cu.reportError(e);
+      this._finishedAutoLogin();
+    }
+  },
+  _finishedAutoLogin: function() {
+    if (!this.hasOwnProperty("_autoLoginPending"))
+      return;
+    delete this._autoLoginPending;
+    AutoLoginCounter.finishedAutoLogin();
+  },
+
+  // Delete the account (from the preferences, mozStorage, and call unInit).
+  remove: function() {
+    let login = Cc["@mozilla.org/login-manager/loginInfo;1"]
+                .createInstance(Ci.nsILoginInfo);
+    let passwordURI = "im://" + this.protocol.id;
+    login.init(passwordURI, null, passwordURI, this.normalizedName, "", "", "");
+    let logins = LoginManager.findLogins({}, passwordURI, null, passwordURI);
+    for each (let l in logins) {
+      if (login.matches(l, true)) {
+        LoginManager.removeLogin(l);
+        break;
+      }
+    }
+    this.unInit();
+    Services.contacts.forgetAccount(this.numericId);
+    this.prefBranch.deleteBranch("");
+  },
+  unInit: function() {
+    // remove any pending reconnection timer.
+    this.cancelReconnection();
+
+    // remove any pending autologin preference used for crash detection.
+    this._finishedAutoLogin();
+
+    // If the first connection was pending on quit, we set it back to unknown.
+    if (this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_PENDING)
+      this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
+
+    // and make sure we cleanup the save pref timer.
+    SavePrefTimer.unInitTimer();
+
+    if (this.prplAccount)
+      this.prplAccount.unInit();
+
+    delete this.protocol;
+    delete this.prplAccount;
+  },
+
+  get _ensurePrplAccount() {
+    if (this.prplAccount)
+      return this.prplAccount;
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+  connect: function() {
+    if (this._passwordRequired) {
+      // If the previous connection attempt failed because we have a wrong password,
+      // clear the passwor cache so that if there's no password in the password
+      // manager the user gets prompted again.
+      if (this.connectionErrorReason == Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED)
+        delete this._password;
+
+      let password = this.password;
+      if (!password) {
+        let prompts = Services.prompt;
+        let shouldSave = {value: false};
+        password = {value: ""};
+        if (!prompts.promptPassword(null, _("passwordPromptTitle", this.name),
+                                    _("passwordPromptText", this.name),
+                                    password, _("passwordPromptSaveCheckbox"),
+                                    shouldSave))
+          return;
+
+        if (shouldSave.value)
+          this.password = password.value;
+        else
+          this._password = password.value;
+      }
+    }
+
+    if (!this._statusObserver) {
+      this._statusObserver = {
+        observe: (function(aSubject, aTopic, aData) {
+          // Disconnect or reconnect the account automatically, otherwise notify
+          // the prplAccount instance.
+          let statusType = aSubject.statusType;
+          if (statusType == Ci.imIStatusInfo.STATUS_OFFLINE &&
+              this.connected)
+            this.prplAccount.disconnect();
+          else if (statusType == Ci.imIStatusInfo.STATUS_OFFLINE &&
+                   this._reconnectTimer)
+            this.cancelReconnection();
+          else if (statusType > Ci.imIStatusInfo.STATUS_OFFLINE &&
+                   this.disconnected)
+            this.prplAccount.connect();
+          else if (this.connected)
+            this.prplAccount.observe(aSubject, aTopic, aData);
+        }).bind(this)
+      };
+
+      this.statusInfo.addObserver(this._statusObserver);
+    }
+
+    this._ensurePrplAccount.connect();
+  },
+  disconnect: function() {
+    if (this._statusObserver) {
+      this.statusInfo.removeObserver(this._statusObserver);
+      delete this._statusObserver;
+    }
+    this._ensurePrplAccount.disconnect();
+  },
+
+  get disconnected() this.connectionState == Ci.imIAccount.STATE_DISCONNECTED,
+  get connected() this.connectionState == Ci.imIAccount.STATE_CONNECTED,
+  get connecting() this.connectionState == Ci.imIAccount.STATE_CONNECTING,
+  get disconnecting() this.connectionState == Ci.imIAccount.STATE_DISCONNECTING,
+
+  cancelReconnection: function() {
+    if (this._reconnectTimer) {
+      clearTimeout(this._reconnectTimer);
+      delete this._reconnectTimer;
+    }
+    delete this.reconnectAttempt;
+    delete this.timeOfNextReconnect;
+  },
+  createConversation: function(aName)
+    this._ensurePrplAccount.createConversation(aName),
+  addBuddy: function(aTag, aName) {
+    this._ensurePrplAccount.addBuddy(aTag, aName);
+  },
+  loadBuddy: function(aBuddy, aTag)
+    this._ensurePrplAccount.loadBuddy(aBuddy, aTag), // FIXME for unknown proto
+  requestBuddyInfo: function(aBuddyName) {
+    this._ensurePrplAccount.requestBuddyInfo(aBuddyName);
+  },
+  getChatRoomFields: function() this._ensurePrplAccount.getChatRoomFields(),
+  getChatRoomDefaultFieldValues: function(aDefaultChatName)
+    this._ensurePrplAccount.getChatRoomDefaultFieldValues(aDefaultChatName),
+  get canJoinChat() this.prplAccount ? this.prplAccount.canJoinChat : false,
+  joinChat: function(aComponents) {
+    this._ensurePrplAccount.joinChat(aComponents);
+  },
+  setBool: function(aName, aVal) {
+    this.prefBranch.setBoolPref(kAccountOptionPrefPrefix + aName, aVal);
+    this._connectionInfoChanged();
+    if (this.prplAccount)
+      this.prplAccount.setBool(aName, aVal);
+    SavePrefTimer.initTimer();
+  },
+  setInt: function(aName, aVal) {
+    this.prefBranch.setIntPref(kAccountOptionPrefPrefix + aName, aVal);
+    this._connectionInfoChanged();
+    if (this.prplAccount)
+      this.prplAccount.setInt(aName, aVal);
+    SavePrefTimer.initTimer();
+  },
+  setString: function(aName, aVal) {
+    let str = Cc["@mozilla.org/supports-string;1"]
+              .createInstance(Ci.nsISupportsString);
+    str.data = aVal;
+    this.prefBranch.setComplexValue(kAccountOptionPrefPrefix + aName,
+                                    Ci.nsISupportsString, str);
+    this._connectionInfoChanged();
+    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,
+
+  get proxyInfo() this._ensurePrplAccount.proxyInfo,
+  set proxyInfo(val) {
+    this._ensurePrplAccount.proxyInfo = val;
+    this._connectionInfoChanged();
+  }
+};
+
+var gAccountsService = null;
+
+function AccountsService() { }
+AccountsService.prototype = {
+  initAccounts: function() {
+    this._initAutoLoginStatus();
+    this._accounts = [];
+    this._accountsById = {};
+    gAccountsService = this;
+    gConvertingOldPasswords =
+      Services.prefs.getBoolPref(kPrefConvertOldPasswords);
+    let accountList = this._accountList;
+    for each (let account in (accountList ? accountList.split(",") : [])) {
+      try {
+        account.trim();
+        if (!account)
+          throw Cr.NS_ERROR_INVALID_ARG;
+        let newAccount = new imAccount(account);
+        this._accounts.push(newAccount);
+        this._accountsById[newAccount.numericId] = newAccount;
+      } catch (e) {
+        Cu.reportError(e);
+        dump(e + " " + e.toSource() + "\n");
+      }
+    }
+    // If the user has canceled a master password prompt, we haven't
+    // been able to save any password, so the old password conversion
+    // still needs to happen.
+    if (gConvertingOldPasswords && !gUserCanceledMasterPasswordPrompt)
+      Services.prefs.setBoolPref(kPrefConvertOldPasswords, false);
+
+    this._prefObserver = this.observe.bind(this);
+    Services.prefs.addObserver(kPrefMessengerAccounts, this._prefObserver, false);
+  },
+
+  _observingAccountListChange: true,
+  _prefObserver: null,
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic != "nsPref:changed" || aData != kPrefMessengerAccounts ||
+       !this._observingAccountListChange)
+      return;
+
+    this._accounts =
+      this._accountList.split(",").map(String.trim)
+          .filter(function (k) k.indexOf(kAccountKeyPrefix) == 0)
+          .map(function (k) parseInt(k.substr(kAccountKeyPrefix.length)))
+          .map(this.getAccountByNumericId, this)
+          .filter(function (a) a);
+
+    Services.obs.notifyObservers(this, "account-list-updated", null);
+  },
+
+  get _accountList() Services.prefs.getCharPref(kPrefMessengerAccounts),
+  set _accountList(aNewList) {
+    this._observingAccountListChange = false;
+    Services.prefs.setCharPref(kPrefMessengerAccounts, aNewList);
+    delete this._observingAccountListChange;
+  },
+
+  unInitAccounts: function() {
+    for each (let account in this._accounts)
+      account.unInit();
+    gAccountsService = null;
+    delete this._accounts;
+    delete this._accountsById;
+    Services.prefs.removeObserver(kPrefMessengerAccounts, this._prefObserver);
+    delete this._prefObserver;
+  },
+
+  autoLoginStatus: Ci.imIAccountsService.AUTOLOGIN_ENABLED,
+  _initAutoLoginStatus: function() {
+    /* If auto-login is already disabled, do nothing */
+    if (this.autoLoginStatus != Ci.imIAccountsService.AUTOLOGIN_ENABLED)
+      return;
+
+    let prefs = Services.prefs;
+    if (!prefs.getIntPref("messenger.startup.action")) {
+      // the value 0 means that we start without connecting the accounts
+      this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_USER_DISABLED;
+      return;
+    }
+
+    /* Disable auto-login if we are running in safe mode */
+    if (Services.appinfo.inSafeMode) {
+      this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_SAFE_MODE;
+      return;
+    }
+
+    /* If the application was started offline, disable auto-login */
+    if (!Services.io.manageOfflineStatus && Services.io.offline) {
+      this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_START_OFFLINE;
+      return;
+    }
+
+    /* Check if we crashed at the last startup during autologin */
+    let autoLoginPending;
+    if (prefs.getPrefType(kPrefAutologinPending) == prefs.PREF_INVALID ||
+        !(autoLoginPending = prefs.getIntPref(kPrefAutologinPending))) {
+      // if the pref isn't set, then we haven't crashed: keep autologin enabled
+      return;
+    }
+
+    // Last autologin hasn't finished properly.
+    // For now, assume it's because of a crash.
+    this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_CRASH;
+    prefs.deleteBranch(kPrefAutologinPending);
+
+    // If the crash reporter isn't built, we can't know anything more.
+    if (!("nsICrashReporter" in Ci))
+      return;
+
+    try {
+      // Try to get more info with breakpad
+      let lastCrashTime = 0;
+
+      /* Locate the LastCrash file */
+      let lastCrash = Services.dirsvc.get("UAppData", Ci.nsILocalFile);
+      lastCrash.append("Crash Reports");
+      lastCrash.append("LastCrash");
+      if (lastCrash.exists()) {
+        /* Ok, the file exists, now let's try to read it */
+        let is = Cc["@mozilla.org/network/file-input-stream;1"]
+                 .createInstance(Ci.nsIFileInputStream);
+        let sis = Cc["@mozilla.org/scriptableinputstream;1"]
+                  .createInstance(Ci.nsIScriptableInputStream);
+        is.init(lastCrash, -1, 0, 0);
+        sstream.init(sis);
+
+        lastCrashTime = parseInt(sstream.read(lastCrash.fileSize));
+
+        sstream.close();
+        fstream.close();
+      }
+      // The file not existing is totally acceptable, it just means that
+      // either we never crashed or breakpad is not enabled.
+      // In this case, lastCrashTime will keep its 0 initialization value.
+
+      /*dump("autoLoginPending = " + autoLoginPending +
+             ", lastCrash = " + lastCrashTime +
+             ", difference = " + lastCrashTime - autoLoginPending + "\n");*/
+
+      if (lastCrashTime < autoLoginPending) {
+        // the last crash caught by breakpad is older than our last autologin
+        // attempt.
+        // If breakpad is currently enabled, we can be confident that
+        // autologin was interrupted for an exterior reason
+        // (application killed by the user, power outage, ...)
+        try {
+          Services.appinfo.QueryInterface(Ci.nsICrashReporter)
+                  .annotateCrashReport("=", "");
+        } catch (e) {
+          // This should fail with NS_ERROR_INVALID_ARG if breakpad is enabled,
+          // and NS_ERROR_NOT_INITIALIZED if it is not.
+          if (e.result != Cr.NS_ERROR_NOT_INITIALIZED)
+            this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_ENABLED;
+        }
+      }
+    } catch (e) {
+      // if we failed to get the last crash time, then keep the
+      // AUTOLOGIN_CRASH value in mAutoLoginStatus and return.
+      return;
+    }
+  },
+
+  processAutoLogin: function() {
+    if (Services.io.offline)
+      throw Cr.NS_ERROR_FAILURE;
+
+    for each (let account in this._accounts)
+      account.checkAutoLogin();
+
+    // Make sure autologin is now enabled, so that we don't display a
+    // message stating that it is disabled and asking the user if it
+    // should be processed now.
+    this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_ENABLED;
+
+    // Notify observers so that any message stating that autologin is
+    // disabled can be removed
+    Services.obs.notifyObservers(this, "autologin-processed", null);
+  },
+
+  _checkingIfPasswordStillMissing: false,
+  _checkIfPasswordStillMissing: function() {
+    // Avoid recursion.
+    if (this._checkingIfPasswordStillMissing)
+      return;
+
+    this._checkingIfPasswordStillMissing = true;
+    for each (let account in this._accounts)
+      account._checkIfPasswordStillMissing();
+    delete this._checkingIfPasswordStillMissing;
+  },
+
+  getAccountById: function(aAccountId) {
+    if (aAccountId.indexOf(kAccountKeyPrefix) != 0)
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    let id = parseInt(aAccountId.substr(kAccountKeyPrefix.length));
+    return this.getAccountByNumericId(id);
+  },
+
+  getAccountByNumericId: function(aAccountId) this._accountsById[aAccountId],
+  getAccounts: function() new nsSimpleEnumerator(this._accounts),
+
+  createAccount: function(aName, aPrpl) {
+    // Ensure an account with the same name and protocol doesn't already exist.
+    let prpl = Services.core.getProtocolById(aPrpl);
+    if (!prpl)
+      throw Cr.NS_ERROR_UNEXPECTED;
+    if (prpl.accountExists(aName)) {
+      Cu.reportError("Attempted to create a duplicate account!");
+      throw Cr.NS_ERROR_ALREADY_INITIALIZED;
+    }
+
+    /* First get a unique id for the new account. */
+    let id;
+    for (id = 1; ; ++id) {
+      if (this._accountsById.hasOwnProperty(id))
+        continue;
+
+      /* id isn't used by a known account, double check it isn't
+       already used in the sqlite database. This should never
+       happen, except if we have a corrupted profile. */
+      if (!Services.contacts.accountIdExists(id))
+        break;
+      Services.console.logStringMessage("No account " + id + " but there is some data in the buddy list for an account with this number. Your profile may be corrupted.");
+    }
+
+    /* Actually create the new account. */
+    let key = kAccountKeyPrefix + id;
+    let account = new imAccount(key, aName, aPrpl);
+
+    /* Keep it in the local account lists. */
+    this._accounts.push(account);
+    this._accountsById[id] = account;
+
+    /* Save the account list pref. */
+    let list = this._accountList;
+    this._accountList = list ? list + "," + key : key;
+
+    Services.obs.notifyObservers(account, "account-added", null);
+    return account;
+  },
+
+  deleteAccount: function(aAccountId) {
+    let account = this.getAccountById(aAccountId);
+    if (!account)
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    let index = this._accounts.indexOf(account);
+    if (index == -1)
+      throw Cr.NS_ERROR_UNEXPECTED;
+
+    let id = account.numericId;
+    account.remove();
+    this._accounts.splice(index, 1);
+    delete this._accountsById[id];
+    Services.obs.notifyObservers(account, "account-removed", null);
+
+    /* Update the account list pref. */
+    let list = this._accountList;
+    this._accountList =
+      list.split(",").filter(function (k) k.trim() != aAccountId).join(",");
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.imIAccountsService]),
+  classDescription: "Accounts",
+  classID: Components.ID("{a94b5427-cd8d-40cf-b47e-b67671953e70}"),
+  contractID: "@mozilla.org/chat/accounts-service;1"
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([AccountsService]);
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imAccounts.manifest
@@ -0,0 +1,2 @@
+component {a94b5427-cd8d-40cf-b47e-b67671953e70} imAccounts.js
+contract @mozilla.org/chat/accounts-service;1 {a94b5427-cd8d-40cf-b47e-b67671953e70}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imCommands.js
@@ -0,0 +1,254 @@
+/* ***** 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
+ * 2011.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * 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 {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource:///modules/imServices.jsm");
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", function()
+  l10nHelper("chrome://chat/locale/commands.properties")
+);
+
+function CommandsService() { }
+CommandsService.prototype = {
+  initCommands: function() {
+    this._commands = {};
+    // The say command is directly implemented in the UI layer, but has a
+    // dummy command registered here so it shows up as a command (e.g. when
+    // using the /help command).
+    this.registerCommand({
+      name: "say",
+      get helpString() _("sayHelpString"),
+      usageContext: Ci.imICommand.CONTEXT_ALL,
+      priority: Ci.imICommand.PRIORITY_HIGH,
+      run: function(aMsg, aConv) {
+        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+      }
+    });
+
+    this.registerCommand({
+      name: "raw",
+      get helpString() _("rawHelpString"),
+      usageContext: Ci.imICommand.CONTEXT_ALL,
+      priority: Ci.imICommand.PRIORITY_DEFAULT,
+      run: function(aMsg, aConv) {
+        aConv.sendMsg(aMsg);
+        return true;
+      }
+    });
+
+    this.registerCommand({
+      // Reference the command service so we can use the internal properties
+      // directly.
+      cmdSrv: this,
+
+      name: "help",
+      get helpString() _("helpHelpString"),
+      usageContext: Ci.imICommand.CONTEXT_ALL,
+      priority: Ci.imICommand.PRIORITY_DEFAULT,
+      run: function(aMsg, aConv) {
+        let conv = Services.conversations.getUIConversation(aConv);
+        if (!conv)
+          return false;
+
+        // Handle when no command is given, list all possible commands that are
+        // available for this conversation (alphabetically).
+        if (!aMsg) {
+          let commands = this.cmdSrv.listCommandsForConversation(aConv, {});
+          if (!commands.length)
+            return false;
+
+          // Concatenate the command names (separated by a comma and space).
+          let cmds = commands.map(function(aCmd) aCmd.name).sort().join(", ");
+          let message = _("commands", cmds);
+
+          // Display the message
+          conv.systemMessage(message);
+          return true;
+        }
+
+        // A command name was given, find the commands that match.
+        let cmdArray = this.cmdSrv._findCommands(aConv, aMsg);
+
+        if (!cmdArray.length) {
+          // No command that matches.
+          let message = _("noCommand", aMsg);
+          conv.systemMessage(message);
+          return true;
+        }
+
+        // Only show the help for the one of the highest priority.
+        let cmd = cmdArray[0];
+
+        let text = cmd.helpString;
+        if (!text)
+          text = _("noHelp", cmd.name);
+
+        // Display the message.
+        conv.systemMessage(text);
+        return true;
+      }
+    });
+
+    // Status commands
+    let status = {
+      back: "AVAILABLE",
+      away: "AWAY",
+      busy: "UNAVAILABLE",
+      dnd: "UNAVAILABLE",
+      offline: "OFFLINE"
+    };
+    for (let cmd in status) {
+      let statusValue = Ci.imIStatusInfo["STATUS_" + status[cmd]];
+      this.registerCommand({
+        name: cmd,
+        get helpString() _("statusCommand", this.name, _(this.name)),
+        usageContext: Ci.imICommand.CONTEXT_ALL,
+        priority: Ci.imICommand.PRIORITY_HIGH,
+        run: function(aMsg) {
+          Services.core.globalUserStatus.setStatus(statusValue, aMsg);
+          return true;
+        }
+      });
+    }
+  },
+  unInitCommands: function() {
+    delete this._commands;
+  },
+
+  registerCommand: function(aCommand, aPrplId) {
+    let name = aCommand.name;
+    if (!name)
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    if (!(this._commands.hasOwnProperty(name)))
+      this._commands[name] = {};
+    this._commands[name][aPrplId || ""] = aCommand;
+  },
+  unregisterCommand: function(aCommandName, aPrplId) {
+    if (this._commands.hasOwnProperty(aCommandName)) {
+      let prplId = aPrplId || "";
+      let commands = this._commands[aCommandName];
+      if (commands.hasOwnProperty(aPrplId))
+        delete commands[aPrplId];
+      if (!Object.keys(commands).length)
+        delete this._commands[aCommandName];
+    }
+  },
+  listCommandsForConversation: function(aConversation, commandCount) {
+    let result = [];
+    let prplId = aConversation && aConversation.account.protocol.id;
+    for (let name in this._commands) {
+      let commands = this._commands[name];
+      if (commands.hasOwnProperty(""))
+        result.push(commands[""]);
+      if (prplId && commands.hasOwnProperty(prplId))
+        result.push(commands[prplId]);
+    }
+    result = result.filter(this._usageContextFilter(aConversation));
+    commandCount.value = result.length;
+    return result;
+  },
+  // List only the commands for a protocol (excluding the global commands).
+  listCommandsForProtocol: function(aPrplId, commandCount) {
+    if (!aPrplId)
+      throw "You must provide a prpl ID.";
+
+    let result = [];
+    for (let name in this._commands) {
+      let commands = this._commands[name];
+      if (commands.hasOwnProperty(aPrplId))
+        result.push(commands[aPrplId]);
+    }
+    commandCount.value = result.length;
+    return result;
+  },
+  _usageContextFilter: function(aConversation) {
+    let usageContext =
+      Ci.imICommand["CONTEXT_" + (aConversation.isChat ? "CHAT" : "IM")];
+    return function(c) c.usageContext & usageContext;
+  },
+  _findCommands: function(aConversation, aName) {
+    if (!(this._commands.hasOwnProperty(aName)))
+      return [];
+
+    // Get the 2 possible commands (the global and the proto specific)
+    let cmdArray = [];
+    let commands = this._commands[aName];
+    if (commands.hasOwnProperty(""))
+      cmdArray.push(commands[""]);
+
+    if (aConversation) {
+      let prplId = aConversation.account.protocol.id;
+      if (commands.hasOwnProperty(prplId))
+        cmdArray.push(commands[prplId]);
+    }
+
+    // Remove the commands that can't apply in this context.
+    cmdArray = cmdArray.filter(this._usageContextFilter(aConversation));
+
+    // Sort the matching commands by priority before returning the array.
+    return cmdArray.sort(function(a, b) b.priority - a.priority);
+  },
+  executeCommand: function (aMessage, aConversation) {
+    if (!aMessage)
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    let matchResult;
+    if (aMessage[0] != "/" ||
+        !(matchResult = /^\/([a-z]+)(?: |$)([\s\S]*)/.exec(aMessage)))
+      return false;
+
+    let [, name, args] = matchResult;
+
+    let cmdArray = this._findCommands(aConversation, name);
+    if (!cmdArray.length)
+      return false;
+
+    // cmdArray contains commands sorted by priority, attempt to apply
+    // them in order until one succeeds.
+    return cmdArray.some(function (aCmd) aCmd.run(args, aConversation));
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.imICommandsService]),
+  classDescription: "Commands",
+  classID: Components.ID("{7cb20c68-ccc8-4a79-b6f1-0b4771ed6c23}"),
+  contractID: "@mozilla.org/chat/commands-service;1"
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([CommandsService]);
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imCommands.manifest
@@ -0,0 +1,2 @@
+component {7cb20c68-ccc8-4a79-b6f1-0b4771ed6c23} imCommands.js
+contract @mozilla.org/chat/commands-service;1 {7cb20c68-ccc8-4a79-b6f1-0b4771ed6c23}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imContacts.js
@@ -0,0 +1,1417 @@
+/* ***** 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
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/imServices.jsm");
+
+var gDBConnection = null;
+
+function getDBConnection()
+{
+  const NS_APP_USER_PROFILE_50_DIR = "ProfD";
+  let dbFile = Services.dirsvc.get(NS_APP_USER_PROFILE_50_DIR, Ci.nsIFile);
+  dbFile.append("blist.sqlite");
+
+  let conn =
+    Cc["@mozilla.org/storage/service;1"].getService(Ci.mozIStorageService)
+                                        .openDatabase(dbFile);
+  if (!conn.connectionReady)
+    throw Cr.NS_ERROR_UNEXPECTED;
+
+  // Grow blist db in 512KB increments.
+  conn.setGrowthIncrement(512 * 1024, "");
+
+  // Create tables and indexes.
+  [
+    "CREATE TABLE IF NOT EXISTS accounts (" +
+      "id INTEGER PRIMARY KEY, " +
+      "name VARCHAR, " +
+      "prpl VARCHAR)",
+
+    "CREATE TABLE IF NOT EXISTS contacts (" +
+      "id INTEGER PRIMARY KEY, " +
+      "firstname VARCHAR, " +
+      "lastname VARCHAR, " +
+      "alias VARCHAR)",
+
+    "CREATE TABLE IF NOT EXISTS buddies (" +
+      "id INTEGER PRIMARY KEY, " +
+      "key VARCHAR NOT NULL, " +
+      "name VARCHAR NOT NULL, " +
+      "srv_alias VARCHAR, " +
+      "position INTEGER, " +
+      "icon BLOB, " +
+      "contact_id INTEGER)",
+    "CREATE INDEX IF NOT EXISTS buddies_contactindex " +
+      "ON buddies (contact_id)",
+
+    "CREATE TABLE IF NOT EXISTS tags (" +
+      "id INTEGER PRIMARY KEY, " +
+      "name VARCHAR UNIQUE NOT NULL, " +
+      "position INTEGER)",
+
+    "CREATE TABLE IF NOT EXISTS contact_tag (" +
+      "contact_id INTEGER NOT NULL, " +
+      "tag_id INTEGER NOT NULL)",
+    "CREATE INDEX IF NOT EXISTS contact_tag_contactindex " +
+      "ON contact_tag (contact_id)",
+    "CREATE INDEX IF NOT EXISTS contact_tag_tagindex " +
+      "ON contact_tag (tag_id)",
+
+    "CREATE TABLE IF NOT EXISTS account_buddy (" +
+      "account_id INTEGER NOT NULL, " +
+      "buddy_id INTEGER NOT NULL, " +
+      "status VARCHAR, " +
+      "tag_id INTEGER)",
+    "CREATE INDEX IF NOT EXISTS account_buddy_accountindex " +
+      "ON account_buddy (account_id)",
+    "CREATE INDEX IF NOT EXISTS account_buddy_buddyindex " +
+      "ON account_buddy (buddy_id)"
+  ].forEach(conn.executeSimpleSQL);
+
+  return conn;
+}
+
+// Wrap all the usage of DBConn inside a transaction that will be
+// commited automatically at the end of the event loop spin so that
+// we flush buddy list data to disk only once per event loop spin.
+var gDBConnWithPendingTransaction = null;
+this.__defineGetter__("DBConn", function() {
+  if (gDBConnWithPendingTransaction)
+    return gDBConnWithPendingTransaction;
+
+  if (!gDBConnection)
+    gDBConnection = getDBConnection();
+  gDBConnWithPendingTransaction = gDBConnection;
+  gDBConnection.beginTransaction();
+  executeSoon(function() {
+    gDBConnWithPendingTransaction.commitTransaction();
+    gDBConnWithPendingTransaction = null;
+  });
+  return gDBConnection;
+});
+
+function TagsService() { }
+TagsService.prototype = {
+  get wrappedJSObject() this,
+  createTag: function(aName) {
+    // If the tag already exists, we don't want to create a duplicate.
+    let tag = this.getTagByName(aName);
+    if (tag)
+      return tag;
+
+    let statement = DBConn.createStatement("INSERT INTO tags (name, position) VALUES(:name, 0)");
+    statement.params.name = aName;
+    statement.executeStep();
+
+    tag = new Tag(DBConn.lastInsertRowID, aName);
+    Tags.push(tag);
+    return tag;
+  },
+  // Get an existing tag by (numeric) id. Returns null if not found.
+  getTagById: function(aId) TagsById[aId],
+  // Get an existing tag by name (will do an SQL query). Returns null
+  // if not found.
+  getTagByName: function(aName) {
+    let statement = DBConn.createStatement("SELECT id FROM tags where name = :name");
+    statement.params.name = aName;
+    if (!statement.executeStep())
+      return null;
+    return this.getTagById(statement.row.id);
+  },
+  // Get an array of all existing tags.
+  getTags: function(aTagCount) {
+    if (aTagCount)
+      aTagCount.value = Tags.length;
+    return Tags;
+  },
+
+  isTagHidden: function(aTag) aTag.id in otherContactsTag._hiddenTags,
+  hideTag: function(aTag) { otherContactsTag.hideTag(aTag); },
+  showTag: function(aTag) { otherContactsTag.showTag(aTag); },
+  get otherContactsTag() {
+    otherContactsTag._initContacts();
+    return otherContactsTag;
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.imITagsService]),
+  classDescription: "Tags",
+  classID: Components.ID("{1fa92237-4303-4384-b8ac-4e65b50810a5}"),
+  contractID: "@mozilla.org/chat/tags-service;1"
+};
+
+// TODO move into the tagsService
+var Tags = [];
+var TagsById = { };
+
+function Tag(aId, aName) {
+  this._id = aId;
+  this._name = aName;
+  this._contacts = [];
+  this._observers = [];
+
+  TagsById[this.id] = this;
+}
+Tag.prototype = {
+  get id() this._id,
+  get name() this._name,
+  set name(aNewName) {
+    let statement = DBConn.createStatement("UPDATE tags SET name = :name WHERE id = :id");
+    statement.params.name = aNewName;
+    statement.params.id = this._id;
+    statement.execute();
+
+    //FIXME move the account buddies if some use this tag as their group
+    return aNewName;
+  },
+  getContacts: function(aContactCount) {
+    let contacts = this._contacts.filter(function(c) !c._empty);
+    if (aContactCount)
+      aContactCount.value = contacts.length;
+    return contacts;
+  },
+  _addContact: function (aContact) {
+    this._contacts.push(aContact);
+  },
+  _removeContact: function (aContact) {
+    let index = this._contacts.indexOf(aContact);
+    if (index != -1)
+      this._contacts.splice(index, 1);
+  },
+
+  addObserver: function(aObserver) {
+    if (this._observers.indexOf(aObserver) == -1)
+      this._observers.push(aObserver);
+  },
+  removeObserver: function(aObserver) {
+    this._observers = this._observers.filter(function(o) o !== aObserver);
+  },
+  notifyObservers: function(aSubject, aTopic, aData) {
+    for each (let observer in this._observers)
+      observer.observe(aSubject, aTopic, aData);
+  },
+
+  getInterfaces: function(countRef) {
+    let interfaces = [Ci.nsIClassInfo, Ci.nsISupports, Ci.imITag];
+    countRef.value = interfaces.length;
+    return interfaces;
+  },
+  getHelperForLanguage: function(language) null,
+  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
+  flags: 0,
+  QueryInterface: XPCOMUtils.generateQI([Ci.imITag, Ci.nsIClassInfo])
+};
+
+
+var otherContactsTag = {
+  hiddenTagsPref: "messenger.buddies.hiddenTags",
+  _hiddenTags: {},
+  _contactsInitialized: false,
+  _saveHiddenTagsPref: function() {
+    Services.prefs.setCharPref(this.hiddenTagsPref,
+                               [id for (id in this._hiddenTags)].join(","));
+  },
+  showTag: function(aTag) {
+    let id = aTag.id;
+    delete this._hiddenTags[id];
+    for each (let contact in this._contacts)
+      if (contact.getTags().some(function(t) t.id == id))
+        this._removeContact(contact);
+
+    aTag.notifyObservers(aTag, "tag-shown", null);
+    Services.obs.notifyObservers(aTag, "tag-shown", null);
+    this._saveHiddenTagsPref();
+  },
+  hideTag: function(aTag) {
+    if (aTag.id < 0 || aTag.id in otherContactsTag._hiddenTags)
+      return;
+
+    this._hiddenTags[aTag.id] = aTag;
+    if (this._contactsInitialized)
+      this._hideTag(aTag);
+
+    aTag.notifyObservers(aTag, "tag-hidden", null);
+    Services.obs.notifyObservers(aTag, "tag-hidden", null);
+    this._saveHiddenTagsPref();
+  },
+  _hideTag: function(aTag) {
+    for each (let contact in aTag.getContacts())
+      if (!(contact.id in this._contacts) &&
+          contact.getTags().every(function(t) t.id in this._hiddenTags, this))
+        this._addContact(contact);
+  },
+  observe: function(aSubject, aTopic, aData) {
+    aSubject.QueryInterface(Ci.imIContact);
+    if (aTopic == "contact-tag-removed") {
+      if (!(aSubject.id in this._contacts) &&
+          !(parseInt(aData) in this._hiddenTags) &&
+          aSubject.getTags().every(function(t) t.id in this._hiddenTags, this))
+        this._addContact(aSubject);
+    }
+    else if (aSubject.id in this._contacts &&
+             (aTopic == "contact-removed" ||
+              (aTopic == "contact-tag-added" &&
+              !(parseInt(aData) in this._hiddenTags))))
+      this._removeContact(aSubject);
+  },
+
+  _initHiddenTags: function() {
+    let pref = Services.prefs.getCharPref(this.hiddenTagsPref);
+    if (!pref)
+      return;
+    for each (let tagId in pref.split(","))
+      this._hiddenTags[tagId] = TagsById[tagId];
+  },
+  _initContacts: function() {
+    if (this._contactsInitialized)
+      return;
+    this._observers = [];
+    this._observer = {
+      self: this,
+      observe: function(aSubject, aTopic, aData) {
+        if (aTopic == "contact-moved-in" && !(aSubject instanceof Contact))
+          return;
+
+        this.self.notifyObservers(aSubject, aTopic, aData);
+      }
+    };
+    this._contacts = {};
+    this._contactsInitialized = true;
+    for each (let tag in this._hiddenTags)
+      this._hideTag(tag);
+    Services.obs.addObserver(this, "contact-tag-added", false);
+    Services.obs.addObserver(this, "contact-tag-removed", false);
+    Services.obs.addObserver(this, "contact-removed", false);
+  },
+
+  // imITag implementation
+  get id() -1,
+  get name() "__others__",
+  set name(aNewName) { throw Cr.NS_ERROR_NOT_AVAILABLE; },
+  getContacts: function(aContactCount) {
+    let contacts = [contact for each (contact in this._contacts)];
+    if (aContactCount)
+      aContactCount.value = contacts.length;
+    return contacts;
+  },
+  _addContact: function (aContact) {
+    this._contacts[aContact.id] = aContact;
+    this.notifyObservers(aContact, "contact-moved-in");
+    for each (let observer in ContactsById[aContact.id]._observers)
+      observer.observe(this, "contact-moved-in", null);
+    aContact.addObserver(this._observer);
+  },
+  _removeContact: function (aContact) {
+    delete this._contacts[aContact.id];
+    aContact.removeObserver(this._observer);
+    this.notifyObservers(aContact, "contact-moved-out");
+    for each (let observer in ContactsById[aContact.id]._observers)
+      observer.observe(this, "contact-moved-out", null);
+  },
+
+  addObserver: function(aObserver) {
+    if (this._observers.indexOf(aObserver) == -1)
+      this._observers.push(aObserver);
+  },
+  removeObserver: function(aObserver) {
+    this._observers = this._observers.filter(function(o) o !== aObserver);
+  },
+  notifyObservers: function(aSubject, aTopic, aData) {
+    for each (let observer in this._observers)
+      observer.observe(aSubject, aTopic, aData);
+  },
+
+  getInterfaces: function(countRef) {
+    let interfaces = [Ci.nsIClassInfo, Ci.nsISupports, Ci.nsIObserver, Ci.imITag];
+    countRef.value = interfaces.length;
+    return interfaces;
+  },
+  getHelperForLanguage: function(language) null,
+  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
+  flags: 0,
+  QueryInterface: XPCOMUtils.generateQI([Ci.imITag, Ci.nsIObserver, Ci.nsIClassInfo])
+};
+
+
+var ContactsById = { };
+var LastDummyContactId = 0;
+function Contact(aId, aAlias) {
+  // Assign a negative id to dummy contacts that have a single buddy
+  this._id = aId || --LastDummyContactId;
+  this._alias = aAlias;
+  this._tags = [];
+  this._buddies = [];
+  this._observers = [];
+
+  ContactsById[this._id] = this;
+}
+Contact.prototype = {
+  _id: 0,
+  get id() this._id,
+  get alias() this._alias,
+  set alias(aNewAlias) {
+    this._ensureNotDummy();
+
+    let statement = DBConn.createStatement("UPDATE contacts SET alias = :alias WHERE id = :id");
+    statement.params.alias = aNewAlias;
+    statement.params.id = this._id;
+    statement.executeAsync();
+
+    let oldDisplayName = this.displayName;
+    this._alias = aNewAlias;
+    this._notifyObservers("display-name-changed", oldDisplayName);
+    for each (let buddy in this._buddies)
+      for each (let accountBuddy in buddy._accounts)
+        accountBuddy.serverAlias = aNewAlias;
+    return aNewAlias;
+  },
+  _ensureNotDummy: function() {
+    if (this._id >= 0)
+      return;
+
+    // Create a real contact for this dummy contact
+    let statement = DBConn.createStatement("INSERT INTO contacts DEFAULT VALUES");
+    statement.execute();
+    delete ContactsById[this._id];
+    let oldId = this._id;
+    this._id = DBConn.lastInsertRowID;
+    ContactsById[this._id] = this;
+    this._notifyObservers("no-longer-dummy", oldId.toString());
+    // Update the contact_id for the single existing buddy of this contact
+    statement = DBConn.createStatement("UPDATE buddies SET contact_id = :id WHERE id = :buddy_id");
+    statement.params.id = this._id;
+    statement.params.buddy_id = this._buddies[0].id;
+    statement.executeAsync();
+  },
+
+  getTags: function(aTagCount) {
+    if (aTagCount)
+      aTagCount.value = this._tags.length;
+    return this._tags;
+  },
+  addTag: function(aTag, aInherited) {
+    if (this.hasTag(aTag))
+      return;
+
+    if (!aInherited) {
+      this._ensureNotDummy();
+      let statement =
+        DBConn.createStatement("INSERT INTO contact_tag (contact_id, tag_id) " +
+                               "VALUES(:contactId, :tagId)");
+      statement.params.contactId = this.id;
+      statement.params.tagId = aTag.id;
+      statement.executeAsync();
+    }
+
+    aTag = TagsById[aTag.id];
+    this._tags.push(aTag);
+    aTag._addContact(this);
+
+    aTag.notifyObservers(this, "contact-moved-in");
+    for each (let observer in this._observers)
+      observer.observe(aTag, "contact-moved-in", null);
+    Services.obs.notifyObservers(this, "contact-tag-added", aTag.id);
+  },
+  /* Remove a tag from the local tags of the contact. */
+  _removeTag: function(aTag) {
+    if (!this.hasTag(aTag) || this._isTagInherited(aTag))
+      return;
+
+    let statement = DBConn.createStatement("DELETE FROM contact_tag " +
+                                           "WHERE contact_id = :contactId " +
+                                           "AND tag_id = :tagId");
+    statement.params.contactId = this.id;
+    statement.params.tagId = aTag.id;
+    statement.executeAsync();
+
+    this._tags = this._tags.filter(function(tag) tag.id != aTag.id);
+    aTag = TagsById[aTag.id];
+    aTag._removeContact(this);
+
+    aTag.notifyObservers(this, "contact-moved-out");
+    for each (let observer in this._observers)
+      observer.observe(aTag, "contact-moved-out", null);
+    Services.obs.notifyObservers(this, "contact-tag-removed", aTag.id);
+  },
+  hasTag: function(aTag) this._tags.some(function (t) t.id == aTag.id),
+  _massMove: false,
+  removeTag: function(aTag) {
+    if (!this.hasTag(aTag))
+      throw "Attempting to remove a tag that the contact doesn't have";
+    if (this._tags.length == 1)
+      throw "Attempting to remove the last tag of a contact";
+
+    this._massMove = true;
+    let hasTag = this.hasTag.bind(this);
+    let newTag = this._tags[this._tags[0].id != aTag.id ? 0 : 1];
+    let moved = false;
+    this._buddies.forEach(function (aBuddy) {
+      aBuddy._accounts.forEach(function (aAccountBuddy) {
+        if (aAccountBuddy.tag.id == aTag.id) {
+          if (aBuddy._accounts.some(function(ab)
+               ab.account.numericId == aAccountBuddy.account.numericId &&
+               ab.tag.id != aTag.id && hasTag(ab.tag))) {
+            // A buddy that already has an accountBuddy of the same
+            // account with another tag of the contact shouldn't be
+            // moved to newTag, just remove the accountBuddy
+            // associated to the tag we are removing.
+            aAccountBuddy.remove();
+            moved = true;
+          }
+          else {
+            try {
+              aAccountBuddy.tag = newTag;
+              moved = true;
+            } catch (e) {
+              // Ignore failures. Some protocol plugins may not implement this.
+            }
+          }
+        }
+      });
+    });
+    this._massMove = false;
+    if (moved)
+      this._moved(aTag, newTag);
+    else {
+      // If we are here, the old tag is not inherited from a buddy, so
+      // just remove the local tag.
+      this._removeTag(aTag);
+    }
+  },
+  _isTagInherited: function(aTag) {
+    for each (let buddy in this._buddies)
+      for each (let accountBuddy in buddy._accounts)
+        if (accountBuddy.tag.id == aTag.id)
+          return true;
+    return false;
+  },
+  _moved: function(aOldTag, aNewTag) {
+    if (this._massMove)
+      return;
+
+    // Avoid xpconnect wrappers.
+    aNewTag = aNewTag && TagsById[aNewTag.id];
+    aOldTag = aOldTag && TagsById[aOldTag.id];
+
+    // Decide what we need to do. Return early if nothing to do.
+    let shouldRemove =
+      aOldTag && this.hasTag(aOldTag) && !this._isTagInherited(aOldTag);
+    let shouldAdd =
+      aNewTag && !this.hasTag(aNewTag) && this._isTagInherited(aNewTag);
+    if (!shouldRemove && !shouldAdd)
+      return;
+
+    // Apply the changes.
+    let tags = this._tags;
+    if (shouldRemove) {
+      tags = tags.filter(function(aTag) aTag.id != aOldTag.id);
+      aOldTag._removeContact(this);
+    }
+    if (shouldAdd) {
+      tags.push(aNewTag);
+      aNewTag._addContact(this);
+    }
+    this._tags = tags;
+
+    // Finally, notify of the changes.
+    if (shouldRemove) {
+      aOldTag.notifyObservers(this, "contact-moved-out");
+      for each (let observer in this._observers)
+        observer.observe(aOldTag, "contact-moved-out", null);
+      Services.obs.notifyObservers(this, "contact-tag-removed", aOldTag.id);
+    }
+    if (shouldAdd) {
+      aNewTag.notifyObservers(this, "contact-moved-in");
+      for each (let observer in this._observers)
+        observer.observe(aNewTag, "contact-moved-in", null);
+      Services.obs.notifyObservers(this, "contact-tag-added", aNewTag.id);
+    }
+    Services.obs.notifyObservers(this, "contact-moved", null);
+  },
+
+  getBuddies: function(aBuddyCount) {
+    if (aBuddyCount)
+      aBuddyCount.value = this._buddies.length;
+    return this._buddies;
+  },
+  get _empty() this._buddies.length == 0 ||
+               this._buddies.every(function(b) b._empty),
+
+  mergeContact: function(aContact) {
+    // Avoid merging the contact with itself or merging into an
+    // already removed contact.
+    if (aContact.id == this.id || !(this.id in ContactsById))
+      throw Components.results.NS_ERROR_INVALID_ARG;
+
+    this._ensureNotDummy();
+    let contact = ContactsById[aContact.id]; // remove XPConnect wrapper
+
+    // Copy all the contact-only tags first, otherwise they would be lost.
+    for each (let tag in contact.getTags())
+      if (!contact._isTagInherited(tag))
+        this.addTag(tag);
+
+    // Adopt each buddy. Removing the last one will delete the contact.
+    for each (let buddy in contact.getBuddies())
+      buddy.contact = this;
+    this._updatePreferredBuddy();
+  },
+  moveBuddyBefore: function(aBuddy, aBeforeBuddy) {
+    let buddy = BuddiesById[aBuddy.id]; // remove XPConnect wrapper
+    let oldPosition = this._buddies.indexOf(buddy);
+    if (oldPosition == -1)
+      throw "aBuddy isn't attached to this contact";
+
+    let newPosition = -1;
+    if (aBeforeBuddy)
+      newPosition = this._buddies.indexOf(BuddiesById[aBeforeBuddy.id]);
+    if (newPosition == -1)
+      newPosition = this._buddies.length - 1;
+
+    if (oldPosition == newPosition)
+      return;
+
+    this._buddies.splice(oldPosition, 1);
+    this._buddies.splice(newPosition, 0, buddy);
+    this._updatePositions(Math.min(oldPosition, newPosition),
+                          Math.max(oldPosition, newPosition));
+    buddy._notifyObservers("position-changed", String(newPosition));
+    this._updatePreferredBuddy(buddy);
+  },
+  adoptBuddy: function(aBuddy) {
+    if (aBuddy.contact.id == this.id)
+      throw Components.results.NS_ERROR_INVALID_ARG;
+
+    let buddy = BuddiesById[aBuddy.id]; // remove XPConnect wrapper
+    buddy.contact = this;
+    this._updatePreferredBuddy(buddy);
+  },
+  _massRemove: false,
+  _removeBuddy: function(aBuddy) {
+    if (this._buddies.length == 1) {
+      if (this._id > 0) {
+        let statement =
+          DBConn.createStatement("DELETE FROM contacts WHERE id = :id");
+        statement.params.id = this._id;
+        statement.executeAsync();
+      }
+      this._notifyObservers("removed");
+      delete ContactsById[this._id];
+
+      for each (let tag in this._tags)
+        tag._removeContact(this);
+      let statement =
+        DBConn.createStatement("DELETE FROM contact_tag WHERE contact_id = :id");
+      statement.params.id = this._id;
+      statement.executeAsync();
+
+      delete this._tags;
+      delete this._buddies;
+      delete this._observers;
+    }
+    else {
+      let index = this._buddies.indexOf(aBuddy);
+      if (index == -1)
+        throw "Removing an unknown buddy from contact " + this._id;
+
+      this._buddies = this._buddies.filter(function(b) b !== aBuddy);
+
+      // If we are actually removing the whole contact, don't bother updating
+      // the positions or the preferred buddy.
+      if (this._massRemove)
+        return;
+
+      // No position to update if the removed buddy is at the last position.
+      if (index < this._buddies.length)
+        this._updatePositions(index);
+
+      if (this._preferredBuddy.id == aBuddy.id)
+        this._updatePreferredBuddy();
+    }
+  },
+  _updatePositions: function(aIndexBegin, aIndexEnd) {
+    if (aIndexEnd === undefined)
+      aIndexEnd = this._buddies.length - 1;
+    if (aIndexBegin > aIndexEnd)
+      throw "_updatePositions: Invalid indexes";
+
+    let statement =
+      DBConn.createStatement("UPDATE buddies SET position = :position " +
+                             "WHERE id = :buddyId");
+    for (let i = aIndexBegin; i <= aIndexEnd; ++i) {
+      statement.params.position = i;
+      statement.params.buddyId = this._buddies[i].id;
+      statement.executeAsync();
+    }
+  },
+
+  detachBuddy: function(aBuddy) {
+    // Should return a new contact with the same list of tags.
+    let buddy = BuddiesById[aBuddy.id];
+    if (buddy.contact.id != this.id)
+      throw Components.results.NS_ERROR_INVALID_ARG;
+    if (buddy.contact._buddies.length == 1)
+      throw Components.results.NS_ERROR_UNEXPECTED;
+
+    // Save the list of tags, it may be destoyed if the buddy was the last one.
+    let tags = buddy.contact.getTags();
+
+    // Create a new dummy contact and use it for the detached buddy.
+    buddy.contact = new Contact();
+
+    // The first tag was inherited during the contact setter.
+    // This will copy the remaining tags.
+    for each (let tag in tags)
+      buddy.contact.addTag(tag);
+
+    return buddy.contact;
+  },
+  remove: function() {
+    this._massRemove = true;
+    for each (let buddy in this._buddies)
+      buddy.remove();
+  },
+
+  // imIStatusInfo implementation
+  _preferredBuddy: null,
+  get preferredBuddy() {
+    if (!this._preferredBuddy)
+      this._updatePreferredBuddy();
+    return this._preferredBuddy;
+  },
+  set preferredBuddy(aBuddy) {
+    let shouldNotify = this._preferredBuddy != null;
+    let oldDisplayName =
+      this._preferredBuddy && this._preferredBuddy.displayName;
+    this._preferredBuddy = aBuddy;
+    if (shouldNotify)
+      this._notifyObservers("preferred-buddy-changed");
+    if (oldDisplayName && this._preferredBuddy.displayName != oldDisplayName)
+      this._notifyObservers("display-name-changed", oldDisplayName);
+    this._updateStatus();
+  },
+  // aBuddy indicate which buddy's availability has changed.
+  _updatePreferredBuddy: function(aBuddy) {
+    if (aBuddy) {
+      aBuddy = BuddiesById[aBuddy.id]; // remove potential XPConnect wrapper
+
+      if (!this._preferredBuddy) {
+        this.preferredBuddy = aBuddy;
+        return;
+      }
+
+      if (aBuddy.id == this._preferredBuddy.id) {
+        // The suggested buddy is already preferred, check if its
+        // availability has changed.
+        if (aBuddy.statusType > this._statusType ||
+            (aBuddy.statusType == this._statusType &&
+             aBuddy.availabilityDetails >= this._availabilityDetails)) {
+          // keep the currently preferred buddy, only update the status.
+          this._updateStatus();
+          return;
+        }
+        // We aren't sure that the currently preferred buddy should
+        // still be preferred. Let's go through the list!
+      }
+      else {
+        // The suggested buddy is not currently preferred. If it is
+        // more available or at a better position, prefer it!
+        if (aBuddy.statusType > this._statusType ||
+            (aBuddy.statusType == this._statusType &&
+             (aBuddy.availabilityDetails > this._availabilityDetails ||
+              (aBuddy.availabilityDetails == this._availabilityDetails &&
+               this._buddies.indexOf(aBuddy) < this._buddies.indexOf(this.preferredBuddy)))))
+          this.preferredBuddy = aBuddy;
+        return;
+      }
+    }
+
+    let preferred;
+    // |this._buddies| is ordered by user preference, so in case of
+    // equal availability, keep the current value of |preferred|.
+    for each (let buddy in this._buddies) {
+      if (!preferred || preferred.statusType < buddy.statusType ||
+          (preferred.statusType == buddy.statusType &&
+           preferred.availabilityDetails < buddy.availabilityDetails))
+        preferred = buddy;
+    }
+    if (preferred && (!this._preferredBuddy ||
+                      preferred.id != this._preferredBuddy.id))
+      this.preferredBuddy = preferred;
+  },
+  _updateStatus: function() {
+    let buddy = this._preferredBuddy; // for convenience
+
+    // Decide which notifications should be fired.
+    let notifications = [];
+    if (this._statusType != buddy.statusType ||
+        this._availabilityDetails != buddy.availabilityDetails)
+      notifications.push("availability-changed");
+    if (this._statusType != buddy.statusType ||
+        this._statusText != buddy.statusText) {
+      notifications.push("status-changed");
+      if (this.online && buddy.statusType <= Ci.imIStatusInfo.STATUS_OFFLINE)
+        notifications.push("signed-off");
+      if (!this.online && buddy.statusType > Ci.imIStatusInfo.STATUS_OFFLINE)
+        notifications.push("signed-on");
+    }
+
+    // Actually change the stored status.
+    [this._statusType, this._statusText, this._availabilityDetails] =
+      [buddy.statusType, buddy.statusText, buddy.availabilityDetails];
+
+    // Fire the notifications.
+    notifications.forEach(function(aTopic) {
+      this._notifyObservers(aTopic);
+    }, this);
+  },
+  get displayName() this._alias || this.preferredBuddy.displayName,
+  get buddyIconFilename() this.preferredBuddy.buddyIconFilename,
+  _statusType: 0,
+  get statusType() this._statusType,
+  get online() this.statusType > Ci.imIStatusInfo.STATUS_OFFLINE,
+  get available() this.statusType == Ci.imIStatusInfo.STATUS_AVAILABLE,
+  get idle() this.statusType == Ci.imIStatusInfo.STATUS_IDLE,
+  get mobile() this.statusType == Ci.imIStatusInfo.STATUS_MOBILE,
+  _statusText: "",
+  get statusText() this._statusText,
+  _availabilityDetails: 0,
+  get availabilityDetails() this._availabilityDetails,
+  get canSendMessage() this.preferredBuddy.canSendMessage,
+  //XXX should we list the buddies in the tooltip?
+  getTooltipInfo: function() this.preferredBuddy.getTooltipInfo(),
+  createConversation: function() {
+    let uiConv = Services.conversations.getUIConversationByContactId(this.id);
+    if (uiConv)
+      return uiConv.target;
+    return this.preferredBuddy.createConversation();
+  },
+
+  addObserver: function(aObserver) {
+    if (this._observers.indexOf(aObserver) == -1)
+      this._observers.push(aObserver);
+  },
+  removeObserver: function(aObserver) {
+    if (!this.hasOwnProperty("_observers"))
+      return;
+
+    this._observers = this._observers.filter(function(o) o !== aObserver);
+  },
+  // internal calls + calls from add-ons
+  notifyObservers: function(aSubject, aTopic, aData) {
+    for each (let observer in this._observers)
+      if ("observe" in observer) // avoid failing on destructed XBL bindings...
+        observer.observe(aSubject, aTopic, aData);
+    for each (let tag in this._tags)
+      tag.notifyObservers(aSubject, aTopic, aData);
+    Services.obs.notifyObservers(aSubject, aTopic, aData);
+  },
+  _notifyObservers: function(aTopic, aData) {
+    this.notifyObservers(this, "contact-" + aTopic, aData);
+  },
+
+  // This is called by the imIBuddy implementations.
+  _observe: function(aSubject, aTopic, aData) {
+    // Forward the notification.
+    this.notifyObservers(aSubject, aTopic, aData);
+
+    let isPreferredBuddy =
+      aSubject instanceof Buddy && aSubject.id == this.preferredBuddy.id;
+    switch (aTopic) {
+      case "buddy-availability-changed":
+        this._updatePreferredBuddy(aSubject);
+        break;
+      case "buddy-status-changed":
+        if (isPreferredBuddy)
+          this._updateStatus();
+        break;
+      case "buddy-display-name-changed":
+        if (isPreferredBuddy && !this._alias)
+          this._notifyObservers("display-name-changed", aData);
+        break;
+      case "buddy-icon-changed":
+        if (isPreferredBuddy)
+          this._notifyObservers("icon-changed");
+        break;
+      case "buddy-added":
+        // Currently buddies are always added in dummy empty contacts,
+        // later we may want to check this._buddies.length == 1.
+        this._notifyObservers("added");
+        break;
+      case "buddy-removed":
+        this._removeBuddy(aSubject);
+    }
+  },
+
+  getInterfaces: function(countRef) {
+    let interfaces = [Ci.nsIClassInfo, Ci.nsISupports, Ci.imIContact];
+    countRef.value = interfaces.length;
+    return interfaces;
+  },
+  getHelperForLanguage: function(language) null,
+  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
+  flags: 0,
+  QueryInterface: XPCOMUtils.generateQI([Ci.imIContact, Ci.nsIClassInfo])
+};
+
+var BuddiesById = { };
+function Buddy(aId, aKey, aName, aSrvAlias, aContactId) {
+  this._id = aId;
+  this._key = aKey;
+  this._name = aName;
+  if (aSrvAlias)
+    this._srvAlias = aSrvAlias;
+  this._accounts = [];
+  this._observers = [];
+
+  if (aContactId)
+    this._contact = ContactsById[aContactId];
+  // Avoid failure if aContactId was invalid.
+  if (!this._contact)
+    this._contact = new Contact(null, null);
+
+  this._contact._buddies.push(this);
+
+  BuddiesById[this._id] = this;
+}
+Buddy.prototype = {
+  get id() this._id,
+  destroy: function() {
+    for each (let ab in this._accounts)
+      ab.unInit();
+    delete this._accounts;
+    delete this._observers;
+    delete this._preferredAccount;
+  },
+  get protocol() this._accounts[0].account.protocol,
+  get userName() this._name,
+  get normalizedName() this._key,
+  _srvAlias: "",
+  _contact: null,
+  get contact() this._contact,
+  set contact(aContact) /* not in imIBuddy */ {
+    if (aContact.id == this._contact.id)
+      throw Components.results.NS_ERROR_INVALID_ARG;
+
+    this._notifyObservers("moved-out-of-contact");
+    this._contact._removeBuddy(this);
+
+    this._contact = aContact;
+    this._contact._buddies.push(this);
+
+    // Ensure all the inherited tags are in the new contact.
+    for each (let accountBuddy in this._accounts)
+      this._contact.addTag(TagsById[accountBuddy.tag.id], true);
+
+    let statement =
+      DBConn.createStatement("UPDATE buddies SET contact_id = :contactId, " +
+                             "position = :position " +
+                             "WHERE id = :buddyId");
+    statement.params.contactId = aContact.id > 0 ? aContact.id : 0;
+    statement.params.position = aContact._buddies.length - 1;
+    statement.params.buddyId = this.id;
+    statement.executeAsync();
+
+    this._notifyObservers("moved-into-contact");
+    return aContact;
+  },
+  _hasAccountBuddy: function(aAccountId, aTagId) {
+    for each (let ab in this._accounts) {
+      if (ab.account.numericId == aAccountId && ab.tag.id == aTagId)
+        return true;
+    }
+    return false;
+  },
+  getAccountBuddies: function(aAccountBuddyCount) {
+    if (aAccountBuddyCount)
+      aAccountBuddyCount.value = this._accounts.length;
+    return this._accounts;
+  },
+
+  _addAccount: function(aAccountBuddy, aTag) {
+    this._accounts.push(aAccountBuddy);
+    let contact = this._contact;
+    if (this._contact._tags.indexOf(aTag) == -1) {
+      this._contact._tags.push(aTag);
+      aTag._addContact(contact);
+    }
+
+    if (!this._preferredAccount)
+      this._preferredAccount = aAccountBuddy;
+  },
+  get _empty() this._accounts.length == 0,
+
+  remove: function() {
+    for each (let account in this._accounts)
+      account.remove();
+  },
+
+  // imIStatusInfo implementation
+  _preferredAccount: null,
+  get preferredAccountBuddy() this._preferredAccount,
+  _isPreferredAccount: function(aAccountBuddy) {
+    if (aAccountBuddy.account.numericId != this._preferredAccount.account.numericId)
+      return false;
+
+    // In case we have more than one accountBuddy for the same buddy
+    // and account (possible if the buddy is in several groups on the
+    // server), the protocol plugin may be broken and not update all
+    // instances, so ensure we handle the notifications on the instance
+    // that is currently being notified of a change:
+    this._preferredAccount = aAccountBuddy;
+
+    return true;
+  },
+  set preferredAccount(aAccount) {
+    let oldDisplayName =
+      this._preferredAccount && this._preferredAccount.displayName;
+    this._preferredAccount = aAccount;
+    this._notifyObservers("preferred-account-changed");
+    if (oldDisplayName && this._preferredAccount.displayName != oldDisplayName)
+      this._notifyObservers("display-name-changed", oldDisplayName);
+    this._updateStatus();
+  },
+  // aAccount indicate which account's availability has changed.
+  _updatePreferredAccount: function(aAccount) {
+    if (aAccount) {
+      if (aAccount.account.numericId == this._preferredAccount.account.numericId) {
+        // The suggested account is already preferred, check if its
+        // availability has changed.
+        if (aAccount.statusType > this._statusType ||
+            (aAccount.statusType == this._statusType &&
+             aAccount.availabilityDetails >= this._availabilityDetails)) {
+          // keep the currently preferred account, only update the status.
+          this._updateStatus();
+          return;
+        }
+        // We aren't sure that the currently preferred account should
+        // still be preferred. Let's go through the list!
+      }
+      else {
+        // The suggested account is not currently preferred. If it is
+        // more available, prefer it!
+        if (aAccount.statusType > this._statusType ||
+            (aAccount.statusType == this._statusType &&
+             aAccount.availabilityDetails > this._availabilityDetails))
+          this.preferredAccount = aAccount;
+        return;
+      }
+    }
+
+    let preferred;
+    //TODO take into account the order of the account-manager list.
+    for each (let account in this._accounts) {
+      if (!preferred || preferred.statusType < account.statusType ||
+          (preferred.statusType == account.statusType &&
+           preferred.availabilityDetails < account.availabilityDetails))
+        preferred = account;
+    }
+    if (!this._preferredAccount) {
+      if (preferred)
+        this.preferredAccount = preferred;
+      return;
+    }
+    if (preferred.account.numericId != this._preferredAccount.account.numericId)
+      this.preferredAccount = preferred;
+    else
+      this._updateStatus();
+  },
+  _updateStatus: function() {
+    let account = this._preferredAccount; // for convenience
+
+    // Decide which notifications should be fired.
+    let notifications = [];
+    if (this._statusType != account.statusType ||
+        this._availabilityDetails != account.availabilityDetails)
+      notifications.push("availability-changed");
+    if (this._statusType != account.statusType ||
+        this._statusText != account.statusText) {
+      notifications.push("status-changed");
+      if (this.online && account.statusType <= Ci.imIStatusInfo.STATUS_OFFLINE)
+        notifications.push("signed-off");
+      if (!this.online && account.statusType > Ci.imIStatusInfo.STATUS_OFFLINE)
+        notifications.push("signed-on");
+    }
+
+    // Actually change the stored status.
+    [this._statusType, this._statusText, this._availabilityDetails] =
+      [account.statusType, account.statusText, account.availabilityDetails];
+
+    // Fire the notifications.
+    notifications.forEach(function(aTopic) {
+      this._notifyObservers(aTopic);
+    }, this);
+  },
+  get displayName() this._preferredAccount && this._preferredAccount.displayName ||
+                    this._srvAlias || this._name,
+  get buddyIconFilename() this._preferredAccount.buddyIconFilename,
+  _statusType: 0,
+  get statusType() this._statusType,
+  get online() this.statusType > Ci.imIStatusInfo.STATUS_OFFLINE,
+  get available() this.statusType == Ci.imIStatusInfo.STATUS_AVAILABLE,
+  get idle() this.statusType == Ci.imIStatusInfo.STATUS_IDLE,
+  get mobile() this.statusType == Ci.imIStatusInfo.STATUS_MOBILE,
+  _statusText: "",
+  get statusText() this._statusText,
+  _availabilityDetails: 0,
+  get availabilityDetails() this._availabilityDetails,
+  get canSendMessage() this._preferredAccount.canSendMessage,
+  //XXX should we list the accounts in the tooltip?
+  getTooltipInfo: function() this._preferredAccount.getTooltipInfo(),
+  createConversation: function() this._preferredAccount.createConversation(),
+
+  addObserver: function(aObserver) {
+    if (this._observers.indexOf(aObserver) == -1)
+      this._observers.push(aObserver);
+  },
+  removeObserver: function(aObserver) {
+    this._observers = this._observers.filter(function(o) o !== aObserver);
+  },
+  // internal calls + calls from add-ons
+  notifyObservers: function(aSubject, aTopic, aData) {
+    try {
+      for each (let observer in this._observers)
+        observer.observe(aSubject, aTopic, aData);
+      this._contact._observe(aSubject, aTopic, aData);
+    } catch (e) {
+      Cu.reportError(e);
+    }
+  },
+  _notifyObservers: function(aTopic, aData) {
+    this.notifyObservers(this, "buddy-" + aTopic, aData);
+  },
+
+  // This is called by the imIAccountBuddy implementations.
+  observe: function(aSubject, aTopic, aData) {
+    // Forward the notification.
+    this.notifyObservers(aSubject, aTopic, aData);
+
+    switch (aTopic) {
+      case "account-buddy-availability-changed":
+        this._updatePreferredAccount(aSubject);
+        break;
+      case "account-buddy-status-changed":
+        if (this._isPreferredAccount(aSubject))
+          this._updateStatus();
+        break;
+      case "account-buddy-display-name-changed":
+        if (this._isPreferredAccount(aSubject)) {
+          this._srvAlias =
+            this.displayName != this.userName ? this.displayName : "";
+          let statement =
+            DBConn.createStatement("UPDATE buddies SET srv_alias = :srvAlias " +
+                                   "WHERE id = :buddyId");
+          statement.params.buddyId = this.id;
+          statement.params.srvAlias = this._srvAlias;
+          statement.executeAsync();
+          this._notifyObservers("display-name-changed", aData);
+        }
+        break;
+      case "account-buddy-icon-changed":
+        if (this._isPreferredAccount(aSubject))
+          this._notifyObservers("icon-changed");
+        break;
+      case "account-buddy-added":
+        if (this._accounts.length == 0) {
+          // Add the new account in the empty buddy instance.
+          // The TagsById hack is to bypass the xpconnect wrapper.
+          this._addAccount(aSubject, TagsById[aSubject.tag.id]);
+          this._updateStatus();
+          this._notifyObservers("added");
+        }
+        else {
+          this._accounts.push(aSubject);
+          this.contact._moved(null, aSubject.tag);
+          this._updatePreferredAccount(aSubject);
+        }
+        break;
+      case "account-buddy-removed":
+        if (this._accounts.length == 1) {
+          let statement =
+            DBConn.createStatement("DELETE FROM buddies WHERE id = :id");
+          statement.params.id = this.id;
+          statement.execute();
+
+          this._notifyObservers("removed");
+
+          delete BuddiesById[this._id];
+          this.destroy();
+        }
+        else {
+          this._accounts = this._accounts.filter(function (ab) {
+            return (ab.account.numericId != aSubject.account.numericId ||
+                    ab.tag.id != aSubject.tag.id);
+          });
+          if (this._preferredAccount.account.numericId == aSubject.account.numericId &&
+              this._preferredAccount.tag.id == aSubject.tag.id) {
+            this._preferredAccount = null;
+            this._updatePreferredAccount();
+          }
+          this.contact._moved(aSubject.tag);
+        }
+        break;
+    }
+  },
+
+  getInterfaces: function(countRef) {
+    let interfaces = [Ci.nsIClassInfo, Ci.nsISupports, Ci.imIBuddy];
+    countRef.value = interfaces.length;
+    return interfaces;
+  },
+  getHelperForLanguage: function(language) null,
+  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
+  flags: 0,
+  QueryInterface: XPCOMUtils.generateQI([Ci.imIBuddy, Ci.nsIClassInfo])
+};
+
+
+function ContactsService() { }
+ContactsService.prototype = {
+  initContacts: function() {
+    let statement = DBConn.createStatement("SELECT id, name FROM tags");
+    while (statement.executeStep())
+      Tags.push(new Tag(statement.getInt32(0), statement.getUTF8String(1)));
+
+    statement = DBConn.createStatement("SELECT id, alias FROM contacts");
+    while (statement.executeStep())
+      new Contact(statement.getInt32(0), statement.getUTF8String(1));
+
+    statement =
+      DBConn.createStatement("SELECT contact_id, tag_id FROM contact_tag");
+    while (statement.executeStep()) {
+      let contact = ContactsById[statement.getInt32(0)];
+      let tag = TagsById[statement.getInt32(1)];
+      contact._tags.push(tag);
+      tag._addContact(contact);
+    }
+
+    statement = DBConn.createStatement("SELECT id, key, name, srv_alias, contact_id FROM buddies ORDER BY position");
+    while (statement.executeStep())
+      new Buddy(statement.getInt32(0), statement.getUTF8String(1),
+                statement.getUTF8String(2), statement.getUTF8String(3),
+                statement.getInt32(4));
+    // FIXME is there a way to enforce that all AccountBuddies of a Buddy have the same protocol?
+
+    statement = DBConn.createStatement("SELECT account_id, buddy_id, tag_id FROM account_buddy");
+    while (statement.executeStep()) {
+      let accountId = statement.getInt32(0);
+      let buddyId = statement.getInt32(1);
+      let tagId = statement.getInt32(2);
+
+      if (!BuddiesById.hasOwnProperty(buddyId)) {
+        Cu.reportError("Corrupted database: account_buddy entry for account " +
+                       accountId + " and tag " + tagId +
+                       " references unknown buddy with id " + buddyId);
+        continue;
+      }
+
+      let buddy = BuddiesById[buddyId];
+      if (buddy._hasAccountBuddy(accountId, tagId)) {
+        Cu.reportError("Corrupted database: duplicated account_buddy entry: " +
+                       "account_id = " + accountId + ", buddy_id = " + buddyId +
+                       ", tag_id = " + tagId);
+        continue;
+      }
+
+      let account = Services.accounts.getAccountByNumericId(accountId);
+      let tag = TagsById[tagId];
+      try {
+        let accountBuddy = account.loadBuddy(buddy, tag);
+        if (accountBuddy)
+          buddy._addAccount(accountBuddy, tag);
+      } catch (e) {
+        // FIXME accountBuddy shouldn't be NULL (once imAccounts.js is finished)
+        // It currently doesn't work right with unknown protocols.
+        Components.utils.reportError(e);
+        dump(e + "\n");
+      }
+    }
+
+    otherContactsTag._initHiddenTags();
+  },
+  unInitContacts: function() {
+    Tags = [];
+    TagsById = { };
+    // Avoid shutdown leaks caused by references to native components
+    // implementing imIAccountBuddy.
+    for each (let buddy in BuddiesById)
+      buddy.destroy();
+    BuddiesById = { };
+    ContactsById = { };
+  },
+
+  getContactById: function(aId) ContactsById[aId],
+  getBuddyById: function(aId) BuddiesById[aId],
+  getBuddyByNameAndProtocol: function(aNormalizedName, aPrpl) {
+    let statement =
+      DBConn.createStatement("SELECT b.id FROM buddies b " +
+                             "JOIN account_buddy ab ON buddy_id = b.id " +
+                             "JOIN accounts a ON account_id = a.id " +
+                             "WHERE b.key = :buddyName and a.prpl = :prplId");
+    statement.params.buddyName = aNormalizedName;
+    statement.params.prplId = aPrpl.id;
+    if (!statement.executeStep())
+      return null;
+    return BuddiesById[statement.row.id];
+  },
+
+  accountBuddyAdded: function(aAccountBuddy) {
+    let account = aAccountBuddy.account;
+    let normalizedName = aAccountBuddy.normalizedName;
+    let buddy = this.getBuddyByNameAndProtocol(normalizedName, account.protocol);
+    if (!buddy) {
+      let statement =
+        DBConn.createStatement("INSERT INTO buddies " +
+                               "(key, name, srv_alias, position) " +
+                               "VALUES(:key, :name, :srvAlias, 0)");
+      let name = aAccountBuddy.userName;
+      let srvAlias = aAccountBuddy.serverAlias;
+      statement.params.key = normalizedName;
+      statement.params.name = name;
+      statement.params.srvAlias = srvAlias;
+      statement.execute();
+      buddy =
+        new Buddy(DBConn.lastInsertRowID, normalizedName, name, srvAlias, 0);
+    }
+
+    // Initialize the 'buddy' field of the imIAccountBuddy instance.
+    aAccountBuddy.buddy = buddy;
+
+    // Ensure we aren't storing a duplicate entry.
+    let accountId = account.numericId;
+    let tagId = aAccountBuddy.tag.id;
+    if (buddy._hasAccountBuddy(accountId, tagId)) {
+      Cu.reportError("Attempting to store a duplicate account buddy " +
+                     normalizedName + ", account id = " + accountId +
+                     ", tag id = " + tagId);
+      return;
+    }
+
+    // Store the new account buddy.
+    let statement =
+      DBConn.createStatement("INSERT INTO account_buddy " +
+                             "(account_id, buddy_id, tag_id) " +
+                             "VALUES(:accountId, :buddyId, :tagId)");
+    statement.params.accountId = accountId;
+    statement.params.buddyId = buddy.id;
+    statement.params.tagId = tagId;
+    statement.execute();
+
+    // Fire the notifications.
+    buddy.observe(aAccountBuddy, "account-buddy-added");
+  },
+  accountBuddyRemoved: function(aAccountBuddy) {
+    let buddy = aAccountBuddy.buddy;
+    let statement =
+      DBConn.createStatement("DELETE FROM account_buddy " +
+                                    "WHERE account_id = :accountId AND " +
+                                          "buddy_id = :buddyId AND " +
+                                          "tag_id = :tagId");
+    statement.params.accountId = aAccountBuddy.account.numericId;
+    statement.params.buddyId = buddy.id;
+    statement.params.tagId = aAccountBuddy.tag.id;
+    statement.execute();
+
+    buddy.observe(aAccountBuddy, "account-buddy-removed");
+  },
+
+  accountBuddyMoved: function(aAccountBuddy, aOldTag, aNewTag) {
+    let buddy = aAccountBuddy.buddy;
+    let statement =
+      DBConn.createStatement("UPDATE account_buddy " +
+                             "SET tag_id = :newTagId " +
+                             "WHERE account_id = :accountId AND " +
+                                   "buddy_id = :buddyId AND " +
+                                   "tag_id = :oldTagId");
+    statement.params.accountId = aAccountBuddy.account.numericId;
+    statement.params.buddyId = buddy.id;
+    statement.params.oldTagId = aOldTag.id;
+    statement.params.newTagId = aNewTag.id;
+    statement.execute();
+
+    buddy.observe(aAccountBuddy, "account-buddy-moved");
+    ContactsById[buddy.contact.id]._moved(aOldTag, aNewTag);
+  },
+
+  storeAccount: function(aId, aUserName, aPrplId) {
+    let statement =
+      DBConn.createStatement("SELECT name, prpl FROM accounts WHERE id = :id");
+    statement.params.id = aId;
+    if (statement.executeStep()) {
+      if (statement.getUTF8String(0) == aUserName &&
+          statement.getUTF8String(1) == aPrplId)
+        return; // The account is already stored correctly.
+      throw Cr.NS_ERROR_UNEXPECTED; // Corrupted database?!?
+    }
+
+    // Actually store the account.
+    statement = DBConn.createStatement("INSERT INTO accounts (id, name, prpl) " +
+                                       "VALUES(:id, :userName, :prplId)");
+    statement.params.id = aId;
+    statement.params.userName = aUserName;
+    statement.params.prplId = aPrplId;
+    statement.execute();
+  },
+  accountIdExists: function(aId) {
+    let statement =
+      DBConn.createStatement("SELECT id FROM accounts WHERE id = :id");
+    statement.params.id = aId;
+    return statement.executeStep();
+  },
+  forgetAccount: function(aId) {
+    let statement =
+      DBConn.createStatement("DELETE FROM accounts WHERE id = :accountId");
+    statement.params.accountId = aId;
+    statement.execute();
+
+    // removing the account from the accounts table is not enought,
+    // we need to remove all the associated account_buddy entries too
+    statement = DBConn.createStatement("DELETE FROM account_buddy " +
+                                       "WHERE account_id = :accountId");
+    statement.params.accountId = aId;
+    statement.execute();
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.imIContactsService]),
+  classDescription: "Contacts",
+  classID: Components.ID("{8c3725dd-ee26-489d-8135-736015af8c7f}"),
+  contractID: "@mozilla.org/chat/contacts-service;1"
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([ContactsService,
+                                                      TagsService]);
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imContacts.manifest
@@ -0,0 +1,4 @@
+component {8c3725dd-ee26-489d-8135-736015af8c7f} imContacts.js
+contract @mozilla.org/chat/contacts-service;1 {8c3725dd-ee26-489d-8135-736015af8c7f}
+component {1fa92237-4303-4384-b8ac-4e65b50810a5} imContacts.js
+contract @mozilla.org/chat/tags-service;1 {1fa92237-4303-4384-b8ac-4e65b50810a5}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imConversations.js
@@ -0,0 +1,470 @@
+/* ***** 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 {classes: Cc, interfaces: Ci, 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");
+
+var gLastUIConvId = 0;
+var gLastPurpleConvId = 0;
+
+XPCOMUtils.defineLazyGetter(this, "bundle", function()
+  Services.strings.createBundle("chrome://chat/locale/conversations.properties")
+);
+
+function UIConversation(aPurpleConversation)
+{
+  this._purpleConv = {};
+  this.id = ++gLastUIConvId;
+  this._observers = [];
+  this._messages = [];
+  this.changeTargetTo(aPurpleConversation);
+  let iface = Ci["prplIConv" + (aPurpleConversation.isChat ? "Chat" : "IM")];
+  this._interfaces = this._interfaces.concat(iface);
+  let contact = this.contact;
+  if (contact) {
+    // XPConnect will create a wrapper around 'this' here,
+    // so the list of exposed interfaces shouldn't change anymore.
+    contact.addObserver(this);
+    this._observedContact = contact;
+  }
+  Services.obs.notifyObservers(this, "new-ui-conversation", null);
+}
+
+UIConversation.prototype = {
+  __proto__: ClassInfo(["imIConversation", "prplIConversation", "nsIObserver"],
+                       "UI conversation"),
+  _observedContact: null,
+  get contact() {
+    let target = this.target;
+    if (!target.isChat && target.buddy)
+      return target.buddy.buddy.contact;
+    return null;
+  },
+  get target() this._purpleConv[this._currentTargetId],
+  set target(aPurpleConversation) {
+    this.changeTargetTo(aPurpleConversation);
+  },
+  _currentTargetId: 0,
+  changeTargetTo: function(aPurpleConversation) {
+    let id = aPurpleConversation.id;
+    if (this._currentTargetId == id)
+      return;
+
+    if (!(id in this._purpleConv)) {
+      this._purpleConv[id] = aPurpleConversation;
+      aPurpleConversation.addObserver(this.observeConv.bind(this, id));
+    }
+
+    let shouldNotify = this._currentTargetId;
+    this._currentTargetId = id;
+    if (!this.isChat) {
+      let buddy = this.buddy;
+      if (buddy)
+        ({statusType: this.statusType, statusText: this.statusText}) = buddy;
+    }
+    if (shouldNotify) {
+      this.notifyObservers(this, "target-purple-conversation-changed");
+      let target = this.target;
+      let params = [target.title, target.account.protocol.name];
+      this.systemMessage(bundle.formatStringFromName("targetChanged",
+                                                     params, params.length));
+    }
+  },
+  // Returns a boolean indicating if the ui-conversation was closed.
+  // If the conversation was closed, aContactId.value is set to the contact id
+  // or 0 if no contact was associated with the conversation.
+  removeTarget: function(aPurpleConversation, aContactId) {
+    let id = aPurpleConversation.id;
+    if (!(id in this._purpleConv))
+      throw "unknown purple conversation";
+
+    delete this._purpleConv[id];
+    if (this._currentTargetId != id)
+      return false;
+
+    for (let newId in this._purpleConv) {
+      this.changeTargetTo(this._purpleConv[newId]);
+      return false;
+    }
+
+    if (this._observedContact) {
+      this._observedContact.removeObserver(this);
+      aContactId.value = this._observedContact.id;
+      delete this._observedContact;
+    }
+    else
+      aContactId.value = 0;
+
+    delete this._currentTargetId;
+    this.notifyObservers(this, "ui-conversation-closed");
+    Services.obs.notifyObservers(this, "ui-conversation-closed", null);
+    return true;
+  },
+
+  _unreadMessageCount: 0,
+  get unreadMessageCount() this._unreadMessageCount,
+  _unreadTargetedMessageCount: 0,
+  get unreadTargetedMessageCount() this._unreadTargetedMessageCount,
+  _unreadIncomingMessageCount: 0,
+  get unreadIncomingMessageCount() this._unreadIncomingMessageCount,
+  markAsRead: function() {
+    delete this._unreadMessageCount;
+    delete this._unreadTargetedMessageCount;
+    delete this._unreadIncomingMessageCount;
+    this._notifyUnreadCountChanged();
+  },
+  _lastNotifiedUnreadCount: 0,
+  _notifyUnreadCountChanged: function() {
+    if (this._unreadIncomingMessageCount == this._lastNotifiedUnreadCount)
+      return;
+
+    this._lastNotifiedUnreadCount = this._unreadIncomingMessageCount;
+    for each (let observer in this._observers)
+      observer.observe(this, "unread-message-count-changed",
+                       this._unreadIncomingMessageCount.toString());
+  },
+  getMessages: function(aMessageCount) {
+    if (aMessageCount)
+      aMessageCount.value = this._messages.length;
+    return this._messages;
+  },
+  checkClose: function() {
+    if (!this._currentTargetId)
+      return true; // already closed.
+
+    if (!Services.prefs.getBoolPref("messenger.conversations.alwaysClose") &&
+        (this.isChat && !this.left ||
+         !this.isChat && this.unreadIncomingMessageCount != 0))
+      return false;
+
+    this.close();
+    return true;
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic == "contact-no-longer-dummy") {
+      let oldId = parseInt(aData);
+      // gConversationsService is ugly... :(
+      delete gConversationsService._uiConvByContactId[oldId];
+      gConversationsService._uiConvByContactId[aSubject.id] = this;
+    }
+    else if (aTopic == "account-buddy-status-changed") {
+      if (!this._statusUpdatePending &&
+          aSubject.account.id == this.account.id &&
+          aSubject.buddy.id == this.buddy.buddy.id) {
+        this._statusUpdatePending = true;
+        Services.tm.mainThread.dispatch(this.updateBuddyStatus.bind(this),
+                                        Ci.nsIEventTarget.DISPATCH_NORMAL);
+      }
+    }
+    else if (aTopic == "account-buddy-icon-changed") {
+      if (!this._statusUpdatePending &&
+          aSubject.account.id == this.account.id &&
+          aSubject.buddy.id == this.buddy.buddy.id) {
+        this._iconUpdatePending = true;
+        Services.tm.mainThread.dispatch(this.updateIcon.bind(this),
+                                        Ci.nsIEventTarget.DISPATCH_NORMAL);
+      }
+    }
+  },
+
+  _iconUpdatePending: false,
+  updateIcon: function() {
+    delete this._iconUpdatePending;
+    this.notifyObservers(this, "update-buddy-icon");
+  },
+
+  _statusUpdatePending: false,
+  updateBuddyStatus: function() {
+    delete this._statusUpdatePending;
+    let {statusType: statusType, statusText: statusText} = this.buddy;
+
+    if (("statusType" in this) && this.statusType == statusType &&
+        this.statusText == statusText)
+      return;
+
+    let wasUnknown = this.statusType == Ci.imIStatusInfo.STATUS_UNKNOWN;
+    this.statusType = statusType;
+    this.statusText = statusText;
+
+    this.notifyObservers(this, "update-buddy-status");
+
+    let msg;
+    if (statusType == Ci.imIStatusInfo.STATUS_UNKNOWN)
+      msg = bundle.formatStringFromName("statusUnknown", [this.title], 1);
+    else {
+      let status = Status.toLabel(statusType);
+      let stringId = wasUnknown ? "statusChangedFromUnknown" : "statusChanged";
+      if (statusText) {
+        msg = bundle.formatStringFromName(stringId + "WithStatusText",
+                                          [this.title, status, statusText],
+                                          3);
+      }
+      else
+        msg = bundle.formatStringFromName(stringId, [this.title, status], 2);
+    }
+    this.systemMessage(msg);
+  },
+
+  _disconnected: false,
+  disconnecting: function() {
+    if (this._disconnected)
+      return;
+
+    this._disconnected = true;
+    if (this.contact)
+      return; // handled by the contact observer.
+
+    this.systemMessage(bundle.GetStringFromName("accountDisconnected"));
+    this.notifyObservers(this, "update-buddy-status");
+  },
+  connected: function() {
+    delete this._disconnected;
+    this.notifyObservers(this, "update-buddy-status");
+  },
+
+  observeConv: function(aTargetId, aSubject, aTopic, aData) {
+    if (aTargetId != this._currentTargetId &&
+        (aTopic == "new-text" ||
+         (aTopic == "update-typing" &&
+          this._purpleConv[aTargetId].typingState == Ci.prplIConvIM.TYPING)))
+      this.target = this._purpleConv[aTargetId];
+    if (aTopic == "new-text")
+      Services.obs.notifyObservers(aSubject, aTopic, aData);
+    this.notifyObservers(aSubject, aTopic, aData);
+  },
+
+  systemMessage: function(aText, aIsError) {
+    let flags = {system: true, noLog: true, error: !!aIsError};
+    (new Message("system", aText, flags)).conversation = this;
+  },
+
+  // prplIConversation
+  get isChat() this.target.isChat,
+  get account() this.target.account,
+  get name() this.target.name,
+  get normalizedName() this.target.normalizedName,
+  get title() this.target.title,
+  sendMsg: function (aMsg) { this.target.sendMsg(aMsg); },
+  unInit: function() {
+    for each (let conv in this._purpleConv)
+      gConversationsService.forgetConversation(conv);
+    if (this._observedContact) {
+      this._observedContact.removeObserver(this);
+      delete this._observedContact;
+    }
+    this._purpleConv = {}; // Prevent .close from failing.
+    delete this._currentTargetId;
+  },
+  close: function() {
+    for each (let conv in this._purpleConv)
+      conv.close();
+    if (!this.hasOwnProperty("_currentTargetId"))
+      return;
+    delete this._currentTargetId;
+    this.notifyObservers(this, "ui-conversation-closed");
+    Services.obs.notifyObservers(this, "ui-conversation-closed", null);
+  },
+  addObserver: function(aObserver) {
+    if (this._observers.indexOf(aObserver) == -1)
+      this._observers.push(aObserver);
+  },
+  removeObserver: function(aObserver) {
+    this._observers = this._observers.filter(function(o) o !== aObserver);
+  },
+  notifyObservers: function(aSubject, aTopic, aData) {
+    if (aTopic == "new-text") {
+      this._messages.push(aSubject);
+      ++this._unreadMessageCount;
+      if (aSubject.incoming && !aSubject.system) {
+        ++this._unreadIncomingMessageCount;
+        if (!this.isChat || aSubject.containsNick)
+          ++this._unreadTargetedMessageCount;
+      }
+    }
+    for each (let observer in this._observers) {
+      if (!observer.observe && this._observers.indexOf(observer) == -1)
+        continue; // observer removed by a previous call to another observer.
+      observer.observe(aSubject, aTopic, aData);
+    }
+    this._notifyUnreadCountChanged();
+  },
+
+  // prplIConvIM
+  get buddy() this.target.buddy,
+  get typingState() this.target.typingState,
+  sendTyping: function(aLength) { this.target.sendTyping(aLength); },
+
+  // Chat only
+  getParticipants: function() this.target.getParticipants(),
+  get topic() this.target.topic,
+  set topic(aTopic) { this.target.topic = aTopic; },
+  get topicSetter() this.target.topicSetter,
+  get topicSettable() this.target.topicSettable,
+  get nick() this.target.nick,
+  get left() this.target.left
+};
+
+var gConversationsService;
+function ConversationsService() { gConversationsService = this; }
+ConversationsService.prototype = {
+  get wrappedJSObject() this,
+
+  initConversations: function() {
+    this._uiConv = {};
+    this._uiConvByContactId = {};
+    this._purpleConversations = [];
+    Services.obs.addObserver(this, "account-disconnecting", false);
+    Services.obs.addObserver(this, "account-connected", false);
+  },
+
+  unInitConversations: function() {
+    for each (let UIConv in this._uiConv)
+      UIConv.unInit();
+    delete this._uiConv;
+    delete this._uiConvByContactId;
+    // This should already be empty, but just to be sure...
+    for each (let purpleConv in this._purpleConversations)
+      purpleConv.unInit();
+    delete this._purpleConversations;
+    Services.obs.removeObserver(this, "account-disconnecting");
+    Services.obs.removeObserver(this, "account-connected");
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic == "account-connected") {
+      for each (let conv in this._uiConv) {
+        if (conv.account.id == aSubject.id)
+          conv.connected();
+      }
+    }
+    else if (aTopic == "account-disconnecting") {
+      for each (let conv in this._uiConv) {
+        if (conv.account.id == aSubject.id)
+          conv.disconnecting();
+      }
+    }
+  },
+
+  addConversation: function(aPurpleConversation) {
+    // Give an id to the new conversation.
+    aPurpleConversation.id = ++gLastPurpleConvId;
+    this._purpleConversations.push(aPurpleConversation);
+
+    // Notify observers.
+    Services.obs.notifyObservers(aPurpleConversation, "new-conversation", null);
+
+    // Update or create the corresponding UI conversation.
+    let contactId;
+    if (!aPurpleConversation.isChat) {
+      let accountBuddy = aPurpleConversation.buddy;
+      if (accountBuddy)
+        contactId = accountBuddy.buddy.contact.id;
+    }
+
+    if (contactId) {
+      if (contactId in this._uiConvByContactId) {
+        let uiConv = this._uiConvByContactId[contactId];
+        uiConv.target = aPurpleConversation;
+        this._uiConv[aPurpleConversation.id] = uiConv;
+        return;
+      }
+    }
+
+    let newUIConv = new UIConversation(aPurpleConversation);
+    this._uiConv[aPurpleConversation.id] = newUIConv;
+    if (contactId)
+      this._uiConvByContactId[contactId] = newUIConv;
+  },
+  removeConversation: function(aPurpleConversation) {
+    Services.obs.notifyObservers(aPurpleConversation, "conversation-closed", null);
+
+    let uiConv = this.getUIConversation(aPurpleConversation);
+    let contactId = {};
+    if (uiConv.removeTarget(aPurpleConversation, contactId)) {
+      delete this._uiConv[aPurpleConversation.id];
+      if (contactId.value)
+        delete this._uiConvByContactId[contactId.value];
+    }
+    this.forgetConversation(aPurpleConversation);
+  },
+  forgetConversation: function(aPurpleConversation) {
+    aPurpleConversation.unInit();
+
+    this._purpleConversations =
+      this._purpleConversations.filter(function(c) c !== aPurpleConversation);
+  },
+
+  getUIConversations: function(aConvCount) {
+    let rv = Object.keys(this._uiConv).map(function (k) this._uiConv[k], this);
+    aConvCount.value = rv.length;
+    return rv;
+  },
+  getUIConversation: function(aPurpleConversation) {
+    let id = aPurpleConversation.id;
+    if (id in this._uiConv)
+      return this._uiConv[id];
+    throw "Unknown conversation";
+  },
+  getUIConversationByContactId: function(aId)
+    (aId in this._uiConvByContactId) ? this._uiConvByContactId[aId] : null,
+
+  getConversations: function() new nsSimpleEnumerator(this._purpleConversations),
+  getConversationById: function(aId) {
+    for each (let conv in this._purpleConversations)
+      if (conv.id == aId)
+        return conv;
+    return null;
+  },
+  getConversationByNameAndAccount: function(aName, aAccount, aIsChat) {
+    for each (let conv in this._purpleConversations)
+      if (conv.name == aName && conv.account.numericId == aAccount.numericId &&
+          conv.isChat == aIsChat)
+        return conv;
+    return null;
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.imIConversationsService]),
+  classDescription: "Conversations",
+  classID: Components.ID("{b2397cd5-c76d-4618-8410-f344c7c6443a}"),
+  contractID: "@mozilla.org/chat/conversations-service;1"
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([ConversationsService]);
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imConversations.manifest
@@ -0,0 +1,2 @@
+component {b2397cd5-c76d-4618-8410-f344c7c6443a} imConversations.js
+contract @mozilla.org/chat/conversations-service;1 {b2397cd5-c76d-4618-8410-f344c7c6443a}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imCore.js
@@ -0,0 +1,384 @@
+/* ***** 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
+ * 2011.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+Cu.import("resource:///modules/imServices.jsm");
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "categoryManager",
+                                   "@mozilla.org/categorymanager;1",
+                                   "nsICategoryManager");
+
+const kQuitApplicationGranted = "quit-application-granted";
+const kProtocolPluginCategory = "im-protocol-plugin";
+
+const kPrefReportIdle =        "messenger.status.reportIdle";
+const kPrefUserIconFilename =  "messenger.status.userIconFileName";
+const kPrefUserDisplayname =   "messenger.status.userDisplayName";
+const kPrefTimeBeforeIdle =    "messenger.status.timeBeforeIdle";
+const kPrefAwayWhenIdle =      "messenger.status.awayWhenIdle";
+const kPrefDefaultMessage =    "messenger.status.defaultIdleAwayMessage";
+
+const NS_IOSERVICE_GOING_OFFLINE_TOPIC = "network:offline-about-to-go-offline";
+const NS_IOSERVICE_OFFLINE_STATUS_TOPIC = "network:offline-status-changed";
+
+function UserStatus()
+{
+  this._observers = [];
+
+  if (Services.prefs.getBoolPref(kPrefReportIdle))
+    this._addIdleObserver();
+  Services.prefs.addObserver(kPrefReportIdle, this, false);
+
+  if (Services.io.offline)
+    this._offlineStatusType = Ci.imIStatusInfo.STATUS_OFFLINE;
+  Services.obs.addObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC, false);
+  Services.obs.addObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, false);
+}
+UserStatus.prototype = {
+  __proto__: ClassInfo("imIUserStatusInfo", "User status info"),
+
+  unInit: function() {
+    this._observers = [];
+    Services.prefs.removeObserver(kPrefReportIdle, this);
+    if (this._observingIdleness)
+      this._removeIdleObserver();
+    Services.obs.removeObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC);
+    Services.obs.removeObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC);
+  },
+  _observingIdleness: false,
+  _addIdleObserver: function() {
+    this._observingIdleness = true;
+    this._idleService =
+      Cc["@mozilla.org/widget/idleservice;1"].getService(Ci.nsIIdleService);
+    Services.obs.addObserver(this, "im-sent", false);
+
+    this._timeBeforeIdle = Services.prefs.getIntPref(kPrefTimeBeforeIdle);
+    if (this._timeBeforeIdle < 0)
+      this._timeBeforeIdle = 0;
+    Services.prefs.addObserver(kPrefTimeBeforeIdle, this, false);
+    if (this._timeBeforeIdle)
+      this._idleService.addIdleObserver(this, this._timeBeforeIdle);
+  },
+  _removeIdleObserver: function() {
+    if (this._timeBeforeIdle)
+      this._idleService.removeIdleObserver(this, this._timeBeforeIdle);
+
+    Services.prefs.removeObserver(kPrefTimeBeforeIdle, this);
+    delete this._timeBeforeIdle;
+
+    Services.obs.removeObserver(this, "im-sent");
+    delete this._idleService;
+    delete this._observingIdleness;
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic == "nsPref:changed") {
+      if (aData == kPrefReportIdle) {
+        let reportIdle = Services.prefs.getBoolPref(kPrefReportIdle);
+        if (reportIdle && !this._observingIdleness)
+          this._addIdleObserver();
+        else if (!reportIdle && this._observingIdleness)
+        this._removeIdleObserver();
+      }
+      else if (aData == kPrefTimeBeforeIdle) {
+        let timeBeforeIdle = Services.prefs.getIntPref(kPrefTimeBeforeIdle);
+        if (timeBeforeIdle != this._timeBeforeIdle) {
+          if (this._timeBeforeIdle)
+            this._idleService.removeIdleObserver(this, this._timeBeforeIdle);
+          this._timeBeforeIdle = timeBeforeIdle;
+          if (this._timeBeforeIdle)
+            this._idleService.addIdleObserver(this, this._timeBeforeIdle);
+        }
+      }
+      else
+        throw Cr.NS_ERROR_UNEXPECTED;
+    }
+    else if (aTopic == NS_IOSERVICE_GOING_OFFLINE_TOPIC)
+      this.offline = true;
+    else if (aTopic == NS_IOSERVICE_OFFLINE_STATUS_TOPIC && aData == "online")
+      this.offline = false;
+    else
+      this._checkIdle();
+  },
+
+  _offlineStatusType: Ci.imIStatusInfo.STATUS_AVAILABLE,
+  set offline(aOffline) {
+    let statusType = this.statusType;
+    let statusText = this.statusText;
+    if (aOffline)
+      this._offlineStatusType = Ci.imIStatusInfo.STATUS_OFFLINE;
+    else
+      delete this._offlineStatusType;
+    if (this.statusType != statusType || this.statusText != statusText)
+      this._notifyObservers("status-changed", this.statusText);
+  },
+
+  _idleTime: 0,
+  get idleTime() this._idleTime,
+  set idleTime(aIdleTime) {
+    this._idleTime = aIdleTime;
+    this._notifyObservers("idle-time-changed", aIdleTime);
+  },
+  _idle: false,
+  _idleStatusText: "",
+  _idleStatusType: Ci.imIStatusInfo.STATUS_AVAILABLE,
+  _checkIdle: function() {
+    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) {
+      this.idleTime = idleTime;
+      if (Services.prefs.getBoolPref(kPrefAwayWhenIdle)) {
+        this._idleStatusType = Ci.imIStatusInfo.STATUS_AWAY;
+        this._idleStatusText =
+          Services.prefs.getComplexValue(kPrefDefaultMessage,
+                                         Ci.nsIPrefLocalizedString).data;
+      }
+    }
+    else {
+      this.idleTime = 0;
+      delete this._idleStatusType;
+      delete this._idleStatusText;
+    }
+    if (this.statusType != statusType || this.statusText != statusText)
+      this._notifyObservers("status-changed", this.statusText);
+  },
+
+  _statusText: "",
+  get statusText() this._statusText || this._idleStatusText,
+  _statusType: Ci.imIStatusInfo.STATUS_AVAILABLE,
+  get statusType() Math.min(this._statusType, this._idleStatusType, this._offlineStatusType),
+  setStatus: function(aStatus, aMessage) {
+    if (aStatus != Ci.imIStatusInfo.STATUS_UNKNOWN)
+      this._statusType = aStatus;
+    if (aStatus != Ci.imIStatusInfo.STATUS_OFFLINE)
+      this._statusText = aMessage;
+    this._notifyObservers("status-changed", aMessage);
+  },
+
+  _getProfileDir: function()
+    Services.dirsvc.get("ProfD", Ci.nsIFile),
+  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-" + 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);
+
+    // Now that the new icon has been copied to the profile directory
+    // and the pref value changed, we can remove the old icon. Ignore
+    // failures so that we always fire the user-icon-changed notification.
+    try {
+      if (oldFileName) {
+        folder.append(oldFileName);
+        if (folder.exists())
+          folder.remove(false);
+      }
+    } catch (e) {
+      Cu.reportError(e);
+    }
+
+    this._notifyObservers("user-icon-changed", newName);
+  },
+  getUserIcon: function() {
+    let filename = Services.prefs.getCharPref(kPrefUserIconFilename);
+    if (!filename)
+      return null; // No icon has been set.
+
+    let file = this._getProfileDir();
+    file.append(filename);
+
+    if (!file.exists()) {
+      Services.console.logStringMessage("Invalid userIconFileName preference");
+      return null;
+    }
+
+    return Services.io.newFileURI(file);
+  },
+
+  get displayName() Services.prefs.getComplexValue(kPrefUserDisplayname,
+                                                   Ci.nsISupportsString).data,
+  set displayName(aDisplayName) {
+    let str = Cc["@mozilla.org/supports-string;1"]
+              .createInstance(Ci.nsISupportsString);
+    str.data = aDisplayName;
+    Services.prefs.setComplexValue(kPrefUserDisplayname, Ci.nsISupportsString,
+                                   str);
+    this._notifyObservers("user-display-name-changed", aDisplayName);
+  },
+
+  addObserver: function(aObserver) {
+    if (this._observers.indexOf(aObserver) == -1)
+      this._observers.push(aObserver);
+  },
+  removeObserver: function(aObserver) {
+    this._observers = this._observers.filter(function(o) o !== aObserver);
+  },
+  _notifyObservers: function(aTopic, aData) {
+    for each (let observer in this._observers)
+      observer.observe(this, aTopic, aData);
+  }
+};
+
+var gCoreService;
+function CoreService() { gCoreService = this; }
+CoreService.prototype = {
+  _initialized: false,
+  globalUserStatus: null,
+  init: function() {
+    if (this._initialized)
+      return;
+
+    Services.obs.addObserver(this, kQuitApplicationGranted, false);
+    this._initialized = true;
+
+    Services.cmd.initCommands();
+    this._protos = {};
+
+    this.globalUserStatus = new UserStatus();
+    this.globalUserStatus.addObserver({
+      observe: function(aSubject, aTopic, aData) {
+        Services.obs.notifyObservers(aSubject, aTopic, aData);
+      }
+    });
+
+    let accounts = Services.accounts;
+    accounts.initAccounts();
+    Services.contacts.initContacts();
+    Services.conversations.initConversations();
+
+    if (accounts.autoLoginStatus == Ci.imIAccountsService.AUTOLOGIN_ENABLED)
+      accounts.processAutoLogin();
+  },
+  observe: function(aObject, aTopic, aData) {
+    if (aTopic == kQuitApplicationGranted)
+      this.quit();
+  },
+  quit: function() {
+    if (!this._initialized)
+      throw Cr.NS_ERROR_NOT_INITIALIZED;
+
+    Services.obs.removeObserver(this, kQuitApplicationGranted);
+    Services.obs.notifyObservers(this, "prpl-quit", null);
+
+    Services.conversations.unInitConversations();
+    Services.accounts.unInitAccounts();
+    Services.contacts.unInitContacts();
+    Services.cmd.unInitCommands();
+
+    this.globalUserStatus.unInit();
+    delete this.globalUserStatus;
+    delete this._protos;
+    delete this._initialized;
+  },
+
+  getProtocols: function() {
+    if (!this._initialized)
+      throw Cr.NS_ERROR_NOT_INITIALIZED;
+
+    let protocols = [];
+    let entries = categoryManager.enumerateCategory(kProtocolPluginCategory);
+    while (entries.hasMoreElements()) {
+      let id = entries.getNext().QueryInterface(Ci.nsISupportsCString).data;
+      let proto = this.getProtocolById(id);
+      if (proto)
+        protocols.push(proto);
+    }
+    return new nsSimpleEnumerator(protocols);
+  },
+
+  getProtocolById: function(aPrplId) {
+    if (!this._initialized)
+      throw Cr.NS_ERROR_NOT_INITIALIZED;
+
+    if (this._protos.hasOwnProperty(aPrplId))
+      return this._protos[aPrplId];
+
+    let cid;
+    try {
+      cid = categoryManager.getCategoryEntry(kProtocolPluginCategory, aPrplId);
+    } catch (e) {
+      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.
+      let error = "failed to create an instance of " + cid + ": " + e;
+      dump(error + "\n");
+      Cu.reportError(error);
+    }
+    if (!proto)
+      return null;
+
+    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: "@mozilla.org/chat/core-service;1"
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([CoreService]);
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imCore.manifest
@@ -0,0 +1,2 @@
+component {073f5953-853c-4a38-bd81-255510c31c2e} imCore.js
+contract @mozilla.org/chat/core-service;1 {073f5953-853c-4a38-bd81-255510c31c2e}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/logger.js
@@ -0,0 +1,527 @@
+/* ***** 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
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * 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 {classes: Cc, interfaces: Ci, utils: Cu, Constructor: CC} = Components;
+
+Cu.import("resource:///modules/hiddenWindow.jsm");
+Cu.import("resource:///modules/imServices.jsm");
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/jsProtoHelper.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "logDir", function() {
+  let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+  file.append("logs");
+  return file;
+});
+
+const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
+                           "nsIFileInputStream",
+                           "init");
+const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1",
+                                "nsIConverterInputStream",
+                                "init");
+const LocalFile = CC("@mozilla.org/file/local;1",
+                     "nsILocalFile",
+                     "initWithPath");
+
+const kLineBreak = "@mozilla.org/windows-registry-key;1" in Cc ? "\r\n" : "\n";
+
+function getLogFolderForAccount(aAccount, aCreate)
+{
+  let file = logDir.clone();
+  function createIfNotExists(aFile) {
+    if (aCreate && !aFile.exists())
+      aFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0777);
+  }
+  createIfNotExists(file);
+  file.append(aAccount.protocol.normalizedName);
+  createIfNotExists(file);
+  file.append(aAccount.normalizedName);
+  createIfNotExists(file);
+  return file;
+}
+
+function getNewLogFileName(aFormat)
+{
+  let date = new Date();
+  let dateTime = date.toLocaleFormat("%Y-%m-%d.%H%M%S");
+  let offset = date.getTimezoneOffset();
+  if (offset < 0) {
+    dateTime += "+";
+    offset *= -1;
+  }
+  else
+    dateTime += "-";
+  let minutes = offset % 60;
+  offset = (offset - minutes) / 60;
+  function twoDigits(aNumber)
+    aNumber == 0 ? "00" : aNumber < 10 ? "0" + aNumber : aNumber;
+  if (!aFormat)
+    aFormat = "txt";
+  return dateTime + twoDigits(offset) + twoDigits(minutes) + "." + aFormat;
+}
+
+/* Conversation logs stuff */
+function ConversationLog(aConversation)
+{
+  this._conv = aConversation;
+}
+ConversationLog.prototype = {
+  _log: null,
+  format: "txt",
+  _init: function cl_init() {
+    let file = getLogFolderForAccount(this._conv.account, true);
+    let name = this._conv.normalizedName;
+    if (this._conv.isChat && this._conv.account.protocol.id != "prpl-twitter")
+      name += ".chat";
+    file.append(name);
+    if (!file.exists())
+      file.create(Ci.nsIFile.DIRECTORY_TYPE, 0777);
+    if (Services.prefs.getCharPref("purple.logging.format") == "json")
+      this.format = "json";
+    file.append(getNewLogFileName(this.format));
+    let os = Cc["@mozilla.org/network/file-output-stream;1"].
+             createInstance(Ci.nsIFileOutputStream);
+    const PR_WRITE_ONLY   = 0x02;
+    const PR_CREATE_FILE  = 0x08;
+    const PR_APPEND       = 0x10;
+    os.init(file, PR_WRITE_ONLY | PR_CREATE_FILE | PR_APPEND, 0666, 0);
+    // just to be really sure everything is in UTF8
+    let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
+                    createInstance(Ci.nsIConverterOutputStream);
+    converter.init(os, "UTF-8", 0, 0);
+    this._log = converter;
+    this._log.writeString(this._getHeader());
+  },
+  _getHeader: function cl_getHeader()
+  {
+    let account = this._conv.account;
+    if (this.format == "json") {
+      return JSON.stringify({date: new Date(),
+                             name: this._conv.name,
+                             title: this._conv.title,
+                             account: account.normalizedName,
+                             protocol: account.protocol.normalizedName
+                            }) + "\n";
+    }
+    return "Conversation with " + this._conv.name +
+           " at " + (new Date).toLocaleString() +
+           " on " + account.name +
+           " (" + account.protocol.normalizedName + ")" + kLineBreak;
+  },
+  _serialize: function cl_serialize(aString) {
+    // TODO cleanup once bug 102699 is fixed
+    let doc = getHiddenHTMLWindow().document;
+    let div = doc.createElementNS("http://www.w3.org/1999/xhtml", "div");
+    div.innerHTML = aString.replace(/\r?\n/g, "<br/>").replace(/<br>/gi, "<br/>");
+    const type = "text/plain";
+    let encoder =
+      Components.classes["@mozilla.org/layout/documentEncoder;1?type=" + type]
+                .createInstance(Components.interfaces.nsIDocumentEncoder);
+    encoder.init(doc, type, 0);
+    encoder.setContainerNode(div);
+    encoder.setNodeFixup({fixupNode: function(aNode, aSerializeKids) {
+      if (aNode.localName == "a" && aNode.hasAttribute("href")) {
+        let url = aNode.getAttribute("href");
+        let content = aNode.textContent;
+        if (url != content)
+          aNode.textContent = content + " (" + url + ")";
+      }
+      return null;
+    }});
+    return encoder.encodeToString();
+  },
+  logMessage: function cl_logMessage(aMessage) {
+    if (!this._log)
+      this._init();
+
+    if (this.format == "json") {
+      let msg = {
+        date: new Date(aMessage.time * 1000),
+        who: aMessage.who,
+        text: aMessage.originalMessage,
+        flags: ["outgoing", "incoming", "system", "autoResponse",
+                "containsNick", "error", "delayed",
+                "noFormat", "containsImages", "notification",
+                "noLinkification"].filter(function(f) aMessage[f])
+      };
+      let alias = aMessage.alias;
+      if (alias && alias != msg.who)
+        msg.alias = alias;
+      this._log.writeString(JSON.stringify(msg) + "\n");
+      return;
+    }
+
+    let date = new Date(aMessage.time * 1000);
+    let line = "(" + date.toLocaleTimeString() + ") ";
+    let msg = this._serialize(aMessage.originalMessage);
+    if (aMessage.system)
+      line += msg;
+    else {
+      let sender = aMessage.alias || aMessage.who;
+      if (aMessage.autoResponse)
+        line += sender + " <AUTO-REPLY>: " + msg;
+      else {
+        if (/^\/me /.test(msg))
+          line += "***" + sender + " " + msg.replace(/^\/me /, "");
+        else
+          line += sender + ": " + msg;
+      }
+    }
+    this._log.writeString(line + kLineBreak);
+  },
+
+  close: function cl_close() {
+    if (this._log) {
+      this._log.close();
+      this._log = null;
+    }
+  }
+};
+
+const dummyConversationLog = {
+  logMessage: function() {},
+  close: function() {}
+};
+
+var gConversationLogs = { };
+function getLogForConversation(aConversation)
+{
+  let id = aConversation.id;
+  if (!(id in gConversationLogs)) {
+    let prefName =
+      "purple.logging.log_" + (aConversation.isChat ? "chats" : "ims");
+    if (Services.prefs.getBoolPref(prefName))
+      gConversationLogs[id] = new ConversationLog(aConversation);
+    else
+      gConversationLogs[id] = dummyConversationLog;
+  }
+  return gConversationLogs[id];
+}
+
+function closeLogForConversation(aConversation)
+{
+  let id = aConversation.id;
+  if (!(id in gConversationLogs))
+    return;
+  gConversationLogs[id].close();
+  delete gConversationLogs[id];
+}
+
+/* System logs stuff */
+function SystemLog(aAccount)
+{
+  this._init(aAccount);
+  this._log.writeString("System log for account " + aAccount.name +
+                        " (" + aAccount.protocol.normalizedName +
+                        ") connected at " +
+                        (new Date()).toLocaleFormat("%c") + kLineBreak);
+}
+SystemLog.prototype = {
+  _log: null,
+  _init: function sl_init(aAccount) {
+    let file = getLogFolderForAccount(aAccount, true);
+    file.append(".system");
+    if (!file.exists())
+      file.create(Ci.nsIFile.DIRECTORY_TYPE, 0777);
+    file.append(getNewLogFileName());
+    let os = Cc["@mozilla.org/network/file-output-stream;1"].
+             createInstance(Ci.nsIFileOutputStream);
+    const PR_WRITE_ONLY   = 0x02;
+    const PR_CREATE_FILE  = 0x08;
+    const PR_APPEND       = 0x10;
+    os.init(file, PR_WRITE_ONLY | PR_CREATE_FILE | PR_APPEND, 0666, 0);
+    // just to be really sure everything is in UTF8
+    let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
+                    createInstance(Ci.nsIConverterOutputStream);
+    converter.init(os, "UTF-8", 0, 0);
+    this._log = converter;
+  },
+  logEvent: function sl_logEvent(aString) {
+    if (!this._log)
+      this._init();
+
+    let date = (new Date()).toLocaleFormat("%x %X");
+    this._log.writeString("---- " + aString + " @ " + date + " ----" + kLineBreak);
+  },
+
+  close: function sl_close() {
+    if (this._log) {
+      this._log.close();
+      this._log = null;
+    }
+  }
+};
+
+const dummySystemLog = {
+  logEvent: function(aString) {},
+  close: function() {}
+};
+
+var gSystemLogs = { };
+function getLogForAccount(aAccount, aCreate)
+{
+  let id = aAccount.id;
+  if (aCreate) {
+    if (id in gSystemLogs)
+      gSystemLogs[id].close();
+    if (!Services.prefs.getBoolPref("purple.logging.log_system"))
+      return dummySystemLog;
+    return (gSystemLogs[id] = new SystemLog(aAccount));
+  }
+
+  return (id in gSystemLogs) && gSystemLogs[id] || dummySystemLog;
+}
+
+function closeLogForAccount(aAccount)
+{
+  let id = aAccount.id;
+  if (!(id in gSystemLogs))
+    return;
+  gSystemLogs[id].close();
+  delete gSystemLogs[id];
+}
+
+function LogMessage(aData, aConversation)
+{
+  this._init(aData.who, aData.text);
+  this._conversation = aConversation;
+  this.time = Math.round(new Date(aData.date) / 1000);
+  if ("alias" in aData)
+    this._alias = aData.alias;
+  for each (let flag in aData.flags)
+    this[flag] = true;
+}
+LogMessage.prototype = GenericMessagePrototype;
+
+function LogConversation(aLineInputStream)
+{
+  let line = {value: ""};
+  let more = aLineInputStream.readLine(line);
+
+  if (!line.value)
+    throw "bad log file";
+
+  let data = JSON.parse(line.value);
+  this.name = data.name;
+  this.title = data.title;
+  this._accountName = data.account;
+  this._protocolName = data.protocol;
+
+  this._messages = [];
+  while (more) {
+    more = aLineInputStream.readLine(line);
+    if (!line.value)
+      break;
+    let data = JSON.parse(line.value);
+    this._messages.push(new LogMessage(data, this));
+  }
+}
+LogConversation.prototype = {
+  __proto__: ClassInfo("imILogConversation", "Log conversation object"),
+  get isChat() false,
+  get buddy() null,
+  get account() ({
+    alias: "",
+    name: this._accountName,
+    normalizedName: this._accountName,
+    protocol: {name: this._protocolName},
+    statusInfo: Services.core.globalUserStatus
+  }),
+  getMessages: function(aMessageCount) {
+    if (aMessageCount)
+      aMessageCount.value = this._messages.length;
+    return this._messages;
+  }
+};
+
+/* Generic log enumeration stuff */
+function Log(aFile)
+{
+  this.file = aFile;
+  this.path = aFile.path;
+  const regexp = /([0-9]{4})-([0-9]{2})-([0-9]{2}).([0-9]{2})([0-9]{2})([0-9]{2})([+-])([0-9]{2})([0-9]{2}).*\.([a-z]+)$/;
+  let r = aFile.leafName.match(regexp);
+  let date = new Date(r[1], r[2] - 1, r[3], r[4], r[5], r[6]);
+  let offset = r[7] * 60 + r[8];
+  if (r[6] == -1)
+    offset *= -1;
+  this.time = date.valueOf() / 1000; // ignore the timezone offset for now (FIXME)
+  this.format = r[10];
+}
+Log.prototype = {
+  __proto__: ClassInfo("imILog", "Log object"),
+  getConversation: function() {
+    if (this.format != "json")
+      return null;
+
+    const PR_RDONLY = 0x01;
+    let fis = new FileInputStream(this.file, PR_RDONLY, 0444,
+                                  Ci.nsIFileInputStream.CLOSE_ON_EOF);
+    let lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
+    lis.QueryInterface(Ci.nsIUnicharLineInputStream);
+    return new LogConversation(lis);
+  }
+};
+
+function LogEnumerator(aEntries)
+{
+  this._entries = aEntries;
+}
+LogEnumerator.prototype = {
+  _entries: [],
+  hasMoreElements: function() {
+    while (this._entries.length > 0 && !this._entries[0].hasMoreElements())
+      this._entries.shift();
+    return this._entries.length > 0;
+  },
+  getNext: function()
+    new Log(this._entries[0].getNext().QueryInterface(Ci.nsIFile)),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator])
+};
+
+function Logger() { }
+Logger.prototype = {
+  _enumerateLogs: function logger__enumerateLogs(aAccount, aNormalizedName) {
+    let file = getLogFolderForAccount(aAccount);
+    file.append(aNormalizedName);
+    if (!file.exists())
+      return EmptyEnumerator;
+
+    return new LogEnumerator([file.directoryEntries]);
+  },
+  getLogFromFile: function logger_getLogFromFile(aFile) new Log(aFile),
+  getLogsForContact: function logger_getLogsForContact(aContact) {
+    let entries = [];
+    aContact.getBuddies().forEach(function (aBuddy) {
+      aBuddy.getAccountBuddies().forEach(function (aAccountBuddy) {
+        let file = getLogFolderForAccount(aAccountBuddy.account);
+        file.append(aAccountBuddy.normalizedName);
+        if (file.exists())
+          entries.push(file.directoryEntries);
+      });
+    });
+    return new LogEnumerator(entries);
+  },
+  getLogsForBuddy: function logger_getLogsForBuddy(aBuddy) {
+    let entries = [];
+    aBuddy.getAccountBuddies().forEach(function (aAccountBuddy) {
+      let file = getLogFolderForAccount(aAccountBuddy.account);
+      file.append(aAccountBuddy.normalizedName);
+      if (file.exists())
+        entries.push(file.directoryEntries);
+    });
+    return new LogEnumerator(entries);
+  },
+  getLogsForAccountBuddy: function logger_getLogsForAccountBuddy(aAccountBuddy)
+    this._enumerateLogs(aAccountBuddy.account, aAccountBuddy.normalizedName),
+  getLogsForConversation: function logger_getLogsForConversation(aConversation) {
+    let name = aConversation.normalizedName;
+    if (aConversation.isChat &&
+        aConversation.account.protocol.id != "prpl-twitter")
+      name += ".chat";
+    return this._enumerateLogs(aConversation.account, name);
+  },
+  getSystemLogsForAccount: function logger_getSystemLogsForAccount(aAccount)
+    this._enumerateLogs(aAccount, ".system"),
+  getSimilarLogs: function(aLog)
+    new LogEnumerator([new LocalFile(aLog.path).parent.directoryEntries]),
+
+  observe: function logger_observe(aSubject, aTopic, aData) {
+    switch (aTopic) {
+    case "profile-after-change":
+      Services.obs.addObserver(this, "final-ui-startup", false);
+      break;
+    case "final-ui-startup":
+      Services.obs.removeObserver(this, "final-ui-startup");
+      ["new-text", "conversation-closed", "conversation-left-chat",
+       "account-connected", "account-disconnected",
+       "account-buddy-status-changed"].forEach(function(aEvent) {
+        Services.obs.addObserver(this, aEvent, false);
+      }, this);
+      break;
+    case "new-text":
+      if (!aSubject.noLog) {
+        let log = getLogForConversation(aSubject.conversation);
+        log.logMessage(aSubject);
+      }
+      break;
+    case "conversation-closed":
+    case "conversation-left-chat":
+      closeLogForConversation(aSubject);
+      break;
+    case "account-connected":
+      getLogForAccount(aSubject, true).logEvent("+++ " + aSubject.name +
+                                                " signed on");
+      break;
+    case "account-disconnected":
+      getLogForAccount(aSubject).logEvent("+++ " + aSubject.name +
+                                          " signed off");
+      closeLogForAccount(aSubject);
+      break;
+    case "account-buddy-status-changed":
+      let status;
+      if (!aSubject.online)
+        status = "Offline";
+      else if (aSubject.mobile)
+        status = "Mobile";
+      else if (aSubject.idle)
+        status = "Idle";
+      else if (aSubject.available)
+        status = "Available";
+      else
+        status = "Unavailable";
+
+      let statusText = aSubject.statusText;
+      if (statusText)
+        status += " (\"" + statusText + "\")";
+
+      let nameText = aSubject.displayName + " (" + aSubject.userName + ")";
+      getLogForAccount(aSubject.account).logEvent(nameText + " is now " + status);
+      break;
+    default:
+      throw "Unexpected notification " + aTopic;
+    }
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.imILogger]),
+  classDescription: "Logger",
+  classID: Components.ID("{fb0dc220-2c7a-4216-9f19-6b8f3480eae9}"),
+  contractID: "@mozilla.org/chat/logger;1"
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([Logger]);
new file mode 100644
--- /dev/null
+++ b/chat/components/src/logger.manifest
@@ -0,0 +1,3 @@
+component {fb0dc220-2c7a-4216-9f19-6b8f3480eae9} logger.js
+contract @mozilla.org/chat/logger;1 {fb0dc220-2c7a-4216-9f19-6b8f3480eae9}
+category profile-after-change Logger @mozilla.org/chat/logger;1
new file mode 100644
--- /dev/null
+++ b/chat/components/src/smileProtocolHandler.js
@@ -0,0 +1,76 @@
+/* ***** 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/imSmileys.jsm");
+
+const kSmileRegexp = /^smile:\/\//;
+
+function smileProtocolHandler() { }
+
+smileProtocolHandler.prototype = {
+  scheme: "smile",
+  defaultPort: -1,
+  protocolFlags: Ci.nsIProtocolHandler.URI_NORELATIVE |
+                 Ci.nsIProtocolHandler.URI_NOAUTH |
+                 Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE |
+                 Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE,
+  newURI: function SPH_newURI(aSpec, aOriginCharset, aBaseURI) {
+    let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI);
+    uri.spec = aSpec;
+    uri.QueryInterface(Ci.nsIMutable);
+    uri.mutable = false;
+    return uri;
+  },
+  newChannel: function SPH_newChannel(aURI) {
+    let smile = aURI.spec.replace(kSmileRegexp, "");
+    let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+    let channel = ios.newChannel(getSmileRealURI(smile), null, null);
+    channel.originalURI = aURI;
+    return channel;
+  },
+  allowPort: function  SPH_allowPort(aPort, aScheme) false,
+
+  classDescription: "Smile Protocol Handler",
+  classID: Components.ID("{04e58eae-dfbc-4c9e-8130-6d9ef19cbff4}"),
+  contractID: "@mozilla.org/network/protocol;1?name=smile",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler])
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([smileProtocolHandler]);
new file mode 100644
--- /dev/null
+++ b/chat/components/src/smileProtocolHandler.manifest
@@ -0,0 +1,2 @@
+component {04e58eae-dfbc-4c9e-8130-6d9ef19cbff4} smileProtocolHandler.js
+contract @mozilla.org/network/protocol;1?name=smile {04e58eae-dfbc-4c9e-8130-6d9ef19cbff4}
new file mode 100644
--- /dev/null
+++ b/chat/content/Makefile.in
@@ -0,0 +1,44 @@
+# ***** 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 Mozilla Browser code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 2002
+# 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 *****
+
+DEPTH		= ../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/chat/content/browserRequest.js
@@ -0,0 +1,175 @@
+/* ***** 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 oauthorizer.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shane Caraveo <shanec@mozillamessaging.com>
+ *   Florian Quèze <florian@instantbird.org>
+ *
+ * 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 wpl = Components.interfaces.nsIWebProgressListener;
+
+var reporterListener = {
+  _isBusy: false,
+  get statusMeter() {
+    delete this.statusMeter;
+    return this.statusMeter = document.getElementById("statusbar-icon");
+  },
+  get securityButton() {
+    delete this.securityButton;
+    return this.securityButton = document.getElementById("security-button");
+  },
+  get securityLabel() {
+    delete this.securityLabel;
+    return this.securityLabel = document.getElementById("security-status");
+  },
+  get securityDisplay() {
+    delete this.securityDisplay;
+    return this.securityDisplay = document.getElementById("security-display");
+  },
+
+  QueryInterface: function(aIID) {
+    if (aIID.equals(Components.interfaces.nsIWebProgressListener)   ||
+        aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+        aIID.equals(Components.interfaces.nsISupports))
+      return this;
+    throw Components.results.NS_NOINTERFACE;
+  },
+  onStateChange: function(/*in nsIWebProgress*/ aWebProgress,
+                     /*in nsIRequest*/ aRequest,
+                     /*in unsigned long*/ aStateFlags,
+                     /*in nsresult*/ aStatus) {
+    if (aStateFlags & wpl.STATE_START &&
+        aStateFlags & wpl.STATE_IS_NETWORK) {
+      this.statusMeter.value = 0;
+      this.statusMeter.parentNode.collapsed = false;
+      this.securityLabel.collapsed = true;
+    }
+    else if (aStateFlags & wpl.STATE_STOP &&
+             aStateFlags & wpl.STATE_IS_NETWORK) {
+      this.statusMeter.parentNode.collapsed = true;
+      this.securityLabel.collapsed = false;
+    }
+  },
+
+  onProgressChange: function(/*in nsIWebProgress*/ aWebProgress,
+                        /*in nsIRequest*/ aRequest,
+                        /*in long*/ aCurSelfProgress,
+                        /*in long */aMaxSelfProgress,
+                        /*in long */aCurTotalProgress,
+                        /*in long */aMaxTotalProgress) {
+    if (aMaxTotalProgress > 0) {
+      let percentage = (aCurTotalProgress * 100) / aMaxTotalProgress;
+      this.statusMeter.value = percentage;
+    }
+  },
+
+  onLocationChange: function(/*in nsIWebProgress*/ aWebProgress,
+                        /*in nsIRequest*/ aRequest,
+                        /*in nsIURI*/ aLocation) {
+    this.securityDisplay.setAttribute('label', aLocation.host);
+  },
+
+  onStatusChange: function(/*in nsIWebProgress*/ aWebProgress,
+                      /*in nsIRequest*/ aRequest,
+                      /*in nsresult*/ aStatus,
+                      /*in wstring*/ aMessage) {
+  },
+
+  onSecurityChange: function(/*in nsIWebProgress*/ aWebProgress,
+                        /*in nsIRequest*/ aRequest,
+                        /*in unsigned long*/ aState) {
+    const wpl_security_bits = wpl.STATE_IS_SECURE |
+                              wpl.STATE_IS_BROKEN |
+                              wpl.STATE_IS_INSECURE |
+                              wpl.STATE_SECURE_HIGH |
+                              wpl.STATE_SECURE_MED |
+                              wpl.STATE_SECURE_LOW;
+    let browser = document.getElementById("requestFrame");
+    let level;
+
+    switch (aState & wpl_security_bits) {
+      case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_HIGH:
+        level = "high";
+        break;
+      case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_MED:
+      case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_LOW:
+        level = "low";
+        break;
+      case wpl.STATE_IS_BROKEN:
+        level = "broken";
+        break;
+    }
+    if (level) {
+      this.securityButton.setAttribute("level", level);
+      this.securityButton.hidden = false;
+      this.securityLabel.setAttribute("label", browser.securityUI.tooltipText);
+    } else {
+      this.securityButton.hidden = true;
+      this.securityButton.removeAttribute("level");
+    }
+    this.securityButton.setAttribute("tooltiptext",
+                                     browser.securityUI.tooltipText);
+  }
+}
+
+function cancelRequest()
+{
+  reportUserClosed();
+  window.close();
+}
+
+function reportUserClosed()
+{
+  let request = window.arguments[0];
+  request.QueryInterface(Components.interfaces.prplIRequestBrowser);
+  request.cancelled();
+}
+
+function loadRequestedUrl()
+{
+  let request = window.arguments[0];
+  request.QueryInterface(Components.interfaces.prplIRequestBrowser);
+  document.getElementById("headerMessage").textContent = request.promptText;
+  let account = request.account;
+  document.getElementById("headerLabel").value =
+    account.protocol.name + " - " + account.name;
+  document.getElementById("headerImage").src =
+    account.protocol.iconBaseURI + "icon48.png";
+
+  let browser = document.getElementById("requestFrame");
+  browser.addProgressListener(reporterListener,
+                              Components.interfaces.nsIWebProgress.NOTIFY_ALL);
+  let url = request.url;
+  if (url != "")
+    browser.setAttribute("src", url);
+  request.loaded(window, browser.webProgress);
+}
new file mode 100644
--- /dev/null
+++ b/chat/content/browserRequest.xul
@@ -0,0 +1,75 @@
+<?xml version="1.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 oauthorizer.
+   -
+   - The Initial Developer of the Original Code is Mozilla.
+   - Portions created by the Initial Developer are Copyright (C) 2010
+   - the Initial Developer. All Rights Reserved.
+   -
+   - Contributor(s):
+   -   Shane Caraveo <shanec@mozillamessaging.com>
+   -   Florian Quèze <florian@instantbird.org>
+   -
+   - 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 LGPL or the GPL. 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 ***** -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://chat/skin/browserRequest.css" type="text/css"?>
+
+<!DOCTYPE window>
+<window id="browserRequest"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        buttons=","
+        onload="loadRequestedUrl()"
+        onclose="reportUserClosed()"
+        title=""
+        width="800"
+        height="500"
+        orient="vertical">
+
+  <script type="application/javascript" src="chrome://chat/content/browserRequest.js"/>
+
+  <keyset id="mainKeyset">
+    <key id="key_close"   key="w" modifiers="accel" oncommand="cancelRequest()"/>
+    <key id="key_close2"  keycode="VK_ESCAPE" oncommand="cancelRequest()"/>
+  </keyset>
+  <hbox id="header">
+    <image id="headerImage" src="chrome://chat/skin/prpl-generic/icon32.png"/>
+    <vbox>
+    <label id="headerLabel"/>
+    <description id="headerMessage"/>
+    </vbox>
+  </hbox>
+  <browser type="content" disablehistory="true" src="about:blank" id="requestFrame" flex="1"/>
+  <statusbar>
+    <statusbarpanel id="security-display" crop="end" flex="1"/>
+    <statusbarpanel id="security-status" crop="end" collapsed="true"/>
+    <statusbarpanel class="statusbarpanel-progress" collapsed="true" id="statusbar-status">
+      <progressmeter class="progressmeter-statusbar" id="statusbar-icon" mode="normal" value="0"/>
+    </statusbarpanel>
+    <statusbarpanel id="security-button" class="statusbarpanel-iconic"/>
+  </statusbar>
+</window>
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/chat/content/convbrowser.xml
@@ -0,0 +1,1036 @@
+<?xml version="1.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 this file as it was released on March 28, 2001.
+   -
+   - The Initial Developer of the Original Code is
+   - Peter Annema.
+   - Portions created by the Initial Developer are Copyright (C) 2001
+   - the Initial Developer. All Rights Reserved.
+   -
+   - Contributor(s):
+   -   Peter Annema <disttsc@bart.nl> (Original Author of <browser>)
+   -   Peter Parente <parente@cs.unc.edu>
+   -   Christopher Thomas <cst@yecc.com>
+   -   Michael Ventnor <m.ventnor@gmail.com>
+   -   Florian Queze <florian@instantbird.org>
+   -
+   - 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 ***** -->
+
+<bindings id="browserBindings"
+          xmlns="http://www.mozilla.org/xbl"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <binding id="browser">
+    <implementation type="application/javascript"
+                    implements="nsIAccessibleProvider, nsIDOMEventListener,
+                                nsIWebProgressListener, nsIController,
+                                nsISelectionListener, nsIObserver">
+      <property name="accessibleType" readonly="true">
+        <getter>
+          <![CDATA[
+            return Components.interfaces.nsIAccessibleProvider.OuterDoc;
+          ]]>
+        </getter>
+      </property>
+
+      <property name="autoscrollEnabled">
+        <getter>
+          <![CDATA[
+            if (this.getAttribute("autoscroll") == "false")
+              return false;
+
+            var enabled = true;
+            try {
+              enabled = Services.prefs.getBoolPref("general.autoScroll");
+            }
+            catch(ex) {
+            }
+
+            return enabled;
+          ]]>
+        </getter>
+      </property>
+
+      <field name="_theme">null</field>
+
+      <property name="theme"
+                readonly="true"
+                onget="return this._theme || (this._theme = getCurrentTheme());;"/>
+
+      <field name="_conv">null</field>
+
+      <field name="_loadState">0</field>
+
+      <method name="init">
+        <parameter name="aConversation"/>
+        <body>
+          <![CDATA[
+            this._conv = aConversation;
+
+            // init is called when the message style preview is
+            // reloaded so we need to reset _theme.
+            this._theme = null;
+
+            // Prevent ongoing asynchronous message display from continuing.
+            this._messageDisplayPending = false;
+
+            // _loadState is 0 while loading conv.html and 1 while
+            // loading the real conversation HTML.
+            this._loadState = 0;
+
+            const URI = "chrome://chat/content/conv.html";
+            const flag = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+            this.webNavigation.loadURI(URI, flag, null, null, null);
+            this.addProgressListener(this);
+
+            if (this._scrollingView)
+              this._autoScrollPopup.hidePopup();
+          ]]>
+        </body>
+      </method>
+
+      <property name="currentURI"
+                onget="return this.webNavigation.currentURI;"
+                readonly="true"/>
+
+      <field name="_docShell">null</field>
+
+      <property name="docShell"
+                onget="return this._docShell || (this._docShell = this.boxObject.QueryInterface(Components.interfaces.nsIContainerBoxObject).docShell);"
+                readonly="true"/>
+
+      <field name="_webNavigation">null</field>
+
+      <property name="webNavigation"
+                onget="return this._webNavigation || (this._webNavigation = this.docShell.QueryInterface(Components.interfaces.nsIWebNavigation));"
+                readonly="true"/>
+
+
+      <field name="_fastFind">null</field>
+      <property name="fastFind"
+                readonly="true">
+        <getter>
+        <![CDATA[
+          if (!this._fastFind) {
+            if (!("@mozilla.org/typeaheadfind;1" in Components.classes))
+              return null;
+
+            if (!this.docShell)
+              return null;
+
+            this._fastFind = Components.classes["@mozilla.org/typeaheadfind;1"]
+                                       .createInstance(Components.interfaces.nsITypeAheadFind);
+            this._fastFind.init(this.docShell);
+          }
+          return this._fastFind;
+        ]]>
+        </getter>
+      </property>
+
+      <property name="webProgress"
+                readonly="true"
+                onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);"/>
+
+      <field name="_contentWindow">null</field>
+
+      <property name="contentWindow"
+                readonly="true"
+                onget="return this._contentWindow || (this._contentWindow = this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow));"/>
+
+      <property name="contentDocument"
+                onget="return this.webNavigation.document;"
+                readonly="true"/>
+
+      <property name="markupDocumentViewer"
+                onget="return this.docShell.contentViewer.QueryInterface(Components.interfaces.nsIMarkupDocumentViewer);"
+                readonly="true"/>
+
+      <field name="magicCopyPref" readonly="true">"messenger.conversations.selections.magicCopyEnabled"</field>
+
+      <property name="magicCopyEnabled"
+                onget="return Services.prefs.getBoolPref(this.magicCopyPref);"
+                readonly="true"/>
+
+      <method name="addProgressListener">
+        <parameter name="aListener"/>
+        <body>
+          <![CDATA[
+            this.webProgress.addProgressListener(aListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
+          ]]>
+        </body>
+      </method>
+
+      <method name="removeProgressListener">
+        <parameter name="aListener"/>
+        <body>
+          <![CDATA[
+            this.webProgress.removeProgressListener(aListener);
+         ]]>
+        </body>
+      </method>
+
+      <field name="mDestroyed">false</field>
+
+      <constructor>
+        <![CDATA[
+          this.addEventListener("scroll", this.browserScroll);
+          this.addEventListener("resize", this.browserResize);
+          Services.prefs.addObserver(this.magicCopyPref, this, false);
+
+          if (!("cleanupImMarkup" in window))
+            Components.utils.import("resource:///modules/imContentSink.jsm");
+          if (!("smileImMarkup" in window))
+            Components.utils.import("resource:///modules/imSmileys.jsm");
+          if (!("getCurrentTheme" in window))
+            Components.utils.import("resource:///modules/imThemes.jsm");
+        ]]>
+      </constructor>
+
+      <destructor>
+        <![CDATA[
+          this.destroy();
+        ]]>
+      </destructor>
+
+      <!-- This is necessary because the destructor doesn't always get called when
+           we are removed from a tabbrowser. This will be explicitly called by tabbrowser -->
+      <method name="destroy">
+        <body>
+          <![CDATA[
+            if (this.mDestroyed)
+              return;
+            this.mDestroyed = true;
+            this._messageDisplayPending = false;
+
+            if (this.mDragDropHandler)
+              this.mDragDropHandler.detach();
+            this.mDragDropHandler = null;
+
+            this._fastFind = null;
+
+            if (this._autoScrollNeedsCleanup) {
+              // we polluted the global scope, so clean it up
+              this._autoScrollPopup.parentNode.removeChild(this._autoScrollPopup);
+            }
+
+            Services.prefs.removeObserver(this.magicCopyPref, this);
+            if (this.magicCopyEnabled)
+              this.contentWindow.controllers.removeController(this);
+          ]]>
+        </body>
+      </method>
+
+      <field name="_autoScrollEnabled">true</field>
+
+      <method name="_updateAutoScrollEnabled">
+        <body>
+          <![CDATA[
+            // Enable auto-scroll if the scrollbar is at the bottom.
+            let body = this.contentDocument.getElementsByTagName("body")[0];
+            this._autoScrollEnabled =
+              body.scrollHeight <= body.scrollTop + body.clientHeight + 10;
+            return this._autoScrollEnabled;
+          ]]>
+         </body>
+      </method>
+
+      <method name="_scrollToElement">
+        <parameter name="aElt"/>
+        <body>
+          <![CDATA[
+            aElt.scrollIntoView(true);
+            this._scrollingIntoView = true;
+          ]]>
+         </body>
+      </method>
+
+      <field name="_textModifiers">[smileTextNode]</field>
+
+      <method name="addTextModifier">
+        <parameter name="aModifier"/>
+        <body>
+          <![CDATA[
+            if (this._textModifiers.indexOf(aModifier) == -1)
+              this._textModifiers.push(aModifier);
+          ]]>
+        </body>
+      </method>
+
+      <!-- These variables are reset in onStateChange. -->
+      <field name="_lastMessage">null</field>
+      <field name="_lastMessageIsContext">true</field>
+      <field name="_firstNonContextElt">null</field>
+
+      <field name="_pendingMessages">[]</field>
+      <field name="_messageDisplayPending">false</field>
+      <method name="appendMessage">
+        <parameter name="aMsg"/>
+        <parameter name="aContext"/>
+        <body>
+          <![CDATA[
+            this._pendingMessages.push({msg: aMsg, context: aContext});
+            if (this._messageDisplayPending)
+              return;
+            this._messageDisplayPending = true;
+            Services.tm.mainThread.dispatch(this.displayPendingMessages.bind(this),
+                                            Ci.nsIEventTarget.DISPATCH_NORMAL);
+          ]]>
+        </body>
+      </method>
+
+      <field name="progressBar">null</field>
+      <!-- These variables are reset in onStateChange. -->
+      <field name="_nextPendingMessageIndex">0</field>
+      <field name="_displayPendingMessagesCalls">0</field>
+      <method name="displayPendingMessages">
+        <body>
+          <![CDATA[
+            if (!this._messageDisplayPending)
+              return;
+
+            let max = this._pendingMessages.length;
+            let begin = Date.now();
+            let i;
+            for (i = this._nextPendingMessageIndex; i < max; ++i) {
+              let msg = this._pendingMessages[i];
+              this.displayMessage(msg.msg, msg.context, i + 1 < max);
+              if (Date.now() - begin > 40)
+                break;
+            }
+            if (i < max - 1) {
+              this._nextPendingMessageIndex = i + 1;
+              if (this.progressBar) {
+                // Show progress bar if after the third call (ca. 120ms)
+                // less than half the messages have been displayed.
+                if (++this._displayPendingMessagesCalls > 2 &&
+                    max > 2 * this._nextPendingMessageIndex)
+                  this.progressBar.hidden = false;
+                this.progressBar.max = max;
+                this.progressBar.value = this._nextPendingMessageIndex;
+              }
+              Services.tm.mainThread.dispatch(this.displayPendingMessages.bind(this),
+                                              Ci.nsIEventTarget.DISPATCH_NORMAL);
+              return;
+            }
+            this._messageDisplayPending = false;
+            this._pendingMessages = [];
+            this._nextPendingMessageIndex = 0;
+            this._displayPendingMessagesCalls = 0;</