emailengine-app 2.68.1 → 2.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.github/workflows/deploy.yml +8 -3
  2. package/.github/workflows/release.yaml +6 -0
  3. package/CHANGELOG.md +59 -0
  4. package/Gruntfile.js +3 -1
  5. package/config/default.toml +2 -0
  6. package/data/google-crawlers.json +7 -1
  7. package/getswagger.sh +40 -4
  8. package/gettext-extract.js +163 -0
  9. package/lib/account.js +135 -72
  10. package/lib/api-routes/account-routes.js +684 -106
  11. package/lib/api-routes/blocklist-routes.js +344 -0
  12. package/lib/api-routes/chat-routes.js +32 -14
  13. package/lib/api-routes/delivery-test-routes.js +346 -0
  14. package/lib/api-routes/export-routes.js +28 -14
  15. package/lib/api-routes/gateway-routes.js +427 -0
  16. package/lib/api-routes/license-routes.js +156 -0
  17. package/lib/api-routes/mailbox-routes.js +344 -0
  18. package/lib/api-routes/message-routes.js +221 -187
  19. package/lib/api-routes/oauth2-app-routes.js +697 -0
  20. package/lib/api-routes/outbox-routes.js +185 -0
  21. package/lib/api-routes/pubsub-routes.js +102 -0
  22. package/lib/api-routes/route-helpers.js +58 -0
  23. package/lib/api-routes/settings-routes.js +357 -0
  24. package/lib/api-routes/stats-routes.js +111 -0
  25. package/lib/api-routes/submit-routes.js +461 -0
  26. package/lib/api-routes/template-routes.js +60 -75
  27. package/lib/api-routes/token-routes.js +297 -0
  28. package/lib/api-routes/webhook-route-routes.js +181 -0
  29. package/lib/autodetect-imap-settings.js +0 -2
  30. package/lib/consts.js +5 -0
  31. package/lib/email-client/base-client.js +28 -6
  32. package/lib/email-client/gmail-client.js +133 -112
  33. package/lib/email-client/imap/mailbox.js +34 -11
  34. package/lib/email-client/imap/subconnection.js +20 -13
  35. package/lib/email-client/imap/sync-operations.js +131 -3
  36. package/lib/email-client/imap-client.js +152 -75
  37. package/lib/email-client/notification-handler.js +1 -4
  38. package/lib/email-client/outlook-client.js +134 -75
  39. package/lib/export.js +97 -20
  40. package/lib/feature-flags.js +2 -2
  41. package/lib/gateway.js +4 -9
  42. package/lib/get-raw-email.js +5 -5
  43. package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
  44. package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
  45. package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -24
  46. package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
  47. package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
  48. package/lib/logger.js +24 -21
  49. package/lib/message-port-stream.js +113 -16
  50. package/lib/metrics-collector.js +0 -2
  51. package/lib/oauth2-apps.js +13 -4
  52. package/lib/outbox.js +24 -40
  53. package/lib/redis-operations.js +1 -1
  54. package/lib/reject-worker-calls.js +42 -0
  55. package/lib/routes-ui.js +37 -8778
  56. package/lib/schemas.js +429 -84
  57. package/lib/sentry.js +139 -0
  58. package/lib/settings.js +9 -3
  59. package/lib/stream-encrypt.js +1 -1
  60. package/lib/templates.js +1 -1
  61. package/lib/tokens.js +5 -3
  62. package/lib/tools.js +70 -4
  63. package/lib/ui-routes/account-routes.js +45 -212
  64. package/lib/ui-routes/admin-config-routes.js +928 -489
  65. package/lib/ui-routes/admin-entities-routes.js +1 -0
  66. package/lib/ui-routes/auth-routes.js +1339 -0
  67. package/lib/ui-routes/dashboard-routes.js +188 -0
  68. package/lib/ui-routes/document-store-routes.js +800 -0
  69. package/lib/ui-routes/export-routes.js +217 -0
  70. package/lib/ui-routes/internals-routes.js +354 -0
  71. package/lib/ui-routes/network-config-routes.js +759 -0
  72. package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +369 -91
  73. package/lib/ui-routes/route-helpers.js +314 -0
  74. package/lib/ui-routes/smtp-test-routes.js +236 -0
  75. package/lib/ui-routes/unsubscribe-routes.js +232 -0
  76. package/lib/webhook-request.js +36 -0
  77. package/lib/webhooks.js +8 -4
  78. package/package.json +13 -12
  79. package/sbom.json +1 -1
  80. package/server.js +222 -39
  81. package/static/licenses.html +160 -300
  82. package/translations/messages.pot +112 -132
  83. package/update-info.sh +19 -1
  84. package/views/config/logging.hbs +48 -0
  85. package/views/dashboard.hbs +7 -26
  86. package/views/internals/index.hbs +15 -0
  87. package/views/tokens/index.hbs +9 -0
  88. package/workers/api.js +200 -4424
  89. package/workers/documents.js +2 -22
  90. package/workers/export.js +103 -104
  91. package/workers/imap-proxy.js +3 -23
  92. package/workers/imap.js +32 -36
  93. package/workers/smtp.js +2 -22
  94. package/workers/submit.js +26 -35
  95. package/workers/webhooks.js +9 -43
