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.
Files changed (65) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/data/google-crawlers.json +1 -1
  3. package/lib/account.js +20 -7
  4. package/lib/api-routes/account-routes.js +28 -5
  5. package/lib/api-routes/chat-routes.js +1 -1
  6. package/lib/api-routes/export-routes.js +316 -0
  7. package/lib/api-routes/message-routes.js +28 -23
  8. package/lib/api-routes/template-routes.js +28 -7
  9. package/lib/arf-detect.js +1 -1
  10. package/lib/autodetect-imap-settings.js +5 -5
  11. package/lib/consts.js +16 -0
  12. package/lib/db.js +3 -0
  13. package/lib/email-client/base-client.js +6 -4
  14. package/lib/email-client/gmail-client.js +205 -35
  15. package/lib/email-client/imap/mailbox.js +99 -8
  16. package/lib/email-client/imap/subconnection.js +5 -5
  17. package/lib/email-client/imap-client.js +76 -19
  18. package/lib/email-client/message-builder.js +3 -1
  19. package/lib/email-client/notification-handler.js +12 -9
  20. package/lib/email-client/outlook-client.js +364 -73
  21. package/lib/email-client/smtp-pool-manager.js +1 -1
  22. package/lib/export.js +528 -0
  23. package/lib/oauth/gmail.js +24 -16
  24. package/lib/oauth/mail-ru.js +26 -13
  25. package/lib/oauth/outlook.js +29 -19
  26. package/lib/oauth/pubsub/google.js +5 -0
  27. package/lib/routes-ui.js +268 -9
  28. package/lib/schemas.js +274 -81
  29. package/lib/stream-encrypt.js +263 -0
  30. package/lib/sub-script.js +2 -2
  31. package/lib/tools.js +194 -12
  32. package/lib/ui-routes/account-routes.js +23 -0
  33. package/lib/ui-routes/admin-config-routes.js +13 -6
  34. package/lib/ui-routes/admin-entities-routes.js +18 -0
  35. package/lib/webhooks.js +16 -20
  36. package/package.json +20 -20
  37. package/sbom.json +1 -1
  38. package/server.js +66 -7
  39. package/static/js/ace/ace.js +1 -1
  40. package/static/js/ace/ext-language_tools.js +1 -1
  41. package/static/licenses.html +118 -149
  42. package/translations/de.mo +0 -0
  43. package/translations/de.po +63 -36
  44. package/translations/en.mo +0 -0
  45. package/translations/en.po +64 -37
  46. package/translations/et.mo +0 -0
  47. package/translations/et.po +63 -36
  48. package/translations/fr.mo +0 -0
  49. package/translations/fr.po +63 -36
  50. package/translations/ja.mo +0 -0
  51. package/translations/ja.po +63 -36
  52. package/translations/messages.pot +84 -51
  53. package/translations/nl.mo +0 -0
  54. package/translations/nl.po +63 -36
  55. package/translations/pl.mo +0 -0
  56. package/translations/pl.po +63 -36
  57. package/views/accounts/account.hbs +375 -2
  58. package/views/config/network.hbs +45 -0
  59. package/views/config/service.hbs +35 -0
  60. package/workers/api.js +130 -47
  61. package/workers/documents.js +3 -2
  62. package/workers/export.js +933 -0
  63. package/workers/imap.js +34 -1
  64. package/workers/submit.js +33 -6
  65. package/workers/webhooks.js +20 -4
