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,77 @@
1
+ 'use strict';
2
+
3
+ const Joi = require('joi');
4
+ const { redis } = require('../db');
5
+ const { failAction, getStats } = require('../tools');
6
+ const { MAX_DAYS_STATS } = require('../consts');
7
+ const packageData = require('../../package.json');
8
+
9
+ async function init(args) {
10
+ const { server, call, CORS_CONFIG } = args;
11
+
12
+ server.route({
13
+ method: 'GET',
14
+ path: '/v1/stats',
15
+
16
+ async handler(request) {
17
+ return await getStats(redis, call, request.query.seconds);
18
+ },
19
+
20
+ options: {
21
+ description: 'Return server stats',
22
+ tags: ['api', 'Stats'],
23
+
24
+ auth: {
25
+ strategy: 'api-token',
26
+ mode: 'required'
27
+ },
28
+ cors: CORS_CONFIG,
29
+
30
+ validate: {
31
+ options: {
32
+ stripUnknown: false,
33
+ abortEarly: false,
34
+ convert: true
35
+ },
36
+ failAction,
37
+
38
+ query: Joi.object({
39
+ seconds: Joi.number()
40
+ .integer()
41
+ .empty('')
42
+ .min(0)
43
+ .max(MAX_DAYS_STATS * 24 * 3600)
44
+ .default(3600)
45
+ .example(3600)
46
+ .description('Duration for counters')
47
+ .label('CounterSeconds')
48
+ }).label('ServerStats')
49
+ },
50
+
51
+ response: {
52
+ schema: Joi.object({
53
+ version: Joi.string().example(packageData.version).description('EmailEngine version number'),
54
+ license: Joi.string().example(packageData.license).description('EmailEngine license'),
55
+ accounts: Joi.number().integer().example(26).description('Number of registered accounts'),
56
+ node: Joi.string().example('16.10.0').description('Node.js Version'),
57
+ redis: Joi.string().example('6.2.4').description('Redis Version'),
58
+ connections: Joi.object({
59
+ init: Joi.number().integer().example(2).description('Accounts not yet initialized'),
60
+ connected: Joi.number().integer().example(8).description('Successfully connected accounts'),
61
+ connecting: Joi.number().integer().example(7).description('Connection is being established'),
62
+ authenticationError: Joi.number().integer().example(3).description('Authentication failed'),
63
+ connectError: Joi.number().integer().example(5).description('Connection failed due to technical error'),
64
+ unset: Joi.number().integer().example(0).description('Accounts without valid IMAP settings'),
65
+ disconnected: Joi.number().integer().example(1).description('IMAP connection was closed')
66
+ })
67
+ .description('Counts of accounts in different connection states')
68
+ .label('ConnectionsStats'),
69
+ counters: Joi.object().label('CounterStats').unknown()
70
+ }).label('SettingsResponse'),
71
+ failAction: 'log'
72
+ }
73
+ }
74
+ });
75
+ }
76
+
77
+ module.exports = init;
@@ -0,0 +1,472 @@
1
+ 'use strict';
2
+
3
+ const Boom = require('@hapi/boom');
4
+ const Joi = require('joi');
5
+ const { redis } = require('../db');
6
+ const { Account } = require('../account');
7
+ const getSecret = require('../get-secret');
8
+ const { failAction } = require('../tools');
9
+ const {
10
+ messageReferenceSchema,
11
+ fromAddressSchema,
12
+ addressSchema,
13
+ idempotencyKeySchema,
14
+ headerTimeoutSchema,
15
+ accountIdSchema,
16
+ templateSchemas,
17
+ settingsSchema,
18
+ ipSchema
19
+ } = require('../schemas');
20
+
21
+ async function init(args) {
22
+ const { server, call, CORS_CONFIG, MAX_ATTACHMENT_SIZE, MAX_BODY_SIZE, MAX_PAYLOAD_TIMEOUT } = args;
23
+
24
+ server.route({
25
+ method: 'POST',
26
+ path: '/v1/account/{account}/submit',
27
+
28
+ async handler(request) {
29
+ let accountObject = new Account({
30
+ redis,
31
+ account: request.params.account,
32
+ call,
33
+ secret: await getSecret(),
34
+ timeout: request.headers['x-ee-timeout']
35
+ });
36
+
37
+ try {
38
+ return await accountObject.queueMessage(request.payload, {
39
+ source: 'api',
40
+ idempotencyKey: request.headers['idempotency-key'],
41
+ useStructuredFormat: request.query.useStructuredFormat
42
+ });
43
+ } catch (err) {
44
+ request.logger.error({ msg: 'API request failed', err });
45
+ if (Boom.isBoom(err)) {
46
+ throw err;
47
+ }
48
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
49
+ if (err.code) {
50
+ error.output.payload.code = err.code;
51
+ }
52
+ if (err.info) {
53
+ error.output.payload.info = err.info;
54
+ }
55
+ throw error;
56
+ }
57
+ },
58
+ options: {
59
+ payload: {
60
+ maxBytes: MAX_BODY_SIZE,
61
+ timeout: MAX_PAYLOAD_TIMEOUT
62
+ },
63
+
64
+ description: 'Submit message for delivery',
65
+ notes: 'Submit message for delivery. If reference message ID is provided then EmailEngine adds all headers and flags required for a reply/forward automatically.',
66
+ tags: ['api', 'Submit'],
67
+
68
+ plugins: {},
69
+
70
+ auth: {
71
+ strategy: 'api-token',
72
+ mode: 'required'
73
+ },
74
+ cors: CORS_CONFIG,
75
+
76
+ validate: {
77
+ options: {
78
+ stripUnknown: false,
79
+ abortEarly: false,
80
+ convert: true
81
+ },
82
+ failAction,
83
+
84
+ params: Joi.object({
85
+ account: accountIdSchema.required()
86
+ }),
87
+
88
+ query: Joi.object({
89
+ documentStore: Joi.boolean()
90
+ .truthy('Y', 'true', '1')
91
+ .falsy('N', 'false', 0)
92
+ .default(false)
93
+ .description('If enabled then fetch email used as a reference template from the Document Store'),
94
+ useStructuredFormat: Joi.boolean()
95
+ .truthy('Y', 'true', '1')
96
+ .falsy('N', 'false', 0)
97
+ .default(false)
98
+ .description(
99
+ 'For MS Graph accounts: If true, uses structured JSON format (respects from field for shared mailboxes, breaks calendar invites and special MIME types). If false, sends as raw MIME (preserves calendar invites, ignores from field). Default is false (raw MIME).'
100
+ )
101
+ }).label('SubmitQuery'),
102
+
103
+ headers: Joi.object({
104
+ 'x-ee-timeout': headerTimeoutSchema,
105
+ 'idempotency-key': idempotencyKeySchema
106
+ }).unknown(),
107
+
108
+ payload: Joi.object({
109
+ reference: messageReferenceSchema,
110
+
111
+ envelope: Joi.object({
112
+ from: Joi.string().email().allow('').example('sender@example.com'),
113
+ to: Joi.array().items(Joi.string().email().required().example('recipient@example.com')).single().label('SmtpEnvelopeTo')
114
+ })
115
+ .description(
116
+ "An optional object specifying the SMTP envelope used during email transmission. If not provided, the envelope is automatically derived from the email's message headers. This is useful when you need the envelope addresses to differ from those in the email headers."
117
+ )
118
+ .label('SMTPEnvelope')
119
+ .when('mailMerge', {
120
+ is: Joi.exist().not(false, null),
121
+ then: Joi.forbidden('y')
122
+ }),
123
+
124
+ raw: Joi.string()
125
+ .base64()
126
+ .max(MAX_ATTACHMENT_SIZE)
127
+ .example('TUlNRS1WZXJzaW9uOiAxLjANClN1YmplY3Q6IGhlbGxvIHdvcmxkDQoNCkhlbGxvIQ0K')
128
+ .description(
129
+ 'A Base64-encoded email message in RFC 822 format. If you provide other fields along with raw, those fields will override the corresponding values in the raw message.'
130
+ )
131
+ .label('RFC822Raw')
132
+ .when('mailMerge', {
133
+ is: Joi.exist().not(false, null),
134
+ then: Joi.forbidden('y')
135
+ }),
136
+
137
+ from: fromAddressSchema,
138
+
139
+ replyTo: Joi.array()
140
+ .items(addressSchema.label('ReplyToAddress'))
141
+ .single()
142
+ .example([{ name: 'From Me', address: 'sender@example.com' }])
143
+ .description('List of Reply-To addresses')
144
+ .label('ReplyTo'),
145
+
146
+ to: Joi.array()
147
+ .items(addressSchema.label('ToAddress'))
148
+ .single()
149
+ .example([{ address: 'recipient@example.com' }])
150
+ .description('List of recipient addresses')
151
+ .label('ToAddressList')
152
+ .when('mailMerge', {
153
+ is: Joi.exist().not(false, null),
154
+ then: Joi.forbidden('y')
155
+ }),
156
+
157
+ cc: Joi.array()
158
+ .items(addressSchema.label('CcAddress'))
159
+ .single()
160
+ .description('List of CC addresses')
161
+ .label('CcAddressList')
162
+ .when('mailMerge', {
163
+ is: Joi.exist().not(false, null),
164
+ then: Joi.forbidden('y')
165
+ }),
166
+
167
+ bcc: Joi.array()
168
+ .items(addressSchema.label('BccAddress'))
169
+ .single()
170
+ .description('List of BCC addresses')
171
+ .label('BccAddressList')
172
+ .when('mailMerge', {
173
+ is: Joi.exist().not(false, null),
174
+ then: Joi.forbidden('y')
175
+ }),
176
+
177
+ subject: templateSchemas.subject,
178
+ text: templateSchemas.text,
179
+ html: templateSchemas.html,
180
+ previewText: templateSchemas.previewText,
181
+
182
+ template: Joi.string().max(256).example('example').description('Stored template ID to load the email content from'),
183
+
184
+ render: Joi.object({
185
+ format: Joi.string()
186
+ .valid('html', 'markdown')
187
+ .default('html')
188
+ .description('Markup language for HTML ("html" or "markdown")')
189
+ .label('RenderFormat'),
190
+ params: Joi.object().label('RenderValues').description('An object of variables for the template renderer')
191
+ })
192
+ .allow(false)
193
+ .description('Template rendering options')
194
+ .when('mailMerge', {
195
+ is: Joi.exist().not(false, null),
196
+ then: Joi.forbidden('y')
197
+ })
198
+ .label('TemplateRender'),
199
+
200
+ mailMerge: Joi.array()
201
+ .items(
202
+ Joi.object({
203
+ to: addressSchema.label('ToAddress').required(),
204
+ messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
205
+ params: Joi.object().label('RenderValues').description('An object of variables for the template renderer'),
206
+ sendAt: Joi.date()
207
+ .iso()
208
+ .example('2021-07-08T07:06:34.336Z')
209
+ .description('Send message at specified time. Overrides message level `sendAt` value.')
210
+ }).label('MailMergeListEntry')
211
+ )
212
+ .min(1)
213
+ .description(
214
+ 'Mail merge options. A separate email is generated for each recipient. Using mail merge disables `messageId`, `envelope`, `to`, `cc`, `bcc`, `render` keys for the message root.'
215
+ )
216
+ .label('MailMergeList'),
217
+
218
+ attachments: Joi.array()
219
+ .items(
220
+ Joi.object({
221
+ filename: Joi.string().max(256).example('transparent.gif'),
222
+ content: Joi.string()
223
+ .base64()
224
+ .max(MAX_ATTACHMENT_SIZE)
225
+ .required()
226
+ .example('R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=')
227
+ .description('Base64 formatted attachment file')
228
+ .when('reference', {
229
+ is: Joi.exist().not(false, null),
230
+ then: Joi.forbidden(),
231
+ otherwise: Joi.required()
232
+ }),
233
+
234
+ contentType: Joi.string().lowercase().max(256).example('image/gif'),
235
+ contentDisposition: Joi.string().lowercase().valid('inline', 'attachment').label('AttachmentContentDisposition'),
236
+ cid: Joi.string().max(256).example('unique-image-id@localhost').description('Content-ID value for embedded images'),
237
+ encoding: Joi.string().valid('base64').default('base64').label('AttachmentEncoding'),
238
+
239
+ reference: Joi.string()
240
+ .base64({ paddingRequired: false, urlSafe: true })
241
+ .max(256)
242
+ .allow(false, null)
243
+ .example('AAAAAQAACnAcde')
244
+ .description(
245
+ 'References an existing attachment by its ID instead of providing new attachment content. If this field is set, the `content` field must not be included. If not set, the `content` field is required.'
246
+ )
247
+ .label('AttachmentReference')
248
+ }).label('UploadAttachment')
249
+ )
250
+ .description('List of attachments')
251
+ .label('UploadAttachmentList'),
252
+
253
+ messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
254
+ headers: Joi.object().label('CustomHeaders').description('Custom Headers').unknown().example({
255
+ 'X-My-Custom-Header': 'Custom header value'
256
+ }),
257
+
258
+ trackingEnabled: Joi.boolean()
259
+ .example(false)
260
+ .description('Should EmailEngine track clicks and opens for this message')
261
+ .meta({ swaggerHidden: true }),
262
+
263
+ trackOpens: Joi.boolean().example(false).description('Should EmailEngine track opens for this message'),
264
+ trackClicks: Joi.boolean().example(false).description('Should EmailEngine track clicks for this message'),
265
+
266
+ copy: Joi.boolean()
267
+ .allow(null)
268
+ .example(null)
269
+ .description(
270
+ "If set then either copies the message to the Sent Mail folder or not. If not set then uses the account's default setting."
271
+ ),
272
+
273
+ sentMailPath: Joi.string()
274
+ .empty('')
275
+ .max(1024)
276
+ .example('Sent Mail')
277
+ .description("Upload sent message to this folder. By default the account's Sent Mail folder is used."),
278
+
279
+ locale: Joi.string().empty('').max(100).example('fr').description('Optional locale').label('MessageLocale'),
280
+ tz: Joi.string().empty('').max(100).example('Europe/Tallinn').description('Optional timezone'),
281
+
282
+ sendAt: Joi.date().iso().example('2021-07-08T07:06:34.336Z').description('Send message at specified time'),
283
+ deliveryAttempts: Joi.number()
284
+ .integer()
285
+ .example(10)
286
+ .description('How many delivery attempts to make until message is considered as failed'),
287
+ gateway: Joi.string().max(256).example('example').description('Optional SMTP gateway ID for message routing').label('MessageGateway'),
288
+
289
+ listId: Joi.string()
290
+ .hostname()
291
+ .example('test-list')
292
+ .description(
293
+ 'List ID for Mail Merge. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.'
294
+ )
295
+ .label('ListID')
296
+ .when('mailMerge', {
297
+ is: Joi.exist().not(false, null),
298
+ then: Joi.optional(),
299
+ otherwise: Joi.forbidden()
300
+ }),
301
+
302
+ dsn: Joi.object({
303
+ id: Joi.string().trim().empty('').max(256).description('The envelope identifier that would be included in the response (ENVID)'),
304
+ return: Joi.string()
305
+ .trim()
306
+ .empty('')
307
+ .valid('headers', 'full')
308
+ .required()
309
+ .description('Specifies if only headers or the entire body of the message should be included in the response (RET)')
310
+ .label('DsnReturn'),
311
+ notify: Joi.array()
312
+ .single()
313
+ .items(Joi.string().valid('never', 'success', 'failure', 'delay').label('NotifyEntry'))
314
+ .description('Defines the conditions under which a DSN response should be sent')
315
+ .label('DsnNotify'),
316
+ recipient: Joi.string().trim().empty('').email().description('The email address the DSN should be sent (ORCPT)')
317
+ })
318
+ .description('Request DSN notifications')
319
+ .label('DSN'),
320
+
321
+ baseUrl: Joi.string()
322
+ .trim()
323
+ .empty('')
324
+ .uri({
325
+ scheme: ['http', 'https'],
326
+ allowRelative: false
327
+ })
328
+ .example('https://customer123.myservice.com')
329
+ .description('Optional base URL for trackers. This URL must point to your EmailEngine instance.'),
330
+
331
+ proxy: settingsSchema.proxyUrl.description('Optional proxy URL to use when connecting to the SMTP server'),
332
+ localAddress: ipSchema.description('Optional local IP address to bind to when connecting to the SMTP server'),
333
+
334
+ dryRun: Joi.boolean()
335
+ .truthy('Y', 'true', '1')
336
+ .falsy('N', 'false', 0)
337
+ .default(false)
338
+ .description(
339
+ 'If true, then EmailEngine does not send the email and returns an RFC822 formatted email file. Tracking information is not added to the email.'
340
+ )
341
+ .label('Preview')
342
+ })
343
+ .oxor('raw', 'html')
344
+ .oxor('raw', 'text')
345
+ .oxor('raw', 'text')
346
+ .oxor('raw', 'attachments')
347
+ .label('SubmitMessage')
348
+ .example({
349
+ to: [
350
+ {
351
+ name: 'Nyan Cat',
352
+ address: 'nyan.cat@example.com'
353
+ }
354
+ ],
355
+ subject: 'What a wonderful message!',
356
+ text: 'Hello from myself!',
357
+ html: '<p>Hello from myself!</p>',
358
+ attachments: [
359
+ {
360
+ filename: 'transparent.gif',
361
+ content: 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=',
362
+ contentType: 'image/gif'
363
+ }
364
+ ]
365
+ })
366
+ },
367
+
368
+ response: {
369
+ schema: Joi.object({
370
+ response: Joi.string().example('Queued for delivery'),
371
+ messageId: Joi.string()
372
+ .example('<a2184d08-a470-fec6-a493-fa211a3756e9@example.com>')
373
+ .description('Message-ID header value. Not present for bulk messages.'),
374
+ queueId: Joi.string().example('d41f0423195f271f').description('Queue identifier for scheduled email. Not present for bulk messages.'),
375
+ sendAt: Joi.date().example('2021-07-08T07:06:34.336Z').description('Scheduled send time'),
376
+
377
+ reference: Joi.object({
378
+ message: Joi.string()
379
+ .base64({ paddingRequired: false, urlSafe: true })
380
+ .max(256)
381
+ .required()
382
+ .example('AAAAAQAACnA')
383
+ .description('Referenced message ID'),
384
+ documentStore: Joi.boolean()
385
+ .example(true)
386
+ .description('Was the message data loaded from the Document Store')
387
+ .label('ResponseDocumentStore')
388
+ .meta({ swaggerHidden: true }),
389
+ success: Joi.boolean().example(true).description('Was the referenced message processed successfully').label('ResponseReferenceSuccess'),
390
+ error: Joi.string().example('Referenced message was not found').description('An error message if referenced message processing failed')
391
+ })
392
+ .description('Reference info if referencing was requested')
393
+ .label('ResponseReference'),
394
+
395
+ preview: Joi.string()
396
+ .base64()
397
+ .example('Q29udGVudC1UeXBlOiBtdWx0aX...')
398
+ .description('Base64 encoded RFC822 email if a preview was requested')
399
+ .label('ResponsePreview'),
400
+
401
+ mailMerge: Joi.array()
402
+ .items(
403
+ Joi.object({
404
+ success: Joi.boolean()
405
+ .example(true)
406
+ .description('Was the referenced message processed successfully')
407
+ .label('ResponseReferenceSuccess'),
408
+ to: addressSchema.label('ToAddressSingle'),
409
+ messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
410
+ queueId: Joi.string()
411
+ .example('d41f0423195f271f')
412
+ .description('Queue identifier for scheduled email. Not present for bulk messages.'),
413
+ reference: Joi.object({
414
+ message: Joi.string()
415
+ .base64({ paddingRequired: false, urlSafe: true })
416
+ .max(256)
417
+ .required()
418
+ .example('AAAAAQAACnA')
419
+ .description('Referenced message ID'),
420
+ documentStore: Joi.boolean()
421
+ .example(true)
422
+ .description('Was the message data loaded from the Document Store')
423
+ .label('ResponseDocumentStore')
424
+ .meta({ swaggerHidden: true }),
425
+ success: Joi.boolean()
426
+ .example(true)
427
+ .description('Was the referenced message processed successfully')
428
+ .label('ResponseReferenceSuccess'),
429
+ error: Joi.string()
430
+ .example('Referenced message was not found')
431
+ .description('An error message if referenced message processing failed')
432
+ })
433
+ .description('Reference info if referencing was requested')
434
+ .label('ResponseReference'),
435
+ sendAt: Joi.date()
436
+ .iso()
437
+ .example('2021-07-08T07:06:34.336Z')
438
+ .description('Send message at specified time. Overrides message level `sendAt` value.'),
439
+ skipped: Joi.object({
440
+ reason: Joi.string().example('unsubscribe').description('Why this message was skipped'),
441
+ listId: Joi.string().example('test-list')
442
+ })
443
+ .description('Info about skipped message. If this value is set, then the message was not sent')
444
+ .label('SkippedMessageInfo')
445
+ })
446
+ .label('BulkResponseEntry')
447
+ .example({
448
+ success: true,
449
+ to: {
450
+ name: 'Andris 2',
451
+ address: 'andris@ethereal.email'
452
+ },
453
+ messageId: '<19b9c433-d428-f6d8-1d00-d666ebcadfc4@ekiri.ee>',
454
+ queueId: '1812477338914c8372a',
455
+ reference: {
456
+ message: 'AAAAAQAACnA',
457
+ success: true
458
+ },
459
+ sendAt: '2021-07-08T07:06:34.336Z'
460
+ })
461
+ .unknown()
462
+ )
463
+ .label('BulkResponseList')
464
+ .description('Bulk message responses')
465
+ }).label('SubmitMessageResponse'),
466
+ failAction: 'log'
467
+ }
468
+ }
469
+ });
470
+ }
471
+
472
+ module.exports = init;
@@ -4,9 +4,9 @@ const { redis } = require('../db');
4
4
  const { Account } = require('../account');
