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,759 @@
1
+ 'use strict';
2
+
3
+ // Admin UI routes for network, SMTP server, IMAP proxy, and browser config pages
4
+ // (/admin/config/{network*,imap-proxy,smtp*,browser}). Extracted verbatim from
5
+ // lib/routes-ui.js. The network page also reloads/deletes the autodetected public
6
+ // interfaces; the SMTP/IMAP-proxy pages manage the built-in MSA and IMAP proxy servers
7
+ // (including on-demand TLS certificate provisioning).
8
+
9
+ const Joi = require('joi');
10
+ const os = require('os');
11
+ const { parentPort } = require('worker_threads');
12
+
13
+ const settings = require('../settings');
14
+ const { redis } = require('../db');
15
+ const { REDIS_PREFIX } = require('../consts');
16
+ const { failAction, getServiceHostname } = require('../tools');
17
+ const { ADDRESS_STRATEGIES, settingsSchema } = require('../schemas');
18
+ const { updatePublicInterfaces } = require('../utils/network');
19
+ const { getServerStatus, cachedTemplates } = require('./route-helpers');
20
+
21
+ const configSmtpSchema = {
22
+ smtpServerEnabled: settingsSchema.smtpServerEnabled.default(false),
23
+ smtpServerPassword: settingsSchema.smtpServerPassword.default(null),
24
+ smtpServerAuthEnabled: settingsSchema.smtpServerAuthEnabled.default(false),
25
+ smtpServerPort: settingsSchema.smtpServerPort,
26
+ smtpServerHost: settingsSchema.smtpServerHost.default('0.0.0.0'),
27
+ smtpServerProxy: settingsSchema.smtpServerProxy.default(false),
28
+ smtpServerTLSEnabled: settingsSchema.smtpServerTLSEnabled.default(false)
29
+ };
30
+
31
+ const configImapProxySchema = {
32
+ imapProxyServerEnabled: settingsSchema.imapProxyServerEnabled.default(false),
33
+ imapProxyServerPassword: settingsSchema.imapProxyServerPassword.default(null),
34
+ imapProxyServerPort: settingsSchema.imapProxyServerPort,
35
+ imapProxyServerHost: settingsSchema.imapProxyServerHost.default('0.0.0.0'),
36
+ imapProxyServerProxy: settingsSchema.imapProxyServerProxy.default(false),
37
+ imapProxyServerTLSEnabled: settingsSchema.imapProxyServerTLSEnabled.default(false)
38
+ };
39
+
40
+ async function listPublicInterfaces(selectedAddresses) {
41
+ let existingAddresses = Object.values(os.networkInterfaces())
42
+ .flatMap(entry => entry)
43
+ .map(entry => entry.address);
44
+
45
+ let entries = await redis.hgetall(`${REDIS_PREFIX}interfaces`);
46
+
47
+ let defaultInterfaces = {};
48
+
49
+ let addresses = Object.keys(entries)
50
+ .map(key => {
51
+ if (/^default:/.test(key)) {
52
+ let family = key.split(':').pop();
53
+ defaultInterfaces[family] = entries[key];
54
+ return false;
55
+ }
56
+
57
+ let entry = entries[key];
58
+ try {
59
+ return JSON.parse(entry);
60
+ } catch (err) {
61
+ return false;
62
+ }
63
+ })
64
+ .filter(entry => entry && entry.family === 'IPv4')
65
+ .map(entry => entry);
66
+
67
+ addresses.forEach(address => {
68
+ if (address.localAddress === defaultInterfaces[address.family]) {
69
+ address.defaultInterface = true;
70
+ }
71
+
72
+ if (selectedAddresses && selectedAddresses.includes(address.localAddress)) {
73
+ address.checked = true;
74
+ }
75
+
76
+ if (!existingAddresses.includes(address.localAddress)) {
77
+ address.notice = 'This address was not found from the current interface listing and will not be used for connections';
78
+ }
79
+ });
80
+
81
+ return addresses.sort((a, b) => {
82
+ if (a.family !== b.family) {
83
+ return a.family.localeCompare(b.family);
84
+ }
85
+ if (a.defaultInterface) {
86
+ return -1;
87
+ }
88
+ if (b.defaultInterface) {
89
+ return 1;
90
+ }
91
+ return (a.name || a.ip).localeCompare(b.name || b.ip);
92
+ });
93
+ }
94
+
95
+ function init(args) {
96
+ const { server, call } = args;
97
+
98
+ server.route({
99
+ method: 'GET',
100
+ path: '/admin/config/network',
101
+ async handler(request, h) {
102
+ let smtpStrategy = (await settings.get('smtpStrategy')) || 'default';
103
+ let imapStrategy = (await settings.get('imapStrategy')) || 'default';
104
+
105
+ let proxyEnabled = await settings.get('proxyEnabled');
106
+ let proxyUrl = await settings.get('proxyUrl');
107
+ let smtpEhloName = await settings.get('smtpEhloName');
108
+ let httpProxyEnabled = await settings.get('httpProxyEnabled');
109
+ let httpProxyUrl = await settings.get('httpProxyUrl');
110
+
111
+ let localAddresses = [].concat((await settings.get('localAddresses')) || []);
112
+
113
+ let smtpStrategies = ADDRESS_STRATEGIES.map(entry => Object.assign({ selected: smtpStrategy === entry.key }, entry));
114
+ let imapStrategies = ADDRESS_STRATEGIES.map(entry => Object.assign({ selected: imapStrategy === entry.key }, entry));
115
+
116
+ return h.view(
117
+ 'config/network',
118
+ {
119
+ pageTitle: 'Network',
120
+ menuConfig: true,
121
+ menuConfigNetwork: true,
122
+
123
+ smtpStrategies,
124
+ imapStrategies,
125
+
126
+ values: {
127
+ proxyEnabled,
128
+ proxyUrl,
129
+ smtpEhloName,
130
+ httpProxyEnabled,
131
+ httpProxyUrl
132
+ },
133
+
134
+ addresses: await listPublicInterfaces(localAddresses),
135
+ addressListTemplate: cachedTemplates.addressList,
136
+ defaultSmtpEhloName: await getServiceHostname()
137
+ },
138
+ {
139
+ layout: 'app'
140
+ }
141
+ );
142
+ }
143
+ });
144
+
145
+ server.route({
146
+ method: 'POST',
147
+ path: '/admin/config/network/reload',
148
+ async handler(request) {
149
+ try {
150
+ await updatePublicInterfaces(redis);
151
+
152
+ let localAddresses = [].concat((await settings.get('localAddresses')) || []);
153
+
154
+ return {
155
+ success: true,
156
+ addresses: await listPublicInterfaces(localAddresses)
157
+ };
158
+ } catch (err) {
159
+ request.logger.error({ msg: 'Failed loading public IP addresses', err });
160
+ return {
161
+ success: false,
162
+ error: err.message
163
+ };
164
+ }
165
+ },
166
+ options: {
167
+ validate: {
168
+ options: {
169
+ stripUnknown: true,
170
+ abortEarly: false,
171
+ convert: true
172
+ },
173
+
174
+ failAction
175
+ }
176
+ }
177
+ });
178
+
179
+ server.route({
180
+ method: 'POST',
181
+ path: '/admin/config/network',
182
+ async handler(request, h) {
183
+ try {
184
+ for (let key of [
185
+ 'smtpStrategy',
186
+ 'imapStrategy',
187
+ 'localAddresses',
188
+ 'proxyUrl',
189
+ 'smtpEhloName',
190
+ 'proxyEnabled',
191
+ 'httpProxyEnabled',
192
+ 'httpProxyUrl'
193
+ ]) {
194
+ await settings.set(key, request.payload[key]);
195
+ }
196
+
197
+ // Notify all workers (including this one) about the settings change; each reloads
198
+ // its HTTP proxy agent via the 'settings' message handler.
199
+ if (parentPort) {
200
+ parentPort.postMessage({ cmd: 'settings', data: request.payload });
201
+ }
202
+
203
+ await request.flash({ type: 'info', message: `Configuration updated` });
204
+
205
+ return h.redirect('/admin/config/network');
206
+ } catch (err) {
207
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
208
+ request.logger.error({ msg: 'Failed to update configuration', err });
209
+
210
+ let smtpStrategies = ADDRESS_STRATEGIES.map(entry => Object.assign({ selected: request.payload.smtpStrategy === entry.key }, entry));
211
+ let imapStrategies = ADDRESS_STRATEGIES.map(entry => Object.assign({ selected: request.payload.imapStrategy === entry.key }, entry));
212
+
213
+ return h.view(
214
+ 'config/network',
215
+ {
216
+ pageTitle: 'Network',
217
+ menuConfig: true,
218
+ menuConfigNetwork: true,
219
+ smtpStrategies,
220
+ imapStrategies,
221
+
222
+ addresses: await listPublicInterfaces(request.payload.localAddresses),
223
+ addressListTemplate: cachedTemplates.addressList,
224
+ defaultSmtpEhloName: await getServiceHostname()
225
+ },
226
+ {
227
+ layout: 'app'
228
+ }
229
+ );
230
+ }
231
+ },
232
+ options: {
233
+ validate: {
234
+ options: {
235
+ stripUnknown: true,
236
+ abortEarly: false,
237
+ convert: true
238
+ },
239
+
240
+ async failAction(request, h, err) {
241
+ let errors = {};
242
+
243
+ if (err.details) {
244
+ err.details.forEach(detail => {
245
+ if (!errors[detail.path]) {
246
+ errors[detail.path] = detail.message;
247
+ }
248
+ });
249
+ }
250
+
251
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
252
+ request.logger.error({ msg: 'Failed to update configuration', err });
253
+
254
+ let smtpStrategies = ADDRESS_STRATEGIES.map(entry => Object.assign({ selected: request.payload.smtpStrategy === entry.key }, entry));
255
+ let imapStrategies = ADDRESS_STRATEGIES.map(entry => Object.assign({ selected: request.payload.imapStrategy === entry.key }, entry));
256
+
257
+ return h
258
+ .view(
259
+ 'config/network',
260
+ {
261
+ pageTitle: 'Network',
262
+ menuConfig: true,
263
+ menuConfigNetwork: true,
264
+ smtpStrategies,
265
+ imapStrategies,
266
+
267
+ addresses: await listPublicInterfaces(request.payload.localAddresses),
268
+ addressListTemplate: cachedTemplates.addressList,
269
+ defaultSmtpEhloName: await getServiceHostname(),
270
+
271
+ errors
272
+ },
273
+ {
274
+ layout: 'app'
275
+ }
276
+ )
277
+ .takeover();
278
+ },
279
+ payload: Joi.object({
280
+ imapStrategy: settingsSchema.imapStrategy.default('default'),
281
+ smtpStrategy: settingsSchema.smtpStrategy.default('default'),
282
+ localAddresses: settingsSchema.localAddresses.default([]),
283
+
284
+ proxyUrl: settingsSchema.proxyUrl,
285
+ smtpEhloName: settingsSchema.smtpEhloName,
286
+ proxyEnabled: settingsSchema.proxyEnabled,
287
+
288
+ httpProxyEnabled: settingsSchema.httpProxyEnabled,
289
+ httpProxyUrl: settingsSchema.httpProxyUrl
290
+ })
291
+ }
292
+ }
293
+ });
294
+
295
+ server.route({
296
+ method: 'POST',
297
+ path: '/admin/config/network/delete',
298
+ async handler(request, h) {
299
+ try {
300
+ let localAddress = request.payload.localAddress;
301
+ let localAddresses = [].concat((await settings.get('localAddresses')) || []);
302
+ if (localAddresses.includes(localAddress)) {
303
+ let list = new Set(localAddresses);
304
+ list.delete(localAddress);
305
+ localAddresses = Array.from(list);
306
+ await settings.set('localAddresses', localAddresses);
307
+ }
308
+
309
+ await redis.hdel(`${REDIS_PREFIX}interfaces`, localAddress);
310
+
311
+ await request.flash({ type: 'info', message: `Address removed` });
312
+ return h.redirect('/admin/config/network');
313
+ } catch (err) {
314
+ await request.flash({ type: 'danger', message: `Couldn't delete address. Try again.` });
315
+ request.logger.error({ msg: 'Failed to delete address', err, localAddress: request.payload.localAddress, remoteAddress: request.app.ip });
316
+ return h.redirect('/admin/config/network');
317
+ }
318
+ },
319
+ options: {
320
+ validate: {
321
+ options: {
322
+ stripUnknown: true,
323
+ abortEarly: false,
324
+ convert: true
325
+ },
326
+
327
+ async failAction(request, h, err) {
328
+ await request.flash({ type: 'danger', message: `Couldn't delete address. Try again.` });
329
+ request.logger.error({ msg: 'Failed to delete address', err });
330
+
331
+ return h.redirect('/admin/config/network').takeover();
332
+ },
333
+
334
+ payload: Joi.object({
335
+ localAddress: Joi.string().ip({
336
+ version: ['ipv4', 'ipv6'],
337
+ cidr: 'forbidden'
338
+ })
339
+ })
340
+ }
341
+ }
342
+ });
343
+
344
+ server.route({
345
+ method: 'GET',
346
+ path: '/admin/config/imap-proxy',
347
+ async handler(request, h) {
348
+ let values = {
349
+ imapProxyServerEnabled: await settings.get('imapProxyServerEnabled'),
350
+ imapProxyServerPassword: await settings.get('imapProxyServerPassword'),
351
+ imapProxyServerPort: await settings.get('imapProxyServerPort'),
352
+ imapProxyServerHost: await settings.get('imapProxyServerHost'),
353
+ imapProxyServerProxy: await settings.get('imapProxyServerProxy'),
354
+ imapProxyServerTLSEnabled: await settings.get('imapProxyServerTLSEnabled')
355
+ };
356
+
357
+ let availableAddresses = new Set(
358
+ Object.values(os.networkInterfaces())
359
+ .flatMap(entry => entry)
360
+ .map(entry => entry.address)
361
+ );
362
+ availableAddresses.add('0.0.0.0');
363
+
364
+ let hostname = await h.serviceDomain();
365
+ let certificateData = await h.getCertificate();
366
+
367
+ return h.view(
368
+ 'config/imap-proxy',
369
+ {
370
+ pageTitle: 'IMAP Proxy',
371
+ menuConfig: true,
372
+ menuConfigImapProxy: true,
373
+
374
+ values,
375
+
376
+ serverState: await getServerStatus('imapProxy'),
377
+ availableAddresses: Array.from(availableAddresses).join(','),
378
+
379
+ serviceDomain: hostname,
380
+ serviceUrl: await settings.get('serviceUrl'),
381
+ certificateData
382
+ },
383
+ {
384
+ layout: 'app'
385
+ }
386
+ );
387
+ }
388
+ });
389
+
390
+ server.route({
391
+ method: 'POST',
392
+ path: '/admin/config/imap-proxy',
393
+ async handler(request, h) {
394
+ try {
395
+ let existingSetup = {};
396
+ let hasServerChanges = false;
397
+
398
+ const systemKeys = ['imapProxyServerEnabled', 'imapProxyServerPort', 'imapProxyServerHost', 'imapProxyServerTLSEnabled'];
399
+ for (let key of systemKeys) {
400
+ existingSetup[key] = await settings.get(key);
401
+ }
402
+
403
+ for (let key of Object.keys(request.payload)) {
404
+ await settings.set(key, request.payload[key]);
405
+ if (systemKeys.includes(key) && request.payload[key] !== existingSetup[key]) {
406
+ hasServerChanges = true;
407
+ }
408
+ }
409
+
410
+ await request.flash({ type: 'info', message: `Configuration updated` });
411
+
412
+ if (hasServerChanges) {
413
+ // request server restart
414
+ try {
415
+ await call({ cmd: 'imapProxyReload' });
416
+ } catch (err) {
417
+ request.logger.error({ msg: 'Reload request failed', action: 'request_reload_imap_proxy', err });
418
+ }
419
+ }
420
+
421
+ return h.redirect('/admin/config/imap-proxy');
422
+ } catch (err) {
423
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
424
+ request.logger.error({ msg: 'Failed to update configuration', err });
425
+
426
+ let availableAddresses = new Set(
427
+ Object.values(os.networkInterfaces())
428
+ .flatMap(entry => entry)
429
+ .map(entry => entry.address)
430
+ );
431
+ availableAddresses.add('0.0.0.0');
432
+
433
+ let hostname = await h.serviceDomain();
434
+ let certificateData = await h.getCertificate();
435
+
436
+ return h.view(
437
+ 'config/imap-proxy',
438
+ {
439
+ pageTitle: 'IMAP Proxy',
440
+ menuConfig: true,
441
+ menuConfigImapProxy: true,
442
+
443
+ serverState: await getServerStatus('imap'),
444
+ availableAddresses: Array.from(availableAddresses).join(','),
445
+
446
+ serviceDomain: hostname,
447
+ serviceUrl: await settings.get('serviceUrl'),
448
+ certificateData
449
+ },
450
+ {
451
+ layout: 'app'
452
+ }
453
+ );
454
+ }
455
+ },
456
+ options: {
457
+ validate: {
458
+ options: {
459
+ stripUnknown: true,
460
+ abortEarly: false,
461
+ convert: true
462
+ },
463
+
464
+ async failAction(request, h, err) {
465
+ let errors = {};
466
+
467
+ if (err.details) {
468
+ err.details.forEach(detail => {
469
+ if (!errors[detail.path]) {
470
+ errors[detail.path] = detail.message;
471
+ }
472
+ });
473
+ }
474
+
475
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
476
+ request.logger.error({ msg: 'Failed to update configuration', err });
477
+
478
+ let availableAddresses = new Set(
479
+ Object.values(os.networkInterfaces())
480
+ .flatMap(entry => entry)
481
+ .map(entry => entry.address)
482
+ );
483
+ availableAddresses.add('0.0.0.0');
484
+
485
+ let hostname = await h.serviceDomain();
486
+ let certificateData = await h.getCertificate();
487
+
488
+ return h
489
+ .view(
490
+ 'config/imap-proxy',
491
+ {
492
+ pageTitle: 'IMAP Proxy',
493
+ menuConfig: true,
494
+ menuConfigImapProxy: true,
495
+
496
+ serverState: await getServerStatus('imapProxy'),
497
+ availableAddresses: Array.from(availableAddresses).join(','),
498
+
499
+ serviceDomain: hostname,
500
+ serviceUrl: await settings.get('serviceUrl'),
501
+ certificateData,
502
+
503
+ errors
504
+ },
505
+ {
506
+ layout: 'app'
507
+ }
508
+ )
509
+ .takeover();
510
+ },
511
+
512
+ payload: Joi.object(configImapProxySchema)
513
+ }
514
+ }
515
+ });
516
+
517
+ server.route({
518
+ method: 'GET',
519
+ path: '/admin/config/smtp',
520
+ async handler(request, h) {
521
+ let values = {
522
+ smtpServerEnabled: await settings.get('smtpServerEnabled'),
523
+ smtpServerPassword: await settings.get('smtpServerPassword'),
524
+ smtpServerAuthEnabled: await settings.get('smtpServerAuthEnabled'),
525
+ smtpServerPort: await settings.get('smtpServerPort'),
526
+ smtpServerHost: await settings.get('smtpServerHost'),
527
+ smtpServerProxy: await settings.get('smtpServerProxy'),
528
+ smtpServerTLSEnabled: await settings.get('smtpServerTLSEnabled')
529
+ };
530
+
531
+ let availableAddresses = new Set(
532
+ Object.values(os.networkInterfaces())
533
+ .flatMap(entry => entry)
534
+ .map(entry => entry.address)
535
+ );
536
+ availableAddresses.add('0.0.0.0');
537
+
538
+ let hostname = await h.serviceDomain();
539
+ let certificateData = await h.getCertificate();
540
+
541
+ return h.view(
542
+ 'config/smtp',
543
+ {
544
+ pageTitle: 'SMTP Interface',
545
+ menuConfig: true,
546
+ menuConfigSmtp: true,
547
+
548
+ values,
549
+
550
+ serverState: await getServerStatus('smtp'),
551
+ availableAddresses: Array.from(availableAddresses).join(','),
552
+
553
+ serviceDomain: hostname,
554
+ serviceUrl: await settings.get('serviceUrl'),
555
+ certificateData
556
+ },
557
+ {
558
+ layout: 'app'
559
+ }
560
+ );
561
+ }
562
+ });
563
+
564
+ server.route({
565
+ method: 'POST',
566
+ path: '/admin/config/smtp',
567
+ async handler(request, h) {
568
+ try {
569
+ let existingSetup = {};
570
+ let hasServerChanges = false;
571
+
572
+ const systemKeys = ['smtpServerEnabled', 'smtpServerPort', 'smtpServerHost', 'smtpServerTLSEnabled'];
573
+ for (let key of systemKeys) {
574
+ existingSetup[key] = await settings.get(key);
575
+ }
576
+
577
+ for (let key of Object.keys(request.payload)) {
578
+ await settings.set(key, request.payload[key]);
579
+ if (systemKeys.includes(key) && request.payload[key] !== existingSetup[key]) {
580
+ hasServerChanges = true;
581
+ }
582
+ }
583
+
584
+ await request.flash({ type: 'info', message: `Configuration updated` });
585
+
586
+ if (hasServerChanges) {
587
+ // request server restart
588
+ try {
589
+ await call({ cmd: 'smtpReload' });
590
+ } catch (err) {
591
+ request.logger.error({ msg: 'Reload request failed', action: 'request_reload_smtp', err });
592
+ }
593
+ }
594
+
595
+ return h.redirect('/admin/config/smtp');
596
+ } catch (err) {
597
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
598
+ request.logger.error({ msg: 'Failed to update configuration', err });
599
+
600
+ let availableAddresses = new Set(
601
+ Object.values(os.networkInterfaces())
602
+ .flatMap(entry => entry)
603
+ .map(entry => entry.address)
604
+ );
605
+ availableAddresses.add('0.0.0.0');
606
+
607
+ let hostname = await h.serviceDomain();
608
+ let certificateData = await h.getCertificate();
609
+
610
+ return h.view(
611
+ 'config/smtp',
612
+ {
613
+ pageTitle: 'SMTP Interface',
614
+ menuConfig: true,
615
+ menuConfigSmtp: true,
616
+
617
+ serverState: await getServerStatus('smtp'),
618
+ availableAddresses: Array.from(availableAddresses).join(','),
619
+
620
+ serviceDomain: hostname,
621
+ serviceUrl: await settings.get('serviceUrl'),
622
+ certificateData
623
+ },
624
+ {
625
+ layout: 'app'
626
+ }
627
+ );
628
+ }
629
+ },
630
+ options: {
631
+ validate: {
632
+ options: {
633
+ stripUnknown: true,
634
+ abortEarly: false,
635
+ convert: true
636
+ },
637
+
638
+ async failAction(request, h, err) {
639
+ let errors = {};
640
+
641
+ if (err.details) {
642
+ err.details.forEach(detail => {
643
+ if (!errors[detail.path]) {
644
+ errors[detail.path] = detail.message;
645
+ }
646
+ });
647
+ }
648
+
649
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
650
+ request.logger.error({ msg: 'Failed to update configuration', err });
651
+
652
+ let availableAddresses = new Set(
653
+ Object.values(os.networkInterfaces())
654
+ .flatMap(entry => entry)
655
+ .map(entry => entry.address)
656
+ );
657
+ availableAddresses.add('0.0.0.0');
658
+
659
+ let hostname = await h.serviceDomain();
660
+ let certificateData = await h.getCertificate();
661
+
662
+ return h
663
+ .view(
664
+ 'config/smtp',
665
+ {
666
+ pageTitle: 'SMTP Interface',
667
+ menuConfig: true,
668
+ menuConfigSmtp: true,
669
+
670
+ serverState: await getServerStatus('smtp'),
671
+ availableAddresses: Array.from(availableAddresses).join(','),
672
+
673
+ serviceDomain: hostname,
674
+ serviceUrl: await settings.get('serviceUrl'),
675
+ certificateData,
676
+
677
+ errors
678
+ },
679
+ {
680
+ layout: 'app'
681
+ }
682
+ )
683
+ .takeover();
684
+ },
685
+
686
+ payload: Joi.object(configSmtpSchema)
687
+ }
688
+ }
689
+ });
690
+
691
+ server.route({
692
+ method: 'POST',
693
+ path: '/admin/config/smtp/certificate',
694
+ async handler(request, h) {
695
+ try {
696
+ let certificateData = await h.getCertificate(true);
697
+ if (!certificateData) {
698
+ throw new Error(`Failed to provision a ceritifcate`);
699
+ }
700
+
701
+ return {
702
+ success: true,
703
+ domain: certificateData.domain,
704
+ fingerprint: certificateData.fingerprint,
705
+ altNames: certificateData.altNames,
706
+ validTo: certificateData.validTo && certificateData.validTo.toISOString(),
707
+ label: certificateData.label
708
+ };
709
+ } catch (err) {
710
+ request.logger.error({ msg: 'Failed to request syncing', err });
711
+ return {
712
+ success: false,
713
+ error: err.message
714
+ };
715
+ }
716
+ }
717
+ });
718
+
719
+ server.route({
720
+ method: 'POST',
721
+ path: '/admin/config/browser',
722
+ async handler(request) {
723
+ for (let key of ['serviceUrl', 'language', 'timezone']) {
724
+ if (request.payload[key]) {
725
+ let existingValue = await settings.get(key);
726
+ if (existingValue === null) {
727
+ await settings.set(key, request.payload[key]);
728
+ }
729
+ }
730
+ }
731
+ return { success: true };
732
+ },
733
+ options: {
734
+ validate: {
735
+ options: {
736
+ stripUnknown: true,
737
+ abortEarly: false,
738
+ convert: true
739
+ },
740
+
741
+ failAction,
742
+
743
+ payload: Joi.object({
744
+ serviceUrl: settingsSchema.serviceUrl.empty('').allow(false),
745
+
746
+ language: Joi.string()
747
+ .empty('')
748
+ .lowercase()
749
+ .regex(/^[a-z0-9]{1,5}([-_][a-z0-9]{1,15})?$/)
750
+ .allow(false),
751
+
752
+ timezone: Joi.string().empty('').allow(false).max(255)
753
+ })
754
+ }
755
+ }
756
+ });
757
+ }
758
+
759
+ module.exports = init;