emailengine-app 2.68.1 → 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.
- package/.github/workflows/deploy.yml +2 -0
- package/.github/workflows/release.yaml +4 -0
- package/CHANGELOG.md +40 -0
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +7 -1
- package/lib/account.js +62 -25
- package/lib/api-routes/account-routes.js +493 -75
- package/lib/api-routes/blocklist-routes.js +337 -0
- package/lib/api-routes/delivery-test-routes.js +321 -0
- package/lib/api-routes/export-routes.js +1 -12
- package/lib/api-routes/gateway-routes.js +376 -0
- package/lib/api-routes/license-routes.js +142 -0
- package/lib/api-routes/mailbox-routes.js +318 -0
- package/lib/api-routes/message-routes.js +21 -129
- package/lib/api-routes/oauth2-app-routes.js +631 -0
- package/lib/api-routes/outbox-routes.js +173 -0
- package/lib/api-routes/pubsub-routes.js +98 -0
- package/lib/api-routes/route-helpers.js +45 -0
- package/lib/api-routes/settings-routes.js +331 -0
- package/lib/api-routes/stats-routes.js +77 -0
- package/lib/api-routes/submit-routes.js +472 -0
- package/lib/api-routes/template-routes.js +7 -55
- package/lib/api-routes/token-routes.js +297 -0
- package/lib/api-routes/webhook-route-routes.js +152 -0
- package/lib/email-client/gmail-client.js +14 -0
- package/lib/email-client/imap/mailbox.js +34 -11
- package/lib/email-client/imap/subconnection.js +20 -12
- package/lib/email-client/imap/sync-operations.js +130 -2
- package/lib/email-client/imap-client.js +116 -58
- package/lib/email-client/outlook-client.js +85 -13
- package/lib/export.js +60 -19
- package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
- package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
- package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -23
- package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- package/lib/message-port-stream.js +113 -16
- package/lib/reject-worker-calls.js +42 -0
- package/lib/routes-ui.js +37 -8778
- package/lib/schemas.js +26 -1
- package/lib/tools.js +68 -0
- package/lib/ui-routes/account-routes.js +40 -210
- package/lib/ui-routes/admin-config-routes.js +913 -487
- package/lib/ui-routes/admin-entities-routes.js +1 -0
- package/lib/ui-routes/auth-routes.js +1339 -0
- package/lib/ui-routes/dashboard-routes.js +188 -0
- package/lib/ui-routes/document-store-routes.js +800 -0
- package/lib/ui-routes/export-routes.js +217 -0
- package/lib/ui-routes/internals-routes.js +354 -0
- package/lib/ui-routes/network-config-routes.js +759 -0
- package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +371 -91
- package/lib/ui-routes/route-helpers.js +316 -0
- package/lib/ui-routes/smtp-test-routes.js +236 -0
- package/lib/ui-routes/unsubscribe-routes.js +234 -0
- package/lib/webhook-request.js +36 -0
- package/package.json +8 -8
- package/sbom.json +1 -1
- package/server.js +214 -16
- package/static/licenses.html +12 -12
- package/translations/messages.pot +129 -149
- package/views/dashboard.hbs +7 -26
- package/views/internals/index.hbs +15 -0
- package/views/tokens/index.hbs +9 -0
- package/workers/api.js +198 -4401
- package/workers/export.js +87 -54
- package/workers/imap.js +29 -13
- package/workers/submit.js +20 -11
- 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;
|