Bug 920801 - Port chat/ changes from Instantbird to comm-central - 6 - Bio 1982 - Create Yahoo! Messenger Protocol Plug-In, r=clokep.
authorQuentin Headen <qheaden@phaseshiftsoftware.com>
Fri, 26 Jul 2013 06:38:42 -0400
changeset 17195 747ff789d4b065c27698a0b5eead2d3715ef6db9
parent 17194 a500f206d3a6d553b34532e638ddb2889af14e8d
child 17196 ef20b332dcb5270ff4a46c07b098130246b33433
push id1103
push usermbanner@mozilla.com
push dateTue, 18 Mar 2014 07:44:06 +0000
treeherdercomm-beta@50c6279a0af0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersclokep
bugs920801
Bug 920801 - Port chat/ changes from Instantbird to comm-central - 6 - Bio 1982 - Create Yahoo! Messenger Protocol Plug-In, r=clokep.
chat/locales/en-US/yahoo.properties
chat/locales/jar.mn
chat/moz.build
chat/protocols/yahoo/moz.build
chat/protocols/yahoo/test/test_yahooLoginHelper.js
chat/protocols/yahoo/test/test_yahoopacket.js
chat/protocols/yahoo/test/xpcshell.ini
chat/protocols/yahoo/yahoo-session.jsm
chat/protocols/yahoo/yahoo.js
chat/protocols/yahoo/yahoo.manifest
new file mode 100644
--- /dev/null
+++ b/chat/locales/en-US/yahoo.properties
@@ -0,0 +1,31 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+login.error.badCredentials=Username or password is incorrect.
+login.error.accountLockedFailed=Account locked due to too many failed login attempts.
+login.error.accountLockedGeneral=Account locked due to too many login attempts.
+login.error.accountDeactivated=Account has been deactivated.
+login.error.usernameNotExist=The username does not exist.
+login.error.unknown=Unknown Error %S
+
+network.error.http=HTTP connection error.
+
+conference.invite.message=Join my conference.
+buddy.invite.message=Would you be my chat buddy?
+
+# Some options are commented out because they aren't used. We do the same thing
+# to their description strings.
+options.pagerPort=Pager port
+#options.transferHost=File transfer server
+#options.transferPort=File transfer port
+#options.chatLocale=Chat room locale
+options.chatEncoding=Encoding
+options.ignoreInvites=Ignore conference and chatroom invitations
+#options.proxySSL=Use account proxy for HTTP and HTTPS connections
+
+# In this message, %S is replaced with the username of the user who left.
+system.message.conferenceLogoff=%S has left the conference.
+system.message.conferenceLogon=%S has joined the conference.
+
+command.help.invite=/invite &lt;user1&gt;[,&lt;user2&gt;,...] [&lt;invite message&gt;]: invite a user into a conference chat.
--- a/chat/locales/jar.mn
+++ b/chat/locales/jar.mn
@@ -10,8 +10,9 @@
 	locale/@AB_CD@/chat/commands.properties (%commands.properties)
 	locale/@AB_CD@/chat/conversations.properties (%conversations.properties)
 	locale/@AB_CD@/chat/facebook.properties	(%facebook.properties)
 	locale/@AB_CD@/chat/irc.properties	(%irc.properties)
 	locale/@AB_CD@/chat/logger.properties (%logger.properties)
 	locale/@AB_CD@/chat/status.properties	(%status.properties)
 	locale/@AB_CD@/chat/twitter.properties	(%twitter.properties)
 	locale/@AB_CD@/chat/xmpp.properties	(%xmpp.properties)
