summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/newmailaccount
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/newmailaccount')
-rw-r--r--comm/mail/components/newmailaccount/content/accountProvisioner.js892
-rw-r--r--comm/mail/components/newmailaccount/content/accountProvisioner.xhtml226
-rw-r--r--comm/mail/components/newmailaccount/content/provisionerCheckout.js157
-rw-r--r--comm/mail/components/newmailaccount/content/uriListener.js281
-rw-r--r--comm/mail/components/newmailaccount/jar.mn9
-rw-r--r--comm/mail/components/newmailaccount/moz.build6
6 files changed, 1571 insertions, 0 deletions
diff --git a/comm/mail/components/newmailaccount/content/accountProvisioner.js b/comm/mail/components/newmailaccount/content/accountProvisioner.js
new file mode 100644
index 0000000000..14ba69c515
--- /dev/null
+++ b/comm/mail/components/newmailaccount/content/accountProvisioner.js
@@ -0,0 +1,892 @@
+/* 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";
+
+/* globals MsgAccountManager, MozElements */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PluralForm: "resource://gre/modules/PluralForm.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AccountCreationUtils:
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm",
+
+ UIDensity: "resource:///modules/UIDensity.jsm",
+ UIFontSize: "resource:///modules/UIFontSize.jsm",
+});
+
+var { gAccountSetupLogger } = AccountCreationUtils;
+
+// AbortController to handle timeouts and abort the fetch requests.
+var gAbortController;
+var RETRY_TIMEOUT = 5000; // 5 seconds
+var CONNECTION_TIMEOUT = 15000; // 15 seconds
+var MAX_SMALL_ADDRESSES = 2;
+
+// Keep track of the prefers-reduce-motion media query for JS based animations.
+var gReducedMotion;
+
+// The main 3 Pane Window that we need to define on load in order to properly
+// update the UI when a new account is created.
+var gMainWindow;
+
+// Define window event listeners.
+window.addEventListener("load", () => {
+ gAccountProvisioner.onLoad();
+});
+window.addEventListener("unload", () => {
+ gAccountProvisioner.onUnload();
+});
+
+// Object to collect all the extra providers attributes to be used when
+// building the URL for the API call to purchase an item.
+var storedData = {};
+
+/**
+ * Helper method to split a value based on its first available blank space.
+ *
+ * @param {string} str - The string to split.
+ * @returns {Array} - An array with the generated first and last name.
+ */
+function splitName(str) {
+ let i = str.lastIndexOf(" ");
+ if (i >= 1) {
+ return [str.substring(0, i), str.substring(i + 1)];
+ }
+ return [str, ""];
+}
+
+/**
+ * Quick and simple HTML sanitization.
+ *
+ * @param {string} inputID - The ID of the currently used input field.
+ * @returns {string} - The HTML sanitized input value.
+ */
+function sanitizeName(inputID) {
+ let div = document.createElement("div");
+ div.textContent = document.getElementById(inputID).value;
+ return div.innerHTML.trim();
+}
+
+/**
+ * Replace occurrences of placeholder with the given node
+ *
+ * @param aTextContainer {Node} - DOM node containing the text child
+ * @param aTextNode {Node} - Text node containing the text, child of the aTextContainer
+ * @param aPlaceholder {String} - String to look for in aTextNode's textContent
+ * @param aReplacement {Node} - DOM node to insert instead of the found replacement
+ */
+function insertHTMLReplacement(
+ aTextContainer,
+ aTextNode,
+ aPlaceholder,
+ aReplacement
+) {
+ if (aTextNode.textContent.includes(aPlaceholder)) {
+ let placeIndex = aTextNode.textContent.indexOf(aPlaceholder);
+ let restNode = aTextNode.splitText(placeIndex + aPlaceholder.length);
+ aTextContainer.insertBefore(aReplacement, restNode);
+ let placeholderNode = aTextNode.splitText(placeIndex);
+ placeholderNode.remove();
+ }
+}
+
+/**
+ * This is our controller for the entire account provisioner setup process.
+ */
+var gAccountProvisioner = {
+ // If the setup wizard has already been initialized.
+ _isInited: false,
+ // If the data fetching of the providers is currently in progress.
+ _isLoadingProviders: false,
+ // If the providers have already been loaded.
+ _isLoadedProviders: false,
+ // Store a timeout retry in case fetching the providers fails.
+ _loadProviderRetryId: null,
+ // Array containing all fetched providers.
+ allProviders: [],
+ // Array containing all fetched provider names that only offer email.
+ mailProviders: [],
+ // Array containing all fetched provider names that also offer custom domain.
+ domainProviders: [],
+ // Handle a timeout to abort the fetch requests.
+ timeoutId: null,
+
+ /**
+ * Returns the URL for retrieving suggested names from the selected providers.
+ */
+ get suggestFromName() {
+ return Services.prefs.getCharPref("mail.provider.suggestFromName");
+ },
+
+ /**
+ * Initialize the main notification box for the account setup process.
+ */
+ get notificationBox() {
+ if (!this._notificationBox) {
+ this._notificationBox = new MozElements.NotificationBox(element => {
+ element.setAttribute("notificationside", "top");
+ document
+ .getElementById("accountProvisionerNotifications")
+ .append(element);
+ });
+ }
+ return this._notificationBox;
+ },
+
+ /**
+ * Clear currently running async fetches and reset important variables.
+ */
+ onUnload() {
+ this.clearAbortTimeout();
+ gAbortController.abort();
+ gAbortController = null;
+ },
+
+ async onLoad() {
+ // We can only init once, so bail out if we've been called again.
+ if (this._isInited) {
+ return;
+ }
+
+ gAccountSetupLogger.debug("Initializing provisioner wizard");
+ gReducedMotion = window.matchMedia(
+ "(prefers-reduced-motion: reduce)"
+ ).matches;
+
+ // Store the main window.
+ gMainWindow = Services.wm.getMostRecentWindow("mail:3pane");
+
+ // Initialize the fetch abort controller.
+ gAbortController = new AbortController();
+
+ // If we have a name stored, populate the search field with it.
+ if ("@mozilla.org/userinfo;1" in Cc) {
+ let userInfo = Cc["@mozilla.org/userinfo;1"].getService(Ci.nsIUserInfo);
+ // Assume that it's a genuine full name if it includes a space.
+ if (userInfo.fullname.includes(" ")) {
+ document.getElementById("mailName").value = userInfo.fullname;
+ document.getElementById("domainName").value = userInfo.fullname;
+ }
+ }
+
+ this.setupEventListeners();
+ await this.tryToFetchProviderList();
+
+ gAccountSetupLogger.debug("Provisioner wizard init complete.");
+
+ // Move the focus on the first available field.
+ document.getElementById("mailName").focus();
+ this._isInited = true;
+
+ Services.telemetry.scalarAdd("tb.account.opened_account_provisioner", 1);
+
+ UIDensity.registerWindow(window);
+ UIFontSize.registerWindow(window);
+ },
+
+ /**
+ * Set up the event listeners for the static elements in the page.
+ */
+ setupEventListeners() {
+ document.getElementById("cancelButton").onclick = () => {
+ window.close();
+ };
+
+ document.getElementById("existingButton").onclick = () => {
+ window.close();
+ gMainWindow.postMessage("open-account-setup-tab", "*");
+ };
+
+ document.getElementById("backButton").onclick = () => {
+ this.backToSetupView();
+ };
+ },
+
+ /**
+ * Return to the initial view without resetting any existing data.
+ */
+ backToSetupView() {
+ this.clearAbortTimeout();
+ this.clearNotifications();
+
+ // Clear search results.
+ let mailResultsArea = document.getElementById("mailResultsArea");
+ while (mailResultsArea.hasChildNodes()) {
+ mailResultsArea.lastChild.remove();
+ }
+ let domainResultsArea = document.getElementById("domainResultsArea");
+ while (domainResultsArea.hasChildNodes()) {
+ domainResultsArea.lastChild.remove();
+ }
+
+ // Update the UI to show the initial view.
+ document.getElementById("mailSearch").hidden = false;
+ document.getElementById("domainSearch").hidden = false;
+ document.getElementById("mailSearchResults").hidden = true;
+ document.getElementById("domainSearchResults").hidden = true;
+
+ // Update the buttons visibility.
+ document.getElementById("backButton").hidden = true;
+ document.getElementById("cancelButton").hidden = false;
+ document.getElementById("existingButton").hidden = false;
+
+ // Move the focus back on the first available field.
+ document.getElementById("mailName").focus();
+ },
+
+ /**
+ * Show a loading notification.
+ */
+ async startLoadingState(stringName) {
+ this.clearNotifications();
+
+ let notificationMessage = await document.l10n.formatValue(stringName);
+
+ gAccountSetupLogger.debug(`Status msg: ${notificationMessage}`);
+
+ let notification = this.notificationBox.appendNotification(
+ "accountSetupLoading",
+ {
+ label: notificationMessage,
+ priority: this.notificationBox.PRIORITY_INFO_LOW,
+ },
+ null
+ );
+ notification.setAttribute("align", "center");
+
+ // Hide the close button to prevent dismissing the notification.
+ notification.removeAttribute("dismissable");
+
+ this.ensureVisibleNotification();
+ },
+
+ /**
+ * Show an error notification in case something went wrong.
+ *
+ * @param {string} stringName - The name of the fluent string that needs to
+ * be attached to the notification.
+ * @param {boolean} isMsgError - True if the message comes from a server error
+ * response or try/catch.
+ */
+ async showErrorNotification(stringName, isMsgError) {
+ gAccountSetupLogger.debug(`Status error: ${stringName}`);
+
+ // Always remove any leftover notification before creating a new one.
+ this.clearNotifications();
+
+ // Fetch the fluent string only if this is not an error message coming from
+ // a previous method.
+ let notificationMessage = isMsgError
+ ? stringName
+ : await document.l10n.formatValue(stringName);
+
+ let notification = this.notificationBox.appendNotification(
+ "accountProvisionerError",
+ {
+ label: notificationMessage,
+ priority: this.notificationBox.PRIORITY_WARNING_MEDIUM,
+ },
+ null
+ );
+
+ // Hide the close button to prevent dismissing the notification.
+ notification.removeAttribute("dismissable");
+
+ this.ensureVisibleNotification();
+ },
+
+ async showSuccessNotification(stringName) {
+ // Always remove any leftover notification before creating a new one.
+ this.clearNotifications();
+
+ let notification = this.notificationBox.appendNotification(
+ "accountProvisionerSuccess",
+ {
+ label: await document.l10n.formatValue(stringName),
+ priority: this.notificationBox.PRIORITY_WARNING_MEDIUM,
+ },
+ null
+ );
+ notification.setAttribute("type", "success");
+ // Hide the close button to prevent dismissing the notification.
+ notification.removeAttribute("dismissable");
+
+ this.ensureVisibleNotification();
+ },
+
+ /**
+ * Clear all leftover notifications.
+ */
+ clearNotifications() {
+ this.notificationBox.removeAllNotifications();
+ },
+
+ /**
+ * Event handler for when the user selects an address by clicking on the price
+ * button for that address. This function spawns the content tab for the
+ * address order form, and then closes the Account Provisioner tab.
+ *
+ * @param {string} providerId - The ID of the chosen provider.
+ * @param {string} email - The chosen email address.
+ * @param {boolean} [isDomain=false] - If the fetched data comes from a domain
+ * search form.
+ */
+ onAddressSelected(providerId, email, isDomain = false) {
+ gAccountSetupLogger.debug("An address was selected by the user.");
+ let provider = this.allProviders.find(p => p.id == providerId);
+
+ let url = provider.api;
+ let inputID = isDomain ? "domainName" : "mailName";
+ let [firstName, lastName] = splitName(sanitizeName(inputID));
+ // Replace the variables in the API url.
+ url = url.replace("{firstname}", firstName);
+ url = url.replace("{lastname}", lastName);
+ url = url.replace("{email}", email);
+
+ // And add the extra data.
+ let data = storedData[providerId];
+ delete data.provider;
+ for (let name in data) {
+ url += `${!url.includes("?") ? "?" : "&"}${name}=${encodeURIComponent(
+ data[name]
+ )}`;
+ }
+
+ gAccountSetupLogger.debug("Opening up a contentTab with the order form.");
+ // Open the checkout content tab.
+ let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane");
+ let tabmail = mail3Pane.document.getElementById("tabmail");
+ tabmail.openTab("provisionerCheckoutTab", {
+ url,
+ realName: (firstName + " " + lastName).trim(),
+ email,
+ });
+
+ let providerHostname = new URL(url).hostname;
+ // Collect telemetry on which provider was selected for a new email account.
+ Services.telemetry.keyedScalarAdd(
+ "tb.account.selected_account_from_provisioner",
+ providerHostname,
+ 1
+ );
+
+ // The user has made a selection. Close the provisioner window and let the
+ // provider setup process take place in a dedicated tab.
+ window.close();
+ },
+
+ /**
+ * Attempt to fetch the provider list from the server.
+ */
+ async tryToFetchProviderList() {
+ // If we're already in the middle of getting the provider list, or we
+ // already got it before, bail out.
+ if (this._isLoadingProviders || this._isLoadedProviders) {
+ return;
+ }
+
+ this._isLoadingProviders = true;
+
+ // If there's a timeout ID for waking the account provisioner, clear it.
+ if (this._loadProviderRetryId) {
+ window.clearTimeout(this._loadProviderRetryId);
+ this._loadProviderRetryId = null;
+ }
+
+ await this.startLoadingState("account-provisioner-fetching-provisioners");
+
+ let providerListUrl = Services.prefs.getCharPref(
+ "mail.provider.providerList"
+ );
+
+ gAccountSetupLogger.debug(
+ `Trying to populate provider list from ${providerListUrl}…`
+ );
+
+ try {
+ let res = await fetch(providerListUrl, {
+ signal: gAbortController.signal,
+ });
+ this.startAbortTimeout();
+ let data = await res.json();
+ this.populateProvidersLists(data);
+ } catch (error) {
+ // Ugh, we couldn't get the JSON file. Maybe we're not online. Or maybe
+ // the server is down, or the file isn't being served. Regardless, if
+ // we get here, none of this stuff is going to work.
+ this._loadProviderRetryId = window.setTimeout(
+ () => this.tryToFetchProviderList(),
+ RETRY_TIMEOUT
+ );
+ this._isLoadingProviders = false;
+ this.showErrorNotification("account-provisioner-connection-issues");
+ gAccountSetupLogger.warn(`Failed to populate providers: ${error}`);
+ }
+ },
+
+ /**
+ * Validate a provider fetched during an API request to be sure we have all
+ * the necessary fields to complete a setup process.
+ *
+ * @param {object} provider - The fetched provider.
+ * @returns {boolean} - True if all the fields in the provider match the
+ * required fields.
+ */
+ providerHasCorrectFields(provider) {
+ let result = true;
+
+ let required = [
+ "id",
+ "label",
+ "paid",
+ "languages",
+ "api",
+ "tos_url",
+ "privacy_url",
+ "sells_domain",
+ ];
+
+ for (let field of required) {
+ let fieldExists = field in provider;
+ result &= fieldExists;
+
+ if (!fieldExists) {
+ gAccountSetupLogger.warn(
+ `A provider did not have the field ${field}, and will be skipped.`
+ );
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * Take the fetched providers, create checkboxes, icons and labels, and insert
+ * them below the corresponding search input.
+ *
+ * @param {?object} data - The object containing all fetched providers.
+ */
+ populateProvidersLists(data) {
+ gAccountSetupLogger.debug("Populating the provider list");
+ this.clearAbortTimeout();
+
+ if (!data || !data.length) {
+ gAccountSetupLogger.warn(
+ "The provider list we got back from the server was empty!"
+ );
+ this.showErrorNotification("account-provisioner-connection-issues");
+ return;
+ }
+
+ let mailProviderList = document.getElementById("mailProvidersList");
+ let domainProviderList = document.getElementById("domainProvidersList");
+
+ this.allProviders = data;
+ this.mailProviders = [];
+ this.domainProviders = [];
+
+ for (let provider of data) {
+ if (!this.providerHasCorrectFields(provider)) {
+ gAccountSetupLogger.warn(
+ "A provider had incorrect fields, and has been skipped"
+ );
+ continue;
+ }
+
+ let entry = document.createElement("li");
+ entry.setAttribute("id", provider.id);
+
+ if (provider.icon) {
+ let icon = document.createElement("img");
+ icon.setAttribute("src", provider.icon);
+ icon.setAttribute("alt", "");
+ entry.appendChild(icon);
+ }
+
+ let name = document.createElement("span");
+ name.textContent = provider.label;
+ entry.appendChild(name);
+
+ if (provider.sells_domain) {
+ domainProviderList.appendChild(entry);
+ this.domainProviders.push(provider.id);
+ } else {
+ mailProviderList.appendChild(entry);
+ this.mailProviders.push(provider.id);
+ }
+ }
+
+ this._isLoadedProviders = true;
+ this.clearNotifications();
+ },
+
+ /**
+ * Enable or disable the form fields when a fetch request starts or ends.
+ *
+ * @param {boolean} state - True if a fetch request is in progress.
+ */
+ updateSearchingState(state) {
+ for (let element of document.querySelectorAll(".disable-on-submit")) {
+ element.disabled = state;
+ }
+ },
+
+ /**
+ * Search for available email accounts.
+ *
+ * @param {DOMEvent} event - The form submit event.
+ */
+ async onMailFormSubmit(event) {
+ // Always prevent the actual form submission.
+ event.preventDefault();
+
+ // Quick HTML sanitization.
+ let name = sanitizeName("mailName");
+
+ // Bail out if the user didn't type anything.
+ if (!name) {
+ return;
+ }
+
+ let resultsArea = document.getElementById("mailSearchResults");
+ resultsArea.hidden = true;
+
+ this.startLoadingState("account-provisioner-searching-email");
+ let data = await this.submitFormRequest(name, this.mailProviders.join(","));
+ this.clearAbortTimeout();
+
+ let count = this.populateSearchResults(data);
+ if (!count) {
+ // Bail out if we didn't get any usable data.
+ gAccountSetupLogger.warn(
+ "We got nothing back from the server for search results!"
+ );
+ this.showErrorNotification("account-provisioner-searching-error");
+ return;
+ }
+
+ let resultsTitle = document.getElementById("mailResultsTitle");
+ let resultsString = await document.l10n.formatValue(
+ "account-provisioner-results-title",
+ { count }
+ );
+ // Attach the sanitized search terms to avoid HTML conversion in fluent.
+ resultsTitle.textContent = `${resultsString} "${name}"`;
+
+ // Hide the domain section.
+ document.getElementById("domainSearch").hidden = true;
+ // Show the results area.
+ resultsArea.hidden = false;
+ // Update the buttons visibility.
+ document.getElementById("cancelButton").hidden = true;
+ document.getElementById("existingButton").hidden = true;
+ // Show the back button.
+ document.getElementById("backButton").hidden = false;
+ },
+
+ /**
+ * Search for available domain names.
+ *
+ * @param {DOMEvent} event - The form submit event.
+ */
+ async onDomainFormSubmit(event) {
+ // Always prevent the actual form submission.
+ event.preventDefault();
+
+ // Quick HTML sanitization.
+ let name = sanitizeName("domainName");
+
+ // Bail out if the user didn't type anything.
+ if (!name) {
+ return;
+ }
+
+ let resultsArea = document.getElementById("domainSearchResults");
+ resultsArea.hidden = true;
+
+ this.startLoadingState("account-provisioner-searching-domain");
+ let data = await this.submitFormRequest(
+ name,
+ this.domainProviders.join(",")
+ );
+ this.clearAbortTimeout();
+
+ let count = this.populateSearchResults(data, true);
+ if (!count) {
+ // Bail out if we didn't get any usable data.
+ gAccountSetupLogger.warn(
+ "We got nothing back from the server for search results!"
+ );
+ this.showErrorNotification("account-provisioner-searching-error");
+ return;
+ }
+
+ let resultsTitle = document.getElementById("domainResultsTitle");
+ let resultsString = await document.l10n.formatValue(
+ "account-provisioner-results-title",
+ { count }
+ );
+ // Attach the sanitized search terms to avoid HTML conversion in fluent.
+ resultsTitle.textContent = `${resultsString} "${name}"`;
+
+ // Hide the mail section.
+ document.getElementById("mailSearch").hidden = true;
+ // Show the results area.
+ resultsArea.hidden = false;
+ // Update the buttons visibility.
+ document.getElementById("cancelButton").hidden = true;
+ document.getElementById("existingButton").hidden = true;
+ // Show the back button.
+ document.getElementById("backButton").hidden = false;
+ },
+
+ /**
+ * Update the UI to show the fetched address data.
+ *
+ * @param {object} data - The fetched data from an email or domain search.
+ * @param {boolean} [isDomain=false] - If the fetched data comes from a domain
+ * search form.
+ */
+ populateSearchResults(data, isDomain = false) {
+ if (!data || !data.length) {
+ return 0;
+ }
+
+ this.clearNotifications();
+
+ let resultsArea = isDomain
+ ? document.getElementById("domainResultsArea")
+ : document.getElementById("mailResultsArea");
+ // Clear previously generated content.
+ while (resultsArea.hasChildNodes()) {
+ resultsArea.lastChild.remove();
+ }
+
+ // Filter out possible errors or empty lists.
+ let validData = data.filter(
+ result => result.succeeded && result.addresses.length
+ );
+
+ if (!validData || !validData.length) {
+ return 0;
+ }
+
+ let providersList = isDomain ? this.domainProviders : this.mailProviders;
+
+ let count = 0;
+ for (let provider of validData) {
+ count += provider.addresses.length;
+
+ // Don't add a provider header if only 1 is currently available.
+ if (providersList.length > 1) {
+ let header = document.createElement("h5");
+ header.classList.add("result-list-header");
+ header.textContent = this.allProviders.find(
+ p => p.id == provider.provider
+ ).label;
+ resultsArea.appendChild(header);
+ }
+
+ let list = document.createElement("ul");
+
+ // Only show a chink of addresses if we got a long list.
+ let isLongList = provider.addresses.length > 5;
+ let addresses = isLongList
+ ? provider.addresses.slice(0, 4)
+ : provider.addresses;
+
+ for (let address of addresses) {
+ list.appendChild(this.createAddressRow(address, provider, isDomain));
+ }
+
+ resultsArea.appendChild(list);
+
+ // If we got more than 5 addresses, create an hidden bug expandable list
+ // with the rest of the data.
+ if (isLongList) {
+ let hiddenList = document.createElement("ul");
+ hiddenList.hidden = true;
+
+ for (let address of provider.addresses.slice(5)) {
+ hiddenList.appendChild(
+ this.createAddressRow(address, provider, isDomain)
+ );
+ }
+
+ let button = document.createElement("button");
+ button.setAttribute("type", "button");
+ button.classList.add("btn-link", "self-center");
+ document.l10n.setAttributes(
+ button,
+ "account-provisioner-all-results-button"
+ );
+ button.onclick = () => {
+ hiddenList.hidden = false;
+ button.hidden = true;
+ };
+
+ resultsArea.appendChild(button);
+ resultsArea.appendChild(hiddenList);
+ }
+ }
+
+ for (let provider of data) {
+ delete provider.succeeded;
+ delete provider.addresses;
+ delete provider.price;
+ storedData[provider.provider] = provider;
+ }
+
+ return count;
+ },
+
+ /**
+ * Create the list item to show the suggested address returned from a search.
+ *
+ * @param {object} address - The address returned from the provider search.
+ * @param {object} provider - The provider from which the address is
+ * @param {boolean} [isDomain=false] - If the fetched data comes from a domain
+ * search form.
+ * available.
+ * @returns {HTMLLIElement}
+ */
+ createAddressRow(address, provider, isDomain = false) {
+ let row = document.createElement("li");
+ row.classList.add("result-item");
+
+ let suggestedAddress = address.address || address;
+
+ let button = document.createElement("button");
+ button.setAttribute("type", "button");
+ button.onclick = () => {
+ this.onAddressSelected(provider.provider, suggestedAddress, isDomain);
+ };
+
+ let leftArea = document.createElement("span");
+ leftArea.classList.add("result-data");
+
+ let name = document.createElement("span");
+ name.classList.add("result-name");
+ name.textContent = suggestedAddress;
+ leftArea.appendChild(name);
+ row.setAttribute("data-label", suggestedAddress);
+
+ let price = document.createElement("span");
+ price.classList.add("result-price");
+
+ // Build the pricing text and handle possible free trials.
+ if (address.price) {
+ if (address.price != 0) {
+ // Some pricing is defined.
+ document.l10n.setAttributes(price, "account-provision-price-per-year", {
+ price: address.price,
+ });
+ } else if (address.price == 0) {
+ // Price is defined by it's zero.
+ document.l10n.setAttributes(price, "account-provisioner-free-account");
+ }
+ } else if (provider.price && provider.price != 0) {
+ // We don't have a price for the current result so let's try to use
+ // the general Provider's price.
+ document.l10n.setAttributes(price, "account-provision-price-per-year", {
+ price: provider.price,
+ });
+ } else {
+ // No price was specified, let's return "Free".
+ document.l10n.setAttributes(price, "account-provisioner-free-account");
+ }
+ leftArea.appendChild(price);
+
+ button.appendChild(leftArea);
+
+ let img = document.createElement("img");
+ document.l10n.setAttributes(img, "account-provisioner-open-in-tab-img");
+ img.setAttribute("alt", "");
+ img.setAttribute("src", "chrome://global/skin/icons/open-in-new.svg");
+ button.appendChild(img);
+
+ row.appendChild(button);
+
+ return row;
+ },
+
+ /**
+ * Fetches a list of suggested email addresses or domain names from a list of
+ * selected providers.
+ *
+ * @param {string} name - The search value typed by the user.
+ * @param {Array} providers - Array of providers to search for.
+ * @returns {object} - A list of available emails or domains.
+ */
+ async submitFormRequest(name, providers) {
+ // If the focused element is disabled by `updateSearchingState`, focus is
+ // lost. Save the focused element to restore it later.
+ let activeElement = document.activeElement;
+ this.updateSearchingState(true);
+
+ let [firstName, lastName] = splitName(name);
+ let url = `${this.suggestFromName}?first_name=${encodeURIComponent(
+ firstName
+ )}&last_name=${encodeURIComponent(lastName)}&providers=${encodeURIComponent(
+ providers
+ )}&version=2`;
+
+ let data;
+ try {
+ let res = await fetch(url, { signal: gAbortController.signal });
+ this.startAbortTimeout();
+ data = await res.json();
+ } catch (error) {
+ gAccountSetupLogger.warn(`Failed to fetch address data: ${error}`);
+ }
+
+ this.updateSearchingState(false);
+ // Restore focus.
+ activeElement.focus();
+ return data;
+ },
+
+ /**
+ * Start a timeout to abort a fetch request based on a time limit.
+ */
+ startAbortTimeout() {
+ this.timeoutId = setTimeout(() => {
+ gAbortController.abort();
+ this.showErrorNotification("account-provisioner-connection-timeout");
+ gAccountSetupLogger.warn("Connection timed out");
+ }, CONNECTION_TIMEOUT);
+ },
+
+ /**
+ * Clear any leftover timeout to prevent an unnecessary fetch abort.
+ */
+ clearAbortTimeout() {
+ if (this.timeoutId) {
+ window.clearTimeout(this.timeoutId);
+ this.timeoutId = null;
+ }
+ },
+
+ /**
+ * Always ensure the notification area is visible when a new notification is
+ * created.
+ */
+ ensureVisibleNotification() {
+ document.getElementById("accountProvisionerNotifications").scrollIntoView({
+ behavior: gReducedMotion ? "auto" : "smooth",
+ block: "start",
+ inline: "nearest",
+ });
+ },
+};
diff --git a/comm/mail/components/newmailaccount/content/accountProvisioner.xhtml b/comm/mail/components/newmailaccount/content/accountProvisioner.xhtml
new file mode 100644
index 0000000000..37f4be1422
--- /dev/null
+++ b/comm/mail/components/newmailaccount/content/accountProvisioner.xhtml
@@ -0,0 +1,226 @@
+<?xml version="1.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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountSetup.css" type="text/css"?>
+
+<!DOCTYPE html>
+
+<html id="accountProvisioner" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title data-l10n-id="account-provisioner-tab-title"></title>
+ <meta name="color-scheme" content="light dark" />
+ <link
+ rel="icon"
+ href="chrome://messenger/skin/icons/new/compact/new-mail.svg"
+ />
+
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/accountProvisioner.ftl" />
+
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/accountUtils.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/newmailaccount/accountProvisioner.js"
+ ></script>
+ </head>
+
+ <body>
+ <header>
+ <h1
+ id="accountProvisionerTitle"
+ data-l10n-id="account-provisioner-title"
+ class="title"
+ ></h1>
+ <p
+ id="accountProvisionerDescription"
+ data-l10n-id="account-provisioner-description"
+ class="description"
+ ></p>
+ </header>
+
+ <section class="main-container">
+ <aside id="setupView" class="column column-wide">
+ <section id="mailSearch">
+ <h3
+ class="service-title"
+ data-l10n-id="account-provisioner-mail-account-title"
+ ></h3>
+
+ <p
+ class="service-description"
+ data-l10n-id="account-provisioner-mail-account-description"
+ >
+ <a
+ href="https://mailfence.com/"
+ data-l10n-name="mailfence-home-link"
+ ></a>
+ </p>
+
+ <form
+ id="mailForm"
+ class="service-form"
+ onsubmit="gAccountProvisioner.onMailFormSubmit(event);"
+ >
+ <div class="service-form-container">
+ <input
+ id="mailName"
+ type="text"
+ data-l10n-id="account-provisioner-mail-input"
+ class="disable-on-submit"
+ autocomplete="off"
+ required="required"
+ />
+ <button
+ type="submit"
+ class="disable-on-submit"
+ data-l10n-id="account-provisioner-search-button"
+ ></button>
+ </div>
+
+ <ul id="mailProvidersList" class="providers-list">
+ <!-- This will be populated in JS. -->
+ </ul>
+ </form>
+
+ <div id="mailSearchResults" hidden="hidden">
+ <h4 id="mailResultsTitle" class="results-title"></h4>
+ <section class="provisioner-results-area">
+ <div id="mailResultsArea" class="results-list"></div>
+ </section>
+ <p
+ data-l10n-id="account-provisioner-mail-results-caption"
+ class="tip-caption"
+ ></p>
+ </div>
+ </section>
+
+ <section id="domainSearch">
+ <h3
+ class="service-title"
+ data-l10n-id="account-provisioner-domain-title"
+ ></h3>
+
+ <p
+ class="service-description"
+ data-l10n-id="account-provisioner-domain-description"
+ >
+ <a href="https://gandi.net/" data-l10n-name="gandi-home-link"></a>
+ </p>
+
+ <form
+ id="domainForm"
+ class="service-form"
+ onsubmit="gAccountProvisioner.onDomainFormSubmit(event);"
+ >
+ <div class="service-form-container">
+ <input
+ id="domainName"
+ type="text"
+ data-l10n-id="account-provisioner-domain-input"
+ class="disable-on-submit"
+ autocomplete="off"
+ required="required"
+ />
+ <button
+ type="submit"
+ class="disable-on-submit"
+ data-l10n-id="account-provisioner-search-button"
+ ></button>
+ </div>
+
+ <ul id="domainProvidersList" class="providers-list">
+ <!-- This will be populated in JS. -->
+ </ul>
+ </form>
+
+ <div id="domainSearchResults" hidden="hidden">
+ <h4 id="domainResultsTitle" class="results-title"></h4>
+ <section class="provisioner-results-area">
+ <div id="domainResultsArea" class="results-list"></div>
+ </section>
+ <p
+ data-l10n-id="account-provisioner-domain-results-caption"
+ class="tip-caption"
+ ></p>
+ </div>
+ </section>
+
+ <div
+ id="accountProvisionerNotifications"
+ class="account-setup-notifications"
+ >
+ <!-- Notifications will be lazily loaded here. -->
+ </div>
+
+ <section class="action-buttons-container provisioner-buttons">
+ <button
+ id="backButton"
+ type="button"
+ data-l10n-id="account-provisioner-button-back"
+ data-l10n-attrs="accesskey"
+ hidden="hidden"
+ ></button>
+ <button
+ id="cancelButton"
+ type="button"
+ data-l10n-id="account-provisioner-button-cancel"
+ data-l10n-attrs="accesskey"
+ ></button>
+ <button
+ id="existingButton"
+ type="button"
+ data-l10n-id="account-provisioner-button-existing"
+ data-l10n-attrs="accesskey"
+ ></button>
+ </section>
+ </aside>
+ <!-- END setupView column -->
+
+ <aside class="column second-column">
+ <article id="step1" class="tip-caption">
+ <img
+ src="chrome://messenger/skin/illustrations/octopus-setup.svg"
+ data-l10n-id="account-provisioner-step1-image"
+ alt=""
+ />
+ <p data-l10n-id="account-provisioner-start-help">
+ <a
+ href="https://www.mozilla.org/privacy/"
+ data-l10n-name="mozilla-privacy-link"
+ ></a>
+ <a
+ href="https://mailfence.com/en/privacy.jsp"
+ data-l10n-name="mailfence-privacy-link"
+ ></a>
+ <a
+ href="https://mailfence.com/en/terms.jsp"
+ data-l10n-name="mailfence-tou-link"
+ ></a>
+ <a
+ href="https://www.gandi.net/contracts/privacy-policy"
+ data-l10n-name="gandi-privacy-link"
+ ></a>
+ <a
+ href="https://www.gandi.net/contracts/terms-of-use"
+ data-l10n-name="gandi-tou-link"
+ ></a>
+ </p>
+ </article>
+ </aside>
+ <!-- END second column-->
+ </section>
+ </body>
+</html>
diff --git a/comm/mail/components/newmailaccount/content/provisionerCheckout.js b/comm/mail/components/newmailaccount/content/provisionerCheckout.js
new file mode 100644
index 0000000000..0fc38c87e5
--- /dev/null
+++ b/comm/mail/components/newmailaccount/content/provisionerCheckout.js
@@ -0,0 +1,157 @@
+/* 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/. */
+
+// mail/base/content/contentAreaClick.js
+/* globals hRefForClickEvent, openLinkExternally */
+// mail/base/content/specialTabs.js
+/* globals specialTabs */
+
+var { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+);
+
+/**
+ * A content tab for the account provisioner. We use Javascript-y magic to
+ * "subclass" specialTabs.contentTabType, and then override the appropriate
+ * members.
+ *
+ * Also note that provisionerCheckoutTab is a singleton (hence the maxTabs: 1).
+ */
+var provisionerCheckoutTabType = Object.create(specialTabs.contentTabType, {
+ name: { value: "provisionerCheckoutTab" },
+ modes: {
+ value: {
+ provisionerCheckoutTab: {
+ type: "provisionerCheckoutTab",
+ maxTabs: 1,
+ },
+ },
+ },
+ _log: {
+ value: new ConsoleAPI({
+ prefix: "mail.provider",
+ maxLogLevel: "warn",
+ maxLogLevelPref: "mail.provider.loglevel",
+ }),
+ },
+});
+
+/**
+ * Here, we're overriding openTab - first we call the openTab of contentTab
+ * (for the context of this provisionerCheckoutTab "aTab") and then passing
+ * special arguments "realName", "email" and "searchEngine" from the caller
+ * of openTab, and passing those to our _setMonitoring function.
+ */
+provisionerCheckoutTabType.openTab = function (aTab, aArgs) {
+ specialTabs.contentTabType.openTab.call(this, aTab, aArgs);
+
+ // Since there's only one tab of this type ever (see the mode definition),
+ // we're OK to stash this stuff here.
+ this._realName = aArgs.realName;
+ this._email = aArgs.email;
+ this._searchEngine = aArgs.searchEngine || "";
+
+ this._setMonitoring(
+ aTab.browser,
+ aArgs.realName,
+ aArgs.email,
+ aArgs.searchEngine
+ );
+};
+
+/**
+ * We're overriding closeTab - first, we call the closeTab of contentTab,
+ * (for the context of this provisionerCheckoutTab "aTab"), and then we
+ * unregister our observer that was registered in _setMonitoring.
+ */
+provisionerCheckoutTabType.closeTab = function (aTab) {
+ specialTabs.contentTabType.closeTab.call(this, aTab);
+ this._log.info("Performing account provisioner cleanup");
+ this._log.info("Removing httpRequestObserver");
+ Services.obs.removeObserver(this._observer, "http-on-examine-response");
+ Services.obs.removeObserver(
+ this._observer,
+ "http-on-examine-cached-response"
+ );
+ Services.obs.removeObserver(this.quitObserver, "mail-unloading-messenger");
+ delete this._observer;
+ this._log.info("Account provisioner cleanup is done.");
+};
+
+/**
+ * Serialize our tab into something we can restore later.
+ */
+provisionerCheckoutTabType.persistTab = function (aTab) {
+ return {
+ tabURI: aTab.browser.currentURI.spec,
+ realName: this._realName,
+ email: this._email,
+ searchEngine: this._searchEngine,
+ };
+};
+
+/**
+ * Re-open the provisionerCheckoutTab with all of the stuff we stashed in
+ * persistTab. This will automatically hook up our monitoring again.
+ */
+provisionerCheckoutTabType.restoreTab = function (aTabmail, aPersistedState) {
+ aTabmail.openTab("provisionerCheckoutTab", {
+ url: aPersistedState.tabURI,
+ realName: aPersistedState.realName,
+ email: aPersistedState.email,
+ searchEngine: aPersistedState.searchEngine,
+ background: true,
+ });
+};
+
+/**
+ * This function registers an observer to watch for HTTP requests where the
+ * contentType contains text/xml.
+ */
+provisionerCheckoutTabType._setMonitoring = function (
+ aBrowser,
+ aRealName,
+ aEmail,
+ aSearchEngine
+) {
+ let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane");
+
+ // We'll construct our special observer (defined in urlListener.js)
+ // that will watch for requests where the contentType contains
+ // text/xml.
+ this._observer = new mail3Pane.httpRequestObserver(aBrowser, {
+ realName: aRealName,
+ email: aEmail,
+ searchEngine: aSearchEngine,
+ });
+
+ // Register our observer
+ Services.obs.addObserver(this._observer, "http-on-examine-response");
+ Services.obs.addObserver(this._observer, "http-on-examine-cached-response");
+ Services.obs.addObserver(this.quitObserver, "mail-unloading-messenger");
+
+ this._log.info("httpRequestObserver wired up.");
+};
+
+/**
+ * This observer listens for the mail-unloading-messenger event fired by each
+ * mail window before they unload. If the mail window is the same window that
+ * this provisionerCheckoutTab belongs to, then we stash a pref so that when
+ * the session restarts, we go straight to the tab, as opposed to showing the
+ * dialog again.
+ */
+provisionerCheckoutTabType.quitObserver = {
+ observe(aSubject, aTopic, aData) {
+ // Make sure we saw the right topic, and that the window that is closing
+ // is the 3pane window that the provisionerCheckoutTab belongs to.
+ if (aTopic == "mail-unloading-messenger" && aSubject === window) {
+ // We quit while the provisionerCheckoutTab was opened. Set our sneaky
+ // pref so that we suppress the dialog on startup.
+ Services.prefs.setBoolPref(
+ "mail.provider.suppress_dialog_on_startup",
+ true
+ );
+ }
+ },
+};
diff --git a/comm/mail/components/newmailaccount/content/uriListener.js b/comm/mail/components/newmailaccount/content/uriListener.js
new file mode 100644
index 0000000000..c4d9177ebe
--- /dev/null
+++ b/comm/mail/components/newmailaccount/content/uriListener.js
@@ -0,0 +1,281 @@
+/* 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/. */
+
+/* globals openAccountSetupTabWithAccount, openAccountProvisionerTab */
+
+/**
+ * This object takes care of intercepting page loads and creating the
+ * corresponding account if the page load turns out to be a text/xml file from
+ * one of our account providers.
+ */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+var { JXON } = ChromeUtils.import("resource:///modules/JXON.jsm");
+
+/**
+ * This is an observer that watches all HTTP requests for one where the
+ * response contentType contains text/xml. Once that observation is
+ * made, we ensure that the associated window for that request matches
+ * the window belonging to the content tab for the account order form.
+ * If so, we attach an nsITraceableListener to read the contents of the
+ * request response, and react accordingly if the contents can be turned
+ * into an email account.
+ *
+ * @param aBrowser The XUL <browser> the request lives in.
+ * @param aParams An object containing various bits of information.
+ * @param aParams.realName The real name of the person
+ * @param aParams.email The email address the person picked.
+ * @param aParams.searchEngine The search engine associated to that provider.
+ */
+function httpRequestObserver(aBrowser, aParams) {
+ this.browser = aBrowser;
+ this.params = aParams;
+}
+
+httpRequestObserver.prototype = {
+ observe(aSubject, aTopic, aData) {
+ if (
+ aTopic != "http-on-examine-response" &&
+ aTopic != "http-on-examine-cached-response"
+ ) {
+ return;
+ }
+
+ if (!(aSubject instanceof Ci.nsIHttpChannel)) {
+ console.error(
+ "Failed to get a nsIHttpChannel when " +
+ "observing http-on-examine-response"
+ );
+ return;
+ }
+ // Helper function to get header values.
+ let getHttpHeader = (httpChannel, header) => {
+ // getResponseHeader throws when header is not set.
+ try {
+ return httpChannel.getResponseHeader(header);
+ } catch (e) {
+ return null;
+ }
+ };
+
+ let contentType = getHttpHeader(aSubject, "Content-Type");
+ if (!contentType || !contentType.toLowerCase().startsWith("text/xml")) {
+ return;
+ }
+
+ // It's possible the account information changed during the setup at the
+ // provider. Check some headers and set them if needed.
+ let name = getHttpHeader(aSubject, "x-thunderbird-account-name");
+ if (name) {
+ this.params.realName = name;
+ }
+ let email = getHttpHeader(aSubject, "x-thunderbird-account-email");
+ if (email) {
+ this.params.email = email;
+ }
+
+ let requestWindow = this._getWindowForRequest(aSubject);
+ if (!requestWindow || requestWindow !== this.browser.innerWindowID) {
+ return;
+ }
+
+ // Ok, we've got a request that looks like a decent candidate.
+ // Let's attach our TracingListener.
+ if (aSubject instanceof Ci.nsITraceableChannel) {
+ let newListener = new TracingListener(this.browser, this.params);
+ newListener.oldListener = aSubject.setNewListener(newListener);
+ }
+ },
+
+ /**
+ * _getWindowForRequest is an internal function that takes an nsIRequest,
+ * and returns the associated window for that request. If it cannot find
+ * an associated window, the function returns null. On exception, the
+ * exception message is logged to the Error Console and null is returned.
+ *
+ * @param aRequest the nsIRequest to analyze
+ */
+ _getWindowForRequest(aRequest) {
+ try {
+ if (aRequest && aRequest.notificationCallbacks) {
+ return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext)
+ .currentWindowContext.innerWindowId;
+ }
+ if (
+ aRequest &&
+ aRequest.loadGroup &&
+ aRequest.loadGroup.notificationCallbacks
+ ) {
+ return aRequest.loadGroup.notificationCallbacks.getInterface(
+ Ci.nsILoadContext
+ ).currentWindowContext.innerWindowId;
+ }
+ } catch (e) {
+ console.error(
+ "Could not find an associated window " +
+ "for an HTTP request. Error: " +
+ e
+ );
+ }
+ return null;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+};
+
+/**
+ * TracingListener is an nsITracableChannel implementation that copies
+ * an incoming stream of data from a request. The data flows through this
+ * nsITracableChannel transparently to the original listener. Once the
+ * response data is fully downloaded, an attempt is made to parse it
+ * as XML, and derive email account data from it.
+ *
+ * @param aBrowser The XUL <browser> the request lives in.
+ * @param aParams An object containing various bits of information.
+ * @param aParams.realName The real name of the person
+ * @param aParams.email The email address the person picked.
+ * @param aParams.searchEngine The search engine associated to that provider.
+ */
+function TracingListener(aBrowser, aParams) {
+ this.chunks = [];
+ this.browser = aBrowser;
+ this.params = aParams;
+ this.oldListener = null;
+}
+
+TracingListener.prototype = {
+ onStartRequest(/* nsIRequest */ aRequest) {
+ this.oldListener.onStartRequest(aRequest);
+ },
+
+ onStopRequest(/* nsIRequest */ aRequest, /* int */ aStatusCode) {
+ const { CreateInBackend } = ChromeUtils.import(
+ "resource:///modules/accountcreation/CreateInBackend.jsm"
+ );
+ const { readFromXML } = ChromeUtils.import(
+ "resource:///modules/accountcreation/readFromXML.jsm"
+ );
+ const { AccountConfig } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountConfig.jsm"
+ );
+
+ let newAccount;
+ try {
+ // Construct the downloaded data (we'll assume UTF-8 bytes) into XML.
+ let xml = this.chunks.join("");
+ let bytes = new Uint8Array(xml.length);
+ for (let i = 0; i < xml.length; i++) {
+ bytes[i] = xml.charCodeAt(i);
+ }
+ xml = new TextDecoder().decode(bytes);
+
+ // Attempt to derive email account information.
+ let domParser = new DOMParser();
+ let accountConfig = readFromXML(
+ JXON.build(domParser.parseFromString(xml, "text/xml"))
+ );
+ AccountConfig.replaceVariables(
+ accountConfig,
+ this.params.realName,
+ this.params.email
+ );
+
+ let host = aRequest.getRequestHeader("Host");
+ let providerHostname = new URL("http://" + host).hostname;
+ // Collect telemetry on which provider the new address was purchased from.
+ Services.telemetry.keyedScalarAdd(
+ "tb.account.new_account_from_provisioner",
+ providerHostname,
+ 1
+ );
+
+ // Create the new account in the back end.
+ newAccount = CreateInBackend.createAccountInBackend(accountConfig);
+
+ let tabmail = document.getElementById("tabmail");
+ // Find the tab associated with this browser, and close it.
+ let myTabInfo = tabmail.tabInfo.filter(
+ function (x) {
+ return "browser" in x && x.browser == this.browser;
+ }.bind(this)
+ )[0];
+ tabmail.closeTab(myTabInfo);
+
+ // Trigger the first login to download the folder structure and messages.
+ newAccount.incomingServer.getNewMessages(
+ newAccount.incomingServer.rootFolder,
+ this._msgWindow,
+ null
+ );
+ } catch (e) {
+ // Something went wrong with account set up. Dump the error out to the
+ // error console, reopen the account provisioner tab, and show an error
+ // dialog to the user.
+ console.error("Problem interpreting provider XML:" + e);
+ openAccountProvisionerTab();
+ Services.prompt.alert(window, null, e);
+
+ this.oldListener.onStopRequest(aRequest, aStatusCode);
+ return;
+ }
+
+ // Open the account setup tab and show the success view or an error if we
+ // weren't able to create the new account.
+ openAccountSetupTabWithAccount(
+ newAccount,
+ this.params.realName,
+ this.params.email
+ );
+
+ this.oldListener.onStopRequest(aRequest, aStatusCode);
+ },
+
+ onDataAvailable(
+ /* nsIRequest */ aRequest,
+ /* nsIInputStream */ aStream,
+ /* int */ aOffset,
+ /* int */ aCount
+ ) {
+ // We want to read the stream of incoming data, but we also want
+ // to make sure it gets passed to the original listener. We do this
+ // by passing the input stream through an nsIStorageStream, writing
+ // the data to that stream, and passing it along to the next listener.
+ let binaryInputStream = Cc[
+ "@mozilla.org/binaryinputstream;1"
+ ].createInstance(Ci.nsIBinaryInputStream);
+ let storageStream = Cc["@mozilla.org/storagestream;1"].createInstance(
+ Ci.nsIStorageStream
+ );
+ let outStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+
+ binaryInputStream.setInputStream(aStream);
+
+ // The segment size of 8192 is a little magical - more or less
+ // copied from nsITraceableChannel example code strewn about the
+ // web.
+ storageStream.init(8192, aCount, null);
+ outStream.setOutputStream(storageStream.getOutputStream(0));
+
+ let data = binaryInputStream.readBytes(aCount);
+ this.chunks.push(data);
+
+ outStream.writeBytes(data, aCount);
+ this.oldListener.onDataAvailable(
+ aRequest,
+ storageStream.newInputStream(0),
+ aOffset,
+ aCount
+ );
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+};
diff --git a/comm/mail/components/newmailaccount/jar.mn b/comm/mail/components/newmailaccount/jar.mn
new file mode 100644
index 0000000000..23554a9584
--- /dev/null
+++ b/comm/mail/components/newmailaccount/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+messenger.jar:
+ content/messenger/newmailaccount/accountProvisioner.xhtml (content/accountProvisioner.xhtml)
+ content/messenger/newmailaccount/accountProvisioner.js (content/accountProvisioner.js)
+ content/messenger/newmailaccount/provisionerCheckout.js (content/provisionerCheckout.js)
+ content/messenger/newmailaccount/uriListener.js (content/uriListener.js)
diff --git a/comm/mail/components/newmailaccount/moz.build b/comm/mail/components/newmailaccount/moz.build
new file mode 100644
index 0000000000..de5cd1bf81
--- /dev/null
+++ b/comm/mail/components/newmailaccount/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]