emailengine-app 2.68.1 → 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/workflows/deploy.yml +2 -0
- package/.github/workflows/release.yaml +4 -0
- package/CHANGELOG.md +40 -0
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +7 -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 +6 -1
- package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -23
- package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- 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 +68 -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 +8 -8
- package/sbom.json +1 -1
- package/server.js +214 -16
- package/static/licenses.html +12 -12
- package/translations/messages.pot +129 -149
- 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
|
@@ -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.
|
|
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}'
|
|
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
|
|
48
|
-
"@bull-board/hapi": "7.1
|
|
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",
|
|
@@ -70,7 +70,7 @@
|
|
|
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.
|
|
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.
|
|
87
|
+
"imapflow": "1.4.0",
|
|
88
88
|
"ioredfour": "1.4.1",
|
|
89
|
-
"ioredis": "5.11.
|
|
89
|
+
"ioredis": "5.11.1",
|
|
90
90
|
"ipaddr.js": "2.4.0",
|
|
91
91
|
"joi": "17.13.3",
|
|
92
92
|
"jquery": "4.0.0",
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
"grunt-wait": "0.3.0",
|
|
128
128
|
"jsxgettext": "0.11.0",
|
|
129
129
|
"pino-pretty": "13.0.0",
|
|
130
|
-
"prettier": "3.8.
|
|
130
|
+
"prettier": "3.8.4",
|
|
131
131
|
"resedit": "3.0.2",
|
|
132
132
|
"spdx-satisfies": "6.0.0",
|
|
133
133
|
"supertest": "7.2.2",
|