diff --git a/lib/account.js b/lib/account.js index b3259480..70ba3148 100644 --- a/lib/account.js +++ b/lib/account.js @@ -395,18 +395,24 @@ class Account { } let accountData = this.unserializeAccountData(result); - if (requireValid && accountData.state !== 'connected') { + if (requireValid && !['connected', 'connecting', 'syncing'].includes(accountData.state)) { let err; switch (accountData.state) { case 'init': err = new Error('Requested account is not yet initialized'); err.code = 'NotYetConnected'; break; + /* + // Check disabled for the following states - allow commands to go through. + // A secondary IMAP connection is opened if possible. + */ + /* case 'connecting': case 'syncing': err = new Error('Requested account is not yet connected'); err.code = 'NotYetConnected'; break; + */ case 'authenticationError': err = new Error('Requested account can not be authenticated'); err.code = 'AuthenticationFails'; diff --git a/lib/connection.js b/lib/connection.js index 5dca6ff5..a859d927 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -113,7 +113,8 @@ class Connection { this.imapConfig = { // Set emitLogs to true if you want to get all the log entries as objects from the IMAP module logger: this.mainLogger.child({ - sub: 'imap-connection' + sub: 'imap-connection', + channel: 'primary' }), clientInfo: { name: packageData.name, @@ -147,6 +148,11 @@ class Connection { this.paused = false; + this.imapClient = null; + this.commandClient = null; + + this.syncing = false; + this.state = 'connecting'; } @@ -179,6 +185,89 @@ class Connection { }, ENSURE_MAIN_TTL); } + async getImapConnection(allowSecondary) { + if (!this.syncing || !allowSecondary) { + return this.imapClient; + } + + try { + const connectionClient = await this.getCommandConnection(); + return connectionClient && connectionClient.usable ? connectionClient : this.imapClient; + } catch (err) { + return this.imapClient; + } + } + + async getCommandConnection() { + if (this.commandClient && this.commandClient.usable) { + // use existing command channel + return this.commandClient; + } + + let lock = this.accountObject.getLock(); + + let connectLock; + let lockKey = ['commandCient', this.account].join(':'); + + try { + connectLock = await lock.waitAcquireLock(lockKey, 5 * 60 * 1000, 1 * 60 * 1000); + if (!connectLock.success) { + this.logger.error({ msg: 'Failed to get lock', lockKey }); + throw new Error('Failed to get connection lock'); + } + } catch (err) { + this.logger.error({ msg: 'Failed to get lock', lockKey, err }); + throw err; + } + + try { + // create a new connection for the command channel + let accountData = await this.accountObject.loadAccountData(); + + if ((!accountData.imap && !accountData.oauth2) || (accountData.imap && accountData.imap.disabled)) { + return null; + } + + const commandCid = `${this.cid}:c`; + + let imapConfig = await this.getImapConfig(accountData); + + this.commandClient = new ImapFlow( + Object.assign({}, imapConfig, { + disableAutoIdle: true, + id: commandCid, + logger: this.logger.child({ + channel: 'command' + }) + }) + ); + + this.commandClient.secondaryConnection = true; + + try { + await this.commandClient.connect(); + this.logger.info({ msg: 'Command channel connected', cid: commandCid, channel: 'command', account: this.account }); + } catch (err) { + this.logger.error({ msg: 'Failed to connect command client', cid: commandCid, channel: 'command', account: this.account, err }); + throw err; + } + + this.commandClient.on('error', err => { + this.logger.error({ msg: 'IMAP connection error', cid: commandCid, channel: 'command', account: this.account, err }); + this.commandClient = null; + }); + + this.commandClient.on('close', async () => { + this.logger.info({ msg: 'Connection closed', cid: commandCid, channel: 'command', account: this.account }); + this.commandClient = null; + }); + + return this.commandClient; + } finally { + await lock.releaseLock(connectLock); + } + } + async ensureMainMailbox() { let mainPath = this.main ? this.main.path : 'INBOX'; if (this.mailbox && normalizePath(this.mailbox.path) === normalizePath(mainPath)) { @@ -534,7 +623,7 @@ class Connection { }); imapClient.on('close', async () => { - this.logger.info({ msg: 'Connection closed', account: this.account }); + this.logger.info({ msg: 'Connection closed', type: 'imapClient', account: this.account }); try { for (let mailbox of this.mailboxes) { @@ -1032,7 +1121,7 @@ class Connection { try { this.imapClient.removeAllListeners(); this.imapClient.on('error', err => { - this.logger.error({ msg: 'IMAP connection error', previous: true, account: this.account, err }); + this.logger.error({ msg: 'IMAP connection error', type: 'imapClient', previous: true, account: this.account, err }); }); this.imapClient.close(); } catch (err) { @@ -1049,24 +1138,20 @@ class Connection { this.syncFrom = accountData.syncFrom; if ((!accountData.imap && !accountData.oauth2) || (accountData.imap && accountData.imap.disabled)) { - // can not make connection + // can not make a connection this.state = 'unset'; return; } let imapConfig = await this.getImapConfig(accountData); - if (featureFlags.enabled('gmail api')) { - if (imapConfig.type === 'api') { - // TODO: handle API accounts - logger.info({ msg: 'API user connection', imapConfig }); - return; - } - } - - imapConfig.expungeHandler = async payload => await this.expungeHandler(payload); + this.imapClient = new ImapFlow( + Object.assign({}, imapConfig, { + expungeHandler: async payload => await this.expungeHandler(payload) + }) + ); - this.imapClient = new ImapFlow(imapConfig); + this.imapClient.primaryConnection = true; // if emitLogs option is true then separate log event is fired for every log entry this.imapClient.on('log', entry => { @@ -1083,7 +1168,7 @@ class Connection { }); this.imapClient.on('error', err => { - this.logger.error({ msg: 'IMAP connection error', account: this.account, err }); + this.logger.error({ msg: 'IMAP connection error', type: 'imapClient', account: this.account, err }); this.reconnect().catch(err => { this.logger.error({ msg: 'IMAP reconnection error', account: this.account, err }); }); @@ -1262,6 +1347,10 @@ class Connection { this.imapClient.close(); } + if (this.commandClient && this.commandClient.usable) { + this.commandClient.close(); + } + clearTimeout(this.refreshListingTimer); clearTimeout(this.untaggedExpungeTimer); clearTimeout(this.resyncTimer); @@ -1338,7 +1427,7 @@ class Connection { textParts = []; } - let result = await mailbox.getText(message, textParts, options); + let result = await mailbox.getText(message, textParts, options, true); if (textType && textType !== '*') { result = { @@ -1366,7 +1455,7 @@ class Connection { let mailbox = this.mailboxes.get(normalizePath(message.path)); - return await mailbox.getMessage(message, options); + return await mailbox.getMessage(message, options, true); } async updateMessage(id, updates) { @@ -1385,7 +1474,7 @@ class Connection { let mailbox = this.mailboxes.get(normalizePath(message.path)); - return await mailbox.updateMessage(message, updates); + return await mailbox.updateMessage(message, updates, true); } async updateMessages(path, search, updates) { @@ -1398,7 +1487,7 @@ class Connection { let mailbox = this.mailboxes.get(normalizePath(path)); - return await mailbox.updateMessages(search, updates); + return await mailbox.updateMessages(search, updates, true); } async listMailboxes(options) { @@ -1424,7 +1513,7 @@ class Connection { } let mailbox = this.mailboxes.get(normalizePath(message.path)); - return await mailbox.moveMessage(message, target); + return await mailbox.moveMessage(message, target, true); } async moveMessages(source, search, target) { @@ -1438,7 +1527,7 @@ class Connection { let mailbox = this.mailboxes.get(normalizePath(source)); - let res = await mailbox.moveMessages(search, target); + let res = await mailbox.moveMessages(search, target, true); // force sync target mailbox try { @@ -1468,7 +1557,7 @@ class Connection { let mailbox = this.mailboxes.get(normalizePath(message.path)); - return await mailbox.deleteMessage(message, force); + return await mailbox.deleteMessage(message, force, true); } async deleteMessages(path, search, force) { @@ -1478,7 +1567,7 @@ class Connection { } let mailbox = this.mailboxes.get(normalizePath(path)); - let res = await mailbox.deleteMessages(search, force); + let res = await mailbox.deleteMessages(search, force, true); // force sync target mailbox try { @@ -1528,7 +1617,7 @@ class Connection { let mailbox = this.mailboxes.get(normalizePath(message.path)); - return mailbox.getAttachment(message, part, options); + return mailbox.getAttachment(message, part, options, true); } async getAttachmentContent(attachmentId, options) { @@ -1582,7 +1671,7 @@ class Connection { let mailbox = this.mailboxes.get(normalizePath(message.path)); - return mailbox.getAttachment(message, false, options); + return mailbox.getAttachment(message, false, options, true); } async listMessages(options) { @@ -1595,23 +1684,26 @@ class Connection { let mailbox = this.mailboxes.get(normalizePath(options.path)); - return mailbox.listMessages(options); + let listing = await mailbox.listMessages(options, true, true); + return listing; } async deleteMailbox(path) { this.checkIMAPConnection(); + const connectionClient = await this.getImapConnection(true); + let result = { path, deleted: false // set to true if mailbox is actually deleted }; try { - let lock = await this.imapClient.getMailboxLock(path); + let lock = await connectionClient.getMailboxLock(path); try { - await this.imapClient.mailboxClose(); + await connectionClient.mailboxClose(); try { - await this.imapClient.mailboxDelete(path); + await connectionClient.mailboxDelete(path); result.deleted = true; } catch (err) { // kind of ignore @@ -1673,8 +1765,11 @@ class Connection { async getQuota() { this.checkIMAPConnection(); + + const connectionClient = await this.getImapConnection(true); + try { - let result = await this.imapClient.getQuota(); + let result = await connectionClient.getQuota(); return (result && result.storage) || false; } catch (err) { if (err.serverResponseCode) { @@ -1695,8 +1790,11 @@ class Connection { async createMailbox(path) { this.checkIMAPConnection(); + + const connectionClient = await this.getImapConnection(true); + try { - let result = await this.imapClient.mailboxCreate(path); + let result = await connectionClient.mailboxCreate(path); if (result) { result.created = !!result.created; } @@ -1727,13 +1825,16 @@ class Connection { async renameMailbox(path, newPath) { this.checkIMAPConnection(); + + const connectionClient = await this.getImapConnection(true); + try { - let result = await this.imapClient.mailboxRename(path, newPath); + let result = await connectionClient.mailboxRename(path, newPath); if (result) { result.renamed = !!result.newPath; try { - await this.imapClient.mailboxSubscribe(result.newPath); + await connectionClient.mailboxSubscribe(result.newPath); } catch (err) { this.logger.debug({ msg: 'Failed to subscribe mailbox', path: result.newPath, err }); } @@ -2099,9 +2200,11 @@ class Connection { raw = Buffer.from(raw); } - await this.imapClient.append(sentMailbox.path, raw, ['\\Seen']); + const connectionClient = await this.getImapConnection(true); - if (this.imapClient.mailbox && !this.imapClient.idling) { + await connectionClient.append(sentMailbox.path, raw, ['\\Seen']); + + if (connectionClient === this.imapClient && this.imapClient.mailbox && !this.imapClient.idling) { // force back to IDLE this.imapClient.idle().catch(err => { this.logger.error({ msg: 'IDLE error', err }); @@ -3191,7 +3294,7 @@ class Connection { data.newline = '\r\n'; if (data.internalDate && !data.date) { - // update Date: header as well + // Update Date: header as well data.date = new Date(data.internalDate); } @@ -3199,35 +3302,39 @@ class Connection { // Upload message to selected folder try { - let lock = await this.imapClient.getMailboxLock(data.path); + const connectionClient = await this.getImapConnection(true); + let response = {}; - try { - let uploadResponse = await this.imapClient.append(data.path, raw, data.flags, data.internalDate); + let uploadResponse = await connectionClient.append(data.path, raw, data.flags, data.internalDate); - if (uploadResponse.uid) { - response.id = await this.packUid(uploadResponse.path, uploadResponse.uid); - } + if (connectionClient === this.imapClient && this.imapClient.mailbox && !this.imapClient.idling) { + // force back to IDLE + this.imapClient.idle().catch(err => { + this.logger.error({ msg: 'IDLE error', err }); + }); + } + + if (uploadResponse.uid) { + response.id = await this.packUid(uploadResponse.path, uploadResponse.uid); + } - response.path = uploadResponse.path; + response.path = uploadResponse.path; - if (uploadResponse.uid) { - response.uid = uploadResponse.uid; - } + if (uploadResponse.uid) { + response.uid = uploadResponse.uid; + } - if (validUidValidity(uploadResponse.uidValidity)) { - response.uidValidity = uploadResponse.uidValidity.toString(); - } + if (validUidValidity(uploadResponse.uidValidity)) { + response.uidValidity = uploadResponse.uidValidity.toString(); + } - if (uploadResponse.seq) { - response.seq = uploadResponse.seq; - } + if (uploadResponse.seq) { + response.seq = uploadResponse.seq; + } - if (messageId) { - response.messageId = messageId; - } - } finally { - lock.release(); + if (messageId) { + response.messageId = messageId; } if (data.reference && data.reference.message) { @@ -3331,6 +3438,7 @@ class Connection { mailbox, getImapConfig: async () => await this.getImapConfig(), logger: this.logger.child({ + channel: 'subconnection', subconnection: mailbox.path }) }); diff --git a/lib/mailbox.js b/lib/mailbox.js index ce2c7438..fbde4f61 100644 --- a/lib/mailbox.js +++ b/lib/mailbox.js @@ -59,12 +59,13 @@ class Mailbox { this.synced = false; } - getMailboxStatus() { - if (!this.connection.imapClient) { + getMailboxStatus(connectionClient) { + connectionClient = connectionClient || this.connection.imapClient; + if (!connectionClient) { throw new Error('IMAP connection not available'); } - let mailboxInfo = this.connection.imapClient.mailbox; + let mailboxInfo = connectionClient.mailbox; let status = { path: this.path @@ -321,14 +322,29 @@ class Mailbox { } } - async getMailboxLock() { - if (!this.connection.imapClient) { + async getMailboxLock(connectionClient) { + connectionClient = connectionClient || this.connection.imapClient; + + if (!connectionClient) { throw new Error('IMAP connection not available'); } - let lock = await this.connection.imapClient.getMailboxLock(this.path, {}); + + let lock = await connectionClient.getMailboxLock(this.path, {}); + + if (connectionClient === this.connection.imapClient) { + clearTimeout(this.connection.completedTimer); + } + return lock; } + onTaskCompleted(connectionClient) { + connectionClient = connectionClient || this.connection.imapClient; + if (connectionClient === this.connection.imapClient) { + this.connection.onTaskCompleted(); + } + } + logEvent(msg, event) { const logObj = Object.assign({ msg }, event); Object.keys(logObj).forEach(key => { @@ -414,6 +430,7 @@ class Mailbox { let mailboxStatus = this.getMailboxStatus(); let lock = await this.getMailboxLock(); + this.connection.syncing = true; try { if (!this.connection.imapClient) { throw new Error('IMAP connection not available'); @@ -548,6 +565,7 @@ class Mailbox { } } finally { lock.release(); + this.connection.syncing = false; } } @@ -1548,6 +1566,7 @@ class Mailbox { let opts = {}; let lock = await this.getMailboxLock(); + this.connection.syncing = true; try { let mailboxStatus = this.getMailboxStatus(); @@ -1746,6 +1765,7 @@ class Mailbox { await this.markUpdated(); } } finally { + this.connection.syncing = false; lock.release(); } } @@ -1871,8 +1891,11 @@ class Mailbox { // Call `clearTimeout(this.connection.completedTimer);` after locking mailbox // Call this.onTaskCompleted() after selected mailbox is processed and lock is released - async getText(message, textParts, options) { + async getText(message, textParts, options, allowSecondary) { options = options || {}; + + const connectionClient = await this.connection.getImapConnection(allowSecondary); + let result = {}; let maxBytes = options.maxBytes || Infinity; @@ -1882,13 +1905,12 @@ class Mailbox { let lock; if (!options.skipLock) { - lock = await this.getMailboxLock(); - clearTimeout(this.connection.completedTimer); + lock = await this.getMailboxLock(connectionClient); } try { for (let part of textParts) { - let { meta, content } = await this.connection.imapClient.download(message.uid, part, { + let { meta, content } = await connectionClient.download(message.uid, part, { uid: true, // make sure we request enough bytes so we would have complete utf-8 codepoints maxBytes: Math.min(reqMaxBytes, MAX_ALLOWED_DOWNLOAD_SIZE), @@ -1931,21 +1953,22 @@ class Mailbox { result.hasMore = hasMore; if (!options.skipLock) { - this.connection.onTaskCompleted(); + this.onTaskCompleted(connectionClient); } return result; } - async getAttachment(message, part, options) { + async getAttachment(message, part, options, allowSecondary) { options = options || {}; - let lock = await this.getMailboxLock(); - clearTimeout(this.connection.completedTimer); + const connectionClient = await this.connection.getImapConnection(allowSecondary); + + let lock = await this.getMailboxLock(connectionClient); let streaming = false; let released = false; try { - let { meta, content } = await this.connection.imapClient.download(message.uid, part, { + let { meta, content } = await connectionClient.download(message.uid, part, { uid: true, maxBytes: Math.min(options.maxBytes, MAX_ALLOWED_DOWNLOAD_SIZE), // future feature @@ -1987,7 +2010,7 @@ class Mailbox { if (!released) { released = true; lock.release(); - this.connection.onTaskCompleted(); + this.connection.onTaskCompleted(connectionClient); } }); @@ -1995,20 +2018,21 @@ class Mailbox { } finally { if (!streaming) { lock.release(); - this.connection.onTaskCompleted(); + this.connection.onTaskCompleted(connectionClient); } } } - async getMessage(message, options) { + async getMessage(message, options, allowSecondary) { options = options || {}; let messageInfo; + const connectionClient = await this.connection.getImapConnection(allowSecondary); + try { let lock; if (!options.skipLock) { - lock = await this.getMailboxLock(); - clearTimeout(this.connection.completedTimer); + lock = await this.getMailboxLock(connectionClient); } try { @@ -2025,11 +2049,11 @@ class Mailbox { labels: true }; - let messageData = await this.connection.imapClient.fetchOne(message.uid, fields, { uid: true }); + let messageData = await connectionClient.fetchOne(message.uid, fields, { uid: true }); if (options.markAsSeen && (!messageData.flags || !messageData.flags.has('\\Seen'))) { // try { - let res = await this.connection.imapClient.messageFlagsAdd(message.uid, ['\\Seen'], { uid: true }); + let res = await connectionClient.messageFlagsAdd(message.uid, ['\\Seen'], { uid: true }); if (res) { messageData.flags.add('\\Seen'); } @@ -2044,7 +2068,7 @@ class Mailbox { return false; } - messageInfo = await this.getMessageInfo(messageData, true); + messageInfo = await this.getMessageInfo(messageData, true, allowSecondary); } finally { if (lock) { lock.release(); @@ -2072,7 +2096,7 @@ class Mailbox { } if (textParts && textParts.length) { - let textContent = await this.getText(message, textParts, options); + let textContent = await this.getText(message, textParts, options, allowSecondary); if (options.textType && options.textType !== '*') { textContent = { [options.textType]: textContent[options.textType] || '', @@ -2109,7 +2133,7 @@ class Mailbox { if (partList.length) { try { - let contentParts = await this.connection.imapClient.downloadMany(messageInfo.uid, partList, { + let contentParts = await connectionClient.downloadMany(messageInfo.uid, partList, { uid: true }); @@ -2161,15 +2185,17 @@ class Mailbox { return messageInfo; } finally { if (!options.skipLock) { - this.connection.onTaskCompleted(); + this.connection.onTaskCompleted(connectionClient); } } } - async updateMessage(message, updates) { + async updateMessage(message, updates, allowSecondary) { updates = updates || {}; - let lock = await this.getMailboxLock(); - clearTimeout(this.connection.completedTimer); + + const connectionClient = await this.connection.getImapConnection(allowSecondary); + + let lock = await this.getMailboxLock(connectionClient); try { let result = {}; @@ -2177,13 +2203,13 @@ class Mailbox { if (updates.flags) { if (updates.flags.set) { // If set exists the ignore add/delete calls - let value = await this.connection.imapClient.messageFlagsSet(message.uid, updates.flags.set, { uid: true }); + let value = await connectionClient.messageFlagsSet(message.uid, updates.flags.set, { uid: true }); result.flags = { set: value }; } else { if (updates.flags.add && updates.flags.add.length) { - let value = await this.connection.imapClient.messageFlagsAdd(message.uid, updates.flags.add, { uid: true }); + let value = await connectionClient.messageFlagsAdd(message.uid, updates.flags.add, { uid: true }); if (!result.flags) { result.flags = {}; } @@ -2191,7 +2217,7 @@ class Mailbox { } if (updates.flags.delete && updates.flags.delete.length) { - let value = await this.connection.imapClient.messageFlagsRemove(message.uid, updates.flags.delete, { uid: true }); + let value = await connectionClient.messageFlagsRemove(message.uid, updates.flags.delete, { uid: true }); if (!result.flags) { result.flags = {}; } @@ -2203,13 +2229,13 @@ class Mailbox { if (updates.labels && this.isGmail) { if (updates.labels.set) { // If set exists the ignore add/delete calls - let value = await this.connection.imapClient.messageFlagsSet(message.uid, updates.labels.set, { uid: true, useLabels: true }); + let value = await connectionClient.messageFlagsSet(message.uid, updates.labels.set, { uid: true, useLabels: true }); result.labels = { set: value }; } else { if (updates.labels.add && updates.labels.add.length) { - let value = await this.connection.imapClient.messageFlagsAdd(message.uid, updates.labels.add, { uid: true, useLabels: true }); + let value = await connectionClient.messageFlagsAdd(message.uid, updates.labels.add, { uid: true, useLabels: true }); if (!result.labels) { result.labels = {}; } @@ -2217,7 +2243,7 @@ class Mailbox { } if (updates.labels.delete && updates.labels.delete.length) { - let value = await this.connection.imapClient.messageFlagsRemove(message.uid, updates.labels.delete, { uid: true, useLabels: true }); + let value = await connectionClient.messageFlagsRemove(message.uid, updates.labels.delete, { uid: true, useLabels: true }); if (!result.labels) { result.labels = {}; } @@ -2229,14 +2255,16 @@ class Mailbox { return result; } finally { lock.release(); - this.connection.onTaskCompleted(); + this.connection.onTaskCompleted(connectionClient); } } - async updateMessages(search, updates) { + async updateMessages(search, updates, allowSecondary) { updates = updates || {}; - let lock = await this.getMailboxLock(); - clearTimeout(this.connection.completedTimer); + + const connectionClient = await this.connection.getImapConnection(allowSecondary); + + let lock = await this.getMailboxLock(connectionClient); try { let result = {}; @@ -2244,13 +2272,13 @@ class Mailbox { if (updates.flags) { if (updates.flags.set) { // If set exists the ignore add/delete calls - let value = await this.connection.imapClient.messageFlagsSet(search, updates.flags.set, { uid: true }); + let value = await connectionClient.messageFlagsSet(search, updates.flags.set, { uid: true }); result.flags = { set: value }; } else { if (updates.flags.add && updates.flags.add.length) { - let value = await this.connection.imapClient.messageFlagsAdd(search, updates.flags.add, { uid: true }); + let value = await connectionClient.messageFlagsAdd(search, updates.flags.add, { uid: true }); if (!result.flags) { result.flags = {}; } @@ -2258,7 +2286,7 @@ class Mailbox { } if (updates.flags.delete && updates.flags.delete.length) { - let value = await this.connection.imapClient.messageFlagsRemove(search, updates.flags.delete, { uid: true }); + let value = await connectionClient.messageFlagsRemove(search, updates.flags.delete, { uid: true }); if (!result.flags) { result.flags = {}; } @@ -2270,13 +2298,13 @@ class Mailbox { if (updates.labels && this.isGmail) { if (updates.labels.set) { // If set exists the ignore add/delete calls - let value = await this.connection.imapClient.messageFlagsSet(search, updates.labels.set, { uid: true, useLabels: true }); + let value = await connectionClient.messageFlagsSet(search, updates.labels.set, { uid: true, useLabels: true }); result.labels = { set: value }; } else { if (updates.labels.add && updates.labels.add.length) { - let value = await this.connection.imapClient.messageFlagsAdd(search, updates.labels.add, { uid: true, useLabels: true }); + let value = await connectionClient.messageFlagsAdd(search, updates.labels.add, { uid: true, useLabels: true }); if (!result.labels) { result.labels = {}; } @@ -2284,7 +2312,7 @@ class Mailbox { } if (updates.labels.delete && updates.labels.delete.length) { - let value = await this.connection.imapClient.messageFlagsRemove(search, updates.labels.delete, { uid: true, useLabels: true }); + let value = await connectionClient.messageFlagsRemove(search, updates.labels.delete, { uid: true, useLabels: true }); if (!result.labels) { result.labels = {}; } @@ -2296,21 +2324,23 @@ class Mailbox { return result; } finally { lock.release(); - this.connection.onTaskCompleted(); + this.connection.onTaskCompleted(connectionClient); } } - async moveMessage(message, target) { + async moveMessage(message, target, allowSecondary) { target = target || {}; - let lock = await this.getMailboxLock(); - clearTimeout(this.connection.completedTimer); + + const connectionClient = await this.connection.getImapConnection(allowSecondary); + + let lock = await this.getMailboxLock(connectionClient); try { let result = {}; if (target.path) { // If set exists the ignore add/delete calls - let value = await this.connection.imapClient.messageMove(message.uid, target.path, { uid: true }); + let value = await connectionClient.messageMove(message.uid, target.path, { uid: true }); result.path = target.path; if (value && value.uidMap && value.uidMap.has(message.uid)) { let uid = value.uidMap.get(message.uid); @@ -2323,21 +2353,23 @@ class Mailbox { return result; } finally { lock.release(); - this.connection.onTaskCompleted(); + this.connection.onTaskCompleted(connectionClient); } } - async moveMessages(search, target) { + async moveMessages(search, target, allowSecondary) { target = target || {}; - let lock = await this.getMailboxLock(); - clearTimeout(this.connection.completedTimer); + + const connectionClient = await this.connection.getImapConnection(allowSecondary); + + let lock = await this.getMailboxLock(connectionClient); try { let result = {}; if (target.path) { // If set exists the ignore add/delete calls - let value = await this.connection.imapClient.messageMove(search, target.path, { uid: true }); + let value = await connectionClient.messageMove(search, target.path, { uid: true }); result.path = target.path; if (value && value.uidMap && value.uidMap.size) { @@ -2352,31 +2384,32 @@ class Mailbox { return result; } finally { lock.release(); - this.connection.onTaskCompleted(); + this.connection.onTaskCompleted(connectionClient); } } - async deleteMessage(message, force) { - let lock = await this.getMailboxLock(); - clearTimeout(this.connection.completedTimer); + async deleteMessage(message, force, allowSecondary) { + const connectionClient = await this.connection.getImapConnection(allowSecondary); + + let lock = await this.getMailboxLock(connectionClient); try { let result = {}; if (['\\Trash', '\\Junk'].includes(this.listingEntry.specialUse) || force) { // delete - result.deleted = await this.connection.imapClient.messageDelete(message.uid, { uid: true }); + result.deleted = await connectionClient.messageDelete(message.uid, { uid: true }); } else { // move to trash // find Trash folder path let trashMailbox = await this.connection.getSpecialUseMailbox('\\Trash'); if (!trashMailbox || normalizePath(trashMailbox.path) === normalizePath(this.path)) { // no Trash found or already in trash - result.deleted = await this.connection.imapClient.messageDelete(message.uid, { uid: true }); + result.deleted = await connectionClient.messageDelete(message.uid, { uid: true }); } else { result.deleted = false; // we have a destionation, so can move message to there - let moved = await await this.connection.imapClient.messageMove(message.uid, trashMailbox.path, { uid: true }); + let moved = await await connectionClient.messageMove(message.uid, trashMailbox.path, { uid: true }); if (moved) { result.moved = { destination: moved.destination @@ -2391,31 +2424,32 @@ class Mailbox { return result; } finally { lock.release(); - this.connection.onTaskCompleted(); + this.connection.onTaskCompleted(connectionClient); } } - async deleteMessages(search, force) { - let lock = await this.getMailboxLock(); - clearTimeout(this.connection.completedTimer); + async deleteMessages(search, force, allowSecondary) { + const connectionClient = await this.connection.getImapConnection(allowSecondary); + + let lock = await this.getMailboxLock(connectionClient); try { let result = {}; if (['\\Trash', '\\Junk'].includes(this.listingEntry.specialUse) || force) { // delete - result.deleted = await this.connection.imapClient.messageDelete(search, { uid: true }); + result.deleted = await connectionClient.messageDelete(search, { uid: true }); } else { // move to trash // find Trash folder path let trashMailbox = await this.connection.getSpecialUseMailbox('\\Trash'); if (!trashMailbox || normalizePath(trashMailbox.path) === normalizePath(this.path)) { // no Trash found or already in trash - result.deleted = await this.connection.imapClient.messageDelete(search, { uid: true }); + result.deleted = await connectionClient.messageDelete(search, { uid: true }); } else { result.deleted = false; // we have a destionation, so can move message to there - let moved = await await this.connection.imapClient.messageMove(search, trashMailbox.path, { uid: true }); + let moved = await await connectionClient.messageMove(search, trashMailbox.path, { uid: true }); if (moved) { result.moved = { destination: moved.destination @@ -2434,27 +2468,28 @@ class Mailbox { return result; } finally { lock.release(); - this.connection.onTaskCompleted(); + this.connection.onTaskCompleted(connectionClient); } } - async listMessages(options) { + async listMessages(options, allowSecondary) { options = options || {}; let page = Number(options.page) || 0; let pageSize = Math.abs(Number(options.pageSize) || 20); - let lock = await this.getMailboxLock(); - clearTimeout(this.connection.completedTimer); + const connectionClient = await this.connection.getImapConnection(allowSecondary); + + let lock = await this.getMailboxLock(connectionClient); try { - let mailboxStatus = this.getMailboxStatus(); + let mailboxStatus = this.getMailboxStatus(connectionClient); let messageCount = mailboxStatus.messages; let uidList; let opts = {}; if (options.search) { - uidList = await this.connection.imapClient.search(options.search, { uid: true }); + uidList = await connectionClient.search(options.search, { uid: true }); uidList = !uidList ? [] : uidList.sort((a, b) => b - a); // newer first messageCount = uidList.length; } @@ -2508,7 +2543,7 @@ class Mailbox { labels: true }; - for await (let messageData of this.connection.imapClient.fetch(range, fields, opts)) { + for await (let messageData of connectionClient.fetch(range, fields, opts)) { if (!messageData || !messageData.uid) { //TODO: support partial responses this.logger.debug({ msg: 'Partial FETCH response', code: 'partial_fetch', query: { range, fields, opts } }); @@ -2537,7 +2572,7 @@ class Mailbox { }; } finally { lock.release(); - this.connection.onTaskCompleted(); + this.connection.onTaskCompleted(connectionClient); } } diff --git a/lib/subconnection.js b/lib/subconnection.js index e54182e9..65a49d96 100644 --- a/lib/subconnection.js +++ b/lib/subconnection.js @@ -105,10 +105,14 @@ class Subconnection extends EventEmitter { let imapConfig = await this.getImapConfig(null, this); - imapConfig.expungeHandler = async payload => await this.expungeHandler(payload); - imapConfig.logger = this.logger; - - this.imapClient = new ImapFlow(imapConfig); + this.imapClient = new ImapFlow( + Object.assign({}, imapConfig, { + logger: this.logger, + expungeHandler: async payload => await this.expungeHandler(payload) + }) + ); + + this.imapClient.subConnection = true; this.imapClient.on('error', err => { this.logger.error({ msg: 'IMAP connection error', account: this.account, err });