emailengine-app 2.68.1 → 2.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy.yml +8 -3
- package/.github/workflows/release.yaml +6 -0
- package/CHANGELOG.md +59 -0
- package/Gruntfile.js +3 -1
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +7 -1
- package/getswagger.sh +40 -4
- package/gettext-extract.js +163 -0
- package/lib/account.js +135 -72
- package/lib/api-routes/account-routes.js +684 -106
- package/lib/api-routes/blocklist-routes.js +344 -0
- package/lib/api-routes/chat-routes.js +32 -14
- package/lib/api-routes/delivery-test-routes.js +346 -0
- package/lib/api-routes/export-routes.js +28 -14
- package/lib/api-routes/gateway-routes.js +427 -0
- package/lib/api-routes/license-routes.js +156 -0
- package/lib/api-routes/mailbox-routes.js +344 -0
- package/lib/api-routes/message-routes.js +221 -187
- package/lib/api-routes/oauth2-app-routes.js +697 -0
- package/lib/api-routes/outbox-routes.js +185 -0
- package/lib/api-routes/pubsub-routes.js +102 -0
- package/lib/api-routes/route-helpers.js +58 -0
- package/lib/api-routes/settings-routes.js +357 -0
- package/lib/api-routes/stats-routes.js +111 -0
- package/lib/api-routes/submit-routes.js +461 -0
- package/lib/api-routes/template-routes.js +60 -75
- package/lib/api-routes/token-routes.js +297 -0
- package/lib/api-routes/webhook-route-routes.js +181 -0
- package/lib/autodetect-imap-settings.js +0 -2
- package/lib/consts.js +5 -0
- package/lib/email-client/base-client.js +28 -6
- package/lib/email-client/gmail-client.js +133 -112
- package/lib/email-client/imap/mailbox.js +34 -11
- package/lib/email-client/imap/subconnection.js +20 -13
- package/lib/email-client/imap/sync-operations.js +131 -3
- package/lib/email-client/imap-client.js +152 -75
- package/lib/email-client/notification-handler.js +1 -4
- package/lib/email-client/outlook-client.js +134 -75
- package/lib/export.js +97 -20
- package/lib/feature-flags.js +2 -2
- package/lib/gateway.js +4 -9
- package/lib/get-raw-email.js +5 -5
- package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
- package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
- package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -24
- package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- package/lib/logger.js +24 -21
- package/lib/message-port-stream.js +113 -16
- package/lib/metrics-collector.js +0 -2
- package/lib/oauth2-apps.js +13 -4
- package/lib/outbox.js +24 -40
- package/lib/redis-operations.js +1 -1
- package/lib/reject-worker-calls.js +42 -0
- package/lib/routes-ui.js +37 -8778
- package/lib/schemas.js +429 -84
- package/lib/sentry.js +139 -0
- package/lib/settings.js +9 -3
- package/lib/stream-encrypt.js +1 -1
- package/lib/templates.js +1 -1
- package/lib/tokens.js +5 -3
- package/lib/tools.js +70 -4
- package/lib/ui-routes/account-routes.js +45 -212
- package/lib/ui-routes/admin-config-routes.js +928 -489
- package/lib/ui-routes/admin-entities-routes.js +1 -0
- package/lib/ui-routes/auth-routes.js +1339 -0
- package/lib/ui-routes/dashboard-routes.js +188 -0
- package/lib/ui-routes/document-store-routes.js +800 -0
- package/lib/ui-routes/export-routes.js +217 -0
- package/lib/ui-routes/internals-routes.js +354 -0
- package/lib/ui-routes/network-config-routes.js +759 -0
- package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +369 -91
- package/lib/ui-routes/route-helpers.js +314 -0
- package/lib/ui-routes/smtp-test-routes.js +236 -0
- package/lib/ui-routes/unsubscribe-routes.js +232 -0
- package/lib/webhook-request.js +36 -0
- package/lib/webhooks.js +8 -4
- package/package.json +13 -12
- package/sbom.json +1 -1
- package/server.js +222 -39
- package/static/licenses.html +160 -300
- package/translations/messages.pot +112 -132
- package/update-info.sh +19 -1
- package/views/config/logging.hbs +48 -0
- package/views/dashboard.hbs +7 -26
- package/views/internals/index.hbs +15 -0
- package/views/tokens/index.hbs +9 -0
- package/workers/api.js +200 -4424
- package/workers/documents.js +2 -22
- package/workers/export.js +103 -104
- package/workers/imap-proxy.js +3 -23
- package/workers/imap.js +32 -36
- package/workers/smtp.js +2 -22
- package/workers/submit.js +26 -35
- package/workers/webhooks.js +9 -43
package/workers/api.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const { parentPort } = require('worker_threads');
|
|
3
|
+
const { parentPort, workerData } = require('worker_threads');
|
|
6
4
|
|
|
7
5
|
const packageData = require('../package.json');
|
|
8
6
|
const config = require('@zone-eu/wild-config');
|
|
@@ -24,49 +22,24 @@ const eulaText = marked.parse(
|
|
|
24
22
|
const {
|
|
25
23
|
getByteSize,
|
|
26
24
|
getDuration,
|
|
27
|
-
getStats,
|
|
28
25
|
flash,
|
|
29
26
|
failAction,
|
|
30
|
-
verifyAccountInfo,
|
|
31
27
|
isEmail,
|
|
32
|
-
getLogs,
|
|
33
28
|
getWorkerCount,
|
|
34
29
|
runPrechecks,
|
|
35
30
|
matcher,
|
|
36
31
|
readEnvValue,
|
|
37
|
-
getSignedFormData,
|
|
38
32
|
threadStats,
|
|
39
33
|
hasEnvValue,
|
|
40
34
|
getBoolean,
|
|
41
35
|
loadTlsConfig,
|
|
42
|
-
|
|
43
|
-
reloadHttpProxyAgent,
|
|
36
|
+
maybeReloadHttpProxyAgent,
|
|
44
37
|
resolveOAuthErrorStatus
|
|
45
38
|
} = require('../lib/tools');
|
|
46
39
|
const { matchIp, detectAutomatedRequest } = require('../lib/utils/network');
|
|
47
40
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
Bugsnag.start({
|
|
51
|
-
apiKey: readEnvValue('BUGSNAG_API_KEY'),
|
|
52
|
-
appVersion: packageData.version,
|
|
53
|
-
logger: {
|
|
54
|
-
debug(...args) {
|
|
55
|
-
logger.debug({ msg: args.shift(), worker: 'api', source: 'bugsnag', args: args.length ? args : undefined });
|
|
56
|
-
},
|
|
57
|
-
info(...args) {
|
|
58
|
-
logger.debug({ msg: args.shift(), worker: 'api', source: 'bugsnag', args: args.length ? args : undefined });
|
|
59
|
-
},
|
|
60
|
-
warn(...args) {
|
|
61
|
-
logger.warn({ msg: args.shift(), worker: 'api', source: 'bugsnag', args: args.length ? args : undefined });
|
|
62
|
-
},
|
|
63
|
-
error(...args) {
|
|
64
|
-
logger.error({ msg: args.shift(), worker: 'api', source: 'bugsnag', args: args.length ? args : undefined });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
logger.notifyError = Bugsnag.notify.bind(Bugsnag);
|
|
69
|
-
}
|
|
41
|
+
const { initSentry } = require('../lib/sentry');
|
|
42
|
+
initSentry('api');
|
|
70
43
|
|
|
71
44
|
const Hapi = require('@hapi/hapi');
|
|
72
45
|
const Boom = require('@hapi/boom');
|
|
@@ -84,20 +57,13 @@ const pathlib = require('path');
|
|
|
84
57
|
const crypto = require('crypto');
|
|
85
58
|
const { Transform, finished } = require('stream');
|
|
86
59
|
const { oauth2Apps, OAUTH_PROVIDERS } = require('../lib/oauth2-apps');
|
|
87
|
-
const { verifyOAuth2App } = require('../lib/oauth/verify-app');
|
|
88
60
|
|
|
89
61
|
const handlebars = require('handlebars');
|
|
90
62
|
const AuthBearer = require('hapi-auth-bearer-token');
|
|
91
63
|
const tokens = require('../lib/tokens');
|
|
92
|
-
const { autodetectImapSettings } = require('../lib/autodetect-imap-settings');
|
|
93
64
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
const { lists } = require('../lib/lists');
|
|
97
|
-
|
|
98
|
-
const { redis, documentsQueue, notifyQueue, submitQueue } = require('../lib/db');
|
|
65
|
+
const { redis, documentsQueue } = require('../lib/db');
|
|
99
66
|
const { Account } = require('../lib/account');
|
|
100
|
-
const { Gateway } = require('../lib/gateway');
|
|
101
67
|
const settings = require('../lib/settings');
|
|
102
68
|
|
|
103
69
|
const getSecret = require('../lib/get-secret');
|
|
@@ -114,7 +80,6 @@ const {
|
|
|
114
80
|
TRACK_OPEN_NOTIFY,
|
|
115
81
|
TRACK_CLICK_NOTIFY,
|
|
116
82
|
REDIS_PREFIX,
|
|
117
|
-
MAX_DAYS_STATS,
|
|
118
83
|
RENEW_TLS_AFTER,
|
|
119
84
|
BLOCK_TLS_RENEW,
|
|
120
85
|
TLS_RENEW_CHECK_INTERVAL,
|
|
@@ -128,45 +93,27 @@ const {
|
|
|
128
93
|
NONCE_BYTES
|
|
129
94
|
} = consts;
|
|
130
95
|
|
|
131
|
-
const { fetch: fetchCmd } = require('undici');
|
|
132
|
-
|
|
133
96
|
const templateRoutes = require('../lib/api-routes/template-routes');
|
|
134
97
|
const chatRoutes = require('../lib/api-routes/chat-routes');
|
|
135
98
|
const bullBoardRoutes = require('../lib/api-routes/bull-board-routes');
|
|
136
99
|
const accountRoutes = require('../lib/api-routes/account-routes');
|
|
137
100
|
const messageRoutes = require('../lib/api-routes/message-routes');
|
|
138
101
|
const exportRoutes = require('../lib/api-routes/export-routes');
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
tokenRestrictionsSchema,
|
|
155
|
-
accountIdSchema,
|
|
156
|
-
ipSchema,
|
|
157
|
-
accountPathSchema,
|
|
158
|
-
defaultAccountTypeSchema,
|
|
159
|
-
fromAddressSchema,
|
|
160
|
-
outboxEntrySchema,
|
|
161
|
-
googleProjectIdSchema,
|
|
162
|
-
googleWorkspaceAccountsSchema,
|
|
163
|
-
googleTopicNameSchema,
|
|
164
|
-
googleSubscriptionNameSchema,
|
|
165
|
-
messageReferenceSchema,
|
|
166
|
-
idempotencyKeySchema,
|
|
167
|
-
headerTimeoutSchema,
|
|
168
|
-
pubSubErrorSchema
|
|
169
|
-
} = require('../lib/schemas');
|
|
102
|
+
const pubsubRoutes = require('../lib/api-routes/pubsub-routes');
|
|
103
|
+
const tokenRoutes = require('../lib/api-routes/token-routes');
|
|
104
|
+
const mailboxRoutes = require('../lib/api-routes/mailbox-routes');
|
|
105
|
+
const settingsRoutes = require('../lib/api-routes/settings-routes');
|
|
106
|
+
const statsRoutes = require('../lib/api-routes/stats-routes');
|
|
107
|
+
const licenseRoutes = require('../lib/api-routes/license-routes');
|
|
108
|
+
const outboxRoutes = require('../lib/api-routes/outbox-routes');
|
|
109
|
+
const webhookRouteRoutes = require('../lib/api-routes/webhook-route-routes');
|
|
110
|
+
const oauth2AppRoutes = require('../lib/api-routes/oauth2-app-routes');
|
|
111
|
+
const gatewayRoutes = require('../lib/api-routes/gateway-routes');
|
|
112
|
+
const deliveryTestRoutes = require('../lib/api-routes/delivery-test-routes');
|
|
113
|
+
const blocklistRoutes = require('../lib/api-routes/blocklist-routes');
|
|
114
|
+
const submitRoutes = require('../lib/api-routes/submit-routes');
|
|
115
|
+
|
|
116
|
+
const { imapSchema, smtpSchema, oauth2Schema, accountIdSchema, headerTimeoutSchema, errorResponses } = require('../lib/schemas');
|
|
170
117
|
|
|
171
118
|
const OAuth2ProviderSchema = Joi.string()
|
|
172
119
|
.valid(...Object.keys(OAUTH_PROVIDERS))
|
|
@@ -176,27 +123,14 @@ const OAuth2ProviderSchema = Joi.string()
|
|
|
176
123
|
.label('OAuth2Provider');
|
|
177
124
|
|
|
178
125
|
const AccountTypeSchema = Joi.string()
|
|
179
|
-
.valid(...['imap'].concat(Object.keys(OAUTH_PROVIDERS)).concat('oauth2'))
|
|
126
|
+
.valid(...['imap'].concat(Object.keys(OAUTH_PROVIDERS)).concat(['oauth2', 'delegated', 'sending', 'invalid']))
|
|
180
127
|
.example('outlook')
|
|
181
|
-
.description(
|
|
128
|
+
.description(
|
|
129
|
+
'Account type: "imap" for IMAP accounts, an OAuth2 provider name for OAuth2 accounts, "oauth2" when the OAuth2 application is missing, "delegated" for delegated accounts, "sending" for send-only accounts, "invalid" when delegation cannot be resolved'
|
|
130
|
+
)
|
|
182
131
|
.required()
|
|
183
132
|
.label('AccountType');
|
|
184
133
|
|
|
185
|
-
function flattenOAuthAppMeta(app) {
|
|
186
|
-
if (!app.meta) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
let authFlag = app.meta.authFlag;
|
|
190
|
-
let pubSubFlag = app.meta.pubSubFlag;
|
|
191
|
-
delete app.meta;
|
|
192
|
-
if (authFlag && authFlag.message) {
|
|
193
|
-
app.lastError = { response: authFlag.message };
|
|
194
|
-
}
|
|
195
|
-
if (pubSubFlag && pubSubFlag.message) {
|
|
196
|
-
app.pubSubError = { message: pubSubFlag.message, description: pubSubFlag.description || null };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
134
|
const SUPPORTED_LOCALES = locales.map(locale => locale.locale);
|
|
201
135
|
|
|
202
136
|
const FLAG_SORT_ORDER = ['\\Inbox', '\\Flagged', '\\Sent', '\\Drafts', '\\All', '\\Archive', '\\Junk', '\\Trash'];
|
|
@@ -260,6 +194,15 @@ const API_TLS = hasEnvValue('EENGINE_API_TLS') ? getBoolean(readEnvValue('EENGIN
|
|
|
260
194
|
// Merge TLS settings from config params and environment
|
|
261
195
|
loadTlsConfig(API_TLS, 'EENGINE_API_TLS_');
|
|
262
196
|
|
|
197
|
+
// Per-worker thread metadata. With multiple API workers (EENGINE_WORKERS_API > 1) the
|
|
198
|
+
// main thread assigns each one an index and whether to bind with SO_REUSEPORT. Only
|
|
199
|
+
// worker 0 runs singleton maintenance tasks (e.g. TLS certificate renewal).
|
|
200
|
+
const WORKER_INDEX = (workerData && workerData.workerIndex) || 0;
|
|
201
|
+
const USE_REUSE_PORT = !!(workerData && workerData.reusePort);
|
|
202
|
+
// Worker 0 is the primary; it runs singleton maintenance tasks (e.g. TLS certificate renewal)
|
|
203
|
+
// that must execute exactly once across all API workers.
|
|
204
|
+
const IS_PRIMARY_API_WORKER = WORKER_INDEX === 0;
|
|
205
|
+
|
|
263
206
|
const ADMIN_ACCESS_ADDRESSES = hasEnvValue('EENGINE_ADMIN_ACCESS_ADDRESSES')
|
|
264
207
|
? readEnvValue('EENGINE_ADMIN_ACCESS_ADDRESSES')
|
|
265
208
|
.split(',')
|
|
@@ -542,6 +485,11 @@ parentPort.on('message', message => {
|
|
|
542
485
|
if (message && message.cmd === 'change') {
|
|
543
486
|
publishChangeEvent(message);
|
|
544
487
|
}
|
|
488
|
+
|
|
489
|
+
if (message && message.cmd === 'settings') {
|
|
490
|
+
// Keep this worker's in-memory HTTP proxy agent in sync when proxy settings change
|
|
491
|
+
maybeReloadHttpProxyAgent(message.data);
|
|
492
|
+
}
|
|
545
493
|
});
|
|
546
494
|
|
|
547
495
|
const init = async () => {
|
|
@@ -620,11 +568,8 @@ const init = async () => {
|
|
|
620
568
|
return formatter.format(intVal);
|
|
621
569
|
});
|
|
622
570
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
host: API_HOST,
|
|
626
|
-
tls: API_TLS,
|
|
627
|
-
|
|
571
|
+
// Base Hapi options shared by both the default and SO_REUSEPORT binding paths
|
|
572
|
+
const serverOptions = {
|
|
628
573
|
state: {
|
|
629
574
|
strictHeader: false
|
|
630
575
|
},
|
|
@@ -646,7 +591,27 @@ const init = async () => {
|
|
|
646
591
|
}).unknown()
|
|
647
592
|
}
|
|
648
593
|
}
|
|
649
|
-
}
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// With multiple API workers we provide our own listener and bind it ourselves with
|
|
597
|
+
// SO_REUSEPORT so the kernel load-balances connections. Hapi forbids port/host when
|
|
598
|
+
// autoListen is false and needs a truthy `tls` flag to treat a provided HTTPS
|
|
599
|
+
// listener correctly. The single-worker path keeps Hapi's default binding unchanged.
|
|
600
|
+
let reusePortListener = null;
|
|
601
|
+
if (USE_REUSE_PORT) {
|
|
602
|
+
const http = require('http');
|
|
603
|
+
const https = require('https');
|
|
604
|
+
reusePortListener = API_TLS ? https.createServer(API_TLS) : http.createServer();
|
|
605
|
+
serverOptions.listener = reusePortListener;
|
|
606
|
+
serverOptions.tls = !!API_TLS;
|
|
607
|
+
serverOptions.autoListen = false;
|
|
608
|
+
} else {
|
|
609
|
+
serverOptions.port = API_PORT;
|
|
610
|
+
serverOptions.host = API_HOST;
|
|
611
|
+
serverOptions.tls = API_TLS;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const server = Hapi.server(serverOptions);
|
|
650
615
|
|
|
651
616
|
let assertPreconditionResult;
|
|
652
617
|
server.decorate('toolkit', 'getESClient', async (...args) => await getESClient(...args));
|
|
@@ -1132,6 +1097,12 @@ Include your token in requests using one of these methods:
|
|
|
1132
1097
|
};
|
|
1133
1098
|
}
|
|
1134
1099
|
|
|
1100
|
+
// Bind the token hash (id) to the request logger so it is included in the per-request
|
|
1101
|
+
// log entry, allowing API requests to be correlated to the token that made them.
|
|
1102
|
+
if (request.logger && typeof request.logger.setBindings === 'function') {
|
|
1103
|
+
request.logger.setBindings({ tokenId: tokenData.id, tokenAccount: tokenData.account || null });
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1135
1106
|
if (scope && tokenData.scopes && !tokenData.scopes.includes(scope) && !tokenData.scopes.includes('*')) {
|
|
1136
1107
|
// failed scope validation
|
|
1137
1108
|
logger.error({
|
|
@@ -1620,99 +1591,6 @@ Include your token in requests using one of these methods:
|
|
|
1620
1591
|
}
|
|
1621
1592
|
});
|
|
1622
1593
|
|
|
1623
|
-
server.route({
|
|
1624
|
-
method: 'GET',
|
|
1625
|
-
path: '/v1/pubsub/status',
|
|
1626
|
-
|
|
1627
|
-
async handler(request) {
|
|
1628
|
-
try {
|
|
1629
|
-
let response = await oauth2Apps.list(request.query.page, request.query.pageSize, { pubsub: true });
|
|
1630
|
-
|
|
1631
|
-
let apps = response.apps.map(app => {
|
|
1632
|
-
flattenOAuthAppMeta(app);
|
|
1633
|
-
return { id: app.id, name: app.name || null, lastError: app.lastError || null, pubSubError: app.pubSubError || null };
|
|
1634
|
-
});
|
|
1635
|
-
|
|
1636
|
-
return {
|
|
1637
|
-
total: response.total,
|
|
1638
|
-
page: response.page,
|
|
1639
|
-
pages: response.pages,
|
|
1640
|
-
apps
|
|
1641
|
-
};
|
|
1642
|
-
} catch (err) {
|
|
1643
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
1644
|
-
if (Boom.isBoom(err)) {
|
|
1645
|
-
throw err;
|
|
1646
|
-
}
|
|
1647
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1648
|
-
if (err.code) {
|
|
1649
|
-
error.output.payload.code = err.code;
|
|
1650
|
-
}
|
|
1651
|
-
throw error;
|
|
1652
|
-
}
|
|
1653
|
-
},
|
|
1654
|
-
|
|
1655
|
-
options: {
|
|
1656
|
-
description: 'List Pub/Sub status',
|
|
1657
|
-
notes: 'Lists Pub/Sub enabled OAuth2 applications and their subscription status',
|
|
1658
|
-
tags: ['api', 'OAuth2 Applications'],
|
|
1659
|
-
|
|
1660
|
-
plugins: {},
|
|
1661
|
-
|
|
1662
|
-
auth: {
|
|
1663
|
-
strategy: 'api-token',
|
|
1664
|
-
mode: 'required'
|
|
1665
|
-
},
|
|
1666
|
-
cors: CORS_CONFIG,
|
|
1667
|
-
|
|
1668
|
-
validate: {
|
|
1669
|
-
options: {
|
|
1670
|
-
stripUnknown: false,
|
|
1671
|
-
abortEarly: false,
|
|
1672
|
-
convert: true
|
|
1673
|
-
},
|
|
1674
|
-
failAction,
|
|
1675
|
-
|
|
1676
|
-
query: Joi.object({
|
|
1677
|
-
page: Joi.number()
|
|
1678
|
-
.integer()
|
|
1679
|
-
.min(0)
|
|
1680
|
-
.max(1024 * 1024)
|
|
1681
|
-
.default(0)
|
|
1682
|
-
.example(0)
|
|
1683
|
-
.description('Page number (zero indexed, so use 0 for first page)')
|
|
1684
|
-
.label('PageNumber'),
|
|
1685
|
-
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
|
|
1686
|
-
}).label('PubSubStatusFilter')
|
|
1687
|
-
},
|
|
1688
|
-
|
|
1689
|
-
response: {
|
|
1690
|
-
schema: Joi.object({
|
|
1691
|
-
total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
|
|
1692
|
-
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
1693
|
-
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
1694
|
-
|
|
1695
|
-
apps: Joi.array()
|
|
1696
|
-
.items(
|
|
1697
|
-
Joi.object({
|
|
1698
|
-
id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
1699
|
-
name: Joi.string().allow(null).max(256).example('My Gmail App').description('Display name for the app'),
|
|
1700
|
-
lastError: Joi.object({
|
|
1701
|
-
response: Joi.string().example('Enable the Cloud Pub/Sub API').description('Setup error message')
|
|
1702
|
-
})
|
|
1703
|
-
.allow(null)
|
|
1704
|
-
.description('Setup error from ensurePubsub, if any')
|
|
1705
|
-
.label('PubSubSetupError'),
|
|
1706
|
-
pubSubError: pubSubErrorSchema.allow(null)
|
|
1707
|
-
}).label('PubSubAppStatus')
|
|
1708
|
-
)
|
|
1709
|
-
.label('PubSubAppStatusList')
|
|
1710
|
-
}).label('PubSubStatusResponse'),
|
|
1711
|
-
failAction: 'log'
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
});
|
|
1715
|
-
|
|
1716
1594
|
server.route({
|
|
1717
1595
|
method: 'GET',
|
|
1718
1596
|
path: '/redirect',
|
|
@@ -2657,4282 +2535,152 @@ Include your token in requests using one of these methods:
|
|
|
2657
2535
|
}
|
|
2658
2536
|
});
|
|
2659
2537
|
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
path: '/v1/token',
|
|
2663
|
-
|
|
2664
|
-
async handler(request) {
|
|
2665
|
-
let accountObject = new Account({
|
|
2666
|
-
redis,
|
|
2667
|
-
account: request.payload.account,
|
|
2668
|
-
call,
|
|
2669
|
-
secret: await getSecret(),
|
|
2670
|
-
timeout: request.headers['x-ee-timeout']
|
|
2671
|
-
});
|
|
2672
|
-
|
|
2673
|
-
try {
|
|
2674
|
-
// throws if account does not exist
|
|
2675
|
-
await accountObject.loadAccountData();
|
|
2676
|
-
|
|
2677
|
-
let token = await tokens.provision(Object.assign({}, request.payload, { remoteAddress: request.app.ip }));
|
|
2678
|
-
|
|
2679
|
-
return { token };
|
|
2680
|
-
} catch (err) {
|
|
2681
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
2682
|
-
if (Boom.isBoom(err)) {
|
|
2683
|
-
throw err;
|
|
2684
|
-
}
|
|
2685
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
2686
|
-
if (err.code) {
|
|
2687
|
-
error.output.payload.code = err.code;
|
|
2688
|
-
}
|
|
2689
|
-
throw error;
|
|
2690
|
-
}
|
|
2691
|
-
},
|
|
2692
|
-
|
|
2693
|
-
options: {
|
|
2694
|
-
description: 'Provision an access token',
|
|
2695
|
-
notes: 'Provisions a new access token for an account',
|
|
2696
|
-
tags: ['api', 'Access Tokens'],
|
|
2538
|
+
// setup template routes
|
|
2539
|
+
await templateRoutes({ server, call, CORS_CONFIG });
|
|
2697
2540
|
|
|
2698
|
-
|
|
2541
|
+
// setup "chat with email" routes
|
|
2542
|
+
await chatRoutes({ server, call, CORS_CONFIG });
|
|
2699
2543
|
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2544
|
+
// setup account CRUD routes
|
|
2545
|
+
await accountRoutes({
|
|
2546
|
+
server,
|
|
2547
|
+
call,
|
|
2548
|
+
documentsQueue,
|
|
2549
|
+
oauth2Schema,
|
|
2550
|
+
imapSchema,
|
|
2551
|
+
smtpSchema,
|
|
2552
|
+
CORS_CONFIG,
|
|
2553
|
+
AccountTypeSchema,
|
|
2554
|
+
OAuth2ProviderSchema,
|
|
2555
|
+
metrics
|
|
2556
|
+
});
|
|
2705
2557
|
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2558
|
+
// setup message routes
|
|
2559
|
+
await messageRoutes({
|
|
2560
|
+
server,
|
|
2561
|
+
call,
|
|
2562
|
+
CORS_CONFIG,
|
|
2563
|
+
MAX_ATTACHMENT_SIZE,
|
|
2564
|
+
MAX_BODY_SIZE,
|
|
2565
|
+
MAX_PAYLOAD_TIMEOUT
|
|
2566
|
+
});
|
|
2713
2567
|
|
|
2714
|
-
|
|
2715
|
-
|
|
2568
|
+
// setup export routes
|
|
2569
|
+
await exportRoutes({
|
|
2570
|
+
server,
|
|
2571
|
+
CORS_CONFIG
|
|
2572
|
+
});
|
|
2716
2573
|
|
|
2717
|
-
|
|
2574
|
+
// setup Pub/Sub status route
|
|
2575
|
+
await pubsubRoutes({ server, CORS_CONFIG });
|
|
2718
2576
|
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
.single()
|
|
2722
|
-
.default(['api'])
|
|
2723
|
-
.required()
|
|
2724
|
-
.description(
|
|
2725
|
-
'Token permission scopes: "api" for REST API access, "smtp" for SMTP submission, "imap-proxy" for IMAP proxy authentication'
|
|
2726
|
-
)
|
|
2727
|
-
.label('Scopes'),
|
|
2577
|
+
// setup access token routes
|
|
2578
|
+
await tokenRoutes({ server, call, CORS_CONFIG });
|
|
2728
2579
|
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
.max(1024 * 1024)
|
|
2732
|
-
.custom((value, helpers) => {
|
|
2733
|
-
try {
|
|
2734
|
-
// check if parsing fails
|
|
2735
|
-
JSON.parse(value);
|
|
2736
|
-
return value;
|
|
2737
|
-
} catch (err) {
|
|
2738
|
-
return helpers.message('Metadata must be a valid JSON string');
|
|
2739
|
-
}
|
|
2740
|
-
})
|
|
2741
|
-
.example('{"example": "value"}')
|
|
2742
|
-
.description('Related metadata in JSON format')
|
|
2743
|
-
.label('JsonMetaData'),
|
|
2580
|
+
// setup mailbox routes
|
|
2581
|
+
await mailboxRoutes({ server, call, CORS_CONFIG, FLAG_SORT_ORDER });
|
|
2744
2582
|
|
|
2745
|
-
|
|
2583
|
+
// setup settings routes
|
|
2584
|
+
await settingsRoutes({ server, notify, CORS_CONFIG });
|
|
2746
2585
|
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
},
|
|
2586
|
+
// setup stats route
|
|
2587
|
+
await statsRoutes({ server, call, CORS_CONFIG });
|
|
2750
2588
|
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
token: Joi.string().length(64).hex().required().example('123456').description('Access token')
|
|
2754
|
-
}).label('CreateTokenResponse'),
|
|
2755
|
-
failAction: 'log'
|
|
2756
|
-
}
|
|
2757
|
-
}
|
|
2758
|
-
});
|
|
2589
|
+
// setup license routes
|
|
2590
|
+
await licenseRoutes({ server, call, CORS_CONFIG });
|
|
2759
2591
|
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
path: '/v1/token/{token}',
|
|
2592
|
+
// setup outbox routes
|
|
2593
|
+
await outboxRoutes({ server, CORS_CONFIG });
|
|
2763
2594
|
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
return { deleted: await tokens.delete(request.params.token, { remoteAddress: request.app.ip }) };
|
|
2767
|
-
} catch (err) {
|
|
2768
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
2769
|
-
if (Boom.isBoom(err)) {
|
|
2770
|
-
throw err;
|
|
2771
|
-
}
|
|
2772
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
2773
|
-
if (err.code) {
|
|
2774
|
-
error.output.payload.code = err.code;
|
|
2775
|
-
}
|
|
2776
|
-
throw error;
|
|
2777
|
-
}
|
|
2778
|
-
},
|
|
2779
|
-
options: {
|
|
2780
|
-
description: 'Remove a token',
|
|
2781
|
-
notes: 'Delete an access token',
|
|
2782
|
-
tags: ['api', 'Access Tokens'],
|
|
2595
|
+
// setup webhook route management routes
|
|
2596
|
+
await webhookRouteRoutes({ server, CORS_CONFIG });
|
|
2783
2597
|
|
|
2784
|
-
|
|
2598
|
+
// setup OAuth2 application routes
|
|
2599
|
+
await oauth2AppRoutes({ server, call, CORS_CONFIG, OAuth2ProviderSchema });
|
|
2785
2600
|
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
mode: 'required'
|
|
2789
|
-
},
|
|
2790
|
-
cors: CORS_CONFIG,
|
|
2601
|
+
// setup SMTP gateway routes
|
|
2602
|
+
await gatewayRoutes({ server, call, CORS_CONFIG });
|
|
2791
2603
|
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
stripUnknown: false,
|
|
2795
|
-
abortEarly: false,
|
|
2796
|
-
convert: true
|
|
2797
|
-
},
|
|
2798
|
-
failAction,
|
|
2604
|
+
// setup delivery test routes
|
|
2605
|
+
await deliveryTestRoutes({ server, call, CORS_CONFIG, SMTP_TEST_HOST });
|
|
2799
2606
|
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
}).label('DeleteTokenRequest')
|
|
2803
|
-
},
|
|
2607
|
+
// setup blocklist routes
|
|
2608
|
+
await blocklistRoutes({ server, call, CORS_CONFIG });
|
|
2804
2609
|
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the token deleted')
|
|
2808
|
-
}).label('DeleteTokenRequestResponse'),
|
|
2809
|
-
failAction: 'log'
|
|
2810
|
-
}
|
|
2811
|
-
}
|
|
2812
|
-
});
|
|
2610
|
+
// setup message submit route
|
|
2611
|
+
await submitRoutes({ server, call, CORS_CONFIG, MAX_ATTACHMENT_SIZE, MAX_BODY_SIZE, MAX_PAYLOAD_TIMEOUT });
|
|
2813
2612
|
|
|
2814
2613
|
server.route({
|
|
2815
2614
|
method: 'GET',
|
|
2816
|
-
path: '/v1/
|
|
2615
|
+
path: '/v1/changes',
|
|
2817
2616
|
|
|
2818
|
-
async handler(request) {
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
}
|
|
2827
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
2828
|
-
if (err.code) {
|
|
2829
|
-
error.output.payload.code = err.code;
|
|
2617
|
+
async handler(request, h) {
|
|
2618
|
+
request.app.stream = new ResponseStream();
|
|
2619
|
+
finished(request.app.stream, err => request.app.stream.finalize(err));
|
|
2620
|
+
setImmediate(() => {
|
|
2621
|
+
try {
|
|
2622
|
+
request.app.stream.write(`: EmailEngine v${packageData.version}\n\n`);
|
|
2623
|
+
} catch (err) {
|
|
2624
|
+
// ignore
|
|
2830
2625
|
}
|
|
2831
|
-
|
|
2832
|
-
|
|
2626
|
+
});
|
|
2627
|
+
return h
|
|
2628
|
+
.response(request.app.stream)
|
|
2629
|
+
.header('X-Accel-Buffering', 'no')
|
|
2630
|
+
.header('Connection', 'keep-alive')
|
|
2631
|
+
.header('Cache-Control', 'no-cache')
|
|
2632
|
+
.type('text/event-stream');
|
|
2833
2633
|
},
|
|
2834
2634
|
|
|
2835
2635
|
options: {
|
|
2836
|
-
description: '
|
|
2837
|
-
notes: '
|
|
2838
|
-
tags: ['api', '
|
|
2636
|
+
description: 'Stream state changes',
|
|
2637
|
+
notes: 'Stream account state changes as an EventSource',
|
|
2638
|
+
tags: ['api', 'Account'],
|
|
2839
2639
|
|
|
2840
|
-
plugins: {
|
|
2640
|
+
plugins: {
|
|
2641
|
+
'hapi-swagger': {
|
|
2642
|
+
produces: ['text/event-stream'],
|
|
2643
|
+
responses: errorResponses(401, 403, 429, 500)
|
|
2644
|
+
}
|
|
2645
|
+
},
|
|
2841
2646
|
|
|
2842
2647
|
auth: {
|
|
2843
2648
|
strategy: 'api-token',
|
|
2844
2649
|
mode: 'required'
|
|
2845
2650
|
},
|
|
2846
|
-
cors: CORS_CONFIG
|
|
2847
|
-
|
|
2848
|
-
validate: {
|
|
2849
|
-
options: {
|
|
2850
|
-
stripUnknown: false,
|
|
2851
|
-
abortEarly: false,
|
|
2852
|
-
convert: true
|
|
2853
|
-
},
|
|
2854
|
-
failAction
|
|
2855
|
-
},
|
|
2856
|
-
|
|
2857
|
-
response: {
|
|
2858
|
-
schema: Joi.object({
|
|
2859
|
-
tokens: Joi.array()
|
|
2860
|
-
.items(
|
|
2861
|
-
Joi.object({
|
|
2862
|
-
account: accountIdSchema.required(),
|
|
2863
|
-
description: Joi.string().empty('').trim().max(1024).required().example('Token description').description('Token description'),
|
|
2864
|
-
metadata: Joi.string()
|
|
2865
|
-
.empty('')
|
|
2866
|
-
.max(1024 * 1024)
|
|
2867
|
-
.custom((value, helpers) => {
|
|
2868
|
-
try {
|
|
2869
|
-
// check if parsing fails
|
|
2870
|
-
JSON.parse(value);
|
|
2871
|
-
return value;
|
|
2872
|
-
} catch (err) {
|
|
2873
|
-
return helpers.message('Metadata must be a valid JSON string');
|
|
2874
|
-
}
|
|
2875
|
-
})
|
|
2876
|
-
.example('{"example": "value"}')
|
|
2877
|
-
.description('Related metadata in JSON format')
|
|
2878
|
-
.label('JsonMetaData'),
|
|
2879
|
-
ip: ipSchema.description('IP address of the requester').label('TokenIP')
|
|
2880
|
-
}).label('RootTokensItem')
|
|
2881
|
-
)
|
|
2882
|
-
.label('RootTokensEntries')
|
|
2883
|
-
}).label('RootTokensResponse'),
|
|
2884
|
-
failAction: 'log'
|
|
2885
|
-
}
|
|
2651
|
+
cors: CORS_CONFIG
|
|
2886
2652
|
}
|
|
2887
2653
|
});
|
|
2888
2654
|
|
|
2889
|
-
|
|
2890
|
-
method: 'GET',
|
|
2891
|
-
path: '/v1/tokens/account/{account}',
|
|
2655
|
+
// Web UI routes
|
|
2892
2656
|
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
// TODO: allow paging
|
|
2896
|
-
return { tokens: (await tokens.list(request.params.account, 0, 1000)).tokens };
|
|
2897
|
-
} catch (err) {
|
|
2898
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
2899
|
-
if (Boom.isBoom(err)) {
|
|
2900
|
-
throw err;
|
|
2901
|
-
}
|
|
2902
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
2903
|
-
if (err.code) {
|
|
2904
|
-
error.output.payload.code = err.code;
|
|
2905
|
-
}
|
|
2906
|
-
throw error;
|
|
2907
|
-
}
|
|
2908
|
-
},
|
|
2657
|
+
await server.register({
|
|
2658
|
+
plugin: Crumb,
|
|
2909
2659
|
|
|
2910
2660
|
options: {
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
tags: ['api', 'Access Tokens'],
|
|
2914
|
-
|
|
2915
|
-
plugins: {},
|
|
2916
|
-
|
|
2917
|
-
auth: {
|
|
2918
|
-
strategy: 'api-token',
|
|
2919
|
-
mode: 'required'
|
|
2661
|
+
cookieOptions: {
|
|
2662
|
+
isSecure: secureCookie
|
|
2920
2663
|
},
|
|
2921
|
-
cors: CORS_CONFIG,
|
|
2922
2664
|
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
}
|
|
2929
|
-
failAction,
|
|
2930
|
-
params: Joi.object({
|
|
2931
|
-
account: accountIdSchema.required()
|
|
2932
|
-
})
|
|
2933
|
-
},
|
|
2665
|
+
skip: (request /*, h*/) => {
|
|
2666
|
+
let tags = (request.route && request.route.settings && request.route.settings.tags) || [];
|
|
2667
|
+
|
|
2668
|
+
if (tags.includes('api') || tags.includes('scope:metrics') || tags.includes('static') || tags.includes('external')) {
|
|
2669
|
+
return true;
|
|
2670
|
+
}
|
|
2934
2671
|
|
|
2935
|
-
|
|
2936
|
-
schema: Joi.object({
|
|
2937
|
-
tokens: Joi.array()
|
|
2938
|
-
.items(
|
|
2939
|
-
Joi.object({
|
|
2940
|
-
account: accountIdSchema.required(),
|
|
2941
|
-
description: Joi.string().empty('').trim().max(1024).required().example('Token description').description('Token description'),
|
|
2942
|
-
metadata: Joi.string()
|
|
2943
|
-
.empty('')
|
|
2944
|
-
.max(1024 * 1024)
|
|
2945
|
-
.custom((value, helpers) => {
|
|
2946
|
-
try {
|
|
2947
|
-
// check if parsing fails
|
|
2948
|
-
JSON.parse(value);
|
|
2949
|
-
return value;
|
|
2950
|
-
} catch (err) {
|
|
2951
|
-
return helpers.message('Metadata must be a valid JSON string');
|
|
2952
|
-
}
|
|
2953
|
-
})
|
|
2954
|
-
.example('{"example": "value"}')
|
|
2955
|
-
.description('Related metadata in JSON format')
|
|
2956
|
-
.label('JsonMetaData'),
|
|
2957
|
-
|
|
2958
|
-
restrictions: tokenRestrictionsSchema,
|
|
2959
|
-
|
|
2960
|
-
ip: ipSchema.description('IP address of the requester').label('TokenIP')
|
|
2961
|
-
}).label('AccountTokensItem')
|
|
2962
|
-
)
|
|
2963
|
-
.label('AccountTokensEntries')
|
|
2964
|
-
}).label('AccountsTokensResponse'),
|
|
2965
|
-
failAction: 'log'
|
|
2672
|
+
return false;
|
|
2966
2673
|
}
|
|
2967
2674
|
}
|
|
2968
2675
|
});
|
|
2969
2676
|
|
|
2970
|
-
server.
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
account: request.payload.account,
|
|
2978
|
-
name: request.payload.name,
|
|
2979
|
-
email: request.payload.email,
|
|
2980
|
-
syncFrom: request.payload.syncFrom,
|
|
2981
|
-
notifyFrom: request.payload.notifyFrom,
|
|
2982
|
-
subconnections: request.payload.subconnections,
|
|
2983
|
-
redirectUrl: request.payload.redirectUrl,
|
|
2984
|
-
delegated: request.payload.delegated,
|
|
2985
|
-
path: request.payload.path && !request.payload.path.includes('*') ? request.payload.path : null,
|
|
2986
|
-
// identify request
|
|
2987
|
-
n: crypto.randomBytes(NONCE_BYTES).toString('base64url'),
|
|
2988
|
-
t: Date.now()
|
|
2989
|
-
});
|
|
2990
|
-
|
|
2991
|
-
let serviceUrl = await settings.get('serviceUrl');
|
|
2992
|
-
if (!serviceUrl) {
|
|
2993
|
-
let err = new Error('Service URL not set up');
|
|
2994
|
-
err.code = 'MissingServiceURLSetup';
|
|
2995
|
-
throw err;
|
|
2996
|
-
}
|
|
2997
|
-
|
|
2998
|
-
let url = new URL(`accounts/new`, serviceUrl);
|
|
2999
|
-
|
|
3000
|
-
url.searchParams.append('data', data);
|
|
3001
|
-
if (signature) {
|
|
3002
|
-
url.searchParams.append('sig', signature);
|
|
3003
|
-
}
|
|
3004
|
-
|
|
3005
|
-
let type = request.payload.type;
|
|
3006
|
-
|
|
3007
|
-
if (type && type !== 'imap') {
|
|
3008
|
-
let oauth2app = await oauth2Apps.get(type);
|
|
3009
|
-
if (!oauth2app || !oauth2app.enabled) {
|
|
3010
|
-
type = false;
|
|
3011
|
-
}
|
|
3012
|
-
}
|
|
3013
|
-
|
|
3014
|
-
if (!type) {
|
|
3015
|
-
let oauth2apps = (await oauth2Apps.list(0, 100)).apps.filter(app => app.includeInListing);
|
|
3016
|
-
if (!oauth2apps.length) {
|
|
3017
|
-
type = 'imap';
|
|
3018
|
-
}
|
|
3019
|
-
}
|
|
3020
|
-
|
|
3021
|
-
if (type) {
|
|
3022
|
-
url.searchParams.append('type', type);
|
|
3023
|
-
}
|
|
3024
|
-
|
|
3025
|
-
return {
|
|
3026
|
-
url: url.href
|
|
3027
|
-
};
|
|
3028
|
-
} catch (err) {
|
|
3029
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
3030
|
-
if (Boom.isBoom(err)) {
|
|
3031
|
-
throw err;
|
|
3032
|
-
}
|
|
3033
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
3034
|
-
if (err.code) {
|
|
3035
|
-
error.output.payload.code = err.code;
|
|
3036
|
-
}
|
|
3037
|
-
throw error;
|
|
3038
|
-
}
|
|
3039
|
-
},
|
|
3040
|
-
|
|
3041
|
-
options: {
|
|
3042
|
-
description: 'Generate authentication link',
|
|
3043
|
-
notes: 'Generates a redirect link to the hosted authentication form',
|
|
3044
|
-
tags: ['api', 'Account'],
|
|
3045
|
-
|
|
3046
|
-
plugins: {},
|
|
3047
|
-
|
|
3048
|
-
auth: {
|
|
3049
|
-
strategy: 'api-token',
|
|
3050
|
-
mode: 'required'
|
|
3051
|
-
},
|
|
3052
|
-
cors: CORS_CONFIG,
|
|
3053
|
-
|
|
3054
|
-
validate: {
|
|
3055
|
-
options: {
|
|
3056
|
-
stripUnknown: false,
|
|
3057
|
-
abortEarly: false,
|
|
3058
|
-
convert: true
|
|
3059
|
-
},
|
|
3060
|
-
failAction,
|
|
3061
|
-
|
|
3062
|
-
payload: Joi.object({
|
|
3063
|
-
account: Joi.string()
|
|
3064
|
-
.empty('')
|
|
3065
|
-
.trim()
|
|
3066
|
-
.max(256)
|
|
3067
|
-
.allow(null)
|
|
3068
|
-
.example('example')
|
|
3069
|
-
.default(null)
|
|
3070
|
-
.description(
|
|
3071
|
-
'Account ID. If set to `null`, a unique ID will be generated automatically. If you provide an existing account ID, the settings for that account will be updated instead'
|
|
3072
|
-
),
|
|
3073
|
-
|
|
3074
|
-
name: Joi.string().empty('').max(256).example('My Email Account').description('Display name for the account'),
|
|
3075
|
-
|
|
3076
|
-
email: Joi.string()
|
|
3077
|
-
.empty('')
|
|
3078
|
-
.email()
|
|
3079
|
-
.example('user@example.com')
|
|
3080
|
-
.description('Specifies the default email address for this account. Users can change it if needed.'),
|
|
3081
|
-
|
|
3082
|
-
delegated: Joi.boolean()
|
|
3083
|
-
.empty('')
|
|
3084
|
-
.truthy('Y', 'true', '1')
|
|
3085
|
-
.falsy('N', 'false', 0)
|
|
3086
|
-
.default(false)
|
|
3087
|
-
.description('If true, configures this account as a shared mailbox. Currently supported by MS365 OAuth2 accounts'),
|
|
3088
|
-
|
|
3089
|
-
syncFrom: accountSchemas.syncFrom,
|
|
3090
|
-
notifyFrom: accountSchemas.notifyFrom,
|
|
3091
|
-
|
|
3092
|
-
subconnections: accountSchemas.subconnections,
|
|
3093
|
-
|
|
3094
|
-
path: accountPathSchema.example(['*']).label('AccountFormPath'),
|
|
3095
|
-
redirectUrl: Joi.string()
|
|
3096
|
-
.empty('')
|
|
3097
|
-
.uri({ scheme: ['http', 'https'], allowRelative: false })
|
|
3098
|
-
.required()
|
|
3099
|
-
.example('https://myapp/account/settings.php')
|
|
3100
|
-
.description('After the authentication process is completed, the user is redirected to this URL'),
|
|
3101
|
-
|
|
3102
|
-
type: defaultAccountTypeSchema
|
|
3103
|
-
}).label('RequestAuthForm')
|
|
3104
|
-
},
|
|
3105
|
-
|
|
3106
|
-
response: {
|
|
3107
|
-
schema: Joi.object({
|
|
3108
|
-
url: Joi.string()
|
|
3109
|
-
.empty('')
|
|
3110
|
-
.uri({ scheme: ['http', 'https'], allowRelative: false })
|
|
3111
|
-
.required()
|
|
3112
|
-
.example('https://ee.example.com/accounts/new?data=eyJhY2NvdW50IjoiZXhh...L0W_BkFH5HW6Krwmr7c&type=imap')
|
|
3113
|
-
.description('Generated URL to the hosted authentication form')
|
|
3114
|
-
}).label('RequestAuthFormResponse'),
|
|
3115
|
-
failAction: 'log'
|
|
3116
|
-
}
|
|
3117
|
-
}
|
|
3118
|
-
});
|
|
3119
|
-
|
|
3120
|
-
server.route({
|
|
3121
|
-
method: 'GET',
|
|
3122
|
-
path: '/v1/account/{account}/mailboxes',
|
|
3123
|
-
|
|
3124
|
-
async handler(request) {
|
|
3125
|
-
let accountObject = new Account({
|
|
3126
|
-
redis,
|
|
3127
|
-
account: request.params.account,
|
|
3128
|
-
call,
|
|
3129
|
-
secret: await getSecret(),
|
|
3130
|
-
timeout: request.headers['x-ee-timeout']
|
|
3131
|
-
});
|
|
3132
|
-
|
|
3133
|
-
try {
|
|
3134
|
-
let mailboxes = await accountObject.getMailboxListing(request.query);
|
|
3135
|
-
|
|
3136
|
-
if (mailboxes && Array.isArray(mailboxes)) {
|
|
3137
|
-
mailboxes = mailboxes.sort((a, b) => {
|
|
3138
|
-
if (a.specialUse && !b.specialUse) {
|
|
3139
|
-
return -1;
|
|
3140
|
-
}
|
|
3141
|
-
if (!a.specialUse && b.specialUse) {
|
|
3142
|
-
return 1;
|
|
3143
|
-
}
|
|
3144
|
-
if (a.specialUse && b.specialUse) {
|
|
3145
|
-
return FLAG_SORT_ORDER.indexOf(a.specialUse) - FLAG_SORT_ORDER.indexOf(b.specialUse);
|
|
3146
|
-
}
|
|
3147
|
-
|
|
3148
|
-
return a.path.localeCompare(b.path);
|
|
3149
|
-
});
|
|
3150
|
-
}
|
|
3151
|
-
|
|
3152
|
-
return { mailboxes };
|
|
3153
|
-
} catch (err) {
|
|
3154
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
3155
|
-
if (Boom.isBoom(err)) {
|
|
3156
|
-
throw err;
|
|
3157
|
-
}
|
|
3158
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
3159
|
-
if (err.code) {
|
|
3160
|
-
error.output.payload.code = err.code;
|
|
3161
|
-
}
|
|
3162
|
-
throw error;
|
|
3163
|
-
}
|
|
3164
|
-
},
|
|
3165
|
-
|
|
3166
|
-
options: {
|
|
3167
|
-
description: 'List mailboxes',
|
|
3168
|
-
notes: 'Lists all available mailboxes',
|
|
3169
|
-
tags: ['api', 'Mailbox'],
|
|
3170
|
-
|
|
3171
|
-
auth: {
|
|
3172
|
-
strategy: 'api-token',
|
|
3173
|
-
mode: 'required'
|
|
3174
|
-
},
|
|
3175
|
-
cors: CORS_CONFIG,
|
|
3176
|
-
|
|
3177
|
-
validate: {
|
|
3178
|
-
options: {
|
|
3179
|
-
stripUnknown: false,
|
|
3180
|
-
abortEarly: false,
|
|
3181
|
-
convert: true
|
|
3182
|
-
},
|
|
3183
|
-
failAction,
|
|
3184
|
-
|
|
3185
|
-
params: Joi.object({
|
|
3186
|
-
account: accountIdSchema.required()
|
|
3187
|
-
}),
|
|
3188
|
-
|
|
3189
|
-
query: Joi.object({
|
|
3190
|
-
counters: Joi.boolean()
|
|
3191
|
-
.truthy('Y', 'true', '1')
|
|
3192
|
-
.falsy('N', 'false', 0)
|
|
3193
|
-
.default(false)
|
|
3194
|
-
.description('If true, then includes message counters in the response')
|
|
3195
|
-
.label('MailboxCounters')
|
|
3196
|
-
}).label('MailboxListQuery')
|
|
3197
|
-
},
|
|
3198
|
-
|
|
3199
|
-
response: {
|
|
3200
|
-
schema: Joi.object({
|
|
3201
|
-
mailboxes: mailboxesSchema
|
|
3202
|
-
}).label('MailboxesFilterResponse'),
|
|
3203
|
-
failAction: 'log'
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3206
|
-
});
|
|
3207
|
-
|
|
3208
|
-
server.route({
|
|
3209
|
-
method: 'POST',
|
|
3210
|
-
path: '/v1/account/{account}/mailbox',
|
|
3211
|
-
|
|
3212
|
-
async handler(request) {
|
|
3213
|
-
let accountObject = new Account({
|
|
3214
|
-
redis,
|
|
3215
|
-
account: request.params.account,
|
|
3216
|
-
call,
|
|
3217
|
-
secret: await getSecret(),
|
|
3218
|
-
timeout: request.headers['x-ee-timeout']
|
|
3219
|
-
});
|
|
3220
|
-
|
|
3221
|
-
try {
|
|
3222
|
-
return await accountObject.createMailbox(request.payload.path);
|
|
3223
|
-
} catch (err) {
|
|
3224
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
3225
|
-
if (Boom.isBoom(err)) {
|
|
3226
|
-
throw err;
|
|
3227
|
-
}
|
|
3228
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
3229
|
-
if (err.code) {
|
|
3230
|
-
error.output.payload.code = err.code;
|
|
3231
|
-
}
|
|
3232
|
-
if (err.info) {
|
|
3233
|
-
error.output.payload.details = err.info;
|
|
3234
|
-
}
|
|
3235
|
-
throw error;
|
|
3236
|
-
}
|
|
3237
|
-
},
|
|
3238
|
-
|
|
3239
|
-
options: {
|
|
3240
|
-
description: 'Create mailbox',
|
|
3241
|
-
notes: 'Create new mailbox folder',
|
|
3242
|
-
tags: ['api', 'Mailbox'],
|
|
3243
|
-
|
|
3244
|
-
plugins: {},
|
|
3245
|
-
|
|
3246
|
-
auth: {
|
|
3247
|
-
strategy: 'api-token',
|
|
3248
|
-
mode: 'required'
|
|
3249
|
-
},
|
|
3250
|
-
cors: CORS_CONFIG,
|
|
3251
|
-
|
|
3252
|
-
validate: {
|
|
3253
|
-
options: {
|
|
3254
|
-
stripUnknown: false,
|
|
3255
|
-
abortEarly: false,
|
|
3256
|
-
convert: true
|
|
3257
|
-
},
|
|
3258
|
-
failAction,
|
|
3259
|
-
|
|
3260
|
-
params: Joi.object({
|
|
3261
|
-
account: accountIdSchema.required()
|
|
3262
|
-
}),
|
|
3263
|
-
|
|
3264
|
-
payload: Joi.object({
|
|
3265
|
-
path: Joi.array()
|
|
3266
|
-
.items(Joi.string().max(256))
|
|
3267
|
-
.single()
|
|
3268
|
-
.example(['Parent folder', 'Subfolder'])
|
|
3269
|
-
.description('Mailbox path as an array or a string. If account is namespaced then namespace prefix is added by default.')
|
|
3270
|
-
.label('MailboxPath')
|
|
3271
|
-
}).label('CreateMailbox')
|
|
3272
|
-
},
|
|
3273
|
-
|
|
3274
|
-
response: {
|
|
3275
|
-
schema: Joi.object({
|
|
3276
|
-
path: Joi.string().required().example('Kalender/S&APw-nnip&AOQ-evad').description('Full path to mailbox').label('MailboxPath'),
|
|
3277
|
-
mailboxId: Joi.string().example('1439876283476').description('Mailbox ID (if server has support)').label('MailboxId'),
|
|
3278
|
-
created: Joi.boolean().example(true).description('Was the mailbox created')
|
|
3279
|
-
}).label('CreateMailboxResponse'),
|
|
3280
|
-
failAction: 'log'
|
|
3281
|
-
}
|
|
3282
|
-
}
|
|
3283
|
-
});
|
|
3284
|
-
|
|
3285
|
-
server.route({
|
|
3286
|
-
method: 'PUT',
|
|
3287
|
-
path: '/v1/account/{account}/mailbox',
|
|
3288
|
-
|
|
3289
|
-
async handler(request) {
|
|
3290
|
-
let accountObject = new Account({
|
|
3291
|
-
redis,
|
|
3292
|
-
account: request.params.account,
|
|
3293
|
-
call,
|
|
3294
|
-
secret: await getSecret(),
|
|
3295
|
-
timeout: request.headers['x-ee-timeout']
|
|
3296
|
-
});
|
|
3297
|
-
|
|
3298
|
-
try {
|
|
3299
|
-
return await accountObject.modifyMailbox(request.payload.path, request.payload.newPath, request.payload.subscribed);
|
|
3300
|
-
} catch (err) {
|
|
3301
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
3302
|
-
if (Boom.isBoom(err)) {
|
|
3303
|
-
throw err;
|
|
3304
|
-
}
|
|
3305
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
3306
|
-
if (err.code) {
|
|
3307
|
-
error.output.payload.code = err.code;
|
|
3308
|
-
}
|
|
3309
|
-
if (err.info) {
|
|
3310
|
-
error.output.payload.details = err.info;
|
|
3311
|
-
}
|
|
3312
|
-
throw error;
|
|
3313
|
-
}
|
|
3314
|
-
},
|
|
3315
|
-
|
|
3316
|
-
options: {
|
|
3317
|
-
description: 'Modify mailbox',
|
|
3318
|
-
notes: 'Modify an existing mailbox folder (rename or change subscription status)',
|
|
3319
|
-
tags: ['api', 'Mailbox'],
|
|
3320
|
-
|
|
3321
|
-
plugins: {},
|
|
3322
|
-
|
|
3323
|
-
auth: {
|
|
3324
|
-
strategy: 'api-token',
|
|
3325
|
-
mode: 'required'
|
|
3326
|
-
},
|
|
3327
|
-
cors: CORS_CONFIG,
|
|
3328
|
-
|
|
3329
|
-
validate: {
|
|
3330
|
-
options: {
|
|
3331
|
-
stripUnknown: false,
|
|
3332
|
-
abortEarly: false,
|
|
3333
|
-
convert: true
|
|
3334
|
-
},
|
|
3335
|
-
failAction,
|
|
3336
|
-
|
|
3337
|
-
params: Joi.object({
|
|
3338
|
-
account: accountIdSchema.required()
|
|
3339
|
-
}),
|
|
3340
|
-
|
|
3341
|
-
payload: Joi.object({
|
|
3342
|
-
path: Joi.string().required().example('Folder Name').description('Mailbox folder path to modify').label('ExistingMailboxPath'),
|
|
3343
|
-
newPath: Joi.array()
|
|
3344
|
-
.items(Joi.string().max(256))
|
|
3345
|
-
.single()
|
|
3346
|
-
.example(['Parent folder', 'Subfolder'])
|
|
3347
|
-
.description('New mailbox path as an array or a string. If account is namespaced then namespace prefix is added by default. Optional.')
|
|
3348
|
-
.label('TargetMailboxPath'),
|
|
3349
|
-
subscribed: Joi.boolean()
|
|
3350
|
-
.example(true)
|
|
3351
|
-
.description('Change mailbox subscription status. Only applies to IMAP accounts, ignored for Gmail and Outlook.')
|
|
3352
|
-
.label('SubscriptionStatus')
|
|
3353
|
-
})
|
|
3354
|
-
.or('newPath', 'subscribed')
|
|
3355
|
-
.label('ModifyMailbox')
|
|
3356
|
-
},
|
|
3357
|
-
|
|
3358
|
-
response: {
|
|
3359
|
-
schema: Joi.object({
|
|
3360
|
-
path: Joi.string().required().example('Mail').description('Mailbox folder path').label('ExistingMailboxPath'),
|
|
3361
|
-
newPath: Joi.string().example('Kalender/S&APw-nnip&AOQ-evad').description('Full path to mailbox if renamed').label('NewMailboxPath'),
|
|
3362
|
-
renamed: Joi.boolean().example(true).description('Was the mailbox renamed'),
|
|
3363
|
-
subscribed: Joi.boolean().example(true).description('Subscription status after modification')
|
|
3364
|
-
}).label('ModifyMailboxResponse'),
|
|
3365
|
-
failAction: 'log'
|
|
3366
|
-
}
|
|
3367
|
-
}
|
|
3368
|
-
});
|
|
3369
|
-
|
|
3370
|
-
server.route({
|
|
3371
|
-
method: 'DELETE',
|
|
3372
|
-
path: '/v1/account/{account}/mailbox',
|
|
3373
|
-
|
|
3374
|
-
async handler(request) {
|
|
3375
|
-
let accountObject = new Account({
|
|
3376
|
-
redis,
|
|
3377
|
-
account: request.params.account,
|
|
3378
|
-
call,
|
|
3379
|
-
secret: await getSecret(),
|
|
3380
|
-
timeout: request.headers['x-ee-timeout']
|
|
3381
|
-
});
|
|
3382
|
-
|
|
3383
|
-
try {
|
|
3384
|
-
return await accountObject.deleteMailbox(request.query.path);
|
|
3385
|
-
} catch (err) {
|
|
3386
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
3387
|
-
if (Boom.isBoom(err)) {
|
|
3388
|
-
throw err;
|
|
3389
|
-
}
|
|
3390
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
3391
|
-
if (err.code) {
|
|
3392
|
-
error.output.payload.code = err.code;
|
|
3393
|
-
}
|
|
3394
|
-
throw error;
|
|
3395
|
-
}
|
|
3396
|
-
},
|
|
3397
|
-
|
|
3398
|
-
options: {
|
|
3399
|
-
description: 'Delete mailbox',
|
|
3400
|
-
notes: 'Delete existing mailbox folder',
|
|
3401
|
-
tags: ['api', 'Mailbox'],
|
|
3402
|
-
|
|
3403
|
-
plugins: {},
|
|
3404
|
-
|
|
3405
|
-
auth: {
|
|
3406
|
-
strategy: 'api-token',
|
|
3407
|
-
mode: 'required'
|
|
3408
|
-
},
|
|
3409
|
-
cors: CORS_CONFIG,
|
|
3410
|
-
|
|
3411
|
-
validate: {
|
|
3412
|
-
options: {
|
|
3413
|
-
stripUnknown: false,
|
|
3414
|
-
abortEarly: false,
|
|
3415
|
-
convert: true
|
|
3416
|
-
},
|
|
3417
|
-
failAction,
|
|
3418
|
-
|
|
3419
|
-
params: Joi.object({
|
|
3420
|
-
account: accountIdSchema.required()
|
|
3421
|
-
}),
|
|
3422
|
-
|
|
3423
|
-
query: Joi.object({
|
|
3424
|
-
path: Joi.string().required().example('My Outdated Mail').description('Mailbox folder path to delete').label('MailboxPath')
|
|
3425
|
-
}).label('DeleteMailbox')
|
|
3426
|
-
},
|
|
3427
|
-
|
|
3428
|
-
response: {
|
|
3429
|
-
schema: Joi.object({
|
|
3430
|
-
path: Joi.string().required().example('Kalender/S&APw-nnip&AOQ-evad').description('Full path to mailbox').label('MailboxPath'),
|
|
3431
|
-
deleted: Joi.boolean().example(true).description('Was the mailbox deleted')
|
|
3432
|
-
}).label('DeleteMailboxResponse'),
|
|
3433
|
-
failAction: 'log'
|
|
3434
|
-
}
|
|
3435
|
-
}
|
|
3436
|
-
});
|
|
3437
|
-
|
|
3438
|
-
server.route({
|
|
3439
|
-
method: 'POST',
|
|
3440
|
-
path: '/v1/account/{account}/submit',
|
|
3441
|
-
|
|
3442
|
-
async handler(request) {
|
|
3443
|
-
let accountObject = new Account({
|
|
3444
|
-
redis,
|
|
3445
|
-
account: request.params.account,
|
|
3446
|
-
call,
|
|
3447
|
-
secret: await getSecret(),
|
|
3448
|
-
timeout: request.headers['x-ee-timeout']
|
|
3449
|
-
});
|
|
3450
|
-
|
|
3451
|
-
try {
|
|
3452
|
-
return await accountObject.queueMessage(request.payload, {
|
|
3453
|
-
source: 'api',
|
|
3454
|
-
idempotencyKey: request.headers['idempotency-key'],
|
|
3455
|
-
useStructuredFormat: request.query.useStructuredFormat
|
|
3456
|
-
});
|
|
3457
|
-
} catch (err) {
|
|
3458
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
3459
|
-
if (Boom.isBoom(err)) {
|
|
3460
|
-
throw err;
|
|
3461
|
-
}
|
|
3462
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
3463
|
-
if (err.code) {
|
|
3464
|
-
error.output.payload.code = err.code;
|
|
3465
|
-
}
|
|
3466
|
-
if (err.info) {
|
|
3467
|
-
error.output.payload.info = err.info;
|
|
3468
|
-
}
|
|
3469
|
-
throw error;
|
|
3470
|
-
}
|
|
3471
|
-
},
|
|
3472
|
-
options: {
|
|
3473
|
-
payload: {
|
|
3474
|
-
maxBytes: MAX_BODY_SIZE,
|
|
3475
|
-
timeout: MAX_PAYLOAD_TIMEOUT
|
|
3476
|
-
},
|
|
3477
|
-
|
|
3478
|
-
description: 'Submit message for delivery',
|
|
3479
|
-
notes: 'Submit message for delivery. If reference message ID is provided then EmailEngine adds all headers and flags required for a reply/forward automatically.',
|
|
3480
|
-
tags: ['api', 'Submit'],
|
|
3481
|
-
|
|
3482
|
-
plugins: {},
|
|
3483
|
-
|
|
3484
|
-
auth: {
|
|
3485
|
-
strategy: 'api-token',
|
|
3486
|
-
mode: 'required'
|
|
3487
|
-
},
|
|
3488
|
-
cors: CORS_CONFIG,
|
|
3489
|
-
|
|
3490
|
-
validate: {
|
|
3491
|
-
options: {
|
|
3492
|
-
stripUnknown: false,
|
|
3493
|
-
abortEarly: false,
|
|
3494
|
-
convert: true
|
|
3495
|
-
},
|
|
3496
|
-
failAction,
|
|
3497
|
-
|
|
3498
|
-
params: Joi.object({
|
|
3499
|
-
account: accountIdSchema.required()
|
|
3500
|
-
}),
|
|
3501
|
-
|
|
3502
|
-
query: Joi.object({
|
|
3503
|
-
documentStore: Joi.boolean()
|
|
3504
|
-
.truthy('Y', 'true', '1')
|
|
3505
|
-
.falsy('N', 'false', 0)
|
|
3506
|
-
.default(false)
|
|
3507
|
-
.description('If enabled then fetch email used as a reference template from the Document Store'),
|
|
3508
|
-
useStructuredFormat: Joi.boolean()
|
|
3509
|
-
.truthy('Y', 'true', '1')
|
|
3510
|
-
.falsy('N', 'false', 0)
|
|
3511
|
-
.default(false)
|
|
3512
|
-
.description(
|
|
3513
|
-
'For MS Graph accounts: If true, uses structured JSON format (respects from field for shared mailboxes, breaks calendar invites and special MIME types). If false, sends as raw MIME (preserves calendar invites, ignores from field). Default is false (raw MIME).'
|
|
3514
|
-
)
|
|
3515
|
-
}).label('SubmitQuery'),
|
|
3516
|
-
|
|
3517
|
-
headers: Joi.object({
|
|
3518
|
-
'x-ee-timeout': headerTimeoutSchema,
|
|
3519
|
-
'idempotency-key': idempotencyKeySchema
|
|
3520
|
-
}).unknown(),
|
|
3521
|
-
|
|
3522
|
-
payload: Joi.object({
|
|
3523
|
-
reference: messageReferenceSchema,
|
|
3524
|
-
|
|
3525
|
-
envelope: Joi.object({
|
|
3526
|
-
from: Joi.string().email().allow('').example('sender@example.com'),
|
|
3527
|
-
to: Joi.array().items(Joi.string().email().required().example('recipient@example.com')).single().label('SmtpEnvelopeTo')
|
|
3528
|
-
})
|
|
3529
|
-
.description(
|
|
3530
|
-
"An optional object specifying the SMTP envelope used during email transmission. If not provided, the envelope is automatically derived from the email's message headers. This is useful when you need the envelope addresses to differ from those in the email headers."
|
|
3531
|
-
)
|
|
3532
|
-
.label('SMTPEnvelope')
|
|
3533
|
-
.when('mailMerge', {
|
|
3534
|
-
is: Joi.exist().not(false, null),
|
|
3535
|
-
then: Joi.forbidden('y')
|
|
3536
|
-
}),
|
|
3537
|
-
|
|
3538
|
-
raw: Joi.string()
|
|
3539
|
-
.base64()
|
|
3540
|
-
.max(MAX_ATTACHMENT_SIZE)
|
|
3541
|
-
.example('TUlNRS1WZXJzaW9uOiAxLjANClN1YmplY3Q6IGhlbGxvIHdvcmxkDQoNCkhlbGxvIQ0K')
|
|
3542
|
-
.description(
|
|
3543
|
-
'A Base64-encoded email message in RFC 822 format. If you provide other fields along with raw, those fields will override the corresponding values in the raw message.'
|
|
3544
|
-
)
|
|
3545
|
-
.label('RFC822Raw')
|
|
3546
|
-
.when('mailMerge', {
|
|
3547
|
-
is: Joi.exist().not(false, null),
|
|
3548
|
-
then: Joi.forbidden('y')
|
|
3549
|
-
}),
|
|
3550
|
-
|
|
3551
|
-
from: fromAddressSchema,
|
|
3552
|
-
|
|
3553
|
-
replyTo: Joi.array()
|
|
3554
|
-
.items(addressSchema.label('ReplyToAddress'))
|
|
3555
|
-
.single()
|
|
3556
|
-
.example([{ name: 'From Me', address: 'sender@example.com' }])
|
|
3557
|
-
.description('List of Reply-To addresses')
|
|
3558
|
-
.label('ReplyTo'),
|
|
3559
|
-
|
|
3560
|
-
to: Joi.array()
|
|
3561
|
-
.items(addressSchema.label('ToAddress'))
|
|
3562
|
-
.single()
|
|
3563
|
-
.example([{ address: 'recipient@example.com' }])
|
|
3564
|
-
.description('List of recipient addresses')
|
|
3565
|
-
.label('ToAddressList')
|
|
3566
|
-
.when('mailMerge', {
|
|
3567
|
-
is: Joi.exist().not(false, null),
|
|
3568
|
-
then: Joi.forbidden('y')
|
|
3569
|
-
}),
|
|
3570
|
-
|
|
3571
|
-
cc: Joi.array()
|
|
3572
|
-
.items(addressSchema.label('CcAddress'))
|
|
3573
|
-
.single()
|
|
3574
|
-
.description('List of CC addresses')
|
|
3575
|
-
.label('CcAddressList')
|
|
3576
|
-
.when('mailMerge', {
|
|
3577
|
-
is: Joi.exist().not(false, null),
|
|
3578
|
-
then: Joi.forbidden('y')
|
|
3579
|
-
}),
|
|
3580
|
-
|
|
3581
|
-
bcc: Joi.array()
|
|
3582
|
-
.items(addressSchema.label('BccAddress'))
|
|
3583
|
-
.single()
|
|
3584
|
-
.description('List of BCC addresses')
|
|
3585
|
-
.label('BccAddressList')
|
|
3586
|
-
.when('mailMerge', {
|
|
3587
|
-
is: Joi.exist().not(false, null),
|
|
3588
|
-
then: Joi.forbidden('y')
|
|
3589
|
-
}),
|
|
3590
|
-
|
|
3591
|
-
subject: templateSchemas.subject,
|
|
3592
|
-
text: templateSchemas.text,
|
|
3593
|
-
html: templateSchemas.html,
|
|
3594
|
-
previewText: templateSchemas.previewText,
|
|
3595
|
-
|
|
3596
|
-
template: Joi.string().max(256).example('example').description('Stored template ID to load the email content from'),
|
|
3597
|
-
|
|
3598
|
-
render: Joi.object({
|
|
3599
|
-
format: Joi.string()
|
|
3600
|
-
.valid('html', 'markdown')
|
|
3601
|
-
.default('html')
|
|
3602
|
-
.description('Markup language for HTML ("html" or "markdown")')
|
|
3603
|
-
.label('RenderFormat'),
|
|
3604
|
-
params: Joi.object().label('RenderValues').description('An object of variables for the template renderer')
|
|
3605
|
-
})
|
|
3606
|
-
.allow(false)
|
|
3607
|
-
.description('Template rendering options')
|
|
3608
|
-
.when('mailMerge', {
|
|
3609
|
-
is: Joi.exist().not(false, null),
|
|
3610
|
-
then: Joi.forbidden('y')
|
|
3611
|
-
})
|
|
3612
|
-
.label('TemplateRender'),
|
|
3613
|
-
|
|
3614
|
-
mailMerge: Joi.array()
|
|
3615
|
-
.items(
|
|
3616
|
-
Joi.object({
|
|
3617
|
-
to: addressSchema.label('ToAddress').required(),
|
|
3618
|
-
messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
|
|
3619
|
-
params: Joi.object().label('RenderValues').description('An object of variables for the template renderer'),
|
|
3620
|
-
sendAt: Joi.date()
|
|
3621
|
-
.iso()
|
|
3622
|
-
.example('2021-07-08T07:06:34.336Z')
|
|
3623
|
-
.description('Send message at specified time. Overrides message level `sendAt` value.')
|
|
3624
|
-
}).label('MailMergeListEntry')
|
|
3625
|
-
)
|
|
3626
|
-
.min(1)
|
|
3627
|
-
.description(
|
|
3628
|
-
'Mail merge options. A separate email is generated for each recipient. Using mail merge disables `messageId`, `envelope`, `to`, `cc`, `bcc`, `render` keys for the message root.'
|
|
3629
|
-
)
|
|
3630
|
-
.label('MailMergeList'),
|
|
3631
|
-
|
|
3632
|
-
attachments: Joi.array()
|
|
3633
|
-
.items(
|
|
3634
|
-
Joi.object({
|
|
3635
|
-
filename: Joi.string().max(256).example('transparent.gif'),
|
|
3636
|
-
content: Joi.string()
|
|
3637
|
-
.base64()
|
|
3638
|
-
.max(MAX_ATTACHMENT_SIZE)
|
|
3639
|
-
.required()
|
|
3640
|
-
.example('R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=')
|
|
3641
|
-
.description('Base64 formatted attachment file')
|
|
3642
|
-
.when('reference', {
|
|
3643
|
-
is: Joi.exist().not(false, null),
|
|
3644
|
-
then: Joi.forbidden(),
|
|
3645
|
-
otherwise: Joi.required()
|
|
3646
|
-
}),
|
|
3647
|
-
|
|
3648
|
-
contentType: Joi.string().lowercase().max(256).example('image/gif'),
|
|
3649
|
-
contentDisposition: Joi.string().lowercase().valid('inline', 'attachment').label('AttachmentContentDisposition'),
|
|
3650
|
-
cid: Joi.string().max(256).example('unique-image-id@localhost').description('Content-ID value for embedded images'),
|
|
3651
|
-
encoding: Joi.string().valid('base64').default('base64').label('AttachmentEncoding'),
|
|
3652
|
-
|
|
3653
|
-
reference: Joi.string()
|
|
3654
|
-
.base64({ paddingRequired: false, urlSafe: true })
|
|
3655
|
-
.max(256)
|
|
3656
|
-
.allow(false, null)
|
|
3657
|
-
.example('AAAAAQAACnAcde')
|
|
3658
|
-
.description(
|
|
3659
|
-
'References an existing attachment by its ID instead of providing new attachment content. If this field is set, the `content` field must not be included. If not set, the `content` field is required.'
|
|
3660
|
-
)
|
|
3661
|
-
.label('AttachmentReference')
|
|
3662
|
-
}).label('UploadAttachment')
|
|
3663
|
-
)
|
|
3664
|
-
.description('List of attachments')
|
|
3665
|
-
.label('UploadAttachmentList'),
|
|
3666
|
-
|
|
3667
|
-
messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
|
|
3668
|
-
headers: Joi.object().label('CustomHeaders').description('Custom Headers').unknown().example({
|
|
3669
|
-
'X-My-Custom-Header': 'Custom header value'
|
|
3670
|
-
}),
|
|
3671
|
-
|
|
3672
|
-
trackingEnabled: Joi.boolean()
|
|
3673
|
-
.example(false)
|
|
3674
|
-
.description('Should EmailEngine track clicks and opens for this message')
|
|
3675
|
-
.meta({ swaggerHidden: true }),
|
|
3676
|
-
|
|
3677
|
-
trackOpens: Joi.boolean().example(false).description('Should EmailEngine track opens for this message'),
|
|
3678
|
-
trackClicks: Joi.boolean().example(false).description('Should EmailEngine track clicks for this message'),
|
|
3679
|
-
|
|
3680
|
-
copy: Joi.boolean()
|
|
3681
|
-
.allow(null)
|
|
3682
|
-
.example(null)
|
|
3683
|
-
.description(
|
|
3684
|
-
"If set then either copies the message to the Sent Mail folder or not. If not set then uses the account's default setting."
|
|
3685
|
-
),
|
|
3686
|
-
|
|
3687
|
-
sentMailPath: Joi.string()
|
|
3688
|
-
.empty('')
|
|
3689
|
-
.max(1024)
|
|
3690
|
-
.example('Sent Mail')
|
|
3691
|
-
.description("Upload sent message to this folder. By default the account's Sent Mail folder is used."),
|
|
3692
|
-
|
|
3693
|
-
locale: Joi.string().empty('').max(100).example('fr').description('Optional locale').label('MessageLocale'),
|
|
3694
|
-
tz: Joi.string().empty('').max(100).example('Europe/Tallinn').description('Optional timezone'),
|
|
3695
|
-
|
|
3696
|
-
sendAt: Joi.date().iso().example('2021-07-08T07:06:34.336Z').description('Send message at specified time'),
|
|
3697
|
-
deliveryAttempts: Joi.number()
|
|
3698
|
-
.integer()
|
|
3699
|
-
.example(10)
|
|
3700
|
-
.description('How many delivery attempts to make until message is considered as failed'),
|
|
3701
|
-
gateway: Joi.string().max(256).example('example').description('Optional SMTP gateway ID for message routing').label('MessageGateway'),
|
|
3702
|
-
|
|
3703
|
-
listId: Joi.string()
|
|
3704
|
-
.hostname()
|
|
3705
|
-
.example('test-list')
|
|
3706
|
-
.description(
|
|
3707
|
-
'List ID for Mail Merge. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.'
|
|
3708
|
-
)
|
|
3709
|
-
.label('ListID')
|
|
3710
|
-
.when('mailMerge', {
|
|
3711
|
-
is: Joi.exist().not(false, null),
|
|
3712
|
-
then: Joi.optional(),
|
|
3713
|
-
otherwise: Joi.forbidden()
|
|
3714
|
-
}),
|
|
3715
|
-
|
|
3716
|
-
dsn: Joi.object({
|
|
3717
|
-
id: Joi.string().trim().empty('').max(256).description('The envelope identifier that would be included in the response (ENVID)'),
|
|
3718
|
-
return: Joi.string()
|
|
3719
|
-
.trim()
|
|
3720
|
-
.empty('')
|
|
3721
|
-
.valid('headers', 'full')
|
|
3722
|
-
.required()
|
|
3723
|
-
.description('Specifies if only headers or the entire body of the message should be included in the response (RET)')
|
|
3724
|
-
.label('DsnReturn'),
|
|
3725
|
-
notify: Joi.array()
|
|
3726
|
-
.single()
|
|
3727
|
-
.items(Joi.string().valid('never', 'success', 'failure', 'delay').label('NotifyEntry'))
|
|
3728
|
-
.description('Defines the conditions under which a DSN response should be sent')
|
|
3729
|
-
.label('DsnNotify'),
|
|
3730
|
-
recipient: Joi.string().trim().empty('').email().description('The email address the DSN should be sent (ORCPT)')
|
|
3731
|
-
})
|
|
3732
|
-
.description('Request DSN notifications')
|
|
3733
|
-
.label('DSN'),
|
|
3734
|
-
|
|
3735
|
-
baseUrl: Joi.string()
|
|
3736
|
-
.trim()
|
|
3737
|
-
.empty('')
|
|
3738
|
-
.uri({
|
|
3739
|
-
scheme: ['http', 'https'],
|
|
3740
|
-
allowRelative: false
|
|
3741
|
-
})
|
|
3742
|
-
.example('https://customer123.myservice.com')
|
|
3743
|
-
.description('Optional base URL for trackers. This URL must point to your EmailEngine instance.'),
|
|
3744
|
-
|
|
3745
|
-
proxy: settingsSchema.proxyUrl.description('Optional proxy URL to use when connecting to the SMTP server'),
|
|
3746
|
-
localAddress: ipSchema.description('Optional local IP address to bind to when connecting to the SMTP server'),
|
|
3747
|
-
|
|
3748
|
-
dryRun: Joi.boolean()
|
|
3749
|
-
.truthy('Y', 'true', '1')
|
|
3750
|
-
.falsy('N', 'false', 0)
|
|
3751
|
-
.default(false)
|
|
3752
|
-
.description(
|
|
3753
|
-
'If true, then EmailEngine does not send the email and returns an RFC822 formatted email file. Tracking information is not added to the email.'
|
|
3754
|
-
)
|
|
3755
|
-
.label('Preview')
|
|
3756
|
-
})
|
|
3757
|
-
.oxor('raw', 'html')
|
|
3758
|
-
.oxor('raw', 'text')
|
|
3759
|
-
.oxor('raw', 'text')
|
|
3760
|
-
.oxor('raw', 'attachments')
|
|
3761
|
-
.label('SubmitMessage')
|
|
3762
|
-
.example({
|
|
3763
|
-
to: [
|
|
3764
|
-
{
|
|
3765
|
-
name: 'Nyan Cat',
|
|
3766
|
-
address: 'nyan.cat@example.com'
|
|
3767
|
-
}
|
|
3768
|
-
],
|
|
3769
|
-
subject: 'What a wonderful message!',
|
|
3770
|
-
text: 'Hello from myself!',
|
|
3771
|
-
html: '<p>Hello from myself!</p>',
|
|
3772
|
-
attachments: [
|
|
3773
|
-
{
|
|
3774
|
-
filename: 'transparent.gif',
|
|
3775
|
-
content: 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=',
|
|
3776
|
-
contentType: 'image/gif'
|
|
3777
|
-
}
|
|
3778
|
-
]
|
|
3779
|
-
})
|
|
3780
|
-
},
|
|
3781
|
-
|
|
3782
|
-
response: {
|
|
3783
|
-
schema: Joi.object({
|
|
3784
|
-
response: Joi.string().example('Queued for delivery'),
|
|
3785
|
-
messageId: Joi.string()
|
|
3786
|
-
.example('<a2184d08-a470-fec6-a493-fa211a3756e9@example.com>')
|
|
3787
|
-
.description('Message-ID header value. Not present for bulk messages.'),
|
|
3788
|
-
queueId: Joi.string().example('d41f0423195f271f').description('Queue identifier for scheduled email. Not present for bulk messages.'),
|
|
3789
|
-
sendAt: Joi.date().example('2021-07-08T07:06:34.336Z').description('Scheduled send time'),
|
|
3790
|
-
|
|
3791
|
-
reference: Joi.object({
|
|
3792
|
-
message: Joi.string()
|
|
3793
|
-
.base64({ paddingRequired: false, urlSafe: true })
|
|
3794
|
-
.max(256)
|
|
3795
|
-
.required()
|
|
3796
|
-
.example('AAAAAQAACnA')
|
|
3797
|
-
.description('Referenced message ID'),
|
|
3798
|
-
documentStore: Joi.boolean()
|
|
3799
|
-
.example(true)
|
|
3800
|
-
.description('Was the message data loaded from the Document Store')
|
|
3801
|
-
.label('ResponseDocumentStore')
|
|
3802
|
-
.meta({ swaggerHidden: true }),
|
|
3803
|
-
success: Joi.boolean().example(true).description('Was the referenced message processed successfully').label('ResponseReferenceSuccess'),
|
|
3804
|
-
error: Joi.string().example('Referenced message was not found').description('An error message if referenced message processing failed')
|
|
3805
|
-
})
|
|
3806
|
-
.description('Reference info if referencing was requested')
|
|
3807
|
-
.label('ResponseReference'),
|
|
3808
|
-
|
|
3809
|
-
preview: Joi.string()
|
|
3810
|
-
.base64()
|
|
3811
|
-
.example('Q29udGVudC1UeXBlOiBtdWx0aX...')
|
|
3812
|
-
.description('Base64 encoded RFC822 email if a preview was requested')
|
|
3813
|
-
.label('ResponsePreview'),
|
|
3814
|
-
|
|
3815
|
-
mailMerge: Joi.array()
|
|
3816
|
-
.items(
|
|
3817
|
-
Joi.object({
|
|
3818
|
-
success: Joi.boolean()
|
|
3819
|
-
.example(true)
|
|
3820
|
-
.description('Was the referenced message processed successfully')
|
|
3821
|
-
.label('ResponseReferenceSuccess'),
|
|
3822
|
-
to: addressSchema.label('ToAddressSingle'),
|
|
3823
|
-
messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
|
|
3824
|
-
queueId: Joi.string()
|
|
3825
|
-
.example('d41f0423195f271f')
|
|
3826
|
-
.description('Queue identifier for scheduled email. Not present for bulk messages.'),
|
|
3827
|
-
reference: Joi.object({
|
|
3828
|
-
message: Joi.string()
|
|
3829
|
-
.base64({ paddingRequired: false, urlSafe: true })
|
|
3830
|
-
.max(256)
|
|
3831
|
-
.required()
|
|
3832
|
-
.example('AAAAAQAACnA')
|
|
3833
|
-
.description('Referenced message ID'),
|
|
3834
|
-
documentStore: Joi.boolean()
|
|
3835
|
-
.example(true)
|
|
3836
|
-
.description('Was the message data loaded from the Document Store')
|
|
3837
|
-
.label('ResponseDocumentStore')
|
|
3838
|
-
.meta({ swaggerHidden: true }),
|
|
3839
|
-
success: Joi.boolean()
|
|
3840
|
-
.example(true)
|
|
3841
|
-
.description('Was the referenced message processed successfully')
|
|
3842
|
-
.label('ResponseReferenceSuccess'),
|
|
3843
|
-
error: Joi.string()
|
|
3844
|
-
.example('Referenced message was not found')
|
|
3845
|
-
.description('An error message if referenced message processing failed')
|
|
3846
|
-
})
|
|
3847
|
-
.description('Reference info if referencing was requested')
|
|
3848
|
-
.label('ResponseReference'),
|
|
3849
|
-
sendAt: Joi.date()
|
|
3850
|
-
.iso()
|
|
3851
|
-
.example('2021-07-08T07:06:34.336Z')
|
|
3852
|
-
.description('Send message at specified time. Overrides message level `sendAt` value.'),
|
|
3853
|
-
skipped: Joi.object({
|
|
3854
|
-
reason: Joi.string().example('unsubscribe').description('Why this message was skipped'),
|
|
3855
|
-
listId: Joi.string().example('test-list')
|
|
3856
|
-
})
|
|
3857
|
-
.description('Info about skipped message. If this value is set, then the message was not sent')
|
|
3858
|
-
.label('SkippedMessageInfo')
|
|
3859
|
-
})
|
|
3860
|
-
.label('BulkResponseEntry')
|
|
3861
|
-
.example({
|
|
3862
|
-
success: true,
|
|
3863
|
-
to: {
|
|
3864
|
-
name: 'Andris 2',
|
|
3865
|
-
address: 'andris@ethereal.email'
|
|
3866
|
-
},
|
|
3867
|
-
messageId: '<19b9c433-d428-f6d8-1d00-d666ebcadfc4@ekiri.ee>',
|
|
3868
|
-
queueId: '1812477338914c8372a',
|
|
3869
|
-
reference: {
|
|
3870
|
-
message: 'AAAAAQAACnA',
|
|
3871
|
-
success: true
|
|
3872
|
-
},
|
|
3873
|
-
sendAt: '2021-07-08T07:06:34.336Z'
|
|
3874
|
-
})
|
|
3875
|
-
.unknown()
|
|
3876
|
-
)
|
|
3877
|
-
.label('BulkResponseList')
|
|
3878
|
-
.description('Bulk message responses')
|
|
3879
|
-
}).label('SubmitMessageResponse'),
|
|
3880
|
-
failAction: 'log'
|
|
3881
|
-
}
|
|
3882
|
-
}
|
|
3883
|
-
});
|
|
3884
|
-
|
|
3885
|
-
server.route({
|
|
3886
|
-
method: 'GET',
|
|
3887
|
-
path: '/v1/settings',
|
|
3888
|
-
|
|
3889
|
-
async handler(request) {
|
|
3890
|
-
let values = {};
|
|
3891
|
-
for (let key of Object.keys(request.query)) {
|
|
3892
|
-
if (request.query[key]) {
|
|
3893
|
-
if (key === 'eventTypes') {
|
|
3894
|
-
values[key] = Object.keys(consts)
|
|
3895
|
-
.map(key => {
|
|
3896
|
-
if (/_NOTIFY?/.test(key)) {
|
|
3897
|
-
return consts[key];
|
|
3898
|
-
}
|
|
3899
|
-
return false;
|
|
3900
|
-
})
|
|
3901
|
-
.map(key => key);
|
|
3902
|
-
continue;
|
|
3903
|
-
}
|
|
3904
|
-
|
|
3905
|
-
let value = await settings.get(key);
|
|
3906
|
-
|
|
3907
|
-
if (settings.encryptedKeys.includes(key)) {
|
|
3908
|
-
// do not reveal secret values
|
|
3909
|
-
// instead show boolean value true if value is set, or false if it's not
|
|
3910
|
-
value = value ? true : false;
|
|
3911
|
-
}
|
|
3912
|
-
|
|
3913
|
-
values[key] = value;
|
|
3914
|
-
}
|
|
3915
|
-
}
|
|
3916
|
-
return values;
|
|
3917
|
-
},
|
|
3918
|
-
options: {
|
|
3919
|
-
description: 'List specific settings',
|
|
3920
|
-
notes: 'List setting values for specific keys',
|
|
3921
|
-
tags: ['api', 'Settings'],
|
|
3922
|
-
|
|
3923
|
-
auth: {
|
|
3924
|
-
strategy: 'api-token',
|
|
3925
|
-
mode: 'required'
|
|
3926
|
-
},
|
|
3927
|
-
cors: CORS_CONFIG,
|
|
3928
|
-
|
|
3929
|
-
validate: {
|
|
3930
|
-
options: {
|
|
3931
|
-
stripUnknown: false,
|
|
3932
|
-
abortEarly: false,
|
|
3933
|
-
convert: true
|
|
3934
|
-
},
|
|
3935
|
-
failAction,
|
|
3936
|
-
|
|
3937
|
-
query: Joi.object(settingsQuerySchema).label('SettingsQuery')
|
|
3938
|
-
},
|
|
3939
|
-
|
|
3940
|
-
response: {
|
|
3941
|
-
schema: Joi.object(settingsSchema).label('SettingsQueryResponse'),
|
|
3942
|
-
failAction: 'log'
|
|
3943
|
-
}
|
|
3944
|
-
}
|
|
3945
|
-
});
|
|
3946
|
-
|
|
3947
|
-
server.route({
|
|
3948
|
-
method: 'POST',
|
|
3949
|
-
path: '/v1/settings',
|
|
3950
|
-
|
|
3951
|
-
async handler(request) {
|
|
3952
|
-
let updated = [];
|
|
3953
|
-
for (let key of Object.keys(request.payload)) {
|
|
3954
|
-
switch (key) {
|
|
3955
|
-
case 'serviceUrl': {
|
|
3956
|
-
let url = new URL(request.payload.serviceUrl);
|
|
3957
|
-
request.payload.serviceUrl = url.origin;
|
|
3958
|
-
break;
|
|
3959
|
-
}
|
|
3960
|
-
|
|
3961
|
-
case 'webhooksEnabled':
|
|
3962
|
-
if (!request.payload.webhooksEnabled) {
|
|
3963
|
-
// clear error message (if exists)
|
|
3964
|
-
await settings.clear('webhookErrorFlag');
|
|
3965
|
-
}
|
|
3966
|
-
break;
|
|
3967
|
-
}
|
|
3968
|
-
|
|
3969
|
-
await settings.set(key, request.payload[key]);
|
|
3970
|
-
updated.push(key);
|
|
3971
|
-
}
|
|
3972
|
-
|
|
3973
|
-
notify('settings', request.payload);
|
|
3974
|
-
if ('httpProxyEnabled' in request.payload || 'httpProxyUrl' in request.payload) {
|
|
3975
|
-
reloadHttpProxyAgent().catch(err => logger.error({ msg: 'Failed to reload HTTP proxy agent', err }));
|
|
3976
|
-
}
|
|
3977
|
-
return { updated };
|
|
3978
|
-
},
|
|
3979
|
-
options: {
|
|
3980
|
-
description: 'Set setting values',
|
|
3981
|
-
notes: 'Set setting values for specific keys',
|
|
3982
|
-
tags: ['api', 'Settings'],
|
|
3983
|
-
|
|
3984
|
-
plugins: {},
|
|
3985
|
-
|
|
3986
|
-
auth: {
|
|
3987
|
-
strategy: 'api-token',
|
|
3988
|
-
mode: 'required'
|
|
3989
|
-
},
|
|
3990
|
-
cors: CORS_CONFIG,
|
|
3991
|
-
|
|
3992
|
-
validate: {
|
|
3993
|
-
options: {
|
|
3994
|
-
stripUnknown: false,
|
|
3995
|
-
abortEarly: false,
|
|
3996
|
-
convert: true
|
|
3997
|
-
},
|
|
3998
|
-
failAction,
|
|
3999
|
-
|
|
4000
|
-
payload: Joi.object(settingsSchema).label('Settings')
|
|
4001
|
-
},
|
|
4002
|
-
|
|
4003
|
-
response: {
|
|
4004
|
-
schema: Joi.object({
|
|
4005
|
-
updated: Joi.array().items(Joi.string().example('notifyHeaders')).description('List of updated setting keys').label('UpdatedSettings')
|
|
4006
|
-
}).label('SettingsUpdatedResponse'),
|
|
4007
|
-
failAction: 'log'
|
|
4008
|
-
}
|
|
4009
|
-
}
|
|
4010
|
-
});
|
|
4011
|
-
|
|
4012
|
-
server.route({
|
|
4013
|
-
method: 'GET',
|
|
4014
|
-
path: '/v1/settings/queue/{queue}',
|
|
4015
|
-
|
|
4016
|
-
async handler(request) {
|
|
4017
|
-
try {
|
|
4018
|
-
let queue = request.params.queue;
|
|
4019
|
-
let values = {
|
|
4020
|
-
queue
|
|
4021
|
-
};
|
|
4022
|
-
|
|
4023
|
-
const [resActive, resDelayed, resPaused, resWaiting, resMeta] = await redis
|
|
4024
|
-
.multi()
|
|
4025
|
-
.llen(`${REDIS_PREFIX}bull:${queue}:active`)
|
|
4026
|
-
.zcard(`${REDIS_PREFIX}bull:${queue}:delayed`)
|
|
4027
|
-
.llen(`${REDIS_PREFIX}bull:${queue}:paused`)
|
|
4028
|
-
.llen(`${REDIS_PREFIX}bull:${queue}:wait`)
|
|
4029
|
-
.hget(`${REDIS_PREFIX}bull:${queue}:meta`, 'paused')
|
|
4030
|
-
.exec();
|
|
4031
|
-
|
|
4032
|
-
if (resActive[0] || resDelayed[0] || resPaused[0] || resWaiting[0]) {
|
|
4033
|
-
// counting failed
|
|
4034
|
-
let err = new Error('Failed to count queue lengtho');
|
|
4035
|
-
err.statusCode = 500;
|
|
4036
|
-
throw err;
|
|
4037
|
-
}
|
|
4038
|
-
|
|
4039
|
-
values.jobs = {
|
|
4040
|
-
active: Number(resActive[1]) || 0,
|
|
4041
|
-
delayed: Number(resDelayed[1]) || 0,
|
|
4042
|
-
paused: Number(resPaused[1]) || 0,
|
|
4043
|
-
waiting: Number(resWaiting[1]) || 0
|
|
4044
|
-
};
|
|
4045
|
-
|
|
4046
|
-
values.paused = !!Number(resMeta[1]) || false;
|
|
4047
|
-
|
|
4048
|
-
return values;
|
|
4049
|
-
} catch (err) {
|
|
4050
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
4051
|
-
if (Boom.isBoom(err)) {
|
|
4052
|
-
throw err;
|
|
4053
|
-
}
|
|
4054
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
4055
|
-
if (err.code) {
|
|
4056
|
-
error.output.payload.code = err.code;
|
|
4057
|
-
}
|
|
4058
|
-
throw error;
|
|
4059
|
-
}
|
|
4060
|
-
},
|
|
4061
|
-
options: {
|
|
4062
|
-
description: 'Show queue information',
|
|
4063
|
-
notes: 'Show queue status and current state',
|
|
4064
|
-
tags: ['api', 'Settings'],
|
|
4065
|
-
|
|
4066
|
-
auth: {
|
|
4067
|
-
strategy: 'api-token',
|
|
4068
|
-
mode: 'required'
|
|
4069
|
-
},
|
|
4070
|
-
cors: CORS_CONFIG,
|
|
4071
|
-
|
|
4072
|
-
validate: {
|
|
4073
|
-
options: {
|
|
4074
|
-
stripUnknown: false,
|
|
4075
|
-
abortEarly: false,
|
|
4076
|
-
convert: true
|
|
4077
|
-
},
|
|
4078
|
-
failAction,
|
|
4079
|
-
|
|
4080
|
-
params: Joi.object({
|
|
4081
|
-
queue: Joi.string()
|
|
4082
|
-
.empty('')
|
|
4083
|
-
.trim()
|
|
4084
|
-
.valid('notify', 'submit', 'documents')
|
|
4085
|
-
.required()
|
|
4086
|
-
.example('notify')
|
|
4087
|
-
.description('Queue ID')
|
|
4088
|
-
.label('QueueId')
|
|
4089
|
-
})
|
|
4090
|
-
},
|
|
4091
|
-
|
|
4092
|
-
response: {
|
|
4093
|
-
schema: Joi.object({
|
|
4094
|
-
queue: Joi.string()
|
|
4095
|
-
.empty('')
|
|
4096
|
-
.trim()
|
|
4097
|
-
.valid('notify', 'submit', 'documents')
|
|
4098
|
-
.required()
|
|
4099
|
-
.example('notify')
|
|
4100
|
-
.description('Queue ID')
|
|
4101
|
-
.label('QueueIdResponse'),
|
|
4102
|
-
jobs: Joi.object({
|
|
4103
|
-
active: Joi.number().integer().example(123).description('Jobs that are currently being processed'),
|
|
4104
|
-
delayed: Joi.number().integer().example(123).description('Jobs that are processed in the future'),
|
|
4105
|
-
paused: Joi.number().integer().example(123).description('Jobs that would be processed once queue processing is resumed'),
|
|
4106
|
-
waiting: Joi.number()
|
|
4107
|
-
.integer()
|
|
4108
|
-
.example(123)
|
|
4109
|
-
.description('Jobs that should be processed, but are waiting until there are any free handlers')
|
|
4110
|
-
}).label('QueueJobs'),
|
|
4111
|
-
paused: Joi.boolean().example(false).description('Is the queue paused or not')
|
|
4112
|
-
}).label('SettingsQueueResponse'),
|
|
4113
|
-
failAction: 'log'
|
|
4114
|
-
}
|
|
4115
|
-
}
|
|
4116
|
-
});
|
|
4117
|
-
|
|
4118
|
-
server.route({
|
|
4119
|
-
method: 'PUT',
|
|
4120
|
-
path: '/v1/settings/queue/{queue}',
|
|
4121
|
-
|
|
4122
|
-
async handler(request) {
|
|
4123
|
-
try {
|
|
4124
|
-
let queue = request.params.queue;
|
|
4125
|
-
|
|
4126
|
-
let queueObj = {
|
|
4127
|
-
documents: documentsQueue,
|
|
4128
|
-
notify: notifyQueue,
|
|
4129
|
-
submit: submitQueue
|
|
4130
|
-
}[queue];
|
|
4131
|
-
|
|
4132
|
-
let values = {
|
|
4133
|
-
queue
|
|
4134
|
-
};
|
|
4135
|
-
|
|
4136
|
-
for (let key of Object.keys(request.payload)) {
|
|
4137
|
-
switch (key) {
|
|
4138
|
-
case 'paused':
|
|
4139
|
-
if (request.payload[key]) {
|
|
4140
|
-
await queueObj.pause();
|
|
4141
|
-
} else {
|
|
4142
|
-
await queueObj.resume();
|
|
4143
|
-
}
|
|
4144
|
-
break;
|
|
4145
|
-
}
|
|
4146
|
-
}
|
|
4147
|
-
|
|
4148
|
-
values.paused = await queueObj.isPaused();
|
|
4149
|
-
|
|
4150
|
-
return values;
|
|
4151
|
-
} catch (err) {
|
|
4152
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
4153
|
-
if (Boom.isBoom(err)) {
|
|
4154
|
-
throw err;
|
|
4155
|
-
}
|
|
4156
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
4157
|
-
if (err.code) {
|
|
4158
|
-
error.output.payload.code = err.code;
|
|
4159
|
-
}
|
|
4160
|
-
throw error;
|
|
4161
|
-
}
|
|
4162
|
-
},
|
|
4163
|
-
options: {
|
|
4164
|
-
description: 'Set queue settings',
|
|
4165
|
-
notes: 'Set queue settings',
|
|
4166
|
-
tags: ['api', 'Settings'],
|
|
4167
|
-
|
|
4168
|
-
plugins: {},
|
|
4169
|
-
|
|
4170
|
-
auth: {
|
|
4171
|
-
strategy: 'api-token',
|
|
4172
|
-
mode: 'required'
|
|
4173
|
-
},
|
|
4174
|
-
cors: CORS_CONFIG,
|
|
4175
|
-
|
|
4176
|
-
validate: {
|
|
4177
|
-
options: {
|
|
4178
|
-
stripUnknown: false,
|
|
4179
|
-
abortEarly: false,
|
|
4180
|
-
convert: true
|
|
4181
|
-
},
|
|
4182
|
-
failAction,
|
|
4183
|
-
|
|
4184
|
-
params: Joi.object({
|
|
4185
|
-
queue: Joi.string()
|
|
4186
|
-
.empty('')
|
|
4187
|
-
.trim()
|
|
4188
|
-
.valid('notify', 'submit', 'documents')
|
|
4189
|
-
.required()
|
|
4190
|
-
.example('notify')
|
|
4191
|
-
.description('Queue ID')
|
|
4192
|
-
.label('QueueIdParam')
|
|
4193
|
-
}),
|
|
4194
|
-
|
|
4195
|
-
payload: Joi.object({
|
|
4196
|
-
paused: Joi.boolean().empty('').example(false).description('Set queue state to paused')
|
|
4197
|
-
}).label('SettingsPutQueuePayload')
|
|
4198
|
-
},
|
|
4199
|
-
|
|
4200
|
-
response: {
|
|
4201
|
-
schema: Joi.object({
|
|
4202
|
-
queue: Joi.string()
|
|
4203
|
-
.empty('')
|
|
4204
|
-
.trim()
|
|
4205
|
-
.valid('notify', 'submit', 'documents')
|
|
4206
|
-
.required()
|
|
4207
|
-
.example('notify')
|
|
4208
|
-
.description('Queue ID')
|
|
4209
|
-
.label('QueueIdPutResponse'),
|
|
4210
|
-
paused: Joi.boolean().example(false).description('Is the queue paused or not')
|
|
4211
|
-
}).label('SettingsPutQueueResponse'),
|
|
4212
|
-
failAction: 'log'
|
|
4213
|
-
}
|
|
4214
|
-
}
|
|
4215
|
-
});
|
|
4216
|
-
|
|
4217
|
-
server.route({
|
|
4218
|
-
method: 'GET',
|
|
4219
|
-
path: '/v1/logs/{account}',
|
|
4220
|
-
|
|
4221
|
-
async handler(request) {
|
|
4222
|
-
return getLogs(redis, request.params.account);
|
|
4223
|
-
},
|
|
4224
|
-
options: {
|
|
4225
|
-
description: 'Return IMAP logs for an account',
|
|
4226
|
-
notes: 'Output is a downloadable text file',
|
|
4227
|
-
tags: ['api', 'Logs'],
|
|
4228
|
-
|
|
4229
|
-
auth: {
|
|
4230
|
-
strategy: 'api-token',
|
|
4231
|
-
mode: 'required'
|
|
4232
|
-
},
|
|
4233
|
-
cors: CORS_CONFIG,
|
|
4234
|
-
|
|
4235
|
-
plugins: {
|
|
4236
|
-
'hapi-swagger': {
|
|
4237
|
-
produces: ['text/plain']
|
|
4238
|
-
}
|
|
4239
|
-
},
|
|
4240
|
-
|
|
4241
|
-
validate: {
|
|
4242
|
-
options: {
|
|
4243
|
-
stripUnknown: false,
|
|
4244
|
-
abortEarly: false,
|
|
4245
|
-
convert: true
|
|
4246
|
-
},
|
|
4247
|
-
failAction,
|
|
4248
|
-
|
|
4249
|
-
params: Joi.object({
|
|
4250
|
-
account: accountIdSchema.required()
|
|
4251
|
-
})
|
|
4252
|
-
}
|
|
4253
|
-
}
|
|
4254
|
-
});
|
|
4255
|
-
|
|
4256
|
-
server.route({
|
|
4257
|
-
method: 'GET',
|
|
4258
|
-
path: '/v1/stats',
|
|
4259
|
-
|
|
4260
|
-
async handler(request) {
|
|
4261
|
-
return await getStats(redis, call, request.query.seconds);
|
|
4262
|
-
},
|
|
4263
|
-
|
|
4264
|
-
options: {
|
|
4265
|
-
description: 'Return server stats',
|
|
4266
|
-
tags: ['api', 'Stats'],
|
|
4267
|
-
|
|
4268
|
-
auth: {
|
|
4269
|
-
strategy: 'api-token',
|
|
4270
|
-
mode: 'required'
|
|
4271
|
-
},
|
|
4272
|
-
cors: CORS_CONFIG,
|
|
4273
|
-
|
|
4274
|
-
validate: {
|
|
4275
|
-
options: {
|
|
4276
|
-
stripUnknown: false,
|
|
4277
|
-
abortEarly: false,
|
|
4278
|
-
convert: true
|
|
4279
|
-
},
|
|
4280
|
-
failAction,
|
|
4281
|
-
|
|
4282
|
-
query: Joi.object({
|
|
4283
|
-
seconds: Joi.number()
|
|
4284
|
-
.integer()
|
|
4285
|
-
.empty('')
|
|
4286
|
-
.min(0)
|
|
4287
|
-
.max(MAX_DAYS_STATS * 24 * 3600)
|
|
4288
|
-
.default(3600)
|
|
4289
|
-
.example(3600)
|
|
4290
|
-
.description('Duration for counters')
|
|
4291
|
-
.label('CounterSeconds')
|
|
4292
|
-
}).label('ServerStats')
|
|
4293
|
-
},
|
|
4294
|
-
|
|
4295
|
-
response: {
|
|
4296
|
-
schema: Joi.object({
|
|
4297
|
-
version: Joi.string().example(packageData.version).description('EmailEngine version number'),
|
|
4298
|
-
license: Joi.string().example(packageData.license).description('EmailEngine license'),
|
|
4299
|
-
accounts: Joi.number().integer().example(26).description('Number of registered accounts'),
|
|
4300
|
-
node: Joi.string().example('16.10.0').description('Node.js Version'),
|
|
4301
|
-
redis: Joi.string().example('6.2.4').description('Redis Version'),
|
|
4302
|
-
connections: Joi.object({
|
|
4303
|
-
init: Joi.number().integer().example(2).description('Accounts not yet initialized'),
|
|
4304
|
-
connected: Joi.number().integer().example(8).description('Successfully connected accounts'),
|
|
4305
|
-
connecting: Joi.number().integer().example(7).description('Connection is being established'),
|
|
4306
|
-
authenticationError: Joi.number().integer().example(3).description('Authentication failed'),
|
|
4307
|
-
connectError: Joi.number().integer().example(5).description('Connection failed due to technical error'),
|
|
4308
|
-
unset: Joi.number().integer().example(0).description('Accounts without valid IMAP settings'),
|
|
4309
|
-
disconnected: Joi.number().integer().example(1).description('IMAP connection was closed')
|
|
4310
|
-
})
|
|
4311
|
-
.description('Counts of accounts in different connection states')
|
|
4312
|
-
.label('ConnectionsStats'),
|
|
4313
|
-
counters: Joi.object().label('CounterStats').unknown()
|
|
4314
|
-
}).label('SettingsResponse'),
|
|
4315
|
-
failAction: 'log'
|
|
4316
|
-
}
|
|
4317
|
-
}
|
|
4318
|
-
});
|
|
4319
|
-
|
|
4320
|
-
server.route({
|
|
4321
|
-
method: 'POST',
|
|
4322
|
-
path: '/v1/verifyAccount',
|
|
4323
|
-
|
|
4324
|
-
async handler(request) {
|
|
4325
|
-
try {
|
|
4326
|
-
return await verifyAccountInfo(redis, request.payload, request.logger.child({ action: 'verify-account' }));
|
|
4327
|
-
} catch (err) {
|
|
4328
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
4329
|
-
if (Boom.isBoom(err)) {
|
|
4330
|
-
throw err;
|
|
4331
|
-
}
|
|
4332
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
4333
|
-
if (err.code) {
|
|
4334
|
-
error.output.payload.code = err.code;
|
|
4335
|
-
}
|
|
4336
|
-
throw error;
|
|
4337
|
-
}
|
|
4338
|
-
},
|
|
4339
|
-
options: {
|
|
4340
|
-
description: 'Verify IMAP and SMTP settings',
|
|
4341
|
-
notes: 'Checks if can connect and authenticate using provided account info',
|
|
4342
|
-
tags: ['api', 'Account'],
|
|
4343
|
-
|
|
4344
|
-
plugins: {},
|
|
4345
|
-
|
|
4346
|
-
auth: {
|
|
4347
|
-
strategy: 'api-token',
|
|
4348
|
-
mode: 'required'
|
|
4349
|
-
},
|
|
4350
|
-
cors: CORS_CONFIG,
|
|
4351
|
-
|
|
4352
|
-
validate: {
|
|
4353
|
-
options: {
|
|
4354
|
-
stripUnknown: false,
|
|
4355
|
-
abortEarly: false,
|
|
4356
|
-
convert: true
|
|
4357
|
-
},
|
|
4358
|
-
failAction,
|
|
4359
|
-
|
|
4360
|
-
payload: Joi.object({
|
|
4361
|
-
mailboxes: Joi.boolean().example(false).description('Include mailbox listing in response').default(false).label('IncludeMailboxes'),
|
|
4362
|
-
imap: Joi.object(imapSchema).allow(false).description('IMAP configuration').label('ImapConfiguration'),
|
|
4363
|
-
smtp: Joi.object(smtpSchema).allow(false).description('SMTP configuration').label('SmtpConfiguration'),
|
|
4364
|
-
proxy: settingsSchema.proxyUrl,
|
|
4365
|
-
smtpEhloName: settingsSchema.smtpEhloName
|
|
4366
|
-
}).label('VerifyAccount')
|
|
4367
|
-
},
|
|
4368
|
-
response: {
|
|
4369
|
-
schema: Joi.object({
|
|
4370
|
-
imap: Joi.object({
|
|
4371
|
-
success: Joi.boolean().example(true).description('Was IMAP account verified').label('VerifyImapSuccess'),
|
|
4372
|
-
error: Joi.string()
|
|
4373
|
-
.example('Something went wrong')
|
|
4374
|
-
.description('Error messages for IMAP verification. Only present if success=false')
|
|
4375
|
-
.label('VerifyImapError'),
|
|
4376
|
-
code: Joi.string()
|
|
4377
|
-
.example('ERR_SSL_WRONG_VERSION_NUMBER')
|
|
4378
|
-
.description('Error code. Only present if success=false')
|
|
4379
|
-
.label('VerifyImapCode')
|
|
4380
|
-
}).label('VerifyImapResult'),
|
|
4381
|
-
smtp: Joi.object({
|
|
4382
|
-
success: Joi.boolean().example(true).description('Was SMTP account verified').label('VerifySmtpSuccess'),
|
|
4383
|
-
error: Joi.string()
|
|
4384
|
-
.example('Something went wrong')
|
|
4385
|
-
.description('Error messages for SMTP verification. Only present if success=false')
|
|
4386
|
-
.label('VerifySmtpError'),
|
|
4387
|
-
code: Joi.string()
|
|
4388
|
-
.example('ERR_SSL_WRONG_VERSION_NUMBER')
|
|
4389
|
-
.description('Error code. Only present if success=false')
|
|
4390
|
-
.label('VerifySmtpCode')
|
|
4391
|
-
}).label('VerifySmtpResult'),
|
|
4392
|
-
mailboxes: shortMailboxesSchema
|
|
4393
|
-
}).label('VerifyAccountResponse'),
|
|
4394
|
-
failAction: 'log'
|
|
4395
|
-
}
|
|
4396
|
-
}
|
|
4397
|
-
});
|
|
4398
|
-
|
|
4399
|
-
server.route({
|
|
4400
|
-
method: 'GET',
|
|
4401
|
-
path: '/v1/license',
|
|
4402
|
-
|
|
4403
|
-
async handler(request) {
|
|
4404
|
-
try {
|
|
4405
|
-
const licenseInfo = await call({ cmd: 'license', timeout: request.headers['x-ee-timeout'] });
|
|
4406
|
-
if (!licenseInfo) {
|
|
4407
|
-
let err = new Error('Failed to load license info');
|
|
4408
|
-
err.statusCode = 403;
|
|
4409
|
-
throw err;
|
|
4410
|
-
}
|
|
4411
|
-
return licenseInfo;
|
|
4412
|
-
} catch (err) {
|
|
4413
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
4414
|
-
if (Boom.isBoom(err)) {
|
|
4415
|
-
throw err;
|
|
4416
|
-
}
|
|
4417
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
4418
|
-
if (err.code) {
|
|
4419
|
-
error.output.payload.code = err.code;
|
|
4420
|
-
}
|
|
4421
|
-
throw error;
|
|
4422
|
-
}
|
|
4423
|
-
},
|
|
4424
|
-
options: {
|
|
4425
|
-
description: 'Request license info',
|
|
4426
|
-
notes: 'Get active license information',
|
|
4427
|
-
tags: ['api', 'License'],
|
|
4428
|
-
|
|
4429
|
-
auth: {
|
|
4430
|
-
strategy: 'api-token',
|
|
4431
|
-
mode: 'required'
|
|
4432
|
-
},
|
|
4433
|
-
cors: CORS_CONFIG,
|
|
4434
|
-
|
|
4435
|
-
response: {
|
|
4436
|
-
schema: licenseSchema.label('LicenseResponse'),
|
|
4437
|
-
failAction: 'log'
|
|
4438
|
-
}
|
|
4439
|
-
}
|
|
4440
|
-
});
|
|
4441
|
-
|
|
4442
|
-
server.route({
|
|
4443
|
-
method: 'DELETE',
|
|
4444
|
-
path: '/v1/license',
|
|
4445
|
-
|
|
4446
|
-
async handler(request) {
|
|
4447
|
-
try {
|
|
4448
|
-
const licenseInfo = await call({ cmd: 'removeLicense', timeout: request.headers['x-ee-timeout'] });
|
|
4449
|
-
if (!licenseInfo) {
|
|
4450
|
-
let err = new Error('Failed to clear license info');
|
|
4451
|
-
err.statusCode = 403;
|
|
4452
|
-
throw err;
|
|
4453
|
-
}
|
|
4454
|
-
return licenseInfo;
|
|
4455
|
-
} catch (err) {
|
|
4456
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
4457
|
-
if (Boom.isBoom(err)) {
|
|
4458
|
-
throw err;
|
|
4459
|
-
}
|
|
4460
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
4461
|
-
if (err.code) {
|
|
4462
|
-
error.output.payload.code = err.code;
|
|
4463
|
-
}
|
|
4464
|
-
throw error;
|
|
4465
|
-
}
|
|
4466
|
-
},
|
|
4467
|
-
options: {
|
|
4468
|
-
description: 'Remove license',
|
|
4469
|
-
notes: 'Remove registered active license',
|
|
4470
|
-
tags: ['api', 'License'],
|
|
4471
|
-
|
|
4472
|
-
plugins: {},
|
|
4473
|
-
|
|
4474
|
-
auth: {
|
|
4475
|
-
strategy: 'api-token',
|
|
4476
|
-
mode: 'required'
|
|
4477
|
-
},
|
|
4478
|
-
cors: CORS_CONFIG,
|
|
4479
|
-
|
|
4480
|
-
response: {
|
|
4481
|
-
schema: Joi.object({
|
|
4482
|
-
active: Joi.boolean().example(false),
|
|
4483
|
-
details: Joi.boolean().example(false),
|
|
4484
|
-
type: Joi.string().example('SSPL-1.0-or-later')
|
|
4485
|
-
}).label('EmptyLicenseResponse'),
|
|
4486
|
-
failAction: 'log'
|
|
4487
|
-
}
|
|
4488
|
-
}
|
|
4489
|
-
});
|
|
4490
|
-
|
|
4491
|
-
server.route({
|
|
4492
|
-
method: 'POST',
|
|
4493
|
-
path: '/v1/license',
|
|
4494
|
-
|
|
4495
|
-
async handler(request) {
|
|
4496
|
-
try {
|
|
4497
|
-
const licenseInfo = await call({ cmd: 'updateLicense', license: request.payload.license, timeout: request.headers['x-ee-timeout'] });
|
|
4498
|
-
if (!licenseInfo) {
|
|
4499
|
-
let err = new Error('Failed to update license. Check license file contents.');
|
|
4500
|
-
err.statusCode = 403;
|
|
4501
|
-
throw err;
|
|
4502
|
-
}
|
|
4503
|
-
return licenseInfo;
|
|
4504
|
-
} catch (err) {
|
|
4505
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
4506
|
-
if (Boom.isBoom(err)) {
|
|
4507
|
-
throw err;
|
|
4508
|
-
}
|
|
4509
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
4510
|
-
if (err.code) {
|
|
4511
|
-
error.output.payload.code = err.code;
|
|
4512
|
-
}
|
|
4513
|
-
throw error;
|
|
4514
|
-
}
|
|
4515
|
-
},
|
|
4516
|
-
options: {
|
|
4517
|
-
description: 'Register a license',
|
|
4518
|
-
notes: 'Set up a license for EmailEngine to unlock all features',
|
|
4519
|
-
tags: ['api', 'License'],
|
|
4520
|
-
|
|
4521
|
-
plugins: {},
|
|
4522
|
-
|
|
4523
|
-
auth: {
|
|
4524
|
-
strategy: 'api-token',
|
|
4525
|
-
mode: 'required'
|
|
4526
|
-
},
|
|
4527
|
-
cors: CORS_CONFIG,
|
|
4528
|
-
|
|
4529
|
-
validate: {
|
|
4530
|
-
options: {
|
|
4531
|
-
stripUnknown: false,
|
|
4532
|
-
abortEarly: false,
|
|
4533
|
-
convert: true
|
|
4534
|
-
},
|
|
4535
|
-
failAction,
|
|
4536
|
-
|
|
4537
|
-
payload: Joi.object({
|
|
4538
|
-
license: Joi.string()
|
|
4539
|
-
.max(10 * 1024)
|
|
4540
|
-
.required()
|
|
4541
|
-
.example('-----BEGIN LICENSE-----\r\n...')
|
|
4542
|
-
.description('License file')
|
|
4543
|
-
}).label('RegisterLicense')
|
|
4544
|
-
},
|
|
4545
|
-
|
|
4546
|
-
response: {
|
|
4547
|
-
schema: licenseSchema.label('LicenseResponse'),
|
|
4548
|
-
failAction: 'log'
|
|
4549
|
-
}
|
|
4550
|
-
}
|
|
4551
|
-
});
|
|
4552
|
-
|
|
4553
|
-
server.route({
|
|
4554
|
-
method: 'GET',
|
|
4555
|
-
path: '/v1/autoconfig',
|
|
4556
|
-
|
|
4557
|
-
async handler(request) {
|
|
4558
|
-
try {
|
|
4559
|
-
let serverSettings = await autodetectImapSettings(request.query.email, request.app.gt);
|
|
4560
|
-
return serverSettings;
|
|
4561
|
-
} catch (err) {
|
|
4562
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
4563
|
-
if (Boom.isBoom(err)) {
|
|
4564
|
-
throw err;
|
|
4565
|
-
}
|
|
4566
|
-
return { imap: false, smtp: false, _source: 'unknown' };
|
|
4567
|
-
}
|
|
4568
|
-
},
|
|
4569
|
-
|
|
4570
|
-
options: {
|
|
4571
|
-
description: 'Discover Email settings',
|
|
4572
|
-
notes: 'Try to discover IMAP and SMTP settings for an email account',
|
|
4573
|
-
tags: ['api', 'Settings'],
|
|
4574
|
-
|
|
4575
|
-
plugins: {},
|
|
4576
|
-
|
|
4577
|
-
auth: {
|
|
4578
|
-
strategy: 'api-token',
|
|
4579
|
-
mode: 'required'
|
|
4580
|
-
},
|
|
4581
|
-
cors: CORS_CONFIG,
|
|
4582
|
-
|
|
4583
|
-
validate: {
|
|
4584
|
-
options: {
|
|
4585
|
-
stripUnknown: false,
|
|
4586
|
-
abortEarly: false,
|
|
4587
|
-
convert: true
|
|
4588
|
-
},
|
|
4589
|
-
failAction,
|
|
4590
|
-
|
|
4591
|
-
query: Joi.object({
|
|
4592
|
-
email: Joi.string()
|
|
4593
|
-
.email()
|
|
4594
|
-
.required()
|
|
4595
|
-
.example('sender@example.com')
|
|
4596
|
-
.description('Email address to discover email settings for')
|
|
4597
|
-
.label('EmailAddress')
|
|
4598
|
-
}).label('AutodiscoverQuery')
|
|
4599
|
-
},
|
|
4600
|
-
|
|
4601
|
-
response: {
|
|
4602
|
-
schema: Joi.object({
|
|
4603
|
-
imap: Joi.object({
|
|
4604
|
-
auth: Joi.object({
|
|
4605
|
-
user: Joi.string().max(256).example('myuser@gmail.com').description('Account username')
|
|
4606
|
-
}).label('DetectedAuthenticationInfo'),
|
|
4607
|
-
|
|
4608
|
-
host: Joi.string().hostname().required().example('imap.gmail.com').description('Hostname to connect to'),
|
|
4609
|
-
port: Joi.number()
|
|
4610
|
-
.integer()
|
|
4611
|
-
.min(1)
|
|
4612
|
-
.max(64 * 1024)
|
|
4613
|
-
.required()
|
|
4614
|
-
.example(993)
|
|
4615
|
-
.description('Service port number'),
|
|
4616
|
-
secure: Joi.boolean().default(false).example(true).description('Should connection use TLS. Usually true for port 993')
|
|
4617
|
-
}).label('ResolvedServerSettings'),
|
|
4618
|
-
smtp: Joi.object({
|
|
4619
|
-
auth: Joi.object({
|
|
4620
|
-
user: Joi.string().max(256).example('myuser@gmail.com').description('Account username')
|
|
4621
|
-
}).label('DetectedAuthenticationInfo'),
|
|
4622
|
-
|
|
4623
|
-
host: Joi.string().hostname().required().example('imap.gmail.com').description('Hostname to connect to'),
|
|
4624
|
-
port: Joi.number()
|
|
4625
|
-
.integer()
|
|
4626
|
-
.min(1)
|
|
4627
|
-
.max(64 * 1024)
|
|
4628
|
-
.required()
|
|
4629
|
-
.example(993)
|
|
4630
|
-
.description('Service port number'),
|
|
4631
|
-
secure: Joi.boolean().default(false).example(true).description('Should connection use TLS. Usually true for port 993')
|
|
4632
|
-
}).label('DiscoveredServerSettings'),
|
|
4633
|
-
_source: Joi.string().example('srv').description('Source for the detected info')
|
|
4634
|
-
}).label('DiscoveredEmailSettings'),
|
|
4635
|
-
failAction: 'log'
|
|
4636
|
-
}
|
|
4637
|
-
}
|
|
4638
|
-
});
|
|
4639
|
-
|
|
4640
|
-
server.route({
|
|
4641
|
-
method: 'GET',
|
|
4642
|
-
path: '/v1/outbox',
|
|
4643
|
-
|
|
4644
|
-
async handler(request) {
|
|
4645
|
-
try {
|
|
4646
|
-
return await outbox.list(Object.assign({ logger }, request.query));
|
|
4647
|
-
} catch (err) {
|
|
4648
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
4649
|
-
if (Boom.isBoom(err)) {
|
|
4650
|
-
throw err;
|
|
4651
|
-
}
|
|
4652
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
4653
|
-
if (err.code) {
|
|
4654
|
-
error.output.payload.code = err.code;
|
|
4655
|
-
}
|
|
4656
|
-
throw error;
|
|
4657
|
-
}
|
|
4658
|
-
},
|
|
4659
|
-
|
|
4660
|
-
options: {
|
|
4661
|
-
description: 'List queued messages',
|
|
4662
|
-
notes: 'Lists messages in the Outbox',
|
|
4663
|
-
tags: ['api', 'Outbox'],
|
|
4664
|
-
|
|
4665
|
-
plugins: {},
|
|
4666
|
-
|
|
4667
|
-
auth: {
|
|
4668
|
-
strategy: 'api-token',
|
|
4669
|
-
mode: 'required'
|
|
4670
|
-
},
|
|
4671
|
-
cors: CORS_CONFIG,
|
|
4672
|
-
|
|
4673
|
-
validate: {
|
|
4674
|
-
options: {
|
|
4675
|
-
stripUnknown: false,
|
|
4676
|
-
abortEarly: false,
|
|
4677
|
-
convert: true
|
|
4678
|
-
},
|
|
4679
|
-
failAction,
|
|
4680
|
-
|
|
4681
|
-
query: Joi.object({
|
|
4682
|
-
page: Joi.number()
|
|
4683
|
-
.integer()
|
|
4684
|
-
.min(0)
|
|
4685
|
-
.max(1024 * 1024)
|
|
4686
|
-
.default(0)
|
|
4687
|
-
.example(0)
|
|
4688
|
-
.description('Page number (zero indexed, so use 0 for first page)')
|
|
4689
|
-
.label('PageNumber'),
|
|
4690
|
-
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
|
|
4691
|
-
}).label('OutbixListFilter')
|
|
4692
|
-
},
|
|
4693
|
-
|
|
4694
|
-
response: {
|
|
4695
|
-
schema: Joi.object({
|
|
4696
|
-
total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
|
|
4697
|
-
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
4698
|
-
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
4699
|
-
|
|
4700
|
-
messages: Joi.array().items(outboxEntrySchema).label('OutboxListEntries')
|
|
4701
|
-
}).label('OutboxListResponse'),
|
|
4702
|
-
failAction: 'log'
|
|
4703
|
-
}
|
|
4704
|
-
}
|
|
4705
|
-
});
|
|
4706
|
-
|
|
4707
|
-
server.route({
|
|
4708
|
-
method: 'GET',
|
|
4709
|
-
path: '/v1/outbox/{queueId}',
|
|
4710
|
-
|
|
4711
|
-
async handler(request) {
|
|
4712
|
-
try {
|
|
4713
|
-
let outboxEntry = await outbox.get({ queueId: request.params.queueId, logger });
|
|
4714
|
-
if (!outboxEntry) {
|
|
4715
|
-
let message = 'Requested queue entry was not found';
|
|
4716
|
-
let error = Boom.boomify(new Error(message), { statusCode: 404 });
|
|
4717
|
-
throw error;
|
|
4718
|
-
}
|
|
4719
|
-
return outboxEntry;
|
|
4720
|
-
} catch (err) {
|
|
4721
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
4722
|
-
if (Boom.isBoom(err)) {
|
|
4723
|
-
throw err;
|
|
4724
|
-
}
|
|
4725
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
4726
|
-
if (err.code) {
|
|
4727
|
-
error.output.payload.code = err.code;
|
|
4728
|
-
}
|
|
4729
|
-
throw error;
|
|
4730
|
-
}
|
|
4731
|
-
},
|
|
4732
|
-
|
|
4733
|
-
options: {
|
|
4734
|
-
description: 'Get queued message',
|
|
4735
|
-
notes: 'Gets a queued message in the Outbox',
|
|
4736
|
-
tags: ['api', 'Outbox'],
|
|
4737
|
-
|
|
4738
|
-
plugins: {},
|
|
4739
|
-
|
|
4740
|
-
auth: {
|
|
4741
|
-
strategy: 'api-token',
|
|
4742
|
-
mode: 'required'
|
|
4743
|
-
},
|
|
4744
|
-
cors: CORS_CONFIG,
|
|
4745
|
-
|
|
4746
|
-
validate: {
|
|
4747
|
-
options: {
|
|
4748
|
-
stripUnknown: false,
|
|
4749
|
-
abortEarly: false,
|
|
4750
|
-
convert: true
|
|
4751
|
-
},
|
|
4752
|
-
failAction,
|
|
4753
|
-
|
|
4754
|
-
params: Joi.object({
|
|
4755
|
-
queueId: Joi.string().max(100).example('d41f0423195f271f').description('Queue identifier for scheduled email').required()
|
|
4756
|
-
}).label('OutboxEntryParams')
|
|
4757
|
-
},
|
|
4758
|
-
|
|
4759
|
-
response: {
|
|
4760
|
-
schema: outboxEntrySchema,
|
|
4761
|
-
failAction: 'log'
|
|
4762
|
-
}
|
|
4763
|
-
}
|
|
4764
|
-
});
|
|
4765
|
-
|
|
4766
|
-
server.route({
|
|
4767
|
-
method: 'DELETE',
|
|
4768
|
-
path: '/v1/outbox/{queueId}',
|
|
4769
|
-
|
|
4770
|
-
async handler(request) {
|
|
4771
|
-
try {
|
|
4772
|
-
return {
|
|
4773
|
-
deleted: await outbox.del({ queueId: request.params.queueId, logger })
|
|
4774
|
-
};
|
|
4775
|
-
} catch (err) {
|
|
4776
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
4777
|
-
if (Boom.isBoom(err)) {
|
|
4778
|
-
throw err;
|
|
4779
|
-
}
|
|
4780
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
4781
|
-
if (err.code) {
|
|
4782
|
-
error.output.payload.code = err.code;
|
|
4783
|
-
}
|
|
4784
|
-
throw error;
|
|
4785
|
-
}
|
|
4786
|
-
},
|
|
4787
|
-
options: {
|
|
4788
|
-
description: 'Remove a message',
|
|
4789
|
-
notes: 'Remove a message from the outbox',
|
|
4790
|
-
tags: ['api', 'Outbox'],
|
|
4791
|
-
|
|
4792
|
-
plugins: {},
|
|
4793
|
-
|
|
4794
|
-
auth: {
|
|
4795
|
-
strategy: 'api-token',
|
|
4796
|
-
mode: 'required'
|
|
4797
|
-
},
|
|
4798
|
-
cors: CORS_CONFIG,
|
|
4799
|
-
|
|
4800
|
-
validate: {
|
|
4801
|
-
options: {
|
|
4802
|
-
stripUnknown: false,
|
|
4803
|
-
abortEarly: false,
|
|
4804
|
-
convert: true
|
|
4805
|
-
},
|
|
4806
|
-
failAction,
|
|
4807
|
-
|
|
4808
|
-
params: Joi.object({
|
|
4809
|
-
queueId: Joi.string().max(100).example('d41f0423195f271f').description('Queue identifier for scheduled email').required()
|
|
4810
|
-
}).label('OutboxEntryParams')
|
|
4811
|
-
},
|
|
4812
|
-
|
|
4813
|
-
response: {
|
|
4814
|
-
schema: Joi.object({
|
|
4815
|
-
deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the message deleted')
|
|
4816
|
-
}).label('DeleteOutboxEntryResponse'),
|
|
4817
|
-
failAction: 'log'
|
|
4818
|
-
}
|
|
4819
|
-
}
|
|
4820
|
-
});
|
|
4821
|
-
|
|
4822
|
-
// setup template routes
|
|
4823
|
-
await templateRoutes({ server, call, CORS_CONFIG });
|
|
4824
|
-
|
|
4825
|
-
// setup "chat with email" routes
|
|
4826
|
-
await chatRoutes({ server, call, CORS_CONFIG });
|
|
4827
|
-
|
|
4828
|
-
// setup account CRUD routes
|
|
4829
|
-
await accountRoutes({
|
|
4830
|
-
server,
|
|
4831
|
-
call,
|
|
4832
|
-
documentsQueue,
|
|
4833
|
-
oauth2Schema,
|
|
4834
|
-
imapSchema,
|
|
4835
|
-
smtpSchema,
|
|
4836
|
-
CORS_CONFIG,
|
|
4837
|
-
AccountTypeSchema
|
|
4838
|
-
});
|
|
4839
|
-
|
|
4840
|
-
// setup message routes
|
|
4841
|
-
await messageRoutes({
|
|
4842
|
-
server,
|
|
4843
|
-
call,
|
|
4844
|
-
CORS_CONFIG,
|
|
4845
|
-
MAX_ATTACHMENT_SIZE,
|
|
4846
|
-
MAX_BODY_SIZE,
|
|
4847
|
-
MAX_PAYLOAD_TIMEOUT
|
|
4848
|
-
});
|
|
4849
|
-
|
|
4850
|
-
// setup export routes
|
|
4851
|
-
await exportRoutes({
|
|
4852
|
-
server,
|
|
4853
|
-
CORS_CONFIG
|
|
4854
|
-
});
|
|
4855
|
-
|
|
4856
|
-
server.route({
|
|
4857
|
-
method: 'GET',
|
|
4858
|
-
path: '/v1/webhookRoutes',
|
|
4859
|
-
|
|
4860
|
-
async handler(request) {
|
|
4861
|
-
try {
|
|
4862
|
-
return await Webhooks.list(request.query.page, request.query.pageSize);
|
|
4863
|
-
} catch (err) {
|
|
4864
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
4865
|
-
if (Boom.isBoom(err)) {
|
|
4866
|
-
throw err;
|
|
4867
|
-
}
|
|
4868
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
4869
|
-
if (err.code) {
|
|
4870
|
-
error.output.payload.code = err.code;
|
|
4871
|
-
}
|
|
4872
|
-
throw error;
|
|
4873
|
-
}
|
|
4874
|
-
},
|
|
4875
|
-
|
|
4876
|
-
options: {
|
|
4877
|
-
description: 'List webhook routes',
|
|
4878
|
-
notes: 'List custom webhook routes',
|
|
4879
|
-
tags: ['api', 'Webhooks'],
|
|
4880
|
-
|
|
4881
|
-
plugins: {},
|
|
4882
|
-
|
|
4883
|
-
auth: {
|
|
4884
|
-
strategy: 'api-token',
|
|
4885
|
-
mode: 'required'
|
|
4886
|
-
},
|
|
4887
|
-
cors: CORS_CONFIG,
|
|
4888
|
-
|
|
4889
|
-
validate: {
|
|
4890
|
-
options: {
|
|
4891
|
-
stripUnknown: false,
|
|
4892
|
-
abortEarly: false,
|
|
4893
|
-
convert: true
|
|
4894
|
-
},
|
|
4895
|
-
failAction,
|
|
4896
|
-
|
|
4897
|
-
query: Joi.object({
|
|
4898
|
-
page: Joi.number()
|
|
4899
|
-
.integer()
|
|
4900
|
-
.min(0)
|
|
4901
|
-
.max(1024 * 1024)
|
|
4902
|
-
.default(0)
|
|
4903
|
-
.example(0)
|
|
4904
|
-
.description('Page number (zero indexed, so use 0 for first page)')
|
|
4905
|
-
.label('PageNumber'),
|
|
4906
|
-
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
|
|
4907
|
-
}).label('WebhookRoutesListRequest')
|
|
4908
|
-
},
|
|
4909
|
-
|
|
4910
|
-
response: {
|
|
4911
|
-
schema: Joi.object({
|
|
4912
|
-
total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
|
|
4913
|
-
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
4914
|
-
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
4915
|
-
|
|
4916
|
-
webhooks: Joi.array()
|
|
4917
|
-
.items(
|
|
4918
|
-
Joi.object({
|
|
4919
|
-
id: Joi.string().max(256).required().example('AAABgS-UcAYAAAABAA').description('Webhook ID'),
|
|
4920
|
-
name: Joi.string().max(256).example('Send to Slack').description('Name of the route').label('WebhookRouteName').required(),
|
|
4921
|
-
description: Joi.string()
|
|
4922
|
-
.allow('')
|
|
4923
|
-
.max(1024)
|
|
4924
|
-
.example('Something about the route')
|
|
4925
|
-
.description('Optional description of the webhook route')
|
|
4926
|
-
.label('WebhookRouteDescription'),
|
|
4927
|
-
created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this route was created'),
|
|
4928
|
-
updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this route was last updated'),
|
|
4929
|
-
enabled: Joi.boolean().example(true).description('Is the route enabled').label('WebhookRouteEnabled'),
|
|
4930
|
-
targetUrl: settingsSchema.webhooks,
|
|
4931
|
-
tcount: Joi.number().integer().example(123).description('How many times this route has been applied')
|
|
4932
|
-
}).label('WebhookRoutesListEntry')
|
|
4933
|
-
)
|
|
4934
|
-
.label('WebhookRoutesList')
|
|
4935
|
-
}).label('WebhookRoutesListResponse'),
|
|
4936
|
-
failAction: 'log'
|
|
4937
|
-
}
|
|
4938
|
-
}
|
|
4939
|
-
});
|
|
4940
|
-
|
|
4941
|
-
server.route({
|
|
4942
|
-
method: 'GET',
|
|
4943
|
-
path: '/v1/webhookRoutes/webhookRoute/{webhookRoute}',
|
|
4944
|
-
|
|
4945
|
-
async handler(request) {
|
|
4946
|
-
try {
|
|
4947
|
-
return await Webhooks.get(request.params.webhookRoute);
|
|
4948
|
-
} catch (err) {
|
|
4949
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
4950
|
-
if (Boom.isBoom(err)) {
|
|
4951
|
-
throw err;
|
|
4952
|
-
}
|
|
4953
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
4954
|
-
if (err.code) {
|
|
4955
|
-
error.output.payload.code = err.code;
|
|
4956
|
-
}
|
|
4957
|
-
throw error;
|
|
4958
|
-
}
|
|
4959
|
-
},
|
|
4960
|
-
|
|
4961
|
-
options: {
|
|
4962
|
-
description: 'Get webhook route information',
|
|
4963
|
-
notes: 'Retrieve webhook route content and information',
|
|
4964
|
-
tags: ['api', 'Webhooks'],
|
|
4965
|
-
|
|
4966
|
-
plugins: {},
|
|
4967
|
-
|
|
4968
|
-
auth: {
|
|
4969
|
-
strategy: 'api-token',
|
|
4970
|
-
mode: 'required'
|
|
4971
|
-
},
|
|
4972
|
-
cors: CORS_CONFIG,
|
|
4973
|
-
|
|
4974
|
-
validate: {
|
|
4975
|
-
options: {
|
|
4976
|
-
stripUnknown: false,
|
|
4977
|
-
abortEarly: false,
|
|
4978
|
-
convert: true
|
|
4979
|
-
},
|
|
4980
|
-
failAction,
|
|
4981
|
-
params: Joi.object({
|
|
4982
|
-
webhookRoute: Joi.string().max(256).required().example('example').description('Webhook ID')
|
|
4983
|
-
}).label('GetWebhookRouteRequest')
|
|
4984
|
-
},
|
|
4985
|
-
|
|
4986
|
-
response: {
|
|
4987
|
-
schema: Joi.object({
|
|
4988
|
-
id: Joi.string().max(256).required().example('AAABgS-UcAYAAAABAA').description('Webhook ID'),
|
|
4989
|
-
name: Joi.string().max(256).example('Send to Slack').description('Name of the route').label('WebhookRouteName').required(),
|
|
4990
|
-
description: Joi.string()
|
|
4991
|
-
.allow('')
|
|
4992
|
-
.max(1024)
|
|
4993
|
-
.example('Something about the route')
|
|
4994
|
-
.description('Optional description of the webhook route')
|
|
4995
|
-
.label('WebhookRouteDescription'),
|
|
4996
|
-
created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this route was created'),
|
|
4997
|
-
updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this route was last updated'),
|
|
4998
|
-
enabled: Joi.boolean().example(true).description('Is the route enabled').label('WebhookRouteEnabled'),
|
|
4999
|
-
targetUrl: settingsSchema.webhooks,
|
|
5000
|
-
tcount: Joi.number().integer().example(123).description('How many times this route has been applied'),
|
|
5001
|
-
content: Joi.object({
|
|
5002
|
-
fn: Joi.string().example('return true;').description('Filter function'),
|
|
5003
|
-
map: Joi.string().example('payload.ts = Date.now(); return payload;').description('Mapping function')
|
|
5004
|
-
}).label('WebhookRouteContent')
|
|
5005
|
-
}).label('WebhookRouteResponse'),
|
|
5006
|
-
failAction: 'log'
|
|
5007
|
-
}
|
|
5008
|
-
}
|
|
5009
|
-
});
|
|
5010
|
-
|
|
5011
|
-
server.route({
|
|
5012
|
-
method: 'GET',
|
|
5013
|
-
path: '/v1/oauth2',
|
|
5014
|
-
|
|
5015
|
-
async handler(request) {
|
|
5016
|
-
try {
|
|
5017
|
-
let response = await oauth2Apps.list(request.query.page, request.query.pageSize);
|
|
5018
|
-
|
|
5019
|
-
for (let app of response.apps) {
|
|
5020
|
-
for (let secretKey of ['clientSecret', 'serviceKey', 'accessToken', 'externalAccount']) {
|
|
5021
|
-
if (app[secretKey]) {
|
|
5022
|
-
app[secretKey] = '******';
|
|
5023
|
-
}
|
|
5024
|
-
}
|
|
5025
|
-
|
|
5026
|
-
if (app.extraScopes && !app.extraScopes.length) {
|
|
5027
|
-
delete app.extraScopes;
|
|
5028
|
-
}
|
|
5029
|
-
|
|
5030
|
-
if (app.app) {
|
|
5031
|
-
delete app.app;
|
|
5032
|
-
}
|
|
5033
|
-
|
|
5034
|
-
flattenOAuthAppMeta(app);
|
|
5035
|
-
}
|
|
5036
|
-
|
|
5037
|
-
return response;
|
|
5038
|
-
} catch (err) {
|
|
5039
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
5040
|
-
if (Boom.isBoom(err)) {
|
|
5041
|
-
throw err;
|
|
5042
|
-
}
|
|
5043
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
5044
|
-
if (err.code) {
|
|
5045
|
-
error.output.payload.code = err.code;
|
|
5046
|
-
}
|
|
5047
|
-
throw error;
|
|
5048
|
-
}
|
|
5049
|
-
},
|
|
5050
|
-
|
|
5051
|
-
options: {
|
|
5052
|
-
description: 'List OAuth2 applications',
|
|
5053
|
-
notes: 'Lists registered OAuth2 applications',
|
|
5054
|
-
tags: ['api', 'OAuth2 Applications'],
|
|
5055
|
-
|
|
5056
|
-
plugins: {},
|
|
5057
|
-
|
|
5058
|
-
auth: {
|
|
5059
|
-
strategy: 'api-token',
|
|
5060
|
-
mode: 'required'
|
|
5061
|
-
},
|
|
5062
|
-
cors: CORS_CONFIG,
|
|
5063
|
-
|
|
5064
|
-
validate: {
|
|
5065
|
-
options: {
|
|
5066
|
-
stripUnknown: false,
|
|
5067
|
-
abortEarly: false,
|
|
5068
|
-
convert: true
|
|
5069
|
-
},
|
|
5070
|
-
failAction,
|
|
5071
|
-
|
|
5072
|
-
query: Joi.object({
|
|
5073
|
-
page: Joi.number()
|
|
5074
|
-
.integer()
|
|
5075
|
-
.min(0)
|
|
5076
|
-
.max(1024 * 1024)
|
|
5077
|
-
.default(0)
|
|
5078
|
-
.example(0)
|
|
5079
|
-
.description('Page number (zero indexed, so use 0 for first page)')
|
|
5080
|
-
.label('PageNumber'),
|
|
5081
|
-
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
|
|
5082
|
-
}).label('GatewaysFilter')
|
|
5083
|
-
},
|
|
5084
|
-
|
|
5085
|
-
response: {
|
|
5086
|
-
schema: Joi.object({
|
|
5087
|
-
total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
|
|
5088
|
-
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
5089
|
-
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
5090
|
-
|
|
5091
|
-
apps: Joi.array()
|
|
5092
|
-
.items(
|
|
5093
|
-
Joi.object({
|
|
5094
|
-
id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
5095
|
-
name: Joi.string().max(256).example('My OAuth2 App').description('Display name for the app'),
|
|
5096
|
-
description: Joi.string().empty('').trim().max(1024).example('App description').description('OAuth2 application description'),
|
|
5097
|
-
title: Joi.string().empty('').trim().max(256).example('App title').description('Title for the application button'),
|
|
5098
|
-
provider: OAuth2ProviderSchema,
|
|
5099
|
-
enabled: Joi.boolean()
|
|
5100
|
-
.truthy('Y', 'true', '1', 'on')
|
|
5101
|
-
.falsy('N', 'false', 0, '')
|
|
5102
|
-
.example(true)
|
|
5103
|
-
.description('Is the application enabled')
|
|
5104
|
-
.label('AppEnabled'),
|
|
5105
|
-
legacy: Joi.boolean()
|
|
5106
|
-
.truthy('Y', 'true', '1', 'on')
|
|
5107
|
-
.falsy('N', 'false', 0, '')
|
|
5108
|
-
.example(true)
|
|
5109
|
-
.description('`true` for older OAuth2 apps set via the settings endpoint'),
|
|
5110
|
-
created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was added').required(),
|
|
5111
|
-
updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was updated'),
|
|
5112
|
-
includeInListing: Joi.boolean()
|
|
5113
|
-
.truthy('Y', 'true', '1', 'on')
|
|
5114
|
-
.falsy('N', 'false', 0, '')
|
|
5115
|
-
.example(true)
|
|
5116
|
-
.description('Is the application listed in the hosted authentication form'),
|
|
5117
|
-
|
|
5118
|
-
clientId: Joi.string()
|
|
5119
|
-
.example('4f05f488-d858-4f2c-bd12-1039062612fe')
|
|
5120
|
-
.description('Client or Application ID for 3-legged OAuth2 applications')
|
|
5121
|
-
.label('OAuth2AppListClientId'),
|
|
5122
|
-
clientSecret: Joi.string()
|
|
5123
|
-
.example('******')
|
|
5124
|
-
.description('Client secret for 3-legged OAuth2 applications. Actual value is not revealed.'),
|
|
5125
|
-
authority: Joi.string().example('common').description('Authorization tenant value for Outlook OAuth2 applications'),
|
|
5126
|
-
redirectUrl: Joi.string()
|
|
5127
|
-
.uri({
|
|
5128
|
-
scheme: ['http', 'https'],
|
|
5129
|
-
allowRelative: false
|
|
5130
|
-
})
|
|
5131
|
-
.example('https://myservice.com/oauth')
|
|
5132
|
-
.description('Redirect URL for 3-legged OAuth2 applications')
|
|
5133
|
-
.label('OAuth2AppListRedirectUrl'),
|
|
5134
|
-
|
|
5135
|
-
serviceClient: Joi.string()
|
|
5136
|
-
.example('9103965568215821627203')
|
|
5137
|
-
.description('Service client ID for 2-legged OAuth2 applications')
|
|
5138
|
-
.label('OAuth2AppListServiceClient'),
|
|
5139
|
-
|
|
5140
|
-
googleProjectId: googleProjectIdSchema,
|
|
5141
|
-
googleWorkspaceAccounts: googleWorkspaceAccountsSchema,
|
|
5142
|
-
googleTopicName: googleTopicNameSchema,
|
|
5143
|
-
googleSubscriptionName: googleSubscriptionNameSchema,
|
|
5144
|
-
|
|
5145
|
-
serviceClientEmail: Joi.string()
|
|
5146
|
-
.email()
|
|
5147
|
-
.example('name@project-123.iam.gserviceaccount.com')
|
|
5148
|
-
.description('Service Client Email for 2-legged OAuth2 applications'),
|
|
5149
|
-
|
|
5150
|
-
serviceKey: Joi.string()
|
|
5151
|
-
.example('******')
|
|
5152
|
-
.description('PEM formatted service secret for 2-legged OAuth2 applications. Actual value is not revealed.'),
|
|
5153
|
-
|
|
5154
|
-
lastError: lastErrorSchema.allow(null),
|
|
5155
|
-
pubSubError: pubSubErrorSchema.allow(null)
|
|
5156
|
-
}).label('OAuth2ResponseItem')
|
|
5157
|
-
)
|
|
5158
|
-
.label('OAuth2Entries')
|
|
5159
|
-
}).label('OAuth2FilterResponse'),
|
|
5160
|
-
failAction: 'log'
|
|
5161
|
-
}
|
|
5162
|
-
}
|
|
5163
|
-
});
|
|
5164
|
-
|
|
5165
|
-
server.route({
|
|
5166
|
-
method: 'GET',
|
|
5167
|
-
path: '/v1/oauth2/{app}',
|
|
5168
|
-
|
|
5169
|
-
async handler(request) {
|
|
5170
|
-
try {
|
|
5171
|
-
let app = await oauth2Apps.get(request.params.app);
|
|
5172
|
-
|
|
5173
|
-
// remove secrets
|
|
5174
|
-
for (let secretKey of ['clientSecret', 'serviceKey', 'accessToken', 'externalAccount']) {
|
|
5175
|
-
if (app[secretKey]) {
|
|
5176
|
-
app[secretKey] = '******';
|
|
5177
|
-
}
|
|
5178
|
-
}
|
|
5179
|
-
|
|
5180
|
-
if (app.extraScopes && !app.extraScopes.length) {
|
|
5181
|
-
delete app.extraScopes;
|
|
5182
|
-
}
|
|
5183
|
-
|
|
5184
|
-
if (app.app) {
|
|
5185
|
-
delete app.app;
|
|
5186
|
-
}
|
|
5187
|
-
|
|
5188
|
-
flattenOAuthAppMeta(app);
|
|
5189
|
-
|
|
5190
|
-
return app;
|
|
5191
|
-
} catch (err) {
|
|
5192
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
5193
|
-
if (Boom.isBoom(err)) {
|
|
5194
|
-
throw err;
|
|
5195
|
-
}
|
|
5196
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
5197
|
-
if (err.code) {
|
|
5198
|
-
error.output.payload.code = err.code;
|
|
5199
|
-
}
|
|
5200
|
-
throw error;
|
|
5201
|
-
}
|
|
5202
|
-
},
|
|
5203
|
-
options: {
|
|
5204
|
-
description: 'Get application info',
|
|
5205
|
-
notes: 'Returns stored information about an OAuth2 application. Secrets are not included.',
|
|
5206
|
-
tags: ['api', 'OAuth2 Applications'],
|
|
5207
|
-
|
|
5208
|
-
auth: {
|
|
5209
|
-
strategy: 'api-token',
|
|
5210
|
-
mode: 'required'
|
|
5211
|
-
},
|
|
5212
|
-
cors: CORS_CONFIG,
|
|
5213
|
-
|
|
5214
|
-
validate: {
|
|
5215
|
-
options: {
|
|
5216
|
-
stripUnknown: false,
|
|
5217
|
-
abortEarly: false,
|
|
5218
|
-
convert: true
|
|
5219
|
-
},
|
|
5220
|
-
failAction,
|
|
5221
|
-
|
|
5222
|
-
params: Joi.object({
|
|
5223
|
-
app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID')
|
|
5224
|
-
})
|
|
5225
|
-
},
|
|
5226
|
-
|
|
5227
|
-
response: {
|
|
5228
|
-
schema: Joi.object({
|
|
5229
|
-
id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
5230
|
-
name: Joi.string().max(256).example('My OAuth2 App').description('Display name for the app'),
|
|
5231
|
-
description: Joi.string().empty('').trim().max(1024).example('App description').description('OAuth2 application description'),
|
|
5232
|
-
title: Joi.string().empty('').trim().max(256).example('App title').description('Title for the application button'),
|
|
5233
|
-
provider: OAuth2ProviderSchema,
|
|
5234
|
-
enabled: Joi.boolean()
|
|
5235
|
-
.truthy('Y', 'true', '1', 'on')
|
|
5236
|
-
.falsy('N', 'false', 0, '')
|
|
5237
|
-
.example(true)
|
|
5238
|
-
.description('Is the application enabled')
|
|
5239
|
-
.label('AppEnabled'),
|
|
5240
|
-
legacy: Joi.boolean()
|
|
5241
|
-
.truthy('Y', 'true', '1', 'on')
|
|
5242
|
-
.falsy('N', 'false', 0, '')
|
|
5243
|
-
.example(true)
|
|
5244
|
-
.description('`true` for older OAuth2 apps set via the settings endpoint'),
|
|
5245
|
-
created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was added').required(),
|
|
5246
|
-
updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was updated'),
|
|
5247
|
-
includeInListing: Joi.boolean()
|
|
5248
|
-
.truthy('Y', 'true', '1', 'on')
|
|
5249
|
-
.falsy('N', 'false', 0, '')
|
|
5250
|
-
.example(true)
|
|
5251
|
-
.description('Is the application listed in the hosted authentication form'),
|
|
5252
|
-
|
|
5253
|
-
clientId: Joi.string()
|
|
5254
|
-
.example('4f05f488-d858-4f2c-bd12-1039062612fe')
|
|
5255
|
-
.description('Client or Application ID for 3-legged OAuth2 applications')
|
|
5256
|
-
.label('OAuth2AppGetClientId'),
|
|
5257
|
-
clientSecret: Joi.string().example('******').description('Client secret for 3-legged OAuth2 applications. Actual value is not revealed.'),
|
|
5258
|
-
authority: Joi.string().example('common').description('Authorization tenant value for Outlook OAuth2 applications'),
|
|
5259
|
-
redirectUrl: Joi.string()
|
|
5260
|
-
.uri({
|
|
5261
|
-
scheme: ['http', 'https'],
|
|
5262
|
-
allowRelative: false
|
|
5263
|
-
})
|
|
5264
|
-
.example('https://myservice.com/oauth')
|
|
5265
|
-
.description('Redirect URL for 3-legged OAuth2 applications')
|
|
5266
|
-
.label('OAuth2AppGetRedirectUrl'),
|
|
5267
|
-
|
|
5268
|
-
googleProjectId: googleProjectIdSchema,
|
|
5269
|
-
googleWorkspaceAccounts: googleWorkspaceAccountsSchema,
|
|
5270
|
-
googleTopicName: googleTopicNameSchema,
|
|
5271
|
-
googleSubscriptionName: googleSubscriptionNameSchema,
|
|
5272
|
-
|
|
5273
|
-
serviceClientEmail: Joi.string()
|
|
5274
|
-
.email()
|
|
5275
|
-
.example('name@project-123.iam.gserviceaccount.com')
|
|
5276
|
-
.description('Service Client Email for 2-legged OAuth2 applications'),
|
|
5277
|
-
|
|
5278
|
-
serviceClient: Joi.string()
|
|
5279
|
-
.example('9103965568215821627203')
|
|
5280
|
-
.description('Service client ID for 2-legged OAuth2 applications')
|
|
5281
|
-
.label('OAuth2AppGetServiceClient'),
|
|
5282
|
-
|
|
5283
|
-
serviceKey: Joi.string()
|
|
5284
|
-
.example('******')
|
|
5285
|
-
.description('PEM formatted service secret for 2-legged OAuth2 applications. Actual value is not revealed.'),
|
|
5286
|
-
|
|
5287
|
-
accounts: Joi.number()
|
|
5288
|
-
.integer()
|
|
5289
|
-
.example(12)
|
|
5290
|
-
.description('The number of accounts registered with this application. Not available for legacy apps.'),
|
|
5291
|
-
|
|
5292
|
-
lastError: lastErrorSchema.allow(null),
|
|
5293
|
-
pubSubError: pubSubErrorSchema.allow(null)
|
|
5294
|
-
}).label('ApplicationResponse'),
|
|
5295
|
-
failAction: 'log'
|
|
5296
|
-
}
|
|
5297
|
-
}
|
|
5298
|
-
});
|
|
5299
|
-
|
|
5300
|
-
server.route({
|
|
5301
|
-
method: 'POST',
|
|
5302
|
-
path: '/v1/oauth2',
|
|
5303
|
-
|
|
5304
|
-
async handler(request) {
|
|
5305
|
-
try {
|
|
5306
|
-
let result = await oauth2Apps.create(request.payload);
|
|
5307
|
-
|
|
5308
|
-
if (result && result.pubsubUpdates && Object.keys(result.pubsubUpdates).length > 0) {
|
|
5309
|
-
await call({ cmd: 'googlePubSub', app: result.id });
|
|
5310
|
-
delete result.pubsubUpdates;
|
|
5311
|
-
}
|
|
5312
|
-
|
|
5313
|
-
return result;
|
|
5314
|
-
} catch (err) {
|
|
5315
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
5316
|
-
if (Boom.isBoom(err)) {
|
|
5317
|
-
throw err;
|
|
5318
|
-
}
|
|
5319
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
5320
|
-
if (err.code) {
|
|
5321
|
-
error.output.payload.code = err.code;
|
|
5322
|
-
}
|
|
5323
|
-
throw error;
|
|
5324
|
-
}
|
|
5325
|
-
},
|
|
5326
|
-
|
|
5327
|
-
options: {
|
|
5328
|
-
description: 'Register OAuth2 application',
|
|
5329
|
-
notes: 'Registers a new OAuth2 application for a specific provider',
|
|
5330
|
-
tags: ['api', 'OAuth2 Applications'],
|
|
5331
|
-
|
|
5332
|
-
plugins: {},
|
|
5333
|
-
|
|
5334
|
-
auth: {
|
|
5335
|
-
strategy: 'api-token',
|
|
5336
|
-
mode: 'required'
|
|
5337
|
-
},
|
|
5338
|
-
cors: CORS_CONFIG,
|
|
5339
|
-
|
|
5340
|
-
validate: {
|
|
5341
|
-
options: {
|
|
5342
|
-
stripUnknown: false,
|
|
5343
|
-
abortEarly: false,
|
|
5344
|
-
convert: true
|
|
5345
|
-
},
|
|
5346
|
-
failAction,
|
|
5347
|
-
|
|
5348
|
-
payload: Joi.object(oauthCreateSchema).tailor('api').label('CreateOAuth2App')
|
|
5349
|
-
},
|
|
5350
|
-
|
|
5351
|
-
response: {
|
|
5352
|
-
schema: Joi.object({
|
|
5353
|
-
id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
5354
|
-
created: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the app created')
|
|
5355
|
-
}).label('CreateAppResponse'),
|
|
5356
|
-
failAction: 'log'
|
|
5357
|
-
}
|
|
5358
|
-
}
|
|
5359
|
-
});
|
|
5360
|
-
|
|
5361
|
-
server.route({
|
|
5362
|
-
method: 'PUT',
|
|
5363
|
-
path: '/v1/oauth2/{app}',
|
|
5364
|
-
|
|
5365
|
-
async handler(request) {
|
|
5366
|
-
try {
|
|
5367
|
-
let result = await oauth2Apps.update(request.params.app, request.payload);
|
|
5368
|
-
|
|
5369
|
-
if (result && result.pubsubUpdates && Object.keys(result.pubsubUpdates).length > 0) {
|
|
5370
|
-
await call({ cmd: 'googlePubSub', app: result.id });
|
|
5371
|
-
delete result.pubsubUpdates;
|
|
5372
|
-
}
|
|
5373
|
-
|
|
5374
|
-
return result;
|
|
5375
|
-
} catch (err) {
|
|
5376
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
5377
|
-
if (Boom.isBoom(err)) {
|
|
5378
|
-
throw err;
|
|
5379
|
-
}
|
|
5380
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
5381
|
-
if (err.code) {
|
|
5382
|
-
error.output.payload.code = err.code;
|
|
5383
|
-
}
|
|
5384
|
-
throw error;
|
|
5385
|
-
}
|
|
5386
|
-
},
|
|
5387
|
-
options: {
|
|
5388
|
-
description: 'Update OAuth2 application',
|
|
5389
|
-
notes: 'Updates OAuth2 application information',
|
|
5390
|
-
tags: ['api', 'OAuth2 Applications'],
|
|
5391
|
-
|
|
5392
|
-
plugins: {},
|
|
5393
|
-
|
|
5394
|
-
auth: {
|
|
5395
|
-
strategy: 'api-token',
|
|
5396
|
-
mode: 'required'
|
|
5397
|
-
},
|
|
5398
|
-
cors: CORS_CONFIG,
|
|
5399
|
-
|
|
5400
|
-
validate: {
|
|
5401
|
-
options: {
|
|
5402
|
-
stripUnknown: false,
|
|
5403
|
-
abortEarly: false,
|
|
5404
|
-
convert: true
|
|
5405
|
-
},
|
|
5406
|
-
failAction,
|
|
5407
|
-
|
|
5408
|
-
params: Joi.object({
|
|
5409
|
-
app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID')
|
|
5410
|
-
}),
|
|
5411
|
-
|
|
5412
|
-
payload: Joi.object({
|
|
5413
|
-
name: Joi.string().trim().empty('').max(256).example('My Gmail App').description('Application name'),
|
|
5414
|
-
description: Joi.string().trim().allow('').max(1024).example('My cool app').description('Application description'),
|
|
5415
|
-
title: Joi.string().allow('').trim().max(256).example('App title').description('Title for the application button'),
|
|
5416
|
-
|
|
5417
|
-
enabled: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').example(true).description('Enable this app'),
|
|
5418
|
-
|
|
5419
|
-
clientId: Joi.string()
|
|
5420
|
-
.trim()
|
|
5421
|
-
.allow('', null, false)
|
|
5422
|
-
.max(256)
|
|
5423
|
-
.example('52422112755-3uov8bjwlrullq122rdm6l8ui25ho7qf.apps.googleusercontent.com')
|
|
5424
|
-
.description('Client or Application ID for 3-legged OAuth2 applications')
|
|
5425
|
-
.label('UpdateOAuth2ClientId'),
|
|
5426
|
-
|
|
5427
|
-
clientSecret: Joi.string()
|
|
5428
|
-
.trim()
|
|
5429
|
-
.empty('')
|
|
5430
|
-
.max(256)
|
|
5431
|
-
.example('boT7Q~dUljnfFdVuqpC11g8nGMjO8kpRAv-ZB')
|
|
5432
|
-
.description('Client secret for 3-legged OAuth2 applications'),
|
|
5433
|
-
|
|
5434
|
-
pubSubApp: Joi.string()
|
|
5435
|
-
.empty('')
|
|
5436
|
-
.base64({ paddingRequired: false, urlSafe: true })
|
|
5437
|
-
.max(512)
|
|
5438
|
-
.example('AAAAAQAACnA')
|
|
5439
|
-
.description('Cloud Pub/Sub app for Gmail API webhooks')
|
|
5440
|
-
.label('UpdatePubSubAppId'),
|
|
5441
|
-
|
|
5442
|
-
extraScopes: Joi.array()
|
|
5443
|
-
.items(Joi.string().trim().max(255).example('User.Read').label('UpdateExtraScopeEntry'))
|
|
5444
|
-
.description('OAuth2 Extra Scopes')
|
|
5445
|
-
.label('UpdateOAuth2ExtraScopes'),
|
|
5446
|
-
|
|
5447
|
-
skipScopes: Joi.array()
|
|
5448
|
-
.items(Joi.string().trim().max(255).example('SMTP.Send').label('UpdateSkipScopeEntry'))
|
|
5449
|
-
.description('OAuth2 scopes to skip from the base set')
|
|
5450
|
-
.label('UpdateOAuth2SkipScopes'),
|
|
5451
|
-
|
|
5452
|
-
serviceClient: Joi.string()
|
|
5453
|
-
.trim()
|
|
5454
|
-
.allow('', null, false)
|
|
5455
|
-
.max(256)
|
|
5456
|
-
.example('7103296518315821565203')
|
|
5457
|
-
.description('Service client ID for 2-legged OAuth2 applications')
|
|
5458
|
-
.label('UpdateServiceClient'),
|
|
5459
|
-
|
|
5460
|
-
googleProjectId: googleProjectIdSchema,
|
|
5461
|
-
googleWorkspaceAccounts: googleWorkspaceAccountsSchema,
|
|
5462
|
-
googleTopicName: googleTopicNameSchema,
|
|
5463
|
-
googleSubscriptionName: googleSubscriptionNameSchema,
|
|
5464
|
-
|
|
5465
|
-
serviceClientEmail: Joi.string()
|
|
5466
|
-
.email()
|
|
5467
|
-
.example('name@project-123.iam.gserviceaccount.com')
|
|
5468
|
-
.description('Service Client Email for 2-legged OAuth2 applications'),
|
|
5469
|
-
|
|
5470
|
-
serviceKey: Joi.string()
|
|
5471
|
-
.trim()
|
|
5472
|
-
.empty('')
|
|
5473
|
-
.max(100 * 1024)
|
|
5474
|
-
.example('-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgk...')
|
|
5475
|
-
.description('PEM formatted service secret for 2-legged OAuth2 applications'),
|
|
5476
|
-
|
|
5477
|
-
authority: Joi.string()
|
|
5478
|
-
.trim()
|
|
5479
|
-
.empty('')
|
|
5480
|
-
.max(1024)
|
|
5481
|
-
.example('common')
|
|
5482
|
-
.description('Authorization tenant value for Outlook OAuth2 applications')
|
|
5483
|
-
.label('SupportedAccountTypes'),
|
|
5484
|
-
|
|
5485
|
-
cloud: Joi.string()
|
|
5486
|
-
.trim()
|
|
5487
|
-
.empty('')
|
|
5488
|
-
.valid('global', 'gcc-high', 'dod', 'china')
|
|
5489
|
-
.example('global')
|
|
5490
|
-
.description('Azure cloud type for Outlook OAuth2 applications')
|
|
5491
|
-
.label('AzureCloud'),
|
|
5492
|
-
|
|
5493
|
-
tenant: Joi.string().trim().empty('').max(1024).example('f8cdef31-a31e-4b4a-93e4-5f571e91255a').label('Directorytenant'),
|
|
5494
|
-
|
|
5495
|
-
redirectUrl: Joi.string()
|
|
5496
|
-
.allow('', null, false)
|
|
5497
|
-
.uri({ scheme: ['http', 'https'], allowRelative: false })
|
|
5498
|
-
.example('https://myservice.com/oauth')
|
|
5499
|
-
.description('Redirect URL for 3-legged OAuth2 applications')
|
|
5500
|
-
.label('UpdateOAuth2RedirectUrl')
|
|
5501
|
-
}).label('UpdateOAuthApp')
|
|
5502
|
-
},
|
|
5503
|
-
|
|
5504
|
-
response: {
|
|
5505
|
-
schema: Joi.object({
|
|
5506
|
-
id: Joi.string().max(256).required().example('example').description('OAuth2 app ID')
|
|
5507
|
-
}).label('UpdateOAuthAppResponse'),
|
|
5508
|
-
failAction: 'log'
|
|
5509
|
-
}
|
|
5510
|
-
}
|
|
5511
|
-
});
|
|
5512
|
-
|
|
5513
|
-
server.route({
|
|
5514
|
-
method: 'DELETE',
|
|
5515
|
-
path: '/v1/oauth2/{app}',
|
|
5516
|
-
|
|
5517
|
-
async handler(request) {
|
|
5518
|
-
try {
|
|
5519
|
-
let result = await oauth2Apps.del(request.params.app);
|
|
5520
|
-
|
|
5521
|
-
try {
|
|
5522
|
-
await call({ cmd: 'googlePubSubRemove', app: request.params.app });
|
|
5523
|
-
} catch (err) {
|
|
5524
|
-
request.logger.error({ msg: 'Failed to notify workers about OAuth2 app deletion', err, app: request.params.app });
|
|
5525
|
-
}
|
|
5526
|
-
|
|
5527
|
-
return result;
|
|
5528
|
-
} catch (err) {
|
|
5529
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
5530
|
-
if (Boom.isBoom(err)) {
|
|
5531
|
-
throw err;
|
|
5532
|
-
}
|
|
5533
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
5534
|
-
if (err.code) {
|
|
5535
|
-
error.output.payload.code = err.code;
|
|
5536
|
-
}
|
|
5537
|
-
throw error;
|
|
5538
|
-
}
|
|
5539
|
-
},
|
|
5540
|
-
options: {
|
|
5541
|
-
description: 'Remove OAuth2 application',
|
|
5542
|
-
notes: 'Delete OAuth2 application data',
|
|
5543
|
-
tags: ['api', 'OAuth2 Applications'],
|
|
5544
|
-
|
|
5545
|
-
plugins: {},
|
|
5546
|
-
|
|
5547
|
-
auth: {
|
|
5548
|
-
strategy: 'api-token',
|
|
5549
|
-
mode: 'required'
|
|
5550
|
-
},
|
|
5551
|
-
cors: CORS_CONFIG,
|
|
5552
|
-
|
|
5553
|
-
validate: {
|
|
5554
|
-
options: {
|
|
5555
|
-
stripUnknown: false,
|
|
5556
|
-
abortEarly: false,
|
|
5557
|
-
convert: true
|
|
5558
|
-
},
|
|
5559
|
-
failAction,
|
|
5560
|
-
|
|
5561
|
-
params: Joi.object({
|
|
5562
|
-
app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID')
|
|
5563
|
-
}).label('DeleteAppRequest')
|
|
5564
|
-
},
|
|
5565
|
-
|
|
5566
|
-
response: {
|
|
5567
|
-
schema: Joi.object({
|
|
5568
|
-
id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
5569
|
-
deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the OAuth2 application deleted'),
|
|
5570
|
-
accounts: Joi.number()
|
|
5571
|
-
.integer()
|
|
5572
|
-
.example(12)
|
|
5573
|
-
.description('The number of accounts registered with this application. Not available for legacy apps.')
|
|
5574
|
-
}).label('DeleteAppRequestResponse'),
|
|
5575
|
-
failAction: 'log'
|
|
5576
|
-
}
|
|
5577
|
-
}
|
|
5578
|
-
});
|
|
5579
|
-
|
|
5580
|
-
server.route({
|
|
5581
|
-
method: 'POST',
|
|
5582
|
-
path: '/v1/oauth2/{app}/verify',
|
|
5583
|
-
|
|
5584
|
-
async handler(request) {
|
|
5585
|
-
try {
|
|
5586
|
-
return await verifyOAuth2App(request.params.app, {
|
|
5587
|
-
account: request.payload.account,
|
|
5588
|
-
testConnection: request.payload.testConnection
|
|
5589
|
-
});
|
|
5590
|
-
} catch (err) {
|
|
5591
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
5592
|
-
if (Boom.isBoom(err)) {
|
|
5593
|
-
throw err;
|
|
5594
|
-
}
|
|
5595
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
5596
|
-
if (err.code) {
|
|
5597
|
-
error.output.payload.code = err.code;
|
|
5598
|
-
}
|
|
5599
|
-
throw error;
|
|
5600
|
-
}
|
|
5601
|
-
},
|
|
5602
|
-
options: {
|
|
5603
|
-
description: 'Verify OAuth2 application setup',
|
|
5604
|
-
notes: 'Runs the provider authentication chain step by step and reports which steps pass or fail, with hints for fixing failures. For service-account apps an optional mailbox address enables the delegation and live mailbox checks.',
|
|
5605
|
-
tags: ['api', 'OAuth2 Applications'],
|
|
5606
|
-
|
|
5607
|
-
plugins: {},
|
|
5608
|
-
|
|
5609
|
-
auth: {
|
|
5610
|
-
strategy: 'api-token',
|
|
5611
|
-
mode: 'required'
|
|
5612
|
-
},
|
|
5613
|
-
cors: CORS_CONFIG,
|
|
5614
|
-
|
|
5615
|
-
validate: {
|
|
5616
|
-
options: {
|
|
5617
|
-
stripUnknown: false,
|
|
5618
|
-
abortEarly: false,
|
|
5619
|
-
convert: true
|
|
5620
|
-
},
|
|
5621
|
-
failAction,
|
|
5622
|
-
|
|
5623
|
-
params: Joi.object({
|
|
5624
|
-
app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID')
|
|
5625
|
-
}),
|
|
5626
|
-
|
|
5627
|
-
payload: Joi.object({
|
|
5628
|
-
account: Joi.string()
|
|
5629
|
-
.trim()
|
|
5630
|
-
.empty('')
|
|
5631
|
-
.max(256)
|
|
5632
|
-
.example('user@example.com')
|
|
5633
|
-
.description('Mailbox address used to verify domain-wide delegation and live mailbox access'),
|
|
5634
|
-
testConnection: Joi.boolean()
|
|
5635
|
-
.truthy('Y', 'true', '1', 'on')
|
|
5636
|
-
.falsy('N', 'false', 0, '')
|
|
5637
|
-
.default(true)
|
|
5638
|
-
.description('Perform the live IMAP/API connection step when an access token is obtained')
|
|
5639
|
-
}).label('VerifyOAuth2AppRequest')
|
|
5640
|
-
},
|
|
5641
|
-
|
|
5642
|
-
response: {
|
|
5643
|
-
schema: Joi.object({
|
|
5644
|
-
app: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
5645
|
-
provider: Joi.string().example('gmailService').description('Provider type'),
|
|
5646
|
-
authMethod: Joi.string().allow(null).example('externalAccount').description('Authentication method for service-account apps'),
|
|
5647
|
-
account: Joi.string().allow(null).example('user@example.com').description('Mailbox used for the delegation/mailbox checks'),
|
|
5648
|
-
ok: Joi.boolean().example(true).description('True when no verification step failed'),
|
|
5649
|
-
steps: Joi.array()
|
|
5650
|
-
.items(
|
|
5651
|
-
Joi.object({
|
|
5652
|
-
id: Joi.string().example('signJwt').description('Step identifier'),
|
|
5653
|
-
label: Joi.string().example('Sign assertion (signJwt)').description('Human readable step name'),
|
|
5654
|
-
status: Joi.string().valid('ok', 'fail', 'skip').example('ok').description('Step outcome'),
|
|
5655
|
-
message: Joi.string().allow(null).example('Assertion signed via IAM signJwt').description('Outcome detail'),
|
|
5656
|
-
hint: Joi.string()
|
|
5657
|
-
.example('Grant roles/iam.serviceAccountTokenCreator to the workload principal')
|
|
5658
|
-
.description('How to fix a failed step')
|
|
5659
|
-
}).label('OAuth2VerifyStep')
|
|
5660
|
-
)
|
|
5661
|
-
.label('OAuth2VerifySteps')
|
|
5662
|
-
}).label('VerifyOAuth2AppResponse'),
|
|
5663
|
-
failAction: 'log'
|
|
5664
|
-
}
|
|
5665
|
-
}
|
|
5666
|
-
});
|
|
5667
|
-
|
|
5668
|
-
server.route({
|
|
5669
|
-
method: 'GET',
|
|
5670
|
-
path: '/v1/gateways',
|
|
5671
|
-
|
|
5672
|
-
async handler(request) {
|
|
5673
|
-
try {
|
|
5674
|
-
let gatewayObject = new Gateway({ redis, gateway: request.params.gateway, call, secret: await getSecret() });
|
|
5675
|
-
|
|
5676
|
-
return await gatewayObject.listGateways(request.query.page, request.query.pageSize);
|
|
5677
|
-
} catch (err) {
|
|
5678
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
5679
|
-
if (Boom.isBoom(err)) {
|
|
5680
|
-
throw err;
|
|
5681
|
-
}
|
|
5682
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
5683
|
-
if (err.code) {
|
|
5684
|
-
error.output.payload.code = err.code;
|
|
5685
|
-
}
|
|
5686
|
-
throw error;
|
|
5687
|
-
}
|
|
5688
|
-
},
|
|
5689
|
-
|
|
5690
|
-
options: {
|
|
5691
|
-
description: 'List gateways',
|
|
5692
|
-
notes: 'Lists registered gateways',
|
|
5693
|
-
tags: ['api', 'SMTP Gateway'],
|
|
5694
|
-
|
|
5695
|
-
plugins: {},
|
|
5696
|
-
|
|
5697
|
-
auth: {
|
|
5698
|
-
strategy: 'api-token',
|
|
5699
|
-
mode: 'required'
|
|
5700
|
-
},
|
|
5701
|
-
cors: CORS_CONFIG,
|
|
5702
|
-
|
|
5703
|
-
validate: {
|
|
5704
|
-
options: {
|
|
5705
|
-
stripUnknown: false,
|
|
5706
|
-
abortEarly: false,
|
|
5707
|
-
convert: true
|
|
5708
|
-
},
|
|
5709
|
-
failAction,
|
|
5710
|
-
|
|
5711
|
-
query: Joi.object({
|
|
5712
|
-
page: Joi.number()
|
|
5713
|
-
.integer()
|
|
5714
|
-
.min(0)
|
|
5715
|
-
.max(1024 * 1024)
|
|
5716
|
-
.default(0)
|
|
5717
|
-
.example(0)
|
|
5718
|
-
.description('Page number (zero indexed, so use 0 for first page)')
|
|
5719
|
-
.label('PageNumber'),
|
|
5720
|
-
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
|
|
5721
|
-
}).label('GatewaysFilter')
|
|
5722
|
-
},
|
|
5723
|
-
|
|
5724
|
-
response: {
|
|
5725
|
-
schema: Joi.object({
|
|
5726
|
-
total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
|
|
5727
|
-
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
5728
|
-
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
5729
|
-
|
|
5730
|
-
gateways: Joi.array()
|
|
5731
|
-
.items(
|
|
5732
|
-
Joi.object({
|
|
5733
|
-
gateway: Joi.string().max(256).required().example('example').description('Gateway ID'),
|
|
5734
|
-
name: Joi.string().max(256).example('My Email Gateway').description('Display name for the gateway'),
|
|
5735
|
-
deliveries: Joi.number().integer().empty('').example(100).description('Count of email deliveries using this gateway'),
|
|
5736
|
-
lastUse: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Last delivery time'),
|
|
5737
|
-
lastError: lastErrorSchema.allow(null)
|
|
5738
|
-
}).label('GatewayResponseItem')
|
|
5739
|
-
)
|
|
5740
|
-
.label('GatewayEntries')
|
|
5741
|
-
}).label('GatewaysFilterResponse'),
|
|
5742
|
-
failAction: 'log'
|
|
5743
|
-
}
|
|
5744
|
-
}
|
|
5745
|
-
});
|
|
5746
|
-
|
|
5747
|
-
server.route({
|
|
5748
|
-
method: 'GET',
|
|
5749
|
-
path: '/v1/gateway/{gateway}',
|
|
5750
|
-
|
|
5751
|
-
async handler(request) {
|
|
5752
|
-
let gatewayObject = new Gateway({ redis, gateway: request.params.gateway, call, secret: await getSecret() });
|
|
5753
|
-
try {
|
|
5754
|
-
let gatewayData = await gatewayObject.loadGatewayData();
|
|
5755
|
-
|
|
5756
|
-
// remove secrets
|
|
5757
|
-
if (gatewayData.pass) {
|
|
5758
|
-
gatewayData.pass = '******';
|
|
5759
|
-
}
|
|
5760
|
-
|
|
5761
|
-
let result = {};
|
|
5762
|
-
|
|
5763
|
-
for (let key of ['gateway', 'name', 'host', 'port', 'user', 'pass', 'secure', 'deliveries', 'lastUse', 'lastError']) {
|
|
5764
|
-
if (key in gatewayData) {
|
|
5765
|
-
result[key] = gatewayData[key];
|
|
5766
|
-
}
|
|
5767
|
-
}
|
|
5768
|
-
|
|
5769
|
-
return result;
|
|
5770
|
-
} catch (err) {
|
|
5771
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
5772
|
-
if (Boom.isBoom(err)) {
|
|
5773
|
-
throw err;
|
|
5774
|
-
}
|
|
5775
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
5776
|
-
if (err.code) {
|
|
5777
|
-
error.output.payload.code = err.code;
|
|
5778
|
-
}
|
|
5779
|
-
throw error;
|
|
5780
|
-
}
|
|
5781
|
-
},
|
|
5782
|
-
options: {
|
|
5783
|
-
description: 'Get gateway info',
|
|
5784
|
-
notes: 'Returns stored information about the gateway. Passwords are not included.',
|
|
5785
|
-
tags: ['api', 'SMTP Gateway'],
|
|
5786
|
-
|
|
5787
|
-
auth: {
|
|
5788
|
-
strategy: 'api-token',
|
|
5789
|
-
mode: 'required'
|
|
5790
|
-
},
|
|
5791
|
-
cors: CORS_CONFIG,
|
|
5792
|
-
|
|
5793
|
-
validate: {
|
|
5794
|
-
options: {
|
|
5795
|
-
stripUnknown: false,
|
|
5796
|
-
abortEarly: false,
|
|
5797
|
-
convert: true
|
|
5798
|
-
},
|
|
5799
|
-
failAction,
|
|
5800
|
-
|
|
5801
|
-
params: Joi.object({
|
|
5802
|
-
gateway: Joi.string().max(256).required().example('example').description('Gateway ID')
|
|
5803
|
-
})
|
|
5804
|
-
},
|
|
5805
|
-
|
|
5806
|
-
response: {
|
|
5807
|
-
schema: Joi.object({
|
|
5808
|
-
gateway: Joi.string().max(256).required().example('example').description('Gateway ID'),
|
|
5809
|
-
|
|
5810
|
-
name: Joi.string().max(256).required().example('My Email Gateway').description('Display name for the gateway'),
|
|
5811
|
-
deliveries: Joi.number().integer().empty('').example(100).description('Count of email deliveries using this gateway'),
|
|
5812
|
-
lastUse: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Last delivery time'),
|
|
5813
|
-
|
|
5814
|
-
user: Joi.string().empty('').trim().max(1024).description('SMTP authentication username').label('UserName'),
|
|
5815
|
-
pass: Joi.string().empty('').max(1024).description('SMTP authentication password').label('Password'),
|
|
5816
|
-
|
|
5817
|
-
host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to').label('Hostname'),
|
|
5818
|
-
port: Joi.number()
|
|
5819
|
-
.integer()
|
|
5820
|
-
.min(1)
|
|
5821
|
-
.max(64 * 1024)
|
|
5822
|
-
.example(465)
|
|
5823
|
-
.description('Service port number')
|
|
5824
|
-
.label('Port'),
|
|
5825
|
-
|
|
5826
|
-
secure: Joi.boolean()
|
|
5827
|
-
.truthy('Y', 'true', '1', 'on')
|
|
5828
|
-
.falsy('N', 'false', 0, '')
|
|
5829
|
-
.default(false)
|
|
5830
|
-
.example(true)
|
|
5831
|
-
.description('Should connection use TLS. Usually true for port 465')
|
|
5832
|
-
.label('GatewayTlsOptions'),
|
|
5833
|
-
|
|
5834
|
-
lastError: lastErrorSchema.allow(null)
|
|
5835
|
-
}).label('GatewayResponse'),
|
|
5836
|
-
failAction: 'log'
|
|
5837
|
-
}
|
|
5838
|
-
}
|
|
5839
|
-
});
|
|
5840
|
-
|
|
5841
|
-
server.route({
|
|
5842
|
-
method: 'POST',
|
|
5843
|
-
path: '/v1/gateway',
|
|
5844
|
-
|
|
5845
|
-
async handler(request) {
|
|
5846
|
-
let gatewayObject = new Gateway({ redis, call, secret: await getSecret() });
|
|
5847
|
-
|
|
5848
|
-
try {
|
|
5849
|
-
let result = await gatewayObject.create(request.payload);
|
|
5850
|
-
return result;
|
|
5851
|
-
} catch (err) {
|
|
5852
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
5853
|
-
if (Boom.isBoom(err)) {
|
|
5854
|
-
throw err;
|
|
5855
|
-
}
|
|
5856
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
5857
|
-
if (err.code) {
|
|
5858
|
-
error.output.payload.code = err.code;
|
|
5859
|
-
}
|
|
5860
|
-
throw error;
|
|
5861
|
-
}
|
|
5862
|
-
},
|
|
5863
|
-
|
|
5864
|
-
options: {
|
|
5865
|
-
description: 'Register new gateway',
|
|
5866
|
-
notes: 'Registers a new SMP gateway',
|
|
5867
|
-
tags: ['api', 'SMTP Gateway'],
|
|
5868
|
-
|
|
5869
|
-
plugins: {},
|
|
5870
|
-
|
|
5871
|
-
auth: {
|
|
5872
|
-
strategy: 'api-token',
|
|
5873
|
-
mode: 'required'
|
|
5874
|
-
},
|
|
5875
|
-
cors: CORS_CONFIG,
|
|
5876
|
-
|
|
5877
|
-
validate: {
|
|
5878
|
-
options: {
|
|
5879
|
-
stripUnknown: false,
|
|
5880
|
-
abortEarly: false,
|
|
5881
|
-
convert: true
|
|
5882
|
-
},
|
|
5883
|
-
failAction,
|
|
5884
|
-
|
|
5885
|
-
payload: Joi.object({
|
|
5886
|
-
gateway: Joi.string().empty('').trim().max(256).default(null).example('sendgun').description('Gateway ID').label('Gateway ID').required(),
|
|
5887
|
-
|
|
5888
|
-
name: Joi.string().empty('').max(256).example('John Smith').description('Account Name').label('Gateway Name').required(),
|
|
5889
|
-
|
|
5890
|
-
user: Joi.string().empty('').trim().default(null).max(1024).description('SMTP authentication username').label('UserName'),
|
|
5891
|
-
pass: Joi.string().empty('').max(1024).default(null).description('SMTP authentication password').label('Password'),
|
|
5892
|
-
|
|
5893
|
-
host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to').label('Hostname').required(),
|
|
5894
|
-
port: Joi.number()
|
|
5895
|
-
.integer()
|
|
5896
|
-
.min(1)
|
|
5897
|
-
.max(64 * 1024)
|
|
5898
|
-
.example(465)
|
|
5899
|
-
.description('Service port number')
|
|
5900
|
-
.label('Port')
|
|
5901
|
-
.required(),
|
|
5902
|
-
|
|
5903
|
-
secure: Joi.boolean()
|
|
5904
|
-
.truthy('Y', 'true', '1', 'on')
|
|
5905
|
-
.falsy('N', 'false', 0, '')
|
|
5906
|
-
.default(false)
|
|
5907
|
-
.example(true)
|
|
5908
|
-
.description('Should connection use TLS. Usually true for port 465')
|
|
5909
|
-
.label('GatewayCreateTlsOptions')
|
|
5910
|
-
}).label('CreateGateway')
|
|
5911
|
-
},
|
|
5912
|
-
|
|
5913
|
-
response: {
|
|
5914
|
-
schema: Joi.object({
|
|
5915
|
-
gateway: Joi.string().max(256).required().example('example').description('Gateway ID'),
|
|
5916
|
-
state: Joi.string()
|
|
5917
|
-
.required()
|
|
5918
|
-
.valid('existing', 'new')
|
|
5919
|
-
.example('new')
|
|
5920
|
-
.description('Is the gateway new or updated existing')
|
|
5921
|
-
.label('CreateGatewayState')
|
|
5922
|
-
}).label('CreateGatewayResponse'),
|
|
5923
|
-
failAction: 'log'
|
|
5924
|
-
}
|
|
5925
|
-
}
|
|
5926
|
-
});
|
|
5927
|
-
|
|
5928
|
-
server.route({
|
|
5929
|
-
method: 'PUT',
|
|
5930
|
-
path: '/v1/gateway/edit/{gateway}',
|
|
5931
|
-
|
|
5932
|
-
async handler(request) {
|
|
5933
|
-
let gatewayObject = new Gateway({ redis, gateway: request.params.gateway, call, secret: await getSecret() });
|
|
5934
|
-
|
|
5935
|
-
try {
|
|
5936
|
-
return await gatewayObject.update(request.payload);
|
|
5937
|
-
} catch (err) {
|
|
5938
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
5939
|
-
if (Boom.isBoom(err)) {
|
|
5940
|
-
throw err;
|
|
5941
|
-
}
|
|
5942
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
5943
|
-
if (err.code) {
|
|
5944
|
-
error.output.payload.code = err.code;
|
|
5945
|
-
}
|
|
5946
|
-
throw error;
|
|
5947
|
-
}
|
|
5948
|
-
},
|
|
5949
|
-
options: {
|
|
5950
|
-
description: 'Update gateway info',
|
|
5951
|
-
notes: 'Updates gateway information',
|
|
5952
|
-
tags: ['api', 'SMTP Gateway'],
|
|
5953
|
-
|
|
5954
|
-
plugins: {},
|
|
5955
|
-
|
|
5956
|
-
auth: {
|
|
5957
|
-
strategy: 'api-token',
|
|
5958
|
-
mode: 'required'
|
|
5959
|
-
},
|
|
5960
|
-
cors: CORS_CONFIG,
|
|
5961
|
-
|
|
5962
|
-
validate: {
|
|
5963
|
-
options: {
|
|
5964
|
-
stripUnknown: false,
|
|
5965
|
-
abortEarly: false,
|
|
5966
|
-
convert: true
|
|
5967
|
-
},
|
|
5968
|
-
failAction,
|
|
5969
|
-
|
|
5970
|
-
params: Joi.object({
|
|
5971
|
-
gateway: Joi.string().max(256).required().example('example').description('Gateway ID')
|
|
5972
|
-
}),
|
|
5973
|
-
|
|
5974
|
-
payload: Joi.object({
|
|
5975
|
-
name: Joi.string().empty('').max(256).example('John Smith').description('Account Name').label('Gateway Name'),
|
|
5976
|
-
|
|
5977
|
-
user: Joi.string().empty('').trim().max(1024).allow(null).description('SMTP authentication username').label('UserName'),
|
|
5978
|
-
pass: Joi.string().empty('').max(1024).allow(null).description('SMTP authentication password').label('Password'),
|
|
5979
|
-
|
|
5980
|
-
host: Joi.string().hostname().empty('').example('smtp.gmail.com').description('Hostname to connect to').label('Hostname'),
|
|
5981
|
-
port: Joi.number()
|
|
5982
|
-
.integer()
|
|
5983
|
-
.min(1)
|
|
5984
|
-
.empty('')
|
|
5985
|
-
.max(64 * 1024)
|
|
5986
|
-
.example(465)
|
|
5987
|
-
.description('Service port number')
|
|
5988
|
-
.label('Port'),
|
|
5989
|
-
|
|
5990
|
-
secure: Joi.boolean()
|
|
5991
|
-
.truthy('Y', 'true', '1', 'on')
|
|
5992
|
-
.falsy('N', 'false', 0, '')
|
|
5993
|
-
.example(true)
|
|
5994
|
-
.description('Should connection use TLS. Usually true for port 465')
|
|
5995
|
-
.label('GatewayUpdateTlsOptions')
|
|
5996
|
-
}).label('UpdateGateway')
|
|
5997
|
-
},
|
|
5998
|
-
|
|
5999
|
-
response: {
|
|
6000
|
-
schema: Joi.object({
|
|
6001
|
-
gateway: Joi.string().max(256).required().example('example').description('Gateway ID')
|
|
6002
|
-
}).label('UpdateGatewayResponse'),
|
|
6003
|
-
failAction: 'log'
|
|
6004
|
-
}
|
|
6005
|
-
}
|
|
6006
|
-
});
|
|
6007
|
-
|
|
6008
|
-
server.route({
|
|
6009
|
-
method: 'DELETE',
|
|
6010
|
-
path: '/v1/gateway/{gateway}',
|
|
6011
|
-
|
|
6012
|
-
async handler(request) {
|
|
6013
|
-
let gatewayObject = new Gateway({
|
|
6014
|
-
redis,
|
|
6015
|
-
gateway: request.params.gateway,
|
|
6016
|
-
secret: await getSecret()
|
|
6017
|
-
});
|
|
6018
|
-
|
|
6019
|
-
try {
|
|
6020
|
-
return await gatewayObject.delete();
|
|
6021
|
-
} catch (err) {
|
|
6022
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
6023
|
-
if (Boom.isBoom(err)) {
|
|
6024
|
-
throw err;
|
|
6025
|
-
}
|
|
6026
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
6027
|
-
if (err.code) {
|
|
6028
|
-
error.output.payload.code = err.code;
|
|
6029
|
-
}
|
|
6030
|
-
throw error;
|
|
6031
|
-
}
|
|
6032
|
-
},
|
|
6033
|
-
options: {
|
|
6034
|
-
description: 'Remove SMTP gateway',
|
|
6035
|
-
notes: 'Delete SMTP gateway data',
|
|
6036
|
-
tags: ['api', 'SMTP Gateway'],
|
|
6037
|
-
|
|
6038
|
-
plugins: {},
|
|
6039
|
-
|
|
6040
|
-
auth: {
|
|
6041
|
-
strategy: 'api-token',
|
|
6042
|
-
mode: 'required'
|
|
6043
|
-
},
|
|
6044
|
-
cors: CORS_CONFIG,
|
|
6045
|
-
|
|
6046
|
-
validate: {
|
|
6047
|
-
options: {
|
|
6048
|
-
stripUnknown: false,
|
|
6049
|
-
abortEarly: false,
|
|
6050
|
-
convert: true
|
|
6051
|
-
},
|
|
6052
|
-
failAction,
|
|
6053
|
-
|
|
6054
|
-
params: Joi.object({
|
|
6055
|
-
gateway: Joi.string().max(256).required().example('example').description('Gateway ID')
|
|
6056
|
-
}).label('DeleteRequest')
|
|
6057
|
-
},
|
|
6058
|
-
|
|
6059
|
-
response: {
|
|
6060
|
-
schema: Joi.object({
|
|
6061
|
-
gateway: Joi.string().max(256).required().example('example').description('Gateway ID'),
|
|
6062
|
-
deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the gateway deleted')
|
|
6063
|
-
}).label('DeleteGatewayResponse'),
|
|
6064
|
-
failAction: 'log'
|
|
6065
|
-
}
|
|
6066
|
-
}
|
|
6067
|
-
});
|
|
6068
|
-
|
|
6069
|
-
server.route({
|
|
6070
|
-
method: 'GET',
|
|
6071
|
-
path: '/v1/account/{account}/oauth-token',
|
|
6072
|
-
|
|
6073
|
-
async handler(request) {
|
|
6074
|
-
let enableOAuthTokensApi = await settings.get('enableOAuthTokensApi');
|
|
6075
|
-
if (!enableOAuthTokensApi) {
|
|
6076
|
-
let error = Boom.boomify(new Error('Disabled API endpoint'), { statusCode: 403 });
|
|
6077
|
-
error.output.payload.code = 'ApiEndpointDisabled';
|
|
6078
|
-
throw error;
|
|
6079
|
-
}
|
|
6080
|
-
|
|
6081
|
-
let accountObject = new Account({
|
|
6082
|
-
redis,
|
|
6083
|
-
account: request.params.account,
|
|
6084
|
-
call,
|
|
6085
|
-
secret: await getSecret(),
|
|
6086
|
-
timeout: request.headers['x-ee-timeout']
|
|
6087
|
-
});
|
|
6088
|
-
|
|
6089
|
-
try {
|
|
6090
|
-
const tokenData = await accountObject.getActiveAccessTokenData();
|
|
6091
|
-
|
|
6092
|
-
// Record metric if token was actually refreshed (not cached)
|
|
6093
|
-
if (!tokenData.cached) {
|
|
6094
|
-
const provider = tokenData.provider || 'unknown';
|
|
6095
|
-
metrics(request.logger, 'oauth2TokenRefresh', 'inc', { status: 'success', provider, statusCode: '200' });
|
|
6096
|
-
}
|
|
6097
|
-
|
|
6098
|
-
return tokenData;
|
|
6099
|
-
} catch (err) {
|
|
6100
|
-
// Record failed token refresh
|
|
6101
|
-
const statusCode = String(err.statusCode || 0);
|
|
6102
|
-
metrics(request.logger, 'oauth2TokenRefresh', 'inc', { status: 'failure', provider: 'unknown', statusCode });
|
|
6103
|
-
|
|
6104
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
6105
|
-
if (Boom.isBoom(err)) {
|
|
6106
|
-
throw err;
|
|
6107
|
-
}
|
|
6108
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
6109
|
-
if (err.code) {
|
|
6110
|
-
error.output.payload.code = err.code;
|
|
6111
|
-
}
|
|
6112
|
-
throw error;
|
|
6113
|
-
}
|
|
6114
|
-
},
|
|
6115
|
-
|
|
6116
|
-
options: {
|
|
6117
|
-
description: 'Get OAuth2 access token',
|
|
6118
|
-
notes: 'Get the active OAuth2 access token for an account. NB! This endpoint is disabled by default and needs activation on the Service configuration page.',
|
|
6119
|
-
tags: ['api', 'Account'],
|
|
6120
|
-
|
|
6121
|
-
plugins: {},
|
|
6122
|
-
|
|
6123
|
-
auth: {
|
|
6124
|
-
strategy: 'api-token',
|
|
6125
|
-
mode: 'required'
|
|
6126
|
-
},
|
|
6127
|
-
cors: CORS_CONFIG,
|
|
6128
|
-
|
|
6129
|
-
validate: {
|
|
6130
|
-
options: {
|
|
6131
|
-
stripUnknown: false,
|
|
6132
|
-
abortEarly: false,
|
|
6133
|
-
convert: true
|
|
6134
|
-
},
|
|
6135
|
-
failAction,
|
|
6136
|
-
params: Joi.object({
|
|
6137
|
-
account: accountIdSchema.required()
|
|
6138
|
-
})
|
|
6139
|
-
},
|
|
6140
|
-
|
|
6141
|
-
response: {
|
|
6142
|
-
schema: Joi.object({
|
|
6143
|
-
account: accountIdSchema.required(),
|
|
6144
|
-
user: Joi.string().max(256).required().example('user@example.com').description('Username'),
|
|
6145
|
-
accessToken: Joi.string().max(256).required().example('aGVsbG8gd29ybGQ=').description('Access Token').label('OAuthAccessToken'),
|
|
6146
|
-
provider: OAuth2ProviderSchema
|
|
6147
|
-
}).label('AccountTokenResponse'),
|
|
6148
|
-
failAction: 'log'
|
|
6149
|
-
}
|
|
6150
|
-
}
|
|
6151
|
-
});
|
|
6152
|
-
|
|
6153
|
-
server.route({
|
|
6154
|
-
method: 'GET',
|
|
6155
|
-
path: '/v1/account/{account}/server-signatures',
|
|
6156
|
-
|
|
6157
|
-
async handler(request) {
|
|
6158
|
-
let accountObject = new Account({
|
|
6159
|
-
redis,
|
|
6160
|
-
account: request.params.account,
|
|
6161
|
-
call,
|
|
6162
|
-
secret: await getSecret(),
|
|
6163
|
-
timeout: request.headers['x-ee-timeout']
|
|
6164
|
-
});
|
|
6165
|
-
try {
|
|
6166
|
-
return await accountObject.listSignatures(request.query);
|
|
6167
|
-
} catch (err) {
|
|
6168
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
6169
|
-
if (Boom.isBoom(err)) {
|
|
6170
|
-
throw err;
|
|
6171
|
-
}
|
|
6172
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
6173
|
-
if (err.code) {
|
|
6174
|
-
error.output.payload.code = err.code;
|
|
6175
|
-
}
|
|
6176
|
-
throw error;
|
|
6177
|
-
}
|
|
6178
|
-
},
|
|
6179
|
-
|
|
6180
|
-
options: {
|
|
6181
|
-
description: 'List Account Signatures',
|
|
6182
|
-
notes: 'Returns signatures associated with the account. Currently only Gmail is supported, and only "new message" signatures from the "sendAs" list are returned.',
|
|
6183
|
-
tags: ['api', 'Account'],
|
|
6184
|
-
|
|
6185
|
-
plugins: {},
|
|
6186
|
-
|
|
6187
|
-
auth: {
|
|
6188
|
-
strategy: 'api-token',
|
|
6189
|
-
mode: 'required'
|
|
6190
|
-
},
|
|
6191
|
-
cors: CORS_CONFIG,
|
|
6192
|
-
|
|
6193
|
-
validate: {
|
|
6194
|
-
options: {
|
|
6195
|
-
stripUnknown: false,
|
|
6196
|
-
abortEarly: false,
|
|
6197
|
-
convert: true
|
|
6198
|
-
},
|
|
6199
|
-
failAction,
|
|
6200
|
-
params: Joi.object({
|
|
6201
|
-
account: accountIdSchema.required()
|
|
6202
|
-
})
|
|
6203
|
-
},
|
|
6204
|
-
|
|
6205
|
-
response: {
|
|
6206
|
-
schema: Joi.object({
|
|
6207
|
-
signatures: Joi.array()
|
|
6208
|
-
.items(
|
|
6209
|
-
Joi.object({
|
|
6210
|
-
address: Joi.string().email().example('user@example.com').description('Email address associated with the signature').required(),
|
|
6211
|
-
signature: Joi.string().example('<div>Best regards,</div>').description('Signature HTML code').required()
|
|
6212
|
-
}).label('SignatureResponseItem')
|
|
6213
|
-
)
|
|
6214
|
-
.label('SignatureEntries')
|
|
6215
|
-
}).label('AccountSignaturesResponse'),
|
|
6216
|
-
failAction: 'log'
|
|
6217
|
-
}
|
|
6218
|
-
}
|
|
6219
|
-
});
|
|
6220
|
-
|
|
6221
|
-
server.route({
|
|
6222
|
-
method: 'POST',
|
|
6223
|
-
path: '/v1/delivery-test/account/{account}',
|
|
6224
|
-
async handler(request) {
|
|
6225
|
-
let accountObject = new Account({
|
|
6226
|
-
redis,
|
|
6227
|
-
account: request.params.account,
|
|
6228
|
-
call,
|
|
6229
|
-
secret: await getSecret(),
|
|
6230
|
-
timeout: request.headers['x-ee-timeout']
|
|
6231
|
-
});
|
|
6232
|
-
|
|
6233
|
-
try {
|
|
6234
|
-
// throws if account does not exist
|
|
6235
|
-
let accountData = await accountObject.loadAccountData();
|
|
6236
|
-
|
|
6237
|
-
request.logger.info({ msg: 'Requested SMTP delivery test', account: request.params.account });
|
|
6238
|
-
|
|
6239
|
-
let headers = {
|
|
6240
|
-
'Content-Type': 'application/json',
|
|
6241
|
-
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
6242
|
-
};
|
|
6243
|
-
|
|
6244
|
-
let res = await fetchCmd(`${SMTP_TEST_HOST}/test-address`, {
|
|
6245
|
-
method: 'post',
|
|
6246
|
-
body: JSON.stringify({
|
|
6247
|
-
version: packageData.version,
|
|
6248
|
-
requestor: '@postalsys/emailengine-app'
|
|
6249
|
-
}),
|
|
6250
|
-
headers,
|
|
6251
|
-
dispatcher: httpAgent.retry
|
|
6252
|
-
});
|
|
6253
|
-
|
|
6254
|
-
if (!res.ok) {
|
|
6255
|
-
let err = new Error(`Invalid response: ${res.status} ${res.statusText}`);
|
|
6256
|
-
err.statusCode = res.status;
|
|
6257
|
-
|
|
6258
|
-
try {
|
|
6259
|
-
err.details = await res.json();
|
|
6260
|
-
} catch (err) {
|
|
6261
|
-
// ignore
|
|
6262
|
-
}
|
|
6263
|
-
|
|
6264
|
-
throw err;
|
|
6265
|
-
}
|
|
6266
|
-
|
|
6267
|
-
let testAccount = await res.json();
|
|
6268
|
-
if (!testAccount || !testAccount.user) {
|
|
6269
|
-
let err = new Error(`Invalid test account`);
|
|
6270
|
-
err.statusCode = 500;
|
|
6271
|
-
|
|
6272
|
-
try {
|
|
6273
|
-
err.details = testAccount;
|
|
6274
|
-
} catch (err) {
|
|
6275
|
-
// ignore
|
|
6276
|
-
}
|
|
6277
|
-
|
|
6278
|
-
throw err;
|
|
6279
|
-
}
|
|
6280
|
-
|
|
6281
|
-
if (request.payload.gateway) {
|
|
6282
|
-
// try to load the gateway, throws if not set
|
|
6283
|
-
let gatewayObject = new Gateway({ redis, gateway: request.payload.gateway, call, secret: await getSecret() });
|
|
6284
|
-
await gatewayObject.loadGatewayData();
|
|
6285
|
-
}
|
|
6286
|
-
|
|
6287
|
-
try {
|
|
6288
|
-
let now = new Date().toISOString();
|
|
6289
|
-
let queueResponse = await accountObject.queueMessage(
|
|
6290
|
-
{
|
|
6291
|
-
account: accountData.account,
|
|
6292
|
-
subject: `Delivery test ${now}`,
|
|
6293
|
-
text: `Hello
|
|
6294
|
-
|
|
6295
|
-
This is an automated email to test deliverability settings. If you see this email, you can safely delete it.
|
|
6296
|
-
|
|
6297
|
-
${now}`,
|
|
6298
|
-
html: `<p>Hello</p>
|
|
6299
|
-
<p>This is an automated email to test deliverability settings. If you see this email, you can safely delete it.</p>
|
|
6300
|
-
<p>${now}</p>`,
|
|
6301
|
-
from: {
|
|
6302
|
-
name: accountData.name,
|
|
6303
|
-
address: accountData.email
|
|
6304
|
-
},
|
|
6305
|
-
to: [{ name: 'Delivery Test Server', address: testAccount.address }],
|
|
6306
|
-
copy: false,
|
|
6307
|
-
gateway: request.payload.gateway,
|
|
6308
|
-
feedbackKey: `${REDIS_PREFIX}test-send:${testAccount.user}`,
|
|
6309
|
-
deliveryAttempts: 1
|
|
6310
|
-
},
|
|
6311
|
-
{ source: 'test' }
|
|
6312
|
-
);
|
|
6313
|
-
|
|
6314
|
-
return {
|
|
6315
|
-
success: !!queueResponse.queueId,
|
|
6316
|
-
deliveryTest: testAccount.user
|
|
6317
|
-
};
|
|
6318
|
-
} catch (err) {
|
|
6319
|
-
return {
|
|
6320
|
-
error: err.message
|
|
6321
|
-
};
|
|
6322
|
-
}
|
|
6323
|
-
} catch (err) {
|
|
6324
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
6325
|
-
if (Boom.isBoom(err)) {
|
|
6326
|
-
throw err;
|
|
6327
|
-
}
|
|
6328
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
6329
|
-
if (err.code) {
|
|
6330
|
-
error.output.payload.code = err.code;
|
|
6331
|
-
}
|
|
6332
|
-
if (err.details) {
|
|
6333
|
-
error.output.payload.details = err.details;
|
|
6334
|
-
}
|
|
6335
|
-
throw error;
|
|
6336
|
-
}
|
|
6337
|
-
},
|
|
6338
|
-
options: {
|
|
6339
|
-
description: 'Create delivery test',
|
|
6340
|
-
notes: 'Initiate a delivery test',
|
|
6341
|
-
tags: ['api', 'Delivery Test'],
|
|
6342
|
-
|
|
6343
|
-
auth: {
|
|
6344
|
-
strategy: 'api-token',
|
|
6345
|
-
mode: 'required'
|
|
6346
|
-
},
|
|
6347
|
-
cors: CORS_CONFIG,
|
|
6348
|
-
|
|
6349
|
-
validate: {
|
|
6350
|
-
options: {
|
|
6351
|
-
stripUnknown: false,
|
|
6352
|
-
abortEarly: false,
|
|
6353
|
-
convert: true
|
|
6354
|
-
},
|
|
6355
|
-
failAction,
|
|
6356
|
-
|
|
6357
|
-
params: Joi.object({
|
|
6358
|
-
account: accountIdSchema.required()
|
|
6359
|
-
}),
|
|
6360
|
-
|
|
6361
|
-
payload: Joi.object({
|
|
6362
|
-
gateway: Joi.string().allow(false, null).empty('').max(256).example(false).description('Optional gateway ID').label('DeliveryTestGateway')
|
|
6363
|
-
}).label('DeliveryStartRequest')
|
|
6364
|
-
},
|
|
6365
|
-
|
|
6366
|
-
response: {
|
|
6367
|
-
schema: Joi.object({
|
|
6368
|
-
success: Joi.boolean().example(true).description('Was the test started').label('ResponseDeliveryStartSuccess'),
|
|
6369
|
-
deliveryTest: Joi.string()
|
|
6370
|
-
.guid({
|
|
6371
|
-
version: ['uuidv4', 'uuidv5']
|
|
6372
|
-
})
|
|
6373
|
-
.example('6420a6ad-7f82-4e4f-8112-82a9dad1f34d')
|
|
6374
|
-
.description('Test ID')
|
|
6375
|
-
}).label('DeliveryStartResponse'),
|
|
6376
|
-
failAction: 'log'
|
|
6377
|
-
}
|
|
6378
|
-
}
|
|
6379
|
-
});
|
|
6380
|
-
|
|
6381
|
-
server.route({
|
|
6382
|
-
method: 'GET',
|
|
6383
|
-
path: '/v1/delivery-test/check/{deliveryTest}',
|
|
6384
|
-
async handler(request) {
|
|
6385
|
-
try {
|
|
6386
|
-
request.logger.info({ msg: 'Requested SMTP delivery test check', deliveryTest: request.params.deliveryTest });
|
|
6387
|
-
|
|
6388
|
-
let deliveryStatus = (await redis.hgetall(`${REDIS_PREFIX}test-send:${request.params.deliveryTest}`)) || {};
|
|
6389
|
-
if (deliveryStatus.success === 'false') {
|
|
6390
|
-
let err = new Error(`Failed to deliver email`);
|
|
6391
|
-
err.statusCode = 500;
|
|
6392
|
-
err.details = deliveryStatus;
|
|
6393
|
-
throw err;
|
|
6394
|
-
}
|
|
6395
|
-
|
|
6396
|
-
let headers = {
|
|
6397
|
-
'Content-Type': 'application/json',
|
|
6398
|
-
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
6399
|
-
};
|
|
6400
|
-
|
|
6401
|
-
let res = await fetchCmd(`${SMTP_TEST_HOST}/test-address/${request.params.deliveryTest}`, {
|
|
6402
|
-
method: 'get',
|
|
6403
|
-
headers,
|
|
6404
|
-
dispatcher: httpAgent.retry
|
|
6405
|
-
});
|
|
6406
|
-
|
|
6407
|
-
if (!res.ok) {
|
|
6408
|
-
let err = new Error(`Invalid response: ${res.status} ${res.statusText}`);
|
|
6409
|
-
err.statusCode = res.status;
|
|
6410
|
-
|
|
6411
|
-
try {
|
|
6412
|
-
err.details = await res.json();
|
|
6413
|
-
} catch (err) {
|
|
6414
|
-
// ignore
|
|
6415
|
-
}
|
|
6416
|
-
|
|
6417
|
-
throw err;
|
|
6418
|
-
}
|
|
6419
|
-
|
|
6420
|
-
let testResponse = await res.json();
|
|
6421
|
-
|
|
6422
|
-
let success = testResponse && testResponse.status === 'success'; //Default
|
|
6423
|
-
|
|
6424
|
-
if (testResponse && success) {
|
|
6425
|
-
let mainSig =
|
|
6426
|
-
testResponse.dkim &&
|
|
6427
|
-
testResponse.dkim.results &&
|
|
6428
|
-
testResponse.dkim.results.find(entry => entry && entry.status && entry.status.result === 'pass' && entry.status.aligned);
|
|
6429
|
-
|
|
6430
|
-
if (!mainSig) {
|
|
6431
|
-
mainSig =
|
|
6432
|
-
testResponse.dkim &&
|
|
6433
|
-
testResponse.dkim.results &&
|
|
6434
|
-
testResponse.dkim.results.find(entry => entry && entry.status && entry.status.result === 'pass');
|
|
6435
|
-
}
|
|
6436
|
-
|
|
6437
|
-
if (!mainSig) {
|
|
6438
|
-
mainSig = testResponse.dkim && testResponse.dkim.results && testResponse.dkim.results[0];
|
|
6439
|
-
}
|
|
6440
|
-
|
|
6441
|
-
testResponse.mainSig = mainSig || {
|
|
6442
|
-
status: {
|
|
6443
|
-
result: 'none'
|
|
6444
|
-
}
|
|
6445
|
-
};
|
|
6446
|
-
|
|
6447
|
-
if (testResponse.spf && testResponse.spf.status && testResponse.spf.status.comment) {
|
|
6448
|
-
testResponse.spf.status.comment = testResponse.spf.status.comment.replace(/^[^:\s]+:s*/, '');
|
|
6449
|
-
}
|
|
6450
|
-
}
|
|
6451
|
-
|
|
6452
|
-
if (testResponse) {
|
|
6453
|
-
if (testResponse.status === 'success') {
|
|
6454
|
-
delete testResponse.status;
|
|
6455
|
-
}
|
|
6456
|
-
delete testResponse.user;
|
|
6457
|
-
}
|
|
6458
|
-
|
|
6459
|
-
return Object.assign({ success }, testResponse || {});
|
|
6460
|
-
} catch (err) {
|
|
6461
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
6462
|
-
if (Boom.isBoom(err)) {
|
|
6463
|
-
throw err;
|
|
6464
|
-
}
|
|
6465
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
6466
|
-
if (err.code) {
|
|
6467
|
-
error.output.payload.code = err.code;
|
|
6468
|
-
}
|
|
6469
|
-
if (err.details) {
|
|
6470
|
-
error.output.payload.details = err.details;
|
|
6471
|
-
}
|
|
6472
|
-
throw error;
|
|
6473
|
-
}
|
|
6474
|
-
},
|
|
6475
|
-
options: {
|
|
6476
|
-
description: 'Check test status',
|
|
6477
|
-
notes: 'Check delivery test status',
|
|
6478
|
-
tags: ['api', 'Delivery Test'],
|
|
6479
|
-
|
|
6480
|
-
auth: {
|
|
6481
|
-
strategy: 'api-token',
|
|
6482
|
-
mode: 'required'
|
|
6483
|
-
},
|
|
6484
|
-
cors: CORS_CONFIG,
|
|
6485
|
-
|
|
6486
|
-
validate: {
|
|
6487
|
-
options: {
|
|
6488
|
-
stripUnknown: false,
|
|
6489
|
-
abortEarly: false,
|
|
6490
|
-
convert: true
|
|
6491
|
-
},
|
|
6492
|
-
failAction,
|
|
6493
|
-
|
|
6494
|
-
params: Joi.object({
|
|
6495
|
-
deliveryTest: Joi.string()
|
|
6496
|
-
.guid({
|
|
6497
|
-
version: ['uuidv4', 'uuidv5']
|
|
6498
|
-
})
|
|
6499
|
-
.example('6420a6ad-7f82-4e4f-8112-82a9dad1f34d')
|
|
6500
|
-
.required()
|
|
6501
|
-
.description('Test ID')
|
|
6502
|
-
}).label('DeliveryCheckParams')
|
|
6503
|
-
},
|
|
6504
|
-
|
|
6505
|
-
response: {
|
|
6506
|
-
schema: Joi.object({
|
|
6507
|
-
success: Joi.boolean().example(true).description('Was the test completed').label('ResponseDeliveryCheckSuccess'),
|
|
6508
|
-
dkim: Joi.object().unknown().description('DKIM results').label('DkimResults'),
|
|
6509
|
-
spf: Joi.object().unknown().description('SPF results').label('SpfResults'),
|
|
6510
|
-
dmarc: Joi.object().unknown().description('DMARC results').label('DmarcResults'),
|
|
6511
|
-
bimi: Joi.object().unknown().description('BIMI results').label('BimiResults'),
|
|
6512
|
-
arc: Joi.object().unknown().description('ARC results').label('ArcResults'),
|
|
6513
|
-
mainSig: Joi.object()
|
|
6514
|
-
.unknown()
|
|
6515
|
-
.description('Primary DKIM signature. `status.aligned` should be set, otherwise DKIM check should not be considered as passed.')
|
|
6516
|
-
.label('MainSignature')
|
|
6517
|
-
}).label('DeliveryCheckResponse'),
|
|
6518
|
-
failAction: 'log'
|
|
6519
|
-
}
|
|
6520
|
-
}
|
|
6521
|
-
});
|
|
6522
|
-
|
|
6523
|
-
server.route({
|
|
6524
|
-
method: 'GET',
|
|
6525
|
-
path: '/v1/blocklists',
|
|
6526
|
-
|
|
6527
|
-
async handler(request) {
|
|
6528
|
-
try {
|
|
6529
|
-
return await lists.list(request.query.page, request.query.pageSize);
|
|
6530
|
-
} catch (err) {
|
|
6531
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
6532
|
-
if (Boom.isBoom(err)) {
|
|
6533
|
-
throw err;
|
|
6534
|
-
}
|
|
6535
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
6536
|
-
if (err.code) {
|
|
6537
|
-
error.output.payload.code = err.code;
|
|
6538
|
-
}
|
|
6539
|
-
throw error;
|
|
6540
|
-
}
|
|
6541
|
-
},
|
|
6542
|
-
|
|
6543
|
-
options: {
|
|
6544
|
-
description: 'List blocklists',
|
|
6545
|
-
notes: 'List blocklists with blocked addresses',
|
|
6546
|
-
tags: ['api', 'Blocklists'],
|
|
6547
|
-
|
|
6548
|
-
plugins: {},
|
|
6549
|
-
|
|
6550
|
-
auth: {
|
|
6551
|
-
strategy: 'api-token',
|
|
6552
|
-
mode: 'required'
|
|
6553
|
-
},
|
|
6554
|
-
cors: CORS_CONFIG,
|
|
6555
|
-
|
|
6556
|
-
validate: {
|
|
6557
|
-
options: {
|
|
6558
|
-
stripUnknown: false,
|
|
6559
|
-
abortEarly: false,
|
|
6560
|
-
convert: true
|
|
6561
|
-
},
|
|
6562
|
-
failAction,
|
|
6563
|
-
|
|
6564
|
-
query: Joi.object({
|
|
6565
|
-
page: Joi.number()
|
|
6566
|
-
.integer()
|
|
6567
|
-
.min(0)
|
|
6568
|
-
.max(1024 * 1024)
|
|
6569
|
-
.default(0)
|
|
6570
|
-
.example(0)
|
|
6571
|
-
.description('Page number (zero indexed, so use 0 for first page)')
|
|
6572
|
-
.label('PageNumber'),
|
|
6573
|
-
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
|
|
6574
|
-
}).label('PageListsRequest')
|
|
6575
|
-
},
|
|
6576
|
-
|
|
6577
|
-
response: {
|
|
6578
|
-
schema: Joi.object({
|
|
6579
|
-
total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
|
|
6580
|
-
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
6581
|
-
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
6582
|
-
|
|
6583
|
-
blocklists: Joi.array()
|
|
6584
|
-
.items(
|
|
6585
|
-
Joi.object({
|
|
6586
|
-
listId: Joi.string().max(256).required().example('example').description('List ID'),
|
|
6587
|
-
count: Joi.number().integer().example(12).description('Count of blocked addresses in this list')
|
|
6588
|
-
}).label('BlocklistsResponseItem')
|
|
6589
|
-
)
|
|
6590
|
-
.label('BlocklistsEntries')
|
|
6591
|
-
}).label('BlocklistsResponse'),
|
|
6592
|
-
failAction: 'log'
|
|
6593
|
-
}
|
|
6594
|
-
}
|
|
6595
|
-
});
|
|
6596
|
-
|
|
6597
|
-
server.route({
|
|
6598
|
-
method: 'GET',
|
|
6599
|
-
path: '/v1/blocklist/{listId}',
|
|
6600
|
-
|
|
6601
|
-
async handler(request) {
|
|
6602
|
-
try {
|
|
6603
|
-
return await lists.listContent(request.params.listId, request.query.page, request.query.pageSize);
|
|
6604
|
-
} catch (err) {
|
|
6605
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
6606
|
-
if (Boom.isBoom(err)) {
|
|
6607
|
-
throw err;
|
|
6608
|
-
}
|
|
6609
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
6610
|
-
if (err.code) {
|
|
6611
|
-
error.output.payload.code = err.code;
|
|
6612
|
-
}
|
|
6613
|
-
throw error;
|
|
6614
|
-
}
|
|
6615
|
-
},
|
|
6616
|
-
|
|
6617
|
-
options: {
|
|
6618
|
-
description: 'List blocklist entries',
|
|
6619
|
-
notes: 'List blocked addresses for a list',
|
|
6620
|
-
tags: ['api', 'Blocklists'],
|
|
6621
|
-
|
|
6622
|
-
plugins: {},
|
|
6623
|
-
|
|
6624
|
-
auth: {
|
|
6625
|
-
strategy: 'api-token',
|
|
6626
|
-
mode: 'required'
|
|
6627
|
-
},
|
|
6628
|
-
cors: CORS_CONFIG,
|
|
6629
|
-
|
|
6630
|
-
validate: {
|
|
6631
|
-
options: {
|
|
6632
|
-
stripUnknown: false,
|
|
6633
|
-
abortEarly: false,
|
|
6634
|
-
convert: true
|
|
6635
|
-
},
|
|
6636
|
-
failAction,
|
|
6637
|
-
|
|
6638
|
-
params: Joi.object({
|
|
6639
|
-
listId: Joi.string()
|
|
6640
|
-
.hostname()
|
|
6641
|
-
.example('test-list')
|
|
6642
|
-
.description('List ID. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.')
|
|
6643
|
-
.label('ListID')
|
|
6644
|
-
.required()
|
|
6645
|
-
}).label('BlocklistListRequest'),
|
|
6646
|
-
|
|
6647
|
-
query: Joi.object({
|
|
6648
|
-
page: Joi.number()
|
|
6649
|
-
.integer()
|
|
6650
|
-
.min(0)
|
|
6651
|
-
.max(1024 * 1024)
|
|
6652
|
-
.default(0)
|
|
6653
|
-
.example(0)
|
|
6654
|
-
.description('Page number (zero indexed, so use 0 for first page)')
|
|
6655
|
-
.label('PageNumber'),
|
|
6656
|
-
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
|
|
6657
|
-
}).label('PageListsRequest')
|
|
6658
|
-
},
|
|
6659
|
-
|
|
6660
|
-
response: {
|
|
6661
|
-
schema: Joi.object({
|
|
6662
|
-
listId: Joi.string().max(256).required().example('example').description('List ID'),
|
|
6663
|
-
total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
|
|
6664
|
-
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
6665
|
-
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
6666
|
-
addresses: Joi.array()
|
|
6667
|
-
.items(
|
|
6668
|
-
Joi.object({
|
|
6669
|
-
recipient: Joi.string().email().example('user@example.com').description('Listed email address').required(),
|
|
6670
|
-
account: accountIdSchema.required().required(),
|
|
6671
|
-
messageId: Joi.string().example('<test123@example.com>').description('Message ID'),
|
|
6672
|
-
source: Joi.string().example('api').description('Which mechanism was used to add the entry'),
|
|
6673
|
-
reason: Joi.string().example('api').description('Why this entry was added'),
|
|
6674
|
-
remoteAddress: Joi.string()
|
|
6675
|
-
.ip({
|
|
6676
|
-
version: ['ipv4', 'ipv6'],
|
|
6677
|
-
cidr: 'optional'
|
|
6678
|
-
})
|
|
6679
|
-
.description('Which IP address triggered the entry'),
|
|
6680
|
-
userAgent: Joi.string().example('Mozilla/5.0 (Macintosh)').description('Which user agent triggered the entry'),
|
|
6681
|
-
created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was added or updated').required()
|
|
6682
|
-
}).label('BlocklistListResponseItem')
|
|
6683
|
-
)
|
|
6684
|
-
.label('BlocklistListEntries')
|
|
6685
|
-
}).label('BlocklistListResponse'),
|
|
6686
|
-
failAction: 'log'
|
|
6687
|
-
}
|
|
6688
|
-
}
|
|
6689
|
-
});
|
|
6690
|
-
|
|
6691
|
-
server.route({
|
|
6692
|
-
method: 'POST',
|
|
6693
|
-
path: '/v1/blocklist/{listId}',
|
|
6694
|
-
async handler(request) {
|
|
6695
|
-
let accountObject = new Account({
|
|
6696
|
-
redis,
|
|
6697
|
-
account: request.payload.account,
|
|
6698
|
-
call,
|
|
6699
|
-
secret: await getSecret(),
|
|
6700
|
-
timeout: request.headers['x-ee-timeout']
|
|
6701
|
-
});
|
|
6702
|
-
|
|
6703
|
-
try {
|
|
6704
|
-
// throws if account does not exist
|
|
6705
|
-
await accountObject.loadAccountData();
|
|
6706
|
-
|
|
6707
|
-
let added = await redis.eeListAdd(
|
|
6708
|
-
`${REDIS_PREFIX}lists:unsub:lists`,
|
|
6709
|
-
`${REDIS_PREFIX}lists:unsub:entries:${request.params.listId}`,
|
|
6710
|
-
request.params.listId,
|
|
6711
|
-
request.payload.recipient.toLowerCase().trim(),
|
|
6712
|
-
JSON.stringify({
|
|
6713
|
-
recipient: request.payload.recipient,
|
|
6714
|
-
account: request.payload.account,
|
|
6715
|
-
source: 'api',
|
|
6716
|
-
reason: request.payload.reason,
|
|
6717
|
-
remoteAddress: request.app.ip,
|
|
6718
|
-
userAgent: request.headers['user-agent'],
|
|
6719
|
-
created: new Date().toISOString()
|
|
6720
|
-
})
|
|
6721
|
-
);
|
|
6722
|
-
|
|
6723
|
-
return {
|
|
6724
|
-
success: true,
|
|
6725
|
-
added: !!added
|
|
6726
|
-
};
|
|
6727
|
-
} catch (err) {
|
|
6728
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
6729
|
-
if (Boom.isBoom(err)) {
|
|
6730
|
-
throw err;
|
|
6731
|
-
}
|
|
6732
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
6733
|
-
if (err.code) {
|
|
6734
|
-
error.output.payload.code = err.code;
|
|
6735
|
-
}
|
|
6736
|
-
if (err.details) {
|
|
6737
|
-
error.output.payload.details = err.details;
|
|
6738
|
-
}
|
|
6739
|
-
throw error;
|
|
6740
|
-
}
|
|
6741
|
-
},
|
|
6742
|
-
options: {
|
|
6743
|
-
description: 'Add to blocklist',
|
|
6744
|
-
notes: 'Add an email address to a blocklist',
|
|
6745
|
-
tags: ['api', 'Blocklists'],
|
|
6746
|
-
|
|
6747
|
-
auth: {
|
|
6748
|
-
strategy: 'api-token',
|
|
6749
|
-
mode: 'required'
|
|
6750
|
-
},
|
|
6751
|
-
cors: CORS_CONFIG,
|
|
6752
|
-
|
|
6753
|
-
validate: {
|
|
6754
|
-
options: {
|
|
6755
|
-
stripUnknown: false,
|
|
6756
|
-
abortEarly: false,
|
|
6757
|
-
convert: true
|
|
6758
|
-
},
|
|
6759
|
-
failAction,
|
|
6760
|
-
|
|
6761
|
-
params: Joi.object({
|
|
6762
|
-
listId: Joi.string()
|
|
6763
|
-
.hostname()
|
|
6764
|
-
.example('test-list')
|
|
6765
|
-
.description('List ID. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.')
|
|
6766
|
-
.label('ListID')
|
|
6767
|
-
.required()
|
|
6768
|
-
}).label('BlocklistListRequest'),
|
|
6769
|
-
|
|
6770
|
-
payload: Joi.object({
|
|
6771
|
-
account: accountIdSchema.required(),
|
|
6772
|
-
recipient: Joi.string().empty('').email().example('user@example.com').description('Email address to add to the list').required(),
|
|
6773
|
-
reason: Joi.string().empty('').default('block').description('Identifier for the blocking reason')
|
|
6774
|
-
}).label('BlocklistListAddPayload')
|
|
6775
|
-
},
|
|
6776
|
-
|
|
6777
|
-
response: {
|
|
6778
|
-
schema: Joi.object({
|
|
6779
|
-
success: Joi.boolean().example(true).description('Was the request successful').label('BlocklistListAddSuccess'),
|
|
6780
|
-
added: Joi.boolean().example(true).description('Was the address added to the list')
|
|
6781
|
-
}).label('BlocklistListAddResponse'),
|
|
6782
|
-
failAction: 'log'
|
|
6783
|
-
}
|
|
6784
|
-
}
|
|
6785
|
-
});
|
|
6786
|
-
|
|
6787
|
-
server.route({
|
|
6788
|
-
method: 'DELETE',
|
|
6789
|
-
path: '/v1/blocklist/{listId}',
|
|
6790
|
-
|
|
6791
|
-
async handler(request) {
|
|
6792
|
-
try {
|
|
6793
|
-
let exists = await redis.hexists(`${REDIS_PREFIX}lists:unsub:lists`, request.params.listId);
|
|
6794
|
-
if (!exists) {
|
|
6795
|
-
let message = 'Requested blocklist was not found';
|
|
6796
|
-
let error = Boom.boomify(new Error(message), { statusCode: 404 });
|
|
6797
|
-
throw error;
|
|
6798
|
-
}
|
|
6799
|
-
|
|
6800
|
-
let deleted = await redis.eeListRemove(
|
|
6801
|
-
`${REDIS_PREFIX}lists:unsub:lists`,
|
|
6802
|
-
`${REDIS_PREFIX}lists:unsub:entries:${request.params.listId}`,
|
|
6803
|
-
request.params.listId,
|
|
6804
|
-
request.query.recipient.toLowerCase().trim()
|
|
6805
|
-
);
|
|
6806
|
-
|
|
6807
|
-
return {
|
|
6808
|
-
deleted: !!deleted
|
|
6809
|
-
};
|
|
6810
|
-
} catch (err) {
|
|
6811
|
-
request.logger.error({ msg: 'API request failed', err });
|
|
6812
|
-
if (Boom.isBoom(err)) {
|
|
6813
|
-
throw err;
|
|
6814
|
-
}
|
|
6815
|
-
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
6816
|
-
if (err.code) {
|
|
6817
|
-
error.output.payload.code = err.code;
|
|
6818
|
-
}
|
|
6819
|
-
throw error;
|
|
6820
|
-
}
|
|
6821
|
-
},
|
|
6822
|
-
options: {
|
|
6823
|
-
description: 'Remove from blocklist',
|
|
6824
|
-
notes: 'Delete a blocked email address from a list',
|
|
6825
|
-
tags: ['api', 'Blocklists'],
|
|
6826
|
-
|
|
6827
|
-
plugins: {},
|
|
6828
|
-
|
|
6829
|
-
auth: {
|
|
6830
|
-
strategy: 'api-token',
|
|
6831
|
-
mode: 'required'
|
|
6832
|
-
},
|
|
6833
|
-
cors: CORS_CONFIG,
|
|
6834
|
-
|
|
6835
|
-
validate: {
|
|
6836
|
-
options: {
|
|
6837
|
-
stripUnknown: false,
|
|
6838
|
-
abortEarly: false,
|
|
6839
|
-
convert: true
|
|
6840
|
-
},
|
|
6841
|
-
failAction,
|
|
6842
|
-
|
|
6843
|
-
params: Joi.object({
|
|
6844
|
-
listId: Joi.string()
|
|
6845
|
-
.hostname()
|
|
6846
|
-
.example('test-list')
|
|
6847
|
-
.description('List ID. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.')
|
|
6848
|
-
.label('ListID')
|
|
6849
|
-
.required()
|
|
6850
|
-
}).label('BlocklistListRequest'),
|
|
6851
|
-
|
|
6852
|
-
query: Joi.object({
|
|
6853
|
-
recipient: Joi.string().empty('').email().example('user@example.com').description('Email address to remove from the list').required()
|
|
6854
|
-
}).label('RecipientQuery')
|
|
6855
|
-
},
|
|
6856
|
-
|
|
6857
|
-
response: {
|
|
6858
|
-
schema: Joi.object({
|
|
6859
|
-
deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the address removed from the list')
|
|
6860
|
-
}).label('DeleteBlocklistResponse'),
|
|
6861
|
-
failAction: 'log'
|
|
6862
|
-
}
|
|
6863
|
-
}
|
|
6864
|
-
});
|
|
6865
|
-
|
|
6866
|
-
server.route({
|
|
6867
|
-
method: 'GET',
|
|
6868
|
-
path: '/v1/changes',
|
|
6869
|
-
|
|
6870
|
-
async handler(request, h) {
|
|
6871
|
-
request.app.stream = new ResponseStream();
|
|
6872
|
-
finished(request.app.stream, err => request.app.stream.finalize(err));
|
|
6873
|
-
setImmediate(() => {
|
|
6874
|
-
try {
|
|
6875
|
-
request.app.stream.write(`: EmailEngine v${packageData.version}\n\n`);
|
|
6876
|
-
} catch (err) {
|
|
6877
|
-
// ignore
|
|
6878
|
-
}
|
|
6879
|
-
});
|
|
6880
|
-
return h
|
|
6881
|
-
.response(request.app.stream)
|
|
6882
|
-
.header('X-Accel-Buffering', 'no')
|
|
6883
|
-
.header('Connection', 'keep-alive')
|
|
6884
|
-
.header('Cache-Control', 'no-cache')
|
|
6885
|
-
.type('text/event-stream');
|
|
6886
|
-
},
|
|
6887
|
-
|
|
6888
|
-
options: {
|
|
6889
|
-
description: 'Stream state changes',
|
|
6890
|
-
notes: 'Stream account state changes as an EventSource',
|
|
6891
|
-
tags: ['api', 'Account'],
|
|
6892
|
-
|
|
6893
|
-
plugins: {
|
|
6894
|
-
'hapi-swagger': {
|
|
6895
|
-
produces: ['text/event-stream']
|
|
6896
|
-
}
|
|
6897
|
-
},
|
|
6898
|
-
|
|
6899
|
-
auth: {
|
|
6900
|
-
strategy: 'api-token',
|
|
6901
|
-
mode: 'required'
|
|
6902
|
-
},
|
|
6903
|
-
cors: CORS_CONFIG
|
|
6904
|
-
}
|
|
6905
|
-
});
|
|
6906
|
-
|
|
6907
|
-
// Web UI routes
|
|
6908
|
-
|
|
6909
|
-
await server.register({
|
|
6910
|
-
plugin: Crumb,
|
|
6911
|
-
|
|
6912
|
-
options: {
|
|
6913
|
-
cookieOptions: {
|
|
6914
|
-
isSecure: secureCookie
|
|
6915
|
-
},
|
|
6916
|
-
|
|
6917
|
-
skip: (request /*, h*/) => {
|
|
6918
|
-
let tags = (request.route && request.route.settings && request.route.settings.tags) || [];
|
|
6919
|
-
|
|
6920
|
-
if (tags.includes('api') || tags.includes('metrics') || tags.includes('external')) {
|
|
6921
|
-
return true;
|
|
6922
|
-
}
|
|
6923
|
-
|
|
6924
|
-
return false;
|
|
6925
|
-
}
|
|
6926
|
-
}
|
|
6927
|
-
});
|
|
6928
|
-
|
|
6929
|
-
server.views({
|
|
6930
|
-
engines: {
|
|
6931
|
-
hbs: handlebars
|
|
6932
|
-
},
|
|
6933
|
-
compileOptions: {
|
|
6934
|
-
preventIndent: true
|
|
6935
|
-
},
|
|
2677
|
+
server.views({
|
|
2678
|
+
engines: {
|
|
2679
|
+
hbs: handlebars
|
|
2680
|
+
},
|
|
2681
|
+
compileOptions: {
|
|
2682
|
+
preventIndent: true
|
|
2683
|
+
},
|
|
6936
2684
|
|
|
6937
2685
|
relativeTo: pathlib.join(__dirname, '..'),
|
|
6938
2686
|
path: './views',
|
|
@@ -7288,9 +3036,7 @@ ${now}`,
|
|
|
7288
3036
|
|
|
7289
3037
|
async handler(request, h) {
|
|
7290
3038
|
const renderedMetrics = await call({ cmd: 'metrics', timeout: request.headers['x-ee-timeout'] });
|
|
7291
|
-
|
|
7292
|
-
response.type('text/plain');
|
|
7293
|
-
return renderedMetrics;
|
|
3039
|
+
return h.response(renderedMetrics).type('text/plain');
|
|
7294
3040
|
},
|
|
7295
3041
|
options: {
|
|
7296
3042
|
tags: ['scope:metrics'],
|
|
@@ -7311,6 +3057,35 @@ ${now}`,
|
|
|
7311
3057
|
|
|
7312
3058
|
await server.start();
|
|
7313
3059
|
|
|
3060
|
+
if (USE_REUSE_PORT) {
|
|
3061
|
+
// Hapi (autoListen:false) wired its request dispatcher to our listener but did not
|
|
3062
|
+
// bind it. Bind now with SO_REUSEPORT so the kernel distributes connections across
|
|
3063
|
+
// all API workers. listen() can throw synchronously (bad args) or emit 'error'
|
|
3064
|
+
// asynchronously (EADDRINUSE/EACCES/ENOTSUP); handle both, mirroring probeReusePort().
|
|
3065
|
+
await new Promise((resolve, reject) => {
|
|
3066
|
+
const onError = err => {
|
|
3067
|
+
let wrapped = new Error(
|
|
3068
|
+
`Failed to bind API worker ${WORKER_INDEX} to ${API_HOST}:${API_PORT} with SO_REUSEPORT` + (err && err.code ? ` (${err.code})` : '')
|
|
3069
|
+
);
|
|
3070
|
+
wrapped.code = err && err.code;
|
|
3071
|
+
wrapped.workerIndex = WORKER_INDEX;
|
|
3072
|
+
wrapped.host = API_HOST;
|
|
3073
|
+
wrapped.port = API_PORT;
|
|
3074
|
+
reject(wrapped);
|
|
3075
|
+
};
|
|
3076
|
+
reusePortListener.once('error', onError);
|
|
3077
|
+
try {
|
|
3078
|
+
reusePortListener.listen({ port: API_PORT, host: API_HOST, reusePort: true }, () => {
|
|
3079
|
+
reusePortListener.removeListener('error', onError);
|
|
3080
|
+
resolve();
|
|
3081
|
+
});
|
|
3082
|
+
} catch (err) {
|
|
3083
|
+
reusePortListener.removeListener('error', onError);
|
|
3084
|
+
onError(err);
|
|
3085
|
+
}
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
|
|
7314
3089
|
// trigger a request to cache swagger.json
|
|
7315
3090
|
setImmediate(() => {
|
|
7316
3091
|
server
|
|
@@ -7340,6 +3115,7 @@ ${now}`,
|
|
|
7340
3115
|
}
|
|
7341
3116
|
|
|
7342
3117
|
if (
|
|
3118
|
+
IS_PRIMARY_API_WORKER &&
|
|
7343
3119
|
currentCert &&
|
|
7344
3120
|
currentCert.validTo < new Date(Date.now() - RENEW_TLS_AFTER) &&
|
|
7345
3121
|
(!currentCert.lastCheck || currentCert.lastCheck < new Date(Date.now() - BLOCK_TLS_RENEW))
|