emailengine-app 2.61.5 → 2.62.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/CHANGELOG.md +88 -0
- package/data/google-crawlers.json +1 -1
- package/lib/account.js +20 -7
- package/lib/api-routes/account-routes.js +28 -5
- package/lib/api-routes/chat-routes.js +1 -1
- package/lib/api-routes/export-routes.js +316 -0
- package/lib/api-routes/message-routes.js +28 -23
- package/lib/api-routes/template-routes.js +28 -7
- package/lib/arf-detect.js +1 -1
- package/lib/autodetect-imap-settings.js +5 -5
- package/lib/consts.js +16 -0
- package/lib/db.js +3 -0
- package/lib/email-client/base-client.js +6 -4
- package/lib/email-client/gmail-client.js +205 -35
- package/lib/email-client/imap/mailbox.js +99 -8
- package/lib/email-client/imap/subconnection.js +5 -5
- package/lib/email-client/imap-client.js +76 -19
- package/lib/email-client/message-builder.js +3 -1
- package/lib/email-client/notification-handler.js +12 -9
- package/lib/email-client/outlook-client.js +364 -73
- package/lib/email-client/smtp-pool-manager.js +1 -1
- package/lib/export.js +528 -0
- package/lib/oauth/gmail.js +24 -16
- package/lib/oauth/mail-ru.js +26 -13
- package/lib/oauth/outlook.js +29 -19
- package/lib/oauth/pubsub/google.js +5 -0
- package/lib/routes-ui.js +268 -9
- package/lib/schemas.js +274 -81
- package/lib/stream-encrypt.js +263 -0
- package/lib/sub-script.js +2 -2
- package/lib/tools.js +194 -12
- package/lib/ui-routes/account-routes.js +23 -0
- package/lib/ui-routes/admin-config-routes.js +13 -6
- package/lib/ui-routes/admin-entities-routes.js +18 -0
- package/lib/webhooks.js +16 -20
- package/package.json +20 -20
- package/sbom.json +1 -1
- package/server.js +66 -7
- package/static/js/ace/ace.js +1 -1
- package/static/js/ace/ext-language_tools.js +1 -1
- package/static/licenses.html +118 -149
- package/translations/de.mo +0 -0
- package/translations/de.po +63 -36
- package/translations/en.mo +0 -0
- package/translations/en.po +64 -37
- package/translations/et.mo +0 -0
- package/translations/et.po +63 -36
- package/translations/fr.mo +0 -0
- package/translations/fr.po +63 -36
- package/translations/ja.mo +0 -0
- package/translations/ja.po +63 -36
- package/translations/messages.pot +84 -51
- package/translations/nl.mo +0 -0
- package/translations/nl.po +63 -36
- package/translations/pl.mo +0 -0
- package/translations/pl.po +63 -36
- package/views/accounts/account.hbs +375 -2
- package/views/config/network.hbs +45 -0
- package/views/config/service.hbs +35 -0
- package/workers/api.js +130 -47
- package/workers/documents.js +3 -2
- package/workers/export.js +933 -0
- package/workers/imap.js +34 -1
- package/workers/submit.js +33 -6
- package/workers/webhooks.js +20 -4
package/lib/oauth/mail-ru.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const packageData = require('../../package.json');
|
|
4
4
|
const { fetch: fetchCmd } = require('undici');
|
|
5
|
-
const { formatPartialSecretKey, structuredClone,
|
|
5
|
+
const { formatPartialSecretKey, structuredClone, httpAgent, formatTokenError } = require('../tools');
|
|
6
6
|
|
|
7
7
|
const MAIL_RU_SCOPES = ['userinfo', 'mail.imap'];
|
|
8
8
|
|
|
@@ -18,11 +18,16 @@ const checkForFlags = err => {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const formatFetchBody = (searchParams, logRaw) => {
|
|
21
|
-
let
|
|
21
|
+
let entries = typeof searchParams === 'string' ? new URLSearchParams(searchParams) : searchParams;
|
|
22
|
+
let data = Object.fromEntries(entries);
|
|
23
|
+
|
|
24
|
+
if (logRaw) {
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
22
27
|
|
|
23
28
|
for (let key of ['refresh_token', 'client_secret']) {
|
|
24
29
|
if (data[key]) {
|
|
25
|
-
data[key] =
|
|
30
|
+
data[key] = formatPartialSecretKey(data[key]);
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -92,7 +97,7 @@ class MailRuOauth {
|
|
|
92
97
|
|
|
93
98
|
return {
|
|
94
99
|
url: url.origin + url.pathname,
|
|
95
|
-
body: url.searchParams
|
|
100
|
+
body: url.searchParams.toString()
|
|
96
101
|
};
|
|
97
102
|
}
|
|
98
103
|
|
|
@@ -112,7 +117,7 @@ class MailRuOauth {
|
|
|
112
117
|
Authorization: `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')}`
|
|
113
118
|
},
|
|
114
119
|
body: tokenRequest.body,
|
|
115
|
-
dispatcher:
|
|
120
|
+
dispatcher: httpAgent.retry
|
|
116
121
|
});
|
|
117
122
|
|
|
118
123
|
let responseJson;
|
|
@@ -154,7 +159,7 @@ class MailRuOauth {
|
|
|
154
159
|
code
|
|
155
160
|
};
|
|
156
161
|
try {
|
|
157
|
-
err.tokenRequest.response = responseJson;
|
|
162
|
+
err.tokenRequest.response = responseJson || { error: 'Failed to parse response' };
|
|
158
163
|
|
|
159
164
|
let flag = checkForFlags(err.tokenRequest.response);
|
|
160
165
|
if (flag) {
|
|
@@ -164,6 +169,7 @@ class MailRuOauth {
|
|
|
164
169
|
} catch (e) {
|
|
165
170
|
// ignore
|
|
166
171
|
}
|
|
172
|
+
err.message = formatTokenError(this.provider, err.tokenRequest);
|
|
167
173
|
throw err;
|
|
168
174
|
}
|
|
169
175
|
|
|
@@ -188,14 +194,15 @@ class MailRuOauth {
|
|
|
188
194
|
let requestUrl = url.origin + url.pathname;
|
|
189
195
|
let method = 'post';
|
|
190
196
|
|
|
197
|
+
const bodyString = url.searchParams.toString();
|
|
191
198
|
let res = await fetchCmd(requestUrl, {
|
|
192
|
-
method
|
|
199
|
+
method,
|
|
193
200
|
headers: {
|
|
194
201
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
195
202
|
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
196
203
|
},
|
|
197
|
-
body:
|
|
198
|
-
dispatcher:
|
|
204
|
+
body: bodyString,
|
|
205
|
+
dispatcher: httpAgent.retry
|
|
199
206
|
});
|
|
200
207
|
|
|
201
208
|
let responseJson;
|
|
@@ -236,7 +243,7 @@ class MailRuOauth {
|
|
|
236
243
|
scopes: this.scopes
|
|
237
244
|
};
|
|
238
245
|
try {
|
|
239
|
-
err.tokenRequest.response = responseJson;
|
|
246
|
+
err.tokenRequest.response = responseJson || { error: 'Failed to parse response' };
|
|
240
247
|
|
|
241
248
|
let flag = checkForFlags(err.tokenRequest.response);
|
|
242
249
|
|
|
@@ -247,6 +254,7 @@ class MailRuOauth {
|
|
|
247
254
|
} catch (e) {
|
|
248
255
|
// ignore
|
|
249
256
|
}
|
|
257
|
+
err.message = formatTokenError(this.provider, err.tokenRequest);
|
|
250
258
|
throw err;
|
|
251
259
|
}
|
|
252
260
|
|
|
@@ -263,18 +271,23 @@ class MailRuOauth {
|
|
|
263
271
|
headers: {
|
|
264
272
|
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
265
273
|
},
|
|
266
|
-
dispatcher:
|
|
274
|
+
dispatcher: httpAgent.retry
|
|
267
275
|
};
|
|
268
276
|
|
|
269
277
|
if (payload) {
|
|
270
278
|
if (!Buffer.isBuffer(payload)) {
|
|
271
279
|
reqData.headers.Accept = 'application/json';
|
|
272
280
|
reqData.headers['Content-Type'] = options?.contentType || 'application/json';
|
|
273
|
-
|
|
281
|
+
// Use string body instead of Buffer to avoid ArrayBuffer detachment on retry
|
|
282
|
+
reqData.body = JSON.stringify(payload);
|
|
274
283
|
} else {
|
|
275
284
|
reqData.headers['Content-Type'] = options?.contentType || 'application/x-www-form-urlencoded';
|
|
285
|
+
reqData.body = payload;
|
|
286
|
+
if (payload.length > 0) {
|
|
287
|
+
// Non-empty buffers use non-retry dispatcher to prevent ArrayBuffer detachment
|
|
288
|
+
reqData.dispatcher = httpAgent.fetch;
|
|
289
|
+
}
|
|
276
290
|
}
|
|
277
|
-
reqData.body = payload;
|
|
278
291
|
}
|
|
279
292
|
|
|
280
293
|
const requestUrl = new URL(url);
|
package/lib/oauth/outlook.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const packageData = require('../../package.json');
|
|
4
|
-
const { formatPartialSecretKey, structuredClone,
|
|
4
|
+
const { formatPartialSecretKey, structuredClone, httpAgent, formatTokenError } = require('../tools');
|
|
5
5
|
|
|
6
6
|
const { fetch: fetchCmd } = require('undici');
|
|
7
7
|
|
|
@@ -127,11 +127,16 @@ const checkForUserFlags = err => {
|
|
|
127
127
|
};
|
|
128
128
|
|
|
129
129
|
const formatFetchBody = (searchParams, logRaw) => {
|
|
130
|
-
let
|
|
130
|
+
let entries = typeof searchParams === 'string' ? new URLSearchParams(searchParams) : searchParams;
|
|
131
|
+
let data = Object.fromEntries(entries);
|
|
132
|
+
|
|
133
|
+
if (logRaw) {
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
131
136
|
|
|
132
137
|
for (let key of ['refresh_token', 'client_secret']) {
|
|
133
138
|
if (data[key]) {
|
|
134
|
-
data[key] =
|
|
139
|
+
data[key] = formatPartialSecretKey(data[key]);
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
|
|
@@ -242,7 +247,7 @@ class OutlookOauth {
|
|
|
242
247
|
|
|
243
248
|
return {
|
|
244
249
|
url: url.origin + url.pathname,
|
|
245
|
-
body: url.searchParams
|
|
250
|
+
body: url.searchParams.toString()
|
|
246
251
|
};
|
|
247
252
|
}
|
|
248
253
|
|
|
@@ -261,7 +266,7 @@ class OutlookOauth {
|
|
|
261
266
|
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
262
267
|
},
|
|
263
268
|
body: tokenRequest.body,
|
|
264
|
-
dispatcher:
|
|
269
|
+
dispatcher: httpAgent.retry
|
|
265
270
|
});
|
|
266
271
|
|
|
267
272
|
let responseJson;
|
|
@@ -304,9 +309,9 @@ class OutlookOauth {
|
|
|
304
309
|
code
|
|
305
310
|
};
|
|
306
311
|
try {
|
|
307
|
-
err.tokenRequest.response = responseJson;
|
|
312
|
+
err.tokenRequest.response = responseJson || { error: 'Failed to parse response' };
|
|
308
313
|
|
|
309
|
-
if (EXPOSE_PARTIAL_SECRET_KEY_REGEX.test(err.tokenRequest.response
|
|
314
|
+
if (EXPOSE_PARTIAL_SECRET_KEY_REGEX.test(err.tokenRequest.response?.error_description)) {
|
|
310
315
|
// key might have been invalidated or renewed
|
|
311
316
|
err.tokenRequest.clientSecret = formatPartialSecretKey(this.clientSecret);
|
|
312
317
|
}
|
|
@@ -319,6 +324,7 @@ class OutlookOauth {
|
|
|
319
324
|
} catch (e) {
|
|
320
325
|
// ignore
|
|
321
326
|
}
|
|
327
|
+
err.message = formatTokenError(this.provider, err.tokenRequest);
|
|
322
328
|
throw err;
|
|
323
329
|
}
|
|
324
330
|
|
|
@@ -349,14 +355,15 @@ class OutlookOauth {
|
|
|
349
355
|
let requestUrl = url.origin + url.pathname;
|
|
350
356
|
let method = 'post';
|
|
351
357
|
|
|
358
|
+
const bodyString = url.searchParams.toString();
|
|
352
359
|
let res = await fetchCmd(requestUrl, {
|
|
353
|
-
method
|
|
360
|
+
method,
|
|
354
361
|
headers: {
|
|
355
362
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
356
363
|
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
357
364
|
},
|
|
358
|
-
body:
|
|
359
|
-
dispatcher:
|
|
365
|
+
body: bodyString,
|
|
366
|
+
dispatcher: httpAgent.retry
|
|
360
367
|
});
|
|
361
368
|
|
|
362
369
|
let responseJson;
|
|
@@ -372,7 +379,7 @@ class OutlookOauth {
|
|
|
372
379
|
this.logger.info({
|
|
373
380
|
msg: 'OAuth2 authentication request',
|
|
374
381
|
action: 'oauth2Fetch',
|
|
375
|
-
fn: '
|
|
382
|
+
fn: 'refreshToken',
|
|
376
383
|
method,
|
|
377
384
|
url: requestUrl,
|
|
378
385
|
success: !!res.ok,
|
|
@@ -398,9 +405,9 @@ class OutlookOauth {
|
|
|
398
405
|
scopes: this.scopes
|
|
399
406
|
};
|
|
400
407
|
try {
|
|
401
|
-
err.tokenRequest.response = responseJson;
|
|
408
|
+
err.tokenRequest.response = responseJson || { error: 'Failed to parse response' };
|
|
402
409
|
|
|
403
|
-
if (EXPOSE_PARTIAL_SECRET_KEY_REGEX.test(err.tokenRequest.response
|
|
410
|
+
if (EXPOSE_PARTIAL_SECRET_KEY_REGEX.test(err.tokenRequest.response?.error_description)) {
|
|
404
411
|
// key might have been invalidated or renewed
|
|
405
412
|
err.tokenRequest.clientSecret = formatPartialSecretKey(this.clientSecret);
|
|
406
413
|
}
|
|
@@ -418,6 +425,7 @@ class OutlookOauth {
|
|
|
418
425
|
} catch (e) {
|
|
419
426
|
// ignore
|
|
420
427
|
}
|
|
428
|
+
err.message = formatTokenError(this.provider, err.tokenRequest);
|
|
421
429
|
throw err;
|
|
422
430
|
}
|
|
423
431
|
|
|
@@ -438,7 +446,7 @@ class OutlookOauth {
|
|
|
438
446
|
Authorization: `Bearer ${accessToken}`,
|
|
439
447
|
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
440
448
|
},
|
|
441
|
-
dispatcher:
|
|
449
|
+
dispatcher: httpAgent.retry
|
|
442
450
|
};
|
|
443
451
|
|
|
444
452
|
if (options.headers) {
|
|
@@ -449,11 +457,16 @@ class OutlookOauth {
|
|
|
449
457
|
if (!Buffer.isBuffer(payload)) {
|
|
450
458
|
reqData.headers.Accept = 'application/json';
|
|
451
459
|
reqData.headers['Content-Type'] = options?.contentType || 'application/json';
|
|
452
|
-
|
|
460
|
+
// Use string body instead of Buffer to avoid ArrayBuffer detachment on retry
|
|
461
|
+
reqData.body = JSON.stringify(payload);
|
|
453
462
|
} else {
|
|
454
463
|
reqData.headers['Content-Type'] = options?.contentType || 'application/x-www-form-urlencoded';
|
|
464
|
+
reqData.body = payload;
|
|
465
|
+
if (payload.length > 0) {
|
|
466
|
+
// Non-empty buffers use non-retry dispatcher to prevent ArrayBuffer detachment
|
|
467
|
+
reqData.dispatcher = httpAgent.fetch;
|
|
468
|
+
}
|
|
455
469
|
}
|
|
456
|
-
reqData.body = payload;
|
|
457
470
|
} else if (payload && method === 'get') {
|
|
458
471
|
let parsedUrl = new URL(url);
|
|
459
472
|
for (let key of Object.keys(payload)) {
|
|
@@ -465,8 +478,6 @@ class OutlookOauth {
|
|
|
465
478
|
url = parsedUrl.href;
|
|
466
479
|
}
|
|
467
480
|
|
|
468
|
-
let retryCount = 0;
|
|
469
|
-
|
|
470
481
|
let startTime = Date.now();
|
|
471
482
|
let res = await fetchCmd(url, reqData);
|
|
472
483
|
let reqTime = Date.now() - startTime;
|
|
@@ -481,7 +492,6 @@ class OutlookOauth {
|
|
|
481
492
|
status: res.status,
|
|
482
493
|
clientId: this.clientId,
|
|
483
494
|
scopes: this.scopes,
|
|
484
|
-
retryCount,
|
|
485
495
|
reqTime
|
|
486
496
|
};
|
|
487
497
|
|
|
@@ -202,6 +202,11 @@ class PubSubInstance {
|
|
|
202
202
|
|
|
203
203
|
await oauth2Apps.setMeta(this.app, { pubSubFlag: null });
|
|
204
204
|
} catch (err) {
|
|
205
|
+
// Transient network errors are expected for long-polling connections
|
|
206
|
+
if (['ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'UND_ERR_SOCKET', 'UND_ERR_CONNECT_TIMEOUT'].includes(err.code)) {
|
|
207
|
+
logger.warn({ msg: 'Transient error pulling subscription messages', app: this.app, code: err.code });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
205
210
|
logger.error({ msg: 'Failed to pull subscription messages', app: this.app, err });
|
|
206
211
|
throw err;
|
|
207
212
|
}
|
package/lib/routes-ui.js
CHANGED
|
@@ -24,8 +24,10 @@ const {
|
|
|
24
24
|
getDuration,
|
|
25
25
|
parseSignedFormData,
|
|
26
26
|
hasEnvValue,
|
|
27
|
-
|
|
27
|
+
httpAgent,
|
|
28
|
+
reloadHttpProxyAgent
|
|
28
29
|
} = require('./tools');
|
|
30
|
+
const { parentPort } = require('worker_threads');
|
|
29
31
|
const { updatePublicInterfaces } = require('./utils/network');
|
|
30
32
|
const packageData = require('../package.json');
|
|
31
33
|
const he = require('he');
|
|
@@ -46,7 +48,8 @@ const {
|
|
|
46
48
|
accountIdSchema,
|
|
47
49
|
defaultAccountTypeSchema,
|
|
48
50
|
googleProjectIdSchema,
|
|
49
|
-
googleWorkspaceAccountsSchema
|
|
51
|
+
googleWorkspaceAccountsSchema,
|
|
52
|
+
exportIdSchema
|
|
50
53
|
} = require('./schemas');
|
|
51
54
|
const fs = require('fs');
|
|
52
55
|
const pathlib = require('path');
|
|
@@ -67,6 +70,7 @@ const { simpleParser } = require('mailparser');
|
|
|
67
70
|
const libmime = require('libmime');
|
|
68
71
|
|
|
69
72
|
const adminEntitiesRoutes = require('./ui-routes/admin-entities-routes');
|
|
73
|
+
const { Export } = require('./export');
|
|
70
74
|
|
|
71
75
|
const {
|
|
72
76
|
DEFAULT_MAX_LOG_LINES,
|
|
@@ -869,6 +873,215 @@ function applyRoutes(server, call) {
|
|
|
869
873
|
// Initialize admin entity routes (webhooks, templates, gateways, tokens)
|
|
870
874
|
adminEntitiesRoutes({ server, call });
|
|
871
875
|
|
|
876
|
+
// Export routes for session-authenticated UI
|
|
877
|
+
|
|
878
|
+
function throwAsBoom(err) {
|
|
879
|
+
if (Boom.isBoom(err)) {
|
|
880
|
+
throw err;
|
|
881
|
+
}
|
|
882
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
883
|
+
if (err.code) {
|
|
884
|
+
error.output.payload.code = err.code;
|
|
885
|
+
}
|
|
886
|
+
throw error;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// List exports for account
|
|
890
|
+
server.route({
|
|
891
|
+
method: 'GET',
|
|
892
|
+
path: '/admin/accounts/{account}/exports',
|
|
893
|
+
async handler(request) {
|
|
894
|
+
try {
|
|
895
|
+
return await Export.list(request.params.account, {
|
|
896
|
+
page: request.query.page,
|
|
897
|
+
pageSize: request.query.pageSize
|
|
898
|
+
});
|
|
899
|
+
} catch (err) {
|
|
900
|
+
request.logger.error({ msg: 'Failed to list exports', err, account: request.params.account });
|
|
901
|
+
throwAsBoom(err);
|
|
902
|
+
}
|
|
903
|
+
},
|
|
904
|
+
options: {
|
|
905
|
+
validate: {
|
|
906
|
+
options: {
|
|
907
|
+
stripUnknown: true,
|
|
908
|
+
abortEarly: false,
|
|
909
|
+
convert: true
|
|
910
|
+
},
|
|
911
|
+
failAction,
|
|
912
|
+
params: Joi.object({
|
|
913
|
+
account: Joi.string().max(256).required()
|
|
914
|
+
}),
|
|
915
|
+
query: Joi.object({
|
|
916
|
+
page: Joi.number().integer().min(0).default(0),
|
|
917
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20)
|
|
918
|
+
})
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// Get export status
|
|
924
|
+
server.route({
|
|
925
|
+
method: 'GET',
|
|
926
|
+
path: '/admin/accounts/{account}/export/{exportId}',
|
|
927
|
+
async handler(request) {
|
|
928
|
+
try {
|
|
929
|
+
const result = await Export.get(request.params.account, request.params.exportId);
|
|
930
|
+
if (!result) {
|
|
931
|
+
throw Boom.notFound('Export not found');
|
|
932
|
+
}
|
|
933
|
+
return result;
|
|
934
|
+
} catch (err) {
|
|
935
|
+
request.logger.error({ msg: 'Failed to get export', err, account: request.params.account, exportId: request.params.exportId });
|
|
936
|
+
throwAsBoom(err);
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
options: {
|
|
940
|
+
validate: {
|
|
941
|
+
options: {
|
|
942
|
+
stripUnknown: true,
|
|
943
|
+
abortEarly: false,
|
|
944
|
+
convert: true
|
|
945
|
+
},
|
|
946
|
+
failAction,
|
|
947
|
+
params: Joi.object({
|
|
948
|
+
account: Joi.string().max(256).required(),
|
|
949
|
+
exportId: exportIdSchema
|
|
950
|
+
})
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
// Create export
|
|
956
|
+
server.route({
|
|
957
|
+
method: 'POST',
|
|
958
|
+
path: '/admin/accounts/{account}/export',
|
|
959
|
+
async handler(request) {
|
|
960
|
+
try {
|
|
961
|
+
return await Export.create(request.params.account, {
|
|
962
|
+
startDate: request.payload.startDate,
|
|
963
|
+
endDate: request.payload.endDate,
|
|
964
|
+
includeAttachments: request.payload.includeAttachments,
|
|
965
|
+
folders: []
|
|
966
|
+
});
|
|
967
|
+
} catch (err) {
|
|
968
|
+
request.logger.error({ msg: 'Failed to create export', err, account: request.params.account });
|
|
969
|
+
throwAsBoom(err);
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
options: {
|
|
973
|
+
validate: {
|
|
974
|
+
options: {
|
|
975
|
+
stripUnknown: true,
|
|
976
|
+
abortEarly: false,
|
|
977
|
+
convert: true
|
|
978
|
+
},
|
|
979
|
+
failAction,
|
|
980
|
+
params: Joi.object({
|
|
981
|
+
account: Joi.string().max(256).required()
|
|
982
|
+
}),
|
|
983
|
+
payload: Joi.object({
|
|
984
|
+
startDate: Joi.date().iso().required(),
|
|
985
|
+
endDate: Joi.date().iso().required(),
|
|
986
|
+
includeAttachments: Joi.boolean().default(false)
|
|
987
|
+
})
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
// Delete export
|
|
993
|
+
server.route({
|
|
994
|
+
method: 'DELETE',
|
|
995
|
+
path: '/admin/accounts/{account}/export/{exportId}',
|
|
996
|
+
async handler(request) {
|
|
997
|
+
try {
|
|
998
|
+
const deleted = await Export.delete(request.params.account, request.params.exportId);
|
|
999
|
+
if (!deleted) {
|
|
1000
|
+
throw Boom.notFound('Export not found');
|
|
1001
|
+
}
|
|
1002
|
+
return { deleted: true };
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
request.logger.error({ msg: 'Failed to delete export', err, account: request.params.account, exportId: request.params.exportId });
|
|
1005
|
+
throwAsBoom(err);
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
options: {
|
|
1009
|
+
validate: {
|
|
1010
|
+
options: {
|
|
1011
|
+
stripUnknown: true,
|
|
1012
|
+
abortEarly: false,
|
|
1013
|
+
convert: true
|
|
1014
|
+
},
|
|
1015
|
+
failAction,
|
|
1016
|
+
params: Joi.object({
|
|
1017
|
+
account: Joi.string().max(256).required(),
|
|
1018
|
+
exportId: exportIdSchema
|
|
1019
|
+
})
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// Download export file
|
|
1025
|
+
server.route({
|
|
1026
|
+
method: 'GET',
|
|
1027
|
+
path: '/admin/accounts/{account}/export/{exportId}/download',
|
|
1028
|
+
async handler(request, h) {
|
|
1029
|
+
try {
|
|
1030
|
+
const { account, exportId } = request.params;
|
|
1031
|
+
const fileInfo = await Export.getFile(account, exportId);
|
|
1032
|
+
if (!fileInfo) {
|
|
1033
|
+
throw Boom.notFound('Export not found');
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const fileReadStream = fs.createReadStream(fileInfo.filePath);
|
|
1037
|
+
let stream = fileReadStream;
|
|
1038
|
+
|
|
1039
|
+
stream.on('error', err => {
|
|
1040
|
+
request.logger.error({ msg: 'Export download stream error', exportId, err });
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// Decrypt file if encrypted
|
|
1044
|
+
if (fileInfo.isEncrypted) {
|
|
1045
|
+
const secret = await getSecret();
|
|
1046
|
+
if (!secret) {
|
|
1047
|
+
fileReadStream.destroy();
|
|
1048
|
+
throw Boom.serverUnavailable('Encryption secret not available for decryption');
|
|
1049
|
+
}
|
|
1050
|
+
const { createDecryptStream } = require('./stream-encrypt');
|
|
1051
|
+
const decryptStream = await createDecryptStream(secret);
|
|
1052
|
+
decryptStream.on('error', err => {
|
|
1053
|
+
request.logger.error({ msg: 'Export decryption error', exportId, err });
|
|
1054
|
+
fileReadStream.destroy();
|
|
1055
|
+
});
|
|
1056
|
+
stream = fileReadStream.pipe(decryptStream);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return h
|
|
1060
|
+
.response(stream)
|
|
1061
|
+
.type('application/gzip')
|
|
1062
|
+
.header('Content-Disposition', `attachment; filename="${fileInfo.filename}"`)
|
|
1063
|
+
.header('Content-Encoding', 'identity');
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
request.logger.error({ msg: 'Failed to download export', err, account: request.params.account, exportId: request.params.exportId });
|
|
1066
|
+
throwAsBoom(err);
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
options: {
|
|
1070
|
+
validate: {
|
|
1071
|
+
options: {
|
|
1072
|
+
stripUnknown: true,
|
|
1073
|
+
abortEarly: false,
|
|
1074
|
+
convert: true
|
|
1075
|
+
},
|
|
1076
|
+
failAction,
|
|
1077
|
+
params: Joi.object({
|
|
1078
|
+
account: Joi.string().max(256).required(),
|
|
1079
|
+
exportId: exportIdSchema
|
|
1080
|
+
})
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
|
|
872
1085
|
const getDefaultPrompt = async () =>
|
|
873
1086
|
await call({
|
|
874
1087
|
cmd: 'openAiDefaultPrompt'
|
|
@@ -2230,7 +2443,7 @@ return true;`
|
|
|
2230
2443
|
}
|
|
2231
2444
|
}),
|
|
2232
2445
|
headers,
|
|
2233
|
-
dispatcher:
|
|
2446
|
+
dispatcher: httpAgent.retry
|
|
2234
2447
|
});
|
|
2235
2448
|
duration = Date.now() - start;
|
|
2236
2449
|
} catch (err) {
|
|
@@ -3293,7 +3506,7 @@ return true;`
|
|
|
3293
3506
|
url: (await settings.get('serviceUrl')) || ''
|
|
3294
3507
|
}),
|
|
3295
3508
|
headers,
|
|
3296
|
-
dispatcher:
|
|
3509
|
+
dispatcher: httpAgent.retry
|
|
3297
3510
|
});
|
|
3298
3511
|
|
|
3299
3512
|
if (!res.ok) {
|
|
@@ -4435,6 +4648,11 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
|
|
|
4435
4648
|
|
|
4436
4649
|
const nonce = data.n || crypto.randomBytes(NONCE_BYTES).toString('base64url');
|
|
4437
4650
|
|
|
4651
|
+
// Validate nonce format (base64url, 21-22 chars; also accept base64 for backward compatibility)
|
|
4652
|
+
if (!/^[A-Za-z0-9_\-+/]{21,22}={0,2}$/.test(nonce)) {
|
|
4653
|
+
throw Boom.badRequest('Invalid nonce format. Please generate a new authentication URL.');
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4438
4656
|
// store account data with atomic SET + EX
|
|
4439
4657
|
await redis.set(`${REDIS_PREFIX}account:add:${nonce}`, JSON.stringify(accountData), 'EX', Math.floor(MAX_FORM_TTL / 1000));
|
|
4440
4658
|
|
|
@@ -4725,11 +4943,29 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
|
|
|
4725
4943
|
case 'EAUTH':
|
|
4726
4944
|
verifyResult.smtp.error = request.app.gt.gettext('Invalid username or password');
|
|
4727
4945
|
break;
|
|
4946
|
+
case 'ENOAUTH':
|
|
4947
|
+
verifyResult.smtp.error = request.app.gt.gettext('Authentication credentials were not provided');
|
|
4948
|
+
break;
|
|
4949
|
+
case 'EOAUTH2':
|
|
4950
|
+
verifyResult.smtp.error = request.app.gt.gettext('OAuth2 authentication failed');
|
|
4951
|
+
break;
|
|
4952
|
+
case 'ETLS':
|
|
4953
|
+
verifyResult.smtp.error = request.app.gt.gettext('TLS protocol error');
|
|
4954
|
+
break;
|
|
4728
4955
|
case 'ESOCKET':
|
|
4729
4956
|
if (/openssl/.test(verifyResult.smtp.error)) {
|
|
4730
4957
|
verifyResult.smtp.error = request.app.gt.gettext('TLS protocol error');
|
|
4731
4958
|
}
|
|
4732
4959
|
break;
|
|
4960
|
+
case 'ETIMEDOUT':
|
|
4961
|
+
verifyResult.smtp.error = request.app.gt.gettext('Connection timed out');
|
|
4962
|
+
break;
|
|
4963
|
+
case 'ECONNECTION':
|
|
4964
|
+
verifyResult.smtp.error = request.app.gt.gettext('Could not connect to server');
|
|
4965
|
+
break;
|
|
4966
|
+
case 'EPROTOCOL':
|
|
4967
|
+
verifyResult.smtp.error = request.app.gt.gettext('Unexpected server response');
|
|
4968
|
+
break;
|
|
4733
4969
|
}
|
|
4734
4970
|
}
|
|
4735
4971
|
}
|
|
@@ -6547,6 +6783,8 @@ return payload;`
|
|
|
6547
6783
|
let proxyEnabled = await settings.get('proxyEnabled');
|
|
6548
6784
|
let proxyUrl = await settings.get('proxyUrl');
|
|
6549
6785
|
let smtpEhloName = await settings.get('smtpEhloName');
|
|
6786
|
+
let httpProxyEnabled = await settings.get('httpProxyEnabled');
|
|
6787
|
+
let httpProxyUrl = await settings.get('httpProxyUrl');
|
|
6550
6788
|
|
|
6551
6789
|
let localAddresses = [].concat((await settings.get('localAddresses')) || []);
|
|
6552
6790
|
|
|
@@ -6566,7 +6804,9 @@ return payload;`
|
|
|
6566
6804
|
values: {
|
|
6567
6805
|
proxyEnabled,
|
|
6568
6806
|
proxyUrl,
|
|
6569
|
-
smtpEhloName
|
|
6807
|
+
smtpEhloName,
|
|
6808
|
+
httpProxyEnabled,
|
|
6809
|
+
httpProxyUrl
|
|
6570
6810
|
},
|
|
6571
6811
|
|
|
6572
6812
|
addresses: await listPublicInterfaces(localAddresses),
|
|
@@ -6619,10 +6859,26 @@ return payload;`
|
|
|
6619
6859
|
path: '/admin/config/network',
|
|
6620
6860
|
async handler(request, h) {
|
|
6621
6861
|
try {
|
|
6622
|
-
for (let key of [
|
|
6862
|
+
for (let key of [
|
|
6863
|
+
'smtpStrategy',
|
|
6864
|
+
'imapStrategy',
|
|
6865
|
+
'localAddresses',
|
|
6866
|
+
'proxyUrl',
|
|
6867
|
+
'smtpEhloName',
|
|
6868
|
+
'proxyEnabled',
|
|
6869
|
+
'httpProxyEnabled',
|
|
6870
|
+
'httpProxyUrl'
|
|
6871
|
+
]) {
|
|
6623
6872
|
await settings.set(key, request.payload[key]);
|
|
6624
6873
|
}
|
|
6625
6874
|
|
|
6875
|
+
await reloadHttpProxyAgent();
|
|
6876
|
+
|
|
6877
|
+
// Notify other workers about settings change
|
|
6878
|
+
if (parentPort) {
|
|
6879
|
+
parentPort.postMessage({ cmd: 'settings', data: request.payload });
|
|
6880
|
+
}
|
|
6881
|
+
|
|
6626
6882
|
await request.flash({ type: 'info', message: `Configuration updated` });
|
|
6627
6883
|
|
|
6628
6884
|
return h.redirect('/admin/config/network');
|
|
@@ -6706,7 +6962,10 @@ return payload;`
|
|
|
6706
6962
|
|
|
6707
6963
|
proxyUrl: settingsSchema.proxyUrl,
|
|
6708
6964
|
smtpEhloName: settingsSchema.smtpEhloName,
|
|
6709
|
-
proxyEnabled: settingsSchema.proxyEnabled
|
|
6965
|
+
proxyEnabled: settingsSchema.proxyEnabled,
|
|
6966
|
+
|
|
6967
|
+
httpProxyEnabled: settingsSchema.httpProxyEnabled,
|
|
6968
|
+
httpProxyUrl: settingsSchema.httpProxyUrl
|
|
6710
6969
|
})
|
|
6711
6970
|
}
|
|
6712
6971
|
}
|
|
@@ -7241,7 +7500,7 @@ Token: ${JSON.stringify(request.params.token)}`
|
|
|
7241
7500
|
requestor: '@postalsys/emailengine-app'
|
|
7242
7501
|
}),
|
|
7243
7502
|
headers,
|
|
7244
|
-
dispatcher:
|
|
7503
|
+
dispatcher: httpAgent.retry
|
|
7245
7504
|
});
|
|
7246
7505
|
|
|
7247
7506
|
if (!res.ok) {
|
|
@@ -7352,7 +7611,7 @@ ${now}`,
|
|
|
7352
7611
|
let res = await fetchCmd(`${SMTP_TEST_HOST}/test-address/${user}`, {
|
|
7353
7612
|
method: 'get',
|
|
7354
7613
|
headers,
|
|
7355
|
-
dispatcher:
|
|
7614
|
+
dispatcher: httpAgent.retry
|
|
7356
7615
|
});
|
|
7357
7616
|
|
|
7358
7617
|
if (!res.ok) {
|