@@ -0,0 +1,344 @@
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 { lists } = require('../lists');
9
+ const { failAction } = require('../tools');
10
+ const { handleError } = require('./route-helpers');
11
+ const { accountIdSchema, errorResponses } = require('../schemas');
12
+ const { REDIS_PREFIX } = require('../consts');
13
+
14
+ async function init(args) {
15
+ const { server, call, CORS_CONFIG } = args;
16
+
17
+ server.route({
18
+ method: 'GET',
19
+ path: '/v1/blocklists',
20
+
21
+ async handler(request) {
22
+ try {
23
+ return await lists.list(request.query.page, request.query.pageSize);
24
+ } catch (err) {
25
+ handleError(request, err);
26
+ }
27
+ },
28
+
29
+ options: {
30
+ description: 'List blocklists',
31
+ notes: 'List blocklists with blocked addresses',
32
+ tags: ['api', 'Blocklists'],
33
+
34
+ plugins: {
35
+ 'hapi-swagger': {
36
+ responses: errorResponses(400, 401, 403, 429, 500)
37
+ }
38
+ },
39
+
40
+ auth: {
41
+ strategy: 'api-token',
42
+ mode: 'required'
43
+ },
44
+ cors: CORS_CONFIG,
45
+
46
+ validate: {
47
+ options: {
48
+ stripUnknown: false,
49
+ abortEarly: false,
50
+ convert: true
51
+ },
52
+ failAction,
53
+
54
+ query: Joi.object({
55
+ page: Joi.number()
56
+ .integer()
57
+ .min(0)
58
+ .max(1024 * 1024)
59
+ .default(0)
60
+ .example(0)
61
+ .description('Page number (zero indexed, so use 0 for first page)')
62
+ .label('PageNumber'),
63
+ pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
64
+ }).label('PageListsRequest')
65
+ },
66
+
67
+ response: {
68
+ schema: Joi.object({
69
+ total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
70
+ page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
71
+ pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
72
+
73
+ blocklists: Joi.array()
74
+ .items(
75
+ Joi.object({
76
+ listId: Joi.string().max(256).required().example('example').description('List ID'),
77
+ count: Joi.number().integer().example(12).description('Count of blocked addresses in this list')
78
+ }).label('BlocklistsResponseItem')
79
+ )
80
+ .label('BlocklistsEntries')
81
+ }).label('BlocklistsResponse'),
82
+ failAction: 'log'
83
+ }
84
+ }
85
+ });
86
+
87
+ server.route({
88
+ method: 'GET',
89
+ path: '/v1/blocklist/{listId}',
90
+
91
+ async handler(request) {
92
+ try {
93
+ return await lists.listContent(request.params.listId, request.query.page, request.query.pageSize);
94
+ } catch (err) {
95
+ handleError(request, err);
96
+ }
97
+ },
98
+
99
+ options: {
100
+ description: 'List blocklist entries',
101
+ notes: 'List blocked addresses for a list',
102
+ tags: ['api', 'Blocklists'],
103
+
104
+ plugins: {
105
+ 'hapi-swagger': {
106
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
107
+ }
108
+ },
109
+
110
+ auth: {
111
+ strategy: 'api-token',
112
+ mode: 'required'
113
+ },
114
+ cors: CORS_CONFIG,
115
+
116
+ validate: {
117
+ options: {
118
+ stripUnknown: false,
119
+ abortEarly: false,
120
+ convert: true
121
+ },
122
+ failAction,
123
+
124
+ params: Joi.object({
125
+ listId: Joi.string()
126
+ .hostname()
127
+ .example('test-list')
128
+ .description('List ID. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.')
129
+ .label('ListID')
130
+ .required()
131
+ }).label('BlocklistListRequest'),
132
+
133
+ query: Joi.object({
134
+ page: Joi.number()
135
+ .integer()
136
+ .min(0)
137
+ .max(1024 * 1024)
138
+ .default(0)
139
+ .example(0)
140
+ .description('Page number (zero indexed, so use 0 for first page)')
141
+ .label('PageNumber'),
142
+ pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
143
+ }).label('PageListsRequest')
144
+ },
145
+
146
+ response: {
147
+ schema: Joi.object({
148
+ listId: Joi.string().max(256).required().example('example').description('List ID'),
149
+ total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
150
+ page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
151
+ pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
152
+ addresses: Joi.array()
153
+ .items(
154
+ Joi.object({
155
+ recipient: Joi.string().email().example('user@example.com').description('Listed email address').required(),
156
+ account: accountIdSchema.required(),
157
+ messageId: Joi.string().example('<test123@example.com>').description('Message ID'),
158
+ source: Joi.string().example('api').description('Which mechanism was used to add the entry'),
159
+ reason: Joi.string().example('block').description('Why this entry was added'),
160
+ remoteAddress: Joi.string()
161
+ .ip({
162
+ version: ['ipv4', 'ipv6'],
163
+ cidr: 'optional'
164
+ })
165
+ .description('Which IP address triggered the entry'),
166
+ userAgent: Joi.string().example('Mozilla/5.0 (Macintosh)').description('Which user agent triggered the entry'),
167
+ created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was added or updated').required()
168
+ }).label('BlocklistListResponseItem')
169
+ )
170
+ .label('BlocklistListEntries')
171
+ }).label('BlocklistListResponse'),
172
+ failAction: 'log'
173
+ }
174
+ }
175
+ });
176
+
177
+ server.route({
178
+ method: 'POST',
179
+ path: '/v1/blocklist/{listId}',
180
+ async handler(request) {
181
+ let accountObject = new Account({
182
+ redis,
183
+ account: request.payload.account,
184
+ call,
185
+ secret: await getSecret(),
186
+ timeout: request.headers['x-ee-timeout']
187
+ });
188
+
189
+ try {
190
+ // throws if account does not exist
191
+ await accountObject.loadAccountData();
192
+
193
+ let added = await redis.eeListAdd(
194
+ `${REDIS_PREFIX}lists:unsub:lists`,
195
+ `${REDIS_PREFIX}lists:unsub:entries:${request.params.listId}`,
196
+ request.params.listId,
197
+ request.payload.recipient.toLowerCase().trim(),
198
+ JSON.stringify({
199
+ recipient: request.payload.recipient,
200
+ account: request.payload.account,
201
+ source: 'api',
202
+ reason: request.payload.reason,
203
+ remoteAddress: request.app.ip,
204
+ userAgent: request.headers['user-agent'],
205
+ created: new Date().toISOString()
206
+ })
207
+ );
208
+
209
+ return {
210
+ success: true,
211
+ added: !!added
212
+ };
213
+ } catch (err) {
214
+ handleError(request, err);
215
+ }
216
+ },
217
+ options: {
218
+ description: 'Add to blocklist',
219
+ notes: 'Add an email address to a blocklist',
220
+ tags: ['api', 'Blocklists'],
221
+
222
+ plugins: {
223
+ 'hapi-swagger': {
224
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
225
+ }
226
+ },
227
+
228
+ auth: {
229
+ strategy: 'api-token',
230
+ mode: 'required'
231
+ },
232
+ cors: CORS_CONFIG,
233
+
234
+ validate: {
235
+ options: {
236
+ stripUnknown: false,
237
+ abortEarly: false,
238
+ convert: true
239
+ },
240
+ failAction,
241
+
242
+ params: Joi.object({
243
+ listId: Joi.string()
244
+ .hostname()
245
+ .example('test-list')
246
+ .description('List ID. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.')
247
+ .label('ListID')
248
+ .required()
249
+ }).label('BlocklistListRequest'),
250
+
251
+ payload: Joi.object({
252
+ account: accountIdSchema.required(),
253
+ recipient: Joi.string().empty('').email().example('user@example.com').description('Email address to add to the list').required(),
254
+ reason: Joi.string().empty('').default('block').description('Identifier for the blocking reason')
255
+ }).label('BlocklistListAddPayload')
256
+ },
257
+
258
+ response: {
259
+ schema: Joi.object({
260
+ success: Joi.boolean().example(true).description('Was the request successful').label('BlocklistListAddSuccess'),
261
+ added: Joi.boolean().example(true).description('Was the address added to the list')
262
+ }).label('BlocklistListAddResponse'),
263
+ failAction: 'log'
264
+ }
265
+ }
266
+ });
267
+
268
+ server.route({
269
+ method: 'DELETE',
270
+ path: '/v1/blocklist/{listId}',
271
+
272
+ async handler(request) {
273
+ try {
274
+ let exists = await redis.hexists(`${REDIS_PREFIX}lists:unsub:lists`, request.params.listId);
275
+ if (!exists) {
276
+ let message = 'Requested blocklist was not found';
277
+ let error = Boom.boomify(new Error(message), { statusCode: 404 });
278
+ throw error;
279
+ }
280
+
281
+ let deleted = await redis.eeListRemove(
282
+ `${REDIS_PREFIX}lists:unsub:lists`,
283
+ `${REDIS_PREFIX}lists:unsub:entries:${request.params.listId}`,
284
+ request.params.listId,
285
+ request.query.recipient.toLowerCase().trim()
286
+ );
287
+
288
+ return {
289
+ deleted: !!deleted
290
+ };
291
+ } catch (err) {
292
+ handleError(request, err);
293
+ }
294
+ },
295
+ options: {
296
+ description: 'Remove from blocklist',
297
+ notes: 'Delete a blocked email address from a list',
298
+ tags: ['api', 'Blocklists'],
299
+
300
+ plugins: {
301
+ 'hapi-swagger': {
302
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
303
+ }
304
+ },
305
+
306
+ auth: {
307
+ strategy: 'api-token',
308
+ mode: 'required'
309
+ },
310
+ cors: CORS_CONFIG,
311
+
312
+ validate: {
313
+ options: {
314
+ stripUnknown: false,
315
+ abortEarly: false,
316
+ convert: true
317
+ },
318
+ failAction,
319
+
320
+ params: Joi.object({
321
+ listId: Joi.string()
322
+ .hostname()
323
+ .example('test-list')
324
+ .description('List ID. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.')
325
+ .label('ListID')
326
+ .required()
327
+ }).label('BlocklistListRequest'),
328
+
329
+ query: Joi.object({
330
+ recipient: Joi.string().empty('').email().example('user@example.com').description('Email address to remove from the list').required()
331
+ }).label('RecipientQuery')
332
+ },
333
+
334
+ response: {
335
+ schema: Joi.object({
336
+ deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the address removed from the list')
337
+ }).label('DeleteBlocklistResponse'),
338
+ failAction: 'log'
339
+ }
340
+ }
341
+ });
342
+ }
343
+
344
+ module.exports = init;
@@ -11,7 +11,7 @@ const util = require('util');
11
11
 
