emailengine-app 2.68.1 → 2.70.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 (95) hide show
  1. package/.github/workflows/deploy.yml +8 -3
  2. package/.github/workflows/release.yaml +6 -0
  3. package/CHANGELOG.md +59 -0
  4. package/Gruntfile.js +3 -1
  5. package/config/default.toml +2 -0
  6. package/data/google-crawlers.json +7 -1
  7. package/getswagger.sh +40 -4
  8. package/gettext-extract.js +163 -0
  9. package/lib/account.js +135 -72
  10. package/lib/api-routes/account-routes.js +684 -106
  11. package/lib/api-routes/blocklist-routes.js +344 -0
  12. package/lib/api-routes/chat-routes.js +32 -14
  13. package/lib/api-routes/delivery-test-routes.js +346 -0
  14. package/lib/api-routes/export-routes.js +28 -14
  15. package/lib/api-routes/gateway-routes.js +427 -0
  16. package/lib/api-routes/license-routes.js +156 -0
  17. package/lib/api-routes/mailbox-routes.js +344 -0
  18. package/lib/api-routes/message-routes.js +221 -187
  19. package/lib/api-routes/oauth2-app-routes.js +697 -0
  20. package/lib/api-routes/outbox-routes.js +185 -0
  21. package/lib/api-routes/pubsub-routes.js +102 -0
  22. package/lib/api-routes/route-helpers.js +58 -0
  23. package/lib/api-routes/settings-routes.js +357 -0
  24. package/lib/api-routes/stats-routes.js +111 -0
  25. package/lib/api-routes/submit-routes.js +461 -0
  26. package/lib/api-routes/template-routes.js +60 -75
  27. package/lib/api-routes/token-routes.js +297 -0
  28. package/lib/api-routes/webhook-route-routes.js +181 -0
  29. package/lib/autodetect-imap-settings.js +0 -2
  30. package/lib/consts.js +5 -0
  31. package/lib/email-client/base-client.js +28 -6
  32. package/lib/email-client/gmail-client.js +133 -112
  33. package/lib/email-client/imap/mailbox.js +34 -11
  34. package/lib/email-client/imap/subconnection.js +20 -13
  35. package/lib/email-client/imap/sync-operations.js +131 -3
  36. package/lib/email-client/imap-client.js +152 -75
  37. package/lib/email-client/notification-handler.js +1 -4
  38. package/lib/email-client/outlook-client.js +134 -75
  39. package/lib/export.js +97 -20
  40. package/lib/feature-flags.js +2 -2
  41. package/lib/gateway.js +4 -9
  42. package/lib/get-raw-email.js +5 -5
  43. package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
  44. package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
  45. package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -24
  46. package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
  47. package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
  48. package/lib/logger.js +24 -21
  49. package/lib/message-port-stream.js +113 -16
  50. package/lib/metrics-collector.js +0 -2
  51. package/lib/oauth2-apps.js +13 -4
  52. package/lib/outbox.js +24 -40
  53. package/lib/redis-operations.js +1 -1
  54. package/lib/reject-worker-calls.js +42 -0
  55. package/lib/routes-ui.js +37 -8778
  56. package/lib/schemas.js +429 -84
  57. package/lib/sentry.js +139 -0
  58. package/lib/settings.js +9 -3
  59. package/lib/stream-encrypt.js +1 -1
  60. package/lib/templates.js +1 -1
  61. package/lib/tokens.js +5 -3
  62. package/lib/tools.js +70 -4
  63. package/lib/ui-routes/account-routes.js +45 -212
  64. package/lib/ui-routes/admin-config-routes.js +928 -489
  65. package/lib/ui-routes/admin-entities-routes.js +1 -0
  66. package/lib/ui-routes/auth-routes.js +1339 -0
  67. package/lib/ui-routes/dashboard-routes.js +188 -0
  68. package/lib/ui-routes/document-store-routes.js +800 -0
  69. package/lib/ui-routes/export-routes.js +217 -0
  70. package/lib/ui-routes/internals-routes.js +354 -0
  71. package/lib/ui-routes/network-config-routes.js +759 -0
  72. package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +369 -91
  73. package/lib/ui-routes/route-helpers.js +314 -0
  74. package/lib/ui-routes/smtp-test-routes.js +236 -0
  75. package/lib/ui-routes/unsubscribe-routes.js +232 -0
  76. package/lib/webhook-request.js +36 -0
  77. package/lib/webhooks.js +8 -4
  78. package/package.json +13 -12
  79. package/sbom.json +1 -1
  80. package/server.js +222 -39
  81. package/static/licenses.html +160 -300
  82. package/translations/messages.pot +112 -132
  83. package/update-info.sh +19 -1
  84. package/views/config/logging.hbs +48 -0
  85. package/views/dashboard.hbs +7 -26
  86. package/views/internals/index.hbs +15 -0
  87. package/views/tokens/index.hbs +9 -0
  88. package/workers/api.js +200 -4424
  89. package/workers/documents.js +2 -22
  90. package/workers/export.js +103 -104
  91. package/workers/imap-proxy.js +3 -23
  92. package/workers/imap.js +32 -36
  93. package/workers/smtp.js +2 -22
  94. package/workers/submit.js +26 -35
  95. package/workers/webhooks.js +9 -43
