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
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+
3
+ // NB! This file is processed by the gettext parser (npm run gettext) and can not use newer syntax like ?.
4
+
5
+ // Public (unauthenticated) subscription-management routes. These render the unsubscribe
6
+ // landing page reached from the List-Unsubscribe link in outgoing messages and process
7
+ // the subscribe/unsubscribe form submission. Extracted verbatim from lib/routes-ui.js.
8
+ // Both routes set `auth: false` and define their own validation failAction handlers.
9
+
10
+ const Joi = require('joi');
11
+ const Boom = require('@hapi/boom');
12
+ const { Account } = require('../account');
13
+ const { redis } = require('../db');
14
+ const { accountIdSchema } = require('../schemas');
15
+ const { REDIS_PREFIX } = require('../consts');
16
+
17
+ function init(args) {
18
+ const { server, call } = args;
19
+
20
+ server.route({
21
+ method: 'GET',
22
+ path: '/unsubscribe',
23
+ async handler(request, h) {
24
+ let data = Buffer.from(request.query.data, 'base64url').toString();
25
+ // do not check signature, validate fields in the submit step
26
+
27
+ data = JSON.parse(data);
28
+
29
+ if (!data || typeof data !== 'object' || data.act !== 'unsub') {
30
+ throw new Error(request.app.gt.gettext('Invalid input'));
31
+ }
32
+
33
+ // throws if account does not exist
34
+ let accountObject = new Account({ redis, account: data.acc });
35
+ await accountObject.loadAccountData();
36
+
37
+ return h.view(
38
+ 'unsubscribe',
39
+ {
40
+ pageTitleFull: request.app.gt.gettext('Subscription Management'),
41
+
42
+ unsubscribed: await redis.hexists(`${REDIS_PREFIX}lists:unsub:entries:${data.list}`, data.rcpt),
43
+ values: {
44
+ listId: data.list,
45
+ account: data.acc,
46
+ messageId: data.msg,
47
+ email: data.rcpt
48
+ }
49
+ },
50
+ {
51
+ layout: 'public'
52
+ }
53
+ );
54
+ },
55
+ options: {
56
+ auth: false,
57
+
58
+ validate: {
59
+ options: {
60
+ stripUnknown: true,
61
+ abortEarly: false,
62
+ convert: true
63
+ },
64
+
65
+ async failAction(request, h, err) {
66
+ request.logger.error({ msg: 'Failed to validate request arguments', err });
67
+ let error = Boom.boomify(new Error(request.app.gt.gettext('Invalid request. Check your input and try again.')), { statusCode: 400 });
68
+ if (err.code) {
69
+ error.output.payload.code = err.code;
70
+ }
71
+ throw error;
72
+ },
73
+
74
+ query: Joi.object({
75
+ data: Joi.string().base64({ paddingRequired: false, urlSafe: true }).required(),
76
+ sig: Joi.string().base64({ paddingRequired: false, urlSafe: true })
77
+ })
78
+ }
79
+ }
80
+ });
81
+
82
+ server.route({
83
+ method: 'POST',
84
+ path: '/unsubscribe/address',
85
+ async handler(request, h) {
86
+ try {
87
+ // throws if account does not exist
88
+ let accountObject = new Account({ redis, account: request.payload.account });
89
+ await accountObject.loadAccountData();
90
+
91
+ let reSubscribed = false;
92
+
93
+ switch (request.payload.action) {
94
+ case 'unsubscribe': {
95
+ let isNew = await redis.eeListAdd(
96
+ `${REDIS_PREFIX}lists:unsub:lists`,
97
+ `${REDIS_PREFIX}lists:unsub:entries:${request.payload.listId}`,
98
+ request.payload.listId,
99
+ request.payload.email.toLowerCase().trim(),
100
+ JSON.stringify({
101
+ recipient: request.payload.email,
102
+ account: request.payload.account,
103
+ source: 'form',
104
+ reason: 'unsubscribe',
105
+ messageId: request.payload.messageId,
106
+ remoteAddress: request.info.remoteAddress,
107
+ userAgent: request.headers['user-agent'],
108
+ created: new Date().toISOString()
109
+ })
110
+ );
111
+
112
+ if (isNew) {
113
+ await call({
114
+ cmd: 'unsubscribe',
115
+ account: request.payload.account,
116
+ payload: {
117
+ recipient: request.payload.email,
118
+ messageId: request.payload.messageId,
119
+ listId: request.payload.listId,
120
+ remoteAddress: request.info.remoteAddress,
121
+ userAgent: request.headers['user-agent']
122
+ }
123
+ });
124
+ }
125
+ break;
126
+ }
127
+
128
+ case 'subscribe': {
129
+ let removed = await redis.eeListRemove(
130
+ `${REDIS_PREFIX}lists:unsub:lists`,
131
+ `${REDIS_PREFIX}lists:unsub:entries:${request.payload.listId}`,
132
+ request.payload.listId,
133
+ request.payload.email.toLowerCase().trim()
134
+ );
135
+
136
+ if (removed) {
137
+ await call({
138
+ cmd: 'subscribe',
139
+ account: request.payload.account,
140
+ payload: {
141
+ recipient: request.payload.email,
142
+ messageId: request.payload.messageId,
143
+ listId: request.payload.listId,
144
+ remoteAddress: request.info.remoteAddress,
145
+ userAgent: request.headers['user-agent']
146
+ }
147
+ });
148
+ }
149
+
150
+ reSubscribed = true;
151
+ break;
152
+ }
153
+ }
154
+
155
+ return h.view(
156
+ 'unsubscribe',
157
+ {
158
+ pageTitleFull: request.app.gt.gettext('Subscription Management'),
159
+
160
+ unsubscribed: await redis.hexists(`${REDIS_PREFIX}lists:unsub:entries:${request.payload.listId}`, request.payload.email),
161
+ values: request.payload,
162
+ reSubscribed
163
+ },
164
+ {
165
+ layout: 'public'
166
+ }
167
+ );
168
+ } catch (err) {
169
+ await request.flash({ type: 'danger', message: request.app.gt.gettext("Couldn't process request. Try again.") });
170
+ request.logger.error({ msg: 'Failed to process subscription request', err });
171
+
172
+ return h.view(
173
+ 'unsubscribe',
174
+ {
175
+ pageTitleFull: request.app.gt.gettext('Subscription Management'),
176
+ unsubscribed: await redis.hexists(`${REDIS_PREFIX}lists:unsub:entries:${request.payload.listId}`, request.payload.email)
177
+ },
178
+ {
179
+ layout: 'public'
180
+ }
181
+ );
182
+ }
183
+ },
184
+ options: {
185
+ auth: false,
186
+ validate: {
187
+ options: {
188
+ stripUnknown: true,
189
+ abortEarly: false,
190
+ convert: true
191
+ },
192
+
193
+ async failAction(request, h, err) {
194
+ let errors = {};
195
+
196
+ if (err.details) {
197
+ err.details.forEach(detail => {
198
+ if (!errors[detail.path]) {
199
+ errors[detail.path] = detail.message;
200
+ }
201
+ });
202
+ }
203
+
204
+ await request.flash({ type: 'danger', message: request.app.gt.gettext("Couldn't process request. Try again.") });
205
+ request.logger.error({ msg: 'Failed to process subscription request', err });
206
+
207
+ return h
208
+ .view(
209
+ 'unsubscribe',
210
+ {
211
+ pageTitleFull: request.app.gt.gettext('Subscription Management'),
212
+ unsubscribed: await redis.hexists(`${REDIS_PREFIX}lists:unsub:entries:${request.payload.listId}`, request.payload.email),
213
+ errors
214
+ },
215
+ {
216
+ layout: 'public'
217
+ }
218
+ )
219
+ .takeover();
220
+ },
221
+
222
+ payload: Joi.object({
223
+ action: Joi.string().valid('subscribe', 'unsubscribe').required(),
224
+ account: accountIdSchema.required(),
225
+ listId: Joi.string().hostname().empty('').example('test-list').label('List ID').required(),
226
+ email: Joi.string().email().empty('').required().description('Email address').required(),
227
+ messageId: Joi.string().empty('').max(996).example('<test123@example.com>').description('Message ID')
228
+ })
229
+ }
230
+ }
231
+ });
232
+ }
233
+
234
+ module.exports = init;
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Performs a single webhook HTTP delivery and always drains the response body.
5
+ *
6
+ * The webhook response body is never used, but with undici's keepAlive dispatcher
7
+ * a connection is only returned to the pool once its body has been consumed.
8
+ * Draining on every path (success and failure) prevents successful deliveries -
9
+ * the common, high-volume case - from pinning pooled sockets.
10
+ *
11
+ * @param {Function} fetchImpl - fetch implementation (undici fetch)
12
+ * @param {string} url - Destination URL
13
+ * @param {Object} options - fetch options (method, body, headers, dispatcher)
14
+ * @returns {Promise<number>} Resolves with the HTTP status code on success
15
+ * @throws {Error} On a non-2xx response; the error carries a `statusCode` property
16
+ */
17
+ async function sendWebhookRequest(fetchImpl, url, options) {
18
+ const res = await fetchImpl(url, options);
19
+
20
+ // Drain the body regardless of status so the socket can be reused.
21
+ try {
22
+ await res.text();
23
+ } catch (err) {
24
+ // ignore drain errors
25
+ }
26
+
27
+ if (!res.ok) {
28
+ let err = new Error(res.statusText || `Invalid response: ${res.status} ${res.statusText}`);
29
+ err.statusCode = res.status;
30
+ throw err;
31
+ }
32
+
33
+ return res.status;
34
+ }
35
+
36
+ module.exports = { sendWebhookRequest };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emailengine-app",
3
- "version": "2.68.0",
3
+ "version": "2.69.0",
4
4
  "private": false,