5
5
  const getSecret = require('../get-secret');
6
6
  const { templates } = require('../templates');
7
- const Boom = require('@hapi/boom');
8
7
  const Joi = require('joi');
9
8
  const { failAction } = require('../tools');
9
+ const { handleError } = require('./route-helpers');
10
10
 
11
11
  const { templateSchemas, accountIdSchema } = require('../schemas');
12
12
 
@@ -43,15 +43,7 @@ async function init(args) {
43
43
  request.payload.content
44
44
  );
45
45
  } catch (err) {
46
- request.logger.error({ msg: 'API request failed', err });
47
- if (Boom.isBoom(err)) {
48
- throw err;
49
- }
50
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
51
- if (err.code) {
52
- error.output.payload.code = err.code;
53
- }
54
- throw error;
46
+ handleError(request, err);
55
47
  }
56
48
  },
57
49
 
@@ -135,15 +127,7 @@ async function init(args) {
135
127
 
136
128
  return await templates.update(request.params.template, meta, request.payload.content);
137
129
  } catch (err) {
138
- request.logger.error({ msg: 'API request failed', err });
139
- if (Boom.isBoom(err)) {
140
- throw err;
141
- }
142
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
143
- if (err.code) {
144
- error.output.payload.code = err.code;
145
- }
146
- throw error;
130
+ handleError(request, err);
147
131
  }
