Bug 1213984 - Remove contacts code from Loop's backend. r=Standard8
authorManuel Casas <manuel.casasbarrado@gmail.com>
Fri, 06 Nov 2015 11:52:24 +0000
changeset 293145 adf257bef068a9efd0ce79d479c1f9d174eebe99
parent 293144 f103f2fdbe131ec3c4f5423588bafb0294d52bf3
child 293146 e28bbac94d69d5972750803b1a9ee347f7e9d73d
push id8824
push userraliiev@mozilla.com
push dateMon, 14 Dec 2015 20:18:56 +0000
treeherdermozilla-aurora@e2031358e2a6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1213984
milestone45.0a1
Bug 1213984 - Remove contacts code from Loop's backend. r=Standard8
browser/app/profile/firefox.js
browser/components/loop/.eslintrc-gecko
browser/components/loop/modules/CardDavImporter.jsm
browser/components/loop/modules/GoogleImporter.jsm
browser/components/loop/modules/LoopContacts.jsm
browser/components/loop/modules/MozLoopAPI.jsm
browser/components/loop/modules/MozLoopService.jsm
browser/components/loop/moz.build
browser/components/loop/test/mochitest/browser.ini
browser/components/loop/test/mochitest/browser_CardDavImporter.js
browser/components/loop/test/mochitest/browser_GoogleImporter.js
browser/components/loop/test/mochitest/browser_LoopContacts.js
browser/components/loop/test/mochitest/fixtures/google_auth.txt
browser/components/loop/test/mochitest/fixtures/google_contacts.txt
browser/components/loop/test/mochitest/fixtures/google_groups.txt
browser/components/loop/test/mochitest/fixtures/google_token.txt
browser/components/loop/test/mochitest/google_service.sjs
testing/profiles/prefs_general.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1407,18 +1407,16 @@ pref("loop.debug.twoWayMediaTelemetry", 
 pref("loop.feedback.dateLastSeenSec", 0);
 pref("loop.feedback.periodSec", 15770000); // 6 months.
 pref("loop.feedback.formURL", "https://www.mozilla.org/firefox/hello/npssurvey/");
 #ifdef DEBUG
 pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
 #else
 pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
 #endif
-pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
-pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
 pref("loop.fxa_oauth.tokendata", "");
 pref("loop.fxa_oauth.profile", "");
 pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc");
 pref("loop.browserSharing.showInfoBar", true);
 
 pref("social.sidebar.unload_timeout_ms", 10000);
 
 // Activation from inside of share panel is possible if activationPanelEnabled
--- a/browser/components/loop/.eslintrc-gecko
+++ b/browser/components/loop/.eslintrc-gecko
@@ -8,44 +8,40 @@
     "destructuring": true,
     "generators": true,
     "restParams": true,
     "spread": true,
     "objectLiteralShorthandMethods": true,
   },
   "globals": {
     // Gecko + Loop Globals.
-    "CardDavImporter": true,
     "Chat": false,
     "ChromeWorker": false,
     "CommonUtils": false,
     "Components": false,
     "convertToRTCStatsReport": false,
     "CustomizableUI": false,
     "deriveHawkCredentials": false,
     "eventEmitter": false,
     "FxAccountsOAuthClient": false,
     "FxAccountsProfileClient": false,
     "gBrowser": false,
     "gDNSService": false,
     "gLoopBundle": false,
-    "GoogleImporter": true,
     "gWM": false,
     "HawkClient": false,
     "injectLoopAPI": true,
     "Iterator": false,
     "Log": false,
     "log": true,
     "LOOP_SESSION_TYPE": true,
     "LoopCalls": true,
-    "LoopContacts": true,
     "loopCrypto": false,
     "LoopRooms": true,
     "LoopRoomsCache": true,
-    "LoopStorage": true,
     "MozLoopPushHandler": true,
     "MozLoopService": true,
     "OS": false,
     "roomsPushNotification": true,
     "Services": false,
     "Social": false,
     "SocialShare": false,
     "Task": false,
deleted file mode 100644
--- a/browser/components/loop/modules/CardDavImporter.jsm
+++ /dev/null
@@ -1,463 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Promise.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/Log.jsm");
-
-this.EXPORTED_SYMBOLS = ["CardDavImporter"];
-
-var log = Log.repository.getLogger("Loop.Importer.CardDAV");
-log.level = Log.Level.Debug;
-log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
-
-const DEPTH_RESOURCE_ONLY = "0";
-const DEPTH_RESOURCE_AND_CHILDREN = "1";
-const DEPTH_RESOURCE_AND_ALL_DESCENDENTS = "infinity";
-
-this.CardDavImporter = function() {
-};
-
-/**
- * CardDAV Address Book importer for Loop.
- *
- * The model for address book importers is to have a single public method,
- * "startImport." When the import is done (or upon a fatal error), the
- * caller's callback method is called.
- *
- * The current model for this importer is based on the subset of CardDAV
- * implemented by Google. In theory, it should work with other CardDAV
- * sources, but it has only been tested against Google at the moment.
- *
- * At the moment, this importer assumes that no local changes will be made
- * to data retreived from a remote source: when performing a re-import,
- * any records that have been previously imported will be completely
- * removed and replaced with the data received from the CardDAV server.
- * Witout this behavior, it would be impossible for users to take any
- * actions to remove fields that are no longer valid.
- */
-
-this.CardDavImporter.prototype = {
-  /**
-   * Begin import of an address book from a CardDAV server.
-   *
-   * @param {Object}   options  Information needed to perform the address
-   *                            book import. The following fields are currently
-   *                            defined:
-   *                              - "host": CardDAV server base address
-   *                                (e.g., "google.com")
-   *                              - "auth": Authentication mechanism to use.
-   *                                Currently, only "basic" is implemented.
-   *                              - "user": Username to use for basic auth
-   *                              - "password": Password to use for basic auth
-   * @param {Function} callback Callback function that will be invoked once the
-   *                            import operation is complete. The first argument
-   *                            passed to the callback will be an 'Error' object
-   *                            or 'null'. If the import operation was
-   *                            successful, then the second parameter will be a
-   *                            count of the number of contacts that were
-   *                            successfully imported.
-   * @param {Object}   db       Database to add imported contacts into.
-   *                            Nominally, this is the LoopContacts API. In
-   *                            practice, anything with the same interface
-   *                            should work here.
-   */
-
-  startImport: function(options, callback, db) {
-    let auth;
-    if (!("auth" in options)) {
-      callback(new Error("No authentication specified"));
-      return;
-    }
-
-    if (options.auth === "basic") {
-      if (!("user" in options) || !("password" in options)) {
-        callback(new Error("Missing user or password for basic authentication"));
-        return;
-      }
-      auth = { method: "basic",
-               user: options.user,
-               password: options.password };
-    } else {
-      callback(new Error("Unknown authentication method"));
-      return;
-    }
-
-    if (!("host" in options)) {
-      callback(new Error("Missing host for CardDav import"));
-      return;
-    }
-    let host = options.host;
-
-    Task.spawn(function* () {
-      log.info("Starting CardDAV import from " + host);
-      let baseURL = "https://" + host;
-      let startURL = baseURL + "/.well-known/carddav";
-      let abookURL;
-
-      // Get list of contact URLs
-      let body = "<d:propfind xmlns:d='DAV:'><d:prop><d:getetag />" +
-                 "</d:prop></d:propfind>";
-      let abook = yield this._davPromise("PROPFIND", startURL, auth,
-                                         DEPTH_RESOURCE_AND_CHILDREN, body);
-
-      // Build multiget REPORT body from URLs in PROPFIND result
-      let contactElements = abook.responseXML.getElementsByTagNameNS(
-                            "DAV:", "href");
-
-      body = "<c:addressbook-multiget xmlns:d='DAV:' " +
-             "xmlns:c='urn:ietf:params:xml:ns:carddav'>" +
-             "<d:prop><d:getetag /> <c:address-data /></d:prop>\n";
-
-      for (let element of contactElements) {
-        let href = element.textContent;
-        if (href.substr(-1) == "/") {
-          abookURL = baseURL + href;
-        } else {
-          body += "<d:href>" + href + "</d:href>\n";
-        }
-      }
-      body += "</c:addressbook-multiget>";
-
-      // Retreive contact URL contents
-      let allEntries = yield this._davPromise("REPORT", abookURL, auth,
-                                              DEPTH_RESOURCE_AND_CHILDREN,
-                                              body);
-
-      // Parse multiget entites and add to DB
-      let addressData = allEntries.responseXML.getElementsByTagNameNS(
-        "urn:ietf:params:xml:ns:carddav", "address-data");
-
-      log.info("Retreived " + addressData.length + " contacts from " +
-                   host + "; importing into database");
-
-      let importCount = 0;
-      for (let i = 0; i < addressData.length; i++) {
-        let vcard = addressData.item(i).textContent;
-        let contact = this._convertVcard(vcard);
-        contact.id += "@" + host;
-        contact.category = ["carddav@" + host];
-
-        let existing = yield this._dbPromise(db, "getByServiceId", contact.id);
-        if (existing) {
-          yield this._dbPromise(db, "remove", existing._guid);
-        }
-
-        // If the contact contains neither email nor phone number, then it
-        // is not useful in the Loop address book: do not add.
-        if (!("tel" in contact) && !("email" in contact)) {
-          continue;
-        }
-
-        yield this._dbPromise(db, "add", contact);
-        importCount++;
-      }
-
-      return importCount;
-    }.bind(this)).then(
-      (result) => {
-        log.info("Import complete: " + result + " contacts imported.");
-        callback(null, result);
-      },
-      (error) => {
-        log.error("Aborting import: " + error.fileName + ":" +
-                      error.lineNumber + ": " + error.message);
-        callback(error);
-    }).then(null,
-      (error) => {
-        log.error("Error in callback: " + error.fileName +
-                      ":" + error.lineNumber + ": " + error.message);
-      callback(error);
-    }).then(null,
-      (error) => {
-        log.error("Error calling failure callback, giving up: " +
-                      error.fileName + ":" + error.lineNumber + ": " +
-                      error.message);
-    });
-  },
-
-  /**
-   * Wrap a LoopContacts-style operation in a promise. The operation is run
-   * immediately, and a corresponding Promise is returned. Error callbacks
-   * cause the promise to be rejected, and success cause it to be resolved.
-   *
-   * @param {Object} db     Object the operation is to be performed on
-   * @param {String} method Name of operation being wrapped
-   * @param {Object} param  Parameter to be passed to the operation
-   *
-   * @return {Object} Promise corresponding to the result of the operation.
-   */
-
-  _dbPromise: function(db, method, param) {
-    return new Promise((resolve, reject) => {
-      db[method](param, (error, result) => {
-        if (error) {
-          reject(error);
-        } else {
-          resolve(result);
-        }
-      });
-    });
-  },
-
-  /**
-   * Converts a contact in VCard format (see RFC 6350) to the format used
-   * by the LoopContacts class.
-   *
-   * @param {String} vcard  The contact to convert, in vcard format
-   * @return {Object}  a LoopContacts-style contact object containing
-   *                   the relevant fields from the vcard.
-   */
-
-  _convertVcard: function(vcard) {
-    let contact = {};
-    let nickname;
-    vcard.split(/[\r\n]+(?! )/).forEach(
-      function(contentline) {
-        contentline = contentline.replace(/[\r\n]+ /g, "");
-        let match = /^(.*?[^\\]):(.*)$/.exec(contentline);
-        if (match) {
-          let nameparam = match[1];
-          let value = match[2];
-
-          // Poor-man's unescaping
-          value = value.replace(/\\:/g, ":");
-          value = value.replace(/\\,/g, ",");
-          value = value.replace(/\\n/gi, "\n");
-          value = value.replace(/\\\\/g, "\\");
-
-          let param = nameparam.split(/;/);
-          let name = param[0];
-          let pref = false;
-          let type = [];
-
-          for (let i = 1; i < param.length; i++) {
-            if (/^PREF/.exec(param[i]) || /^TYPE=PREF/.exec(param[i])) {
-              pref = true;
-            }
-            let typeMatch = /^TYPE=(.*)/.exec(param[i]);
-            if (typeMatch) {
-              type.push(typeMatch[1].toLowerCase());
-            }
-          }
-
-          if (!type.length) {
-            type.push("other");
-          }
-
-          if (name === "FN") {
-            value = value.replace(/\\;/g, ";");
-            contact.name = [value];
-          }
-
-          if (name === "N") {
-            // Because we don't have lookbehinds, matching unescaped
-            // semicolons is a pain. Luckily, we know that \r and \n
-            // cannot appear in the strings, so we use them to swap
-            // unescaped semicolons for \n.
-            value = value.replace(/\\;/g, "\r");
-            value = value.replace(/;/g, "\n");
-            value = value.replace(/\r/g, ";");
-
-            let family, given, additional, prefix, suffix;
-            let values = value.split(/\n/);
-            if (values.length >= 5) {
-              [family, given, additional, prefix, suffix] = values;
-              if (prefix.length) {
-                contact.honorificPrefix = [prefix];
-              }
-              if (given.length) {
-                contact.givenName = [given];
-              }
-              if (additional.length) {
-                contact.additionalName = [additional];
-              }
-              if (family.length) {
-                contact.familyName = [family];
-              }
-              if (suffix.length) {
-                contact.honorificSuffix = [suffix];
-              }
-            }
-          }
-
-          if (name === "EMAIL") {
-            value = value.replace(/\\;/g, ";");
-            if (!("email" in contact)) {
-              contact.email = [];
-            }
-            contact.email.push({
-              pref: pref,
-              type: type,
-              value: value
-            });
-          }
-
-          if (name === "NICKNAME") {
-            value = value.replace(/\\;/g, ";");
-            // We don't store nickname in contact because it's not
-            // a supported field. We're saving it off here in case we
-            // need to use it if the fullname is blank.
-            nickname = value;
-          }
-
-          if (name === "ADR") {
-            value = value.replace(/\\;/g, "\r");
-            value = value.replace(/;/g, "\n");
-            value = value.replace(/\r/g, ";");
-            let pobox, extra, street, locality, region, code, country;
-            let values = value.split(/\n/);
-            if (values.length >= 7) {
-              [pobox, extra, street, locality, region, code, country] = values;
-              if (!("adr" in contact)) {
-                contact.adr = [];
-              }
-              contact.adr.push({
-                pref: pref,
-                type: type,
-                streetAddress: (street || pobox) + (extra ? (" " + extra) : ""),
-                locality: locality,
-                region: region,
-                postalCode: code,
-                countryName: country
-              });
-            }
-          }
-
-          if (name === "TEL") {
-            value = value.replace(/\\;/g, ";");
-            if (!("tel" in contact)) {
-              contact.tel = [];
-            }
-            contact.tel.push({
-              pref: pref,
-              type: type,
-              value: value
-            });
-          }
-
-          if (name === "ORG") {
-            value = value.replace(/\\;/g, "\r");
-            value = value.replace(/;/g, "\n");
-            value = value.replace(/\r/g, ";");
-            if (!("org" in contact)) {
-              contact.org = [];
-            }
-            contact.org.push(value.replace(/\n.*/, ""));
-          }
-
-          if (name === "TITLE") {
-            value = value.replace(/\\;/g, ";");
-            if (!("jobTitle" in contact)) {
-              contact.jobTitle = [];
-            }
-            contact.jobTitle.push(value);
-          }
-
-          if (name === "BDAY") {
-            value = value.replace(/\\;/g, ";");
-            contact.bday = Date.parse(value);
-          }
-
-          if (name === "UID") {
-            contact.id = value;
-          }
-
-          if (name === "NOTE") {
-            value = value.replace(/\\;/g, ";");
-            if (!("note" in contact)) {
-              contact.note = [];
-            }
-            contact.note.push(value);
-          }
-
-        }
-      }
-    );
-
-    // Basic sanity checking: make sure the name field isn't empty
-    if (!("name" in contact) || contact.name[0].length == 0) {
-      if (("familyName" in contact) && ("givenName" in contact)) {
-        // First, try to synthesize a full name from the name fields.
-        // Ordering is culturally sensitive, but we don't have
-        // cultural origin information available here. The best we
-        // can really do is "family, given additional"
-        contact.name = [contact.familyName[0] + ", " + contact.givenName[0]];
-        if (("additionalName" in contact)) {
-          contact.name[0] += " " + contact.additionalName[0];
-        }
-      } else {
-        if (nickname) {
-          contact.name = [nickname];
-        } else if ("familyName" in contact) {
-          contact.name = [contact.familyName[0]];
-        } else if ("givenName" in contact) {
-          contact.name = [contact.givenName[0]];
-        } else if ("org" in contact) {
-          contact.name = [contact.org[0]];
-        } else if ("email" in contact) {
-          contact.name = [contact.email[0].value];
-        } else if ("tel" in contact) {
-          contact.name = [contact.tel[0].value];
-        }
-      }
-    }
-
-    return contact;
-  },
-
-  /**
-   * Issues a CardDAV request (see RFC 6352) and returns a Promise to represent
-   * the success or failure state of the request.
-   *
-   * @param {String} method WebDAV method to use (e.g., "PROPFIND")
-   * @param {String} url    HTTP URL to use for the request
-   * @param {Object} auth   Object with authentication-related configuration.
-   *                        See documentation for startImport for details.
-   * @param {Number} depth  Value to use for the WebDAV (HTTP) "Depth" header
-   * @param {String} body   Body to include in the WebDAV (HTTP) request
-   *
-   * @return {Object} Promise representing the request operation outcome.
-   *                  If resolved, the resolution value is the XMLHttpRequest
-   *                  that was used to perform the request.
-   */
-  _davPromise: function(method, url, auth, depth, body) {
-    return new Promise((resolve, reject) => {
-      let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(
-                Ci.nsIXMLHttpRequest);
-      let user = "";
-      let password = "";
-
-      if (auth.method == "basic") {
-        user = auth.user;
-        password = auth.password;
-      }
-
-      req.open(method, url, true, user, password);
-
-      req.setRequestHeader("Depth", depth);
-      req.setRequestHeader("Content-Type", "application/xml; charset=utf-8");
-
-      req.onload = function() {
-        if (req.status < 400) {
-          resolve(req);
-        } else {
-          reject(new Error(req.status + " " + req.statusText));
-        }
-      };
-
-      req.onerror = function(error) {
-        reject(error);
-      };
-
-      req.send(body);
-    });
-  }
-};
deleted file mode 100644
--- a/browser/components/loop/modules/GoogleImporter.jsm
+++ /dev/null
@@ -1,603 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Timer.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
-                                  "resource://gre/modules/Promise.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
-                                  "resource://gre/modules/Task.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Log",
-                                  "resource://gre/modules/Log.jsm");
-
-this.EXPORTED_SYMBOLS = ["GoogleImporter"];
-
-var log = Log.repository.getLogger("Loop.Importer.Google");
-log.level = Log.Level.Debug;
-log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
-
-/**
- * Helper function that reads and maps the respective node value from specific
- * XML DOMNodes to fields on a `target` object.
- * Example: the value for field 'fullName' can be read from the XML DOMNode
- *          'name', so that's the mapping we need to make; get the nodeValue of
- *          the node called 'name' and tack it to the target objects' 'fullName'
- *          property.
- *
- * @param {Map}        fieldMap    Map object containing the field name -> node
- *                                 name mapping
- * @param {XMLDOMNode} node        DOM node to fetch the values from for each field
- * @param {String}     ns XML      namespace for the DOM nodes to retrieve. Optional.
- * @param {Object}     target      Object to store the values found. Optional.
- *                                 Defaults to a new object.
- * @param {Boolean}    wrapInArray Indicates whether to map the field values in
- *                                 an Array. Optional. Defaults to `false`.
- * @returns The `target` object with the node values mapped to the appropriate fields.
- */
-const extractFieldsFromNode = function(fieldMap, node, ns = null, target = {}, wrapInArray = false) {
-  for (let [field, nodeName] of fieldMap) {
-    let nodeList = ns ? node.getElementsByTagNameNS(ns, nodeName) :
-                        node.getElementsByTagName(nodeName);
-    if (nodeList.length) {
-      if (!nodeList[0].firstChild) {
-        continue;
-      }
-      let value = nodeList[0].textContent;
-      target[field] = wrapInArray ? [value] : value;
-    }
-  }
-  return target;
-};
-
-/**
- * Helper function that reads the type of (email-)address or phone number from an
- * XMLDOMNode.
- *
- * @param {XMLDOMNode} node
- * @returns String that depicts the type of field value.
- */
-const getFieldType = function(node) {
-  if (node.hasAttribute("rel")) {
-    let rel = node.getAttribute("rel");
-    // The 'rel' attribute is formatted like: http://schemas.google.com/g/2005#work.
-    return rel.substr(rel.lastIndexOf("#") + 1);
-  }
-  if (node.hasAttribute("label")) {
-    return node.getAttribute("label");
-  }
-  return "other";
-};
-
-/**
- * Fetch the preferred entry of a contact. Returns the first entry when no
- * preferred flag is set.
- *
- * @param {Object} contact The contact object to check for preferred entries
- * @param {String} which   Type of entry to check. Optional, defaults to 'email'
- * @throws An Error when no (preferred) entries are listed for this contact.
- */
-const getPreferred = function(contact, which = "email") {
-  if (!(which in contact) || !contact[which].length) {
-    throw new Error("No " + which + " entry available.");
-  }
-  let preferred = contact[which][0];
-  contact[which].some(function(entry) {
-    if (entry.pref) {
-      preferred = entry;
-      return true;
-    }
-    return false;
-  });
-  return preferred;
-};
-
-/**
- * Fetch an auth token (clientID or client secret), which may be overridden by
- * a pref if it's set.
- *
- * @param {String}  paramValue Initial, default, value of the parameter
- * @param {String}  prefName   Fully qualified name of the pref to check for
- * @param {Boolean} encode     Whether to URLEncode the param string
- */
-const getUrlParam = function(paramValue, prefName, encode = true) {
-  if (Services.prefs.getPrefType(prefName)) {
-    paramValue = Services.prefs.getCharPref(prefName);
-  }
-  paramValue = Services.urlFormatter.formatURL(paramValue);
-
-  return encode ? encodeURIComponent(paramValue) : paramValue;
-};
-
-var gAuthWindow, gProfileId;
-const kAuthWindowSize = {
-  width: 420,
-  height: 460
-};
-const kContactsMaxResults = 10000000;
-const kContactsChunkSize = 100;
-const kTitlebarPollTimeout = 200;
-const kNS_GD = "http://schemas.google.com/g/2005";
-
-/**
- * GoogleImporter class.
- *
- * Main entrypoint is the `startImport` method which calls several tasks necessary
- * to import contacts from Google.
- * Authentication is performed using an OAuth strategy which is loaded in a popup
- * window.
- */
-this.GoogleImporter = function() {};
-
-this.GoogleImporter.prototype = {
-  /**
-   * Start the import process of contacts from the Google service, using its Contacts
-   * API - https://developers.google.com/google-apps/contacts/v3/.
-   * The import consists of four tasks:
-   * 1. Get the authentication code which can be used to retrieve an OAuth token
-   *    pair. This is the bulk of the authentication flow that will be handled in
-   *    a popup window by Google. The user will need to login to the Google service
-   *    with his or her account and grant permission to our app to manage their
-   *    contacts.
-   * 2. Get the tokenset from the Google service, using the authentication code
-   *    that was retrieved in task 1.
-   * 3. Fetch all the contacts from the Google service, using the OAuth tokenset
-   *    that was retrieved in task 2.
-   * 4. Process the contacts, map them to the MozContact format and store each
-   *    contact in the database, if it doesn't exist yet.
-   *
-   * @param {Object}       options   Options to control the behavior of the import.
-   *                                 Not used by this importer class.
-   * @param {Function}     callback  Function to invoke when the import process
-   *                                 is done or when an error occurs that halts
-   *                                 the import process. The first argument passed
-   *                                 in an Error object or `null` and the second
-   *                                 argument is an object with import statistics.
-   * @param {LoopContacts} db        Instance of the LoopContacts database object,
-   *                                 which will store the newly found contacts
-   * @param {nsIDomWindow} windowRef Reference to the ChromeWindow the import is
-   *                                 invoked from. It will be used to be able to
-   *                                 open a window for the OAuth process with chrome
-   *                                 privileges.
-   */
-  startImport: function(options, callback, db, windowRef) {
-    Task.spawn(function* () {
-      let code = yield this._promiseAuthCode(windowRef);
-      let tokenSet = yield this._promiseTokenSet(code);
-      let contactEntries = yield this._getContactEntries(tokenSet);
-      let { total, success, ids } = yield this._processContacts(contactEntries, db, tokenSet);
-      yield this._purgeContacts(ids, db);
-
-      return {
-        total: total,
-        success: success
-      };
-    }.bind(this)).then(stats => callback(null, stats),
-                       error => callback(error))
-                 .then(null, ex => log.error(ex.fileName + ":" + ex.lineNumber + ": " + ex.message));
-  },
-
-  /**
-   * Task that yields an authentication code that is returned after the user signs
-   * in to the Google service. This code can be used by this class to retrieve an
-   * OAuth tokenset.
-   *
-   * @param {nsIDOMWindow} windowRef Reference to the ChromeWindow the import is
-   *                                 invoked from. It will be used to be able to
-   *                                 open a window for the OAuth process with chrome
-   *                                 privileges.
-   * @throws An `Error` object when authentication fails, or the authentication
-   *         code as a String.
-   */
-  _promiseAuthCode: Task.async(function* (windowRef) {
-    // Close a window that got lost in a previous login attempt.
-    if (gAuthWindow && !gAuthWindow.closed) {
-      gAuthWindow.close();
-      gAuthWindow = null;
-    }
-
-    let url = getUrlParam("https://accounts.google.com/o/oauth2/",
-                          "loop.oauth.google.URL", false) +
-              "auth?response_type=code&client_id=" +
-              getUrlParam("%GOOGLE_OAUTH_API_CLIENTID%", "loop.oauth.google.clientIdOverride");
-    for (let param of ["redirect_uri", "scope"]) {
-      url += "&" + param + "=" + encodeURIComponent(
-             Services.prefs.getCharPref("loop.oauth.google." + param));
-    }
-    const features = "centerscreen,resizable=yes,toolbar=no,menubar=no,status=no,directories=no," +
-                     "width=" + kAuthWindowSize.width + ",height=" + kAuthWindowSize.height;
-    gAuthWindow = windowRef.openDialog(windowRef.getBrowserURL(), "_blank", features, url);
-    gAuthWindow.focus();
-
-    let code;
-
-    function promiseTimeOut() {
-      return new Promise(resolve => {
-        setTimeout(resolve, kTitlebarPollTimeout);
-      });
-    }
-
-    // The following loops runs as long as the OAuth windows' titlebar doesn't
-    // yield a response from the Google service. If an error occurs, the loop
-    // will terminate early.
-    while (!code) {
-      if (!gAuthWindow || gAuthWindow.closed) {
-        throw new Error("Popup window was closed before authentication succeeded");
-      }
-
-      let matches = gAuthWindow.document.title.match(/(error|code)=([^\s]+)/);
-      if (matches && matches.length) {
-        let [, type, message] = matches;
-        gAuthWindow.close();
-        gAuthWindow = null;
-        if (type == "error") {
-          throw new Error("Google authentication failed with error: " + message.trim());
-        } else if (type == "code") {
-          code = message.trim();
-        } else {
-          throw new Error("Unknown response from Google");
-        }
-      } else {
-        yield promiseTimeOut();
-      }
-    }
-
-    return code;
-  }),
-
-  /**
-   * Fetch an OAuth tokenset, that will be used to authenticate Google API calls,
-   * using the authentication token retrieved in `_promiseAuthCode`.
-   *
-   * @param {String} code The authentication code.
-   * @returns an `Error` object upon failure or an object containing OAuth tokens.
-   */
-  _promiseTokenSet: function(code) {
-    return new Promise(function(resolve, reject) {
-      let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
-                      .createInstance(Ci.nsIXMLHttpRequest);
-
-      request.open("POST", getUrlParam("https://accounts.google.com/o/oauth2/",
-                                       "loop.oauth.google.URL",
-                                       false) + "token");
-
-      request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
-
-      request.onload = function() {
-        if (request.status < 400) {
-          let tokenSet = JSON.parse(request.responseText);
-          tokenSet.date = Date.now();
-          resolve(tokenSet);
-        } else {
-          reject(new Error(request.status + " " + request.statusText));
-        }
-      };
-
-      request.onerror = function(error) {
-        reject(error);
-      };
-
-      let body = "grant_type=authorization_code&code=" + encodeURIComponent(code) +
-                 "&client_id=" + getUrlParam("%GOOGLE_OAUTH_API_CLIENTID%",
-                                             "loop.oauth.google.clientIdOverride") +
-                 "&client_secret=" + getUrlParam("%GOOGLE_OAUTH_API_KEY%",
-                                                 "loop.oauth.google.clientSecretOverride") +
-                 "&redirect_uri=" + encodeURIComponent(Services.prefs.getCharPref(
-                                                       "loop.oauth.google.redirect_uri"));
-
-      request.send(body);
-    });
-  },
-
-  _promiseRequestXML: function(URL, tokenSet) {
-    return new Promise((resolve, reject) => {
-      let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
-                      .createInstance(Ci.nsIXMLHttpRequest);
-
-      request.open("GET", URL);
-
-      request.setRequestHeader("Content-Type", "application/xml; charset=utf-8");
-      request.setRequestHeader("GData-Version", "3.0");
-      request.setRequestHeader("Authorization", "Bearer " + tokenSet.access_token);
-
-      request.onload = function() {
-        if (request.status < 400) {
-          let doc = request.responseXML;
-          // First get the profile id, which is present in each XML request.
-          let currNode = doc.documentElement.firstChild;
-          while (currNode) {
-            if (currNode.nodeType == 1 && currNode.localName == "id") {
-              gProfileId = currNode.textContent;
-              break;
-            }
-            currNode = currNode.nextSibling;
-          }
-
-          resolve(doc);
-        } else {
-          reject(new Error(request.status + " " + request.statusText));
-        }
-      };
-
-      request.onerror = function(error) {
-        reject(error);
-      };
-
-      request.send();
-    });
-  },
-
-  /**
-   * Fetches all the contacts in a users' address book.
-   *
-   * @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contacts
-   *
-   * @param {Object} tokenSet OAuth tokenset used to authenticate the request
-   * @returns An `Error` object upon failure or an Array of contact XML nodes.
-   */
-  _getContactEntries: Task.async(function* (tokenSet) {
-    let URL = getUrlParam("https://www.google.com/m8/feeds/contacts/default/full",
-                          "loop.oauth.google.getContactsURL",
-                          false) + "?max-results=" + kContactsMaxResults;
-    let xmlDoc = yield this._promiseRequestXML(URL, tokenSet);
-    // Then kick of the importing of contact entries.
-    return Array.prototype.slice.call(xmlDoc.querySelectorAll("entry"));
-  }),
-
-  /**
-   * Fetches the default group from a users' address book, called 'Contacts'.
-   *
-   * @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contact_groups
-   *
-   * @param {Object} tokenSet OAuth tokenset used to authenticate the request
-   * @returns An `Error` object upon failure or the String group ID.
-   */
-  _getContactsGroupId: Task.async(function* (tokenSet) {
-    let URL = getUrlParam("https://www.google.com/m8/feeds/groups/default/full",
-                          "loop.oauth.google.getGroupsURL",
-                          false) + "?max-results=" + kContactsMaxResults;
-    let xmlDoc = yield this._promiseRequestXML(URL, tokenSet);
-    let contactsEntry = xmlDoc.querySelector("systemGroup[id=\"Contacts\"]");
-    if (!contactsEntry) {
-      throw new Error("Contacts group not present");
-    }
-    // Select the actual <entry> node, which is the parent of the <systemGroup>
-    // node we just selected.
-    contactsEntry = contactsEntry.parentNode;
-    return contactsEntry.getElementsByTagName("id")[0].textContent;
-  }),
-
-  /**
-   * Process the contact XML nodes that Google provides, convert them to the MozContact
-   * format, check if the contact already exists in the database and when it doesn't,
-   * store it permanently.
-   * During this process statistics are collected about the amount of successful
-   * imports. The consumer of this class may use these statistics to inform the
-   * user.
-   * Note: only contacts that are part of the 'Contacts' system group will be
-   *       imported.
-   *
-   * @param {Array}        contactEntries List of XML DOMNodes contact entries.
-   * @param {LoopContacts} db             Instance of the LoopContacts database
-   *                                      object, which will store the newly found
-   *                                      contacts.
-   * @param {Object}       tokenSet       OAuth tokenset used to authenticate a
-   *                                      request
-   * @returns An `Error` object upon failure or an Object with statistics in the
-   *          following format: `{ total: 25, success: 13, ids: {} }`.
-   */
-  _processContacts: Task.async(function* (contactEntries, db, tokenSet) {
-    let stats = {
-      total: contactEntries.length,
-      success: 0,
-      ids: {}
-    };
-
-    // Contacts that are _not_ part of the 'Contacts' group will be ignored.
-    let contactsGroupId = yield this._getContactsGroupId(tokenSet);
-
-    for (let entry of contactEntries) {
-      let contact = this._processContactFields(entry);
-
-      stats.ids[contact.id] = 1;
-      let existing = yield db.promise("getByServiceId", contact.id);
-      if (existing) {
-        yield db.promise("remove", existing._guid);
-      }
-
-      // After contact removal, check if the entry is part of the correct group.
-      if (!entry.querySelector("groupMembershipInfo[deleted=\"false\"][href=\"" +
-                               contactsGroupId + "\"]")) {
-        continue;
-      }
-
-      // If the contact contains neither email nor phone number, then it is not
-      // useful in the Loop address book: do not add.
-      if (!("email" in contact) && !("tel" in contact)) {
-        continue;
-      }
-
-      yield db.promise("add", contact);
-      stats.success++;
-    }
-
-    return stats;
-  }),
-
-  /**
-   * Parse an XML node to map the appropriate data to MozContact field equivalents.
-   *
-   * @param {XMLDOMNode} entry The contact XML node in Google format to process.
-   * @returns `null` if the contact entry appears to be invalid or an Object containing
-   *          all the contact data found in the XML.
-   */
-  _processContactFields: function(entry) {
-    // Basic fields in the main 'atom' namespace.
-    let contact = extractFieldsFromNode(new Map([
-      ["id", "id"],
-      // published: n/a
-      ["updated", "updated"]
-      // bday: n/a
-    ]), entry);
-
-    // Fields that need to wrapped in an Array.
-    extractFieldsFromNode(new Map([
-      ["name", "fullName"],
-      ["givenName", "givenName"],
-      ["familyName", "familyName"],
-      ["additionalName", "additionalName"]
-    ]), entry, kNS_GD, contact, true);
-
-    // The 'note' field needs to wrapped in an array, but its source node is not
-    // namespaced.
-    extractFieldsFromNode(new Map([
-      ["note", "content"]
-    ]), entry, null, contact, true);
-
-    // Process physical, earthly addresses.
-    let addressNodes = entry.getElementsByTagNameNS(kNS_GD, "structuredPostalAddress");
-    if (addressNodes.length) {
-      contact.adr = [];
-      for (let [, addressNode] of Iterator(addressNodes)) {
-        let adr = extractFieldsFromNode(new Map([
-          ["countryName", "country"],
-          ["locality", "city"],
-          ["postalCode", "postcode"],
-          ["region", "region"],
-          ["streetAddress", "street"]
-        ]), addressNode, kNS_GD);
-        if (Object.keys(adr).length) {
-          adr.pref = (addressNode.getAttribute("primary") == "true");
-          adr.type = [getFieldType(addressNode)];
-          contact.adr.push(adr);
-        }
-      }
-    }
-
-    // Process email addresses.
-    let emailNodes = entry.getElementsByTagNameNS(kNS_GD, "email");
-    if (emailNodes.length) {
-      contact.email = [];
-      for (let [, emailNode] of Iterator(emailNodes)) {
-        contact.email.push({
-          pref: (emailNode.getAttribute("primary") == "true"),
-          type: [getFieldType(emailNode)],
-          value: emailNode.getAttribute("address")
-        });
-      }
-    }
-
-    // Process telephone numbers.
-    let phoneNodes = entry.getElementsByTagNameNS(kNS_GD, "phoneNumber");
-    if (phoneNodes.length) {
-      contact.tel = [];
-      for (let [, phoneNode] of Iterator(phoneNodes)) {
-        let phoneNumber = phoneNode.hasAttribute("uri") ?
-          phoneNode.getAttribute("uri").replace("tel:", "") :
-          phoneNode.textContent;
-        contact.tel.push({
-          pref: (phoneNode.getAttribute("primary") == "true"),
-          type: [getFieldType(phoneNode)],
-          value: phoneNumber
-        });
-      }
-    }
-
-    let orgNodes = entry.getElementsByTagNameNS(kNS_GD, "organization");
-    if (orgNodes.length) {
-      contact.org = [];
-      contact.jobTitle = [];
-      for (let [, orgNode] of Iterator(orgNodes)) {
-        let orgElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgName")[0];
-        let titleElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgTitle")[0];
-        contact.org.push(orgElement ? orgElement.textContent : "");
-        contact.jobTitle.push(titleElement ? titleElement.textContent : "");
-      }
-    }
-
-    contact.category = ["google"];
-
-    // Basic sanity checking: make sure the name field isn't empty
-    if (!("name" in contact) || contact.name[0].length == 0) {
-      if (("familyName" in contact) && ("givenName" in contact)) {
-        // First, try to synthesize a full name from the name fields.
-        // Ordering is culturally sensitive, but we don't have
-        // cultural origin information available here. The best we
-        // can really do is "family, given additional"
-        contact.name = [contact.familyName[0] + ", " + contact.givenName[0]];
-        if (("additionalName" in contact)) {
-          contact.name[0] += " " + contact.additionalName[0];
-        }
-      } else {
-        let profileTitle = extractFieldsFromNode(new Map([["title", "title"]]), entry);
-        if (("title" in profileTitle)) {
-          contact.name = [profileTitle.title];
-        } else if ("familyName" in contact) {
-          contact.name = [contact.familyName[0]];
-        } else if ("givenName" in contact) {
-          contact.name = [contact.givenName[0]];
-        } else if ("org" in contact) {
-          contact.name = [contact.org[0]];
-        } else {
-          let email;
-          try {
-            email = getPreferred(contact);
-          } catch (ex) {
-            // Do nothing
-          }
-          if (email) {
-            contact.name = [email.value];
-          } else {
-            let tel;
-            try {
-              tel = getPreferred(contact, "tel");
-            } catch (ex) {
-              // Do nothing
-            }
-            if (tel) {
-              contact.name = [tel.value];
-            }
-          }
-        }
-      }
-    }
-
-    return contact;
-  },
-
-  /**
-   * Remove all contacts from the database that are not present anymore in the
-   * remote data-source.
-   *
-   * @param {Object}       ids Map of IDs collected earlier of all the contacts
-   *                           that are available on the remote data-source
-   * @param {LoopContacts} db  Instance of the LoopContacts database object, which
-   *                           will store the newly found contacts
-   */
-  _purgeContacts: Task.async(function* (ids, db) {
-    let contacts = yield db.promise("getAll");
-    let profileId = "https://www.google.com/m8/feeds/contacts/" + encodeURIComponent(gProfileId);
-    let processed = 0;
-
-    function promiseSkipABeat() {
-      return new Promise(resolve => Services.tm.currentThread.dispatch(resolve,
-                                      Ci.nsIThread.DISPATCH_NORMAL));
-    }
-
-    for (let [guid, contact] of Iterator(contacts)) {
-      if (++processed % kContactsChunkSize === 0) {
-        // Skip a beat every time we processed a chunk.
-        yield promiseSkipABeat;
-      }
-
-      if (contact.id.indexOf(profileId) >= 0 && !ids[contact.id]) {
-        yield db.promise("remove", guid);
-      }
-    }
-  })
-};
deleted file mode 100644
--- a/browser/components/loop/modules/LoopContacts.jsm
+++ /dev/null
@@ -1,961 +0,0 @@
-/* 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/. */
-"use strict";
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "console",
-                                  "resource://gre/modules/Console.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
-                                  "resource:///modules/loop/LoopStorage.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
-                                  "resource://gre/modules/Promise.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "CardDavImporter",
-                                  "resource:///modules/loop/CardDavImporter.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "GoogleImporter",
-                                  "resource:///modules/loop/GoogleImporter.jsm");
-XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
-  const { EventEmitter } = Cu.import("resource://devtools/shared/event-emitter.js", {});
-  return new EventEmitter();
-});
-
-this.EXPORTED_SYMBOLS = ["LoopContacts"];
-
-const kObjectStoreName = "contacts";
-
-/*
- * The table used to store contacts information contains two identifiers,
- * both of which can be used to look up entries in the table. The table
- * key path (primary index, which must be unique) is "_guid", and is
- * automatically generated by IndexedDB when an entry is first inserted.
- * The other identifier, "id", is the supposedly unique key assigned to this
- * entry by whatever service generated it (e.g., Google Contacts). While
- * this key should, in theory, be completely unique, we don't use it
- * as the key path to avoid generating errors when an external database
- * violates this constraint. This second ID is referred to as the "serviceId".
- */
-const kKeyPath = "_guid";
-const kServiceIdIndex = "id";
-
-/**
- * Contacts validation.
- *
- * To allow for future integration with the Contacts API and/ or potential
- * integration with contact synchronization across devices (including Firefox OS
- * devices), we are using objects with properties having the same names and
- * structure as those used by mozContact.
- *
- * See https://developer.mozilla.org/en-US/docs/Web/API/mozContact for more
- * information.
- */
-const kFieldTypeString = "string";
-const kFieldTypeNumber = "number";
-const kFieldTypeNumberOrString = "number|string";
-const kFieldTypeArray = "array";
-const kFieldTypeBool = "boolean";
-const kContactFields = {
-  "id": {
-    // Because "id" is externally generated, it might be numeric
-    type: kFieldTypeNumberOrString
-  },
-  "published": {
-    // mozContact, from which we are derived, defines dates as
-    // "a Date object, which will eventually be converted to a
-    // long long" -- to be forwards compatible, we allow both
-    // formats for now.
-    type: kFieldTypeNumberOrString
-  },
-  "updated": {
-    // mozContact, from which we are derived, defines dates as
-    // "a Date object, which will eventually be converted to a
-    // long long" -- to be forwards compatible, we allow both
-    // formats for now.
-    type: kFieldTypeNumberOrString
-  },
-  "bday": {
-    // mozContact, from which we are derived, defines dates as
-    // "a Date object, which will eventually be converted to a
-    // long long" -- to be forwards compatible, we allow both
-    // formats for now.
-    type: kFieldTypeNumberOrString
-  },
-  "blocked": {
-    type: kFieldTypeBool
-  },
-  "adr": {
-    type: kFieldTypeArray,
-    contains: {
-      "countryName": {
-        type: kFieldTypeString
-      },
-      "locality": {
-        type: kFieldTypeString
-      },
-      "postalCode": {
-        // In some (but not all) locations, postal codes can be strictly numeric
-        type: kFieldTypeNumberOrString
-      },
-      "pref": {
-        type: kFieldTypeBool
-      },
-      "region": {
-        type: kFieldTypeString
-      },
-      "streetAddress": {
-        type: kFieldTypeString
-      },
-      "type": {
-        type: kFieldTypeArray,
-        contains: kFieldTypeString
-      }
-    }
-  },
-  "email": {
-    type: kFieldTypeArray,
-    contains: {
-      "pref": {
-        type: kFieldTypeBool
-      },
-      "type": {
-        type: kFieldTypeArray,
-        contains: kFieldTypeString
-      },
-      "value": {
-        type: kFieldTypeString
-      }
-    }
-  },
-  "tel": {
-    type: kFieldTypeArray,
-    contains: {
-      "pref": {
-        type: kFieldTypeBool
-      },
-      "type": {
-        type: kFieldTypeArray,
-        contains: kFieldTypeString
-      },
-      "value": {
-        type: kFieldTypeString
-      }
-    }
-  },
-  "name": {
-    type: kFieldTypeArray,
-    contains: kFieldTypeString
-  },
-  "honorificPrefix": {
-    type: kFieldTypeArray,
-    contains: kFieldTypeString
-  },
-  "givenName": {
-    type: kFieldTypeArray,
-    contains: kFieldTypeString
-  },
-  "additionalName": {
-    type: kFieldTypeArray,
-    contains: kFieldTypeString
-  },
-  "familyName": {
-    type: kFieldTypeArray,
-    contains: kFieldTypeString
-  },
-  "honorificSuffix": {
-    type: kFieldTypeArray,
-    contains: kFieldTypeString
-  },
-  "category": {
-    type: kFieldTypeArray,
-    contains: kFieldTypeString
-  },
-  "org": {
-    type: kFieldTypeArray,
-    contains: kFieldTypeString
-  },
-  "jobTitle": {
-    type: kFieldTypeArray,
-    contains: kFieldTypeString
-  },
-  "note": {
-    type: kFieldTypeArray,
-    contains: kFieldTypeString
-  }
-};
-
-/**
- * Compares the properties contained in an object to the definition as defined in
- * `kContactFields`.
- * If a property is encountered that is not found in the spec, an Error is thrown.
- * If a property is encountered with an invalid value, an Error is thrown.
- *
- * Please read the spec at https://wiki.mozilla.org/Loop/Architecture/Address_Book
- * for more information.
- *
- * @param {Object} obj The contact object, or part of it when called recursively
- * @param {Object} def The definition of properties to validate against. Defaults
- *                     to `kContactFields`
- */
-const validateContact = function(obj, def = kContactFields) {
-  for (let propName of Object.getOwnPropertyNames(obj)) {
-    // Ignore internal properties.
-    if (propName.startsWith("_")) {
-      continue;
-    }
-
-    let propDef = def[propName];
-    if (!propDef) {
-      throw new Error("Field '" + propName + "' is not supported for contacts");
-    }
-
-    let val = obj[propName];
-
-    switch (propDef.type) {
-      case kFieldTypeString:
-        if (typeof val != kFieldTypeString) {
-          throw new Error("Field '" + propName + "' must be of type String");
-        }
-        break;
-      case kFieldTypeNumberOrString:
-        let type = typeof val;
-        if (type != kFieldTypeNumber && type != kFieldTypeString) {
-          throw new Error("Field '" + propName + "' must be of type Number or String");
-        }
-        break;
-      case kFieldTypeBool:
-        if (typeof val != kFieldTypeBool) {
-          throw new Error("Field '" + propName + "' must be of type Boolean");
-        }
-        break;
-      case kFieldTypeArray:
-        if (!Array.isArray(val)) {
-          throw new Error("Field '" + propName + "' must be an Array");
-        }
-
-        let contains = propDef.contains;
-        // If the type of `contains` is a scalar value, it means that the array
-        // consists of items of only that type.
-        let isScalarCheck = (typeof contains == kFieldTypeString);
-        for (let arrayValue of val) {
-          if (isScalarCheck) {
-            if (typeof arrayValue != contains) {
-              throw new Error("Field '" + propName + "' must be of type " + contains);
-            }
-          } else {
-            validateContact(arrayValue, contains);
-          }
-        }
-        break;
-    }
-  }
-};
-
-/**
- * Provides a method to perform multiple operations in a single transaction on the
- * contacts store.
- *
- * @param {String}   operation Name of an operation supported by `IDBObjectStore`
- * @param {Array}    data      List of objects that will be passed to the object
- *                             store operation
- * @param {Function} callback  Function that will be invoked once the operations
- *                             have finished. The first argument passed will be
- *                             an `Error` object or `null`. The second argument
- *                             will be the `data` Array, if all operations finished
- *                             successfully.
- */
-const batch = function(operation, data, callback) {
-  let processed = [];
-  if (!LoopContactsInternal.hasOwnProperty(operation) ||
-    typeof LoopContactsInternal[operation] != "function") {
-    callback(new Error("LoopContactsInternal does not contain a '" +
-             operation + "' method"));
-    return;
-  }
-  LoopStorage.asyncForEach(data, (item, next) => {
-    LoopContactsInternal[operation](item, (err, result) => {
-      if (err) {
-        next(err);
-        return;
-      }
-      processed.push(result);
-      next();
-    });
-  }, err => {
-    if (err) {
-      callback(err, processed);
-      return;
-    }
-    callback(null, processed);
-  });
-};
-
-/**
- * Extend a `target` object with the properties defined in `source`.
- *
- * @param {Object} target The target object to receive properties defined in `source`
- * @param {Object} source The source object to copy properties from
- */
-const extend = function(target, source) {
-  for (let key of Object.getOwnPropertyNames(source)) {
-    target[key] = source[key];
-  }
-  return target;
-};
-
-LoopStorage.on("upgrade", function(e, db) {
-  if (db.objectStoreNames.contains(kObjectStoreName)) {
-    return;
-  }
-
-  // Create the 'contacts' store as it doesn't exist yet.
-  let store = db.createObjectStore(kObjectStoreName, {
-    keyPath: kKeyPath,
-    autoIncrement: true
-  });
-  store.createIndex(kServiceIdIndex, kServiceIdIndex, { unique: false });
-});
-
-/**
- * The Contacts class.
- *
- * Each method that is a member of this class requires the last argument to be a
- * callback Function. MozLoopAPI will cause things to break if this invariant is
- * violated. You'll notice this as well in the documentation for each method.
- */
-var LoopContactsInternal = Object.freeze({
-  /**
-   * Map of contact importer names to instances
-   */
-  _importServices: {
-    "carddav": new CardDavImporter(),
-    "google": new GoogleImporter()
-  },
-
-  /**
-   * Add a contact to the data store.
-   *
-   * @param {Object}   details  An object that will be added to the data store
-   *                            as-is. Please read https://wiki.mozilla.org/Loop/Architecture/Address_Book
-   *                            for more information of this objects' structure
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be the contact object, if it was stored successfully.
-   */
-  add: function(details, callback) {
-    if (!(kServiceIdIndex in details)) {
-      callback(new Error("No '" + kServiceIdIndex + "' field present"));
-      return;
-    }
-    try {
-      validateContact(details);
-    } catch (ex) {
-      callback(ex);
-      return;
-    }
-
-    LoopStorage.getStore(kObjectStoreName, (err, store) => {
-      if (err) {
-        callback(err);
-        return;
-      }
-
-      let contact = extend({}, details);
-      let now = Date.now();
-      // The data source should have included "published" and "updated" values
-      // for any imported records, and we need to keep track of those dated for
-      // sync purposes (i.e., when we add functionality to push local changes to
-      // a remote server from which we originally got a contact). We also need
-      // to track the time at which *we* added and most recently changed the
-      // contact, so as to determine whether the local or the remote store has
-      // fresher data.
-      //
-      // For clarity: the fields "published" and "updated" indicate when the
-      // *remote* data source published and updated the contact. The fields
-      // "_date_add" and "_date_lch" track when the *local* data source
-      // created and updated the contact.
-      contact.published = contact.published ? new Date(contact.published).getTime() : now;
-      contact.updated = contact.updated ? new Date(contact.updated).getTime() : now;
-      contact._date_add = contact._date_lch = now;
-
-      let request;
-      try {
-        request = store.add(contact);
-      } catch (ex) {
-        callback(ex);
-        return;
-      }
-
-      request.onsuccess = event => {
-        contact[kKeyPath] = event.target.result;
-        eventEmitter.emit("add", contact);
-        callback(null, contact);
-      };
-
-      request.onerror = event => callback(event.target.error);
-    }, "readwrite");
-  },
-
-  /**
-   * Add a batch of contacts to the data store.
-   *
-   * @param {Array}    contacts A list of contact objects to be added
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be the list of added contacts.
-   */
-  addMany: function(contacts, callback) {
-    batch("add", contacts, callback);
-  },
-
-  /**
-   * Remove a contact from the data store.
-   *
-   * @param {String}   guid     String identifier of the contact to remove
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be the result of the operation.
-   */
-  remove: function(guid, callback) {
-    this.get(guid, (err, contact) => {
-      if (err) {
-        callback(err);
-        return;
-      }
-
-      LoopStorage.getStore(kObjectStoreName, (error, store) => {
-        if (error) {
-          callback(error);
-          return;
-        }
-
-        let request;
-        try {
-          request = store.delete(guid);
-        } catch (ex) {
-          callback(ex);
-          return;
-        }
-
-        request.onsuccess = event => {
-          if (contact) {
-            eventEmitter.emit("remove", contact);
-          }
-          callback(null, event.target.result);
-        };
-        request.onerror = event => callback(event.target.error);
-      }, "readwrite");
-    });
-  },
-
-  /**
-   * Remove a batch of contacts from the data store.
-   *
-   * @param {Array}    guids    A list of IDs of the contacts to remove
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be the list of IDs, if successfull.
-   */
-  removeMany: function(guids, callback) {
-    batch("remove", guids, callback);
-  },
-
-  /**
-   * Remove _all_ contacts from the data store.
-   * CAUTION: this method will clear the whole data store - you won't have any
-   *          contacts left!
-   *
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be the result of the operation, if successfull.
-   */
-  removeAll: function(callback) {
-    LoopStorage.getStore(kObjectStoreName, (err, store) => {
-      if (err) {
-        callback(err);
-        return;
-      }
-
-      let request;
-      try {
-        request = store.clear();
-      } catch (ex) {
-        callback(ex);
-        return;
-      }
-
-      request.onsuccess = event => {
-        eventEmitter.emit("removeAll", event.target.result);
-        callback(null, event.target.result);
-      };
-      request.onerror = event => callback(event.target.error);
-    }, "readwrite");
-  },
-
-  /**
-   * Retrieve a specific contact from the data store.
-   *
-   * @param {String}   guid     String identifier of the contact to retrieve
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be the contact object, if successful.
-   *                            If no object matching guid could be found,
-   *                            then the callback is called with both arguments
-   *                            set to `null`.
-   */
-  get: function(guid, callback) {
-    LoopStorage.getStore(kObjectStoreName, (err, store) => {
-      if (err) {
-        callback(err);
-        return;
-      }
-
-      let request;
-      try {
-        request = store.get(guid);
-      } catch (ex) {
-        callback(ex);
-        return;
-      }
-
-      request.onsuccess = event => {
-        if (!event.target.result) {
-          callback(null, null);
-          return;
-        }
-        let contact = extend({}, event.target.result);
-        contact[kKeyPath] = guid;
-        callback(null, contact);
-      };
-      request.onerror = event => callback(event.target.error);
-    });
-  },
-
-  /**
-   * Retrieve a specific contact from the data store using the kServiceIdIndex
-   * property.
-   *
-   * @param {String}   serviceId String identifier of the contact to retrieve
-   * @param {Function} callback  Function that will be invoked once the operation
-   *                             finished. The first argument passed will be an
-   *                             `Error` object or `null`. The second argument will
-   *                             be the contact object, if successfull.
-   *                             If no object matching serviceId could be found,
-   *                             then the callback is called with both arguments
-   *                             set to `null`.
-   */
-  getByServiceId: function(serviceId, callback) {
-    LoopStorage.getStore(kObjectStoreName, (err, store) => {
-      if (err) {
-        callback(err);
-        return;
-      }
-
-      let index = store.index(kServiceIdIndex);
-      let request;
-      try {
-        request = index.get(serviceId);
-      } catch (ex) {
-        callback(ex);
-        return;
-      }
-
-      request.onsuccess = event => {
-        if (!event.target.result) {
-          callback(null, null);
-          return;
-        }
-
-        let contact = extend({}, event.target.result);
-        callback(null, contact);
-      };
-      request.onerror = event => callback(event.target.error);
-    });
-  },
-
-  /**
-   * Retrieve _all_ contacts from the data store.
-   * CAUTION: If the amount of contacts is very large (say > 100000), this method
-   *          may slow down your application!
-   *
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be an `Array` of contact objects, if successfull.
-   */
-  getAll: function(callback) {
-    LoopStorage.getStore(kObjectStoreName, (err, store) => {
-      if (err) {
-        callback(err);
-        return;
-      }
-
-      let cursorRequest = store.openCursor();
-      let contactsList = [];
-
-      cursorRequest.onsuccess = event => {
-        let cursor = event.target.result;
-        // No more results, return the list.
-        if (!cursor) {
-          callback(null, contactsList);
-          return;
-        }
-
-        let contact = extend({}, cursor.value);
-        contact[kKeyPath] = cursor.key;
-        contactsList.push(contact);
-
-        cursor.continue();
-      };
-
-      cursorRequest.onerror = event => callback(event.target.error);
-    });
-  },
-
-  /**
-   * Retrieve an arbitrary amount of contacts from the data store.
-   * CAUTION: If the amount of contacts is very large (say > 1000), this method
-   *          may slow down your application!
-   *
-   * @param {Array}    guids    List of contact IDs to retrieve contact objects of
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be an `Array` of contact objects, if successfull.
-   */
-  getMany: function(guids, callback) {
-    let contacts = [];
-    LoopStorage.asyncParallel(guids, (guid, next) => {
-      this.get(guid, (err, contact) => {
-        if (err) {
-          next(err);
-          return;
-        }
-        contacts.push(contact);
-        next();
-      });
-    }, err => {
-      callback(err, !err ? contacts : null);
-    });
-  },
-
-  /**
-   * Update a specific contact in the data store.
-   * The contact object is modified by replacing the fields passed in the `details`
-   * param and any fields not passed in are left unchanged.
-   *
-   * @param {Object}   details  An object that will be updated in the data store
-   *                            as-is. Please read https://wiki.mozilla.org/Loop/Architecture/Address_Book
-   *                            for more information of this objects' structure
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be the contact object, if successfull.
-   */
-  update: function(details, callback) {
-    if (!(kKeyPath in details)) {
-      callback(new Error("No '" + kKeyPath + "' field present"));
-      return;
-    }
-    try {
-      validateContact(details);
-    } catch (ex) {
-      callback(ex);
-      return;
-    }
-
-    let guid = details[kKeyPath];
-
-    this.get(guid, (err, contact) => {
-      if (err) {
-        callback(err);
-        return;
-      }
-
-      if (!contact) {
-        callback(new Error("Contact with " + kKeyPath + " '" +
-                           guid + "' could not be found"));
-        return;
-      }
-
-      LoopStorage.getStore(kObjectStoreName, (error, store) => {
-        if (error) {
-          callback(error);
-          return;
-        }
-
-        let previous = extend({}, contact);
-        // Update the contact with properties provided by `details`.
-        extend(contact, details);
-
-        details._date_lch = Date.now();
-        let request;
-        try {
-          request = store.put(contact);
-        } catch (ex) {
-          callback(ex);
-          return;
-        }
-
-        request.onsuccess = event => {
-          eventEmitter.emit("update", contact, previous);
-          callback(null, event.target.result);
-        };
-        request.onerror = event => callback(event.target.error);
-      }, "readwrite");
-    });
-  },
-
-  /**
-   * Block a specific contact in the data store.
-   *
-   * @param {String}   guid     String identifier of the contact to block
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be the contact object, if successfull.
-   */
-  block: function(guid, callback) {
-    this.get(guid, (err, contact) => {
-      if (err) {
-        callback(err);
-        return;
-      }
-
-      if (!contact) {
-        callback(new Error("Contact with " + kKeyPath + " '" +
-                           guid + "' could not be found"));
-        return;
-      }
-
-      contact.blocked = true;
-      this.update(contact, callback);
-    });
-  },
-
-  /**
-   * Un-block a specific contact in the data store.
-   *
-   * @param {String}   guid     String identifier of the contact to unblock
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be the contact object, if successfull.
-   */
-  unblock: function(guid, callback) {
-    this.get(guid, (err, contact) => {
-      if (err) {
-        callback(err);
-        return;
-      }
-
-      if (!contact) {
-        callback(new Error("Contact with " + kKeyPath + " '" +
-                           guid + "' could not be found"));
-        return;
-      }
-
-      contact.blocked = false;
-      this.update(contact, callback);
-    });
-  },
-
-  /**
-   * Import a list of (new) contacts from an external data source.
-   *
-   * @param {Object}   options  Property bag of options for the importer
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be the result of the operation, if successfull.
-   */
-  startImport: function(options, windowRef, callback) {
-    if (!("service" in options)) {
-      callback(new Error("No import service specified in options"));
-      return;
-    }
-    if (!(options.service in this._importServices)) {
-      callback(new Error("Unknown import service specified: " + options.service));
-      return;
-    }
-    this._importServices[options.service].startImport(options, callback,
-                                                      LoopContacts, windowRef);
-  },
-
-  /**
-   * Search through the data store for contacts that match a certain (sub-)string.
-   * NB: The current implementation is very simple, naive if you will; we fetch
-   *     _all_ the contacts via `getAll()` and iterate over all of them to find
-   *     the contacts matching the supplied query (brute-force search in
-   *     exponential time).
-   *
-   * @param {Object}   query    Needle to search for in our haystack of contacts
-   * @param {Function} callback Function that will be invoked once the operation
-   *                            finished. The first argument passed will be an
-   *                            `Error` object or `null`. The second argument will
-   *                            be an `Array` of contact objects, if successfull.
-   *
-   * Example:
-   *   LoopContacts.search({
-   *     q: "foo@bar.com",
-   *     field: "email" // 'email' is the default.
-   *   }, function(err, contacts) {
-   *     if (err) {
-   *       throw err;
-   *     }
-   *     console.dir(contacts);
-   *   });
-   */
-  search: function(query, callback) {
-    if (!("q" in query) || !query.q) {
-      callback(new Error("Nothing to search for. 'q' is required."));
-      return;
-    }
-    if (!("field" in query)) {
-      query.field = "email";
-    }
-    let queryValue = query.q;
-    if (query.field == "tel") {
-      queryValue = queryValue.replace(/[\D]+/g, "");
-    }
-
-    const checkForMatch = function(fieldValue) {
-      if (typeof fieldValue == "string") {
-        if (query.field == "tel") {
-          return fieldValue.replace(/[\D]+/g, "").endsWith(queryValue);
-        }
-        return fieldValue == queryValue;
-      }
-      if (typeof fieldValue == "number" || typeof fieldValue == "boolean") {
-        return fieldValue == queryValue;
-      }
-      if ("value" in fieldValue) {
-        return checkForMatch(fieldValue.value);
-      }
-      return false;
-    };
-
-    let foundContacts = [];
-    this.getAll((err, contacts) => {
-      if (err) {
-        callback(err);
-        return;
-      }
-
-      for (let contact of contacts) {
-        let matchWith = contact[query.field];
-        if (!matchWith) {
-          continue;
-        }
-
-        // Many fields are defined as Arrays.
-        if (Array.isArray(matchWith)) {
-          for (let fieldValue of matchWith) {
-            if (checkForMatch(fieldValue)) {
-              foundContacts.push(contact);
-              break;
-            }
-          }
-        } else if (checkForMatch(matchWith)) {
-          foundContacts.push(contact);
-        }
-      }
-
-      callback(null, foundContacts);
-    });
-  }
-});
-
-/**
- * Public Loop Contacts API.
- *
- * LoopContacts implements the EventEmitter interface by exposing three methods -
- * `on`, `once` and `off` - to subscribe to events.
- * At this point the following events may be subscribed to:
- *  - 'add':       A new contact object was successfully added to the data store.
- *  - 'remove':    A contact was successfully removed from the data store.
- *  - 'removeAll': All contacts were successfully removed from the data store.
- *  - 'update':    A contact object was successfully updated with changed
- *                 properties in the data store.
- */
-this.LoopContacts = Object.freeze({
-  add: function(details, callback) {
-    return LoopContactsInternal.add(details, callback);
-  },
-
-  addMany: function(contacts, callback) {
-    return LoopContactsInternal.addMany(contacts, callback);
-  },
-
-  remove: function(guid, callback) {
-    return LoopContactsInternal.remove(guid, callback);
-  },
-
-  removeMany: function(guids, callback) {
-    return LoopContactsInternal.removeMany(guids, callback);
-  },
-
-  removeAll: function(callback) {
-    return LoopContactsInternal.removeAll(callback);
-  },
-
-  get: function(guid, callback) {
-    return LoopContactsInternal.get(guid, callback);
-  },
-
-  getByServiceId: function(serviceId, callback) {
-    return LoopContactsInternal.getByServiceId(serviceId, callback);
-  },
-
-  getAll: function(callback) {
-    return LoopContactsInternal.getAll(callback);
-  },
-
-  getMany: function(guids, callback) {
-    return LoopContactsInternal.getMany(guids, callback);
-  },
-
-  update: function(details, callback) {
-    return LoopContactsInternal.update(details, callback);
-  },
-
-  block: function(guid, callback) {
-    return LoopContactsInternal.block(guid, callback);
-  },
-
-  unblock: function(guid, callback) {
-    return LoopContactsInternal.unblock(guid, callback);
-  },
-
-  startImport: function(options, windowRef, callback) {
-    return LoopContactsInternal.startImport(options, windowRef, callback);
-  },
-
-  search: function(query, callback) {
-    return LoopContactsInternal.search(query, callback);
-  },
-
-  promise: function(method, ...params) {
-    return new Promise((resolve, reject) => {
-      this[method](...params, (error, result) => {
-        if (error) {
-          reject(error);
-        } else {
-          resolve(result);
-        }
-      });
-    });
-  },
-
-  on: (...params) => eventEmitter.on(...params),
-
-  once: (...params) => eventEmitter.once(...params),
-
-  off: (...params) => eventEmitter.off(...params)
-});
--- a/browser/components/loop/modules/MozLoopAPI.jsm
+++ b/browser/components/loop/modules/MozLoopAPI.jsm
@@ -6,23 +6,18 @@
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/loop/MozLoopService.jsm");
 Cu.import("resource:///modules/loop/LoopRooms.jsm");
