/* 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/. */ import { MailServices } from "resource:///modules/MailServices.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { default: "resource:///modules/qrcode.mjs", MailStringUtils: "resource:///modules/MailStringUtils.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "log", () => console.createInstance({ prefix: "QRExport", maxLogLevel: "Warn", maxLogLevelPref: "mail.qrexport.loglevel", }) ); /** * Incoming server types supported by the Android app. */ const INCOMING_SERVER_TYPES = new Set(["imap", "pop3"]); /** * Auth methods not supported by the Android app. */ const UNSUPPORTED_AUTH_METHODS = new Set([ // Unsupported: Ci.nsMsgAuthMethod.GSSAPI, Ci.nsMsgAuthMethod.NTLM, Ci.nsMsgAuthMethod.External, // Do not map to anything: Ci.nsMsgAuthMethod.old, Ci.nsMsgAuthMethod.secure, Ci.nsMsgAuthMethod.anything, ]); // QR Code data content constants // These should match with https://github.com/thunderbird/thunderbird-android/blob/4ebcca8d893bc57df9ef293fdf7d6d8fe46becad/feature/migration/qrcode/src/main/kotlin/app/k9mail/feature/migration/qrcode/AccountData.kt#L48-L105 const QR_DATA_FORMAT_VERSION = 1; const INCOMING_PROTOCOL = new Map([ ["imap", 0], ["pop3", 1], ]); const SOCKET_TYPES = new Map([ [Ci.nsMsgSocketType.plain, 0], [Ci.nsMsgSocketType.alwaysSTARTTLS, 2], [Ci.nsMsgSocketType.SSL, 3], ]); const AUTH_METHODS = new Map([ [Ci.nsMsgAuthMethod.none, 0], // [Ci.nsMsgAuthMethod.old, "old"], [Ci.nsMsgAuthMethod.passwordCleartext, 1], [Ci.nsMsgAuthMethod.passwordEncrypted, 2], [Ci.nsMsgAuthMethod.GSSAPI, 3], [Ci.nsMsgAuthMethod.NTLM, 4], [Ci.nsMsgAuthMethod.External, 5], // [Ci.nsMsgAuthMethod.secure, "secure"], // [Ci.nsMsgAuthMethod.anything, "anything"], [Ci.nsMsgAuthMethod.OAuth2, 6], ]); const OUTGOING_PROTOCOL_SMTP = 0; const ACCOUNTS_PER_QR_CODE = 3; const MAX_CHUNK_LENGTH = 800; export const QRExport = { /** * Eligible accounts fulfill: * - Incoming must be IAMP or POP3. * - Outboing must be SMTP. * - authMethod can't be "GSSAPI", "NTLM", "external". * - email address must be ASCII only. * * @returns {nsIMsgAccount[]} Eligible accounts. */ getEligibleAccounts() { return MailServices.accounts.accounts.filter(account => { if (!account.defaultIdentity) { lazy.log.debug(`${account.key} ineligible: no default identity`); return false; } // For each account we want ingoing and outgoing and the default identity. const incomingServer = account.incomingServer; if (!INCOMING_SERVER_TYPES.has(incomingServer.type)) { lazy.log.debug( `${account.key} type ineligible: ${incomingServer.type}` ); return false; } if (UNSUPPORTED_AUTH_METHODS.has(incomingServer.authMethod)) { lazy.log.debug( `${account.key} authMethod ineligible: ${incomingServer.authMethod}` ); return false; } const identity = account.defaultIdentity; // eslint-disable-next-line no-control-regex if (!/^[\x00-\x7F]+$/.test(identity.email)) { lazy.log.debug(`${identity.email} ineligible: non-ascii email`); return false; } const outgoingServer = identity.smtpServerKey ? MailServices.outgoingServer.getServerByKey(identity.smtpServerKey) : MailServices.outgoingServer.defaultServer; if (!(outgoingServer instanceof Ci.nsISmtpServer)) { lazy.log.debug( `${account.key} outgoing server ineligible: ${outgoingServer?.type}` ); return false; } if (UNSUPPORTED_AUTH_METHODS.has(outgoingServer.authMethod)) { lazy.log.debug( `${account.key} outgoing server authMethod ineligible: ${outgoingServer.authMethod}` ); return false; } lazy.log.debug(`${account.key} (${identity.fullName}) is eligible`); return true; }); }, /** * Generate the QR codes to export the selected accounts. Splits the accounts * into chunks of 3 accounts per QR code. * * @param {string[]} accountKeys - Accounts that should be exported. * @param {boolean} includePasswords - If passwords should be included in the export data. * @returns {Promise} Returns an array of SVG URLs, each representing a QR code. */ async getQRCodes(accountKeys, includePasswords) { if (!Services.prefs.getBoolPref("mail.qrexport.enabled")) { throw Components.Exception( "QR export is disabled by enterprise policy", Cr.NS_ERROR_NOT_AVAILABLE ); } const accounts = await Promise.all( accountKeys.map(key => this.getAccountData(key, includePasswords)) ); // For practical purposes each QR code should hold no more than 1000 // characters, optimally 800 characters maximum. const chunkCount = Math.ceil(accounts.length / ACCOUNTS_PER_QR_CODE); const qrCodes = []; for (let i = 0; i < chunkCount; i++) { const chunkOffset = i * ACCOUNTS_PER_QR_CODE; const chunk = accounts .slice(chunkOffset, chunkOffset + ACCOUNTS_PER_QR_CODE) .flat(); const chunkPart = i + 1; // 1-indexed const chunkData = this.getQRData(chunk, chunkPart, chunkCount); const serializedChunk = JSON.stringify(chunkData); if (serializedChunk.length > MAX_CHUNK_LENGTH) { lazy.log.warn( `QR code #${chunkPart} is longer than expected; result might be hard to read.` ); } qrCodes.push(this.renderQR(serializedChunk)); } return qrCodes; }, /** * Generate the data for a QR code with a chunk of account data. * * @param {Array} data - Account data contained in this chunk. * @param {number} part - 1-based index of this chunk. * @param {number} count - Total number of QR codes. * @returns {Array} */ getQRData(data, part, count) { return [QR_DATA_FORMAT_VERSION, [part, count], ...data]; }, /** * Generate a minimal account description for serialization to JSON. * * @param {string} accountKey - Key of the account to get the data for. * @param {boolean} includePasswords - If the result should include passwords. * @returns {Promise} Array structure with account data to serialize to JSON. * @see https://docs.google.com/document/d/1siSwPzNPkwq4BL5G3z2K4zzRJuL9N9zbPodMOggXbdA/edit for format */ async getAccountData(accountKey, includePasswords) { const account = MailServices.accounts.getAccount(accountKey); const incomingServer = account.incomingServer; const defaultSmtpServerKey = account.defaultIdentity.smtpServerKey; const outgoingServer = account.defaultIdentity.smtpServerKey ? MailServices.outgoingServer.getServerByKey( account.defaultIdentity.smtpServerKey ) : MailServices.outgoingServer.defaultServer; outgoingServer.QueryInterface(Ci.nsISmtpServer); const identities = account.identities.filter( identity => (!identity.smtpServerKey || identity.smtpServerKey == defaultSmtpServerKey) && /^[\x00-\x7F]+$/.test(identity.email) // eslint-disable-line no-control-regex ); let outgoingPassword = ""; if (includePasswords) { outgoingPassword = outgoingServer.password || (await outgoingServer.wrappedJSObject._getPasswordWithoutUI()) || ""; } return [ [ INCOMING_PROTOCOL.get(incomingServer.type), incomingServer.hostname, incomingServer.port, SOCKET_TYPES.get(incomingServer.socketType), AUTH_METHODS.get(incomingServer.authMethod), incomingServer.username, incomingServer.prettyName, (includePasswords && !incomingServer.passwordPromptRequired && incomingServer.password) || "", ], [ [ [ OUTGOING_PROTOCOL_SMTP, outgoingServer.hostname, outgoingServer.port, SOCKET_TYPES.get(outgoingServer.socketType), AUTH_METHODS.get(outgoingServer.authMethod), outgoingServer.username, outgoingPassword, ], ...identities.map(identity => [identity.email, identity.fullName]), ], ], ]; }, /** * Renders the given data into a QR code with L error correction. * * @param {string} data - Data to encode in the QR code. * @returns {string} QR code rendered as BMP URI. */ renderQR(data) { const qrOptions = { errorCorrectionLevel: lazy.default.ErrorCorrectionLevel.L, }; const matrix = lazy.default.generate( lazy.MailStringUtils.stringToByteString(data), qrOptions ); return lazy.default.render("bmp-uri", matrix); }, /** * Check if an account's exported servers would authenticate with OAuth. * * @param {nsIMsgAccount} account * @returns {{incoming: boolean, outgoing: boolean}} If the incoming or * outgoing servers use OAuth as authentication method. */ getAccountOAuthUsage(account) { const outgoingServer = account.defaultIdentity.smtpServerKey ? MailServices.outgoingServer.getServerByKey( account.defaultIdentity.smtpServerKey ) : MailServices.outgoingServer.defaultServer; return { incoming: account.incomingServer.authMethod === Ci.nsMsgAuthMethod.OAuth2, outgoing: outgoingServer.authMethod === Ci.nsMsgAuthMethod.OAuth2, }; }, };