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
|
@@ -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 {
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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'
|
|
1386
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 } =
|
|
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:
|
|
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
|
}
|