-Cu.import("resource:///modules/loop/LoopContacts.jsm");
 Cu.importGlobalProperties(["Blob"]);
 
-XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
-                                        "resource:///modules/loop/LoopContacts.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
-                                        "resource:///modules/loop/LoopStorage.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
                                         "resource://gre/modules/MozSocialAPI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
                                         "resource://gre/modules/PageMetadata.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                         "resource://gre/modules/PluralForm.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
                                         "resource://gre/modules/UpdateUtils.jsm");
@@ -218,17 +213,16 @@ const injectObjectAPI = function(api, ta
  * See the documentation on the individual functions for details of the API.
  *
  * @param {nsIDOMWindow} targetWindow The content window to attach the API.
  */
 function injectLoopAPI(targetWindow) {
   let ringer;
   let ringerStopper;
   let appVersionInfo;
-  let contactsAPI;
   let roomsAPI;
   let callsAPI;
   let savedWindowListeners = new Map();
   let socialProviders;
   const kShareWidgetId = "social-share-button";
   let socialShareButtonListenersAdded = false;
 
 
@@ -382,71 +376,31 @@ function injectLoopAPI(targetWindow) {
       writable: true,
       value: function(conversationWindowId) {
         return cloneValueInto(MozLoopService.getConversationWindowData(conversationWindowId),
           targetWindow);
       }
     },
 
     /**
-     * Returns the contacts API.
-     *
-     * @returns {Object} The contacts API object
-     */
-    contacts: {
-      enumerable: true,
-      get: function() {
-        if (contactsAPI) {
-          return contactsAPI;
-        }
-
-        // Make a database switch when a userProfile is active already.
-        let profile = MozLoopService.userProfile;
-        if (profile) {
-          LoopStorage.switchDatabase(profile.uid);
-        }
-        return contactsAPI = injectObjectAPI(LoopContacts, targetWindow);
-      }
-    },
-
-    /**
      * Returns the rooms API.
      *
      * @returns {Object} The rooms API object
      */
     rooms: {
       enumerable: true,
       get: function() {
         if (roomsAPI) {
           return roomsAPI;
         }
         return roomsAPI = injectObjectAPI(LoopRooms, targetWindow);
       }
     },
 
     /**
-     * Import a list of (new) contacts from an external data source.
-     *
-     * @param {Object}   options  Property bag of options for the importer
-     * @param {Function} callback Function that will be invoked once the operation
-     *                            finished. The first argument passed will be an
-     *                            `Error` object or `null`. The second argument will
-     *                            be the result of the operation, if successfull.
-     */
-    startImport: {
-      enumerable: true,
-      writable: true,
-      value: function(options, callback) {
-        LoopContacts.startImport(options, getChromeWindow(targetWindow), function(...results) {
-          invokeCallback(callback, ...[cloneValueInto(r, targetWindow) for (r of results)]);
-        });
-      }
-    },
-
-    /**
      * Returns translated strings associated with an element. Designed
      * for use with l10n.js
      *
      * @param {String} key The element id
      * @returns {Object} A JSON string containing the localized
      *                   attribute/value pairs for the element.
      */
     getStrings: {
--- a/browser/components/loop/modules/MozLoopService.jsm
+++ b/browser/components/loop/modules/MozLoopService.jsm
@@ -142,22 +142,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/FxAccountsProfileClient.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
                                   "resource://services-common/hawkclient.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
                                   "resource://services-common/hawkrequest.js");
 
-XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
-                                  "resource:///modules/loop/LoopContacts.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
-                                  "resource:///modules/loop/LoopStorage.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms",
                                   "resource:///modules/loop/LoopRooms.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "roomsPushNotification",
                                   "resource:///modules/loop/LoopRooms.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
                                   "resource:///modules/loop/MozLoopPushHandler.jsm");
@@ -302,17 +296,16 @@ var MozLoopServiceInternal = {
   set doNotDisturb(aFlag) {
     Services.prefs.setBoolPref("loop.do_not_disturb", Boolean(aFlag));
     this.notifyStatusChanged();
   },
 
   notifyStatusChanged: function(aReason = null) {
     log.debug("notifyStatusChanged with reason:", aReason);
     let profile = MozLoopService.userProfile;
-    LoopStorage.switchDatabase(profile && profile.uid);
     LoopRooms.maybeRefresh(profile && profile.uid);
     Services.obs.notifyObservers(null, "loop-status-changed", aReason);
   },
 
   /**
    * Record an error and notify interested UI with the relevant user-facing strings attached.
    *
    * @param {String} errorType a key to identify the type of error. Only one
--- a/browser/components/loop/moz.build
+++ b/browser/components/loop/moz.build
@@ -10,22 +10,18 @@ XPCSHELL_TESTS_MANIFESTS += ['test/xpcsh
 
 BROWSER_CHROME_MANIFESTS += [
     'test/mochitest/browser.ini',
 ]
 
 EXTRA_JS_MODULES.loop += [
     'content/shared/js/crypto.js',
     'content/shared/js/utils.js',
-    'modules/CardDavImporter.jsm',
-    'modules/GoogleImporter.jsm',
-    'modules/LoopContacts.jsm',
     'modules/LoopRooms.jsm',
     'modules/LoopRoomsCache.jsm',
-    'modules/LoopStorage.jsm',
     'modules/MozLoopAPI.jsm',
     'modules/MozLoopPushHandler.jsm',
     'modules/MozLoopService.jsm',
     'modules/MozLoopWorker.js',
 ]
 
 with Files('**'):
     BUG_COMPONENT = ('Loop', 'Client')
--- a/browser/components/loop/test/mochitest/browser.ini
+++ b/browser/components/loop/test/mochitest/browser.ini
@@ -1,24 +1,16 @@
 [DEFAULT]
 support-files =
-    fixtures/google_auth.txt
-    fixtures/google_contacts.txt
-    fixtures/google_groups.txt
-    fixtures/google_token.txt
-    google_service.sjs
     head.js
     loop_fxa.sjs
     test_loopLinkClicker_channel.html
     ../../../../base/content/test/general/browser_fxa_oauth_with_keys.html
 
-[browser_CardDavImporter.js]
 [browser_fxa_login.js]
-[browser_GoogleImporter.js]
-skip-if = e10s
 [browser_loop_fxa_server.js]
 [browser_LoopRooms_channel.js]
 [browser_mozLoop_appVersionInfo.js]
 [browser_mozLoop_chat.js]
 [browser_mozLoop_context.js]
 [browser_mozLoop_prefs.js]
 [browser_mozLoop_doNotDisturb.js]
 skip-if = buildapp == 'mulet'
deleted file mode 100644
--- a/browser/components/loop/test/mochitest/browser_CardDavImporter.js
+++ /dev/null
@@ -1,326 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-const { CardDavImporter } = Cu.import("resource:///modules/loop/CardDavImporter.jsm", {});
-
-const kAuth = {
-  "method": "basic",
-  "user": "username",
-  "password": "p455w0rd"
-};
-
-
-// "pid" for "provider ID"
-var vcards = [
-    "VERSION:3.0\n" +
-    "N:Smith;John;;;\n" +
-    "FN:John Smith\n" +
-    "EMAIL;TYPE=work:john.smith@example.com\n" +
-    "REV:2011-07-12T14:43:20Z\n" +
-    "UID:pid1\n" +
-    "END:VCARD\n",
-
-    "VERSION:3.0\n" +
-    "N:Smith;Jane;;;\n" +
-    "FN:Jane Smith\n" +
-    "EMAIL:jane.smith@example.com\n" +
-    "REV:2011-07-12T14:43:20Z\n" +
-    "UID:pid2\n" +
-    "END:VCARD\n",
-
-    "VERSION:3.0\n" +
-    "N:García Fernández;Miguel Angel;José Antonio;Mr.;Jr.\n" +
-    "FN:Mr. Miguel Angel José Antonio\n  García Fernández, Jr.\n" +
-    "EMAIL:mike@example.org\n" +
-    "EMAIL;PREF=1;TYPE=work:miguel.angel@example.net\n" +
-    "EMAIL;TYPE=home;UNKNOWNPARAMETER=frotz:majacf@example.com\n" +
-    "TEL:+3455555555\n" +
-    "TEL;PREF=1;TYPE=work:+3455556666\n" +
-    "TEL;TYPE=home;UNKNOWNPARAMETER=frotz:+3455557777\n" +
-    "ADR:;Suite 123;Calle Aduana\\, 29;MADRID;;28070;SPAIN\n" +
-    "ADR;TYPE=work:P.O. BOX 555;;;Washington;DC;20024-00555;USA\n" +
-    "ORG:Acme España SL\n" +
-    "TITLE:President\n" +
-    "BDAY:1965-05-05\n" +
-    "NOTE:Likes tulips\n" +
-    "REV:2011-07-12T14:43:20Z\n" +
-    "UID:pid3\n" +
-    "END:VCARD\n",
-
-    "VERSION:3.0\n" +
-    "N:Jones;Bob;;;\n" +
-    "EMAIL:bob.jones@example.com\n" +
-    "REV:2011-07-12T14:43:20Z\n" +
-    "UID:pid4\n" +
-    "END:VCARD\n",
-
-    "VERSION:3.0\n" +
-    "N:Jones;Davy;Randall;;\n" +
-    "EMAIL:davy.jones@example.com\n" +
-    "REV:2011-07-12T14:43:20Z\n" +
-    "UID:pid5\n" +
-    "END:VCARD\n",
-
-    "VERSION:3.0\n" +
-    "EMAIL:trip@example.com\n" +
-    "NICKNAME:Trip\n" +
-    "REV:2011-07-12T14:43:20Z\n" +
-    "UID:pid6\n" +
-    "END:VCARD\n",
-
-    "VERSION:3.0\n" +
-    "EMAIL:acme@example.com\n" +
-    "ORG:Acme, Inc.\n" +
-    "REV:2011-07-12T14:43:20Z\n" +
-    "UID:pid7\n" +
-    "END:VCARD\n",
-
-    "VERSION:3.0\n" +
-    "EMAIL:anyone@example.com\n" +
-    "REV:2011-07-12T14:43:20Z\n" +
-    "UID:pid8\n" +
-    "END:VCARD\n"
-];
-
-
-const monkeyPatchImporter = function(importer) {
-  // Set up the response bodies
-  let listPropfind =
-    '<?xml version="1.0" encoding="UTF-8"?>\n' +
-    '<d:multistatus xmlns:card="urn:ietf:params:xml:ns:carddav"\n' +
-    '               xmlns:d="DAV:">\n' +
-    " <d:response>\n" +
-    "  <d:href>/carddav/abook/</d:href>\n" +
-    "  <d:propstat>\n" +
-    "   <d:status>HTTP/1.1 200 OK</d:status>\n" +
-    "  </d:propstat>\n" +
-    "  <d:propstat>\n" +
-    "   <d:status>HTTP/1.1 404 Not Found</d:status>\n" +
-    "   <d:prop>\n" +
-    "    <d:getetag/>\n" +
-    "   </d:prop>\n" +
-    "  </d:propstat>\n" +
-    " </d:response>\n";
-
-  let listReportMultiget =
-    '<?xml version="1.0" encoding="UTF-8"?>\n' +
-    '<d:multistatus xmlns:card="urn:ietf:params:xml:ns:carddav"\n' +
-    '               xmlns:d="DAV:">\n';
-
-  vcards.forEach(vcard => {
-    let uid = /\nUID:(.*?)\n/.exec(vcard);
-    listPropfind +=
-      " <d:response>\n" +
-      "  <d:href>/carddav/abook/" + uid + "</d:href>\n" +
-      "  <d:propstat>\n" +
-      "   <d:status>HTTP/1.1 200 OK</d:status>\n" +
-      "   <d:prop>\n" +
-      '    <d:getetag>"2011-07-12T07:43:20.855-07:00"</d:getetag>\n' +
-      "   </d:prop>\n" +
-      "  </d:propstat>\n" +
-      " </d:response>\n";
-
-    listReportMultiget +=
-      " <d:response>\n" +
-      "  <d:href>/carddav/abook/" + uid + "</d:href>\n" +
-      "  <d:propstat>\n" +
-      "   <d:status>HTTP/1.1 200 OK</d:status>\n" +
-      "   <d:prop>\n" +
-      '    <d:getetag>"2011-07-12T07:43:20.855-07:00"</d:getetag>\n' +
-      "    <card:address-data>" + vcard + "</card:address-data>\n" +
-      "   </d:prop>\n" +
-      "  </d:propstat>\n" +
-      " </d:response>\n";
-  });
-
-  listPropfind += "</d:multistatus>\n";
-  listReportMultiget += "</d:multistatus>\n";
-
-  importer._davPromise = function(method, url, auth, depth, body) {
-    return new Promise((resolve, reject) => {
-
-      if (auth.method != "basic" ||
-          auth.user != kAuth.user ||
-          auth.password != kAuth.password) {
-        reject(new Error("401 Auth Failure"));
-        return;
-      }
-
-      let request = method + " " + url + " " + depth;
-      let xmlParser = new DOMParser();
-      let responseXML;
-      switch (request) {
-        case "PROPFIND https://example.com/.well-known/carddav 1":
-          responseXML = xmlParser.parseFromString(listPropfind, "text/xml");
-          break;
-        case "REPORT https://example.com/carddav/abook/ 1":
-          responseXML = xmlParser.parseFromString(listReportMultiget, "text/xml");
-          break;
-        default:
-          reject(new Error("404 Not Found"));
-          return;
-      }
-      resolve({ "responseXML": responseXML });
-    });
-  }.bind(importer);
-  return importer;
-};
-
-add_task(function* test_CardDavImport() {
-  let importer = monkeyPatchImporter(new CardDavImporter());
-  yield new Promise((resolve, reject) => {
-    info("Initiating import");
-    importer.startImport({
-        "host": "example.com",
-        "auth": kAuth.method,
-        "user": kAuth.user,
-        "password": kAuth.password
-      }, (err, result) => { err ? reject(err) : resolve(result); }, mockDb);
-  });
-  info("Import succeeded");
-
-  Assert.equal(vcards.length, Object.keys(mockDb._store).length,
-               "Should import all VCards into database");
-
-  // Basic checks
-  let c = mockDb._store[1];
-  Assert.equal(c.name[0], "John Smith", "Full name should match");
-  Assert.equal(c.givenName[0], "John", "Given name should match");
-  Assert.equal(c.familyName[0], "Smith", "Family name should match");
-  Assert.equal(c.email[0].type, "work", "Email type should match");
-  Assert.equal(c.email[0].value, "john.smith@example.com", "Email should match");
-  Assert.equal(c.email[0].pref, false, "Pref should match");
-  Assert.equal(c.id, "pid1@example.com", "UID should match and be scoped to provider");
-
-  c = mockDb._store[2];
-  Assert.equal(c.name[0], "Jane Smith", "Full name should match");
-  Assert.equal(c.givenName[0], "Jane", "Given name should match");
-  Assert.equal(c.familyName[0], "Smith", "Family name should match");
-  Assert.equal(c.email[0].type, "other", "Email type should match");
-  Assert.equal(c.email[0].value, "jane.smith@example.com", "Email should match");
-  Assert.equal(c.email[0].pref, false, "Pref should match");
-  Assert.equal(c.id, "pid2@example.com", "UID should match and be scoped to provider");
-
-  // Check every field
-  c = mockDb._store[3];
-  Assert.equal(c.name[0], "Mr. Miguel Angel José Antonio García Fernández, Jr.", "Full name should match");
-  Assert.equal(c.givenName[0], "Miguel Angel", "Given name should match");
-  Assert.equal(c.additionalName[0], "José Antonio", "Other name should match");
-  Assert.equal(c.familyName[0], "García Fernández", "Family name should match");
-  Assert.equal(c.email.length, 3, "Email count should match");
-  Assert.equal(c.email[0].type, "other", "Email type should match");
-  Assert.equal(c.email[0].value, "mike@example.org", "Email should match");
-  Assert.equal(c.email[0].pref, false, "Pref should match");
-  Assert.equal(c.email[1].type, "work", "Email type should match");
-  Assert.equal(c.email[1].value, "miguel.angel@example.net", "Email should match");
-  Assert.equal(c.email[1].pref, true, "Pref should match");
-  Assert.equal(c.email[2].type, "home", "Email type should match");
-  Assert.equal(c.email[2].value, "majacf@example.com", "Email should match");
-  Assert.equal(c.email[2].pref, false, "Pref should match");
-  Assert.equal(c.tel.length, 3, "Phone number count should match");
-  Assert.equal(c.tel[0].type, "other", "Phone type should match");
-  Assert.equal(c.tel[0].value, "+3455555555", "Phone number should match");
-  Assert.equal(c.tel[0].pref, false, "Pref should match");
-  Assert.equal(c.tel[1].type, "work", "Phone type should match");
-  Assert.equal(c.tel[1].value, "+3455556666", "Phone number should match");
-  Assert.equal(c.tel[1].pref, true, "Pref should match");
-  Assert.equal(c.tel[2].type, "home", "Phone type should match");
-  Assert.equal(c.tel[2].value, "+3455557777", "Phone number should match");
-  Assert.equal(c.tel[2].pref, false, "Pref should match");
-  Assert.equal(c.adr.length, 2, "Address count should match");
-  Assert.equal(c.adr[0].pref, false, "Pref should match");
-  Assert.equal(c.adr[0].type, "other", "Type should match");
-  Assert.equal(c.adr[0].streetAddress, "Calle Aduana, 29 Suite 123", "Street address should match");
-  Assert.equal(c.adr[0].locality, "MADRID", "Locality should match");
-  Assert.equal(c.adr[0].postalCode, "28070", "Post code should match");
-  Assert.equal(c.adr[0].countryName, "SPAIN", "Country should match");
-  Assert.equal(c.adr[1].pref, false, "Pref should match");
-  Assert.equal(c.adr[1].type, "work", "Type should match");
-  Assert.equal(c.adr[1].streetAddress, "P.O. BOX 555", "Street address should match");
-  Assert.equal(c.adr[1].locality, "Washington", "Locality should match");
-  Assert.equal(c.adr[1].region, "DC", "Region should match");
-  Assert.equal(c.adr[1].postalCode, "20024-00555", "Post code should match");
-  Assert.equal(c.adr[1].countryName, "USA", "Country should match");
-  Assert.equal(c.org[0], "Acme España SL", "Org should match");
-  Assert.equal(c.jobTitle[0], "President", "Title should match");
-  Assert.equal(c.note[0], "Likes tulips", "Note should match");
-  let bday = new Date(c.bday);
-  Assert.equal(bday.getUTCFullYear(), 1965, "Birthday year should match");
-  Assert.equal(bday.getUTCMonth(), 4, "Birthday month should match");
-  Assert.equal(bday.getUTCDate(), 5, "Birthday day should match");
-  Assert.equal(c.id, "pid3@example.com", "UID should match and be scoped to provider");
-
-  // Check name synthesis
-  c = mockDb._store[4];
-  Assert.equal(c.name[0], "Jones, Bob", "Full name should be synthesized correctly");
-  c = mockDb._store[5];
-  Assert.equal(c.name[0], "Jones, Davy Randall", "Full name should be synthesized correctly");
-  c = mockDb._store[6];
-  Assert.equal(c.name[0], "Trip", "Full name should be synthesized correctly");
-  c = mockDb._store[7];
-  Assert.equal(c.name[0], "Acme, Inc.", "Full name should be synthesized correctly");
-  c = mockDb._store[8];
-  Assert.equal(c.name[0], "anyone@example.com", "Full name should be synthesized correctly");
-
-  // Check that a re-import doesn't cause contact duplication.
-  yield new Promise((resolve, reject) => {
-    info("Initiating import");
-    importer.startImport({
-        "host": "example.com",
-        "auth": kAuth.method,
-        "user": kAuth.user,
-        "password": kAuth.password
-      }, (err, result) => { err ? reject(err) : resolve(result); }, mockDb);
-  });
-  Assert.equal(vcards.length, Object.keys(mockDb._store).length,
-               "Second import shouldn't increase DB size");
-
-  // Check that errors are propagated back to caller
-  let error = yield new Promise((resolve, reject) => {
-    info("Initiating import");
-    importer.startImport({
-        "host": "example.com",
-        "auth": kAuth.method,
-        "user": kAuth.user,
-        "password": "invalidpassword"
-      }, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
-  });
-  Assert.equal(error.message, "401 Auth Failure", "Auth error should propagate");
-
-  error = yield new Promise((resolve, reject) => {
-    info("Initiating import");
-    importer.startImport({
-        "host": "example.invalid",
-        "auth": kAuth.method,
-        "user": kAuth.user,
-        "password": kAuth.password
-      }, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
-  });
-  Assert.equal(error.message, "404 Not Found", "Not found error should propagate");
-
-  let tmp = mockDb.getByServiceId;
-  mockDb.getByServiceId = function(serviceId, callback) {
-    callback(new Error("getByServiceId failed"));
-  };
-  error = yield new Promise((resolve, reject) => {
-    info("Initiating import");
-    importer.startImport({
-        "host": "example.com",
-        "auth": kAuth.method,
-        "user": kAuth.user,
-        "password": kAuth.password
-      }, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
-  });
-  Assert.equal(error.message, "getByServiceId failed", "Database error should propagate");
-  mockDb.getByServiceId = tmp;
-
-  error = yield new Promise((resolve, reject) => {
-    info("Initiating import");
-    importer.startImport({
-        "host": "example.com"
-      }, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
-  });
-  Assert.equal(error.message, "No authentication specified", "Missing parameters should generate error");
-});
deleted file mode 100644
--- a/browser/components/loop/test/mochitest/browser_GoogleImporter.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-const { GoogleImporter } = Cu.import("resource:///modules/loop/GoogleImporter.jsm", {});
-
-var importer = new GoogleImporter();
-
-function promiseImport() {
-  return new Promise(function(resolve, reject) {
-    importer.startImport({}, function(err, stats) {
-      if (err) {
-        reject(err);
-      } else {
-        resolve(stats);
-      }
-    }, mockDb, window);
-  });
-}
-
-const kIncomingTotalContactsCount = 8;
-const kExpectedImportCount = 7;
-
-add_task(function* test_GoogleImport() {
-  let stats;
-  // An error may throw and the test will fail when that happens.
-  stats = yield promiseImport();
-
-  // Assert the world.
-  Assert.equal(stats.total, kIncomingTotalContactsCount, kIncomingTotalContactsCount + " contacts should get processed");
-  Assert.equal(stats.success, kExpectedImportCount, kExpectedImportCount + " contacts should be imported");
-
-  yield promiseImport();
-  Assert.equal(mockDb.size, kExpectedImportCount, "Database should be the same size after reimport");
-
-  let currentContact = kExpectedImportCount;
-
-  let c = mockDb._store[mockDb._next_guid - currentContact];
-  Assert.equal(c.name[0], "John Smith", "Full name should match");
-  Assert.equal(c.givenName[0], "John", "Given name should match");
-  Assert.equal(c.familyName[0], "Smith", "Family name should match");
-  Assert.equal(c.email[0].type, "other", "Email type should match");
-  Assert.equal(c.email[0].value, "john.smith@example.com", "Email should match");
-  Assert.equal(c.email[0].pref, true, "Pref should match");
-  Assert.equal(c.category[0], "google", "Category should match");
-  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/0", "UID should match and be scoped to provider");
-
-  c = mockDb._store[mockDb._next_guid - (--currentContact)];
-  Assert.equal(c.name[0], "Jane Smith", "Full name should match");
-  Assert.equal(c.givenName[0], "Jane", "Given name should match");
-  Assert.equal(c.familyName[0], "Smith", "Family name should match");
-  Assert.equal(c.email[0].type, "other", "Email type should match");
-  Assert.equal(c.email[0].value, "jane.smith@example.com", "Email should match");
-  Assert.equal(c.email[0].pref, true, "Pref should match");
-  Assert.equal(c.category[0], "google", "Category should match");
-  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/1", "UID should match and be scoped to provider");
-
-  c = mockDb._store[mockDb._next_guid - (--currentContact)];
-  Assert.equal(c.name[0], "Davy Randall Jones", "Full name should match");
-  Assert.equal(c.givenName[0], "Davy Randall", "Given name should match");
-  Assert.equal(c.familyName[0], "Jones", "Family name should match");
-  Assert.equal(c.email[0].type, "other", "Email type should match");
-  Assert.equal(c.email[0].value, "davy.jones@example.com", "Email should match");
-  Assert.equal(c.email[0].pref, true, "Pref should match");
-  Assert.equal(c.category[0], "google", "Category should match");
-  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/2", "UID should match and be scoped to provider");
-
-  c = mockDb._store[mockDb._next_guid - (--currentContact)];
-  Assert.equal(c.name[0], "noname@example.com", "Full name should match");
-  Assert.equal(c.email[0].type, "other", "Email type should match");
-  Assert.equal(c.email[0].value, "noname@example.com", "Email should match");
-  Assert.equal(c.email[0].pref, true, "Pref should match");
-  Assert.equal(c.category[0], "google", "Category should match");
-  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/3", "UID should match and be scoped to provider");
-
-  c = mockDb._store[mockDb._next_guid - (--currentContact)];
-  Assert.equal(c.name[0], "lycnix", "Full name should match");
-  Assert.equal(c.email[0].type, "other", "Email type should match");
-  Assert.equal(c.email[0].value, "lycnix", "Email should match");
-  Assert.equal(c.email[0].pref, true, "Pref should match");
-  Assert.equal(c.category[0], "google", "Category should match");
-  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/7", "UID should match and be scoped to provider");
-
-  c = mockDb._store[mockDb._next_guid - (--currentContact)];
-  Assert.equal(c.name[0], "+31-6-12345678", "Full name should match");
-  Assert.equal(c.tel[0].type, "mobile", "Phone type should match");
-  Assert.equal(c.tel[0].value, "+31-6-12345678", "Phone should match");
-  Assert.equal(c.tel[0].pref, false, "Pref should match");
-  Assert.equal(c.category[0], "google", "Category should match");
-  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/8", "UID should match and be scoped to provider");
-
-  c = mockDb._store[mockDb._next_guid - (--currentContact)];
-  Assert.equal(c.name[0], "215234523452345", "Full name should match");
-  Assert.equal(c.tel[0].type, "mobile", "Phone type should match");
-  Assert.equal(c.tel[0].value, "215234523452345", "Phone should match");
-  Assert.equal(c.tel[0].pref, false, "Pref should match");
-  Assert.equal(c.category[0], "google", "Category should match");
-  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/6", "UID should match and be scoped to provider");
-
-  c = yield mockDb.promise("getByServiceId", "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/9");
-  Assert.equal(c, null, "Contacts that are not part of the default group should not be imported");
-});
deleted file mode 100644
--- a/browser/components/loop/test/mochitest/browser_LoopContacts.js
+++ /dev/null
@@ -1,489 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-const { LoopContacts } = Cu.import("resource:///modules/loop/LoopContacts.jsm", {});
-const { LoopStorage } = Cu.import("resource:///modules/loop/LoopStorage.jsm", {});
-
-XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
-                                   "@mozilla.org/uuid-generator;1",
-                                   "nsIUUIDGenerator");
-
-const kContacts = [{
-  id: 1,
-  name: ["Ally Avocado"],
-  email: [{
-    "pref": true,
-    "type": ["work"],
-    "value": "ally@mail.com"
-  }],
-  tel: [{
-    "pref": true,
-    "type": ["mobile"],
-    "value": "+31-6-12345678"
-  }],
-  category: ["google"],
-  published: 1406798311748,
-  updated: 1406798311748
-}, {
-  id: 2,
-  name: ["Bob Banana"],
-  email: [{
-    "pref": true,
-    "type": ["work"],
-    "value": "bob@gmail.com"
-  }],
-  tel: [{
-    "pref": true,
-    "type": ["mobile"],
-    "value": "+1-214-5551234"
-  }],
-  category: ["local"],
-  published: 1406798311748,
-  updated: 1406798311748
-}, {
-  id: 3,
-  name: ["Caitlin Cantaloupe"],
-  email: [{
-    "pref": true,
-    "type": ["work"],
-    "value": "caitlin.cant@hotmail.com"
-  }],
-  category: ["local"],
-  published: 1406798311748,
-  updated: 1406798311748
-}, {
-  id: 4,
-  name: ["Dave Dragonfruit"],
-  email: [{
-    "pref": true,
-    "type": ["work"],
-    "value": "dd@dragons.net"
-  }],
-  category: ["google"],
-  published: 1406798311748,
-  updated: 1406798311748
-}];
-
-const kDanglingContact = {
-  id: 5,
-  name: ["Ellie Eggplant"],
-  email: [{
-    "pref": true,
-    "type": ["work"],
-    "value": "ellie@yahoo.com"
-  }],
-  category: ["google"],
-  blocked: true,
-  published: 1406798311748,
-  updated: 1406798311748
-};
-
-const promiseLoadContacts = function() {
-  return new Promise((resolve, reject) => {
-    LoopContacts.removeAll(err => {
-      if (err) {
-        reject(err);
-        return;
-      }
-
-      gExpectedAdds.push(...kContacts);
-      LoopContacts.addMany(kContacts, (error, contacts) => {
-        if (error) {
-          reject(error);
-          return;
-        }
-        resolve(contacts);
-      });
-    });
-  });
-};
-
-// Get a copy of a contact without private properties.
-const normalizeContact = function(contact) {
-  let result = {};
-  // Get a copy of contact without private properties.
-  for (let prop of Object.getOwnPropertyNames(contact)) {
-    if (!prop.startsWith("_")) {
-      result[prop] = contact[prop];
-    }
-  }
-  return result;
-};
-
-const compareContacts = function(contact1, contact2) {
-  Assert.ok("_guid" in contact1, "First contact should have an ID.");
-  Assert.deepEqual(normalizeContact(contact1), normalizeContact(contact2));
-};
-
-// LoopContacts emits various events. Test if they work as expected here.
-var gExpectedAdds = [];
-var gExpectedRemovals = [];
-var gExpectedUpdates = [];
-
-const onContactAdded = function(e, contact) {
-  let expectedIds = gExpectedAdds.map(contactEntry => contactEntry.id);
-  let idx = expectedIds.indexOf(contact.id);
-  Assert.ok(idx > -1, "Added contact should be expected");
-  let expected = gExpectedAdds[idx];
-  compareContacts(contact, expected);
-  gExpectedAdds.splice(idx, 1);
-};
-
-const onContactRemoved = function(e, contact) {
-  let idx = gExpectedRemovals.indexOf(contact._guid);
-  Assert.ok(idx > -1, "Removed contact should be expected");
-  gExpectedRemovals.splice(idx, 1);
-};
-
-const onContactUpdated = function(e, contact) {
-  let idx = gExpectedUpdates.indexOf(contact._guid);
-  Assert.ok(idx > -1, "Updated contact should be expected");
-  gExpectedUpdates.splice(idx, 1);
-};
-
-LoopContacts.on("add", onContactAdded);
-LoopContacts.on("remove", onContactRemoved);
-LoopContacts.on("update", onContactUpdated);
-
-registerCleanupFunction(function() {
-  LoopContacts.removeAll(() => {});
-  LoopContacts.off("add", onContactAdded);
-  LoopContacts.off("remove", onContactRemoved);
-  LoopContacts.off("update", onContactUpdated);
-});
-
-// Test adding a contact.
-add_task(function* () {
-  let contacts = yield promiseLoadContacts();
-  for (let i = 0, l = contacts.length; i < l; ++i) {
-    compareContacts(contacts[i], kContacts[i]);
-  }
-
-  info("Add a contact.");
-  yield new Promise((resolve, reject) => {
-    gExpectedAdds.push(kDanglingContact);
-    LoopContacts.add(kDanglingContact, (err, contact) => {
-      Assert.ok(!err, "There shouldn't be an error");
-      compareContacts(contact, kDanglingContact);
-
-      info("Check if it's persisted.");
-      LoopContacts.get(contact._guid, (error, contactEntry) => {
-        Assert.ok(!error, "There shouldn't be an error");
-        compareContacts(contactEntry, kDanglingContact);
-        resolve();
-      });
-    });
-  });
-});
-
-add_task(function* () {
-  info("Test removing all contacts.");
-  let contacts = yield promiseLoadContacts();
-
-  yield new Promise((resolve, reject) => {
-    LoopContacts.removeAll(function(err) {
-      Assert.ok(!err, "There shouldn't be an error");
-      LoopContacts.getAll(function(error, found) {
-        Assert.ok(!error, "There shouldn't be an error");
-        Assert.equal(found.length, 0, "There shouldn't be any contacts left");
-        resolve();
-      });
-    });
-  });
-});
-
-// Test retrieving a contact.
-add_task(function* () {
-  let contacts = yield promiseLoadContacts();
-
-  info("Get a single contact.");
-  yield new Promise((resolve, reject) => {
-    LoopContacts.get(contacts[1]._guid, (err, contact) => {
-      Assert.ok(!err, "There shouldn't be an error");
-      compareContacts(contact, kContacts[1]);
-      resolve();
-    });
-  });
-
-  info("Get a single contact by id.");
-  yield new Promise((resolve, reject) => {
-    LoopContacts.getByServiceId(2, (err, contact) => {
-      Assert.ok(!err, "There shouldn't be an error");
-      compareContacts(contact, kContacts[1]);
-      resolve();
-    });
-  });
-
-  info("Get a couple of contacts.");
-  yield new Promise((resolve, reject) => {
-    let toRetrieve = [contacts[0], contacts[2], contacts[3]];
-    LoopContacts.getMany(toRetrieve.map(contact => contact._guid), (err, result) => {
-      Assert.ok(!err, "There shouldn't be an error");
-      Assert.equal(result.length, toRetrieve.length, "Result list should be the same " +
-                   "size as the list of items to retrieve");
-
-      function resultFilter(c) {
-        return c._guid == this._guid;
-      }
-
-      for (let contact of toRetrieve) {
-        let found = result.filter(resultFilter.bind(contact));
-        Assert.ok(found.length, "Contact " + contact._guid + " should be in the list");
-        compareContacts(found[0], contact);
-      }
-      resolve();
-    });
-  });
-
-  info("Get all contacts.");
-  yield new Promise((resolve, reject) => {
-    LoopContacts.getAll((err, allContacts) => {
-      Assert.ok(!err, "There shouldn't be an error");
-      for (let i = 0, l = allContacts.length; i < l; ++i) {
-        compareContacts(allContacts[i], kContacts[i]);
-      }
-      resolve();
-    });
-  });
-
-  info("Get a non-existent contact.");
-  return new Promise((resolve, reject) => {
-    LoopContacts.get(1000, (err, contact) => {
-      Assert.ok(!err, "There shouldn't be an error");
-      Assert.ok(!contact, "There shouldn't be a contact");
-      resolve();
-    });
-  });
-});
-
-// Test removing a contact.
-add_task(function* () {
-  let contacts = yield promiseLoadContacts();
-
-  info("Remove a single contact.");
-  yield new Promise((resolve, reject) => {
-    let toRemove = contacts[2]._guid;
-    gExpectedRemovals.push(toRemove);
-    LoopContacts.remove(toRemove, err => {
-      Assert.ok(!err, "There shouldn't be an error");
-
-      LoopContacts.get(toRemove, (error, contact) => {
-        Assert.ok(!error, "There shouldn't be an error");
-        Assert.ok(!contact, "There shouldn't be a contact");
-        resolve();
-      });
-    });
-  });
-
-  info("Remove a non-existing contact.");
-  yield new Promise((resolve, reject) => {
-    LoopContacts.remove(1000, (err, contact) => {
-      Assert.ok(!err, "There shouldn't be an error");
-      Assert.ok(!contact, "There shouldn't be a contact");
-      resolve();
-    });
-  });
-
-  info("Remove multiple contacts.");
-  yield new Promise((resolve, reject) => {
-    let toRemove = [contacts[0]._guid, contacts[1]._guid];
-    gExpectedRemovals.push(...toRemove);
-    LoopContacts.removeMany(toRemove, err => {
-      Assert.ok(!err, "There shouldn't be an error");
-
-      LoopContacts.getAll((error, allContacts) => {
-        Assert.ok(!error, "There shouldn't be an error");
-        let ids = allContacts.map(contact => contact._guid);
-        Assert.equal(ids.indexOf(toRemove[0]), -1, "Contact '" + toRemove[0] +
-                                                   "' shouldn't be there");
-        Assert.equal(ids.indexOf(toRemove[1]), -1, "Contact '" + toRemove[1] +
-                                                   "' shouldn't be there");
-        resolve();
-      });
-    });
-  });
-});
-
-// Test updating a contact.
-add_task(function* () {
-  let contacts = yield promiseLoadContacts();
-
-  const newBday = (new Date(403920000000)).toISOString();
-
-  info("Update a single contact.");
-  yield new Promise((resolve, reject) => {
-    let toUpdate = {
-      _guid: contacts[2]._guid,
-      bday: newBday
-    };
-    gExpectedUpdates.push(contacts[2]._guid);
-    LoopContacts.update(toUpdate, (err, result) => {
-      Assert.ok(!err, "There shouldn't be an error");
-      Assert.equal(result, toUpdate._guid, "Result should be the same as the contact ID");
-
-      LoopContacts.get(toUpdate._guid, (error, contact) => {
-        Assert.ok(!error, "There shouldn't be an error");
-        Assert.equal(contact.bday, newBday, "Birthday should be the same");
-        info("Check that all other properties were left intact.");
-        contacts[2].bday = newBday;
-        compareContacts(contact, contacts[2]);
-        resolve();
-      });
-    });
-  });
-
-  info("Update a non-existing contact.");
-  yield new Promise((resolve, reject) => {
-    let toUpdate = {
-      _guid: 1000,
-      bday: newBday
-    };
-    LoopContacts.update(toUpdate, (err, contact) => {
-      Assert.ok(err, "There should be an error");
-      Assert.equal(err.message, "Contact with _guid '1000' could not be found",
-                   "Error message should be correct");
-      resolve();
-    });
-  });
-});
-
-// Test blocking and unblocking a contact.
-add_task(function* () {
-  let contacts = yield promiseLoadContacts();
-
-  info("Block contact.");
-  yield new Promise((resolve, reject) => {
-    let toBlock = contacts[1]._guid;
-    gExpectedUpdates.push(toBlock);
-    LoopContacts.block(toBlock, (err, result) => {
-      Assert.ok(!err, "There shouldn't be an error");
-      Assert.equal(result, toBlock, "Result should be the same as the contact ID");
-
-      LoopContacts.get(toBlock, (error, contact) => {
-        Assert.ok(!error, "There shouldn't be an error");
-        Assert.strictEqual(contact.blocked, true, "Blocked status should be set");
-        info("Check that all other properties were left intact.");
-        delete contact.blocked;
-        compareContacts(contact, contacts[1]);
-        resolve();
-      });
-    });
-  });
-
-  info("Block a non-existing contact.");
-  yield new Promise((resolve, reject) => {
-    LoopContacts.block(1000, err => {
-      Assert.ok(err, "There should be an error");
-      Assert.equal(err.message, "Contact with _guid '1000' could not be found",
-                   "Error message should be correct");
-      resolve();
-    });
-  });
-
-  info("Unblock a contact.");
-  yield new Promise((resolve, reject) => {
-    let toUnblock = contacts[1]._guid;
-    gExpectedUpdates.push(toUnblock);
-    LoopContacts.unblock(toUnblock, (err, result) => {
-      Assert.ok(!err, "There shouldn't be an error");
-      Assert.equal(result, toUnblock, "Result should be the same as the contact ID");
-
-      LoopContacts.get(toUnblock, (error, contact) => {
-        Assert.ok(!error, "There shouldn't be an error");
-        Assert.strictEqual(contact.blocked, false, "Blocked status should be set");
-        info("Check that all other properties were left intact.");
-        delete contact.blocked;
-        compareContacts(contact, contacts[1]);
-        resolve();
-      });
-    });
-  });
-
-  info("Unblock a non-existing contact.");
-  yield new Promise((resolve, reject) => {
-    LoopContacts.unblock(1000, err => {
-      Assert.ok(err, "There should be an error");
-      Assert.equal(err.message, "Contact with _guid '1000' could not be found",
-                   "Error message should be correct");
-      resolve();
-    });
-  });
-});
-
-// Test if the event emitter implementation doesn't leak and is working as expected.
-add_task(function* () {
-  yield promiseLoadContacts();
-
-  Assert.strictEqual(gExpectedAdds.length, 0, "No contact additions should be expected anymore");
-  Assert.strictEqual(gExpectedRemovals.length, 0, "No contact removals should be expected anymore");
-  Assert.strictEqual(gExpectedUpdates.length, 0, "No contact updates should be expected anymore");
-});
-
-// Test switching between different databases.
-add_task(function* () {
-  Assert.equal(LoopStorage.databaseName, "default", "First active partition should be the default");
-  yield promiseLoadContacts();
-
-  let uuid = uuidgen.generateUUID().toString().replace(/[{}]+/g, "");
-  LoopStorage.switchDatabase(uuid);
-  Assert.equal(LoopStorage.databaseName, uuid, "The active partition should have changed");
-
-  yield promiseLoadContacts();
-
-  let contacts = yield promiseLoadContacts();
-  for (let i = 0, l = contacts.length; i < l; ++i) {
-    compareContacts(contacts[i], kContacts[i]);
-  }
-
-  LoopStorage.switchDatabase();
-  Assert.equal(LoopStorage.databaseName, "default", "The active partition should have changed");
-
-  contacts = yield LoopContacts.promise("getAll");
-  for (let i = 0, l = contacts.length; i < l; ++i) {
-    compareContacts(contacts[i], kContacts[i]);
-  }
-});
-
-// Test searching for contacts.
-add_task(function* () {
-  yield promiseLoadContacts();
-
-  let contacts = yield LoopContacts.promise("search", {
-    q: "bob@gmail.com"
-  });
-  Assert.equal(contacts.length, 1, "There should be one contact found");
-  compareContacts(contacts[0], kContacts[1]);
-
-  // Test searching by name.
-  contacts = yield LoopContacts.promise("search", {
-    q: "Ally Avocado",
-    field: "name"
-  });
-  Assert.equal(contacts.length, 1, "There should be one contact found");
-  compareContacts(contacts[0], kContacts[0]);
-
-  // Test searching for multiple contacts.
-  contacts = yield LoopContacts.promise("search", {
-    q: "google",
-    field: "category"
-  });
-  Assert.equal(contacts.length, 2, "There should be two contacts found");
-
-  // Test searching for telephone numbers.
-  contacts = yield LoopContacts.promise("search", {
-    q: "+31612345678",
-    field: "tel"
-  });
-  Assert.equal(contacts.length, 1, "There should be one contact found");
-  compareContacts(contacts[0], kContacts[0]);
-
-  // Test searching for telephone numbers without prefixes.
-  contacts = yield LoopContacts.promise("search", {
-    q: "5551234",
-    field: "tel"
-  });
-  Assert.equal(contacts.length, 1, "There should be one contact found");
-  compareContacts(contacts[0], kContacts[1]);
-});
deleted file mode 100644
--- a/browser/components/loop/test/mochitest/fixtures/google_auth.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head><title>Success code=test-code</title></head>
-<body>Le Code.</body>
-</html>
deleted file mode 100644
--- a/browser/components/loop/test/mochitest/fixtures/google_contacts.txt
+++ /dev/null
@@ -1,140 +0,0 @@
-<?xml version='1.0' encoding='UTF-8'?>
-<feed gd:etag="W/&quot;DUQNRHc8cCt7I2A9XRdSF04.&quot;" xmlns="http://www.w3.org/2005/Atom" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gContact="http://schemas.google.com/contact/2008" xmlns:gd="http://schemas.google.com/g/2005" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/">
-  <id>tester@mochi.com</id>
-  <updated>2014-09-26T13:16:35.978Z</updated>
-  <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
-  <title>Mochi Tester's Contacts</title>
-  <link href="http://www.google.com/" rel="alternate" type="text/html"/>
-  <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml"/>
-  <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#post" type="application/atom+xml"/>
-  <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/batch" rel="http://schemas.google.com/g/2005#batch" type="application/atom+xml"/>
-  <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full?max-results=25" rel="self" type="application/atom+xml"/>
-  <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full?start-index=26&amp;max-results=25" rel="next" type="application/atom+xml"/>
-  <author>
-    <name>Mochi Tester</name>
-    <email>tester@mochi.com</email>
-  </author>
-  <generator uri="http://www.google.com/m8/feeds" version="1.0">Contacts</generator>
-  <openSearch:totalResults>25</openSearch:totalResults>
-  <openSearch:startIndex>1</openSearch:startIndex>
-  <openSearch:itemsPerPage>10000000</openSearch:itemsPerPage>
-  <entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
-    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/0</id>
-    <updated>2012-08-17T23:50:36.892Z</updated>
-    <app:edited xmlns:app="http://www.w3.org/2007/app">2012-08-17T23:50:36.892Z</app:edited>
-    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
-    <title>John Smith</title>
-    <link gd:etag="&quot;Ug92D34SfCt7I2BmLHJTRgVzTlgrJXEAU08.&quot;" href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/0" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/0" rel="self" type="application/atom+xml"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/0" rel="edit" type="application/atom+xml"/>
-    <gd:name>
-      <gd:fullName>John Smith</gd:fullName>
-      <gd:givenName>John</gd:givenName>
-      <gd:familyName>Smith</gd:familyName>
-    </gd:name>
-    <gd:email address="john.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
-    <gContact:website href="http://www.google.com/profiles/109576547678240773721" rel="profile"/>
-    <gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
-  </entry>
-  <entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
-    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/1</id>
-    <updated>2012-08-17T23:50:36.892Z</updated>
-    <app:edited xmlns:app="http://www.w3.org/2007/app">2012-08-17T23:50:36.892Z</app:edited>
-    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
-    <title>Jane Smith</title>
-    <link gd:etag="&quot;WA9BY1xFWit7I2BhLEkieCxLHEYTGCYuNxo.&quot;" href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/1" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/1" rel="self" type="application/atom+xml"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/1" rel="edit" type="application/atom+xml"/>
-    <gd:name>
-      <gd:fullName>Jane Smith</gd:fullName>
-      <gd:givenName>Jane</gd:givenName>
-      <gd:familyName>Smith</gd:familyName>
-    </gd:name>
-    <gd:email address="jane.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
-    <gContact:website href="http://www.google.com/profiles/112886528199784431028" rel="profile"/>
-    <gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
-  </entry>
-  <entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
-    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/2</id>
-    <updated>2012-08-17T23:50:36.892Z</updated>
-    <app:edited xmlns:app="http://www.w3.org/2007/app">2012-08-17T23:50:36.892Z</app:edited>
-    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
-    <title>Davy Randall Jones</title>
-    <link gd:etag="&quot;KiV2PkYRfCt7I2BuD1AzEBFxD1VcGjwBUyA.&quot;" href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/2" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/2" rel="self" type="application/atom+xml"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/2" rel="edit" type="application/atom+xml"/>
-    <gd:name>
-      <gd:fullName>Davy Randall Jones</gd:fullName>
-      <gd:givenName>Davy Randall</gd:givenName>
-      <gd:familyName>Jones</gd:familyName>
-    </gd:name>
-    <gd:email address="davy.jones@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
-    <gContact:website href="http://www.google.com/profiles/109710625881478599011" rel="profile"/>
-    <gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
-  </entry>
-  <entry gd:etag="&quot;Q3w7ezVSLit7I2A9WB5WGUkNRgE.&quot;">
-    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/3</id>
-    <updated>2007-08-01T05:45:52.203Z</updated>
-    <app:edited xmlns:app="http://www.w3.org/2007/app">2007-08-01T05:45:52.203Z</app:edited>
-    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
-    <title/>
-    <link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/3" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="self" type="application/atom+xml"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="edit" type="application/atom+xml"/>
-    <gd:email address="noname@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
-    <gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
-  </entry>
-  <entry gd:etag="&quot;Q3w7ezVSLit7I2A9WB5WGUkNRgE.&quot;">
-    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/7</id>
-    <updated>2007-08-01T05:45:52.203Z</updated>
-    <app:edited xmlns:app="http://www.w3.org/2007/app">2007-08-01T05:45:52.203Z</app:edited>
-    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
-    <title/>
-    <link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/7" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="self" type="application/atom+xml"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="edit" type="application/atom+xml"/>
-    <gd:email address="lycnix" primary="true" rel="http://schemas.google.com/g/2005#other"/>
-    <gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
-  </entry>
-  <entry gd:etag="&quot;RXkzfjVSLit7I2A9XRdRGUgITgA.&quot;">
-    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/8</id>
-    <updated>2014-10-10T14:55:44.786Z</updated>
-    <app:edited xmlns:app="http://www.w3.org/2007/app">2014-10-10T14:55:44.786Z</app:edited>
-    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
-    <title/>
-    <link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/8" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/8" rel="self" type="application/atom+xml"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/8" rel="edit" type="application/atom+xml"/>
-    <gd:phoneNumber rel="http://schemas.google.com/g/2005#mobile" uri="tel:+31-6-12345678">0612345678</gd:phoneNumber>
-    <gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
-  </entry>
-  <entry gd:etag="&quot;SX8-ejVSLit7I2A9XRdQFUkDRgY.&quot;">
-    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/6</id>
-    <updated>2014-10-17T12:32:08.152Z</updated>
-    <app:edited xmlns:app="http://www.w3.org/2007/app">2014-10-17T12:32:08.152Z</app:edited>
-    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
-    <title/>
-    <link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/6" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/6" rel="self" type="application/atom+xml"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/6" rel="edit" type="application/atom+xml"/>
-    <gd:phoneNumber rel="http://schemas.google.com/g/2005#mobile">215234523452345</gd:phoneNumber>
-    <gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
-  </entry>
-  <entry gd:etag="&quot;Rn8zejVSLit7I2A9WhVRFUQOQQc.&quot;">
-    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/9</id>
-    <updated>2012-03-24T13:10:37.182Z</updated>
-    <app:edited xmlns:app="http://www.w3.org/2007/app">2012-03-24T13:10:37.182Z</app:edited>
-    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
-    <title>Little Smith</title>
-    <link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/9" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/9" rel="self" type="application/atom+xml"/>
-    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/9" rel="edit" type="application/atom+xml"/>
-    <gd:name>
-      <gd:fullName>Little Smith</gd:fullName>
-      <gd:givenName>Little</gd:givenName>
-      <gd:familyName>Smith</gd:familyName>
-    </gd:name>
-    <gd:email address="littlebabysmith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
-    <gContact:website href="http://www.google.com/profiles/111456826635924971693" rel="profile"/>
-  </entry>
-</feed>
deleted file mode 100644
--- a/browser/components/loop/test/mochitest/fixtures/google_groups.txt
+++ /dev/null
@@ -1,56 +0,0 @@
-<?xml version='1.0' encoding='UTF-8'?>
-<feed gd:etag="W/&quot;CEIAQngzfyt7I2A9XRdXFEQ.&quot;" xmlns="http://www.w3.org/2005/Atom" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gContact="http://schemas.google.com/contact/2008" xmlns:gd="http://schemas.google.com/g/2005" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/">
-  <id>tester@mochi.com</id>
-  <updated>2014-10-28T10:35:43.687Z</updated>
-  <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
-  <title>Mochi Tester's Contact Groups</title>
-  <link href="http://www.google.com/" rel="alternate" type="text/html"/>
-  <link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml"/>
-  <link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#post" type="application/atom+xml"/>
-  <link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/batch" rel="http://schemas.google.com/g/2005#batch" type="application/atom+xml"/>
-  <link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full?max-results=10000000" rel="self" type="application/atom+xml"/>
-  <author>
-    <name>Mochi Tester</name>
-    <email>tester@mochi.com</email>
-  </author>
-  <generator uri="http://www.google.com/m8/feeds" version="1.0">Contacts</generator>
-  <openSearch:totalResults>4</openSearch:totalResults>
-  <openSearch:startIndex>1</openSearch:startIndex>
-  <openSearch:itemsPerPage>10000000</openSearch:itemsPerPage>
-  <entry gd:etag="&quot;YDwreyM.&quot;">
-    <id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6</id>
-    <updated>1970-01-01T00:00:00.000Z</updated>
-    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
-    <title>System Group: My Contacts</title>
-    <content>System Group: My Contacts</content>
-    <link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/6" rel="self" type="application/atom+xml"/>
-    <gContact:systemGroup id="Contacts"/>
-  </entry>
-  <entry gd:etag="&quot;YDwreyM.&quot;">
-    <id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/d</id>
-    <updated>1970-01-01T00:00:00.000Z</updated>
-    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
-    <title>System Group: Friends</title>
-    <content>System Group: Friends</content>
-    <link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/d" rel="self" type="application/atom+xml"/>
-    <gContact:systemGroup id="Friends"/>
-  </entry>
-  <entry gd:etag="&quot;YDwreyM.&quot;">
-    <id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/e</id>
-    <updated>1970-01-01T00:00:00.000Z</updated>
-    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
-    <title>System Group: Family</title>
-    <content>System Group: Family</content>
-    <link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/e" rel="self" type="application/atom+xml"/>
-    <gContact:systemGroup id="Family"/>
-  </entry>
-  <entry gd:etag="&quot;YDwreyM.&quot;">
-    <id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/f</id>
-    <updated>1970-01-01T00:00:00.000Z</updated>
-    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
-    <title>System Group: Coworkers</title>
-    <content>System Group: Coworkers</content>
-    <link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/f" rel="self" type="application/atom+xml"/>
-    <gContact:systemGroup id="Coworkers"/>
-  </entry>
-</feed>
deleted file mode 100644
--- a/browser/components/loop/test/mochitest/fixtures/google_token.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "access_token": "test-token"
-}
deleted file mode 100644
--- a/browser/components/loop/test/mochitest/google_service.sjs
+++ /dev/null
@@ -1,157 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-const {classes: Cc, interfaces: Ci, Constructor: CC} = Components;
-const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
-                             "nsIBinaryInputStream",
-                             "setInputStream");
-
-function handleRequest(req, res) {
-  try {
-    reallyHandleRequest(req, res);
-  } catch (ex) {
-    res.setStatusLine("1.0", 200, "AlmostOK");
-    let msg = "Error handling request: " + ex + "\n" + ex.stack;
-    log(msg);
-    res.write(msg);
-  }
-}
-
-function log(msg) {
-  // dump("GOOGLE-SERVER-MOCK: " + msg + "\n");
-}
-
-const kBasePath = "browser/browser/components/loop/test/mochitest/fixtures/";
-
-const kStatusCodes = {
-  400: "Bad Request",
-  401: "Unauthorized",
-  403: "Forbidden",
-  404: "Not Found",
-  405: "Method Not Allowed",
-  500: "Internal Server Error",
-  501: "Not Implemented",
-  503: "Service Unavailable"
-};
-
-function HTTPError(code = 500, message) {
-  this.code = code;
-  this.name = kStatusCodes[code] || "HTTPError";
-  this.message = message || this.name;
-}
-HTTPError.prototype = new Error();
-HTTPError.prototype.constructor = HTTPError;
-
-function sendError(res, err) {
-  if (!(err instanceof HTTPError)) {
-    err = new HTTPError(typeof err == "number" ? err : 500,
-                        err.message || typeof err == "string" ? err : "");
-  }
-  res.setStatusLine("1.1", err.code, err.name);
-  res.write(err.message);
-}
-
-function parseQuery(query, params = {}) {
-  for (let param of query.replace(/^[?&]/, "").split(/(?:&|\?)/)) {
-    param = param.split("=");
-    if (!param[0])
-      continue;
-    params[unescape(param[0])] = unescape(param[1]);
-  }
-  return params;
-}
-
-function getRequestBody(req) {
-  let avail;
-  let bytes = [];
-  let body = new BinaryInputStream(req.bodyInputStream);
-
-  while ((avail = body.available()) > 0)
-    Array.prototype.push.apply(bytes, body.readByteArray(avail));
-
-  return String.fromCharCode.apply(null, bytes);
-}
-
-function getInputStream(path) {
-  let file = Cc["@mozilla.org/file/directory_service;1"]
-               .getService(Ci.nsIProperties)
-               .get("CurWorkD", Ci.nsILocalFile);
-  for (let part of path.split("/"))
-    file.append(part);
-  let fileStream  = Cc["@mozilla.org/network/file-input-stream;1"]
-                      .createInstance(Ci.nsIFileInputStream);
-  fileStream.init(file, 1, 0, false);
-  return fileStream;
-}
-
-function checkAuth(req) {
-  if (!req.hasHeader("Authorization"))
-    throw new HTTPError(401, "No Authorization header provided.");
-
-  let auth = req.getHeader("Authorization");
-  if (auth != "Bearer test-token")
-    throw new HTTPError(401, "Invalid Authorization header content: '" + auth + "'");
-}
-
-function reallyHandleRequest(req, res) {
-  log("method: " + req.method);
-
-  let body = getRequestBody(req);
-  log("body: " + body);
-
-  let contentType = req.hasHeader("Content-Type") ? req.getHeader("Content-Type") : null;
-  log("contentType: " + contentType);
-
-  let params = parseQuery(req.queryString);
-  parseQuery(body, params);
-  log("params: " + JSON.stringify(params));
-
-  // Delegate an authentication request to the correct handler.
-  if ("action" in params) {
-    methodHandlers[params.action](req, res, params);
-  } else {
-    sendError(res, 501);
-  }
-}
-
-function respondWithFile(res, fileName, mimeType) {
-  res.setStatusLine("1.1", 200, "OK");
-  res.setHeader("Content-Type", mimeType);
-
-  let inputStream = getInputStream(kBasePath + fileName);
-  res.bodyOutputStream.writeFrom(inputStream, inputStream.available());
-  inputStream.close();
-}
-
-const methodHandlers = {
-  auth: function(req, res, params) {
-    respondWithFile(res, "google_auth.txt", "text/html");
-  },
-
-  token: function(req, res, params) {
-    respondWithFile(res, "google_token.txt", "application/json");
-  },
-
-  contacts: function(req, res, params) {
-    try {
-      checkAuth(req);
-    } catch (ex) {
-      sendError(res, ex, ex.code);
-      return;
-    }
-
-    respondWithFile(res, "google_contacts.txt", "text/xml");
-  },
-
-  groups: function(req, res, params) {
-    try {
-      checkAuth(req);
-    } catch (ex) {
-      sendError(res, ex, ex.code);
-    }
-
-    respondWithFile(res, "google_groups.txt", "text/xml");
-  }
-};
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -285,19 +285,16 @@ user_pref("dom.apps.customization.enable
 // Don't fetch or send directory tiles data from real servers
 user_pref("browser.newtabpage.directory.source", 'data:application/json,{"testing":1}');
 user_pref("browser.newtabpage.directory.ping", "");
 
 // Enable Loop
 user_pref("loop.debug.loglevel", "All");
 user_pref("loop.enabled", true);
 user_pref("loop.throttled", false);
-user_pref("loop.oauth.google.URL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=");
-user_pref("loop.oauth.google.getContactsURL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=contacts");
-user_pref("loop.oauth.google.getGroupsURL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=groups");
 user_pref("loop.server", "http://%(server)s/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?");
 user_pref("loop.CSP","default-src 'self' about: file: chrome: data: wss://* http://* https://*");
 
 // Ensure UITour won't hit the network
 user_pref("browser.uitour.pinnedTabUrl", "http://%(server)s/uitour-dummy/pinnedTab");
 user_pref("browser.uitour.url", "http://%(server)s/uitour-dummy/tour");
 
 // Tell the search service we are running in the US.  This also has the desired