diff options
Diffstat (limited to 'comm/mailnews/imap/src/ImapIncomingServer.jsm')
-rw-r--r-- | comm/mailnews/imap/src/ImapIncomingServer.jsm | 783 |
1 files changed, 783 insertions, 0 deletions
diff --git a/comm/mailnews/imap/src/ImapIncomingServer.jsm b/comm/mailnews/imap/src/ImapIncomingServer.jsm new file mode 100644 index 0000000000..c9cf86bfdc --- /dev/null +++ b/comm/mailnews/imap/src/ImapIncomingServer.jsm @@ -0,0 +1,783 @@ +/* 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 = ["ImapIncomingServer"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { MsgIncomingServer } = ChromeUtils.import( + "resource:///modules/MsgIncomingServer.jsm" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + ImapClient: "resource:///modules/ImapClient.jsm", + ImapCapFlags: "resource:///modules/ImapUtils.jsm", + ImapUtils: "resource:///modules/ImapUtils.jsm", + MailUtils: "resource:///modules/MailUtils.jsm", +}); + +/** + * @implements {nsIImapServerSink} + * @implements {nsIImapIncomingServer} + * @implements {nsIMsgIncomingServer} + * @implements {nsIUrlListener} + * @implements {nsISupportsWeakReference} + * @implements {nsISubscribableServer} + */ +class ImapIncomingServer extends MsgIncomingServer { + QueryInterface = ChromeUtils.generateQI([ + "nsIImapServerSink", + "nsIImapIncomingServer", + "nsIMsgIncomingServer", + "nsIUrlListener", + "nsISupportsWeakReference", + "nsISubscribableServer", + ]); + + _logger = lazy.ImapUtils.logger; + + constructor() { + super(); + + this._userAuthenticated = false; + + // nsIMsgIncomingServer attributes. + this.localStoreType = "imap"; + this.localDatabaseType = "imap"; + this.canBeDefaultServer = true; + this.canSearchMessages = true; + + // nsISubscribableServer attributes. + this.supportsSubscribeSearch = false; + + // nsIImapIncomingServer attributes that map directly to pref values. + this._mapAttrsToPrefs([ + ["Bool", "allowUTF8Accept", "allow_utf8_accept"], + ["Bool", "autoSyncOfflineStores", "autosync_offline_stores"], + ["Bool", "checkAllFoldersForNew", "check_all_folders_for_new"], + ["Bool", "cleanupInboxOnExit", "cleanup_inbox_on_exit"], + ["Bool", "downloadBodiesOnGetNewMail", "download_bodies_on_get_new_mail"], + ["Bool", "dualUseFolders", "dual_use_folders"], + ["Bool", "fetchByChunks", "fetch_by_chunks"], + ["Bool", "forceSelect", "force_select_imap"], + ["Bool", "isGMailServer", "is_gmail"], + ["Bool", "offlineDownload", "offline_download"], + ["Bool", "sendID", "send_client_info"], + ["Bool", "useCompressDeflate", "use_compress_deflate"], + ["Bool", "useCondStore", "use_condstore"], + ["Bool", "useIdle", "use_idle"], + ["Char", "adminUrl", "admin_url"], + ["Char", "otherUsersNamespace", "namespace.other_users"], + ["Char", "personalNamespace", "namespace.personal"], + ["Char", "publicNamespace", "namespace.public"], + ["Char", "serverIDPref", "serverIDResponse"], + ["Int", "autoSyncMaxAgeDays", "autosync_max_age_days"], + ["Int", "timeOutLimits", "timeout"], + ]); + } + + /** + * Most of nsISubscribableServer interfaces are delegated to + * this._subscribable. + */ + get _subscribable() { + if (!this._subscribableServer) { + this._subscribableServer = Cc[ + "@mozilla.org/messenger/subscribableserver;1" + ].createInstance(Ci.nsISubscribableServer); + this._subscribableServer.setIncomingServer(this); + } + return this._subscribableServer; + } + + /** @see nsISubscribableServer */ + get folderView() { + return this._subscribable.folderView; + } + + get subscribeListener() { + return this._subscribable.subscribeListener; + } + + set subscribeListener(value) { + this._subscribable.subscribeListener = value; + } + + set delimiter(value) { + this._subscribable.delimiter = value; + } + + subscribeCleanup() { + this._subscribableServer = null; + } + + startPopulating(msgWindow, forceToServer, getOnlyNew) { + this._loadingInSubscribeDialog = true; + this._subscribable.startPopulating(msgWindow, forceToServer, getOnlyNew); + this.delimiter = "/"; + this.setShowFullName(false); + MailServices.imap.getListOfFoldersOnServer(this, msgWindow); + } + + stopPopulating(msgWindow) { + this._loadingInSubscribeDialog = false; + this._subscribable.stopPopulating(msgWindow); + } + + addTo(name, addAsSubscribed, subscribable, changeIfExists) { + this._subscribable.addTo( + name, + addAsSubscribed, + subscribable, + changeIfExists + ); + } + + subscribe(name) { + this.subscribeToFolder(name, true); + } + + unsubscribe(name) { + this.subscribeToFolder(name, false); + } + + commitSubscribeChanges() { + this.performExpand(); + } + + setAsSubscribed(path) { + this._subscribable.setAsSubscribed(path); + } + + updateSubscribed() {} + + setState(path, state) { + return this._subscribable.setState(path, state); + } + + setShowFullName(showFullName) { + this._subscribable.setShowFullName(showFullName); + } + + hasChildren(path) { + return this._subscribable.hasChildren(path); + } + + isSubscribed(path) { + return this._subscribable.isSubscribed(path); + } + + isSubscribable(path) { + return this._subscribable.isSubscribable(path); + } + + getLeafName(path) { + return this._subscribable.getLeafName(path); + } + + getFirstChildURI(path) { + return this._subscribable.getFirstChildURI(path); + } + + getChildURIs(path) { + return this._subscribable.getChildURIs(path); + } + + /** @see nsIUrlListener */ + OnStartRunningUrl() {} + + OnStopRunningUrl(url, exitCode) { + switch (url.QueryInterface(Ci.nsIImapUrl).imapAction) { + case Ci.nsIImapUrl.nsImapDiscoverAllAndSubscribedBoxesUrl: + this.stopPopulating(); + break; + } + } + + /** @see nsIMsgIncomingServer */ + get serverRequiresPasswordForBiff() { + return !this._userAuthenticated; + } + + get offlineSupportLevel() { + const OFFLINE_SUPPORT_LEVEL_UNDEFINED = -1; + const OFFLINE_SUPPORT_LEVEL_REGULAR = 10; + let level = this.getIntValue("offline_support_level"); + return level != OFFLINE_SUPPORT_LEVEL_UNDEFINED + ? level + : OFFLINE_SUPPORT_LEVEL_REGULAR; + } + + get constructedPrettyName() { + let identity = MailServices.accounts.getFirstIdentityForServer(this); + let email; + if (identity) { + email = identity.email; + } else { + email = `${this.username}`; + if (this.hostName) { + email += `@${this.hostName}`; + } + } + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/imapMsgs.properties" + ); + return bundle.formatStringFromName("imapDefaultAccountName", [email]); + } + + performBiff(msgWindow) { + this.performExpand(msgWindow); + } + + performExpand(msgWindow) { + this._setFolderToUnverified(); + this.hasDiscoveredFolders = false; + MailServices.imap.discoverAllFolders(this.rootFolder, this, msgWindow); + } + + /** + * Recursively set a folder and its subFolders to unverified state. + * + * @param {nsIMsgFolder} folder - The folder to operate on. + */ + _setFolderToUnverified(folder) { + if (!folder) { + this._setFolderToUnverified(this.rootFolder); + return; + } + + folder.QueryInterface( + Ci.nsIMsgImapMailFolder + ).verifiedAsOnlineFolder = false; + for (let child of folder.subFolders) { + this._setFolderToUnverified(child); + } + } + + closeCachedConnections() { + // Close all connections. + for (let client of this._connections) { + client.logout(); + } + // Cancel all waitings in queue. + for (let resolve of this._connectionWaitingQueue) { + resolve(false); + } + this._connections = []; + } + + verifyLogon(urlListener, msgWindow) { + return MailServices.imap.verifyLogon( + this.rootFolder, + urlListener, + msgWindow + ); + } + + subscribeToFolder(name, subscribe) { + let folder = this.rootMsgFolder.findSubFolder(name); + if (subscribe) { + return MailServices.imap.subscribeFolder(folder, name, null); + } + return MailServices.imap.unsubscribeFolder(folder, name, null); + } + + /** @see nsIImapServerSink */ + + /** @type {boolean} - User has authenticated with the server. */ + get userAuthenticated() { + return this._userAuthenticated; + } + + set userAuthenticated(value) { + this._userAuthenticated = value; + if (value) { + MailServices.accounts.userNeedsToAuthenticate = false; + } + } + + possibleImapMailbox(folderPath, delimiter, boxFlags) { + let explicitlyVerify = false; + + if (folderPath.endsWith("/")) { + folderPath = folderPath.slice(0, -1); + if (!folderPath) { + throw Components.Exception( + "Empty folder path", + Cr.NS_ERROR_INVALID_ARG + ); + } + explicitlyVerify = !(boxFlags & lazy.ImapUtils.FLAG_NAMESPACE); + } + + if (this.hasDiscoveredFolders && this._loadingInSubscribeDialog) { + // Populate the subscribe dialog. + let noSelect = boxFlags & lazy.ImapUtils.FLAG_NO_SELECT; + this.addTo( + folderPath, + this.doingLsub && !noSelect, + !noSelect, + this.doingLsub + ); + return false; + } + + let slashIndex = folderPath.indexOf("/"); + let token = folderPath; + let rest = ""; + if (slashIndex > 0) { + token = folderPath.slice(0, slashIndex); + rest = folderPath.slice(slashIndex); + } + + folderPath = (/^inbox/i.test(token) ? "INBOX" : token) + rest; + + let uri = this.rootFolder.URI; + let parentName = folderPath; + let parentUri = uri; + let hasParent = false; + let lastSlashIndex = folderPath.lastIndexOf("/"); + if (lastSlashIndex > 0) { + parentName = parentName.slice(0, lastSlashIndex); + hasParent = true; + parentUri += "/" + parentName; + } + + if (/^inbox/i.test(folderPath) && delimiter == "|") { + delimiter = "/"; + this.rootFolder.QueryInterface( + Ci.nsIMsgImapMailFolder + ).hierarchyDelimiter = delimiter; + } + + uri += "/" + folderPath; + let child = this.rootFolder.getChildWithURI( + uri, + true, + /^inbox/i.test(folderPath) + ); + + let isNewFolder = !child; + if (isNewFolder) { + if (hasParent) { + let parent = this.rootFolder.getChildWithURI( + parentUri, + true, + /^inbox/i.test(parentName) + ); + + if (!parent) { + this.possibleImapMailbox( + parentName, + delimiter, + lazy.ImapUtils.FLAG_NO_SELECT | + (boxFlags & + (lazy.ImapUtils.FLAG_PUBLIC_MAILBOX | + lazy.ImapUtils.FLAG_OTHER_USERS_MAILBOX | + lazy.ImapUtils.FLAG_PERSONAL_MAILBOX)) + ); + } + } + this.rootFolder + .QueryInterface(Ci.nsIMsgImapMailFolder) + .createClientSubfolderInfo(folderPath, delimiter, boxFlags, false); + child = this.rootFolder.getChildWithURI( + uri, + true, + /^inbox/i.test(folderPath) + ); + } + if (child) { + let imapFolder = child.QueryInterface(Ci.nsIMsgImapMailFolder); + imapFolder.verifiedAsOnlineFolder = true; + imapFolder.hierarchyDelimiter = delimiter; + if (boxFlags & lazy.ImapUtils.FLAG_IMAP_TRASH) { + if (this.deleteModel == Ci.nsMsgImapDeleteModels.MoveToTrash) { + child.setFlag(Ci.nsMsgFolderFlags.Trash); + } + } + imapFolder.boxFlags = boxFlags; + imapFolder.explicitlyVerify = explicitlyVerify; + let onlineName = imapFolder.onlineName; + folderPath = folderPath.replaceAll("/", delimiter); + if (delimiter != "/") { + folderPath = decodeURIComponent(folderPath); + } + + if (boxFlags & lazy.ImapUtils.FLAG_IMAP_INBOX) { + // GMail gives us a localized name for the inbox but doesn't let + // us select that localized name. + imapFolder.onlineName = "INBOX"; + } else if (!onlineName || onlineName != folderPath) { + imapFolder.onlineName = folderPath; + } + + child.prettyName = imapFolder.name; + if (isNewFolder) { + // Close the db so we don't hold open all the .msf files for new folders. + child.msgDatabase = null; + } + } + + return isNewFolder; + } + + discoveryDone() { + this.hasDiscoveredFolders = true; + // No need to verify the root. + this.rootFolder.QueryInterface( + Ci.nsIMsgImapMailFolder + ).verifiedAsOnlineFolder = true; + let unverified = this._getUnverifiedFolders(this.rootFolder); + this._logger.debug( + `discoveryDone, unverified folders count=${unverified.length}.` + ); + for (let folder of unverified) { + if (folder.flags & Ci.nsMsgFolderFlags.Virtual) { + // Do not remove virtual folders. + continue; + } + let imapFolder = folder.QueryInterface(Ci.nsIMsgImapMailFolder); + if ( + !this.usingSubscription || + imapFolder.explicitlyVerify || + (folder.hasSubFolders && this._noDescendentsAreVerified(folder)) + ) { + imapFolder.explicitlyVerify = false; + imapFolder.list(); + } else if (folder.parent) { + imapFolder.removeLocalSelf(); + this._logger.debug(`Removed unverified folder name=${folder.name}`); + } + } + } + + /** + * Find local folders that do not exist on the server. + * + * @param {nsIMsgFolder} parentFolder - The folder to check. + * @returns {nsIMsgFolder[]} + */ + _getUnverifiedFolders(parentFolder) { + let folders = []; + let imapFolder = parentFolder.QueryInterface(Ci.nsIMsgImapMailFolder); + if (!imapFolder.verifiedAsOnlineFolder || imapFolder.explicitlyVerify) { + folders.push(imapFolder); + } + for (let folder of parentFolder.subFolders) { + folders.push(...this._getUnverifiedFolders(folder)); + } + return folders; + } + + /** + * Returns true if all sub folders are unverified. + * + * @param {nsIMsgFolder} parentFolder - The folder to check. + * @returns {nsIMsgFolder[]} + */ + _noDescendentsAreVerified(parentFolder) { + for (let folder of parentFolder.subFolders) { + let imapFolder = folder.QueryInterface(Ci.nsIMsgImapMailFolder); + if ( + imapFolder.verifiedAsOnlineFolder || + !this._noDescendentsAreVerified(folder) + ) { + return false; + } + } + return true; + } + + onlineFolderRename(msgWindow, oldName, newName) { + let folder = this._getFolder(oldName).QueryInterface( + Ci.nsIMsgImapMailFolder + ); + let index = newName.lastIndexOf("/"); + let parent = + index > 0 ? this._getFolder(newName.slice(0, index)) : this.rootFolder; + folder.renameLocal(newName, parent); + if (parent instanceof Ci.nsIMsgImapMailFolder) { + try { + parent.renameClient(msgWindow, folder, oldName, newName); + } catch (e) { + this._logger.error("renameClient failed", e); + } + } + + this._getFolder(newName).NotifyFolderEvent("RenameCompleted"); + } + + /** + * Given a canonical folder name, returns the corresponding msg folder. + * + * @param {string} name - The canonical folder name, e.g. a/b/c. + * @returns {nsIMsgFolder} The corresponding msg folder. + */ + _getFolder(name) { + return lazy.MailUtils.getOrCreateFolder(this.rootFolder.URI + "/" + name); + } + + abortQueuedUrls() {} + + setCapability(capabilityFlags) { + this._capabilityFlags = capabilityFlags; + if (capabilityFlags & lazy.ImapCapFlags.Gmail) { + this.isGMailServer = true; + } + } + + /** @see nsIImapIncomingServer */ + getCapability() { + return this._capabilityFlags; + } + + get deleteModel() { + return this.getIntValue("delete_model"); + } + + set deleteModel(value) { + this.setIntValue("delete_model", value); + let trashFolder = this._getFolder(this.trashFolderName); + if (trashFolder) { + if (value == Ci.nsMsgImapDeleteModels.MoveToTrash) { + trashFolder.setFlag(Ci.nsMsgFolderFlags.Trash); + } else { + trashFolder.clearFlag(Ci.nsMsgFolderFlags.Trash); + } + } + } + + get usingSubscription() { + return this.getBoolValue("using_subscription"); + } + + get trashFolderName() { + return this.getUnicharValue("trash_folder_name") || "Trash"; + } + + get maximumConnectionsNumber() { + let maxConnections = this.getIntValue("max_cached_connections", 0); + if (maxConnections > 0) { + return maxConnections; + } + // The default is 5 connections, if the pref value is 0, we use the default. + // If it's negative, treat it as 1. + maxConnections = maxConnections == 0 ? 5 : 1; + this.maximumConnectionsNumber = maxConnections; + return maxConnections; + } + + set maximumConnectionsNumber(value) { + this.setIntValue("max_cached_connections", value); + } + + GetNewMessagesForNonInboxFolders( + folder, + msgWindow, + forceAllFolders, + performingBiff + ) { + let flags = folder.flags; + + if ( + folder.QueryInterface(Ci.nsIMsgImapMailFolder).canOpenFolder && + ((forceAllFolders && + !( + flags & + (Ci.nsMsgFolderFlags.Inbox | + Ci.nsMsgFolderFlags.Trash | + Ci.nsMsgFolderFlags.Junk | + Ci.nsMsgFolderFlags.Virtual) + )) || + flags & Ci.nsMsgFolderFlags.CheckNew) + ) { + folder.gettingNewMessages = true; + if (performingBiff) { + folder.performingBiff = true; + } + } + + if ( + Services.prefs.getBoolPref("mail.imap.use_status_for_biff", false) && + !MailServices.mailSession.IsFolderOpenInWindow(folder) + ) { + folder.updateStatus(this, msgWindow); + } else { + folder.updateFolder(msgWindow); + } + + for (let subFolder of folder.subFolders) { + this.GetNewMessagesForNonInboxFolders( + subFolder, + msgWindow, + forceAllFolders, + performingBiff + ); + } + } + + CloseConnectionForFolder(folder) { + for (let client of this._connections) { + if (client.folder == folder) { + client.logout(); + } + } + } + + _capabilities = []; + + set capabilities(value) { + this._capabilities = value; + this.setCapability(lazy.ImapCapFlags.stringsToFlags(value)); + } + + // @type {ImapClient[]} - An array of connections. + _connections = []; + // @type {Function[]} - An array of Promise.resolve functions. + _connectionWaitingQueue = []; + + /** + * Wait for a free connection. + * + * @param {nsIMsgFolder} folder - The folder to operate on. + * @returns {ImapClient} + */ + async _waitForNextClient(folder) { + // Wait until a connection is available. canGetNext is false when + // closeCachedConnections is called. + let canGetNext = await new Promise(resolve => + this._connectionWaitingQueue.push(resolve) + ); + if (canGetNext) { + return this._getNextClient(folder); + } + return null; + } + + /** + * Check if INBOX folder is selected in a connection. + * + * @param {ImapClient} client - The client to check. + * @returns {boolean} + */ + _isInboxConnection(client) { + return client.folder?.onlineName.toUpperCase() == "INBOX"; + } + + /** + * Get a free connection that can be used. + * + * @param {nsIMsgFolder} folder - The folder to operate on. + * @returns {ImapClient} + */ + async _getNextClient(folder) { + let client; + + for (client of this._connections) { + if (folder && client.folder == folder) { + if (client.busy) { + // Prevent operating on the same folder in two connections. + return this._waitForNextClient(folder); + } + // If we're idling in the target folder, reuse it. + client.busy = true; + return client; + } + } + + // Create a new client if the pool is not full. + if (this._connections.length < this.maximumConnectionsNumber) { + client = new lazy.ImapClient(this); + this._connections.push(client); + client.busy = true; + return client; + } + + let freeConnections = this._connections.filter(c => !c.busy); + + // Wait if no free connection. + if (!freeConnections.length) { + return this._waitForNextClient(folder); + } + + // Reuse any free connection if only have one connection or IDLE not used. + if ( + this.maximumConnectionsNumber <= 1 || + !this.useIdle || + !this._capabilities.includes("IDLE") + ) { + freeConnections[0].busy = true; + return freeConnections[0]; + } + + // Reuse non-inbox free connection. + client = freeConnections.find(c => !this._isInboxConnection(c)); + if (client) { + client.busy = true; + return client; + } + return this._waitForNextClient(folder); + } + + /** + * Do some actions with a connection. + * + * @param {nsIMsgFolder} folder - The folder to operate on. + * @param {Function} handler - A callback function to take a ImapClient + * instance, and do some actions. + */ + async withClient(folder, handler) { + let client = await this._getNextClient(folder); + if (!client) { + return; + } + let startIdle = async () => { + if (!this.useIdle || !this._capabilities.includes("IDLE")) { + return; + } + + // IDLE is configed and supported, use IDLE to receive server pushes. + let hasInboxConnection = this._connections.some(c => + this._isInboxConnection(c) + ); + let alreadyIdling = + client.folder && + this._connections.find( + c => c != client && !c.busy && c.folder == client.folder + ); + if (!hasInboxConnection) { + client.selectFolder( + this.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox) + ); + } else if (client.folder && !alreadyIdling) { + client.idle(); + } else if (alreadyIdling) { + client.folder = null; + } + }; + client.onFree = () => { + client.busy = false; + let resolve = this._connectionWaitingQueue.shift(); + if (resolve) { + // Resolve the first waiting in queue. + resolve(true); + } else if (client.isOnline) { + startIdle(); + } + }; + handler(client); + client.connect(); + } +} + +ImapIncomingServer.prototype.classID = Components.ID( + "{b02a4e1c-0d9e-498c-8b9d-18917ba9f65b}" +); |