@@ -0,0 +1,263 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { Transform } = require('stream');
5
+
6
+ const MAGIC = Buffer.from('EE01');
7
+ const VERSION = 1;
8
+ const CHUNK_SIZE = 64 * 1024;
9
+ const IV_LENGTH = 12;
10
+ const AUTH_TAG_LENGTH = 16;
11
+ const SALT_LENGTH = 16;
12
+ const HEADER_SIZE = MAGIC.length + 4 + 4 + SALT_LENGTH;
13
+ const MAX_CHUNK_SIZE = CHUNK_SIZE * 4;
14
+
15
+ function deriveKeyAsync(password, salt) {
16
+ return new Promise((resolve, reject) => {
17
+ crypto.scrypt(password, salt, 32, (err, key) => {
18
+ if (err) reject(err);
19
+ else resolve(key);
20
+ });
21
+ });
22
+ }
23
+
24
+ class EncryptStream extends Transform {
25
+ constructor({ key, salt }) {
26
+ super();
27
+ this.key = key;
28
+ this.salt = salt;
29
+ this.buffer = Buffer.alloc(0);
30
+ this.headerWritten = false;
31
+ }
32
+
33
+ _writeHeader() {
34
+ if (this.headerWritten) return;
35
+
36
+ const versionBuf = Buffer.alloc(4);
37
+ versionBuf.writeUInt32LE(VERSION);
38
+
39
+ const chunkSizeBuf = Buffer.alloc(4);
40
+ chunkSizeBuf.writeUInt32LE(CHUNK_SIZE);
41
+
42
+ this.push(Buffer.concat([MAGIC, versionBuf, chunkSizeBuf, this.salt]));
43
+ this.headerWritten = true;
44
+ }
45
+
46
+ _encryptChunk(chunk) {
47
+ const iv = crypto.randomBytes(IV_LENGTH);
48
+ const cipher = crypto.createCipheriv('aes-256-gcm', this.key, iv, { authTagLength: AUTH_TAG_LENGTH });
49
+
50
+ const encrypted = Buffer.concat([cipher.update(chunk), cipher.final()]);
51
+ const authTag = cipher.getAuthTag();
52
+
53
+ const chunkHeader = Buffer.alloc(IV_LENGTH + 4);
54
+ iv.copy(chunkHeader, 0);
55
+ chunkHeader.writeUInt32LE(encrypted.length, IV_LENGTH);
56
+
57
+ return Buffer.concat([chunkHeader, encrypted, authTag]);
58
+ }
59
+
60
+ _transform(data, encoding, callback) {
61
+ try {
62
+ this._writeHeader();
63
+
64
+ this.buffer = Buffer.concat([this.buffer, data]);
65
+
66
+ while (this.buffer.length >= CHUNK_SIZE) {
67
+ const chunk = this.buffer.subarray(0, CHUNK_SIZE);
68
+ this.buffer = this.buffer.subarray(CHUNK_SIZE);
69
+ this.push(this._encryptChunk(chunk));
70
+ }
71
+
72
+ callback();
73
+ } catch (err) {
74
+ callback(err);
75
+ }
76
+ }
77
+
78
+ _flush(callback) {
79
+ try {
80
+ this._writeHeader();
81
+
82
+ if (this.buffer.length > 0) {
83
+ this.push(this._encryptChunk(this.buffer));
84
+ }
85
+
86
+ this.key = null;
87
+ callback();
88
+ } catch (err) {
89
+ callback(err);
90
+ }
91
+ }
92
+ }
93
+
94
+ class DecryptStream extends Transform {
95
+ constructor(secret) {
96
+ super();
97
+ this.secret = secret;
98
+ this.buffer = Buffer.alloc(0);
99
+ this.headerParsed = false;
100
+ this.key = null;
101
+ this.chunkSize = CHUNK_SIZE;
102
+ }
103
+
104
+ _parseHeader() {
105
+ if (this.buffer.length < HEADER_SIZE) {
106
+ return false;
107
+ }
108
+
109
+ let offset = 0;
110
+
111
+ const magic = this.buffer.subarray(offset, offset + MAGIC.length);
112
+ if (!magic.equals(MAGIC)) {
113
+ throw new Error('Invalid encrypted file format: bad magic bytes');
114
+ }
115
+ offset += MAGIC.length;
116
+
117
+ const version = this.buffer.readUInt32LE(offset);
118
+ if (version !== VERSION) {
119
+ throw new Error(`Unsupported encryption version: ${version}`);
120
+ }
121
+ offset += 4;
122
+
123
+ this.chunkSize = this.buffer.readUInt32LE(offset);
124
+ if (this.chunkSize > MAX_CHUNK_SIZE) {
125
+ throw new Error('Invalid chunk size in header');
126
+ }
127
+ offset += 4;
128
+
129
+ this.salt = this.buffer.subarray(offset, offset + SALT_LENGTH);
130
+
131
+ this.buffer = this.buffer.subarray(HEADER_SIZE);
132
+ this.headerParsed = true;
133
+
134
+ return true;
135
+ }
136
+
137
+ _decryptChunk() {
138
+ const minChunkHeader = IV_LENGTH + 4;
139
+ if (this.buffer.length < minChunkHeader) {
140
+ return null;
141
+ }
142
+
143
+ const iv = this.buffer.subarray(0, IV_LENGTH);
144
+ const encryptedLength = this.buffer.readUInt32LE(IV_LENGTH);
145
+ if (encryptedLength > this.chunkSize + 256) {
146
+ throw new Error('Invalid encrypted chunk length');
147
+ }
148
+ const totalChunkSize = minChunkHeader + encryptedLength + AUTH_TAG_LENGTH;
149
+
150
+ if (this.buffer.length < totalChunkSize) {
151
+ return null;
152
+ }
153
+
154
+ const encryptedData = this.buffer.subarray(minChunkHeader, minChunkHeader + encryptedLength);
155
+ const authTag = this.buffer.subarray(minChunkHeader + encryptedLength, totalChunkSize);
156
+
157
+ const decipher = crypto.createDecipheriv('aes-256-gcm', this.key, iv, { authTagLength: AUTH_TAG_LENGTH });
158
+ decipher.setAuthTag(authTag);
159
+
160
+ let decrypted;
161
+ try {
162
+ decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
163
+ } catch (err) {
164
+ if (err.message.includes('auth')) {
165
+ throw new Error('Decryption failed: invalid secret or corrupted data');
166
+ }
167
+ throw err;
168
+ }
169
+
170
+ this.buffer = this.buffer.subarray(totalChunkSize);
171
+
172
+ return decrypted;
173
+ }
174
+
175
+ _processBuffer(callback) {
176
+ try {
177
+ let decrypted;
178
+ while ((decrypted = this._decryptChunk()) !== null) {
179
+ this.push(decrypted);
180
+ }
181
+ callback();
182
+ } catch (err) {
183
+ callback(err);
184
+ }
185
+ }
186
+
187
+ _transform(data, encoding, callback) {
188
+ this.buffer = Buffer.concat([this.buffer, data]);
189
+
190
+ if (!this.headerParsed) {
191
+ try {
192
+ if (!this._parseHeader()) {
193
+ callback();
194
+ return;
195
+ }
196
+ } catch (err) {
197
+ callback(err);
198
+ return;
199
+ }
200
+ crypto.scrypt(this.secret, this.salt, 32, (err, key) => {
201
+ if (err) {
202
+ callback(err);
203
+ return;
204
+ }
205
+ this.key = key;
206
+ this.secret = null;
207
+ this._processBuffer(callback);
208
+ });
209
+ return;
210
+ }
211
+
212
+ this._processBuffer(callback);
213
+ }
214
+
215
+ _flush(callback) {
216
+ try {
217
+ if (this.buffer.length > 0) {
218
+ if (!this.headerParsed) {
219
+ throw new Error('Invalid encrypted file: incomplete header');
220
+ }
221
+
222
+ const decrypted = this._decryptChunk();
223
+ if (decrypted !== null) {
224
+ this.push(decrypted);
225
+ }
226
+
227
+ if (this.buffer.length > 0) {
228
+ throw new Error('Invalid encrypted file: incomplete final chunk');
229
+ }
230
+ }
231
+
232
+ this.key = null;
233
+ callback();
234
+ } catch (err) {
235
+ callback(err);
236
+ }
237
+ }
238
+ }
239
+
240
+ async function createEncryptStream(secret) {
241
+ if (!secret) {
242
+ throw new Error('Encryption secret is required');
243
+ }
244
+ const salt = crypto.randomBytes(SALT_LENGTH);
245
+ const key = await deriveKeyAsync(secret, salt);
246
+ return new EncryptStream({ key, salt });
247
+ }
248
+
249
+ async function createDecryptStream(secret) {
250
+ if (!secret) {
251
+ throw new Error('Decryption secret is required');
252
+ }
253
+ return new DecryptStream(secret);
254
+ }
255
+
256
+ module.exports = {
257
+ createEncryptStream,
258
+ createDecryptStream,
259
+ MAGIC,
260
+ VERSION,
261
+ CHUNK_SIZE,
262
+ HEADER_SIZE
263
+ };
package/lib/sub-script.js CHANGED
@@ -4,7 +4,7 @@ const crypto = require('crypto');
4
4
  const vm = require('vm');
