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.
Files changed (74) hide show
  1. package/.github/codeql/codeql-config.yml +16 -0
  2. package/.github/workflows/codeql.yml +102 -0
  3. package/.github/workflows/deploy.yml +8 -0
  4. package/.github/workflows/release.yaml +4 -0
  5. package/.github/workflows/test.yml +3 -0
  6. package/CHANGELOG.md +49 -0
  7. package/SECURITY.md +80 -0
  8. package/SECURITY.txt +27 -0
  9. package/config/default.toml +2 -0
  10. package/data/google-crawlers.json +13 -1
  11. package/lib/account.js +62 -25
  12. package/lib/api-routes/account-routes.js +493 -75
  13. package/lib/api-routes/blocklist-routes.js +337 -0
  14. package/lib/api-routes/delivery-test-routes.js +321 -0
  15. package/lib/api-routes/export-routes.js +1 -12
  16. package/lib/api-routes/gateway-routes.js +376 -0
  17. package/lib/api-routes/license-routes.js +142 -0
  18. package/lib/api-routes/mailbox-routes.js +318 -0
  19. package/lib/api-routes/message-routes.js +21 -129
  20. package/lib/api-routes/oauth2-app-routes.js +631 -0
  21. package/lib/api-routes/outbox-routes.js +173 -0
  22. package/lib/api-routes/pubsub-routes.js +98 -0
  23. package/lib/api-routes/route-helpers.js +45 -0
  24. package/lib/api-routes/settings-routes.js +331 -0
  25. package/lib/api-routes/stats-routes.js +77 -0
  26. package/lib/api-routes/submit-routes.js +472 -0
  27. package/lib/api-routes/template-routes.js +7 -55
  28. package/lib/api-routes/token-routes.js +297 -0
  29. package/lib/api-routes/webhook-route-routes.js +152 -0
  30. package/lib/email-client/gmail-client.js +14 -0
  31. package/lib/email-client/imap/mailbox.js +34 -11
  32. package/lib/email-client/imap/subconnection.js +20 -12
  33. package/lib/email-client/imap/sync-operations.js +130 -2
  34. package/lib/email-client/imap-client.js +116 -58
  35. package/lib/email-client/outlook-client.js +85 -13
  36. package/lib/export.js +60 -19
  37. package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
  38. package/lib/imapproxy/imap-core/lib/imap-command.js +7 -2
  39. package/lib/imapproxy/imap-core/lib/imap-connection.js +113 -23
  40. package/lib/imapproxy/imap-core/lib/imap-server.js +25 -1
  41. package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
  42. package/lib/imapproxy/imap-server.js +92 -29
  43. package/lib/message-port-stream.js +113 -16
  44. package/lib/reject-worker-calls.js +42 -0
  45. package/lib/routes-ui.js +37 -8778
  46. package/lib/schemas.js +26 -1
  47. package/lib/tools.js +73 -0
  48. package/lib/ui-routes/account-routes.js +40 -210
  49. package/lib/ui-routes/admin-config-routes.js +913 -487
  50. package/lib/ui-routes/admin-entities-routes.js +1 -0
  51. package/lib/ui-routes/auth-routes.js +1339 -0
  52. package/lib/ui-routes/dashboard-routes.js +188 -0
  53. package/lib/ui-routes/document-store-routes.js +800 -0
  54. package/lib/ui-routes/export-routes.js +217 -0
  55. package/lib/ui-routes/internals-routes.js +354 -0
  56. package/lib/ui-routes/network-config-routes.js +759 -0
  57. package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +371 -91
  58. package/lib/ui-routes/route-helpers.js +316 -0
  59. package/lib/ui-routes/smtp-test-routes.js +236 -0
  60. package/lib/ui-routes/unsubscribe-routes.js +234 -0
  61. package/lib/webhook-request.js +36 -0
  62. package/package.json +17 -17
  63. package/sbom.json +1 -1
  64. package/server.js +217 -19
  65. package/static/licenses.html +52 -182
  66. package/translations/messages.pot +131 -151
  67. package/views/dashboard.hbs +7 -26
  68. package/views/internals/index.hbs +15 -0
  69. package/views/tokens/index.hbs +9 -0
  70. package/workers/api.js +198 -4401
  71. package/workers/export.js +87 -54
  72. package/workers/imap.js +29 -13
  73. package/workers/submit.js +20 -11
  74. 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.messagePort.postMessage({
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.messagePort.postMessage({
32
- done: true
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.messagePort.on('message', message => {
52
- if (message && (message.done || message.value)) {
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 };