12
12
  const LOG_VERBOSE = getBoolean(process.env.EE_OPENAPI_VERBOSE);
13
13
 
14
- const { accountIdSchema, addressSchema, messageSpecialUseSchema, fromAddressSchema } = require('../schemas');
14
+ const { accountIdSchema, messageSpecialUseSchema, responseAddressSchema, responseFromAddressSchema, errorResponses } = require('../schemas');
15
15
 
16
16
  function getDateValue(str) {
17
17
  try {
@@ -97,7 +97,7 @@ async function init(args) {
97
97
  startTime: sortingResponse?.start_time,
98
98
  endTime: sortingResponse?.end_time,
99
99
  model: sortingResponse?.model,
100
- tokens: sortingResponse.tokens
100
+ tokens: sortingResponse?.tokens
101
101
  });
102
102
 
103
103
  // Step 2. Embeddings for the request
@@ -195,7 +195,7 @@ async function init(args) {
195
195
  filter: vectorsFilter?.bool?.must
196
196
  });
197
197
  return {
198
- success: true,
198
+ success: false,
199
199
  answer: null,
200
200
  processPipeline
201
201
  };
@@ -281,7 +281,7 @@ async function init(args) {
281
281
  message: 'Retrieved the answer',
282
282
  messages: queryResponse?.messageId?.length || 0,
283
283
  model: queryResponse?.model,
284
- tokens: queryResponse.tokens
284
+ tokens: queryResponse?.tokens
285
285
  });