5
5
  const logger = require('./logger');
6
6
  const settings = require('./settings');
7
- const { retryAgent } = require('./tools');
7
+ const { httpAgent } = require('./tools');
8
8
 
9
9
  const { SUBSCRIPT_RUNTIME_TIMEOUT } = require('./consts');
10
10
 
@@ -46,7 +46,7 @@ const wrappedFetch = (...args) => {
46
46
  opts = args[1];
47
47
  }
48
48
 
49
- return fetchCmd(args[0], Object.assign({}, opts, { dispatcher: retryAgent }));
49
+ return fetchCmd(args[0], Object.assign({}, opts, { dispatcher: httpAgent.retry }));
50
50
  };
51
51
 
52
52
  class SubScript {
package/lib/tools.js CHANGED
@@ -45,20 +45,168 @@ const bullmqPackage = require('bullmq/package.json');
45
45
  // Network utilities - imported for internal use only
46
46
  const { getLocalAddress } = require('./utils/network');
47
47
 
48
- const { fetch: fetchCmd, Agent, RetryAgent } = require('undici');
48
+ const { fetch: fetchCmd, Agent, RetryAgent, ProxyAgent } = require('undici');
49
+ const tls = require('tls');
50
+ const { SocksClient } = require('socks');
49
51
 
50
- const fetchAgent = new Agent({
52
+ const AGENT_OPTS = {
51
53
  strictContentLength: false,
52
54
  connectTimeout: Math.min(30000, URL_FETCH_TIMEOUT), // up to 30s for connection
53
55
  headersTimeout: URL_FETCH_TIMEOUT, // Full timeout (90s)
54
56
  bodyTimeout: URL_FETCH_TIMEOUT // Full timeout (90s)
55
- });
57
+ };
56
58
 
57
- const retryAgent = new RetryAgent(fetchAgent, {
59
+ const RETRY_OPTS = {
58
60
  maxRetries: URL_FETCH_RETRY_MAX,
59
61
  methods: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
60
62
  statusCodes: [429] // do not retry 5xx errors
61
- });
63
+ };
64
+
65
+ // Shared mutable object -- consumers import the object reference and access
66
+ // .fetch / .retry at call time, so replacing properties here is visible everywhere.
67
+ const httpAgent = {
68
+ fetch: new Agent(AGENT_OPTS),
69
+ retry: null
70
+ };
71
+ httpAgent.retry = new RetryAgent(httpAgent.fetch, RETRY_OPTS);
72
+
73
+ // Backward-compat aliases (read from httpAgent at call time via getter)
74
+ // backward-compat aliases are defined as getters on module.exports below
75
+
76
+ const SOCKS4_PROTOCOLS = new Set(['socks4:', 'socks4a:']);
77
+
78
+ function createSocksAgent(proxyUrl, agentOpts) {
79
+ let parsed = new URL(proxyUrl);
80
+ let proxyType = SOCKS4_PROTOCOLS.has(parsed.protocol) ? 4 : 5;
81
+ let proxyHost = parsed.hostname;
82
+ let proxyPort = Number(parsed.port) || 1080;
83
+
84
+ let proxy = {
85
+ host: proxyHost,
86
+ port: proxyPort,
87
+ type: proxyType
88
+ };
89
+
90
+ if (parsed.username) {
91
+ proxy.userId = decodeURIComponent(parsed.username);
92
+ proxy.password = parsed.password ? decodeURIComponent(parsed.password) : '';
93
+ }
94
+
95
+ return new Agent(
96
+ Object.assign({}, agentOpts, {
97
+ connect: async opts => {
98
+ let { hostname, port, protocol } = opts;
99
+ let useTls = protocol === 'https:';
100
+
101
+ let info = await SocksClient.createConnection({
102
+ proxy,
103
+ command: 'connect',
104
+ destination: {
105
+ host: hostname,
106
+ port: Number(port) || (useTls ? 443 : 80)
107
+ },
108
+ timeout: agentOpts.connectTimeout || 30000
109
+ });
110
+
111
+ let socket = info.socket;
112
+
113
+ if (useTls) {
114
+ let rawSocket = socket;
115
+ socket = tls.connect(Object.assign({ socket: rawSocket, servername: hostname }, TLS_DEFAULTS));
116
+ try {
117
+ await new Promise((resolve, reject) => {
118
+ socket.once('secureConnect', resolve);
119
+ socket.once('error', reject);
120
+ });
121
+ } catch (err) {
122
+ socket.destroy();
123
+ rawSocket.destroy();
124
+ throw err;
125
+ }
126
+ }
127
+
128
+ return socket;
129
+ }
130
+ })
131
+ );
132
+ }
133
+
134
+ let _reloadPromise = null;
135
+
136
+ async function reloadHttpProxyAgent() {
137
+ if (_reloadPromise) {
138
+ return _reloadPromise;
139
+ }
140
+ _reloadPromise = _doReloadHttpProxyAgent();
141
+ try {
142
+ return await _reloadPromise;
143
+ } finally {
144
+ _reloadPromise = null;
145
+ }
146
+ }
147
+
148
+ async function _doReloadHttpProxyAgent() {
149
+ let enabled, proxyUrl;
150
+ try {
151
+ enabled = await settings.get('httpProxyEnabled');
152
+ proxyUrl = await settings.get('httpProxyUrl');
153
+ } catch (err) {
154
+ logger.error({ msg: 'Failed to read HTTP proxy settings', err });
155
+ return;
156
+ }
157
+
158
+ // Environment variable overrides
159
+ let envEnabled = process.env.EENGINE_HTTP_PROXY_ENABLED;
160
+ let envUrl = process.env.EENGINE_HTTP_PROXY_URL;
161
+ if (typeof envUrl === 'string' && envUrl) {
162
+ proxyUrl = envUrl;
163
+ }
164
+ if (typeof envEnabled === 'string' && envEnabled) {
165
+ enabled = /^(true|1|y|on|yes)$/i.test(envEnabled);
166
+ }
167
+
168
+ let oldFetch = httpAgent.fetch;
169
+
170
+ try {
171
+ if (enabled && proxyUrl) {
172
+ let parsed = new URL(proxyUrl);
173
+ let scheme = parsed.protocol.replace(/:$/, '').toLowerCase();
174
+
175
+ if (['socks', 'socks4', 'socks4a', 'socks5'].includes(scheme)) {
176
+ httpAgent.fetch = createSocksAgent(proxyUrl, AGENT_OPTS);
177
+ } else {
178
+ httpAgent.fetch = new ProxyAgent(Object.assign({}, AGENT_OPTS, { uri: proxyUrl }));
179
+ }
180
+
181
+ // Sanitize credentials for logging
182
+ if (parsed.username || parsed.password) {
183
+ parsed.username = '';
184
+ parsed.password = '';
185
+ }
186
+ proxyUrl = parsed.toString();
187
+ } else {
188
+ httpAgent.fetch = new Agent(AGENT_OPTS);
189
+ }
190
+
191
+ httpAgent.retry = new RetryAgent(httpAgent.fetch, RETRY_OPTS);
192
+
193
+ logger.info({
194
+ msg: 'HTTP proxy agent reloaded',
195
+ httpProxyEnabled: !!enabled,
196
+ httpProxyUrl: enabled && proxyUrl ? proxyUrl : null
197
+ });
198
+
199
+ if (oldFetch && oldFetch !== httpAgent.fetch) {
200
+ try {
201
+ await oldFetch.close();
202
+ } catch (err) {
203
+ // ignore close errors
204
+ }
205
+ }
206
+ } catch (err) {
207
+ logger.error({ msg: 'Failed to create HTTP proxy agent, keeping existing agent', err });
208
+ }
209
+ }
62
210
 
63
211
  class LRUCache extends Map {
64
212
  constructor(maxSize = 1000) {
@@ -77,6 +225,22 @@ class LRUCache extends Map {
77
225
 
78
226
  const regexCache = new LRUCache(1000);
79
227
 
228
+ function formatTokenError(provider, tokenRequest) {
229
+ let parts = [`Token request failed for ${provider}`];
230
+ if (tokenRequest) {
231
+ let detail = `${tokenRequest.grant || 'unknown'}, HTTP ${tokenRequest.status || '?'}`;
232
+ parts[0] += ` (${detail})`;
233
+ if (tokenRequest.response) {
234
+ let resp = tokenRequest.response;
235
+ let errorParts = [resp.error, resp.error_description].filter(Boolean);
236
+ if (errorParts.length) {
237
+ parts.push(errorParts.join(' - '));
238
+ }
239
+ }
240
+ }
241
+ return parts.join(': ');
242
+ }
243
+
80
244
  module.exports = {
81
245
  /**
82
246
  * Helper function to set specific bit in a buffer
@@ -348,7 +512,7 @@ module.exports = {
348
512
  parsed.searchParams.set('account', account);
349
513
  parsed.searchParams.set('proto', proto);
350
514
 
351
- let authResponse = await fetchCmd(parsed.toString(), { method: 'GET', headers, dispatcher: retryAgent });
515
+ let authResponse = await fetchCmd(parsed.toString(), { method: 'GET', headers, dispatcher: httpAgent.retry });
352
516
  if (!authResponse.ok) {
353
517
  throw new Error(`Invalid response: ${authResponse.status} ${authResponse.statusText}`);
354
518
  }
@@ -1335,7 +1499,7 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
1335
1499
  'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
1336
1500
  };
1337
1501
 
1338
- let releaseResponse = await fetchCmd(releaseUrl, { method: 'GET', headers, dispatcher: retryAgent });
1502
+ let releaseResponse = await fetchCmd(releaseUrl, { method: 'GET', headers, dispatcher: httpAgent.retry });
1339
1503
  if (!releaseResponse.ok) {
1340
1504
  let err = new Error(`Failed loading release data`);
1341
1505
  err.response = {
@@ -1382,8 +1546,12 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
1382
1546
  },
1383
1547
 
1384
1548
  validUidValidity(value) {
1385
- if (typeof value === 'bigint' || typeof value === 'number') {
1386
- return true;
1549
+ if (typeof value === 'bigint') {
1550
+ return value > BigInt(0);
1551
+ }
1552
+
1553
+ if (typeof value === 'number') {
1554
+ return Number.isFinite(value) && value > 0;
1387
1555
  }
1388
1556
 
1389
1557
  if (isNaN(value)) {
@@ -1762,7 +1930,10 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
1762
1930
 
1763
1931
  prepareUrl(url, baseUrl, queryParams) {
1764
1932
  let { pathname: baseUrlPathname } = new URL(baseUrl);
1765
- baseUrlPathname = baseUrlPathname.replace(/\/?/, '/');
1933
+ // Ensure pathname ends with trailing slash for proper path concatenation
1934
+ if (!baseUrlPathname.endsWith('/')) {
1935
+ baseUrlPathname += '/';
1936
+ }
1766
1937
 
1767
1938
  const urlObj = new URL(baseUrlPathname + url.replace(/^\//, ''), baseUrl);
1768
1939
 
@@ -1777,11 +1948,22 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
1777
1948
  return urlObj.href;
1778
1949
  },
1779
1950
 
1780
- retryAgent,
1951
+ httpAgent,
1952
+ reloadHttpProxyAgent,
1953
+ createSocksAgent,
1954
+
1955
+ get fetchAgent() {
1956
+ return httpAgent.fetch;
1957
+ },
1958
+ get retryAgent() {
1959
+ return httpAgent.retry;
1960
+ },
1781
1961
 
1782
1962
  LRUCache,
1783
1963
 
1784
- normalizeHashKeys
1964
+ normalizeHashKeys,
1965
+
1966
+ formatTokenError
1785
1967
  };
1786
1968
 
1787
1969
  function msgpackDecode(buf) {
@@ -520,6 +520,11 @@ function init(args) {
520
520
 
521
521
  const nonce = data.n || crypto.randomBytes(NONCE_BYTES).toString('base64url');
522
522
 
523
+ // Validate nonce format (base64url, 21-22 chars; also accept base64 for backward compatibility)
524
+ if (!/^[A-Za-z0-9_\-+/]{21,22}={0,2}$/.test(nonce)) {
525
+ throw Boom.badRequest('Invalid nonce format. Please generate a new authentication URL.');
526
+ }
527
+
523
528
  // store account data with atomic SET + EX
524
529
  await redis.set(`${REDIS_PREFIX}account:add:${nonce}`, JSON.stringify(accountData), 'EX', Math.floor(MAX_FORM_TTL / 1000));
525
530
 
@@ -814,11 +819,29 @@ function init(args) {
814
819
  case 'EAUTH':
815
820
  verifyResult.smtp.error = request.app.gt.gettext('Invalid username or password');
816
821
  break;
822
+ case 'ENOAUTH':
823
+ verifyResult.smtp.error = request.app.gt.gettext('Authentication credentials were not provided');
824
+ break;
825
+ case 'EOAUTH2':
826
+ verifyResult.smtp.error = request.app.gt.gettext('OAuth2 authentication failed');
827
+ break;
828
+ case 'ETLS':
829
+ verifyResult.smtp.error = request.app.gt.gettext('TLS protocol error');
830
+ break;
817
831
  case 'ESOCKET':
818
832
  if (/openssl/.test(verifyResult.smtp.error)) {
819
833
  verifyResult.smtp.error = request.app.gt.gettext('TLS protocol error');
820
834
  }
821
835
  break;
836
+ case 'ETIMEDOUT':
837
+ verifyResult.smtp.error = request.app.gt.gettext('Connection timed out');
838
+ break;
839
+ case 'ECONNECTION':
840
+ verifyResult.smtp.error = request.app.gt.gettext('Could not connect to server');
841
+ break;
842
+ case 'EPROTOCOL':
843
+ verifyResult.smtp.error = request.app.gt.gettext('Unexpected server response');
844
+ break;
822
845
  }
823
846
  }
824
847
  }
@@ -15,11 +15,12 @@ const consts = require('../consts');
15
15
  const packageData = require('../../package.json');
16
16
  const timezonesList = require('timezones-list').default;
17
17
 
18
- const { failAction, getByteSize, formatByteSize, getDuration, readEnvValue, hasEnvValue, retryAgent } = require('../tools');
18
+ const { failAction, getByteSize, formatByteSize, getDuration, readEnvValue, hasEnvValue, httpAgent } = require('../tools');
19
19
 
20
20
  const { settingsSchema } = require('../schemas');
21
21
 
22
- const { DEFAULT_MAX_LOG_LINES, DEFAULT_DELIVERY_ATTEMPTS, REDIS_PREFIX, NONCE_BYTES } = consts;
22
+ const { DEFAULT_MAX_LOG_LINES, DEFAULT_DELIVERY_ATTEMPTS, REDIS_PREFIX, NONCE_BYTES, DEFAULT_GMAIL_EXPORT_BATCH_SIZE, DEFAULT_OUTLOOK_EXPORT_BATCH_SIZE } =
23
+ consts;
23
24
 
24
25
  const { fetch: fetchCmd } = require('undici');
25
26
 
@@ -359,7 +360,9 @@ function init(args) {
359
360
  enableOAuthTokensApi: (await settings.get('enableOAuthTokensApi')) || false,
360
361
  ignoreMailCertErrors: (await settings.get('ignoreMailCertErrors')) || false,
361
362
  locale: (await settings.get('locale')) || false,
362
- timezone: (await settings.get('timezone')) || false
363
+ timezone: (await settings.get('timezone')) || false,
364
+ gmailExportBatchSize: (await settings.get('gmailExportBatchSize')) || DEFAULT_GMAIL_EXPORT_BATCH_SIZE,
365
+ outlookExportBatchSize: (await settings.get('outlookExportBatchSize')) || DEFAULT_OUTLOOK_EXPORT_BATCH_SIZE
363
366
  };
364
367
 
365
368
  if (typeof values.trackClicks !== 'boolean') {
@@ -423,7 +426,9 @@ function init(args) {
423
426
  locale: request.payload.locale,
424
427
  timezone: request.payload.timezone,
425
428
  deliveryAttempts: request.payload.deliveryAttempts,
426
- imapIndexer: request.payload.imapIndexer
429
+ imapIndexer: request.payload.imapIndexer,
430
+ gmailExportBatchSize: request.payload.gmailExportBatchSize,
431
+ outlookExportBatchSize: request.payload.outlookExportBatchSize
427
432
  };
428
433
 
429
434
  if (request.payload.serviceUrl) {
@@ -530,7 +535,9 @@ function init(args) {
530
535
  .empty('')
531
536
  .valid(...locales.map(locale => locale.locale))
532
537
  .default('en'),
533
- timezone: settingsSchema.timezone.empty('')
538
+ timezone: settingsSchema.timezone.empty(''),
539
+ gmailExportBatchSize: settingsSchema.gmailExportBatchSize.default(DEFAULT_GMAIL_EXPORT_BATCH_SIZE),
540
+ outlookExportBatchSize: settingsSchema.outlookExportBatchSize.default(DEFAULT_OUTLOOK_EXPORT_BATCH_SIZE)
534
541
  })
535
542
  }
536
543
  }
@@ -821,7 +828,7 @@ function init(args) {
821
828
  data: { nonce: crypto.randomBytes(NONCE_BYTES).toString('base64url') }
822
829
  }),
823
830
  headers,
824
- dispatcher: retryAgent
831
+ dispatcher: httpAgent.retry
825
832
  });
826
833
  duration = Date.now() - start;
827
834
  } catch (err) {
@@ -2022,11 +2022,29 @@ return payload;`)
2022
2022
  case 'EAUTH':
2023
2023
  verifyResult.smtp.error = request.app.gt.gettext('Invalid username or password');
2024
2024
  break;
2025
+ case 'ENOAUTH':
2026
+ verifyResult.smtp.error = request.app.gt.gettext('Authentication credentials were not provided');
2027
+ break;
2028
+ case 'EOAUTH2':
2029
+ verifyResult.smtp.error = request.app.gt.gettext('OAuth2 authentication failed');
2030
+ break;
2031
+ case 'ETLS':
2032
+ verifyResult.smtp.error = request.app.gt.gettext('TLS protocol error');
2033
+ break;
2025
2034
  case 'ESOCKET':
2026
2035
  if (/openssl/.test(verifyResult.smtp.error)) {
2027
2036
  verifyResult.smtp.error = request.app.gt.gettext('TLS protocol error');
2028
2037
  }
2029
2038
  break;
2039
+ case 'ETIMEDOUT':
2040
+ verifyResult.smtp.error = request.app.gt.gettext('Connection timed out');
2041
+ break;
2042
+ case 'ECONNECTION':
2043
+ verifyResult.smtp.error = request.app.gt.gettext('Could not connect to server');
2044
+ break;
2045
+ case 'EPROTOCOL':
2046
+ verifyResult.smtp.error = request.app.gt.gettext('Unexpected server response');
2047
+ break;
2030
2048
  }
2031
2049
  }
2032
2050
  }