148
132
  },
149
133
 
@@ -220,15 +204,7 @@ async function init(args) {
220
204
 
221
205
  return await templates.list(request.query.account, request.query.page, request.query.pageSize);
222
206
  } catch (err) {
223
- request.logger.error({ msg: 'API request failed', err });
224
- if (Boom.isBoom(err)) {
225
- throw err;
226
- }
227
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
228
- if (err.code) {
229
- error.output.payload.code = err.code;
230
- }
231
- throw error;
207
+ handleError(request, err);
232
208
  }
233
209
  },
234
210
 
@@ -310,15 +286,7 @@ async function init(args) {
310
286
  try {
311
287
  return await templates.get(request.params.template);
312
288
  } catch (err) {
313
- request.logger.error({ msg: 'API request failed', err });
314
- if (Boom.isBoom(err)) {
315
- throw err;
316
- }
317
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
318
- if (err.code) {
319
- error.output.payload.code = err.code;
320
- }
321
- throw error;
289
+ handleError(request, err);
322
290
  }
323
291
  },
324
292
 
@@ -390,15 +358,7 @@ async function init(args) {
390
358
  try {
391
359
  return await templates.del(request.params.template);
392
360
  } catch (err) {
393
- request.logger.error({ msg: 'API request failed', err });
394
- if (Boom.isBoom(err)) {
395
- throw err;
396
- }
397
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
398
- if (err.code) {
399
- error.output.payload.code = err.code;
400
- }
401
- throw error;
361
+ handleError(request, err);
402
362
  }
403
363
  },
404
364
  options: {
@@ -451,15 +411,7 @@ async function init(args) {
451
411
 
452
412
  return await templates.flush(request.params.account);
453
413
  } catch (err) {
454
- request.logger.error({ msg: 'API request failed', err });
455
- if (Boom.isBoom(err)) {
456
- throw err;
457
- }
458
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
459
- if (err.code) {
460
- error.output.payload.code = err.code;
461
- }
462
- throw error;
414
+ handleError(request, err);
463
415
  }
464
416
  },
465
417
  options: {