/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const EXPORTED_SYMBOLS = ["ImapClient"]; var { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); var { clearTimeout, setTimeout } = ChromeUtils.importESModule( "resource://gre/modules/Timer.sys.mjs" ); var { MailStringUtils } = ChromeUtils.import( "resource:///modules/MailStringUtils.jsm" ); var { ImapAuthenticator } = ChromeUtils.import( "resource:///modules/MailAuthenticator.jsm" ); var { ImapResponse } = ChromeUtils.import( "resource:///modules/ImapResponse.jsm" ); var { ImapUtils } = ChromeUtils.import("resource:///modules/ImapUtils.jsm"); // There can be multiple ImapClient running concurrently, assign each logger a // unique prefix. let loggerInstanceId = 0; const PR_UINT32_MAX = 0xffffffff; /** * A class to interact with IMAP server. */ class ImapClient { _logger = console.createInstance({ prefix: `mailnews.imap.${loggerInstanceId++}`, maxLogLevel: "Warn", maxLogLevelPref: "mailnews.imap.loglevel", }); /** * @param {nsIImapIncomingServer} server - The associated server instance. */ constructor(server) { this._server = server.QueryInterface(Ci.nsIMsgIncomingServer); this._serverSink = this._server.QueryInterface(Ci.nsIImapServerSink); this._authenticator = new ImapAuthenticator(server); // Auth methods detected from the CAPABILITY response. this._supportedAuthMethods = []; // Subset of _supportedAuthMethods that are allowed by user preference. this._possibleAuthMethods = []; // Auth methods set by user preference. this._preferredAuthMethods = { [Ci.nsMsgAuthMethod.passwordCleartext]: ["PLAIN", "LOGIN"], [Ci.nsMsgAuthMethod.passwordEncrypted]: ["CRAM-MD5"], [Ci.nsMsgAuthMethod.GSSAPI]: ["GSSAPI"], [Ci.nsMsgAuthMethod.NTLM]: ["NTLM"], [Ci.nsMsgAuthMethod.OAuth2]: ["XOAUTH2"], [Ci.nsMsgAuthMethod.External]: ["EXTERNAL"], }[server.authMethod] || []; // The next auth method to try if the current failed. this._nextAuthMethod = null; this._tag = Math.floor(100 * Math.random()); this._charsetManager = Cc[ "@mozilla.org/charset-converter-manager;1" ].getService(Ci.nsICharsetConverterManager); this._messageUids = []; this._messages = new Map(); this._loadPrefs(); } /** * @type {boolean} - Whether the socket is open. */ get isOnline() { return this._socket?.readyState == "open"; } /** * Load imap related preferences, many behaviors depend on these pref values. */ _loadPrefs() { this._prefs = { tcpTimeout: Services.prefs.getIntPref("mailnews.tcptimeout"), }; } /** * Reset some internal states to be safely reused. */ _reset() { this.onData = () => {}; this.onDone = () => {}; this._actionAfterDiscoverAllFolders = null; this.channel = null; this._urlListener = null; this._msgWindow = null; this._authenticating = false; this.verifyLogon = false; this._idling = false; if (this._idleTimer) { clearTimeout(this._idleTimer); this._idleTimer = null; } } /** * Initiate a connection to the server */ connect() { if (this.isOnline) { // Reuse the connection. this.onReady(); this._setSocketTimeout(this._prefs.tcpTimeout); } else { let hostname = this._server.hostName.toLowerCase(); this._logger.debug(`Connecting to ${hostname}:${this._server.port}`); this._greeted = false; this._capabilities = null; this._secureTransport = this._server.socketType == Ci.nsMsgSocketType.SSL; this._socket = new TCPSocket(hostname, this._server.port, { binaryType: "arraybuffer", useSecureTransport: this._secureTransport, }); this._socket.onopen = this._onOpen; this._socket.onerror = this._onError; } } /** * Set socket timeout in seconds. * * @param {number} timeout - The timeout in seconds. */ _setSocketTimeout(timeout) { this._socket.transport?.setTimeout( Ci.nsISocketTransport.TIMEOUT_READ_WRITE, timeout ); } /** * Construct an nsIMsgMailNewsUrl instance, setup urlListener to notify when * the current request is finished. * * @param {nsIUrlListener} urlListener - Callback for the request. * @param {nsIMsgWindow} msgWindow - The associated msg window. * @param {nsIMsgMailNewsUrl} [runningUri] - The url to run, if provided. * @returns {nsIMsgMailNewsUrl} */ startRunningUrl(urlListener, msgWindow, runningUri) { this._urlListener = urlListener; this._msgWindow = msgWindow; this.runningUri = runningUri; if (!this.runningUri) { this.runningUri = Services.io.newURI( `imap://${this._server.hostName}:${this._server.port}` ); } this._urlListener?.OnStartRunningUrl(this.runningUri, Cr.NS_OK); this.runningUri .QueryInterface(Ci.nsIMsgMailNewsUrl) .SetUrlState(true, Cr.NS_OK); return this.runningUri; } /** * Discover all folders if the current server hasn't already discovered. */ _discoverAllFoldersIfNecessary = () => { if (this._server.hasDiscoveredFolders) { this.onReady(); return; } this._actionAfterDiscoverAllFolders = this.onReady; this.discoverAllFolders(this._server.rootFolder); }; /** * Discover all folders. * * @param {nsIMsgFolder} folder - The associated folder. */ discoverAllFolders(folder) { this._logger.debug("discoverAllFolders", folder.URI); let handleListResponse = res => { this._hasTrash = res.mailboxes.some( mailbox => mailbox.flags & ImapUtils.FLAG_IMAP_TRASH ); if (!this._hasTrash) { let trashFolderName = this._server.trashFolderName.toLowerCase(); let trashMailbox = res.mailboxes.find( mailbox => mailbox.name.toLowerCase() == trashFolderName ); if (trashMailbox) { this._hasTrash = true; trashMailbox.flags |= ImapUtils.FLAG_IMAP_TRASH; } } for (let mailbox of res.mailboxes) { this._serverSink.possibleImapMailbox( mailbox.name.replaceAll(mailbox.delimiter, "/"), mailbox.delimiter, mailbox.flags ); } }; if (this._capabilities.includes("LIST-EXTENDED")) { this._nextAction = res => { handleListResponse(res); this._actionFinishFolderDiscovery(); }; let command = 'LIST (SUBSCRIBED) "" "*"'; if (this._capabilities.includes("SPECIAL-USE")) { command += " RETURN (SPECIAL-USE)"; // rfc6154 } this._sendTagged(command); return; } this._nextAction = res => { this._nextAction = res2 => { // Per rfc3501#section-6.3.9, if LSUB returns different flags from LIST, // use the LIST responses. for (let mailbox of res2.mailboxes) { let mailboxFromList = res.mailboxes.find(x => x.name == mailbox.name); if ( mailboxFromList?.flags && mailboxFromList?.flags != mailbox.flags ) { mailbox.flags = mailboxFromList.flags; } } handleListResponse(res2); this._actionFinishFolderDiscovery(); }; this._sendTagged('LSUB "" "*"'); }; let command = 'LIST "" "*"'; if (this._capabilities.includes("SPECIAL-USE")) { command += " RETURN (SPECIAL-USE)"; // rfc6154 } this._sendTagged(command); } /** * Discover all folders for the subscribe dialog. * * @param {nsIMsgFolder} folder - The associated folder. */ discoverAllAndSubscribedFolders(folder) { this._logger.debug("discoverAllAndSubscribedFolders", folder.URI); let handleListResponse = res => { for (let mailbox of res.mailboxes) { this._serverSink.possibleImapMailbox( mailbox.name.replaceAll(mailbox.delimiter, "/"), mailbox.delimiter, mailbox.flags ); } }; this._nextAction = res => { handleListResponse(res); this._server.doingLsub = false; this._nextAction = res2 => { // Per rfc3501#section-6.3.9, if LSUB returns different flags from LIST, // use the LIST responses. for (let mailbox of res2.mailboxes) { let mailboxFromList = res.mailboxes.find(x => x.name == mailbox.name); if ( mailboxFromList?.flags && mailboxFromList?.flags != mailbox.flags ) { mailbox.flags = mailboxFromList.flags; } } handleListResponse(res2); this._actionDone(); }; this._sendTagged('LIST "" "*"'); }; this._sendTagged('LSUB "" "*"'); this._server.doingLsub = true; } /** * Select a folder. * * @param {nsIMsgFolder} folder - The folder to select. */ selectFolder(folder) { this._logger.debug("selectFolder", folder.URI); if (this.folder == folder) { this._actionNoop(); return; } this._actionAfterSelectFolder = this._actionUidFetch; this._nextAction = this._actionSelectResponse(folder); this._sendTagged(`SELECT "${this._getServerFolderName(folder)}"`); } /** * Rename a folder. * * @param {nsIMsgFolder} folder - The folder to rename. * @param {string} newName - The new folder name. */ renameFolder(folder, newName) { this._logger.debug("renameFolder", folder.URI, newName); let delimiter = folder.QueryInterface(Ci.nsIMsgImapMailFolder).hierarchyDelimiter || "/"; let names = this._getAncestorFolderNames(folder); let oldName = this._getServerFolderName(folder); newName = this._encodeMailboxName([...names, newName].join(delimiter)); this._nextAction = this._actionRenameResponse(oldName, newName); this._sendTagged(`RENAME "${oldName}" "${newName}"`); } /** * Move a source folder to be a child of another folder. * * @param {nsIMsgFolder} srcFolder - The source folder to move. * @param {nsIMsgFolder} dstFolder - The target parent folder. */ moveFolder(srcFolder, dstFolder) { this._logger.debug("moveFolder", srcFolder.URI, dstFolder.URI); let oldName = this._getServerFolderName(srcFolder); let newName = this._getServerSubFolderName(dstFolder, srcFolder.name); this._nextAction = this._actionRenameResponse(oldName, newName, true); this._sendTagged(`RENAME "${oldName}" "${newName}"`); } /** * Send LIST command for a folder. * * @param {nsIMsgFolder} folder - The folder to list. */ listFolder(folder) { this._logger.debug("listFolder", folder.URI); this._actionList(this._getServerFolderName(folder), () => { this._actionDone(); }); } /** * Send DELETE command for a folder and all subfolders. * * @param {nsIMsgFolder} folder - The folder to delete. */ deleteFolder(folder) { this._logger.debug("deleteFolder", folder.URI); this._nextAction = res => { // Leaves have longer names than parent mailbox, sort them by the name // length, so that leaf mailbox will be deleted first. let mailboxes = res.mailboxes.sort( (x, y) => y.name.length - x.name.length ); let selfName = this._getServerFolderName(folder); let selfIncluded = false; this._nextAction = () => { let mailbox = mailboxes.shift(); if (mailbox) { this._sendTagged(`DELETE "${mailbox.name}"`); if (!selfIncluded && selfName == mailbox.name) { selfIncluded = true; } } else if (!selfIncluded) { this._nextAction = () => this._actionDone(); this._sendTagged(`DELETE "${this._getServerFolderName(folder)}"`); } else { this._actionDone(); } }; this._nextAction(); }; this._sendTagged(`LIST "" "${this._getServerFolderName(folder)}"`); } /** * Ensure a folder exists on the server. Create one if not already exists. * * @param {nsIMsgFolder} parent - The parent folder to check. * @param {string} folderName - The folder name. */ ensureFolderExists(parent, folderName) { this._logger.debug("ensureFolderExists", parent.URI, folderName); let mailboxName = this._getServerSubFolderName(parent, folderName); this._nextAction = res => { if (res.mailboxes.length) { // Already exists. this._actionDone(); return; } // Create one and subscribe to it. this._actionCreateAndSubscribe(mailboxName, res => { this._actionList(mailboxName, () => this._actionDone()); }); }; this._sendTagged(`LIST "" "${mailboxName}"`); } /** * Create a folder on the server. * * @param {nsIMsgFolder} parent - The parent folder to check. * @param {string} folderName - The folder name. */ createFolder(parent, folderName) { this._logger.debug("createFolder", parent.URI, folderName); let mailboxName = this._getServerSubFolderName(parent, folderName); this._actionCreateAndSubscribe(mailboxName, res => { this._actionList(mailboxName, () => this._actionDone()); }); } /** * Subscribe a folder. * * @param {nsIMsgFolder} folder - The folder to subscribe. * @param {string} folderName - The folder name. */ subscribeFolder(folder, folderName) { this._logger.debug("subscribeFolder", folder.URI, folderName); this._nextAction = () => this._server.performExpand(); this._sendTagged(`SUBSCRIBE "${folderName}"`); } /** * Unsubscribe a folder. * * @param {nsIMsgFolder} folder - The folder to unsubscribe. * @param {string} folderName - The folder name. */ unsubscribeFolder(folder, folderName) { this._logger.debug("unsubscribeFolder", folder.URI, folderName); this._nextAction = () => this._server.performExpand(); this._sendTagged(`UNSUBSCRIBE "${folderName}"`); } /** * Fetch the attribute of messages. * * @param {nsIMsgFolder} folder - The folder to check. * @param {string} uids - The message uids. * @param {string} attribute - The message attribute to fetch */ fetchMsgAttribute(folder, uids, attribute) { this._logger.debug("fetchMsgAttribute", folder.URI, uids, attribute); this._nextAction = res => { if (res.done) { let resultAttributes = res.messages .map(m => m.customAttributes[attribute]) .flat(); this.runningUri.QueryInterface(Ci.nsIImapUrl).customAttributeResult = resultAttributes.length > 1 ? `(${resultAttributes.join(" ")})` : resultAttributes[0]; this._actionDone(); } }; this._sendTagged(`UID FETCH ${uids} (${attribute})`); } /** * Delete all the messages in a folder. * * @param {nsIMsgFolder} folder - The folder to delete messages. */ deleteAllMessages(folder) { this._logger.debug("deleteAllMessages", folder.URI); this._actionInFolder(folder, () => { if (!this._messages.size) { this._actionDone(); return; } this._nextAction = () => this.expunge(folder); this._sendTagged("UID STORE 1:* +FLAGS.SILENT (\\Deleted)"); }); } /** * Search in a folder. * * @param {nsIMsgFolder} folder - The folder to delete messages. * @param {string} searchCommand - The SEARCH command together with the search * criteria. */ search(folder, searchCommand) { this._logger.debug("search", folder.URI); this._actionInFolder(folder, () => { this._nextAction = res => { this.onData(res.search); this._actionDone(); }; this._sendTagged(`UID ${searchCommand}`); }); } /** * Get the names of all ancestor folders. For example, * folder a/b/c will return ['a', 'b']. * * @param {nsIMsgFolder} folder - The input folder. * @returns {string[]} */ _getAncestorFolderNames(folder) { let matches = /imap:\/\/[^/]+\/(.+)/.exec(folder.URI); return matches[1].split("/").slice(0, -1); } /** * When UTF8 is enabled, use the name directly. Otherwise, encode to mUTF-7. * * @param {string} name - The mailbox name. */ _encodeMailboxName(name) { return this._utf8Enabled ? name : this._charsetManager.unicodeToMutf7(name); } /** * Get the server name of a msg folder. * * @param {nsIMsgFolder} folder - The input folder. * @returns {string} */ _getServerFolderName(folder) { if (folder.isServer) { return ""; } if (folder.onlineName) { return folder.onlineName.replaceAll('"', '\\"'); } let delimiter = folder.QueryInterface(Ci.nsIMsgImapMailFolder).hierarchyDelimiter || "/"; let names = this._getAncestorFolderNames(folder); return this._encodeMailboxName( [...names, folder.name].join(delimiter) ).replaceAll('"', '\\"'); } /** * Get the server name of a sub folder. The sub folder may or may not exist on * the server. * * @param {nsIMsgFolder} parent - The parent folder. * @param {string} folderName - The sub folder name. * @returns {string} */ _getServerSubFolderName(parent, folderName) { folderName = this._encodeMailboxName(folderName); let mailboxName = this._getServerFolderName(parent); if (mailboxName) { let delimiter = parent.QueryInterface( Ci.nsIMsgImapMailFolder ).hierarchyDelimiter; // @see nsImapCore.h. const ONLINE_HIERARCHY_SEPARATOR_UNKNOWN = "^"; if (!delimiter || delimiter == ONLINE_HIERARCHY_SEPARATOR_UNKNOWN) { delimiter = "/"; } return mailboxName + delimiter + folderName; } return folderName; } /** * Fetch the full content of a message by UID. * * @param {nsIMsgFolder} folder - The associated folder. * @param {number} uid - The message uid. * @param {number} [size] - The body size to fetch. */ fetchMessage(folder, uid, size) { this._logger.debug(`fetchMessage folder=${folder.name} uid=${uid}`); if (folder.hasMsgOffline(uid, null, 10)) { this.onDone = () => {}; this.channel?.readFromLocalCache(); this._actionDone(); return; } this._actionInFolder(folder, () => { this._nextAction = this._actionUidFetchBodyResponse; let command; if (size) { command = `UID FETCH ${uid} (UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (Content-Type Content-Transfer-Encoding)] BODY.PEEK[TEXT]<0.${size}>)`; } else { command = `UID FETCH ${uid} (UID RFC822.SIZE FLAGS BODY.PEEK[])`; } this._sendTagged(command); }); } /** * Add, remove or replace flags of specified messages. * * @param {string} action - "+" means add, "-" means remove, "" means replace. * @param {nsIMsgFolder} folder - The target folder. * @param {string} messageIds - Message UIDs, e.g. "23,30:33". * @param {number} flags - The internal flags number to update. */ updateMessageFlags(action, folder, messageIds, flags) { this._actionInFolder(folder, () => { this._nextAction = () => this._actionDone(); // _supportedFlags is available after _actionSelectResponse. let flagsStr = ImapUtils.flagsToString(flags, this._supportedFlags); this._sendTagged(`UID STORE ${messageIds} ${action}FLAGS (${flagsStr})`); }); } /** * Send EXPUNGE command to a folder. * * @param {nsIMsgFolder} folder - The associated folder. */ expunge(folder) { this._actionInFolder(folder, () => { this._nextAction = () => this._actionDone(); this._sendTagged("EXPUNGE"); }); } /** * Move or copy messages from a folder to another folder. * * @param {nsIMsgFolder} folder - The source folder. * @param {nsIMsgFolder} folder - The target folder. * @param {string} messageIds - The message identifiers. * @param {boolean} idsAreUids - If true messageIds are UIDs, otherwise, * messageIds are sequences. * @param {boolean} isMove - If true, use MOVE command when supported. */ copy(folder, dstFolder, messageIds, idsAreUids, isMove) { let command = idsAreUids ? "UID " : ""; command += isMove && this._capabilities.includes("MOVE") ? "MOVE " // rfc6851 : "COPY "; command += messageIds + ` "${this._getServerFolderName(dstFolder)}"`; this._actionInFolder(folder, () => { this._nextAction = this._actionNoopResponse; this._sendTagged(command); }); } /** * Upload a message file to a folder. * * @param {nsIFile} file - The message file to upload. * @param {nsIMsgFolder} dstFolder - The target folder. * @param {nsImapMailCopyState} copyState - A state used by nsImapMailFolder. * @param {boolean} isDraft - Is the uploaded file a draft. */ async uploadMessageFromFile(file, dstFolder, copyState, isDraft) { this._logger.debug("uploadMessageFromFile", file.path, dstFolder.URI); let mailbox = this._getServerFolderName(dstFolder); let content = MailStringUtils.uint8ArrayToByteString( await IOUtils.read(file.path) ); this._nextAction = res => { if (res.tag != "+") { this._actionDone(Cr.NS_ERROR_FAILURE); return; } this._nextAction = res => { this._folderSink = dstFolder.QueryInterface(Ci.nsIImapMailFolderSink); if ( // See rfc4315. this._capabilities.includes("UIDPLUS") && res.attributes.appenduid ) { // The response is like ` OK [APPENDUID ]`. this._folderSink.setAppendMsgUid( res.attributes.appenduid[1], this.runningUri ); } this._actionDone(); if (res.exists) { // FIXME: _actionNoopResponse should be enough here, but it breaks // test_imapAttachmentSaves.js. this.folder = null; } try { this._folderSink.copyNextStreamMessage(true, copyState); } catch (e) { this._logger.warn("copyNextStreamMessage failed", e); } }; this._send(content + (this._utf8Enabled ? ")" : "")); }; let outKeywords = {}; let flags = dstFolder .QueryInterface(Ci.nsIImapMessageSink) .getCurMoveCopyMessageInfo(this.runningUri, {}, outKeywords); let flagString = ImapUtils.flagsToString(flags, this._supportedFlags); if (isDraft && !/\b\Draft\b/.test(flagString)) { flagString += " \\Draft"; } if (outKeywords.value) { flagString += " " + outKeywords.value; } let open = this._utf8Enabled ? "UTF8 (~{" : "{"; let command = `APPEND "${mailbox}" (${flagString.trim()}) ${open}${ content.length }}`; this._sendTagged(command); } /** * Check the status of a folder. * * @param {nsIMsgFolder} folder - The folder to check. */ updateFolderStatus(folder) { this._logger.debug("updateFolderStatus", folder.URI); if (this._folder == folder) { // According to rfc3501, "the STATUS command SHOULD NOT be used on the // currently selected mailbox", so use NOOP instead. this._actionNoop(); return; } this._nextAction = res => { if (res.status == "OK") { folder .QueryInterface(Ci.nsIImapMailFolderSink) .UpdateImapMailboxStatus(this, { QueryInterface: ChromeUtils.generateQI(["nsIMailboxSpec"]), nextUID: res.attributes.uidnext, numMessages: res.attributes.messages.length, numUnseenMessages: res.attributes.unseen, }); folder.msgDatabase = null; } this._actionDone(); }; this._sendTagged( `STATUS "${this._getServerFolderName(folder)}" (UIDNEXT MESSAGES UNSEEN)` ); } /** * Update message flags. * * @param {nsIMsgFolder} folder - The associated folder. * @param {string} flagsToAdd - The flags to add. * @param {string} flagsToSubtract - The flags to subtract. * @param {string} uids - The message uids. */ storeCustomKeywords(folder, flagsToAdd, flagsToSubtract, uids) { this._logger.debug( "storeCustomKeywords", folder.URI, flagsToAdd, flagsToSubtract, uids ); let subtractFlags = () => { if (flagsToSubtract) { this._nextAction = () => { this._actionDone(); }; this._sendTagged(`UID STORE ${uids} -FLAGS (${flagsToSubtract})`); } else { this._actionDone(); } }; this._actionInFolder(folder, () => { if (flagsToAdd) { this._nextAction = () => { subtractFlags(); }; this._sendTagged(`UID STORE ${uids} +FLAGS (${flagsToAdd})`); } else { subtractFlags(); } }); } /** * Get message headers by the specified uids. * * @param {nsIMsgFolder} folder - The folder of the messages. * @param {string[]} uids - The message uids. */ getHeaders(folder, uids) { this._logger.debug("getHeaders", folder.URI, uids); this._actionInFolder(folder, () => { this._nextAction = this._actionUidFetchHeaderResponse; let extraItems = ""; if (this._server.isGMailServer) { extraItems += "X-GM-MSGID X-GM-THRID X-GM-LABELS "; } this._sendTagged( `UID FETCH ${uids} (UID ${extraItems}RFC822.SIZE FLAGS BODY.PEEK[HEADER])` ); }); } /** * Send IDLE command to the server. */ idle() { if (!this.folder) { this._actionDone(); return; } this._nextAction = res => { if (res.tag == "*") { this.folder.performingBiff = true; this._actionNoopResponse(res); } }; this._sendTagged("IDLE"); this._setSocketTimeout(PR_UINT32_MAX); this._idling = true; this._idleTimer = setTimeout(() => { this.endIdle(() => { this._actionNoop(); }); // Per rfc2177, should terminate the IDLE and re-issue it at least every // 29 minutes. But in practice many servers timeout before that. A noop // every 5min is better than timeout. }, 5 * 60 * 1000); this._logger.debug(`Idling in ${this.folder.URI}`); } /** * Send DONE to end the IDLE command. * * @param {Function} nextAction - Callback function after IDLE is ended. */ endIdle(nextAction) { this._nextAction = res => { if (res.status == "OK") { nextAction(); } }; this._send("DONE"); this._idling = false; this.busy = true; clearTimeout(this._idleTimer); this._idleTimer = null; } /** * Send LOGOUT and close the socket. */ logout() { this._sendTagged("LOGOUT"); this._socket.close(); } /** * The open event handler. */ _onOpen = () => { this._logger.debug("Connected"); this._socket.ondata = this._onData; this._socket.onclose = this._onClose; this._nextAction = res => { this._greeted = true; this._actionCapabilityResponse(res); }; this._setSocketTimeout(this._prefs.tcpTimeout); }; /** * The data event handler. * * @param {TCPSocketEvent} event - The data event. */ _onData = async event => { // Without this, some tests are blocked waiting for response from Maild.jsm. // Don't know the real cause, but possibly because ImapClient and Maild runs // on the same process. We also have this in Pop3Client. await new Promise(resolve => setTimeout(resolve)); let stringPayload = this._utf8Enabled ? new TextDecoder().decode(event.data) : MailStringUtils.uint8ArrayToByteString(new Uint8Array(event.data)); this._logger.debug(`S: ${stringPayload}`); if (!this._response || this._idling || this._response.done) { this._response = new ImapResponse(); this._response.onMessage = this._onMessage; } this._response.parse(stringPayload); if ( !this._authenticating && this._response.done && this._response.status && this._response.tag != "+" && !["OK", "+"].includes(this._response.status) ) { this._actionDone(ImapUtils.NS_MSG_ERROR_IMAP_COMMAND_FAILED); return; } if (!this._greeted || this._idling || this._response.done) { this._nextAction?.(this._response); } }; /** * The error event handler. * * @param {TCPSocketErrorEvent} event - The error event. */ _onError = async event => { this._logger.error(`${event.name}: a ${event.message} error occurred`); if (event.errorCode == Cr.NS_ERROR_NET_TIMEOUT) { this._actionError("imapNetTimeoutError"); this._actionDone(event.errorCode); return; } let secInfo = await event.target.transport?.tlsSocketControl?.asyncGetSecurityInfo(); if (secInfo) { this._logger.error(`SecurityError info: ${secInfo.errorCodeString}`); if (secInfo.failedCertChain.length) { let chain = secInfo.failedCertChain.map(c => { return c.commonName + "; serial# " + c.serialNumber; }); this._logger.error(`SecurityError cert chain: ${chain.join(" <- ")}`); } this.runningUri.failedSecInfo = secInfo; this._server.closeCachedConnections(); } else { this.logout(); } this._actionDone(event.errorCode); }; /** * The close event handler. */ _onClose = () => { this._logger.debug("Connection closed."); this.folder = null; }; /** * Send a command to the server. * * @param {string} str - The command string to send. * @param {boolean} [suppressLogging=false] - Whether to suppress logging the str. */ _send(str, suppressLogging) { if (suppressLogging && AppConstants.MOZ_UPDATE_CHANNEL != "default") { this._logger.debug( "C: Logging suppressed (it probably contained auth information)" ); } else { // Do not suppress for non-release builds, so that debugging auth problems // is easier. this._logger.debug(`C: ${str}`); } if (!this.isOnline) { if (!str.includes("LOGOUT")) { this._logger.warn( `Failed to send because socket state is ${this._socket?.readyState}` ); } return; } let encode = this._utf8Enabled ? x => new TextEncoder().encode(x) : MailStringUtils.byteStringToUint8Array; this._socket.send(encode(str + "\r\n").buffer); } /** * Same as _send, but prepend a tag to the command. */ _sendTagged(str, suppressLogging) { if (this._idling) { let nextAction = this._nextAction; this.endIdle(() => { this._nextAction = nextAction; this._sendTagged(str, suppressLogging); }); } else { this._send(`${this._getNextTag()} ${str}`, suppressLogging); } } /** * Get the next command tag. * * @returns {number} */ _getNextTag() { this._tag = (this._tag + 1) % 100; return this._tag; } /** * Send CAPABILITY command to the server. */ _actionCapability() { this._nextAction = this._actionCapabilityResponse; this._sendTagged("CAPABILITY"); } /** * Handle the capability response. * * @param {ImapResponse} res - Response received from the server. */ _actionCapabilityResponse = res => { if (res.capabilities) { this._capabilities = res.capabilities; this._server.wrappedJSObject.capabilities = res.capabilities; if (this._capabilities.includes("X-GM-EXT-1")) { this._server.isGMailServer = true; } this._supportedAuthMethods = res.authMethods; this._actionChooseFirstAuthMethod(); } else { this._actionCapability(); } }; /** * Decide the first auth method to try. */ _actionChooseFirstAuthMethod = () => { if ( [ Ci.nsMsgSocketType.trySTARTTLS, Ci.nsMsgSocketType.alwaysSTARTTLS, ].includes(this._server.socketType) && !this._secureTransport ) { if (this._capabilities.includes("STARTTLS")) { // Init STARTTLS negotiation if required by user pref and supported. this._nextAction = this._actionStarttlsResponse; this._sendTagged("STARTTLS"); } else { // Abort if not supported. this._logger.error("Server doesn't support STARTTLS. Aborting."); this._actionError("imapServerDisconnected"); this._actionDone(Cr.NS_ERROR_FAILURE); } return; } this._possibleAuthMethods = this._preferredAuthMethods.filter(x => this._supportedAuthMethods.includes(x) ); if ( !this._possibleAuthMethods.length && this._server.authMethod == Ci.nsMsgAuthMethod.passwordCleartext && !this._capabilities.includes("LOGINDISABLED") ) { this._possibleAuthMethods = ["OLDLOGIN"]; } this._logger.debug(`Possible auth methods: ${this._possibleAuthMethods}`); this._nextAuthMethod = this._possibleAuthMethods[0]; if (this._capabilities.includes("CLIENTID") && this._server.clientid) { this._nextAction = res => { if (res.status == "OK") { this._actionAuth(); } else { this._actionDone(Cr.NS_ERROR_FAILURE); } }; this._sendTagged(`CLIENTID UUID ${this._server.clientid}`); } else { this._actionAuth(); } }; /** * Handle the STARTTLS response. * * @param {ImapResponse} res - The server response. */ _actionStarttlsResponse(res) { if (!res.status == "OK") { this._actionDone(Cr.NS_ERROR_FAILURE); return; } this._socket.upgradeToSecure(); this._secureTransport = true; this._actionCapability(); } /** * Init authentication depending on server capabilities and user prefs. */ _actionAuth = async () => { if (!this._nextAuthMethod) { this._socket.close(); this._actionDone(Cr.NS_ERROR_FAILURE); return; } this._authenticating = true; this._currentAuthMethod = this._nextAuthMethod; this._nextAuthMethod = this._possibleAuthMethods[ this._possibleAuthMethods.indexOf(this._currentAuthMethod) + 1 ]; switch (this._currentAuthMethod) { case "OLDLOGIN": this._nextAction = this._actionAuthResponse; let password = await this._getPassword(); this._sendTagged( `LOGIN ${this._authenticator.username} ${password}`, true ); break; case "PLAIN": this._nextAction = this._actionAuthPlain; this._sendTagged("AUTHENTICATE PLAIN"); break; case "LOGIN": this._nextAction = this._actionAuthLoginUser; this._sendTagged("AUTHENTICATE LOGIN"); break; case "CRAM-MD5": this._nextAction = this._actionAuthCramMd5; this._sendTagged("AUTHENTICATE CRAM-MD5"); break; case "GSSAPI": { this._nextAction = this._actionAuthGssapi; this._authenticator.initGssapiAuth("imap"); let token; try { token = this._authenticator.getNextGssapiToken(""); } catch (e) { this._logger.error(e); this._actionDone(Cr.NS_ERROR_FAILURE); return; } this._sendTagged(`AUTHENTICATE GSSAPI ${token}`, true); break; } case "NTLM": { this._nextAction = this._actionAuthNtlm; this._authenticator.initNtlmAuth("imap"); let token; try { token = this._authenticator.getNextNtlmToken(""); } catch (e) { this._logger.error(e); this._actionDone(Cr.NS_ERROR_FAILURE); return; } this._sendTagged(`AUTHENTICATE NTLM ${token}`, true); break; } case "XOAUTH2": this._nextAction = this._actionAuthResponse; let token = await this._authenticator.getOAuthToken(); this._sendTagged(`AUTHENTICATE XOAUTH2 ${token}`, true); break; case "EXTERNAL": this._nextAction = this._actionAuthResponse; this._sendTagged( `AUTHENTICATE EXTERNAL ${this._authenticator.username}` ); break; default: this._actionDone(); } }; /** * @param {ImapResponse} res - Response received from the server. */ _actionAuthResponse = res => { this._authenticating = false; if (this.verifyLogon) { this._actionDone(res.status == "OK" ? Cr.NS_OK : Cr.NS_ERROR_FAILURE); return; } if (res.status == "OK") { this._serverSink.userAuthenticated = true; if (res.capabilities) { this._capabilities = res.capabilities; this._server.wrappedJSObject.capabilities = res.capabilities; this._actionId(); } else { this._nextAction = res => { this._capabilities = res.capabilities; this._server.wrappedJSObject.capabilities = res.capabilities; this._actionId(); }; this._sendTagged("CAPABILITY"); } return; } if ( ["OLDLOGIN", "PLAIN", "LOGIN", "CRAM-MD5"].includes( this._currentAuthMethod ) ) { // Ask user what to do. let action = this._authenticator.promptAuthFailed(); if (action == 1) { // Cancel button pressed. this._socket.close(); this._actionDone(Cr.NS_ERROR_FAILURE); return; } if (action == 2) { // 'New password' button pressed. this._authenticator.forgetPassword(); } // Retry. this._nextAuthMethod = this._possibleAuthMethods[0]; this._actionAuth(); return; } this._logger.error("Authentication failed."); this._actionDone(Cr.NS_ERROR_FAILURE); }; /** * Returns the saved/cached server password, or show a password dialog. If the * user cancels the dialog, stop the process. * * @returns {string} The server password. */ async _getPassword() { try { let password = await this._authenticator.getPassword(); return password; } catch (e) { if (e.result == Cr.NS_ERROR_ABORT) { this._actionDone(e.result); } throw e; } } /** * The second step of PLAIN auth. Send the auth token to the server. * * @param {ImapResponse} res - Response received from the server. */ _actionAuthPlain = async res => { this._nextAction = this._actionAuthResponse; this._send(await this._authenticator.getPlainToken(), true); }; /** * The second step of LOGIN auth. Send the username to the server. * * @param {ImapResponse} res - The server response. */ _actionAuthLoginUser = res => { this._nextAction = this._actionAuthLoginPass; this._send(btoa(this._authenticator.username), true); }; /** * The third step of LOGIN auth. Send the password to the server. * * @param {ImapResponse} res - The server response. */ _actionAuthLoginPass = async res => { this._nextAction = this._actionAuthResponse; let password = MailStringUtils.stringToByteString( await this._getPassword() ); this._send(btoa(password), true); }; /** * The second step of CRAM-MD5 auth, send a HMAC-MD5 signature to the server. * * @param {ImapResponse} res - The server response. */ _actionAuthCramMd5 = async res => { this._nextAction = this._actionAuthResponse; let password = await this._getPassword(); this._send( this._authenticator.getCramMd5Token(password, res.statusText), true ); }; /** * The second and next step of GSSAPI auth. * * @param {ImapResponse} res - The server response. */ _actionAuthGssapi = res => { if (res.tag != "+") { this._actionAuthResponse(res); return; } // Server returns a challenge, we send a new token. Can happen multiple times. let token; try { token = this._authenticator.getNextGssapiToken(res.statusText); } catch (e) { this._logger.error(e); this._actionAuthResponse(res); return; } this._send(token, true); }; /** * The second and next step of NTLM auth. * * @param {ImapResponse} res - The server response. */ _actionAuthNtlm = res => { if (res.tag != "+") { this._actionAuthResponse(res); return; } // Server returns a challenge, we send a new token. Can happen multiple times. let token; try { token = this._authenticator.getNextNtlmToken(res.statusText); } catch (e) { this._logger.error(e); this._actionAuthResponse(res); return; } this._send(token, true); }; /** * Send ID command to the server. * * @param {Function} [actionAfter] - A callback after processing ID command. */ _actionId = (actionAfter = this._actionEnableUtf8) => { if (this._capabilities.includes("ID") && Services.appinfo.name) { this._nextAction = res => { this._server.serverIDPref = res.id; actionAfter(); }; this._sendTagged( `ID ("name" "${Services.appinfo.name}" "version" "${Services.appinfo.version}")` ); } else { actionAfter(); } }; /** * Enable UTF8 if supported by the server. * * @param {Function} [actionAfter] - A callback after processing ENABLE UTF8. */ _actionEnableUtf8 = (actionAfter = this._discoverAllFoldersIfNecessary) => { if ( this._server.allowUTF8Accept && (this._capabilities.includes("UTF8=ACCEPT") || this._capabilities.includes("UTF8=ONLY")) ) { this._nextAction = res => { this._utf8Enabled = res.status == "OK"; this._server.utf8AcceptEnabled = this._utf8Enabled; actionAfter(); }; this._sendTagged("ENABLE UTF8=ACCEPT"); } else { this._utf8Enabled = false; actionAfter(); } }; /** * Execute an action with a folder selected. * * @param {nsIMsgFolder} folder - The folder to select. * @param {Function} actionInFolder - The action to execute. */ _actionInFolder(folder, actionInFolder) { if (this.folder == folder) { // If already in the folder, execute the action now. actionInFolder(); } else { // Send the SELECT command and queue the action. this._actionAfterSelectFolder = actionInFolder; this._nextAction = this._actionSelectResponse(folder); this._sendTagged(`SELECT "${this._getServerFolderName(folder)}"`); } } /** * Send LSUB or LIST command depending on the server capabilities. * * @param {string} [mailbox="*"] - The mailbox to list, default to list all. */ _actionListOrLsub(mailbox = "*") { this._nextAction = this._actionListResponse(); let command = this._capabilities.includes("LIST-EXTENDED") ? "LIST (SUBSCRIBED)" // rfc5258 : "LSUB"; command += ` "" "${mailbox}"`; if (this._capabilities.includes("SPECIAL-USE")) { command += " RETURN (SPECIAL-USE)"; // rfc6154 } this._sendTagged(command); this._listInboxSent = false; } /** * Handle LIST response. * * @param {Function} actionAfterResponse - A callback after handling the response. * @param {ImapResponse} res - Response received from the server. */ _actionListResponse = (actionAfterResponse = this._actionFinishFolderDiscovery) => res => { if (!this._hasInbox) { this._hasInbox = res.mailboxes.some( mailbox => mailbox.flags & ImapUtils.FLAG_IMAP_INBOX ); } for (let mailbox of res.mailboxes) { this._serverSink.possibleImapMailbox( mailbox.name.replaceAll(mailbox.delimiter, "/"), mailbox.delimiter, mailbox.flags ); } actionAfterResponse(res); }; /** * Send LIST command. * * @param {string} folderName - The name of the folder to list. * @param {Function} actionAfterResponse - A callback after handling the response. */ _actionList(folderName, actionAfterResponse) { this._nextAction = this._actionListResponse(actionAfterResponse); this._sendTagged(`LIST "" "${folderName}"`); } /** * Finish folder discovery after checking Inbox and Trash folders. */ _actionFinishFolderDiscovery = () => { if (!this._hasInbox && !this._listInboxSent) { this._actionList("INBOX"); this._listInboxSent = true; return; } if (!this._hasTrash && !this._listTrashSent) { this._actionCreateTrashFolderIfNeeded(); return; } this._serverSink.discoveryDone(); this._actionAfterDiscoverAllFolders ? this._actionAfterDiscoverAllFolders() : this._actionDone(); }; /** * If Trash folder is not found on server, create one and subscribe to it. */ _actionCreateTrashFolderIfNeeded() { let trashFolderName = this._server.trashFolderName; this._actionList(trashFolderName, res => { this._hasTrash = res.mailboxes.length > 0; if (this._hasTrash) { // Trash folder exists. this._actionFinishFolderDiscovery(); } else { // Trash folder doesn't exist, create one and subscribe to it. this._nextAction = res => { this._actionList(trashFolderName, () => { // After subscribing, finish folder discovery. this._nextAction = this._actionFinishFolderDiscovery; this._sendTagged(`SUBSCRIBE "${trashFolderName}"`); }); }; this._sendTagged(`CREATE "${trashFolderName}"`); } }); this._listTrashSent = true; } /** * Create and subscribe to a folder. * * @param {string} folderName - The folder name. * @param {Function} callbackAfterSubscribe - The action after the subscribe * command. */ _actionCreateAndSubscribe(folderName, callbackAfterSubscribe) { this._nextAction = res => { this._nextAction = callbackAfterSubscribe; this._sendTagged(`SUBSCRIBE "${folderName}"`); }; this._sendTagged(`CREATE "${folderName}"`); } /** * Handle SELECT response. */ _actionSelectResponse = folder => res => { if (folder) { this.folder = folder; } this._supportedFlags = res.permanentflags || res.flags; this._folderState = res; if (this._capabilities.includes("QUOTA")) { this._actionGetQuotaData(); } else { this._actionAfterSelectFolder(); } }; /** * Send GETQUOTAROOT command and handle the response. */ _actionGetQuotaData() { this._folderSink = this.folder.QueryInterface(Ci.nsIImapMailFolderSink); this._nextAction = res => { const INVALIDATE_QUOTA = 0; const STORE_QUOTA = 1; const VALIDATE_QUOTA = 2; for (let root of res.quotaRoots || []) { this._folderSink.setFolderQuotaData(INVALIDATE_QUOTA, root, 0, 0); } for (let [mailbox, resource, usage, limit] of res.quotas || []) { this._folderSink.setFolderQuotaData( STORE_QUOTA, mailbox ? `${mailbox} / ${resource}` : resource, usage, limit ); } this._folderSink.setFolderQuotaData(VALIDATE_QUOTA, "", 0, 0); this._actionAfterSelectFolder(); }; this._sendTagged( `GETQUOTAROOT "${this._getServerFolderName(this.folder)}"` ); this._folderSink.folderQuotaCommandIssued = true; } /** * Handle RENAME response. Three steps are involved. * * @param {string} oldName - The old folder name. * @param {string} newName - The new folder name. * @param {boolean} [isMove] - Is it response to MOVE command. * @param {ImapResponse} res - The server response. */ _actionRenameResponse = (oldName, newName, isMove) => res => { // Step 3: Rename the local folder and send LIST command to re-sync folders. let actionAfterUnsubscribe = () => { this._serverSink.onlineFolderRename(this._msgWindow, oldName, newName); if (isMove) { this._actionDone(); } else { this._actionListOrLsub(newName); } }; // Step 2: unsubscribe to the oldName. this._nextAction = () => { this._nextAction = actionAfterUnsubscribe; this._sendTagged(`UNSUBSCRIBE "${oldName}"`); }; // Step 1: subscribe to the newName. this._sendTagged(`SUBSCRIBE "${newName}"`); }; /** * Send UID FETCH request to the server. */ _actionUidFetch() { if (this.runningUri.imapAction == Ci.nsIImapUrl.nsImapLiteSelectFolder) { this._nextAction = () => this._actionDone(); } else { this._nextAction = this._actionUidFetchResponse; } this._sendTagged("UID FETCH 1:* (FLAGS)"); } /** * Handle UID FETCH response. * * @param {ImapResponse} res - Response received from the server. */ _actionUidFetchResponse(res) { let outFolderInfo = {}; this.folder.getDBFolderInfoAndDB(outFolderInfo); let highestUid = outFolderInfo.value.getUint32Property( "highestRecordedUID", 0 ); this._folderSink = this.folder.QueryInterface(Ci.nsIImapMailFolderSink); this._folderSink.UpdateImapMailboxInfo(this, this._getMailboxSpec()); let latestUid = this._messageUids.at(-1); if (latestUid > highestUid) { let extraItems = ""; if (this._server.isGMailServer) { extraItems += "X-GM-MSGID X-GM-THRID X-GM-LABELS "; } this._nextAction = this._actionUidFetchHeaderResponse; this._sendTagged( `UID FETCH ${ highestUid + 1 }:${latestUid} (UID ${extraItems}RFC822.SIZE FLAGS BODY.PEEK[HEADER])` ); } else { this._folderSink.headerFetchCompleted(this); if (this._bodysToDownload.length) { let uids = this._bodysToDownload.join(","); this._nextAction = this._actionUidFetchBodyResponse; this._sendTagged( `UID FETCH ${uids} (UID RFC822.SIZE FLAGS BODY.PEEK[])` ); return; } this._actionDone(); } } /** * Make an nsIMailboxSpec instance to interact with nsIImapMailFolderSink. * * @returns {nsIMailboxSpec} */ _getMailboxSpec() { return { QueryInterface: ChromeUtils.generateQI(["nsIMailboxSpec"]), folder_UIDVALIDITY: this._folderState.uidvalidity, box_flags: this._folderState.flags, supportedUserFlags: this._folderState.supportedUserFlags, nextUID: this._folderState.attributes.uidnext, numMessages: this._messages.size, numUnseenMessages: this._folderState.attributes.unseen, flagState: this.flagAndUidState, }; } /** * Handle UID FETCH BODY.PEEK[HEADER] response. * * @param {ImapResponse} res - Response received from the server. */ _actionUidFetchHeaderResponse(res) { this.folder .QueryInterface(Ci.nsIImapMailFolderSink) .headerFetchCompleted(this); if (this._bodysToDownload.length) { // nsImapMailFolder decides to fetch the full body by calling // NotifyBodysToDownload. let uids = this._bodysToDownload.join(","); this._nextAction = this._actionUidFetchBodyResponse; this._sendTagged(`UID FETCH ${uids} (UID RFC822.SIZE FLAGS BODY.PEEK[])`); return; } this._actionDone(); } /** * Handle UID FETCH BODY response. * * @param {ImapResponse} res - Response received from the server. */ _actionUidFetchBodyResponse(res) { this._actionDone(); } /** * Handle a single message data response. * * @param {MessageData} msg - Message data parsed in ImapResponse. */ _onMessage = msg => { this._msgSink = this.folder.QueryInterface(Ci.nsIImapMessageSink); this._folderSink = this.folder.QueryInterface(Ci.nsIImapMailFolderSink); // Handle message flags. if ((msg.uid || msg.sequence) && msg.flags != undefined) { let uid = msg.uid; if (uid && msg.sequence) { this._messageUids[msg.sequence] = uid; this._messages.set(uid, msg); } else if (msg.sequence) { uid = this._messageUids[msg.sequence]; } if (uid) { this.folder .QueryInterface(Ci.nsIImapMessageSink) .notifyMessageFlags( msg.flags, msg.keywords, uid, this._folderState.highestmodseq ); } } if (msg.body) { if (!msg.body.endsWith("\r\n")) { msg.body += "\r\n"; } if (msg.bodySection.length == 1 && msg.bodySection[0] == "HEADER") { // Handle message headers. this._messageUids[msg.sequence] = msg.uid; this._messages.set(msg.uid, msg); this._folderSink.StartMessage(this.runningUri); let hdrXferInfo = { numHeaders: 1, getHeader() { return { msgUid: msg.uid, msgSize: msg.size, get msgHdrs() { let sepIndex = msg.body.indexOf("\r\n\r\n"); return sepIndex == -1 ? msg.body + "\r\n" : msg.body.slice(0, sepIndex + 2); }, }; }, }; this._folderSink.parseMsgHdrs(this, hdrXferInfo); } else { // Handle message body. let shouldStoreMsgOffline = false; try { shouldStoreMsgOffline = this.folder.shouldStoreMsgOffline(msg.uid); } catch (e) {} if ( (shouldStoreMsgOffline || this.runningUri.QueryInterface(Ci.nsIImapUrl) .storeResultsOffline) && msg.body ) { this._folderSink.StartMessage(this.runningUri); this._msgSink.parseAdoptedMsgLine(msg.body, msg.uid, this.runningUri); this._msgSink.normalEndMsgWriteStream( msg.uid, true, this.runningUri, msg.body.length ); this._folderSink.EndMessage(this.runningUri, msg.uid); } this.onData?.(msg.body); // Release some memory. msg.body = ""; } } }; /** * Send NOOP command. */ _actionNoop() { this._nextAction = this._actionNoopResponse; this._sendTagged("NOOP"); } /** * Handle NOOP response. * * @param {ImapResponse} res - Response received from the server. */ _actionNoopResponse(res) { if ( (res.exists && res.exists != this._folderState.exists) || res.expunged.length ) { // Handle messages number changes, re-sync the folder. this._folderState.exists = res.exists; this._actionAfterSelectFolder = this._actionUidFetch; this._nextAction = this._actionSelectResponse(); if (res.expunged.length) { this._messageUids = []; this._messages.clear(); } let folder = this.folder; this.folder = null; this.selectFolder(folder); } else if (res.messages.length || res.exists) { let outFolderInfo = {}; this.folder.getDBFolderInfoAndDB(outFolderInfo); let highestUid = outFolderInfo.value.getUint32Property( "highestRecordedUID", 0 ); this._nextAction = this._actionUidFetchResponse; this._sendTagged(`UID FETCH ${highestUid + 1}:* (FLAGS)`); } else { if (res.exists == 0) { this._messageUids = []; this._messages.clear(); this.folder .QueryInterface(Ci.nsIImapMailFolderSink) .UpdateImapMailboxInfo(this, this._getMailboxSpec()); } if (!this._idling) { this._actionDone(); } } } /** * Show an error prompt. * * @param {string} errorName - An error name corresponds to an entry of * imapMsgs.properties. */ _actionError(errorName) { if (!this._msgWindow) { return; } let bundle = Services.strings.createBundle( "chrome://messenger/locale/imapMsgs.properties" ); let errorMsg = bundle.formatStringFromName(errorName, [ this._server.hostName, ]); Services.prompt.alert(this._msgWindow.domWindow, null, errorMsg); } /** * Finish a request and do necessary cleanup. */ _actionDone = (status = Cr.NS_OK) => { this._logger.debug(`Done with status=${status}`); this._nextAction = null; this._urlListener?.OnStopRunningUrl(this.runningUri, status); this.runningUri.SetUrlState(false, status); this.onDone?.(status); this._reset(); // Tell ImapIncomingServer this client can be reused now. this.onFree?.(); }; /** @see nsIImapProtocol */ NotifyBodysToDownload(keys) { this._logger.debug("NotifyBodysToDownload", keys); this._bodysToDownload = keys; } GetRunningUrl() { this._logger.debug("GetRunningUrl"); } get flagAndUidState() { // The server sequence is 1 based, nsIImapFlagAndUidState sequence is 0 based. let getUidOfMessage = index => this._messageUids[index + 1]; let getMessageFlagsByUid = uid => this._messages.get(uid)?.flags; return { QueryInterface: ChromeUtils.generateQI(["nsIImapFlagAndUidState"]), numberOfMessages: this._messages.size, getUidOfMessage, getMessageFlags: index => getMessageFlagsByUid(getUidOfMessage(index)), hasMessage: uid => this._messages.has(uid), getMessageFlagsByUid, getCustomFlags: uid => this._messages.get(uid)?.keywords, getCustomAttribute: (uid, name) => { let value = this._messages.get(uid)?.customAttributes[name]; return Array.isArray(value) ? value.join(" ") : value; }, }; } }