emailengine-app 2.61.1 → 2.61.3

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 (136) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/data/google-crawlers.json +1 -1
  3. package/lib/account/account-state.js +248 -0
  4. package/lib/account.js +45 -193
  5. package/lib/api-routes/account-routes.js +1023 -0
  6. package/lib/api-routes/message-routes.js +1377 -0
  7. package/lib/consts.js +12 -2
  8. package/lib/email-client/base-client.js +282 -771
  9. package/lib/email-client/gmail/gmail-api.js +243 -0
  10. package/lib/email-client/gmail-client.js +145 -53
  11. package/lib/email-client/imap/mailbox.js +24 -698
  12. package/lib/email-client/imap/sync-operations.js +812 -0
  13. package/lib/email-client/imap-client.js +1 -1
  14. package/lib/email-client/message-builder.js +566 -0
  15. package/lib/email-client/notification-handler.js +314 -0
  16. package/lib/email-client/outlook/graph-api.js +326 -0
  17. package/lib/email-client/outlook-client.js +159 -113
  18. package/lib/email-client/smtp-pool-manager.js +196 -0
  19. package/lib/imapproxy/imap-server.js +3 -12
  20. package/lib/oauth/gmail.js +4 -4
  21. package/lib/oauth/mail-ru.js +30 -5
  22. package/lib/oauth/outlook.js +57 -3
  23. package/lib/oauth/pubsub/google.js +30 -11
  24. package/lib/oauth/scope-checker.js +202 -0
  25. package/lib/oauth2-apps.js +8 -4
  26. package/lib/redis-operations.js +484 -0
  27. package/lib/routes-ui.js +283 -2582
  28. package/lib/tools.js +4 -196
  29. package/lib/ui-routes/account-routes.js +1931 -0
  30. package/lib/ui-routes/admin-config-routes.js +1233 -0
  31. package/lib/ui-routes/admin-entities-routes.js +2367 -0
  32. package/lib/ui-routes/oauth-routes.js +992 -0
  33. package/lib/utils/network.js +237 -0
  34. package/package.json +10 -10
  35. package/sbom.json +1 -1
  36. package/static/js/app.js +5 -5
  37. package/static/licenses.html +79 -19
  38. package/translations/de.mo +0 -0
  39. package/translations/de.po +97 -86
  40. package/translations/en.mo +0 -0
  41. package/translations/en.po +80 -75
  42. package/translations/et.mo +0 -0
  43. package/translations/et.po +96 -86
  44. package/translations/fr.mo +0 -0
  45. package/translations/fr.po +97 -86
  46. package/translations/ja.mo +0 -0
  47. package/translations/ja.po +96 -86
  48. package/translations/messages.pot +105 -91
  49. package/translations/nl.mo +0 -0
  50. package/translations/nl.po +98 -86
  51. package/translations/pl.mo +0 -0
  52. package/translations/pl.po +96 -86
  53. package/views/account/security.hbs +4 -4
  54. package/views/accounts/account.hbs +13 -13
  55. package/views/accounts/register/imap-server.hbs +12 -12
  56. package/views/config/document-store/pre-processing/index.hbs +4 -2
  57. package/views/config/oauth/app.hbs +6 -7
  58. package/views/config/oauth/index.hbs +2 -2
  59. package/views/config/service.hbs +3 -4
  60. package/views/dashboard.hbs +5 -7
  61. package/views/error.hbs +22 -7
  62. package/views/gateways/gateway.hbs +2 -2
  63. package/views/partials/add_account_modal.hbs +7 -10
  64. package/views/partials/document_store_header.hbs +1 -1
  65. package/views/partials/editor_scope_info.hbs +0 -1
  66. package/views/partials/oauth_config_header.hbs +1 -1
  67. package/views/partials/side_menu.hbs +3 -3
  68. package/views/partials/webhook_form.hbs +2 -2
  69. package/views/templates/index.hbs +1 -1
  70. package/views/templates/template.hbs +8 -8
  71. package/views/tokens/index.hbs +6 -6
  72. package/views/tokens/new.hbs +1 -1
  73. package/views/webhooks/index.hbs +4 -4
  74. package/views/webhooks/webhook.hbs +7 -7
  75. package/workers/api.js +148 -2436
  76. package/workers/smtp.js +2 -1
  77. package/lib/imapproxy/imap-core/test/client.js +0 -46
  78. package/lib/imapproxy/imap-core/test/fixtures/append.eml +0 -1196
  79. package/lib/imapproxy/imap-core/test/fixtures/chunks.js +0 -44
  80. package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +0 -6
  81. package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +0 -599
  82. package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +0 -32
  83. package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +0 -6
  84. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +0 -599
  85. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +0 -2740
  86. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +0 -1411
  87. package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +0 -85
  88. package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +0 -582
  89. package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +0 -599
  90. package/lib/imapproxy/imap-core/test/fixtures/simple.eml +0 -42
  91. package/lib/imapproxy/imap-core/test/fixtures/simple.json +0 -164
  92. package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +0 -671
  93. package/lib/imapproxy/imap-core/test/imap-compiler-test.js +0 -272
  94. package/lib/imapproxy/imap-core/test/imap-indexer-test.js +0 -236
  95. package/lib/imapproxy/imap-core/test/imap-parser-test.js +0 -922
  96. package/lib/imapproxy/imap-core/test/memory-notifier.js +0 -129
  97. package/lib/imapproxy/imap-core/test/prepare.sh +0 -74
  98. package/lib/imapproxy/imap-core/test/protocol-test.js +0 -1756
  99. package/lib/imapproxy/imap-core/test/search-test.js +0 -1356
  100. package/lib/imapproxy/imap-core/test/test-client.js +0 -152
  101. package/lib/imapproxy/imap-core/test/test-server.js +0 -623
  102. package/lib/imapproxy/imap-core/test/tools-test.js +0 -22
  103. package/test/api-test.js +0 -899
  104. package/test/autoreply-test.js +0 -327
  105. package/test/bounce-test.js +0 -151
  106. package/test/complaint-test.js +0 -256
  107. package/test/fixtures/autoreply/LICENSE +0 -27
  108. package/test/fixtures/autoreply/rfc3834-01.eml +0 -23
  109. package/test/fixtures/autoreply/rfc3834-02.eml +0 -24
  110. package/test/fixtures/autoreply/rfc3834-03.eml +0 -26
  111. package/test/fixtures/autoreply/rfc3834-04.eml +0 -48
  112. package/test/fixtures/autoreply/rfc3834-05.eml +0 -19
  113. package/test/fixtures/autoreply/rfc3834-06.eml +0 -59
  114. package/test/fixtures/bounces/163.eml +0 -2521
  115. package/test/fixtures/bounces/fastmail.eml +0 -242
  116. package/test/fixtures/bounces/gmail.eml +0 -252
  117. package/test/fixtures/bounces/hotmail.eml +0 -655
  118. package/test/fixtures/bounces/mailru.eml +0 -121
  119. package/test/fixtures/bounces/outlook.eml +0 -1107
  120. package/test/fixtures/bounces/postfix.eml +0 -101
  121. package/test/fixtures/bounces/rambler.eml +0 -116
  122. package/test/fixtures/bounces/workmail.eml +0 -142
  123. package/test/fixtures/bounces/yahoo.eml +0 -139
  124. package/test/fixtures/bounces/zoho.eml +0 -83
  125. package/test/fixtures/bounces/zonemta.eml +0 -100
  126. package/test/fixtures/complaints/LICENSE +0 -27
  127. package/test/fixtures/complaints/amazonses.eml +0 -72
  128. package/test/fixtures/complaints/dmarc.eml +0 -59
  129. package/test/fixtures/complaints/hotmail.eml +0 -49
  130. package/test/fixtures/complaints/optout.eml +0 -40
  131. package/test/fixtures/complaints/standard-arf.eml +0 -68
  132. package/test/fixtures/complaints/yahoo.eml +0 -68
  133. package/test/oauth2-apps-test.js +0 -301
  134. package/test/sendonly-test.js +0 -160
  135. package/test/test-config.js +0 -34
  136. package/test/webhooks-server.js +0 -39