286
286
 
287
287
  if (queryResponse?.answer) {
@@ -360,7 +360,7 @@ async function init(args) {
360
360
  }
361
361
 
362
362
  return {
363
- success: !!(responseData.answer || responseData.message),
363
+ success: !!responseData.answer,
364
364
  ...responseData,
365
365
  processPipeline
366
366
  };
@@ -399,9 +399,13 @@ async function init(args) {
399
399
  options: {
400
400
  description: 'Chat with emails',
401
401
  notes: 'Use OpenAI API and embeddings stored in the Document Store to "chat" with account emails. Requires Document Store indexing and the "Chat with emails" feature to be enabled.',
402
- tags: ['Deprecated endpoints (Document Store)'],
402
+ tags: ['api', 'Deprecated endpoints (Document Store)'],
403
403
 
404
- plugins: {},
404
+ plugins: {
405
+ 'hapi-swagger': {
406
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
407
+ }
408
+ },
405
409
 
406
410
  auth: {
407
411
  strategy: 'api-token',
@@ -434,30 +438,44 @@ async function init(args) {
434
438
 
435
439
  response: {
436
440
  schema: Joi.object({
437
- success: Joi.boolean().example(true).description('Was the request successful').label('ReturnChatResponseSuccess'),
438
- answer: Joi.string().trim().example('Last tuesday').description('Chat response').label('ChatResponse').required(),
441
+ success: Joi.boolean()
442
+ .example(true)
443
+ .description('Whether the LLM produced an answer. Can be false on a 200 response when no answer was found')
444
+ .label('ReturnChatResponseSuccess'),
445
+ answer: Joi.string()
446
+ .trim()
447
+ .allow(null)
448
+ .example('Last tuesday')
449
+ .description('Chat response. Not present (or null) when no answer was produced')
450
+ .label('ChatResponse'),
439
451
  messages: Joi.array()
440
452
  .items(
441
453
  Joi.object({
442
454
  id: Joi.string().example('AAAAAgAACrI').description('Unique identifier for the message').label('ChatMessageId'),
443
455
  path: Joi.string().example('INBOX').description('Folder this message was found from').label('ChatMessagePath'),
444
456
  date: Joi.date().iso().example('2023-09-29T10:03:49.000Z').description('Date of the email'),
445
- from: fromAddressSchema,
457
+ from: responseFromAddressSchema,
446
458
 
447
459
  to: Joi.array()
448
- .items(addressSchema)
460
+ .items(responseAddressSchema)
449
461
  .single()
450
462
  .description('List of addresses')
451
463
  .example([{ address: 'recipient@example.com' }])
452
- .label('AddressList'),
453
- subject: Joi.string().allow('').example('What a wonderful message').description('Message subject'),
464
+ .label('ChatToAddressList'),
465
+ cc: Joi.array().items(responseAddressSchema).single().description('List of CC addresses').label('ChatCcAddressList'),
466
+ bcc: Joi.array().items(responseAddressSchema).single().description('List of BCC addresses').label('ChatBccAddressList'),
467
+ subject: Joi.string().example('What a wonderful message').description('Message subject'),
454
468
  messageSpecialUse: messageSpecialUseSchema
455
469
  })
456
470
  .description('Email that best matched the question')
457
471
  .label('ChatResponseMessage')
458
472
  )
459
473
  .description('Emails that best matched the question')
460
- .label('ChatResponseMessages')
474
+ .label('ChatResponseMessages'),
475
+ processPipeline: Joi.array()
476
+ .items(Joi.object().unknown().label('ChatProcessPipelineStep'))
477
+ .description('Diagnostic information about the chat processing steps, including timings and token usage')
478
+ .label('ChatProcessPipeline')
461
479
  }).label('ReturnChatResponse'),
462
480
  failAction: 'log'
463
481
  }