emailengine-app 2.68.0 → 2.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.github/codeql/codeql-config.yml +16 -0
  2. package/.github/workflows/codeql.yml +102 -0
  3. package/.github/workflows/deploy.yml +8 -0
  4. package/.github/workflows/release.yaml +4 -0
  5. package/.github/workflows/test.yml +3 -0
  6. package/CHANGELOG.md +49 -0
  7. package/SECURITY.md +80 -0
  8. package/SECURITY.txt +27 -0
  9. package/config/default.toml +2 -0
  10. package/data/google-crawlers.json +13 -1
  11. package/lib/account.js +62 -25
  12. package/lib/api-routes/account-routes.js +493 -75
  13. package/lib/api-routes/blocklist-routes.js +337 -0
  14. package/lib/api-routes/delivery-test-routes.js +321 -0
  15. package/lib/api-routes/export-routes.js +1 -12
  16. package/lib/api-routes/gateway-routes.js +376 -0
  17. package/lib/api-routes/license-routes.js +142 -0
  18. package/lib/api-routes/mailbox-routes.js +318 -0
  19. package/lib/api-routes/message-routes.js +21 -129
  20. package/lib/api-routes/oauth2-app-routes.js +631 -0
  21. package/lib/api-routes/outbox-routes.js +173 -0
  22. package/lib/api-routes/pubsub-routes.js +98 -0
  23. package/lib/api-routes/route-helpers.js +45 -0
  24. package/lib/api-routes/settings-routes.js +331 -0
  25. package/lib/api-routes/stats-routes.js +77 -0
  26. package/lib/api-routes/submit-routes.js +472 -0
  27. package/lib/api-routes/template-routes.js +7 -55
  28. package/lib/api-routes/token-routes.js +297 -0
  29. package/lib/api-routes/webhook-route-routes.js +152 -0
  30. package/lib/email-client/gmail-client.js +14 -0
  31. package/lib/email-client/imap/mailbox.js +34 -11
  32. package/lib/email-client/imap/subconnection.js +20 -12
  33. package/lib/email-client/imap/sync-operations.js +130 -2
  34. package/lib/email-client/imap-client.js +116 -58
  35. package/lib/email-client/outlook-client.js +85 -13
  36. package/lib/export.js +60 -19
  37. package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
  38. package/lib/imapproxy/imap-core/lib/imap-command.js +7 -2
  39. package/lib/imapproxy/imap-core/lib/imap-connection.js +113 -23
  40. package/lib/imapproxy/imap-core/lib/imap-server.js +25 -1
  41. package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
  42. package/lib/imapproxy/imap-server.js +92 -29
  43. package/lib/message-port-stream.js +113 -16
  44. package/lib/reject-worker-calls.js +42 -0
  45. package/lib/routes-ui.js +37 -8778
  46. package/lib/schemas.js +26 -1
  47. package/lib/tools.js +73 -0
  48. package/lib/ui-routes/account-routes.js +40 -210
  49. package/lib/ui-routes/admin-config-routes.js +913 -487
  50. package/lib/ui-routes/admin-entities-routes.js +1 -0
  51. package/lib/ui-routes/auth-routes.js +1339 -0
  52. package/lib/ui-routes/dashboard-routes.js +188 -0
  53. package/lib/ui-routes/document-store-routes.js +800 -0
  54. package/lib/ui-routes/export-routes.js +217 -0
  55. package/lib/ui-routes/internals-routes.js +354 -0
  56. package/lib/ui-routes/network-config-routes.js +759 -0
  57. package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +371 -91
  58. package/lib/ui-routes/route-helpers.js +316 -0
  59. package/lib/ui-routes/smtp-test-routes.js +236 -0
  60. package/lib/ui-routes/unsubscribe-routes.js +234 -0
  61. package/lib/webhook-request.js +36 -0
  62. package/package.json +17 -17
  63. package/sbom.json +1 -1
  64. package/server.js +217 -19
  65. package/static/licenses.html +52 -182
  66. package/translations/messages.pot +131 -151
  67. package/views/dashboard.hbs +7 -26
  68. package/views/internals/index.hbs +15 -0
  69. package/views/tokens/index.hbs +9 -0
  70. package/workers/api.js +198 -4401
  71. package/workers/export.js +87 -54
  72. package/workers/imap.js +29 -13
  73. package/workers/submit.js +20 -11
  74. package/workers/webhooks.js +6 -20
