emailengine-app 2.63.4 → 2.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/data/google-crawlers.json +1 -1
- package/eslint.config.js +2 -0
- package/lib/account.js +6 -2
- package/lib/consts.js +17 -1
- package/lib/email-client/gmail/gmail-api.js +1 -12
- package/lib/email-client/imap-client.js +5 -3
- package/lib/email-client/outlook/graph-api.js +7 -13
- package/lib/email-client/outlook-client.js +363 -167
- package/lib/imapproxy/imap-server.js +1 -0
- package/lib/oauth/gmail.js +12 -1
- package/lib/oauth/pubsub/google.js +253 -85
- package/lib/oauth2-apps.js +554 -377
- package/lib/routes-ui.js +186 -91
- package/lib/schemas.js +18 -1
- package/lib/ui-routes/account-routes.js +1 -1
- package/lib/ui-routes/admin-entities-routes.js +3 -3
- package/lib/ui-routes/oauth-routes.js +9 -3
- package/package.json +9 -9
- package/sbom.json +1 -1
- package/server.js +54 -22
- package/static/licenses.html +27 -27
- package/translations/de.mo +0 -0
- package/translations/de.po +54 -42
- package/translations/en.mo +0 -0
- package/translations/en.po +55 -43
- package/translations/et.mo +0 -0
- package/translations/et.po +54 -42
- package/translations/fr.mo +0 -0
- package/translations/fr.po +54 -42
- package/translations/ja.mo +0 -0
- package/translations/ja.po +54 -42
- package/translations/messages.pot +74 -52
- package/translations/nl.mo +0 -0
- package/translations/nl.po +54 -42
- package/translations/pl.mo +0 -0
- package/translations/pl.po +54 -42
- package/views/config/oauth/app.hbs +12 -0
- package/views/config/oauth/index.hbs +2 -0
- package/views/config/oauth/subscriptions.hbs +175 -0
- package/views/error.hbs +4 -4
- package/views/partials/oauth_tabs.hbs +8 -0
- package/workers/api.js +174 -96
- package/workers/documents.js +1 -0
- package/workers/imap.js +30 -47
- package/workers/smtp.js +1 -0
- package/workers/submit.js +1 -0
- package/workers/webhooks.js +42 -30
package/workers/api.js
CHANGED
|
@@ -163,7 +163,8 @@ const {
|
|
|
163
163
|
googleSubscriptionNameSchema,
|
|
164
164
|
messageReferenceSchema,
|
|
165
165
|
idempotencyKeySchema,
|
|
166
|
-
headerTimeoutSchema
|
|
166
|
+
headerTimeoutSchema,
|
|
167
|
+
pubSubErrorSchema
|
|
167
168
|
} = require('../lib/schemas');
|
|
168
169
|
|
|
169
170
|
const OAuth2ProviderSchema = Joi.string()
|
|
@@ -180,6 +181,21 @@ const AccountTypeSchema = Joi.string()
|
|
|
180
181
|
.required()
|
|
181
182
|
.label('AccountType');
|
|
182
183
|
|
|
184
|
+
function flattenOAuthAppMeta(app) {
|
|
185
|
+
if (!app.meta) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
let authFlag = app.meta.authFlag;
|
|
189
|
+
let pubSubFlag = app.meta.pubSubFlag;
|
|
190
|
+
delete app.meta;
|
|
191
|
+
if (authFlag && authFlag.message) {
|
|
192
|
+
app.lastError = { response: authFlag.message };
|
|
193
|
+
}
|
|
194
|
+
if (pubSubFlag && pubSubFlag.message) {
|
|
195
|
+
app.pubSubError = { message: pubSubFlag.message, description: pubSubFlag.description || null };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
183
199
|
const SUPPORTED_LOCALES = locales.map(locale => locale.locale);
|
|
184
200
|
|
|
185
201
|
const FLAG_SORT_ORDER = ['\\Inbox', '\\Flagged', '\\Sent', '\\Drafts', '\\All', '\\Archive', '\\Junk', '\\Trash'];
|
|
@@ -362,6 +378,7 @@ async function call(message, transferList) {
|
|
|
362
378
|
err.statusCode = 504;
|
|
363
379
|
err.code = 'Timeout';
|
|
364
380
|
err.ttl = ttl;
|
|
381
|
+
callQueue.delete(mid);
|
|
365
382
|
reject(err);
|
|
366
383
|
}, ttl);
|
|
367
384
|
|
|
@@ -1557,11 +1574,12 @@ Include your token in requests using one of these methods:
|
|
|
1557
1574
|
let checkKey = `${REDIS_PREFIX}test:${Date.now()}`;
|
|
1558
1575
|
let expected = crypto.randomBytes(8).toString('hex');
|
|
1559
1576
|
let res = await redis.multi().set(checkKey, expected).get(checkKey).del(checkKey).exec();
|
|
1560
|
-
if (res[1] && res[1][1] === expected && res[2] && res[2][1] === 1) {
|
|
1561
|
-
|
|
1577
|
+
if (!(res[1] && res[1][1] === expected && res[2] && res[2][1] === 1)) {
|
|
1578
|
+
let error = Boom.boomify(new Error('Database check failed'), { statusCode: 500 });
|
|
1579
|
+
throw error;
|
|
1562
1580
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1581
|
+
|
|
1582
|
+
return { success: true };
|
|
1565
1583
|
},
|
|
1566
1584
|
options: {
|
|
1567
1585
|
description: 'Health check',
|
|
@@ -1570,6 +1588,99 @@ Include your token in requests using one of these methods:
|
|
|
1570
1588
|
}
|
|
1571
1589
|
});
|
|
1572
1590
|
|
|
1591
|
+
server.route({
|
|
1592
|
+
method: 'GET',
|
|
1593
|
+
path: '/v1/pubsub/status',
|
|
1594
|
+
|
|
1595
|
+
async handler(request) {
|
|
1596
|
+
try {
|
|
1597
|
+
let response = await oauth2Apps.list(request.query.page, request.query.pageSize, { pubsub: true });
|
|
1598
|
+
|
|
1599
|
+
let apps = response.apps.map(app => {
|
|
1600
|
+
flattenOAuthAppMeta(app);
|
|
1601
|
+
return { id: app.id, name: app.name || null, lastError: app.lastError || null, pubSubError: app.pubSubError || null };
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
return {
|
|
1605
|
+
total: response.total,
|
|
1606
|
+
page: response.page,
|
|
1607
|
+
pages: response.pages,
|
|
1608
|
+
apps
|
|
1609
|
+
};
|
|
1610
|
+
} catch (err) {
|
|
1611
|
+
request.logger.error({ msg: 'API request failed', err });
|
|
1612
|
+
if (Boom.isBoom(err)) {
|
|
1613
|
+
throw err;
|
|
1614
|
+
}
|
|
1615
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1616
|
+
if (err.code) {
|
|
1617
|
+
error.output.payload.code = err.code;
|
|
1618
|
+
}
|
|
1619
|
+
throw error;
|
|
1620
|
+
}
|
|
1621
|
+
},
|
|
1622
|
+
|
|
1623
|
+
options: {
|
|
1624
|
+
description: 'List Pub/Sub status',
|
|
1625
|
+
notes: 'Lists Pub/Sub enabled OAuth2 applications and their subscription status',
|
|
1626
|
+
tags: ['api', 'OAuth2 Applications'],
|
|
1627
|
+
|
|
1628
|
+
plugins: {},
|
|
1629
|
+
|
|
1630
|
+
auth: {
|
|
1631
|
+
strategy: 'api-token',
|
|
1632
|
+
mode: 'required'
|
|
1633
|
+
},
|
|
1634
|
+
cors: CORS_CONFIG,
|
|
1635
|
+
|
|
1636
|
+
validate: {
|
|
1637
|
+
options: {
|
|
1638
|
+
stripUnknown: false,
|
|
1639
|
+
abortEarly: false,
|
|
1640
|
+
convert: true
|
|
1641
|
+
},
|
|
1642
|
+
failAction,
|
|
1643
|
+
|
|
1644
|
+
query: Joi.object({
|
|
1645
|
+
page: Joi.number()
|
|
1646
|
+
.integer()
|
|
1647
|
+
.min(0)
|
|
1648
|
+
.max(1024 * 1024)
|
|
1649
|
+
.default(0)
|
|
1650
|
+
.example(0)
|
|
1651
|
+
.description('Page number (zero indexed, so use 0 for first page)')
|
|
1652
|
+
.label('PageNumber'),
|
|
1653
|
+
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
|
|
1654
|
+
}).label('PubSubStatusFilter')
|
|
1655
|
+
},
|
|
1656
|
+
|
|
1657
|
+
response: {
|
|
1658
|
+
schema: Joi.object({
|
|
1659
|
+
total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
|
|
1660
|
+
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
1661
|
+
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
1662
|
+
|
|
1663
|
+
apps: Joi.array()
|
|
1664
|
+
.items(
|
|
1665
|
+
Joi.object({
|
|
1666
|
+
id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
1667
|
+
name: Joi.string().allow(null).max(256).example('My Gmail App').description('Display name for the app'),
|
|
1668
|
+
lastError: Joi.object({
|
|
1669
|
+
response: Joi.string().example('Enable the Cloud Pub/Sub API').description('Setup error message')
|
|
1670
|
+
})
|
|
1671
|
+
.allow(null)
|
|
1672
|
+
.description('Setup error from ensurePubsub, if any')
|
|
1673
|
+
.label('PubSubSetupError'),
|
|
1674
|
+
pubSubError: pubSubErrorSchema.allow(null)
|
|
1675
|
+
}).label('PubSubAppStatus')
|
|
1676
|
+
)
|
|
1677
|
+
.label('PubSubAppStatusList')
|
|
1678
|
+
}).label('PubSubStatusResponse'),
|
|
1679
|
+
failAction: 'log'
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1573
1684
|
server.route({
|
|
1574
1685
|
method: 'GET',
|
|
1575
1686
|
path: '/redirect',
|
|
@@ -1936,6 +2047,10 @@ Include your token in requests using one of these methods:
|
|
|
1936
2047
|
|
|
1937
2048
|
const outlookSubscription = accountData.outlookSubscription;
|
|
1938
2049
|
|
|
2050
|
+
// Deduplicate lifecycle events within the same batch to prevent
|
|
2051
|
+
// concurrent handlers racing (e.g., two subscriptionRemoved entries)
|
|
2052
|
+
const seenLifecycleEvents = new Set();
|
|
2053
|
+
|
|
1939
2054
|
for (let entry of (request.payload && request.payload.value) || []) {
|
|
1940
2055
|
request.logger.debug({
|
|
1941
2056
|
msg: 'MS Graph subscription event',
|
|
@@ -1960,82 +2075,43 @@ Include your token in requests using one of these methods:
|
|
|
1960
2075
|
continue;
|
|
1961
2076
|
}
|
|
1962
2077
|
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
2078
|
+
// Route recognized lifecycle events to the IMAP worker
|
|
2079
|
+
// so the live client with its OAuth state handles them
|
|
2080
|
+
if (entry.lifecycleEvent === 'reauthorizationRequired' || entry.lifecycleEvent === 'subscriptionRemoved') {
|
|
2081
|
+
const dedupeKey = `${entry.lifecycleEvent}:${entry.subscriptionId}`;
|
|
2082
|
+
if (seenLifecycleEvents.has(dedupeKey)) {
|
|
2083
|
+
request.logger.debug({
|
|
2084
|
+
msg: 'Skipping duplicate lifecycle event in batch',
|
|
2085
|
+
lifecycleEvent: entry.lifecycleEvent,
|
|
2086
|
+
subscriptionId: entry.subscriptionId,
|
|
1969
2087
|
account: request.query.account
|
|
1970
2088
|
});
|
|
1971
|
-
|
|
1972
|
-
// Use the unified renewal method from OutlookClient
|
|
1973
|
-
// We need to create a client instance to call the renewal method
|
|
1974
|
-
const { OutlookClient } = require('../lib/email-client/outlook-client');
|
|
1975
|
-
const client = new OutlookClient(accountData, {
|
|
1976
|
-
redis,
|
|
1977
|
-
secret: await getSecret(),
|
|
1978
|
-
logger: request.logger
|
|
1979
|
-
});
|
|
1980
|
-
|
|
1981
|
-
try {
|
|
1982
|
-
// Force renewal when we get reauthorizationRequired
|
|
1983
|
-
const renewalResult = await client.renewSubscription(true);
|
|
1984
|
-
|
|
1985
|
-
if (renewalResult.success) {
|
|
1986
|
-
request.logger.info({
|
|
1987
|
-
msg: 'Successfully renewed subscription from lifecycle event',
|
|
1988
|
-
subscriptionId: outlookSubscription.id,
|
|
1989
|
-
account: request.query.account,
|
|
1990
|
-
newExpirationDateTime: renewalResult.expirationDateTime
|
|
1991
|
-
});
|
|
1992
|
-
} else {
|
|
1993
|
-
request.logger.error({
|
|
1994
|
-
msg: 'Failed to renew subscription from lifecycle event',
|
|
1995
|
-
subscriptionId: outlookSubscription.id,
|
|
1996
|
-
account: request.query.account,
|
|
1997
|
-
reason: renewalResult.reason,
|
|
1998
|
-
error: renewalResult.error
|
|
1999
|
-
});
|
|
2000
|
-
}
|
|
2001
|
-
} catch (err) {
|
|
2002
|
-
request.logger.error({
|
|
2003
|
-
msg: 'Exception while renewing subscription from lifecycle event',
|
|
2004
|
-
subscriptionId: outlookSubscription.id,
|
|
2005
|
-
account: request.query.account,
|
|
2006
|
-
err
|
|
2007
|
-
});
|
|
2008
|
-
} finally {
|
|
2009
|
-
// Clean up client instance
|
|
2010
|
-
if (client && typeof client.close === 'function') {
|
|
2011
|
-
try {
|
|
2012
|
-
await client.close();
|
|
2013
|
-
} catch (cleanupErr) {
|
|
2014
|
-
request.logger.debug({
|
|
2015
|
-
msg: 'Error closing client after lifecycle renewal',
|
|
2016
|
-
account: request.query.account,
|
|
2017
|
-
err: cleanupErr
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
break;
|
|
2089
|
+
continue;
|
|
2024
2090
|
}
|
|
2091
|
+
seenLifecycleEvents.add(dedupeKey);
|
|
2025
2092
|
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2093
|
+
request.logger.info({
|
|
2094
|
+
msg: 'Received lifecycle event',
|
|
2095
|
+
lifecycleEvent: entry.lifecycleEvent,
|
|
2096
|
+
subscriptionId: outlookSubscription.id,
|
|
2097
|
+
account: request.query.account
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
// Fire-and-forget: return HTTP 202 immediately so Microsoft
|
|
2101
|
+
// does not time out the lifecycle webhook delivery
|
|
2102
|
+
call({
|
|
2103
|
+
cmd: 'subscriptionLifecycle',
|
|
2104
|
+
account: request.query.account,
|
|
2105
|
+
event: entry.lifecycleEvent,
|
|
2106
|
+
timeout: consts.OUTLOOK_SUBSCRIPTION_LOCK_TTL
|
|
2107
|
+
}).catch(err => {
|
|
2108
|
+
request.logger.error({
|
|
2109
|
+
msg: 'Failed to handle lifecycle event via worker',
|
|
2110
|
+
account: request.query.account,
|
|
2111
|
+
lifecycleEvent: entry.lifecycleEvent,
|
|
2112
|
+
err
|
|
2036
2113
|
});
|
|
2037
|
-
|
|
2038
|
-
}
|
|
2114
|
+
});
|
|
2039
2115
|
}
|
|
2040
2116
|
}
|
|
2041
2117
|
|
|
@@ -4876,13 +4952,7 @@ Include your token in requests using one of these methods:
|
|
|
4876
4952
|
delete app.app;
|
|
4877
4953
|
}
|
|
4878
4954
|
|
|
4879
|
-
|
|
4880
|
-
let authFlag = app.meta.authFlag;
|
|
4881
|
-
delete app.meta;
|
|
4882
|
-
if (authFlag && authFlag.message) {
|
|
4883
|
-
app.lastError = { response: authFlag.message };
|
|
4884
|
-
}
|
|
4885
|
-
}
|
|
4955
|
+
flattenOAuthAppMeta(app);
|
|
4886
4956
|
}
|
|
4887
4957
|
|
|
4888
4958
|
return response;
|
|
@@ -5002,7 +5072,8 @@ Include your token in requests using one of these methods:
|
|
|
5002
5072
|
.example('******')
|
|
5003
5073
|
.description('PEM formatted service secret for 2-legged OAuth2 applications. Actual value is not revealed.'),
|
|
5004
5074
|
|
|
5005
|
-
lastError: lastErrorSchema.allow(null)
|
|
5075
|
+
lastError: lastErrorSchema.allow(null),
|
|
5076
|
+
pubSubError: pubSubErrorSchema.allow(null)
|
|
5006
5077
|
}).label('OAuth2ResponseItem')
|
|
5007
5078
|
)
|
|
5008
5079
|
.label('OAuth2Entries')
|
|
@@ -5035,13 +5106,7 @@ Include your token in requests using one of these methods:
|
|
|
5035
5106
|
delete app.app;
|
|
5036
5107
|
}
|
|
5037
5108
|
|
|
5038
|
-
|
|
5039
|
-
let authFlag = app.meta.authFlag;
|
|
5040
|
-
delete app.meta;
|
|
5041
|
-
if (authFlag && authFlag.message) {
|
|
5042
|
-
app.lastError = { response: authFlag.message };
|
|
5043
|
-
}
|
|
5044
|
-
}
|
|
5109
|
+
flattenOAuthAppMeta(app);
|
|
5045
5110
|
|
|
5046
5111
|
return app;
|
|
5047
5112
|
} catch (err) {
|
|
@@ -5145,7 +5210,8 @@ Include your token in requests using one of these methods:
|
|
|
5145
5210
|
.example(12)
|
|
5146
5211
|
.description('The number of accounts registered with this application. Not available for legacy apps.'),
|
|
5147
5212
|
|
|
5148
|
-
lastError: lastErrorSchema.allow(null)
|
|
5213
|
+
lastError: lastErrorSchema.allow(null),
|
|
5214
|
+
pubSubError: pubSubErrorSchema.allow(null)
|
|
5149
5215
|
}).label('ApplicationResponse'),
|
|
5150
5216
|
failAction: 'log'
|
|
5151
5217
|
}
|
|
@@ -5160,8 +5226,9 @@ Include your token in requests using one of these methods:
|
|
|
5160
5226
|
try {
|
|
5161
5227
|
let result = await oauth2Apps.create(request.payload);
|
|
5162
5228
|
|
|
5163
|
-
if (result && result.pubsubUpdates && result.pubsubUpdates.
|
|
5229
|
+
if (result && result.pubsubUpdates && Object.keys(result.pubsubUpdates).length > 0) {
|
|
5164
5230
|
await call({ cmd: 'googlePubSub', app: result.id });
|
|
5231
|
+
delete result.pubsubUpdates;
|
|
5165
5232
|
}
|
|
5166
5233
|
|
|
5167
5234
|
return result;
|
|
@@ -5220,8 +5287,9 @@ Include your token in requests using one of these methods:
|
|
|
5220
5287
|
try {
|
|
5221
5288
|
let result = await oauth2Apps.update(request.params.app, request.payload);
|
|
5222
5289
|
|
|
5223
|
-
if (result && result.pubsubUpdates && result.pubsubUpdates.
|
|
5290
|
+
if (result && result.pubsubUpdates && Object.keys(result.pubsubUpdates).length > 0) {
|
|
5224
5291
|
await call({ cmd: 'googlePubSub', app: result.id });
|
|
5292
|
+
delete result.pubsubUpdates;
|
|
5225
5293
|
}
|
|
5226
5294
|
|
|
5227
5295
|
return result;
|
|
@@ -5369,7 +5437,15 @@ Include your token in requests using one of these methods:
|
|
|
5369
5437
|
|
|
5370
5438
|
async handler(request) {
|
|
5371
5439
|
try {
|
|
5372
|
-
|
|
5440
|
+
let result = await oauth2Apps.del(request.params.app);
|
|
5441
|
+
|
|
5442
|
+
try {
|
|
5443
|
+
await call({ cmd: 'googlePubSubRemove', app: request.params.app });
|
|
5444
|
+
} catch (err) {
|
|
5445
|
+
request.logger.error({ msg: 'Failed to notify workers about OAuth2 app deletion', err, app: request.params.app });
|
|
5446
|
+
}
|
|
5447
|
+
|
|
5448
|
+
return result;
|
|
5373
5449
|
} catch (err) {
|
|
5374
5450
|
request.logger.error({ msg: 'API request failed', err });
|
|
5375
5451
|
if (Boom.isBoom(err)) {
|
|
@@ -6942,11 +7018,13 @@ ${now}`,
|
|
|
6942
7018
|
// Replace error with friendly HTML
|
|
6943
7019
|
const error = response;
|
|
6944
7020
|
const ctx = {
|
|
7021
|
+
statusCode: error.output.statusCode,
|
|
6945
7022
|
message:
|
|
6946
7023
|
error.output.statusCode === 404
|
|
6947
7024
|
? request.app.gt.gettext('Requested page not found')
|
|
6948
7025
|
: (error.output && error.output.payload && error.output.payload.message) || request.app.gt.gettext('Something went wrong'),
|
|
6949
|
-
details: error.output && error.output.payload && error.output.payload.details
|
|
7026
|
+
details: error.output && error.output.payload && error.output.payload.details,
|
|
7027
|
+
templateLocale: request.app.locale
|
|
6950
7028
|
};
|
|
6951
7029
|
|
|
6952
7030
|
if (error.output && error.output.payload) {
|
package/workers/documents.js
CHANGED
package/workers/imap.js
CHANGED
|
@@ -78,10 +78,6 @@ class ConnectionHandler {
|
|
|
78
78
|
this.mids = 0;
|
|
79
79
|
|
|
80
80
|
this.accounts = new Map();
|
|
81
|
-
|
|
82
|
-
// Reconnection metrics tracking
|
|
83
|
-
this.reconnectMetrics = new Map(); // Track metrics per account
|
|
84
|
-
this.metricsWindow = 60000; // 1-minute window
|
|
85
81
|
}
|
|
86
82
|
|
|
87
83
|
async init() {
|
|
@@ -639,6 +635,34 @@ class ConnectionHandler {
|
|
|
639
635
|
return await accountData.connection.externalNotify(message);
|
|
640
636
|
}
|
|
641
637
|
|
|
638
|
+
async subscriptionLifecycle(message) {
|
|
639
|
+
if (!this.accounts.has(message.account)) {
|
|
640
|
+
throw NO_ACTIVE_HANDLER_RESP_ERR;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
let accountData = this.accounts.get(message.account);
|
|
644
|
+
if (!accountData.connection) {
|
|
645
|
+
throw NO_ACTIVE_HANDLER_RESP_ERR;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
let connection = accountData.connection;
|
|
649
|
+
|
|
650
|
+
switch (message.event) {
|
|
651
|
+
case 'reauthorizationRequired':
|
|
652
|
+
return await connection.renewSubscription({ force: true });
|
|
653
|
+
|
|
654
|
+
case 'subscriptionRemoved':
|
|
655
|
+
// Clear stored subscription since MS deleted it server-side, under lock
|
|
656
|
+
logger.info({ msg: 'Handling subscriptionRemoved lifecycle event', account: message.account });
|
|
657
|
+
await connection.ensureSubscription({ clearExisting: true });
|
|
658
|
+
return true;
|
|
659
|
+
|
|
660
|
+
default:
|
|
661
|
+
logger.warn({ msg: 'Unknown subscription lifecycle event', event: message.event, account: message.account });
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
642
666
|
async getQuota(message) {
|
|
643
667
|
if (!this.accounts.has(message.account)) {
|
|
644
668
|
throw NO_ACTIVE_HANDLER_RESP_ERR;
|
|
@@ -725,49 +749,6 @@ class ConnectionHandler {
|
|
|
725
749
|
};
|
|
726
750
|
}
|
|
727
751
|
|
|
728
|
-
/**
|
|
729
|
-
* Track reconnection attempts for monitoring (without blocking)
|
|
730
|
-
* @param {string} account - Account identifier
|
|
731
|
-
*/
|
|
732
|
-
trackReconnection(account) {
|
|
733
|
-
const now = Date.now();
|
|
734
|
-
const metrics = this.reconnectMetrics.get(account) || {
|
|
735
|
-
attempts: [],
|
|
736
|
-
warnings: 0
|
|
737
|
-
};
|
|
738
|
-
|
|
739
|
-
// Clean old attempts outside window
|
|
740
|
-
metrics.attempts = metrics.attempts.filter(t => now - t < this.metricsWindow);
|
|
741
|
-
metrics.attempts.push(now);
|
|
742
|
-
|
|
743
|
-
// Log warning if excessive reconnections
|
|
744
|
-
if (metrics.attempts.length > 20) {
|
|
745
|
-
// More than 20 per minute
|
|
746
|
-
metrics.warnings++;
|
|
747
|
-
logger.warn({
|
|
748
|
-
msg: 'Excessive reconnection rate detected',
|
|
749
|
-
account,
|
|
750
|
-
rate: `${metrics.attempts.length}/min`,
|
|
751
|
-
totalWarnings: metrics.warnings
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
// Emit metrics for monitoring/alerting
|
|
755
|
-
try {
|
|
756
|
-
parentPort.postMessage({
|
|
757
|
-
cmd: 'metrics',
|
|
758
|
-
key: 'imap.reconnect.excessive',
|
|
759
|
-
method: 'inc',
|
|
760
|
-
args: [1],
|
|
761
|
-
meta: { account }
|
|
762
|
-
});
|
|
763
|
-
} catch (err) {
|
|
764
|
-
logger.error({ msg: 'Failed to send metrics', err });
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
this.reconnectMetrics.set(account, metrics);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
752
|
async getAttachment(message) {
|
|
772
753
|
if (!this.accounts.has(message.account)) {
|
|
773
754
|
throw NO_ACTIVE_HANDLER_RESP_ERR;
|
|
@@ -892,6 +873,7 @@ class ConnectionHandler {
|
|
|
892
873
|
case 'uploadMessage':
|
|
893
874
|
case 'subconnections':
|
|
894
875
|
case 'externalNotify':
|
|
876
|
+
case 'subscriptionLifecycle':
|
|
895
877
|
case 'listSignatures':
|
|
896
878
|
return await this[message.cmd](message);
|
|
897
879
|
|
|
@@ -940,6 +922,7 @@ class ConnectionHandler {
|
|
|
940
922
|
err.statusCode = 504;
|
|
941
923
|
err.code = 'Timeout';
|
|
942
924
|
err.ttl = ttl;
|
|
925
|
+
this.callQueue.delete(mid);
|
|
943
926
|
reject(err);
|
|
944
927
|
}, ttl);
|
|
945
928
|
|
package/workers/smtp.js
CHANGED
package/workers/submit.js
CHANGED
package/workers/webhooks.js
CHANGED
|
@@ -67,6 +67,7 @@ async function call(message, transferList) {
|
|
|
67
67
|
err.statusCode = 504;
|
|
68
68
|
err.code = 'Timeout';
|
|
69
69
|
err.ttl = ttl;
|
|
70
|
+
callQueue.delete(mid);
|
|
70
71
|
reject(err);
|
|
71
72
|
}, ttl);
|
|
72
73
|
|
|
@@ -117,6 +118,14 @@ async function onCommand(command) {
|
|
|
117
118
|
case 'googlePubSub':
|
|
118
119
|
await googlePubSub.update(command.app);
|
|
119
120
|
return true;
|
|
121
|
+
case 'googlePubSubRemove':
|
|
122
|
+
googlePubSub.remove(command.app);
|
|
123
|
+
return true;
|
|
124
|
+
case 'close':
|
|
125
|
+
clearTimeout(startRetryTimer);
|
|
126
|
+
googlePubSub.stopAll();
|
|
127
|
+
await notifyWorker.close(true);
|
|
128
|
+
return true;
|
|
120
129
|
default:
|
|
121
130
|
logger.debug({ msg: 'Unhandled command', command });
|
|
122
131
|
return 999;
|
|
@@ -132,6 +141,18 @@ setInterval(() => {
|
|
|
132
141
|
}
|
|
133
142
|
}, 10 * 1000).unref();
|
|
134
143
|
|
|
144
|
+
// Clean up Pub/Sub instances when parent port closes
|
|
145
|
+
parentPort.on('close', () => {
|
|
146
|
+
clearTimeout(startRetryTimer);
|
|
147
|
+
googlePubSub.stopAll();
|
|
148
|
+
// notifyWorker.close() may throw synchronously if not yet initialized
|
|
149
|
+
try {
|
|
150
|
+
notifyWorker.close(true).catch(() => {});
|
|
151
|
+
} catch {
|
|
152
|
+
// ignore
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
135
156
|
// Send initial ready signal
|
|
136
157
|
parentPort.postMessage({ cmd: 'ready' });
|
|
137
158
|
|
|
@@ -461,28 +482,6 @@ const notifyWorker = new Worker(
|
|
|
461
482
|
status: 'success'
|
|
462
483
|
});
|
|
463
484
|
} catch (err) {
|
|
464
|
-
/*
|
|
465
|
-
// do not disable by default
|
|
466
|
-
if (err.status === 410) {
|
|
467
|
-
// disable webhook
|
|
468
|
-
logger.error({
|
|
469
|
-
msg: 'Webhooks were disabled by server',
|
|
470
|
-
action: 'webhook',
|
|
471
|
-
queue: job.queue.name,
|
|
472
|
-
code: 'disabled_by_server',
|
|
473
|
-
job: job.id,
|
|
474
|
-
webhooks,
|
|
475
|
-
accountWebhooks: !!accountWebhooks,
|
|
476
|
-
event: job.name,
|
|
477
|
-
status: err.status,
|
|
478
|
-
account: job.data.account,
|
|
479
|
-
route: customRoute && customRoute.id,
|
|
480
|
-
err
|
|
481
|
-
});
|
|
482
|
-
await settings.set('webhooksEnabled', false);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
*/
|
|
486
485
|
logger.error({
|
|
487
486
|
msg: 'Failed posting webhook',
|
|
488
487
|
action: 'webhook',
|
|
@@ -606,13 +605,26 @@ notifyWorker.on('failed', async job => {
|
|
|
606
605
|
});
|
|
607
606
|
});
|
|
608
607
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
608
|
+
let startRetryTimer = null;
|
|
609
|
+
|
|
610
|
+
(function startGooglePubSub(attempt) {
|
|
611
|
+
googlePubSub
|
|
612
|
+
.start()
|
|
613
|
+
.then(() => {
|
|
614
|
+
logger.info({ msg: 'Started processing Google pub/sub' });
|
|
615
|
+
})
|
|
616
|
+
.catch(err => {
|
|
617
|
+
let maxNormalAttempts = 20;
|
|
618
|
+
let delay;
|
|
619
|
+
if (attempt < maxNormalAttempts) {
|
|
620
|
+
delay = Math.min(5000 * Math.pow(2, Math.min(attempt, 10)), 60000);
|
|
621
|
+
logger.error({ msg: 'Failed to start processing Google pub/sub', err, attempt: attempt + 1, retryMs: delay });
|
|
622
|
+
} else {
|
|
623
|
+
delay = 5 * 60 * 1000;
|
|
624
|
+
logger.warn({ msg: 'Failed to start processing Google pub/sub (reduced frequency)', err, attempt: attempt + 1, retryMs: delay });
|
|
625
|
+
}
|
|
626
|
+
startRetryTimer = setTimeout(() => startGooglePubSub(attempt + 1), delay);
|
|
627
|
+
});
|
|
628
|
+
})(0);
|
|
617
629
|
|
|
618
630
|
logger.info({ msg: 'Started Webhooks worker thread', version: packageData.version });
|