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
@@ -4,25 +4,47 @@ const { redis } = require('../db');
4
4
  const { Account } = require('../account');
5
5
  const getSecret = require('../get-secret');
6
6
  const settings = require('../settings');
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 {
12
12
  accountIdSchema,
13
13
  messageDetailsSchema,
14
14
  messageListSchema,
15
+ messageEntrySchema,
15
16
  documentStoreSchema,
17
+ documentStoreQuerySchema,
16
18
  searchSchema,
17
19
  messageUpdateSchema,
18
20
  addressSchema,
19
21
  fromAddressSchema,
20
- messageReferenceSchema
22
+ messageReferenceSchema,
23
+ emailIdSchema,
24
+ threadIdSchema,
25
+ errorResponses
21
26
  } = require('../schemas');
22
27
 
23
28
  const listMessageFolderPathDescription =
24
29
  'Mailbox folder path. Can use special use labels like "\\Sent". Special value "\\All" is available for Gmail IMAP, Gmail API, MS Graph API accounts.';
25
30
 
31
+ // Response field for a flag or label update operation. IMAP accounts return a boolean (whether
32
+ // the operation succeeded), API-based accounts echo the requested values as an array.
33
+ const updateOpResultSchema = (op, noun, entryLabel, example, label) =>
34
+ Joi.alternatives()
35
+ .try(Joi.boolean(), Joi.array().items(Joi.string().label(entryLabel)))
36
+ .example(example)
37
+ .description(`Did the ${op} operation succeed (boolean, IMAP accounts) or the requested ${noun} (array, API-based accounts)`)
38
+ .label(label);
39
+
40
+ // Source-to-target ID map for messages that were moved to another folder. Only the IMAP backend
41
+ // provides this info.
42
+ const idMapSchema = Joi.array()
43
+ .items(Joi.array().length(2).items(Joi.string().max(256).required().description('Message ID')).label('IdMapTuple'))
44
+ .example([['AAAAAQAACnA', 'AAAAAwAAAD4']])
45
+ .description('An optional map of source and target ID values, if the server provided this info (IMAP accounts only)')
46
+ .label('IdMapArray');
47
+
26
48
  async function init(args) {
27
49
  const { server, call, CORS_CONFIG, MAX_ATTACHMENT_SIZE, MAX_BODY_SIZE, MAX_PAYLOAD_TIMEOUT } = args;
28
50
 
@@ -44,15 +66,7 @@ async function init(args) {
44
66
  const response = await accountObject.getRawMessage(request.params.message);
45
67
  return h.response(response);
46
68
  } catch (err) {
47
- request.logger.error({ msg: 'API request failed', err });
48
- if (Boom.isBoom(err)) {
49
- throw err;
50
- }
51
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
52
- if (err.code) {
53
- error.output.payload.code = err.code;
54
- }
55
- throw error;
69
+ handleError(request, err);
56
70
  }
57
71
  },
