Bug 953999 - Skype protocol plugin. r=aleth,florian
authorPatrick Cloke <clokep@gmail.com>
Sat, 14 Feb 2015 17:10:23 -0500
changeset 20448 30b50e6fa1f9a930f3857b95123e3e9a3067690d
parent 20447 3c43f4fdb8012513cfa5b690f6b593e55a643942
child 20449 40330e8b3ee071a9ac9d8b3335e5fdc838626ff4
push id1491
push usermbanner@mozilla.com
push dateMon, 10 Aug 2015 18:57:41 +0000
treeherdercomm-aurora@102abb2cdd3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaleth, florian
bugs953999
Bug 953999 - Skype protocol plugin. r=aleth,florian
chat/chat-prefs.js
chat/locales/en-US/skype.properties
chat/locales/jar.mn
chat/modules/ArrayBufferUtils.jsm
chat/modules/BigInteger.jsm
chat/modules/moz.build
chat/moz.build
chat/protocols/skype/icons/prpl-skype-32.png
chat/protocols/skype/icons/prpl-skype-48.png
chat/protocols/skype/icons/prpl-skype.png
chat/protocols/skype/jar.mn
chat/protocols/skype/moz.build
chat/protocols/skype/skype.js
chat/protocols/skype/skype.manifest
chat/protocols/skype/test/test_MagicSha256.js
chat/protocols/skype/test/test_contactUrlToName.js
chat/protocols/skype/test/xpcshell.ini
im/installer/package-manifest.in
im/test/xpcshell.ini
mail/installer/package-manifest.in
--- a/chat/chat-prefs.js
+++ b/chat/chat-prefs.js
@@ -75,16 +75,18 @@ pref("messenger.status.userIconFileName"
 pref("messenger.status.userDisplayName", "");
 
 // Default message used when quitting IRC. This is overridable per account.
 pref("chat.irc.defaultQuitMessage", "");
 // If this is true, requestRooomInfo will return LIST results when it is
 // called automatically by the awesometab. Otherwise, requestRoomInfo will
 // only do so when explicitly requested by the user, e.g. via the /list command.
 pref("chat.irc.automaticList", true);
+// Disable Skype until it can be tested further.
+pref("chat.prpls.prpl-skype.disable", true);
 
 // 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)
new file mode 100644
--- /dev/null
+++ b/chat/locales/en-US/skype.properties
@@ -0,0 +1,14 @@
+# 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/.
+
+# LOCALIZATION NOTE (connection.*):
+#   These will show in the account manager to show progress during a connection.
+connecting.authenticating=Authenticating
+connecting.registrationToken=Getting registration token
+
+# LOCALIZATION NOTE (error.*):
+#   These will show in the account manager if the account is disconnected
+#   because of an error.
+error.auth=Failed to authenticate to the server
+error.registrationToken=Failed getting Registration Token
--- a/chat/locales/jar.mn
+++ b/chat/locales/jar.mn
@@ -9,12 +9,13 @@
 	locale/@AB_CD@/chat/accounts.properties (%accounts.properties)
 	locale/@AB_CD@/chat/imtooltip.properties (%imtooltip.properties)
 	locale/@AB_CD@/chat/commands.properties (%commands.properties)
 	locale/@AB_CD@/chat/contacts.properties	(%contacts.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/skype.properties	(%skype.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/modules/ArrayBufferUtils.jsm
+++ b/chat/modules/ArrayBufferUtils.jsm
@@ -3,18 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*
  * JavaScript ArrayBuffers are missing a variety of useful methods which are
  * provided by this module.
  */
 
 const EXPORTED_SYMBOLS = ["copyBytes", "ArrayBufferToBytes",
-  "BytesToArrayBuffer", "StringToBytes", "ArrayBufferToString",
-  "ArrayBufferToHexString"];
+  "BytesToArrayBuffer", "StringToBytes", "StringToArrayBuffer",
+  "ArrayBufferToString", "ArrayBufferToHexString"];
 
 /*
  * aTarget / aSource are ArrayBuffers.
  *
  * Note that this is very similar to ArrayBuffer.slice except that it allows
  * for an offset in the target as well as the source.
  */
 function copyBytes(aTarget, aSource, aTargetOffset = 0, aSourceOffset = 0,
@@ -30,13 +30,15 @@ function BytesToArrayBuffer(aBytes = [])
   let buf = new ArrayBuffer(aBytes.length);
   let view = new Uint8Array(buf);
   view.set(aBytes);
   return buf;
 }
 
 function StringToBytes(aString) [aString.charCodeAt(i) for (i in aString)];
 
+function StringToArrayBuffer(aString) BytesToArrayBuffer(StringToBytes(aString))
+
 function ArrayBufferToString(aData)
   [String.fromCharCode(b) for (b of new Uint8Array(aData))].join("");
 
 function ArrayBufferToHexString(aData)
   "0x" + [("0" + b.toString(16)).slice(-2) for (b of new Uint8Array(aData))].join(" ");
new file mode 100644
--- /dev/null
+++ b/chat/modules/BigInteger.jsm
@@ -0,0 +1,689 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * For more information, please refer to <http://unlicense.org>
+ */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["bigInt"];
+
+var bigInt = (function () {
+    var base = 10000000, logBase = 7;
+    var sign = {
+        positive: false,
+        negative: true
+    };
+
+    function BigInteger(value, sign) {
+        this.value = value;
+        this.sign = sign;
+    }
+
+    function trim(value) {
+        var i = value.length - 1;
+        while (value[i] === 0 && i > 0) i--;
+        return value.slice(0, i + 1);
+    }
+
+    function fastAdd(a, b) {
+        var sign = b < 0;
+        if (a.sign !== sign) {
+            if(sign) return fastSubtract(a.abs(), -b);
+            return fastSubtract(a.abs(), b).negate();
+        }
+        if (sign) b = -b;
+        var value = a.value,
+            result = [],
+            carry = 0;
+        for (var i = 0; i < value.length || carry > 0; i++) {
+            var sum = (value[i] || 0) + (i > 0 ? 0 : b) + carry;
+            carry = sum >= base ? 1 : 0;
+            result.push(sum % base);
+        }
+        return new BigInteger(result, a.sign);
+    }
+
+    function fastSubtract(a, b) {
+        if (a.sign !== (b < 0)) return fastAdd(a, -b);
+        var sign = false;
+        if (a.sign) sign = true;
+        var value = a.value;
+        if (value.length === 1 && value[0] < b) return new BigInteger([b - value[0]], !sign);
+        if (sign) b = -b;
+        var result = [],
+            borrow = 0;
+        for (var i = 0; i < value.length; i++) {
+            var tmp = value[i] - borrow - (i > 0 ? 0 : b);
+            borrow = tmp < 0 ? 1 : 0;
+            result.push((borrow * base) + tmp);
+        }
+
+        return new BigInteger(result, sign);
+    }
+
+    function fastMultiply(a, b) {
+        var value = a.value,
+            sign = b < 0,
+            result = [],
+            carry = 0;
+        if (sign) b = -b;
+        for (var i = 0; i < value.length || carry > 0; i++) {
+            var product = (value[i] || 0) * b + carry;
+            carry = (product / base) | 0;
+            result.push(product % base);
+        }
+        return new BigInteger(result, sign ? !a.sign : a.sign);
+    }
+
+    function fastDivMod(a, b) {
+        if (b === 0) throw new Error("Cannot divide by zero.");
+        var value = a.value,
+            sign = b < 0,
+            result = [],
+            remainder = 0;
+        if (sign) b = -b;
+        for (var i = value.length - 1; i >= 0; i--) {
+            var divisor = remainder * base + value[i];
+            remainder = divisor % b;
+            result.push(divisor / b | 0);
+        }
+        return {
+            quotient: new BigInteger(trim(result.reverse()), sign ? !a.sign : a.sign),
+            remainder: new BigInteger([remainder], a.sign)
+        };
+    }
+
+    function isSmall(n) {
+        return ((typeof n === "number" || typeof n === "string") && +n <= base) ||
+            (n instanceof BigInteger && n.value.length <= 1);
+    }
+
+    BigInteger.prototype.negate = function () {
+        return new BigInteger(this.value, !this.sign);
+    };
+    BigInteger.prototype.abs = function () {
+        return new BigInteger(this.value, sign.positive);
+    };
+    BigInteger.prototype.add = function (n) {
+        if(isSmall(n)) return fastAdd(this, +n);
+        n = parseInput(n);
+        if (this.sign !== n.sign) {
+            if (this.sign === sign.positive) return this.abs().subtract(n.abs());
+            return n.abs().subtract(this.abs());
+        }
+        var a = this.value, b = n.value;
+        var result = [],
+            carry = 0,
+            length = Math.max(a.length, b.length);
+        for (var i = 0; i < length || carry > 0; i++) {
+            var sum = (a[i] || 0) + (b[i] || 0) + carry;
+            carry = sum >= base ? 1 : 0;
+            result.push(sum % base);
+        }
+        return new BigInteger(trim(result), this.sign);
+    };
+    BigInteger.prototype.plus = function (n) {
+        return this.add(n);
+    };
+    BigInteger.prototype.subtract = function (n) {
+        if (isSmall(n)) return fastSubtract(this, +n);
+        n = parseInput(n);
+        if (this.sign !== n.sign) return this.add(n.negate());
+        if (this.sign === sign.negative) return n.negate().subtract(this.negate());
+        if (this.compare(n) < 0) return n.subtract(this).negate();
+        var a = this.value, b = n.value;
+        var result = [],
+            borrow = 0,
+            length = Math.max(a.length, b.length);
+        for (var i = 0; i < length; i++) {
+            var ai = a[i] || 0, bi = b[i] || 0;
+            var tmp = ai - borrow;
+            borrow = tmp < bi ? 1 : 0;
+            result.push((borrow * base) + tmp - bi);
+        }
+        return new BigInteger(trim(result), sign.positive);
+    };
+    BigInteger.prototype.minus = function (n) {
+        return this.subtract(n);
+    };
+    BigInteger.prototype.multiply = function (n) {
+        if (isSmall(n)) return fastMultiply(this, +n);
+        n = parseInput(n);
+        var sign = this.sign !== n.sign;
+
+        var a = this.value, b = n.value;
+        var length = Math.max(a.length, b.length);
+        var resultSum = [];
+        for (var i = 0; i < length; i++) {
+            resultSum[i] = [];
+            var j = i;
+            while (j--) {
+                resultSum[i].push(0);
+            }
+        }
+        var carry = 0;
+        for (var i = 0; i < a.length; i++) {
+            var x = a[i];
+            for (var j = 0; j < b.length || carry > 0; j++) {
+                var y = b[j];
+                var product = y ? (x * y) + carry : carry;
+                carry = Math.floor(product / base);
+                resultSum[i].push(product % base);
+            }
+        }
+        var max = -1;
+        for (var i = 0; i < resultSum.length; i++) {
+            var len = resultSum[i].length;
+            if (len > max) max = len;
+        }
+        var result = [], carry = 0;
+        for (var i = 0; i < max || carry > 0; i++) {
+            var sum = carry;
+            for (var j = 0; j < resultSum.length; j++) {
+                sum += resultSum[j][i] || 0;
+            }
+            carry = sum > base ? Math.floor(sum / base) : 0;
+            sum -= carry * base;
+            result.push(sum);
+        }
+        return new BigInteger(trim(result), sign);
+    };
+    BigInteger.prototype.times = function (n) {
+        return this.multiply(n);
+    };
+    BigInteger.prototype.divmod = function (n) {
+        if (isSmall(n)) return fastDivMod(this, +n);
+        n = parseInput(n);
+        var quotientSign = this.sign !== n.sign;
+        if (this.equals(0)) return {
+            quotient: new BigInteger([0], sign.positive),
+            remainder: new BigInteger([0], sign.positive)
+        };
+        if (n.equals(0)) throw new Error("Cannot divide by zero");
+        var a = this.value, b = n.value;
+        var result = [], remainder = [];
+        for (var i = a.length - 1; i >= 0; i--) {
+            var m = [a[i]].concat(remainder);
+            var quotient = goesInto(b, m);
+            result.push(quotient.result);
+            remainder = quotient.remainder;
+        }
+        result.reverse();
+        return {
+            quotient: new BigInteger(trim(result), quotientSign),
+            remainder: new BigInteger(trim(remainder), this.sign)
+        };
+    };
+    BigInteger.prototype.divide = function (n) {
+        return this.divmod(n).quotient;
+    };
+    BigInteger.prototype.over = function (n) {
+        return this.divide(n);
+    };
+    BigInteger.prototype.mod = function (n) {
+        return this.divmod(n).remainder;
+    };
+    BigInteger.prototype.remainder = function (n) {
+        return this.mod(n);
+    };
+    BigInteger.prototype.pow = function (n) {
+        n = parseInput(n);
+        var a = this, b = n, r = ONE;
+        if (b.equals(ZERO)) return r;
+        if (a.equals(ZERO) || b.lesser(ZERO)) return ZERO;
+        while (true) {
+            if (b.isOdd()) {
+                r = r.times(a);
+            }
+            b = b.divide(2);
+            if (b.equals(ZERO)) break;
+            a = a.times(a);
+        }
+        return r;
+    };
+    BigInteger.prototype.modPow = function (exp, mod) {
+        exp = parseInput(exp);
+        mod = parseInput(mod);
+        if (mod.equals(ZERO)) throw new Error("Cannot take modPow with modulus 0");
+        var r = ONE,
+            base = this.mod(mod);
+        if (base.equals(ZERO)) return ZERO;
+        while (exp.greater(0)) {
+            if (exp.isOdd()) r = r.multiply(base).mod(mod);
+            exp = exp.divide(2);
+            base = base.square().mod(mod);
+        }
+        return r;
+    };
+    BigInteger.prototype.square = function () {
+        return this.multiply(this);
+    };
+    function gcd(a, b) {
+        a = parseInput(a).abs();
+        b = parseInput(b).abs();
+        if (a.equals(b)) return a;
+        if (a.equals(ZERO)) return b;
+        if (b.equals(ZERO)) return a;
+        if (a.isEven()) {
+            if (b.isOdd()) {
+                return gcd(a.divide(2), b);
+            }
+            return gcd(a.divide(2), b.divide(2)).multiply(2);
+        }
+        if (b.isEven()) {
+            return gcd(a, b.divide(2));
+        }
+        if (a.greater(b)) {
+            return gcd(a.subtract(b).divide(2), b);
+        }
+        return gcd(b.subtract(a).divide(2), a);
+    }
+    function lcm(a, b) {
+        a = parseInput(a).abs();
+        b = parseInput(b).abs();
+        return a.multiply(b).divide(gcd(a, b));
+    }
+    BigInteger.prototype.next = function () {
+        return fastAdd(this, 1);
+    };
+    BigInteger.prototype.prev = function () {
+        return fastSubtract(this, 1);
+    };
+    BigInteger.prototype.compare = function (n) {
+        var first = this, second = parseInput(n);
+        if (first.value.length === 1 && second.value.length === 1 && first.value[0] === 0 && second.value[0] === 0) return 0;
+        if (second.sign !== first.sign) return first.sign === sign.positive ? 1 : -1;
+        var multiplier = first.sign === sign.positive ? 1 : -1;
+        var a = first.value, b = second.value,
+            length = Math.max(a.length, b.length) - 1;
+        for (var i = length; i >= 0; i--) {
+            var ai = (a[i] || 0), bi = (b[i] || 0);
+            if (ai > bi) return 1 * multiplier;
+            if (bi > ai) return -1 * multiplier;
+        }
+        return 0;
+    };
+    BigInteger.prototype.compareTo = function (n) {
+        return this.compare(n);
+    };
+    BigInteger.prototype.compareAbs = function (n) {
+        return this.abs().compare(n.abs());
+    };
+    BigInteger.prototype.equals = function (n) {
+        return this.compare(n) === 0;
+    };
+    BigInteger.prototype.notEquals = function (n) {
+        return !this.equals(n);
+    };
+    BigInteger.prototype.lesser = function (n) {
+        return this.compare(n) < 0;
+    };
+    BigInteger.prototype.greater = function (n) {
+        return this.compare(n) > 0;
+    };
+    BigInteger.prototype.greaterOrEquals = function (n) {
+        return this.compare(n) >= 0;
+    };
+    BigInteger.prototype.lesserOrEquals = function (n) {
+        return this.compare(n) <= 0;
+    };
+
+    BigInteger.prototype.lt = BigInteger.prototype.lesser;
+    BigInteger.prototype.leq = BigInteger.prototype.lesserOrEquals;
+    BigInteger.prototype.gt = BigInteger.prototype.greater;
+    BigInteger.prototype.geq = BigInteger.prototype.greaterOrEquals;
+    BigInteger.prototype.eq = BigInteger.prototype.equals;
+    BigInteger.prototype.neq = BigInteger.prototype.notEquals;
+
+    function max (a, b) {
+        a = parseInput(a);
+        b = parseInput(b);
+        return a.greater(b) ? a : b;
+    }
+    function min (a, b) {
+        a = parseInput(a);
+        b = parseInput(b);
+        return a.lesser(b) ? a : b;
+    }
+    BigInteger.prototype.isPositive = function () {
+        return this.sign === sign.positive;
+    };
+    BigInteger.prototype.isNegative = function () {
+        return this.sign === sign.negative;
+    };
+    BigInteger.prototype.isEven = function () {
+        return this.value[0] % 2 === 0;
+    };
+    BigInteger.prototype.isOdd = function () {
+        return this.value[0] % 2 === 1;
+    };
+    BigInteger.prototype.isUnit = function () {
+        return this.value.length === 1 && this.value[0] === 1;
+    };
+    BigInteger.prototype.isDivisibleBy = function (n) {
+        return this.mod(n).equals(ZERO);
+    };
+    BigInteger.prototype.isPrime = function () {
+        var n = this.abs(),
+            nPrev = n.prev();
+        if (n.isUnit()) return false;
+        if (n.equals(2) || n.equals(3) || n.equals(5)) return true;
+        if (n.isEven() || n.isDivisibleBy(3) || n.isDivisibleBy(5)) return false;
+        if (n.lesser(25)) return true;
+        var a = [2, 3, 5, 7, 11, 13, 17, 19],
+            b = nPrev,
+            d, t, i, x;
+        while (b.isEven()) b = b.divide(2);
+        for (i = 0; i < a.length; i++) {
+            x = bigInt(a[i]).modPow(b, n);
+            if (x.equals(ONE) || x.equals(nPrev)) continue;
+            for (t = true, d = b; t && d.lesser(nPrev); d = d.multiply(2)) {
+                x = x.square().mod(n);
+                if (x.equals(nPrev)) t = false;
+            }
+            if (t) return false;
+        }
+        return true;
+    };
+    function randBetween (a, b) {
+        a = parseInput(a);
+        b = parseInput(b);
+        var low = min(a, b), high = max(a, b);
+        var range = high.subtract(low);
+        var length = range.value.length - 1;
+        var result = [], restricted = true;
+        for (var i = length; i >= 0; i--) {
+            var top = restricted ? range.value[i] : base;
+            var digit = Math.floor(Math.random() * top);
+            result.unshift(digit);
+            if (digit < top) restricted = false;
+        }
+        return low.add(new BigInteger(result, false));
+    }
+
+    var powersOfTwo = [1];
+    while (powersOfTwo[powersOfTwo.length - 1] <= base) powersOfTwo.push(2 * powersOfTwo[powersOfTwo.length - 1]);
+    var powers2Length = powersOfTwo.length, highestPower2 = powersOfTwo[powers2Length - 1];
+
+    BigInteger.prototype.shiftLeft = function (n) {
+        if (!isSmall(n)) {
+            if (n.isNegative()) return this.shiftRight(n.abs());
+            return this.times(bigInt(2).pow(n));
+        }
+        n = +n;
+        if (n < 0) return this.shiftRight(-n);
+        var result = this;
+        while (n >= powers2Length) {
+            result = fastMultiply(result, highestPower2);
+            n -= powers2Length - 1;
+        }
+        return fastMultiply(result, powersOfTwo[n]);
+    };
+
+    BigInteger.prototype.shiftRight = function (n) {
+        if (!isSmall(n)) {
+            if (n.isNegative()) return this.shiftLeft(n.abs());
+            return this.over(bigInt(2).pow(n));
+        }
+        n = +n;
+        if (n < 0) return this.shiftLeft(-n);
+        var result = this;
+        while (n >= powers2Length) {
+            if (result.equals(ZERO)) return result;
+            result = fastDivMod(result, highestPower2).quotient;
+            n -= powers2Length - 1;
+        }
+        return fastDivMod(result, powersOfTwo[n]).quotient;
+    };
+
+    // Reference: http://en.wikipedia.org/wiki/Bitwise_operation#Mathematical_equivalents
+    function bitwise(x, y, fn) {
+        var sum = ZERO;
+        var limit = max(x.abs(), y.abs());
+        var n = 0, _2n = ONE;
+        while (_2n.lesserOrEquals(limit)) {
+            var xMod, yMod;
+            xMod = x.over(_2n).isEven() ? 0 : 1;
+            yMod = y.over(_2n).isEven() ? 0 : 1;
+
+            sum = sum.add(_2n.times(fn(xMod, yMod)));
+
+            _2n = fastMultiply(_2n, 2);
+        }
+        return sum;
+    }
+
+    BigInteger.prototype.not = function () {
+        var body = bitwise(this, this, function (xMod) { return (xMod + 1) % 2; });
+        return !this.sign ? body.negate() : body;
+    };
+
+    BigInteger.prototype.and = function (n) {
+        n = parseInput(n);
+        var body = bitwise(this, n, function (xMod, yMod) { return xMod * yMod; });
+        return this.sign && n.sign ? body.negate() : body;
+    };
+
+    BigInteger.prototype.or = function (n) {
+        n = parseInput(n);
+        var body = bitwise(this, n, function (xMod, yMod) { return (xMod + yMod + xMod * yMod) % 2 });
+        return this.sign || n.sign ? body.negate() : body;
+    };
+
+    BigInteger.prototype.xor = function (n) {
+        n = parseInput(n);
+        var body = bitwise(this, n, function (xMod, yMod) { return (xMod + yMod) % 2; });
+        return this.sign ^ n.sign ? body.negate() : body;
+    };
+
+    BigInteger.prototype.toString = function (radix) {
+        if (radix === undefined) {
+            radix = 10;
+        }
+        if (radix !== 10) return toBase(this, radix);
+        var first = this;
+        var str = "", len = first.value.length;
+        if (len === 0) {
+            return "0";
+        }
+        while (len--) {
+            if (first.value[len].toString().length === 8) str += first.value[len];
+            else str += (base.toString() + first.value[len]).slice(-logBase);
+        }
+        while (str[0] === "0") {
+            str = str.slice(1);
+        }
+        if (!str.length) str = "0";
+        if (str === "0") return str;
+        var s = first.sign === sign.positive ? "" : "-";
+        return s + str;
+    };
+    BigInteger.prototype.toJSNumber = function () {
+        return this.valueOf();
+    };
+    BigInteger.prototype.valueOf = function () {
+        if (this.value.length === 1) return this.sign ? -this.value[0] : this.value[0];
+        return +this.toString();
+    };
+
+    var goesInto = function (a, b) {
+        var a = new BigInteger(a, sign.positive), b = new BigInteger(b, sign.positive);
+        if (a.equals(0)) throw new Error("Cannot divide by 0");
+        var n = 0;
+        do {
+            var inc = 1;
+            var c = a, t = c.times(10);
+            while (t.lesser(b)) {
+                c = t;
+                inc *= 10;
+                t = t.times(10);
+            }
+            while (c.lesserOrEquals(b)) {
+                b = b.minus(c);
+                n += inc;
+            }
+        } while (a.lesserOrEquals(b));
+
+        return {
+            remainder: b.value,
+            result: n
+        };
+    };
+
+    var ZERO = new BigInteger([0], sign.positive);
+    var ONE = new BigInteger([1], sign.positive);
+    var MINUS_ONE = new BigInteger([1], sign.negative);
+
+
+    function parseInput(text) {
+        if (text instanceof BigInteger) return text;
+        if (Math.abs(+text) < base && +text === (+text | 0)) {
+            var value = +text;
+            return new BigInteger([Math.abs(value)], (value < 0 || (1 / value) === -Infinity));
+        }
+        text += "";
+        var s = sign.positive, value = [];
+        if (text[0] === "-") {
+            s = sign.negative;
+            text = text.slice(1);
+        }
+        var text = text.split(/e/i);
+        if (text.length > 2) throw new Error("Invalid integer: " + text.join("e"));
+        if (text[1]) {
+            var exp = text[1];
+            if (exp[0] === "+") exp = exp.slice(1);
+            exp = parseInput(exp);
+            if (exp.lesser(0)) throw new Error("Cannot include negative exponent part for integers");
+            while (exp.notEquals(0)) {
+                text[0] += "0";
+                exp = exp.prev();
+            }
+        }
+        text = text[0];
+        if (text === "-0") text = "0";
+        var isValid = /^([0-9][0-9]*)$/.test(text);
+        if (!isValid) throw new Error("Invalid integer: " + text);
+        while (text.length) {
+            var divider = text.length > logBase ? text.length - logBase : 0;
+            value.push(+text.slice(divider));
+            text = text.slice(0, divider);
+        }
+        return new BigInteger(value, s);
+    }
+
+    var parseBase = function (text, base) {
+        base = parseInput(base);
+        var val = ZERO;
+        var digits = [];
+        var i;
+        var isNegative = false;
+        function parseToken(text) {
+            var c = text[i].toLowerCase();
+            if (i === 0 && text[i] === "-") {
+                isNegative = true;
+                return;
+            }
+            if (/[0-9]/.test(c)) digits.push(parseInput(c));
+            else if (/[a-z]/.test(c)) digits.push(parseInput(c.charCodeAt(0) - 87));
+            else if (c === "<") {
+                var start = i;
+                do { i++; } while (text[i] !== ">");
+                digits.push(parseInput(text.slice(start + 1, i)));
+            }
+            else throw new Error(c + " is not a valid character");
+        }
+        for (i = 0; i < text.length; i++) {
+            parseToken(text);
+        }
+        digits.reverse();
+        for (i = 0; i < digits.length; i++) {
+            val = val.add(digits[i].times(base.pow(i)));
+        }
+        return isNegative ? val.negate() : val;
+    };
+
+    function stringify(digit) {
+        var v = digit.value;
+        if (v.length === 1 && v[0] <= 36) {
+            return "0123456789abcdefghijklmnopqrstuvwxyz".charAt(v[0]);
+        }
+        return "<" + v + ">";
+    }
+
+    function toBase(n, base) {
+        base = bigInt(base);
+        if (base.equals(0)) {
+            if (n.equals(0)) return "0";
+            throw new Error("Cannot convert nonzero numbers to base 0.");
+        }
+        if (base.equals(-1)) {
+            if (n.equals(0)) return "0";
+            if (n.lesser(0)) return Array(1 - n).join("10");
+            return "1" + Array(+n).join("01");
+        }
+        var minusSign = "";
+        if (n.isNegative() && base.isPositive()) {
+            minusSign = "-";
+            n = n.abs();
+        }
+        if (base.equals(1)) {
+            if (n.equals(0)) return "0";
+            return minusSign + Array(+n + 1).join(1);
+        }
+        var out = [];
+        var left = n, divmod;
+        while (left.lesser(0) || left.compareAbs(base) >= 0) {
+            divmod = left.divmod(base);
+            left = divmod.quotient;
+            var digit = divmod.remainder;
+            if (digit.lesser(0)) {
+                digit = base.minus(digit).abs();
+                left = left.next();
+            }
+            out.push(stringify(digit));
+        }
+        out.push(stringify(left));
+        return minusSign + out.reverse().join("");
+    }
+
+    var fnReturn = function (a, b) {
+        if (typeof a === "undefined") return ZERO;
+        if (typeof b !== "undefined") return parseBase(a, b);
+        return parseInput(a);
+    };
+    fnReturn.zero = ZERO;
+    fnReturn.one = ONE;
+    fnReturn.minusOne = MINUS_ONE;
+    fnReturn.randBetween = randBetween;
+    fnReturn.min = min;
+    fnReturn.max = max;
+    fnReturn.gcd = gcd;
+    fnReturn.lcm = lcm;
+    return fnReturn;
+})();
+
+if (typeof module !== "undefined") {
+    module.exports = bigInt;
+}
--- a/chat/modules/moz.build
+++ b/chat/modules/moz.build
@@ -2,16 +2,17 @@
 # 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_JS_MODULES += [
     'ArrayBufferUtils.jsm',
+    'BigInteger.jsm',
     'imContentSink.jsm',
     'imServices.jsm',
     'imSmileys.jsm',
     'imStatusUtils.jsm',
     'imThemes.jsm',
     'imXPCOMUtils.jsm',
     'jsProtoHelper.jsm',
     'NormalizedMap.jsm',
--- a/chat/moz.build
+++ b/chat/moz.build
@@ -9,16 +9,17 @@ DIRS += [
     'modules',
     'content',
     'themes',
     'locales',
     'protocols/facebook',
     'protocols/gtalk',
     'protocols/irc',
     'protocols/odnoklassniki',
+    'protocols/skype',
     'protocols/twitter',
     'protocols/xmpp',
     'protocols/yahoo',
 ]
 
 if CONFIG['MOZ_DEBUG']:
     DIRS += ['protocols/jsTest']
 
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..90984bc748eff02450decdc876f039c54b5ef238
GIT binary patch
literal 1265
zc$@+81P=R&P)<h;3K|Lk000e1NJLTq000&M000&U1^@s6#I$TX00004b3#c}2nYxW
zd<bNS00009a7bBm000fw000fw0YWI7cmMzZ8FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H11a3)0K~y-6jg?z$R7Duae>3Nty_J<(+EQAJManfufl351kch-c
zB=Lf5VxkZFVgirolhW*i`lJuW7fJ9EFQG(1;{y>)L?lEY0@eoP-U}B?DU_wV=j@&{
z^ZBrC=^_}v<e!sFW`6UZ|9qLFL<9ic(U}6)JFfQvN>z(Uzm3A3Ko8)JYS?Bo-TaSI
zB7*MdoUI-Ar@0R{*FL*=cG7hnu8#P0_gwaKg}g~8V#;+K`mPPDOV@{k$jB~hqPJ}}
zbLF03wPm|@T(5WYx|K}}>#Nn3z~UQl72M}3L<xa`$SPtEh_xK(8L&SdKAJE1#aC@M
z^UdABF0gKSYjf@V`f7C|XDC^P5g`(|=nF<55-=hd5wwDN4@}bw8lOsU+k4<M-LWHP
zvm3s?6RfrCbv)fRCo$;T3aFW&x|kyAlsE7VoX!~rOPbf#JYE%xxm$HdXWPFWi!@J7
zC+Nw8odB)z6qzQMmXsppD2zZ?&ho=R#POS!OMy`3IXtuC(bV_99_j#|uW+oiK0oA_
zaCO;{9R<HW!()D0bD<!7-XAhhv@D*i**w!DZ-jyo&Wu<VHr1-hiP&1*(V4CQi?v;s
zb3Spc#`8?MifM5Lz&oc)eB2wc_E5kF=R!{25-$3}<v{2ySo#A+`@B|w4HaN(qMqSG
zNwi`-hFn;71n}1E7|%7hOp7bF^@Y50EEr#0vd~ws+&`r*?s(p&3NT6~97m~=lslkk
z;cVWr<*yP21M}0GH(EV*FH5quEzZ(vU2#ddP^g)dM65JbW~_8tlqR%_OM!6greUBc
z?7nJv;-Js_=R!`7SOi#6<FIW}f+dr0!vh0xM}fjtW-Ru?h3mP}nY`gb!4j1dU`?IF
zlo)){7qb3Hz{&$YzYG~11*@hw<A9_i_>lqb03-5U*Qv9%E}>DQgN}lC=f&x4Pq4bS
z3^71cqCDe#K`I<4T^EJaOzaG8vzgxELU8d$-lrj1KB5uWGZ-<+RebnxoZZWkY;RA{
zTBY#~>>o1Y;G%_sRWvs=mVo8A9Rp&bFT2n8mu6Hc^r-FZsS+FiEVA=TM3n<^4Zqzm
zynHm^MtIxtm=Q;h9}FnJ6^O`3U8hcMoYT@&x2QVF@nK8J!oF*Ueb)^CRVf$Jt}r-s
z-3886IJVi$kcpyo-|qQ2ANqM#)w?t%6|TM~Z@k*79ezLE8;%6Qr#74E8yi<+4?TeH
z=zK&w?ygxgTI!b0eJEZxDMet*Il2lwttc8{AP|&-=A`0AF30x0`*YSr(``0$^R8f}
zlu}Q=RHK%x-lS9M7X>w|zNR`%c&;jj5yi;J)P}}*Yg3&o)^fD_MDg#zp_BI9A8*Q6
z?;QaGAP|w;V5L+HNKH_Z>2vFuKDULE?-7<f951OCJl3vj8)u3&1>1dahwS;{Fi-&e
z3Ex{2!46PW0WK>ss@T|N0})Ufb?8q}9>|GEP!Yvg9Dqal=NTJSd;-`2;V5`aArOhk
bcnkdl&L2Eg{gip>00000NkvXXu0mjfz++Bl
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1c487b7372027e3dd17093b407ce874715e38707
GIT binary patch
literal 3002
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..13e296143b0edfdb75c5841975d4c1ac013c102c
GIT binary patch
literal 885
zc$@)s1B(2KP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004b3#c}2nYxW
zd<bNS00009a7bBm000fw000fw0YWI7cmMzZ8FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H10`f^jK~y-6U6V~{TxAr7pYz>&lexK>%uE(xG%=A<qJgCK5B!Nz
zR9a9FBUG{}E((GR?M`%+FuD*I3gSXU=*9{LOBW&-(1oB2D`FebW~io>q)9Y?ZFDlZ
zbMN<k$Hl~0J(~ju&cn-j&nv15fSI1THwdG*5s6fN-CJ7+6zBl{1ZI70@)!PBqN+r%
zpL@R;M<<RP9*cXsVp(hKu<=*ZWlq(BNz=5ovby4%nZ4?reb(0|>-zwi`uM({r0c6w
zPaR9V2)S1rV7*{f$rY?`_sdJm{)f4XyVlwXUz_|Az?d+ce)O)<SVd@gCG*g7ka}(F
zH;{VBJRIonH%G=FNlXx&Gt)Ce0HGiU`^qJgt71J^g^{9Rq8d`|6aeZC&xcDnKR1+8
zPmz&3M!M!N*Ix!+G{F4z<}T1)b_&l8gv0^7I-l|G)trHXaI7z6IrVI`;J|GI0jej=
z^h_8!XMfz@X-<e3vFcHUOrcZYdalf_Sw2~@6hlZIq)v(Ah;q4Cwl>$FFsS-KM7%03
z4+~AtI}15+AbfDRlds2PP7a2o{$J}-C-haTMc{5DB5{skBU3JHJ62NV?5gF-^Shj0
z%vsNrlY<c--BTcFpM*e!P>cf3sOqIFt80zlTWtpbYIk-p6dT@J$a(s^ly9~iw-$tQ
z)FyNU;L)~=z^6;A%TX!xM5YZmR5ZMIsFSJT4qmuD;-N~wm8NGiYk#d%rQDqu6Gp?v
z*Cw}B<^0;_4Q?s42`~NB;`Cz9{gr^{2P5Vio>wkq`!~qEQ0|Tqkzr9)%=FB|NfLkY
z%+bdS>v_P^&YnO(_5yKpjg|ryuU+%s&&|!sKVb@MXHO+voo_z&-~)wfrH30CG;)VX
zfDzbr%8rHpSV)ZH%x9mcHn*eufW1VHn=m|6N|L^zf&QS<okUI*k4mMxD81bUe*0s~
zeLHtC&vW;hojU%;e*iau4gyDk(%vWv!e|T_Q@G%5KILnZzXJRXvaV_YJT@c=00000
LNkvXXu0mjf#jcdK
new file mode 100644
--- /dev/null
+++ b/chat/protocols/skype/jar.mn
@@ -0,0 +1,9 @@
+# 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/.
+
+chat.jar:
+% skin prpl-skype classic/1.0 %skin/classic/prpl/skype/
+	skin/classic/prpl/skype/icon32.png	(icons/prpl-skype-32.png)
+	skin/classic/prpl/skype/icon48.png	(icons/prpl-skype-48.png)
+	skin/classic/prpl/skype/icon.png	(icons/prpl-skype.png)
new file mode 100644
--- /dev/null
+++ b/chat/protocols/skype/moz.build
@@ -0,0 +1,13 @@
+# 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 += [
+    'skype.js',
+    'skype.manifest',
+]
+
+JAR_MANIFESTS += ['jar.mn']
new file mode 100644
--- /dev/null
+++ b/chat/protocols/skype/skype.js
@@ -0,0 +1,852 @@
+/* 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, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Http.jsm");
+Cu.import("resource:///modules/ArrayBufferUtils.jsm");
+Cu.import("resource:///modules/BigInteger.jsm");
+Cu.import("resource:///modules/imServices.jsm");
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/jsProtoHelper.jsm");
+
+// Constants used by the login process. This emulates a captured session using
+// official means.
+const kLockAndKeyAppId = "msmsgs@msnmsgr.com";
+const kLockAndKeySecret = "Q1P7W2E4J9R8U3S5";
+const kClientId = "578134";
+const kClientInfo = "os=Windows; osVer=8.1; proc=Win32; lcid=en-us; " +
+  "deviceType=1; country=n/a; clientName=swx-skype.com; clientVer=908/1.0.0.20";
+
+const kLoginHost = "login.skype.com";
+const kContactsHost = "api.skype.com";
+const kMessagesHost = "client-s.gateway.messenger.live.com";
+
+// Map from strings returned by the SkypeWeb API to statuses.
+const kStatusMap = {
+  "Online": "AVAILABLE",
+  "Offline": "OFFLINE",
+  "Idle": "IDLE",
+  "Away": "AWAY",
+  "Hidden": "INVISIBLE"
+};
+
+XPCOMUtils.defineLazyGetter(this, "_", function()
+  l10nHelper("chrome://chat/locale/skype.properties")
+);
+
+/*
+ * Convert a URL to a user's name.
+ *
+ * E.g. https://bay-client-s.gateway.messenger.live.com/v1/users/ME/contacts/8:clokep
+ *      https://bay-client-s.gateway.messenger.live.com/v1/users/8:clokep/presenceDocs/messagingService
+ *
+ *
+ * Note that some contacts might have a /1: in them (instead of a /8:), that's
+ * for MSN linked contacts.
+ */
+function contactUrlToName(aUrl) {
+  let start = aUrl.indexOf("/8:");
+  if (start == -1)
+    return null;
+  // Skip over the separator.
+  start += "/8:".length;
+
+  let end = aUrl.indexOf("/", start);
+  if (end == -1)
+    end = undefined;
+
+  return aUrl.slice(start, end);
+}
+
+function SkypeConversation(aAccount, aName) {
+  this.buddy = aAccount._buddies.get(aName);
+  this._init(aAccount, aName);
+}
+SkypeConversation.prototype = {
+  __proto__: GenericConvIMPrototype,
+  _account: null,
+
+  sendMsg: function(aMessage) {
+    if (!aMessage.length)
+      return;
+
+    let target = "8:" + this.name;
+    let url = "https://" + kMessagesHost + "/v1/users/ME/conversations/" +
+              target + "/messages";
+
+    let clientMessageId = Date.now().toString();
+    let message = {
+      "clientmessageid": clientMessageId,
+      "content": aMessage,
+      "messagetype": "RichText",
+      "contenttype": "text",
+    };
+    let options = {
+      onLoad: (aResponse, aXhr) => {
+        this._account.LOG("Message response: " + aResponse);
+        this._account.LOG("Successfully sent message: " + aMessage)
+      },
+      onError: this._account._onHttpFailure("sending message"),
+      postData: JSON.stringify(message),
+      logger: this._account.logger
+    };
+
+    // TODO Track the messages we sent?
+    this._account._messagesRequest(url, options);
+  }
+};
+
+function SkypeAccountBuddy(aAccount, aBuddy, aTag, aUserName) {
+  aAccount.LOG("Creating account buddy for " + aUserName);
+
+  this._init(aAccount, aBuddy, aTag, aUserName);
+}
+SkypeAccountBuddy.prototype = {
+  __proto__: GenericAccountBuddyPrototype,
+  _info: null,
+  mood: null,
+
+  // Called when the user wants to chat with the buddy.
+  createConversation: function() this._account.createConversation(this.userName),
+
+  // Returns a list of imITooltipInfo objects to be displayed when the user
+  // hovers over the buddy.
+  getTooltipInfo: function() {
+    if (!this._info)
+      return EmptyEnumerator;
+
+    let tooltipInfo = [];
+    for (let info in this._info) {
+      // If there's no value, skip the element.
+      if (!this._info[info])
+        continue;
+
+      // TODO Put real labels on here.
+      tooltipInfo.push(new TooltipInfo(info, this._info[info]));
+    }
+    if (this.mood)
+      tooltipInfo.push(new TooltipInfo("Mood", this.mood));
+
+    return new nsSimpleEnumerator(tooltipInfo);
+  },
+
+  remove: function() {
+    this._account.removeBuddy(this);
+    GenericAccountBuddyPrototype.remove.call(this);
+  }
+};
+
+/*
+ * Cut out a part of a larger string bordered by aStart and aEnd. Returns an
+ * empty string if the needle is not found.
+ */
+function extractString(aStr, aStart, aEnd) {
+  // First find the start index, then offset by the string length.
+  let startIndex = aStr.indexOf(aStart) + aStart.length;
+  if (startIndex == -1)
+    return "";
+  // Now find the next occurrence of end after the start.
+  let endIndex = aStr.indexOf(aEnd, startIndex);
+  if (endIndex == -1)
+    return "";
+
+  return aStr.slice(startIndex, endIndex);
+}
+
+/*
+ * A magic method (originally from MSN) to stop 3rd parties from connecting.
+ * Differs from MSN by swapping MD5 for SHA256.
+ *
+ * A pre-emptive apology is necessary for those about to embark on the journey
+ * of understanding this code. I wish you luck and God's speed.
+ */
+function magicSha256(aInput) {
+  let productId = kLockAndKeyAppId;
+
+  // Create a SHA 256 hash.
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                    .createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  let data = converter.convertToByteArray(aInput);
+  let productKey = converter.convertToByteArray(kLockAndKeySecret);
+
+  let hash =
+    Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+  hash.init(hash.SHA256);
+  hash.update(data, data.length);
+  hash.update(productKey, productKey.length);
+  // Finalize the hash as a set of bytes.
+  let sha256Hash = hash.finish(false);
+
+  // Split it into four integers (note that this ignores the second half of the
+  // hash).
+  let sha256Buffer = StringToArrayBuffer(sha256Hash);
+  let view = new DataView(sha256Buffer, 0, 16);
+
+  let sha256Parts = [];
+  let newHashParts = [];
+  for (let i = 0; i < 4; ++i) {
+    // Ensure little-endianness is used.
+    sha256Parts.push(view.getUint32(i * 4, true));
+
+    newHashParts.push(sha256Parts[i]);
+    sha256Parts[i] &= 0x7FFFFFFF;
+  }
+
+  // Make a new string and pad with '0' to a length that's a multiple of 8.
+  let buf = aInput + productId;
+  let len = buf.length;
+  let modLen = len % 8;
+  if (modLen != 0) {
+    let fix = 8 - modLen;
+    buf += "0".repeat(fix);
+    len += fix;
+  }
+
+  // Split into integers.
+  view = new DataView(StringToArrayBuffer(buf));
+
+  // This is magic.
+  let nHigh = bigInt(0);
+  let nLow = bigInt(0);
+  for (let i = 0; i < (len / 4); i += 2) {
+    let temp = bigInt(0x0E79A9C1).times(view.getUint32(i * 4, true))
+                                 .divmod(0x7FFFFFFF).remainder;
+    temp = temp.plus(nLow).times(sha256Parts[0]).plus(sha256Parts[1])
+                                                .divmod(0x7FFFFFFF).remainder;
+    nHigh = nHigh.plus(temp);
+
+    temp = temp.plus(view.getUint32((i + 1) * 4, true)).divmod(0x7FFFFFFF).remainder;
+    nLow = temp.times(sha256Parts[2]).plus(sha256Parts[3]).divmod(0x7FFFFFFF).remainder;
+    nHigh = nHigh.plus(nLow);
+  }
+  nLow = nLow.plus(sha256Parts[1]).divmod(0x7FFFFFFF).remainder.toJSNumber();
+  nHigh = nHigh.plus(sha256Parts[3]).divmod(0x7FFFFFFF).remainder.toJSNumber();
+
+  newHashParts[0] ^= nLow;
+  newHashParts[1] ^= nHigh;
+  newHashParts[2] ^= nLow;
+  newHashParts[3] ^= nHigh;
+
+  // Make a string of the parts and convert to hexadecimal.
+  let output = "";
+  for (let i = 0; i < 4; ++i) {
+    let part = newHashParts[i];
+    // Adjust to little-endianness.
+    part = ((part & 0xFF) << 24) | ((part & 0xFF00) << 8) |
+           ((part >> 8) & 0xFF00) | ((part >> 24) & 0xFF);
+
+    // JavaScript likes to use signed numbers, force this to give us the
+    // unsigned representation.
+    if (part < 0)
+      part += 0xFFFFFFFF + 1;
+
+    let hexPart = part.toString(16);
+    // Ensure that the string has 8 characters (4 bytes).
+    output += "0".repeat(8 - hexPart.length) + hexPart;
+  }
+
+  return output;
+}
+
+// TODO Add tests for this function.
+// Calculate the timezone offset of the local computer as [+-]HH:MM.
+function getTimezone() {
+  /*
+   * Zero-pad aNum to the length of aLen.
+   */
+  function zeroPad(aNum, aLen) {
+    let nStr = aNum.toString();
+    let nLen = nStr.length;
+
+    if (nLen > aLen) {
+      throw "Can't zero-pad when longer than expected length: " + nStr +
+        ".length > " + aLen;
+    }
+
+    return "0".repeat(aLen - nLen) + nStr;
+  };
+
+  // Invert the sign of the timezone from JavaScript's date object.
+  let sign = "+";
+  let timezone = new Date().getTimezoneOffset() * -1;
+  if (timezone < 0)
+    sign = "-";
+  timezone = Math.abs(timezone);
+
+  // Separate the timezone into hours and minutes.
+  let minutes = timezone % 60;
+  let hours = (timezone - minutes) / 60;
+
+  // Ensure both hours and minutes are two digits long.
+  minutes = zeroPad(minutes, 2);
+  hours = zeroPad(hours, 2);
+
+  // The final timezone string.
+  return sign + hours + "|" + minutes;
+}
+
+function SkypeAccount(aProtoInstance, aImAccount) {
+  this._init(aProtoInstance, aImAccount);
+
+  // Initialize some maps.
+  this._buddies = new Map();
+  this._conversations = new Map();
+  this._chats = new Map();
+
+  this._logger = {log: this.LOG.bind(this), debug: this.DEBUG.bind(this)};
+}
+SkypeAccount.prototype = {
+  __proto__: GenericAccountPrototype,
+  // A Map holding the list of buddies associated with their usernames.
+  _buddies: null,
+  // A Map holding the list of open conversations by the username of the buddy.
+  _conversations: null,
+  // A Map holding the list of open (multiple user) chats by name.
+  _chats: null,
+  // The current request in the polling loop.
+  _request: null,
+  // The timer for the next poll.
+  _poller: null,
+
+  // Some tokens.
+  _skypeToken: null,
+  _registrationToken: null,
+
+  // Logger used for HTTP requests.
+  _logger: null,
+
+  mapStatusString: function(aStatus) {
+    if (aStatus in kStatusMap)
+      return Ci.imIStatusInfo["STATUS_" + kStatusMap[aStatus]];
+
+    // Uh-oh, we got something not in the map.
+    this.WARN("Received unknown status type: " + aStatus);
+    return Ci.imIStatusInfo.STATUS_UNKNOWN;
+  },
+
+  connect: function() {
+    this.reportConnecting();
+
+    this.LOG("STARTING Login");
+
+    // Perform the request to get the session token values.
+    let loginUrl = "https://" + kLoginHost + "/login";
+    let options = {
+      onLoad: this._onPieResponse.bind(this),
+      onError: this._onHttpFailure("requesting pie"),
+      logger: this.logger
+    }
+    httpRequest(loginUrl, options);
+  },
+
+  /*
+   * Generates a callback which causes the account to enter an error state with
+   * the given error string.
+   */
+  _onHttpFailure: function(aErrorStr) {
+    return (aError, aResponse, aXhr) => {
+      this.ERROR("HTTP failure occurred: " + aErrorStr + "\n" + aError);
+      this._disconnectWithAuthFailure();
+    };
+  },
+
+  _onHttpError: function(aError, aResponse, aXhr) {
+    this.ERROR("Received error response:\n" + aError);
+  },
+
+  // Mmmmm...pie.
+  _onPieResponse: function(aResponse, aXhr) {
+    this.reportConnecting(_("connecting.authenticating"));
+
+    // Parse the pie/etm and do the actual login.
+    let loginUrl = "https://" + kLoginHost + "/login";
+
+    let params = [["client_id", kClientId],
+                  ["redirect_uri", "https://web.skype.com"]];
+    loginUrl += "?" +
+      params.map(function(p) p.map(encodeURIComponent).join("=")).join("&");
+
+    this.LOG("Received PIE response:\n" + aResponse);
+
+    // Note that the response is really just an HTML page with some JavaScript
+    // and forms that these values are being pulled from.
+    let pie = extractString(aResponse, "=\"pie\" value=\"", "\"");
+    if (!pie) {
+      this.ERROR("pie value not found.")
+      this._disconnectWithAuthFailure();
+      return;
+    }
+    let etm = extractString(aResponse, "=\"etm\" value=\"", "\"");
+    if (!etm) {
+      this.ERROR("etm value not found.")
+      this._disconnectWithAuthFailure();
+      return;
+    }
+
+    let options = {
+      onLoad: this._onLoginResponse.bind(this),
+      onError: this._onHttpFailure("requesting skypetoken"),
+      postData: [["username", this.name],
+                 ["password", this.imAccount.password],
+                 ["timezone_field", getTimezone()],
+                 ["pie", pie],
+                 ["etm", etm],
+                 ["js_time", Date.now()],
+                 ["client_id", kClientId],
+                 ["redirect_uri", "https://web.skype.com/"]],
+      headers: [["Connection", "close"],
+                // BehaviorOverride is a custom microsoft header. It stops the
+                // response from doing a 302 Found Location redirect, since
+                // there are important headers that need to be plucked before
+                // the redirect happens.
+                ["BehaviorOverride", "redirectAs404"]],
+      logger: this._logger
+    }
+    httpRequest(loginUrl, options);
+  },
+  _onLoginResponse: function(aResponse, aXhr) {
+    this.LOG("Received LOGIN response:\n" + aResponse);
+
+    let refreshToken =
+      extractString(aResponse, "=\"skypetoken\" value=\"", "\"");
+    if (!refreshToken) {
+      this.ERROR("skypetoken value not found.")
+      this._disconnectWithAuthFailure();
+      return;
+    }
+
+    // All done!
+    this._skypeToken = refreshToken;
+    this.LOG("Recevied Skype token: " + this._skypeToken);
+
+    if (this._registrationToken) {
+      // Subscribe to receive particular events.
+      this._subscribe();
+      return;
+    }
+
+    this.reportConnecting(_("connecting.registrationToken"));
+
+    // Request the registration token.
+    let messagesUrl = "https://" + kMessagesHost + "/v1/users/ME/endpoints";
+    // The current time in seconds, converted to a string.
+    let curTime = String(Math.floor(Date.now() / 1000));
+    let response = magicSha256(curTime);
+    let options = {
+      onLoad: this._onRegistrationTokenReceived.bind(this),
+      onError: (aError, aResponse, aXhr) => {
+        this.ERROR("HTTP failure occurred: requesting registration token\n" +
+                   aError);
+        this._disconnectWithAuthFailure("error.registrationToken");
+      },
+      postData: "{}", // Empty JSON object.
+      headers: [["Connection", "close"],
+                // BehaviorOverride is a custom microsoft header. It stops the
+                // response from doing a 302 Found Location redirect, since
+                // there are important headers that need to be plucked before
+                // the redirect happens.
+                ["BehaviorOverride", "redirectAs404"],
+                ["LockAndKey", "appId=" + kLockAndKeyAppId +
+                               "; time=" + curTime +
+                               "; lockAndKeyResponse=" + response],
+                ["ClientInfo", kClientInfo],
+                ["Authentication", "skypetoken=" + this._skypeToken]],
+      logger: this._logger
+    }
+    httpRequest(messagesUrl, options);
+  },
+  _onRegistrationTokenReceived: function(aResponse, aXhr) {
+    this.LOG("Registration token received: " + aResponse);
+
+    let registrationToken = aXhr.getResponseHeader("Set-RegistrationToken");
+    this.LOG("regToken: " + registrationToken);
+    if (!registrationToken) {
+      this.ERROR("registraation token value not found.")
+      this._disconnectWithAuthFailure();
+      return;
+    }
+
+    this._registrationToken = registrationToken;
+    this._subscribe();
+  },
+
+  // Subscribe to the events we want to see.
+  _subscribe: function() {
+    this.LOG("Sending subscription.");
+
+    // Subscribe to particular events.
+    let messagesUrl =
+      "https://" + kMessagesHost + "/v1/users/ME/endpoints/SELF/subscriptions";
+    // The endpoints to subscribe to.
+    let subscriptions = {
+      "interestedResources": ["/v1/users/ME/conversations/ALL/properties",
+                              "/v1/users/ME/conversations/ALL/messages",
+                              "/v1/users/ME/contacts/ALL",
+                              "/v1/threads/ALL"],
+      "template": "raw",
+      "channelType": "httpLongPoll"
+    };
+    let options = {
+      onLoad: this._onSubscription.bind(this),
+      onError: this._onHttpFailure("subscribing to notifications"),
+      postData: JSON.stringify(subscriptions),
+      logger: this._logger
+    };
+    this._messagesRequest(messagesUrl, options);
+  },
+
+  _onSubscription: function(aResponse, aXhr) {
+    this.LOG("Got subscription response: " + aResponse);
+    this.reportConnected();
+
+    // TODO Check auth requests.
+
+    // Get friends list.
+    let contactListUrl = "https://" + kContactsHost + "/users/self/contacts";
+    let options = {
+      onLoad: this._onContactsList.bind(this),
+      onError: this._onHttpError.bind(this),
+      logger: this._logger
+    }
+    this._contactsRequest(contactListUrl, options);
+
+    // Poll for messages.
+    this._getMessages();
+  },
+
+  _onContactsList: function(aResponse, aXhr) {
+    this.LOG("Contacts list: " + aResponse);
+
+    let buddies = JSON.parse(aResponse);
+    if (!buddies) {
+      this.ERROR("Unable to parse JSON response: " + aResponse);
+      return;
+    }
+
+    // You have no friends. :( Nothing to do, just move along.
+    if (!buddies.length)
+      return;
+
+    // This gets a little confusing, buddyObj refers to the JSON that was parsed
+    // and returned from the server, buddy refers to the prplIAccountBuddy.
+    for (let buddyObj of buddies) {
+      let buddy = this._buddies.get(buddyObj.skypename);
+      if (!buddy) {
+        buddy = new SkypeAccountBuddy(
+          this, null, Services.tags.defaultTag, buddyObj.skypename);
+
+        // Store the buddy for later.
+        this._buddies.set(buddyObj.skypename, buddy);
+
+        // Notify the UI of the buddy.
+        Services.contacts.accountBuddyAdded(buddy);
+      }
+
+      // TODO There is also fullname / skypename.
+      // Note that display_name is the public alias that the buddy has set for
+      // themselves, skypename is the buddy's unique ID name, fullname is their
+      // real name.
+      if (buddyObj.display_name)
+        buddy.serverAlias = buddyObj.display_name;
+      // Store the buddy info into the object for tooltips.
+      buddy._info = buddyObj;
+
+      // Set the buddy's status to offline until we get an update.
+      buddy.setStatus(Ci.imIStatusInfo.STATUS_OFFLINE, "");
+    }
+
+    // Download profiles.
+    let profilesUrl =
+      "https://" + kContactsHost + "/users/self/contacts/profiles";
+    let options = {
+      postData: buddies.map((b) => ["contacts[]", b.skypename]),
+      onLoad: this._onProfiles.bind(this),
+      onError: this._onHttpError.bind(this),
+      logger: this._logger
+    };
+    this.LOG(JSON.stringify(options));
+    this._contactsRequest(profilesUrl, options);
+
+    // Subscribe to user statuses.
+    let contactsUrl = "https://" + kMessagesHost + "/v1/users/ME/contacts";
+    let contacts = buddies.map((b) => {
+      return {"id": "8:" + b.skypename};
+    });
+    options = {
+      postData: JSON.stringify({"contacts": contacts}),
+      onLoad: (aResponse, aXhr) =>
+        this.LOG("Successfully subscribed to contacts."),
+      onError: this._onHttpError.bind(this),
+      logger: this._logger
+    };
+    this.LOG(JSON.stringify(options));
+    this._messagesRequest(contactsUrl, options);
+  },
+
+  _onProfiles: function(aResponse, aXhr) {
+    this.LOG("Profiles: " + aResponse);
+
+    let skypeContacts = JSON.parse(aResponse);
+
+    // TODO Error checking.
+
+    for (let skypeContact of skypeContacts) {
+      let username = skypeContact.username;
+
+      let buddy = this._buddies.get(username);
+      if (!buddy)
+        continue;
+
+      // Set some properties on the buddy.
+      buddy.serverAlias = skypeContact.displayname;
+      // TODO There's also firstname and lastname fields.
+      buddy.mood = skypeContact.mood;
+
+      // TODO Download the file and store it in the profile.
+      let avatarUrl = skypeContact.avatarUrl;
+      if (!avatarUrl) {
+        avatarUrl = "https://" + kContactsHost + "/users/" + buddy.userName +
+          "/profile/avatar";
+      }
+      buddy.buddyIconFilename = skypeContact.avatarUrl;
+    }
+  },
+
+  /*
+   * Download the actual messages, this will recurse through its callback.
+   */
+  _getMessages: function() {
+    let messagesUrl = "https://" + kMessagesHost +
+      "/v1/users/ME/endpoints/SELF/subscriptions/0/poll";
+    let options = {
+      method: "POST",
+      onLoad: this._onMessages.bind(this),
+      onError: this._onHttpError.bind(this),
+      logger: this._logger
+    };
+    this._request = this._messagesRequest(messagesUrl, options);
+  },
+
+  _onMessages: function(aResponse, aXhr) {
+    this.LOG("Messages: " + aResponse);
+
+    // Poll for new events by performing another XHR in 1 second.
+    this._request = null;
+    this._poller = setTimeout(this._getMessages.bind(this), 1000);
+
+    // Empty responses are received as keep alives.
+    if (!aResponse)
+      return;
+
+    // Otherwise, parse the response as JSON.
+    let obj = JSON.parse(aResponse);
+    if (!obj) {
+      this.ERROR("Unable to parse JSON response: " + aResponse);
+      return;
+    }
+
+    // If no messages, nothing to do.
+    if (!("eventMessages" in obj))
+      return;
+
+    for (let message of obj.eventMessages) {
+      // The type of message (e.g. new message, new status).
+      let resourceType = message.resourceType;
+      // The message object.
+      let resource = message.resource;
+
+      // Based on what the message is, totally different things are done below.
+      // Sorry for the mess. We can probably abstract this better.
+      if (resourceType == "NewMessage") {
+        let messageType = resource.messagetype;
+        let from = contactUrlToName(resource.from);
+        if (!from) {
+          this.WARN("Received a message without a parseable from field: " +
+                    resource.from);
+          return;
+        }
+
+        // TODO Handle composetime field?
+        let conversationLink = resource.conversationLink;
+
+        // Check if the conversation is a chat.
+        if (conversationLink.indexOf("/19:") != -1) {
+          // TODO
+          this.WARN("Received message from MUC.");
+          continue;
+        }
+
+        // Get or create the conversation.
+        let conversationName = contactUrlToName(conversationLink);
+        let conv = this._conversations.get(conversationName);
+
+        let messageTypeParts = messageType.split("/");
+        // Set the typing state (if the conversation exists).
+        if (messageTypeParts[0] == "Control" && conv) {
+          let typingState = null;
+          // NotTyping or Typing.
+          if (messageTypeParts[1] == "Typing")
+            typingState = Ci.prplIConvIM.TYPING;
+          else if (messageTypeParts[1] == "ClearTyping")
+            typingState = Ci.prplIConvIM.NOT_TYPING;
+          if (typingState !== null)
+            conv.updateTyping(typingState);
+          // TODO There doesn't seem to be a "typed" state.
+        } else if (messageType == "RichText" || messageType == "Text") {
+          // Create a conversation if it doesn't exist.
+          if (!conv)
+            conv = this.createConversation(conversationName);
+
+          // TODO Handle RichText vs. Text.
+
+          // Put the message into the conversation.
+          let options = {};
+          if (from == this.name)
+            options.outgoing = true;
+          else
+            options.incoming = true;
+          conv.writeMessage(from, resource.content, options);
+        }
+      } else if (resourceType == "UserPresence") {
+        // Ignore our own statuses.
+        let from = contactUrlToName(resource.selfLink);
+        if (!from)
+          continue;
+
+        // Get the buddy and update the status.
+        let buddy = this._buddies.get(from);
+        if (buddy)
+          buddy.setStatus(this.mapStatusString(resource.status), "");
+      } else if (resourceType == "EndpointPresence") {
+        // Nothing to do.
+      } else if (resourceType == "ConversationUpdate") {
+        // Nothing to do.
+      } else if (resourceType == "ThreadUpdate") {
+        // Nothing to do.
+      } else {
+        this.WARN("Unhandled resource type: " + resourceType);
+      }
+    }
+  },
+
+  /*
+   * Make a request to the Skype contacts API, this is essentially just
+   * httpRequest, but auto-adds a bunch of headers that are necessary.
+   */
+  _contactsRequest: function(aUrl, aOptions = {}) {
+    let headers = aOptions.headers || [];
+
+    // Add some special Skype headers.
+    headers = headers.concat([
+      ["X-Skypetoken", this._skypeToken],
+      ["X-Stratus-Caller", "swx-skype.com"],
+      ["X-Stratus-Request", "abcd1234"],
+      ["Origin", "https://web.skype.com"],
+      ["Referer", "https://web.skype.com/main"],
+      ["Accept", "application/json; ver=1.0;"],
+    ]);
+
+    aOptions.headers = headers;
+
+    return httpRequest(aUrl, aOptions);
+  },
+
+  /*
+   * Make a request to the Skype messages API, this is essentially just
+   * httpRequest, but auto-adds a bunch of headers that are necessary.
+   */
+  _messagesRequest: function(aUrl, aOptions = {}) {
+    let headers = aOptions.headers || [];
+
+    // Add some special Skype headers.
+    headers = headers.concat([
+      ["RegistrationToken", this._registrationToken],
+      ["Referer", "https://web.skype.com/main"],
+      ["Accept", "application/json; ver=1.0;"],
+      ["ClientInfo", kClientInfo],
+    ]);
+
+    aOptions.headers = headers;
+
+    return httpRequest(aUrl, aOptions);
+  },
+
+  // Helper function to disconnect with authentication failed.
+  _disconnectWithAuthFailure: function(aMessageId="error.auth") {
+    this.reportDisconnecting(Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED,
+                             _(aMessageId));
+    this.reportDisconnected();
+  },
+
+  disconnect: function() {
+    if (this.disconnected || this.disconnecting)
+      return;
+
+    clearTimeout(this._poller);
+    if (this._request)
+      this._request.abort();
+
+    this._request = null;
+    this._poller = null;
+
+    // Mark all contacts on the account as having an unknown status.
+    this._buddies.forEach(function(aBuddy)
+      aBuddy.setStatus(Ci.imIStatusInfo.STATUS_UNKNOWN, ""));
+
+    this.reportDisconnected();
+  },
+
+  // TODO?
+  observe: function(aSubject, aTopic, aData) {},
+
+  remove: function() {
+    this._conversations.forEach(conv => conv.close());
+    delete this._conversations;
+    this.buddies.forEach(function(aBuddy) aBuddy.remove());
+    delete this.buddies;
+  },
+
+  unInit: function() {
+    delete this.imAccount;
+    clearTimeout(this._poller);
+  },
+
+  createConversation: function(aName) {
+    let conv = new SkypeConversation(this, aName);
+    this._conversations.set(aName, conv);
+    return conv;
+  },
+
+  // Called when the user adds or authorizes a new contact.
+  addBuddy: function(aTag, aName) {},
+
+  loadBuddy: function(aBuddy, aTag) {
+    let buddy = new SkypeAccountBuddy(this, aBuddy, aTag);
+    this._buddies.set(buddy.userName, buddy);
+
+    return buddy;
+  },
+
+  // TODO Add support for MUCs.
+  get canJoinChat() false,
+  chatRoomFields: {},
+  joinChat: function(aComponents) {}
+};
+
+function SkypeProtocol() {}
+SkypeProtocol.prototype = {
+  __proto__: GenericProtocolPrototype,
+  get name() "Skype",
+  get iconBaseURI() "chrome://prpl-skype/skin/",
+  get baseId() "prpl-skype",
+
+  get passwordOptional() false,
+
+  getAccount: function(aImAccount) new SkypeAccount(this, aImAccount),
+  classID: Components.ID("{8446c0f6-9f59-4710-844e-eaa6c1f49d35}")
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([SkypeProtocol]);
new file mode 100644
--- /dev/null
+++ b/chat/protocols/skype/skype.manifest
@@ -0,0 +1,3 @@
+component {8446c0f6-9f59-4710-844e-eaa6c1f49d35} skype.js
+contract @mozilla.org/chat/skype;1 {8446c0f6-9f59-4710-844e-eaa6c1f49d35}
+category im-protocol-plugin prpl-skype @mozilla.org/chat/skype;1
new file mode 100644
--- /dev/null
+++ b/chat/protocols/skype/test/test_MagicSha256.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+let skype = {};
+Services.scriptloader.loadSubScript("resource:///components/skype.js", skype);
+
+const data = {
+  "1416264993": "3a33ac47fe2ec1a33d569f4be5c69ddc",
+  "1416387358": "eca9716e1eedcbe93320ba794cea3388",
+  "1416392361": "2ed6fc80c3303caa137ae3fd4fcc7d80"
+};
+
+function run_test() {
+  add_test(test_MagicSha256);
+
+  run_next_test();
+}
+
+function test_MagicSha256() {
+  for (let input in data)
+    equal(data[input], skype.magicSha256(input));
+
+  run_next_test();
+}
new file mode 100644
--- /dev/null
+++ b/chat/protocols/skype/test/test_contactUrlToName.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+let skype = {};
+Services.scriptloader.loadSubScript("resource:///components/skype.js", skype);
+
+const data = {
+  "https://bay-client-s.gateway.messenger.live.com/v1/users/ME/contacts/8:clokep":
+    "clokep",
+  "https://bay-client-s.gateway.messenger.live.com/v1/users/8:clokep/presenceDocs/messagingService":
+    "clokep"
+};
+
+function run_test() {
+  add_test(test_contactUrlToName);
+
+  run_next_test();
+}
+
+function test_contactUrlToName() {
+  for (let input in data)
+    equal(data[input], skype.urlToName(input));
+
+  run_next_test();
+}
new file mode 100644
--- /dev/null
+++ b/chat/protocols/skype/test/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head =
+tail =
+
+[test_contactUrlToName.js]
+[test_MagicSha256.js]
--- a/im/installer/package-manifest.in
+++ b/im/installer/package-manifest.in
@@ -222,20 +222,22 @@
 @RESPATH@/components/imConversations.js
 @RESPATH@/components/imConversations.manifest
 @RESPATH@/components/imCore.js
 @RESPATH@/components/imCore.manifest
 @RESPATH@/components/facebook.js
 @RESPATH@/components/facebook.manifest
 @RESPATH@/components/gtalk.js
 @RESPATH@/components/gtalk.manifest
+@RESPATH@/components/irc.js
+@RESPATH@/components/irc.manifest
+@RESPATH@/components/skype.js
+@RESPATH@/components/skype.manifest
 @RESPATH@/components/twitter.js
 @RESPATH@/components/twitter.manifest
-@RESPATH@/components/irc.js
-@RESPATH@/components/irc.manifest
 @RESPATH@/components/xmpp.js
 @RESPATH@/components/xmpp.manifest
 @RESPATH@/components/yahoo.js
 @RESPATH@/components/yahoo.manifest
 @RESPATH@/components/odnoklassniki.js
 @RESPATH@/components/odnoklassniki.manifest
 @RESPATH@/components/smileProtocolHandler.js
 @RESPATH@/components/smileProtocolHandler.manifest
--- a/im/test/xpcshell.ini
+++ b/im/test/xpcshell.ini
@@ -1,9 +1,10 @@
 ; 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/.
 
 [include:chat/modules/test/xpcshell.ini]
 [include:chat/components/src/test/xpcshell.ini]
 [include:chat/protocols/irc/test/xpcshell.ini]
+[include:chat/protocols/skype/test/xpcshell.ini]
 [include:chat/protocols/yahoo/test/xpcshell.ini]
 #[include:extensions/purple/purplexpcom/src/test/xpcshell.ini]
--- a/mail/installer/package-manifest.in
+++ b/mail/installer/package-manifest.in
@@ -273,20 +273,22 @@
 @RESPATH@/components/imConversations.js
 @RESPATH@/components/imConversations.manifest
 @RESPATH@/components/imCore.js
 @RESPATH@/components/imCore.manifest
 @RESPATH@/components/facebook.js
 @RESPATH@/components/facebook.manifest
 @RESPATH@/components/gtalk.js
 @RESPATH@/components/gtalk.manifest
+@RESPATH@/components/irc.js
+@RESPATH@/components/irc.manifest
+@RESPATH@/components/skype.js
+@RESPATH@/components/skype.manifest
 @RESPATH@/components/twitter.js
 @RESPATH@/components/twitter.manifest
-@RESPATH@/components/irc.js
-@RESPATH@/components/irc.manifest
 @RESPATH@/components/xmpp.js
 @RESPATH@/components/xmpp.manifest
 @RESPATH@/components/yahoo.js
 @RESPATH@/components/yahoo.manifest
 @RESPATH@/components/smileProtocolHandler.js
 @RESPATH@/components/smileProtocolHandler.manifest
 @RESPATH@/components/logger.js
 @RESPATH@/components/logger.manifest