5
5
  "productTitle": "EmailEngine",
6
6
  "description": "Email Sync Engine",
@@ -17,7 +17,7 @@
17
17
  "build-dist": "pkg --compress Brotli package.json && npm install && node winconf.js",
18
18
  "build-dist-fast": "pkg --debug package.json && npm install && node winconf.js",
19
19
  "licenses": "license-checker --excludePackages 'emailengine-app' --json | node list-generate.js > static/licenses.html",
20
- "gettext": "find ./views -name \"*.hbs\" -print0 | xargs -0 xgettext-template -L Handlebars -o translations/messages.pot --force-po && jsxgettext --parser-options '{\"ecmaVersion\": 2018}' lib/routes-ui.js workers/api.js lib/tools.js lib/autodetect-imap-settings.js lib/ui-routes/account-routes.js lib/ui-routes/admin-config-routes.js lib/ui-routes/admin-entities-routes.js -j -o translations/messages.pot",
20
+ "gettext": "find ./views -name \"*.hbs\" -print0 | xargs -0 xgettext-template -L Handlebars -o translations/messages.pot --force-po && jsxgettext --parser-options '{\"ecmaVersion\": 2018}' workers/api.js lib/tools.js lib/autodetect-imap-settings.js lib/ui-routes/admin-entities-routes.js lib/ui-routes/account-routes.js lib/ui-routes/oauth-config-routes.js lib/ui-routes/admin-config-routes.js lib/ui-routes/route-helpers.js lib/ui-routes/unsubscribe-routes.js -j -o translations/messages.pot",
21
21
  "prepare-docker": "echo \"EE_DOCKER_LEGACY=$EE_DOCKER_LEGACY\" >> system.env && cat system.env",