@@ -0,0 +1,346 @@
1
+ 'use strict';
2
+
3
+ const Boom = require('@hapi/boom');
4
+ const Joi = require('joi');
5
+ const { fetch: fetchCmd } = require('undici');
6
+ const { redis } = require('../db');
7
+ const { Account } = require('../account');
8
+ const { Gateway } = require('../gateway');
9
+ const getSecret = require('../get-secret');
10
+ const { failAction, httpAgent } = require('../tools');
11
+ const { accountIdSchema, errorResponses } = require('../schemas');
12
+ const { REDIS_PREFIX } = require('../consts');
13
+ const packageData = require('../../package.json');
14
+
15
+ async function init(args) {
16
+ const { server, call, CORS_CONFIG, SMTP_TEST_HOST } = args;
17
+
18
+ server.route({
19
+ method: 'POST',
20
+ path: '/v1/delivery-test/account/{account}',
21
+ async handler(request) {
22
+ let accountObject = new Account({
23
+ redis,
24
+ account: request.params.account,
25
+ call,
26
+ secret: await getSecret(),
27
+ timeout: request.headers['x-ee-timeout']
28
+ });
29
+
30
+ try {
31
+ // throws if account does not exist
32
+ let accountData = await accountObject.loadAccountData();
33
+
34
+ request.logger.info({ msg: 'Requested SMTP delivery test', account: request.params.account });
35
+
36
+ let headers = {
37
+ 'Content-Type': 'application/json',
38
+ 'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
39
+ };
40
+
41
+ let res = await fetchCmd(`${SMTP_TEST_HOST}/test-address`, {
42
+ method: 'post',
43
+ body: JSON.stringify({
44
+ version: packageData.version,
45
+ requestor: '@postalsys/emailengine-app'
46
+ }),
47
+ headers,
48
+ dispatcher: httpAgent.retry
49
+ });
50
+
51
+ if (!res.ok) {
52
+ let err = new Error(`Invalid response: ${res.status} ${res.statusText}`);
53
+ err.statusCode = res.status;
54
+
55
+ try {
56
+ err.details = await res.json();
57
+ } catch (err) {
58
+ // ignore
59
+ }
60
+
61
+ throw err;
62
+ }
63
+
64
+ let testAccount = await res.json();
65
+ if (!testAccount || !testAccount.user) {
66
+ let err = new Error(`Invalid test account`);
67
+ err.statusCode = 500;
68
+
69
+ try {
70
+ err.details = testAccount;
71
+ } catch (err) {
72
+ // ignore
73
+ }
74
+
75
+ throw err;
76
+ }
77
+
78
+ if (request.payload.gateway) {
79
+ // try to load the gateway, throws if not set
80
+ let gatewayObject = new Gateway({ redis, gateway: request.payload.gateway, call, secret: await getSecret() });
81
+ await gatewayObject.loadGatewayData();
82
+ }
83
+
84
+ try {
85
+ let now = new Date().toISOString();
86
+ let queueResponse = await accountObject.queueMessage(
87
+ {
88
+ account: accountData.account,
89
+ subject: `Delivery test ${now}`,
90
+ text: `Hello
91
+
92
+ This is an automated email to test deliverability settings. If you see this email, you can safely delete it.
93
+
94
+ ${now}`,
95
+ html: `<p>Hello</p>
96
+ <p>This is an automated email to test deliverability settings. If you see this email, you can safely delete it.</p>
97
+ <p>${now}</p>`,
98
+ from: {
99
+ name: accountData.name,
100
+ address: accountData.email
101
+ },
102
+ to: [{ name: 'Delivery Test Server', address: testAccount.address }],
103
+ copy: false,
104
+ gateway: request.payload.gateway,
105
+ feedbackKey: `${REDIS_PREFIX}test-send:${testAccount.user}`,
106
+ deliveryAttempts: 1
107
+ },
108
+ { source: 'test' }
109
+ );
110
+
111
+ return {
112
+ success: !!queueResponse.queueId,
113
+ deliveryTest: testAccount.user
114
+ };
115
+ } catch (err) {
116
+ return {
117
+ error: err.message
118
+ };
119
+ }
120
+ } catch (err) {
121
+ request.logger.error({ msg: 'API request failed', err });
122
+ if (Boom.isBoom(err)) {
123
+ throw err;
124
+ }
125
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
126
+ if (err.code) {
127
+ error.output.payload.code = err.code;
128
+ }
129
+ if (err.details) {
130
+ error.output.payload.details = err.details;
131
+ }
132
+ throw error;
133
+ }
134
+ },
135
+ options: {
136
+ description: 'Create delivery test',
137
+ notes: 'Initiate a delivery test',
138
+ tags: ['api', 'Delivery Test'],
139
+
140
+ plugins: {
141
+ 'hapi-swagger': {
142
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
143
+ }
144
+ },
145
+
146
+ auth: {
147
+ strategy: 'api-token',
148
+ mode: 'required'
149
+ },
150
+ cors: CORS_CONFIG,
151
+
152
+ validate: {
153
+ options: {
154
+ stripUnknown: false,
155
+ abortEarly: false,
156
+ convert: true
157
+ },
158
+ failAction,
159
+
160
+ params: Joi.object({
161
+ account: accountIdSchema.required()
162
+ }),
163
+
164
+ payload: Joi.object({
165
+ gateway: Joi.string().allow(false, null).empty('').max(256).example(false).description('Optional gateway ID').label('DeliveryTestGateway')
166
+ }).label('DeliveryStartRequest')
167
+ },
168
+
169
+ response: {
170
+ schema: Joi.object({
171
+ success: Joi.boolean()
172
+ .example(true)
173
+ .description('Was the test started. Not present if queueing the test message failed')
174
+ .label('ResponseDeliveryStartSuccess'),
175
+ deliveryTest: Joi.string()
176
+ .guid({
177
+ version: ['uuidv4', 'uuidv5']
178
+ })
179
+ .example('6420a6ad-7f82-4e4f-8112-82a9dad1f34d')
180
+ .description('Test ID. Not present if queueing the test message failed'),
181
+ error: Joi.string()
182
+ .example('Oops, something went wrong')
183
+ .description('Error message. Only present if queueing the test message failed - in that case success and deliveryTest are not set')
184
+ .label('ResponseDeliveryStartError')
185
+ }).label('DeliveryStartResponse'),
186
+ failAction: 'log'
187
+ }
188
+ }
189
+ });
190
+
191
+ server.route({
192
+ method: 'GET',
193
+ path: '/v1/delivery-test/check/{deliveryTest}',
194
+ async handler(request) {
195
+ try {
196
+ request.logger.info({ msg: 'Requested SMTP delivery test check', deliveryTest: request.params.deliveryTest });
197
+
198
+ let deliveryStatus = (await redis.hgetall(`${REDIS_PREFIX}test-send:${request.params.deliveryTest}`)) || {};
199
+ if (deliveryStatus.success === 'false') {
200
+ let err = new Error(`Failed to deliver email`);
201
+ err.statusCode = 500;
202
+ err.details = deliveryStatus;
203
+ throw err;
204
+ }
205
+
206
+ let headers = {
207
+ 'Content-Type': 'application/json',
208
+ 'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
209
+ };
210
+
211
+ let res = await fetchCmd(`${SMTP_TEST_HOST}/test-address/${request.params.deliveryTest}`, {
212
+ method: 'get',
213
+ headers,
214
+ dispatcher: httpAgent.retry
215
+ });
216
+
217
+ if (!res.ok) {
218
+ let err = new Error(`Invalid response: ${res.status} ${res.statusText}`);
219
+ err.statusCode = res.status;
220
+
221
+ try {
222
+ err.details = await res.json();
223
+ } catch (err) {
224
+ // ignore
225
+ }
226
+
227
+ throw err;
228
+ }
229
+
230
+ let testResponse = await res.json();
231
+
232
+ let success = testResponse && testResponse.status === 'success'; //Default
233
+
234
+ if (testResponse && success) {
235
+ let mainSig =
236
+ testResponse.dkim &&
237
+ testResponse.dkim.results &&
238
+ testResponse.dkim.results.find(entry => entry && entry.status && entry.status.result === 'pass' && entry.status.aligned);
239
+
240
+ if (!mainSig) {
241
+ mainSig =
242
+ testResponse.dkim &&
243
+ testResponse.dkim.results &&
244
+ testResponse.dkim.results.find(entry => entry && entry.status && entry.status.result === 'pass');
245
+ }
246
+
247
+ if (!mainSig) {
248
+ mainSig = testResponse.dkim && testResponse.dkim.results && testResponse.dkim.results[0];
249
+ }
250
+
251
+ testResponse.mainSig = mainSig || {
252
+ status: {
253
+ result: 'none'
254
+ }
255
+ };
256
+
257
+ if (testResponse.spf && testResponse.spf.status && testResponse.spf.status.comment) {
258
+ testResponse.spf.status.comment = testResponse.spf.status.comment.replace(/^[^:\s]+:\s*/, '');
259
+ }
260
+ }
261
+
262
+ if (testResponse) {
263
+ if (testResponse.status === 'success') {
264
+ delete testResponse.status;
265
+ }
266
+ delete testResponse.user;
267
+ }
268
+
269
+ return Object.assign({ success }, testResponse || {});
270
+ } catch (err) {
271
+ request.logger.error({ msg: 'API request failed', err });
272
+ if (Boom.isBoom(err)) {
273
+ throw err;
274
+ }
275
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
276
+ if (err.code) {
277
+ error.output.payload.code = err.code;
278
+ }
279
+ if (err.details) {
280
+ error.output.payload.details = err.details;
281
+ }
282
+ throw error;
283
+ }
284
+ },
285
+ options: {
286
+ description: 'Check test status',
287
+ notes: 'Check delivery test status',
288
+ tags: ['api', 'Delivery Test'],
289
+
290
+ plugins: {
291
+ 'hapi-swagger': {
292
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
293
+ }
294
+ },
295
+
296
+ auth: {
297
+ strategy: 'api-token',
298
+ mode: 'required'
299
+ },
300
+ cors: CORS_CONFIG,
301
+
302
+ validate: {
303
+ options: {
304
+ stripUnknown: false,
305
+ abortEarly: false,
306
+ convert: true
307
+ },
308
+ failAction,
309
+
310
+ params: Joi.object({
311
+ deliveryTest: Joi.string()
312
+ .guid({
313
+ version: ['uuidv4', 'uuidv5']
314
+ })
315
+ .example('6420a6ad-7f82-4e4f-8112-82a9dad1f34d')
316
+ .required()
317
+ .description('Test ID')
318
+ }).label('DeliveryCheckParams')
319
+ },
320
+
321
+ response: {
322
+ schema: Joi.object({
323
+ success: Joi.boolean().example(true).description('Was the test completed').label('ResponseDeliveryCheckSuccess'),
324
+ status: Joi.string()
325
+ .example('pending')
326
+ .description('Test status. Only present while the test message has not yet been received (success=false)')
327
+ .label('ResponseDeliveryCheckStatus'),
328
+ dkim: Joi.object().unknown().description('DKIM results').label('DkimResults'),
329
+ spf: Joi.object().unknown().description('SPF results').label('SpfResults'),
330
+ dmarc: Joi.object().unknown().description('DMARC results').label('DmarcResults'),
331
+ bimi: Joi.object().unknown().description('BIMI results').label('BimiResults'),
332
+ arc: Joi.object().unknown().description('ARC results').label('ArcResults'),
333
+ mainSig: Joi.object()
334
+ .unknown()
335
+ .description('Primary DKIM signature. `status.aligned` should be set, otherwise DKIM check should not be considered as passed.')
336
+ .label('MainSignature')
337
+ })
338
+ .unknown()
339
+ .label('DeliveryCheckResponse'),
340
+ failAction: 'log'
341
+ }
342
+ }
343
+ });
344
+ }
345
+
346
+ module.exports = init;
@@ -5,21 +5,10 @@ const { Export } = require('../export');
5
5
  const Boom = require('@hapi/boom');
