emailengine-app 2.68.1 → 2.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy.yml +2 -0
- package/.github/workflows/release.yaml +4 -0
- package/CHANGELOG.md +40 -0
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +7 -1
- package/lib/account.js +62 -25
- package/lib/api-routes/account-routes.js +493 -75
- package/lib/api-routes/blocklist-routes.js +337 -0
- package/lib/api-routes/delivery-test-routes.js +321 -0
- package/lib/api-routes/export-routes.js +1 -12
- package/lib/api-routes/gateway-routes.js +376 -0
- package/lib/api-routes/license-routes.js +142 -0
- package/lib/api-routes/mailbox-routes.js +318 -0
- package/lib/api-routes/message-routes.js +21 -129
- package/lib/api-routes/oauth2-app-routes.js +631 -0
- package/lib/api-routes/outbox-routes.js +173 -0
- package/lib/api-routes/pubsub-routes.js +98 -0
- package/lib/api-routes/route-helpers.js +45 -0
- package/lib/api-routes/settings-routes.js +331 -0
- package/lib/api-routes/stats-routes.js +77 -0
- package/lib/api-routes/submit-routes.js +472 -0
- package/lib/api-routes/template-routes.js +7 -55
- package/lib/api-routes/token-routes.js +297 -0
- package/lib/api-routes/webhook-route-routes.js +152 -0
- package/lib/email-client/gmail-client.js +14 -0
- package/lib/email-client/imap/mailbox.js +34 -11
- package/lib/email-client/imap/subconnection.js +20 -12
- package/lib/email-client/imap/sync-operations.js +130 -2
- package/lib/email-client/imap-client.js +116 -58
- package/lib/email-client/outlook-client.js +85 -13
- package/lib/export.js +60 -19
- package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
- package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
- package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -23
- package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- package/lib/message-port-stream.js +113 -16
- package/lib/reject-worker-calls.js +42 -0
- package/lib/routes-ui.js +37 -8778
- package/lib/schemas.js +26 -1
- package/lib/tools.js +68 -0
- package/lib/ui-routes/account-routes.js +40 -210
- package/lib/ui-routes/admin-config-routes.js +913 -487
- package/lib/ui-routes/admin-entities-routes.js +1 -0
- package/lib/ui-routes/auth-routes.js +1339 -0
- package/lib/ui-routes/dashboard-routes.js +188 -0
- package/lib/ui-routes/document-store-routes.js +800 -0
- package/lib/ui-routes/export-routes.js +217 -0
- package/lib/ui-routes/internals-routes.js +354 -0
- package/lib/ui-routes/network-config-routes.js +759 -0
- package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +371 -91
- package/lib/ui-routes/route-helpers.js +316 -0
- package/lib/ui-routes/smtp-test-routes.js +236 -0
- package/lib/ui-routes/unsubscribe-routes.js +234 -0
- package/lib/webhook-request.js +36 -0
- package/package.json +8 -8
- package/sbom.json +1 -1
- package/server.js +214 -16
- package/static/licenses.html +12 -12
- package/translations/messages.pot +129 -149
- package/views/dashboard.hbs +7 -26
- package/views/internals/index.hbs +15 -0
- package/views/tokens/index.hbs +9 -0
- package/workers/api.js +198 -4401
- package/workers/export.js +87 -54
- package/workers/imap.js +29 -13
- package/workers/submit.js +20 -11
- package/workers/webhooks.js +6 -20
package/workers/api.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// NB! This file is processed by gettext parser and can not use newer syntax like ?.
|
|
4
4
|
|
|
5
|
-
const { parentPort } = require('worker_threads');
|
|
5
|
+
const { parentPort, workerData } = require('worker_threads');
|
|
6
6
|
|
|
7
7
|
const packageData = require('../package.json');
|
|
8
8
|
const config = require('@zone-eu/wild-config');
|
|
@@ -24,23 +24,18 @@ const eulaText = marked.parse(
|
|
|
24
24
|
const {
|
|
25
25
|
getByteSize,
|
|
26
26
|
getDuration,
|
|
27
|
-
getStats,
|
|
28
27
|
flash,
|
|
29
28
|
failAction,
|
|
30
|
-
verifyAccountInfo,
|
|
31
29
|
isEmail,
|
|
32
|
-
getLogs,
|
|
33
30
|
getWorkerCount,
|
|
34
31
|
runPrechecks,
|
|
35
32
|
matcher,
|
|
36
33
|
readEnvValue,
|
|
37
|
-
getSignedFormData,
|
|
38
34
|
threadStats,
|
|
39
35
|
hasEnvValue,
|
|
40
36
|
getBoolean,
|
|
41
37
|
loadTlsConfig,
|
|
42
|
-
|
|
43
|
-
reloadHttpProxyAgent,
|
|
38
|
+
maybeReloadHttpProxyAgent,
|
|
44
39
|
resolveOAuthErrorStatus
|
|
45
40
|
} = require('../lib/tools');
|
|
46
41
|
const { matchIp, detectAutomatedRequest } = require('../lib/utils/network');
|
|
@@ -84,20 +79,13 @@ const pathlib = require('path');
|
|
|
84
79
|
const crypto = require('crypto');
|
|
85
80
|
const { Transform, finished } = require('stream');
|
|
86
81
|
const { oauth2Apps, OAUTH_PROVIDERS } = require('../lib/oauth2-apps');
|
|
87
|
-
const { verifyOAuth2App } = require('../lib/oauth/verify-app');
|
|
88
82
|
|
|
89
83
|
const handlebars = require('handlebars');
|
|
90
84
|
const AuthBearer = require('hapi-auth-bearer-token');
|
|
91
85
|
const tokens = require('../lib/tokens');
|
|
92
|
-
const { autodetectImapSettings } = require('../lib/autodetect-imap-settings');
|
|
93
86
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
const { lists } = require('../lib/lists');
|
|
97
|
-
|
|
98
|
-
const { redis, documentsQueue, notifyQueue, submitQueue } = require('../lib/db');
|
|
87
|
+
const { redis, documentsQueue } = require('../lib/db');
|
|
99
88
|
const { Account } = require('../lib/account');
|
|
100
|
-
const { Gateway } = require('../lib/gateway');
|
|
101
89
|
const settings = require('../lib/settings');
|
|
102
90
|
|
|
103
91
|
const getSecret = require('../lib/get-secret');
|
|
@@ -114,7 +102,6 @@ const {
|
|
|
114
102
|
TRACK_OPEN_NOTIFY,
|
|
115
103
|
TRACK_CLICK_NOTIFY,
|
|
116
104
|
REDIS_PREFIX,
|
|
117
|
-
MAX_DAYS_STATS,
|
|
118
105
|
RENEW_TLS_AFTER,
|
|
119
106
|
BLOCK_TLS_RENEW,
|
|
120
107
|
TLS_RENEW_CHECK_INTERVAL,
|
|
@@ -128,45 +115,27 @@ const {
|
|
|
128
115
|
NONCE_BYTES
|
|
129
116
|
} = consts;
|
|
130
117
|
|
|
131
|
-
const { fetch: fetchCmd } = require('undici');
|
|
132
|
-
|
|
133
118
|
const templateRoutes = require('../lib/api-routes/template-routes');
|
|
134
119
|
const chatRoutes = require('../lib/api-routes/chat-routes');
|
|
135
120
|
const bullBoardRoutes = require('../lib/api-routes/bull-board-routes');
|
|
136
121
|
const accountRoutes = require('../lib/api-routes/account-routes');
|
|
137
122
|
const messageRoutes = require('../lib/api-routes/message-routes');
|
|
138
123
|
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');
|
|
124
|
+
const pubsubRoutes = require('../lib/api-routes/pubsub-routes');
|
|
125
|
+
const tokenRoutes = require('../lib/api-routes/token-routes');
|
|
126
|
+
const mailboxRoutes = require('../lib/api-routes/mailbox-routes');
|
|
127
|
+
const settingsRoutes = require('../lib/api-routes/settings-routes');
|
|
128
|
+
const statsRoutes = require('../lib/api-routes/stats-routes');
|
|
129
|
+
const licenseRoutes = require('../lib/api-routes/license-routes');
|
|
130
|
+
const outboxRoutes = require('../lib/api-routes/outbox-routes');
|
|
131
|
+
const webhookRouteRoutes = require('../lib/api-routes/webhook-route-routes');
|
|
132
|
+
const oauth2AppRoutes = require('../lib/api-routes/oauth2-app-routes');
|
|
133
|
+
const gatewayRoutes = require('../lib/api-routes/gateway-routes');
|
|
134
|
+
const deliveryTestRoutes = require('../lib/api-routes/delivery-test-routes');
|
|
135
|
+
const blocklistRoutes = require('../lib/api-routes/blocklist-routes');
|
|
136
|
+
const submitRoutes = require('../lib/api-routes/submit-routes');
|
|
137
|
+
|
|
138
|
+
const { imapSchema, smtpSchema, oauth2Schema, accountIdSchema, headerTimeoutSchema } = require('../lib/schemas');
|
|
170
139
|
|
|
171
140
|
const OAuth2ProviderSchema = Joi.string()
|
|
172
141
|
.valid(...Object.keys(OAUTH_PROVIDERS))
|
|
@@ -182,21 +151,6 @@ const AccountTypeSchema = Joi.string()
|
|
|
182
151
|
.required()
|
|
183
152
|
.label('AccountType');
|
|
184
153
|
|
|
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
154
|
const SUPPORTED_LOCALES = locales.map(locale => locale.locale);
|
|
201
155
|
|
|
202
156
|
const FLAG_SORT_ORDER = ['\\Inbox', '\\Flagged', '\\Sent', '\\Drafts', '\\All', '\\Archive', '\\Junk', '\\Trash'];
|
|
@@ -260,6 +214,15 @@ const API_TLS = hasEnvValue('EENGINE_API_TLS') ? getBoolean(readEnvValue('EENGIN
|
|
|
260
214
|
// Merge TLS settings from config params and environment
|
|
261
215
|
loadTlsConfig(API_TLS, 'EENGINE_API_TLS_');
|
|
262
216
|
|
|
217
|
+
// Per-worker thread metadata. With multiple API workers (EENGINE_WORKERS_API > 1) the
|
|
218
|
+
// main thread assigns each one an index and whether to bind with SO_REUSEPORT. Only
|
|
219
|
+
// worker 0 runs singleton maintenance tasks (e.g. TLS certificate renewal).
|
|
220
|
+
const WORKER_INDEX = (workerData && workerData.workerIndex) || 0;
|
|
221
|
+
const USE_REUSE_PORT = !!(workerData && workerData.reusePort);
|
|
222
|
+
// Worker 0 is the primary; it runs singleton maintenance tasks (e.g. TLS certificate renewal)
|
|
223
|
+
// that must execute exactly once across all API workers.
|
|
224
|
+
const IS_PRIMARY_API_WORKER = WORKER_INDEX === 0;
|
|
225
|
+
|
|
263
226
|
const ADMIN_ACCESS_ADDRESSES = hasEnvValue('EENGINE_ADMIN_ACCESS_ADDRESSES')
|
|
264
227
|
? readEnvValue('EENGINE_ADMIN_ACCESS_ADDRESSES')
|
|
265
228
|
.split(',')
|
|
@@ -542,6 +505,11 @@ parentPort.on('message', message => {
|
|
|
542
505
|
if (message && message.cmd === 'change') {
|
|
543
506
|
publishChangeEvent(message);
|
|
544
507
|
}
|
|
508
|
+
|
|
509
|
+
if (message && message.cmd === 'settings') {
|
|
510
|
+
// Keep this worker's in-memory HTTP proxy agent in sync when proxy settings change
|
|
511
|
+
maybeReloadHttpProxyAgent(message.data);
|
|
512
|
+
}
|
|
545
513
|
});
|
|
546
514
|
|
|
547
515
|
const init = async () => {
|
|
@@ -620,11 +588,8 @@ const init = async () => {
|
|
|
620
588
|
return formatter.format(intVal);
|
|
621
589
|
});
|
|
622
590
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
host: API_HOST,
|
|
626
|
-
tls: API_TLS,
|
|
627
|
-
|
|
591
|
+
// Base Hapi options shared by both the default and SO_REUSEPORT binding paths
|
|
592
|
+
const serverOptions = {
|
|
628
593
|
state: {
|
|
629
594
|
strictHeader: false
|
|
630
595
|
},
|
|
@@ -646,7 +611,27 @@ const init = async () => {
|
|
|
646
611
|
}).unknown()
|
|
647
612
|
}
|
|
648
613
|
}
|
|
649
|
-
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// With multiple API workers we provide our own listener and bind it ourselves with
|
|
617
|
+
// SO_REUSEPORT so the kernel load-balances connections. Hapi forbids port/host when
|
|
618
|
+
// autoListen is false and needs a truthy `tls` flag to treat a provided HTTPS
|
|
619
|
+
// listener correctly. The single-worker path keeps Hapi's default binding unchanged.
|
|
620
|
+
let reusePortListener = null;
|
|
621
|
+
if (USE_REUSE_PORT) {
|
|
622
|
+
const http = require('http');
|
|
623
|
+
const https = require('https');
|
|
624
|
+
reusePortListener = API_TLS ? https.createServer(API_TLS) : http.createServer();
|
|
625
|
+
serverOptions.listener = reusePortListener;
|
|
626
|
+
serverOptions.tls = !!API_TLS;
|
|
627
|
+
serverOptions.autoListen = false;
|
|
628
|
+
} else {
|
|
629
|
+
serverOptions.port = API_PORT;
|
|
630
|
+
serverOptions.host = API_HOST;
|
|
631
|
+
serverOptions.tls = API_TLS;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const server = Hapi.server(serverOptions);
|
|
650
635
|
|
|
651
636
|
let assertPreconditionResult;
|
|
652
637
|
server.decorate('toolkit', 'getESClient', async (...args) => await getESClient(...args));
|
|
@@ -1132,6 +1117,12 @@ Include your token in requests using one of these methods:
|
|
|
1132
1117
|
};
|
|
1133
1118
|
}
|
|
1134
1119
|
|
|
1120
|
+
// Bind the token hash (id) to the request logger so it is included in the per-request
|
|
1121
|
+
// log entry, allowing API requests to be correlated to the token that made them.
|
|
1122
|
+
if (request.logger && typeof request.logger.setBindings === 'function') {
|
|
1123
|
+
request.logger.setBindings({ tokenId: tokenData.id, tokenAccount: tokenData.account || null });
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1135
1126
|
if (scope && tokenData.scopes && !tokenData.scopes.includes(scope) && !tokenData.scopes.includes('*')) {
|
|
1136
1127
|
// failed scope validation
|
|
1137
1128
|
logger.error({
|
|
@@ -1620,99 +1611,6 @@ Include your token in requests using one of these methods:
|
|
|
1620
1611
|
}
|
|
1621
1612
|
});
|
|
1622
1613
|
|
|
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
1614
|
server.route({
|
|
1717
1615
|
method: 'GET',
|
|
1718
1616
|
path: '/redirect',
|
|
@@ -2657,4290 +2555,159 @@ Include your token in requests using one of these methods:
|
|
|
2657
2555
|
}
|
|
2658
2556
|
});
|
|
2659
2557
|
|
|
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'],
|
|
2558
|
+
// setup template routes
|
|
2559
|
+
await templateRoutes({ server, call, CORS_CONFIG });
|
|
2697
2560
|
|
|
2698
|
-
|
|
2561
|
+
// setup "chat with email" routes
|
|
2562
|
+
await chatRoutes({ server, call, CORS_CONFIG });
|
|
2699
2563
|
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2564
|
+
// setup account CRUD routes
|
|
2565
|
+
await accountRoutes({
|
|
2566
|
+
server,
|
|
2567
|
+
call,
|
|
2568
|
+
documentsQueue,
|
|
2569
|
+
oauth2Schema,
|
|
2570
|
+
imapSchema,
|
|
2571
|
+
smtpSchema,
|
|
2572
|
+
CORS_CONFIG,
|
|
2573
|
+
AccountTypeSchema,
|
|
2574
|
+
OAuth2ProviderSchema,
|
|
2575
|
+
metrics
|
|
2576
|
+
});
|
|
2705
2577
|
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2578
|
+
// setup message routes
|
|
2579
|
+
await messageRoutes({
|
|
2580
|
+
server,
|
|
2581
|
+
call,
|
|
2582
|
+
CORS_CONFIG,
|
|
2583
|
+
MAX_ATTACHMENT_SIZE,
|
|
2584
|
+
MAX_BODY_SIZE,
|
|
2585
|
+
MAX_PAYLOAD_TIMEOUT
|
|
2586
|
+
});
|
|
2713
2587
|
|
|
2714
|
-
|
|
2715
|
-
|
|
2588
|
+
// setup export routes
|
|
2589
|
+
await exportRoutes({
|
|
2590
|
+
server,
|
|
2591
|
+
CORS_CONFIG
|
|
2592
|
+
});
|
|
2716
2593
|
|
|
2717
|
-
|
|
2594
|
+
// setup Pub/Sub status route
|
|
2595
|
+
await pubsubRoutes({ server, CORS_CONFIG });
|
|
2718
2596
|
|
|
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'),
|
|
2597
|
+
// setup access token routes
|
|
2598
|
+
await tokenRoutes({ server, call, CORS_CONFIG });
|
|
2728
2599
|
|
|
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'),
|
|
2600
|
+
// setup mailbox routes
|
|
2601
|
+
await mailboxRoutes({ server, call, CORS_CONFIG, FLAG_SORT_ORDER });
|
|
2744
2602
|
|
|
2745
|
-
|
|
2603
|
+
// setup settings routes
|
|
2604
|
+
await settingsRoutes({ server, notify, CORS_CONFIG });
|
|
2746
2605
|
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
},
|
|
2606
|
+
// setup stats route
|
|
2607
|
+
await statsRoutes({ server, call, CORS_CONFIG });
|
|
2750
2608
|
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
token: Joi.string().length(64).hex().required().example('123456').description('Access token')
|
|
2754
|
-
}).label('CreateTokenResponse'),
|
|
2755
|
-
failAction: 'log'
|
|
2756
|
-
}
|
|
2757
|
-
}
|
|
2758
|
-
});
|
|
2609
|
+
// setup license routes
|
|
2610
|
+
await licenseRoutes({ server, call, CORS_CONFIG });
|
|
2759
2611
|
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
path: '/v1/token/{token}',
|
|
2612
|
+
// setup outbox routes
|
|
2613
|
+
await outboxRoutes({ server, CORS_CONFIG });
|
|
2763
2614
|
|
|
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'],
|
|
2615
|
+
// setup webhook route management routes
|
|
2616
|
+
await webhookRouteRoutes({ server, CORS_CONFIG });
|
|
2783
2617
|
|
|
2784
|
-
|
|
2618
|
+
// setup OAuth2 application routes
|
|
2619
|
+
await oauth2AppRoutes({ server, call, CORS_CONFIG, OAuth2ProviderSchema });
|
|
2785
2620
|
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
mode: 'required'
|
|
2789
|
-
},
|
|
2790
|
-
cors: CORS_CONFIG,
|
|
2621
|
+
// setup SMTP gateway routes
|
|
2622
|
+
await gatewayRoutes({ server, call, CORS_CONFIG });
|
|
2791
2623
|
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
stripUnknown: false,
|
|
2795
|
-
abortEarly: false,
|
|
2796
|
-
convert: true
|
|
2797
|
-
},
|
|
2798
|
-
failAction,
|
|
2624
|
+
// setup delivery test routes
|
|
2625
|
+
await deliveryTestRoutes({ server, call, CORS_CONFIG, SMTP_TEST_HOST });
|
|
2799
2626
|
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
}).label('DeleteTokenRequest')
|
|
2803
|
-
},
|
|
2627
|
+
// setup blocklist routes
|
|
2628
|
+
await blocklistRoutes({ server, call, CORS_CONFIG });
|
|
2804
2629
|
|
|
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
|
-
});
|
|
2630
|
+
// setup message submit route
|
|
2631
|
+
await submitRoutes({ server, call, CORS_CONFIG, MAX_ATTACHMENT_SIZE, MAX_BODY_SIZE, MAX_PAYLOAD_TIMEOUT });
|
|
2813
2632
|
|
|
2814
2633
|
server.route({
|
|
2815
2634
|
method: 'GET',
|
|
2816
|
-
path: '/v1/
|
|
2635
|
+
path: '/v1/changes',
|
|
2817
2636
|
|
|
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;
|
|
2637
|
+
async handler(request, h) {
|
|
2638
|
+
request.app.stream = new ResponseStream();
|
|
2639
|
+
finished(request.app.stream, err => request.app.stream.finalize(err));
|
|
2640
|
+
setImmediate(() => {
|
|
2641
|
+
try {
|
|
2642
|
+
request.app.stream.write(`: EmailEngine v${packageData.version}\n\n`);
|
|
2643
|
+
} catch (err) {
|
|
2644
|
+
// ignore
|
|
2830
2645
|
}
|
|
2831
|
-
|
|
2832
|
-
|
|
2646
|
+
});
|
|
2647
|
+
return h
|
|
2648
|
+
.response(request.app.stream)
|
|
2649
|
+
.header('X-Accel-Buffering', 'no')
|
|
2650
|
+
.header('Connection', 'keep-alive')
|
|
2651
|
+
.header('Cache-Control', 'no-cache')
|
|
2652
|
+
.type('text/event-stream');
|
|
2833
2653
|
},
|
|
2834
2654
|
|
|
2835
2655
|
options: {
|
|
2836
|
-
description: '
|
|
2837
|
-
notes: '
|
|
2838
|
-
tags: ['api', '
|
|
2656
|
+
description: 'Stream state changes',
|
|
2657
|
+
notes: 'Stream account state changes as an EventSource',
|
|
2658
|
+
tags: ['api', 'Account'],
|
|
2839
2659
|
|
|
2840
|
-
plugins: {
|
|
2660
|
+
plugins: {
|
|
2661
|
+
'hapi-swagger': {
|
|
2662
|
+
produces: ['text/event-stream']
|
|
2663
|
+
}
|
|
2664
|
+
},
|
|
2841
2665
|
|
|
2842
2666
|
auth: {
|
|
2843
2667
|
strategy: 'api-token',
|
|
2844
2668
|
mode: 'required'
|
|
2845
2669
|
},
|
|
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
|
-
}
|
|
2670
|
+
cors: CORS_CONFIG
|
|
2886
2671
|
}
|
|
2887
2672
|
});
|
|
2888
2673
|
|
|
2889
|
-
|
|
2890
|
-
method: 'GET',
|
|
2891
|
-
path: '/v1/tokens/account/{account}',
|
|
2674
|
+
// Web UI routes
|
|
2892
2675
|
|
|
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
|
-
},
|
|
2676
|
+
await server.register({
|
|
2677
|
+
plugin: Crumb,
|
|
2909
2678
|
|
|
2910
2679
|
options: {
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
tags: ['api', 'Access Tokens'],
|
|
2914
|
-
|
|
2915
|
-
plugins: {},
|
|
2916
|
-
|
|
2917
|
-
auth: {
|
|
2918
|
-
strategy: 'api-token',
|
|
2919
|
-
mode: 'required'
|
|
2680
|
+
cookieOptions: {
|
|
2681
|
+
isSecure: secureCookie
|
|
2920
2682
|
},
|
|
2921
|
-
cors: CORS_CONFIG,
|
|
2922
2683
|
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
}
|
|
2929
|
-
failAction,
|
|
2930
|
-
params: Joi.object({
|
|
2931
|
-
account: accountIdSchema.required()
|
|
2932
|
-
})
|
|
2933
|
-
},
|
|
2684
|
+
skip: (request /*, h*/) => {
|
|
2685
|
+
let tags = (request.route && request.route.settings && request.route.settings.tags) || [];
|
|
2686
|
+
|
|
2687
|
+
if (tags.includes('api') || tags.includes('metrics') || tags.includes('external')) {
|
|
2688
|
+
return true;
|
|
2689
|
+
}
|
|
2934
2690
|
|
|
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'
|
|
2691
|
+
return false;
|
|
2966
2692
|
}
|
|
2967
2693
|
}
|
|
2968
2694
|
});
|
|
2969
2695
|
|
|
2970
|
-
server.
|
|
2971
|
-
|
|
2972
|
-
|
|
2696
|
+
server.views({
|
|
2697
|
+
engines: {
|
|
2698
|
+
hbs: handlebars
|
|
2699
|
+
},
|
|
2700
|
+
compileOptions: {
|
|
2701
|
+
preventIndent: true
|
|
2702
|
+
},
|
|
2973
2703
|
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
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
|
-
});
|
|
2704
|
+
relativeTo: pathlib.join(__dirname, '..'),
|
|
2705
|
+
path: './views',
|
|
2706
|
+
layout: 'app',
|
|
2707
|
+
layoutPath: './views/layout',
|
|
2708
|
+
partialsPath: './views/partials',
|
|
2990
2709
|
|
|
2991
|
-
|
|
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
|
-
},
|
|
6936
|
-
|
|
6937
|
-
relativeTo: pathlib.join(__dirname, '..'),
|
|
6938
|
-
path: './views',
|
|
6939
|
-
layout: 'app',
|
|
6940
|
-
layoutPath: './views/layout',
|
|
6941
|
-
partialsPath: './views/partials',
|
|
6942
|
-
|
|
6943
|
-
isCached: false,
|
|
2710
|
+
isCached: false,
|
|
6944
2711
|
|
|
6945
2712
|
async context(request) {
|
|
6946
2713
|
const pendingMessages = await flash(redis, request);
|
|
@@ -7311,6 +3078,35 @@ ${now}`,
|
|
|
7311
3078
|
|
|
7312
3079
|
await server.start();
|
|
7313
3080
|
|
|
3081
|
+
if (USE_REUSE_PORT) {
|
|
3082
|
+
// Hapi (autoListen:false) wired its request dispatcher to our listener but did not
|
|
3083
|
+
// bind it. Bind now with SO_REUSEPORT so the kernel distributes connections across
|
|
3084
|
+
// all API workers. listen() can throw synchronously (bad args) or emit 'error'
|
|
3085
|
+
// asynchronously (EADDRINUSE/EACCES/ENOTSUP); handle both, mirroring probeReusePort().
|
|
3086
|
+
await new Promise((resolve, reject) => {
|
|
3087
|
+
const onError = err => {
|
|
3088
|
+
let wrapped = new Error(
|
|
3089
|
+
`Failed to bind API worker ${WORKER_INDEX} to ${API_HOST}:${API_PORT} with SO_REUSEPORT` + (err && err.code ? ` (${err.code})` : '')
|
|
3090
|
+
);
|
|
3091
|
+
wrapped.code = err && err.code;
|
|
3092
|
+
wrapped.workerIndex = WORKER_INDEX;
|
|
3093
|
+
wrapped.host = API_HOST;
|
|
3094
|
+
wrapped.port = API_PORT;
|
|
3095
|
+
reject(wrapped);
|
|
3096
|
+
};
|
|
3097
|
+
reusePortListener.once('error', onError);
|
|
3098
|
+
try {
|
|
3099
|
+
reusePortListener.listen({ port: API_PORT, host: API_HOST, reusePort: true }, () => {
|
|
3100
|
+
reusePortListener.removeListener('error', onError);
|
|
3101
|
+
resolve();
|
|
3102
|
+
});
|
|
3103
|
+
} catch (err) {
|
|
3104
|
+
reusePortListener.removeListener('error', onError);
|
|
3105
|
+
onError(err);
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
|
|
7314
3110
|
// trigger a request to cache swagger.json
|
|
7315
3111
|
setImmediate(() => {
|
|
7316
3112
|
server
|
|
@@ -7340,6 +3136,7 @@ ${now}`,
|
|
|
7340
3136
|
}
|
|
7341
3137
|
|
|
7342
3138
|
if (
|
|
3139
|
+
IS_PRIMARY_API_WORKER &&
|
|
7343
3140
|
currentCert &&
|
|
7344
3141
|
currentCert.validTo < new Date(Date.now() - RENEW_TLS_AFTER) &&
|
|
7345
3142
|
(!currentCert.lastCheck || currentCert.lastCheck < new Date(Date.now() - BLOCK_TLS_RENEW))
|