@@ -0,0 +1,800 @@
1
+ 'use strict';
2
+
3
+ // Admin UI routes for the Document Store config pages (/admin/config/document-store*).
4
+ // Extracted verbatim from lib/routes-ui.js: Elasticsearch-backed document store settings,
5
+ // the chat/embeddings model, pre-processing scripts, field mappings, and the connection
6
+ // test. The Document Store (Elasticsearch indexing) is a deprecated feature.
7
+
8
+ const Joi = require('joi');
9
+ const assert = require('assert');
10
+ const Boom = require('@hapi/boom');
11
+ const { Client: ElasticSearch } = require('@elastic/elasticsearch');
12
+
13
+ const settings = require('../settings');
14
+ const { redis } = require('../db');
15
+ const { REDIS_PREFIX } = require('../consts');
16
+ const { failAction } = require('../tools');
17
+ const { settingsSchema } = require('../schemas');
18
+ const { defaultMappings } = require('../es');
19
+ const { getESClient } = require('../document-store');
20
+ const { getOpenAiModels, OPEN_AI_MODELS, getExampleDocumentsPayloads } = require('./route-helpers');
21
+
22
+ const FIELD_TYPES = [
23
+ {
24
+ type: 'keyword',
25
+ name: 'Keyword - for exact matches'
26
+ },
27
+ {
28
+ type: 'text',
29
+ name: 'Text - for fulltext search'
30
+ },
31
+ {
32
+ type: 'html',
33
+ name: 'HTML - a text field with HTML analyzer (does not index HTML tags)'
34
+ },
35
+ {
36
+ type: 'filename',
37
+ name: 'File name - a text field with filename analyzer (ngram)'
38
+ },
39
+ {
40
+ type: 'boolean',
41
+ name: 'Boolean'
42
+ },
43
+ {
44
+ type: 'date',
45
+ name: 'Date - date and date-time values'
46
+ },
47
+ {
48
+ type: 'long',
49
+ name: 'Number, long - from -2^63 to 2^63-1'
50
+ },
51
+ {
52
+ type: 'integer',
53
+ name: 'Number, integer - from -2^31 to 2^31-1'
54
+ },
55
+ {
56
+ type: 'short',
57
+ name: 'Number, short - from -32,768 to 32,767'
58
+ },
59
+ {
60
+ type: 'byte',
61
+ name: 'Number, short - from -128 to 127'
62
+ },
63
+ {
64
+ type: 'double',
65
+ name: 'Number, double - a double-precision 64-bit IEEE 754 floating point number'
66
+ }
67
+ ];
68
+
69
+ const defaultMappingsList = Object.keys(defaultMappings)
70
+ .map(key => {
71
+ let type = defaultMappings[key].type || (defaultMappings[key].properties ? 'object' : 'text');
72
+ if (defaultMappings[key].analyzer === 'htmlStripAnalyzer') {
73
+ type += ' (HTML)';
74
+ }
75
+ if (defaultMappings[key].analyzer === 'filenameIndex') {
76
+ type += ' (filename)';
77
+ }
78
+ return {
79
+ key,
80
+ type,
81
+ indexed: defaultMappings[key].index !== false
82
+ };
83
+ })
84
+ .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()));
85
+
86
+ const configDocumentStoreSchema = {
87
+ documentStoreEnabled: settingsSchema.documentStoreEnabled.default(false),
88
+ documentStoreUrl: settingsSchema.documentStoreUrl.default(''),
89
+ documentStoreIndex: settingsSchema.documentStoreIndex.default('emailengine'),
90
+ documentStoreAuthEnabled: settingsSchema.documentStoreAuthEnabled.default(false),
91
+ documentStoreUsername: settingsSchema.documentStoreUsername.default(''),
92
+ documentStorePassword: settingsSchema.documentStorePassword
93
+ };
94
+
95
+ function init(args) {
96
+ const { server } = args;
97
+
98
+ server.route({
99
+ method: 'GET',
100
+ path: '/admin/config/document-store',
101
+ async handler(request, h) {
102
+ let documentStoreEnabled = await settings.get('documentStoreEnabled');
103
+ let documentStoreUrl = await settings.get('documentStoreUrl');
104
+ let documentStoreIndex = (await settings.get('documentStoreIndex')) || 'emailengine';
105
+ let documentStoreGenerateEmbeddings = await settings.get('documentStoreGenerateEmbeddings');
106
+ let documentStoreAuthEnabled = await settings.get('documentStoreAuthEnabled');
107
+ let documentStoreUsername = await settings.get('documentStoreUsername');
108
+ let hasDocumentStorePassword = !!(await settings.get('documentStorePassword'));
109
+
110
+ return h.view(
111
+ 'config/document-store/index',
112
+ {
113
+ menuConfig: true,
114
+ menuConfigDocumentStore: true,
115
+ documentStoreSettings: true,
116
+
117
+ values: {
118
+ documentStoreEnabled,
119
+ documentStoreUrl,
120
+ documentStoreIndex,
121
+ documentStoreAuthEnabled,
122
+ documentStoreUsername,
123
+ documentStoreGenerateEmbeddings
124
+ },
125
+
126
+ hasDocumentStorePassword,
127
+ hasOpenAiAPIKey: !!(await settings.get('openAiAPIKey'))
128
+ },
129
+ {
130
+ layout: 'app'
131
+ }
132
+ );
133
+ }
134
+ });
135
+
136
+ server.route({
137
+ method: 'POST',
138
+ path: '/admin/config/document-store',
139
+ async handler(request, h) {
140
+ try {
141
+ if (!request.payload.documentStoreUrl) {
142
+ request.payload.documentStoreEnabled = false;
143
+ }
144
+
145
+ if (!request.payload.documentStoreUsername) {
146
+ request.payload.documentStoreAuthEnabled = false;
147
+ // clear password as well if no username set
148
+ request.payload.documentStorePassword = '';
149
+ }
150
+
151
+ for (let key of Object.keys(request.payload)) {
152
+ await settings.set(key, request.payload[key]);
153
+ }
154
+
155
+ await request.flash({ type: 'info', message: `Configuration updated` });
156
+
157
+ return h.redirect('/admin/config/document-store');
158
+ } catch (err) {
159
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
160
+ request.logger.error({ msg: 'Failed to update configuration', err });
161
+
162
+ let hasDocumentStorePassword = !!(await settings.get('documentStorePassword'));
163
+
164
+ return h.view(
165
+ 'config/document-store/index',
166
+ {
167
+ menuConfig: true,
168
+ menuConfigDocumentStore: true,
169
+ documentStoreSettings: true,
170
+
171
+ hasDocumentStorePassword,
172
+ hasOpenAiAPIKey: !!(await settings.get('openAiAPIKey'))
173
+ },
174
+ {
175
+ layout: 'app'
176
+ }
177
+ );
178
+ }
179
+ },
180
+ options: {
181
+ validate: {
182
+ options: {
183
+ stripUnknown: true,
184
+ abortEarly: false,
185
+ convert: true
186
+ },
187
+
188
+ async failAction(request, h, err) {
189
+ let errors = {};
190
+
191
+ if (err.details) {
192
+ err.details.forEach(detail => {
193
+ if (!errors[detail.path]) {
194
+ errors[detail.path] = detail.message;
195
+ }
196
+ });
197
+ }
198
+
199
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
200
+ request.logger.error({ msg: 'Failed to update configuration', err });
201
+
202
+ let hasDocumentStorePassword = !!(await settings.get('documentStorePassword'));
203
+
204
+ return h
205
+ .view(
206
+ 'config/document-store/index',
207
+ {
208
+ menuConfig: true,
209
+ menuConfigDocumentStore: true,
210
+ documentStoreSettings: true,
211
+
212
+ hasDocumentStorePassword,
213
+ hasOpenAiAPIKey: !!(await settings.get('openAiAPIKey')),
214
+
215
+ errors
216
+ },
217
+ {
218
+ layout: 'app'
219
+ }
220
+ )
221
+ .takeover();
222
+ },
223
+
224
+ payload: Joi.object(configDocumentStoreSchema)
225
+ }
226
+ }
227
+ });
228
+
229
+ server.route({
230
+ method: 'GET',
231
+ path: '/admin/config/document-store/chat',
232
+ async handler(request, h) {
233
+ let documentStoreChatModel = await settings.get('documentStoreChatModel');
234
+
235
+ return h.view(
236
+ 'config/document-store/chat',
237
+ {
238
+ menuConfig: true,
239
+ menuConfigDocumentStore: true,
240
+ documentStoreChat: true,
241
+
242
+ documentStoreEnabled: await settings.get('documentStoreEnabled'),
243
+ hasOpenAiAPIKey: !!(await settings.get('openAiAPIKey')),
244
+ indexInfo: await settings.get('embeddings:index'),
245
+
246
+ openAiModels: await getOpenAiModels(OPEN_AI_MODELS, documentStoreChatModel),
247
+
248
+ values: {
249
+ documentStoreGenerateEmbeddings: (await settings.get(`documentStoreGenerateEmbeddings`)) || false
250
+ }
251
+ },
252
+ {
253
+ layout: 'app'
254
+ }
255
+ );
256
+ }
257
+ });
258
+
259
+ server.route({
260
+ method: 'POST',
261
+ path: '/admin/config/document-store/chat',
262
+ async handler(request, h) {
263
+ try {
264
+ for (let key of Object.keys(request.payload)) {
265
+ await settings.set(key, request.payload[key]);
266
+ }
267
+
268
+ await request.flash({ type: 'info', message: `Configuration updated` });
269
+
270
+ return h.redirect('/admin/config/document-store/chat');
271
+ } catch (err) {
272
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
273
+ request.logger.error({ msg: 'Failed to update configuration', err });
274
+
275
+ return h.view(
276
+ 'config/document-store/index',
277
+ {
278
+ menuConfig: true,
279
+ menuConfigDocumentStore: true,
280
+ documentStoreChat: true,
281
+
282
+ documentStoreEnabled: await settings.get('documentStoreEnabled'),
283
+ hasOpenAiAPIKey: !!(await settings.get('openAiAPIKey')),
284
+ indexInfo: await settings.get('embeddings:index'),
285
+
286
+ openAiModels: await getOpenAiModels(OPEN_AI_MODELS, request.payload.documentStoreChatModel)
287
+ },
288
+ {
289
+ layout: 'app'
290
+ }
291
+ );
292
+ }
293
+ },
294
+ options: {
295
+ validate: {
296
+ options: {
297
+ stripUnknown: true,
298
+ abortEarly: false,
299
+ convert: true
300
+ },
301
+
302
+ async failAction(request, h, err) {
303
+ let errors = {};
304
+
305
+ if (err.details) {
306
+ err.details.forEach(detail => {
307
+ if (!errors[detail.path]) {
308
+ errors[detail.path] = detail.message;
309
+ }
310
+ });
311
+ }
312
+
313
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
314
+ request.logger.error({ msg: 'Failed to update configuration', err });
315
+
316
+ return h
317
+ .view(
318
+ 'config/document-store/index',
319
+ {
320
+ menuConfig: true,
321
+ menuConfigDocumentStore: true,
322
+ documentStoreChat: true,
323
+
324
+ documentStoreEnabled: await settings.get('documentStoreEnabled'),
325
+ hasOpenAiAPIKey: !!(await settings.get('openAiAPIKey')),
326
+ indexInfo: await settings.get('embeddings:index'),
327
+
328
+ openAiModels: await getOpenAiModels(OPEN_AI_MODELS, request.payload.documentStoreChatModel),
329
+
330
+ errors
331
+ },
332
+ {
333
+ layout: 'app'
334
+ }
335
+ )
336
+ .takeover();
337
+ },
338
+
339
+ payload: Joi.object({
340
+ documentStoreGenerateEmbeddings: settingsSchema.documentStoreGenerateEmbeddings.default(false),
341
+ openAiAPIKey: settingsSchema.openAiAPIKey.empty(''),
342
+ documentStoreChatModel: settingsSchema.documentStoreChatModel.empty('')
343
+ })
344
+ }
345
+ }
346
+ });
347
+
348
+ server.route({
349
+ method: 'GET',
350
+ path: '/admin/config/document-store/pre-processing',
351
+ async handler(request, h) {
352
+ return h.view(
353
+ 'config/document-store/pre-processing/index',
354
+ {
355
+ menuConfig: true,
356
+ menuConfigDocumentStore: true,
357
+ documentStorePreProcessing: true,
358
+
359
+ values: {
360
+ enabled: (await settings.get(`documentStorePreProcessingEnabled`)) || false,
361
+
362
+ contentFnJson: JSON.stringify(
363
+ (await settings.get(`documentStorePreProcessingFn`)) ||
364
+ `// Pass all emails
365
+ return true;`
366
+ ),
367
+ contentMapJson: JSON.stringify(
368
+ (await settings.get(`documentStorePreProcessingMap`)) ||
369
+ `// By default the output payload is returned unmodified.
370
+ return payload;`
371
+ )
372
+ },
373
+
374
+ examplePayloadsJson: JSON.stringify(await getExampleDocumentsPayloads()),
375
+ scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
376
+ },
377
+ {
378
+ layout: 'app'
379
+ }
380
+ );
381
+ }
382
+ });
383
+
384
+ server.route({
385
+ method: 'POST',
386
+ path: '/admin/config/document-store/pre-processing',
387
+ async handler(request, h) {
388
+ let contentFn, contentMap;
389
+ try {
390
+ if (request.payload.contentFnJson === '') {
391
+ contentFn = null;
392
+ } else {
393
+ contentFn = JSON.parse(request.payload.contentFnJson);
394
+ if (typeof contentFn !== 'string') {
395
+ throw new Error('Invalid Format');
396
+ }
397
+ }
398
+ } catch (err) {
399
+ err.details = {
400
+ contentFnJson: 'Invalid JSON'
401
+ };
402
+ throw err;
403
+ }
404
+
405
+ try {
406
+ if (request.payload.contentMapJson === '') {
407
+ contentMap = null;
408
+ } else {
409
+ contentMap = JSON.parse(request.payload.contentMapJson);
410
+ if (typeof contentMap !== 'string') {
411
+ throw new Error('Invalid Format');
412
+ }
413
+ }
414
+ } catch (err) {
415
+ err.details = {
416
+ contentMapJson: 'Invalid JSON'
417
+ };
418
+ throw err;
419
+ }
420
+
421
+ try {
422
+ await settings.setMulti({
423
+ documentStorePreProcessingEnabled: request.payload.enabled,
424
+ documentStorePreProcessingFn: contentFn,
425
+ documentStorePreProcessingMap: contentMap
426
+ });
427
+
428
+ await request.flash({ type: 'info', message: `Document Store rules saved` });
429
+ return h.redirect(`/admin/config/document-store/pre-processing`);
430
+ } catch (err) {
431
+ await request.flash({ type: 'danger', message: `Couldn't save Document Store rules. Try again.` });
432
+ request.logger.error({ msg: 'Failed to update Document Store pre-processing rules', err });
433
+
434
+ return h.view(
435
+ 'config/document-store/pre-processing/index',
436
+ {
437
+ menuConfig: true,
438
+ menuConfigDocumentStore: true,
439
+ documentStorePreProcessing: true,
440
+
441
+ errors: err.details,
442
+
443
+ examplePayloadsJson: JSON.stringify(await getExampleDocumentsPayloads()),
444
+ scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
445
+ },
446
+ {
447
+ layout: 'app'
448
+ }
449
+ );
450
+ }
451
+ },
452
+ options: {
453
+ validate: {
454
+ options: {
455
+ stripUnknown: true,
456
+ abortEarly: false,
457
+ convert: true
458
+ },
459
+
460
+ async failAction(request, h, err) {
461
+ let errors = {};
462
+
463
+ if (err.details) {
464
+ err.details.forEach(detail => {
465
+ if (!errors[detail.path]) {
466
+ errors[detail.path] = detail.message;
467
+ }
468
+ });
469
+ }
470
+
471
+ await request.flash({ type: 'danger', message: `Couldn't save Document Store rules. Try again.` });
472
+ request.logger.error({ msg: 'Failed to update Document Store pre-processing rules', err });
473
+
474
+ return h
475
+ .view(
476
+ 'config/document-store/pre-processing/index',
477
+ {
478
+ menuConfig: true,
479
+ menuConfigDocumentStore: true,
480
+ documentStorePreProcessing: true,
481
+
482
+ errors,
483
+
484
+ examplePayloadsJson: JSON.stringify(await getExampleDocumentsPayloads()),
485
+ scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
486
+ },
487
+ {
488
+ layout: 'app'
489
+ }
490
+ )
491
+ .takeover();
492
+ },
493
+
494
+ payload: Joi.object({
495
+ enabled: Joi.boolean()
496
+ .truthy('Y', 'true', '1', 'on')
497
+ .falsy('N', 'false', 0, '')
498
+ .default(false)
499
+ .example(false)
500
+ .description('Is the pre-processing enabled'),
501
+ contentFnJson: Joi.string()
502
+ .max(1024 * 1024)
503
+ .default('')
504
+ .allow('')
505
+ .trim()
506
+ .description('Filter function'),
507
+ contentMapJson: Joi.string()
508
+ .max(1024 * 1024)
509
+ .default('')
510
+ .allow('')
511
+ .trim()
512
+ .description('Map function')
513
+ })
514
+ }
515
+ }
516
+ });
517
+
518
+ server.route({
519
+ method: 'GET',
520
+ path: '/admin/config/document-store/mappings',
521
+ async handler(request, h) {
522
+ let customMappings = (await redis.hgetall(`${REDIS_PREFIX}mappings`)) || {};
523
+ const customMappingsList = Object.keys(customMappings)
524
+ .map(key => {
525
+ let value;
526
+ try {
527
+ value = JSON.parse(customMappings[key]);
528
+ } catch (err) {
529
+ return null;
530
+ }
531
+
532
+ let type = value.type || (value.properties ? 'object' : 'text');
533
+ if (value.analyzer === 'htmlStripAnalyzer') {
534
+ type += ' (HTML)';
535
+ }
536
+ if (value.analyzer === 'filenameIndex') {
537
+ type += ' (filename)';
538
+ }
539
+ return {
540
+ key,
541
+ type,
542
+ indexed: value.index !== false
543
+ };
544
+ })
545
+ .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()));
546
+ return h.view(
547
+ 'config/document-store/mappings/index',
548
+ {
549
+ menuConfig: true,
550
+ menuConfigDocumentStore: true,
551
+ documentStoreMappings: true,
552
+
553
+ defaultMappingsList,
554
+ customMappingsList
555
+ },
556
+ {
557
+ layout: 'app'
558
+ }
559
+ );
560
+ }
561
+ });
562
+
563
+ server.route({
564
+ method: 'GET',
565
+ path: '/admin/config/document-store/mappings/new',
566
+ async handler(request, h) {
567
+ return h.view(
568
+ 'config/document-store/mappings/new',
569
+ {
570
+ menuConfig: true,
571
+ menuConfigDocumentStore: true,
572
+ documentStoreMappings: true,
573
+
574
+ fieldTypes: FIELD_TYPES.map(entry => ({ type: entry.type, name: entry.name, selected: false })),
575
+
576
+ values: {
577
+ indexed: true
578
+ }
579
+ },
580
+ {
581
+ layout: 'app'
582
+ }
583
+ );
584
+ }
585
+ });
586
+
587
+ server.route({
588
+ method: 'POST',
589
+ path: '/admin/config/document-store/mappings/new',
590
+ async handler(request, h) {
591
+ try {
592
+ const { index, client } = await getESClient(request.logger);
593
+ if (!client) {
594
+ return;
595
+ }
596
+
597
+ let mappingEntry = {};
598
+ switch (request.payload.type) {
599
+ case 'html':
600
+ mappingEntry[request.payload.field] = {
601
+ type: 'text',
602
+ analyzer: 'htmlStripAnalyzer',
603
+ index: !!request.payload.indexed
604
+ };
605
+ break;
606
+ case 'filename':
607
+ mappingEntry[request.payload.field] = {
608
+ type: 'text',
609
+ analyzer: 'filenameIndex',
610
+ search_analyzer: 'filenameSearch',
611
+ index: !!request.payload.indexed
612
+ };
613
+ break;
614
+ default: {
615
+ mappingEntry[request.payload.field] = {
616
+ type: request.payload.type,
617
+ index: !!request.payload.indexed
618
+ };
619
+ }
620
+ }
621
+
622
+ try {
623
+ const updateRes = await client.indices.putMapping({ index, properties: mappingEntry });
624
+ assert(updateRes && updateRes.acknowledged);
625
+ } catch (err) {
626
+ if (err.meta && err.meta.body && err.meta.body.error && err.meta.body.error.reason) {
627
+ let error = Boom.boomify(new Error(err.meta.body.error.reason), { statusCode: err.meta.statusCode || 500 });
628
+ throw error;
629
+ }
630
+ throw err;
631
+ }
632
+
633
+ await redis.hset(`${REDIS_PREFIX}mappings`, request.payload.field, JSON.stringify(mappingEntry[request.payload.field]));
634
+
635
+ await request.flash({ type: 'info', message: `Mapping created` });
636
+ return h.redirect('/admin/config/document-store/mappings');
637
+ } catch (err) {
638
+ if (Boom.isBoom(err)) {
639
+ await request.flash({ type: 'danger', message: err.message });
640
+ } else {
641
+ await request.flash({ type: 'danger', message: err.responseText || `Couldn't create mapping. Try again.` });
642
+ }
643
+ request.logger.error({ msg: 'Failed to create mapping', err });
644
+
645
+ return h.view(
646
+ 'config/document-store/mappings/new',
647
+ {
648
+ menuConfig: true,
649
+ menuConfigDocumentStore: true,
650
+ documentStoreMappings: true,
651
+
652
+ fieldTypes: FIELD_TYPES.map(entry => ({ type: entry.type, name: entry.name, selected: request.payload.type === entry.type }))
653
+ },
654
+ {
655
+ layout: 'app'
656
+ }
657
+ );
658
+ }
659
+ },
660
+ options: {
661
+ validate: {
662
+ options: {
663
+ stripUnknown: true,
664
+ abortEarly: false,
665
+ convert: true
666
+ },
667
+
668
+ async failAction(request, h, err) {
669
+ let errors = {};
670
+
671
+ if (err.details) {
672
+ err.details.forEach(detail => {
673
+ if (!errors[detail.path]) {
674
+ errors[detail.path] = detail.message;
675
+ }
676
+ });
677
+ }
678
+
679
+ await request.flash({ type: 'danger', message: `Couldn't create mapping. Try again.` });
680
+ request.logger.error({ msg: 'Failed to create mapping', err });
681
+
682
+ return h
683
+ .view(
684
+ 'config/document-store/mappings/new',
685
+ {
686
+ menuConfig: true,
687
+ menuConfigDocumentStore: true,
688
+ documentStoreMappings: true,
689
+
690
+ fieldTypes: FIELD_TYPES.map(entry => ({ type: entry.type, name: entry.name, selected: request.payload.type === entry.type })),
691
+
692
+ errors
693
+ },
694
+ {
695
+ layout: 'app'
696
+ }
697
+ )
698
+ .takeover();
699
+ },
700
+ payload: Joi.object({
701
+ field: Joi.string()
702
+ .empty('')
703
+ .trim()
704
+ .lowercase()
705
+ .pattern(/^[-_+]|[\\/*?"<>| ,#:]/, { name: 'allowed elasticsearch field', invert: true })
706
+ .invalid(...Object.keys(defaultMappings))
707
+ .required()
708
+ .label('Field name'),
709
+ type: Joi.string()
710
+ .empty('')
711
+ .trim()
712
+ .valid(...FIELD_TYPES.map(entry => entry.type))
713
+ .default('text')
714
+ .label('Field type'),
715
+ indexed: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '')
716
+ })
717
+ }
718
+ }
719
+ });
720
+
721
+ server.route({
722
+ method: 'POST',
723
+ path: '/admin/config/document-store/test',
724
+ async handler(request) {
725
+ const { documentStoreUrl, documentStoreAuthEnabled, documentStoreUsername, documentStorePassword } = request.payload;
726
+
727
+ let clientConfig = {
728
+ node: { url: new URL(documentStoreUrl), tls: { rejectUnauthorized: false } },
729
+ auth:
730
+ documentStoreAuthEnabled && documentStoreUsername
731
+ ? {
732
+ username: documentStoreUsername,
733
+ password: documentStorePassword || (await settings.get('documentStorePassword'))
734
+ }
735
+ : false
736
+ };
737
+
738
+ const client = new ElasticSearch(clientConfig);
739
+
740
+ let start = Date.now();
741
+ let duration;
742
+ try {
743
+ let clusterInfo;
744
+
745
+ try {
746
+ clusterInfo = await client.info();
747
+ duration = Date.now() - start;
748
+ } catch (err) {
749
+ duration = Date.now() - start;
750
+ throw err;
751
+ }
752
+
753
+ if (!clusterInfo || !clusterInfo.name) {
754
+ let err = new Error(`Invalid response from server`);
755
+ throw err;
756
+ }
757
+
758
+ return {
759
+ success: true,
760
+ duration
761
+ };
762
+ } catch (err) {
763
+ request.logger.error({
764
+ msg: 'Failed posting request',
765
+ documentStoreUrl,
766
+ documentStoreAuthEnabled,
767
+ documentStoreUsername,
768
+ command: 'info',
769
+ err
770
+ });
771
+ return {
772
+ success: false,
773
+ duration,
774
+ error: err.message
775
+ };
776
+ }
777
+ },
778
+ options: {
779
+ tags: ['test'],
780
+ validate: {
781
+ options: {
782
+ stripUnknown: true,
783
+ abortEarly: false,
784
+ convert: true
785
+ },
786
+
787
+ failAction,
788
+
789
+ payload: Joi.object({
790
+ documentStoreUrl: settingsSchema.documentStoreUrl.required(),
791
+ documentStoreAuthEnabled: settingsSchema.documentStoreAuthEnabled.default(false),
792
+ documentStoreUsername: settingsSchema.documentStoreUsername.default(''),
793
+ documentStorePassword: settingsSchema.documentStorePassword
794
+ })
795
+ }
796
+ }
797
+ });
798
+ }
799
+
800
+ module.exports = init;