+	locale/@AB_CD@/chat/yahoo.properties	(%yahoo.properties)
--- a/chat/moz.build
+++ b/chat/moz.build
@@ -12,13 +12,14 @@ PARALLEL_DIRS += [
     'locales',
     'protocols/facebook',
     'protocols/gtalk',
     'protocols/irc',
     'protocols/odnoklassniki',
     'protocols/twitter',
     'protocols/vkontakte',
     'protocols/xmpp',
+    'protocols/yahoo',
 ]
 
 if CONFIG['MOZ_DEBUG']:
     PARALLEL_DIRS += ['protocols/jsTest']
 
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/moz.build
@@ -0,0 +1,17 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
+
+EXTRA_COMPONENTS += [
+    'yahoo.js',
+    'yahoo.manifest',
+]
+
+EXTRA_JS_MODULES += [
+    'yahoo-session.jsm',
+]
+
+JAR_MANIFESTS += ['jar.mn']
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/test/test_yahooLoginHelper.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource:///modules/ArrayBufferUtils.jsm");
+Components.utils.import("resource:///modules/Services.jsm");
+Components.utils.import("resource:///modules/yahoo-session.jsm");
+let yahoo = {};
+Services.scriptloader.loadSubScript("resource:///modules/yahoo-session.jsm", yahoo);
+
+// Preset test values.
+const kUsername = "testUser";
+const kPassword = "instantbird";
+const kPagerIp = "123.456.78.9";
+const kCrumb = "MG-Z/jNG+Q==";
+const kChallengeString = "AEF08DBAC33F9EEDABCFEA==";
+const kYCookie = "OTJmMTQyOTU1ZGQ4MDA3Y2I2ODljMTU5";
+const kTCookie = "NTdlZmIzY2Q4ODI3ZTc3NTIxYTk1MDhm";
+const kToken = "MThmMzg3OWM3ODcxMW";
+
+const kPagerAddressResponse = "COLO_CAPACITY=1\r\nCS_IP_ADDRESS=" + kPagerIp;
+const kTokenResponse = "0\r\n" + kToken + "\r\npartnerid=dummyValue";
+const kCookieResponse = "0\r\ncrumb=" + kCrumb + "\r\nY=" + kYCookie +
+                        "\r\nT=" + kTCookie + "\r\ncookievalidfor=86400";
+
+/* In each test, we override the function that would normally be called next in
+ * the login process. We do this so that we can intercept the login process,
+ * preventing calls to real Yahoo! servers, and do equality testing. */
+function run_test()
+{
+  add_test(test_pagerAddress);
+  add_test(test_challengeString);
+  add_test(test_loginToken);
+  add_test(test_cookies);
+  run_next_test();
+}
+
+function test_pagerAddress()
+{
+  let helper = new yahoo.YahooLoginHelper({}, {});
+
+  helper._getChallengeString = function() {
+    do_check_eq(kPagerIp, helper._session.pagerAddress);
+    run_next_test();
+  };
+
+  helper._onPagerAddressResponse(kPagerAddressResponse, null);
+}
+
+function test_challengeString()
+{
+  let helper = new yahoo.YahooLoginHelper({}, {});
+
+  helper._getLoginToken = function() {
+    do_check_eq(kChallengeString, helper._challengeString);
+    run_next_test();
+  };
+
+  let response = new yahoo.YahooPacket(yahoo.kPacketType.AuthResponse, 0, 0);
+  response.addValue(1, helper._username);
+  response.addValue(94, kChallengeString);
+  response.addValue(13, 0);
+  helper._onChallengeStringResponse(response.toArrayBuffer());
+}
+
+function test_loginToken()
+{
+  let helper = new yahoo.YahooLoginHelper({}, {});
+
+  helper._getCookies = function() {
+    do_check_eq(kToken, helper._loginToken);
+    run_next_test();
+  };
+
+  helper._onLoginTokenResponse(kTokenResponse, null);
+}
+
+function test_cookies()
+{
+  let helper = new yahoo.YahooLoginHelper({}, {});
+
+  helper._sendPagerAuthResponse = function() {
+    do_check_eq(kCrumb, helper._crumb);
+    do_check_eq(kYCookie, helper._yCookie);
+    do_check_eq(kTCookie, helper._tCookie);
+    run_next_test();
+  };
+
+  helper._onLoginCookiesResponse(kCookieResponse, null);
+}
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/test/test_yahoopacket.js
@@ -0,0 +1,217 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource:///modules/ArrayBufferUtils.jsm");
+Components.utils.import("resource:///modules/Services.jsm");
+Components.utils.import("resource:///modules/yahoo-session.jsm");
+let yahoo = {};
+Services.scriptloader.loadSubScript("resource:///modules/yahoo-session.jsm", yahoo);
+
+const kPacketIdBytes = StringToBytes(yahoo.kPacketIdentfier);
+const kHelloKey = 1;
+const kHelloValue = "Hello";
+const kWorldKey = 20;
+const kWorldValue = "World";
+const kNumberKey = 4;
+const kNumberValue = 32;
+const kParamsKey = 60;
+const kParam1Value = "param1";
+const kParam2Value = "param2";
+const kPacketDataString = "1\xC0\x80Hello\xC0\x8020\xC0\x80World\xC0\x80" +
+                          "4\xC0\x8032\xC0\x8060\xC0\x80param1\xC0\x80" +
+                          "60\xC0\x80param2\xC0\x80";
+
+function run_test()
+{
+  add_test(test_headerCreation);
+  add_test(test_fullPacketCreation);
+  add_test(test_packetDecoding);
+  add_test(test_extractPackets);
+  add_test(test_malformedPacketExtraction);
+
+  run_next_test();
+}
+
+function test_headerCreation()
+{
+  let packetLength = 0;
+  // Random numbers.
+  let serviceNumber = 0x57;
+  let status = 0x04;
+  let sessionId = 0x57842390;
+
+  let packet = new yahoo.YahooPacket(serviceNumber, status, sessionId);
+  let buf = packet.toArrayBuffer();
+  let view = new DataView(buf);
+
+  // Ensure that the first 4 bytes contain the YMSG identifier.
+  for (let i = 0; i < kPacketIdBytes.length; ++i)
+    do_check_eq(kPacketIdBytes[i], view.getUint8(i));
+
+  do_check_eq(yahoo.kProtocolVersion, view.getUint16(4));
+  do_check_eq(yahoo.kVendorId, view.getUint16(6));
+  do_check_eq(packetLength, view.getUint16(8));
+  do_check_eq(serviceNumber, view.getUint16(10));
+  do_check_eq(status, view.getUint32(12));
+  do_check_eq(sessionId, view.getUint32(16));
+
+  run_next_test();
+}
+
+function test_fullPacketCreation()
+{
+  packetLength = kPacketDataString.length;
+  // Random numbers.
+  let serviceNumber = 0x55;
+  let status = 0x02;
+  let sessionId = 0x12567800;
+
+  let packet = new yahoo.YahooPacket(serviceNumber, status, sessionId);
+  packet.addValue(kHelloKey, kHelloValue);
+  packet.addValue(kWorldKey, kWorldValue);
+  packet.addValue(kNumberKey, kNumberValue);
+  packet.addValues(kParamsKey, [kParam1Value, kParam2Value]);
+  let buf = packet.toArrayBuffer();
+  let view = new DataView(buf);
+
+  // Header check.
+
+  // Ensure that the first 4 bytes contain the YMSG identifier.
+  for (let i = 0; i < kPacketIdBytes.length; ++i)
+    do_check_eq(kPacketIdBytes[i], view.getUint8(i));
+
+  do_check_eq(yahoo.kProtocolVersion, view.getUint16(4));
+  do_check_eq(yahoo.kVendorId, view.getUint16(6));
+  do_check_eq(packetLength, view.getUint16(8));
+  do_check_eq(serviceNumber, view.getUint16(10));
+  do_check_eq(status, view.getUint32(12));
+  do_check_eq(sessionId, view.getUint32(16));
+
+  // Packet data check.
+  let dataBytes = StringToBytes(kPacketDataString);
+  for (let i = 0; i < dataBytes.length; ++i)
+    do_check_eq(dataBytes[i], view.getUint8(yahoo.kPacketHeaderSize + i));
+  run_next_test()
+}
+
+function test_packetDecoding()
+{
+  let packetLength = kPacketDataString.length;
+  // Random numbers.
+  let serviceNumber = 0x20;
+  let status = 0x06;
+  let sessionId = 0x13319AB2;
+
+  let buf = new ArrayBuffer(yahoo.kPacketHeaderSize + packetLength);
+  let view = new DataView(buf);
+
+  for (let i = 0; i < kPacketIdBytes.length; ++i)
+    view.setUint8(i, kPacketIdBytes[i]);
+
+  view.setUint16(4, yahoo.kProtocolVersion);
+  view.setUint16(6, yahoo.kVendorId);
+  view.setUint16(8, packetLength);
+  view.setUint16(10, serviceNumber);
+  view.setUint32(12, status);
+  view.setUint32(16, sessionId);
+
+  let dataBuf = BytesToArrayBuffer(StringToBytes(kPacketDataString));
+  copyBytes(buf, dataBuf, 20);
+
+  // Now we decode and test.
+  let packet = new yahoo.YahooPacket();
+  packet.fromArrayBuffer(buf);
+
+  // Test header information.
+  do_check_eq(serviceNumber, packet.service);
+  do_check_eq(status, packet.status);
+  do_check_eq(sessionId, packet.sessionId);
+
+  // Test the getting of single packet data values.
+  do_check_eq(kHelloValue, packet.getValue(kHelloKey));
+  do_check_eq(kWorldValue, packet.getValue(kWorldKey));
+  do_check_eq(kNumberValue, packet.getValue(kNumberKey));
+
+  // Test the getting of multiple values with a single key.
+  let multiValue = packet.getValues(kParamsKey);
+  do_check_eq(2, multiValue.length);
+  do_check_eq(kParam1Value, multiValue[0]);
+  do_check_eq(kParam2Value, multiValue[1]);
+
+  // Test if certain keys are non-existant.
+  do_check_true(packet.hasKey(kHelloKey));
+  do_check_false(packet.hasKey(500)); // There is no key 500.
+
+  run_next_test();
+}
+
+function test_extractPackets()
+{
+  // Some constants for each packet.
+  const kP1Service = 0x47;
+  const kP1Status = 0;
+  const kP1SessionId = 0x12345678;
+  // Used for testing packet verification.
+  const kP1FuzzerKey = 42;
+  const kP1FuzzerValue = "I am using the YMSG protocol!";
+
+  const kP2Service = 0x57;
+  const kP2Status = 5;
+  const kP2SessionId = 0x87654321;
+
+  // First, create two packets and obtain their buffers.
+  let packet1 = new yahoo.YahooPacket(kP1Service, kP1Status, kP1SessionId);
+  packet1.addValue(kHelloKey, kHelloValue);
+  packet1.addValue(kP1FuzzerKey, kP1FuzzerValue);
+  let packet1Buffer = packet1.toArrayBuffer();
+
+  let packet2 = new yahoo.YahooPacket(kP2Service, kP2Status, kP2SessionId);
+  packet2.addValue(kWorldKey, kWorldValue);
+  let packet2Buffer = packet2.toArrayBuffer();
+
+  // Create one full buffer with both packets inside.
+  let fullBuffer = new ArrayBuffer(packet1Buffer.byteLength +
+                                   packet2Buffer.byteLength);
+  copyBytes(fullBuffer, packet1Buffer);
+  copyBytes(fullBuffer, packet2Buffer, packet1Buffer.byteLength);
+
+  // Now, run the packets through the extractPackets() method.
+  let [extractedPackets, bytesHandled] =
+      yahoo.YahooPacket.extractPackets(fullBuffer);
+  do_check_eq(2, extractedPackets.length);
+
+  // Packet 1 checks.
+  let p1 = extractedPackets[0];
+  do_check_eq(kP1Service, p1.service);
+  do_check_eq(kP1Status, p1.status);
+  do_check_eq(kP1SessionId, p1.sessionId);
+  do_check_true(p1.hasKey(kHelloKey));
+  do_check_eq(kHelloValue, p1.getValue(kHelloKey));
+
+  // Packet 2 checks.
+  let p2 = extractedPackets[1];
+  do_check_eq(kP2Service, p2.service);
+  do_check_eq(kP2Status, p2.status);
+  do_check_eq(kP2SessionId, p2.sessionId);
+  do_check_true(p2.hasKey(kWorldKey));
+  do_check_eq(kWorldValue, p2.getValue(kWorldKey));
+
+  // Check if all the bytes were handled.
+  do_check_eq(fullBuffer.byteLength, bytesHandled);
+
+  run_next_test();
+}
+
+function test_malformedPacketExtraction()
+{
+  const kInvalidPacketData = "MSYG1\xC0\x80Hello\xC0\x8020\xC0\x80World\xC0\x80";
+  let buffer = BytesToArrayBuffer(StringToBytes(kInvalidPacketData));
+  let malformed = false;
+  try {
+    yahoo.YahooPacket.extractPackets(buffer);
+  } catch(e) {
+    malformed = true;
+  }
+  do_check_true(malformed);
+  run_next_test();
+}
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/test/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+head =
+tail =
+run-sequentially = Avoid bustage.
+
+[test_yahoopacket.js]
+[test_yahooLoginHelper.js]
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/yahoo-session.jsm
@@ -0,0 +1,918 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["YahooSession"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource:///modules/ArrayBufferUtils.jsm");
+Cu.import("resource:///modules/http.jsm");
+Cu.import("resource:///modules/imServices.jsm");
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/socket.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", function()
+  l10nHelper("chrome://chat/locale/yahoo.properties")
+);
+
+const kProtocolVersion = 16;
+const kVendorId = 0;
+
+const kPacketDataDelimiter = "\xC0\x80";
+const kPacketIdentifier = "YMSG";
+const kPacketHeaderSize = 20;
+
+const kPacketType = {
+  // Sent by a client when logging off of the Yahoo! network.
+  Logoff:         0x02,
+  // Sent by a client when a message is sent to a buddy.
+  Message:        0x06,
+  // Used for inviting others to a conference.
+  ConfInvite:     0x18,
+  // Used as a notification when you or someone else joins a conference room.
+  ConfLogon:      0x19,
+  // Used as a notification when you or someone else leaves a conference room.
+  ConfLogoff:     0x1b,
+  // This is sent by the client when additional users are invited to the
+  // conference, but it can be sent as the first invite as well.
+  ConfAddInvite:  0x1c,
+  // Broadcast to all users in a conference room when someone posts a message.
+  ConfMessage:    0x1d,
+  // Used for typing notifications.
+  Notify:         0x4b,
+  // These two are used during initial authentication with the pager server.
+  AuthResponse:   0x54,
+  Auth:           0x57,
+  // Buddy list controls.
+  AddBuddy:       0x83,
+  RemoveBuddy:    0x84,
+  // This is sent when you reject a Yahoo! user's buddy request.
+  BuddyReqReject: 0x86,
+  // This is sent whenever a buddy changes their status.
+  StatusUpdate:   0xc6,
+  // This is sent when someone wishes to become your buddy.
+  BuddyAuth:      0xd6,
+  // Holds the initial status of all buddies when a user first logs in.
+  StatusInitial:  0xf0,
+  // Contains the buddy list sent from the server.
+  List:           0xf1,
+  // Sent back to the pager server after each received message. Sending this
+  // prevents echoed messages when chatting with the official Yahoo! client.
+  MessageAck:     0xfb
+};
+
+const kPacketStatuses = {
+  Typing: 0x16
+};
+
+// Each Yahoo! error code is mapped to a two-element array. The first element
+// contains the last part of the name of its localized string. This is appended
+// to "login.error." to obtain the string. The second element is the
+// Instantbird error that is given to the error handler.
+const kLoginStatusErrors = {
+  "1212" : ["badCredentials",
+            Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED],
+  "1213" : ["accountLockedFailed",
+            Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED],
+  "1218" : ["accountDeactivated",
+            Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED],
+  "1235" : ["usernameNotExist",
+            Ci.prplIAccount.ERROR_INVALID_USERNAME],
+  "1236" : ["accountLockedGeneral",
+            Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED]
+};
+
+// These are the status codes that buddies can send us.
+const kBuddyStatuses = {
+  // Available.
+  "0"   : Ci.imIStatusInfo.STATUS_AVAILABLE,
+  // Be right back.
+  "1"   : Ci.imIStatusInfo.STATUS_AWAY,
+  // Busy.
+  "2"   : Ci.imIStatusInfo.STATUS_UNAVAILABLE,
+    // Not at home.
+  "3"   : Ci.imIStatusInfo.STATUS_AWAY,
+  // Not at desk.
+  "4"   : Ci.imIStatusInfo.STATUS_AWAY,
+  // Not in office.
+  "5"   : Ci.imIStatusInfo.STATUS_AWAY,
+  // On phone.
+  "6"   : Ci.imIStatusInfo.STATUS_AWAY,
+  // On vacation.
+  "7"   : Ci.imIStatusInfo.STATUS_AWAY,
+  // Out to lunch.
+  "8"   : Ci.imIStatusInfo.STATUS_AWAY,
+  // Stepped out.
+  "9"   : Ci.imIStatusInfo.STATUS_AWAY,
+    // Invisible.
+  "12"  : Ci.imIStatusInfo.STATUS_INVISIBLE,
+  // Custom status.
+  "99"  : Ci.imIStatusInfo.STATUS_AWAY,
+  // Idle.
+  "999" : Ci.imIStatusInfo.STATUS_IDLE
+};
+
+/* The purpose of the YahooSession object is to serve as a gateway between the
+ * protocol plug-in and the Yahoo! Messenger servers. Anytime an object outside
+ * of this file wishes to communicate with the servers, it should do it through
+ * one of the methods provided by YahooSession. By centralizing such network
+ * access, we can easily catch errors, and ensure that communication is handled
+ * correctly. */
+function YahooSession(aAccount)
+{
+  this._account = aAccount;
+  this.binaryMode = true;
+}
+YahooSession.prototype = {
+  __proto__: Socket,
+  _account: null,
+  _socket: null,
+  _username: null,
+  // This is the IPv4 address to the pager server which is the gateway into the
+  // Yahoo! Messenger network.
+  pagerAddress: null,
+  // The session ID is obtained during the login process and is maintained
+  // throughout the session. This helps the pager server identify the client.
+  sessionId: null,
+
+  // Public methods.
+  login: function() {
+    this._account.reportConnecting();
+    new YahooLoginHelper(this).login(this._account);
+  },
+
+  addBuddyToServer: function(aBuddy) {
+    let packet = new YahooPacket(kPacketType.AddBuddy, 0, this.sessionId);
+    packet.addValue(14, _("buddy.invite.message"));
+    packet.addValue(65, aBuddy.tag.name);
+    packet.addValue(97, "1"); // UTF-8 encoding.
+    packet.addValue(1, this._account.cleanUsername);
+    // The purpose of these two values are unknown.
+    packet.addValue(302, "319");
+    packet.addValue(300, "319");
+    packet.addValue(7, aBuddy.userName);
+    // The purpose of these three values are also unknown.
+    packet.addValue(334, "0");
+    packet.addValue(301, "319");
+    packet.addValue(303, "319");
+    this.sendBinaryData(packet.toArrayBuffer());
+  },
+
+  removeBuddyFromServer: function(aBuddy) {
+    let packet = new YahooPacket(kPacketType.RemoveBuddy, 0, this.sessionId);
+    packet.addValue(1, this._account.cleanUsername);
+    packet.addValue(7, aBuddy.userName);
+    packet.addValue(65, aBuddy.tag.name);
+    this.sendBinaryData(packet.toArrayBuffer());
+  },
+
+  setStatus: function(aStatus, aMessage) {
+    let packet = new YahooPacket(kPacketType.StatusUpdate, 0, this.sessionId);
+
+    // When a custom status message is used, key 10 is set to 99, and key 97
+    // is set to 1. Otherwise, key 10 is set to our current status code.
+    if (aMessage && aMessage.length > 0) {
+      packet.addValue(10, "99");
+      packet.addValue(97, "1");
+    } else {
+      let statusCode;
+      switch(aStatus) {
+        // Available
+        case Ci.imIStatusInfo.STATUS_AVAILABLE:
+        case Ci.imIStatusInfo.STATUS_MOBILE:
+          statusCode = "0";
+          break;
+        // Away
+        case Ci.imIStatusInfo.STATUS_AWAY:
+          statusCode = "1";
+          break;
+        // Busy
+        case Ci.imIStatusInfo.STATUS_UNAVAILABLE:
+          statusCode = "2";
+          break;
+        // Invisible
+        case Ci.imIStatusInfo.STATUS_INVISIBLE:
+          statusCode = "12";
+          break;
+        // Idle
+        case Ci.imIStatusInfo.STATUS_IDLE:
+          statusCode = "999";
+          break;
+      }
+      packet.addValue(10, statusCode);
+    }
+
+    // Key 19 is always set as the status messgae, even when the message is
+    // empty. If key 10 is set to 99, the message is used.
+    packet.addValue(19, aMessage);
+
+    // Key 47 is always set to either 0, if we are available, or 1, if we are
+    // not available. The value is used by the server if key 10 is set to 99.
+    // Otherwise, the value of key 10 is used to determine our status.
+    packet.addValue(47, (aStatus == Ci.imIStatusInfo.STATUS_AVAILABLE) ?
+                    "0" : "1");
+    this.sendBinaryData(packet.toArrayBuffer());
+  },
+
+  sendChatMessage: function(aName, aMessage) {
+    let packet = new YahooPacket(kPacketType.Message, 0, this.sessionId);
+    // XXX Key 0 is the user ID, and key 1 is the active ID. We need to find
+    // the difference between these two. Alias maybe?
+    packet.addValue(0, this._account.cleanUsername);
+    packet.addValue(1, this._account.cleanUsername);
+    packet.addValue(5, aName);
+    packet.addValue(14, aMessage);
+    this.sendBinaryData(packet.toArrayBuffer());
+  },
+
+  sendConferenceMessage: function(aRecipients, aRoom, aMessage) {
+    let packet = new YahooPacket(kPacketType.ConfMessage, 0, this.sessionId);
+    packet.addValue(1, this._account.cleanUsername);
+    packet.addValues(53, aRecipients);
+    packet.addValue(57, aRoom);
+    packet.addValue(14, aMessage);
+    packet.addValue(97, "1"); // Use UTF-8 encoding.
+    this.sendBinaryData(packet.toArrayBuffer());
+  },
+
+  sendTypingStatus: function(aBuddyName, aIsTyping) {
+    let packet = new YahooPacket(kPacketType.Notify, kPacketStatuses.Typing,
+                                 this.sessionId);
+    packet.addValue(1, this._account.cleanUsername);
+    packet.addValue(5, aBuddyName);
+    packet.addValue(13, aIsTyping ? "1" : "0");
+    packet.addValue(14, " "); // Key 14 contains a single space.
+    packet.addValue(49, "TYPING");
+    this.sendBinaryData(packet.toArrayBuffer());
+  },
+
+  acceptConferenceInvite: function(aOwner, aRoom, aParticipants) {
+    let packet = new YahooPacket(kPacketType.ConfLogon, 0, this.sessionId);
+    packet.addValue(1, this._account.cleanUsername);
+    packet.addValue(3, this._account.cleanUsername);
+    packet.addValue(57, aRoom);
+    packet.addValues(3, aParticipants);
+    this.sendBinaryData(packet.toArrayBuffer());
+  },
+
+  createConference: function(aRoom) {
+    let packet = new YahooPacket(kPacketType.ConfLogon, 0, this.sessionId);
+    packet.addValue(1, this._account.cleanUsername);
+    packet.addValue(3, this._account.cleanUsername);
+    packet.addValue(57, aRoom);
+    this.sendBinaryData(packet.toArrayBuffer());
+  },
+
+  inviteToConference: function(aInvitees, aRoom, aParticipants, aMessage) {
+    let packet = new YahooPacket(kPacketType.ConfAddInvite, 0, this.sessionId);
+    packet.addValue(1, this._account.cleanUsername);
+    packet.addValues(51, aInvitees);
+    packet.addValues(53, aParticipants);
+    packet.addValue(57, aRoom);
+    packet.addValue(58, aMessage);
+    packet.addValue(13, "0");
+    this.sendBinaryData(packet.toArrayBuffer());
+  },
+
+  sendConferenceLogoff: function(aName, aParticipants, aRoom) {
+    let packet = new YahooPacket(kPacketType.ConfLogoff, 0, this.sessionId);
+    packet.addValue(1, aName);
+    packet.addValues(3, aParticipants);
+    packet.addValue(57, aRoom);
+    this.sendBinaryData(packet.toArrayBuffer());
+  },
+
+  // Callbacks.
+  onLoginComplete: function() {
+    this._account.reportConnected();
+  },
+
+  onSessionError: function(aError, aMessage) {
+    this._account.reportDisconnecting(aError, aMessage);
+    if (this.isConnected)
+      this.disconnect();
+    this._account.reportDisconnected();
+  },
+
+  // Private methods.
+
+  // Socket Event Callbacks.
+  LOG: function(aString) this._account.LOG(aString),
+
+  DEBUG: function(aString) this._account.DEBUG(aString),
+
+  onConnection: function() {
+    // We send an authentication request packet as soon as we connect to the
+    // pager server.
+    let packet = new YahooPacket(kPacketType.Auth, 0, 0);
+    packet.addValue(1, this._account.cleanUsername);
+    this.sendBinaryData(packet.toArrayBuffer());
+  },
+
+  onConnectionTimedOut: function() {
+    this.onSessionError(Ci.prplIAccount.NETWORK_ERROR, "");
+  },
+
+  onConnectionReset: function() {
+    this.onSessionError(Ci.prplIAccount.NETWORK_ERROR, "");
+  },
+
+  // Called when the other end has closed the connection.
+  onConnectionClosed: function() {
+    if (!this._account.connected)
+      return;
+    this._account.reportDisconnecting(Ci.prplIAccount.NO_ERROR, "");
+    this._account.reportDisconnected();
+  },
+
+  onBinaryDataReceived: function(aData) {
+    let packets;
+    let bytesHandled;
+    try {
+      [packets, bytesHandled] = YahooPacket.extractPackets(aData);
+    } catch(e) {
+      this._account.ERROR(e);
+      this.onSessionError(Ci.prplIAccount.NETWORK_ERROR, "");
+      return 0;
+    }
+
+    for each (let packet in packets) {
+      if (YahooPacketHandler.hasOwnProperty(packet.service)) {
+        try {
+          YahooPacketHandler[packet.service].call(this._account, packet);
+        } catch(e) {
+          this._account.ERROR(e);
+        }
+      } else {
+        this._account.WARN("No handler for Yahoo! packet " +
+                           packet.service.toString(16) + ".");
+      }
+    }
+    return bytesHandled;
+  }
+};
+
+/* The purpose of YahooLoginHelper is to separate the complicated login logic
+ * from the YahooSession object. Logging in on Yahoo!'s network is the most
+ * complicated stage of a session due to the authentication system that is
+ * employed. The login steps are listed below.
+ *
+ * 1) Get the address of a "pager" server. This pager will be our gateway to
+ *    the network.
+ *
+ * 2) Obtain the challenge string from the pager. This string is used to help
+ *    create the base64 response string needed for the final step.
+ *
+ * 3) Obtain a token from the login server via HTTP.
+ *
+ * 4) Obtain the login crumb, Y-Cookie, and T-Cookie from the login server via
+ *    HTTP. These will also be used in the final response packet to the pager.
+ *
+ * 5) Create the base64 response string from the MD5 hash of the crumb and
+ *    challenge string, and build a packet containing the username, password,
+ *    response string, version numbers, crumb, and cookies, sending it to the
+ *    pager for a final authenticatcation.
+ *
+ * If all goes well after the 5th step, the user is considered logged in. */
+function YahooLoginHelper(aSession)
+{
+  this._session = aSession;
+}
+YahooLoginHelper.prototype = {
+  // YahooSession object passed in constructor.
+  _session: null,
+  // YahooAccount object passed to login().
+  _account: null,
+  // The username, stripped of any @yahoo.com or @yahoo.co.jp suffix.
+  _username: null,
+  // The authentication challenge string sent from the Yahoo!'s login server.
+  _challengeString: null,
+  // The authentication token sent from Yahoo!'s login server.
+  _loginToken: null,
+  // Crumb and cookie strings sent from Yahoo!'s login server, and used in
+  // the final authentication request to the pager server.
+  _crumb: null,
+  _yCookie: null,
+  _tCookie: null,
+
+  // Public methods.
+  login: function(aAccount) {
+    this._account = aAccount;
+    this._getPagerAddress();
+  },
+
+  // Private methods.
+  _getPagerAddress: function() {
+    doXHRequest(this._account._protocol.pagerRequestUrl, null, null,
+                this._onPagerAddressResponse, this._onHttpError, this,
+                "GET", null);
+  },
+
+  _getChallengeString: function() {
+    let port = this._account.getInt("port");
+    this._session.connect(this._session.pagerAddress, port);
+    // We want to handle a challenge string when the server responds.
+    this._session.onBinaryDataReceived =
+      this._onChallengeStringResponse.bind(this);
+  },
+
+  _getLoginToken: function() {
+    // TODO - Simplify this using map and join.
+    let url = this._account._protocol.loginTokenGetUrl;
+    url += "?src=ymsgr&";
+    url += "login=" + percentEncode(this._account.cleanUsername) + "&";
+    url += "passwd=" + percentEncode(this._account.imAccount.password) + "&";
+    url += "chal=" + percentEncode(this._challengeString);
+
+    doXHRequest(url, null, null, this._onLoginTokenResponse,
+                this._onHttpError, this, "GET", null);
+  },
+
+  _getCookies: function() {
+    // TODO - Simplify this using map and join.
+    let url = this._account._protocol.loginTokenLoginUrl;
+    url += "?src=ymsgr&";
+    url += "token=" + this._loginToken;
+
+    doXHRequest(url, null, null, this._onLoginCookiesResponse,
+                this._onHttpError, this, "GET", null);
+  },
+
+  _sendPagerAuthResponse: function() {
+    let response = this._calculatePagerResponse();
+    let packet = new YahooPacket(kPacketType.AuthResponse, 0,
+                                 this._session.sessionId);
+    // Build the key/value pairs.
+    packet.addValue(1, this._account.cleanUsername);
+    packet.addValue(0, this._account.cleanUsername);
+    packet.addValue(277, this._yCookie);
+    packet.addValue(278, this._tCookie);
+    packet.addValue(307, response);
+    packet.addValue(244, this._account.protocol.buildId);
+    packet.addValue(2, this._account.cleanUsername);
+    packet.addValue(2, "1");
+    packet.addValue(98, "us");
+    this._session.sendBinaryData(packet.toArrayBuffer());
+
+    // We want to handle a final login confirmation packet when the server
+    // responds.
+    this._session.onBinaryDataReceived = this._onFinalLoginResponse.bind(this);
+  },
+
+  _calculatePagerResponse: function() {
+    let hasher = Cc["@mozilla.org/security/hash;1"]
+                   .createInstance(Ci.nsICryptoHash);
+    hasher.init(hasher.MD5);
+
+    let crypt = this._crumb + this._challengeString;
+    let cryptData = StringToBytes(crypt);
+    hasher.update(cryptData, cryptData.length);
+
+    // The protocol requires replacing + with ., / with _, and = with - within
+    // the base64 response string.
+    return btoa(hasher.finish(false)).replace(/\+/g, ".").replace(/\//g, "_")
+                                     .replace(/=/g, "-");
+  },
+
+  _handleLoginError: function(aErrorCode) {
+    let errorInfo = kLoginStatusErrors[aErrorCode];
+    let errorMessage;
+    let error;
+
+    // If we find information on the error code we received, we will use that
+    // information. If the error wasn't found in our error table, just throw a
+    // generic error with the code included.
+    if (errorInfo) {
+      errorMessage = _("login.error." + errorInfo[0]);
+      error = errorInfo[1];
+    } else {
+      errorMessage = _("login.error.unknown", aErrorCode);
+      error = Ci.prplIAccount.ERROR_OTHER_ERROR;
+      // We also throw a console error because we didn't expect
+      // this error code.
+      this._account.ERROR("Received unknown error from pager server. Code: " +
+                          aErrorCode);
+    }
+    this._session.onSessionError(error, errorMessage);
+  },
+
+  _onHttpError: function(aError, aStatusText, aXHR) {
+    this._session.onSessionError(Ci.prplIAccount.NETWORK_ERROR,
+                               _("network.error.http"));
+  },
+
+  // HTTP Response Callbacks.
+  _onPagerAddressResponse: function(aResponse, aXHR) {
+    this._session.pagerAddress =
+      aResponse.substring(aResponse.lastIndexOf("=") + 1);
+    this._getChallengeString();
+  },
+
+  _onLoginTokenResponse: function(aResponse, aXHR) {
+    let responseParams = aResponse.split("\r\n");
+    // Status code "0" means success.
+    let statusCode = responseParams[0];
+    if (statusCode != "0") {
+      this._handleLoginError(statusCode);
+      return;
+    }
+
+    this._loginToken = responseParams[1].replace("ymsgr=", "");
+    this._getCookies();
+  },
+
+  _onLoginCookiesResponse: function(aResponse, aXHR) {
+    let responseParams = aResponse.split("\r\n");
+    // Status code "0" means success.
+    let statusCode = responseParams[0];
+    if (statusCode != "0") {
+      this._handleLoginError(statusCode);
+      return;
+    }
+
+    this._crumb = responseParams[1].replace("crumb=", "");
+    this._yCookie = responseParams[2].substring(2); // Remove the "Y=" bit.
+    this._tCookie = responseParams[3].substring(2); // Remove the "T=" bit.
+    this._sendPagerAuthResponse();
+  },
+
+  // TCP Response Callbacks.
+  _onChallengeStringResponse: function(aData) {
+    let packet = new YahooPacket();
+    packet.fromArrayBuffer(aData);
+    // The value of the challenge string is associated with key 94.
+    this._challengeString = packet.getValue(94);
+    this._session.sessionId = packet.sessionId;
+    this._getLoginToken();
+  },
+
+  _onFinalLoginResponse: function(aData) {
+    this._session.onLoginComplete();
+    // We need to restore data handling to the YahooSession object since our
+    // login steps are complete.
+    this._session.onBinaryDataReceived =
+      YahooSession.prototype.onBinaryDataReceived.bind(this._session);
+  }
+};
+
+/* The YahooPacket class represents a single Yahoo! Messenger data packet.
+ * Using this class allows you to easily create packets, stuff them with
+ * required data, and convert them to/from ArrayBuffer objects. */
+function YahooPacket(aService, aStatus, aSessionId)
+{
+  this.service = aService;
+  this.status = aStatus;
+  this.sessionId = aSessionId;
+  this.keyValuePairs = [];
+}
+YahooPacket.prototype = {
+  service: null,
+  status: null,
+  sessionId: null,
+  keyValuePairs: null,
+
+  // Public methods.
+
+  // Add a single key/value pair.
+  addValue: function(aKey, aValue) {
+    let pair = {
+      key: aKey.toString(), // The server handles keys as ASCII number values.
+      value: aValue
+    };
+
+    this.keyValuePairs.push(pair);
+  },
+
+  // Add multiple key/value pairs with the same key but different values
+  // stored in an array.
+  addValues: function(aKey, aValues) {
+    for each (let value in aValues)
+      this.addValue(aKey, value);
+  },
+
+  // This method returns the first value found with the given key.
+  getValue: function(aKey) {
+    for (let i = 0; i < this.keyValuePairs.length; ++i) {
+      let pair = this.keyValuePairs[i];
+      // The server handles keys as ASCII number values.
+      if (pair.key == aKey.toString())
+        return pair.value;
+    }
+
+    // Throw an error if the key wasn't found.
+    throw "Required key " + aKey + " wasn't found. Packet Service: " +
+          this.service.toString(16);
+  },
+
+  // This method returns all of the values found with the given key. In some
+  // packets, one key is associated with multiple values. If that is the case,
+  // use this method to retrieve all of them instead of just the first one.
+  getValues: function(aKey) {
+    let values = [];
+    for (let i = 0; i < this.keyValuePairs.length; ++i) {
+      let pair = this.keyValuePairs[i];
+      // The server handles keys as ASCII number values.
+      if (pair.key == aKey.toString())
+        values.push(pair.value);
+    }
+
+    // Throw an error if no keys were found.
+    if (values.length == 0) {
+      throw "Required key " + aKey + " wasn't found. Packet Service: " +
+            this.service.toString(16);
+    }
+    return values;
+  },
+
+  hasKey: function(aKey) {
+    for (let i = 0; i < this.keyValuePairs.length; ++i) {
+      // The server handles keys as ASCII number values.
+      if (this.keyValuePairs[i].key == aKey.toString())
+        return true;
+    }
+    return false;
+  },
+
+  toArrayBuffer: function() {
+    let dataString = "";
+    for (let i = 0; i < this.keyValuePairs.length; ++i) {
+      let pair = this.keyValuePairs[i];
+      dataString += pair.key + kPacketDataDelimiter;
+      dataString += pair.value + kPacketDataDelimiter;
+    }
+
+    let packetLength = dataString.length;
+    let buffer = new ArrayBuffer(kPacketHeaderSize + packetLength);
+
+    // Build header.
+    let view = new DataView(buffer);
+    let idBytes = StringToBytes(kPacketIdentifier);
+    view.setUint8(0, idBytes[0]);
+    view.setUint8(1, idBytes[1]);
+    view.setUint8(2, idBytes[2]);
+    view.setUint8(3, idBytes[3]);
+    view.setUint16(4, kProtocolVersion);
+    view.setUint16(6, 0); // Vendor ID
+    view.setUint16(8, packetLength);
+    view.setUint16(10, this.service);
+    view.setUint32(12, this.status);
+    view.setUint32(16, this.sessionId);
+
+    // Copy in data.
+    copyBytes(buffer, BytesToArrayBuffer(StringToBytes(dataString)), kPacketHeaderSize);
+
+    return buffer;
+  },
+
+  fromArrayBuffer: function(aBuffer) {
+    let view = new DataView(aBuffer);
+    this.length = view.getUint16(8) + kPacketHeaderSize;
+    this.service = view.getUint16(10);
+    this.status = view.getUint32(12);
+    this.sessionId = view.getUint32(16);
+
+    let dataString = ArrayBufferToString(aBuffer).substring(kPacketHeaderSize);
+    let delimitedData = dataString.split(kPacketDataDelimiter);
+    // Since the data should also end with a trailing delmiter, split() will
+    // add an empty element at the end. We need to pop this element off.
+    delimitedData.pop();
+
+    // If we don't have an even number of delimitedData elements, that means
+    // we are either missing a key or a value.
+    if (delimitedData.length % 2 != 0) {
+      throw "Odd number of data elements. Either a key or value is missing. "
+            "Num of elements: " + delimitedData.length;
+    }
+
+    for (let i = 0; i < delimitedData.length; i += 2) {
+      let key = delimitedData[i];
+      let value = delimitedData[i + 1];
+      if (key && value) {
+        let pair = {
+          key: key,
+          value: value
+        };
+        this.keyValuePairs.push(pair);
+      }
+    }
+  }
+};
+YahooPacket.extractPackets = function(aData, aOnNetworkError) {
+  let packets = [];
+  let bytesHandled = 0;
+
+  while (aData.byteLength >= kPacketHeaderSize) {
+    if (ArrayBufferToString(aData.slice(0, kPacketIdentifier.length)) !=
+        kPacketIdentifier) {
+      throw "Malformed packet received. Packet content: " +
+            ArrayBufferToHexString(aData);
+    }
+
+    let packetView = new DataView(aData);
+    let packetLength = packetView.getUint16(8) + kPacketHeaderSize;
+    // Don't process half packets.
+    if (packetLength > aData.byteLength)
+      break;
+    let packet = new YahooPacket();
+    packet.fromArrayBuffer(aData.slice(0, packetLength));
+    packets.push(packet);
+    bytesHandled += packetLength;
+    aData = aData.slice(packetLength);
+  }
+  return [packets, bytesHandled];
+}
+
+/* In YahooPacketHandler, each handler function is assosiated with a packet
+ * service number. You can use the kPacketType enumeration to understand
+ * what kind of packet each number is linked to.
+ *
+ * Keep in mind too that "this" in each function will be bound to a
+ * YahooAccount object, since they are all invoked using call(). */
+const YahooPacketHandler = {
+  // Buddy logoff.
+  0x02: function(aPacket) {
+    let name = aPacket.getValue(7);
+    this.setBuddyStatus(name, Ci.imIStatusInfo.STATUS_OFFLINE, "");
+  },
+
+  // Incoming chat message.
+  0x06: function(aPacket) {
+    let from = aPacket.getValue(4);
+    let to = aPacket.getValue(5);
+    let message = aPacket.getValue(14);
+    this.receiveMessage(from, message);
+
+    // The official Yahoo! Messenger desktop client requires message ACKs to be
+    // sent back to the server. The web client doesn't require this. A good
+    // indication of when an ACK is required is when key 429 is sent, which
+    // contains the ID of the message. When a message is sent from the official
+    // desktop client, and no ACK is sent back, the message is resent seconds
+    // later.
+    if (aPacket.hasKey(429)) {
+      let messageId = aPacket.getValue(429);
+      let packet = new YahooPacket(kPacketType.MessageAck, 0, aPacket.sessionId);
+      // Some keys have an unknown purpose, so we set a constant value.
+      packet.addValue(1, to);
+      packet.addValue(5, from);
+      packet.addValue(302, "430");
+      packet.addValue(430, messageId);
+      packet.addValue(303, 430);
+      packet.addValue(450, 0);
+      this._session._socket.sendBinaryData(packet.toArrayBuffer());
+    }
+  },
+
+  // New mail notification.
+  // TODO: Implement this handler when mail notifications are handled in the
+  // base code.
+  0x0b: function(aPacket) {},
+
+  // Server ping.
+  // TODO: Add support for ping replies.
+  0x12: function(aPacket) {},
+
+  // Conference invitation.
+  0x18: function(aPacket) {
+    let owner = aPacket.getValue(50);
+    let roomName = aPacket.getValue(57);
+    let participants = aPacket.getValues(52);
+    // The owner is also a participant.
+    participants.push(owner);
+    let message = aPacket.getValue(58);
+    this.receiveConferenceInvite(owner, roomName, participants, message);
+  },
+
+  // Conference logon.
+  0x19: function(aPacket) {
+    let userName = aPacket.getValue(53);
+    let room = aPacket.getValue(57);
+    this.receiveConferenceLogon(room, userName);
+  },
+
+  // Conference logoff
+  0x1b: function(aPacket) {
+    let userName = aPacket.getValue(56);
+    let roomName = aPacket.getValue(57);
+    this.receiveConferenceLogoff(roomName, userName);
+  },
+
+  // Conference additional invitation. NOTE: Since this packet has the same
+  // structure as the normal conference invite (packet 0x18), we simply
+  // reuse that handler.
+  0x1c: function(aPacket) YahooPacketHandler[0x18].call(this, aPacket),
+
+  // Conference message.
+  0x1d: function(aPacket) {
+    let from = aPacket.getValue(3);
+    let room = aPacket.getValue(57);
+    let message = aPacket.getValue(14);
+    this.receiveConferenceMessage(from, room, message);
+  },
+
+  // Typing notification.
+  0x4b: function(aPacket) {
+    let name = aPacket.getValue(4);
+    let isTyping = (aPacket.getValue(13) == "1");
+    this.receiveTypingNotification(name, isTyping);
+  },
+
+  // Legacy Yahoo! buddy list. Packet 0xf1 has replaced this.
+  0x55: function(aPacket) {},
+
+  // Authentication acknowledgement. We can ignore this since we are known
+  // to be authenticated if we are receiving other packets anyway.
+  0x57: function(aPacket) {},
+
+  // Buddy status update.
+  0xc6: function (aPacket) {
+    let name = aPacket.getValue(7);
+    // Try to grab a status from the well-used buddy statuses. If we can't
+    // find it there, try the extra ones.
+    let status = kBuddyStatuses[aPacket.getValue(10)];
+    let message = "";
+    if (aPacket.hasKey(19))
+      message = aPacket.getValue(19);
+
+    this.setBuddyStatus(name, status, message);
+  },
+
+  // Buddy authorization request.
+  0xd6: function(aPacket) {
+    let authRequest = {
+      _account: this,
+      _session: this._session,
+      get account() this.imAccount,
+      userName: aPacket.getValue(4),
+      grant: function() {
+        let packet = new YahooPacket(kPacketType.BuddyAuth, 0,
+                             this._session.sessionId);
+        packet.addValue(1, this._account.cleanUsername);
+        packet.addValue(5, this.userName);
+        // Misc. Unknown flags.
+        packet.addValue(13, 1);
+        packet.addValue(334, 0);
+        this._session._socket.sendBinaryData(packet.toArrayBuffer());
+
+        // TODO - Possibly allow different tags to be used.
+        this._account.addBuddy(Services.tags.createTag("Friends"),
+                               this.userName);
+      },
+      deny: function() {
+        let packet = new YahooPacket(kPacketType.BuddyReqReject, 0,
+                                     this._session.sessionId);
+        packet.addValue(1, this._account.cleanUsername);
+        packet.addValue(7, this.userName);
+        packet.addValue(14, "");
+        this._session._socket.sendBinaryData(packet.toArrayBuffer());
+      },
+      cancel: function() {
+        Services.obs.notifyObservers(this,
+                                     "buddy-authorization-request-canceled",
+                                     null);
+      },
+      QueryInterface: XPCOMUtils.generateQI([Ci.prplIBuddyRequest])
+    };
+    Services.obs.notifyObservers(authRequest, "buddy-authorization-request",
+                                 null);
+  },
+
+  // XXX: What does this packet do?
+  0xef: function(aPacket) {},
+
+  // Initial user status.
+  0xf0: function (aPacket) {
+    // The protocol correctly orders groups of key/values per buddy. So we can
+    // place all occurances of a certain key in each array, and match them up.
+
+    // Return early if we find no buddy names.
+    if (!aPacket.hasKey(7))
+      return;
+    let buddyNames = aPacket.getValues(7);
+    let buddyStatuses = aPacket.getValues(10);
+    let statusMessages;
+    if (aPacket.hasKey(19))
+      statusMessages = aPacket.getValues(19);
+
+    for (let i in buddyNames) {
+      let name = buddyNames[i];
+      let status = kBuddyStatuses[buddyStatuses[i]];
+      let message = statusMessages ? statusMessages[i] : "";
+
+      this.setBuddyStatus(name, status, message);
+    }
+  },
+
+  // Friends and groups list.
+  0xf1: function(aPacket) {
+    let tagName = "";
+    for each (let pair in aPacket.keyValuePairs) {
+      if (pair.key == "65")
+        tagName = pair.value;
+      else if (pair.key == "7") {
+        let buddyName = pair.value;
+        this.addBuddyFromServer(Services.tags.createTag(tagName), buddyName);
+      }
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/yahoo.js
@@ -0,0 +1,480 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource:///modules/imServices.jsm");
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/jsProtoHelper.jsm");
+Cu.import("resource:///modules/yahoo-session.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", function()
+  l10nHelper("chrome://chat/locale/yahoo.properties")
+);
+
+function YahooConversation(aAccount, aName)
+{
+  this._buddyUserName = aName;
+  this._account = aAccount;
+  this.buddy = aAccount.getBuddy(aName);
+  this._init(aAccount);
+}
+YahooConversation.prototype = {
+  _account: null,
+  _buddyUserName: null,
+  _typingTimer: null,
+
+  close: function() {
+    this._account.deleteConversation(this._buddyUserName);
+    GenericConvChatPrototype.close.call(this);
+  },
+
+  sendMsg: function (aMsg) {
+    // Deliver the message, then write it to the window.
+    this._account._session.sendChatMessage(this._buddyUserName,
+                                           this._account.encodeMessage(aMsg));
+    this.finishedComposing();
+    this.writeMessage(this._account.imAccount.alias, aMsg, {outgoing: true});
+  },
+
+  sendTyping: function(aString) {
+    if (aString.length) {
+      if (!this._typingTimer)
+        this._account._session.sendTypingStatus(this._buddyUserName, true);
+      this._refreshTypingTimer();
+    }
+    return Ci.prplIConversation.NO_TYPING_LIMIT;
+  },
+
+  finishedComposing: function() {
+    this._account._session.sendTypingStatus(this._buddyUserName, false);
+    this._cancelTypingTimer();
+  },
+
+  _refreshTypingTimer: function() {
+    this._cancelTypingTimer();
+    this._typingTimer = setTimeout(this.finishedComposing.bind(this), 10000);
+  },
+
+  _cancelTypingTimer: function() {
+    if (!this._typingTimer)
+      return;
+    clearTimeout(this._typingTimer);
+    delete this._typingTimer
+    this._typingTimer = null;
+  },
+
+  get name() this._buddyUserName
+};
+YahooConversation.prototype.__proto__ = GenericConvIMPrototype;
+
+function YahooConference(aAccount, aRoom, aOwner)
+{
+  this._account = aAccount;
+  this._roomName = aRoom;
+  this._owner = aOwner;
+  this._init(aAccount, aRoom, aAccount.cleanUsername);
+}
+YahooConference.prototype = {
+  _account: null,
+  _roomName: null,
+  _owner: null,
+
+  close: function() {
+    this._account._session.sendConferenceLogoff(this._account.cleanUsername,
+                                                this.getParticipantNames(),
+                                                this._roomName);
+    this._account.deleteConference(this._roomName);
+    GenericConvChatPrototype.close.call(this);
+  },
+
+  sendMsg: function(aMsg) {
+    this._account._session.sendConferenceMessage(this.getParticipantNames(),
+                                                 this._roomName,
+                                                 this._account.encodeMessage(aMsg));
+  },
+
+  addParticipant: function(aName) {
+    let buddy = new YahooConferenceBuddy(aName, this);
+    this._participants[aName] = buddy;
+    this.notifyObservers(new nsSimpleEnumerator([buddy]), "chat-buddy-add");
+    this.writeMessage(this._roomName,
+                      _("system.message.conferenceLogon", aName),
+                      {system: true});
+  },
+
+  removeParticipant: function(aName) {
+    // In case we receive two logoff packets, make sure that the user is
+    // actually here before continuing.
+    if (!this._participants[aName])
+      return;
+
+    let stringNickname = Cc["@mozilla.org/supports-string;1"]
+                           .createInstance(Ci.nsISupportsString);
+    stringNickname.data = aName;
+    this.notifyObservers(new nsSimpleEnumerator([stringNickname]),
+                         "chat-buddy-remove");
+    delete this._participants[aName];
+    this.writeMessage(this._roomName,
+                      _("system.message.conferenceLogoff", aName),
+                      {system: true});
+  },
+
+  getParticipantNames: function()
+    [this._participants[i].name for (i in this._participants)],
+};
+YahooConference.prototype.__proto__ = GenericConvChatPrototype;
+
+function YahooConferenceBuddy(aName, aConference)
+{
+  this._name = aName;
+  this._conference = aConference;
+}
+YahooConferenceBuddy.prototype = {
+  _conference: null,
+
+  get founder() this._conference._owner == this._name
+}
+YahooConferenceBuddy.prototype.__proto__ = GenericConvChatBuddyPrototype;
+
+function YahooAccountBuddy(aAccount, aBuddy, aTag, aUserName)
+{
+  this._init(aAccount, aBuddy, aTag, aUserName);
+}
+YahooAccountBuddy.prototype = {
+  // This removes the buddy locally, and from the Yahoo! servers.
+  remove: function() this._account.removeBuddy(this, true),
+  // This removes the buddy locally, but keeps him on the servers.
+  removeLocal: function() this._account.removeBuddy(this, false),
+  createConversation: function() this._account.createConversation(this.userName)
+}
+YahooAccountBuddy.prototype.__proto__ = GenericAccountBuddyPrototype;
+
+function YahooAccount(aProtoInstance, aImAccount)
+{
+  this._init(aProtoInstance, aImAccount);
+  this._buddies = new Map();
+  this._conversations = new Map();
+  this._conferences = new Map();
+  this._protocol = aProtoInstance;
+  this._converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+      .createInstance(Ci.nsIScriptableUnicodeConverter);
+  this._converter.charset = this.getString("local_charset") || "UTF-8";
+}
+YahooAccount.prototype = {
+  // YahooSession object passed in constructor.
+  _session: null,
+  // A Map holding the list of buddies associated with their usernames.
+  _buddies: null,
+  // A Map holding the list of open buddy conversations associated with the
+  // username of the buddy.
+  _conversations: null,
+  // A Map holding the list of open conference rooms associated with the room
+  // name.
+  _conferences: null,
+  // YahooProtocol object passed in the constructor.
+  _protocol: null,
+  // An nsIScriptableUnicodeConverter used to convert incoming/outgoing chat
+  // messages to the correct charset.
+  _converter: null,
+  // This is simply incremented by one everytime a new conference room is
+  // created. It is appened to the end of the room name when a new room is
+  // created, ensuring name uniqueness.
+  _roomsCreated: 0,
+
+  connect: function() {
+    this._session = new YahooSession(this);
+    this._session.login(this.imAccount.name, this.imAccount.password);
+  },
+
+  disconnect: function(aSilent) {
+    if (this.connected) {
+      this.reportDisconnecting(Ci.prplIAccount.NO_ERROR, "");
+      if (this._session.isConnected)
+        this._session.disconnect();
+      this.reportDisconnected();
+    }
+    // buddy[1] is the actual object.
+    for (let buddy of this._buddies)
+      buddy[1].setStatus(Ci.imIStatusInfo.STATUS_UNKNOWN, "");
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic != "status-changed")
+      return;
+
+    this._session.setStatus(aSubject.statusType, aData);
+  },
+
+  remove: function() {
+    for each(let conv in this._conversations)
+      conv.close();
+    delete this._conversations;
+    for (let buddy of this._buddies)
+      buddy[1].removeLocal(); // buddy[1] is the actual object.
+  },
+
+  unInit: function() {
+    this.disconnect(true);
+    delete this.imAccount;
+  },
+
+  createConversation: function(aName) {
+    let conv = new YahooConversation(this, aName);
+    this._conversations.set(aName, conv);
+    return conv;
+  },
+
+  deleteConversation: function(aName) {
+    if (this._conversations.has(aName))
+      this._conversations.delete(aName);
+  },
+
+  receiveConferenceInvite: function(aOwner, aRoom, aParticipants, aMessage) {
+    // Do nothing if we wish to ignore invites.
+    if (!Services.prefs.getIntPref("messenger.conversations.autoAcceptChatInvitations") ||
+        this.getBool("ignore_invites"))
+      return;
+
+    let conf = new YahooConference(this, aRoom, aOwner);
+    this._conferences.set(aRoom, conf);
+
+    for each (let participant in aParticipants)
+      conf.addParticipant(participant);
+
+    this._session.acceptConferenceInvite(aOwner, aRoom,
+                                         conf.getParticipantNames());
+  },
+
+  receiveConferenceLogon: function(aRoom, aUsername) {
+    if (!this._conferences.has(aRoom))
+      return;
+    let conf = this._conferences.get(aRoom);
+    conf.addParticipant(aUsername);
+  },
+
+  receiveConferenceLogoff: function(aRoom, aUsername) {
+    if (!this._conferences.has(aRoom))
+      return;
+    let conf = this._conferences.get(aRoom);
+    conf.removeParticipant(aUsername);
+  },
+
+  deleteConference: function(aName) {
+    if (this._conferences.has(aName))
+      this._conferences.delete(aName);
+  },
+
+  // Called when the user adds a new contact within Instantbird.
+  addBuddy: function(aTag, aName) {
+    let buddy = new YahooAccountBuddy(this, null, aTag, aName);
+    this._buddies.set(buddy.userName, buddy);
+    this._session.addBuddyToServer(buddy);
+    Services.contacts.accountBuddyAdded(buddy);
+  },
+
+  // Called for each buddy that is sent in a list packet from Yahoo! on login.
+  addBuddyFromServer: function(aTag, aName) {
+    let buddy;
+    if (this._buddies.has(aName))
+      buddy = this._buddies.get(aName);
+    else {
+      buddy = new YahooAccountBuddy(this, null, aTag, aName);
+      Services.contacts.accountBuddyAdded(buddy);
+      this._buddies.set(aName, buddy);
+    }
+
+    // Set all new buddies as offline because a following status packet will
+    // tell their status if they are online.
+    buddy.setStatus(Ci.imIStatusInfo.STATUS_OFFLINE, "");
+  },
+
+  // Called when a user removes a contact from within Instantbird.
+  removeBuddy: function(aBuddy, aRemoveFromServer) {
+    if (aRemoveFromServer)
+      this._session.removeBuddyFromServer(aBuddy);
+    this._buddies.delete(aBuddy.userName);
+    Services.contacts.accountBuddyRemoved(aBuddy);
+  },
+
+  loadBuddy: function(aBuddy, aTag) {
+    let buddy = new YahooAccountBuddy(this, aBuddy, aTag);
+    this._buddies.set(buddy.userName, buddy);
+
+    return buddy;
+  },
+
+  setBuddyStatus: function(aName, aStatus, aMessage) {
+    if (!this._buddies.has(aName))
+      return;
+    this._buddies.get(aName).setStatus(aStatus, aMessage);
+  },
+
+  getBuddy: function(aName) {
+    if (this._buddies.has(aName))
+      return this._buddies.get(aName);
+    return null;
+  },
+
+  receiveMessage: function(aName, aMessage) {
+    let conv;
+    // Check if we have an existing converstaion open with this user. If not,
+    // create one and add it to the list.
+    if (!this._conversations.has(aName))
+      conv = this.createConversation(aName);
+    else
+      conv = this._conversations.get(aName);
+
+    conv.writeMessage(aName, this.decodeMessage(aMessage), {incoming: true});
+  },
+
+  receiveConferenceMessage: function(aName, aRoom, aMessage) {
+    if (!this._conferences.has(aRoom))
+      return;
+
+    this._conferences.get(aRoom).writeMessage(aName,
+                                              this.decodeMessage(aMessage),
+                                              {incoming: true});
+  },
+
+  receiveTypingNotification: function(aName, aIsTyping) {
+    if (!this._conversations.has(aName))
+      return;
+
+    if (aIsTyping)
+      this._conversations.get(aName).updateTyping(Ci.prplIConvIM.TYPING);
+    else
+      this._conversations.get(aName).updateTyping(Ci.prplIConvIM.NOT_TYPING);
+  },
+
+  encodeMessage: function(aMessage) {
+    // Try to perform a convertion from JavaScript UTF-16 into the charset
+    // specified in the options. If the conversion fails, just leave
+    // the message as it is.
+    let encodedMsg;
+    try {
+      encodedMsg = this._account._converter.ConvertFromUnicode(aMessage);
+    } catch (e) {
+      encodedMsg = aMessage;
+      this.WARN("Could not encode UTF-16 message into " +
+                this._converter.charset + ". Message: " + aMessage);
+    }
+    return encodedMsg;
+  },
+
+  decodeMessage: function(aMessage) {
+    // Try to perform a convertion from the charset specified in the options
+    // to JavaScript UTF-16. If the conversion fails, just leave the message
+    // as it is.
+    let decodedMsg;
+    try {
+      decodedMsg = this._account._converter.ConvertToUnicode(aMessage);
+    } catch (e) {
+      decodedMsg = aMessage;
+      this.WARN("Could not deccode " + this._converter.charset +
+                " message into UTF-16. Message: " + aMessage);
+    }
+    return decodedMsg;
+  },
+
+  get canJoinChat() true,
+  // Strip @yahoo.com or @yahoo.co.jp from username. Other email domains
+  // can be left alone.
+  get cleanUsername() this.name.replace(this._protocol.emailSuffix, ""),
+  chatRoomFields: {},
+  joinChat: function(aComponents) {
+    // Use _roomsCreated to append a unique number to the room name. We add 1
+    // so that we can start the room numbers from 1 instead of 0.
+    let roomName = this.cleanUsername + "-" + (++this._roomsCreated);
+    let conf = new YahooConference(this, roomName, this.cleanUsername);
+    this._conferences.set(roomName, conf);
+    this._session.createConference(roomName);
+    this._roomsCreated++;
+  }
+};
+YahooAccount.prototype.__proto__ = GenericAccountPrototype;
+
+function YahooProtocol() {
+  this.registerCommands();
+}
+YahooProtocol.prototype = {
+  __proto__: GenericProtocolPrototype,
+  // Protocol specific connection parameters.
+  pagerRequestUrl: "http://vcs1.msg.yahoo.com/capacity",
+  loginTokenGetUrl: "https://login.yahoo.com/config/pwtoken_get",
+  loginTokenLoginUrl: "https://login.yahoo.com/config/pwtoken_login",
+  buildId: "4194239",
+  emailSuffix: "@yahoo.com",
+
+  get id() "prpl-yahoo",
+  get name() "Yahoo",
+  options: {
+    port: {get label() _("options.pagerPort"), default: 5050},
+    //xfer_host: {get label() _("options.transferHost"), default: "filetransfer.msg.yahoo.com"},
+    //xfer_port: {get label() _("options.transferPort"), default: 80},
+    //room_list_locale: {get label() _("options.chatLocale"), default: "us"},
+    local_charset: {get label() _("options.chatEncoding"), default: "UTF-8"},
+    ignore_invites: {get label() _("options.ignoreInvites"), default: false}
+    //proxy_ssl: {get label() _("options.proxySSL"), default: false}
+  },
+  commands: [
+    {
+      name: "invite",
+      get helpString() _("command.help.invite"),
+      run: function(aMsg, aConv) {
+        if (aMsg.trim().length == 0)
+          return false;
+
+        let splitPosition = aMsg.indexOf(" "); // Split at first space.
+        let invitees;
+        let message;
+
+        // If we have an invite message.
+        if (splitPosition > 0) {
+          invitees = aMsg.substring(0, splitPosition).split(",");
+          message = aMsg.substring(splitPosition);
+        } else {
+          invitees = aMsg.split(",");
+          message = _("conference.invite.message"); // Use default message.
+        }
+
+        let conf = aConv.wrappedJSObject;
+        conf._account._session.inviteToConference(invitees, conf._roomName,
+                                                  conf.getParticipantNames(),
+                                                  message);
+        return true;
+      }
+    }
+  ],
+  getAccount: function(aImAccount) new YahooAccount(this, aImAccount),
+  classID: Components.ID("{50ea817e-5d79-4657-91ae-aa0a52bdb98c}")
+};
+
+function YahooJapanProtocol() {
+  this.registerCommands();
+}
+YahooJapanProtocol.prototype = {
+  __proto__: YahooProtocol.prototype,
+  // Protocol specific connection parameters.
+  pagerRequestUrl: "http://cs1.yahoo.co.jp/capacity",
+  loginTokenGetUrl: "https://login.yahoo.co.jp/config/pwtoken_get",
+  loginTokenLoginUrl: "https://login.yahoo.co.jp/config/pwtoken_login",
+  buildId: "4186047",
+  emailSuffix: "@yahoo.co.jp",
+
+  get id() "prpl-yahoojp",
+  get name() "Yahoo JAPAN",
+  options: {
+    port: {get label() _("options.pagerPort"), default: 5050},
+    //xfer_host: {get label() _("options.transferHost"), default: "filetransfer.msg.yahoo.com"},
+    //xfer_port: {get label() _("options.transferPort"), default: 80},
+    //room_list_locale: {get label() _("options.chatLocale"), default: "jp"},
+    local_charset: {get label() _("options.chatEncoding"), default: "UTF-8"},
+    ignore_invites: {get label() _("options.ignoreInvites"), default: false}
+    //proxy_ssl: {get label() _("options.proxySSL"), default: false}
+  },
+  classID: Components.ID("{5f6dc733-ec0d-4de8-8adc-e4967064ed38}")
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([YahooProtocol, YahooJapanProtocol]);
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/yahoo.manifest
@@ -0,0 +1,7 @@
+component {50ea817e-5d79-4657-91ae-aa0a52bdb98c} yahoo.js
+contract @mozilla.org/chat/yahoo;1 {50ea817e-5d79-4657-91ae-aa0a52bdb98c}
+category im-protocol-plugin prpl-yahoo @mozilla.org/chat/yahoo;1
+
+component {5f6dc733-ec0d-4de8-8adc-e4967064ed38} yahoo.js
+contract @mozilla.org/chat/yahoojp;1 {5f6dc733-ec0d-4de8-8adc-e4967064ed38}
+category im-protocol-plugin prpl-yahoojp @mozilla.org/chat/yahoojp;1