emailengine-app 2.63.4 → 2.65.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/.github/workflows/test.yml +4 -0
- package/CHANGELOG.md +70 -0
- package/copy-static-files.sh +1 -1
- package/data/google-crawlers.json +1 -1
- package/eslint.config.js +2 -0
- package/lib/account.js +13 -9
- package/lib/api-routes/account-routes.js +7 -1
- 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 +9 -15
- package/lib/email-client/outlook-client.js +406 -177
- package/lib/export.js +17 -0
- package/lib/imapproxy/imap-server.js +3 -2
- package/lib/oauth/gmail.js +12 -1
- package/lib/oauth/outlook.js +99 -1
- package/lib/oauth/pubsub/google.js +253 -85
- package/lib/oauth2-apps.js +620 -389
- package/lib/outbox.js +1 -1
- package/lib/routes-ui.js +193 -238
- package/lib/schemas.js +189 -12
- package/lib/ui-routes/account-routes.js +7 -2
- package/lib/ui-routes/admin-entities-routes.js +3 -3
- package/lib/ui-routes/oauth-routes.js +27 -175
- package/package.json +21 -21
- package/sbom.json +1 -1
- package/server.js +54 -22
- package/static/licenses.html +30 -90
- 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 +93 -71
- 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/edit.hbs +2 -0
- package/views/config/oauth/index.hbs +4 -1
- package/views/config/oauth/new.hbs +2 -0
- package/views/config/oauth/subscriptions.hbs +175 -0
- package/views/error.hbs +4 -4
- package/views/partials/oauth_form.hbs +179 -4
- package/views/partials/oauth_tabs.hbs +8 -0
- package/views/partials/scope_info.hbs +10 -0
- package/workers/api.js +174 -96
- package/workers/documents.js +1 -0
- package/workers/export.js +6 -2
- package/workers/imap.js +33 -49
- package/workers/smtp.js +1 -0
- package/workers/submit.js +1 -0
- package/workers/webhooks.js +42 -30
package/lib/export.js
CHANGED
|
@@ -242,6 +242,7 @@ class Export {
|
|
|
242
242
|
},
|
|
243
243
|
created: toIsoDate(data.created),
|
|
244
244
|
expiresAt: toIsoDate(data.expiresAt),
|
|
245
|
+
truncated: data.truncated === '1' || undefined,
|
|
245
246
|
error: data.error || null
|
|
246
247
|
};
|
|
247
248
|
|
|
@@ -322,6 +323,15 @@ class Export {
|
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
325
|
|
|
326
|
+
static async startProcessing(account, exportId) {
|
|
327
|
+
const exportKey = getExportKey(account, exportId);
|
|
328
|
+
const maxAge = await getExportMaxAge();
|
|
329
|
+
const ttl = Math.ceil(maxAge / 1000);
|
|
330
|
+
const newExpiresAt = Date.now() + maxAge;
|
|
331
|
+
|
|
332
|
+
await redis.multi().hmset(exportKey, { status: 'processing', phase: 'indexing', expiresAt: newExpiresAt }).expire(exportKey, ttl).exec();
|
|
333
|
+
}
|
|
334
|
+
|
|
325
335
|
static async queueMessage(account, exportId, messageInfo) {
|
|
326
336
|
const queueKey = getExportQueueKey(account, exportId);
|
|
327
337
|
const exportKey = getExportKey(account, exportId);
|
|
@@ -415,6 +425,13 @@ class Export {
|
|
|
415
425
|
logger.error({ msg: 'Export failed', account, exportId, error });
|
|
416
426
|
}
|
|
417
427
|
|
|
428
|
+
static async deleteFully(account, exportId) {
|
|
429
|
+
const exportKey = getExportKey(account, exportId);
|
|
430
|
+
const queueKey = getExportQueueKey(account, exportId);
|
|
431
|
+
|
|
432
|
+
await redis.multi().del(exportKey).del(queueKey).srem(ACTIVE_EXPORTS_KEY, `${account}:${exportId}`).exec();
|
|
433
|
+
}
|
|
434
|
+
|
|
418
435
|
static async markInterruptedAsFailed() {
|
|
419
436
|
const activeExports = await redis.smembers(ACTIVE_EXPORTS_KEY);
|
|
420
437
|
|
|
@@ -4,7 +4,7 @@ const { parentPort } = require('worker_threads');
|
|
|
4
4
|
|
|
5
5
|
const config = require('@zone-eu/wild-config');
|
|
6
6
|
const logger = require('../logger');
|
|
7
|
-
const { oauth2Apps, oauth2ProviderData } = require('../oauth2-apps');
|
|
7
|
+
const { oauth2Apps, oauth2ProviderData, isApiBasedApp } = require('../oauth2-apps');
|
|
8
8
|
|
|
9
9
|
const { getDuration, getBoolean, resolveCredentials, hasEnvValue, readEnvValue, emitChangeEvent, loadTlsConfig } = require('../tools');
|
|
10
10
|
const { matchIp, getLocalAddress } = require('../utils/network');
|
|
@@ -48,6 +48,7 @@ async function call(message, transferList) {
|
|
|
48
48
|
err.statusCode = 504;
|
|
49
49
|
err.code = 'Timeout';
|
|
50
50
|
err.ttl = ttl;
|
|
51
|
+
callQueue.delete(mid);
|
|
51
52
|
reject(err);
|
|
52
53
|
}, ttl);
|
|
53
54
|
|
|
@@ -213,7 +214,7 @@ async function onAuth(auth, session) {
|
|
|
213
214
|
throw respErr;
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
if (accountData?._app
|
|
217
|
+
if (isApiBasedApp(accountData?._app)) {
|
|
217
218
|
let respErr = new Error('IMAP is not supported for API-based accounts');
|
|
218
219
|
respErr.authenticationFailed = true;
|
|
219
220
|
respErr.serverResponseCode = 'ACCOUNTDISABLED';
|
package/lib/oauth/gmail.js
CHANGED
|
@@ -481,7 +481,8 @@ class GmailOauth {
|
|
|
481
481
|
Authorization: `Bearer ${accessToken}`,
|
|
482
482
|
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
483
483
|
},
|
|
484
|
-
dispatcher: httpAgent.retry
|
|
484
|
+
dispatcher: httpAgent.retry,
|
|
485
|
+
signal: options.signal || undefined
|
|
485
486
|
};
|
|
486
487
|
|
|
487
488
|
if (payload && method !== 'get') {
|
|
@@ -535,6 +536,16 @@ class GmailOauth {
|
|
|
535
536
|
reqTime
|
|
536
537
|
};
|
|
537
538
|
|
|
539
|
+
// Capture Retry-After header for rate limiting (429) responses
|
|
540
|
+
if (res.status === 429) {
|
|
541
|
+
let retryAfterHeader = res.headers.get('retry-after');
|
|
542
|
+
if (retryAfterHeader) {
|
|
543
|
+
let parsed = parseInt(retryAfterHeader, 10);
|
|
544
|
+
err.retryAfter = isNaN(parsed) ? null : parsed;
|
|
545
|
+
err.oauthRequest.retryAfter = err.retryAfter;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
538
549
|
try {
|
|
539
550
|
err.oauthRequest.response = await res.json();
|
|
540
551
|
|
package/lib/oauth/outlook.js
CHANGED
|
@@ -174,7 +174,8 @@ class OutlookOauth {
|
|
|
174
174
|
this.logRaw = opts.logRaw;
|
|
175
175
|
this.logger = opts.logger;
|
|
176
176
|
|
|
177
|
-
this.
|
|
177
|
+
this.useClientCredentials = !!opts.useClientCredentials;
|
|
178
|
+
this.provider = opts.provider || 'outlook';
|
|
178
179
|
|
|
179
180
|
this.setFlag = opts.setFlag;
|
|
180
181
|
|
|
@@ -199,9 +200,18 @@ class OutlookOauth {
|
|
|
199
200
|
this.entraEndpoint = 'https://login.microsoftonline.com';
|
|
200
201
|
this.apiBase = 'https://graph.microsoft.com';
|
|
201
202
|
}
|
|
203
|
+
|
|
204
|
+
// Client credentials use a single .default scope; override whatever was set above
|
|
205
|
+
if (this.useClientCredentials) {
|
|
206
|
+
this.scopes = [`${this.apiBase}/.default`];
|
|
207
|
+
}
|
|
202
208
|
}
|
|
203
209
|
|
|
204
210
|
generateAuthUrl(opts) {
|
|
211
|
+
if (this.useClientCredentials) {
|
|
212
|
+
throw new Error('Client credentials flow does not support interactive authorization');
|
|
213
|
+
}
|
|
214
|
+
|
|
205
215
|
opts = opts || {};
|
|
206
216
|
|
|
207
217
|
const url = new URL(`${this.entraEndpoint}/${this.authority}/oauth2/v2.0/authorize`);
|
|
@@ -335,6 +345,10 @@ class OutlookOauth {
|
|
|
335
345
|
}
|
|
336
346
|
|
|
337
347
|
async refreshToken(opts) {
|
|
348
|
+
if (this.useClientCredentials) {
|
|
349
|
+
return this.getClientCredentialsToken();
|
|
350
|
+
}
|
|
351
|
+
|
|
338
352
|
opts = opts || {};
|
|
339
353
|
if (!opts.refreshToken) {
|
|
340
354
|
throw new Error('Refresh token not provided');
|
|
@@ -435,6 +449,90 @@ class OutlookOauth {
|
|
|
435
449
|
return responseJson;
|
|
436
450
|
}
|
|
437
451
|
|
|
452
|
+
async getClientCredentialsToken() {
|
|
453
|
+
const url = new URL(`${this.entraEndpoint}/${this.authority}/oauth2/v2.0/token`);
|
|
454
|
+
|
|
455
|
+
url.searchParams.set('client_id', this.clientId);
|
|
456
|
+
url.searchParams.set('client_secret', this.clientSecret);
|
|
457
|
+
url.searchParams.set('scope', this.scopes.join(' '));
|
|
458
|
+
url.searchParams.set('grant_type', 'client_credentials');
|
|
459
|
+
|
|
460
|
+
let requestUrl = url.origin + url.pathname;
|
|
461
|
+
let method = 'post';
|
|
462
|
+
|
|
463
|
+
const bodyString = url.searchParams.toString();
|
|
464
|
+
let res = await fetchCmd(requestUrl, {
|
|
465
|
+
method,
|
|
466
|
+
headers: {
|
|
467
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
468
|
+
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
469
|
+
},
|
|
470
|
+
body: bodyString,
|
|
471
|
+
dispatcher: httpAgent.retry
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
let responseJson;
|
|
475
|
+
try {
|
|
476
|
+
responseJson = await res.json();
|
|
477
|
+
} catch (err) {
|
|
478
|
+
if (this.logger) {
|
|
479
|
+
this.logger.error({ msg: 'Failed to retrieve JSON', err });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (this.logger) {
|
|
484
|
+
this.logger.info({
|
|
485
|
+
msg: 'OAuth2 authentication request',
|
|
486
|
+
action: 'oauth2Fetch',
|
|
487
|
+
fn: 'getClientCredentialsToken',
|
|
488
|
+
method,
|
|
489
|
+
url: requestUrl,
|
|
490
|
+
success: !!res.ok,
|
|
491
|
+
status: res.status,
|
|
492
|
+
request: formatFetchBody(url.searchParams, this.logRaw),
|
|
493
|
+
response: formatFetchResponse(responseJson, this.logRaw)
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (!res.ok) {
|
|
498
|
+
let err = new Error('Token request failed');
|
|
499
|
+
err.code = 'ETokenRefresh';
|
|
500
|
+
|
|
501
|
+
err.statusCode = res.status;
|
|
502
|
+
err.tokenRequest = {
|
|
503
|
+
url: requestUrl,
|
|
504
|
+
method,
|
|
505
|
+
authority: this.authority,
|
|
506
|
+
grant: 'client_credentials',
|
|
507
|
+
provider: this.provider,
|
|
508
|
+
status: res.status,
|
|
509
|
+
clientId: this.clientId,
|
|
510
|
+
scopes: this.scopes
|
|
511
|
+
};
|
|
512
|
+
try {
|
|
513
|
+
err.tokenRequest.response = responseJson || { error: 'Failed to parse response' };
|
|
514
|
+
|
|
515
|
+
if (EXPOSE_PARTIAL_SECRET_KEY_REGEX.test(err.tokenRequest.response?.error_description)) {
|
|
516
|
+
err.tokenRequest.clientSecret = formatPartialSecretKey(this.clientSecret);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let flag = checkForFlags(err.tokenRequest.response);
|
|
520
|
+
if (flag) {
|
|
521
|
+
await this.setFlag(flag);
|
|
522
|
+
}
|
|
523
|
+
} catch (e) {
|
|
524
|
+
// ignore
|
|
525
|
+
}
|
|
526
|
+
err.message = formatTokenError(this.provider, err.tokenRequest);
|
|
527
|
+
throw err;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// clear potential auth flag
|
|
531
|
+
await this.setFlag();
|
|
532
|
+
|
|
533
|
+
return responseJson;
|
|
534
|
+
}
|
|
535
|
+
|
|
438
536
|
async request(accessToken, url, method, payload, options) {
|
|
439
537
|
options = options || {};
|
|
440
538
|
|