summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/newmailaccount/content/uriListener.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/newmailaccount/content/uriListener.js')
-rw-r--r--comm/mail/components/newmailaccount/content/uriListener.js281
1 files changed, 281 insertions, 0 deletions
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",
+ ]),
+};