emailengine-app 2.68.0 → 2.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/codeql/codeql-config.yml +16 -0
- package/.github/workflows/codeql.yml +102 -0
- package/.github/workflows/deploy.yml +8 -0
- package/.github/workflows/release.yaml +4 -0
- package/.github/workflows/test.yml +3 -0
- package/CHANGELOG.md +49 -0
- package/SECURITY.md +80 -0
- package/SECURITY.txt +27 -0
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +13 -1
- package/lib/account.js +62 -25
- package/lib/api-routes/account-routes.js +493 -75
- package/lib/api-routes/blocklist-routes.js +337 -0
- package/lib/api-routes/delivery-test-routes.js +321 -0
- package/lib/api-routes/export-routes.js +1 -12
- package/lib/api-routes/gateway-routes.js +376 -0
- package/lib/api-routes/license-routes.js +142 -0
- package/lib/api-routes/mailbox-routes.js +318 -0
- package/lib/api-routes/message-routes.js +21 -129
- package/lib/api-routes/oauth2-app-routes.js +631 -0
- package/lib/api-routes/outbox-routes.js +173 -0
- package/lib/api-routes/pubsub-routes.js +98 -0
- package/lib/api-routes/route-helpers.js +45 -0
- package/lib/api-routes/settings-routes.js +331 -0
- package/lib/api-routes/stats-routes.js +77 -0
- package/lib/api-routes/submit-routes.js +472 -0
- package/lib/api-routes/template-routes.js +7 -55
- package/lib/api-routes/token-routes.js +297 -0
- package/lib/api-routes/webhook-route-routes.js +152 -0
- package/lib/email-client/gmail-client.js +14 -0
- package/lib/email-client/imap/mailbox.js +34 -11
- package/lib/email-client/imap/subconnection.js +20 -12
- package/lib/email-client/imap/sync-operations.js +130 -2
- package/lib/email-client/imap-client.js +116 -58
- package/lib/email-client/outlook-client.js +85 -13
- package/lib/export.js +60 -19
- package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
- package/lib/imapproxy/imap-core/lib/imap-command.js +7 -2
- package/lib/imapproxy/imap-core/lib/imap-connection.js +113 -23
- package/lib/imapproxy/imap-core/lib/imap-server.js +25 -1
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- package/lib/imapproxy/imap-server.js +92 -29
- package/lib/message-port-stream.js +113 -16
- package/lib/reject-worker-calls.js +42 -0
- package/lib/routes-ui.js +37 -8778
- package/lib/schemas.js +26 -1
- package/lib/tools.js +73 -0
- package/lib/ui-routes/account-routes.js +40 -210
- package/lib/ui-routes/admin-config-routes.js +913 -487
- package/lib/ui-routes/admin-entities-routes.js +1 -0
- package/lib/ui-routes/auth-routes.js +1339 -0
- package/lib/ui-routes/dashboard-routes.js +188 -0
- package/lib/ui-routes/document-store-routes.js +800 -0
- package/lib/ui-routes/export-routes.js +217 -0
- package/lib/ui-routes/internals-routes.js +354 -0
- package/lib/ui-routes/network-config-routes.js +759 -0
- package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +371 -91
- package/lib/ui-routes/route-helpers.js +316 -0
- package/lib/ui-routes/smtp-test-routes.js +236 -0
- package/lib/ui-routes/unsubscribe-routes.js +234 -0
- package/lib/webhook-request.js +36 -0
- package/package.json +17 -17
- package/sbom.json +1 -1
- package/server.js +217 -19
- package/static/licenses.html +52 -182
- package/translations/messages.pot +131 -151
- package/views/dashboard.hbs +7 -26
- package/views/internals/index.hbs +15 -0
- package/views/tokens/index.hbs +9 -0
- package/workers/api.js +198 -4401
- package/workers/export.js +87 -54
- package/workers/imap.js +29 -13
- package/workers/submit.js +20 -11
- package/workers/webhooks.js +6 -20
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { Writable, Readable } = require('stream');
|
|
3
|
+
const { Writable, Readable, pipeline } = require('stream');
|
|
4
4
|
|
|
5
5
|
// const { MessageChannel } = require('worker_threads');
|
|
6
6
|
// const { port1, port2 } = new MessageChannel();
|
|
@@ -9,6 +9,41 @@ class MessagePortWritable extends Writable {
|
|
|
9
9
|
constructor(messagePort) {
|
|
10
10
|
super();
|
|
11
11
|
this.messagePort = messagePort;
|
|
12
|
+
this.portClosed = false;
|
|
13
|
+
|
|
14
|
+
// The reader side posts { cancel: true } when it is torn down (e.g. the
|
|
15
|
+
// HTTP client aborted the download). Stop the transfer so the upstream
|
|
16
|
+
// source - and the IMAP lock it holds - is released.
|
|
17
|
+
this.onPortMessage = message => {
|
|
18
|
+
if (message && message.cancel) {
|
|
19
|
+
this.destroy();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
this.messagePort.on('message', this.onPortMessage);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
postToPort(message) {
|
|
26
|
+
if (this.portClosed) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
this.messagePort.postMessage(message);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
// ignore - the channel may already be torn down
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
closePort() {
|
|
37
|
+
if (this.portClosed) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.portClosed = true;
|
|
41
|
+
this.messagePort.removeListener('message', this.onPortMessage);
|
|
42
|
+
try {
|
|
43
|
+
this.messagePort.close();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// ignore
|
|
46
|
+
}
|
|
12
47
|
}
|
|
13
48
|
|
|
14
49
|
_write(chunk, encoding, done) {
|
|
@@ -20,24 +55,25 @@ class MessagePortWritable extends Writable {
|
|
|
20
55
|
chunk = Buffer.from(chunk, encoding);
|
|
21
56
|
}
|
|
22
57
|
|
|
23
|
-
this.
|
|
24
|
-
value: chunk,
|
|
25
|
-
done: false
|
|
26
|
-
});
|
|
58
|
+
this.postToPort({ value: chunk, done: false });
|
|
27
59
|
done();
|
|
28
60
|
}
|
|
29
61
|
|
|
30
62
|
_final(done) {
|
|
31
|
-
this.
|
|
32
|
-
|
|
33
|
-
});
|
|
34
|
-
try {
|
|
35
|
-
this.messagePort.close();
|
|
36
|
-
} catch (err) {
|
|
37
|
-
//ignore
|
|
38
|
-
}
|
|
63
|
+
this.postToPort({ done: true });
|
|
64
|
+
this.closePort();
|
|
39
65
|
done();
|
|
40
66
|
}
|
|
67
|
+
|
|
68
|
+
_destroy(err, done) {
|
|
69
|
+
// Abnormal termination: tell the reader so a truncated transfer is not
|
|
70
|
+
// mistaken for a complete message, then release the port.
|
|
71
|
+
if (err) {
|
|
72
|
+
this.postToPort({ error: err.message || 'Stream error' });
|
|
73
|
+
}
|
|
74
|
+
this.closePort();
|
|
75
|
+
done(err);
|
|
76
|
+
}
|
|
41
77
|
}
|
|
42
78
|
|
|
43
79
|
class MessagePortReadable extends Readable {
|
|
@@ -46,17 +82,28 @@ class MessagePortReadable extends Readable {
|
|
|
46
82
|
this.messagePort = messagePort;
|
|
47
83
|
|
|
48
84
|
this.canRead = false;
|
|
85
|
+
this.portClosed = false;
|
|
49
86
|
|
|
50
87
|
this.readableQueue = [];
|
|
51
|
-
this.
|
|
52
|
-
if (message
|
|
88
|
+
this.onPortMessage = message => {
|
|
89
|
+
if (!message) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (message.error) {
|
|
93
|
+
// The producer failed mid-transfer - surface it as a stream error
|
|
94
|
+
// rather than letting the consumer believe it received everything.
|
|
95
|
+
this.destroy(new Error(message.error));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (message.done || message.value) {
|
|
53
99
|
this.readableQueue.push(message);
|
|
54
100
|
|
|
55
101
|
if (this.canRead && this.readableQueue.length === 1) {
|
|
56
102
|
this._processReading();
|
|
57
103
|
}
|
|
58
104
|
}
|
|
59
|
-
}
|
|
105
|
+
};
|
|
106
|
+
this.messagePort.on('message', this.onPortMessage);
|
|
60
107
|
}
|
|
61
108
|
|
|
62
109
|
_processReading() {
|
|
@@ -73,7 +120,57 @@ class MessagePortReadable extends Readable {
|
|
|
73
120
|
this.canRead = true;
|
|
74
121
|
this._processReading();
|
|
75
122
|
}
|
|
123
|
+
|
|
124
|
+
_destroy(err, done) {
|
|
125
|
+
// Consumer is gone (client aborted, error, or clean end): tell the producer
|
|
126
|
+
// to stop and release the port + listener so nothing leaks across threads.
|
|
127
|
+
if (!this.portClosed) {
|
|
128
|
+
this.portClosed = true;
|
|
129
|
+
try {
|
|
130
|
+
this.messagePort.postMessage({ cancel: true });
|
|
131
|
+
} catch (cancelErr) {
|
|
132
|
+
// ignore - the peer may already be closed
|
|
133
|
+
}
|
|
134
|
+
this.messagePort.removeListener('message', this.onPortMessage);
|
|
135
|
+
try {
|
|
136
|
+
this.messagePort.close();
|
|
137
|
+
} catch (closeErr) {
|
|
138
|
+
// ignore
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
done(err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Pipes a readable source into a MessagePortWritable so that an error on either
|
|
147
|
+
* side tears the transfer down instead of surfacing as an unhandled 'error'
|
|
148
|
+
* event. A mid-download failure on an IMAP source stream would otherwise crash
|
|
149
|
+
* the worker (and every account assigned to it).
|
|
150
|
+
*
|
|
151
|
+
* @param {Readable} source - Source stream (e.g. an IMAP download stream)
|
|
152
|
+
* @param {MessagePortWritable} writable - Destination bound to a MessagePort
|
|
153
|
+
* @param {Object} [logger] - Optional logger used to report transfer failures
|
|
154
|
+
*/
|
|
155
|
+
function pipeToMessagePort(source, writable, logger) {
|
|
156
|
+
pipeline(source, writable, err => {
|
|
157
|
+
if (!err || !logger) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// A consumer that aborts mid-download closes the destination early; that
|
|
161
|
+
// is expected (not a failure) so it should not be logged at error level.
|
|
162
|
+
if (err.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
|
163
|
+
if (typeof logger.debug === 'function') {
|
|
164
|
+
logger.debug({ msg: 'Message stream transfer aborted by consumer', err });
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (typeof logger.error === 'function') {
|
|
169
|
+
logger.error({ msg: 'Message stream transfer failed', err });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
76
172
|
}
|
|
77
173
|
|
|
78
174
|
module.exports.MessagePortWritable = MessagePortWritable;
|
|
79
175
|
module.exports.MessagePortReadable = MessagePortReadable;
|
|
176
|
+
module.exports.pipeToMessagePort = pipeToMessagePort;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reject every pending cross-thread call routed to a worker thread that has
|
|
5
|
+
* terminated. Without this, a call placed against a worker that then crashes or
|
|
6
|
+
* is force-restarted keeps waiting until its own timeout fires - which can be
|
|
7
|
+
* minutes (the submitMessage floor) or hours (a large X-EE-Timeout). Rejecting
|
|
8
|
+
* here lets the caller fail fast with a retryable error the instant the worker
|
|
9
|
+
* is known to be gone.
|
|
10
|
+
*
|
|
11
|
+
* The stored `reject` wrapper clears the entry's timeout, so we only need to
|
|
12
|
+
* drop the entry from the queue and invoke it.
|
|
13
|
+
*
|
|
14
|
+
* @param {Map} callQueue - mid -> { resolve, reject, timer, worker }
|
|
15
|
+
* @param {Worker} worker - The terminated worker thread
|
|
16
|
+
* @param {Error} err - Rejection reason. May be a shared instance reused across
|
|
17
|
+
* concurrent rejections, so callers must not attach per-call fields to it.
|
|
18
|
+
* @returns {number} Number of pending calls that were rejected
|
|
19
|
+
*/
|
|
20
|
+
function rejectWorkerCalls(callQueue, worker, err) {
|
|
21
|
+
let rejected = 0;
|
|
22
|
+
|
|
23
|
+
// Deleting the current entry while iterating a Map is safe in JS.
|
|
24
|
+
for (let [mid, entry] of callQueue) {
|
|
25
|
+
if (entry.worker !== worker) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
callQueue.delete(mid);
|
|
30
|
+
try {
|
|
31
|
+
entry.reject(err);
|
|
32
|
+
} catch (rejectErr) {
|
|
33
|
+
// A consumer that throws synchronously from its rejection path must
|
|
34
|
+
// not stop us from cleaning up the remaining pending calls.
|
|
35
|
+
}
|
|
36
|
+
rejected++;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return rejected;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { rejectWorkerCalls };
|