@@ -0,0 +1,1377 @@
1
+ 'use strict';
2
+
3
+ const { redis } = require('../db');
4
+ const { Account } = require('../account');
5
+ const getSecret = require('../get-secret');
6
+ const settings = require('../settings');
7
+ const Boom = require('@hapi/boom');
8
+ const Joi = require('joi');
9
+ const { failAction } = require('../tools');
10
+
11
+ const {
12
+ accountIdSchema,
13
+ messageDetailsSchema,
14
+ messageListSchema,
15
+ documentStoreSchema,
16
+ searchSchema,
17
+ messageUpdateSchema,
18
+ addressSchema,
19
+ fromAddressSchema,
20
+ messageReferenceSchema
21
+ } = require('../schemas');
22
+
23
+ const listMessageFolderPathDescription =
24
+ '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
+
26
+ async function init(args) {
27
+ const { server, call, CORS_CONFIG, MAX_ATTACHMENT_SIZE, MAX_BODY_SIZE, MAX_PAYLOAD_TIMEOUT } = args;
28
+
29
+ // GET /v1/account/{account}/message/{message}/source - Download raw message
30
+ server.route({
31
+ method: 'GET',
32
+ path: '/v1/account/{account}/message/{message}/source',
33
+
34
+ async handler(request, h) {
35
+ let accountObject = new Account({
36
+ redis,
37
+ account: request.params.account,
38
+ call,
39
+ secret: await getSecret(),
40
+ timeout: request.headers['x-ee-timeout']
41
+ });
42
+
43
+ try {
44
+ const response = await accountObject.getRawMessage(request.params.message);
45
+ return h.response(response);
46
+ } 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;
56
+ }
57
+ },
58
+ options: {
59
+ description: 'Download raw message',
60
+ notes: 'Fetches raw message as a stream',
61
+ tags: ['api', 'Message'],
62
+
63
+ auth: {
64
+ strategy: 'api-token',
65
+ mode: 'required'
66
+ },
67
+ cors: CORS_CONFIG,
68
+
69
+ plugins: {
70
+ 'hapi-swagger': {
71
+ produces: ['message/rfc822']
72
+ }
73
+ },
74
+
75
+ validate: {
76
+ options: {
77
+ stripUnknown: false,
78
+ abortEarly: false,
79
+ convert: true
80
+ },
81
+ failAction,
82
+
83
+ params: Joi.object({
84
+ account: accountIdSchema.required(),
85
+ message: Joi.string().base64({ paddingRequired: false, urlSafe: true }).max(256).example('AAAAAQAACnA').required().description('Message ID')
86
+ }).label('RawMessageRequest')
87
+ }
88
+ }
89
+ });
90
+
91
+ // GET /v1/account/{account}/message/{message} - Get message information
92
+ server.route({
93
+ method: 'GET',
94
+ path: '/v1/account/{account}/message/{message}',
95
+
96
+ async handler(request, h) {
97
+ let accountObject = new Account({
98
+ redis,
99
+ account: request.params.account,
100
+ call,
101
+ secret: await getSecret(),
102
+ esClient: await h.getESClient(request.logger),
103
+ timeout: request.headers['x-ee-timeout']
104
+ });
105
+
106
+ try {
107
+ return await accountObject.getMessage(request.params.message, request.query);
108
+ } 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;
118
+ }
119
+ },
120
+ options: {
121
+ description: 'Get message information',
122
+ notes: 'Returns details of a specific message. By default text content is not included, use textType value to force retrieving text',
123
+ tags: ['api', 'Message'],
124
+
125
+ auth: {
126
+ strategy: 'api-token',
127
+ mode: 'required'
128
+ },
129
+ cors: CORS_CONFIG,
130
+
131
+ validate: {
132
+ options: {
133
+ stripUnknown: false,
134
+ abortEarly: false,
135
+ convert: true
136
+ },
137
+ failAction,
138
+
139
+ query: Joi.object({
140
+ maxBytes: Joi.number()
141
+ .integer()
142
+ .min(0)
143
+ .max(1024 * 1024 * 1024)
144
+ .example(5 * 1025 * 1024)
145
+ .description('Max length of text content'),
146
+ textType: Joi.string()
147
+ .lowercase()
148
+ .valid('html', 'plain', '*')
149
+ .example('*')
150
+ .description('Which text content to return, use * for all. By default text content is not returned.'),
151
+
152
+ webSafeHtml: Joi.boolean()
153
+ .truthy('Y', 'true', '1')
154
+ .falsy('N', 'false', 0)
155
+ .default(false)
156
+ .description(
157
+ 'Shorthand option to fetch and preprocess HTML and inline images. Overrides `textType`, `preProcessHtml`, and `embedAttachedImages` options.'
158
+ )
159
+ .label('WebSafeHtml'),
160
+
161
+ embedAttachedImages: Joi.boolean()
162
+ .truthy('Y', 'true', '1')
163
+ .falsy('N', 'false', 0)
164
+ .default(false)
165
+ .description('If true, then fetches attached images and embeds these in the HTML as data URIs')
166
+ .label('EmbedImages'),
167
+
168
+ preProcessHtml: Joi.boolean()
169
+ .truthy('Y', 'true', '1')
170
+ .falsy('N', 'false', 0)
171
+ .default(false)
172
+ .description('If true, then pre-processes HTML for compatibility')
173
+ .label('PreProcess'),
174
+
175
+ markAsSeen: Joi.boolean()
176
+ .truthy('Y', 'true', '1')
177
+ .falsy('N', 'false', 0)
178
+ .default(false)
179
+ .description('If true, then marks unseen email as seen while returning the message')
180
+ .label('MarkAsSeen'),
181
+
182
+ documentStore: documentStoreSchema.default(false)
183
+ }),
184
+
185
+ params: Joi.object({
186
+ account: accountIdSchema.required(),
187
+ message: Joi.string().base64({ paddingRequired: false, urlSafe: true }).max(256).required().example('AAAAAQAACnA').description('Message ID')
188
+ })
189
+ },
190
+
191
+ response: {
192
+ schema: messageDetailsSchema,
193
+ failAction: 'log'
194
+ }
195
+ }
196
+ });
197
+
198
+ // POST /v1/account/{account}/message - Upload message
199
+ server.route({
200
+ method: 'POST',
201
+ path: '/v1/account/{account}/message',
202
+
203
+ async handler(request) {
204
+ let accountObject = new Account({
205
+ redis,
206
+ account: request.params.account,
207
+ call,
208
+ secret: await getSecret(),
209
+ timeout: request.headers['x-ee-timeout']
210
+ });
211
+
212
+ try {
213
+ return await accountObject.uploadMessage(request.payload);
214
+ } catch (err) {
215
+ request.logger.error({ msg: 'API request failed', err });
216
+ if (Boom.isBoom(err)) {
217
+ throw err;
218
+ }
219
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
220
+ if (err.code) {
221
+ error.output.payload.code = err.code;
222
+ }
223
+ throw error;
224
+ }
225
+ },
226
+ options: {
227
+ payload: {
228
+ maxBytes: MAX_BODY_SIZE,
229
+ timeout: MAX_PAYLOAD_TIMEOUT
230
+ },
231
+
232
+ description: 'Upload message',
233
+ notes: 'Upload a message structure, compile it into an EML file and store it into selected mailbox.',
234
+ tags: ['api', 'Message'],
235
+
236
+ plugins: {},
237
+
238
+ auth: {
239
+ strategy: 'api-token',
240
+ mode: 'required'
241
+ },
242
+ cors: CORS_CONFIG,
243
+
244
+ validate: {
245
+ options: {
246
+ stripUnknown: false,
247
+ abortEarly: false,
248
+ convert: true
249
+ },
250
+ failAction,
251
+
252
+ params: Joi.object({
253
+ account: accountIdSchema.required()
254
+ }),
255
+
256
+ payload: Joi.object({
257
+ path: Joi.string().required().example('INBOX').description('Target mailbox folder path'),
258
+
259
+ flags: Joi.array().items(Joi.string().max(128)).example(['\\Seen', '\\Draft']).default([]).description('Message flags').label('Flags'),
260
+ internalDate: Joi.date().iso().example('2021-07-08T07:06:34.336Z').description('Sets the internal date for this message'),
261
+
262
+ reference: messageReferenceSchema,
263
+
264
+ raw: Joi.string()
265
+ .base64()
266
+ .max(MAX_ATTACHMENT_SIZE)
267
+ .example('TUlNRS1WZXJzaW9uOiAxLjANClN1YmplY3Q6IGhlbGxvIHdvcmxkDQoNCkhlbGxvIQ0K')
268
+ .description(
269
+ '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.'
270
+ )
271
+ .label('RFC822Raw'),
272
+
273
+ from: fromAddressSchema,
274
+
275
+ to: Joi.array()
276
+ .items(addressSchema)
277
+ .single()
278
+ .description('List of addresses')
279
+ .example([{ address: 'recipient@example.com' }])
280
+ .label('AddressList'),
281
+
282
+ cc: Joi.array().items(addressSchema).single().description('List of addresses').label('AddressList'),
283
+
284
+ bcc: Joi.array().items(addressSchema).single().description('List of addresses').label('AddressList'),
285
+
286
+ subject: Joi.string()
287
+ .allow('')
288
+ .max(10 * 1024)
289
+ .example('What a wonderful message')
290
+ .description('Message subject'),
291
+
292
+ text: Joi.string().max(MAX_ATTACHMENT_SIZE).example('Hello from myself!').description('Message Text'),
293
+
294
+ html: Joi.string().max(MAX_ATTACHMENT_SIZE).example('<p>Hello from myself!</p>').description('Message HTML'),
295
+
296
+ attachments: Joi.array()
297
+ .items(
298
+ Joi.object({
299
+ filename: Joi.string().max(256).example('transparent.gif'),
300
+ content: Joi.string()
301
+ .base64()
302
+ .max(MAX_ATTACHMENT_SIZE)
303
+ .required()
304
+ .example('R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=')
305
+ .description('Base64 formatted attachment file')
306
+ .when('reference', {
307
+ is: Joi.exist().not(false, null),
308
+ then: Joi.forbidden(),
309
+ otherwise: Joi.required()
310
+ }),
311
+
312
+ contentType: Joi.string().lowercase().max(256).example('image/gif'),
313
+ contentDisposition: Joi.string().lowercase().valid('inline', 'attachment'),
314
+ cid: Joi.string().max(256).example('unique-image-id@localhost').description('Content-ID value for embedded images'),
315
+ encoding: Joi.string().valid('base64').default('base64'),
316
+
317
+ reference: Joi.string()
318
+ .base64({ paddingRequired: false, urlSafe: true })
319
+ .max(256)
320
+ .allow(false, null)
321
+ .example('AAAAAQAACnAcde')
322
+ .description(
323
+ '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.'
324
+ )
325
+ }).label('UploadAttachment')
326
+ )
327
+ .description('List of attachments')
328
+ .label('UploadAttachmentList'),
329
+
330
+ messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
331
+ headers: Joi.object().label('CustomHeaders').description('Custom Headers').unknown().example({
332
+ 'X-My-Custom-Header': 'Custom header value'
333
+ }),
334
+
335
+ locale: Joi.string().empty('').max(100).example('fr').description('Optional locale'),
336
+ tz: Joi.string().empty('').max(100).example('Europe/Tallinn').description('Optional timezone')
337
+ }).label('MessageUpload')
338
+ },
339
+
340
+ response: {
341
+ schema: Joi.object({
342
+ id: Joi.string()
343
+ .example('AAAAAgAACrI')
344
+ .description(
345
+ 'Unique identifier for the message. NB! This and other fields might not be present if server did not provide enough information'
346
+ )
347
+ .label('MessageAppendId'),
348
+ path: Joi.string().example('INBOX').description('Folder this message was uploaded to').label('MessageAppendPath'),
349
+ uid: Joi.number().integer().example(12345).description('UID of uploaded message'),
350
+ uidValidity: Joi.string().example('12345').description('UIDVALIDITY of the target folder. Numeric value cast as string.'),
351
+ seq: Joi.number().integer().example(12345).description('Sequence number of uploaded message'),
352
+
353
+ messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
354
+
355
+ reference: Joi.object({
356
+ message: Joi.string()
357
+ .base64({ paddingRequired: false, urlSafe: true })
358
+ .max(256)
359
+ .required()
360
+ .example('AAAAAQAACnA')
361
+ .description('Referenced message ID'),
362
+ success: Joi.boolean().example(true).description('Was the referenced message processed').label('ResponseReferenceSuccess'),
363
+ documentStore: documentStoreSchema.default(false),
364
+ error: Joi.string().example('Referenced message was not found').description('An error message if referenced message processing failed')
365
+ })
366
+ .description('Reference info if referencing was requested')
367
+ .label('ResponseReference')
368
+ }).label('MessageUploadResponse'),
369
+ failAction: 'log'
370
+ }
371
+ }
372
+ });
373
+
374
+ // PUT /v1/account/{account}/message/{message} - Update message
375
+ server.route({
376
+ method: 'PUT',
377
+ path: '/v1/account/{account}/message/{message}',
378
+
379
+ async handler(request) {
380
+ let accountObject = new Account({
381
+ redis,
382
+ account: request.params.account,
383
+ call,
384
+ secret: await getSecret(),
385
+ timeout: request.headers['x-ee-timeout']
386
+ });
387
+
388
+ try {
389
+ return await accountObject.updateMessage(request.params.message, request.payload);
390
+ } catch (err) {
391
+ request.logger.error({ msg: 'API request failed', err });
392
+ if (Boom.isBoom(err)) {
393
+ throw err;
394
+ }
395
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
396
+ if (err.code) {
397
+ error.output.payload.code = err.code;
398
+ }
399
+ throw error;
400
+ }
401
+ },
402
+ options: {
403
+ description: 'Update message',
404
+ notes: 'Update message information. Mainly this means changing message flag values',
405
+ tags: ['api', 'Message'],
406
+
407
+ plugins: {},
408
+
409
+ auth: {
410
+ strategy: 'api-token',
411
+ mode: 'required'
412
+ },
413
+ cors: CORS_CONFIG,
414
+
415
+ validate: {
416
+ options: {
417
+ stripUnknown: false,
418
+ abortEarly: false,
419
+ convert: true
420
+ },
421
+ failAction,
422
+
423
+ params: Joi.object({
424
+ account: accountIdSchema.required(),
425
+ message: Joi.string().max(256).required().example('AAAAAQAACnA').description('Message ID')
426
+ }),
427
+
428
+ payload: messageUpdateSchema
429
+ },
430
+ response: {
431
+ schema: Joi.object({
432
+ flags: Joi.object({
433
+ add: Joi.array().items(Joi.string()).example(['\\Seen', '\\Flagged']),
434
+ delete: Joi.array().items(Joi.string()).example(['\\Draft']),
435
+ set: Joi.array().items(Joi.string()).example(['\\Seen'])
436
+ }).label('FlagResponse'),
437
+ labels: Joi.object({
438
+ add: Joi.array().items(Joi.string()).example(['Label1', 'Label2']),
439
+ delete: Joi.array().items(Joi.string()).example(['Label3']),
440
+ set: Joi.array().items(Joi.string()).example(['Label1'])
441
+ }).label('FlagResponse')
442
+ }).label('MessageUpdateResponse'),
443
+ failAction: 'log'
444
+ }
445
+ }
446
+ });
447
+
448
+ // PUT /v1/account/{account}/messages - Update multiple messages
449
+ server.route({
450
+ method: 'PUT',
451
+ path: '/v1/account/{account}/messages',
452
+
453
+ async handler(request) {
454
+ let accountObject = new Account({
455
+ redis,
456
+ account: request.params.account,
457
+ call,
458
+ secret: await getSecret(),
459
+ timeout: request.headers['x-ee-timeout']
460
+ });
461
+
462
+ try {
463
+ return await accountObject.updateMessages(request.query.path, request.payload.search, request.payload.update);
464
+ } catch (err) {
465
+ request.logger.error({ msg: 'API request failed', err });
466
+ if (Boom.isBoom(err)) {
467
+ throw err;
468
+ }
469
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
470
+ if (err.code) {
471
+ error.output.payload.code = err.code;
472
+ }
473
+ throw error;
474
+ }
475
+ },
476
+ options: {
477
+ description: 'Update messages',
478
+ notes: 'Update message information for matching emails',
479
+ tags: ['api', 'Multi Message Actions'],
480
+
481
+ plugins: {},
482
+
483
+ auth: {
484
+ strategy: 'api-token',
485
+ mode: 'required'
486
+ },
487
+ cors: CORS_CONFIG,
488
+
489
+ validate: {
490
+ options: {
491
+ stripUnknown: false,
492
+ abortEarly: false,
493
+ convert: true
494
+ },
495
+ failAction,
496
+
497
+ params: Joi.object({
498
+ account: accountIdSchema.required()
499
+ }),
500
+
501
+ query: Joi.object({
502
+ path: Joi.string().empty('').required().example('INBOX').description(listMessageFolderPathDescription)
503
+ }).label('MessagesUpdateQuery'),
504
+
505
+ payload: Joi.object({
506
+ search: searchSchema,
507
+ update: messageUpdateSchema
508
+ }).label('MessagesUpdateRequest')
509
+ },
510
+ response: {
511
+ schema: Joi.object({
512
+ flags: Joi.object({
513
+ add: Joi.array().items(Joi.string()).example(['\\Seen', '\\Flagged']),
514
+ delete: Joi.array().items(Joi.string()).example(['\\Draft']),
515
+ set: Joi.array().items(Joi.string()).example(['\\Seen'])
516
+ }).label('FlagResponse'),
517
+ labels: Joi.object({
518
+ add: Joi.array().items(Joi.string()).example(['Label1', 'Label2']),
519
+ delete: Joi.array().items(Joi.string()).example(['Label3']),
520
+ set: Joi.array().items(Joi.string()).example(['Label1'])
521
+ }).label('FlagResponse')
522
+ }).label('MessageUpdateResponse'),
523
+ failAction: 'log'
524
+ }
525
+ }
526
+ });
527
+
528
+ // PUT /v1/account/{account}/message/{message}/move - Move a message
529
+ server.route({
530
+ method: 'PUT',
531
+ path: '/v1/account/{account}/message/{message}/move',
532
+
533
+ async handler(request) {
534
+ let accountObject = new Account({
535
+ redis,
536
+ account: request.params.account,
537
+ call,
538
+ secret: await getSecret(),
539
+ timeout: request.headers['x-ee-timeout']
540
+ });
541
+
542
+ try {
543
+ let sourceOption = null;
544
+ if (request.payload.source) {
545
+ sourceOption = { path: request.payload.source };
546
+ }
547
+ return await accountObject.moveMessage(request.params.message, { path: request.payload.path }, { source: sourceOption });
548
+ } catch (err) {
549
+ request.logger.error({ msg: 'API request failed', err });
550
+ if (Boom.isBoom(err)) {
551
+ throw err;
552
+ }
553
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
554
+ if (err.code) {
555
+ error.output.payload.code = err.code;
556
+ }
557
+ throw error;
558
+ }
559
+ },
560
+ options: {
561
+ description: 'Move a message to a specified folder',
562
+ notes: 'Moves a message to a target folder',
563
+ tags: ['api', 'Message'],
564
+
565
+ plugins: {},
566
+
567
+ auth: {
568
+ strategy: 'api-token',
569
+ mode: 'required'
570
+ },
571
+ cors: CORS_CONFIG,
572
+
573
+ validate: {
574
+ options: {
575
+ stripUnknown: false,
576
+ abortEarly: false,
577
+ convert: true
578
+ },
579
+ failAction,
580
+
581
+ params: Joi.object({
582
+ account: accountIdSchema.required(),
583
+ message: Joi.string().max(256).required().example('AAAAAQAACnA').description('Message ID')
584
+ }),
585
+
586
+ payload: Joi.object({
587
+ path: Joi.string().required().example('INBOX').description('Destination mailbox folder path'),
588
+ source: Joi.string()
589
+ .example('INBOX')
590
+ .description('Source mailbox folder path (Gmail API only). Needed to remove the label from the message.')
591
+ })
592
+ .example({ path: 'Target/Folder' })
593
+ .label('MessageMove')
594
+ },
595
+
596
+ response: {
597
+ schema: Joi.object({
598
+ path: Joi.string().required().example('INBOX').description('Destination mailbox folder path'),
599
+ id: Joi.string().max(256).example('AAAAAQAACnA').description('ID of the moved message. Only included if the server provides it.'),
600
+ uid: Joi.number()
601
+ .integer()
602
+ .example(12345)
603
+ .description('UID of the moved message, applies only to IMAP accounts. Only included if the server provides it.')
604
+ }).label('MessageMoveResponse'),
605
+ failAction: 'log'
606
+ }
607
+ }
608
+ });
609
+
610
+ // PUT /v1/account/{account}/messages/move - Move multiple messages
611
+ server.route({
612
+ method: 'PUT',
613
+ path: '/v1/account/{account}/messages/move',
614
+
615
+ async handler(request) {
616
+ let accountObject = new Account({
617
+ redis,
618
+ account: request.params.account,
619
+ call,
620
+ secret: await getSecret(),
621
+ timeout: request.headers['x-ee-timeout']
622
+ });
623
+
624
+ try {
625
+ return await accountObject.moveMessages(request.query.path, request.payload.search, { path: request.payload.path });
626
+ } catch (err) {
627
+ request.logger.error({ msg: 'API request failed', err });
628
+ if (Boom.isBoom(err)) {
629
+ throw err;
630
+ }
631
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
632
+ if (err.code) {
633
+ error.output.payload.code = err.code;
634
+ }
635
+ throw error;
636
+ }
637
+ },
638
+ options: {
639
+ description: 'Move messages',
640
+ notes: 'Move messages matching to a search query to another folder',
641
+ tags: ['api', 'Multi Message Actions'],
642
+
643
+ plugins: {},
644
+
645
+ auth: {
646
+ strategy: 'api-token',
647
+ mode: 'required'
648
+ },
649
+ cors: CORS_CONFIG,
650
+
651
+ validate: {
652
+ options: {
653
+ stripUnknown: false,
654
+ abortEarly: false,
655
+ convert: true
656
+ },
657
+ failAction,
658
+
659
+ params: Joi.object({
660
+ account: accountIdSchema.required()
661
+ }),
662
+
663
+ query: Joi.object({
664
+ path: Joi.string().empty('').required().example('INBOX').description(listMessageFolderPathDescription)
665
+ }).label('MessagesMoveQuery'),
666
+
667
+ payload: Joi.object({
668
+ search: searchSchema,
669
+ path: Joi.string().required().example('INBOX').description('Target mailbox folder path')
670
+ }).label('MessagesMoveRequest')
671
+ },
672
+
673
+ response: {
674
+ schema: Joi.object({
675
+ path: Joi.string().required().example('INBOX').description('Target mailbox folder path'),
676
+
677
+ idMap: Joi.array()
678
+ .items(Joi.array().length(2).items(Joi.string().max(256).required().description('Message ID')).label('IdMapTuple'))
679
+ .example([['AAAAAQAACnA', 'AAAAAwAAAD4']])
680
+ .description('An optional map of source and target ID values, if the server provided this info')
681
+ .label('IdMapArray'),
682
+
683
+ emailIds: Joi.array()
684
+ .items(Joi.string().example('1278455344230334865'))
685
+ .description('An optional list of emailId values, if the server supports unique email IDs')
686
+ .label('EmailIdsArray')
687
+ }).label('MessagesMoveResponse'),
688
+ failAction: 'log'
689
+ }
690
+ }
691
+ });
692
+
693
+ // DELETE /v1/account/{account}/message/{message} - Delete message
694
+ server.route({
695
+ method: 'DELETE',
696
+ path: '/v1/account/{account}/message/{message}',
697
+
698
+ async handler(request) {
699
+ let accountObject = new Account({
700
+ redis,
701
+ account: request.params.account,
702
+ call,
703
+ secret: await getSecret(),
704
+ timeout: request.headers['x-ee-timeout']
705
+ });
706
+
707
+ try {
708
+ return await accountObject.deleteMessage(request.params.message, request.query.force);
709
+ } catch (err) {
710
+ request.logger.error({ msg: 'API request failed', err });
711
+ if (Boom.isBoom(err)) {
712
+ throw err;
713
+ }
714
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
715
+ if (err.code) {
716
+ error.output.payload.code = err.code;
717
+ }
718
+ throw error;
719
+ }
720
+ },
721
+ options: {
722
+ description: 'Delete message',
723
+ notes: 'Move message to Trash or delete it if already in Trash',
724
+ tags: ['api', 'Message'],
725
+
726
+ plugins: {},
727
+
728
+ auth: {
729
+ strategy: 'api-token',
730
+ mode: 'required'
731
+ },
732
+ cors: CORS_CONFIG,
733
+
734
+ validate: {
735
+ options: {
736
+ stripUnknown: false,
737
+ abortEarly: false,
738
+ convert: true
739
+ },
740
+ failAction,
741
+
742
+ query: Joi.object({
743
+ force: Joi.boolean()
744
+ .truthy('Y', 'true', '1')
745
+ .falsy('N', 'false', 0)
746
+ .default(false)
747
+ .description('Delete message even if not in Trash. Not supported for Gmail API accounts.')
748
+ .label('ForceDelete')
749
+ }).label('MessageDeleteQuery'),
750
+
751
+ params: Joi.object({
752
+ account: accountIdSchema.required(),
753
+ message: Joi.string().max(256).required().example('AAAAAQAACnA').description('Message ID')
754
+ }).label('MessageDelete')
755
+ },
756
+ response: {
757
+ schema: Joi.object({
758
+ deleted: Joi.boolean().example(false).description('Was the delete action executed'),
759
+ moved: Joi.object({
760
+ destination: Joi.string().required().example('Trash').description('Trash folder path').label('TrashPath'),
761
+ message: Joi.string().required().example('AAAAAwAAAWg').description('Message ID in Trash').label('TrashMessageId')
762
+ }).description('Present if message was moved to Trash')
763
+ }).label('MessageDeleteResponse'),
764
+ failAction: 'log'
765
+ }
766
+ }
767
+ });
768
+
769
+ // PUT /v1/account/{account}/messages/delete - Delete multiple messages
770
+ server.route({
771
+ method: 'PUT',
772
+ path: '/v1/account/{account}/messages/delete',
773
+
774
+ async handler(request) {
775
+ let accountObject = new Account({
776
+ redis,
777
+ account: request.params.account,
778
+ call,
779
+ secret: await getSecret(),
780
+ timeout: request.headers['x-ee-timeout']
781
+ });
782
+
783
+ try {
784
+ return await accountObject.deleteMessages(request.query.path, request.payload.search, request.query.force);
785
+ } catch (err) {
786
+ request.logger.error({ msg: 'API request failed', err });
787
+ if (Boom.isBoom(err)) {
788
+ throw err;
789
+ }
790
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
791
+ if (err.code) {
792
+ error.output.payload.code = err.code;
793
+ }
794
+ throw error;
795
+ }
796
+ },
797
+ options: {
798
+ description: 'Delete messages',
799
+ notes: 'Move messages to Trash or delete these if already in Trash',
800
+ tags: ['api', 'Multi Message Actions'],
801
+
802
+ plugins: {},
803
+
804
+ auth: {
805
+ strategy: 'api-token',
806
+ mode: 'required'
807
+ },
808
+ cors: CORS_CONFIG,
809
+
810
+ validate: {
811
+ options: {
812
+ stripUnknown: false,
813
+ abortEarly: false,
814
+ convert: true
815
+ },
816
+ failAction,
817
+
818
+ params: Joi.object({
819
+ account: accountIdSchema.required()
820
+ }),
821
+
822
+ query: Joi.object({
823
+ path: Joi.string().empty('').required().example('INBOX').description(listMessageFolderPathDescription),
824
+ force: Joi.boolean()
825
+ .truthy('Y', 'true', '1')
826
+ .falsy('N', 'false', 0)
827
+ .default(false)
828
+ .description('Delete messages even if not in Trash')
829
+ .label('ForceDelete')
830
+ }).label('MessagesDeleteQuery'),
831
+
832
+ payload: Joi.object({
833
+ search: searchSchema
834
+ }).label('MessagesDeleteRequest')
835
+ },
836
+
837
+ response: {
838
+ schema: Joi.object({
839
+ deleted: Joi.boolean().example(false).description('Was the delete action executed'),
840
+ moved: Joi.object({
841
+ destination: Joi.string().required().example('Trash').description('Trash folder path').label('TrashPath'),
842
+
843
+ idMap: Joi.array()
844
+ .items(Joi.array().length(2).items(Joi.string().max(256).required().description('Message ID')).label('IdMapTuple'))
845
+ .example([['AAAAAQAACnA', 'AAAAAwAAAD4']])
846
+ .description('An optional map of source and target ID values, if the server provided this info')
847
+ .label('IdMapArray'),
848
+
849
+ emailIds: Joi.array()
850
+ .items(Joi.string().example('1278455344230334865'))
851
+ .description('An optional list of emailId values, if the server supports unique email IDs')
852
+ .label('EmailIdsArray')
853
+ })
854
+ .label('MessagesMovedToTrash')
855
+ .description('Value is present if messages were moved to Trash')
856
+ }).label('MessagesDeleteResponse'),
857
+ failAction: 'log'
858
+ }
859
+ }
860
+ });
861
+
862
+ // GET /v1/account/{account}/messages - List messages in a folder
863
+ server.route({
864
+ method: 'GET',
865
+ path: '/v1/account/{account}/messages',
866
+
867
+ async handler(request, h) {
868
+ let accountObject = new Account({
869
+ redis,
870
+ account: request.params.account,
871
+ call,
872
+ secret: await getSecret(),
873
+ esClient: await h.getESClient(request.logger),
874
+ timeout: request.headers['x-ee-timeout']
875
+ });
876
+
877
+ try {
878
+ return await accountObject.listMessages(request.query);
879
+ } catch (err) {
880
+ request.logger.error({ msg: 'API request failed', err });
881
+ if (Boom.isBoom(err)) {
882
+ throw err;
883
+ }
884
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
885
+ if (err.code) {
886
+ error.output.payload.code = err.code;
887
+ }
888
+ throw error;
889
+ }
890
+ },
891
+ options: {
892
+ description: 'List messages in a folder',
893
+ notes: 'Lists messages in a mailbox folder',
894
+ tags: ['api', 'Message'],
895
+
896
+ auth: {
897
+ strategy: 'api-token',
898
+ mode: 'required'
899
+ },
900
+ cors: CORS_CONFIG,
901
+
902
+ validate: {
903
+ options: {
904
+ stripUnknown: false,
905
+ abortEarly: false,
906
+ convert: true
907
+ },
908
+ failAction,
909
+
910
+ params: Joi.object({
911
+ account: accountIdSchema.required().label('AccountId')
912
+ }),
913
+
914
+ query: Joi.object({
915
+ path: Joi.string().required().example('INBOX').description(listMessageFolderPathDescription).label('SpecialPath'),
916
+
917
+ cursor: Joi.string()
918
+ .trim()
919
+ .empty('')
920
+ .max(1024 * 1024)
921
+ .example('imap_kcQIji3UobDDTxc')
922
+ .description('Paging cursor from `nextPageCursor` or `prevPageCursor` value')
923
+ .label('PageCursor'),
924
+ page: Joi.number()
925
+ .integer()
926
+ .min(0)
927
+ .max(1024 * 1024)
928
+ .default(0)
929
+ .example(0)
930
+ .description(
931
+ 'Page number (zero-indexed, so use 0 for the first page). Only supported for IMAP accounts. Deprecated; use the paging cursor instead. If the page cursor value is provided, then the page number value is ignored.'
932
+ )
933
+ .label('PageNumber'),
934
+
935
+ pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize'),
936
+ documentStore: documentStoreSchema.default(false)
937
+ }).label('MessageQuery')
938
+ },
939
+
940
+ response: {
941
+ schema: messageListSchema,
942
+ failAction: 'log'
943
+ }
944
+ }
945
+ });
946
+
947
+ // POST /v1/account/{account}/search - Search for messages
948
+ server.route({
949
+ method: 'POST',
950
+ path: '/v1/account/{account}/search',
951
+
952
+ async handler(request, h) {
953
+ let accountObject = new Account({
954
+ redis,
955
+ account: request.params.account,
956
+ call,
957
+ secret: await getSecret(),
958
+ esClient: await h.getESClient(request.logger),
959
+ timeout: request.headers['x-ee-timeout']
960
+ });
961
+
962
+ let extraValidationErrors = [];
963
+
964
+ if (request.query.documentStore) {
965
+ for (let key of ['seq', 'modseq']) {
966
+ if (request.payload.search && key in request.payload.search) {
967
+ extraValidationErrors.push({ message: 'Not available when using Document Store', context: { key } });
968
+ }
969
+ }
970
+ } else {
971
+ for (let key of ['documentQuery']) {
972
+ if (key in request.payload) {
973
+ extraValidationErrors.push({ message: 'Requires Document Store to be enabled', context: { key } });
974
+ }
975
+ }
976
+ }
977
+
978
+ if (extraValidationErrors.length) {
979
+ let error = new Error('Input validation failed');
980
+ error.details = extraValidationErrors;
981
+ return failAction(request, h, error);
982
+ }
983
+
984
+ try {
985
+ return await accountObject.searchMessages(Object.assign(request.query, request.payload));
986
+ } catch (err) {
987
+ request.logger.error({ msg: 'API request failed', err });
988
+ if (Boom.isBoom(err)) {
989
+ throw err;
990
+ }
991
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
992
+ if (err.code) {
993
+ error.output.payload.code = err.code;
994
+ }
995
+ throw error;
996
+ }
997
+ },
998
+ options: {
999
+ description: 'Search for messages',
1000
+ notes: 'Filter messages from a mailbox folder by search options. Search is performed against a specific folder and not for the entire account.',
1001
+ tags: ['api', 'Message'],
1002
+
1003
+ plugins: {},
1004
+
1005
+ auth: {
1006
+ strategy: 'api-token',
1007
+ mode: 'required'
1008
+ },
1009
+ cors: CORS_CONFIG,
1010
+
1011
+ validate: {
1012
+ options: {
1013
+ stripUnknown: false,
1014
+ abortEarly: false,
1015
+ convert: true
1016
+ },
1017
+ failAction,
1018
+
1019
+ params: Joi.object({
1020
+ account: accountIdSchema.required()
1021
+ }),
1022
+
1023
+ query: Joi.object({
1024
+ path: Joi.string()
1025
+ .when('documentStore', {
1026
+ is: true,
1027
+ then: Joi.optional(),
1028
+ otherwise: Joi.required()
1029
+ })
1030
+ .example('INBOX')
1031
+ .description(listMessageFolderPathDescription)
1032
+ .label('Path'),
1033
+
1034
+ cursor: Joi.string()
1035
+ .trim()
1036
+ .empty('')
1037
+ .max(1024 * 1024)
1038
+ .example('imap_kcQIji3UobDDTxc')
1039
+ .description('Paging cursor from `nextPageCursor` or `prevPageCursor` value')
1040
+ .label('PageCursor'),
1041
+ page: Joi.number()
1042
+ .integer()
1043
+ .min(0)
1044
+ .max(1024 * 1024)
1045
+ .default(0)
1046
+ .example(0)
1047
+ .description(
1048
+ 'Page number (zero-indexed, so use 0 for the first page). Only supported for IMAP accounts. Deprecated; use the paging cursor instead. If the page cursor value is provided, then the page number value is ignored.'
1049
+ )
1050
+ .label('PageNumber'),
1051
+
1052
+ pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page'),
1053
+
1054
+ useOutlookSearch: Joi.boolean()
1055
+ .truthy('Y', 'true', '1')
1056
+ .falsy('N', 'false', 0)
1057
+ .description(
1058
+ '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.'
1059
+ )
1060
+ .label('useOutlookSearch')
1061
+ .optional(),
1062
+
1063
+ documentStore: documentStoreSchema.default(false).meta({ swaggerHidden: true }),
1064
+ exposeQuery: Joi.boolean()
1065
+ .truthy('Y', 'true', '1')
1066
+ .falsy('N', 'false', 0)
1067
+ .description('If enabled then returns the ElasticSearch query for debugging as part of the response')
1068
+ .label('exposeQuery')
1069
+ .when('documentStore', {
1070
+ is: true,
1071
+ then: Joi.optional(),
1072
+ otherwise: Joi.forbidden()
1073
+ })
1074
+ .meta({ swaggerHidden: true })
1075
+ }),
1076
+
1077
+ payload: Joi.object({
1078
+ search: searchSchema,
1079
+ documentQuery: Joi.object()
1080
+ .min(1)
1081
+ .description('Document Store query. Only allowed with `documentStore`.')
1082
+ .label('DocumentQuery')
1083
+ .unknown()
1084
+ .meta({ swaggerHidden: true })
1085
+ })
1086
+ .label('SearchQuery')
1087
+ .example({
1088
+ search: {
1089
+ unseen: true,
1090
+ flagged: true,
1091
+ from: 'nyan.cat@example.com',
1092
+ body: 'Hello world',
1093
+ subject: 'Hello world',
1094
+ sentBefore: '2024-08-09',
1095
+ sentSince: '2022-08-09',
1096
+ emailId: '1278455344230334865',
1097
+ threadId: '1266894439832287888',
1098
+ header: {
1099
+ 'Message-ID': '<12345@example.com>'
1100
+ },
1101
+ gmailRaw: 'has:attachment in:unread'
1102
+ }
1103
+ })
1104
+ },
1105
+
1106
+ response: {
1107
+ schema: messageListSchema,
1108
+ failAction: 'log'
1109
+ }
1110
+ }
1111
+ });
1112
+
1113
+ // POST /v1/unified/search - Unified search for messages
1114
+ server.route({
1115
+ method: 'POST',
1116
+ path: '/v1/unified/search',
1117
+
1118
+ async handler(request, h) {
1119
+ let accountObject = new Account({
1120
+ redis,
1121
+ call,
1122
+ secret: await getSecret(),
1123
+ esClient: await h.getESClient(request.logger),
1124
+ timeout: request.headers['x-ee-timeout']
1125
+ });
1126
+
1127
+ let extraValidationErrors = [];
1128
+
1129
+ for (let key of ['seq', 'modseq']) {
1130
+ if (request.payload.search && key in request.payload.search) {
1131
+ extraValidationErrors.push({ message: 'Not available when using Document Store', context: { key } });
1132
+ }
1133
+ }
1134
+
1135
+ if (extraValidationErrors.length) {
1136
+ let error = new Error('Input validation failed');
1137
+ error.details = extraValidationErrors;
1138
+ return failAction(request, h, error);
1139
+ }
1140
+
1141
+ let documentStoreEnabled = await settings.get('documentStoreEnabled');
1142
+ if (!documentStoreEnabled) {
1143
+ let error = new Error('Document store not enabled');
1144
+ error.details = extraValidationErrors;
1145
+ return failAction(request, h, error);
1146
+ }
1147
+
1148
+ try {
1149
+ return await accountObject.searchMessages(Object.assign({ documentStore: true }, request.query, request.payload), { unified: true });
1150
+ } catch (err) {
1151
+ request.logger.error({ msg: 'API request failed', err });
1152
+ if (Boom.isBoom(err)) {
1153
+ throw err;
1154
+ }
1155
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
1156
+ if (err.code) {
1157
+ error.output.payload.code = err.code;
1158
+ }
1159
+ throw error;
1160
+ }
1161
+ },
1162
+ options: {
1163
+ description: 'Unified search for messages',
1164
+ notes: 'Filter messages from the Document Store for multiple accounts or paths. Document Store must be enabled for the unified search to work.',
1165
+ tags: ['Deprecated endpoints (Document Store)'],
1166
+
1167
+ plugins: {},
1168
+
1169
+ auth: {
1170
+ strategy: 'api-token',
1171
+ mode: 'required'
1172
+ },
1173
+ cors: CORS_CONFIG,
1174
+
1175
+ validate: {
1176
+ options: {
1177
+ stripUnknown: false,
1178
+ abortEarly: false,
1179
+ convert: true
1180
+ },
1181
+ failAction,
1182
+
1183
+ query: Joi.object({
1184
+ page: Joi.number()
1185
+ .integer()
1186
+ .min(0)
1187
+ .max(1024 * 1024)
1188
+ .default(0)
1189
+ .example(0)
1190
+ .description('Page number (zero indexed, so use 0 for first page)'),
1191
+ pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page'),
1192
+ exposeQuery: Joi.boolean()
1193
+ .truthy('Y', 'true', '1')
1194
+ .falsy('N', 'false', 0)
1195
+ .description('If enabled then returns the ElasticSearch query for debugging as part of the response')
1196
+ .label('exposeQuery')
1197
+ .optional()
1198
+ .meta({ swaggerHidden: true })
1199
+ }),
1200
+
1201
+ payload: Joi.object({
1202
+ accounts: Joi.array()
1203
+ .items(Joi.string().empty('').trim().max(256).example('example'))
1204
+ .single()
1205
+ .description('Optional list of account ID values')
1206
+ .label('UnifiedSearchAccounts'),
1207
+ paths: Joi.array()
1208
+ .items(Joi.string().optional().example('INBOX'))
1209
+ .single()
1210
+ .description('Optional list of mailbox folder paths or specialUse flags')
1211
+ .label('UnifiedSearchPaths'),
1212
+ search: searchSchema,
1213
+ documentQuery: Joi.object().min(1).description('Document Store query').label('DocumentQuery').unknown().meta({ swaggerHidden: true })
1214
+ }).label('UnifiedSearchQuery')
1215
+ },
1216
+
1217
+ response: {
1218
+ schema: messageListSchema,
1219
+ failAction: 'log'
1220
+ }
1221
+ }
1222
+ });
1223
+
1224
+ // GET /v1/account/{account}/text/{text} - Retrieve message text
1225
+ server.route({
1226
+ method: 'GET',
1227
+ path: '/v1/account/{account}/text/{text}',
1228
+
1229
+ async handler(request, h) {
1230
+ let accountObject = new Account({
1231
+ redis,
1232
+ account: request.params.account,
1233
+ call,
1234
+ secret: await getSecret(),
1235
+ esClient: await h.getESClient(request.logger),
1236
+ timeout: request.headers['x-ee-timeout']
1237
+ });
1238
+
1239
+ try {
1240
+ return await accountObject.getText(request.params.text, request.query);
1241
+ } catch (err) {
1242
+ request.logger.error({ msg: 'API request failed', err });
1243
+ if (Boom.isBoom(err)) {
1244
+ throw err;
1245
+ }
1246
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
1247
+ if (err.code) {
1248
+ error.output.payload.code = err.code;
1249
+ }
1250
+ throw error;
1251
+ }
1252
+ },
1253
+ options: {
1254
+ description: 'Retrieve message text',
1255
+ notes: 'Retrieves message text',
1256
+ tags: ['api', 'Message'],
1257
+
1258
+ auth: {
1259
+ strategy: 'api-token',
1260
+ mode: 'required'
1261
+ },
1262
+ cors: CORS_CONFIG,
1263
+
1264
+ validate: {
1265
+ options: {
1266
+ stripUnknown: false,
1267
+ abortEarly: false,
1268
+ convert: true
1269
+ },
1270
+ failAction,
1271
+
1272
+ query: Joi.object({
1273
+ maxBytes: Joi.number()
1274
+ .integer()
1275
+ .min(0)
1276
+ .max(1024 * 1024 * 1024)
1277
+ .example(MAX_ATTACHMENT_SIZE)
1278
+ .description('Max length of text content'),
1279
+ textType: Joi.string()
1280
+ .lowercase()
1281
+ .valid('html', 'plain', '*')
1282
+ .default('*')
1283
+ .example('*')
1284
+ .description('Which text content to return, use * for all. By default all contents are returned.'),
1285
+ documentStore: documentStoreSchema.default(false)
1286
+ }),
1287
+
1288
+ params: Joi.object({
1289
+ account: accountIdSchema.required(),
1290
+ text: Joi.string()
1291
+ .base64({ paddingRequired: false, urlSafe: true })
1292
+ .max(10 * 1024)
1293
+ .required()
1294
+ .example('AAAAAQAACnAcdfaaN')
1295
+ .description('Message text ID')
1296
+ }).label('Text')
1297
+ },
1298
+
1299
+ response: {
1300
+ schema: Joi.object({
1301
+ plain: Joi.string().example('Hello world').description('Plaintext content'),
1302
+ html: Joi.string().example('<p>Hello world</p>').description('HTML content'),
1303
+ hasMore: Joi.boolean().example(false).description('Is the current text output capped or not')
1304
+ }).label('TextResponse'),
1305
+ failAction: 'log'
1306
+ }
1307
+ }
1308
+ });
1309
+
1310
+ // GET /v1/account/{account}/attachment/{attachment} - Download attachment
1311
+ server.route({
1312
+ method: 'GET',
1313
+ path: '/v1/account/{account}/attachment/{attachment}',
1314
+
1315
+ async handler(request) {
1316
+ let accountObject = new Account({
1317
+ redis,
1318
+ account: request.params.account,
1319
+ call,
1320
+ secret: await getSecret(),
1321
+ timeout: request.headers['x-ee-timeout']
1322
+ });
1323
+
1324
+ try {
1325
+ return await accountObject.getAttachment(request.params.attachment);
1326
+ } catch (err) {
1327
+ request.logger.error({ msg: 'API request failed', err });
1328
+ if (Boom.isBoom(err)) {
1329
+ throw err;
1330
+ }
1331
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
1332
+ if (err.code) {
1333
+ error.output.payload.code = err.code;
1334
+ }
1335
+ throw error;
1336
+ }
1337
+ },
1338
+ options: {
1339
+ description: 'Download attachment',
1340
+ notes: 'Fetches attachment file as a binary stream',
1341
+ tags: ['api', 'Message'],
1342
+
1343
+ auth: {
1344
+ strategy: 'api-token',
1345
+ mode: 'required'
1346
+ },
1347
+ cors: CORS_CONFIG,
1348
+
1349
+ plugins: {
1350
+ 'hapi-swagger': {
1351
+ produces: ['application/octet-stream']
1352
+ }
1353
+ },
1354
+
1355
+ validate: {
1356
+ options: {
1357
+ stripUnknown: false,
1358
+ abortEarly: false,
1359
+ convert: true
1360
+ },
1361
+ failAction,
1362
+
1363
+ params: Joi.object({
1364
+ account: accountIdSchema.required(),
1365
+ attachment: Joi.string()
1366
+ .base64({ paddingRequired: false, urlSafe: true })
1367
+ .max(2 * 1024)
1368
+ .required()
1369
+ .example('AAAAAQAACnAcde')
1370
+ .description('Attachment ID')
1371
+ })
1372
+ }
1373
+ }
1374
+ });
1375
+ }
1376
+
1377
+ module.exports = init;