22
22
  "update": "rm -rf node_modules package-lock.json && ncu -u && npm install && ./copy-static-files.sh && npm run licenses && npm run gettext",
23
23
  "test-gmail-api": "node lib/email-client/gmail-client.js --dbs.redis=redis://127.0.0.1/11",
@@ -44,8 +44,8 @@
44
44
  "homepage": "https://emailengine.app/",
45
45
  "dependencies": {
46
46
  "@bugsnag/js": "8.9.0",
47
- "@bull-board/api": "7.1.5",
48
- "@bull-board/hapi": "7.1.5",
47
+ "@bull-board/api": "7.2.1",
48
+ "@bull-board/hapi": "7.2.1",
49
49
  "@elastic/elasticsearch": "8.15.3",
50
50
  "@hapi/accept": "6.0.3",
51
51
  "@hapi/bell": "13.1.0",
@@ -59,18 +59,18 @@
59
59
  "@postalsys/bounce-classifier": "3.0.0",
60
60
  "@postalsys/certs": "1.0.14",
61
61
  "@postalsys/ee-client": "1.3.0",
62
- "@postalsys/email-ai-tools": "1.13.4",
63
- "@postalsys/email-text-tools": "2.4.5",
62
+ "@postalsys/email-ai-tools": "1.13.5",
63
+ "@postalsys/email-text-tools": "2.4.6",
64
64
  "@postalsys/gettext": "4.1.1",
65
65
  "@postalsys/joi-messages": "1.0.5",
66
66
  "@postalsys/templates": "2.0.1",
67
67
  "@simplewebauthn/browser": "13.3.0",
68
- "@simplewebauthn/server": "13.3.0",
69
- "@zone-eu/mailsplit": "5.4.11",
68
+ "@simplewebauthn/server": "13.3.1",
69
+ "@zone-eu/mailsplit": "5.4.12",
70
70
  "@zone-eu/wild-config": "1.7.5",
71
71
  "ace-builds": "1.44.0",
72
72
  "base32.js": "0.1.0",
73
- "bullmq": "5.77.3",
73
+ "bullmq": "5.78.0",
74
74
  "compare-versions": "6.1.1",
75
75
  "dotenv": "17.4.2",
76
76
  "encoding-japanese": "2.2.0",
@@ -84,9 +84,9 @@
84
84
  "html-to-text": "10.0.0",
85
85
  "ical.js": "1.5.0",
86
86
  "iconv-lite": "0.7.2",
87
- "imapflow": "1.3.3",
87
+ "imapflow": "1.4.0",
88
88
  "ioredfour": "1.4.1",
89
- "ioredis": "5.10.1",
89
+ "ioredis": "5.11.1",
90
90
  "ipaddr.js": "2.4.0",
91
91
  "joi": "17.13.3",
92
92
  "jquery": "4.0.0",
@@ -94,21 +94,21 @@
94
94
  "libmime": "5.3.8",
95
95
  "libqp": "2.1.1",
96
96
  "license-checker": "25.0.1",
97
- "mailparser": "3.9.8",
97
+ "mailparser": "3.9.9",
98
98
  "marked": "9.1.6",
99
99
  "minimist": "1.2.8",
100
100
  "msgpack5": "6.0.2",
101
101
  "murmurhash": "2.0.1",
102
102
  "nanoid": "3.3.8",
103
- "nodemailer": "8.0.8",
103
+ "nodemailer": "8.0.10",
104
104
  "pino": "10.3.1",
105
105
  "popper.js": "1.16.1",
106
106
  "prom-client": "15.1.3",
107
107
  "psl": "1.15.0",
108
- "pubface": "1.1.1",
108
+ "pubface": "1.1.2",
109
109
  "punycode.js": "2.3.1",
110
110
  "qrcode": "1.5.4",
111
- "smtp-server": "3.18.4",
111
+ "smtp-server": "3.18.5",
112
112
  "socks": "2.8.9",
113
113
  "speakeasy": "2.0.0",
114
114
  "startbootstrap-sb-admin-2": "3.3.7",
@@ -120,14 +120,14 @@
120
120
  "@eslint/js": "10.0.1",
121
121
  "chai": "4.3.10",
122
122
  "eerawlog": "1.5.3",
123
- "eslint": "10.4.0",
123
+ "eslint": "10.4.1",
124
124
  "grunt": "1.6.2",
125
125
  "grunt-cli": "1.5.0",
126
126
  "grunt-shell-spawn": "0.5.0",
127
127
  "grunt-wait": "0.3.0",
128
128
  "jsxgettext": "0.11.0",
129
129
  "pino-pretty": "13.0.0",
130
- "prettier": "3.8.3",
130
+ "prettier": "3.8.4",
131
131
  "resedit": "3.0.2",
132
132
  "spdx-satisfies": "6.0.0",
133
133
  "supertest": "7.2.2",