From dec8b2b2f789349f4e8b244302e47dc3410e793b Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Tue, 17 Feb 2026 15:26:45 +0200 Subject: [PATCH 01/32] add initial support for creating cluster redis connection --- lib/db.js | 75 +++++++++++++++++++++++++++++++++++++++------------- lib/tools.js | 44 +++++++++++++++++++++++++++--- 2 files changed, 98 insertions(+), 21 deletions(-) diff --git a/lib/db.js b/lib/db.js index b19bda9e..7fd6fa5b 100644 --- a/lib/db.js +++ b/lib/db.js @@ -42,33 +42,72 @@ let getDBConnection = (main, config, callback) => { ); }; -module.exports.connect = callback => { - const REDIS_CONF = Object.assign( - { - // some defaults - maxRetriesPerRequest: null, - showFriendlyErrorStack: true, - retryStrategy(times) { - const delay = !times ? 1000 : Math.min(2 ** times * 500, 15 * 1000); - log.info('Redis', 'Connection retry times=%s delay=%s', times, delay); - return delay; - }, - connectionName: `${packageData.name}@${packageData.version}[${process.pid}]` +let getRedisConf = defaultConfig => { + const redisDefaults = { + // some defaults + maxRetriesPerRequest: null, + showFriendlyErrorStack: true, + retryStrategy(times) { + const delay = !times ? 1000 : Math.min(2 ** times * 500, 15 * 1000); + log.info('Redis', 'Connection retry times=%s delay=%s', times, delay); + return delay; }, - typeof config.dbs.redis === 'string' ? redisUrl(config.dbs.redis) : config.dbs.redis || {} - ); + connectionName: `${packageData.name}@${packageData.version}[${process.pid}]` + }; + + const redisConfig = typeof defaultConfig === 'string' ? redisUrl(defaultConfig) : defaultConfig || {}; + + if (redisConfig && redisConfig.cluster) { + let clusterConfig = Object.assign({}, redisConfig); + let nodeDefaults = {}; + + for (let key of ['password', 'username', 'tls']) { + if (typeof clusterConfig[key] !== 'undefined') { + nodeDefaults[key] = clusterConfig[key]; + } + } + + let nodes = [] + .concat(clusterConfig.nodes || []) + .map(node => Object.assign({}, nodeDefaults, node || {})) + .filter(node => node && node.host); + + delete clusterConfig.cluster; + delete clusterConfig.nodes; + delete clusterConfig.password; + delete clusterConfig.db; + delete clusterConfig.username; + delete clusterConfig.tls; + + let redisOptions = Object.assign({}, redisDefaults, nodeDefaults, clusterConfig.redisOptions || {}); + + return { + cluster: true, + connectionName: redisOptions.connectionName, + nodes, + options: Object.assign({}, clusterConfig, { redisOptions }) + }; + } + + return Object.assign({}, redisDefaults, redisConfig); +}; + +module.exports.connect = callback => { + const REDIS_CONF = getRedisConf(config.dbs.redis); module.exports.redisConfig = REDIS_CONF; + console.log(REDIS_CONF); + module.exports.redis = REDIS_CONF.cluster ? new Redis.Cluster(REDIS_CONF.nodes, REDIS_CONF.options) : new Redis(REDIS_CONF); module.exports.queueConf = { - connection: Object.assign({ connectionName: `${REDIS_CONF.connectionName}[notify]` }, REDIS_CONF), + connection: REDIS_CONF.cluster ? module.exports.redis : Object.assign({ connectionName: `${REDIS_CONF.connectionName}[notify]` }, REDIS_CONF), prefix: `wd:bull` }; - module.exports.redis = new Redis(REDIS_CONF); errors.registerRedisErrorLogger(module.exports.redis, { role: 'primary', connectionName: REDIS_CONF.connectionName, - mode: Array.isArray(REDIS_CONF.sentinels) ? 'sentinel' : 'direct', - sentinelCount: Array.isArray(REDIS_CONF.sentinels) ? REDIS_CONF.sentinels.length : undefined + mode: REDIS_CONF.cluster ? 'cluster' : Array.isArray(REDIS_CONF.sentinels) ? 'sentinel' : 'direct', + sentinelCount: Array.isArray(REDIS_CONF.sentinels) ? REDIS_CONF.sentinels.length : undefined, + clusterNodeCount: REDIS_CONF.cluster && Array.isArray(REDIS_CONF.nodes) ? REDIS_CONF.nodes.length : undefined }); getDBConnection(false, config.dbs.mongo, (err, db) => { diff --git a/lib/tools.js b/lib/tools.js index 673be35c..cd5d5bee 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -135,7 +135,7 @@ function getWildcardAddresses(username, domain) { // returns a redis config object with a retry strategy function redisConfig(defaultConfig) { - return { + const redisDefaults = { // some defaults showFriendlyErrorStack: true, retryStrategy(times) { @@ -143,8 +143,46 @@ function redisConfig(defaultConfig) { log.info('Redis', 'Connection retry times=%s delay=%s', times, delay); return delay; }, - connectionName: `${packageData.name}@${packageData.version}[${process.pid}]`, - ...(typeof defaultConfig === 'string' ? redisUrl(defaultConfig) : defaultConfig || {}) // unwrap other config + connectionName: `${packageData.name}@${packageData.version}[${process.pid}]` + }; + + let parsedConfig = typeof defaultConfig === 'string' ? redisUrl(defaultConfig) : defaultConfig || {}; + + if (parsedConfig && parsedConfig.cluster) { + let clusterConfig = Object.assign({}, parsedConfig); + let nodeDefaults = {}; + + for (let key of ['password', 'username', 'tls']) { + if (typeof clusterConfig[key] !== 'undefined') { + nodeDefaults[key] = clusterConfig[key]; + } + } + + let nodes = [] + .concat(clusterConfig.nodes || []) + .map(node => Object.assign({}, nodeDefaults, node || {})) + .filter(node => node && node.host); + + delete clusterConfig.cluster; + delete clusterConfig.nodes; + delete clusterConfig.password; + delete clusterConfig.db; + delete clusterConfig.username; + delete clusterConfig.tls; + + let redisOptions = Object.assign({}, redisDefaults, nodeDefaults, clusterConfig.redisOptions || {}); + + return { + cluster: true, + connectionName: redisOptions.connectionName, + nodes, + options: Object.assign({}, clusterConfig, { redisOptions }) + }; + } + + return { + ...redisDefaults, + ...parsedConfig // unwrap other config }; } From cf231175bc3fd8c5a1eb820994dd85e7237f7b8b Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Wed, 18 Feb 2026 15:23:51 +0200 Subject: [PATCH 02/32] for locks use first node as main one, create separate connection for it, add hash tags for prefixes and namespaces --- api.js | 4 ++-- lib/acme/certs.js | 5 +++-- lib/attachments/gridstore-storage.js | 19 +++++++++++-------- lib/db.js | 6 ++++-- lib/events.js | 2 +- tasks.js | 4 ++-- 6 files changed, 23 insertions(+), 17 deletions(-) diff --git a/api.js b/api.js index 46291b30..86de230e 100644 --- a/api.js +++ b/api.js @@ -560,8 +560,8 @@ module.exports = done => { server.loggelf = (message, requiredKeys = []) => loggelf(message, requiredKeys); server.lock = new Lock({ - redis: db.redis, - namespace: 'mail' + redis: db.redis.isCluster ? db.redis.redisMaster : db.redis, + namespace: db.redis.isCluster ? '{mail}' : 'mail' }); acmeRoutes(db, server, { disableRedirect: true }); diff --git a/lib/acme/certs.js b/lib/acme/certs.js index af56a0c5..032dee34 100644 --- a/lib/acme/certs.js +++ b/lib/acme/certs.js @@ -14,6 +14,7 @@ const { Resolver } = require('dns').promises; const resolver = new Resolver(); const Joi = require('joi'); const { SettingsHandler } = require('../settings-handler'); +const { default: Redis } = require('ioredis'); if (config.resolver && config.resolver.ns && config.resolver.ns.length) { resolver.setServers([].concat(config.resolver.ns || [])); @@ -188,8 +189,8 @@ const acquireCert = async (domain, acmeOptions, certificateData, certHandler) => // Use locking to avoid race conditions, first try gets the lock, others wait until first is finished if (!getLock) { let lock = new Lock({ - redis: certHandler.redis, - namespace: 'acme' + redis: certHandler.redis.isCluster ? certHandler.redis.redisMaster : certHandler.redis, + namespace: certHandler.redis.isCluster ? '{acme}' : 'acme' }); getLock = (...args) => lock.waitAcquireLock(...args); releaseLock = (...args) => lock.releaseLock(...args); diff --git a/lib/attachments/gridstore-storage.js b/lib/attachments/gridstore-storage.js index 0b089258..8922f82f 100644 --- a/lib/attachments/gridstore-storage.js +++ b/lib/attachments/gridstore-storage.js @@ -21,8 +21,8 @@ class GridstoreStorage { this.decodeBase64 = (options.options && options.options.decodeBase64) || false; this.lock = new RedFour({ - redis: options.redis, - namespace: 'wildduck' + redis: options.redis.isCluster ? options.redis.redisMaster : options.redis, + namespace: options.redis.isCluster ? '{wildduck}' : 'wildduck' }); this.gridfs = options.gridfs; @@ -305,12 +305,15 @@ class GridstoreStorage { } // partial chunks for a probably deleted message detected, try to clean up - setTimeout(() => { - if (returned) { - return; - } - this.cleanupGarbage(id, tryStore); - }, 100 + 200 * Math.random()); + setTimeout( + () => { + if (returned) { + return; + } + this.cleanupGarbage(id, tryStore); + }, + 100 + 200 * Math.random() + ); } ); } else { diff --git a/lib/db.js b/lib/db.js index 7fd6fa5b..8cc5dda6 100644 --- a/lib/db.js +++ b/lib/db.js @@ -96,11 +96,13 @@ module.exports.connect = callback => { const REDIS_CONF = getRedisConf(config.dbs.redis); module.exports.redisConfig = REDIS_CONF; - console.log(REDIS_CONF); module.exports.redis = REDIS_CONF.cluster ? new Redis.Cluster(REDIS_CONF.nodes, REDIS_CONF.options) : new Redis(REDIS_CONF); + if (REDIS_CONF.cluster) { + module.exports.redis.redisMaster = new Redis(module.exports.redis.nodes('master')[0]); // used for locks and so on, that do not work with or support Redis cluster mode + } module.exports.queueConf = { connection: REDIS_CONF.cluster ? module.exports.redis : Object.assign({ connectionName: `${REDIS_CONF.connectionName}[notify]` }, REDIS_CONF), - prefix: `wd:bull` + prefix: REDIS_CONF.cluster ? `{wd:bull}` : `wd:bull` }; errors.registerRedisErrorLogger(module.exports.redis, { role: 'primary', diff --git a/lib/events.js b/lib/events.js index 0b881e93..52fa309b 100644 --- a/lib/events.js +++ b/lib/events.js @@ -55,7 +55,7 @@ module.exports = { if (!webhooksQueue) { webhooksQueue = new Queue('webhooks', { connection: redisClient, - prefix: `wd:bull` + prefix: redisClient.isCluster ? `{wd:bull}` : `wd:bull` }); } diff --git a/tasks.js b/tasks.js index b64d6126..be2320cb 100644 --- a/tasks.js +++ b/tasks.js @@ -87,8 +87,8 @@ module.exports.start = callback => { }; gcLock = new RedFour({ - redis: db.redis, - namespace: 'wildduck' + redis: db.redis.isCluster ? db.redis.redisMaster : db.redis, + namespace: db.redis.isCluster ? '{wildduck}' : 'wildduck' }); messageHandler = new MessageHandler({ From 581cb50c01b400751b7637b8e963535425b4535e Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 19 Feb 2026 09:39:28 +0200 Subject: [PATCH 03/32] for locks using single master node remove hash tags on namespace --- imap.js | 2 +- lib/acme/certs.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/imap.js b/imap.js index b26d6f36..eab1f137 100644 --- a/imap.js +++ b/imap.js @@ -154,7 +154,7 @@ let createInterface = (ifaceOptions, callback) => { server.notifier = notifier; server.lock = new Lock({ - redis: db.redis, + redis: db.redis.isCluster ? db.redis.redisMaster : db.redis, namespace: 'mail' }); diff --git a/lib/acme/certs.js b/lib/acme/certs.js index 032dee34..ca95d2c2 100644 --- a/lib/acme/certs.js +++ b/lib/acme/certs.js @@ -14,7 +14,6 @@ const { Resolver } = require('dns').promises; const resolver = new Resolver(); const Joi = require('joi'); const { SettingsHandler } = require('../settings-handler'); -const { default: Redis } = require('ioredis'); if (config.resolver && config.resolver.ns && config.resolver.ns.length) { resolver.setServers([].concat(config.resolver.ns || [])); @@ -190,7 +189,7 @@ const acquireCert = async (domain, acmeOptions, certificateData, certHandler) => if (!getLock) { let lock = new Lock({ redis: certHandler.redis.isCluster ? certHandler.redis.redisMaster : certHandler.redis, - namespace: certHandler.redis.isCluster ? '{acme}' : 'acme' + namespace: 'acme' }); getLock = (...args) => lock.waitAcquireLock(...args); releaseLock = (...args) => lock.releaseLock(...args); From cefb793e1e2b6d349f4d28a4a838020fb94b39e1 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 19 Feb 2026 09:39:45 +0200 Subject: [PATCH 04/32] for locks using single master node remove hash tags on namespace | 2 --- api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.js b/api.js index 86de230e..0d4d648b 100644 --- a/api.js +++ b/api.js @@ -561,7 +561,7 @@ module.exports = done => { server.lock = new Lock({ redis: db.redis.isCluster ? db.redis.redisMaster : db.redis, - namespace: db.redis.isCluster ? '{mail}' : 'mail' + namespace: 'mail' }); acmeRoutes(db, server, { disableRedirect: true }); From c05c5216dca31ab9aa7b7fd0fcbd2d94c6e4ef58 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 19 Feb 2026 10:08:06 +0200 Subject: [PATCH 05/32] for locks using single master node remove hash tags on namespace | 3 --- tasks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.js b/tasks.js index be2320cb..24687823 100644 --- a/tasks.js +++ b/tasks.js @@ -88,7 +88,7 @@ module.exports.start = callback => { gcLock = new RedFour({ redis: db.redis.isCluster ? db.redis.redisMaster : db.redis, - namespace: db.redis.isCluster ? '{wildduck}' : 'wildduck' + namespace: 'wildduck' }); messageHandler = new MessageHandler({ From 951e4c9b97ee90df36aacb730481ee4569e946aa Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 19 Feb 2026 10:10:29 +0200 Subject: [PATCH 06/32] for locks using single master node remove hash tags on namespace | 4 | gridstore lock --- lib/attachments/gridstore-storage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/attachments/gridstore-storage.js b/lib/attachments/gridstore-storage.js index 8922f82f..f2ae1873 100644 --- a/lib/attachments/gridstore-storage.js +++ b/lib/attachments/gridstore-storage.js @@ -22,7 +22,7 @@ class GridstoreStorage { this.lock = new RedFour({ redis: options.redis.isCluster ? options.redis.redisMaster : options.redis, - namespace: options.redis.isCluster ? '{wildduck}' : 'wildduck' + namespace: 'wildduck' }); this.gridfs = options.gridfs; From d9ec3473f5d6591fe69467dfa65e3d7e420e7408 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 19 Feb 2026 11:24:32 +0200 Subject: [PATCH 07/32] ZMS-6: add backwards compatible initial support for redis cluster | for ioredfour locks add hash tags when in cluster mode --- api.js | 2 +- bin/access-tokens | 14 ++++---------- imap.js | 2 +- indexer.js | 4 ++-- lib/acme/certs.js | 2 +- lib/api/messages.js | 3 ++- lib/api/users.js | 2 +- lib/attachments/gridstore-storage.js | 2 +- lib/handlers/on-append.js | 6 ++++-- lib/handlers/on-fetch.js | 6 ++++-- lib/imap-notifier.js | 7 ++++--- lib/tasks/clear-folder.js | 4 +++- lib/tools.js | 14 +++++++++++++- lib/user-handler.js | 11 +++++++---- pop3.js | 10 ++++++---- tasks.js | 2 +- 16 files changed, 55 insertions(+), 36 deletions(-) diff --git a/api.js b/api.js index 0d4d648b..86de230e 100644 --- a/api.js +++ b/api.js @@ -561,7 +561,7 @@ module.exports = done => { server.lock = new Lock({ redis: db.redis.isCluster ? db.redis.redisMaster : db.redis, - namespace: 'mail' + namespace: db.redis.isCluster ? '{mail}' : 'mail' }); acmeRoutes(db, server, { disableRedirect: true }); diff --git a/bin/access-tokens b/bin/access-tokens index 179a8492..66a76681 100755 --- a/bin/access-tokens +++ b/bin/access-tokens @@ -40,10 +40,7 @@ let argv = yargs await dbconnect(); let accessToken = crypto.randomBytes(20).toString('hex'); - let tokenHash = crypto - .createHash('sha256') - .update(accessToken) - .digest('hex'); + let tokenHash = crypto.createHash('sha256').update(accessToken).digest('hex'); let key = 'tn:token:' + tokenHash; let tokenData = { @@ -64,7 +61,7 @@ let argv = yargs }; await db.redis - .multi() + .pipeline() .hmset(key, tokenData) .sadd('tn:user:' + argv.user, tokenHash) .exec(); @@ -92,10 +89,7 @@ let argv = yargs let accessToken = argv.token; - let tokenHash = crypto - .createHash('sha256') - .update(accessToken) - .digest('hex'); + let tokenHash = crypto.createHash('sha256').update(accessToken).digest('hex'); let key = 'tn:token:' + tokenHash; @@ -132,7 +126,7 @@ let argv = yargs process.exit(); } - let query = await db.redis.multi().del('tn:user:' + user); + let query = await db.redis.pipeline().del('tn:user:' + user); tokens.forEach(tokenHash => { query = query.del('tn:token:' + tokenHash); diff --git a/imap.js b/imap.js index eab1f137..4b5e5f13 100644 --- a/imap.js +++ b/imap.js @@ -155,7 +155,7 @@ let createInterface = (ifaceOptions, callback) => { server.lock = new Lock({ redis: db.redis.isCluster ? db.redis.redisMaster : db.redis, - namespace: 'mail' + namespace: db.redis.isCluster ? '{mail}' : 'mail' }); // setup command handlers for the server instance diff --git a/indexer.js b/indexer.js index 5fc4a579..26fa5c85 100644 --- a/indexer.js +++ b/indexer.js @@ -290,8 +290,8 @@ function indexingJob(esclient) { case 'new': { // check tombstone for race conditions (might be already deleted) - let [[err1, isDeleted1], [err2, isDeleted2]] = await db.redis - .multi() + const [[err1, isDeleted1], [err2, isDeleted2]] = await db.redis + .pipeline() .sismember(tombstoneTdy, data.message) .sismember(tombstoneYdy, data.message) .exec(); diff --git a/lib/acme/certs.js b/lib/acme/certs.js index ca95d2c2..c54d50d6 100644 --- a/lib/acme/certs.js +++ b/lib/acme/certs.js @@ -189,7 +189,7 @@ const acquireCert = async (domain, acmeOptions, certificateData, certHandler) => if (!getLock) { let lock = new Lock({ redis: certHandler.redis.isCluster ? certHandler.redis.redisMaster : certHandler.redis, - namespace: 'acme' + namespace: certHandler.redis.isCluster ? '{acme}' : 'acme' }); getLock = (...args) => lock.waitAcquireLock(...args); releaseLock = (...args) => lock.releaseLock(...args); diff --git a/lib/api/messages.js b/lib/api/messages.js index 2b5d41dc..af30a0de 100644 --- a/lib/api/messages.js +++ b/lib/api/messages.js @@ -2199,8 +2199,9 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti } try { + const mailboxKey = tools.redisClusterValue(mailbox, db.redis); // clear counters - await db.redis.multi().del(`total:${mailbox}`).del(`unseen:${mailbox}`).exec(); + await db.redis.multi().del(`total:${mailboxKey}`).del(`unseen:${mailboxKey}`).exec(); } catch (err) { // ignore } diff --git a/lib/api/users.js b/lib/api/users.js index 86727823..ff3342b1 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -969,7 +969,7 @@ module.exports = (db, server, userHandler, settingsHandler) => { let response; try { response = await db.redis - .multi() + .pipeline() // sending counters are stored in Redis // sent messages diff --git a/lib/attachments/gridstore-storage.js b/lib/attachments/gridstore-storage.js index f2ae1873..8922f82f 100644 --- a/lib/attachments/gridstore-storage.js +++ b/lib/attachments/gridstore-storage.js @@ -22,7 +22,7 @@ class GridstoreStorage { this.lock = new RedFour({ redis: options.redis.isCluster ? options.redis.redisMaster : options.redis, - namespace: 'wildduck' + namespace: options.redis.isCluster ? '{wildduck}' : 'wildduck' }); this.gridfs = options.gridfs; diff --git a/lib/handlers/on-append.js b/lib/handlers/on-append.js index f93a24be..862c60eb 100644 --- a/lib/handlers/on-append.js +++ b/lib/handlers/on-append.js @@ -45,7 +45,9 @@ module.exports = (server, messageHandler, userCache) => (path, flags, date, raw, return callback(err); } - messageHandler.counters.ttlcounter('iup:' + session.user.id, 0, limit, false, (err, res) => { + const counterKey = 'iup:' + tools.redisClusterValue(session.user.id, db.redis); + + messageHandler.counters.ttlcounter(counterKey, 0, limit, false, (err, res) => { if (err) { return callback(err); } @@ -58,7 +60,7 @@ module.exports = (server, messageHandler, userCache) => (path, flags, date, raw, return callback(err); } - messageHandler.counters.ttlcounter('iup:' + session.user.id, raw.length, limit, false, () => { + messageHandler.counters.ttlcounter(counterKey, raw.length, limit, false, () => { flags = Array.isArray(flags) ? flags : [].concat(flags || []); messageHandler.encryptMessage( diff --git a/lib/handlers/on-fetch.js b/lib/handlers/on-fetch.js index 84c8754c..6ddb1f36 100644 --- a/lib/handlers/on-fetch.js +++ b/lib/handlers/on-fetch.js @@ -45,7 +45,9 @@ module.exports = (server, messageHandler, userCache) => (mailbox, options, sessi return callback(err); } - messageHandler.counters.ttlcounter('idw:' + session.user.id, 0, limit, false, (err, res) => { + const counterKey = 'idw:' + tools.redisClusterValue(session.user.id, db.redis); + + messageHandler.counters.ttlcounter(counterKey, 0, limit, false, (err, res) => { if (err) { return callback(err); } @@ -364,7 +366,7 @@ module.exports = (server, messageHandler, userCache) => (mailbox, options, sessi }); let limiter = new LimitedFetch({ - key: 'idw:' + session.user.id, + key: counterKey, ttlcounter: messageHandler.counters.ttlcounter, maxBytes: limit }); diff --git a/lib/imap-notifier.js b/lib/imap-notifier.js index 12889579..af9bcaaa 100644 --- a/lib/imap-notifier.js +++ b/lib/imap-notifier.js @@ -373,15 +373,16 @@ class ImapNotifier extends EventEmitter { let mailbox = row[0]; let delta = row[1]; + let mailboxKey = tools.redisClusterValue(mailbox, this.redis); - await this.redis.cachedcounter(`total:${mailbox}`, delta.total, consts.MAILBOX_COUNTER_TTL); + await this.redis.cachedcounter(`total:${mailboxKey}`, delta.total, consts.MAILBOX_COUNTER_TTL); if (delta.unseenChange) { // Message info changed in mailbox, so just te be sure, clear the unseen counter as well // Unseen counter is more volatile and also easier to count (usually only a small number on indexed messages) - await this.redis.del('unseen:' + mailbox); + await this.redis.del(`unseen:${mailboxKey}`); } else if (delta.unseen) { - await this.redis.cachedcounter(`unseen:${mailbox}`, delta.unseen, consts.MAILBOX_COUNTER_TTL); + await this.redis.cachedcounter(`unseen:${mailboxKey}`, delta.unseen, consts.MAILBOX_COUNTER_TTL); } } } diff --git a/lib/tasks/clear-folder.js b/lib/tasks/clear-folder.js index 7e62673a..a0842c32 100644 --- a/lib/tasks/clear-folder.js +++ b/lib/tasks/clear-folder.js @@ -2,6 +2,7 @@ const log = require('npmlog'); const db = require('../db'); +const tools = require('../tools'); let run = async (task, data, options) => { const messageHandler = options.messageHandler; @@ -48,8 +49,9 @@ let run = async (task, data, options) => { await cursor.close(); try { + const mailboxKey = tools.redisClusterValue(mailbox, db.redis); // clear counters - await db.redis.multi().del(`total:${mailbox}`).del(`unseen:${mailbox}`).exec(); + await db.redis.multi().del(`total:${mailboxKey}`).del(`unseen:${mailboxKey}`).exec(); } catch (err) { // ignore } diff --git a/lib/tools.js b/lib/tools.js index cd5d5bee..4cf73257 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -186,6 +186,16 @@ function redisConfig(defaultConfig) { }; } +function redisClusterValue(value, redis) { + value = value === null || typeof value === 'undefined' ? '' : value.toString(); + if (!value || !redis || !redis.isCluster) { + return value; + } + // strip existing braces to avoid nested hash tags + value = value.replace(/[{}]/g, ''); + return `{${value}}`; +} + function decodeAddresses(addresses) { addresses.forEach(address => { address.name = (address.name || '').toString(); @@ -221,7 +231,8 @@ function flatAddresses(addresses) { } async function getMailboxCounter(db, mailbox, type) { - const cacheKey = `${type || 'total'}:${mailbox.toString()}`; + const mailboxKey = redisClusterValue(mailbox, db.redis); + const cacheKey = `${type || 'total'}:${mailboxKey}`; try { // Check cache for pre-calculated counter value @@ -817,6 +828,7 @@ module.exports = { normalizeIp, getHostname, getWildcardAddresses, + redisClusterValue, redisConfig, checkRangeQuery, decodeAddresses, diff --git a/lib/user-handler.js b/lib/user-handler.js index 1ce7ec10..b5d80ad6 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -1834,7 +1834,7 @@ class UserHandler { } if (data.featureFlags && Object.keys(data.featureFlags).length) { - let req = this.redis.multi(); + let req = this.redis.pipeline(); for (let featureFlag of Object.keys(data.featureFlags)) { if (data.featureFlags[featureFlag]) { req = req.sadd(`feature:${featureFlag}`, user.toString()); @@ -3145,6 +3145,7 @@ class UserHandler { ['pop3MaxMessages', 'pxm'], ['receivedMax', 'rl:rcpt'] ]); + let slotKeys = new Set(['iup', 'idw', 'pdw', 'pxm']); let flushKeys = []; let flushHKeys = []; @@ -3154,7 +3155,9 @@ class UserHandler { } if (resetKeys.has(key)) { - flushKeys.push(resetKeys.get(key) + ':' + user); + let prefix = resetKeys.get(key); + let keySuffix = slotKeys.has(prefix) ? tools.redisClusterValue(user, this.redis) : user.toString(); + flushKeys.push(`${prefix}:${keySuffix}`); } if (key === 'imapMaxConnections') { flushHKeys.push({ key: 'lim:imap', value: user.toString() }); @@ -3394,7 +3397,7 @@ class UserHandler { // check if we need to reset any ttl counters if (flushKeys.length || flushHKeys.length) { - let flushreq = this.redis.multi(); + let flushreq = this.redis.pipeline(); flushKeys.forEach(key => { flushreq = flushreq.del(key); @@ -3616,7 +3619,7 @@ class UserHandler { // remove feature flag entries if (existingAccount.featureFlags && Object.keys(existingAccount.featureFlags).length) { - let req = this.redis.multi(); + let req = this.redis.pipeline(); for (let featureFlag of Object.keys(existingAccount.featureFlags)) { if (existingAccount.featureFlags[featureFlag]) { req = req.srem(`feature:${featureFlag}`, user.toString()); diff --git a/pop3.js b/pop3.js index de8ea452..29edb18f 100644 --- a/pop3.js +++ b/pop3.js @@ -133,7 +133,7 @@ const serverOptions = { session.user.mailbox = mailbox._id; db.redis - .multi() + .pipeline() // "new" limit store .hget(`pxm:${session.user.id}`, mailbox._id.toString()) // fallback store @@ -179,7 +179,7 @@ const serverOptions = { } // try to update index, ignore result db.redis - .multi() + .pipeline() // update limit store .hset(`pxm:${session.user.id}`, mailbox._id.toString(), oldestMessageData.uid) // delete fallback store as it is no longer needed @@ -218,7 +218,9 @@ const serverOptions = { return callback(err); } - messageHandler.counters.ttlcounter('pdw:' + session.user.id, 0, limit, false, (err, res) => { + let counterKey = 'pdw:' + tools.redisClusterValue(session.user.id, db.redis); + + messageHandler.counters.ttlcounter(counterKey, 0, limit, false, (err, res) => { if (err) { return callback(err); } @@ -256,7 +258,7 @@ const serverOptions = { } let limiter = new LimitedFetch({ - key: 'pdw:' + session.user.id, + key: counterKey, ttlcounter: messageHandler.counters.ttlcounter, maxBytes: limit, skipCounter: true diff --git a/tasks.js b/tasks.js index 24687823..be2320cb 100644 --- a/tasks.js +++ b/tasks.js @@ -88,7 +88,7 @@ module.exports.start = callback => { gcLock = new RedFour({ redis: db.redis.isCluster ? db.redis.redisMaster : db.redis, - namespace: 'wildduck' + namespace: db.redis.isCluster ? '{wildduck}' : 'wildduck' }); messageHandler = new MessageHandler({ From c72349089d442f3ea30bf82e6687b5941076b8b2 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 19 Feb 2026 12:06:23 +0200 Subject: [PATCH 08/32] ioredfour now support cluster mode, simplify config, db.js remove redisMaster, imap-notifier instead of separate new Redis() for subscriber just duplicate connection --- api.js | 4 ++-- imap.js | 4 ++-- lib/acme/certs.js | 4 ++-- lib/attachments/gridstore-storage.js | 4 ++-- lib/db.js | 3 --- lib/imap-notifier.js | 3 ++- tasks.js | 4 ++-- 7 files changed, 12 insertions(+), 14 deletions(-) diff --git a/api.js b/api.js index 86de230e..46291b30 100644 --- a/api.js +++ b/api.js @@ -560,8 +560,8 @@ module.exports = done => { server.loggelf = (message, requiredKeys = []) => loggelf(message, requiredKeys); server.lock = new Lock({ - redis: db.redis.isCluster ? db.redis.redisMaster : db.redis, - namespace: db.redis.isCluster ? '{mail}' : 'mail' + redis: db.redis, + namespace: 'mail' }); acmeRoutes(db, server, { disableRedirect: true }); diff --git a/imap.js b/imap.js index 4b5e5f13..b26d6f36 100644 --- a/imap.js +++ b/imap.js @@ -154,8 +154,8 @@ let createInterface = (ifaceOptions, callback) => { server.notifier = notifier; server.lock = new Lock({ - redis: db.redis.isCluster ? db.redis.redisMaster : db.redis, - namespace: db.redis.isCluster ? '{mail}' : 'mail' + redis: db.redis, + namespace: 'mail' }); // setup command handlers for the server instance diff --git a/lib/acme/certs.js b/lib/acme/certs.js index c54d50d6..af56a0c5 100644 --- a/lib/acme/certs.js +++ b/lib/acme/certs.js @@ -188,8 +188,8 @@ const acquireCert = async (domain, acmeOptions, certificateData, certHandler) => // Use locking to avoid race conditions, first try gets the lock, others wait until first is finished if (!getLock) { let lock = new Lock({ - redis: certHandler.redis.isCluster ? certHandler.redis.redisMaster : certHandler.redis, - namespace: certHandler.redis.isCluster ? '{acme}' : 'acme' + redis: certHandler.redis, + namespace: 'acme' }); getLock = (...args) => lock.waitAcquireLock(...args); releaseLock = (...args) => lock.releaseLock(...args); diff --git a/lib/attachments/gridstore-storage.js b/lib/attachments/gridstore-storage.js index 8922f82f..bfc8b52d 100644 --- a/lib/attachments/gridstore-storage.js +++ b/lib/attachments/gridstore-storage.js @@ -21,8 +21,8 @@ class GridstoreStorage { this.decodeBase64 = (options.options && options.options.decodeBase64) || false; this.lock = new RedFour({ - redis: options.redis.isCluster ? options.redis.redisMaster : options.redis, - namespace: options.redis.isCluster ? '{wildduck}' : 'wildduck' + redis: options.redis, + namespace: 'wildduck' }); this.gridfs = options.gridfs; diff --git a/lib/db.js b/lib/db.js index 8cc5dda6..7878178a 100644 --- a/lib/db.js +++ b/lib/db.js @@ -97,9 +97,6 @@ module.exports.connect = callback => { module.exports.redisConfig = REDIS_CONF; module.exports.redis = REDIS_CONF.cluster ? new Redis.Cluster(REDIS_CONF.nodes, REDIS_CONF.options) : new Redis(REDIS_CONF); - if (REDIS_CONF.cluster) { - module.exports.redis.redisMaster = new Redis(module.exports.redis.nodes('master')[0]); // used for locks and so on, that do not work with or support Redis cluster mode - } module.exports.queueConf = { connection: REDIS_CONF.cluster ? module.exports.redis : Object.assign({ connectionName: `${REDIS_CONF.connectionName}[notify]` }, REDIS_CONF), prefix: REDIS_CONF.cluster ? `{wd:bull}` : `wd:bull` diff --git a/lib/imap-notifier.js b/lib/imap-notifier.js index af9bcaaa..d876842e 100644 --- a/lib/imap-notifier.js +++ b/lib/imap-notifier.js @@ -33,7 +33,8 @@ class ImapNotifier extends EventEmitter { this.connectionSessions = new WeakMap(); // Subscriber needs its own client connection. This is relevant only in the context of IMAP - this.subscriber = new Redis(tools.redisConfig(config.dbs.redis)); + + this.subscriber = this.redis.duplicate(); // connection agnostic (standalone or Cluster) errors.registerRedisErrorLogger(this.subscriber, { role: 'imap-notifier-subscriber' }); this._listeners = new EventEmitter(); diff --git a/tasks.js b/tasks.js index be2320cb..b64d6126 100644 --- a/tasks.js +++ b/tasks.js @@ -87,8 +87,8 @@ module.exports.start = callback => { }; gcLock = new RedFour({ - redis: db.redis.isCluster ? db.redis.redisMaster : db.redis, - namespace: db.redis.isCluster ? '{wildduck}' : 'wildduck' + redis: db.redis, + namespace: 'wildduck' }); messageHandler = new MessageHandler({ From ecc09c68ec9025bf3ab16974d238f81debc4e604 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 19 Feb 2026 16:09:18 +0200 Subject: [PATCH 09/32] use multi() only for same slot keys --- .github/workflows/test.yml | 70 ++++++++++++++++++++++++++++++++++++++ bin/access-tokens | 17 ++++----- config/dbs.toml | 32 ++++++++++++++--- indexer.js | 8 +++-- lib/api/users.js | 61 ++++++++++++++++----------------- lib/user-handler.js | 32 ++++++++--------- pop3.js | 45 +++++++++++++----------- 7 files changed, 180 insertions(+), 85 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fdcd2f97..550f1d9b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,3 +57,73 @@ jobs: env: APPCONF_dbs_redis: redis://127.0.0.1:6379/1 APPCONF_dbs_mongodb: 'mongodb://127.0.0.1:27017/wildduck-test?authSource=admin' + + test-redis-cluster: + runs-on: ubuntu-latest + services: + redis-cluster: + image: grokzen/redis-cluster:7.0.10 + env: + IP: 127.0.0.1 + INITIAL_PORT: 7000 + MASTERS: 3 + SLAVES_PER_MASTER: 0 + options: >- + --health-cmd "redis-cli -c -p 7000 ping" + --health-interval 10s + --health-timeout 5s + --health-retries 20 + ports: + - 7000:7000 + - 7001:7001 + - 7002:7002 + + steps: + - uses: actions/checkout@v6 + + - name: Start MongoDB + uses: supercharge/mongodb-github-action@1.12.1 + with: + mongodb-version: 6.0 + mongodb-port: 27017 + mongodb-db: wildduck-test + + - name: Use Node.js 24.x + uses: actions/setup-node@v6 + with: + node-version: 24.x + + - name: Install mongosh and mpop + run: | + sudo apt-get update + sudo apt-get install -y wget gnupg mpop + wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add - + echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list + sudo apt-get update + sudo apt-get install -y mongodb-mongosh + + - run: which mongosh + - run: which mpop + - run: npm install + + - name: Wait for Redis Cluster + run: | + for port in 7000 7001 7002; do + for i in $(seq 1 60); do + redis-cli -c -h 127.0.0.1 -p "$port" ping && break + sleep 1 + done + done + redis-cli -c -h 127.0.0.1 -p 7000 cluster info | grep 'cluster_state:ok' + + - name: Reset DB and Redis cluster + run: | + mongosh --eval 'db.dropDatabase()' wildduck-test + for port in 7000 7001 7002; do + redis-cli -c -h 127.0.0.1 -p "$port" flushdb + done + + - name: Run tests against Redis cluster + run: npm run runtest + env: + APPCONF_dbs_mongodb: 'mongodb://127.0.0.1:27017/wildduck-test?authSource=admin' diff --git a/bin/access-tokens b/bin/access-tokens index 66a76681..17f2e441 100755 --- a/bin/access-tokens +++ b/bin/access-tokens @@ -60,11 +60,8 @@ let argv = yargs .digest('hex') }; - await db.redis - .pipeline() - .hmset(key, tokenData) - .sadd('tn:user:' + argv.user, tokenHash) - .exec(); + await db.redis.hmset(key, tokenData); + await db.redis.sadd('tn:user:' + argv.user, tokenHash); console.error('Generated access token for %s[%s]:', tokenData.user, tokenData.role); @@ -126,13 +123,11 @@ let argv = yargs process.exit(); } - let query = await db.redis.pipeline().del('tn:user:' + user); + await db.redis.del('tn:user:' + user); - tokens.forEach(tokenHash => { - query = query.del('tn:token:' + tokenHash); - }); - - await query.exec(); + for (let tokenHash of tokens) { + await db.redis.del('tn:token:' + tokenHash); + } console.error('Deleted %s tokens for %s', tokens.length, user); diff --git a/config/dbs.toml b/config/dbs.toml index a76db114..9e623a44 100644 --- a/config/dbs.toml +++ b/config/dbs.toml @@ -1,5 +1,5 @@ # mongodb connection string for the main database -mongo="mongodb://127.0.0.1:27017/wildduck" +mongo = "mongodb://127.0.0.1:27017/wildduck" # redis connection string to connect to a single master (see below for Sentinel example) #redis="redis://127.0.0.1:6379/3" @@ -22,14 +22,14 @@ mongo="mongodb://127.0.0.1:27017/wildduck" # Optional database name or connection url for ZoneMTA queue database. This is # used to push outbound emails to the sending queue -sender="zone-mta" +sender = "zone-mta" #queued="mail" [redis] -host="127.0.0.1" -port=6379 -db=3 +host = "127.0.0.1" +port = 6379 +db = 3 ## Connect to Redis Sentinel instead of single master # [redis] @@ -45,3 +45,25 @@ db=3 # [[redis.sentinels]] # host="54.36.85.115" # port=26379 + +## Connect to Redis Cluster instead of a single master +# [redis] +# cluster = true +# password = "" # global/fallback password +# [[redis.nodes]] +# host="54.36.85.113" +# port=26379 +# password="" # per node password +# [[redis.nodes]] +# host = "127.0.0.1" +# port = 7000 +# [[redis.nodes]] +# host = "127.0.0.1" +# port = 7001 +# [[redis.nodes]] +# host = "127.0.0.1" +# port = 7002 +# [[redis.nodes]] +# host="54.36.85.115" +# port=26379 +# password="" diff --git a/indexer.js b/indexer.js index 26fa5c85..2d385c32 100644 --- a/indexer.js +++ b/indexer.js @@ -13,6 +13,7 @@ const { ObjectId } = require('mongodb'); const libmime = require('libmime'); const punycode = require('punycode.js'); const { getClient } = require('./lib/elasticsearch'); +const tools = require('./lib/tools'); let loggelf; let processlock; @@ -283,15 +284,16 @@ function indexingJob(esclient) { const dateKeyTdy = new Date().toISOString().substring(0, 10).replace(/-/g, ''); const dateKeyYdy = new Date(Date.now() - 24 * 3600 * 1000).toISOString().substring(0, 10).replace(/-/g, ''); - const tombstoneTdy = `indexer:tomb:${dateKeyTdy}`; - const tombstoneYdy = `indexer:tomb:${dateKeyYdy}`; + const tombstoneTag = tools.redisClusterValue('indexer:tomb', db.redis); + const tombstoneTdy = `${tombstoneTag}:${dateKeyTdy}`; + const tombstoneYdy = `${tombstoneTag}:${dateKeyYdy}`; switch (data.action) { case 'new': { // check tombstone for race conditions (might be already deleted) const [[err1, isDeleted1], [err2, isDeleted2]] = await db.redis - .pipeline() + .multi() .sismember(tombstoneTdy, data.message) .sismember(tombstoneYdy, data.message) .exec(); diff --git a/lib/api/users.js b/lib/api/users.js index ff3342b1..6e8e720e 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -968,37 +968,36 @@ module.exports = (db, server, userHandler, settingsHandler) => { let response; try { - response = await db.redis - .pipeline() - // sending counters are stored in Redis - - // sent messages - .get('wdr:' + userData._id.toString()) - .ttl('wdr:' + userData._id.toString()) - - // forwarded messages - .get('wdf:' + userData._id.toString()) - .ttl('wdf:' + userData._id.toString()) - - // rate limited recipient - .get('rl:rcpt:' + userData._id.toString()) - .ttl('rl:rcpt:' + userData._id.toString()) - - // rate limited imap uploads - .get('iup:' + userData._id.toString()) - .ttl('iup:' + userData._id.toString()) - - // rate limited imap downloads - .get('idw:' + userData._id.toString()) - .ttl('idw:' + userData._id.toString()) - - // rate limited pop3 downloads - .get('pdw:' + userData._id.toString()) - .ttl('pdw:' + userData._id.toString()) - - .hget('lim:imap', userData._id.toString()) - - .exec(); + let userId = userData._id.toString(); + let userRateKey = tools.redisClusterValue(userId, db.redis); + + let [ + recipientsResponse, + forwardsResponse, + receivedResponse, + imapUploadResponse, + imapDownloadResponse, + pop3DownloadResponse, + imapConnection + ] = await Promise.all([ + db.redis.multi().get(`wdr:${userId}`).ttl(`wdr:${userId}`).exec(), + db.redis.multi().get(`wdf:${userId}`).ttl(`wdf:${userId}`).exec(), + db.redis.multi().get(`rl:rcpt:${userId}`).ttl(`rl:rcpt:${userId}`).exec(), + db.redis.multi().get(`iup:${userRateKey}`).ttl(`iup:${userRateKey}`).exec(), + db.redis.multi().get(`idw:${userRateKey}`).ttl(`idw:${userRateKey}`).exec(), + db.redis.multi().get(`pdw:${userRateKey}`).ttl(`pdw:${userRateKey}`).exec(), + db.redis.hget('lim:imap', userId) + ]); + + response = [ + ...(recipientsResponse || []), + ...(forwardsResponse || []), + ...(receivedResponse || []), + ...(imapUploadResponse || []), + ...(imapDownloadResponse || []), + ...(pop3DownloadResponse || []), + [null, imapConnection] + ]; } catch (err) { // ignore errors.notify(err, { userId: user }); diff --git a/lib/user-handler.js b/lib/user-handler.js index b5d80ad6..d1a2c822 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -1834,14 +1834,14 @@ class UserHandler { } if (data.featureFlags && Object.keys(data.featureFlags).length) { - let req = this.redis.pipeline(); + let req = []; for (let featureFlag of Object.keys(data.featureFlags)) { if (data.featureFlags[featureFlag]) { - req = req.sadd(`feature:${featureFlag}`, user.toString()); + req.push(this.redis.sadd(`feature:${featureFlag}`, user.toString())); } } try { - await req.exec(); + await Promise.all(req); } catch (err) { log.error('Redis', 'FEATUREFAIL failed to set feature flags id=%s error=%s', user, err.message); } @@ -3397,19 +3397,19 @@ class UserHandler { // check if we need to reset any ttl counters if (flushKeys.length || flushHKeys.length) { - let flushreq = this.redis.pipeline(); + // just call the operations and hope for the best, no problems if fails + try { + let flushreq = []; - flushKeys.forEach(key => { - flushreq = flushreq.del(key); - }); + flushKeys.forEach(key => { + flushreq.push(this.redis.del(key)); + }); - flushHKeys.forEach(entry => { - flushreq = flushreq.hdel(entry.key, entry.value); - }); + flushHKeys.forEach(entry => { + flushreq.push(this.redis.hdel(entry.key, entry.value)); + }); - // just call the operations and hope for the best, no problems if fails - try { - await flushreq.exec(); + await Promise.all(flushreq); } catch (err) { // ignore } @@ -3619,14 +3619,14 @@ class UserHandler { // remove feature flag entries if (existingAccount.featureFlags && Object.keys(existingAccount.featureFlags).length) { - let req = this.redis.pipeline(); + let req = []; for (let featureFlag of Object.keys(existingAccount.featureFlags)) { if (existingAccount.featureFlags[featureFlag]) { - req = req.srem(`feature:${featureFlag}`, user.toString()); + req.push(this.redis.srem(`feature:${featureFlag}`, user.toString())); } } try { - await req.exec(); + await Promise.all(req); } catch (err) { log.error('Redis', 'FEATUREFAIL failed to update feature flags id=%s error=%s', user, err.message); } diff --git a/pop3.js b/pop3.js index 29edb18f..9b288bd2 100644 --- a/pop3.js +++ b/pop3.js @@ -131,20 +131,17 @@ const serverOptions = { } session.user.mailbox = mailbox._id; + let messageIndexKey = `pxm:${tools.redisClusterValue(session.user.id, db.redis)}`; - db.redis - .pipeline() - // "new" limit store - .hget(`pxm:${session.user.id}`, mailbox._id.toString()) - // fallback store - .hget(`pop3uid`, mailbox._id.toString()) - .exec((err, res) => { - let lastIndex = res && ((res[0] && res[0][1]) || (res[1] && res[1][1])); + db.redis.hget(messageIndexKey, mailbox._id.toString(), (err, messageIndex) => { + let lastIndex = !err && messageIndex; - let query = { - mailbox: mailbox._id - }; - if (!err && lastIndex && !isNaN(lastIndex)) { + let query = { + mailbox: mailbox._id + }; + + let processMessages = () => { + if (lastIndex && !isNaN(lastIndex)) { query.uid = { $gte: Number(lastIndex) }; } @@ -177,14 +174,11 @@ const serverOptions = { if (!oldestMessageData || !oldestMessageData.uid) { return done(); } - // try to update index, ignore result - db.redis - .pipeline() - // update limit store - .hset(`pxm:${session.user.id}`, mailbox._id.toString(), oldestMessageData.uid) + + db.redis.hset(messageIndexKey, mailbox._id.toString(), oldestMessageData.uid, () => { // delete fallback store as it is no longer needed - .hdel(`pop3uid`, mailbox._id.toString()) - .exec(done); + db.redis.hdel(`pop3uid`, mailbox._id.toString(), done); + }); }; updateUIDIndex(() => @@ -207,7 +201,20 @@ const serverOptions = { ); }); }); + }; + + if (lastIndex) { + return processMessages(); + } + + // fallback store + db.redis.hget(`pop3uid`, mailbox._id.toString(), (fallbackErr, fallbackIndex) => { + if (!fallbackErr && fallbackIndex) { + lastIndex = fallbackIndex; + } + processMessages(); }); + }); } ); }, From d54fc5203a66539241c55c60f86bb830aa424d29 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 19 Feb 2026 16:09:40 +0200 Subject: [PATCH 10/32] add cluster specific package json command --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 03d458e6..fda06856 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "server.js", "scripts": { "test": "mongosh --eval 'db.dropDatabase()' wildduck-test && redis-cli -n 13 flushdb && npm run runtest", + "test:cluster": "mongosh --eval 'db.dropDatabase()' wildduck-test && redis-cli --cluster call 127.0.0.1:7000 flushdb && npm run runtest", "test:proto": "NODE_ENV=test grunt proto", "printconf": "NODE_CONFIG_ONLY=true npm start", "runtest": "NODE_ENV=test grunt", From f9263dcebb471d76f49a43887e068ad260d7a691 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 19 Feb 2026 16:26:10 +0200 Subject: [PATCH 11/32] fix cluster tests workflow --- .github/workflows/test.yml | 7 ++++--- config/test-cluster.toml | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 config/test-cluster.toml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 550f1d9b..a78e9e0f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -93,10 +93,10 @@ jobs: with: node-version: 24.x - - name: Install mongosh and mpop + - name: Install mongosh, mpop and redis-cli run: | sudo apt-get update - sudo apt-get install -y wget gnupg mpop + sudo apt-get install -y wget gnupg mpop redis-tools wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add - echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list sudo apt-get update @@ -104,6 +104,7 @@ jobs: - run: which mongosh - run: which mpop + - run: which redis-cli - run: npm install - name: Wait for Redis Cluster @@ -124,6 +125,6 @@ jobs: done - name: Run tests against Redis cluster - run: npm run runtest + run: NODE_ENV=test-cluster npx grunt env: APPCONF_dbs_mongodb: 'mongodb://127.0.0.1:27017/wildduck-test?authSource=admin' diff --git a/config/test-cluster.toml b/config/test-cluster.toml new file mode 100644 index 00000000..acb1cdbd --- /dev/null +++ b/config/test-cluster.toml @@ -0,0 +1,41 @@ +[api] +port = 8080 +host = "127.0.0.1" +secure = false + +[log] +level = "silly" + +[dbs] +# mongodb connection string for the main database +mongo = "mongodb://127.0.0.1:27017/wildduck-test" + +dbname = "wildduck-test" + +[dbs.redis] +cluster = true +password = "" + +[[dbs.redis.nodes]] +host = "127.0.0.1" +port = 7000 + +[[dbs.redis.nodes]] +host = "127.0.0.1" +port = 7001 + +[[dbs.redis.nodes]] +host = "127.0.0.1" +port = 7002 + +[imap] +port = 9993 +host = "127.0.0.1" + +[lmtp] +enabled = true +port = 2424 + +[pwned] +enabled = true +type = "softfail" From b9a912fa9b00ec69877bf7e82cb5b591812a384b Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 19 Feb 2026 19:33:54 +0200 Subject: [PATCH 12/32] grunt, don't hardcode node_env --- Gruntfile.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 2318e217..fe0710de 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,7 +1,5 @@ 'use strict'; -process.env.NODE_ENV = 'test'; - module.exports = function (grunt) { // Project configuration. grunt.initConfig({ From d106bdef29c956935cd7809665d9ead419029e79 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Fri, 20 Feb 2026 12:47:33 +0200 Subject: [PATCH 13/32] imap-notifier, in redis cluster mode use sharded pub/sub --- lib/imap-notifier.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/imap-notifier.js b/lib/imap-notifier.js index d876842e..d7e9b7c7 100644 --- a/lib/imap-notifier.js +++ b/lib/imap-notifier.js @@ -19,6 +19,9 @@ class ImapNotifier extends EventEmitter { errors.registerRedisErrorLogger(this.redis, { role: 'imap-notifier' }); // if separate redis will add the appropriate role this.counters = counters(this.redis); + this.subscribeFnName = this.redis.isCluster ? 'ssubscribe' : 'subscribe'; + this.publishFnName = this.redis.isCluster ? 'spublish' : 'publish'; + this.logger = options.logger || { info: log.silly.bind(log, 'IMAP'), debug: log.silly.bind(log, 'IMAP'), @@ -119,7 +122,7 @@ class ImapNotifier extends EventEmitter { } }); - this.subscriber.subscribe('wd_events'); + this.subscriber[this.subscribeFnName]('wd_events'); } /** @@ -306,7 +309,7 @@ class ImapNotifier extends EventEmitter { e: user.toString(), p: payload }); - this.redis.publish('wd_events', data); + this.redis[this.publishFnName]('wd_events', data); }); } From 9cf7228a9dbbc6f47c80e3b63907e1e2e6575800 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Mon, 23 Feb 2026 11:32:44 +0200 Subject: [PATCH 14/32] imap-notifier, if using redis cluster set shardedSubscribers: true --- lib/imap-notifier.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/imap-notifier.js b/lib/imap-notifier.js index d7e9b7c7..e0b8164d 100644 --- a/lib/imap-notifier.js +++ b/lib/imap-notifier.js @@ -37,7 +37,7 @@ class ImapNotifier extends EventEmitter { // Subscriber needs its own client connection. This is relevant only in the context of IMAP - this.subscriber = this.redis.duplicate(); // connection agnostic (standalone or Cluster) + this.subscriber = this.redis.duplicate([], this.redis.isCluster ? { shardedSubscribers: true } : {}); // connection agnostic (standalone or Cluster) errors.registerRedisErrorLogger(this.subscriber, { role: 'imap-notifier-subscriber' }); this._listeners = new EventEmitter(); From ff281c21240767342a9a121dfebf4b20c306434d Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 26 Feb 2026 09:49:15 +0200 Subject: [PATCH 15/32] update ioredfour dependency --- package-lock.json | 1272 ++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 625 insertions(+), 649 deletions(-) diff --git a/package-lock.json b/package-lock.json index 41eaba48..348f2af9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "http-parser-js": "0.5.10", "humanname": "0.2.2", "iconv-lite": "0.7.2", - "ioredfour": "1.3.0-ioredis-07", + "ioredfour": "1.4.0", "ioredis": "5.9.1", "ipaddr.js": "2.3.0", "isemail": "3.2.0", @@ -232,100 +232,50 @@ } }, "node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.969.0.tgz", - "integrity": "sha512-Ck202yziyzEu8O8XUZ9fyQOrjXAOFCOk3iAS2aqz517Pyb6ys81pf8DqOKEoYrtcKPY3Yn7Jb5zcLi0FVUMgTQ==", + "version": "3.998.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.998.0.tgz", + "integrity": "sha512-Ir/B7XnNsVr6O/dkUJnfnTsWsxnj3dM1XRowEDzEC3Jm6H9hnFFNnAd2mo45L50Yo62w0+tGgNeO6RYaJgyJgQ==", "license": "Apache-2.0", "optional": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.969.0", - "@aws-sdk/credential-provider-node": "3.969.0", - "@aws-sdk/middleware-host-header": "3.969.0", - "@aws-sdk/middleware-logger": "3.969.0", - "@aws-sdk/middleware-recursion-detection": "3.969.0", - "@aws-sdk/middleware-user-agent": "3.969.0", - "@aws-sdk/region-config-resolver": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@aws-sdk/util-endpoints": "3.969.0", - "@aws-sdk/util-user-agent-browser": "3.969.0", - "@aws-sdk/util-user-agent-node": "3.969.0", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.20.5", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.6", - "@smithy/middleware-retry": "^4.4.22", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.7", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.21", - "@smithy/util-defaults-mode-node": "^4.2.24", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.969.0.tgz", - "integrity": "sha512-Qn0Uz6o15q2S+1E6OpwRKmaAMoT4LktEn+Oibk28qb2Mne+emaDawhZXahOJb/wFw5lN2FEH7XoiSNenNNUmCw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.969.0", - "@aws-sdk/middleware-host-header": "3.969.0", - "@aws-sdk/middleware-logger": "3.969.0", - "@aws-sdk/middleware-recursion-detection": "3.969.0", - "@aws-sdk/middleware-user-agent": "3.969.0", - "@aws-sdk/region-config-resolver": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@aws-sdk/util-endpoints": "3.969.0", - "@aws-sdk/util-user-agent-browser": "3.969.0", - "@aws-sdk/util-user-agent-node": "3.969.0", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.20.5", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.6", - "@smithy/middleware-retry": "^4.4.22", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.7", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.21", - "@smithy/util-defaults-mode-node": "^4.2.24", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.14", + "@aws-sdk/credential-provider-node": "^3.972.13", + "@aws-sdk/middleware-host-header": "^3.972.5", + "@aws-sdk/middleware-logger": "^3.972.5", + "@aws-sdk/middleware-recursion-detection": "^3.972.5", + "@aws-sdk/middleware-user-agent": "^3.972.14", + "@aws-sdk/region-config-resolver": "^3.972.5", + "@aws-sdk/types": "^3.973.3", + "@aws-sdk/util-endpoints": "^3.996.2", + "@aws-sdk/util-user-agent-browser": "^3.972.5", + "@aws-sdk/util-user-agent-node": "^3.972.13", + "@smithy/config-resolver": "^4.4.9", + "@smithy/core": "^3.23.6", + "@smithy/fetch-http-handler": "^5.3.11", + "@smithy/hash-node": "^4.2.10", + "@smithy/invalid-dependency": "^4.2.10", + "@smithy/middleware-content-length": "^4.2.10", + "@smithy/middleware-endpoint": "^4.4.20", + "@smithy/middleware-retry": "^4.4.37", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/middleware-stack": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/node-http-handler": "^4.4.12", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-body-length-browser": "^4.2.1", + "@smithy/util-body-length-node": "^4.2.2", + "@smithy/util-defaults-mode-browser": "^4.3.36", + "@smithy/util-defaults-mode-node": "^4.2.39", + "@smithy/util-endpoints": "^3.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-retry": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -333,24 +283,24 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.969.0.tgz", - "integrity": "sha512-qqmQt4z5rEK1OYVkVkboWgy/58CC5QaQ7oy0tvLe3iri/mfZbgJkA+pkwQyRP827DfCBZ3W7Ki9iwSa+B2U7uQ==", + "version": "3.973.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.14.tgz", + "integrity": "sha512-iAQ1jIGESTVjoqNNY9VlsE9FnCz+Hc8s+dgurF6WrgFyVIw+uggH+V102RFhwjRv4dLSSLfzjDwvQnLszov7TQ==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/types": "3.969.0", - "@aws-sdk/xml-builder": "3.969.0", - "@smithy/core": "^3.20.5", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.10.7", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/types": "^3.973.3", + "@aws-sdk/xml-builder": "^3.972.7", + "@smithy/core": "^3.23.6", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -358,16 +308,16 @@ } }, "node_modules/@aws-sdk/credential-provider-cognito-identity": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.969.0.tgz", - "integrity": "sha512-Fqt+qa9/yEn5wr2fnpocOB4BBGkatxVfXn9WKXwItkgbhjtIz3DEwMCOBFiOiOf1WZn3+E/JupH4oBP3QkGlOw==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.5.tgz", + "integrity": "sha512-UpWOvLGadDIAE1QnhpygZ+m2YeTH+uq1voVXpZclHOxMt0YUWDl5GPE728eYmrIgTp++Bg/AbuixB1Q35pJyOg==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/client-cognito-identity": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/nested-clients": "^3.996.2", + "@aws-sdk/types": "^3.973.3", + "@smithy/property-provider": "^4.2.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -375,16 +325,16 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.969.0.tgz", - "integrity": "sha512-yS96heH5XDUqS3qQNcdObKKMOqZaivuNInMVRpRli48aXW8fX1M3fY67K/Onlqa3Wxu6WfDc3ZGF52SywdLvbg==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.12.tgz", + "integrity": "sha512-WPtj/iAYHHd+NDM6AZoilZwUz0nMaPxbTPGLA7nhyIYRZN2L8trqfbNvm7g/Jr3gzfKp1LpO6AtBTnrhz9WW2g==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/core": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.14", + "@aws-sdk/types": "^3.973.3", + "@smithy/property-provider": "^4.2.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -392,21 +342,21 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.969.0.tgz", - "integrity": "sha512-QCEFxBiUYFUW5VG6k8jKhT4luZndpC7uUY4u1olwt+OnJrl3N2yC7oS34isVBa3ioXZ4A0YagbXTa/3mXUhlAA==", + "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.14.tgz", + "integrity": "sha512-umtjCicH2o/Fcc8Fu1562UkDyt6gql4czTYVlUfHfAM8S4QEKggzmtHYYYpPfQcjFj1ajyy68ahYSuF67x4ptQ==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/core": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.7", - "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.10", + "@aws-sdk/core": "^3.973.14", + "@aws-sdk/types": "^3.973.3", + "@smithy/fetch-http-handler": "^5.3.11", + "@smithy/node-http-handler": "^4.4.12", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.15", "tslib": "^2.6.2" }, "engines": { @@ -414,25 +364,25 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.969.0.tgz", - "integrity": "sha512-lsXyTDkUrZPxjr0XruZrqdcHY9zHcIuoY3TOCQEm23VTc8Np2BenTtjGAIexkL3ar69K4u3FVLQroLpmFxeXqA==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.12.tgz", + "integrity": "sha512-qjzgnMl6GIBbVeK74jBqSF07+s6kyeZl5R88qjMs302JlqkxE57jkvflDmZ9I017ffEWqIUa9/M4Hfp28qyu1g==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/core": "3.969.0", - "@aws-sdk/credential-provider-env": "3.969.0", - "@aws-sdk/credential-provider-http": "3.969.0", - "@aws-sdk/credential-provider-login": "3.969.0", - "@aws-sdk/credential-provider-process": "3.969.0", - "@aws-sdk/credential-provider-sso": "3.969.0", - "@aws-sdk/credential-provider-web-identity": "3.969.0", - "@aws-sdk/nested-clients": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.14", + "@aws-sdk/credential-provider-env": "^3.972.12", + "@aws-sdk/credential-provider-http": "^3.972.14", + "@aws-sdk/credential-provider-login": "^3.972.12", + "@aws-sdk/credential-provider-process": "^3.972.12", + "@aws-sdk/credential-provider-sso": "^3.972.12", + "@aws-sdk/credential-provider-web-identity": "^3.972.12", + "@aws-sdk/nested-clients": "^3.996.2", + "@aws-sdk/types": "^3.973.3", + "@smithy/credential-provider-imds": "^4.2.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -440,19 +390,19 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.969.0.tgz", - "integrity": "sha512-bIRFDf54qIUFFLTZNYt40d6EseNeK9w80dHEs7BVEAWoS23c9+MSqkdg/LJBBK9Kgy01vRmjiedfBZN+jGypLw==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.12.tgz", + "integrity": "sha512-AO57y46PzG24bJzxWLk+FYJG6MzxvXoFXnOKnmKUGV43ub4/FS/4Rz7zCC6ThqUotgqEFd30l5LTAd65RP65pg==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/core": "3.969.0", - "@aws-sdk/nested-clients": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.14", + "@aws-sdk/nested-clients": "^3.996.2", + "@aws-sdk/types": "^3.973.3", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -460,23 +410,23 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.969.0.tgz", - "integrity": "sha512-lImMjcy/5SGDIBk7PFJCqFO4rFuapKCvo1z2PidD3Cbz2D7wsJnyqUNQIp5Ix0Xc3/uAYG9zXI9kgaMf1dspIQ==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.13.tgz", + "integrity": "sha512-ME2sgus+gFRtiudy5Xqj9iT/tj8lHOIGrFgktuO5skJU4EngOvTZ1Hpj8mknrW4FgWXmpWhc88NtEscUuuDpKw==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/credential-provider-env": "3.969.0", - "@aws-sdk/credential-provider-http": "3.969.0", - "@aws-sdk/credential-provider-ini": "3.969.0", - "@aws-sdk/credential-provider-process": "3.969.0", - "@aws-sdk/credential-provider-sso": "3.969.0", - "@aws-sdk/credential-provider-web-identity": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/credential-provider-env": "^3.972.12", + "@aws-sdk/credential-provider-http": "^3.972.14", + "@aws-sdk/credential-provider-ini": "^3.972.12", + "@aws-sdk/credential-provider-process": "^3.972.12", + "@aws-sdk/credential-provider-sso": "^3.972.12", + "@aws-sdk/credential-provider-web-identity": "^3.972.12", + "@aws-sdk/types": "^3.973.3", + "@smithy/credential-provider-imds": "^4.2.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -484,17 +434,17 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.969.0.tgz", - "integrity": "sha512-2qQkM0rwd8Hl9nIHtUaqT8Z/djrulovqx/wBHsbRKaISwc2fiT3De1Lk1jx34Jzrz/dTHAMJJi+cML1N4Lk3kw==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.12.tgz", + "integrity": "sha512-msxrHBpVP5AOIDohNPCINUtL47f7XI1TEru3N13uM3nWUMvIRA1vFa8Tlxbxm1EntPPvLAxRmvE5EbjDjOZkbw==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/core": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.14", + "@aws-sdk/types": "^3.973.3", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -502,19 +452,19 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.969.0.tgz", - "integrity": "sha512-JHqXw9Ct3dtZB86/zGFJYWyodr961GyIrqTBhV0brrZFPvcinM9abDSK58jt6GNBM2lqfMCvXL6I4ahNsMdkrg==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.12.tgz", + "integrity": "sha512-D5iC5546hJyhobJN0szOT4KVeJQ8z/meZq2B3lEDZFcvHONKw+tzq36DAJUy3qLTueeB2geSxiHXngQlA11eoA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/client-sso": "3.969.0", - "@aws-sdk/core": "3.969.0", - "@aws-sdk/token-providers": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.14", + "@aws-sdk/nested-clients": "^3.996.2", + "@aws-sdk/token-providers": "3.998.0", + "@aws-sdk/types": "^3.973.3", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -522,18 +472,18 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.969.0.tgz", - "integrity": "sha512-mKCZtqrs3ts3YmIjT4NFlYgT2Oe6syW0nX5m2l7iyrFrLXw26Zo3rx29DjGzycPdJHZZvsIy5y6yqChDuF65ng==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.12.tgz", + "integrity": "sha512-yluBahBVsduoA/zgV0NAXtwwXvQ6tNn95dNA3Hg+vISdiPWA46QY0d9PLO2KpNbjtm+1oGcWxemS4fYTwJ0W1w==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/core": "3.969.0", - "@aws-sdk/nested-clients": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.14", + "@aws-sdk/nested-clients": "^3.996.2", + "@aws-sdk/types": "^3.973.3", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -541,31 +491,31 @@ } }, "node_modules/@aws-sdk/credential-providers": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.969.0.tgz", - "integrity": "sha512-VTbLK8N34DyG0NprD7K7/atf2pMTaWKnQg59NNhlPDkagwv32rQBUVSUQqCTJ3kshzS96oH3FpGKd22uCPp3Tg==", + "version": "3.998.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.998.0.tgz", + "integrity": "sha512-7IlzfghtMGwkoGCwbxJc32pY8HYEYOu+CH0Gr8Fmon2QbmAmR24z4tKCGVBy9XSJe3mvnoz6fqGnBZyrZIAnYg==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/client-cognito-identity": "3.969.0", - "@aws-sdk/core": "3.969.0", - "@aws-sdk/credential-provider-cognito-identity": "3.969.0", - "@aws-sdk/credential-provider-env": "3.969.0", - "@aws-sdk/credential-provider-http": "3.969.0", - "@aws-sdk/credential-provider-ini": "3.969.0", - "@aws-sdk/credential-provider-login": "3.969.0", - "@aws-sdk/credential-provider-node": "3.969.0", - "@aws-sdk/credential-provider-process": "3.969.0", - "@aws-sdk/credential-provider-sso": "3.969.0", - "@aws-sdk/credential-provider-web-identity": "3.969.0", - "@aws-sdk/nested-clients": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.20.5", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/client-cognito-identity": "3.998.0", + "@aws-sdk/core": "^3.973.14", + "@aws-sdk/credential-provider-cognito-identity": "^3.972.5", + "@aws-sdk/credential-provider-env": "^3.972.12", + "@aws-sdk/credential-provider-http": "^3.972.14", + "@aws-sdk/credential-provider-ini": "^3.972.12", + "@aws-sdk/credential-provider-login": "^3.972.12", + "@aws-sdk/credential-provider-node": "^3.972.13", + "@aws-sdk/credential-provider-process": "^3.972.12", + "@aws-sdk/credential-provider-sso": "^3.972.12", + "@aws-sdk/credential-provider-web-identity": "^3.972.12", + "@aws-sdk/nested-clients": "^3.996.2", + "@aws-sdk/types": "^3.973.3", + "@smithy/config-resolver": "^4.4.9", + "@smithy/core": "^3.23.6", + "@smithy/credential-provider-imds": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -573,15 +523,15 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.969.0.tgz", - "integrity": "sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.5.tgz", + "integrity": "sha512-dVA0m1cEQ2iA6yB19aHvWNeUVTuvTt3AXzT0aiIu2uxk0S7AcmwDCDaRgYa/v+eFHcJVxEnpYTozqA7X62xinw==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/types": "3.969.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.3", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -589,14 +539,14 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.969.0.tgz", - "integrity": "sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.5.tgz", + "integrity": "sha512-03RqplLZjUTkYi0dDPR/bbOLnDLFNdaVvNENgA3XK7Ph1MhEBhUYlgoGfOyRAKApDZ+WG4ykOoA8jI8J04jmFA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/types": "3.969.0", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.3", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -604,16 +554,16 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.969.0.tgz", - "integrity": "sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.5.tgz", + "integrity": "sha512-2QSuuVkpHTe84+mDdnFjHX8rAP3g0yYwLVAhS3lQN1rW5Z/zNsf8/pYQrLjLO4n4sPCsUAkTa0Vrod0lk+o1Tg==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/types": "3.969.0", + "@aws-sdk/types": "^3.973.3", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -621,18 +571,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.969.0.tgz", - "integrity": "sha512-Y6WkW8QQ2X9jG9HNBWyzp5KlJOCtLqX8VIvGLoGc2wXdZH7dgOy62uFhkfnHbgfiel6fkNYaycjGx/yyxi0JLQ==", + "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.14.tgz", + "integrity": "sha512-PzDz+yRAQuIzd+4ZY3s6/TYRzlNKAn4Gae3E5uLV7NnYHqrZHFoAfKE4beXcu3C51pA2/FQ3X2qOGSYqUoN1WQ==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/core": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@aws-sdk/util-endpoints": "3.969.0", - "@smithy/core": "^3.20.5", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.14", + "@aws-sdk/types": "^3.973.3", + "@aws-sdk/util-endpoints": "^3.996.2", + "@smithy/core": "^3.23.6", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -640,49 +590,49 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.969.0.tgz", - "integrity": "sha512-MJrejgODxVYZjQjSpPLJkVuxnbrue1x1R8+as3anT5V/wk9Qc/Pf5B1IFjM3Ak6uOtzuRYNY4auOvcg4U8twDA==", + "version": "3.996.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.2.tgz", + "integrity": "sha512-W+u6EM8WRxOIhAhR2mXMHSaUygqItpTehkgxLwJngXqr9RlAR4t6CtECH7o7QK0ct3oyi5Z8ViDHtPbel+D2Rg==", "license": "Apache-2.0", "optional": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.969.0", - "@aws-sdk/middleware-host-header": "3.969.0", - "@aws-sdk/middleware-logger": "3.969.0", - "@aws-sdk/middleware-recursion-detection": "3.969.0", - "@aws-sdk/middleware-user-agent": "3.969.0", - "@aws-sdk/region-config-resolver": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@aws-sdk/util-endpoints": "3.969.0", - "@aws-sdk/util-user-agent-browser": "3.969.0", - "@aws-sdk/util-user-agent-node": "3.969.0", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.20.5", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.6", - "@smithy/middleware-retry": "^4.4.22", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.7", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.21", - "@smithy/util-defaults-mode-node": "^4.2.24", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.14", + "@aws-sdk/middleware-host-header": "^3.972.5", + "@aws-sdk/middleware-logger": "^3.972.5", + "@aws-sdk/middleware-recursion-detection": "^3.972.5", + "@aws-sdk/middleware-user-agent": "^3.972.14", + "@aws-sdk/region-config-resolver": "^3.972.5", + "@aws-sdk/types": "^3.973.3", + "@aws-sdk/util-endpoints": "^3.996.2", + "@aws-sdk/util-user-agent-browser": "^3.972.5", + "@aws-sdk/util-user-agent-node": "^3.972.13", + "@smithy/config-resolver": "^4.4.9", + "@smithy/core": "^3.23.6", + "@smithy/fetch-http-handler": "^5.3.11", + "@smithy/hash-node": "^4.2.10", + "@smithy/invalid-dependency": "^4.2.10", + "@smithy/middleware-content-length": "^4.2.10", + "@smithy/middleware-endpoint": "^4.4.20", + "@smithy/middleware-retry": "^4.4.37", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/middleware-stack": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/node-http-handler": "^4.4.12", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-body-length-browser": "^4.2.1", + "@smithy/util-body-length-node": "^4.2.2", + "@smithy/util-defaults-mode-browser": "^4.3.36", + "@smithy/util-defaults-mode-node": "^4.2.39", + "@smithy/util-endpoints": "^3.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-retry": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -690,16 +640,16 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.969.0.tgz", - "integrity": "sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.5.tgz", + "integrity": "sha512-AOitrygDwfTNCLCW7L+GScDy1p49FZ6WutTUFWROouoPetfVNmpL4q8TWD3MhfY/ynhoGhleUQENrBH374EU8w==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/types": "3.969.0", - "@smithy/config-resolver": "^4.4.6", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.3", + "@smithy/config-resolver": "^4.4.9", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -707,18 +657,18 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.969.0.tgz", - "integrity": "sha512-ucs6QczPkvGinbGmhMlPCQnagGJ+xsM6itsSWlJzxo9YsP6jR75cBU8pRdaM7nEbtCDnrUHf8W9g3D2Hd9mgVA==", + "version": "3.998.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.998.0.tgz", + "integrity": "sha512-JFzi44tQnENZQ+1DYcHfoa/wTRKkccz0VsNMow0rvsxZtqUEkeV2pYFbir35mHTyUKju9995ay1MAGxLt1dpRA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/core": "3.969.0", - "@aws-sdk/nested-clients": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.14", + "@aws-sdk/nested-clients": "^3.996.2", + "@aws-sdk/types": "^3.973.3", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -726,13 +676,13 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.969.0.tgz", - "integrity": "sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ==", + "version": "3.973.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.3.tgz", + "integrity": "sha512-tma6D8/xHZHJEUqmr6ksZjZ0onyIUqKDQLyp50ttZJmS0IwFYzxBgp5CxFvpYAnah52V3UtgrqGA6E83gtT7NQ==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -740,16 +690,16 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.969.0.tgz", - "integrity": "sha512-H2x2UwYiA1pHg40jE+OCSc668W9GXRShTiCWy1UPKtZKREbQ63Mgd7NAj+bEMsZUSCdHywqmSsLqKM9IcqQ3Bg==", + "version": "3.996.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.2.tgz", + "integrity": "sha512-83E6T1CKi0/IozPzqRBKqduW0mS4UQdI3soBH6CG7UgupTADWunqEMOTuPWCs9XGjpJJ4ujj+yu7pn8svhp5yg==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/types": "3.969.0", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", + "@aws-sdk/types": "^3.973.3", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-endpoints": "^3.3.1", "tslib": "^2.6.2" }, "engines": { @@ -757,9 +707,9 @@ } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.2.tgz", - "integrity": "sha512-qKgO7wAYsXzhwCHhdbaKFyxd83Fgs8/1Ka+jjSPrv2Ll7mB55Wbwlo0kkfMLh993/yEc8aoDIAc1Fz9h4Spi4Q==", + "version": "3.965.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.4.tgz", + "integrity": "sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -770,29 +720,29 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.969.0.tgz", - "integrity": "sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.5.tgz", + "integrity": "sha512-2ja1WqtuBaEAMgVoHYuWx393DF6ULqdt3OozeO7BosqouYaoU47Adtp9vEF+GImSG/Q8A+dqfwDULTTdMkHGUQ==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/types": "3.969.0", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.3", + "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.969.0.tgz", - "integrity": "sha512-D11ZuXNXdUMv8XTthMx+LPzkYNQAeQ68FnCTGnFLgLpnR8hVTeZMBBKjQ77wYGzWDk/csHKdCy697gU1On5KjA==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.13.tgz", + "integrity": "sha512-PHErmuu+v6iAST48zcsB2cYwDKW45gk6qCp49t1p0NGZ4EaFPr/tA5jl0X/ekDwvWbuT0LTj++fjjdVQAbuh0Q==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@aws-sdk/middleware-user-agent": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/middleware-user-agent": "^3.972.14", + "@aws-sdk/types": "^3.973.3", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -808,14 +758,14 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.969.0.tgz", - "integrity": "sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.7.tgz", + "integrity": "sha512-9GF86s6mHuc1TYCbuKatMDWl2PyK3KIkpRaI7ul2/gYZPfaLzKZ+ISHhxzVb9KVeakf75tUQe6CXW2gugSCXNw==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0", - "fast-xml-parser": "5.2.5", + "@smithy/types": "^4.13.0", + "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" }, "engines": { @@ -823,9 +773,9 @@ } }, "node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz", + "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==", "funding": [ { "type": "github", @@ -835,7 +785,7 @@ "license": "MIT", "optional": true, "dependencies": { - "strnum": "^2.1.0" + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -996,9 +946,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1082,9 +1032,9 @@ "license": "BSD-3-Clause" }, "node_modules/@hapi/tlds": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.4.tgz", - "integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", "license": "BSD-3-Clause", "engines": { "node": ">=14.0.0" @@ -1262,9 +1212,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.4.tgz", - "integrity": "sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g==", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", + "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", "license": "MIT", "optional": true, "dependencies": { @@ -1700,13 +1650,13 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", - "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.10.tgz", + "integrity": "sha512-qocxM/X4XGATqQtUkbE9SPUB6wekBi+FyJOMbPj0AhvyvFGYEmOlz6VB22iMePCQsFmMIvFSeViDvA7mZJG47g==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1714,17 +1664,17 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", - "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.9.tgz", + "integrity": "sha512-ejQvXqlcU30h7liR9fXtj7PIAau1t/sFbJpgWPfiYDs7zd16jpH0IsSXKcba2jF6ChTXvIjACs27kNMc5xxE2Q==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.1", + "@smithy/util-endpoints": "^3.3.1", + "@smithy/util-middleware": "^4.2.10", "tslib": "^2.6.2" }, "engines": { @@ -1732,21 +1682,21 @@ } }, "node_modules/@smithy/core": { - "version": "3.20.5", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.5.tgz", - "integrity": "sha512-0Tz77Td8ynHaowXfOdrD0F1IH4tgWGUhwmLwmpFyTbr+U9WHXNNp9u/k2VjBXGnSe7BwjBERRpXsokGTXzNjhA==", + "version": "3.23.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.6.tgz", + "integrity": "sha512-4xE+0L2NrsFKpEVFlFELkIHQddBvMbQ41LRIP74dGCXnY1zQ9DgksrBcRBDJT+iOzGy4VEJIeU3hkUK5mn06kg==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/middleware-serde": "^4.2.9", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.10", - "@smithy/util-utf8": "^4.2.0", - "@smithy/uuid": "^1.1.0", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-body-length-browser": "^4.2.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-stream": "^4.5.15", + "@smithy/util-utf8": "^4.2.1", + "@smithy/uuid": "^1.1.1", "tslib": "^2.6.2" }, "engines": { @@ -1754,16 +1704,16 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", - "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.10.tgz", + "integrity": "sha512-3bsMLJJLTZGZqVGGeBVFfLzuRulVsGTj12BzRKODTHqUABpIr0jMN1vN3+u6r2OfyhAQ2pXaMZWX/swBK5I6PQ==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", "tslib": "^2.6.2" }, "engines": { @@ -1771,16 +1721,16 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", - "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.11.tgz", + "integrity": "sha512-wbTRjOxdFuyEg0CpumjZO0hkUl+fetJFqxNROepuLIoijQh51aMBmzFLfoQdwRjxsuuS2jizzIUTjPWgd8pd7g==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/querystring-builder": "^4.2.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", + "@smithy/protocol-http": "^5.3.10", + "@smithy/querystring-builder": "^4.2.10", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", "tslib": "^2.6.2" }, "engines": { @@ -1788,15 +1738,15 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", - "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.10.tgz", + "integrity": "sha512-1VzIOI5CcsvMDvP3iv1vG/RfLJVVVc67dCRyLSB2Hn9SWCZrDO3zvcIzj3BfEtqRW5kcMg5KAeVf1K3dR6nD3w==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-buffer-from": "^4.2.1", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -1804,13 +1754,13 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", - "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.10.tgz", + "integrity": "sha512-vy9KPNSFUU0ajFYk0sDZIYiUlAWGEAhRfehIr5ZkdFrRFTAuXEPUd41USuqHU6vvLX4r6Q9X7MKBco5+Il0Org==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1818,9 +1768,9 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.1.tgz", + "integrity": "sha512-Yfu664Qbf1B4IYIsYgKoABt010daZjkaCRvdU/sPnZG6TtHOB0md0RjNdLGzxe5UIdn9js4ftPICzmkRa9RJ4Q==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -1831,14 +1781,14 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", - "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.10.tgz", + "integrity": "sha512-TQZ9kX5c6XbjhaEBpvhSvMEZ0klBs1CFtOdPFwATZSbC9UeQfKHPLPN9Y+I6wZGMOavlYTOlHEPDrt42PMSH9w==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1846,19 +1796,19 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.6.tgz", - "integrity": "sha512-dpq3bHqbEOBqGBjRVHVFP3eUSPpX0BYtg1D5d5Irgk6orGGAuZfY22rC4sErhg+ZfY/Y0kPqm1XpAmDZg7DeuA==", + "version": "4.4.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.20.tgz", + "integrity": "sha512-9W6Np4ceBP3XCYAGLoMCmn8t2RRVzuD1ndWPLBbv7H9CrwM9Bprf6Up6BM9ZA/3alodg0b7Kf6ftBK9R1N04vw==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/core": "^3.20.5", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-middleware": "^4.2.8", + "@smithy/core": "^3.23.6", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-middleware": "^4.2.10", "tslib": "^2.6.2" }, "engines": { @@ -1866,20 +1816,20 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.22", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.22.tgz", - "integrity": "sha512-vwWDMaObSMjw6WCC/3Ae9G7uul5Sk95jr07CDk1gkIMpaDic0phPS1MpVAZ6+YkF7PAzRlpsDjxPwRlh/S11FQ==", + "version": "4.4.37", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.37.tgz", + "integrity": "sha512-/1psZZllBBSQ7+qo5+hhLz7AEPGLx3Z0+e3ramMBEuPK2PfvLK4SrncDB9VegX5mBn+oP/UTDrM6IHrFjvX1ZA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/service-error-classification": "^4.2.8", - "@smithy/smithy-client": "^4.10.7", - "@smithy/types": "^4.12.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/uuid": "^1.1.0", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/service-error-classification": "^4.2.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-retry": "^4.2.10", + "@smithy/uuid": "^1.1.1", "tslib": "^2.6.2" }, "engines": { @@ -1887,14 +1837,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", - "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.11.tgz", + "integrity": "sha512-STQdONGPwbbC7cusL60s7vOa6He6A9w2jWhoapL0mgVjmR19pr26slV+yoSP76SIssMTX/95e5nOZ6UQv6jolg==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1902,13 +1852,13 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", - "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.10.tgz", + "integrity": "sha512-pmts/WovNcE/tlyHa8z/groPeOtqtEpp61q3W0nW1nDJuMq/x+hWa/OVQBtgU0tBqupeXq0VBOLA4UZwE8I0YA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1916,15 +1866,15 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", - "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.10.tgz", + "integrity": "sha512-UALRbJtVX34AdP2VECKVlnNgidLHA2A7YgcJzwSBg1hzmnO/bZBHl/LDQQyYifzUwp1UOODnl9JJ3KNawpUJ9w==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1932,16 +1882,16 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz", - "integrity": "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==", + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.12.tgz", + "integrity": "sha512-zo1+WKJkR9x7ZtMeMDAAsq2PufwiLDmkhcjpWPRRkmeIuOm6nq1qjFICSZbnjBvD09ei8KMo26BWxsu2BUU+5w==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/abort-controller": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/querystring-builder": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/abort-controller": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/querystring-builder": "^4.2.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1949,13 +1899,13 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", - "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.10.tgz", + "integrity": "sha512-5jm60P0CU7tom0eNrZ7YrkgBaoLFXzmqB0wVS+4uK8PPGmosSrLNf6rRd50UBvukztawZ7zyA8TxlrKpF5z9jw==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1963,13 +1913,13 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", - "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.10.tgz", + "integrity": "sha512-2NzVWpYY0tRdfeCJLsgrR89KE3NTWT2wGulhNUxYlRmtRmPwLQwKzhrfVaiNlA9ZpJvbW7cjTVChYKgnkqXj1A==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1977,14 +1927,14 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", - "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.10.tgz", + "integrity": "sha512-HeN7kEvuzO2DmAzLukE9UryiUvejD3tMp9a1D1NJETerIfKobBUCLfviP6QEk500166eD2IATaXM59qgUI+YDA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-uri-escape": "^4.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-uri-escape": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -1992,13 +1942,13 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", - "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.10.tgz", + "integrity": "sha512-4Mh18J26+ao1oX5wXJfWlTT+Q1OpDR8ssiC9PDOuEgVBGloqg18Fw7h5Ct8DyT9NBYwJgtJ2nLjKKFU6RP1G1Q==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -2006,26 +1956,26 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", - "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.10.tgz", + "integrity": "sha512-0R/+/Il5y8nB/By90o8hy/bWVYptbIfvoTYad0igYQO5RefhNCDmNzqxaMx7K1t/QWo0d6UynqpqN5cCQt1MCg==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0" + "@smithy/types": "^4.13.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", - "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.5.tgz", + "integrity": "sha512-pHgASxl50rrtOztgQCPmOXFjRW+mCd7ALr/3uXNzRrRoGV5G2+78GOsQ3HlQuBVHCh9o6xqMNvlIKZjWn4Euug==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -2033,19 +1983,19 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", - "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.10.tgz", + "integrity": "sha512-Wab3wW8468WqTKIxI+aZe3JYO52/RYT/8sDOdzkUhjnLakLe9qoQqIcfih/qxcF4qWEFoWBszY0mj5uxffaVXA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-uri-escape": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.1", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-uri-escape": "^4.2.1", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -2053,18 +2003,18 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.10.7", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.7.tgz", - "integrity": "sha512-Uznt0I9z3os3Z+8pbXrOSCTXCA6vrjyN7Ub+8l2pRDum44vLv8qw0qGVkJN0/tZBZotaEFHrDPKUoPNueTr5Vg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.0.tgz", + "integrity": "sha512-R8bQ9K3lCcXyZmBnQqUZJF4ChZmtWT5NLi6x5kgWx5D+/j0KorXcA0YcFg/X5TOgnTCy1tbKc6z2g2y4amFupQ==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/core": "^3.20.5", - "@smithy/middleware-endpoint": "^4.4.6", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.10", + "@smithy/core": "^3.23.6", + "@smithy/middleware-endpoint": "^4.4.20", + "@smithy/middleware-stack": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.15", "tslib": "^2.6.2" }, "engines": { @@ -2072,9 +2022,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", - "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", + "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -2085,14 +2035,14 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", - "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.10.tgz", + "integrity": "sha512-uypjF7fCDsRk26u3qHmFI/ePL7bxxB9vKkE+2WKEciHhz+4QtbzWiHRVNRJwU3cKhrYDYQE3b0MRFtqfLYdA4A==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/querystring-parser": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/querystring-parser": "^4.2.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -2100,14 +2050,14 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", - "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.1.tgz", + "integrity": "sha512-BKGuawX4Doq/bI/uEmg+Zyc36rJKWuin3py89PquXBIBqmbnJwBBsmKhdHfNEp0+A4TDgLmT/3MSKZ1SxHcR6w==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.1", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -2115,9 +2065,9 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", - "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.1.tgz", + "integrity": "sha512-SiJeLiozrAoCrgDBUgsVbmqHmMgg/2bA15AzcbcW+zan7SuyAVHN4xTSbq0GlebAIwlcaX32xacnrG488/J/6g==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -2128,9 +2078,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", - "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.2.tgz", + "integrity": "sha512-4rHqBvxtJEBvsZcFQSPQqXP2b/yy/YlB66KlcEgcH2WNoOKCKB03DSLzXmOsXjbl8dJ4OEYTn31knhdznwk7zw==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -2141,13 +2091,13 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.1.tgz", + "integrity": "sha512-/swhmt1qTiVkaejlmMPPDgZhEaWb/HWMGRBheaxwuVkusp/z+ErJyQxO6kaXumOciZSWlmq6Z5mNylCd33X7Ig==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -2155,9 +2105,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", - "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.1.tgz", + "integrity": "sha512-462id/00U8JWFw6qBuTSWfN5TxOHvDu4WliI97qOIOnuC/g+NDAknTU8eoGXEPlLkRVgWEr03jJBLV4o2FL8+A==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -2168,15 +2118,15 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.21", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.21.tgz", - "integrity": "sha512-DtmVJarzqtjghtGjCw/PFJolcJkP7GkZgy+hWTAN3YLXNH+IC82uMoMhFoC3ZtIz5mOgCm5+hOGi1wfhVYgrxw==", + "version": "4.3.36", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.36.tgz", + "integrity": "sha512-R0smq7EHQXRVMxkAxtH5akJ/FvgAmNF6bUy/GwY/N20T4GrwjT633NFm0VuRpC+8Bbv8R9A0DoJ9OiZL/M3xew==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.10.7", - "@smithy/types": "^4.12.0", + "@smithy/property-provider": "^4.2.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -2184,18 +2134,18 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.24", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.24.tgz", - "integrity": "sha512-JelBDKPAVswVY666rezBvY6b0nF/v9TXjUbNwDNAyme7qqKYEX687wJv0uze8lBIZVbg30wlWnlYfVSjjpKYFA==", + "version": "4.2.39", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.39.tgz", + "integrity": "sha512-otWuoDm35btJV1L8MyHrPl462B07QCdMTktKc7/yM+Psv6KbED/ziXiHnmr7yPHUjfIwE9S8Max0LO24Mo3ZVg==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/config-resolver": "^4.4.6", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.10.7", - "@smithy/types": "^4.12.0", + "@smithy/config-resolver": "^4.4.9", + "@smithy/credential-provider-imds": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -2203,14 +2153,14 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", - "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.1.tgz", + "integrity": "sha512-xyctc4klmjmieQiF9I1wssBWleRV0RhJ2DpO8+8yzi2LO1Z+4IWOZNGZGNj4+hq9kdo+nyfrRLmQTzc16Op2Vg==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -2218,9 +2168,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", - "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.1.tgz", + "integrity": "sha512-c1hHtkgAWmE35/50gmdKajgGAKV3ePJ7t6UtEmpfCWJmQE9BQAQPz0URUVI89eSkcDqCtzqllxzG28IQoZPvwA==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -2231,13 +2181,13 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", - "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.10.tgz", + "integrity": "sha512-LxaQIWLp4y0r72eA8mwPNQ9va4h5KeLM0I3M/HV9klmFaY2kN766wf5vsTzmaOpNNb7GgXAd9a25P3h8T49PSA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -2245,14 +2195,14 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", - "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.10.tgz", + "integrity": "sha512-HrBzistfpyE5uqTwiyLsFHscgnwB0kgv8vySp7q5kZ0Eltn/tjosaSGGDj/jJ9ys7pWzIP/icE2d+7vMKXLv7A==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/service-error-classification": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/service-error-classification": "^4.2.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -2260,19 +2210,19 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.10", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.10.tgz", - "integrity": "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==", + "version": "4.5.15", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.15.tgz", + "integrity": "sha512-OlOKnaqnkU9X+6wEkd7mN+WB7orPbCVDauXOj22Q7VtiTkvy7ZdSsOg4QiNAZMgI4OkvNf+/VLUC3VXkxuWJZw==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/fetch-http-handler": "^5.3.11", + "@smithy/node-http-handler": "^4.4.12", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-buffer-from": "^4.2.1", + "@smithy/util-hex-encoding": "^4.2.1", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -2280,9 +2230,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", - "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.1.tgz", + "integrity": "sha512-YmiUDn2eo2IOiWYYvGQkgX5ZkBSiTQu4FlDo5jNPpAxng2t6Sjb6WutnZV9l6VR4eJul1ABmCrnWBC9hKHQa6Q==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -2293,13 +2243,13 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.1.tgz", + "integrity": "sha512-DSIwNaWtmzrNQHv8g7DBGR9mulSit65KSj5ymGEIAknmIN8IpbZefEep10LaMG/P/xquwbmJ1h9ectz8z6mV6g==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -2307,9 +2257,9 @@ } }, "node_modules/@smithy/uuid": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", - "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.1.tgz", + "integrity": "sha512-dSfDCeihDmZlV2oyr0yWPTUfh07suS+R5OB+FZGiv/hHyK3hrFBW5rR1UYjfa57vBsrP9lciFkRPzebaV1Qujw==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -2339,12 +2289,12 @@ } }, "node_modules/@types/node": { - "version": "25.0.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz", - "integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==", + "version": "25.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.1.tgz", + "integrity": "sha512-hj9YIJimBCipHVfHKRMnvmHg+wfhKc0o4mTtXh9pKBjC8TLJzz0nzGmLi5UJsYAUgSvXFHgb0V2oY10DUFtImw==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/root__asn1": { @@ -2731,9 +2681,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "peer": true, @@ -3084,9 +3034,9 @@ } }, "node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/boolbase": { @@ -3106,9 +3056,9 @@ } }, "node_modules/bowser": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", - "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT", "optional": true }, @@ -3726,9 +3676,9 @@ } }, "node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", "engines": { @@ -4834,9 +4784,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -5234,9 +5184,9 @@ } }, "node_modules/figlet": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.9.4.tgz", - "integrity": "sha512-uN6QE+TrzTAHC1IWTyrc4FfGo2KH/82J8Jl1tyKB7+z5DBit/m3D++Iu5lg91qJMnQQ3vpJrj5gxcK/pk4R9tQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.10.0.tgz", + "integrity": "sha512-aktIwEZZ6Gp9AWdMXW4YCi0J2Ahuxo67fNJRUIWD81w8pQ0t9TS8FFpbl27ChlTLF06VkwjDesZSzEVzN75rzA==", "dev": true, "license": "MIT", "dependencies": { @@ -5620,9 +5570,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", "license": "MIT", "engines": { "node": ">=18" @@ -5726,7 +5676,7 @@ "version": "8.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -5765,9 +5715,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -6273,7 +6223,7 @@ "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -6436,9 +6386,9 @@ } }, "node_modules/har-validator/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -6885,9 +6835,9 @@ "license": "MIT" }, "node_modules/ioredfour": { - "version": "1.3.0-ioredis-07", - "resolved": "https://registry.npmjs.org/ioredfour/-/ioredfour-1.3.0-ioredis-07.tgz", - "integrity": "sha512-0dGQF+AFfFyt8AQgNd03QRO8zMdY3Y8VhXaCroA5ZRApW70TJxNB+LcVkH0zL4bBzocR5iqVRUN/TB2kfSh0rw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ioredfour/-/ioredfour-1.4.0.tgz", + "integrity": "sha512-H/4UgMANW6qO3Lfo/fg/EIB0Zznrr/h11xL1G80RpgN4ym7L07P6rTjkvcDn9L0FyuaT2q2rIIJ1cDZw7ytiMw==", "license": "MIT", "dependencies": { "ioredis": "5.3.2" @@ -7595,9 +7545,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.clone": { @@ -7856,6 +7806,18 @@ "node": ">=6.0.0" } }, + "node_modules/mailauth/node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, "node_modules/mailauth/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -8112,9 +8074,9 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -8131,11 +8093,11 @@ "license": "MIT" }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -8207,14 +8169,27 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/mocha/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/mocha/node_modules/chokidar": { @@ -8285,6 +8260,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -8313,13 +8289,13 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.8.tgz", + "integrity": "sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -8960,9 +8936,9 @@ } }, "node_modules/nan": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", - "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", "license": "MIT", "optional": true }, @@ -10165,9 +10141,9 @@ } }, "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.6" @@ -10649,9 +10625,9 @@ "license": "MIT" }, "node_modules/restify/node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -10760,7 +10736,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -11293,9 +11269,9 @@ } }, "node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -11622,9 +11598,9 @@ } }, "node_modules/superagent/node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11744,21 +11720,21 @@ } }, "node_modules/tldts": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", - "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", + "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", "license": "MIT", "dependencies": { - "tldts-core": "^7.0.19" + "tldts-core": "^7.0.23" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", - "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", + "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", "license": "MIT" }, "node_modules/to-readable-stream": { @@ -11950,9 +11926,9 @@ } }, "node_modules/underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "license": "MIT" }, "node_modules/underscore.string": { @@ -11979,9 +11955,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/unique-string": { diff --git a/package.json b/package.json index fda06856..d523ccff 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "http-parser-js": "0.5.10", "humanname": "0.2.2", "iconv-lite": "0.7.2", - "ioredfour": "1.3.0-ioredis-07", + "ioredfour": "1.4.0", "ioredis": "5.9.1", "ipaddr.js": "2.3.0", "isemail": "3.2.0", From 34b55809fc8f8e5b4c1d75e5edb742139ff397cf Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 26 Feb 2026 10:02:07 +0200 Subject: [PATCH 16/32] on cluster tests set log level error --- config/test-cluster.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/test-cluster.toml b/config/test-cluster.toml index acb1cdbd..9c36cc8a 100644 --- a/config/test-cluster.toml +++ b/config/test-cluster.toml @@ -4,7 +4,7 @@ host = "127.0.0.1" secure = false [log] -level = "silly" +level = "error" [dbs] # mongodb connection string for the main database From 19149a63b4ea1dba567207e79418089492b97046 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 26 Feb 2026 10:27:15 +0200 Subject: [PATCH 17/32] revert test-cluster.toml change, increase protocol tests timeout --- config/test-cluster.toml | 2 +- imap-core/test/protocol-test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/test-cluster.toml b/config/test-cluster.toml index 9c36cc8a..acb1cdbd 100644 --- a/config/test-cluster.toml +++ b/config/test-cluster.toml @@ -4,7 +4,7 @@ host = "127.0.0.1" secure = false [log] -level = "error" +level = "silly" [dbs] # mongodb connection string for the main database diff --git a/imap-core/test/protocol-test.js b/imap-core/test/protocol-test.js index b239cc8e..1086d542 100644 --- a/imap-core/test/protocol-test.js +++ b/imap-core/test/protocol-test.js @@ -16,7 +16,7 @@ chai.config.includeStack = true; const { MAX_SUB_MAILBOXES, MAX_MAILBOX_NAME_LENGTH } = require('../../lib/consts.js'); describe('IMAP Protocol integration tests', function () { - this.timeout(100000); // eslint-disable-line no-invalid-this + this.timeout(200000); // eslint-disable-line no-invalid-this let port = 9993; beforeEach(function (done) { From 2dd7b2a5944ffe8e6cc1cd8195371510d333e9ac Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 26 Feb 2026 11:03:56 +0200 Subject: [PATCH 18/32] revert protocol test timeout, imapnotifier use correct message event for cluster --- imap-core/test/protocol-test.js | 2 +- lib/imap-notifier.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/imap-core/test/protocol-test.js b/imap-core/test/protocol-test.js index 1086d542..b239cc8e 100644 --- a/imap-core/test/protocol-test.js +++ b/imap-core/test/protocol-test.js @@ -16,7 +16,7 @@ chai.config.includeStack = true; const { MAX_SUB_MAILBOXES, MAX_MAILBOX_NAME_LENGTH } = require('../../lib/consts.js'); describe('IMAP Protocol integration tests', function () { - this.timeout(200000); // eslint-disable-line no-invalid-this + this.timeout(100000); // eslint-disable-line no-invalid-this let port = 9993; beforeEach(function (done) { diff --git a/lib/imap-notifier.js b/lib/imap-notifier.js index e0b8164d..fc629eb2 100644 --- a/lib/imap-notifier.js +++ b/lib/imap-notifier.js @@ -21,6 +21,7 @@ class ImapNotifier extends EventEmitter { this.subscribeFnName = this.redis.isCluster ? 'ssubscribe' : 'subscribe'; this.publishFnName = this.redis.isCluster ? 'spublish' : 'publish'; + this.messageEventName = this.redis.isCluster ? 'smessage' : 'message'; this.logger = options.logger || { info: log.silly.bind(log, 'IMAP'), @@ -82,7 +83,7 @@ class ImapNotifier extends EventEmitter { } }; - this.subscriber.on('message', (channel, message) => { + this.subscriber.on(this.messageEventName, (channel, message) => { if (channel === 'wd_events') { let data; // if e present at beginning, check if p also is present From 50fc6f5afd50969c36f9b48183b1f111022c6ee1 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 26 Feb 2026 11:10:15 +0200 Subject: [PATCH 19/32] add api-methods endpoint on cluster tests too --- api.js | 2 +- bin/access-tokens | 26 ++++++++++--- lib/api/users.js | 94 ++++++++++++++++++++++++++++++++--------------- 3 files changed, 86 insertions(+), 36 deletions(-) diff --git a/api.js b/api.js index 46291b30..c00aa2c1 100644 --- a/api.js +++ b/api.js @@ -588,7 +588,7 @@ module.exports = done => { settingsRoutes(db, server, settingsHandler); healthRoutes(db, server, loggelf); - if (process.env.NODE_ENV === 'test') { + if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'test-cluster') { server.get( { name: 'api-methods', path: '/api-methods' }, tools.responseWrapper(async (req, res) => { diff --git a/bin/access-tokens b/bin/access-tokens index 17f2e441..e883a65b 100755 --- a/bin/access-tokens +++ b/bin/access-tokens @@ -60,8 +60,15 @@ let argv = yargs .digest('hex') }; - await db.redis.hmset(key, tokenData); - await db.redis.sadd('tn:user:' + argv.user, tokenHash); + if (db.redis.isCluster) { + await Promise.all([db.redis.hmset(key, tokenData), db.redis.sadd('tn:user:' + argv.user, tokenHash)]); + } else { + await db.redis + .multi() + .hmset(key, tokenData) + .sadd('tn:user:' + argv.user, tokenHash) + .exec(); + } console.error('Generated access token for %s[%s]:', tokenData.user, tokenData.role); @@ -123,10 +130,19 @@ let argv = yargs process.exit(); } - await db.redis.del('tn:user:' + user); + if (db.redis.isCluster) { + await db.redis.del('tn:user:' + user); + + for (let tokenHash of tokens) { + await db.redis.del('tn:token:' + tokenHash); + } + } else { + let query = db.redis.multi().del('tn:user:' + user); + tokens.forEach(tokenHash => { + query = query.del('tn:token:' + tokenHash); + }); - for (let tokenHash of tokens) { - await db.redis.del('tn:token:' + tokenHash); + await query.exec(); } console.error('Deleted %s tokens for %s', tokens.length, user); diff --git a/lib/api/users.js b/lib/api/users.js index 6e8e720e..04d53166 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -968,36 +968,70 @@ module.exports = (db, server, userHandler, settingsHandler) => { let response; try { - let userId = userData._id.toString(); - let userRateKey = tools.redisClusterValue(userId, db.redis); - - let [ - recipientsResponse, - forwardsResponse, - receivedResponse, - imapUploadResponse, - imapDownloadResponse, - pop3DownloadResponse, - imapConnection - ] = await Promise.all([ - db.redis.multi().get(`wdr:${userId}`).ttl(`wdr:${userId}`).exec(), - db.redis.multi().get(`wdf:${userId}`).ttl(`wdf:${userId}`).exec(), - db.redis.multi().get(`rl:rcpt:${userId}`).ttl(`rl:rcpt:${userId}`).exec(), - db.redis.multi().get(`iup:${userRateKey}`).ttl(`iup:${userRateKey}`).exec(), - db.redis.multi().get(`idw:${userRateKey}`).ttl(`idw:${userRateKey}`).exec(), - db.redis.multi().get(`pdw:${userRateKey}`).ttl(`pdw:${userRateKey}`).exec(), - db.redis.hget('lim:imap', userId) - ]); - - response = [ - ...(recipientsResponse || []), - ...(forwardsResponse || []), - ...(receivedResponse || []), - ...(imapUploadResponse || []), - ...(imapDownloadResponse || []), - ...(pop3DownloadResponse || []), - [null, imapConnection] - ]; + if (!db.redis.isCluster) { + response = await db.redis + .multi() + // sending counters are stored in Redis + + // sent messages + .get('wdr:' + userData._id.toString()) + .ttl('wdr:' + userData._id.toString()) + + // forwarded messages + .get('wdf:' + userData._id.toString()) + .ttl('wdf:' + userData._id.toString()) + + // rate limited recipient + .get('rl:rcpt:' + userData._id.toString()) + .ttl('rl:rcpt:' + userData._id.toString()) + + // rate limited imap uploads + .get('iup:' + userData._id.toString()) + .ttl('iup:' + userData._id.toString()) + + // rate limited imap downloads + .get('idw:' + userData._id.toString()) + .ttl('idw:' + userData._id.toString()) + + // rate limited pop3 downloads + .get('pdw:' + userData._id.toString()) + .ttl('pdw:' + userData._id.toString()) + + .hget('lim:imap', userData._id.toString()) + + .exec(); + } else { + let userId = userData._id.toString(); + let userRateKey = tools.redisClusterValue(userId, db.redis); + + let [ + recipientsResponse, + forwardsResponse, + receivedResponse, + imapUploadResponse, + imapDownloadResponse, + pop3DownloadResponse, + imapConnection + ] = await Promise.all([ + db.redis.multi().get(`wdr:${userId}`).ttl(`wdr:${userId}`).exec(), + db.redis.multi().get(`wdf:${userId}`).ttl(`wdf:${userId}`).exec(), + db.redis.multi().get(`rl:rcpt:${userId}`).ttl(`rl:rcpt:${userId}`).exec(), + db.redis.multi().get(`iup:${userRateKey}`).ttl(`iup:${userRateKey}`).exec(), + db.redis.multi().get(`idw:${userRateKey}`).ttl(`idw:${userRateKey}`).exec(), + db.redis.multi().get(`pdw:${userRateKey}`).ttl(`pdw:${userRateKey}`).exec(), + db.redis.hget('lim:imap', userId) + ]); + + response = [ + ...(recipientsResponse || [[]]), + ...(forwardsResponse || [[]]), + ...(receivedResponse || [[]]), + ...(imapUploadResponse || [[]]), + ...(imapDownloadResponse || [[]]), + ...(pop3DownloadResponse || [[]]), + [null, imapConnection] + ]; + } } catch (err) { // ignore errors.notify(err, { userId: user }); From 5b9202617aebcbe85ef4d35deaa703bab1130c7b Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 26 Feb 2026 11:15:01 +0200 Subject: [PATCH 20/32] keep redis multi calls for standalone redis --- lib/user-handler.js | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/user-handler.js b/lib/user-handler.js index d1a2c822..443e2986 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -3399,17 +3399,28 @@ class UserHandler { if (flushKeys.length || flushHKeys.length) { // just call the operations and hope for the best, no problems if fails try { - let flushreq = []; + if (this.redis.isCluster) { + let flushreq = []; - flushKeys.forEach(key => { - flushreq.push(this.redis.del(key)); - }); + flushKeys.forEach(key => { + flushreq.push(this.redis.del(key)); + }); - flushHKeys.forEach(entry => { - flushreq.push(this.redis.hdel(entry.key, entry.value)); - }); + flushHKeys.forEach(entry => { + flushreq.push(this.redis.hdel(entry.key, entry.value)); + }); - await Promise.all(flushreq); + await Promise.all(flushreq); + } else { + let flushreq = this.redis.multi(); + flushKeys.forEach(key => { + flushreq = flushreq.del(key); + }); + flushHKeys.forEach(entry => { + flushreq = flushreq.hdel(entry.key, entry.value); + }); + await flushreq.exec(); + } } catch (err) { // ignore } From 6da02a55243ad9b8e1adcc62469a58686e670f69 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Mon, 2 Mar 2026 15:20:58 +0200 Subject: [PATCH 21/32] hardcode new hash tagged key --- indexer.js | 3 +- lib/api/addresses.js | 8 ++-- lib/api/messages.js | 6 +-- lib/api/submit.js | 4 +- lib/api/users.js | 79 ++++++++++++--------------------------- lib/filter-handler.js | 6 +-- lib/handlers/on-append.js | 2 +- lib/handlers/on-fetch.js | 2 +- lib/imap-notifier.js | 6 +-- lib/tasks/clear-folder.js | 3 +- lib/tools.js | 13 +------ lib/user-handler.js | 6 +-- pop3.js | 4 +- 13 files changed, 47 insertions(+), 95 deletions(-) diff --git a/indexer.js b/indexer.js index 2d385c32..1481369d 100644 --- a/indexer.js +++ b/indexer.js @@ -13,7 +13,6 @@ const { ObjectId } = require('mongodb'); const libmime = require('libmime'); const punycode = require('punycode.js'); const { getClient } = require('./lib/elasticsearch'); -const tools = require('./lib/tools'); let loggelf; let processlock; @@ -284,7 +283,7 @@ function indexingJob(esclient) { const dateKeyTdy = new Date().toISOString().substring(0, 10).replace(/-/g, ''); const dateKeyYdy = new Date(Date.now() - 24 * 3600 * 1000).toISOString().substring(0, 10).replace(/-/g, ''); - const tombstoneTag = tools.redisClusterValue('indexer:tomb', db.redis); + const tombstoneTag = '{indexer:tomb}'; const tombstoneTdy = `${tombstoneTag}:${dateKeyTdy}`; const tombstoneYdy = `${tombstoneTag}:${dateKeyYdy}`; diff --git a/lib/api/addresses.js b/lib/api/addresses.js index 7dc09667..6b615df2 100644 --- a/lib/api/addresses.js +++ b/lib/api/addresses.js @@ -2400,8 +2400,8 @@ module.exports = (db, server, userHandler, settingsHandler) => { response = await db.redis .multi() // sending counters are stored in Redis - .get('wdf:' + addressData._id.toString()) - .ttl('wdf:' + addressData._id.toString()) + .get(`wdf:{${addressData._id.toString()}}`) + .ttl(`wdf:{${addressData._id.toString()}}`) .exec(); } catch (err) { // ignore @@ -2568,8 +2568,8 @@ module.exports = (db, server, userHandler, settingsHandler) => { response = await db.redis .multi() // sending counters are stored in Redis - .get('wdf:' + addressData._id.toString()) - .ttl('wdf:' + addressData._id.toString()) + .get(`wdf:{${addressData._id.toString()}}`) + .ttl(`wdf:{${addressData._id.toString()}}`) .exec(); } catch (err) { // ignore diff --git a/lib/api/messages.js b/lib/api/messages.js index af30a0de..87a6f02a 100644 --- a/lib/api/messages.js +++ b/lib/api/messages.js @@ -2199,7 +2199,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti } try { - const mailboxKey = tools.redisClusterValue(mailbox, db.redis); + const mailboxKey = `{${mailbox}}`; // clear counters await db.redis.multi().del(`total:${mailboxKey}`).del(`unseen:${mailboxKey}`).exec(); } catch (err) { @@ -3111,7 +3111,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti let limitCheck; try { - limitCheck = await messageHandler.counters.asyncTTLCounter('wdr:' + userData._id.toString(), 0, maxRecipients, false); + limitCheck = await messageHandler.counters.asyncTTLCounter(`wdr:{${userData._id.toString()}}`, 0, maxRecipients, false); } catch (err) { res.status(500); return res.json({ @@ -4162,7 +4162,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti } // Update counters only after message has been succesfully sent and not rejected - messageHandler.counters.ttlcounter('wdr:' + userData._id.toString(), envelope.to.length, maxRecipients, false, err => { + messageHandler.counters.ttlcounter(`wdr:{${userData._id.toString()}}`, envelope.to.length, maxRecipients, false, err => { if (err) { err.responseCode = 500; err.code = 'InternalDatabaseError'; diff --git a/lib/api/submit.js b/lib/api/submit.js index d319db33..0444a85e 100644 --- a/lib/api/submit.js +++ b/lib/api/submit.js @@ -426,7 +426,7 @@ module.exports = (db, server, messageHandler, userHandler, settingsHandler) => { return; } - messageHandler.counters.ttlcounter('wdr:' + userData._id.toString(), 0, maxRecipients, false, (err, result) => { + messageHandler.counters.ttlcounter(`wdr:{${userData._id.toString()}}`, 0, maxRecipients, false, (err, result) => { if (err) { err.responseCode = 500; err.code = 'InternalDatabaseError'; @@ -488,7 +488,7 @@ module.exports = (db, server, messageHandler, userHandler, settingsHandler) => { } messageHandler.counters.ttlcounter( - 'wdr:' + userData._id.toString(), + `wdr:{${userData._id.toString()}}`, compiledEnvelope.to.length, maxRecipients, false, diff --git a/lib/api/users.js b/lib/api/users.js index 04d53166..fb0d4e14 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -968,70 +968,37 @@ module.exports = (db, server, userHandler, settingsHandler) => { let response; try { - if (!db.redis.isCluster) { - response = await db.redis - .multi() - // sending counters are stored in Redis + response = await db.redis + .multi() + // sending counters are stored in Redis - // sent messages - .get('wdr:' + userData._id.toString()) - .ttl('wdr:' + userData._id.toString()) + // sent messages + .get(`wdr:{${userData._id.toString()}}`) + .ttl(`wdr:{${userData._id.toString()}}`) - // forwarded messages - .get('wdf:' + userData._id.toString()) - .ttl('wdf:' + userData._id.toString()) + // forwarded messages + .get(`wdf:{${userData._id.toString()}}`) + .ttl(`wdf:{${userData._id.toString()}}`) - // rate limited recipient - .get('rl:rcpt:' + userData._id.toString()) - .ttl('rl:rcpt:' + userData._id.toString()) + // rate limited recipient + .get(`rl:rcpt:{${userData._id.toString()}}`) + .ttl(`rl:rcpt:{${userData._id.toString()}}`) - // rate limited imap uploads - .get('iup:' + userData._id.toString()) - .ttl('iup:' + userData._id.toString()) + // rate limited imap uploads + .get(`iup:{${userData._id.toString()}}`) + .ttl(`iup:{${userData._id.toString()}}`) - // rate limited imap downloads - .get('idw:' + userData._id.toString()) - .ttl('idw:' + userData._id.toString()) + // rate limited imap downloads + .get(`idw:{${userData._id.toString()}}`) + .ttl(`idw:{${userData._id.toString()}}`) - // rate limited pop3 downloads - .get('pdw:' + userData._id.toString()) - .ttl('pdw:' + userData._id.toString()) + // rate limited pop3 downloads + .get(`pdw:{${userData._id.toString()}}`) + .ttl(`pdw:{${userData._id.toString()}}`) - .hget('lim:imap', userData._id.toString()) + .hget(`lim:imap:{${userData._id.toString()}}`, userData._id.toString()) - .exec(); - } else { - let userId = userData._id.toString(); - let userRateKey = tools.redisClusterValue(userId, db.redis); - - let [ - recipientsResponse, - forwardsResponse, - receivedResponse, - imapUploadResponse, - imapDownloadResponse, - pop3DownloadResponse, - imapConnection - ] = await Promise.all([ - db.redis.multi().get(`wdr:${userId}`).ttl(`wdr:${userId}`).exec(), - db.redis.multi().get(`wdf:${userId}`).ttl(`wdf:${userId}`).exec(), - db.redis.multi().get(`rl:rcpt:${userId}`).ttl(`rl:rcpt:${userId}`).exec(), - db.redis.multi().get(`iup:${userRateKey}`).ttl(`iup:${userRateKey}`).exec(), - db.redis.multi().get(`idw:${userRateKey}`).ttl(`idw:${userRateKey}`).exec(), - db.redis.multi().get(`pdw:${userRateKey}`).ttl(`pdw:${userRateKey}`).exec(), - db.redis.hget('lim:imap', userId) - ]); - - response = [ - ...(recipientsResponse || [[]]), - ...(forwardsResponse || [[]]), - ...(receivedResponse || [[]]), - ...(imapUploadResponse || [[]]), - ...(imapDownloadResponse || [[]]), - ...(pop3DownloadResponse || [[]]), - [null, imapConnection] - ]; - } + .exec(); } catch (err) { // ignore errors.notify(err, { userId: user }); diff --git a/lib/filter-handler.js b/lib/filter-handler.js index 602747f3..215f626e 100644 --- a/lib/filter-handler.js +++ b/lib/filter-handler.js @@ -414,13 +414,13 @@ class FilterHandler { // check limiting counters try { let counterResult = await this.ttlcounter( - 'wdf:' + userData._id.toString(), + `wdf:{${userData._id.toString()}}`, forwardTargets.size, userData.forwards || consts.MAX_FORWARDS, false ); if (!counterResult.success) { - log.silly('Filter', 'FRWRDFAIL key=%s error=%s', 'wdf:' + userData._id.toString(), 'Precondition failed'); + log.silly('Filter', 'FRWRDFAIL key=%s error=%s', `wdf:{${userData._id.toString()}}`, 'Precondition failed'); logdata.short_message = '[FRWRDFAIL] Skipped forwarding due to rate limiting'; logdata._error = 'Skipped forwarding due to rate limiting'; @@ -431,7 +431,7 @@ class FilterHandler { } } catch (err) { // failed checks, ignore - log.info('Filter', 'FRWRDFAIL key=%s error=%s', 'wdf:' + userData._id.toString(), err.message); + log.info('Filter', 'FRWRDFAIL key=%s error=%s', `wdf:{${userData._id.toString()}}`, err.message); logdata.short_message = '[FRWRDFAIL] Skipped forwarding due to database error'; logdata._error = err.message; diff --git a/lib/handlers/on-append.js b/lib/handlers/on-append.js index 862c60eb..ff23c666 100644 --- a/lib/handlers/on-append.js +++ b/lib/handlers/on-append.js @@ -45,7 +45,7 @@ module.exports = (server, messageHandler, userCache) => (path, flags, date, raw, return callback(err); } - const counterKey = 'iup:' + tools.redisClusterValue(session.user.id, db.redis); + const counterKey = `iup:{${session.user.id.toString()}}`; messageHandler.counters.ttlcounter(counterKey, 0, limit, false, (err, res) => { if (err) { diff --git a/lib/handlers/on-fetch.js b/lib/handlers/on-fetch.js index 6ddb1f36..6cd25f77 100644 --- a/lib/handlers/on-fetch.js +++ b/lib/handlers/on-fetch.js @@ -45,7 +45,7 @@ module.exports = (server, messageHandler, userCache) => (mailbox, options, sessi return callback(err); } - const counterKey = 'idw:' + tools.redisClusterValue(session.user.id, db.redis); + const counterKey = `idw:{${session.user.id.toString()}}`; messageHandler.counters.ttlcounter(counterKey, 0, limit, false, (err, res) => { if (err) { diff --git a/lib/imap-notifier.js b/lib/imap-notifier.js index fc629eb2..a641fec7 100644 --- a/lib/imap-notifier.js +++ b/lib/imap-notifier.js @@ -378,7 +378,7 @@ class ImapNotifier extends EventEmitter { let mailbox = row[0]; let delta = row[1]; - let mailboxKey = tools.redisClusterValue(mailbox, this.redis); + let mailboxKey = `{${mailbox}}`; await this.redis.cachedcounter(`total:${mailboxKey}`, delta.total, consts.MAILBOX_COUNTER_TTL); @@ -397,7 +397,7 @@ class ImapNotifier extends EventEmitter { return callback(null, true); } - let rlkey = 'lim:' + data.service; + let rlkey = `lim:${data.service}:{${data.user.toString()}}`; const { closed: socketClosed, connecting, destroyed } = data.session?.socket || {}; if (!socketClosed && !connecting && !destroyed) { @@ -432,7 +432,7 @@ class ImapNotifier extends EventEmitter { let entry = this.connectionSessions.get(data.session); this.connectionSessions.delete(data.session); - let rlkey = 'lim:' + entry.service; + let rlkey = `lim:${entry.service}:{${entry.user.toString()}}`; this.counters.limitedcounter(rlkey, entry.user, -1, 0, err => { if (err) { this.logger.debug('[%s] Failed to release connection for user %s. %s', data.session.id, entry.user, err.message); diff --git a/lib/tasks/clear-folder.js b/lib/tasks/clear-folder.js index a0842c32..d3aa2427 100644 --- a/lib/tasks/clear-folder.js +++ b/lib/tasks/clear-folder.js @@ -2,7 +2,6 @@ const log = require('npmlog'); const db = require('../db'); -const tools = require('../tools'); let run = async (task, data, options) => { const messageHandler = options.messageHandler; @@ -49,7 +48,7 @@ let run = async (task, data, options) => { await cursor.close(); try { - const mailboxKey = tools.redisClusterValue(mailbox, db.redis); + const mailboxKey = `{${mailbox}}`; // clear counters await db.redis.multi().del(`total:${mailboxKey}`).del(`unseen:${mailboxKey}`).exec(); } catch (err) { diff --git a/lib/tools.js b/lib/tools.js index 4cf73257..74a9a603 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -186,16 +186,6 @@ function redisConfig(defaultConfig) { }; } -function redisClusterValue(value, redis) { - value = value === null || typeof value === 'undefined' ? '' : value.toString(); - if (!value || !redis || !redis.isCluster) { - return value; - } - // strip existing braces to avoid nested hash tags - value = value.replace(/[{}]/g, ''); - return `{${value}}`; -} - function decodeAddresses(addresses) { addresses.forEach(address => { address.name = (address.name || '').toString(); @@ -231,7 +221,7 @@ function flatAddresses(addresses) { } async function getMailboxCounter(db, mailbox, type) { - const mailboxKey = redisClusterValue(mailbox, db.redis); + const mailboxKey = `{${mailbox}}`; const cacheKey = `${type || 'total'}:${mailboxKey}`; try { @@ -828,7 +818,6 @@ module.exports = { normalizeIp, getHostname, getWildcardAddresses, - redisClusterValue, redisConfig, checkRangeQuery, decodeAddresses, diff --git a/lib/user-handler.js b/lib/user-handler.js index 443e2986..1030c07f 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -3145,7 +3145,6 @@ class UserHandler { ['pop3MaxMessages', 'pxm'], ['receivedMax', 'rl:rcpt'] ]); - let slotKeys = new Set(['iup', 'idw', 'pdw', 'pxm']); let flushKeys = []; let flushHKeys = []; @@ -3156,11 +3155,10 @@ class UserHandler { if (resetKeys.has(key)) { let prefix = resetKeys.get(key); - let keySuffix = slotKeys.has(prefix) ? tools.redisClusterValue(user, this.redis) : user.toString(); - flushKeys.push(`${prefix}:${keySuffix}`); + flushKeys.push(`${prefix}:{${user.toString()}}`); } if (key === 'imapMaxConnections') { - flushHKeys.push({ key: 'lim:imap', value: user.toString() }); + flushHKeys.push({ key: `lim:imap:{${user.toString()}}`, value: user.toString() }); } if (key === 'suspended' && data.suspended) { diff --git a/pop3.js b/pop3.js index 9b288bd2..4d839093 100644 --- a/pop3.js +++ b/pop3.js @@ -131,7 +131,7 @@ const serverOptions = { } session.user.mailbox = mailbox._id; - let messageIndexKey = `pxm:${tools.redisClusterValue(session.user.id, db.redis)}`; + let messageIndexKey = `pxm:{${session.user.id.toString()}}`; db.redis.hget(messageIndexKey, mailbox._id.toString(), (err, messageIndex) => { let lastIndex = !err && messageIndex; @@ -225,7 +225,7 @@ const serverOptions = { return callback(err); } - let counterKey = 'pdw:' + tools.redisClusterValue(session.user.id, db.redis); + let counterKey = `pdw:{${session.user.id.toString()}}`; messageHandler.counters.ttlcounter(counterKey, 0, limit, false, (err, res) => { if (err) { From 72d38de43e9dcc6eafeeb728690c58851544b81c Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Mon, 2 Mar 2026 15:41:47 +0200 Subject: [PATCH 22/32] for mailbox counters use per user hash tags --- lib/api/mailboxes.js | 8 ++++---- lib/api/messages.js | 10 +++++----- lib/api/updates.js | 8 ++++---- lib/imap-notifier.js | 7 +++++-- lib/tasks/clear-folder.js | 2 +- lib/tools.js | 37 +++++++++++++++++++++++++++++-------- 6 files changed, 48 insertions(+), 24 deletions(-) diff --git a/lib/api/mailboxes.js b/lib/api/mailboxes.js index 899c90b8..ecd9378e 100644 --- a/lib/api/mailboxes.js +++ b/lib/api/mailboxes.js @@ -242,7 +242,7 @@ module.exports = (db, server, mailboxHandler) => { counterOps.push( (async () => { try { - total = await getMailboxCounter(db, mailboxData._id); + total = await getMailboxCounter(db, mailboxData._id, false, mailboxData.user); } catch (err) { // ignore } @@ -253,7 +253,7 @@ module.exports = (db, server, mailboxHandler) => { counterOps.push( (async () => { try { - unseen = await getMailboxCounter(db, mailboxData._id, 'unseen'); + unseen = await getMailboxCounter(db, mailboxData._id, 'unseen', mailboxData.user); } catch (err) { // ignore } @@ -518,13 +518,13 @@ module.exports = (db, server, mailboxHandler) => { let total, unseen; try { - total = await getMailboxCounter(db, mailboxData._id); + total = await getMailboxCounter(db, mailboxData._id, false, mailboxData.user); } catch (err) { // ignore } try { - unseen = await getMailboxCounter(db, mailboxData._id, 'unseen'); + unseen = await getMailboxCounter(db, mailboxData._id, 'unseen', mailboxData.user); } catch (err) { // ignore } diff --git a/lib/api/messages.js b/lib/api/messages.js index 87a6f02a..c48e131f 100644 --- a/lib/api/messages.js +++ b/lib/api/messages.js @@ -526,7 +526,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti filter.unseen = filterUnseen; } - let total = await getFilteredMessageCount(filter); + let total = await getFilteredMessageCount(filter, user); let opts = { limit, @@ -870,7 +870,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti query = prepared.query; } - let total = await getFilteredMessageCount(filter); + let total = await getFilteredMessageCount(filter, user); log.verbose('API', 'Searching %s', JSON.stringify(filter)); let opts = { @@ -2199,7 +2199,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti } try { - const mailboxKey = `{${mailbox}}`; + const mailboxKey = `{${user.toString()}}:${mailbox}`; // clear counters await db.redis.multi().del(`total:${mailboxKey}`).del(`unseen:${mailboxKey}`).exec(); } catch (err) { @@ -3884,10 +3884,10 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti }) ); - async function getFilteredMessageCount(filter) { + async function getFilteredMessageCount(filter, user) { if (Object.keys(filter).length === 1 && filter.mailbox) { // Try to use cached value to get the count - return await getMailboxCounter(db, filter.mailbox); + return await getMailboxCounter(db, filter.mailbox, false, user); } return await db.database.collection('messages').countDocuments(filter); diff --git a/lib/api/updates.js b/lib/api/updates.js index 4b2b166d..fdfabd4e 100644 --- a/lib/api/updates.js +++ b/lib/api/updates.js @@ -9,9 +9,9 @@ const base32 = require('base32.js'); const { sessSchema, sessIPSchema } = require('../schemas'); const { userId } = require('../schemas/request/general-schemas'); -const getMailboxCounterCb = (db, mailbox, type, callback) => { +const getMailboxCounterCb = (db, mailbox, type, user, callback) => { tools - .getMailboxCounter(db, mailbox, type) + .getMailboxCounter(db, mailbox, type, user) .then(sum => callback(null, sum)) .catch(err => callback(err)); }; @@ -276,11 +276,11 @@ function loadJournalStream(db, req, res, user, lastEventId, done) { }); } let mailbox = new ObjectId(mailboxes[mailboxPos++]); - getMailboxCounterCb(db, mailbox, false, (err, total) => { + getMailboxCounterCb(db, mailbox, false, user, (err, total) => { if (err) { // ignore } - getMailboxCounterCb(db, mailbox, 'unseen', (err, unseen) => { + getMailboxCounterCb(db, mailbox, 'unseen', user, (err, unseen) => { if (err) { // ignore } diff --git a/lib/imap-notifier.js b/lib/imap-notifier.js index a641fec7..ff225e86 100644 --- a/lib/imap-notifier.js +++ b/lib/imap-notifier.js @@ -343,7 +343,7 @@ class ImapNotifier extends EventEmitter { (Array.isArray(entries) ? entries : [].concat(entries || [])).forEach(entry => { let m = entry.mailbox.toString(); if (!counters.has(m)) { - counters.set(m, { total: 0, unseen: 0, unseenChange: false }); + counters.set(m, { user: entry.user && entry.user.toString(), total: 0, unseen: 0, unseenChange: false }); } switch (entry && entry.command) { @@ -378,7 +378,10 @@ class ImapNotifier extends EventEmitter { let mailbox = row[0]; let delta = row[1]; - let mailboxKey = `{${mailbox}}`; + if (!delta.user) { + continue; + } + let mailboxKey = `{${delta.user}}:${mailbox}`; await this.redis.cachedcounter(`total:${mailboxKey}`, delta.total, consts.MAILBOX_COUNTER_TTL); diff --git a/lib/tasks/clear-folder.js b/lib/tasks/clear-folder.js index d3aa2427..3b53f76d 100644 --- a/lib/tasks/clear-folder.js +++ b/lib/tasks/clear-folder.js @@ -48,7 +48,7 @@ let run = async (task, data, options) => { await cursor.close(); try { - const mailboxKey = `{${mailbox}}`; + const mailboxKey = `{${user.toString()}}:${mailbox}`; // clear counters await db.redis.multi().del(`total:${mailboxKey}`).del(`unseen:${mailboxKey}`).exec(); } catch (err) { diff --git a/lib/tools.js b/lib/tools.js index 74a9a603..0907d4c1 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -220,15 +220,34 @@ function flatAddresses(addresses) { return list; } -async function getMailboxCounter(db, mailbox, type) { - const mailboxKey = `{${mailbox}}`; - const cacheKey = `${type || 'total'}:${mailboxKey}`; +async function getMailboxCounter(db, mailbox, type, user) { + // fallback (should not happen) + if (!user) { + let mailboxObjectId = mailbox; + + let mailboxData = await db.database.collection('mailboxes').findOne( + { + _id: mailboxObjectId + }, + { + projection: { + user: true + } + } + ); + user = mailboxData && mailboxData.user; + } + + const mailboxKey = user ? `{${user}}:${mailbox}` : false; + const cacheKey = mailboxKey ? `${type || 'total'}:${mailboxKey}` : false; try { // Check cache for pre-calculated counter value - let sum = await db.redis.get(cacheKey); - if (sum !== null && !isNaN(sum) && Number(sum) >= 0) { - return Number(sum); + if (cacheKey) { + let sum = await db.redis.get(cacheKey); + if (sum !== null && !isNaN(sum) && Number(sum) >= 0) { + return Number(sum); + } } // calculate sum @@ -237,11 +256,13 @@ async function getMailboxCounter(db, mailbox, type) { query[type] = true; } - sum = await db.database.collection('messages').countDocuments(query); + let sum = await db.database.collection('messages').countDocuments(query); // Cache calculated sum in redis - await db.redis.multi().set(cacheKey, sum).expire(cacheKey, consts.MAILBOX_COUNTER_TTL).exec(); + if (cacheKey) { + await db.redis.multi().set(cacheKey, sum).expire(cacheKey, consts.MAILBOX_COUNTER_TTL).exec(); + } return sum; } catch (err) { From b881528b303f128b7bbede50160487423c10bc3e Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Mon, 2 Mar 2026 15:45:59 +0200 Subject: [PATCH 23/32] tools. getMailboxCounter use const instead of let where possible --- lib/tools.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/tools.js b/lib/tools.js index 0907d4c1..b077eeb5 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -244,19 +244,19 @@ async function getMailboxCounter(db, mailbox, type, user) { try { // Check cache for pre-calculated counter value if (cacheKey) { - let sum = await db.redis.get(cacheKey); + const sum = await db.redis.get(cacheKey); if (sum !== null && !isNaN(sum) && Number(sum) >= 0) { return Number(sum); } } // calculate sum - let query = { mailbox }; + const query = { mailbox }; if (type) { query[type] = true; } - let sum = await db.database.collection('messages').countDocuments(query); + const sum = await db.database.collection('messages').countDocuments(query); // Cache calculated sum in redis From 1131607b133de3705d56fcf4e09c5dd2a656f71b Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Mon, 2 Mar 2026 15:47:39 +0200 Subject: [PATCH 24/32] user-handler flushreq use multi() instead of multiple promises | revert prev change --- lib/user-handler.js | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/lib/user-handler.js b/lib/user-handler.js index 1030c07f..f3b5f8f9 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -3397,28 +3397,14 @@ class UserHandler { if (flushKeys.length || flushHKeys.length) { // just call the operations and hope for the best, no problems if fails try { - if (this.redis.isCluster) { - let flushreq = []; - - flushKeys.forEach(key => { - flushreq.push(this.redis.del(key)); - }); - - flushHKeys.forEach(entry => { - flushreq.push(this.redis.hdel(entry.key, entry.value)); - }); - - await Promise.all(flushreq); - } else { - let flushreq = this.redis.multi(); - flushKeys.forEach(key => { - flushreq = flushreq.del(key); - }); - flushHKeys.forEach(entry => { - flushreq = flushreq.hdel(entry.key, entry.value); - }); - await flushreq.exec(); - } + let flushreq = this.redis.multi(); + flushKeys.forEach(key => { + flushreq = flushreq.del(key); + }); + flushHKeys.forEach(entry => { + flushreq = flushreq.hdel(entry.key, entry.value); + }); + await flushreq.exec(); } catch (err) { // ignore } From f9bec692afe292ad9b31ca1b395291ae342171a9 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Mon, 2 Mar 2026 15:49:42 +0200 Subject: [PATCH 25/32] hardcode wd:bull value as hash tag --- lib/db.js | 2 +- lib/events.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/db.js b/lib/db.js index 7878178a..60436d97 100644 --- a/lib/db.js +++ b/lib/db.js @@ -99,7 +99,7 @@ module.exports.connect = callback => { module.exports.redis = REDIS_CONF.cluster ? new Redis.Cluster(REDIS_CONF.nodes, REDIS_CONF.options) : new Redis(REDIS_CONF); module.exports.queueConf = { connection: REDIS_CONF.cluster ? module.exports.redis : Object.assign({ connectionName: `${REDIS_CONF.connectionName}[notify]` }, REDIS_CONF), - prefix: REDIS_CONF.cluster ? `{wd:bull}` : `wd:bull` + prefix: `{wd:bull}` }; errors.registerRedisErrorLogger(module.exports.redis, { role: 'primary', diff --git a/lib/events.js b/lib/events.js index 52fa309b..1990a3e2 100644 --- a/lib/events.js +++ b/lib/events.js @@ -55,7 +55,7 @@ module.exports = { if (!webhooksQueue) { webhooksQueue = new Queue('webhooks', { connection: redisClient, - prefix: redisClient.isCluster ? `{wd:bull}` : `wd:bull` + prefix: `{wd:bull}` }); } From 48cf52cb594d01aa69fc9e98b4b4a85ff8a7985e Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 23 Apr 2026 11:17:24 +0300 Subject: [PATCH 26/32] fix imap notifier and migrations --- lib/imap-notifier.js | 77 +++++++++++++------ .../database/addressregister_add_disabled.js | 2 +- migrations/users/addresses_to_domaincache.js | 2 +- .../users/domainaliases_to_domaincache.js | 2 +- 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/lib/imap-notifier.js b/lib/imap-notifier.js index b10f04b7..58e505a1 100644 --- a/lib/imap-notifier.js +++ b/lib/imap-notifier.js @@ -21,6 +21,16 @@ const USER_WORKERS_TTL_MS = USER_WORKERS_TTL * 1000; const USE_WORKER_WD_EVENTS_SETTING = 'const:imap:use_wd_worker_channels'; const USE_WORKER_WD_EVENTS_CACHE_TTL = 5000; // ms const WORKER_ID = `${config?.dbs?.workerId || os.hostname()}:${process.pid}`; +const USER_REGISTRY_STATE = { + counts: new Map(), + workerId: WORKER_ID, + workerChannel: `${WORKER_CHANNEL_PREFIX}${WORKER_ID}`, + useWorkerWdEvents: false, + useWorkerWdEventsUpdated: 0, + useWorkerWdEventsPending: null, + timer: null, + redis: null +}; class ImapNotifier extends EventEmitter { constructor(options) { @@ -31,17 +41,7 @@ class ImapNotifier extends EventEmitter { errors.registerRedisErrorLogger(this.redis, { role: 'imap-notifier' }); // if separate redis will add the appropriate role this.counters = counters(this.redis); this.settingsHandler = options.settingsHandler || new SettingsHandler({ db: this.database }); - this._userRegistryState = { - counts: new Map(), - workerId: WORKER_ID, - workerChannel: `${WORKER_CHANNEL_PREFIX}${WORKER_ID}`, - useWorkerWdEvents: false, - useWorkerWdEventsUpdated: 0, - useWorkerWdEventsPending: null, - timer: null, - redis: null - }; - + this._userRegistryState = USER_REGISTRY_STATE; this.subscribeFnName = this.redis.isCluster ? 'ssubscribe' : 'subscribe'; this.publishFnName = this.redis.isCluster ? 'spublish' : 'publish'; this.messageEventName = this.redis.isCluster ? 'smessage' : 'message'; @@ -106,7 +106,7 @@ class ImapNotifier extends EventEmitter { } }; - this.subscriber.on('message', (channel, message) => { + this.subscriber.on(this.messageEventName, (channel, message) => { if (channel !== this._userRegistryState.workerChannel && channel !== LEGACY_CHANNEL) { return; } @@ -138,8 +138,8 @@ class ImapNotifier extends EventEmitter { } }); - this.subscriber.subscribe(this._userRegistryState.workerChannel); - this.subscriber.subscribe(LEGACY_CHANNEL); + this.subscriber[this.subscribeFnName](this._userRegistryState.workerChannel); + this.subscriber[this.subscribeFnName](LEGACY_CHANNEL); if (!this._userRegistryState.timer) { this._userRegistryState.redis = this.redis; @@ -340,7 +340,7 @@ class ImapNotifier extends EventEmitter { this._usesWorkerWdEvents() .then(useWorkerWdEvents => { if (!useWorkerWdEvents) { - return this.redis.publish(LEGACY_CHANNEL, data); + return this.redis[this.publishFnName](LEGACY_CHANNEL, data); } const key = this._getUserWorkersKey(userId); @@ -351,9 +351,13 @@ class ImapNotifier extends EventEmitter { return; } - const pipeline = this.redis.pipeline(); - workers.forEach(workerId => pipeline.publish(this._getWorkerChannel(workerId), data)); - return pipeline.exec(); + if (!this.redis.isCluster) { + const pipeline = this.redis.pipeline(); + workers.forEach(workerId => pipeline[this.publishFnName](this._getWorkerChannel(workerId), data)); + return pipeline.exec(); + } + + return Promise.all(workers.map(workerId => this.redis[this.publishFnName](this._getWorkerChannel(workerId), data))); }); }) .catch(() => false); @@ -443,16 +447,43 @@ class ImapNotifier extends EventEmitter { } let redis = this._userRegistryState.redis || this.redis; - let pipeline = redis.pipeline(); let now = Date.now(); let minScore = now - USER_WORKERS_TTL_MS; + let commands = []; for (let userId of counts.keys()) { let key = this._getUserWorkersKey(userId); - pipeline.zremrangebyscore(key, 0, minScore); - pipeline.zadd(key, now, this._userRegistryState.workerId); - pipeline.expire(key, USER_WORKERS_TTL + 60); + + if (!redis.isCluster) { + commands.push( + ['zremrangebyscore', key, 0, minScore], + ['zadd', key, now, this._userRegistryState.workerId], + ['expire', key, USER_WORKERS_TTL + 60] + ); + continue; + } + + commands.push( + redis + .multi() + .zremrangebyscore(key, 0, minScore) + .zadd(key, now, this._userRegistryState.workerId) + .expire(key, USER_WORKERS_TTL + 60) + .exec() + ); + } + + if (!commands.length) { + return; } - pipeline.exec().catch(() => false); + + if (!redis.isCluster) { + return redis + .pipeline(commands) + .exec() + .catch(() => false); + } + + Promise.all(commands).catch(() => false); } /** diff --git a/migrations/database/addressregister_add_disabled.js b/migrations/database/addressregister_add_disabled.js index fd3e8dca..a0ac96d4 100644 --- a/migrations/database/addressregister_add_disabled.js +++ b/migrations/database/addressregister_add_disabled.js @@ -3,7 +3,7 @@ // MongoDB Migration Script: addressregister add disabled field to all current addressregister entries in DB const config = require('@zone-eu/wild-config'); -const ENABLED = process.env.NODE_ENV === 'test' ? false : !!config?.migrations?.database?.addressregisterAddDisabled?.enabled; +const ENABLED = /^test/.test(process.env.NODE_ENV || '') ? false : !!config?.migrations?.database?.addressregisterAddDisabled?.enabled; const BATCH_SIZE = 1000; async function addDisabledToAddressregister() { diff --git a/migrations/users/addresses_to_domaincache.js b/migrations/users/addresses_to_domaincache.js index 70089139..d4803d98 100644 --- a/migrations/users/addresses_to_domaincache.js +++ b/migrations/users/addresses_to_domaincache.js @@ -3,7 +3,7 @@ // MongoDB Migration Script: addresses to domaincache const config = require('@zone-eu/wild-config'); -const ENABLED = process.env.NODE_ENV === 'test' ? false : !!config?.migrations?.users?.addressesToDomaincache?.enabled; +const ENABLED = /^test/.test(process.env.NODE_ENV || '') ? false : !!config?.migrations?.users?.addressesToDomaincache?.enabled; // Extract domain from email address function extractDomain(email) { diff --git a/migrations/users/domainaliases_to_domaincache.js b/migrations/users/domainaliases_to_domaincache.js index 3ee0a999..be5753fb 100644 --- a/migrations/users/domainaliases_to_domaincache.js +++ b/migrations/users/domainaliases_to_domaincache.js @@ -2,7 +2,7 @@ /* global db, log */ const config = require('@zone-eu/wild-config'); -const ENABLED = process.env.NODE_ENV === 'test' ? false : !!config?.migrations?.users?.domainaliasesToDomaincache?.enabled; +const ENABLED = /^test/.test(process.env.NODE_ENV || '') ? false : !!config?.migrations?.users?.domainaliasesToDomaincache?.enabled; async function migrateDomainAliasesToCache() { const sourceCollection = db.collection('domainaliases'); From 5daf4b4ff9ebe5cb213776dee6691f777b988f5f Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 23 Apr 2026 11:18:24 +0300 Subject: [PATCH 27/32] remove unnecessary file --- test_multipart_text_3.eml | 71 --------------------------------------- 1 file changed, 71 deletions(-) delete mode 100644 test_multipart_text_3.eml diff --git a/test_multipart_text_3.eml b/test_multipart_text_3.eml deleted file mode 100644 index baf85dcb..00000000 --- a/test_multipart_text_3.eml +++ /dev/null @@ -1,71 +0,0 @@ -Message-ID: -Date: Tue, 6 Jan 2026 18:28:16 +0200 -MIME-Version: 1.0 -User-Agent: Mozilla Thunderbird -Content-Language: la-VA -To: =?UTF-8?Q?L=C3=B5ks?= -From: =?UTF-8?Q?L=C3=B5ks?= -Subject: This is a test letter #2 -Content-Type: multipart/alternative; boundary="e60533cb-5476-4254-b4f0-c0d6502883cc"; charset=UTF-8 -Content-Transfer-Encoding: 7bit - - - - ---e60533cb-5476-4254-b4f0-c0d6502883cc -Content-Transfer-Encoding: 7bit -Content-Type: text/plain; charset=UTF-8 - - - - -Lorem, - -Ipsum dolor sit amet, consectetur adipiscing elit. Donec in nulla -eu tortor lobortis condimentum quis quis enim. Fusce commodo nibh -a neque pretium aliquet. Quisque aliquet non erat in maximus. Sed -sagittis rutrum risus ac convallis. Phasellus porta ipsum vel -tortor faucibus, molestie luctus metus tristique. Phasellus -fringilla pharetra ullamcorper. Nam congue mi tristique, sodales -ante sed, volutpat eros. - -Proin erat quam, -cursus sit amet condimentum auctor, -ultrices eu nulla. - - - ---e60533cb-5476-4254-b4f0-c0d6502883cc -Content-Transfer-Encoding: 7bit -Content-Type: text/html; charset=UTF-8 - - - - - - - - - - - - Lorem, -

Ipsum dolor sit amet, consectetur adipiscing elit. Donec in nulla - eu tortor lobortis condimentum quis quis enim. Fusce commodo nibh - a neque pretium aliquet. Quisque aliquet non erat in maximus. Sed - sagittis rutrum risus ac convallis. Phasellus porta ipsum vel - tortor faucibus, molestie luctus metus tristique. Phasellus - fringilla pharetra ullamcorper. Nam congue mi tristique, sodales - ante sed, volutpat eros.

-

Proin erat quam,
- cursus sit amet condimentum auctor,
- ultrices eu nulla.

- - - - - ---e60533cb-5476-4254-b4f0-c0d6502883cc-- - - - From d1042673d8bc33c631c37dc7ad84091d551a1620 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 23 Apr 2026 11:19:08 +0300 Subject: [PATCH 28/32] remove dangling file --- addressregister_migration_add_disabled.js | 76 ----------------------- 1 file changed, 76 deletions(-) delete mode 100644 addressregister_migration_add_disabled.js diff --git a/addressregister_migration_add_disabled.js b/addressregister_migration_add_disabled.js deleted file mode 100644 index 79f7e9ac..00000000 --- a/addressregister_migration_add_disabled.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict'; - -// MongoDB Migration Script: addressregister add disabled field to all current addressregister entries in DB -const ENABLED = true; -const BATCH_SIZE = 1000; - -function addDisabledToAddressregister() { - print('Starting migration: Adding disabled field to addressregister collection'); - - const collection = db.getCollection('addressregister'); - - // Get current max id - const maxIdDoc = collection.find({}).sort({ _id: -1 }).limit(1).toArray(); - const maxIdAtStart = maxIdDoc.length > 0 ? maxIdDoc[0]._id : null; - - if (!maxIdAtStart) { - print('No documents found. Migration skipped.'); - return; - } - - const totalToMigrate = collection.countDocuments({ - disabled: { $exists: false }, - _id: { $lte: maxIdAtStart } // Only count documents that exist now - }); - - if (totalToMigrate === 0) { - print('All documents already have disabled field. Migration skipped.'); - return; - } - - print(`Migrating ${totalToMigrate} documents (up to _id: ${maxIdAtStart}) in batches of ${BATCH_SIZE}...`); - - let processedCount = 0; - let batchNumber = 0; - let lastId = null; - let running = true; - - while (running) { - const query = { - disabled: { $exists: false }, - _id: { $lte: maxIdAtStart } // Cap at migration start - }; - - // cursor based pagination - if (lastId) { - query._id = { $gt: lastId, $lte: maxIdAtStart }; - } - - // Find batch of documents without the field and capped at max id - const batch = collection.find(query, { _id: 1 }).sort({ _id: 1 }).limit(BATCH_SIZE).toArray(); - - if (batch.length === 0) { - running = false; - break; - } - - // Update this batch - const ids = batch.map(doc => doc._id); - const result = collection.updateMany({ _id: { $in: ids } }, { $set: { disabled: false } }); - - processedCount += result.modifiedCount; - batchNumber++; - - // Update lastId for next iteration - lastId = batch[batch.length - 1]._id; - - // Log progress - print(`Progress: Batch ${batchNumber} - ${processedCount}/${totalToMigrate} (${((processedCount / totalToMigrate) * 100).toFixed(1)}%)`); - } - - print(`✅ Migration complete! Updated ${processedCount} documents`); -} - -if (ENABLED) { - addDisabledToAddressregister(); -} From 5a246b9cbde0869a73153081b56039396021f549 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 23 Apr 2026 11:26:49 +0300 Subject: [PATCH 29/32] add missing fixture --- test/fixtures/multipart-2.eml | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 test/fixtures/multipart-2.eml diff --git a/test/fixtures/multipart-2.eml b/test/fixtures/multipart-2.eml new file mode 100644 index 00000000..1f3ef97d --- /dev/null +++ b/test/fixtures/multipart-2.eml @@ -0,0 +1,63 @@ +Message-ID: +Date: Tue, 6 Jan 2026 18:28:16 +0200 +MIME-Version: 1.0 +User-Agent: Mozilla Thunderbird +Content-Language: la-VA +To: =?UTF-8?Q?L=C3=B5ks?= +From: =?UTF-8?Q?L=C3=B5ks?= +Subject: This is a test letter #2 +Content-Type: multipart/alternative; boundary="e60533cb-5476-4254-b4f0-c0d6502883cc"; charset=UTF-8 +Content-Transfer-Encoding: 7bit + + + +--e60533cb-5476-4254-b4f0-c0d6502883cc +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; charset=UTF-8 + + + +Lorem, + +Ipsum dolor sit amet, consectetur adipiscing elit. Donec in nulla +eu tortor lobortis condimentum quis quis enim. Fusce commodo nibh +a neque pretium aliquet. Quisque aliquet non erat in maximus. Sed +sagittis rutrum risus ac convallis. Phasellus porta ipsum vel +tortor faucibus, molestie luctus metus tristique. Phasellus +fringilla pharetra ullamcorper. Nam congue mi tristique, sodales +ante sed, volutpat eros. + +Proin erat quam, +cursus sit amet condimentum auctor, +ultrices eu nulla. + + +--e60533cb-5476-4254-b4f0-c0d6502883cc +Content-Transfer-Encoding: 7bit +Content-Type: text/html; charset=UTF-8 + + + + + + + + + + + Lorem, +

Ipsum dolor sit amet, consectetur adipiscing elit. Donec in nulla + eu tortor lobortis condimentum quis quis enim. Fusce commodo nibh + a neque pretium aliquet. Quisque aliquet non erat in maximus. Sed + sagittis rutrum risus ac convallis. Phasellus porta ipsum vel + tortor faucibus, molestie luctus metus tristique. Phasellus + fringilla pharetra ullamcorper. Nam congue mi tristique, sodales + ante sed, volutpat eros.

+

Proin erat quam,
+ cursus sit amet condimentum auctor,
+ ultrices eu nulla.

+ + + + +--e60533cb-5476-4254-b4f0-c0d6502883cc-- From e9baab663c6afb115b771a7b5d6d355ca0649a24 Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 23 Apr 2026 11:28:27 +0300 Subject: [PATCH 30/32] fix package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 04d779e1..ce7e4b8c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "server.js", "scripts": { "test": "mongosh --eval 'db.dropDatabase()' wildduck-test && redis-cli -n 13 flushdb && npm run runtest", - "test:cluster": "mongosh --eval 'db.dropDatabase()' wildduck-test && redis-cli --cluster call 127.0.0.1:7000 flushdb && npm run runtest", + "test:cluster": "mongosh --eval 'db.dropDatabase()' wildduck-test && redis-cli --cluster call 127.0.0.1:7000 flushdb && NODE_ENV=test-cluster grunt", "test:proto": "NODE_ENV=test grunt proto", "printconf": "NODE_CONFIG_ONLY=true npm start", "runtest": "NODE_ENV=test grunt", From 67be8e367e0798b20df1e24e900638f8c8c2048c Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Thu, 23 Apr 2026 11:58:43 +0300 Subject: [PATCH 31/32] add hash tags to redis keys conditionally --- indexer.js | 3 +- lib/api/addresses.js | 10 ++- lib/api/messages.js | 27 +++++-- lib/api/submit.js | 154 ++++++++++++++++++++------------------ lib/api/users.js | 27 +++---- lib/db.js | 3 +- lib/events.js | 3 +- lib/filter-handler.js | 12 +-- lib/handlers/on-append.js | 5 +- lib/handlers/on-fetch.js | 2 +- lib/imap-notifier.js | 20 ++--- lib/tasks/clear-folder.js | 3 +- lib/tools.js | 13 +++- lib/user-handler.js | 4 +- pop3.js | 4 +- 15 files changed, 160 insertions(+), 130 deletions(-) diff --git a/indexer.js b/indexer.js index 21c9cd1f..b44a92b8 100644 --- a/indexer.js +++ b/indexer.js @@ -12,6 +12,7 @@ const counters = require('./lib/counters'); const { ObjectId } = require('mongodb'); const libmime = require('libmime'); const punycode = require('punycode.js'); +const tools = require('./lib/tools'); const { getClient } = require('./lib/elasticsearch'); const { normalizeLoggelfMessage } = require('./lib/loggelf-message'); @@ -284,7 +285,7 @@ function indexingJob(esclient) { const dateKeyTdy = new Date().toISOString().substring(0, 10).replace(/-/g, ''); const dateKeyYdy = new Date(Date.now() - 24 * 3600 * 1000).toISOString().substring(0, 10).replace(/-/g, ''); - const tombstoneTag = '{indexer:tomb}'; + const tombstoneTag = tools.redisHashTag(db.redis, 'indexer:tomb'); const tombstoneTdy = `${tombstoneTag}:${dateKeyTdy}`; const tombstoneYdy = `${tombstoneTag}:${dateKeyYdy}`; diff --git a/lib/api/addresses.js b/lib/api/addresses.js index 6b615df2..ae928648 100644 --- a/lib/api/addresses.js +++ b/lib/api/addresses.js @@ -2397,11 +2397,12 @@ module.exports = (db, server, userHandler, settingsHandler) => { let response; try { + const addressKey = tools.redisHashTag(db.redis, addressData._id.toString()); response = await db.redis .multi() // sending counters are stored in Redis - .get(`wdf:{${addressData._id.toString()}}`) - .ttl(`wdf:{${addressData._id.toString()}}`) + .get(`wdf:${addressKey}`) + .ttl(`wdf:${addressKey}`) .exec(); } catch (err) { // ignore @@ -2565,11 +2566,12 @@ module.exports = (db, server, userHandler, settingsHandler) => { let response; try { + const addressKey = tools.redisHashTag(db.redis, addressData._id.toString()); response = await db.redis .multi() // sending counters are stored in Redis - .get(`wdf:{${addressData._id.toString()}}`) - .ttl(`wdf:{${addressData._id.toString()}}`) + .get(`wdf:${addressKey}`) + .ttl(`wdf:${addressKey}`) .exec(); } catch (err) { // ignore diff --git a/lib/api/messages.js b/lib/api/messages.js index e309b738..a97d113a 100644 --- a/lib/api/messages.js +++ b/lib/api/messages.js @@ -2210,7 +2210,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti } try { - const mailboxKey = `{${user.toString()}}:${mailbox}`; + const mailboxKey = `${tools.redisHashTag(db.redis, user.toString())}:${mailbox}`; // clear counters await db.redis.multi().del(`total:${mailboxKey}`).del(`unseen:${mailboxKey}`).exec(); } catch (err) { @@ -3140,7 +3140,12 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti let limitCheck; try { - limitCheck = await messageHandler.counters.asyncTTLCounter(`wdr:{${userData._id.toString()}}`, 0, maxRecipients, false); + limitCheck = await messageHandler.counters.asyncTTLCounter( + `wdr:${tools.redisHashTag(messageHandler.redis, userData._id.toString())}`, + 0, + maxRecipients, + false + ); } catch (err) { log.error('API', 'Failed to check draft submit rate limit for user=%s message=%s error=%s', userData._id, messageData._id, err.message); res.status(500); @@ -4191,13 +4196,19 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti } // Update counters only after message has been succesfully sent and not rejected - messageHandler.counters.ttlcounter(`wdr:{${userData._id.toString()}}`, envelope.to.length, maxRecipients, false, err => { - if (err) { - err.responseCode = 500; - err.code = 'InternalDatabaseError'; - return reject(err); + messageHandler.counters.ttlcounter( + `wdr:${tools.redisHashTag(messageHandler.redis, userData._id.toString())}`, + envelope.to.length, + maxRecipients, + false, + err => { + if (err) { + err.responseCode = 500; + err.code = 'InternalDatabaseError'; + return reject(err); + } } - }); + ); // Update addressregister - output can be ignored as it is not an important operation messageHandler.updateAddressRegister( diff --git a/lib/api/submit.js b/lib/api/submit.js index 4a6dbadc..310814ff 100644 --- a/lib/api/submit.js +++ b/lib/api/submit.js @@ -432,92 +432,98 @@ module.exports = (db, server, messageHandler, userHandler, settingsHandler) => { const recipientCount = compiledEnvelope.to.length; - messageHandler.counters.ttlcounter(`wdr:{${userData._id.toString()}}`, 0, maxRecipients, false, (err, result) => { - if (err) { - err.responseCode = 500; - err.code = 'InternalDatabaseError'; - return callback(err); - } + messageHandler.counters.ttlcounter( + `wdr:${tools.redisHashTag(messageHandler.redis, userData._id.toString())}`, + 0, + maxRecipients, + false, + (err, result) => { + if (err) { + err.responseCode = 500; + err.code = 'InternalDatabaseError'; + return callback(err); + } - let success = result.success; - let sent = result.value; - let ttl = result.ttl; + let success = result.success; + let sent = result.value; + let ttl = result.ttl; + + let ttlHuman = false; + if (ttl && ttl > 0) { + if (ttl < 60) { + ttlHuman = ttl + ' seconds'; + } else if (ttl < 3600) { + ttlHuman = Math.round(ttl / 60) + ' minutes'; + } else { + ttlHuman = Math.round(ttl / 3600) + ' hours'; + } + } - let ttlHuman = false; - if (ttl && ttl > 0) { - if (ttl < 60) { - ttlHuman = ttl + ' seconds'; - } else if (ttl < 3600) { - ttlHuman = Math.round(ttl / 60) + ' minutes'; - } else { - ttlHuman = Math.round(ttl / 3600) + ' hours'; + if (!success || sent + recipientCount > maxRecipients) { + log.info('API', 'RCPTDENY denied sent=%s allowed=%s expires=%ss.', sent, maxRecipients, ttl); + let err = new Error( + 'You reached a daily sending limit for your account' + + (ttl && ttl > 0 ? '. Limit expires in ' + ttlHuman : '') + ); + err.responseCode = 403; + err.code = 'RateLimitedError'; + return setImmediate(() => callback(err)); } - } - if (!success || sent + recipientCount > maxRecipients) { - log.info('API', 'RCPTDENY denied sent=%s allowed=%s expires=%ss.', sent, maxRecipients, ttl); - let err = new Error( - 'You reached a daily sending limit for your account' + - (ttl && ttl > 0 ? '. Limit expires in ' + ttlHuman : '') - ); - err.responseCode = 403; - err.code = 'RateLimitedError'; - return setImmediate(() => callback(err)); - } + // push message to outbound queue + let message = maildrop.push( + { + user: userData._id, + userEmail: userData.address, + parentId: messageId, + reason: 'submit', + from: compiledEnvelope.from, + to: compiledEnvelope.to, + sendTime, + origin: options.ip, + passwordType: 'master', + runPlugins: true, + mtaRelay: userData.mtaRelay || false + }, + (err, ...args) => { + if (err || !args[0]) { + if (err) { + if (!err.code && err.name === 'SMTPReject') { + err.code = 'MessageRejected'; + } - // push message to outbound queue - let message = maildrop.push( - { - user: userData._id, - userEmail: userData.address, - parentId: messageId, - reason: 'submit', - from: compiledEnvelope.from, - to: compiledEnvelope.to, - sendTime, - origin: options.ip, - passwordType: 'master', - runPlugins: true, - mtaRelay: userData.mtaRelay || false - }, - (err, ...args) => { - if (err || !args[0]) { - if (err) { - if (!err.code && err.name === 'SMTPReject') { - err.code = 'MessageRejected'; + err.code = err.code || 'ERRCOMPOSE'; } - - err.code = err.code || 'ERRCOMPOSE'; + err.responseCode = 500; + return callback(err, ...args); } - err.responseCode = 500; - return callback(err, ...args); - } - messageHandler.counters.ttlcounter( - `wdr:{${userData._id.toString()}}`, - recipientCount, - maxRecipients, - false, - err => { - if (err) { - err.responseCode = 500; - err.code = 'InternalDatabaseError'; - return setImmediate(() => callback(err)); + messageHandler.counters.ttlcounter( + `wdr:${tools.redisHashTag(messageHandler.redis, userData._id.toString())}`, + recipientCount, + maxRecipients, + false, + err => { + if (err) { + err.responseCode = 500; + err.code = 'InternalDatabaseError'; + return setImmediate(() => callback(err)); + } + + let outbound = args[0].id; + return next(null, outbound); } + ); + } + ); - let outbound = args[0].id; - return next(null, outbound); - } - ); + if (message) { + let stream = compiled.createReadStream(); + stream.once('error', err => message.emit('error', err)); + stream.pipe(collector).pipe(message); } - ); - - if (message) { - let stream = compiled.createReadStream(); - stream.once('error', err => message.emit('error', err)); - stream.pipe(collector).pipe(message); } - }); + ); }; addToDeliveryQueue((err, outbound) => { diff --git a/lib/api/users.js b/lib/api/users.js index 6f3c78b1..a5943967 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -1029,35 +1029,36 @@ module.exports = (db, server, userHandler, settingsHandler) => { let response; try { + const userKey = tools.redisHashTag(db.redis, userData._id.toString()); response = await db.redis .multi() // sending counters are stored in Redis // sent messages - .get(`wdr:{${userData._id.toString()}}`) - .ttl(`wdr:{${userData._id.toString()}}`) + .get(`wdr:${userKey}`) + .ttl(`wdr:${userKey}`) // forwarded messages - .get(`wdf:{${userData._id.toString()}}`) - .ttl(`wdf:{${userData._id.toString()}}`) + .get(`wdf:${userKey}`) + .ttl(`wdf:${userKey}`) // rate limited recipient - .get(`rl:rcpt:{${userData._id.toString()}}`) - .ttl(`rl:rcpt:{${userData._id.toString()}}`) + .get(`rl:rcpt:${userKey}`) + .ttl(`rl:rcpt:${userKey}`) // rate limited imap uploads - .get(`iup:{${userData._id.toString()}}`) - .ttl(`iup:{${userData._id.toString()}}`) + .get(`iup:${userKey}`) + .ttl(`iup:${userKey}`) // rate limited imap downloads - .get(`idw:{${userData._id.toString()}}`) - .ttl(`idw:{${userData._id.toString()}}`) + .get(`idw:${userKey}`) + .ttl(`idw:${userKey}`) // rate limited pop3 downloads - .get(`pdw:{${userData._id.toString()}}`) - .ttl(`pdw:{${userData._id.toString()}}`) + .get(`pdw:${userKey}`) + .ttl(`pdw:${userKey}`) - .hget(`lim:imap:{${userData._id.toString()}}`, userData._id.toString()) + .hget(`lim:imap:${userKey}`, userData._id.toString()) .exec(); } catch (err) { diff --git a/lib/db.js b/lib/db.js index 60436d97..029ac297 100644 --- a/lib/db.js +++ b/lib/db.js @@ -4,6 +4,7 @@ const config = require('@zone-eu/wild-config'); const mongodb = require('mongodb'); const Redis = require('ioredis'); const redisUrl = require('./redis-url'); +const tools = require('./tools'); const log = require('npmlog'); const errors = require('./errors'); const packageData = require('../package.json'); @@ -99,7 +100,7 @@ module.exports.connect = callback => { module.exports.redis = REDIS_CONF.cluster ? new Redis.Cluster(REDIS_CONF.nodes, REDIS_CONF.options) : new Redis(REDIS_CONF); module.exports.queueConf = { connection: REDIS_CONF.cluster ? module.exports.redis : Object.assign({ connectionName: `${REDIS_CONF.connectionName}[notify]` }, REDIS_CONF), - prefix: `{wd:bull}` + prefix: tools.redisHashTag(REDIS_CONF, 'wd:bull') }; errors.registerRedisErrorLogger(module.exports.redis, { role: 'primary', diff --git a/lib/events.js b/lib/events.js index 1990a3e2..f81794b9 100644 --- a/lib/events.js +++ b/lib/events.js @@ -4,6 +4,7 @@ const { Queue } = require('bullmq'); const log = require('npmlog'); +const tools = require('./tools'); let webhooksQueue; @@ -55,7 +56,7 @@ module.exports = { if (!webhooksQueue) { webhooksQueue = new Queue('webhooks', { connection: redisClient, - prefix: `{wd:bull}` + prefix: tools.redisHashTag(redisClient, 'wd:bull') }); } diff --git a/lib/filter-handler.js b/lib/filter-handler.js index c4fc7168..833f2a85 100644 --- a/lib/filter-handler.js +++ b/lib/filter-handler.js @@ -431,15 +431,11 @@ class FilterHandler { } // check limiting counters + const userKey = tools.redisHashTag(this.messageHandler.redis, userData._id.toString()); try { - let counterResult = await this.ttlcounter( - `wdf:{${userData._id.toString()}}`, - forwardTargets.size, - userData.forwards || consts.MAX_FORWARDS, - false - ); + const counterResult = await this.ttlcounter(`wdf:${userKey}`, forwardTargets.size, userData.forwards || consts.MAX_FORWARDS, false); if (!counterResult.success) { - log.silly('Filter', 'FRWRDFAIL key=%s error=%s', `wdf:{${userData._id.toString()}}`, 'Precondition failed'); + log.silly('Filter', 'FRWRDFAIL key=%s error=%s', `wdf:${userKey}`, 'Precondition failed'); logdata.short_message = '[FRWRDFAIL] Skipped forwarding due to rate limiting'; logdata._error = 'Skipped forwarding due to rate limiting'; @@ -450,7 +446,7 @@ class FilterHandler { } } catch (err) { // failed checks, ignore - log.info('Filter', 'FRWRDFAIL key=%s error=%s', `wdf:{${userData._id.toString()}}`, err.message); + log.info('Filter', 'FRWRDFAIL key=%s error=%s', `wdf:${userKey}`, err.message); logdata.short_message = '[FRWRDFAIL] Skipped forwarding due to database error'; logdata._error = err.message; diff --git a/lib/handlers/on-append.js b/lib/handlers/on-append.js index ba4c9d44..784a864d 100644 --- a/lib/handlers/on-append.js +++ b/lib/handlers/on-append.js @@ -45,7 +45,7 @@ module.exports = (server, messageHandler, userCache) => (path, flags, date, raw, return callback(err); } - const counterKey = `iup:{${session.user.id.toString()}}`; + const counterKey = `iup:${tools.redisHashTag(db.redis, session.user.id.toString())}`; messageHandler.counters.ttlcounter(counterKey, 0, limit, false, (err, res) => { if (err) { @@ -64,8 +64,7 @@ module.exports = (server, messageHandler, userCache) => (path, flags, date, raw, flags = Array.isArray(flags) ? flags : [].concat(flags || []); (async () => { - let encryptionKey = - userData.encryptMessages && !flags.includes('\\Draft') ? tools.getUserEncryptionKey(userData) : false; + const encryptionKey = userData.encryptMessages && !flags.includes('\\Draft') ? tools.getUserEncryptionKey(userData) : false; if (encryptionKey) { try { let encryptResult = await messageHandler.encryptMessageAsync(encryptionKey, raw); diff --git a/lib/handlers/on-fetch.js b/lib/handlers/on-fetch.js index 6cd25f77..b3ba4342 100644 --- a/lib/handlers/on-fetch.js +++ b/lib/handlers/on-fetch.js @@ -45,7 +45,7 @@ module.exports = (server, messageHandler, userCache) => (mailbox, options, sessi return callback(err); } - const counterKey = `idw:{${session.user.id.toString()}}`; + const counterKey = `idw:${tools.redisHashTag(db.redis, session.user.id.toString())}`; messageHandler.counters.ttlcounter(counterKey, 0, limit, false, (err, res) => { if (err) { diff --git a/lib/imap-notifier.js b/lib/imap-notifier.js index 58e505a1..0bfad7ff 100644 --- a/lib/imap-notifier.js +++ b/lib/imap-notifier.js @@ -42,9 +42,9 @@ class ImapNotifier extends EventEmitter { this.counters = counters(this.redis); this.settingsHandler = options.settingsHandler || new SettingsHandler({ db: this.database }); this._userRegistryState = USER_REGISTRY_STATE; - this.subscribeFnName = this.redis.isCluster ? 'ssubscribe' : 'subscribe'; - this.publishFnName = this.redis.isCluster ? 'spublish' : 'publish'; - this.messageEventName = this.redis.isCluster ? 'smessage' : 'message'; + this.subscribeFnName = tools.isRedisCluster(this.redis) ? 'ssubscribe' : 'subscribe'; + this.publishFnName = tools.isRedisCluster(this.redis) ? 'spublish' : 'publish'; + this.messageEventName = tools.isRedisCluster(this.redis) ? 'smessage' : 'message'; this.logger = options.logger || { info: log.silly.bind(log, 'IMAP'), @@ -61,7 +61,7 @@ class ImapNotifier extends EventEmitter { // Subscriber needs its own client connection. This is relevant only in the context of IMAP - this.subscriber = this.redis.duplicate([], this.redis.isCluster ? { shardedSubscribers: true } : {}); // connection agnostic (standalone or Cluster) + this.subscriber = this.redis.duplicate([], tools.isRedisCluster(this.redis) ? { shardedSubscribers: true } : {}); // connection agnostic (standalone or Cluster) errors.registerRedisErrorLogger(this.subscriber, { role: 'imap-notifier-subscriber' }); this._listeners = new EventEmitter(); @@ -351,7 +351,7 @@ class ImapNotifier extends EventEmitter { return; } - if (!this.redis.isCluster) { + if (!tools.isRedisCluster(this.redis)) { const pipeline = this.redis.pipeline(); workers.forEach(workerId => pipeline[this.publishFnName](this._getWorkerChannel(workerId), data)); return pipeline.exec(); @@ -453,7 +453,7 @@ class ImapNotifier extends EventEmitter { for (let userId of counts.keys()) { let key = this._getUserWorkersKey(userId); - if (!redis.isCluster) { + if (!tools.isRedisCluster(redis)) { commands.push( ['zremrangebyscore', key, 0, minScore], ['zadd', key, now, this._userRegistryState.workerId], @@ -476,7 +476,7 @@ class ImapNotifier extends EventEmitter { return; } - if (!redis.isCluster) { + if (!tools.isRedisCluster(redis)) { return redis .pipeline(commands) .exec() @@ -553,7 +553,7 @@ class ImapNotifier extends EventEmitter { if (!delta.user) { continue; } - let mailboxKey = `{${delta.user}}:${mailbox}`; + let mailboxKey = `${tools.redisHashTag(this.redis, delta.user)}:${mailbox}`; await this.redis.cachedcounter(`total:${mailboxKey}`, delta.total, consts.MAILBOX_COUNTER_TTL); @@ -572,7 +572,7 @@ class ImapNotifier extends EventEmitter { return callback(null, true); } - let rlkey = `lim:${data.service}:{${data.user.toString()}}`; + let rlkey = `lim:${data.service}:${tools.redisHashTag(this.redis, data.user.toString())}`; const { closed: socketClosed, connecting, destroyed } = data.session?.socket || {}; if (!socketClosed && !connecting && !destroyed) { @@ -607,7 +607,7 @@ class ImapNotifier extends EventEmitter { let entry = this.connectionSessions.get(data.session); this.connectionSessions.delete(data.session); - let rlkey = `lim:${entry.service}:{${entry.user.toString()}}`; + let rlkey = `lim:${entry.service}:${tools.redisHashTag(this.redis, entry.user.toString())}`; this.counters.limitedcounter(rlkey, entry.user, -1, 0, err => { if (err) { this.logger.debug('[%s] Failed to release connection for user %s. %s', data.session.id, entry.user, err.message); diff --git a/lib/tasks/clear-folder.js b/lib/tasks/clear-folder.js index 3b53f76d..48e26f99 100644 --- a/lib/tasks/clear-folder.js +++ b/lib/tasks/clear-folder.js @@ -2,6 +2,7 @@ const log = require('npmlog'); const db = require('../db'); +const tools = require('../tools'); let run = async (task, data, options) => { const messageHandler = options.messageHandler; @@ -48,7 +49,7 @@ let run = async (task, data, options) => { await cursor.close(); try { - const mailboxKey = `{${user.toString()}}:${mailbox}`; + const mailboxKey = `${tools.redisHashTag(db.redis, user.toString())}:${mailbox}`; // clear counters await db.redis.multi().del(`total:${mailboxKey}`).del(`unseen:${mailboxKey}`).exec(); } catch (err) { diff --git a/lib/tools.js b/lib/tools.js index be0aed98..ef47718a 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -185,6 +185,15 @@ function redisConfig(defaultConfig) { }; } +function isRedisCluster(redis) { + return !!(redis && (redis.isCluster || redis.cluster)); +} + +function redisHashTag(redis, value) { + value = typeof value === 'undefined' || value === null ? '' : value.toString(); + return isRedisCluster(redis) ? `{${value}}` : value; +} + function decodeAddresses(addresses) { addresses.forEach(address => { address.name = (address.name || '').toString(); @@ -237,7 +246,7 @@ async function getMailboxCounter(db, mailbox, type, user) { user = mailboxData && mailboxData.user; } - const mailboxKey = user ? `{${user}}:${mailbox}` : false; + const mailboxKey = user ? `${redisHashTag(db.redis, user)}:${mailbox}` : false; const cacheKey = mailboxKey ? `${type || 'total'}:${mailboxKey}` : false; try { @@ -894,6 +903,8 @@ module.exports = { getHostname, getWildcardAddresses, redisConfig, + isRedisCluster, + redisHashTag, checkRangeQuery, decodeAddresses, flatAddresses, diff --git a/lib/user-handler.js b/lib/user-handler.js index dbdf67c6..1d73f5b9 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -3260,10 +3260,10 @@ class UserHandler { if (resetKeys.has(key)) { let prefix = resetKeys.get(key); - flushKeys.push(`${prefix}:{${user.toString()}}`); + flushKeys.push(`${prefix}:${tools.redisHashTag(this.redis, user.toString())}`); } if (key === 'imapMaxConnections') { - flushHKeys.push({ key: `lim:imap:{${user.toString()}}`, value: user.toString() }); + flushHKeys.push({ key: `lim:imap:${tools.redisHashTag(this.redis, user.toString())}`, value: user.toString() }); } if (key === 'suspended' && data.suspended) { diff --git a/pop3.js b/pop3.js index 88b0000f..5b51dbb5 100644 --- a/pop3.js +++ b/pop3.js @@ -132,7 +132,7 @@ const serverOptions = { } session.user.mailbox = mailbox._id; - let messageIndexKey = `pxm:{${session.user.id.toString()}}`; + let messageIndexKey = `pxm:${tools.redisHashTag(db.redis, session.user.id.toString())}`; db.redis.hget(messageIndexKey, mailbox._id.toString(), (err, messageIndex) => { let lastIndex = !err && messageIndex; @@ -226,7 +226,7 @@ const serverOptions = { return callback(err); } - let counterKey = `pdw:{${session.user.id.toString()}}`; + let counterKey = `pdw:${tools.redisHashTag(db.redis, session.user.id.toString())}`; messageHandler.counters.ttlcounter(counterKey, 0, limit, false, (err, res) => { if (err) { From b2ea96d167896fdcbc75179c3068a086af5f542a Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Tue, 26 May 2026 09:35:57 +0300 Subject: [PATCH 32/32] fix imap notifier --- lib/imap-notifier.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/imap-notifier.js b/lib/imap-notifier.js index 0bfad7ff..c969edbe 100644 --- a/lib/imap-notifier.js +++ b/lib/imap-notifier.js @@ -37,7 +37,9 @@ class ImapNotifier extends EventEmitter { super(); this.database = options.database; - this.redis = options.redis || new Redis(tools.redisConfig(config.dbs.redis)); + + const redisConfig = options.redis ? false : tools.redisConfig(config.dbs.redis); + this.redis = options.redis || (redisConfig.cluster ? new Redis.Cluster(redisConfig.nodes, redisConfig.options) : new Redis(redisConfig)); errors.registerRedisErrorLogger(this.redis, { role: 'imap-notifier' }); // if separate redis will add the appropriate role this.counters = counters(this.redis); this.settingsHandler = options.settingsHandler || new SettingsHandler({ db: this.database });