emailengine-app 2.70.0 → 2.72.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/codeql.yml +3 -0
- package/.github/workflows/e2e.yml +56 -0
- package/.github/workflows/test.yml +81 -12
- package/.ncurc.js +20 -20
- package/CHANGELOG.md +25 -0
- package/Gruntfile.js +19 -23
- package/bin/emailengine.js +8 -1
- package/config/default.toml +5 -0
- package/config/e2e.toml +35 -0
- package/config/test.toml +5 -0
- package/data/google-crawlers.json +1 -1
- package/getswagger.sh +4 -0
- package/lib/account.js +31 -25
- package/lib/api-routes/message-routes.js +125 -121
- package/lib/auth-token.js +83 -0
- package/lib/delivery-error.js +62 -0
- package/lib/document-store.js +22 -1
- package/lib/email-client/base-client.js +3 -2
- package/lib/email-client/gmail-client.js +33 -1
- package/lib/email-client/imap/mailbox.js +2 -2
- package/lib/email-client/notification-handler.js +2 -2
- package/lib/export.js +12 -0
- package/lib/feature-flags.js +6 -0
- package/lib/imap-proxy-auth.js +81 -0
- package/lib/imapproxy/imap-server.js +8 -103
- package/lib/license-beacon.js +367 -0
- package/lib/logger.js +11 -1
- package/lib/oauth/gmail.js +3 -0
- package/lib/oauth/outlook.js +3 -0
- package/lib/oauth2-apps.js +100 -11
- package/lib/routes-ui.js +2 -1
- package/lib/smtp-auth.js +70 -0
- package/lib/sub-script.js +8 -2
- package/lib/tools.js +26 -2
- package/lib/ui-routes/admin-config-routes.js +4 -3
- package/lib/ui-routes/document-store-routes.js +7 -1
- package/package.json +30 -24
- package/playwright.config.js +45 -0
- package/sbom.json +1 -1
- package/server.js +30 -8
- package/static/licenses.html +108 -128
- package/test-coverage-plan.md +233 -0
- package/translations/de.mo +0 -0
- package/translations/de.po +154 -142
- package/translations/et.mo +0 -0
- package/translations/et.po +129 -131
- package/translations/fr.mo +0 -0
- package/translations/fr.po +133 -136
- package/translations/ja.mo +0 -0
- package/translations/ja.po +126 -129
- package/translations/messages.pot +37 -37
- package/translations/nl.mo +0 -0
- package/translations/nl.po +128 -130
- package/translations/pl.mo +0 -0
- package/translations/pl.po +125 -128
- package/views/dashboard.hbs +22 -0
- package/workers/api.js +22 -5
- package/workers/export.js +58 -43
- package/workers/smtp.js +5 -85
- package/workers/submit.js +2 -12
package/lib/oauth2-apps.js
CHANGED
|
@@ -852,8 +852,10 @@ class OAuth2AppsHandler {
|
|
|
852
852
|
return { id, deleted: false, accounts: 0 };
|
|
853
853
|
}
|
|
854
854
|
|
|
855
|
-
// Acquire the same lock used by ensurePubsub to prevent racing with recovery
|
|
856
|
-
|
|
855
|
+
// Acquire the same lock used by ensurePubsub to prevent racing with recovery. Mirror the
|
|
856
|
+
// deletion gate below (pubSubTopic || pubSubSubscription) so an app that only has a
|
|
857
|
+
// subscription recorded is still locked before its resources are deleted.
|
|
858
|
+
let needsPubSubLock = appData.pubSubTopic || appData.pubSubSubscription || appData.baseScopes === 'pubsub';
|
|
857
859
|
let delLock;
|
|
858
860
|
|
|
859
861
|
if (needsPubSubLock) {
|
|
@@ -867,11 +869,21 @@ class OAuth2AppsHandler {
|
|
|
867
869
|
logger.error({ msg: 'Failed to acquire lock for pubsub app deletion', lockKey, err });
|
|
868
870
|
throw err;
|
|
869
871
|
}
|
|
872
|
+
|
|
873
|
+
// Re-read under the lock so the ownership-flag deletion decision below acts on the
|
|
874
|
+
// current record, not the snapshot taken before the lock - a concurrent ensurePubsub /
|
|
875
|
+
// recovery run may have adopted or created a resource in the meantime. Keep the pre-lock
|
|
876
|
+
// snapshot if the app was deleted while waiting, so best-effort GCP cleanup still runs.
|
|
877
|
+
let lockedData = await this.get(id);
|
|
878
|
+
if (lockedData) {
|
|
879
|
+
appData = lockedData;
|
|
880
|
+
}
|
|
870
881
|
}
|
|
871
882
|
|
|
872
883
|
try {
|
|
873
|
-
if (appData.pubSubTopic) {
|
|
874
|
-
// try to delete
|
|
884
|
+
if (appData.pubSubTopic || appData.pubSubSubscription) {
|
|
885
|
+
// try to delete the Pub/Sub resources EmailEngine created (deleteTopic skips any
|
|
886
|
+
// resource marked unmanaged, i.e. adopted from a pre-existing GCP setup)
|
|
875
887
|
try {
|
|
876
888
|
await this.deleteTopic(appData);
|
|
877
889
|
} catch (err) {
|
|
@@ -1037,8 +1049,16 @@ class OAuth2AppsHandler {
|
|
|
1037
1049
|
throw new Error('Failed to get access token');
|
|
1038
1050
|
}
|
|
1039
1051
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1052
|
+
// Only delete resources EmailEngine provisioned. A `false` managed flag means the resource
|
|
1053
|
+
// was adopted from a pre-existing GCP setup and must be left in place. Legacy apps created
|
|
1054
|
+
// before ownership tracking have the flag undefined and are treated as managed (deleted),
|
|
1055
|
+
// preserving the prior behavior.
|
|
1056
|
+
if (appData.pubSubSubscriptionManaged !== false) {
|
|
1057
|
+
await this._deletePubSubResource(client, accessToken, appData, 'subscription', appData.pubSubSubscription);
|
|
1058
|
+
}
|
|
1059
|
+
if (appData.pubSubTopicManaged !== false) {
|
|
1060
|
+
await this._deletePubSubResource(client, accessToken, appData, 'topic', appData.pubSubTopic);
|
|
1061
|
+
}
|
|
1042
1062
|
}
|
|
1043
1063
|
|
|
1044
1064
|
async ensurePubsub(appData) {
|
|
@@ -1093,6 +1113,23 @@ class OAuth2AppsHandler {
|
|
|
1093
1113
|
}
|
|
1094
1114
|
|
|
1095
1115
|
try {
|
|
1116
|
+
// Re-read inside the lock so the "persist only if absent" guards below act on the
|
|
1117
|
+
// current state, not the caller's pre-lock snapshot (which can be stale after a
|
|
1118
|
+
// concurrent ensurePubsub run). Falls back to the passed-in data if the app was
|
|
1119
|
+
// deleted while waiting for the lock (preserves the prior proceed-anyway behavior).
|
|
1120
|
+
let current = (await this.get(appData.id)) || appData;
|
|
1121
|
+
|
|
1122
|
+
// Record a marker for a Pub/Sub resource that already existed in GCP (adopted, not
|
|
1123
|
+
// created by us): mark it unmanaged so deletion never removes it, and surface it in
|
|
1124
|
+
// results on first adoption so the create/update route restarts the pull worker. The
|
|
1125
|
+
// !current guard keeps re-runs no-ops (no redundant writes or restarts).
|
|
1126
|
+
const adoptExistingResource = async (key, value) => {
|
|
1127
|
+
if (!current[key]) {
|
|
1128
|
+
await this.update(appData.id, { [key]: value, [`${key}Managed`]: false }, { partial: true });
|
|
1129
|
+
results[key] = value;
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1096
1133
|
let client = await this.getClient(appData.id);
|
|
1097
1134
|
|
|
1098
1135
|
// Step 1. Get access token for service client
|
|
@@ -1109,6 +1146,9 @@ class OAuth2AppsHandler {
|
|
|
1109
1146
|
/*
|
|
1110
1147
|
{name: 'projects/...'}
|
|
1111
1148
|
*/
|
|
1149
|
+
|
|
1150
|
+
// Adopt a pre-existing topic so the Gmail watch can arm (kept unmanaged - we did not create it).
|
|
1151
|
+
await adoptExistingResource('pubSubTopic', topicName);
|
|
1112
1152
|
} catch (err) {
|
|
1113
1153
|
if (TRANSIENT_NETWORK_CODES.has(err.code)) {
|
|
1114
1154
|
logger.warn({ msg: 'Network error checking Pub/Sub topic', app: appData.id, code: err.code });
|
|
@@ -1149,7 +1189,9 @@ class OAuth2AppsHandler {
|
|
|
1149
1189
|
await this.update(
|
|
1150
1190
|
appData.id,
|
|
1151
1191
|
{
|
|
1152
|
-
pubSubTopic: topicName
|
|
1192
|
+
pubSubTopic: topicName,
|
|
1193
|
+
// EmailEngine created this topic, so it may delete it on app removal.
|
|
1194
|
+
pubSubTopicManaged: true
|
|
1153
1195
|
},
|
|
1154
1196
|
{ partial: true }
|
|
1155
1197
|
);
|
|
@@ -1169,8 +1211,9 @@ class OAuth2AppsHandler {
|
|
|
1169
1211
|
});
|
|
1170
1212
|
throw err;
|
|
1171
1213
|
case 409:
|
|
1172
|
-
// already exists
|
|
1214
|
+
// already exists (raced) - adopt it; mark unmanaged to be safe against deletion.
|
|
1173
1215
|
logger.info({ msg: 'Topic already exists', app: appData.id, topic: topicName });
|
|
1216
|
+
await adoptExistingResource('pubSubTopic', topicName);
|
|
1174
1217
|
break;
|
|
1175
1218
|
default:
|
|
1176
1219
|
throw err;
|
|
@@ -1193,12 +1236,44 @@ class OAuth2AppsHandler {
|
|
|
1193
1236
|
{name: 'projects/...'}
|
|
1194
1237
|
*/
|
|
1195
1238
|
|
|
1239
|
+
// Adopt a pre-existing subscription so the pull worker recognizes it (also stops the
|
|
1240
|
+
// recovery loop in lib/oauth/pubsub/google.js); kept unmanaged - we did not create it.
|
|
1241
|
+
await adoptExistingResource('pubSubSubscription', subscriptionName);
|
|
1242
|
+
|
|
1196
1243
|
// Patch existing subscription's expiration policy if it differs from desired
|
|
1197
1244
|
// When no explicit TTL is configured, use Google's default (31 days) for comparison
|
|
1198
1245
|
let effectivePolicy = desiredExpirationPolicy ?? { ttl: GMAIL_PUBSUB_DEFAULT_EXPIRATION_TTL };
|
|
1199
1246
|
let currentTtl = subscriptionData?.expirationPolicy?.ttl || null;
|
|
1200
1247
|
let desiredTtl = effectivePolicy.ttl || null;
|
|
1201
|
-
|
|
1248
|
+
|
|
1249
|
+
// Adopted (pre-existing, unmanaged) subscriptions belong to the customer - never
|
|
1250
|
+
// rewrite their lifecycle policy. A subscription is ours to manage only when it is
|
|
1251
|
+
// already recorded as managed (true) or predates ownership tracking (undefined);
|
|
1252
|
+
// a freshly adopted one (marker absent in the pre-lock snapshot) or an explicit
|
|
1253
|
+
// `false` flag is hands-off.
|
|
1254
|
+
let subscriptionManagedByUs = !!current.pubSubSubscription && current.pubSubSubscriptionManaged !== false;
|
|
1255
|
+
|
|
1256
|
+
if (!subscriptionManagedByUs && desiredExpirationPolicy !== null) {
|
|
1257
|
+
// Do not mutate the customer's subscription, but warn when an explicitly
|
|
1258
|
+
// configured expiration is longer than theirs: the subscription may expire on
|
|
1259
|
+
// its own and silently disable Gmail push.
|
|
1260
|
+
let toSeconds = ttl => {
|
|
1261
|
+
let n = ttl ? parseInt(ttl, 10) : NaN;
|
|
1262
|
+
return Number.isFinite(n) ? n : null;
|
|
1263
|
+
};
|
|
1264
|
+
let currentSeconds = toSeconds(currentTtl);
|
|
1265
|
+
let desiredSeconds = toSeconds(desiredTtl);
|
|
1266
|
+
// desiredSeconds === null means EmailEngine wants an indefinite subscription
|
|
1267
|
+
if (currentSeconds !== null && (desiredSeconds === null || currentSeconds < desiredSeconds)) {
|
|
1268
|
+
logger.warn({
|
|
1269
|
+
msg: 'Adopted Pub/Sub subscription expires sooner than the configured expiration; leaving it unchanged',
|
|
1270
|
+
app: appData.id,
|
|
1271
|
+
subscription: subscriptionName,
|
|
1272
|
+
currentTtl,
|
|
1273
|
+
desiredTtl
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
} else if (subscriptionManagedByUs && currentTtl !== desiredTtl) {
|
|
1202
1277
|
try {
|
|
1203
1278
|
await client.request(accessToken, subscriptionUrl, 'PATCH', {
|
|
1204
1279
|
subscription: { expirationPolicy: effectivePolicy },
|
|
@@ -1271,7 +1346,9 @@ class OAuth2AppsHandler {
|
|
|
1271
1346
|
await this.update(
|
|
1272
1347
|
appData.id,
|
|
1273
1348
|
{
|
|
1274
|
-
pubSubSubscription: subscriptionName
|
|
1349
|
+
pubSubSubscription: subscriptionName,
|
|
1350
|
+
// EmailEngine created this subscription, so it may delete it on app removal.
|
|
1351
|
+
pubSubSubscriptionManaged: true
|
|
1275
1352
|
},
|
|
1276
1353
|
{ partial: true }
|
|
1277
1354
|
);
|
|
@@ -1296,8 +1373,9 @@ class OAuth2AppsHandler {
|
|
|
1296
1373
|
});
|
|
1297
1374
|
throw err;
|
|
1298
1375
|
case 409:
|
|
1299
|
-
// already exists
|
|
1376
|
+
// already exists (raced) - adopt it; mark unmanaged to be safe against deletion.
|
|
1300
1377
|
logger.info({ msg: 'Subscription already exists', app: appData.id, subscription: subscriptionName });
|
|
1378
|
+
await adoptExistingResource('pubSubSubscription', subscriptionName);
|
|
1301
1379
|
break;
|
|
1302
1380
|
default:
|
|
1303
1381
|
throw err;
|
|
@@ -1350,6 +1428,17 @@ class OAuth2AppsHandler {
|
|
|
1350
1428
|
|
|
1351
1429
|
if (existingPolicy) {
|
|
1352
1430
|
logger.debug({ msg: 'Gmail publisher policy already exists', app: appData.id, topic: topicName });
|
|
1431
|
+
|
|
1432
|
+
// The required publisher binding is already in place (granted manually or by a
|
|
1433
|
+
// prior run). Record the marker so the Gmail watch can arm - it gates on this being
|
|
1434
|
+
// truthy. Store the same {members, role} object shape the create path uses. No
|
|
1435
|
+
// ownership flag: this marker has no deletion side effect (only read at the watch
|
|
1436
|
+
// gate). Only populate results (and write) on first adoption, so a re-run is a no-op
|
|
1437
|
+
// and does not trigger a redundant pull-worker restart.
|
|
1438
|
+
if (!current.pubSubIamPolicy) {
|
|
1439
|
+
results.iamPolicy = { members: [member], role };
|
|
1440
|
+
await this.update(appData.id, { pubSubIamPolicy: results.iamPolicy }, { partial: true });
|
|
1441
|
+
}
|
|
1353
1442
|
} else {
|
|
1354
1443
|
logger.debug({ msg: 'Granting access to Gmail publisher', app: appData.id, topic: topicName });
|
|
1355
1444
|
let existingBindings = (getIamPolicyRes && getIamPolicyRes.bindings) || [];
|
package/lib/routes-ui.js
CHANGED
|
@@ -35,7 +35,8 @@ function applyRoutes(server, call) {
|
|
|
35
35
|
// Network, SMTP server, IMAP proxy, and browser config routes
|
|
36
36
|
networkConfigRoutes({ server, call });
|
|
37
37
|
|
|
38
|
-
// Document Store (Elasticsearch) config routes
|
|
38
|
+
// Document Store (Elasticsearch) config routes (deprecated feature; the module self-gates
|
|
39
|
+
// and registers no routes unless the Document Store feature is enabled)
|
|
39
40
|
documentStoreRoutes({ server });
|
|
40
41
|
|
|
41
42
|
// Admin auth and user-profile routes (login, logout, TOTP, passkeys, password)
|
package/lib/smtp-auth.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// SMTP submission-server authentication handler. Extracted from workers/smtp.js
|
|
4
|
+
// so the auth logic can be unit tested without booting the worker thread (which
|
|
5
|
+
// connects to a parentPort and starts listening at require time).
|
|
6
|
+
|
|
7
|
+
const logger = require('./logger');
|
|
8
|
+
const settings = require('./settings');
|
|
9
|
+
const { redis } = require('./db');
|
|
10
|
+
const { Account } = require('./account');
|
|
11
|
+
const getSecret = require('./get-secret');
|
|
12
|
+
const { validateAuthToken, REASON_MESSAGES } = require('./auth-token');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds the SMTP server onAuth handler.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} deps
|
|
18
|
+
* @param {WeakMap} deps.accountCache - session -> Account cache shared with the worker
|
|
19
|
+
* @param {Function} deps.call - RPC function passed to the Account instance
|
|
20
|
+
* @returns {Function} async onAuth(auth, session)
|
|
21
|
+
*/
|
|
22
|
+
function createSmtpAuthHandler({ accountCache, call }) {
|
|
23
|
+
return async function onAuth(auth, session) {
|
|
24
|
+
if (!session.eeAuthEnabled) {
|
|
25
|
+
throw new Error('Authentication not enabled');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let account = auth.username;
|
|
29
|
+
|
|
30
|
+
let smtpPassword = await settings.get('smtpServerPassword');
|
|
31
|
+
if (!smtpPassword || auth.password !== smtpPassword) {
|
|
32
|
+
// fall back to API token authentication
|
|
33
|
+
let result = await validateAuthToken({
|
|
34
|
+
password: auth.password,
|
|
35
|
+
account: auth.username,
|
|
36
|
+
requiredScope: 'smtp',
|
|
37
|
+
remoteAddress: session.remoteAddress
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!result.authenticated) {
|
|
41
|
+
throw new Error(REASON_MESSAGES[result.reason] || 'Failed to authenticate user');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let accountObject = new Account({ account, redis, call, secret: await getSecret() });
|
|
46
|
+
let accountData;
|
|
47
|
+
try {
|
|
48
|
+
accountData = await accountObject.loadAccountData();
|
|
49
|
+
} catch (err) {
|
|
50
|
+
let respErr = new Error('Failed to authenticate user');
|
|
51
|
+
|
|
52
|
+
if (!err.output || err.output.statusCode !== 404) {
|
|
53
|
+
// only log non-obvious errors
|
|
54
|
+
logger.error({ msg: 'Failed to load account data', account: auth.username, err });
|
|
55
|
+
respErr.statusCode = 454;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw respErr;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!accountData) {
|
|
62
|
+
throw new Error('Failed to authenticate user');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
accountCache.set(session, accountObject);
|
|
66
|
+
return { user: accountData.account };
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { createSmtpAuthHandler };
|
package/lib/sub-script.js
CHANGED
|
@@ -67,11 +67,17 @@ class SubScript {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
async exec(payload) {
|
|
70
|
+
async exec(payload, options) {
|
|
71
71
|
if (!this.fn) {
|
|
72
72
|
throw new Error('Subscript not compiled');
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Allow callers (and tests) to shorten the execution timeout, but never
|
|
76
|
+
// raise it above SUBSCRIPT_RUNTIME_TIMEOUT - the sandbox cap is a hard
|
|
77
|
+
// ceiling. A falsy override (including 0, which vm treats as "no limit")
|
|
78
|
+
// falls back to the default so the cap can never be disabled.
|
|
79
|
+
let runtimeTimeout = options && options.timeout ? Math.min(options.timeout, SUBSCRIPT_RUNTIME_TIMEOUT) : SUBSCRIPT_RUNTIME_TIMEOUT;
|
|
80
|
+
|
|
75
81
|
let start = Date.now();
|
|
76
82
|
|
|
77
83
|
try {
|
|
@@ -93,7 +99,7 @@ class SubScript {
|
|
|
93
99
|
|
|
94
100
|
vm.createContext(ctx);
|
|
95
101
|
let result = await this.fn.runInContext(ctx, {
|
|
96
|
-
timeout:
|
|
102
|
+
timeout: runtimeTimeout,
|
|
97
103
|
microtaskMode: 'afterEvaluate'
|
|
98
104
|
});
|
|
99
105
|
|
package/lib/tools.js
CHANGED
|
@@ -57,7 +57,26 @@ const AGENT_OPTS = {
|
|
|
57
57
|
const RETRY_OPTS = {
|
|
58
58
|
maxRetries: URL_FETCH_RETRY_MAX,
|
|
59
59
|
methods: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
|
|
60
|
-
statusCodes: [429] // do not retry 5xx errors
|
|
60
|
+
statusCodes: [429], // do not retry 5xx errors
|
|
61
|
+
// undici does not retry transient DNS failures (EAI_AGAIN) or socket connect
|
|
62
|
+
// timeouts (ETIMEDOUT) by default, so a name-resolution blip bubbles up as a hard
|
|
63
|
+
// `TypeError: fetch failed`. Add them to the retry list. Passing errorCodes REPLACES
|
|
64
|
+
// undici's defaults, so its default codes are re-listed here.
|
|
65
|
+
errorCodes: [
|
|
66
|
+
// undici defaults
|
|
67
|
+
'ECONNRESET',
|
|
68
|
+
'ECONNREFUSED',
|
|
69
|
+
'ENOTFOUND',
|
|
70
|
+
'ENETDOWN',
|
|
71
|
+
'ENETUNREACH',
|
|
72
|
+
'EHOSTDOWN',
|
|
73
|
+
'EHOSTUNREACH',
|
|
74
|
+
'EPIPE',
|
|
75
|
+
'UND_ERR_SOCKET',
|
|
76
|
+
// additions: transient DNS / socket connect-timeout failures
|
|
77
|
+
'EAI_AGAIN',
|
|
78
|
+
'ETIMEDOUT'
|
|
79
|
+
]
|
|
61
80
|
};
|
|
62
81
|
|
|
63
82
|
// Shared mutable object -- consumers import the object reference and access
|
|
@@ -2198,6 +2217,7 @@ vWuuhT9ely8AUX2F
|
|
|
2198
2217
|
mF3GOI+Ev7mJODtG
|
|
2199
2218
|
nQNwPlZ+tyx24XVo
|
|
2200
2219
|
olObiSmdzVaMp7lH
|
|
2220
|
+
SruXHv6plKL9YuAW
|
|
2201
2221
|
cyp1GKV4Cl+eC8G/
|
|
2202
2222
|
Q1y/TzbtUQCqnotR
|
|
2203
2223
|
59Q2qOkuyTLzQDhd
|
|
@@ -2221,9 +2241,13 @@ EsbWnuADOq9qe/EZ
|
|
|
2221
2241
|
YfUSxQtq1+kpwW/W
|
|
2222
2242
|
QodzxvoJ6NPGhCgW
|
|
2223
2243
|
5/VK+O950efBio0t
|
|
2224
|
-
RA/lrkwxcTX80nxX
|
|
2225
2244
|
b85BMAFRHn10uX8y
|
|
2226
2245
|
vV2RgfFH2YFTYsly
|
|
2246
|
+
zcmIO3obSzUVHGtF
|
|
2247
|
+
tzsA68uFVAffHmec
|
|
2248
|
+
DkCU3OpaSa+rIVfU
|
|
2249
|
+
dcDjqpxVxivT46Rl
|
|
2250
|
+
QSgNcpNukC9CBNfe
|
|
2227
2251
|
`
|
|
2228
2252
|
.split(/\r?\n/)
|
|
2229
2253
|
.map(l => l.trim())
|
|
@@ -23,6 +23,7 @@ const timezonesList = require('timezones-list').default;
|
|
|
23
23
|
const { redis, submitQueue, notifyQueue, documentsQueue } = require('../db');
|
|
24
24
|
const { getByteSize, formatByteSize, getDuration, failAction, hasEnvValue, readEnvValue, httpAgent } = require('../tools');
|
|
25
25
|
const { llmPreProcess } = require('../llm-pre-process');
|
|
26
|
+
const { documentStoreFeatureEnabled } = require('../document-store');
|
|
26
27
|
const { locales } = require('../translations');
|
|
27
28
|
const { settingsSchema } = require('../schemas');
|
|
28
29
|
const { getOpenAiModels, OPEN_AI_MODELS, getExampleDocumentsPayloads } = require('./route-helpers');
|
|
@@ -217,7 +218,7 @@ function init(args) {
|
|
|
217
218
|
values,
|
|
218
219
|
|
|
219
220
|
webhookErrorFlag: await settings.get('webhookErrorFlag'),
|
|
220
|
-
documentStoreEnabled: (await settings.get('documentStoreEnabled')) || false
|
|
221
|
+
documentStoreEnabled: (documentStoreFeatureEnabled && (await settings.get('documentStoreEnabled'))) || false
|
|
221
222
|
},
|
|
222
223
|
{
|
|
223
224
|
layout: 'app'
|
|
@@ -307,7 +308,7 @@ function init(args) {
|
|
|
307
308
|
),
|
|
308
309
|
|
|
309
310
|
webhookErrorFlag: await settings.get('webhookErrorFlag'),
|
|
310
|
-
documentStoreEnabled: (await settings.get('documentStoreEnabled')) || false
|
|
311
|
+
documentStoreEnabled: (documentStoreFeatureEnabled && (await settings.get('documentStoreEnabled'))) || false
|
|
311
312
|
},
|
|
312
313
|
{
|
|
313
314
|
layout: 'app'
|
|
@@ -356,7 +357,7 @@ function init(args) {
|
|
|
356
357
|
errors,
|
|
357
358
|
|
|
358
359
|
webhookErrorFlag: await settings.get('webhookErrorFlag'),
|
|
359
|
-
documentStoreEnabled: (await settings.get('documentStoreEnabled')) || false
|
|
360
|
+
documentStoreEnabled: (documentStoreFeatureEnabled && (await settings.get('documentStoreEnabled'))) || false
|
|
360
361
|
},
|
|
361
362
|
{
|
|
362
363
|
layout: 'app'
|
|
@@ -16,7 +16,7 @@ const { REDIS_PREFIX } = require('../consts');
|
|
|
16
16
|
const { failAction } = require('../tools');
|
|
17
17
|
const { settingsSchema } = require('../schemas');
|
|
18
18
|
const { defaultMappings } = require('../es');
|
|
19
|
-
const { getESClient } = require('../document-store');
|
|
19
|
+
const { getESClient, documentStoreFeatureEnabled } = require('../document-store');
|
|
20
20
|
const { getOpenAiModels, OPEN_AI_MODELS, getExampleDocumentsPayloads } = require('./route-helpers');
|
|
21
21
|
|
|
22
22
|
const FIELD_TYPES = [
|
|
@@ -95,6 +95,12 @@ const configDocumentStoreSchema = {
|
|
|
95
95
|
function init(args) {
|
|
96
96
|
const { server } = args;
|
|
97
97
|
|
|
98
|
+
// Deprecated Document Store feature: when the gate is off, register no routes so that
|
|
99
|
+
// every /admin/config/document-store* page behaves like a regular 404.
|
|
100
|
+
if (!documentStoreFeatureEnabled) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
98
104
|
server.route({
|
|
99
105
|
method: 'GET',
|
|
100
106
|
path: '/admin/config/document-store',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emailengine-app",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.72.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"productTitle": "EmailEngine",
|
|
6
6
|
"description": "Email Sync Engine",
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"single": "EE_OPENAPI_VERBOSE=true EENGINE_LOG_RAW=true EENGINE_SECRET=your-encryption-key EENGINE_WORKERS=1 node --inspect server --dbs.redis='redis://127.0.0.1:6379/6' --api.port=7003 --api.host=0.0.0.0 | tee $HOME/ee.log.single.txt | pino-pretty",
|
|
12
12
|
"gmail": "EE_OPENAPI_VERBOSE=true EENGINE_LOG_RAW=true EENGINE_SECRET=your-encryption-key EENGINE_WORKERS=2 EENGINE_CORS_ORIGIN='*' node --inspect server --dbs.redis='redis://127.0.0.1:6379/11' --api.port=7003 --api.host=0.0.0.0 | tee $HOME/ee.log.gmail.txt | pino-pretty",
|
|
13
13
|
"test": "NODE_ENV=test grunt",
|
|
14
|
+
"test:unit": "NODE_ENV=test grunt test-unit",
|
|
15
|
+
"test:integration": "NODE_ENV=test grunt test-integration",
|
|
16
|
+
"test:e2e": "playwright test",
|
|
17
|
+
"test:e2e:install": "playwright install --with-deps chromium",
|
|
14
18
|
"lint": "npx eslint 'lib/**/*.js' 'workers/**/*.js' 'test/**/*.js' server.js Gruntfile.js",
|
|
15
19
|
"swagger": "./getswagger.sh",
|
|
16
20
|
"build-source": "rm -rf node_modules && npm install && rm -rf node_modules && npm ci --omit=dev && rm -rf node_modules/ace-builds node_modules/@postalsys/ee-client && ./update-info.sh",
|
|
@@ -43,8 +47,8 @@
|
|
|
43
47
|
},
|
|
44
48
|
"homepage": "https://emailengine.app/",
|
|
45
49
|
"dependencies": {
|
|
46
|
-
"@bull-board/api": "8.0.
|
|
47
|
-
"@bull-board/hapi": "8.0.
|
|
50
|
+
"@bull-board/api": "8.0.1",
|
|
51
|
+
"@bull-board/hapi": "8.0.1",
|
|
48
52
|
"@elastic/elasticsearch": "8.15.3",
|
|
49
53
|
"@hapi/accept": "6.0.3",
|
|
50
54
|
"@hapi/bell": "13.1.0",
|
|
@@ -52,25 +56,25 @@
|
|
|
52
56
|
"@hapi/cookie": "12.0.1",
|
|
53
57
|
"@hapi/crumb": "9.0.1",
|
|
54
58
|
"@hapi/hapi": "21.4.9",
|
|
55
|
-
"@hapi/inert": "7.1.
|
|
59
|
+
"@hapi/inert": "7.1.2",
|
|
56
60
|
"@hapi/vision": "7.0.3",
|
|
57
61
|
"@phc/pbkdf2": "1.1.14",
|
|
58
62
|
"@postalsys/bounce-classifier": "3.0.0",
|
|
59
|
-
"@postalsys/certs": "1.0.
|
|
63
|
+
"@postalsys/certs": "1.0.15",
|
|
60
64
|
"@postalsys/ee-client": "1.3.0",
|
|
61
|
-
"@postalsys/email-ai-tools": "1.13.
|
|
62
|
-
"@postalsys/email-text-tools": "2.4.
|
|
65
|
+
"@postalsys/email-ai-tools": "1.13.6",
|
|
66
|
+
"@postalsys/email-text-tools": "2.4.7",
|
|
63
67
|
"@postalsys/gettext": "4.1.1",
|
|
64
68
|
"@postalsys/joi-messages": "1.0.5",
|
|
65
69
|
"@postalsys/templates": "2.0.1",
|
|
66
|
-
"@sentry/node": "10.
|
|
70
|
+
"@sentry/node": "10.58.0",
|
|
67
71
|
"@simplewebauthn/browser": "13.3.0",
|
|
68
72
|
"@simplewebauthn/server": "13.3.1",
|
|
69
73
|
"@zone-eu/mailsplit": "5.4.12",
|
|
70
74
|
"@zone-eu/wild-config": "1.7.5",
|
|
71
75
|
"ace-builds": "1.44.0",
|
|
72
76
|
"base32.js": "0.1.0",
|
|
73
|
-
"bullmq": "5.
|
|
77
|
+
"bullmq": "5.79.0",
|
|
74
78
|
"compare-versions": "6.1.1",
|
|
75
79
|
"dotenv": "17.4.2",
|
|
76
80
|
"encoding-japanese": "2.2.0",
|
|
@@ -84,50 +88,50 @@
|
|
|
84
88
|
"html-to-text": "10.0.0",
|
|
85
89
|
"ical.js": "1.5.0",
|
|
86
90
|
"iconv-lite": "0.7.2",
|
|
87
|
-
"imapflow": "1.4.
|
|
91
|
+
"imapflow": "1.4.1",
|
|
88
92
|
"ioredfour": "1.4.1",
|
|
89
93
|
"ioredis": "5.11.1",
|
|
90
94
|
"ipaddr.js": "2.4.0",
|
|
91
|
-
"joi": "17.13.
|
|
95
|
+
"joi": "17.13.4",
|
|
92
96
|
"jquery": "4.0.0",
|
|
93
97
|
"libbase64": "1.3.0",
|
|
94
98
|
"libmime": "5.3.8",
|
|
95
99
|
"libqp": "2.1.1",
|
|
96
100
|
"license-checker": "25.0.1",
|
|
97
|
-
"mailparser": "3.9.
|
|
98
|
-
"marked": "
|
|
101
|
+
"mailparser": "3.9.10",
|
|
102
|
+
"marked": "15.0.12",
|
|
99
103
|
"minimist": "1.2.8",
|
|
100
104
|
"msgpack5": "6.0.2",
|
|
101
105
|
"murmurhash": "2.0.1",
|
|
102
|
-
"nanoid": "3.3.
|
|
103
|
-
"nodemailer": "
|
|
106
|
+
"nanoid": "3.3.13",
|
|
107
|
+
"nodemailer": "9.0.1",
|
|
104
108
|
"pino": "10.3.1",
|
|
105
109
|
"popper.js": "1.16.1",
|
|
106
110
|
"prom-client": "15.1.3",
|
|
107
111
|
"psl": "1.15.0",
|
|
108
|
-
"pubface": "1.1.
|
|
112
|
+
"pubface": "1.1.3",
|
|
109
113
|
"punycode.js": "2.3.1",
|
|
110
114
|
"qrcode": "1.5.4",
|
|
111
|
-
"smtp-server": "3.
|
|
115
|
+
"smtp-server": "3.19.0",
|
|
112
116
|
"socks": "2.8.9",
|
|
113
117
|
"speakeasy": "2.0.0",
|
|
114
118
|
"startbootstrap-sb-admin-2": "3.3.7",
|
|
115
119
|
"timezones-list": "3.1.0",
|
|
116
|
-
"undici": "7.
|
|
120
|
+
"undici": "7.28.0",
|
|
117
121
|
"xml2js": "0.6.2"
|
|
118
122
|
},
|
|
119
123
|
"devDependencies": {
|
|
120
124
|
"@eslint/js": "10.0.1",
|
|
121
|
-
"
|
|
122
|
-
"acorn
|
|
123
|
-
"
|
|
125
|
+
"@playwright/test": "1.61.0",
|
|
126
|
+
"acorn": "8.17.0",
|
|
127
|
+
"acorn-walk": "8.3.5",
|
|
128
|
+
"chai": "4.5.0",
|
|
124
129
|
"eerawlog": "1.5.3",
|
|
125
|
-
"eslint": "10.
|
|
130
|
+
"eslint": "10.5.0",
|
|
126
131
|
"grunt": "1.6.2",
|
|
127
132
|
"grunt-cli": "1.5.0",
|
|
128
133
|
"grunt-shell-spawn": "0.5.0",
|
|
129
|
-
"
|
|
130
|
-
"pino-pretty": "13.0.0",
|
|
134
|
+
"pino-pretty": "13.1.3",
|
|
131
135
|
"prettier": "3.8.4",
|
|
132
136
|
"resedit": "3.0.2",
|
|
133
137
|
"spdx-satisfies": "6.0.0",
|
|
@@ -158,6 +162,8 @@
|
|
|
158
162
|
"node_modules/@postalsys/joi-messages/translations/*",
|
|
159
163
|
"node_modules/@postalsys/bounce-classifier/model/**/*",
|
|
160
164
|
"node_modules/jsdom/lib/jsdom/browser/default-stylesheet.css",
|
|
165
|
+
"node_modules/nodemailer/lib/well-known/*.json",
|
|
166
|
+
"node_modules/nodemailer/package.json",
|
|
161
167
|
"LICENSE_EMAILENGINE.txt",
|
|
162
168
|
"version-info.json",
|
|
163
169
|
"sbom.json"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Playwright config for the EmailEngine happy-path e2e suite (test/e2e). Boots a fresh
|
|
4
|
+
// EmailEngine via the webServer command (flush the isolated Redis db 14, then `node server.js`
|
|
5
|
+
// with NODE_ENV=e2e -> config/e2e.toml) and drives it with a real browser.
|
|
6
|
+
//
|
|
7
|
+
// Run once: npm run test:e2e:install (fetch the Chromium browser)
|
|
8
|
+
// Run suite: npm run test:e2e
|
|
9
|
+
|
|
10
|
+
const { defineConfig, devices } = require('@playwright/test');
|
|
11
|
+
|
|
12
|
+
const PORT = 7099;
|
|
13
|
+
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
14
|
+
|
|
15
|
+
module.exports = defineConfig({
|
|
16
|
+
testDir: './test/e2e',
|
|
17
|
+
testMatch: '**/*.spec.js',
|
|
18
|
+
// One worker against one app + one database keeps the fresh-instance flow predictable.
|
|
19
|
+
fullyParallel: false,
|
|
20
|
+
workers: 1,
|
|
21
|
+
forbidOnly: !!process.env.CI,
|
|
22
|
+
retries: process.env.CI ? 1 : 0,
|
|
23
|
+
// The single happy-path test chains several slow external steps (Ethereal provisioning,
|
|
24
|
+
// trial activation against postalsys.com, IMAP connect, message read-back).
|
|
25
|
+
timeout: 300000,
|
|
26
|
+
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
|
|
27
|
+
use: {
|
|
28
|
+
baseURL: BASE_URL,
|
|
29
|
+
trace: 'on-first-retry'
|
|
30
|
+
},
|
|
31
|
+
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
|
32
|
+
webServer: {
|
|
33
|
+
// Flush the isolated e2e Redis DB so every run starts from a clean, unconfigured instance.
|
|
34
|
+
command: 'node test/e2e/flush-redis.js && node server.js',
|
|
35
|
+
url: BASE_URL,
|
|
36
|
+
timeout: 120000,
|
|
37
|
+
// Always boot fresh: the suite asserts fresh-instance behaviour (enabling auth, activating
|
|
38
|
+
// the trial), so a reused server with those already set would break it. The dedicated
|
|
39
|
+
// db 14 makes the pre-boot flush safe.
|
|
40
|
+
reuseExistingServer: false,
|
|
41
|
+
stdout: 'pipe',
|
|
42
|
+
stderr: 'pipe',
|
|
43
|
+
env: Object.assign({}, process.env, { NODE_ENV: 'e2e' })
|
|
44
|
+
}
|
|
45
|
+
});
|