emailengine-app 2.61.1 → 2.61.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/data/google-crawlers.json +1 -1
- package/lib/account/account-state.js +248 -0
- package/lib/account.js +45 -193
- package/lib/api-routes/account-routes.js +1023 -0
- package/lib/api-routes/message-routes.js +1377 -0
- package/lib/consts.js +12 -2
- package/lib/email-client/base-client.js +282 -771
- package/lib/email-client/gmail/gmail-api.js +243 -0
- package/lib/email-client/gmail-client.js +145 -53
- package/lib/email-client/imap/mailbox.js +24 -698
- package/lib/email-client/imap/sync-operations.js +812 -0
- package/lib/email-client/imap-client.js +1 -1
- package/lib/email-client/message-builder.js +566 -0
- package/lib/email-client/notification-handler.js +314 -0
- package/lib/email-client/outlook/graph-api.js +326 -0
- package/lib/email-client/outlook-client.js +159 -113
- package/lib/email-client/smtp-pool-manager.js +196 -0
- package/lib/imapproxy/imap-server.js +3 -12
- package/lib/oauth/gmail.js +4 -4
- package/lib/oauth/mail-ru.js +30 -5
- package/lib/oauth/outlook.js +57 -3
- package/lib/oauth/pubsub/google.js +30 -11
- package/lib/oauth/scope-checker.js +202 -0
- package/lib/oauth2-apps.js +8 -4
- package/lib/redis-operations.js +484 -0
- package/lib/routes-ui.js +283 -2582
- package/lib/tools.js +4 -196
- package/lib/ui-routes/account-routes.js +1931 -0
- package/lib/ui-routes/admin-config-routes.js +1233 -0
- package/lib/ui-routes/admin-entities-routes.js +2367 -0
- package/lib/ui-routes/oauth-routes.js +992 -0
- package/lib/utils/network.js +237 -0
- package/package.json +10 -10
- package/sbom.json +1 -1
- package/static/js/app.js +5 -5
- package/static/licenses.html +79 -19
- package/translations/de.mo +0 -0
- package/translations/de.po +97 -86
- package/translations/en.mo +0 -0
- package/translations/en.po +80 -75
- package/translations/et.mo +0 -0
- package/translations/et.po +96 -86
- package/translations/fr.mo +0 -0
- package/translations/fr.po +97 -86
- package/translations/ja.mo +0 -0
- package/translations/ja.po +96 -86
- package/translations/messages.pot +105 -91
- package/translations/nl.mo +0 -0
- package/translations/nl.po +98 -86
- package/translations/pl.mo +0 -0
- package/translations/pl.po +96 -86
- package/views/account/security.hbs +4 -4
- package/views/accounts/account.hbs +13 -13
- package/views/accounts/register/imap-server.hbs +12 -12
- package/views/config/document-store/pre-processing/index.hbs +4 -2
- package/views/config/oauth/app.hbs +6 -7
- package/views/config/oauth/index.hbs +2 -2
- package/views/config/service.hbs +3 -4
- package/views/dashboard.hbs +5 -7
- package/views/error.hbs +22 -7
- package/views/gateways/gateway.hbs +2 -2
- package/views/partials/add_account_modal.hbs +7 -10
- package/views/partials/document_store_header.hbs +1 -1
- package/views/partials/editor_scope_info.hbs +0 -1
- package/views/partials/oauth_config_header.hbs +1 -1
- package/views/partials/side_menu.hbs +3 -3
- package/views/partials/webhook_form.hbs +2 -2
- package/views/templates/index.hbs +1 -1
- package/views/templates/template.hbs +8 -8
- package/views/tokens/index.hbs +6 -6
- package/views/tokens/new.hbs +1 -1
- package/views/webhooks/index.hbs +4 -4
- package/views/webhooks/webhook.hbs +7 -7
- package/workers/api.js +148 -2436
- package/workers/smtp.js +2 -1
- package/lib/imapproxy/imap-core/test/client.js +0 -46
- package/lib/imapproxy/imap-core/test/fixtures/append.eml +0 -1196
- package/lib/imapproxy/imap-core/test/fixtures/chunks.js +0 -44
- package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +0 -6
- package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +0 -32
- package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +0 -6
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +0 -2740
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +0 -1411
- package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +0 -85
- package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +0 -582
- package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/simple.eml +0 -42
- package/lib/imapproxy/imap-core/test/fixtures/simple.json +0 -164
- package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +0 -671
- package/lib/imapproxy/imap-core/test/imap-compiler-test.js +0 -272
- package/lib/imapproxy/imap-core/test/imap-indexer-test.js +0 -236
- package/lib/imapproxy/imap-core/test/imap-parser-test.js +0 -922
- package/lib/imapproxy/imap-core/test/memory-notifier.js +0 -129
- package/lib/imapproxy/imap-core/test/prepare.sh +0 -74
- package/lib/imapproxy/imap-core/test/protocol-test.js +0 -1756
- package/lib/imapproxy/imap-core/test/search-test.js +0 -1356
- package/lib/imapproxy/imap-core/test/test-client.js +0 -152
- package/lib/imapproxy/imap-core/test/test-server.js +0 -623
- package/lib/imapproxy/imap-core/test/tools-test.js +0 -22
- package/test/api-test.js +0 -899
- package/test/autoreply-test.js +0 -327
- package/test/bounce-test.js +0 -151
- package/test/complaint-test.js +0 -256
- package/test/fixtures/autoreply/LICENSE +0 -27
- package/test/fixtures/autoreply/rfc3834-01.eml +0 -23
- package/test/fixtures/autoreply/rfc3834-02.eml +0 -24
- package/test/fixtures/autoreply/rfc3834-03.eml +0 -26
- package/test/fixtures/autoreply/rfc3834-04.eml +0 -48
- package/test/fixtures/autoreply/rfc3834-05.eml +0 -19
- package/test/fixtures/autoreply/rfc3834-06.eml +0 -59
- package/test/fixtures/bounces/163.eml +0 -2521
- package/test/fixtures/bounces/fastmail.eml +0 -242
- package/test/fixtures/bounces/gmail.eml +0 -252
- package/test/fixtures/bounces/hotmail.eml +0 -655
- package/test/fixtures/bounces/mailru.eml +0 -121
- package/test/fixtures/bounces/outlook.eml +0 -1107
- package/test/fixtures/bounces/postfix.eml +0 -101
- package/test/fixtures/bounces/rambler.eml +0 -116
- package/test/fixtures/bounces/workmail.eml +0 -142
- package/test/fixtures/bounces/yahoo.eml +0 -139
- package/test/fixtures/bounces/zoho.eml +0 -83
- package/test/fixtures/bounces/zonemta.eml +0 -100
- package/test/fixtures/complaints/LICENSE +0 -27
- package/test/fixtures/complaints/amazonses.eml +0 -72
- package/test/fixtures/complaints/dmarc.eml +0 -59
- package/test/fixtures/complaints/hotmail.eml +0 -49
- package/test/fixtures/complaints/optout.eml +0 -40
- package/test/fixtures/complaints/standard-arf.eml +0 -68
- package/test/fixtures/complaints/yahoo.eml +0 -68
- package/test/oauth2-apps-test.js +0 -301
- package/test/sendonly-test.js +0 -160
- package/test/test-config.js +0 -34
- package/test/webhooks-server.js +0 -39
|
@@ -0,0 +1,2367 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Boom = require('@hapi/boom');
|
|
4
|
+
const Joi = require('joi');
|
|
5
|
+
|
|
6
|
+
const settings = require('../settings');
|
|
7
|
+
const tokens = require('../tokens');
|
|
8
|
+
const { redis } = require('../db');
|
|
9
|
+
const getSecret = require('../get-secret');
|
|
10
|
+
const { failAction, verifyAccountInfo } = require('../tools');
|
|
11
|
+
const { templateSchemas, accountIdSchema } = require('../schemas');
|
|
12
|
+
const { Account } = require('../account');
|
|
13
|
+
const { Gateway } = require('../gateway');
|
|
14
|
+
const { templates } = require('../templates');
|
|
15
|
+
const { webhooks } = require('../webhooks');
|
|
16
|
+
const consts = require('../consts');
|
|
17
|
+
const wellKnownServices = require('nodemailer/lib/well-known/services.json');
|
|
18
|
+
const exampleWebhookPayloads = require('../payload-examples-webhooks.json');
|
|
19
|
+
|
|
20
|
+
const { DEFAULT_PAGE_SIZE } = consts;
|
|
21
|
+
|
|
22
|
+
const notificationTypes = Object.keys(consts)
|
|
23
|
+
.map(key => {
|
|
24
|
+
if (/_NOTIFY$/.test(key)) {
|
|
25
|
+
return key.replace(/_NOTIFY$/, '');
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
})
|
|
29
|
+
.filter(key => key)
|
|
30
|
+
.map(key => ({
|
|
31
|
+
key,
|
|
32
|
+
name: consts[`${key}_NOTIFY`],
|
|
33
|
+
description: consts[`${key}_DESCRIPTION`]
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const CODE_FORMATS = [
|
|
37
|
+
{
|
|
38
|
+
format: 'html',
|
|
39
|
+
name: 'HTML'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
format: 'markdown',
|
|
43
|
+
name: 'Markdown'
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
async function getExampleWebhookPayloads() {
|
|
48
|
+
let serviceUrl = await settings.get('serviceUrl');
|
|
49
|
+
let date = new Date().toISOString();
|
|
50
|
+
|
|
51
|
+
let examplePayloads = structuredClone(exampleWebhookPayloads);
|
|
52
|
+
|
|
53
|
+
examplePayloads.forEach(payload => {
|
|
54
|
+
if (payload && payload.content) {
|
|
55
|
+
if (typeof payload.content.serviceUrl === 'string') {
|
|
56
|
+
payload.content.serviceUrl = serviceUrl;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof payload.content.date === 'string') {
|
|
60
|
+
payload.content.date = date;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (payload.content.data && typeof payload.content.data.date === 'string') {
|
|
64
|
+
payload.content.data.date = date;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (payload.content.data && typeof payload.content.data.created === 'string') {
|
|
68
|
+
payload.content.data.created = date;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return examplePayloads;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function init(args) {
|
|
76
|
+
const { server, call } = args;
|
|
77
|
+
|
|
78
|
+
// Webhook routes
|
|
79
|
+
|
|
80
|
+
server.route({
|
|
81
|
+
method: 'GET',
|
|
82
|
+
path: '/admin/webhooks',
|
|
83
|
+
async handler(request, h) {
|
|
84
|
+
let data = await webhooks.list(request.query.page - 1, request.query.pageSize);
|
|
85
|
+
|
|
86
|
+
let nextPage = false;
|
|
87
|
+
let prevPage = false;
|
|
88
|
+
|
|
89
|
+
if (request.query.account) {
|
|
90
|
+
let accountObject = new Account({ redis, account: request.query.account });
|
|
91
|
+
data.account = await accountObject.loadAccountData();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let getPagingUrl = page => {
|
|
95
|
+
let url = new URL(`admin/webhooks`, 'http://localhost');
|
|
96
|
+
|
|
97
|
+
if (page) {
|
|
98
|
+
url.searchParams.append('page', page);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (request.query.pageSize !== DEFAULT_PAGE_SIZE) {
|
|
102
|
+
url.searchParams.append('pageSize', request.query.pageSize);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return url.pathname + url.search;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (data.pages > data.page + 1) {
|
|
109
|
+
nextPage = getPagingUrl(data.page + 2);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (data.page > 0) {
|
|
113
|
+
prevPage = getPagingUrl(data.page);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let newLink = new URL('/admin/webhooks/new', 'http://localhost');
|
|
117
|
+
|
|
118
|
+
return h.view(
|
|
119
|
+
'webhooks/index',
|
|
120
|
+
{
|
|
121
|
+
pageTitle: 'Webhook Routing',
|
|
122
|
+
menuWebhooks: true,
|
|
123
|
+
|
|
124
|
+
newLink: newLink.pathname + newLink.search,
|
|
125
|
+
|
|
126
|
+
showPaging: data.pages > 1,
|
|
127
|
+
nextPage,
|
|
128
|
+
prevPage,
|
|
129
|
+
firstPage: data.page === 0,
|
|
130
|
+
pageLinks: new Array(data.pages || 1).fill(0).map((z, i) => ({
|
|
131
|
+
url: getPagingUrl(i + 1),
|
|
132
|
+
title: i + 1,
|
|
133
|
+
active: i === data.page
|
|
134
|
+
})),
|
|
135
|
+
|
|
136
|
+
webhooksEnabled: await settings.get('webhooksEnabled'),
|
|
137
|
+
|
|
138
|
+
webhooks: data.webhooks
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
layout: 'app'
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
options: {
|
|
147
|
+
validate: {
|
|
148
|
+
options: {
|
|
149
|
+
stripUnknown: true,
|
|
150
|
+
abortEarly: false,
|
|
151
|
+
convert: true
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
async failAction(request, h /*, err*/) {
|
|
155
|
+
return h.redirect('/admin/webhooks').takeover();
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
query: Joi.object({
|
|
159
|
+
page: Joi.number().integer().min(1).max(1000000).default(1),
|
|
160
|
+
pageSize: Joi.number().integer().min(1).max(250).default(DEFAULT_PAGE_SIZE)
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
server.route({
|
|
167
|
+
method: 'GET',
|
|
168
|
+
path: '/admin/webhooks/new',
|
|
169
|
+
async handler(request, h) {
|
|
170
|
+
const values = {
|
|
171
|
+
name: '',
|
|
172
|
+
description: '',
|
|
173
|
+
|
|
174
|
+
contentFnJson: JSON.stringify(`/*
|
|
175
|
+
// The following example passes webhooks for new emails that appear in the Inbox of the user "testaccount".
|
|
176
|
+
// NB! Gmail webhooks are always emitted from the "All Mail" folder, not the Inbox, so we need to check both the path and label values.
|
|
177
|
+
|
|
178
|
+
const isInbox = payload.path === 'INBOX' || payload.data?.labels?.includes('\\\\Inbox');
|
|
179
|
+
if (payload.event === 'messageNew' && payload.account === 'testaccount' && isInbox) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
*/
|
|
183
|
+
|
|
184
|
+
return true; // pass all`),
|
|
185
|
+
contentMapJson: JSON.stringify(`// By default the output payload is returned unmodified.
|
|
186
|
+
|
|
187
|
+
return payload;`)
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return h.view(
|
|
191
|
+
'webhooks/new',
|
|
192
|
+
{
|
|
193
|
+
pageTitle: 'Webhook Routing',
|
|
194
|
+
menuWebhooks: true,
|
|
195
|
+
values,
|
|
196
|
+
|
|
197
|
+
examplePayloadsJson: JSON.stringify(await getExampleWebhookPayloads()),
|
|
198
|
+
notificationTypesJson: JSON.stringify(notificationTypes),
|
|
199
|
+
scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
layout: 'app'
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
options: {
|
|
208
|
+
validate: {
|
|
209
|
+
options: {
|
|
210
|
+
stripUnknown: true,
|
|
211
|
+
abortEarly: false,
|
|
212
|
+
convert: true
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
async failAction(request, h /*, err*/) {
|
|
216
|
+
return h.redirect('/admin/webhooks').takeover();
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
query: Joi.object({})
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
server.route({
|
|
225
|
+
method: 'POST',
|
|
226
|
+
path: '/admin/webhooks/new',
|
|
227
|
+
async handler(request, h) {
|
|
228
|
+
let contentFn, contentMap;
|
|
229
|
+
try {
|
|
230
|
+
if (request.payload.contentFnJson === '') {
|
|
231
|
+
contentFn = null;
|
|
232
|
+
} else {
|
|
233
|
+
contentFn = JSON.parse(request.payload.contentFnJson);
|
|
234
|
+
if (typeof contentFn !== 'string') {
|
|
235
|
+
throw new Error('Invalid Format');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
err.details = {
|
|
240
|
+
contentFnJson: 'Invalid JSON'
|
|
241
|
+
};
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
if (request.payload.contentMapJson === '') {
|
|
247
|
+
contentMap = null;
|
|
248
|
+
} else {
|
|
249
|
+
contentMap = JSON.parse(request.payload.contentMapJson);
|
|
250
|
+
if (typeof contentMap !== 'string') {
|
|
251
|
+
throw new Error('Invalid Format');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch (err) {
|
|
255
|
+
err.details = {
|
|
256
|
+
contentMapJson: 'Invalid JSON'
|
|
257
|
+
};
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let customHeaders = request.payload.customHeaders
|
|
262
|
+
.split(/[\r\n]+/)
|
|
263
|
+
.map(header => header.trim())
|
|
264
|
+
.filter(header => header)
|
|
265
|
+
.map(line => {
|
|
266
|
+
let sep = line.indexOf(':');
|
|
267
|
+
if (sep >= 0) {
|
|
268
|
+
return {
|
|
269
|
+
key: line.substring(0, sep).trim(),
|
|
270
|
+
value: line.substring(sep + 1).trim()
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
key: line,
|
|
275
|
+
value: ''
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
let createRequest = await webhooks.create(
|
|
281
|
+
{
|
|
282
|
+
name: request.payload.name,
|
|
283
|
+
description: request.payload.description,
|
|
284
|
+
targetUrl: request.payload.targetUrl,
|
|
285
|
+
enabled: request.payload.enabled,
|
|
286
|
+
|
|
287
|
+
customHeaders
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
fn: contentFn,
|
|
291
|
+
map: contentMap
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
await request.flash({ type: 'info', message: `Webhook created` });
|
|
296
|
+
return h.redirect(`/admin/webhooks/webhook/${createRequest.id}`);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
await request.flash({ type: 'danger', message: `Couldn't create webhook. Try again.` });
|
|
299
|
+
request.logger.error({ msg: 'Failed to create webhook routing', err });
|
|
300
|
+
|
|
301
|
+
return h.view(
|
|
302
|
+
'webhooks/new',
|
|
303
|
+
{
|
|
304
|
+
pageTitle: 'Webhook Routing',
|
|
305
|
+
menuWebhooks: true,
|
|
306
|
+
errors: err.details,
|
|
307
|
+
|
|
308
|
+
examplePayloadsJson: JSON.stringify(await getExampleWebhookPayloads()),
|
|
309
|
+
notificationTypesJson: JSON.stringify(notificationTypes),
|
|
310
|
+
scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
layout: 'app'
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
options: {
|
|
319
|
+
validate: {
|
|
320
|
+
options: {
|
|
321
|
+
stripUnknown: true,
|
|
322
|
+
abortEarly: false,
|
|
323
|
+
convert: true
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
async failAction(request, h, err) {
|
|
327
|
+
let errors = {};
|
|
328
|
+
|
|
329
|
+
if (err.details) {
|
|
330
|
+
err.details.forEach(detail => {
|
|
331
|
+
if (!errors[detail.path]) {
|
|
332
|
+
errors[detail.path] = detail.message;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await request.flash({ type: 'danger', message: `Couldn't create webhook. Try again.` });
|
|
338
|
+
request.logger.error({ msg: 'Failed to create webhook routing', err });
|
|
339
|
+
|
|
340
|
+
return h
|
|
341
|
+
.view(
|
|
342
|
+
'templates/new',
|
|
343
|
+
{
|
|
344
|
+
pageTitle: 'Templates',
|
|
345
|
+
menuTemplates: true,
|
|
346
|
+
errors,
|
|
347
|
+
|
|
348
|
+
examplePayloadsJson: JSON.stringify(await getExampleWebhookPayloads()),
|
|
349
|
+
notificationTypesJson: JSON.stringify(notificationTypes)
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
layout: 'app'
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
.takeover();
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
payload: Joi.object({
|
|
359
|
+
name: Joi.string().max(256).example('Transaction receipt').description('Name of the routing').label('RoutingName').required(),
|
|
360
|
+
description: Joi.string()
|
|
361
|
+
.allow('')
|
|
362
|
+
.max(1024)
|
|
363
|
+
.example('Something about the routing')
|
|
364
|
+
.description('Optional description of the webhook routing')
|
|
365
|
+
.label('RoutingDescription'),
|
|
366
|
+
targetUrl: Joi.string()
|
|
367
|
+
.uri({
|
|
368
|
+
scheme: ['http', 'https'],
|
|
369
|
+
allowRelative: false
|
|
370
|
+
})
|
|
371
|
+
.allow('')
|
|
372
|
+
.default('')
|
|
373
|
+
.example('https://myservice.com/imap/webhooks')
|
|
374
|
+
.description('Webhook target URL'),
|
|
375
|
+
enabled: Joi.boolean()
|
|
376
|
+
.truthy('Y', 'true', '1', 'on')
|
|
377
|
+
.falsy('N', 'false', 0, '')
|
|
378
|
+
.default(false)
|
|
379
|
+
.example(false)
|
|
380
|
+
.description('Is the routing enabled'),
|
|
381
|
+
customHeaders: Joi.string()
|
|
382
|
+
.allow('')
|
|
383
|
+
.trim()
|
|
384
|
+
.max(10 * 1024)
|
|
385
|
+
.description('Custom request headers'),
|
|
386
|
+
contentFnJson: Joi.string()
|
|
387
|
+
.max(1024 * 1024)
|
|
388
|
+
.default('')
|
|
389
|
+
.allow('')
|
|
390
|
+
.trim()
|
|
391
|
+
.description('Filter function'),
|
|
392
|
+
contentMapJson: Joi.string()
|
|
393
|
+
.max(1024 * 1024)
|
|
394
|
+
.default('')
|
|
395
|
+
.allow('')
|
|
396
|
+
.trim()
|
|
397
|
+
.description('Map function')
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
server.route({
|
|
404
|
+
method: 'GET',
|
|
405
|
+
path: '/admin/webhooks/webhook/{webhook}',
|
|
406
|
+
async handler(request, h) {
|
|
407
|
+
let webhook = await webhooks.get(request.params.webhook);
|
|
408
|
+
if (!webhook) {
|
|
409
|
+
let error = Boom.boomify(new Error('Webhook Route was not found.'), { statusCode: 404 });
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
webhook.targetUrlShort = webhook.targetUrl ? new URL(webhook.targetUrl).hostname : false;
|
|
414
|
+
|
|
415
|
+
const errorLog = ((await webhooks.getErrorLog(webhook.id)) || []).map(entry => {
|
|
416
|
+
if (entry.error && typeof entry.error === 'string') {
|
|
417
|
+
entry.error = entry.error
|
|
418
|
+
.replace(/\r?\n/g, '\n')
|
|
419
|
+
.replace(/^\s+at\s+.*$/gm, '')
|
|
420
|
+
.replace(/\n+/g, '\n')
|
|
421
|
+
.trim()
|
|
422
|
+
.replace(/(evalmachine.<anonymous>:)(\d+)/, (o, p, n) => p + (Number(n) - 1));
|
|
423
|
+
}
|
|
424
|
+
return entry;
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return h.view(
|
|
428
|
+
'webhooks/webhook',
|
|
429
|
+
{
|
|
430
|
+
pageTitle: 'Webhook Routing',
|
|
431
|
+
menuWebhooks: true,
|
|
432
|
+
webhook,
|
|
433
|
+
|
|
434
|
+
errorLog
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
layout: 'app'
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
options: {
|
|
443
|
+
validate: {
|
|
444
|
+
options: {
|
|
445
|
+
stripUnknown: true,
|
|
446
|
+
abortEarly: false,
|
|
447
|
+
convert: true
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
async failAction(request, h /*, err*/) {
|
|
451
|
+
return h.redirect('/admin/webhooks').takeover();
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
params: Joi.object({
|
|
455
|
+
webhook: Joi.string()
|
|
456
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
457
|
+
.max(512)
|
|
458
|
+
.example('AAAAAQAACnA')
|
|
459
|
+
.required()
|
|
460
|
+
.description('Webhook Route ID')
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
server.route({
|
|
467
|
+
method: 'GET',
|
|
468
|
+
path: '/admin/webhooks/webhook/{webhook}/edit',
|
|
469
|
+
async handler(request, h) {
|
|
470
|
+
let webhook = await webhooks.get(request.params.webhook);
|
|
471
|
+
if (!webhook) {
|
|
472
|
+
let error = Boom.boomify(new Error('Webhook Route not found.'), { statusCode: 404 });
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const values = {
|
|
477
|
+
webhook: webhook.id,
|
|
478
|
+
name: webhook.name,
|
|
479
|
+
description: webhook.description,
|
|
480
|
+
targetUrl: webhook.targetUrl,
|
|
481
|
+
enabled: webhook.enabled,
|
|
482
|
+
contentFnJson: JSON.stringify(webhook.content.fn || ''),
|
|
483
|
+
contentMapJson: JSON.stringify(webhook.content.map || ''),
|
|
484
|
+
|
|
485
|
+
customHeaders: []
|
|
486
|
+
.concat(webhook.customHeaders || [])
|
|
487
|
+
.map(entry => `${entry.key}: ${entry.value}`.trim())
|
|
488
|
+
.join('\n')
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
return h.view(
|
|
492
|
+
'webhooks/edit',
|
|
493
|
+
{
|
|
494
|
+
pageTitle: 'Webhook Routing',
|
|
495
|
+
menuWebhooks: true,
|
|
496
|
+
|
|
497
|
+
webhook,
|
|
498
|
+
|
|
499
|
+
values,
|
|
500
|
+
|
|
501
|
+
examplePayloadsJson: JSON.stringify(await getExampleWebhookPayloads()),
|
|
502
|
+
notificationTypesJson: JSON.stringify(notificationTypes),
|
|
503
|
+
scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
layout: 'app'
|
|
507
|
+
}
|
|
508
|
+
);
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
options: {
|
|
512
|
+
validate: {
|
|
513
|
+
options: {
|
|
514
|
+
stripUnknown: true,
|
|
515
|
+
abortEarly: false,
|
|
516
|
+
convert: true
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
async failAction(request, h /*, err*/) {
|
|
520
|
+
return h.redirect('/admin/webhooks').takeover();
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
params: Joi.object({
|
|
524
|
+
webhook: Joi.string()
|
|
525
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
526
|
+
.max(512)
|
|
527
|
+
.example('AAAAAQAACnA')
|
|
528
|
+
.required()
|
|
529
|
+
.description('Webhook Route ID')
|
|
530
|
+
})
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
server.route({
|
|
536
|
+
method: 'POST',
|
|
537
|
+
path: '/admin/webhooks/edit',
|
|
538
|
+
async handler(request, h) {
|
|
539
|
+
let contentFn, contentMap;
|
|
540
|
+
try {
|
|
541
|
+
if (request.payload.contentFnJson === '') {
|
|
542
|
+
contentFn = null;
|
|
543
|
+
} else {
|
|
544
|
+
contentFn = JSON.parse(request.payload.contentFnJson);
|
|
545
|
+
if (typeof contentFn !== 'string') {
|
|
546
|
+
throw new Error('Invalid Format');
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} catch (err) {
|
|
550
|
+
err.details = {
|
|
551
|
+
contentFnJson: 'Invalid JSON'
|
|
552
|
+
};
|
|
553
|
+
throw err;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
if (request.payload.contentMapJson === '') {
|
|
558
|
+
contentMap = null;
|
|
559
|
+
} else {
|
|
560
|
+
contentMap = JSON.parse(request.payload.contentMapJson);
|
|
561
|
+
if (typeof contentMap !== 'string') {
|
|
562
|
+
throw new Error('Invalid Format');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
} catch (err) {
|
|
566
|
+
err.details = {
|
|
567
|
+
contentMapJson: 'Invalid JSON'
|
|
568
|
+
};
|
|
569
|
+
throw err;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
let customHeaders = request.payload.customHeaders
|
|
573
|
+
.split(/[\r\n]+/)
|
|
574
|
+
.map(header => header.trim())
|
|
575
|
+
.filter(header => header)
|
|
576
|
+
.map(line => {
|
|
577
|
+
let sep = line.indexOf(':');
|
|
578
|
+
if (sep >= 0) {
|
|
579
|
+
return {
|
|
580
|
+
key: line.substring(0, sep).trim(),
|
|
581
|
+
value: line.substring(sep + 1).trim()
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
key: line,
|
|
586
|
+
value: ''
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
await webhooks.update(
|
|
592
|
+
request.payload.webhook,
|
|
593
|
+
{
|
|
594
|
+
name: request.payload.name,
|
|
595
|
+
description: request.payload.description,
|
|
596
|
+
targetUrl: request.payload.targetUrl,
|
|
597
|
+
enabled: request.payload.enabled,
|
|
598
|
+
|
|
599
|
+
customHeaders
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
fn: contentFn,
|
|
603
|
+
map: contentMap
|
|
604
|
+
}
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
await request.flash({ type: 'info', message: `Webhook saved` });
|
|
608
|
+
return h.redirect(`/admin/webhooks/webhook/${request.payload.webhook}`);
|
|
609
|
+
} catch (err) {
|
|
610
|
+
await request.flash({ type: 'danger', message: `Couldn't save webhook. Try again.` });
|
|
611
|
+
request.logger.error({ msg: 'Failed to update Webhook Route', err });
|
|
612
|
+
|
|
613
|
+
let webhook = await webhooks.get(request.payload.webhook);
|
|
614
|
+
if (!webhook) {
|
|
615
|
+
let error = Boom.boomify(new Error('Webhook Route not found.'), { statusCode: 404 });
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return h.view(
|
|
620
|
+
'webhooks/edit',
|
|
621
|
+
{
|
|
622
|
+
pageTitle: 'Webhook Routing',
|
|
623
|
+
menuWebhooks: true,
|
|
624
|
+
|
|
625
|
+
webhook,
|
|
626
|
+
|
|
627
|
+
errors: err.details,
|
|
628
|
+
|
|
629
|
+
examplePayloadsJson: JSON.stringify(await getExampleWebhookPayloads()),
|
|
630
|
+
notificationTypesJson: JSON.stringify(notificationTypes),
|
|
631
|
+
scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
layout: 'app'
|
|
635
|
+
}
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
options: {
|
|
640
|
+
validate: {
|
|
641
|
+
options: {
|
|
642
|
+
stripUnknown: true,
|
|
643
|
+
abortEarly: false,
|
|
644
|
+
convert: true
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
async failAction(request, h, err) {
|
|
648
|
+
let errors = {};
|
|
649
|
+
|
|
650
|
+
if (err.details) {
|
|
651
|
+
err.details.forEach(detail => {
|
|
652
|
+
if (!errors[detail.path]) {
|
|
653
|
+
errors[detail.path] = detail.message;
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
await request.flash({ type: 'danger', message: `Couldn't save webhook. Try again.` });
|
|
659
|
+
request.logger.error({ msg: 'Failed to update Webhook Route', err });
|
|
660
|
+
|
|
661
|
+
let webhook = await webhooks.get(request.payload.webhook);
|
|
662
|
+
if (!webhook) {
|
|
663
|
+
let error = Boom.boomify(new Error('Webhook Route not found.'), { statusCode: 404 });
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return h
|
|
668
|
+
.view(
|
|
669
|
+
'webhooks/edit',
|
|
670
|
+
{
|
|
671
|
+
pageTitle: 'Webhook Routing',
|
|
672
|
+
menuWebhooks: true,
|
|
673
|
+
|
|
674
|
+
webhook,
|
|
675
|
+
|
|
676
|
+
errors,
|
|
677
|
+
|
|
678
|
+
examplePayloadsJson: JSON.stringify(await getExampleWebhookPayloads()),
|
|
679
|
+
notificationTypesJson: JSON.stringify(notificationTypes),
|
|
680
|
+
scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}')
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
layout: 'app'
|
|
684
|
+
}
|
|
685
|
+
)
|
|
686
|
+
.takeover();
|
|
687
|
+
},
|
|
688
|
+
|
|
689
|
+
payload: Joi.object({
|
|
690
|
+
webhook: Joi.string()
|
|
691
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
692
|
+
.max(512)
|
|
693
|
+
.example('AAAAAQAACnA')
|
|
694
|
+
.required()
|
|
695
|
+
.description('Webhook Route ID'),
|
|
696
|
+
|
|
697
|
+
name: Joi.string().max(256).example('Transaction receipt').description('Name of the routing').label('RoutingName').required(),
|
|
698
|
+
description: Joi.string()
|
|
699
|
+
.allow('')
|
|
700
|
+
.max(1024)
|
|
701
|
+
.example('Something about the routing')
|
|
702
|
+
.description('Optional description of the webhook routing')
|
|
703
|
+
.label('RoutingDescription'),
|
|
704
|
+
targetUrl: Joi.string()
|
|
705
|
+
.uri({
|
|
706
|
+
scheme: ['http', 'https'],
|
|
707
|
+
allowRelative: false
|
|
708
|
+
})
|
|
709
|
+
.allow('')
|
|
710
|
+
.default('')
|
|
711
|
+
.example('https://myservice.com/imap/webhooks')
|
|
712
|
+
.description('Webhook target URL'),
|
|
713
|
+
enabled: Joi.boolean()
|
|
714
|
+
.truthy('Y', 'true', '1', 'on')
|
|
715
|
+
.falsy('N', 'false', 0, '')
|
|
716
|
+
.default(false)
|
|
717
|
+
.example(false)
|
|
718
|
+
.description('Is the routing enabled'),
|
|
719
|
+
customHeaders: Joi.string()
|
|
720
|
+
.allow('')
|
|
721
|
+
.trim()
|
|
722
|
+
.max(10 * 1024)
|
|
723
|
+
.description('Custom request headers'),
|
|
724
|
+
contentFnJson: Joi.string()
|
|
725
|
+
.max(1024 * 1024)
|
|
726
|
+
.default('')
|
|
727
|
+
.allow('')
|
|
728
|
+
.trim()
|
|
729
|
+
.description('Filter function'),
|
|
730
|
+
contentMapJson: Joi.string()
|
|
731
|
+
.max(1024 * 1024)
|
|
732
|
+
.default('')
|
|
733
|
+
.allow('')
|
|
734
|
+
.trim()
|
|
735
|
+
.description('Map function')
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
server.route({
|
|
742
|
+
method: 'POST',
|
|
743
|
+
path: '/admin/webhooks/delete',
|
|
744
|
+
async handler(request, h) {
|
|
745
|
+
try {
|
|
746
|
+
await webhooks.del(request.payload.webhook);
|
|
747
|
+
|
|
748
|
+
await request.flash({ type: 'info', message: `Webhook deleted` });
|
|
749
|
+
|
|
750
|
+
let accountWebhooksLink = new URL('/admin/webhooks', 'http://localhost');
|
|
751
|
+
|
|
752
|
+
return h.redirect(accountWebhooksLink.pathname + accountWebhooksLink.search);
|
|
753
|
+
} catch (err) {
|
|
754
|
+
await request.flash({ type: 'danger', message: `Couldn't delete webhook. Try again.` });
|
|
755
|
+
request.logger.error({ msg: 'Failed to delete Webhook Route', err, webhook: request.payload.webhook, remoteAddress: request.app.ip });
|
|
756
|
+
return h.redirect(`/admin/webhooks/webhook/${request.payload.webhook}`);
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
options: {
|
|
760
|
+
validate: {
|
|
761
|
+
options: {
|
|
762
|
+
stripUnknown: true,
|
|
763
|
+
abortEarly: false,
|
|
764
|
+
convert: true
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
async failAction(request, h, err) {
|
|
768
|
+
await request.flash({ type: 'danger', message: `Couldn't delete webhook. Try again.` });
|
|
769
|
+
request.logger.error({ msg: 'Failed to delete delete Webhook Route', err });
|
|
770
|
+
|
|
771
|
+
return h.redirect('/admin/webhooks').takeover();
|
|
772
|
+
},
|
|
773
|
+
|
|
774
|
+
payload: Joi.object({
|
|
775
|
+
webhook: Joi.string()
|
|
776
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
777
|
+
.max(512)
|
|
778
|
+
.example('AAAAAQAACnA')
|
|
779
|
+
.required()
|
|
780
|
+
.description('Webhook Route ID')
|
|
781
|
+
})
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// Template routes
|
|
787
|
+
|
|
788
|
+
server.route({
|
|
789
|
+
method: 'GET',
|
|
790
|
+
path: '/admin/templates',
|
|
791
|
+
async handler(request, h) {
|
|
792
|
+
let data = await templates.list(request.query.account, request.query.page - 1, request.query.pageSize);
|
|
793
|
+
|
|
794
|
+
let nextPage = false;
|
|
795
|
+
let prevPage = false;
|
|
796
|
+
|
|
797
|
+
if (request.query.account) {
|
|
798
|
+
let accountObject = new Account({ redis, account: request.query.account });
|
|
799
|
+
data.account = await accountObject.loadAccountData();
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
let getPagingUrl = page => {
|
|
803
|
+
let url = new URL(`admin/templates`, 'http://localhost');
|
|
804
|
+
url.searchParams.append('page', page);
|
|
805
|
+
|
|
806
|
+
if (request.query.account) {
|
|
807
|
+
url.searchParams.append('account', request.query.account);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (request.query.pageSize !== DEFAULT_PAGE_SIZE) {
|
|
811
|
+
url.searchParams.append('pageSize', request.query.pageSize);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return url.pathname + url.search;
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
if (data.pages > data.page + 1) {
|
|
818
|
+
nextPage = getPagingUrl(data.page + 2);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (data.page > 0) {
|
|
822
|
+
prevPage = getPagingUrl(data.page);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
let newLink = new URL('/admin/templates/new', 'http://localhost');
|
|
826
|
+
if (request.query.account) {
|
|
827
|
+
newLink.searchParams.append('account', request.query.account);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return h.view(
|
|
831
|
+
'templates/index',
|
|
832
|
+
{
|
|
833
|
+
pageTitle: 'Templates',
|
|
834
|
+
menuTemplates: true,
|
|
835
|
+
|
|
836
|
+
account: data.account,
|
|
837
|
+
newLink: newLink.pathname + newLink.search,
|
|
838
|
+
|
|
839
|
+
showPaging: data.pages > 1,
|
|
840
|
+
nextPage,
|
|
841
|
+
prevPage,
|
|
842
|
+
firstPage: data.page === 0,
|
|
843
|
+
pageLinks: new Array(data.pages || 1).fill(0).map((z, i) => ({
|
|
844
|
+
url: getPagingUrl(i + 1),
|
|
845
|
+
title: i + 1,
|
|
846
|
+
active: i === data.page
|
|
847
|
+
})),
|
|
848
|
+
|
|
849
|
+
templates: data.templates
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
layout: 'app'
|
|
853
|
+
}
|
|
854
|
+
);
|
|
855
|
+
},
|
|
856
|
+
|
|
857
|
+
options: {
|
|
858
|
+
validate: {
|
|
859
|
+
options: {
|
|
860
|
+
stripUnknown: true,
|
|
861
|
+
abortEarly: false,
|
|
862
|
+
convert: true
|
|
863
|
+
},
|
|
864
|
+
|
|
865
|
+
async failAction(request, h /*, err*/) {
|
|
866
|
+
return h.redirect('/admin/templates').takeover();
|
|
867
|
+
},
|
|
868
|
+
|
|
869
|
+
query: Joi.object({
|
|
870
|
+
account: accountIdSchema.default(null),
|
|
871
|
+
page: Joi.number().integer().min(1).max(1000000).default(1),
|
|
872
|
+
pageSize: Joi.number().integer().min(1).max(250).default(DEFAULT_PAGE_SIZE)
|
|
873
|
+
})
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
server.route({
|
|
879
|
+
method: 'GET',
|
|
880
|
+
path: '/admin/templates/template/{template}',
|
|
881
|
+
async handler(request, h) {
|
|
882
|
+
let template = await templates.get(request.params.template);
|
|
883
|
+
if (!template) {
|
|
884
|
+
let error = Boom.boomify(new Error('Template not found.'), { statusCode: 404 });
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
let account;
|
|
889
|
+
if (template.account) {
|
|
890
|
+
let accountObject = new Account({ redis, account: template.account });
|
|
891
|
+
account = await accountObject.loadAccountData();
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
|
|
895
|
+
if (account) {
|
|
896
|
+
accountTemplatesLink.searchParams.append('account', account.account);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return h.view(
|
|
900
|
+
'templates/template',
|
|
901
|
+
{
|
|
902
|
+
pageTitle: 'Templates',
|
|
903
|
+
menuTemplates: true,
|
|
904
|
+
|
|
905
|
+
account,
|
|
906
|
+
|
|
907
|
+
accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
|
|
908
|
+
|
|
909
|
+
format: CODE_FORMATS.find(entry => entry.format === template.format),
|
|
910
|
+
|
|
911
|
+
template
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
layout: 'app'
|
|
915
|
+
}
|
|
916
|
+
);
|
|
917
|
+
},
|
|
918
|
+
|
|
919
|
+
options: {
|
|
920
|
+
validate: {
|
|
921
|
+
options: {
|
|
922
|
+
stripUnknown: true,
|
|
923
|
+
abortEarly: false,
|
|
924
|
+
convert: true
|
|
925
|
+
},
|
|
926
|
+
|
|
927
|
+
async failAction(request, h /*, err*/) {
|
|
928
|
+
return h.redirect('/admin/templates').takeover();
|
|
929
|
+
},
|
|
930
|
+
|
|
931
|
+
params: Joi.object({
|
|
932
|
+
template: Joi.string()
|
|
933
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
934
|
+
.max(512)
|
|
935
|
+
.example('AAAAAQAACnA')
|
|
936
|
+
.required()
|
|
937
|
+
.description('Template ID')
|
|
938
|
+
})
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
server.route({
|
|
944
|
+
method: 'GET',
|
|
945
|
+
path: '/admin/templates/template/{template}/edit',
|
|
946
|
+
async handler(request, h) {
|
|
947
|
+
let template = await templates.get(request.params.template);
|
|
948
|
+
if (!template) {
|
|
949
|
+
let error = Boom.boomify(new Error('Template not found.'), { statusCode: 404 });
|
|
950
|
+
throw error;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
let account;
|
|
954
|
+
if (template.account) {
|
|
955
|
+
let accountObject = new Account({ redis, account: template.account });
|
|
956
|
+
account = await accountObject.loadAccountData();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
|
|
960
|
+
if (account) {
|
|
961
|
+
accountTemplatesLink.searchParams.append('account', account.account);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const values = {
|
|
965
|
+
template: template.id,
|
|
966
|
+
name: template.name,
|
|
967
|
+
description: template.description,
|
|
968
|
+
subject: template.content.subject,
|
|
969
|
+
format: template.format,
|
|
970
|
+
previewText: template.content.previewText
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
return h.view(
|
|
974
|
+
'templates/edit',
|
|
975
|
+
{
|
|
976
|
+
pageTitle: 'Templates',
|
|
977
|
+
menuTemplates: true,
|
|
978
|
+
|
|
979
|
+
account,
|
|
980
|
+
|
|
981
|
+
accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
|
|
982
|
+
|
|
983
|
+
template,
|
|
984
|
+
|
|
985
|
+
formats: CODE_FORMATS.map(format => Object.assign({ selected: format.format === values.format }, format)),
|
|
986
|
+
|
|
987
|
+
values,
|
|
988
|
+
|
|
989
|
+
contentHtmlJson: JSON.stringify(template.content.html || ''),
|
|
990
|
+
contentTextJson: JSON.stringify(template.content.text || '')
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
layout: 'app'
|
|
994
|
+
}
|
|
995
|
+
);
|
|
996
|
+
},
|
|
997
|
+
|
|
998
|
+
options: {
|
|
999
|
+
validate: {
|
|
1000
|
+
options: {
|
|
1001
|
+
stripUnknown: true,
|
|
1002
|
+
abortEarly: false,
|
|
1003
|
+
convert: true
|
|
1004
|
+
},
|
|
1005
|
+
|
|
1006
|
+
async failAction(request, h /*, err*/) {
|
|
1007
|
+
return h.redirect('/admin/templates').takeover();
|
|
1008
|
+
},
|
|
1009
|
+
|
|
1010
|
+
params: Joi.object({
|
|
1011
|
+
template: Joi.string()
|
|
1012
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
1013
|
+
.max(512)
|
|
1014
|
+
.example('AAAAAQAACnA')
|
|
1015
|
+
.required()
|
|
1016
|
+
.description('Template ID')
|
|
1017
|
+
})
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
server.route({
|
|
1023
|
+
method: 'POST',
|
|
1024
|
+
path: '/admin/templates/edit',
|
|
1025
|
+
async handler(request, h) {
|
|
1026
|
+
try {
|
|
1027
|
+
await templates.update(
|
|
1028
|
+
request.payload.template,
|
|
1029
|
+
{
|
|
1030
|
+
name: request.payload.name,
|
|
1031
|
+
description: request.payload.description,
|
|
1032
|
+
format: request.payload.format
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
subject: request.payload.subject,
|
|
1036
|
+
html: request.payload.contentHtml,
|
|
1037
|
+
text: request.payload.contentText,
|
|
1038
|
+
previewText: request.payload.previewText
|
|
1039
|
+
}
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
await request.flash({ type: 'info', message: `Template saved` });
|
|
1043
|
+
return h.redirect(`/admin/templates/template/${request.payload.template}`);
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
await request.flash({ type: 'danger', message: `Couldn't save template. Try again.` });
|
|
1046
|
+
request.logger.error({ msg: 'Failed to update template', err });
|
|
1047
|
+
|
|
1048
|
+
let template = await templates.get(request.payload.template);
|
|
1049
|
+
if (!template) {
|
|
1050
|
+
let error = Boom.boomify(new Error('Template not found.'), { statusCode: 404 });
|
|
1051
|
+
throw error;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
let account;
|
|
1055
|
+
if (template.account) {
|
|
1056
|
+
let accountObject = new Account({ redis, account: template.account });
|
|
1057
|
+
account = await accountObject.loadAccountData();
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
|
|
1061
|
+
if (account) {
|
|
1062
|
+
accountTemplatesLink.searchParams.append('account', account.account);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return h.view(
|
|
1066
|
+
'templates/edit',
|
|
1067
|
+
{
|
|
1068
|
+
pageTitle: 'Templates',
|
|
1069
|
+
menuTemplates: true,
|
|
1070
|
+
|
|
1071
|
+
account,
|
|
1072
|
+
|
|
1073
|
+
accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
|
|
1074
|
+
|
|
1075
|
+
template,
|
|
1076
|
+
|
|
1077
|
+
formats: CODE_FORMATS.map(format => Object.assign({ selected: format.format === request.payload.format }, format)),
|
|
1078
|
+
|
|
1079
|
+
errors: err.details,
|
|
1080
|
+
|
|
1081
|
+
contentHtmlJson: JSON.stringify(request.payload.contentHtml || ''),
|
|
1082
|
+
contentTextJson: JSON.stringify(request.payload.contentText || '')
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
layout: 'app'
|
|
1086
|
+
}
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
options: {
|
|
1091
|
+
validate: {
|
|
1092
|
+
options: {
|
|
1093
|
+
stripUnknown: true,
|
|
1094
|
+
abortEarly: false,
|
|
1095
|
+
convert: true
|
|
1096
|
+
},
|
|
1097
|
+
|
|
1098
|
+
async failAction(request, h, err) {
|
|
1099
|
+
let errors = {};
|
|
1100
|
+
|
|
1101
|
+
if (err.details) {
|
|
1102
|
+
err.details.forEach(detail => {
|
|
1103
|
+
if (!errors[detail.path]) {
|
|
1104
|
+
errors[detail.path] = detail.message;
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
await request.flash({ type: 'danger', message: `Couldn't save template. Try again.` });
|
|
1110
|
+
request.logger.error({ msg: 'Failed to update template', err });
|
|
1111
|
+
|
|
1112
|
+
let template = await templates.get(request.payload.template);
|
|
1113
|
+
if (!template) {
|
|
1114
|
+
let error = Boom.boomify(new Error('Template not found.'), { statusCode: 404 });
|
|
1115
|
+
throw error;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
let account;
|
|
1119
|
+
if (template.account) {
|
|
1120
|
+
let accountObject = new Account({ redis, account: template.account });
|
|
1121
|
+
account = await accountObject.loadAccountData();
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
|
|
1125
|
+
if (account) {
|
|
1126
|
+
accountTemplatesLink.searchParams.append('account', account.account);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return h
|
|
1130
|
+
.view(
|
|
1131
|
+
'templates/edit',
|
|
1132
|
+
{
|
|
1133
|
+
pageTitle: 'Templates',
|
|
1134
|
+
menuTemplates: true,
|
|
1135
|
+
|
|
1136
|
+
account,
|
|
1137
|
+
|
|
1138
|
+
accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
|
|
1139
|
+
|
|
1140
|
+
template,
|
|
1141
|
+
|
|
1142
|
+
formats: CODE_FORMATS.map(format => Object.assign({ selected: format.format === request.payload.format }, format)),
|
|
1143
|
+
|
|
1144
|
+
errors,
|
|
1145
|
+
|
|
1146
|
+
contentHtmlJson: JSON.stringify(request.payload.contentHtml || ''),
|
|
1147
|
+
contentTextJson: JSON.stringify(request.payload.contentText || '')
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
layout: 'app'
|
|
1151
|
+
}
|
|
1152
|
+
)
|
|
1153
|
+
.takeover();
|
|
1154
|
+
},
|
|
1155
|
+
|
|
1156
|
+
payload: Joi.object({
|
|
1157
|
+
template: Joi.string()
|
|
1158
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
1159
|
+
.max(512)
|
|
1160
|
+
.example('AAAAAQAACnA')
|
|
1161
|
+
.required()
|
|
1162
|
+
.description('Template ID'),
|
|
1163
|
+
|
|
1164
|
+
name: Joi.string().max(256).example('Transaction receipt').description('Name of the template').label('TemplateName').required(),
|
|
1165
|
+
description: Joi.string()
|
|
1166
|
+
.allow('')
|
|
1167
|
+
.max(1024)
|
|
1168
|
+
.example('Something about the template')
|
|
1169
|
+
.description('Optional description of the template')
|
|
1170
|
+
.label('TemplateDescription'),
|
|
1171
|
+
format: Joi.string().valid('html', 'markdown').default('html').description('Markup language for HTML ("html" or "markdown")'),
|
|
1172
|
+
subject: templateSchemas.subject,
|
|
1173
|
+
contentText: templateSchemas.text,
|
|
1174
|
+
contentHtml: templateSchemas.html,
|
|
1175
|
+
previewText: templateSchemas.previewText
|
|
1176
|
+
})
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
server.route({
|
|
1182
|
+
method: 'GET',
|
|
1183
|
+
path: '/admin/templates/new',
|
|
1184
|
+
async handler(request, h) {
|
|
1185
|
+
let account;
|
|
1186
|
+
if (request.query.account) {
|
|
1187
|
+
let accountObject = new Account({ redis, account: request.query.account });
|
|
1188
|
+
account = await accountObject.loadAccountData();
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
|
|
1192
|
+
if (account) {
|
|
1193
|
+
accountTemplatesLink.searchParams.append('account', account.account);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const values = {
|
|
1197
|
+
account: request.query.account,
|
|
1198
|
+
name: '',
|
|
1199
|
+
description: '',
|
|
1200
|
+
subject: '',
|
|
1201
|
+
format: 'html',
|
|
1202
|
+
contentHtml: '',
|
|
1203
|
+
contentText: '',
|
|
1204
|
+
previewText: ''
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
return h.view(
|
|
1208
|
+
'templates/new',
|
|
1209
|
+
{
|
|
1210
|
+
pageTitle: 'Templates',
|
|
1211
|
+
menuTemplates: true,
|
|
1212
|
+
|
|
1213
|
+
account,
|
|
1214
|
+
|
|
1215
|
+
accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
|
|
1216
|
+
|
|
1217
|
+
formats: CODE_FORMATS.map(format => Object.assign({ selected: format.format === values.format }, format)),
|
|
1218
|
+
|
|
1219
|
+
values,
|
|
1220
|
+
|
|
1221
|
+
contentHtmlJson: JSON.stringify(''),
|
|
1222
|
+
contentTextJson: JSON.stringify('')
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
layout: 'app'
|
|
1226
|
+
}
|
|
1227
|
+
);
|
|
1228
|
+
},
|
|
1229
|
+
|
|
1230
|
+
options: {
|
|
1231
|
+
validate: {
|
|
1232
|
+
options: {
|
|
1233
|
+
stripUnknown: true,
|
|
1234
|
+
abortEarly: false,
|
|
1235
|
+
convert: true
|
|
1236
|
+
},
|
|
1237
|
+
|
|
1238
|
+
async failAction(request, h /*, err*/) {
|
|
1239
|
+
return h.redirect('/admin/templates').takeover();
|
|
1240
|
+
},
|
|
1241
|
+
|
|
1242
|
+
query: Joi.object({
|
|
1243
|
+
account: accountIdSchema.default(null)
|
|
1244
|
+
})
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
server.route({
|
|
1250
|
+
method: 'POST',
|
|
1251
|
+
path: '/admin/templates/new',
|
|
1252
|
+
async handler(request, h) {
|
|
1253
|
+
try {
|
|
1254
|
+
let createRequest = await templates.create(
|
|
1255
|
+
request.payload.account,
|
|
1256
|
+
{
|
|
1257
|
+
name: request.payload.name,
|
|
1258
|
+
description: request.payload.description,
|
|
1259
|
+
format: request.payload.format
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
subject: request.payload.subject,
|
|
1263
|
+
html: request.payload.contentHtml,
|
|
1264
|
+
text: request.payload.contentText,
|
|
1265
|
+
previewText: request.payload.previewText
|
|
1266
|
+
}
|
|
1267
|
+
);
|
|
1268
|
+
|
|
1269
|
+
await request.flash({ type: 'info', message: `Template created` });
|
|
1270
|
+
return h.redirect(`/admin/templates/template/${createRequest.id}`);
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
await request.flash({ type: 'danger', message: `Couldn't create template. Try again.` });
|
|
1273
|
+
request.logger.error({ msg: 'Failed to create template', err });
|
|
1274
|
+
|
|
1275
|
+
let account;
|
|
1276
|
+
if (request.payload.account) {
|
|
1277
|
+
let accountObject = new Account({ redis, account: request.payload.account });
|
|
1278
|
+
account = await accountObject.loadAccountData();
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
|
|
1282
|
+
if (account) {
|
|
1283
|
+
accountTemplatesLink.searchParams.append('account', account.account);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
return h.view(
|
|
1287
|
+
'templates/new',
|
|
1288
|
+
{
|
|
1289
|
+
pageTitle: 'Templates',
|
|
1290
|
+
menuTemplates: true,
|
|
1291
|
+
|
|
1292
|
+
account,
|
|
1293
|
+
|
|
1294
|
+
accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
|
|
1295
|
+
|
|
1296
|
+
formats: CODE_FORMATS.map(format => Object.assign({ selected: format.format === request.payload.format }, format)),
|
|
1297
|
+
|
|
1298
|
+
errors: err.details,
|
|
1299
|
+
|
|
1300
|
+
contentHtmlJson: JSON.stringify(request.payload.contentHtml || ''),
|
|
1301
|
+
contentTextJson: JSON.stringify(request.payload.contentText || '')
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
layout: 'app'
|
|
1305
|
+
}
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
},
|
|
1309
|
+
options: {
|
|
1310
|
+
validate: {
|
|
1311
|
+
options: {
|
|
1312
|
+
stripUnknown: true,
|
|
1313
|
+
abortEarly: false,
|
|
1314
|
+
convert: true
|
|
1315
|
+
},
|
|
1316
|
+
|
|
1317
|
+
async failAction(request, h, err) {
|
|
1318
|
+
let errors = {};
|
|
1319
|
+
|
|
1320
|
+
if (err.details) {
|
|
1321
|
+
err.details.forEach(detail => {
|
|
1322
|
+
if (!errors[detail.path]) {
|
|
1323
|
+
errors[detail.path] = detail.message;
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
await request.flash({ type: 'danger', message: `Couldn't create template. Try again.` });
|
|
1329
|
+
request.logger.error({ msg: 'Failed to create template', err });
|
|
1330
|
+
|
|
1331
|
+
let account;
|
|
1332
|
+
if (request.payload.account) {
|
|
1333
|
+
let accountObject = new Account({ redis, account: request.payload.account });
|
|
1334
|
+
account = await accountObject.loadAccountData();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
|
|
1338
|
+
if (account) {
|
|
1339
|
+
accountTemplatesLink.searchParams.append('account', account.account);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
return h
|
|
1343
|
+
.view(
|
|
1344
|
+
'templates/new',
|
|
1345
|
+
{
|
|
1346
|
+
pageTitle: 'Templates',
|
|
1347
|
+
menuTemplates: true,
|
|
1348
|
+
|
|
1349
|
+
account,
|
|
1350
|
+
|
|
1351
|
+
accountTemplatesLink: accountTemplatesLink.pathname + accountTemplatesLink.search,
|
|
1352
|
+
|
|
1353
|
+
formats: CODE_FORMATS.map(format => Object.assign({ selected: format.format === request.payload.format }, format)),
|
|
1354
|
+
|
|
1355
|
+
errors,
|
|
1356
|
+
|
|
1357
|
+
contentHtmlJson: JSON.stringify(request.payload.contentHtml || ''),
|
|
1358
|
+
contentTextJson: JSON.stringify(request.payload.contentText || '')
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
layout: 'app'
|
|
1362
|
+
}
|
|
1363
|
+
)
|
|
1364
|
+
.takeover();
|
|
1365
|
+
},
|
|
1366
|
+
|
|
1367
|
+
payload: Joi.object({
|
|
1368
|
+
account: accountIdSchema.default(null),
|
|
1369
|
+
|
|
1370
|
+
name: Joi.string().max(256).example('Transaction receipt').description('Name of the template').label('TemplateName').required(),
|
|
1371
|
+
description: Joi.string()
|
|
1372
|
+
.allow('')
|
|
1373
|
+
.max(1024)
|
|
1374
|
+
.example('Something about the template')
|
|
1375
|
+
.description('Optional description of the template')
|
|
1376
|
+
.label('TemplateDescription'),
|
|
1377
|
+
format: Joi.string().valid('html', 'markdown').default('html').description('Markup language for HTML ("html" or "markdown")'),
|
|
1378
|
+
subject: templateSchemas.subject,
|
|
1379
|
+
contentText: templateSchemas.text,
|
|
1380
|
+
contentHtml: templateSchemas.html,
|
|
1381
|
+
previewText: templateSchemas.previewText
|
|
1382
|
+
})
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
server.route({
|
|
1388
|
+
method: 'POST',
|
|
1389
|
+
path: '/admin/templates/delete',
|
|
1390
|
+
async handler(request, h) {
|
|
1391
|
+
try {
|
|
1392
|
+
let templateResponse = await templates.del(request.payload.template);
|
|
1393
|
+
|
|
1394
|
+
await request.flash({ type: 'info', message: `Template deleted` });
|
|
1395
|
+
|
|
1396
|
+
let accountTemplatesLink = new URL('/admin/templates', 'http://localhost');
|
|
1397
|
+
if (templateResponse && templateResponse.account) {
|
|
1398
|
+
accountTemplatesLink.searchParams.append('account', templateResponse.account);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
return h.redirect(accountTemplatesLink.pathname + accountTemplatesLink.search);
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
await request.flash({ type: 'danger', message: `Couldn't delete template. Try again.` });
|
|
1404
|
+
request.logger.error({ msg: 'Failed to delete the template', err, template: request.payload.template, remoteAddress: request.app.ip });
|
|
1405
|
+
return h.redirect(`/admin/templates/template/${request.payload.template}`);
|
|
1406
|
+
}
|
|
1407
|
+
},
|
|
1408
|
+
options: {
|
|
1409
|
+
validate: {
|
|
1410
|
+
options: {
|
|
1411
|
+
stripUnknown: true,
|
|
1412
|
+
abortEarly: false,
|
|
1413
|
+
convert: true
|
|
1414
|
+
},
|
|
1415
|
+
|
|
1416
|
+
async failAction(request, h, err) {
|
|
1417
|
+
await request.flash({ type: 'danger', message: `Couldn't delete account. Try again.` });
|
|
1418
|
+
request.logger.error({ msg: 'Failed to delete delete the account', err });
|
|
1419
|
+
|
|
1420
|
+
return h.redirect('/admin/templates').takeover();
|
|
1421
|
+
},
|
|
1422
|
+
|
|
1423
|
+
payload: Joi.object({
|
|
1424
|
+
template: Joi.string()
|
|
1425
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
1426
|
+
.max(512)
|
|
1427
|
+
.example('AAAAAQAACnA')
|
|
1428
|
+
.required()
|
|
1429
|
+
.description('Template ID')
|
|
1430
|
+
})
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
server.route({
|
|
1436
|
+
method: 'POST',
|
|
1437
|
+
path: '/admin/templates/test',
|
|
1438
|
+
async handler(request) {
|
|
1439
|
+
try {
|
|
1440
|
+
request.logger.info({ msg: 'Trying to send test message', payload: request.payload });
|
|
1441
|
+
|
|
1442
|
+
let template = await templates.get(request.payload.template);
|
|
1443
|
+
if (!template) {
|
|
1444
|
+
return {
|
|
1445
|
+
error: 'Template was not found'
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
let accountId = template.account || request.payload.account;
|
|
1450
|
+
if (!accountId) {
|
|
1451
|
+
return { error: 'Account ID not provided' };
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
let accountObject = new Account({ redis, account: accountId, call, secret: await getSecret() });
|
|
1455
|
+
|
|
1456
|
+
let account;
|
|
1457
|
+
try {
|
|
1458
|
+
account = await accountObject.loadAccountData();
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
return {
|
|
1461
|
+
error: err.message
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
try {
|
|
1466
|
+
return await accountObject.queueMessage(
|
|
1467
|
+
{
|
|
1468
|
+
account: account.account,
|
|
1469
|
+
template: template.id,
|
|
1470
|
+
from: {
|
|
1471
|
+
name: account.name,
|
|
1472
|
+
address: account.email
|
|
1473
|
+
},
|
|
1474
|
+
to: [{ name: '', address: request.payload.to }],
|
|
1475
|
+
render: {
|
|
1476
|
+
params: request.payload.params || {}
|
|
1477
|
+
},
|
|
1478
|
+
copy: false,
|
|
1479
|
+
deliveryAttempts: 0
|
|
1480
|
+
},
|
|
1481
|
+
{ source: 'ui' }
|
|
1482
|
+
);
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
return {
|
|
1485
|
+
error: err.message
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
} catch (err) {
|
|
1489
|
+
request.logger.error({ msg: 'Failed sending test message', err });
|
|
1490
|
+
return {
|
|
1491
|
+
success: false,
|
|
1492
|
+
error: err.message
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
},
|
|
1496
|
+
options: {
|
|
1497
|
+
tags: ['test'],
|
|
1498
|
+
validate: {
|
|
1499
|
+
options: {
|
|
1500
|
+
stripUnknown: true,
|
|
1501
|
+
abortEarly: false,
|
|
1502
|
+
convert: true
|
|
1503
|
+
},
|
|
1504
|
+
|
|
1505
|
+
failAction,
|
|
1506
|
+
|
|
1507
|
+
payload: Joi.object({
|
|
1508
|
+
account: accountIdSchema.default(null),
|
|
1509
|
+
template: Joi.string()
|
|
1510
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
1511
|
+
.max(512)
|
|
1512
|
+
.example('AAAAAQAACnA')
|
|
1513
|
+
.required()
|
|
1514
|
+
.description('Template ID'),
|
|
1515
|
+
to: Joi.string().email().required().description('Recipient address'),
|
|
1516
|
+
params: Joi.object().description('Optional handlebars values').unknown()
|
|
1517
|
+
})
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
// Gateway routes
|
|
1523
|
+
|
|
1524
|
+
server.route({
|
|
1525
|
+
method: 'GET',
|
|
1526
|
+
path: '/admin/gateways',
|
|
1527
|
+
async handler(request, h) {
|
|
1528
|
+
let gatewayObject = new Gateway({ redis });
|
|
1529
|
+
|
|
1530
|
+
let gateways = await gatewayObject.listGateways(request.query.page - 1, request.query.pageSize);
|
|
1531
|
+
|
|
1532
|
+
if (gateways.pages < request.query.page) {
|
|
1533
|
+
request.query.page = gateways.pages;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
let nextPage = false;
|
|
1537
|
+
let prevPage = false;
|
|
1538
|
+
|
|
1539
|
+
let getPagingUrl = page => {
|
|
1540
|
+
let url = new URL(`admin/gateways`, 'http://localhost');
|
|
1541
|
+
url.searchParams.append('page', page);
|
|
1542
|
+
if (request.query.pageSize !== DEFAULT_PAGE_SIZE) {
|
|
1543
|
+
url.searchParams.append('pageSize', request.query.pageSize);
|
|
1544
|
+
}
|
|
1545
|
+
return url.pathname + url.search;
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
if (gateways.pages > gateways.page + 1) {
|
|
1549
|
+
nextPage = getPagingUrl(gateways.page + 2);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
if (gateways.page > 0) {
|
|
1553
|
+
prevPage = getPagingUrl(gateways.page);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
return h.view(
|
|
1557
|
+
'gateways/index',
|
|
1558
|
+
{
|
|
1559
|
+
pageTitle: 'Email Gateways',
|
|
1560
|
+
menuGateways: true,
|
|
1561
|
+
|
|
1562
|
+
showPaging: gateways.pages > 1,
|
|
1563
|
+
nextPage,
|
|
1564
|
+
prevPage,
|
|
1565
|
+
firstPage: gateways.page === 0,
|
|
1566
|
+
pageLinks: new Array(gateways.pages || 1).fill(0).map((z, i) => ({
|
|
1567
|
+
url: getPagingUrl(i + 1),
|
|
1568
|
+
title: i + 1,
|
|
1569
|
+
active: i === gateways.page
|
|
1570
|
+
})),
|
|
1571
|
+
|
|
1572
|
+
gateways: gateways.gateways.map(entry => {
|
|
1573
|
+
let label = {};
|
|
1574
|
+
if (entry.deliveries && !entry.lastError) {
|
|
1575
|
+
label.type = 'success';
|
|
1576
|
+
label.name = 'Connected';
|
|
1577
|
+
} else if (entry.lastError) {
|
|
1578
|
+
label.type = 'danger';
|
|
1579
|
+
label.name = 'Error';
|
|
1580
|
+
label.error = entry.lastError.response;
|
|
1581
|
+
} else {
|
|
1582
|
+
label.type = 'info';
|
|
1583
|
+
label.name = 'Not used';
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
return Object.assign(entry, {
|
|
1587
|
+
timeStr: entry.lastUse ? entry.lastUse.toISOString() : null,
|
|
1588
|
+
label
|
|
1589
|
+
});
|
|
1590
|
+
})
|
|
1591
|
+
},
|
|
1592
|
+
{
|
|
1593
|
+
layout: 'app'
|
|
1594
|
+
}
|
|
1595
|
+
);
|
|
1596
|
+
},
|
|
1597
|
+
|
|
1598
|
+
options: {
|
|
1599
|
+
validate: {
|
|
1600
|
+
options: {
|
|
1601
|
+
stripUnknown: true,
|
|
1602
|
+
abortEarly: false,
|
|
1603
|
+
convert: true
|
|
1604
|
+
},
|
|
1605
|
+
|
|
1606
|
+
async failAction(request, h /*, err*/) {
|
|
1607
|
+
return h.redirect('/admin/gateways').takeover();
|
|
1608
|
+
},
|
|
1609
|
+
|
|
1610
|
+
query: Joi.object({
|
|
1611
|
+
page: Joi.number().integer().min(1).max(1000000).default(1),
|
|
1612
|
+
pageSize: Joi.number().integer().min(1).max(250).default(DEFAULT_PAGE_SIZE)
|
|
1613
|
+
})
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
server.route({
|
|
1619
|
+
method: 'GET',
|
|
1620
|
+
path: '/admin/gateways/new',
|
|
1621
|
+
async handler(request, h) {
|
|
1622
|
+
return h.view(
|
|
1623
|
+
'gateways/new',
|
|
1624
|
+
{
|
|
1625
|
+
pageTitle: 'Email Gateways',
|
|
1626
|
+
menuGateways: true,
|
|
1627
|
+
wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key])))
|
|
1628
|
+
},
|
|
1629
|
+
{
|
|
1630
|
+
layout: 'app'
|
|
1631
|
+
}
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
server.route({
|
|
1637
|
+
method: 'GET',
|
|
1638
|
+
path: '/admin/gateways/gateway/{gateway}',
|
|
1639
|
+
async handler(request, h) {
|
|
1640
|
+
let gatewayObject = new Gateway({ gateway: request.params.gateway, redis, secret: await getSecret() });
|
|
1641
|
+
let gatewayData = await gatewayObject.loadGatewayData();
|
|
1642
|
+
|
|
1643
|
+
let label = {};
|
|
1644
|
+
if (gatewayData.deliveries && !gatewayData.lastError) {
|
|
1645
|
+
label.type = 'success';
|
|
1646
|
+
label.name = 'Connected';
|
|
1647
|
+
} else if (gatewayData.lastError) {
|
|
1648
|
+
label.type = 'danger';
|
|
1649
|
+
label.name = 'Error';
|
|
1650
|
+
label.error = gatewayData.lastError.response;
|
|
1651
|
+
} else {
|
|
1652
|
+
label.type = 'info';
|
|
1653
|
+
label.name = 'Not used';
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
return h.view(
|
|
1657
|
+
'gateways/gateway',
|
|
1658
|
+
{
|
|
1659
|
+
pageTitle: 'Email Gateways',
|
|
1660
|
+
menuGateways: true,
|
|
1661
|
+
wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key]))),
|
|
1662
|
+
|
|
1663
|
+
gateway: gatewayData,
|
|
1664
|
+
label
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
layout: 'app'
|
|
1668
|
+
}
|
|
1669
|
+
);
|
|
1670
|
+
},
|
|
1671
|
+
|
|
1672
|
+
options: {
|
|
1673
|
+
validate: {
|
|
1674
|
+
options: {
|
|
1675
|
+
stripUnknown: true,
|
|
1676
|
+
abortEarly: false,
|
|
1677
|
+
convert: true
|
|
1678
|
+
},
|
|
1679
|
+
|
|
1680
|
+
async failAction(request, h, err) {
|
|
1681
|
+
await request.flash({ type: 'danger', message: `Invalid gateway request: ${err.message}` });
|
|
1682
|
+
return h.redirect('/admin/gateways').takeover();
|
|
1683
|
+
},
|
|
1684
|
+
|
|
1685
|
+
params: Joi.object({
|
|
1686
|
+
gateway: Joi.string().max(256).required().example('sendgun').description('Gateway ID')
|
|
1687
|
+
})
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
server.route({
|
|
1693
|
+
method: 'GET',
|
|
1694
|
+
path: '/admin/gateways/edit/{gateway}',
|
|
1695
|
+
async handler(request, h) {
|
|
1696
|
+
let gatewayObject = new Gateway({ gateway: request.params.gateway, redis, secret: await getSecret() });
|
|
1697
|
+
let gatewayData = await gatewayObject.loadGatewayData();
|
|
1698
|
+
|
|
1699
|
+
let hasSMTPPass = !!gatewayData.pass;
|
|
1700
|
+
delete gatewayData.pass;
|
|
1701
|
+
|
|
1702
|
+
return h.view(
|
|
1703
|
+
'gateways/edit',
|
|
1704
|
+
{
|
|
1705
|
+
pageTitle: 'Email Gateways',
|
|
1706
|
+
menuGateways: true,
|
|
1707
|
+
wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key]))),
|
|
1708
|
+
values: gatewayData,
|
|
1709
|
+
gatewayData,
|
|
1710
|
+
hasSMTPPass
|
|
1711
|
+
},
|
|
1712
|
+
{
|
|
1713
|
+
layout: 'app'
|
|
1714
|
+
}
|
|
1715
|
+
);
|
|
1716
|
+
},
|
|
1717
|
+
|
|
1718
|
+
options: {
|
|
1719
|
+
validate: {
|
|
1720
|
+
options: {
|
|
1721
|
+
stripUnknown: true,
|
|
1722
|
+
abortEarly: false,
|
|
1723
|
+
convert: true
|
|
1724
|
+
},
|
|
1725
|
+
|
|
1726
|
+
async failAction(request, h, err) {
|
|
1727
|
+
await request.flash({ type: 'danger', message: `Invalid gateway request: ${err.message}` });
|
|
1728
|
+
return h.redirect('/admin/gateways').takeover();
|
|
1729
|
+
},
|
|
1730
|
+
|
|
1731
|
+
params: Joi.object({
|
|
1732
|
+
gateway: Joi.string().max(256).required().example('sendgun').description('Gateway ID')
|
|
1733
|
+
})
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
server.route({
|
|
1739
|
+
method: 'POST',
|
|
1740
|
+
path: '/admin/gateways/new',
|
|
1741
|
+
async handler(request, h) {
|
|
1742
|
+
try {
|
|
1743
|
+
let gatewayData = {
|
|
1744
|
+
gateway: request.payload.gateway || null,
|
|
1745
|
+
name: request.payload.name || null,
|
|
1746
|
+
host: request.payload.host || null,
|
|
1747
|
+
port: request.payload.port || null,
|
|
1748
|
+
secure: request.payload.secure || null,
|
|
1749
|
+
user: request.payload.user || null,
|
|
1750
|
+
pass: request.payload.pass || null,
|
|
1751
|
+
tls: {}
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
let gatewayObject = new Gateway({ redis, secret: await getSecret() });
|
|
1755
|
+
let result = await gatewayObject.create(gatewayData);
|
|
1756
|
+
|
|
1757
|
+
if (result.state === 'new') {
|
|
1758
|
+
await request.flash({ type: 'success', message: `Added new SMTP gateway`, result });
|
|
1759
|
+
} else {
|
|
1760
|
+
await request.flash({ type: 'success', message: `Updated SMTP gateway`, result });
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
return h.redirect(`/admin/gateways/gateway/${encodeURIComponent(result.gateway)}?state=${result.state}`);
|
|
1764
|
+
} catch (err) {
|
|
1765
|
+
await request.flash({ type: 'danger', message: `Couldn't add gateway. Try again.` });
|
|
1766
|
+
request.logger.error({ msg: 'Failed to add new gateway', err });
|
|
1767
|
+
|
|
1768
|
+
return h.view(
|
|
1769
|
+
'gateways/new',
|
|
1770
|
+
{
|
|
1771
|
+
pageTitle: 'Email Gateways',
|
|
1772
|
+
menuGateways: true,
|
|
1773
|
+
wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key])))
|
|
1774
|
+
},
|
|
1775
|
+
{
|
|
1776
|
+
layout: 'app'
|
|
1777
|
+
}
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
},
|
|
1781
|
+
|
|
1782
|
+
options: {
|
|
1783
|
+
validate: {
|
|
1784
|
+
options: {
|
|
1785
|
+
stripUnknown: true,
|
|
1786
|
+
abortEarly: false,
|
|
1787
|
+
convert: true
|
|
1788
|
+
},
|
|
1789
|
+
|
|
1790
|
+
async failAction(request, h, err) {
|
|
1791
|
+
let errors = {};
|
|
1792
|
+
|
|
1793
|
+
if (err.details) {
|
|
1794
|
+
err.details.forEach(detail => {
|
|
1795
|
+
if (!errors[detail.path]) {
|
|
1796
|
+
errors[detail.path] = detail.message;
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
await request.flash({ type: 'danger', message: `Couldn't add gateway. Try again.` });
|
|
1802
|
+
request.logger.error({ msg: 'Failed to add new gateway', err });
|
|
1803
|
+
|
|
1804
|
+
return h
|
|
1805
|
+
.view(
|
|
1806
|
+
'gateways/new',
|
|
1807
|
+
{
|
|
1808
|
+
pageTitle: 'Email Gateways',
|
|
1809
|
+
menuGateways: true,
|
|
1810
|
+
wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key]))),
|
|
1811
|
+
|
|
1812
|
+
errors
|
|
1813
|
+
},
|
|
1814
|
+
{
|
|
1815
|
+
layout: 'app'
|
|
1816
|
+
}
|
|
1817
|
+
)
|
|
1818
|
+
.takeover();
|
|
1819
|
+
},
|
|
1820
|
+
|
|
1821
|
+
payload: Joi.object({
|
|
1822
|
+
gateway: Joi.string().empty('').trim().max(256).default(null).example('sendgun').description('Gateway ID').label('Gateway ID'),
|
|
1823
|
+
|
|
1824
|
+
name: Joi.string().empty('').max(256).example('John Smith').description('Account Name').label('Gateway Name').required(),
|
|
1825
|
+
|
|
1826
|
+
user: Joi.string().empty('').trim().max(1024).default(null).label('UserName'),
|
|
1827
|
+
pass: Joi.string().empty('').max(1024).default(null).label('Password'),
|
|
1828
|
+
|
|
1829
|
+
host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to').label('Hostname').required(),
|
|
1830
|
+
port: Joi.number()
|
|
1831
|
+
.integer()
|
|
1832
|
+
.min(1)
|
|
1833
|
+
.max(64 * 1024)
|
|
1834
|
+
.example(465)
|
|
1835
|
+
.description('Service port number')
|
|
1836
|
+
.label('Port')
|
|
1837
|
+
.required(),
|
|
1838
|
+
|
|
1839
|
+
secure: Joi.boolean()
|
|
1840
|
+
.truthy('Y', 'true', '1', 'on')
|
|
1841
|
+
.falsy('N', 'false', 0, '')
|
|
1842
|
+
.default(false)
|
|
1843
|
+
.example(true)
|
|
1844
|
+
.description('Should connection use TLS. Usually true for port 465')
|
|
1845
|
+
.label('TLS')
|
|
1846
|
+
})
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
server.route({
|
|
1852
|
+
method: 'POST',
|
|
1853
|
+
path: '/admin/gateways/edit',
|
|
1854
|
+
async handler(request, h) {
|
|
1855
|
+
try {
|
|
1856
|
+
let gatewayData = {
|
|
1857
|
+
gateway: request.payload.gateway || null,
|
|
1858
|
+
name: request.payload.name || null,
|
|
1859
|
+
host: request.payload.host || null,
|
|
1860
|
+
port: request.payload.port || null,
|
|
1861
|
+
secure: request.payload.secure || null,
|
|
1862
|
+
user: request.payload.user || null
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
if (request.payload.pass) {
|
|
1866
|
+
gatewayData.pass = request.payload.pass;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
if (!request.payload.user && !request.payload.pass) {
|
|
1870
|
+
gatewayData.pass = null;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
let gatewayObject = new Gateway({ gateway: request.payload.gateway, redis, secret: await getSecret() });
|
|
1874
|
+
let result = await gatewayObject.update(gatewayData);
|
|
1875
|
+
|
|
1876
|
+
await request.flash({ type: 'success', message: `Updated SMTP gateway`, result });
|
|
1877
|
+
|
|
1878
|
+
return h.redirect(`/admin/gateways/gateway/${encodeURIComponent(result.gateway)}`);
|
|
1879
|
+
} catch (err) {
|
|
1880
|
+
await request.flash({ type: 'danger', message: `Couldn't save gateway. Try again.` });
|
|
1881
|
+
request.logger.error({ msg: 'Failed to update gateway', err });
|
|
1882
|
+
|
|
1883
|
+
let gatewayObject = new Gateway({ gateway: request.payload.gateway, redis, secret: await getSecret() });
|
|
1884
|
+
let gatewayData = await gatewayObject.loadGatewayData();
|
|
1885
|
+
|
|
1886
|
+
let hasSMTPPass = !!gatewayData.pass;
|
|
1887
|
+
|
|
1888
|
+
return h.view(
|
|
1889
|
+
'gateways/edit',
|
|
1890
|
+
{
|
|
1891
|
+
pageTitle: 'Email Gateways',
|
|
1892
|
+
menuGateways: true,
|
|
1893
|
+
wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key]))),
|
|
1894
|
+
hasSMTPPass,
|
|
1895
|
+
gatewayData
|
|
1896
|
+
},
|
|
1897
|
+
{
|
|
1898
|
+
layout: 'app'
|
|
1899
|
+
}
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
},
|
|
1903
|
+
|
|
1904
|
+
options: {
|
|
1905
|
+
validate: {
|
|
1906
|
+
options: {
|
|
1907
|
+
stripUnknown: true,
|
|
1908
|
+
abortEarly: false,
|
|
1909
|
+
convert: true
|
|
1910
|
+
},
|
|
1911
|
+
|
|
1912
|
+
async failAction(request, h, err) {
|
|
1913
|
+
let errors = {};
|
|
1914
|
+
|
|
1915
|
+
if (err.details) {
|
|
1916
|
+
err.details.forEach(detail => {
|
|
1917
|
+
if (!errors[detail.path]) {
|
|
1918
|
+
errors[detail.path] = detail.message;
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
await request.flash({ type: 'danger', message: `Couldn't save gateway. Try again.` });
|
|
1924
|
+
request.logger.error({ msg: 'Failed to update gateway', err });
|
|
1925
|
+
|
|
1926
|
+
let gatewayObject = new Gateway({ gateway: request.payload.gateway, redis, secret: await getSecret() });
|
|
1927
|
+
let gatewayData = await gatewayObject.loadGatewayData();
|
|
1928
|
+
|
|
1929
|
+
let hasSMTPPass = !!gatewayData.pass;
|
|
1930
|
+
|
|
1931
|
+
return h
|
|
1932
|
+
.view(
|
|
1933
|
+
'gateways/edit',
|
|
1934
|
+
{
|
|
1935
|
+
pageTitle: 'Email Gateways',
|
|
1936
|
+
menuGateways: true,
|
|
1937
|
+
wellKnownServices: JSON.stringify(Object.keys(wellKnownServices).map(key => Object.assign({ key }, wellKnownServices[key]))),
|
|
1938
|
+
hasSMTPPass,
|
|
1939
|
+
gatewayData,
|
|
1940
|
+
|
|
1941
|
+
errors
|
|
1942
|
+
},
|
|
1943
|
+
{
|
|
1944
|
+
layout: 'app'
|
|
1945
|
+
}
|
|
1946
|
+
)
|
|
1947
|
+
.takeover();
|
|
1948
|
+
},
|
|
1949
|
+
|
|
1950
|
+
payload: Joi.object({
|
|
1951
|
+
gateway: Joi.string().empty('').trim().max(256).default(null).example('sendgun').description('Gateway ID').label('Gateway ID').required(),
|
|
1952
|
+
|
|
1953
|
+
name: Joi.string().empty('').max(256).example('John Smith').description('Account Name').label('Gateway Name').required(),
|
|
1954
|
+
|
|
1955
|
+
user: Joi.string().empty('').trim().max(1024).default(null).label('UserName'),
|
|
1956
|
+
pass: Joi.string().empty('').max(1024).default(null).label('Password'),
|
|
1957
|
+
|
|
1958
|
+
host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to').label('Hostname').required(),
|
|
1959
|
+
port: Joi.number()
|
|
1960
|
+
.integer()
|
|
1961
|
+
.min(1)
|
|
1962
|
+
.max(64 * 1024)
|
|
1963
|
+
.example(465)
|
|
1964
|
+
.description('Service port number')
|
|
1965
|
+
.label('Port')
|
|
1966
|
+
.required(),
|
|
1967
|
+
|
|
1968
|
+
secure: Joi.boolean()
|
|
1969
|
+
.truthy('Y', 'true', '1', 'on')
|
|
1970
|
+
.falsy('N', 'false', 0, '')
|
|
1971
|
+
.default(false)
|
|
1972
|
+
.example(true)
|
|
1973
|
+
.description('Should connection use TLS. Usually true for port 465')
|
|
1974
|
+
.label('TLS')
|
|
1975
|
+
})
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
server.route({
|
|
1981
|
+
method: 'POST',
|
|
1982
|
+
path: '/admin/gateways/test',
|
|
1983
|
+
async handler(request) {
|
|
1984
|
+
let { gateway, host, port, user, pass, secure } = request.payload;
|
|
1985
|
+
|
|
1986
|
+
try {
|
|
1987
|
+
if (user && !pass && gateway) {
|
|
1988
|
+
let gatewayObject = new Gateway({ gateway, redis, secret: await getSecret() });
|
|
1989
|
+
try {
|
|
1990
|
+
let gatewayData = await gatewayObject.loadGatewayData();
|
|
1991
|
+
if (gatewayData) {
|
|
1992
|
+
pass = gatewayData.pass || '';
|
|
1993
|
+
}
|
|
1994
|
+
} catch (err) {
|
|
1995
|
+
// ignore
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
let accountData = {
|
|
2000
|
+
smtp: {
|
|
2001
|
+
host,
|
|
2002
|
+
port,
|
|
2003
|
+
secure,
|
|
2004
|
+
auth:
|
|
2005
|
+
user || pass
|
|
2006
|
+
? {
|
|
2007
|
+
user,
|
|
2008
|
+
pass: pass || ''
|
|
2009
|
+
}
|
|
2010
|
+
: false
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
|
|
2014
|
+
let verifyResult = await verifyAccountInfo(redis, accountData, request.logger.child({ gateway, action: 'verify-gateway' }));
|
|
2015
|
+
|
|
2016
|
+
if (verifyResult) {
|
|
2017
|
+
if (verifyResult.smtp && verifyResult.smtp.error && verifyResult.smtp.code) {
|
|
2018
|
+
switch (verifyResult.smtp.code) {
|
|
2019
|
+
case 'EDNS':
|
|
2020
|
+
verifyResult.smtp.error = request.app.gt.gettext('Server hostname was not found');
|
|
2021
|
+
break;
|
|
2022
|
+
case 'EAUTH':
|
|
2023
|
+
verifyResult.smtp.error = request.app.gt.gettext('Invalid username or password');
|
|
2024
|
+
break;
|
|
2025
|
+
case 'ESOCKET':
|
|
2026
|
+
if (/openssl/.test(verifyResult.smtp.error)) {
|
|
2027
|
+
verifyResult.smtp.error = request.app.gt.gettext('TLS protocol error');
|
|
2028
|
+
}
|
|
2029
|
+
break;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
return verifyResult.smtp;
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
request.logger.error({ msg: 'Failed posting request', host, port, user, pass: !!pass, err });
|
|
2037
|
+
return {
|
|
2038
|
+
success: false,
|
|
2039
|
+
error: err.message
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
},
|
|
2043
|
+
options: {
|
|
2044
|
+
tags: ['test'],
|
|
2045
|
+
validate: {
|
|
2046
|
+
options: {
|
|
2047
|
+
stripUnknown: true,
|
|
2048
|
+
abortEarly: false,
|
|
2049
|
+
convert: true
|
|
2050
|
+
},
|
|
2051
|
+
|
|
2052
|
+
failAction,
|
|
2053
|
+
|
|
2054
|
+
payload: Joi.object({
|
|
2055
|
+
gateway: Joi.string().empty('').trim().max(256).example('sendgun').description('Gateway ID'),
|
|
2056
|
+
user: Joi.string().empty('').trim().max(1024).label('UserName'),
|
|
2057
|
+
pass: Joi.string().empty('').max(1024).label('Password'),
|
|
2058
|
+
host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to').label('Hostname'),
|
|
2059
|
+
port: Joi.number()
|
|
2060
|
+
.integer()
|
|
2061
|
+
.min(1)
|
|
2062
|
+
.max(64 * 1024)
|
|
2063
|
+
.example(465)
|
|
2064
|
+
.description('Service port number')
|
|
2065
|
+
.label('Port'),
|
|
2066
|
+
secure: Joi.boolean()
|
|
2067
|
+
.truthy('Y', 'true', '1', 'on')
|
|
2068
|
+
.falsy('N', 'false', 0, '')
|
|
2069
|
+
.default(false)
|
|
2070
|
+
.example(true)
|
|
2071
|
+
.description('Should connection use TLS. Usually true for port 465')
|
|
2072
|
+
.label('TLS')
|
|
2073
|
+
})
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
server.route({
|
|
2079
|
+
method: 'POST',
|
|
2080
|
+
path: '/admin/gateways/delete/{gateway}',
|
|
2081
|
+
async handler(request, h) {
|
|
2082
|
+
try {
|
|
2083
|
+
let gatewayObject = new Gateway({ redis, gateway: request.params.gateway, secret: await getSecret() });
|
|
2084
|
+
|
|
2085
|
+
let deleted = await gatewayObject.delete();
|
|
2086
|
+
if (deleted) {
|
|
2087
|
+
await request.flash({ type: 'info', message: `Gateway deleted` });
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
return h.redirect('/admin/gateways');
|
|
2091
|
+
} catch (err) {
|
|
2092
|
+
await request.flash({ type: 'danger', message: `Couldn't delete gateway. Try again.` });
|
|
2093
|
+
request.logger.error({ msg: 'Failed to delete the gateway', err, gateway: request.payload.gateway, remoteAddress: request.app.ip });
|
|
2094
|
+
return h.redirect(`/admin/gateways/${request.params.gateway}`);
|
|
2095
|
+
}
|
|
2096
|
+
},
|
|
2097
|
+
options: {
|
|
2098
|
+
validate: {
|
|
2099
|
+
options: {
|
|
2100
|
+
stripUnknown: true,
|
|
2101
|
+
abortEarly: false,
|
|
2102
|
+
convert: true
|
|
2103
|
+
},
|
|
2104
|
+
|
|
2105
|
+
async failAction(request, h, err) {
|
|
2106
|
+
await request.flash({ type: 'danger', message: `Couldn't delete gateway. Try again.` });
|
|
2107
|
+
request.logger.error({ msg: 'Failed to delete delete the gateway', err });
|
|
2108
|
+
|
|
2109
|
+
return h.redirect('/admin/gateways').takeover();
|
|
2110
|
+
},
|
|
2111
|
+
|
|
2112
|
+
params: Joi.object({
|
|
2113
|
+
gateway: Joi.string().max(256).required().example('sendgun').description('Gateway ID')
|
|
2114
|
+
})
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
// Token routes
|
|
2120
|
+
|
|
2121
|
+
server.route({
|
|
2122
|
+
method: 'GET',
|
|
2123
|
+
path: '/admin/tokens',
|
|
2124
|
+
async handler(request, h) {
|
|
2125
|
+
let accountData;
|
|
2126
|
+
if (request.query.account) {
|
|
2127
|
+
let accountObject = new Account({ redis, account: request.query.account });
|
|
2128
|
+
accountData = await accountObject.loadAccountData();
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
const data = await tokens.list(request.query.account, request.query.page - 1, request.query.pageSize);
|
|
2132
|
+
|
|
2133
|
+
data.tokens.forEach(entry => {
|
|
2134
|
+
entry.access = entry.access || {};
|
|
2135
|
+
entry.access.timeStr =
|
|
2136
|
+
entry.access && entry.access.time && typeof entry.access.time.toISOString === 'function' ? entry.access.time.toISOString() : null;
|
|
2137
|
+
entry.scopes = entry.scopes
|
|
2138
|
+
? entry.scopes.map((scope, i) => ({
|
|
2139
|
+
name: scope === '*' ? 'all scopes' : scope,
|
|
2140
|
+
first: !i
|
|
2141
|
+
}))
|
|
2142
|
+
: false;
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
let nextPage = false;
|
|
2146
|
+
let prevPage = false;
|
|
2147
|
+
|
|
2148
|
+
let getPagingUrl = page => {
|
|
2149
|
+
let url = new URL(`admin/tokens`, 'http://localhost');
|
|
2150
|
+
|
|
2151
|
+
if (page) {
|
|
2152
|
+
url.searchParams.append('page', page);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
if (request.query.pageSize !== DEFAULT_PAGE_SIZE) {
|
|
2156
|
+
url.searchParams.append('pageSize', request.query.pageSize);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
return url.pathname + url.search;
|
|
2160
|
+
};
|
|
2161
|
+
|
|
2162
|
+
if (data.pages > data.page + 1) {
|
|
2163
|
+
nextPage = getPagingUrl(data.page + 2);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
if (data.page > 0) {
|
|
2167
|
+
prevPage = getPagingUrl(data.page);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
let newLink = new URL('/admin/tokens/new', 'http://localhost');
|
|
2171
|
+
if (request.query.account) {
|
|
2172
|
+
newLink.searchParams.append('account', request.query.account);
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
return h.view(
|
|
2176
|
+
'tokens/index',
|
|
2177
|
+
{
|
|
2178
|
+
pageTitle: 'Access Tokens',
|
|
2179
|
+
menuTokens: true,
|
|
2180
|
+
data,
|
|
2181
|
+
|
|
2182
|
+
account: accountData,
|
|
2183
|
+
|
|
2184
|
+
showPaging: data.pages > 1,
|
|
2185
|
+
nextPage,
|
|
2186
|
+
prevPage,
|
|
2187
|
+
firstPage: data.page === 0,
|
|
2188
|
+
pageLinks: new Array(data.pages || 1).fill(0).map((z, i) => ({
|
|
2189
|
+
url: getPagingUrl(i + 1, request.query.state, request.query.query),
|
|
2190
|
+
title: i + 1,
|
|
2191
|
+
active: i === data.page
|
|
2192
|
+
})),
|
|
2193
|
+
|
|
2194
|
+
newLink: newLink.pathname + newLink.search
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
layout: 'app'
|
|
2198
|
+
}
|
|
2199
|
+
);
|
|
2200
|
+
},
|
|
2201
|
+
|
|
2202
|
+
options: {
|
|
2203
|
+
validate: {
|
|
2204
|
+
options: {
|
|
2205
|
+
stripUnknown: true,
|
|
2206
|
+
abortEarly: false,
|
|
2207
|
+
convert: true
|
|
2208
|
+
},
|
|
2209
|
+
|
|
2210
|
+
async failAction(request, h /*, err*/) {
|
|
2211
|
+
return h.redirect('/admin/tokens').takeover();
|
|
2212
|
+
},
|
|
2213
|
+
|
|
2214
|
+
query: Joi.object({
|
|
2215
|
+
account: accountIdSchema.default(null),
|
|
2216
|
+
page: Joi.number().integer().min(1).max(1000000).default(1),
|
|
2217
|
+
pageSize: Joi.number().integer().min(1).max(250).default(DEFAULT_PAGE_SIZE)
|
|
2218
|
+
})
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
2222
|
+
|
|
2223
|
+
server.route({
|
|
2224
|
+
method: 'GET',
|
|
2225
|
+
path: '/admin/tokens/new',
|
|
2226
|
+
async handler(request, h) {
|
|
2227
|
+
let accountTokensLink = new URL('/admin/tokens', 'http://localhost');
|
|
2228
|
+
|
|
2229
|
+
let accountData;
|
|
2230
|
+
if (request.query.account) {
|
|
2231
|
+
let accountObject = new Account({ redis, account: request.query.account });
|
|
2232
|
+
accountData = await accountObject.loadAccountData();
|
|
2233
|
+
accountTokensLink.searchParams.append('account', request.query.account);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
return h.view(
|
|
2237
|
+
'tokens/new',
|
|
2238
|
+
{
|
|
2239
|
+
pageTitle: 'Access Tokens',
|
|
2240
|
+
menuTokens: true,
|
|
2241
|
+
values: {
|
|
2242
|
+
scopesAll: true,
|
|
2243
|
+
allAccounts: !request.query.account,
|
|
2244
|
+
account: request.query.account
|
|
2245
|
+
},
|
|
2246
|
+
account: accountData,
|
|
2247
|
+
accountTokensLink: accountTokensLink.pathname + accountTokensLink.search
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
layout: 'app'
|
|
2251
|
+
}
|
|
2252
|
+
);
|
|
2253
|
+
},
|
|
2254
|
+
|
|
2255
|
+
options: {
|
|
2256
|
+
validate: {
|
|
2257
|
+
options: {
|
|
2258
|
+
stripUnknown: true,
|
|
2259
|
+
abortEarly: false,
|
|
2260
|
+
convert: true
|
|
2261
|
+
},
|
|
2262
|
+
|
|
2263
|
+
async failAction(request, h /*, err*/) {
|
|
2264
|
+
return h.redirect('/admin/tokens').takeover();
|
|
2265
|
+
},
|
|
2266
|
+
|
|
2267
|
+
query: Joi.object({
|
|
2268
|
+
account: accountIdSchema.default(null)
|
|
2269
|
+
})
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
});
|
|
2273
|
+
|
|
2274
|
+
server.route({
|
|
2275
|
+
method: 'POST',
|
|
2276
|
+
path: '/admin/tokens/new',
|
|
2277
|
+
|
|
2278
|
+
async handler(request) {
|
|
2279
|
+
try {
|
|
2280
|
+
let data = {
|
|
2281
|
+
ip: request.app.ip,
|
|
2282
|
+
remoteAddress: request.app.ip,
|
|
2283
|
+
description: request.payload.description,
|
|
2284
|
+
scopes: request.payload.scopes
|
|
2285
|
+
};
|
|
2286
|
+
|
|
2287
|
+
if (request.payload.account) {
|
|
2288
|
+
let accountObject = new Account({ redis, account: request.payload.account });
|
|
2289
|
+
await accountObject.loadAccountData();
|
|
2290
|
+
data.account = request.payload.account;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
let token = await tokens.provision(data);
|
|
2294
|
+
|
|
2295
|
+
return {
|
|
2296
|
+
success: true,
|
|
2297
|
+
token
|
|
2298
|
+
};
|
|
2299
|
+
} catch (err) {
|
|
2300
|
+
request.logger.error({ msg: 'Failed to generate token', err, remoteAddress: request.app.ip, description: request.payload.description });
|
|
2301
|
+
if (Boom.isBoom(err)) {
|
|
2302
|
+
return Object.assign({ success: false }, err.output.payload);
|
|
2303
|
+
}
|
|
2304
|
+
return { success: false, error: err.code || 'Error', message: err.message };
|
|
2305
|
+
}
|
|
2306
|
+
},
|
|
2307
|
+
options: {
|
|
2308
|
+
validate: {
|
|
2309
|
+
options: {
|
|
2310
|
+
stripUnknown: true,
|
|
2311
|
+
abortEarly: false,
|
|
2312
|
+
convert: true
|
|
2313
|
+
},
|
|
2314
|
+
|
|
2315
|
+
failAction,
|
|
2316
|
+
|
|
2317
|
+
payload: Joi.object({
|
|
2318
|
+
description: Joi.string().empty('').trim().max(1024).required().example('Token description').description('Token description'),
|
|
2319
|
+
scopes: Joi.array()
|
|
2320
|
+
.items(Joi.string().valid('*', 'api', 'metrics', 'smtp', 'imap-proxy'))
|
|
2321
|
+
.required()
|
|
2322
|
+
.label('Scopes'),
|
|
2323
|
+
account: accountIdSchema.default(null)
|
|
2324
|
+
})
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
});
|
|
2328
|
+
|
|
2329
|
+
server.route({
|
|
2330
|
+
method: 'POST',
|
|
2331
|
+
path: '/admin/tokens/delete',
|
|
2332
|
+
async handler(request, h) {
|
|
2333
|
+
try {
|
|
2334
|
+
let deleted = await tokens.delete(request.payload.token, { remoteAddress: request.app.ip });
|
|
2335
|
+
if (deleted) {
|
|
2336
|
+
await request.flash({ type: 'info', message: `Token deleted` });
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
return h.redirect('/admin/tokens');
|
|
2340
|
+
} catch (err) {
|
|
2341
|
+
await request.flash({ type: 'danger', message: `Couldn't delete token. Try again.` });
|
|
2342
|
+
request.logger.error({ msg: 'Failed to delete access token', err, token: request.payload.token, remoteAddress: request.app.ip });
|
|
2343
|
+
return h.redirect('/admin/tokens');
|
|
2344
|
+
}
|
|
2345
|
+
},
|
|
2346
|
+
options: {
|
|
2347
|
+
validate: {
|
|
2348
|
+
options: {
|
|
2349
|
+
stripUnknown: true,
|
|
2350
|
+
abortEarly: false,
|
|
2351
|
+
convert: true
|
|
2352
|
+
},
|
|
2353
|
+
|
|
2354
|
+
async failAction(request, h, err) {
|
|
2355
|
+
await request.flash({ type: 'danger', message: `Couldn't delete token. Try again.` });
|
|
2356
|
+
request.logger.error({ msg: 'Failed to delete access token', err });
|
|
2357
|
+
|
|
2358
|
+
return h.redirect('/admin/tokens').takeover();
|
|
2359
|
+
},
|
|
2360
|
+
|
|
2361
|
+
payload: Joi.object({ token: Joi.string().length(64).hex().required().example('123456').description('Access token') })
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
module.exports = init;
|