emailengine-app 2.71.0 → 2.72.1
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 +8 -0
- package/.ncurc.js +20 -20
- package/CHANGELOG.md +14 -0
- package/config/e2e.toml +35 -0
- package/data/google-crawlers.json +1 -1
- package/lib/api-routes/route-helpers.js +11 -5
- package/lib/auth-token.js +83 -0
- package/lib/delivery-error.js +62 -0
- package/lib/email-client/gmail-client.js +33 -1
- package/lib/imap-proxy-auth.js +81 -0
- package/lib/imapproxy/imap-server.js +8 -103
- package/lib/oauth/gmail.js +3 -0
- package/lib/oauth/outlook.js +3 -0
- package/lib/oauth2-apps.js +100 -11
- package/lib/smtp-auth.js +70 -0
- package/lib/sub-script.js +8 -2
- package/package.json +21 -18
- package/playwright.config.js +45 -0
- package/sbom.json +1 -1
- package/static/licenses.html +171 -31
- package/test-coverage-plan.md +233 -0
- package/translations/messages.pot +1 -1
- package/workers/smtp.js +5 -85
- package/workers/submit.js +2 -12
package/lib/oauth/gmail.js
CHANGED
|
@@ -824,6 +824,9 @@ class GmailOauth {
|
|
|
824
824
|
}
|
|
825
825
|
|
|
826
826
|
module.exports.GmailOauth = GmailOauth;
|
|
827
|
+
// Exported for unit testing the auth error-to-flag mapping.
|
|
828
|
+
module.exports.checkForFlags = checkForFlags;
|
|
829
|
+
module.exports.checkForUserFlags = checkForUserFlags;
|
|
827
830
|
module.exports.GMAIL_SCOPES = GMAIL_SCOPES;
|
|
828
831
|
module.exports.GMAIL_API_SCOPES = GMAIL_API_SCOPES;
|
|
829
832
|
module.exports.OPENID_SCOPES = OPENID_SCOPES;
|
package/lib/oauth/outlook.js
CHANGED
|
@@ -701,3 +701,6 @@ class OutlookOauth {
|
|
|
701
701
|
module.exports.OutlookOauth = OutlookOauth;
|
|
702
702
|
module.exports.outlookScopes = outlookScopes;
|
|
703
703
|
module.exports.OUTLOOK_API_SCOPES = OUTLOOK_API_SCOPES;
|
|
704
|
+
// Exported for unit testing the auth error-to-flag mapping.
|
|
705
|
+
module.exports.checkForFlags = checkForFlags;
|
|
706
|
+
module.exports.checkForUserFlags = checkForUserFlags;
|
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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emailengine-app",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.72.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"productTitle": "EmailEngine",
|
|
6
6
|
"description": "Email Sync Engine",
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"test": "NODE_ENV=test grunt",
|
|
14
14
|
"test:unit": "NODE_ENV=test grunt test-unit",
|
|
15
15
|
"test:integration": "NODE_ENV=test grunt test-integration",
|
|
16
|
+
"test:e2e": "playwright test",
|
|
17
|
+
"test:e2e:install": "playwright install --with-deps chromium",
|
|
16
18
|
"lint": "npx eslint 'lib/**/*.js' 'workers/**/*.js' 'test/**/*.js' server.js Gruntfile.js",
|
|
17
19
|
"swagger": "./getswagger.sh",
|
|
18
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",
|
|
@@ -45,8 +47,8 @@
|
|
|
45
47
|
},
|
|
46
48
|
"homepage": "https://emailengine.app/",
|
|
47
49
|
"dependencies": {
|
|
48
|
-
"@bull-board/api": "8.0.
|
|
49
|
-
"@bull-board/hapi": "8.0.
|
|
50
|
+
"@bull-board/api": "8.0.1",
|
|
51
|
+
"@bull-board/hapi": "8.0.1",
|
|
50
52
|
"@elastic/elasticsearch": "8.15.3",
|
|
51
53
|
"@hapi/accept": "6.0.3",
|
|
52
54
|
"@hapi/bell": "13.1.0",
|
|
@@ -54,25 +56,25 @@
|
|
|
54
56
|
"@hapi/cookie": "12.0.1",
|
|
55
57
|
"@hapi/crumb": "9.0.1",
|
|
56
58
|
"@hapi/hapi": "21.4.9",
|
|
57
|
-
"@hapi/inert": "7.1.
|
|
59
|
+
"@hapi/inert": "7.1.2",
|
|
58
60
|
"@hapi/vision": "7.0.3",
|
|
59
61
|
"@phc/pbkdf2": "1.1.14",
|
|
60
62
|
"@postalsys/bounce-classifier": "3.0.0",
|
|
61
63
|
"@postalsys/certs": "1.0.15",
|
|
62
64
|
"@postalsys/ee-client": "1.3.0",
|
|
63
|
-
"@postalsys/email-ai-tools": "1.13.
|
|
64
|
-
"@postalsys/email-text-tools": "2.4.
|
|
65
|
+
"@postalsys/email-ai-tools": "1.13.8",
|
|
66
|
+
"@postalsys/email-text-tools": "2.4.8",
|
|
65
67
|
"@postalsys/gettext": "4.1.1",
|
|
66
68
|
"@postalsys/joi-messages": "1.0.5",
|
|
67
69
|
"@postalsys/templates": "2.0.1",
|
|
68
|
-
"@sentry/node": "10.
|
|
70
|
+
"@sentry/node": "10.59.0",
|
|
69
71
|
"@simplewebauthn/browser": "13.3.0",
|
|
70
72
|
"@simplewebauthn/server": "13.3.1",
|
|
71
73
|
"@zone-eu/mailsplit": "5.4.12",
|
|
72
74
|
"@zone-eu/wild-config": "1.7.5",
|
|
73
75
|
"ace-builds": "1.44.0",
|
|
74
76
|
"base32.js": "0.1.0",
|
|
75
|
-
"bullmq": "5.
|
|
77
|
+
"bullmq": "5.79.0",
|
|
76
78
|
"compare-versions": "6.1.1",
|
|
77
79
|
"dotenv": "17.4.2",
|
|
78
80
|
"encoding-japanese": "2.2.0",
|
|
@@ -86,7 +88,7 @@
|
|
|
86
88
|
"html-to-text": "10.0.0",
|
|
87
89
|
"ical.js": "1.5.0",
|
|
88
90
|
"iconv-lite": "0.7.2",
|
|
89
|
-
"imapflow": "1.4.
|
|
91
|
+
"imapflow": "1.4.2",
|
|
90
92
|
"ioredfour": "1.4.1",
|
|
91
93
|
"ioredis": "5.11.1",
|
|
92
94
|
"ipaddr.js": "2.4.0",
|
|
@@ -96,39 +98,40 @@
|
|
|
96
98
|
"libmime": "5.3.8",
|
|
97
99
|
"libqp": "2.1.1",
|
|
98
100
|
"license-checker": "25.0.1",
|
|
99
|
-
"mailparser": "3.9.
|
|
100
|
-
"marked": "
|
|
101
|
+
"mailparser": "3.9.11",
|
|
102
|
+
"marked": "15.0.12",
|
|
101
103
|
"minimist": "1.2.8",
|
|
102
104
|
"msgpack5": "6.0.2",
|
|
103
105
|
"murmurhash": "2.0.1",
|
|
104
|
-
"nanoid": "3.3.
|
|
105
|
-
"nodemailer": "9.0.
|
|
106
|
+
"nanoid": "3.3.13",
|
|
107
|
+
"nodemailer": "9.0.1",
|
|
106
108
|
"pino": "10.3.1",
|
|
107
109
|
"popper.js": "1.16.1",
|
|
108
110
|
"prom-client": "15.1.3",
|
|
109
111
|
"psl": "1.15.0",
|
|
110
|
-
"pubface": "1.1.
|
|
112
|
+
"pubface": "1.1.4",
|
|
111
113
|
"punycode.js": "2.3.1",
|
|
112
114
|
"qrcode": "1.5.4",
|
|
113
|
-
"smtp-server": "3.19.
|
|
115
|
+
"smtp-server": "3.19.1",
|
|
114
116
|
"socks": "2.8.9",
|
|
115
117
|
"speakeasy": "2.0.0",
|
|
116
118
|
"startbootstrap-sb-admin-2": "3.3.7",
|
|
117
119
|
"timezones-list": "3.1.0",
|
|
118
|
-
"undici": "7.
|
|
120
|
+
"undici": "7.28.0",
|
|
119
121
|
"xml2js": "0.6.2"
|
|
120
122
|
},
|
|
121
123
|
"devDependencies": {
|
|
122
124
|
"@eslint/js": "10.0.1",
|
|
125
|
+
"@playwright/test": "1.61.0",
|
|
123
126
|
"acorn": "8.17.0",
|
|
124
127
|
"acorn-walk": "8.3.5",
|
|
125
|
-
"chai": "4.
|
|
128
|
+
"chai": "4.5.0",
|
|
126
129
|
"eerawlog": "1.5.3",
|
|
127
130
|
"eslint": "10.5.0",
|
|
128
131
|
"grunt": "1.6.2",
|
|
129
132
|
"grunt-cli": "1.5.0",
|
|
130
133
|
"grunt-shell-spawn": "0.5.0",
|
|
131
|
-
"pino-pretty": "13.
|
|
134
|
+
"pino-pretty": "13.1.3",
|
|
132
135
|
"prettier": "3.8.4",
|
|
133
136
|
"resedit": "3.0.2",
|
|
134
137
|
"spdx-satisfies": "6.0.0",
|
|
@@ -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
|
+
});
|