6
6
  const Joi = require('joi');
7
7
  const { failAction } = require('../tools');
8
- const { accountIdSchema, exportRequestSchema, exportStatusSchema, exportListSchema, exportIdSchema } = require('../schemas');
8
+ const { accountIdSchema, exportRequestSchema, exportStatusSchema, exportListSchema, exportIdSchema, errorResponses } = require('../schemas');
9
9
  const getSecret = require('../get-secret');
10
10
  const { createDecryptStream } = require('../stream-encrypt');
11
-
12
- function handleError(request, err) {
13
- request.logger.error({ msg: 'API request failed', err });
14
- if (Boom.isBoom(err)) {
15
- throw err;
16
- }
17
- const error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
18
- if (err.code) {
19
- error.output.payload.code = err.code;
20
- }
21
- throw error;
22
- }
11
+ const { handleError } = require('./route-helpers');
23
12
 
24
13
  async function init(args) {
25
14
  const { server, CORS_CONFIG } = args;
@@ -48,6 +37,12 @@ async function init(args) {
48
37
  notes: 'Creates a new bulk message export job. The export runs asynchronously and notifies via webhook when complete.',
49
38
  tags: ['api', 'Export (Beta)'],
50
39
 
40
+ plugins: {
41
+ 'hapi-swagger': {
42
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
43
+ }
44
+ },
45
+
51
46
  auth: {
52
47
  strategy: 'api-token',
53
48
  mode: 'required'
@@ -101,6 +96,12 @@ async function init(args) {
101
96
  notes: 'Returns the status and progress of an export job.',
102
97
  tags: ['api', 'Export (Beta)'],
103
98
 
99
+ plugins: {
100
+ 'hapi-swagger': {
101
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
102
+ }
103
+ },
104
+
104
105
  auth: {
105
106
  strategy: 'api-token',
106
107
  mode: 'required'
@@ -178,7 +179,8 @@ async function init(args) {
178
179
 
179
180
  plugins: {
180
181
  'hapi-swagger': {
181
- produces: ['application/gzip']
182
+ produces: ['application/gzip'],
183
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
182
184
  }
183
185
  },
184
186
 
@@ -225,6 +227,12 @@ async function init(args) {
225
227
  notes: 'Cancels a pending export or deletes a completed export. Removes both Redis data and the export file.',
226
228
  tags: ['api', 'Export (Beta)'],
227
229
 
230
+ plugins: {
231
+ 'hapi-swagger': {
232
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
233
+ }
234
+ },
235
+
228
236
  auth: {
229
237
  strategy: 'api-token',
230
238
  mode: 'required'
@@ -274,6 +282,12 @@ async function init(args) {
274
282
  notes: 'Lists all exports for an account with pagination.',
275
283
  tags: ['api', 'Export (Beta)'],
276
284
 
285
+ plugins: {
286
+ 'hapi-swagger': {
287
+ responses: errorResponses(400, 401, 403, 429, 500)
288
+ }
289
+ },
290
+
277
291
  auth: {
278
292
  strategy: 'api-token',
279
293
  mode: 'required'