58
72
  options: {
@@ -68,7 +82,8 @@ async function init(args) {
68
82
 
69
83
  plugins: {
70
84
  'hapi-swagger': {
71
- produces: ['message/rfc822']
85
+ produces: ['message/rfc822'],
86
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
72
87
  }
73
88
  },
74
89
 
@@ -106,15 +121,7 @@ async function init(args) {
106
121
  try {
107
122
  return await accountObject.getMessage(request.params.message, request.query);
108
123
  } catch (err) {
109
- request.logger.error({ msg: 'API request failed', err });
110
- if (Boom.isBoom(err)) {
111
- throw err;
112
- }
113
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
114
- if (err.code) {
115
- error.output.payload.code = err.code;
116
- }
117
- throw error;
124
+ handleError(request, err);
118
125
  }
119
126
  },
120
127
  options: {
@@ -122,6 +129,12 @@ async function init(args) {
122
129
  notes: 'Returns details of a specific message. By default text content is not included, use textType value to force retrieving text',
123
130
  tags: ['api', 'Message'],
124
131
 
132
+ plugins: {
133
+ 'hapi-swagger': {
134
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
135
+ }
136
+ },
137
+
125
138
  auth: {
126
139
  strategy: 'api-token',
127
140
  mode: 'required'
@@ -213,15 +226,7 @@ async function init(args) {
213
226
  try {
214
227
  return await accountObject.uploadMessage(request.payload);
215
228
  } catch (err) {
216
- request.logger.error({ msg: 'API request failed', err });
217
- if (Boom.isBoom(err)) {
218
- throw err;
219
- }
220
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
221
- if (err.code) {
222
- error.output.payload.code = err.code;
223
- }
224
- throw error;
229
+ handleError(request, err);
225
230
  }
226
231
  },
227
232
  options: {
@@ -234,7 +239,11 @@ async function init(args) {
234
239
  notes: 'Upload a message structure, compile it into an EML file and store it into selected mailbox.',
235
240
  tags: ['api', 'Message'],
236
241
 
237
- plugins: {},
242
+ plugins: {
243
+ 'hapi-swagger': {
244
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
245
+ }
246
+ },
238
247
 
239
248
  auth: {
240
249
  strategy: 'api-token',
@@ -348,9 +357,11 @@ async function init(args) {
348
357
  )
349
358
  .label('MessageAppendId'),
350
359
  path: Joi.string().example('INBOX').description('Folder this message was uploaded to').label('MessageAppendPath'),
351
- uid: Joi.number().integer().example(12345).description('UID of uploaded message'),
352
- uidValidity: Joi.string().example('12345').description('UIDVALIDITY of the target folder. Numeric value cast as string.'),
353
- seq: Joi.number().integer().example(12345).description('Sequence number of uploaded message'),
360
+ uid: Joi.number().integer().example(12345).description('UID of uploaded message (IMAP accounts only)'),
361
+ uidValidity: Joi.string()
362
+ .example('12345')
363
+ .description('UIDVALIDITY of the target folder. Numeric value cast as string. IMAP accounts only.'),
364
+ seq: Joi.number().integer().example(12345).description('Sequence number of uploaded message (IMAP accounts only)'),
354
365
 
355
366
  messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
356
367
 
@@ -358,9 +369,9 @@ async function init(args) {
358
369
  message: Joi.string()
359
370
  .base64({ paddingRequired: false, urlSafe: true })
360
371
  .max(256)
361
- .required()
362
372
  .example('AAAAAQAACnA')
363
- .description('Referenced message ID'),
373
+ .description('Referenced message ID. Not present when only a thread ID was referenced'),
374
+ threadId: threadIdSchema.description('Referenced thread ID (Gmail API accounts only)').label('ResponseReferenceThreadId'),
364
375
  success: Joi.boolean().example(true).description('Was the referenced message processed').label('ResponseReferenceSuccess'),
365
376
  documentStore: documentStoreSchema.default(false),
366
377
  error: Joi.string().example('Referenced message was not found').description('An error message if referenced message processing failed')
@@ -390,15 +401,7 @@ async function init(args) {
390
401
  try {
391
402
  return await accountObject.updateMessage(request.params.message, request.payload);
392
403
  } 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;
404
+ handleError(request, err);
402
405
  }
403
406
  },
404
407
  options: {
@@ -406,7 +409,11 @@ async function init(args) {
406
409
  notes: 'Update message information. Mainly this means changing message flag values',
407
410
  tags: ['api', 'Message'],
408
411
 
409
- plugins: {},
412
+ plugins: {
413
+ 'hapi-swagger': {
414
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
415
+ }
416
+ },
410
417
 
411
418
  auth: {
412
419
  strategy: 'api-token',
@@ -432,14 +439,24 @@ async function init(args) {
432
439
  response: {
433
440
  schema: Joi.object({
434
441
  flags: Joi.object({
435
- add: Joi.array().items(Joi.string().label('FlagEntry')).example(['\\Seen', '\\Flagged']).label('FlagAddList'),
436
- delete: Joi.array().items(Joi.string().label('FlagEntry')).example(['\\Draft']).label('FlagDeleteList'),
437
- set: Joi.array().items(Joi.string().label('FlagEntry')).example(['\\Seen']).label('FlagSetList')
442
+ add: updateOpResultSchema('add', 'flags', 'FlagEntry', true, 'FlagAddResult'),
443
+ delete: updateOpResultSchema('delete', 'flags', 'FlagEntry', false, 'FlagDeleteResult'),
444
+ set: updateOpResultSchema('set', 'flags', 'FlagEntry', true, 'FlagSetResult'),
445
+ result: Joi.array()
446
+ .items(Joi.string().label('FlagEntry'))
447
+ .example(['\\Seen'])
448
+ .description('Resulting flag set after the update (Gmail API and MS Graph API accounts only)')
449
+ .label('FlagResultList')
438
450
  }).label('FlagUpdateResponse'),
439
451
  labels: Joi.object({
440
- add: Joi.array().items(Joi.string().label('LabelEntry')).example(['Label1', 'Label2']).label('LabelAddList'),
441
- delete: Joi.array().items(Joi.string().label('LabelEntry')).example(['Label3']).label('LabelDeleteList'),
442
- set: Joi.array().items(Joi.string().label('LabelEntry')).example(['Label1']).label('LabelSetList')
452
+ add: updateOpResultSchema('add', 'labels', 'LabelEntry', ['Label1', 'Label2'], 'LabelAddResult'),
453
+ delete: updateOpResultSchema('delete', 'labels', 'LabelEntry', ['Label3'], 'LabelDeleteResult'),
454
+ set: updateOpResultSchema('set', 'labels', 'LabelEntry', ['Label1'], 'LabelSetResult'),
455
+ result: Joi.array()
456
+ .items(Joi.string().label('LabelEntry'))
457
+ .example(['Label1'])
458
+ .description('Resulting label set after the update (Gmail API and MS Graph API accounts only)')
459
+ .label('LabelResultList')
443
460
  }).label('LabelUpdateResponse')
444
461
  }).label('MessageUpdateResponse'),
445
462
  failAction: 'log'
@@ -464,15 +481,7 @@ async function init(args) {
464
481
  try {
465
482
  return await accountObject.updateMessages(request.query.path, request.payload.search, request.payload.update);
466
483
  } catch (err) {
467
- request.logger.error({ msg: 'API request failed', err });
468
- if (Boom.isBoom(err)) {
469
- throw err;
470
- }
471
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
472
- if (err.code) {
473
- error.output.payload.code = err.code;
474
- }
475
- throw error;
484
+ handleError(request, err);
476
485
  }
477
486
  },
478
487
  options: {
@@ -480,7 +489,11 @@ async function init(args) {
480
489
  notes: 'Update message information for matching emails',
481
490
  tags: ['api', 'Multi Message Actions'],
482
491
 
483
- plugins: {},
492
+ plugins: {
493
+ 'hapi-swagger': {
494
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
495
+ }
496
+ },
484
497
 
485
498
  auth: {
486
499
  strategy: 'api-token',
@@ -512,15 +525,19 @@ async function init(args) {
512
525
  response: {
513
526
  schema: Joi.object({
514
527
  flags: Joi.object({
515
- add: Joi.array().items(Joi.string().label('BulkFlagEntry')).example(['\\Seen', '\\Flagged']).label('BulkFlagAddList'),
516
- delete: Joi.array().items(Joi.string().label('BulkFlagEntry')).example(['\\Draft']).label('BulkFlagDeleteList'),
517
- set: Joi.array().items(Joi.string().label('BulkFlagEntry')).example(['\\Seen']).label('BulkFlagSetList')
528
+ add: updateOpResultSchema('add', 'flags', 'BulkFlagEntry', true, 'BulkFlagAddResult'),
529
+ delete: updateOpResultSchema('delete', 'flags', 'BulkFlagEntry', false, 'BulkFlagDeleteResult'),
530
+ set: updateOpResultSchema('set', 'flags', 'BulkFlagEntry', true, 'BulkFlagSetResult')
518
531
  }).label('BulkFlagUpdateResponse'),
519
532
  labels: Joi.object({
520
- add: Joi.array().items(Joi.string().label('BulkLabelEntry')).example(['Label1', 'Label2']).label('BulkLabelAddList'),
521
- delete: Joi.array().items(Joi.string().label('BulkLabelEntry')).example(['Label3']).label('BulkLabelDeleteList'),
522
- set: Joi.array().items(Joi.string().label('BulkLabelEntry')).example(['Label1']).label('BulkLabelSetList')
523
- }).label('BulkLabelUpdateResponse')
533
+ add: updateOpResultSchema('add', 'labels', 'BulkLabelEntry', ['Label1', 'Label2'], 'BulkLabelAddResult'),
534
+ delete: updateOpResultSchema('delete', 'labels', 'BulkLabelEntry', ['Label3'], 'BulkLabelDeleteResult'),
535
+ set: updateOpResultSchema('set', 'labels', 'BulkLabelEntry', ['Label1'], 'BulkLabelSetResult')
536
+ }).label('BulkLabelUpdateResponse'),
537
+ emailIds: Joi.array()
538
+ .items(emailIdSchema)
539
+ .description('List of updated email IDs (Gmail API and MS Graph API accounts only)')
540
+ .label('BulkUpdatedEmailIds')
524
541
  }).label('BulkMessageUpdateResponse'),
525
542
  failAction: 'log'
526
543
  }
@@ -548,15 +565,7 @@ async function init(args) {
548
565
  }
549
566
  return await accountObject.moveMessage(request.params.message, { path: request.payload.path }, { source: sourceOption });
550
567
  } catch (err) {
551
- request.logger.error({ msg: 'API request failed', err });
552
- if (Boom.isBoom(err)) {
553
- throw err;
554
- }
555
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
556
- if (err.code) {
557
- error.output.payload.code = err.code;
558
- }
559
- throw error;
568
+ handleError(request, err);
560
569
  }
561
570
  },
562
571
  options: {
@@ -564,7 +573,11 @@ async function init(args) {
564
573
  notes: 'Moves a message to a target folder',
565
574
  tags: ['api', 'Message'],
566
575
 
567
- plugins: {},
576
+ plugins: {
577
+ 'hapi-swagger': {
578
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
579
+ }
580
+ },
568
581
 
569
582
  auth: {
570
583
  strategy: 'api-token',
@@ -626,15 +639,7 @@ async function init(args) {
626
639
  try {
627
640
  return await accountObject.moveMessages(request.query.path, request.payload.search, { path: request.payload.path });
628
641
  } catch (err) {
629
- request.logger.error({ msg: 'API request failed', err });
630
- if (Boom.isBoom(err)) {
631
- throw err;
632
- }
633
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
634
- if (err.code) {
635
- error.output.payload.code = err.code;
636
- }
637
- throw error;
642
+ handleError(request, err);
638
643
  }
639
644
  },
640
645
  options: {
@@ -642,7 +647,11 @@ async function init(args) {
642
647
  notes: 'Move messages matching to a search query to another folder',
643
648
  tags: ['api', 'Multi Message Actions'],
644
649
 
645
- plugins: {},
650
+ plugins: {
651
+ 'hapi-swagger': {
652
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
653
+ }
654
+ },
646
655
 
647
656
  auth: {
648
657
  strategy: 'api-token',
@@ -676,16 +685,15 @@ async function init(args) {
676
685
  schema: Joi.object({
677
686
  path: Joi.string().required().example('INBOX').description('Target mailbox folder path'),
678
687
 
679
- idMap: Joi.array()
680
- .items(Joi.array().length(2).items(Joi.string().max(256).required().description('Message ID')).label('IdMapTuple'))
681
- .example([['AAAAAQAACnA', 'AAAAAwAAAD4']])
682
- .description('An optional map of source and target ID values, if the server provided this info')
683
- .label('IdMapArray'),
688
+ idMap: idMapSchema,
684
689
 
685
690
  emailIds: Joi.array()
686
- .items(Joi.string().example('1278455344230334865'))
687
- .description('An optional list of emailId values, if the server supports unique email IDs')
688
- .label('EmailIdsArray')
691
+ .items(emailIdSchema)
692
+ .allow(null)
693
+ .description(
694
+ 'An optional list of emailId values, if the server supports unique email IDs (Gmail API and MS Graph API accounts only). Null when no messages matched the search (Gmail API)'
695
+ )
696
+ .label('MovedEmailIdsArray')
689
697
  }).label('MessagesMoveResponse'),
690
698
  failAction: 'log'
691
699
  }
@@ -709,15 +717,7 @@ async function init(args) {
709
717
  try {
710
718
  return await accountObject.deleteMessage(request.params.message, request.query.force);
711
719
  } catch (err) {
712
- request.logger.error({ msg: 'API request failed', err });
713
- if (Boom.isBoom(err)) {
714
- throw err;
715
- }
716
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
717
- if (err.code) {
718
- error.output.payload.code = err.code;
719
- }
720
- throw error;
720
+ handleError(request, err);
721
721
  }
722
722
  },
723
723
  options: {
@@ -725,7 +725,11 @@ async function init(args) {
725
725
  notes: 'Move message to Trash or delete it if already in Trash',
726
726
  tags: ['api', 'Message'],
727
727
 
728
- plugins: {},
728
+ plugins: {
729
+ 'hapi-swagger': {
730
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
731
+ }
732
+ },
729
733
 
730
734
  auth: {
731
735
  strategy: 'api-token',
@@ -757,10 +761,20 @@ async function init(args) {
757
761
  },
758
762
  response: {
759
763
  schema: Joi.object({
760
- deleted: Joi.boolean().example(false).description('Was the delete action executed'),
764
+ deleted: Joi.boolean()
765
+ .example(false)
766
+ .description(
767
+ 'Was the message deleted permanently. IMAP accounts return false when the message was moved to Trash, Gmail API and MS Graph API accounts return true also for moves to Trash'
768
+ ),
761
769
  moved: Joi.object({
762
- destination: Joi.string().required().example('Trash').description('Trash folder path').label('TrashPath'),
763
- message: Joi.string().required().example('AAAAAwAAAWg').description('Message ID in Trash').label('TrashMessageId')
770
+ destination: Joi.string()
771
+ .example('Trash')
772
+ .description('Trash folder path. Can be missing if the server did not provide the destination folder')
773
+ .label('TrashPath'),
774
+ message: Joi.string()
775
+ .example('AAAAAwAAAWg')
776
+ .description('Message ID in Trash. Can be missing if the server did not provide the new message ID')
777
+ .label('TrashMessageId')
764
778
  })
765
779
  .description('Present if message was moved to Trash')
766
780
  .label('MessageMovedToTrash')
@@ -787,15 +801,7 @@ async function init(args) {
787
801
  try {
788
802
  return await accountObject.deleteMessages(request.query.path, request.payload.search, request.query.force);
789
803
  } catch (err) {
790
- request.logger.error({ msg: 'API request failed', err });
791
- if (Boom.isBoom(err)) {
792
- throw err;
793
- }
794
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
795
- if (err.code) {
796
- error.output.payload.code = err.code;
797
- }
798
- throw error;
804
+ handleError(request, err);
799
805
  }
800
806
  },
801
807
  options: {
@@ -803,7 +809,11 @@ async function init(args) {
803
809
  notes: 'Move messages to Trash or delete these if already in Trash',
804
810
  tags: ['api', 'Multi Message Actions'],
805
811
 
806
- plugins: {},
812
+ plugins: {
813
+ 'hapi-swagger': {
814
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
815
+ }
816
+ },
807
817
 
808
818
  auth: {
809
819
  strategy: 'api-token',
@@ -829,7 +839,7 @@ async function init(args) {
829
839
  .truthy('Y', 'true', '1')
830
840
  .falsy('N', 'false', 0)
831
841
  .default(false)
832
- .description('Delete messages even if not in Trash')
842
+ .description('Delete messages even if not in Trash. Not supported for Gmail API accounts (messages are always moved to Trash)')
833
843
  .label('ForceDelete')
834
844
  }).label('MessagesDeleteQuery'),
835
845
 
@@ -840,23 +850,33 @@ async function init(args) {
840
850
 
841
851
  response: {
842
852
  schema: Joi.object({
843
- deleted: Joi.boolean().example(false).description('Was the delete action executed'),
853
+ deleted: Joi.boolean()
854
+ .example(false)
855
+ .description(
856
+ 'Was the delete action executed. IMAP accounts return false when messages were moved to Trash, Gmail API and MS Graph API accounts return true also for moves to Trash'
857
+ ),
844
858
  moved: Joi.object({
845
859
  destination: Joi.string().required().example('Trash').description('Trash folder path').label('TrashPath'),
846
860
 
847
- idMap: Joi.array()
848
- .items(Joi.array().length(2).items(Joi.string().max(256).required().description('Message ID')).label('IdMapTuple'))
849
- .example([['AAAAAQAACnA', 'AAAAAwAAAD4']])
850
- .description('An optional map of source and target ID values, if the server provided this info')
851
- .label('IdMapArray'),
861
+ idMap: idMapSchema,
852
862
 
853
863
  emailIds: Joi.array()
854
- .items(Joi.string().example('1278455344230334865'))
855
- .description('An optional list of emailId values, if the server supports unique email IDs')
856
- .label('EmailIdsArray')
864
+ .items(emailIdSchema)
865
+ .description(
866
+ 'An optional list of emailId values, if the server supports unique email IDs (Gmail API and MS Graph API accounts only)'
867
+ )
868
+ .label('TrashedEmailIdsArray')
857
869
  })
858
870
  .label('MessagesMovedToTrash')
859
- .description('Value is present if messages were moved to Trash')
871
+ .description('Value is present if messages were moved to Trash'),
872
+ deletedMessages: Joi.object({
873
+ emailIds: Joi.array()
874
+ .items(emailIdSchema)
875
+ .description('List of emailId values of the permanently deleted messages')
876
+ .label('DeletedEmailIdsArray')
877
+ })
878
+ .label('MessagesDeleted')
879
+ .description('Value is present if messages were deleted permanently (MS Graph API accounts only)')
860
880
  }).label('MessagesDeleteResponse'),
861
881
  failAction: 'log'
862
882
  }
@@ -881,15 +901,7 @@ async function init(args) {
881
901
  try {
882
902
  return await accountObject.listMessages(request.query);
883
903
  } catch (err) {
884
- request.logger.error({ msg: 'API request failed', err });
885
- if (Boom.isBoom(err)) {
886
- throw err;
887
- }
888
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
889
- if (err.code) {
890
- error.output.payload.code = err.code;
891
- }
892
- throw error;
904
+ handleError(request, err);
893
905
  }
894
906
  },
895
907
  options: {
@@ -897,6 +909,12 @@ async function init(args) {
897
909
  notes: 'Lists messages in a mailbox folder',
898
910
  tags: ['api', 'Message'],
899
911
 
912
+ plugins: {
913
+ 'hapi-swagger': {
914
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
915
+ }
916
+ },
917
+
900
918
  auth: {
901
919
  strategy: 'api-token',
902
920
  mode: 'required'
@@ -988,15 +1006,7 @@ async function init(args) {
988
1006
  try {
989
1007
  return await accountObject.searchMessages(Object.assign(request.query, request.payload));
990
1008
  } catch (err) {
991
- request.logger.error({ msg: 'API request failed', err });
992
- if (Boom.isBoom(err)) {
993
- throw err;
994
- }
995
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
996
- if (err.code) {
997
- error.output.payload.code = err.code;
998
- }
999
- throw error;
1009
+ handleError(request, err);
1000
1010
  }
1001
1011
  },
1002
1012
  options: {
@@ -1004,7 +1014,11 @@ async function init(args) {
1004
1014
  notes: 'Filter messages from a mailbox folder by search options. Search is performed against a specific folder and not for the entire account.',
1005
1015
  tags: ['api', 'Message'],
1006
1016
 
1007
- plugins: {},
1017
+ plugins: {
1018
+ 'hapi-swagger': {
1019
+ responses: errorResponses(400, 401, 403, 404, 422, 429, 500, 503)
1020
+ }
1021
+ },
1008
1022
 
1009
1023
  auth: {
1010
1024
  strategy: 'api-token',
@@ -1059,7 +1073,7 @@ async function init(args) {
1059
1073
  .truthy('Y', 'true', '1')
1060
1074
  .falsy('N', 'false', 0)
1061
1075
  .description(
1062
- 'MS Graph only. If enabled, uses the $search parameter for MS Graph search queries instead of $filter. This allows searching the "to", "cc", "bcc", "larger", "smaller", "body", "before", "sentBefore", "since", and the "sentSince" fields. Note that $search returns up to 1,000 results, does not indicate the total number of matching results or pages, and returns results sorted by relevance rather than date.'
1076
+ 'MS Graph only. If enabled, uses the $search parameter for MS Graph search queries instead of $filter. This allows searching the "to", "cc", "bcc", "larger", "smaller", "body", "before", "sentBefore", "since", and the "sentSince" fields. Note that $search returns up to 1,000 results, does not indicate the total number of matching results or pages, and returns results sorted by relevance rather than date. The "labels" filter is not available in this mode - leave this option disabled to filter by category.'
1063
1077
  )
1064
1078
  .label('useOutlookSearch')
1065
1079
  .optional(),
@@ -1068,7 +1082,7 @@ async function init(args) {
1068
1082
  exposeQuery: Joi.boolean()
1069
1083
  .truthy('Y', 'true', '1')
1070
1084
  .falsy('N', 'false', 0)
1071
- .description('If enabled then returns the ElasticSearch query for debugging as part of the response')
1085
+ .description('If enabled then includes the combined query (as the documentStoreQuery field) in the response for debugging')
1072
1086
  .label('exposeQuery')
1073
1087
  .when('documentStore', {
1074
1088
  is: true,
@@ -1102,7 +1116,11 @@ async function init(args) {
1102
1116
  header: {
1103
1117
  'Message-ID': '<12345@example.com>'
1104
1118
  },
1105
- gmailRaw: 'has:attachment in:unread'
1119
+ gmailRaw: 'has:attachment in:unread',
1120
+ labels: {
1121
+ has: ['Important'],
1122
+ not: ['Horizon']
1123
+ }
1106
1124
  }
1107
1125
  })
1108
1126
  },
@@ -1152,23 +1170,19 @@ async function init(args) {
1152
1170
  try {
1153
1171
  return await accountObject.searchMessages(Object.assign({ documentStore: true }, request.query, request.payload), { unified: true });
1154
1172
  } catch (err) {
1155
- request.logger.error({ msg: 'API request failed', err });
1156
- if (Boom.isBoom(err)) {
1157
- throw err;
1158
- }
1159
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
1160
- if (err.code) {
1161
- error.output.payload.code = err.code;
1162
- }
1163
- throw error;
1173
+ handleError(request, err);
1164
1174
  }
1165
1175
  },
1166
1176
  options: {
1167
1177
  description: 'Unified search for messages',
1168
1178
  notes: 'Filter messages from the Document Store for multiple accounts or paths. Document Store must be enabled for the unified search to work.',
1169
- tags: ['Deprecated endpoints (Document Store)'],
1179
+ tags: ['api', 'Deprecated endpoints (Document Store)'],
1170
1180
 
1171
- plugins: {},
1181
+ plugins: {
1182
+ 'hapi-swagger': {
1183
+ responses: errorResponses(400, 401, 403, 429, 500)
1184
+ }
1185
+ },
1172
1186
 
1173
1187
  auth: {
1174
1188
  strategy: 'api-token',
@@ -1196,7 +1210,7 @@ async function init(args) {
1196
1210
  exposeQuery: Joi.boolean()
1197
1211
  .truthy('Y', 'true', '1')
1198
1212
  .falsy('N', 'false', 0)
1199
- .description('If enabled then returns the ElasticSearch query for debugging as part of the response')
1213
+ .description('If enabled then includes the combined query (as the documentStoreQuery field) in the response for debugging')
1200
1214
  .label('exposeQuery')
1201
1215
  .optional()
1202
1216
  .meta({ swaggerHidden: true })
@@ -1219,7 +1233,34 @@ async function init(args) {
1219
1233
  },
1220
1234
 
1221
1235
  response: {
1222
- schema: messageListSchema,
1236
+ schema: Joi.object({
1237
+ total: Joi.number()
1238
+ .integer()
1239
+ .example(120)
1240
+ .description('Total number of matching messages (capped at 10000 by the Document Store)')
1241
+ .label('UnifiedTotalNumber'),
1242
+ page: Joi.number().integer().example(0).description('Current page number (zero-based)').label('UnifiedPageNumber'),
1243
+ pages: Joi.number().integer().example(24).description('Total number of pages available').label('UnifiedPagesNumber'),
1244
+ accounts: Joi.array()
1245
+ .items(Joi.string().example('example'))
1246
+ .description('Account filter used for the search, if provided')
1247
+ .label('UnifiedSearchAccountsEcho'),
1248
+ paths: Joi.array()
1249
+ .items(Joi.string().example('INBOX'))
1250
+ .description('Path filter used for the search, if provided')
1251
+ .label('UnifiedSearchPathsEcho'),
1252
+ messages: Joi.array()
1253
+ .items(
1254
+ messageEntrySchema
1255
+ .keys({
1256
+ account: accountIdSchema.description('Account ID this message belongs to')
1257
+ })
1258
+ .unknown()
1259
+ .label('UnifiedMessageListEntry')
1260
+ )
1261
+ .label('UnifiedPageMessages'),
1262
+ documentStoreQuery: documentStoreQuerySchema
1263
+ }).label('UnifiedSearchResponse'),
1223
1264
  failAction: 'log'
1224
1265
  }
1225
1266
  }
@@ -1243,15 +1284,7 @@ async function init(args) {
1243
1284
  try {
1244
1285
  return await accountObject.getText(request.params.text, request.query);
1245
1286
  } catch (err) {
1246
- request.logger.error({ msg: 'API request failed', err });
1247
- if (Boom.isBoom(err)) {
1248
- throw err;
1249
- }
1250
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
1251
- if (err.code) {
1252
- error.output.payload.code = err.code;
1253
- }
1254
- throw error;
1287
+ handleError(request, err);
1255
1288
  }
1256
1289
  },
1257
1290
  options: {
@@ -1259,6 +1292,12 @@ async function init(args) {
1259
1292
  notes: 'Retrieves message text',
1260
1293
  tags: ['api', 'Message'],
1261
1294
 
1295
+ plugins: {
1296
+ 'hapi-swagger': {
1297
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
1298
+ }
1299
+ },
1300
+
1262
1301
  auth: {
1263
1302
  strategy: 'api-token',
1264
1303
  mode: 'required'
@@ -1279,7 +1318,7 @@ async function init(args) {
1279
1318
  .min(0)
1280
1319
  .max(1024 * 1024 * 1024)
1281
1320
  .example(MAX_ATTACHMENT_SIZE)
1282
- .description('Max length of text content'),
1321
+ .description('Max length of text content. Ignored for Gmail API and MS Graph API accounts (full content is always returned)'),
1283
1322
  textType: Joi.string()
1284
1323
  .lowercase()
1285
1324
  .valid('html', 'plain', '*')
@@ -1305,7 +1344,9 @@ async function init(args) {
1305
1344
  schema: Joi.object({
1306
1345
  plain: Joi.string().example('Hello world').description('Plaintext content'),
1307
1346
  html: Joi.string().example('<p>Hello world</p>').description('HTML content'),
1308
- hasMore: Joi.boolean().example(false).description('Is the current text output capped or not')
1347
+ hasMore: Joi.boolean()
1348
+ .example(false)
1349
+ .description('Is the current text output capped or not. Always false for Gmail API and MS Graph API accounts')
1309
1350
  }).label('TextResponse'),
1310
1351
  failAction: 'log'
1311
1352
  }
@@ -1329,15 +1370,7 @@ async function init(args) {
1329
1370
  try {
1330
1371
  return await accountObject.getAttachment(request.params.attachment);
1331
1372
  } catch (err) {
1332
- request.logger.error({ msg: 'API request failed', err });
1333
- if (Boom.isBoom(err)) {
1334
- throw err;
1335
- }
1336
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
1337
- if (err.code) {
1338
- error.output.payload.code = err.code;
1339
- }
1340
- throw error;
1373
+ handleError(request, err);
1341
1374
  }
1342
1375
  },
1343
1376
  options: {
@@ -1353,7 +1386,8 @@ async function init(args) {
1353
1386
 
1354
1387
  plugins: {
1355
1388
  'hapi-swagger': {
1356
- produces: ['application/octet-stream']
1389
+ produces: ['application/octet-stream'],
1390
+ responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
1357
1391
  }
1358
1392
  },
1359
1393